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,对音视频开发有个清晰的认知后,你将能构建功能更强大、性能更优越的音视频应用。