基于ffmpeg和SDL的音视频播放器

一.播放器的多线程模型

本文从播放器的多线程的模型开始分析播放器的源码

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值