这个系列文章我们来介绍音视频相关面试题,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍构建视频播放器。
——来自公众号“关键帧Keyframe”的分享
学习和提升音视频开发技术,推荐你加入我们的知识星球:【关键帧的音视频开发圈】

1、总体架构
graph TD
A[数据线程 read_thread] -->|AVPacket| B[解码线程 decode_thread<br>视频/音频各1个]
B -->|AVFrame| C[渲染线程<br>视频 SDL2<br>音频 SDL_Audio]
C --> D[同步时钟<br>master clock]
D --> E[主线程 UI/事件]
口诀:“三线程一包一帧一时钟”
- 三线程:读、解、渲
- 一包:AVPacket 队列
- 一帧:AVFrame 队列
- 一时钟:主时钟(Audio → Video → External)
2、开发准备
| 组件 | 版本/链接 | 备注 |
|---|---|---|
| FFmpeg | 6.1 LTS | 自带硬解封装 |
| SDL2 | 2.30+ | 跨平台渲染 + 音频回调 |
| CMake | ≥3.20 | 示例工程已配置 |
| 源码仓库 | https://github.com/0voice/player-demo | 本章代码实时更新 |
git clone --depth=1 https://github.com/0voice/player-demo
cd player-demo
cmake -B build -DENABLE_D3D11=ON -DENABLE_VAAPI=ON
cmake --build build --parallel
3、主流程拆解
3.1、初始化(main.c)
SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
avdevice_register_all();
avformat_network_init();
PlayerState *is = player_create();
SDL_CreateThread(read_thread, "read", is); // 读线程
SDL_CreateThread(video_decode_thread, "vdec", is);
SDL_CreateThread(audio_decode_thread, "adec", is);
event_loop(is); // 主线程收 SDL 事件
3.2、解封装线程(read_thread)
static int read_thread(void *arg)
{
PlayerState *is = arg;
AVFormatContext *ic = NULL;
avformat_open_input(&ic, is->filename, NULL, NULL);
avformat_find_stream_info(ic, NULL);
/* 选择最佳流 */
int video_index = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
int audio_index = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
is->video_st = ic->streams[video_index];
is->audio_st = ic->streams[audio_index];
/* 打开解码器 + 硬解 */
decoder_init(&is->viddec, is->video_st, AV_HWDEVICE_TYPE_D3D11); // 可切换
decoder_init(&is->auddec, is->audio_st, AV_HWDEVICE_TYPE_NONE);
/* 循环读包 */
for (;;) {
AVPacket *pkt = av_packet_alloc();
if (av_read_frame(ic, pkt) < 0) break;
if (pkt->stream_index == video_index)
packet_queue_put(&is->videoq, pkt);
elseif (pkt->stream_index == audio_index)
packet_queue_put(&is->audioq, pkt);
else
av_packet_free(&pkt);
}
}
3.3、解码线程(通用模板)
static int decode_thread(void *arg)
{
Decoder *d = arg;
PlayerState *is = d->is;
AVFrame *frame = av_frame_alloc();
for (;;) {
AVPacket *pkt = packet_queue_get(d->queue);
avcodec_send_packet(d->ctx, pkt);
while (avcodec_receive_frame(d->ctx, frame) == 0) {
frame_queue_put(d->fq, frame);
frame = av_frame_alloc();
}
av_packet_free(&pkt);
}
}
3.4、音频渲染(SDL_AudioSpec 回调)
void sdl_audio_callback(void *userdata, Uint8 *stream, int len)
{
PlayerState *is = userdata;
int len1 = 0;
double pts;
while (len > 0) {
if (is->audio_buf_index >= is->audio_buf_size) {
is->audio_buf_size = audio_decode_frame(is, &pts);
is->audio_buf_index = 0;
}
len1 = is->audio_buf_size - is->audio_buf_index;
if (len1 > len) len1 = len;
memcpy(stream, is->audio_buf + is->audio_buf_index, len1);
len -= len1;
stream += len1;
is->audio_buf_index += len1;
}
/* 更新音频时钟 */
is->audio_clock = pts + (double)len1 / (2*2*is->audio_tgt.freq); // 2 ch 16bit
}
3.5、视频渲染 + 同步(核心)
static void video_refresh(PlayerState *is, SDL_Renderer *renderer)
{
double last_duration, pts_delay, sync_threshold, diff;
AVFrame *vp = frame_queue_peek(&is->pictq);
/* 计算视频时钟与主时钟的差值 */
diff = vp->pts - get_master_clock(is);
/* 根据差值决定显示 or drop */
sync_threshold = FFMAX(0.04, FFMIN(0.1, last_duration));
if (diff <= -sync_threshold) {
frame_queue_next(&is->pictq); // 落后太多,丢帧
return;
}
/* 上传纹理 */
if (is->hw_decoder)
SDL_UpdateNVTexture(texture, NULL, vp->data[0], vp->linesize[0],
vp->data[1], vp->linesize[1]);
else
SDL_UpdateYUVTexture(texture, NULL, vp->data[0], vp->linesize[0],
vp->data[1], vp->linesize[1],
vp->data[2], vp->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
/* 计算准确帧间隔并延时 */
pts_delay = vp->pts - is->frame_last_pts;
if (pts_delay <= 0 || pts_delay >= 1.0) pts_delay = last_duration;
is->frame_timer += pts_delay;
double delay = is->frame_timer - av_gettime_relative() / 1000000.0;
if (delay > 0) SDL_Delay((Uint32)(delay * 1000));
}
4、硬解接入(以 D3D11 为例)
AVBufferRef *hw_device_ctx = NULL;
av_hwdevice_ctx_create(&hw_device_ctx, AV_HWDEVICE_TYPE_D3D11VA, NULL, NULL, 0);
codec_ctx->hw_device_ctx = hw_device_ctx;
/* 解码后帧为 AV_PIX_FMT_D3D11,SDL2.30 已支持直接创建 D3D11Texture */
| 平台 | 硬解类型 | 输出像素格式 | SDL 支持情况 |
|---|---|---|---|
| Win11 | D3D11VA | AV_PIX_FMT_D3D11 | ≥2.30 |
| Linux | VAAPI | AV_PIX_FMT_VAAPI_VLD | 需 SDL 分支 |
| macOS | VideoToolbox | AV_PIX_FMT_VIDEOTOOLBOX | 原生纹理 |
5、音画同步策略对比
| 策略 | 实现方式 | 延迟 | 适用场景 |
|---|---|---|---|
| Audio Master | 音频时钟为主,视频同步到音频 | 低 | 通用播放器 |
| External | 系统时钟为主,音/视频都追赶 | 中 | 直播、连麦 |
| Video Master | 视频为主,音频丢包或重采样 | 高 | 特殊录屏需求 |
默认采用 Audio Master,代码见 get_master_clock():
double get_master_clock(PlayerState *is)
{
return is->auddec.pkt_serial ? is->audio_clock : is->video_clock;
}
6、拖拽进度/快进快退
case SDL_KEYDOWN:
if (event.key.keysym.sym == SDLK_LEFT)
stream_seek(is, -10.0, 0);
else if (event.key.keysym.sym == SDLK_RIGHT)
stream_seek(is, 10.0, 0);
break;
void stream_seek(PlayerState *is, double incr, int rel)
{
double pos = get_master_clock(is);
if (rel) pos +=
版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。