【音视频】构建视频播放器

这个系列文章我们来介绍音视频相关面试题,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,本篇介绍构建视频播放器。

——来自公众号“关键帧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、开发准备

组件版本/链接备注
FFmpeg6.1 LTS自带硬解封装
SDL22.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 支持情况
Win11D3D11VAAV_PIX_FMT_D3D11≥2.30
LinuxVAAPIAV_PIX_FMT_VAAPI_VLD需 SDL 分支
macOSVideoToolboxAV_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 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论