EasyPlayerPro(Windows)開發之框架講解

EasyPlayerPro for Windows是基於ffmpeg進行開發的全功能播放器,開發過程中參考了很多開源的播放器,諸如vlc和ffplay等,其中最強大的莫過於vlc,但是鑑於vlc框架過於龐大而其中仍存在諸多問題而捨棄了,而其他的更傾向於演示demo,只能提供部分借鑑意義;故而,EasyPlayerPro 一貫秉承Easy系列小而精,接口簡單功能強大的宗旨從新設計了一套框架,該套框架能適應多線程調用以及多個播放實例同時運行,和EasyPlayer一樣Easy; 當然,在此也鄭重的感謝各大開源播放器以及ffmpeg的作者的無私奉獻。

EasyPlayerPro分爲三大模塊:打開模塊,讀取流數據模塊,解碼模塊和渲染模塊,其中:

(1) 打開模塊
打開流模塊很簡單,教科書式的調用方法:

    player->avformat_context = avformat_alloc_context();
    player->avformat_context->interrupt_callback.callback = interrupt_cb;
    player->avformat_context->interrupt_callback.opaque = player;

    // open input file
    AVDictionary *options = NULL;
    //av_dict_set(&options, "rtsp_transport", "udp", 0);
    if (avformat_open_input(&player->avformat_context, url, fmt, &options) != 0) 
    {
        goto error_handler;
    }

    // find stream info
    if (avformat_find_stream_info(player->avformat_context, NULL) < 0) 
    {
        goto error_handler;
    }

    // set current audio & video stream
    for (i=0,idx=-1,cur=-1; i<(int)player->avformat_context->nb_streams; i++) {
switch (type) {
    case AVMEDIA_TYPE_AUDIO:
        // get last codec context
        if (player->acodec_context) {
            lastctxt = player->acodec_context;
        }

        // get new acodec_context & astream_timebase
        player->acodec_context   = player->avformat_context->streams[idx]->codec;
        player->astream_timebase = player->avformat_context->streams[idx]->time_base;

        // reopen codec
        if (lastctxt) avcodec_close(lastctxt);
        decoder = avcodec_find_decoder(player->acodec_context->codec_id);
        if (decoder && avcodec_open2(player->acodec_context, decoder, NULL) == 0) {
            player->astream_index = idx;
        }
        else {
            av_log(NULL, AV_LOG_WARNING, "failed to find or open decoder for audio !\n");
            player->astream_index = -1;
        }
        break;

    case AVMEDIA_TYPE_VIDEO:
        // get last codec context
        if (player->vcodec_context) {
            lastctxt = player->vcodec_context;
        }

        // get new vcodec_context & vstream_timebase
        player->vcodec_context   = player->avformat_context->streams[idx]->codec;
        player->vstream_timebase = player->avformat_context->streams[idx]->time_base;

        // reopen codec
        if (lastctxt) avcodec_close(lastctxt);
        decoder = avcodec_find_decoder(player->vcodec_context->codec_id);
        if (decoder && avcodec_open2(player->vcodec_context, decoder, NULL) == 0) {
            player->vstream_index = idx;
        }
        else {
            av_log(NULL, AV_LOG_WARNING, "failed to find or open decoder for video !\n");
            player->vstream_index = -1;
        }
        break;

    case AVMEDIA_TYPE_SUBTITLE:
        return -1; // todo...
        }
    }
    if (idx == -1) return -1;
    // for audio
    if (player->astream_index != -1)
    {
        arate   = player->acodec_context->sample_rate;
        aformat = player->acodec_context->sample_fmt;
        alayout = player->acodec_context->channel_layout;
        //++ fix audio channel layout issue
        if (alayout == 0) {
            alayout = av_get_default_channel_layout(player->acodec_context->channels);
        }
        //-- fix audio channel layout issue
    }

    // for video
    if (player->vstream_index != -1) {
        vrate = player->avformat_context->streams[player->vstream_index]->r_frame_rate;
        if (vrate.num / vrate.den >= 100) {
            vrate.num = 25;
            vrate.den = 1;
        }
        player->vcodec_context->pix_fmt = vformat;
        width   = player->vcodec_context->width;
        height  = player->vcodec_context->height;
    }

首先,avformat_open_input打開一個流,爲了避免在打開流的時候出現阻塞,我們創建一個線程來執行,同時,爲了防止ffmpeg內部出現持久行的阻塞,我們傳入阻塞回調函數,在關閉流或者其他必要的時候解除阻塞;avformat_find_stream_info獲取流的解碼信息,根據音視頻以及字幕的解碼信息初始化解碼器;

(2) 讀取流數據模塊

        retv = av_read_frame(player->avformat_context, packet);
        //++ play completed ++//
        if (retv < 0)
        {
            if (player->avformat_context->pb && player->avformat_context->pb->error)
            {       
                //告知播放實時流中斷
                player->error_flag = 1;
                //創建斷線重連錯誤檢測線程
                //  [9/4/2017 swordtwelve]
                break;
            }
            player->player_status |= PS_D_PAUSE;
            pktqueue_write_post_i(player->pktqueue, packet);
            usleep(20*1000); 
            continue;
        }
        //-- play completed --//
        player->error_flag = 0;//-1=初始化 0=正常 1-n錯誤代碼

        // audio
        if (packet->stream_index == player->astream_index)
        {
            pktqueue_write_post_a(player->pktqueue, packet);
        }

        // video
        if (packet->stream_index == player->vstream_index)
        {
            pktqueue_write_post_v(player->pktqueue, packet);
        }

        if (  packet->stream_index != player->astream_index
            && packet->stream_index != player->vstream_index )
        {
            av_packet_unref(packet); // free packet
            pktqueue_write_post_i(player->pktqueue, packet);
        }
    }

讀取數據模塊超級簡單,創建一個線程循環執行av_read_frame,讀取到一幀就將其放入隊列,這裏採用了ffplay的阻塞的方式來處理隊列的消費者和生產者的問題,這塊有待優化,後續將改成無鎖循環隊列模式,如EasyPlayer。

(3) 解碼模塊
解碼模塊分爲音頻和視頻解碼模塊,音視頻的解碼流程非常相似,
主要分爲三步:
a. 從隊列中讀取音視頻編碼數據;
b. 音視頻分別採用avcodec_decode_audio4和avcodec_decode_video2進行解碼;
c. 音視頻渲染;
這裏着重講解視頻的解碼後的過程,其中涉及到解碼後的原始圖像數據進行處理,解碼出一幀圖像以後,我們需要對其進行字幕和圖像或者其他的視頻圖像的疊加,藉助ffmpeg強大的圖像轉換和縮放能力,藉助VFX庫我們很容易實現:

            consumed = avcodec_decode_video2(player->vcodec_context, vframe, &gotvideo, packet);
            if (consumed < 0) {
                av_log(NULL, AV_LOG_WARNING, "an error occurred during decoding video.\n");
                break;
            }

            if (gotvideo) 
            {
                // 解碼視頻幀添加特技處理 [9/7/2017 dingshuai]
                // 1. 疊加圖片
                // 2. 疊加字母
                // 3. 畫框...
                // 對解碼幀進行特技處理(字符,圖片疊加,添加特效) [Dingshuai 2017/08/07]
#if 1
                WaterMarkInfo g_waterMarkInfo = player->vfxConfigInfo.warkMarkInfo;
                if (g_waterMarkInfo.bIsUseWaterMark)
                {
                    if (player->vcodec_context->width != vframe->width   || 
                        player->vcodec_context->height != vframe->height || 
                        player->vfxConfigInfo.warkMarkInfo.bResetWaterMark )
                    {
                        //初始化水印疊加
                        //;表示臺標位置:1 == 左上 2 == 右上 3 == 左下 4 == 右下
                        //eWaterMarkPos = 3

                        //;水印頂點x軸座標,建議不小於0;不大於視頻寬度
                        //nLeftTopX = 0

                        //;水印頂點y軸座標,建議不小於0;不大於視頻高度
                        //nLeftTopY = 480

                        //;水印風格:0 - 6
                        //eWatermarkStyle = 3

                        //;水印圖像文件路徑LOGO.png
                        //strWMFilePath = .\Res\logo.png
                        switch (g_waterMarkInfo.eWaterMarkPos)
                        {
                        case POS_LEFT_TOP:
                            g_waterMarkInfo.nLeftTopX = 0;
                            g_waterMarkInfo.nLeftTopY = 0;
                            break;
                        case POS_RIGHT_TOP:
                            g_waterMarkInfo.nLeftTopX = vframe->width;
                            g_waterMarkInfo.nLeftTopY = 0;
                            break;
                        case POS_LEFT_BOTTOM:
                            g_waterMarkInfo.nLeftTopX = 0;
                            g_waterMarkInfo.nLeftTopY = vframe->height;
                            break;
                        case POS_RIGHT_BOTTOM:
                            g_waterMarkInfo.nLeftTopX = vframe->width;
                            g_waterMarkInfo.nLeftTopY = vframe->height;
                            break;
                        }

                        player->vfxHandle->SetVideoInVideoParam( 101, 0, 0, vframe->width,
                            vframe->height, 100, 100, 100);

                        player->vfxHandle->SetLogoImage(g_waterMarkInfo.strWMFilePath, g_waterMarkInfo.nLeftTopX,
                            g_waterMarkInfo.nLeftTopY, g_waterMarkInfo.bIsUseWaterMark, g_waterMarkInfo.eWatermarkStyle);

                        player->vfxConfigInfo.warkMarkInfo.bResetWaterMark = FALSE;
                    }
                }


                //初始化字幕信息
                VideoTittleInfo tittleInfo = player->vfxConfigInfo.tittleInfo;
                if(tittleInfo.bResetTittleInfo)
                {

                    //  -->1、初始化創建字幕指針,並初始化視頻長寬參數       m_pVideoVfxMakerInfo->nDesWidth, m_pVideoVfxMakerInfo->nDesHeight,  m_pVideoVfxMakerInfo->strDesBytesType);
                    player->vfxHandle->CreateOverlayTitle(vframe->width, vframe->height, ("YUY2"));

                    //  -->2、設置字幕文字信息
                    LOGFONTA inFont;
                    inFont.lfHeight      = tittleInfo.nTittleHeight;
                    inFont.lfWidth       = tittleInfo.nTittleWidth;
                    inFont.lfEscapement  = 0;
                    inFont.lfOrientation = 0;
                    inFont.lfWeight      = tittleInfo.nFontWeight;//FW_NORMAL;
                    inFont.lfItalic      = 0;
                    inFont.lfUnderline   = 0;
                    inFont.lfStrikeOut   = 0;
                    inFont.lfCharSet        =GB2312_CHARSET;// ANSI_CHARSET;//134
                    inFont.lfOutPrecision   =3;// OUT_DEFAULT_PRECIS;
                    inFont.lfClipPrecision  = 2;//CLIP_DEFAULT_PRECIS;
                    inFont.lfQuality        = 1;//PROOF_QUALITY;
                    inFont.lfPitchAndFamily = 0;//49;//49

                    strcpy(inFont.lfFaceName, tittleInfo.strFontType);//"華文新魏");//"華文隸書");"隸書"

                    POINT pointTitle;

                    if(tittleInfo.nMoveType==0)
                    {
                        pointTitle= tittleInfo.ptStartPosition;
                        if(pointTitle.x<=0) pointTitle.x=1;
                        if(pointTitle.x>=vframe->width) pointTitle.x=vframe->width/2;
                    }
                    else if(tittleInfo.nMoveType==1)//從左往右
                    {

                        pointTitle.x =  -1;
                        pointTitle.y = tittleInfo.ptStartPosition.y;
                    }
                    else if(tittleInfo.nMoveType==2)
                    {
                        pointTitle.x =  vframe->width+1;
                        pointTitle.y = tittleInfo.ptStartPosition.y;
                    }

                    player->vfxHandle->SetOverlayTitleInfo(tittleInfo.strTittleContent, 
                        inFont, tittleInfo.nColorR, tittleInfo.nColorG,
                        tittleInfo.nColorB, pointTitle);

                    //-->3、設置字幕運行抓狀態
                    player->vfxHandle->SetOverlayTitleState(tittleInfo.nState);

                    player->vfxConfigInfo.tittleInfo.bResetTittleInfo = FALSE;
                }

                if (player->vfxHandle && (g_waterMarkInfo.bIsUseWaterMark || tittleInfo.nState))//logo-水印 + 字幕 + ???
                {
                    if (player->vcodec_context->width != vframe->width   || 
                        player->vcodec_context->height != vframe->height )
                    {
                        if (pVfxBuffer)
                        {
                            free(pVfxBuffer);
                            pVfxBuffer = NULL;
                        }
                    }

                    int nBufSize = vframe->width*vframe->height << 1;
                    if (!pVfxBuffer)
                    {
                        pVfxBuffer = (BYTE*)malloc(nBufSize); //緩存寫入源數據 
                        memset(pVfxBuffer, 0x00, nBufSize);
                    }

                    AVFrame src;
                    av_image_fill_arrays(src.data, src.linesize, pVfxBuffer, outPixelFormat, vframe->width, vframe->height, 1);
                    //YUV420 -> YUY2
                    ConvertColorSpace(&src, outPixelFormat, vframe, inPixelFormat, vframe->width, vframe->height);
//                  av_image_copy_to_buffer(pVfxBuffer, nBufSize,
//                      vframe->data, vframe->linesize, AV_PIX_FMT_YUYV422,  vframe->width, vframe->height, 1);

                    //水印疊加
                    if(g_waterMarkInfo.bIsUseWaterMark)
                        player->vfxHandle->AddWaterMask(pVfxBuffer);
                    //OSD疊加
                    if(tittleInfo.nState)
                        player->vfxHandle->DoOverlayTitle(pVfxBuffer);

                    //YUY2 -> I420
                    //ConvertColorSpace(vframe, inPixelFormat, &src, outPixelFormat, vframe->width, vframe->height);
                    av_image_fill_arrays(vframe->data, vframe->linesize, pVfxBuffer, outPixelFormat, vframe->width, vframe->height, 1);
                    int nPixelFmt = AV_PIX_FMT_YUYV422;
                    player_setparam(player, PARAM_RENDER_OUTFORMAT, &nPixelFmt); 
                }
                else
                {
                    int nPixelFmt = AV_PIX_FMT_YUV420P;
                    player_setparam(player, PARAM_RENDER_OUTFORMAT, &nPixelFmt); 
                }
#endif

由於視頻渲染需要一定的時間,我們也將解碼幀數據進入隊列進行緩存,從而保證播放的流暢性;

(4) 渲染模塊
渲染模塊分爲音頻渲染和視頻渲染,音頻渲染即播放,使用waveOutOpen,waveOutWrite等waveout函數即可實現,下面重點說一下視頻渲染,視頻渲染通俗講也就是圖像繪製,Windows平臺可採用D3D,DDraw, GDI,OpenGL等多種方式進行呈現,本文主要採用3種渲染方式,D3D,GDI和OpenGL;
爲了保證渲染的流暢性,我們創建線程執行渲染,
a. 讀取解碼圖像隊列;
b. 音視頻時間戳同步處理;
c. D3D/gdi/openGL渲染:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章