FFmpeg入门:最简单的视频播放器
FFmpeg入门第一篇,制作一个简单的MP4视频播放器。
整体流程
话不多说,直接上流程图
视频播放速率控制
这里可以直接看图中的帧率同步模块,可以分为如下几步
- 获取到当前帧的预期播放时间:根据时间基time_base和帧pts计算;time_base*pts;
- 解码渲染过程中,获取当前时间av_gettime,计算和预期播放时间的时间差;
- 使用SDL_Delay函数延迟播放时间差。
源代码
tutorial01.h
c
//
// tutorial02.h
// tutorial02
//
// Created by chenhuaiyi on 2025/2/13.
//
#ifndef tutorial02_h
#define tutorial02_h
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
#include <SDL.h>
#include <SDL_thread.h>
#include <stdio.h>
/**
宏定义
*/
#define SFM_REFRESH_EVENT (SDL_USEREVENT+1)
/**
全局变量
*/
extern int thread_exit;
/**
方法声明
*/
char* get_frame_type(AVFrame* frame);
#endif /* tutorial02_h */
tutorial02.c
c
// tutorial02.c
// A pedagogical video player that will stream through every video frame as fast as it can.
#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif
#include "tutorial02.h"
// 控制程序是否结束
int thread_exit=0;
/**
获取帧类型
*/
char* get_frame_type(AVFrame* frame) {
switch (frame->pict_type) {
case AV_PICTURE_TYPE_I:
return "I";
break;
case AV_PICTURE_TYPE_P:
return "P";
break;
case AV_PICTURE_TYPE_B:
return "B";
break;
case AV_PICTURE_TYPE_S:
return "S";
break;
case AV_PICTURE_TYPE_SI:
return "SI";
break;
case AV_PICTURE_TYPE_SP:
return "SP";
break;
case AV_PICTURE_TYPE_BI:
return "BI";
break;
default:
return "N";
break;
}
}
int main(int argc, char *argv[]) {
AVFormatContext* pFormatCtx = NULL;
int i, videoStream;
AVCodecContext* pCodecCtx = avcodec_alloc_context3(NULL);;
AVCodecParameters* pCodecParam = NULL;
const AVCodec* pCodec = NULL;
AVFrame* pFrame = NULL;
AVFrame* pFrame2 = NULL;
AVPacket packet;
AVDictionary* optionsDict = NULL;
struct SwsContext* sws_ctx = NULL;
SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;
SDL_Texture* texture=NULL;
SDL_Rect rect;
SDL_Event event;
if(argc < 2) {
fprintf(stderr, "Usage: test <file>\n");
exit(1);
}
// SDL组件创建
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
exit(1);
}
// 1. 打开视频文件,获取格式上下文
if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
return -1; // Couldn't open file
// 2. 对文件探测流信息
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
return -1; // Couldn't find stream information
// 打印信息
av_dump_format(pFormatCtx, 0, argv[1], 0);
// 3.找到对应的视频流
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1){
return -1; // Didn't find a video stream
}
// 4. 将视频流编码参数写入上下文
pCodecParam = pFormatCtx->streams[videoStream]->codecpar;
avcodec_parameters_to_context(pCodecCtx, pCodecParam);
// 5. 查找流的编码器
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// 6. 打开流的编解码器
if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0)
return -1; // Could not open codec
// 7.申请缩放颜色空间格式的上下文
sws_ctx =
sws_getContext
(
pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtx->width,
pCodecCtx->height,
AV_PIX_FMT_YUV420P,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
// 8.帧分配
pFrame=av_frame_alloc();
pFrame2=av_frame_alloc();
// 9.计算并分配frame帧所占内存空间
int numBytes=av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
uint8_t* buffer=(uint8_t*)av_malloc(numBytes*sizeof(uint8_t));
av_image_fill_arrays(pFrame2->data, pFrame2->linesize, buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1);
i=0;
Uint32 start_time = SDL_GetTicks();
// SDL2的最新创建和渲染窗口方式
window = SDL_CreateWindow("SDL2 window", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, pCodecCtx->width, pCodecCtx->height, SDL_WINDOW_SHOWN);
if (!window) {
printf("SDL_CreateWindow Error: %s\n", SDL_GetError());
SDL_Quit();
return 1;
}
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer) {
printf("SDL_CreateRenderer Error: %s\n", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);
AVRational time_base = pFormatCtx->streams[videoStream]->time_base;
int64_t av_start_time = av_gettime(); // 播放开始时间
int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE; // pts单位(ms*1000)
int64_t frame_start_time = av_gettime();
// 循环一:对文件上下文持续读取packet
while(av_read_frame(pFormatCtx, &packet)>=0) {
if(packet.stream_index==videoStream) {
// 将packet写入编解码器
int ret = avcodec_send_packet(pCodecCtx, &packet);
if (ret < 0) {
printf("packet resolve error!");
break;
}
// 循环二:从解码器中不断读取帧
while(!avcodec_receive_frame(pCodecCtx, pFrame)) { // 解码得到数据frame
// 帧格式转化,转为YUV420P
sws_scale
(
sws_ctx,
(uint8_t const * const *)pFrame->data,
pFrame->linesize,
0,
pCodecCtx->height,
pFrame2->data,
pFrame2->linesize
);
// 将AVFrame的数据写入到texture中,然后渲染后windows上
rect.x = 0;
rect.y = 0;
rect.w = pCodecCtx->width;
rect.h = pCodecCtx->height;
// 更新纹理
SDL_UpdateYUVTexture(texture, &rect,
pFrame2->data[0], pFrame2->linesize[0], // Y
pFrame2->data[1], pFrame2->linesize[1], // U
pFrame2->data[2], pFrame2->linesize[2]); // V
// 渲染页面
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
int64_t pts = pFrame->pts; // pts
int64_t actual_playback_time = av_start_time + pts * frame_delay; // 实际播放时间
int64_t current_time = av_gettime();
if (actual_playback_time > current_time) {
SDL_Delay((Uint32)(actual_playback_time-current_time)/1000); // 延迟当前时间和实际播放时间
}
i++;
printf("第%i帧 | 属于%s | pts为%d | 时长为%.2fms | 实际播放点为%.2fs | 预期播放点为%.2fs\n ",
i,
get_frame_type(pFrame),
(int)pFrame->pts,
(double)(av_gettime() - frame_start_time)/1000,
(double)(av_gettime() - av_start_time)/AV_TIME_BASE,
pFrame->pts * av_q2d(time_base));
frame_start_time = av_gettime();
}
}
// Free the packet that was allocated by av_read_frame
av_packet_unref(&packet);
SDL_PollEvent(&event);
switch(event.type) {
case SDL_QUIT:
SDL_Quit();
exit(0);
break;
default:
break;
}
}
Uint32 endTime = SDL_GetTicks();
/**
打印一些关键参数
*/
printf("格式: %s\n", pFormatCtx->iformat->name);
printf("时长: %lld us\n", pFormatCtx->duration);
printf("编码器: %s (%s)\n", pCodec->name, avcodec_get_name(pCodecCtx->codec_id));
printf("分辨率: %dx%d\n", pCodecCtx->width, pCodecCtx->height);
printf("帧率: %.2f\n", av_q2d(pFormatCtx->streams[videoStream]->avg_frame_rate));
printf("帧数: %lld\n", pFormatCtx->streams[videoStream]->nb_frames);
printf("比特率: %lld\n", pFormatCtx->bit_rate);
printf("pts单位(ms*1000): %.2f\n", av_q2d(pFormatCtx->streams[videoStream]->time_base) * AV_TIME_BASE);
printf("视频持续时长为 %d,视频帧总数为 %d\n", (endTime-start_time)/1000, i);
// Free the YUV frame
av_free(pFrame);
av_free(pFrame2);
// Close the codec param
avcodec_parameters_free(&pCodecParam);
// Close the codec
avcodec_free_context(&pCodecCtx);
// Close the video file
avformat_close_input(&pFormatCtx);
return 0;
}
文章最后,感谢音视频领域的大神@雷霄骅,雷神永垂不朽!!!