FFmepg-- 39-ffplay源码-ffplay 播放器中视频输出和尺寸变换

文章目录

整体架构

ffplay 使用 SDL(Simple DirectMedia Layer)作为跨平台的图形/音频渲染后端,其视频显示流程主要由以下模块构成:

  • 主事件循环 (event_loop)
  • 帧刷新逻辑 (video_refresh)
  • 帧显示逻辑 (video_display → video_image_display)
  • 图像格式适配与上传 (upload_texture)
  • 窗口尺寸与宽高比计算 (set_default_window_size → calculate_display_rect)

关键函数功能详解

视频输出初始化 (main 函数中)
c 复制代码
SDL_Init(SDL_INIT_VIDEO | ...)
window = SDL_CreateWindow(...)
renderer = SDL_CreateRenderer(window, ...)
  • 初始化 SDL 视频子系统。
  • 创建窗口和硬件加速渲染器(失败则回退到软件渲染)。
  • 后续所有视频帧都通过此 renderer 渲染到窗口。
窗口尺寸与宽高比处理

set_default_window_size(width, height, sar)

  • 根据视频原始分辨率 (width, height) 和样本宽高比 SAR (sar) 计算默认窗口大小。
  • 若未指定 -x/-y 参数,则以视频高度为基准。

calculate_display_rect(...)

  • 核心功能:在给定屏幕区域 (scr_width, scr_height) 内,按视频 SAR + 分辨率计算出保持比例的显示矩形。
  • 实现"黑边填充"(letterboxing/pillarboxing)以避免拉伸变形。

关键公式:

c 复制代码
aspect_ratio = sar * (pic_width / pic_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;
视频刷新与同步 (video_refresh)

这是音视频同步的核心逻辑,主要步骤:

  • 检查帧队列:若无帧可显示,跳过。

  • 序列校验:若发生 seek(serial 变化),丢弃旧帧。

  • 计算上一帧应显示时长:

    c 复制代码
    last_duration = nextvp->pts - lastvp->pts
    delay = compute_target_delay(last_duration, is) // 考虑主时钟(如音频)同步
  • 判断是否继续显示上一帧:若当前时间 < frame_timer + delay,则 sleep 并跳转到 display(不换帧)。

  • 判断是否丢帧(防卡顿):非逐帧模式 + 启用 framedrop + 非视频主同步 + 当前时间已超下一帧显示时间。

  • 更新时钟 & 标记强制刷新:

    c 复制代码
    is->force_refresh = 1
    video_display(is)
视频显示 (video_display → video_image_display)
c 复制代码
video_display(is) {
    video_image_display(is); // 显示视频帧
    SDL_RenderPresent(renderer);
}

video_image_display

  • 获取上次显示的帧:vp = frame_queue_peek_last(&is->pictq)

  • 调用 calculate_display_rect 计算当前窗口下的显示区域 rect

  • 上传纹理(仅当未上传过):upload_texture(&is->vid_texture, vp->frame, ...)

  • 渲染:

    c 复制代码
    SDL_RenderCopyEx(renderer, vid_texture, NULL, &rect, 0, NULL, flip_v ? SDL_FLIP_VERTICAL : 0)
图像格式转换与纹理上传 (upload_texture)

该函数处理 FFmpeg 像素格式 → SDL 纹理格式的映射:

  • 查找映射:通过 sdl_texture_format_map[] 表将 AVPixelFormat 映射为 SDL_PixelFormat(如 AV_PIX_FMT_YUV420PSDL_PIXELFORMAT_IYUV)。
  • 重分配纹理(如格式/尺寸变化):realloc_texture() 销毁旧纹理,创建新纹理。
  • 数据上传(分三种情况):
    • SDL 支持 YUV(如 IYUV)→ SDL_UpdateYUVTexture
    • SDL 支持 RGB → SDL_UpdateTexture
    • 不支持格式(SDL_PIXELFORMAT_UNKNOWN)→ 使用 sws_scale 转为 BGRA

关于 sws_scale 转换:

  • 使用 sws_getCachedContext 复用或创建 SwsContext
  • 目标格式固定为 AV_PIX_FMT_BGRA(对应 SDL_PIXELFORMAT_BGRA32)。
  • 转换后通过 SDL_LockTexture + sws_scale + SDL_UnlockTexture 写入纹理。

ffplay 视频显示主流程

复制代码
main()
│
├─ SDL_Init(SDL_INIT_VIDEO | ...)
├─ SDL_CreateWindow(...) → window
├─ SDL_CreateRenderer(...) → renderer
│
└─ event_loop(is)
     │
     └─ while (!abort_request)
          │
          ├─ video_refresh(is, &remaining_time)
          │    │
          │    ├─ if (pictq.size == 0) → return
          │    │
          │    ├─ vp = frame_queue_peek_last(&is->pictq)
          │    ├─ nextvp = frame_queue_peek(&is->pictq)
          │    │
          │    ├─ if (vp->serial != is->videoq.serial) → frame_queue_next(); goto retry
          │    │
          │    ├─ last_duration = nextvp->pts - vp->pts
          │    ├─ delay = compute_target_delay(last_duration, is)
          │    │
          │    ├─ time = av_gettime_relative() / 1000000.0
          │    │
          │    ├─ if (time < is->frame_timer + delay)
          │    │     └─ remaining_time = FFMIN(delay - (time - is->frame_timer), remaining_time)
          │    │        → 不换帧,仅 display
          │    │
          │    ├─ else
          │    │     ├─ is->frame_timer += delay
          │    │     ├─ if (is->frame_timer < time) is->frame_timer = time
          │    │     │
          │    │     ├─ if (framedrop && !is->step &&
          │    │     │       is->video_st && is->audio_st &&
          │    │     │       time > is->frame_timer + duration)
          │    │     │     └─ frame_queue_next(); goto retry   // 丢帧
          │    │     │
          │    │     └─ frame_queue_next()
          │    │         → is->force_refresh = 1
          │    │
          │    └─ video_display(is)
          │
          └─ SDL_Delay(remaining_time * 1000)

video_display() 子流程

复制代码
video_display(is)
│
└─ if (is->video_st)
     └─ video_image_display(is)
          │
          ├─ vp = frame_queue_peek_last(&is->pictq)
          │
          ├─ calculate_display_rect(&rect,
          │      is->xleft, is->ytop,
          │      is->width, is->height,
          │      vp->width, vp->height,
          │      vp->sar)
          │
          ├─ if (!vid_texture || format/size changed)
          │     └─ upload_texture(&vid_texture, vp->frame, ...)
          │          │
          │          ├─ 查找 sdl_format = map[av_pix_fmt]
          │          │
          │          ├─ if (sdl_format == UNKNOWN)
          │          │     └─ sws_scale(vp->frame → BGRA)
          │          │
          │          ├─ realloc_texture if needed
          │          │
          │          └─ SDL_UpdateYUVTexture / SDL_UpdateTexture / Lock+sws+Unlock
          │
          └─ SDL_RenderClear(renderer)
          └─ SDL_RenderCopyEx(renderer, vid_texture, NULL, &rect, ...)
          └─ SDL_RenderPresent(renderer)

calculate_display_rect() 尺寸计算逻辑

复制代码
calculate_display_rect(rect, scr_x, scr_y, scr_w, scr_h, pic_w, pic_h, sar)
│
├─ aspect_ratio = sar * (pic_w / pic_h)   // SAR × PAR = DAR
│
├─ height = scr_h
├─ width = round(aspect_ratio * height) & ~1   // 对齐偶数
│
├─ if (width > scr_w)
│    ├─ width = scr_w
│    └─ height = round(width / aspect_ratio) & ~1
│
├─ x = scr_x + (scr_w - width) / 2
├─ y = scr_y + (scr_h - height) / 2
│
└─ rect = {x, y, width, height}
  • frame_queue_peek_last():获取当前应显示的帧(已出队但未释放)
  • frame_queue_next():提交当前帧,移动读指针,释放旧帧
  • is->force_refresh:标记需要重绘(即使时间未到)
  • 窗口 resize 事件 → 触发 set_default_window_size() → 下次 video_display 使用新 is->width/height

性能与算法选择(sws_scale)

算法 缩小 (1920→400) FPS 放大 (768→1080) FPS 主观效果
SWS_FAST_BILINEAR 228 103 效果好,速度快(推荐默认)
SWS_POINT 427 112 极快,缩小锐利,放大有锯齿
SWS_BICUBIC 80 78 平滑但慢

总结:ffplay 视频输出核心思想

功能 实现方式 特点
跨平台显示 SDL 统一 Windows/Linux/macOS 接口
保持宽高比 calculate_display_rect 黑边填充,不拉伸
窗口缩放 SDL 硬件缩放 不调用 sws_scale,高效
格式兼容 映射表 + sws_scale 备用 尽量避免 CPU 转换
音视频同步 video_refresh + compute_target_delay 以音频为主时钟(默认)
丢帧策略 framedrop 机制 防卡顿,保证实时性

code

c 复制代码
/* called to display each frame */
/* 非暂停或强制刷新的时候,循环调用video_refresh */
static void video_refresh(void *opaque, double *remaining_time)
{
    VideoState *is = opaque;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
        check_external_clock_speed(is);

    if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
            video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
    }

    if (is->video_st) {
retry:
        if (frame_queue_nb_remaining(&is->pictq) == 0) {// 帧队列是否为空
            // nothing to do, no picture to display in the queue
            // 什么都不做,队列中没有图像可显示
        } else { // 重点是音视频同步
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* dequeue the picture */
            // 从队列取出上一个Frame
            lastvp = frame_queue_peek_last(&is->pictq);//读取上一帧
            vp = frame_queue_peek(&is->pictq);  // 读取待显示帧
            // lastvp 上一帧(正在显示的帧)
            // vp 等待显示的帧

            if (vp->serial != is->videoq.serial) {
                // 如果不是最新的播放序列,则将其出队列,以尽快读取最新序列的帧
                frame_queue_next(&is->pictq);
                goto retry;
            }

            if (lastvp->serial != vp->serial) {
                // 新的播放序列重置当前时间
                is->frame_timer = av_gettime_relative() / 1000000.0;
            }

            if (is->paused)
            {
                goto display;
                printf("视频暂停is->paused");
            }
            /* compute nominal last_duration */
            //lastvp上一帧,vp当前帧 ,nextvp下一帧
            //last_duration 计算上一帧应显示的时长
            last_duration = vp_duration(is, lastvp, vp);

            // 经过compute_target_delay方法,计算出待显示帧vp需要等待的时间
            // 如果以video同步,则delay直接等于last_duration。
            // 如果以audio或外部时钟同步,则需要比对主时钟调整待显示帧vp要等待的时间。
            delay = compute_target_delay(last_duration, is);

            time= av_gettime_relative()/1000000.0;
            // is->frame_timer 实际上就是上一帧lastvp的播放时间,
            // is->frame_timer + delay 是待显示帧vp该播放的时间
            if (time < is->frame_timer + delay) { //判断是否继续显示上一帧
                // 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。
                // 计算出最小等待时间
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            // 走到这一步,说明已经到了或过了该显示的时间,待显示帧vp的状态变更为当前要显示的帧

            is->frame_timer += delay;   // 更新当前帧播放的时间
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) {
                is->frame_timer = time; //如果和系统时间差距太大,就纠正为系统时间
            }
            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新video时钟
            SDL_UnlockMutex(is->pictq.mutex);
            //丢帧逻辑
            if (frame_queue_nb_remaining(&is->pictq) > 1) {//有nextvp才会检测是否该丢帧
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);
                if(!is->step        // 非逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放
                        && (framedrop>0 ||      // cpu解帧过慢
                            (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) // 非视频同步方式
                        && time > is->frame_timer + duration // 确实落后了一帧数据
                        ) {
                    printf("%s(%d) dif:%lfs, drop frame\n", __FUNCTION__, __LINE__,
                           (is->frame_timer + duration) - time);
                    is->frame_drops_late++;             // 统计丢帧情况
                    frame_queue_next(&is->pictq);       // 这里实现真正的丢帧
                    //(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)
                    goto retry; //回到函数开始位置,继续重试
                }
            }

            if (is->subtitle_st) {
                while (frame_queue_nb_remaining(&is->subpq) > 0) {
                    sp = frame_queue_peek(&is->subpq);

                    if (frame_queue_nb_remaining(&is->subpq) > 1)
                        sp2 = frame_queue_peek_next(&is->subpq);
                    else
                        sp2 = NULL;

                    if (sp->serial != is->subtitleq.serial
                            || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                            || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                    {
                        if (sp->uploaded) {
                            int i;
                            for (i = 0; i < sp->sub.num_rects; i++) {
                                AVSubtitleRect *sub_rect = sp->sub.rects[i];
                                uint8_t *pixels;
                                int pitch, j;

                                if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {
                                    for (j = 0; j < sub_rect->h; j++, pixels += pitch)
                                        memset(pixels, 0, sub_rect->w << 2);
                                    SDL_UnlockTexture(is->sub_texture);
                                }
                            }
                        }
                        frame_queue_next(&is->subpq);
                    } else {
                        break;
                    }
                }
            }

            frame_queue_next(&is->pictq);   // 当前vp帧出队列
            is->force_refresh = 1;          /* 说明需要刷新视频帧 */

            if (is->step && !is->paused)
                stream_toggle_pause(is);
        }
display:
        /* display picture */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display(is); // 重点是显示
    }
    is->force_refresh = 0;
    if (show_status) {
        static int64_t last_time;
        int64_t cur_time;
        int aqsize, vqsize, sqsize;
        double av_diff;

        cur_time = av_gettime_relative();
        if (!last_time || (cur_time - last_time) >= 30000) {
            aqsize = 0;
            vqsize = 0;
            sqsize = 0;
            if (is->audio_st)
                aqsize = is->audioq.size;
            if (is->video_st)
                vqsize = is->videoq.size;
            if (is->subtitle_st)
                sqsize = is->subtitleq.size;
            av_diff = 0;
            if (is->audio_st && is->video_st)
                av_diff = get_clock(&is->audclk) - get_clock(&is->vidclk);
            else if (is->video_st)
                av_diff = get_master_clock(is) - get_clock(&is->vidclk);
            else if (is->audio_st)
                av_diff = get_master_clock(is) - get_clock(&is->audclk);
            av_log(NULL, AV_LOG_INFO,
                   "%7.2f %s:%7.3f fd=%4d aq=%5dKB vq=%5dKB sq=%5dB f=%"PRId64"/%"PRId64"   \r",
                   get_master_clock(is),
                   (is->audio_st && is->video_st) ? "A-V" : (is->video_st ? "M-V" : (is->audio_st ? "M-A" : "   ")),
                   av_diff,
                   is->frame_drops_early + is->frame_drops_late,
                   aqsize / 1024,
                   vqsize / 1024,
                   sqsize,
                   is->video_st ? is->viddec.avctx->pts_correction_num_faulty_dts : 0,
                   is->video_st ? is->viddec.avctx->pts_correction_num_faulty_pts : 0);
            fflush(stdout);
            last_time = cur_time;
        }
    }
}
相关推荐
AuroraWanderll2 小时前
类和对象(四):默认成员函数详解与运算符重载(下)
c语言·数据结构·c++·算法·stl
Cinema KI2 小时前
二叉搜索树的那些事儿
数据结构·c++
ZEGO即构开发者2 小时前
uni-app 集成音视频 SDK 全攻略:30 分钟搭建跨端视频通话功能
uni-app·音视频·视频通话功能
Trouvaille ~2 小时前
【C++篇】C++11新特性详解(一):基础特性与类的增强
c++·stl·c++11·类和对象·语法·默认成员函数·初始化列表
CSDN_RTKLIB3 小时前
【类定义系列一】C++ 头文件 / 源文件分离
开发语言·c++
CoderCodingNo3 小时前
【GESP】C++五级真题(埃氏筛思想考点) luogu-B3929 [GESP202312 五级] 小杨的幸运数
数据结构·c++·算法
charlee443 小时前
C++中JSON序列化和反序列化的实现
c++·json·序列化·结构体·nlohmann/json
挖矿大亨3 小时前
c++中值传递时是如何触发拷贝构造函数的
开发语言·c++
顶点多余3 小时前
继承和多态
c++·servlet