FFmpeg 实战:从零开始写一个简易视频播放器
FFmpeg 是音视频开发领域的“瑞士军刀”,其强大的功能不仅体现在命令行上,更在于其丰富的 libav
系列库。本文将带领大家利用 FFmpeg 的核心库,从零开始用 C/C++ 实现一个简易的视频播放器。
内容参考自 GitHub 项目:awesome_audio_video_learning
完成本教程后,你将对播放器的基本工作原理、多线程协同以及音视频同步有更深入的理解。
项目环境准备
本文所有代码基于 Linux 环境,需要安装 FFmpeg 及其开发库。
- FFmpeg 开发库:
libavformat
、libavcodec
、libavutil
等。 - 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,对音视频开发有个清晰的认知后,你将能构建功能更强大、性能更优越的音视频应用。