C++使用ffmpeg + SDL2.0搭建简单播放器
前言
本文参考雷霄骅的博文https://blog.csdn.net/leixiaohua1020/article/details/8652605,使用c++根据ffmpeg-4.1版本改写(原文代码基于旧版本ffmpeg)。代码见下文。
本文代码地址见https://github.com/2997215859/ffplay-learn/blob/master/Video/print_info.cpp
本文代码基于ffmpeg-4.1版本,事先需要安装好ffmpeg
本文代码提供CMakeLists.txt,见附录CMakeLists.txt部分,或根据CMakeLists.txt改写。需要链接的库如下(基本上安装ffmpeg、ffplay、SDL2之后就有了)。
1 avdevice avfilter avformat avcodec swscale swresample postproc avutil m xcb xcb-shm xcb xcb-shape xcb xcb-xfixes xcb-render xcb-shape xcb asound pthread m fontconfig freetype freetype z bz2 lzma SDL2 SDL2main
流程
解码器即C++中使用ffmpeg解码视频到YUV数据示例,SDL渲染是一个封装了音视频底层接口的库,本文使用2.0版本。
解码器主要使用ffmpeg中的几个函数:avformat_alloc_context, avcodec_find_decoder, avcodec_send_packet, avcodec_receive_frame
代码剖析
读取视频格式并获取视频流的索引
string filepath = "/home/sensetime/videos/big_buck_bunny_720p_30mb.mp4";
avdevice_register_all();
avformat_network_init();
AVFormatContext *avFormatContext = avformat_alloc_context();
if (avformat_open_input(&avFormatContext, filepath.c_str(), NULL, NULL) != 0) { // 打开输入流
cerr << "Failed to open input stream" << endl;
exit(1);
}
if (avformat_find_stream_info(avFormatContext, NULL) < 0) { // 分析流信息
cerr << "Failed to find stream information." << endl;
exit(1);
}
int videoIndex = -1;
for (int i=0;i<avFormatContext->nb_streams;i++) { // 查找第一个视频流的索引
if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoIndex = i;
break;
}
}
if (videoIndex == -1) {
cerr << "Failed to find a video stream." << endl;
exit(1);
}构造解码器和解码上下文
AVStream *avStream = avFormatContext->streams[videoIndex];
AVCodec *avCodec = avcodec_find_decoder(avStream->codecpar->codec_id);
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec); // 为该解码器新建解码上下文
if (!avCodecContext) {
cerr << "Failed to allocate the decoder context for stream" << endl;
return -1;
}
if(avcodec_parameters_to_context(avCodecContext, avStream->codecpar) < 0) { // 将流的解码参数拷贝到解码上下文
cerr << "Failed to copy decoder parameters to input decoder context." << endl;
return -1;
}
avCodecContext->pkt_timebase = avStream->time_base;
cout << avCodec->id << endl;
if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
cerr << "Failed to open codec." << endl;
exit(1);
}
解码
解码使用avcodec_send_packet, avcodec_receive_frame这一对函数解码,代码结构如下
...
// yuvFrame用于存储新帧,这段代码是为其分配存储空间
AVFrame *yuvFrame = av_frame_alloc();
int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, avCodecContext->width, avCodecContext->height, 1);
unsigned char *buffer = (unsigned char*)av_malloc(bufferSize);
av_image_fill_arrays(yuvFrame->data, yuvFrame->linesize, buffer, AV_PIX_FMT_YUV420P, avCodecContext->width, avCodecContext->height, 1);
...
...
// swsContext是用于转换帧格式(比如长宽、像素存储方式等)的上下文类型,frame将由此上下文转换到yuvFrame上,再由SDL渲染到显示器
SwsContext *imgConvertContext = sws_getContext(avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_YUV420P,
SWS_BICUBIC, NULL, NULL, NULL);
...
while (av_read_frame(avFormatContext, avPacket) >= 0) { // 从format上下文中读取压缩编码的数据包packet
if (avPacket->stream_index == videoIndex) { // 只处理视频类型的packet,音频类型的packet
if (avcodec_send_packet(avCodecContext, avPacket) < 0) { // 将packet提交到解码上下文
cerr << "Failed to submitting the packet to the decoder\n";
return -1;
}
int ret;
while (true) {
SDL_WaitEvent(&event); // 阻塞等待事件
if (event.type == SFM_REFRESH_EVENT) {
if (avcodec_receive_frame(avCodecContext, frame) >= 0) { // 从解码上下文中读取帧frame
// 使用sws_scale函数转换帧frame的格式,填写到新的帧yumFrame
sws_scale(imgConvertContext, frame->data, frame->linesize, 0, avCodecContext->height, yuvFrame->data, yuvFrame->linesize);
yuvFrame->height = frame->height;
yuvFrame->width = frame->width;
...
// 中间这块可以对yuvFrame做其他处理,比如画文字、画框
...
// 使用SDL渲染yuvFrame
...
} else {
break;
}
} else if(event.type==SDL_KEYDOWN){
//Pause
if(event.key.keysym.sym==SDLK_SPACE)
stopRefresh =! stopRefresh;
}else if(event.type==SDL_QUIT){
stopThread = 1;
}else if(event.type==SFM_BREAK_EVENT){
break;
}
}
}
av_packet_unref(avPacket);
}
SDL渲染
SDL 渲染前,需要建立渲染窗口和纹理
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
cerr << "Failed to initialization SDL - " << SDL_GetError();
exit(1);
}
int screenW = avCodecContext->width;
int screenH = avCodecContext->height;
SDL_Window *screen = SDL_CreateWindow("simple ffmpeg player's window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screenW, screenH, SDL_WINDOW_OPENGL);
if (!screen) {
cerr << "SDL: could not create window - " << SDL_GetError();
exit(1);
}
SDL_Renderer *sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
SDL_Texture *sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, avCodecContext->width, avCodecContext->height);
SDL_Rect sdlRect;
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = screenW;
sdlRect.h = screenH;SDL渲染帧
SDL_UpdateTexture(sdlTexture, NULL, yuvFrame->data[0], yuvFrame->linesize[0]); SDL_RenderClear(sdlRenderer); SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL); SDL_RenderPresent(sdlRenderer);
刷新线程
刷新线程使用SDL_CreateThread创建
SDL_CreateThread(refreshThread, NULL, NULL);
int refreshThread (void *opaque) {
stopThread = false;
SDL_Event event;
event.type = SFM_REFRESH_EVENT;
while (!stopThread) {
if (!stopRefresh) {
SDL_PushEvent(&event);
}
SDL_Delay(40); // 40ms 刷新一次SDL
}
stopThread = false;
stopRefresh = false;
{
SDL_Event event;
event.type = SFM_BREAK_EVENT;
SDL_PushEvent(&event);
}
return 0;
}
附录
完整代码
#include <atomic>
#include <string>
#include <iostream>
#ifdef __cplusplus
extern "C" {
#endif
#include <libavutil/avutil.h>
#include <libavdevice/avdevice.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <SDL2/SDL.h>
#include <libavutil/parseutils.h>
#include <libavutil/bprint.h>
#include <libavutil/tree.h>
#include <libavutil/eval.h>
#include <libavutil/lfg.h>
#include <libavutil/timecode.h>
#include <libavutil/file.h>
#include <libavutil/random_seed.h>
#include <fenv.h>
#ifdef __cplusplus
}
#endif
#define SFM_REFRESH_EVENT (SDL_USEREVENT + 1)
#define SFM_BREAK_EVENT (SDL_USEREVENT + 2)
using namespace std;
atomic<bool> stopThread(false);
atomic<bool> stopRefresh(false);
int refreshThread (void *opaque) {
stopThread = false;
SDL_Event event;
event.type = SFM_REFRESH_EVENT;
while (!stopThread) {
if (!stopRefresh) {
SDL_PushEvent(&event);
}
SDL_Delay(40); // 40ms 刷新一次SDL
}
stopThread = false;
stopRefresh = false;
{
SDL_Event event;
event.type = SFM_BREAK_EVENT;
SDL_PushEvent(&event);
}
return 0;
}
int main () {
string filepath = "/home/sensetime/videos/big_buck_bunny_720p_30mb.mp4";
avdevice_register_all();
avformat_network_init();
AVFormatContext *avFormatContext = avformat_alloc_context();
if (avformat_open_input(&avFormatContext, filepath.c_str(), NULL, NULL) != 0) { // 打开输入流
cerr << "Failed to open input stream" << endl;
exit(1);
}
if (avformat_find_stream_info(avFormatContext, NULL) < 0) { // 分析流信息
cerr << "Failed to find stream information." << endl;
exit(1);
}
int videoIndex = -1;
for (int i=0;i<avFormatContext->nb_streams;i++) { // 查找第一个视频流的索引
if (avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
videoIndex = i;
break;
}
}
if (videoIndex == -1) {
cerr << "Failed to find a video stream." << endl;
exit(1);
}
AVStream *avStream = avFormatContext->streams[videoIndex];
AVCodec *avCodec = avcodec_find_decoder(avStream->codecpar->codec_id);
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec); // 为该解码器新建解码上下文
if (!avCodecContext) {
cerr << "Failed to allocate the decoder context for stream" << endl;
return -1;
}
if(avcodec_parameters_to_context(avCodecContext, avStream->codecpar) < 0) { // 将流的解码参数拷贝到解码上下文
cerr << "Failed to copy decoder parameters to input decoder context." << endl;
return -1;
}
avCodecContext->pkt_timebase = avStream->time_base;
cout << avCodec->id << endl;
if (avcodec_open2(avCodecContext, avCodec, NULL) < 0) {
cerr << "Failed to open codec." << endl;
exit(1);
}
// yuvFrame用于存储新帧,这段代码是为其分配存储空间
AVFrame *yuvFrame = av_frame_alloc();
int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, avCodecContext->width, avCodecContext->height, 1);
unsigned char *buffer = (unsigned char*)av_malloc(bufferSize);
av_image_fill_arrays(yuvFrame->data, yuvFrame->linesize, buffer, AV_PIX_FMT_YUV420P, avCodecContext->width, avCodecContext->height, 1);
//Output Info-----------------------------
printf("---------------- File Information ---------------\n");
av_dump_format(avFormatContext, 0, filepath.c_str(),0);
printf("-------------------------------------------------\n");
SwsContext *imgConvertContext = sws_getContext(avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_YUV420P,
SWS_BICUBIC, NULL, NULL, NULL);
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
cerr << "Failed to initialization SDL - " << SDL_GetError();
exit(1);
}
int screenW = avCodecContext->width;
int screenH = avCodecContext->height;
SDL_Window *screen = SDL_CreateWindow("simple ffmpeg player's window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screenW, screenH, SDL_WINDOW_OPENGL);
if (!screen) {
cerr << "SDL: could not create window - " << SDL_GetError();
exit(1);
}
SDL_Renderer *sdlRenderer = SDL_CreateRenderer(screen, -1, 0);
SDL_Texture *sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, avCodecContext->width, avCodecContext->height);
SDL_Rect sdlRect;
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = screenW;
sdlRect.h = screenH;
SDL_CreateThread(refreshThread, NULL, NULL);
SDL_Event event;
AVPacket *avPacket = (AVPacket*) av_malloc(sizeof(AVPacket));
AVFrame *frame = av_frame_alloc();
double timeBase = av_q2d(avStream->time_base);
double frameRate = av_q2d(avStream->avg_frame_rate);
double interval = 1 / (timeBase * frameRate);
while (av_read_frame(avFormatContext, avPacket) >= 0) { // 从format上下文中读取压缩编码的数据包
if (avPacket->stream_index == videoIndex) { // 只处理视频类型的packet,音频类型的packet
if (avcodec_send_packet(avCodecContext, avPacket) < 0) { // 将packet提交到解码上下文
cerr << "Failed to submitting the packet to the decoder\n";
return -1;
}
int ret;
while (true) {
SDL_WaitEvent(&event);// 阻塞等待事件
if (event.type == SFM_REFRESH_EVENT) {
if (avcodec_receive_frame(avCodecContext, frame) >= 0) { // 从解码上下文中读取帧frame
sws_scale(imgConvertContext, frame->data, frame->linesize, 0, avCodecContext->height, yuvFrame->data, yuvFrame->linesize);
yuvFrame->height = frame->height;
yuvFrame->width = frame->width;
SDL_UpdateTexture(sdlTexture, NULL, yuvFrame->data[0], yuvFrame->linesize[0]);
SDL_RenderClear(sdlRenderer);
SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL);
SDL_RenderPresent(sdlRenderer);
av_frame_unref(frame);
} else {
break;
}
} else if(event.type==SDL_KEYDOWN){
//Pause
if(event.key.keysym.sym==SDLK_SPACE)
stopRefresh =! stopRefresh;
}else if(event.type==SDL_QUIT){
stopThread = 1;
}else if(event.type==SFM_BREAK_EVENT){
break;
}
}
}
av_packet_unref(avPacket);
}
sws_freeContext(imgConvertContext);
SDL_Quit();
av_frame_unref(frame);
av_frame_unref(yuvFrame);
avcodec_close(avCodecContext);
avformat_close_input(&avFormatContext);
return 0;
} CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
project(player)
set(CMAKE_CXX_STANDARD 11)
include_directories(./)
include_directories(/usr/include/.)
include_directories(/usr/local/include/.)
link_directories(/usr/lib/)
link_directories(/usr/local/lib/)
# 设置可执行文件生成路径
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin")
# 生成debug版本
SET(CMAKE_BUILD_TYPE "release")
if (CMAKE_BUILD_TYPE STREQUAL debug)
add_definitions(-D_DEBUG)
endif ()
SET(CMAKE_CXX_FLAGS_DEBUG "$ENV{CXXFLAGS} -O0 -Wall -g -ggdb -std=c++11")
SET(CMAKE_CXX_FLAGS_RELEASE "$ENV{CXXFLAGS} -O3 -Wall -std=c++11")
link_directories(/usr/include/freetype2/.)
add_executable(simple_player
simple_player.cpp)
target_link_libraries(simple_player
avdevice
avfilter
avformat
avcodec
swscale
swresample
postproc
avutil
# avresample
m xcb xcb-shm xcb xcb-shape xcb xcb-xfixes xcb-render xcb-shape xcb asound
pthread m fontconfig freetype freetype
z
bz2
lzma
SDL2
SDL2main)
没有帐号?立即注册