一.播放器的多线程模型
本文从播放器的多线程的模型开始分析播放器的源码
1.主线程
我们的设计思路是主线程不添加过多的业务逻辑,专注于SDL事件的响应,提高用户的响应速度
首先要介绍一个超级重要的结构体VidoeState,这里面存放了视频文件的信息(上下文、解码器、各种参数等)和写代码过程中要使用的数据结构(队列、index、锁等),这个结构体要作为函数参数把各个线程串起来。
主线程首先做了一些初始化,然后创建了一个定时器,每隔40ms触发一次FF_REFRESH_EVENT事件,开启解复用线程,然后开始做SDL循环等待 (SDL_WaitEvent),FF_REFRESH_EVENT事件触发video_refresh_timer函数刷新视频帧,这个函数涉及到视频的同步很重要后面再说。
int main(int argc, char *argv[])
{
SDL_Event event;
//创建全局状态对象
VideoState *is;//important struct
is = av_mallocz(sizeof(VideoState));//将分配的内存块所有字节置0 给VideoState结构体分配空间
if (argc < 2) {
fprintf(stderr, "Usage: test <file>\n");
exit(1);
}
//读写打开或建立一个二进制文件,允许读和写
yuvfd = fopen("testout.yuv", "wb+");
audiofd = fopen("testout.pcm", "wb+");
// Register all formats and codecs
av_register_all();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
exit(1);
}
//init mutex lock
text_mutex = SDL_CreateMutex();//sdl lock?
//Copy the string src to dst, but no more than size
av_strlcpy(is->filename, argv[1], sizeof(is->filename));//复制视频文件路径名
is->pictq_mutex = SDL_CreateMutex();//解码后图像帧队列
is->pictq_cond = SDL_CreateCond();
//start a timer
//40ms push a refresh event
schedule_refresh(is, 40);//40ms回调一次 触发FF_REFRESH_EVENT 渲染视频帧
is->av_sync_type = DEFAULT_AV_SYNC_TYPE;//0 AV_SYNC_AUDIO_MASTER
//creat demux thread 解复用线程
is->parse_tid = SDL_CreateThread(demux_thread, "demux_thread", is);
if (!is->parse_tid) {
av_free(is);
return -1;
}
//sdl event loop wait
for (;;) {
SDL_WaitEvent(&event);
switch (event.type) {
case FF_QUIT_EVENT:
case SDL_QUIT://退出进程事件
is->quit = 1;//把quit标志置为1 线程退出机制
SDL_Quit();
return 0;
break;
case FF_REFRESH_EVENT://视频显示刷新事件
//视频刷新定时器
video_refresh_timer(event.user.data1);//event.user.data1 is
break;
default:
break;
}
}
fclose(yuvfd);
fclose(audiofd);
return 0;
}
2.解复用线程
解复用线程的目的就是把视频文件拆成音频和视频两路流 ,并打开解码器。然后创建了视频解码线程
具体步骤就是首先打开多媒体问价文件,然后把文件容器封装信息及码流参数传递给我们定义的VideoState结构体,然后遍历视频文件中的所有流,找到视频流和音频流的index,然后用到一个很重要的函数stream_component_open,这个函数主要功能是根据指定类型打开流,找到对应的解码器、创建对应的音频配置、保存关键信息到 VideoState、启动音频和视频解码线程。
然后创建SDL窗口,创建渲染器,创建纹理,然后开始循环av_read_frame从多媒体文件中读取包,将视频包和音频包存放到不同的队列中,这里涉及到几个队列操作的函数,然后每次循环判断quit标志有没有被置为1,为1就延迟100ms后退出。
int demux_thread(void *arg) {
int err_code;
char errors[1024] = {0,};
VideoState *is = (VideoState *) arg;//传递用户参数
AVFormatContext *pFormatCtx;//保存文件容器封装信息及码流参数的结构体
AVPacket pkt1, *packet = &pkt1;//在栈上创建临时数据包对象并关联指针
int video_index = -1;//视频流类型标号初始化为-1
int audio_index = -1;//音频流类型标号初始化为-1
int i;//循环变量
is->videoStream = -1;//视频流类型标号初始化为-1
is->audioStream = -1;//音频流类型标号初始化为-1
global_video_state = is;//传递全局状态参量结构体
/* open input file, and allocate format context *///打开多媒体文件
if ((err_code = avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)) < 0) {
av_strerror(err_code, errors, 1024);
fprintf(stderr, "Could not open source file %s, %d(%s)\n", is->filename, err_code, errors);
return -1;
}
is->pFormatCtx = pFormatCtx;//传递文件容器封装信息及码流参数
// Retrieve stream information 查找流相关的信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0)
return -1; // Couldn't find stream information
// Dump information about file onto standard error,打印pFormatCtx中的码流信息
av_dump_format(pFormatCtx, 0, is->filename, 0);
// Find the first video stream and audio stream
for (i = 0; i < pFormatCtx->nb_streams; i++) {//遍历文件中包含的所有流媒体类型(视频流、音频流、字幕流等)
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO &&
video_index < 0)//若文件中包含有视频流
{
video_index = i;//用视频流类型的标号修改标识,使之不为-1
}
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO &&
audio_index < 0)//用视频流类型的标号修改标识,使之不为-1
{
audio_index = i;//用音频流类型的标号修改标识,使之不为-1
}
}
if (audio_index >= 0) {//检查文件中是否存在音频流
stream_component_open(is, audio_index);//根据指定类型打开音频流 设置音频相关参数
/*
* set code paramter 、init and open decoder 、sws init and set opts、packet_queue_init、creat decoder thread
*/
}
if (video_index >= 0) {
stream_component_open(is, video_index);//根据指定类型打开视频流 设置音频相关参数
}
if (is->videoStream < 0 || is->audioStream < 0) {//检查文件中是否存在音视频流
fprintf(stderr, "%s: could not open codecs\n", is->filename);
goto fail;//跳转至异常处理
}
//creat window from SDL 创建窗口
win = SDL_CreateWindow("Media Player",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
is->video_ctx->width, is->video_ctx->height,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!win) {
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
renderer = SDL_CreateRenderer(win, -1, 0);//创建渲染器
//IYUV: Y + U + V (3 planes)
//YV12: Y + V + U (3 planes)
Uint32 pixformat = SDL_PIXELFORMAT_IYUV;
//create texture for render
texture = SDL_CreateTexture(renderer,//创建纹理
pixformat,
SDL_TEXTUREACCESS_STREAMING,
is->video_ctx->width,
is->video_ctx->height);
// main decode loop
for (;;) {
if (is->quit)//检查退出进程标识
{
break;
}
// Seek stuff goes here,检查音视频编码数据包(未解码)队列长度是否溢出
if (is->audioq.size > MAX_AUDIOQ_SIZE ||
is->videoq.size > MAX_VIDEOQ_SIZE) {
SDL_Delay(10);
continue;
}
/*-----------------------
* read in a packet and store it in the AVPacket struct
* ffmpeg allocates the internal data for us,which is pointed to by packet.data
* this is freed by the av_free_packet()
-----------------------*/
if (av_read_frame(is->pFormatCtx, packet) < 0) {//从多媒体文件中读取一个包
if (is->pFormatCtx->pb->error == 0) {
SDL_Delay(100); /* no error; wait for user input */
continue;
} else {
break;
}
}
//将视频包和音频包存放到不同的队列中
// Is this a packet from the video stream?
if (packet->stream_index == is->videoStream)//检查数据包是否为视频类型
{
packet_queue_put(&is->videoq, packet);//向队列中插入数据包
} else if (packet->stream_index == is->audioStream)//检查数据包是否为音频类型
{
packet_queue_put(&is->audioq, packet);//向队列中插入数据包
} else {//检查数据包是否为字幕类型
av_free_packet(packet);//释放packet中保存的(字幕)编码数据
}
}
/* all done - wait for it */
while (!is->quit) {
SDL_Delay(100);
}
fail:
if (1) {
SDL_Event event;//SDL事件对象
event.type = FF_QUIT_EVENT;//指定退出事件类型
event.user.data1 = is;//传递用户数据
SDL_PushEvent(&event);//将该事件对象压入SDL后台事件队列
}
return 0;
}
3.视频解码线程
视频解码线程的目的就是从视频流队列中提取数据包,然后解码后放入VideoPicture类型的数组缓冲区去
首先从队列中提取数据包到packet,并将提取的数据包出队列,然后调用avcodec_decode_video2解码packet得到一帧数据,然后av_frame_get_best_effort_timestamp得到当前帧的pts,调用synchronize_video更新视频帧的 PTS,然后调用queue_picture将解码成功的frame放进数据缓冲区。
//视频解码线程函数
int decode_video_thread(void *arg)
{
VideoState *is = (VideoState *) arg;//传递用户数据
AVPacket pkt1, *packet = &pkt