文章目录
-
-
- 整体架构
- 关键函数功能详解
- [ffplay 视频显示主流程](#ffplay 视频显示主流程)
- [video_display() 子流程](#video_display() 子流程)
- [calculate_display_rect() 尺寸计算逻辑](#calculate_display_rect() 尺寸计算逻辑)
- 性能与算法选择(sws_scale)
- [总结:ffplay 视频输出核心思想](#总结:ffplay 视频输出核心思想)
- code
-
整体架构
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 变化),丢弃旧帧。
-
计算上一帧应显示时长:
clast_duration = nextvp->pts - lastvp->pts delay = compute_target_delay(last_duration, is) // 考虑主时钟(如音频)同步 -
判断是否继续显示上一帧:若当前时间 < frame_timer + delay,则 sleep 并跳转到 display(不换帧)。
-
判断是否丢帧(防卡顿):非逐帧模式 + 启用 framedrop + 非视频主同步 + 当前时间已超下一帧显示时间。
-
更新时钟 & 标记强制刷新:
cis->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, ...) -
渲染:
cSDL_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_YUV420P→SDL_PIXELFORMAT_IYUV)。 - 重分配纹理(如格式/尺寸变化):
realloc_texture()销毁旧纹理,创建新纹理。 - 数据上传(分三种情况):
- SDL 支持 YUV(如 IYUV)→
SDL_UpdateYUVTexture - SDL 支持 RGB →
SDL_UpdateTexture - 不支持格式(
SDL_PIXELFORMAT_UNKNOWN)→ 使用sws_scale转为 BGRA
- SDL 支持 YUV(如 IYUV)→
关于 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;
}
}
}