基于 FFMPEG + SDL2 的视频播放器
基于 FFMPEG + SDL2 的视频播放器
本文是在雷神的 最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0) 的基础上修改的,对雷神代码中 ffmpeg 弃用的函数做了修改。与雷神博客一样同样实现了普通版本和 su 版本。
SU 版特征如下:
- SDL弹出的窗口可以移动了
- 画面显示是严格的40ms一帧
所有代码均在 VS2017 C++17,SDL-2.0.14 版本上测试通过。
FFMPEG 解码流程
VideoPlayer 类声明
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "SDL.h"
#ifdef __cplusplus
}
#endif
#include <iostream>
class VideoPlayer {
public:
VideoPlayer(const std::string& file_path);
~VideoPlayer();
// call this function before use VideoPlayer.
bool InitVideoPlayer();
void Show();
private:
AVFormatContext* fmt_ctx_{ nullptr };
AVCodecContext* codec_ctx_{ nullptr };
AVCodec* codec_{ nullptr };
AVCodecParameters *parameters_{ nullptr };
struct SwsContext *sws_ctx_{ nullptr };
AVFrame* frame_{ nullptr };
AVFrame* yuv_frame_{ nullptr };
AVPacket* packet_{ nullptr };
bool is_init_{ false };
int video_index_{ -1 };
std::string file_path_;
unsigned char* out_buffer_{ nullptr };
SDL_Window* window_{ nullptr };
};
测试主程序
#include "VideoPlayer.h"
int main(int argc, char** argv) {
std::string file_path("output.mp4");
VideoPlayer player(file_path);
if (!player.InitVideoPlayer()) {
return -1;
}
// 如果使用 Su 版本调用 player.SuShow()
// player.SuShow()
player.Show();
return 0;
}
VideoPlayer 实现
#include "VideoPlayer.h"
// 定义刷新事件
#define SFM_REFRESH_EVENT (SDL_USEREVENT + 1)
#define SFM_BREAK_EVENT (SDL_USEREVENT + 2)
static bool thread_exit = 0;
static bool thread_pause = 0;
static int SfpRefreshThread(void *opaque) {
thread_exit = false;
thread_pause = false;
while (!thread_exit) {
if (!thread_pause) {
SDL_Event event;
event.type = SFM_REFRESH_EVENT;
SDL_PushEvent(&event);
}
SDL_Delay(40);
}
thread_exit = false;
thread_pause = false;
SDL_Event event;
event.type = SFM_BREAK_EVENT;
SDL_PushEvent(&event);
return 0;
}
VideoPlayer::VideoPlayer(const std::string& file_path) : file_path_(file_path) {
// 新版的 ffmpeg 已经不需要执行 av_register_all() 了
avformat_network_init();
}
VideoPlayer::~VideoPlayer() {
// 释放所有资源
av_frame_free(&yuv_frame_);
av_frame_free(&frame_);
avcodec_close(codec_ctx_);
avformat_close_input(&fmt_ctx_);
}
bool VideoPlayer::InitVideoPlayer() {
fmt_ctx_ = avformat_alloc_context();
if (avformat_open_input(&fmt_ctx_, file_path_.c_str(), nullptr, nullptr) != 0)
{
av_log(nullptr, AV_LOG_ERROR, "Couldn't open input stream");
return false;
}
if (avformat_find_stream_info(fmt_ctx_, nullptr) < 0)
{
av_log(nullptr, AV_LOG_ERROR, "Couldn't find stream information");
return false;
}
// 老版中根据 AVFormatContext->stream[video_index]->codec 的方式已被弃用
// 新版本中使用 av_find_best_stream(), 在第二个参数中指定要找到的流是视频还是音频即可
video_index_ = av_find_best_stream(fmt_ctx_, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_index_ == -1) {
av_log(nullptr, AV_LOG_ERROR, "Didn't find a video stream");
return false;
}
parameters_ = fmt_ctx_->streams[video_index_]->codecpar;
codec_ = avcodec_find_decoder(parameters_->codec_id);
if (!codec_) {
av_log(nullptr, AV_LOG_ERROR, "Couldn't open codec");
return false;
}
codec_ctx_ = avcodec_alloc_context3(codec_);
if (avcodec_parameters_to_context(codec_ctx_, parameters_) < 0) {
av_log(nullptr, AV_LOG_ERROR, "Couldn't copy params to context");
return false;
}
// 使用 avcodec_open2 替换原先的 avcodec_open 来打开解码器
if (avcodec_open2(codec_ctx_, codec_, nullptr) != 0) {
av_log(nullptr, AV_LOG_ERROR, "Couldn't open codec.");
return false;
}
frame_ = av_frame_alloc();
yuv_frame_ = av_frame_alloc();
// 填充 YUV 数据内容,防止因为 yuv_frame 中没有为 data 分配内存空间而报错
out_buffer_ = static_cast<unsigned char*>(av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, codec_ctx_->width, codec_ctx_->height, 1)));
av_image_fill_arrays(yuv_frame_->data, yuv_frame_->linesize,
const_cast<const uint8_t*>(out_buffer_),
AV_PIX_FMT_YUV420P,
codec_ctx_->width, codec_ctx_->height, 1);
packet_ = av_packet_alloc();
// 打印一些关于 output format 的信息,例如比特率,延时之类
// 此处第二个参数指定要打印的流的索引
av_dump_format(fmt_ctx_, 0, file_path_.c_str(), 0);
// 创建编码转换器,通过使用 sws_scale 函数将我们读入的视频帧转换为我们需要的格式
// 此处的 AV_PIX_FMT_YUV420P 指定要转换成的格式
sws_ctx_ = sws_getContext(codec_ctx_->width, codec_ctx_->height, codec_ctx_->pix_fmt,
codec_ctx_->width, codec_ctx_->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, nullptr, nullptr, nullptr);
is_init_ = true;
}
void VideoPlayer::Show() {
if (!is_init_) {
av_log(nullptr, AV_LOG_ERROR, "You should init VideoPlayer first.");
return;
}
if (SDL_Init(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))) {
av_log(nullptr, AV_LOG_ERROR, "Could not initialize SDL - %s", SDL_GetError());
return;
}
int h = codec_ctx_->height;
int w = codec_ctx_->width;
window_ = SDL_CreateWindow("__name__", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
w, h, SDL_WINDOW_SHOWN);
if (!window_) {
av_log(nullptr, AV_LOG_ERROR, "SDL: could not create window - exiting:%s", SDL_GetError());
return;
}
SDL_Renderer* renderer = SDL_CreateRenderer(window_, -1, 0);
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, w, h);
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = w;
rect.h = h;
while (av_read_frame(fmt_ctx_, packet_) >= 0) {
// 我们在此处仅处理视频帧,不处理音频帧
if (packet_->stream_index == video_index_) {
avcodec_send_packet(codec_ctx_, packet_);
while (avcodec_receive_frame(codec_ctx_, frame_) == 0) {
sws_scale(sws_ctx_, (const unsigned char* const *)frame_->data, frame_->linesize, 0, h, yuv_frame_->data, yuv_frame_->linesize);
SDL_UpdateYUVTexture(texture, &rect, yuv_frame_->data[0], yuv_frame_->linesize[0],
yuv_frame_->data[1], yuv_frame_->linesize[1],
yuv_frame_->data[2], yuv_frame_->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, nullptr, &rect);
SDL_RenderPresent(renderer);
SDL_Delay(40);
}
}
av_packet_unref(packet_);
}
// 在解码器上下文中可能还剩有以下没处理的视频,将这些视频处理完后显示
while (true) {
int ret = avcodec_send_packet(codec_ctx_, packet_);
if (ret != 0) {
break;
}
while (avcodec_receive_frame(codec_ctx_, frame_) == 0) {
sws_scale(sws_ctx_, (const unsigned char* const *)frame_->data, frame_->linesize, 0, h, yuv_frame_->data, yuv_frame_->linesize);
SDL_UpdateYUVTexture(texture, &rect, yuv_frame_->data[0], yuv_frame_->linesize[0],
yuv_frame_->data[1], yuv_frame_->linesize[1],
yuv_frame_->data[2], yuv_frame_->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, nullptr, &rect);
SDL_RenderPresent(renderer);
SDL_Delay(40);
}
}
sws_freeContext(sws_ctx_);
SDL_Quit();
}
void VideoPlayer::SuShow()
{
if (!is_init_) {
av_log(nullptr, AV_LOG_ERROR, "You should init VideoPlayer first.");
return;
}
if (SDL_Init(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER))) {
av_log(nullptr, AV_LOG_ERROR, "Could not initialize SDL - %s", SDL_GetError());
return;
}
int h = codec_ctx_->height;
int w = codec_ctx_->width;
window_ = SDL_CreateWindow("__name__", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
w, h, SDL_WINDOW_SHOWN);
if (!window_) {
av_log(nullptr, AV_LOG_ERROR, "SDL: could not create window - exiting:%s", SDL_GetError());
return;
}
SDL_Renderer* renderer = SDL_CreateRenderer(window_, -1, 0);
SDL_Texture* texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, w, h);
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.w = w;
rect.h = h;
SDL_Thread *sdl_thread = SDL_CreateThread(SfpRefreshThread, nullptr, nullptr);
SDL_Event event;
while (true) {
SDL_WaitEvent(&event);
if (event.type == SFM_REFRESH_EVENT) {
// 当当前的事件是需要刷新显示时,一直循环直到当前的包为视频包时结束
while (true) {
if (av_read_frame(fmt_ctx_, packet_) < 0) thread_exit = true;
if (packet_->stream_index == video_index_) break;
}
avcodec_send_packet(codec_ctx_, packet_);
while (avcodec_receive_frame(codec_ctx_, frame_) == 0) {
sws_scale(sws_ctx_, (const unsigned char* const *)frame_->data, frame_->linesize, 0, h, yuv_frame_->data, yuv_frame_->linesize);
SDL_UpdateYUVTexture(texture, &rect, yuv_frame_->data[0], yuv_frame_->linesize[0],
yuv_frame_->data[1], yuv_frame_->linesize[1],
yuv_frame_->data[2], yuv_frame_->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, nullptr, nullptr);
SDL_RenderPresent(renderer);
}
av_packet_unref(packet_);
} else if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_SPACE) thread_pause = !thread_pause;
} else if (event.type == SDL_QUIT) {
thread_exit = true;
} else if (event.type == SFM_BREAK_EVENT) {
break;
}
}
sws_freeContext(sws_ctx_);
SDL_Quit();
}
还没有评论,来说两句吧...