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

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

前两章,我们已经了解了分别如何构建一个简单和音频播放器和视频播放器。
FFmpeg入门:最简单的音频播放器
FFmpeg入门:最简单的视频播放器

本章我们将结合上述两章的知识,看看如何融合成一个完整的音视频播放器,跟上我的节奏,本章将是咱们后续完成一个完整的音视频播放器的起点。

整体流程图

话不多说,先上图
在这里插入图片描述
这个图似乎有点复杂了,这里我会分别将每个模块拿出来讲述,方便大家一步一步分析整个流程。

第一步:初始化

在这里插入图片描述
我们首先关注整个流程图的最上面一部分,这部分其实和之前的流程一样,主要就是将做一些前置的初始化工作:

1:打开文件,获取文件上下文
2:找到对应的音频/视频流,获取到Codec上下文
3:打开解码器
4:分配输出空间缓存,用于后续存储解码的输出数据
5:音频/视频帧的格式转化上下文
6:初始化SDL组件,主要是视频的播放窗口和音频播放器

代码(省略了部分校验和参数初始化,方便阅读,原码见文章末尾):

	/** 初始化函数 */init_video_state(&video_state);audio_param = video_state->audioParam;video_param = video_state->videoParam;avformat_network_init();// 1. 打开视频文件,获取格式上下文if(avformat_open_input(&video_state->formatCtx, argv[1], NULL, NULL)!=0){printf("Couldn't open input stream.\n");return -1;}// 2. 对文件探测流信息if(avformat_find_stream_info(video_state->formatCtx, NULL) < 0){printf("Couldn't find stream information.\n");return -1;}// 3. 找到对应的 音频流/视频流 索引video_state->audioStream=-1;video_state->videoStream=-1;for(int i=0; i < video_state->formatCtx->nb_streams; i++) {if(video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){video_state->audioStream=i;}if (video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {video_state->videoStream=i;}}// 4. 将 音频流/视频流 编码参数写入上下文AVCodecParameters* aCodecParam = video_state->formatCtx->streams[video_state->audioStream]->codecpar;avcodec_parameters_to_context(video_state->aCodecCtx, aCodecParam);AVCodecParameters* vCodecParam = video_state->formatCtx->streams[video_state->videoStream]->codecpar;avcodec_parameters_to_context(video_state->vCodecCtx, vCodecParam);// 5. 查找流的编码器video_state->aCodec = avcodec_find_decoder(video_state->aCodecCtx->codec_id);video_state->vCodec = avcodec_find_decoder(video_state->vCodecCtx->codec_id);// 6. 打开流的编解码器if(avcodec_open2(video_state->aCodecCtx, video_state->aCodec, NULL)<0){printf("Could not open audio codec.\n");return -1;}if(avcodec_open2(video_state->vCodecCtx, video_state->vCodec, NULL)<0){printf("Could not open video codec.\n");return -1;}/** 音频输出信息构建 */audio_output_set(video_state);/** 视频输出信息构建 */video_output_set(video_state);//	SDL 初始化
#if USE_SDLif(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf( "Could not initialize SDL - %s\n", SDL_GetError());return -1;}/** 初始化音频SDL设备 */SDL_AudioSpec wanted_spec;wanted_spec.freq = audio_param->out_sample_rate;					// 采样率wanted_spec.format = AUDIO_S16SYS;									// 采样格式 16bitwanted_spec.channels = audio_param->out_channels;					// 通道数wanted_spec.silence = 0;wanted_spec.samples = audio_param->out_nb_samples;					// 单帧处理的采样点wanted_spec.callback = fill_audio;									// 回调函数wanted_spec.userdata = video_state->aCodecCtx;						// 回调函数的参数/** 初始化视频SDL设备 */SDL_Window*       window = NULL;SDL_Renderer*     renderer = NULL;SDL_Texture*      texture= NULL;/** 窗口 */window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,video_state->vCodecCtx->width,video_state->vCodecCtx->height,SDL_WINDOW_SHOWN);/** 渲染 */renderer = SDL_CreateRenderer(window,-1,SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);/** 纹理 */texture = SDL_CreateTexture(renderer,SDL_PIXELFORMAT_YV12,SDL_TEXTUREACCESS_STREAMING,video_state->vCodecCtx->width,video_state->vCodecCtx->height);// 打开音频播放器if (SDL_OpenAudio(&wanted_spec, NULL)<0) {printf("can't open audio.\n");return -1;}#endif// 音频上下文格式转换swr_alloc_set_opts2(&video_state->swrCtx,&audio_param->out_channel_layout,			// 输出layoutaudio_param->out_sample_fmt,				// 输出格式audio_param->out_sample_rate,				// 输出采样率&video_state->aCodecCtx->ch_layout,			// 输入layoutvideo_state->aCodecCtx->sample_fmt,			// 输入格式video_state->aCodecCtx->sample_rate,		// 输入采样率0, NULL);swr_init(video_state->swrCtx);// 视频上下文格式转换video_state->swsCtx = sws_getContext(video_state->vCodecCtx->width,			// src 宽video_state->vCodecCtx->height,		// src 高video_state->vCodecCtx->pix_fmt,		// src 格式video_param->width,					// dst 宽video_param->height,					// dst 高video_param->pix_fmt,					// dst 格式SWS_BILINEAR,NULL,NULL,NULL);// 开始播放SDL_PauseAudio(0);

第二步:packet队列写入

做完准备工作之后,我们就将源文件中的输入packet都取出来,放入到对应的音频packet队列和视频packet队列中,方便后续使用。然后然后分别启动音频解码进程和视频解码进程同时进行解码。

  1. 读出packet,判断packet类型
  2. 根据类型放入音频和视频packet队列
  3. 创建解码进程

在这里插入图片描述

// 循环1: 从文件中读取packetwhile(av_read_frame(video_state->formatCtx, packet)>=0){/** 写入音频pkt队列 */if(packet->stream_index==video_state->audioStream){packet_queue_push(video_state->aQueue, packet);}/** 写入视频pkt队列 */if (packet->stream_index==video_state->videoStream) {packet_queue_push(video_state->vQueue, packet);}av_packet_unref(packet);SDL_PollEvent(&event);switch(event.type) {case SDL_QUIT:SDL_Quit();exit(0);break;default:break;}}printf("audio queue.size=%d\n", video_state->aQueue->size);// 创建一个线程并启动SDL_CreateThread(audio_thread, "audio_thread", video_state);SDL_CreateThread(video_thread, "video_thread", video_state);

第三步:音频解码+播放

接下来会分别讲一下音频和视频解码进程,这两个进程是同时开始的。

首先音频解码进程的步骤可以参考之前的音频播放器文章。我简单说一下步骤

  1. 从音频packet队列中取出packet
  2. 对packet进行解码得到frame
  3. 按音频输出格式进行swr_convert转换得到输出值,并写入buffer。
  4. SDL音频播放器通过回调函数从buffer不断读取数据播放。

在这里插入图片描述
代码如下:

/**音频线程*/
int audio_thread(void *arg) {/**1. 从packet_queue队列中取出packet2. 将packet进行解码3. 写入到sdl的缓冲区中*/ 	VideoState* video_state = (VideoState*) arg;AudioParam* audio_param = video_state->audioParam;PacketQueue* queue = video_state->aQueue;audio_param->index = 0;AVRational time_base = video_state->formatCtx->streams[video_state->audioStream]->time_base;int64_t av_start_time = av_gettime();								// 播放开始时间戳AVPacket 	packet;int 		ret;AVFrame* 	pFrame = av_frame_alloc();for(;;) {if (queue->size > 0) {packet_queue_pop(queue, &packet);// 将packet写入编解码器ret = avcodec_send_packet(video_state->aCodecCtx, &packet);// 获取解码后的帧while (!avcodec_receive_frame(video_state->aCodecCtx, pFrame)) {// 格式转化swr_convert(video_state->swrCtx, &audio_param->out_buffer, audio_param->out_buffer_size,(const uint8_t **)pFrame->data, pFrame->nb_samples);audio_param->index++;printf("第%d帧 | pts:%lld | 帧大小(采样点):%d | 实际播放点%.2fs | 预期播放点%.2fs\n",audio_param->index,packet.pts,packet.size,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));#if USE_SDL// 设置读取的音频数据audio_info.audio_len = audio_param->out_buffer_size;audio_info.audio_pos = (Uint8 *) audio_param->out_buffer;// 等待SDL播放完成while(audio_info.audio_len > 0)SDL_Delay(0.5);
#endif}av_packet_unref(&packet);}else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 0;
}

第四步:视频解码(子线程)+播放(主线程)

说视频的解码和播放之前,先提一点:SDL的主窗口操作是需要在主线程中进行的。因此我们不能再解码子线程中直接渲染SDL窗口,否则会造成内存泄漏。知道这个知识之后,更能理解接下来的流程分析。

我们视频解码播放拆成两个部分:解码+播放

第一部分:解码子线程,在子线程中完成解码,通过标识符的方式通知到主线程帧已更新,并渲染出来。
第二部分:主线程播放,循环监听子线程的通知标识,并更新窗口帧进行显示。

在这里插入图片描述
代码如下:

视频解码子线程

/**视频线程*/
int video_thread(void *arg) {/**1. 从视频pkt队列中读出packet2. 送入解码器解码并取出3. 使用SDL进行渲染4. 根据pts计算延迟SDL_DELAY*/VideoState* 	video_state = (VideoState*) arg;PacketQueue* 	video_queue = video_state->vQueue;AVCodecContext* pCodecCtx = video_state->vCodecCtx;AVFrame* 		out_frame = video_state->videoParam->out_frame;AVPacket packet;AVFrame* pFrame = av_frame_alloc();AVRational time_base = video_state->formatCtx->streams[video_state->videoStream]->time_base;int64_t av_start_time = av_gettime();							// 开始播放时间(ms*1000)int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE;		// pts单位(ms*1000)int64_t frame_start_time = av_gettime();for (;;) {if (video_queue->size > 0) {packet_queue_pop(video_queue, &packet);// 将packet写入编解码器int ret = avcodec_send_packet(pCodecCtx, &packet);// 从解码器中取出原始帧while (!avcodec_receive_frame(pCodecCtx, pFrame)) {// 帧格式转化,转为YUV420Psws_scale(video_state->swsCtx,						// sws_context转换(uint8_t const * const *)pFrame->data,	// 输入 datapFrame->linesize,							// 输入 每行数据的大小(对齐)0,										// 输入 Y轴位置pCodecCtx->height,						// 输入 heightout_frame->data,							// 输出 dataout_frame->linesize);						// 输出 linesize// 帧更新video_state->videoParam->frame_update = 1;// 计算延迟int64_t pts = pFrame->pts;											// ptsint64_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);	// 延迟当前时间和实际播放时间}video_state->videoParam->index++;printf("第%i帧 | 属于%s | pts为%d | 时长为%.2fms | 实际播放点为%.2fs | 预期播放点为%.2fs\n ",video_state->videoParam->index,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();}av_packet_unref(&packet);} else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 1;
}

渲染主线程

while (!video_state->isEnd) {// 处理事件(必须由主线程执行)while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {video_state->isEnd = 1;}}if (video_state->videoParam->frame_update) {// 将AVFrame的数据写入到texture中,然后渲染后windows上rect.x = 0;rect.y = 0;rect.w = video_state->vCodecCtx->width;rect.h = video_state->vCodecCtx->height;out_frame = video_state->videoParam->out_frame;// 更新纹理SDL_UpdateYUVTexture(texture, &rect,out_frame->data[0], out_frame->linesize[0],	// 	Yout_frame->data[1], out_frame->linesize[1],	// 	Uout_frame->data[2], out_frame->linesize[2]);	//  V// 渲染页面SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, NULL, NULL);SDL_RenderPresent(renderer);// 重置标志video_state->videoParam->frame_update = 0;}}

完整代码

sample_player.h

//
//  sample_player.h
//  learning
//
//  Created by chenhuaiyi on 2025/2/26.
//#ifndef sample_player_h
#define sample_player_h#include <stdio.h>
// ffmpeg
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
#include "libavutil/time.h"
#include "libavutil/fifo.h"
#include "libavutil/channel_layout.h"
// SDL
#include "SDL.h"
#include "SDL_thread.h"/**宏定义*/
#define USE_SDL 1typedef struct MyAVPacketList {AVPacket 		*pkt;int 			serial;
} MyAVPacketList;/**packet队列*/
typedef struct PacketQueue {AVFifo* 		pkt_list;		// fifo队列int 			size;			// 队列大小SDL_mutex*		mutex;			// 互斥信号量SDL_cond*		cond;			// 条件变量,阻塞线程
} PacketQueue;/**数据类型定义*/
typedef struct AudioInfo{Uint32  		audio_len;		// 缓冲区长度Uint8*			audio_pos;		// 缓冲区起始地址指针
} AudioInfo;/**语音输出参数*/
typedef struct AudioParam {AVChannelLayout 	out_channel_layout;			// layoutint 				out_nb_samples;				// 每一帧的样本数enum AVSampleFormat out_sample_fmt;				// 格式int 				out_sample_rate;			// 采样率int 				out_channels;				// 输出通道数int					index;						// 音频帧总数int 				out_buffer_size;			// 音频输出缓冲区大小uint8_t* 			out_buffer;					// 音频输出缓冲区
} AudioParam;/**视频输出参数*/
typedef struct VideoParam {int 				width;						// 宽int 				height;						// 高enum AVPixelFormat	pix_fmt;					// 格式 YUV420Pint 				num_bytes;					// 单帧字节数int					index;						// 视频帧总数AVFrame*			out_frame;					// 输出帧int					frame_update;				// 帧更新标识
} VideoParam;/**全局参数*/
typedef struct VideoState {AVFormatContext* 	formatCtx;			// format上下文int				 	audioStream;		// 音频流索引AVCodecContext*  	aCodecCtx;			// 音频codec上下文const AVCodec*	 	aCodec;				// 音频解码器AudioParam*		 	audioParam;			// 音频参数SwrContext*			swrCtx;				// 音频上线文转换int					videoStream;		// 视频流索引AVCodecContext*  	vCodecCtx;			// 视频codec上新闻const AVCodec*	 	vCodec;				// 视频解码器VideoParam*			videoParam;			// 视频参数struct SwsContext* 	swsCtx;				// 视频上下文转换PacketQueue*		aQueue;				// 音频pkt队列PacketQueue*		vQueue;				// 视频pkt队列int					isEnd;				// 结束标志
} VideoState;/**全局变量*/
extern AudioInfo audio_info;#endif /* sample_player_h */

utils.h

//
//  utils.h
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#ifndef utils_h
#define utils_h#include "sample_player.h"int init_video_state(VideoState** video_state);int destory_video_state(VideoState** video_state);int packet_queue_push(PacketQueue* q, AVPacket* pkt);int packet_queue_init(PacketQueue** q, size_t max_size);int packet_queue_pop(PacketQueue* q, AVPacket* pkt);void packet_queue_destroy(PacketQueue** q);char* get_frame_type(AVFrame* frame);#endif /* utils_h */

manager.h

//
//  manager.h
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#ifndef manager_h
#define manager_h#include "sample_player.h"/**音频输出信息设置*/
int audio_output_set(VideoState* video_state);/**视频输出信息设置*/
int video_output_set(VideoState* video_state);/**音频SDL初始化*/
int audio_sdl_set(VideoState* video_state, SDL_AudioSpec* wanted_spec, void (*fn)(void*, Uint8*, int));/**视频SDL初始化*/
int video_sdl_set(VideoState* video_state, SDL_Window** window, SDL_Renderer** renderer, SDL_Texture** texture);#endif /* manager_h */

utils.c

//
//  utils.c
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#include "utils.h"/**初始化VideoState*/
int init_video_state(VideoState** video_state) {*video_state = av_malloc(sizeof(VideoState));(*video_state)->formatCtx = avformat_alloc_context();(*video_state)->audioStream = 0;(*video_state)->aCodecCtx = avcodec_alloc_context3(NULL);(*video_state)->audioParam = av_malloc(sizeof(AudioParam));(*video_state)->videoStream = 0;(*video_state)->vCodecCtx = avcodec_alloc_context3(NULL);(*video_state)->videoParam = av_malloc(sizeof(VideoParam));(*video_state)->videoParam->frame_update = 0;/** pkt队列初始化 */(*video_state)->aQueue = av_malloc(sizeof(PacketQueue));packet_queue_init(&(*video_state)->aQueue, 1);(*video_state)->vQueue = av_malloc(sizeof(PacketQueue));packet_queue_init(&(*video_state)->vQueue, 1);(*video_state)->isEnd = 0;return 1;
}/**销毁VideoState*/
int destory_video_state(VideoState** video_state){swr_free(&(*video_state)->swrCtx);avcodec_free_context(&(*video_state)->aCodecCtx);av_free((*video_state)->audioParam->out_buffer);av_free((*video_state)->audioParam);sws_freeContext((*video_state)->swsCtx);avcodec_free_context(&(*video_state)->vCodecCtx);av_frame_free(&(*video_state)->videoParam->out_frame);av_free((*video_state)->videoParam);/** 队列释放 */packet_queue_destroy(&(*video_state)->aQueue);packet_queue_destroy(&(*video_state)->vQueue);if ((*video_state)->formatCtx != NULL) {avformat_close_input(&(*video_state)->formatCtx);(*video_state)->formatCtx = NULL;}av_free(*video_state);return 1;
}/**初始化队列*/
int packet_queue_init(PacketQueue** q, size_t max_size) {// 创建一个 AVFifo 队列,每个元素的大小为 sizeof(AVPacket)*q = av_malloc(sizeof(PacketQueue));(*q)->pkt_list = av_fifo_alloc2(max_size, sizeof(MyAVPacketList), AV_FIFO_FLAG_AUTO_GROW);(*q)->size = 0;(*q)->mutex = SDL_CreateMutex();(*q)->cond = SDL_CreateCond();if (!(*q)->pkt_list) {return -1;}return 0;
}/**写入队列*/
int packet_queue_push(PacketQueue* q, AVPacket* pkt) {MyAVPacketList pNode;if (!q || !pkt) {return -1;}AVPacket* pkt1 = av_packet_alloc();if (!pkt1) {av_packet_unref(pkt);return -1;}SDL_LockMutex(q->mutex);av_packet_ref(pkt1, pkt);pNode.pkt = pkt1;// 将 pkt 压入队列if (av_fifo_write(q->pkt_list, &pNode, 1) < 0) {SDL_UnlockMutex(q->mutex);return -1;}q->size++;SDL_CondSignal(q->cond);SDL_UnlockMutex(q->mutex);return 0;
}/**弹出队列*/
int packet_queue_pop(PacketQueue* q, AVPacket* pkt) {if (!q || !pkt) {return -1;}SDL_LockMutex(q->mutex);MyAVPacketList pNode;// 从队列中弹出一个元素, 没找到则阻塞线程,等待生产者释放if (av_fifo_read(q->pkt_list, &pNode, 1) < 0) {SDL_CondWait(q->cond, q->mutex);}q->size--;av_packet_move_ref(pkt, pNode.pkt);av_packet_free(&pNode.pkt);SDL_UnlockMutex(q->mutex);return 0;
}/**销毁队列*/
void packet_queue_destroy(PacketQueue** q) {if ((*q) && (*q)->pkt_list) {// 释放队列中的所有 AVPacketMyAVPacketList pNode;SDL_LockMutex((*q)->mutex);while (av_fifo_read((*q)->pkt_list, &pNode, 1) >= 0) {av_packet_free(&pNode.pkt);  // 释放 AVPacket 的资源}SDL_UnlockMutex((*q)->mutex);// 释放 AVFifo 队列(*q)->size = 0;av_fifo_freep2(&(*q)->pkt_list);SDL_DestroyMutex((*q)->mutex);SDL_DestroyCond((*q)->cond);av_free(*q);}
}/**获取帧类型*/
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;}
}

manager.c

//
//  manager.c
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/27.
//#include "manager.h"/**音频输出信息构建*/
int audio_output_set(VideoState* video_state) {AudioParam* audio_param = video_state->audioParam;// 输出用到的信息av_channel_layout_default(&audio_param->out_channel_layout, 2);audio_param->out_nb_samples = video_state->aCodecCtx->frame_size;			// 编解码器每个帧需要处理或者输出的采样点的大小 AAC:1024  MP3:1152audio_param->out_sample_fmt = AV_SAMPLE_FMT_S16;							// 采样格式audio_param->out_sample_rate = 44100;										// 采样率audio_param->out_channels = audio_param->out_channel_layout.nb_channels;	// 通道数// 获取需要使用的缓冲区大小 -> 通道数,单通道样本数,位深 1024(单帧处理的采样点)*2(双通道)*2(16bit对应2字节)audio_param->out_buffer_size = av_samples_get_buffer_size(NULL, audio_param->out_channels,audio_param->out_nb_samples,audio_param->out_sample_fmt, 1);// 分配缓冲区空间audio_param->out_buffer = NULL;av_samples_alloc(&audio_param->out_buffer, NULL, audio_param->out_channels,audio_param->out_nb_samples, audio_param->out_sample_fmt, 1);return 1;
}/**视频输出信息构建*/
int video_output_set(VideoState* video_state) {VideoParam* video_param = video_state->videoParam;// 基础信息video_param->width = video_state->vCodecCtx->width;video_param->height = video_state->vCodecCtx->height;video_param->pix_fmt = AV_PIX_FMT_YUV420P;// 计算单帧大小, 分配单帧内存video_param->num_bytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, video_param->width, video_param->height, 1);video_param->out_frame = av_frame_alloc();av_image_alloc(video_param->out_frame->data, video_param->out_frame->linesize,video_param->width, video_param->height, AV_PIX_FMT_YUV420P, 1);return 1;
}/**音频SDL初始化*/
int audio_sdl_set(VideoState* video_state, SDL_AudioSpec* wanted_spec, void (*fn)(void*, Uint8*, int)) {AudioParam* audio_param = video_state->audioParam;wanted_spec->freq = audio_param->out_sample_rate;					// 采样率wanted_spec->format = AUDIO_S16SYS;									// 采样格式 16bitwanted_spec->channels = audio_param->out_channels;					// 通道数wanted_spec->silence = 0;wanted_spec->samples = audio_param->out_nb_samples;					// 单帧处理的采样点wanted_spec->callback = fn;											// 回调函数wanted_spec->userdata = video_state->aCodecCtx;						// 回调函数的参数return 1;
}/**视频SDL初始化*/
int video_sdl_set(VideoState* video_state, SDL_Window** window, SDL_Renderer** renderer, SDL_Texture** texture){AVCodecContext* pCodecCtx = video_state->vCodecCtx;/** 窗口 */*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);return 1;
}

main.c

//
//  main.c
//  sample_player
//
//  Created by chenhuaiyi on 2025/2/26.
//#include "utils.h"
#include "manager.h"AudioInfo audio_info;/* udata: 传入的参数* stream: SDL音频缓冲区* len: SDL音频缓冲区大小* 回调函数*/
void fill_audio(void *udata, Uint8 *stream, int len){SDL_memset(stream, 0, len);			// 必须重置,不然全是电音!!!if(audio_info.audio_len==0){					// 有音频数据时才调用return;}len = (len>audio_info.audio_len ? audio_info.audio_len : len);	// 最多填充缓冲区大小的数据SDL_MixAudio(stream, audio_info.audio_pos, len, SDL_MIX_MAXVOLUME);audio_info.audio_pos += len;audio_info.audio_len -= len;
}/**音频线程*/
int audio_thread(void *arg) {/**1. 从packet_queue队列中取出packet2. 将packet进行解码3. 写入到sdl的缓冲区中*/ 	VideoState* video_state = (VideoState*) arg;AudioParam* audio_param = video_state->audioParam;PacketQueue* queue = video_state->aQueue;audio_param->index = 0;AVRational time_base = video_state->formatCtx->streams[video_state->audioStream]->time_base;int64_t av_start_time = av_gettime();								// 播放开始时间戳AVPacket 	packet;int 		ret;AVFrame* 	pFrame = av_frame_alloc();for(;;) {if (queue->size > 0) {packet_queue_pop(queue, &packet);// 将packet写入编解码器ret = avcodec_send_packet(video_state->aCodecCtx, &packet);if ( ret < 0 ) {printf("send packet error\n");return -1;}// 获取解码后的帧while (!avcodec_receive_frame(video_state->aCodecCtx, pFrame)) {// 格式转化swr_convert(video_state->swrCtx, &audio_param->out_buffer, audio_param->out_buffer_size,(const uint8_t **)pFrame->data, pFrame->nb_samples);audio_param->index++;printf("第%d帧 | pts:%lld | 帧大小(采样点):%d | 实际播放点%.2fs | 预期播放点%.2fs\n",audio_param->index,packet.pts,packet.size,(double)(av_gettime() - av_start_time)/AV_TIME_BASE,pFrame->pts * av_q2d(time_base));#if USE_SDL// 设置读取的音频数据audio_info.audio_len = audio_param->out_buffer_size;audio_info.audio_pos = (Uint8 *) audio_param->out_buffer;// 等待SDL播放完成while(audio_info.audio_len > 0)SDL_Delay(0.5);
#endif}av_packet_unref(&packet);}else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 0;
}/**视频线程*/
int video_thread(void *arg) {/**1. 从视频pkt队列中读出packet2. 送入解码器解码并取出3. 使用SDL进行渲染4. 根据pts计算延迟SDL_DELAY*/VideoState* 	video_state = (VideoState*) arg;PacketQueue* 	video_queue = video_state->vQueue;AVCodecContext* pCodecCtx = video_state->vCodecCtx;AVFrame* 		out_frame = video_state->videoParam->out_frame;AVPacket packet;AVFrame* pFrame = av_frame_alloc();AVRational time_base = video_state->formatCtx->streams[video_state->videoStream]->time_base;int64_t av_start_time = av_gettime();							// 开始播放时间(ms*1000)int64_t frame_delay = av_q2d(time_base) * AV_TIME_BASE;		// pts单位(ms*1000)int64_t frame_start_time = av_gettime();for (;;) {if (video_queue->size > 0) {packet_queue_pop(video_queue, &packet);// 将packet写入编解码器int ret = avcodec_send_packet(pCodecCtx, &packet);if (ret < 0) {printf("packet resolve error!");break;}// 从解码器中取出原始帧while (!avcodec_receive_frame(pCodecCtx, pFrame)) {// 帧格式转化,转为YUV420Psws_scale(video_state->swsCtx,						// sws_context转换(uint8_t const * const *)pFrame->data,	// 输入 datapFrame->linesize,							// 输入 每行数据的大小(对齐)0,										// 输入 Y轴位置pCodecCtx->height,						// 输入 heightout_frame->data,							// 输出 dataout_frame->linesize);						// 输出 linesize// 帧更新video_state->videoParam->frame_update = 1;// 计算延迟int64_t pts = pFrame->pts;											// ptsint64_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);	// 延迟当前时间和实际播放时间}video_state->videoParam->index++;printf("第%i帧 | 属于%s | pts为%d | 时长为%.2fms | 实际播放点为%.2fs | 预期播放点为%.2fs\n ",video_state->videoParam->index,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();}av_packet_unref(&packet);} else {break;}}av_frame_free(&pFrame);// 结束video_state->isEnd = 1;return 1;
}int main(int argc, char* argv[])
{VideoState* 		video_state;AudioParam*			audio_param;VideoParam*			video_param;SDL_Event 			event;SDL_Rect      		rect;if(argc < 2) {fprintf(stderr, "Usage: test <file>\n");exit(1);}/** 初始化函数 */init_video_state(&video_state);audio_param = video_state->audioParam;video_param = video_state->videoParam;avformat_network_init();// 1. 打开视频文件,获取格式上下文if(avformat_open_input(&video_state->formatCtx, argv[1], NULL, NULL)!=0){printf("Couldn't open input stream.\n");return -1;}// 2. 对文件探测流信息if(avformat_find_stream_info(video_state->formatCtx, NULL) < 0){printf("Couldn't find stream information.\n");return -1;}// 打印信息av_dump_format(video_state->formatCtx, 0, argv[1], 0);// 3. 找到对应的 音频流/视频流 索引video_state->audioStream=-1;video_state->videoStream=-1;for(int i=0; i < video_state->formatCtx->nb_streams; i++) {if(video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){video_state->audioStream=i;}if (video_state->formatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_VIDEO) {video_state->videoStream=i;}}if(video_state->audioStream==-1){printf("Didn't find a audio stream.\n");return -1;}if (video_state->videoStream==-1) {printf("Didn't find a video stream.\n");return -1;}// 4. 将 音频流/视频流 编码参数写入上下文AVCodecParameters* aCodecParam = video_state->formatCtx->streams[video_state->audioStream]->codecpar;avcodec_parameters_to_context(video_state->aCodecCtx, aCodecParam);
//	avcodec_parameters_free(&aCodecParam); 这个是不需要手动释放的AVCodecParameters* vCodecParam = video_state->formatCtx->streams[video_state->videoStream]->codecpar;avcodec_parameters_to_context(video_state->vCodecCtx, vCodecParam);
//	avcodec_parameters_free(&vCodecParam);// 5. 查找流的编码器video_state->aCodec = avcodec_find_decoder(video_state->aCodecCtx->codec_id);if(video_state->aCodec==NULL){printf("Audio codec not found.\n");return -1;}video_state->vCodec = avcodec_find_decoder(video_state->vCodecCtx->codec_id);if(video_state->vCodec==NULL){printf("Video codec not found.\n");return -1;}// 6. 打开流的编解码器if(avcodec_open2(video_state->aCodecCtx, video_state->aCodec, NULL)<0){printf("Could not open audio codec.\n");return -1;}if(avcodec_open2(video_state->vCodecCtx, video_state->vCodec, NULL)<0){printf("Could not open video codec.\n");return -1;}/** 音频输出信息构建 */audio_output_set(video_state);/** 视频输出信息构建 */video_output_set(video_state);//	SDL 初始化
#if USE_SDLif(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {printf( "Could not initialize SDL - %s\n", SDL_GetError());return -1;}// 在 main 函数开始处添加SDL_SetHint(SDL_HINT_VIDEO_MAC_FULLSCREEN_SPACES, "0");SDL_SetHint(SDL_HINT_MAC_BACKGROUND_APP, "1");/** 初始化音频SDL设备 */SDL_AudioSpec wanted_spec;//	audio_sdl_set(video_state, &wanted_spec, fill_audio);wanted_spec.freq = audio_param->out_sample_rate;					// 采样率wanted_spec.format = AUDIO_S16SYS;									// 采样格式 16bitwanted_spec.channels = audio_param->out_channels;					// 通道数wanted_spec.silence = 0;wanted_spec.samples = audio_param->out_nb_samples;					// 单帧处理的采样点wanted_spec.callback = fill_audio;									// 回调函数wanted_spec.userdata = video_state->aCodecCtx;						// 回调函数的参数/** 初始化视频SDL设备 */SDL_Window*       window = NULL;SDL_Renderer*     renderer = NULL;SDL_Texture*      texture= NULL;//	video_sdl_set(video_state, &window, &renderer, &texture);/** 窗口 */window = SDL_CreateWindow("SDL2 window",SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,video_state->vCodecCtx->width,video_state->vCodecCtx->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,video_state->vCodecCtx->width,video_state->vCodecCtx->height);// 打开音频播放器if (SDL_OpenAudio(&wanted_spec, NULL)<0) {printf("can't open audio.\n");return -1;}#endif// 音频上下文格式转换swr_alloc_set_opts2(&video_state->swrCtx,&audio_param->out_channel_layout,			// 输出layoutaudio_param->out_sample_fmt,				// 输出格式audio_param->out_sample_rate,				// 输出采样率&video_state->aCodecCtx->ch_layout,			// 输入layoutvideo_state->aCodecCtx->sample_fmt,			// 输入格式video_state->aCodecCtx->sample_rate,		// 输入采样率0, NULL);swr_init(video_state->swrCtx);// 视频上下文格式转换video_state->swsCtx = sws_getContext(video_state->vCodecCtx->width,			// src 宽video_state->vCodecCtx->height,		// src 高video_state->vCodecCtx->pix_fmt,		// src 格式video_param->width,					// dst 宽video_param->height,					// dst 高video_param->pix_fmt,					// dst 格式SWS_BILINEAR,NULL,NULL,NULL);// 开始播放SDL_PauseAudio(0);int64_t av_start_time = av_gettime();	// 播放开始时间戳AVPacket* packet = av_packet_alloc();	// packet初始化// 循环1: 从文件中读取packetwhile(av_read_frame(video_state->formatCtx, packet)>=0){/** 写入音频pkt队列 */if(packet->stream_index==video_state->audioStream){packet_queue_push(video_state->aQueue, packet);}/** 写入视频pkt队列 */if (packet->stream_index==video_state->videoStream) {packet_queue_push(video_state->vQueue, packet);}av_packet_unref(packet);SDL_PollEvent(&event);switch(event.type) {case SDL_QUIT:SDL_Quit();exit(0);break;default:break;}}printf("audio queue.size=%d\n", video_state->aQueue->size);// 创建一个线程并启动SDL_CreateThread(audio_thread, "audio_thread", video_state);SDL_CreateThread(video_thread, "video_thread", video_state);//	video_thread(video_state);AVFrame* out_frame = NULL;while (!video_state->isEnd) {// 处理事件(必须由主线程执行)while (SDL_PollEvent(&event)) {if (event.type == SDL_QUIT) {video_state->isEnd = 1;}}if (video_state->videoParam->frame_update) {// 将AVFrame的数据写入到texture中,然后渲染后windows上rect.x = 0;rect.y = 0;rect.w = video_state->vCodecCtx->width;rect.h = video_state->vCodecCtx->height;out_frame = video_state->videoParam->out_frame;// 更新纹理SDL_UpdateYUVTexture(texture, &rect,out_frame->data[0], out_frame->linesize[0],	// 	Yout_frame->data[1], out_frame->linesize[1],	// 	Uout_frame->data[2], out_frame->linesize[2]);	//  V// 渲染页面SDL_RenderClear(renderer);SDL_RenderCopy(renderer, texture, NULL, NULL);SDL_RenderPresent(renderer);// 重置标志video_state->videoParam->frame_update = 0;}}// 打印参数printf("格式: %s\n", video_state->formatCtx->iformat->name);printf("时长: %lld us\n", video_state->formatCtx->duration);printf("音频持续时长为 %.2f,音频帧总数为 %d\n", (double)(av_gettime()-av_start_time)/AV_TIME_BASE, audio_param->index);printf("码率: %lld\n", video_state->formatCtx->bit_rate);printf("编码器: %s (%s)\n", video_state->aCodecCtx->codec->long_name, avcodec_get_name(video_state->aCodecCtx->codec_id));printf("通道数: %d\n", video_state->aCodecCtx->ch_layout.nb_channels);printf("采样率: %d \n", video_state->aCodecCtx->sample_rate);printf("单通道每帧的采样点数目: %d\n", video_state->aCodecCtx->frame_size);printf("pts单位(ms*1000): %.2f\n", av_q2d(video_state->formatCtx->streams[video_state->audioStream]->time_base) * AV_TIME_BASE);// 释放空间av_packet_free(&packet);#if USE_SDLSDL_CloseAudio();SDL_DestroyTexture(texture);SDL_DestroyRenderer(renderer);SDL_DestroyWindow(window);SDL_Quit();
#endifdestory_video_state(&video_state);return 0;
}

结语和展望

终于做完了,恭喜你,完成了一个非常粗糙,而且有很多问题的简单音视频播放器。接下来几期,我们跟着大家一起对这个简单的播放器进行优化。当然我也是个小萌新,所以一步一步来嘛哈哈。先抛出几个问题:

  1. 时钟同步怎么做
  2. 如何边读出packet,边解码frame并播放
  3. 我们如何对输出的解码帧进行转化

ps. 鼓励大家阅读ffplay源码,所有的问题都能迎刃而解,哈哈哈!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/30979.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ThinkPHP框架

在电脑C磁盘中安装composer 命令 在电脑的D盘中创建cd文件夹 切换磁盘 创建tp框架 创建一个aa的网站&#xff0c;更换路径到上一步下载的tp框架路径 在管理中修改路径 下载压缩包public和view 将前面代码中的public和view文件替换 在PHPStom 中打开文件 运行指定路径 修改demo…

HTTPS加密原理详解

目录 HTTPS是什么 加密是什么 HTTPS的工作流程 1.使用对称加密 2.引入非对称加密 3.引入证书机制 客户端验证证书真伪的过程 签名的加密流程 整体工作流程 总结 HTTPS是什么 HTTPS协议也是一个应用程协议&#xff0c;是在HTTP的基础上加入了一个加密层&#xff0c;由…

基于SpringBoot的餐厅点餐管理系统设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

第十五届蓝桥杯省赛电子类单片机学习过程记录(客观题)

客观试题: 01.典型的BUCK电源电路包含哪些关键器件(ABCD) A. 电容 B. 二极管 C. 电感 D. MOSFET 解析: 典型的 BUCK 电源电路是一种降压型的直流-直流转换电路,它包含以下关键器件: A.电容:电容在电路中起到滤波的作用。输入电容用于平滑输入电压的波动,减少电源噪声对…

基于单片机的智慧农业大棚系统(论文+源码)

1系统整体设计 经过上述的方案分析&#xff0c;采用STM32单片机为核心&#xff0c;结合串口通信模块&#xff0c;温湿度传感器&#xff0c;光照传感器&#xff0c;土壤湿度传感器&#xff0c;LED灯等硬件设备来构成整个控制系统。系统可以实现环境的温湿度检测&#xff0c;土壤…

【GPT入门】第1课准备环境

【GPT入门】第1课 准备环境 1.安装conda环境 参考我的安装文档&#xff1a;https://blog.csdn.net/spark_dev/article/details/145071250 2.安装idea,或其它开发软件 3.idea中选择conda的python idea会为每个项目配置一个独立的python环境&#xff0c;方便python版本管理 新建…

【hello git】git rebase、git merge、git stash、git cherry-pick

目录 一、git merge&#xff1a;保留了原有分支的提交结构 二、git rebase&#xff1a;提交分支更加整洁 三、git stash 四、git cherry-pick 共同点&#xff1a;将 一个分支的提交 合并到 到另一个上分支上去 一、git merge&#xff1a;保留了原有分支的提交结构 现有一个模型…

Phi-4-multimodal:图、文、音频统一的多模态大模型架构、训练方法、数据细节

Phi-4-Multimodal 是一种参数高效的多模态模型&#xff0c;通过 LoRA 适配器和模式特定路由器实现文本、视觉和语音/音频的无缝集成。训练过程包括多阶段优化&#xff0c;确保在不同模式和任务上的性能&#xff0c;数据来源多样&#xff0c;覆盖高质量网络和合成数据。它的设计…

简单的二元语言模型bigram实现

内容总结归纳自视频&#xff1a;【珍藏】从头开始用代码构建GPT - 大神Andrej Karpathy 的“神经网络从Zero到Hero 系列”之七_哔哩哔哩_bilibili 项目&#xff1a;https://github.com/karpathy/ng-video-lecture Bigram模型是基于当前Token预测下一个Token的模型。例如&#x…

猫耳大型活动提效——组件低代码化

1. 引言 猫耳前端在开发活动的过程中&#xff0c;经历过传统的 pro code 阶段&#xff0c;即活动页面完全由前端开发编码实现&#xff0c;直到 2020 年接入公司内部的低代码活动平台&#xff0c;满足了大部分日常活动的需求&#xff0c;运营可自主配置活动并上线&#xff0c;释…

数据库基础以及基本建库建表的简单操作

文章目录 一、数据库是啥1.1、数据库的概念1.1、关系型数据库、非关系型数据库1.1、数据库服务器&#xff0c;数据库与表之间的关系 二、为啥要使用数据库2.1&#xff1a;传统数据文件存储2.2&#xff1a;数据库存储数据2.3、结论 三、使用数据库了会咋样四、应该咋用数据库&am…

常用无功功率算法的C语言实现(二)

0 前言 尽管数字延迟法和积分移相法在不间断采样的无功功率计算中得到了广泛应用,但它们仍存在一些固有缺陷。 对于数字延迟法而言,其需要额外存储至少1/4周期的采样点,在高采样频率的场景下,这对存储资源的需求不可忽视。而积分移相法虽然避免了额外的存储开销,但为了抑制…

【Linux】初识线程

目录 一、什么是线程&#xff1a; 重定义线程和进程&#xff1a; 执行流&#xff1a; Linux中线程的实现方案&#xff1a; 二、再谈进程地址空间 三、小结&#xff1a; 1、概念&#xff1a; 2、进程与线程的关系&#xff1a; 3、线程优点&#xff1a; 4、线程…

【单片机】ARM 处理器简介

ARM 公司简介 ARM&#xff08;Advanced RISC Machine&#xff09; 是英国 ARM 公司&#xff08;原 Acorn RISC Machine&#xff09; 开发的一种精简指令集&#xff08;RISC&#xff09; 处理器架构。ARM 处理器因其低功耗、高性能、广泛适用性&#xff0c;成为嵌入式系统、移动…

​​《从事件冒泡到处理:前端事件系统的“隐形逻辑”》

“那天在document见到你的第一眼&#xff0c;我就下定决心要陪你到天荒地老” ---React 我将从事件从出现到被处理的各个过程来介绍事件机制&#xff1a; 这张图片给我们展示了react事件的各个阶段&#xff0c;我们可以看到有DOM&#xff0c;合成事件层&#xff0c;还有…

Django小白级开发入门

1、Django概述 Django是一个开放源代码的Web应用框架&#xff0c;由Python写成。采用了MTV的框架模式&#xff0c;即模型M&#xff0c;视图V和模版T。 Django 框架的核心组件有&#xff1a; 用于创建模型的对象关系映射为最终用户设计较好的管理界面URL 设计设计者友好的模板…

课程《Deep Learning Specialization》

在coursera上&#xff0c;Deep Learning Specialization 课程内容如下图所示&#xff1a;

Java【网络原理】(3)网络编程续

目录 1.前言 2.正文 2.1ServerSocket类 2.2Socket类 2.3Tcp回显服务器 2.3.1TcpEchoServer 2.3.2TcpEchoClient 3.小结 1.前言 哈喽大家好&#xff0c;今天继续进行计算机网络的初阶学习&#xff0c;今天学习的是tcp回显服务器的实现&#xff0c;正文开始 2.正文 在…

安装remixd,在VScode创建hardhat

在终端&#xff0c;以管理员身份&#xff0c;cmd 需要科学上网 npm install -g remix-project/remixd 在vscode插件中&#xff0c;安装solidity插件&#xff0c;是暗灰色那款 1.将nodeJs的版本升级至18以上 2.在vscode打开一个新的文件&#xff0c;在终端输入 npx hardhat 3.…

微服务拆分-远程调用

我们在查询购物车列表的时候&#xff0c;它有一个需求&#xff0c;就是不仅仅要查出购物车当中的这些商品信息&#xff0c;同时还要去查到购物车当中这些商品的最新的价格和状态信息&#xff0c;跟购物车当中的快照进行一个对比&#xff0c;从而去提醒用户。 现在我们已经做了服…