FFmpeg 是音视频开发领域的“瑞士军刀”,其强大的功能不仅体现在命令行上,更在于其丰富的 libav 系列库。本文将带领大家利用 FFmpeg 的核心库,从零开始用 C/C++ 实现一个简易的视频播放器

内容参考自 GitHub 项目:awesome_audio_video_learning

完成本教程后,你将对播放器的基本工作原理、多线程协同以及音视频同步有更深入的理解。


项目环境准备

本文所有代码基于 Linux 环境,需要安装 FFmpeg 及其开发库。

  • FFmpeg 开发库libavformatlibavcodeclibavutil 等。
  • SDL2 库:用于窗口创建、事件处理和视频渲染。

在 Ubuntu 系统上,可以通过以下命令安装:

sudo apt update
sudo apt install build-essential libavformat-dev libavcodec-dev libavutil-dev libswscale-dev libsdl2-dev

播放器核心流程解析

一个视频播放器可以抽象为以下几个核心步骤:

1. 解封装(Demuxing):从视频文件中读取封装好的数据包(AVPacket),并将其分发给不同的流(音频流、视频流)。
2. 解码(Decoding):将数据包中的编码数据解码为原始的音视频帧(AVFrame)。
3. 音视频同步:确保音频和视频的播放时间点一致,避免音画不同步。
4. 渲染(Rendering):将解码后的视频帧显示到屏幕上,将音频帧送入声卡播放。

我们将使用多线程来并行处理这些任务,以保证流畅播放。

关键代码实现

下面,我们将逐步构建播放器的主要逻辑。

1. 初始化 FFmpeg 和 SDL2

在开始前,需要初始化 FFmpeg 的所有组件,并创建 SDL2 窗口。

#include <iostream>
  #include <SDL2/SDL.h>
    extern "C" {
    #include <libavformat/avformat.h>
      #include <libavcodec/avcodec.h>
        #include <libavutil/imgutils.h>
          #include <libswscale/swscale.h>
            }
            // 全局变量和初始化函数
            SDL_Window *window = nullptr;
            SDL_Renderer *renderer = nullptr;
            SDL_Texture *texture = nullptr;
            AVFormatContext *fmt_ctx = nullptr;
            AVCodecContext *video_codec_ctx = nullptr;
            int video_stream_idx = -1;
            void init() {
            avformat_network_init();
            // 初始化网络
            if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
            std::cerr <<
            "SDL_Init 失败: " <<
            SDL_GetError() << std::endl;
            exit(1);
            }
            }
2. 解封装与流信息获取

使用 avformat_open_input() 打开视频文件,并使用 avformat_find_stream_info() 读取流信息。

bool open_media(const char* filename) {
if (avformat_open_input(&fmt_ctx, filename, nullptr, nullptr) <
0) {
std::cerr <<
"无法打开文件: " << filename << std::endl;
return false;
}
if (avformat_find_stream_info(fmt_ctx, nullptr) <
0) {
std::cerr <<
"无法找到流信息" << std::endl;
return false;
}
// 找到视频流
video_stream_idx = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_stream_idx <
0) {
std::cerr <<
"无法找到视频流" << std::endl;
return false;
}
// 找到解码器并打开
AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_idx]->codecpar->codec_id);
if (!codec) {
std::cerr <<
"找不到解码器" << std::endl;
return false;
}
video_codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(video_codec_ctx, fmt_ctx->streams[video_stream_idx]->codecpar);
if (avcodec_open2(video_codec_ctx, codec, nullptr) <
0) {
std::cerr <<
"无法打开解码器" << std::endl;
return false;
}
return true;
}
3. 解码与渲染循环

这是播放器的核心循环。我们将在主线程中进行解封装和解码,并利用 SDL2 进行渲染。

int main(int argc, char* argv[]) {
if (argc <
2) {
std::cerr <<
"用法: " << argv[0] <<
" <视频文件>" << std::endl;
  return -1;
  }
  init();
  if (!open_media(argv[1])) {
  return -1;
  }
  // 创建SDL窗口和纹理
  window = SDL_CreateWindow("FFmpeg Player", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
  video_codec_ctx->width, video_codec_ctx->height, SDL_WINDOW_SHOWN);
  renderer = SDL_CreateRenderer(window, -1, 0);
  texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
  video_codec_ctx->width, video_codec_ctx->height);
  AVPacket *packet = av_packet_alloc();
  AVFrame *frame = av_frame_alloc();
  SwsContext *sws_ctx = sws_getContext(video_codec_ctx->width, video_codec_ctx->height, video_codec_ctx->pix_fmt,
  video_codec_ctx->width, video_codec_ctx->height, AV_PIX_FMT_YUV420P,
  SWS_BILINEAR, nullptr, nullptr, nullptr);
  // 主循环
  while (av_read_frame(fmt_ctx, packet) >= 0) {
  if (packet->stream_index == video_stream_idx) {
  avcodec_send_packet(video_codec_ctx, packet);
  int ret = avcodec_receive_frame(video_codec_ctx, frame);
  if (ret == 0) {
  // 解码成功,进行渲染
  SDL_Event event;
  while (SDL_PollEvent(&event)) {
  if (event.type == SDL_QUIT) {
  // ... 退出逻辑
  }
  }
  // 将解码后的YUV帧转换为SDL可渲染的YV12格式
  uint8_t *pixels[4];
  int pitch[4];
  SDL_LockTexture(texture, nullptr, (void**)pixels, pitch);
  sws_scale(sws_ctx, (uint8_t const * const *)frame->data, frame->linesize, 0, frame->height, pixels, pitch);
  SDL_UnlockTexture(texture);
  // 渲染到屏幕
  SDL_RenderClear(renderer);
  SDL_RenderCopy(renderer, texture, nullptr, nullptr);
  SDL_RenderPresent(renderer);
  }
  }
  av_packet_unref(packet);
  }
  // 释放资源
  sws_freeContext(sws_ctx);
  av_frame_free(&frame);
  av_packet_free(&packet);
  avcodec_close(video_codec_ctx);
  avformat_close_input(&fmt_ctx);
  SDL_Quit();
  return 0;
  }

总结与展望

本文实现了一个最简单的视频播放器,它能够:

  • 打开一个视频文件。
  • 读取视频流,找到正确的解码器。
  • 循环读取数据包并解码为帧。
  • 使用 SDL2 渲染视频帧。

当然,这个播放器还有许多可以完善的地方,例如:

1. 多线程:将解封装、解码和渲染放入不同的线程,以实现真正的并行处理。
2. 音视频同步:加入音频流处理,并使用时间戳(PTS)进行音画同步。
3. 优化:实现播放控制(暂停、快进)、缓冲区管理和错误处理。

想要继续深入学习音视频开发的同学,可以去 GitHub 里面查看这个项目,awesome_audio_video_learning,对音视频开发有个清晰的认知后,你将能构建功能更强大、性能更优越的音视频应用。

posted on 2025-09-14 19:43  ycfenxi  阅读(62)  评论(0)    收藏  举报