FFmpeg入门:最简单的视频播放器

FFmpeg入门:最简单的视频播放器

FFmpeg入门第一篇,制作一个简单的MP4视频播放器。

整体流程

话不多说,直接上流程图

视频播放速率控制

这里可以直接看图中的帧率同步模块,可以分为如下几步

  1. 获取到当前帧的预期播放时间:根据时间基time_base和帧pts计算;time_base*pts;
  2. 解码渲染过程中,获取当前时间av_gettime,计算和预期播放时间的时间差;
  3. 使用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;
}

文章最后,感谢音视频领域的大神@雷霄骅,雷神永垂不朽!!!

相关推荐
Black蜡笔小新28 分钟前
嵌入式轻量化SDK设计,EasyRTC音视频通话SDK压缩至500K-800K
大数据·音视频·webrtc·sdk·p2p·webp2p
Black蜡笔小新30 分钟前
实时音视频通信EasyRTC嵌入式WebRTC音视频通话SDK体积缩小90%
网络协议·音视频·webrtc·实时音视频·p2p
BT-BOX15 小时前
基于51单片机超声波测量报警LCD1602显示( proteus仿真+程序+设计报告+讲解视频)
51单片机·proteus·音视频
gma99915 小时前
【音视频】 H264 H265
音视频
快乐非自愿1 天前
Java中使用FFmpeg拉取RTSP流
java·开发语言·ffmpeg
Antonio9151 天前
【音视频】RGG、YUV基础
音视频
道系女孩~1 天前
php中使用laravel9项目 使用FFMpeg视频剪辑功能
开发语言·ffmpeg·php
AITIME论道1 天前
多镜头视频生成、机器人抓取、扩散模型个性化 | Big Model weekly第58期
机器人·音视频
yqcoder1 天前
fluent-ffmpeg 依赖详解
ffmpeg
慢一点会很快1 天前
【音视频】VLC播放器
音视频·媒体