本篇主要看显示一帧视频的函数:video_display
video_display
这个不单单是处理一帧视频图像的显示,还兼容处理了音频波形的显示和字幕的显示,但由于对波形显示多少有些复杂,晦涩难懂,所以波形的处理代码跳过。
函数一上来就先判断宽度,如果没有设置宽度就设置为视频的宽度:
staticvoidvideo_display(VideoState *is)
{
//检查宽
if (!is->width)
video_open(is);
// 抹黑背景
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// 显示音频波形的模型,咱这里跳过就不看了哈
if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is);
// 视频模式
elseif (is->video_st)
video_image_display(is);
// 呈现到屏幕上
SDL_RenderPresent(renderer);
}这里面的renderer是个中间缓冲区,也就是说代码都是围绕这个renderer,最终让这renderer显示到屏幕上。
video_open
这里面就是弄下窗口参数,都是SDL的函数。
staticintvideo_open(VideoState *is)
{
int w,h;
w = screen_width ? screen_width : default_width;
h = screen_height ? screen_height : default_height;
if (!window_title)
window_title = input_filename;
// 设置窗口标题
SDL_SetWindowTitle(window, window_title);
// 设置窗口大小
SDL_SetWindowSize(window, w, h);
// 设置窗口位置
SDL_SetWindowPosition(window, screen_left, screen_top);
// 设置窗口全屏
if (is_full_screen)
SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
// 显示窗口
SDL_ShowWindow(window);
is->width = w;
is->height = h;
return0;
}video_image_display
视频图像显示+字幕显示,要显示视频图像的第一步就是要拿到一帧图像数据:
// 获取一帧图像数据
vp = frame_queue_peek_last(&is->pictq);
// vulkan渲染, 太高级了咱跳过。
if (vk_renderer) {
vk_renderer_display(vk_renderer, vp->frame);
return;
}之后,有字幕的话也要取一帧字幕数据:
// 有字幕
if (is->subtitle_st) {
// 取一帧字幕数据
if (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
// 字幕到了该显示的时间, start_display_time指的是显示推迟时间。
if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
// 字幕文件上传到GPU中,上传一次就行了,中间基本不再有变化
if (!sp->uploaded) {
...
// 分配字幕纹理数据 打开混合模式,混合模式就是为了背景透明不完全遮挡视频。
if (realloc_texture(&is->sub_texture, SDL_PIXELFORMAT_ARGB8888, sp->width, sp->height, SDL_BLENDMODE_BLEND, 1) < 0)纹理数据有时并不只有一个,所以用到了循环处理:
// 处理多个矩形
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
// 剪切矩形防止超过屏幕
sub_rect->x = av_clip(sub_rect->x, 0, sp->width );
sub_rect->y = av_clip(sub_rect->y, 0, sp->height);
sub_rect->w = av_clip(sub_rect->w, 0, sp->width - sub_rect->x);
sub_rect->h = av_clip(sub_rect->h, 0, sp->height - sub_rect->y);
// 格式转换器,要转成BGRA格式
is->sub_convert_ctx = sws_getCachedContext(is->sub_convert_ctx,
sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,
sub_rect->w, sub_rect->h, AV_PIX_FMT_BGRA,
0, NULL, NULL, NULL);
if (!is->sub_convert_ctx) {
av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
return;
}
// 执行格式转换
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)pixels, pitch)) {
sws_scale(is->sub_convert_ctx, (constuint8_t * const *)sub_rect->data, sub_rect->linesize,
0, sub_rect->h, pixels, pitch);
SDL_UnlockTexture(is->sub_texture);
}
}
sp->uploaded = 1;字幕处理完,就要来处理视频数据了:
// 按视频比例显示,比如4:3、16:9等比例显示
// SAR(Sample Aspect Ratio)像素比
// DAR(Display Aspect Ratio)显示比
// DAR = SAR * (picture_width / picture_height)
staticvoidcalculate_display_rect(SDL_Rect *rect,
int scr_xleft, int scr_ytop, int scr_width, int scr_height,
int pic_width, int pic_height, AVRational pic_sar)
{
AVRational aspect_ratio = pic_sar;
int64_t width, height, x, y;
// 防止无效像素比
if (av_cmp_q(aspect_ratio, av_make_q(0, 1)) <= 0)
aspect_ratio = av_make_q(1, 1);
// 计算显示比
aspect_ratio = av_mul_q(aspect_ratio, av_make_q(pic_width, pic_height));
// 先用高度计算宽度
height = scr_height;
width = av_rescale(height, aspect_ratio.num, aspect_ratio.den) & ~1;
// 如果宽度超过屏幕宽度,则用宽度计算高度
if (width > scr_width) {
width = scr_width;
height = av_rescale(width, aspect_ratio.den, aspect_ratio.num) & ~1;
}
x = (scr_width - width) / 2;
y = (scr_height - height) / 2;
rect->x = scr_xleft + x;
rect->y = scr_ytop + y;
rect->w = FFMAX((int)width, 1);
rect->h = FFMAX((int)height, 1);
}处理完视频比例,接下来是选择转换格式,YUV -> RGB, 转换标准选错了那显示颜色会偏色的:
staticvoidset_sdl_yuv_conversion_mode(AVFrame *frame)
{
SDL_YUV_CONVERSION_MODE mode = SDL_YUV_CONVERSION_AUTOMATIC;
if (frame && (frame->format == AV_PIX_FMT_YUV420P || frame->format == AV_PIX_FMT_YUYV422 || frame->format == AV_PIX_FMT_UYVY422)) {
if (frame->color_range == AVCOL_RANGE_JPEG)
mode = SDL_YUV_CONVERSION_JPEG;
elseif (frame->colorspace == AVCOL_SPC_BT709)
mode = SDL_YUV_CONVERSION_BT709;
elseif (frame->colorspace == AVCOL_SPC_BT470BG || frame->colorspace == AVCOL_SPC_SMPTE170M)
mode = SDL_YUV_CONVERSION_BT601;
}
SDL_SetYUVConversionMode(mode); /* FIXME: no support for linear transfer */
}然后上传视频纹理:
if (upload_texture(&is->vid_texture, vp->frame) < 0) {视频纹理、字幕纹理都上传完,就可以开始显示视频了:
// 把视频纹理搬到缓冲区上
SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0);字幕纹理也搬到缓冲区上:
if (sp) {
#if USE_ONEPASS_SUBTITLE_RENDER
SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
#else
int i;
double xratio = (double)rect.w / (double)sp->width;
double yratio = (double)rect.h / (double)sp->height;
for (i = 0; i < sp->sub.num_rects; i++) {
SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i];
SDL_Rect target = {.x = rect.x + sub_rect->x * xratio,
.y = rect.y + sub_rect->y * yratio,
.w = sub_rect->w * xratio,
.h = sub_rect->h * yratio};
SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target);
}
#endif
}然后再经过最开始的那个呈现函数,视频就显示在屏幕上了。
夜雨聆风