FFmpeg開發筆記(九):ffmpeg解碼rtsp流並使用SDL同步播放

紅胖子(紅模仿)發表於2020-11-10

前言

  ffmpeg播放rtsp網路流和攝像頭流。

 

Demo

  使用ffmpeg播放區域網rtsp1080p海康攝像頭:延遲0.2s,存在馬賽克
  在這裡插入圖片描述

  使用ffmpeg播放網路rtsp檔案流:偶爾卡頓,延遲看不出
  在這裡插入圖片描述
  使用vlc軟體播放區域網rtsp1080p海康攝像頭:演示2s,不存在馬賽克
  在這裡插入圖片描述
  使用vlc軟體播放網路rtsp檔案流:不卡頓,延遲看不出
  在這裡插入圖片描述

 

FFmpeg基本播放流程

ffmpeg解碼流程

  ffmpeg新增API的解碼執行流程。
  新api解碼基本流程如下:
  在這裡插入圖片描述

步驟一:註冊:

  使用ffmpeg對應的庫,都需要進行註冊,可以註冊子項也可以註冊全部。

步驟二:開啟檔案:

  開啟檔案,根據檔名資訊獲取對應的ffmpeg全域性上下文。

步驟三:探測流資訊:

  一定要探測流資訊,拿到流編碼的編碼格式,不探測流資訊則其流編碼器拿到的編碼型別可能為空,後續進行資料轉換的時候就無法知曉原始格式,導致錯誤。

步驟四:查詢對應的解碼器

  依據流的格式查詢解碼器,軟解碼還是硬解碼是在此處決定的,但是特別注意是否支援硬體,需要自己查詢本地的硬體解碼器對應的標識,並查詢其是否支援。普遍操作是,列舉支援檔案字尾解碼的所有解碼器進行查詢,查詢到了就是可以硬解了(此處,不做過多的討論,對應硬解碼後續會有文章進行進一步研究)。
  (注意:解碼時查詢解碼器,編碼時查詢編碼器,兩者函式不同,不要弄錯了,否則後續能開啟但是資料是錯的)

步驟五:開啟解碼器

  開打解碼器的時候,播放的是rtsp流,需要設定一些引數,在ffmpeg中引數的設定是通過AVDictionary來設定的。
  使用以上設定的引數,傳入並開啟獲取到的解碼器。

AVDictionary *pAVDictionary = 0
// 設定快取大小 1024000byte
av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0);
// 設定超時時間 20s
av_dict_set(&pAVDictionary, "stimeout", "20000000", 0);
// 設定最大延時 3s
av_dict_set(&pAVDictionary, "max_delay", "30000000", 0);
// 設定開啟方式 tcp/udp
av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0);
ret = avcodec_open2(pAVCodecContext, pAVCodec, &pAVDictionary);
if(ret)
{
    LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
    return;
}

步驟六:申請縮放資料格式轉換結構體

  此處特別注意,基本上解碼的資料都是yuv系列格式,但是我們顯示的資料是rgb等相關顏色空間的資料,所以此處轉換結構體就是進行轉換前到轉換後的描述,給後續轉換函式提供轉碼依據,是很關鍵並且非常常用的結構體。

步驟七:申請快取區

  申請一個快取區outBuffer,fill到我們目標幀資料的data上,比如rgb資料,QAVFrame的data上存是有指定格式的資料,且儲存有規則,而fill到outBuffer(自己申請的目標格式一幀快取區),則是我們需要的資料格式儲存順序。
  舉個例子,解碼轉換後的資料為rgb888,實際直接用data資料是錯誤的,但是用outBuffer就是對的,所以此處應該是ffmpeg的fill函式做了一些轉換。
進入迴圈解碼:

步驟八:分組資料包送往解碼器(此處由一個步驟變為了步驟八和步驟九)

  拿取封裝的一個packet,判斷packet資料的型別進行送往解碼器解碼。

步驟九:從解碼器快取中獲取解碼後的資料

  一個包可能存在多組資料,老的api獲取的是第一個,新的api分開後,可以迴圈獲取,直至獲取不到跳轉“步驟十二”。

步驟十一:自行處理

  拿到了原始資料自行處理。
  不斷迴圈,直到拿取pakcet函式成功,但是無法got一幀資料,則代表檔案解碼已經完成。
  幀率需要自己控制迴圈,此處只是迴圈拿取,可加延遲等。

步驟十二:釋放QAVPacket

  此處要單獨列出是因為,其實很多網上和開發者的程式碼:
  在進入迴圈解碼前進行了av_new_packet,迴圈中未av_free_packet,造成記憶體溢位;
  在進入迴圈解碼前進行了av_new_packet,迴圈中進行av_free_pakcet,那麼一次new對應無數次free,在編碼器上是不符合前後一一對應規範的。
  檢視原始碼,其實可以發現av_read_frame時,自動進行了av_new_packet(),那麼其實對於packet,只需要進行一次av_packet_alloc()即可,解碼完後av_free_packet。
  執行完後,返回執行“步驟八:獲取一幀packet”,一次迴圈結束。

步驟十三:釋放轉換結構體

  全部解碼完成後,安裝申請順序,進行對應資源的釋放。

步驟十四:關閉解碼/編碼器

  關閉之前開啟的解碼/編碼器。

步驟十五:關閉上下文

  關閉檔案上下文後,要對之前申請的變數按照申請的順序,依次釋放。

 

補充

  ffmpeg開啟rtsp出現嚴重的馬賽克和部分卡頓,需要修改檔案udp.c的快取區大小,修改後需要重新編譯。
  實測更改後的馬賽克會好一些,相比較軟體來說有一些差距的,這部分需要繼續優化。
  編譯請參照《FFmpeg開發筆記(三):ffmpeg介紹、windows編譯以及開發環境搭建

 

Demo原始碼

void FFmpegManager::testDecodeRtspSyncShow()
{
    QString rtspUrl = "http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear2/prog_index.m3u8";
//    QString rtspUrl = "rtsp://admin:Admin123@192.168.1.65:554/h264/ch1/main/av_stream";

    // SDL相關變數預先定義
    SDL_Window *pSDLWindow = 0;
    SDL_Renderer *pSDLRenderer = 0;
    SDL_Surface *pSDLSurface = 0;
    SDL_Texture *pSDLTexture = 0;
    SDL_Event event;

    qint64 startTime = 0;                           // 記錄播放開始
    int currentFrame = 0;                           // 當前幀序號
    double fps = 0;                                 // 幀率
    double interval = 0;                            // 幀間隔

    // ffmpeg相關變數預先定義與分配
    AVFormatContext *pAVFormatContext = 0;          // ffmpeg的全域性上下文,所有ffmpeg操作都需要
    AVStream *pAVStream = 0;                        // ffmpeg流資訊
    AVCodecContext *pAVCodecContext = 0;            // ffmpeg編碼上下文
    AVCodec *pAVCodec = 0;                          // ffmpeg編碼器
    AVPacket *pAVPacket = 0;                        // ffmpag單幀資料包
    AVFrame *pAVFrame = 0;                          // ffmpeg單幀快取
    AVFrame *pAVFrameRGB32 = 0;                     // ffmpeg單幀快取轉換顏色空間後的快取
    struct SwsContext *pSwsContext = 0;             // ffmpeg編碼資料格式轉換
    AVDictionary *pAVDictionary = 0;                // ffmpeg資料字典,用於配置一些編碼器屬性等

    int ret = 0;                                    // 函式執行結果
    int videoIndex = -1;                            // 音訊流所在的序號
    int numBytes = 0;                               // 解碼後的資料長度
    uchar *outBuffer = 0;                           // 解碼後的資料存放快取區

    pAVFormatContext = avformat_alloc_context();    // 分配
    pAVPacket = av_packet_alloc();                  // 分配
    pAVFrame = av_frame_alloc();                    // 分配
    pAVFrameRGB32 = av_frame_alloc();               // 分配

    if(!pAVFormatContext || !pAVPacket || !pAVFrame || !pAVFrameRGB32)
    {
        LOG << "Failed to alloc";
        return;
    }
    // 步驟一:註冊所有容器和編解碼器(也可以只註冊一類,如註冊容器、註冊編碼器等)
    av_register_all();
    avformat_network_init();
    // 步驟二:開啟檔案(ffmpeg成功則返回0)
    LOG << "開啟:" << rtspUrl;
    ret = avformat_open_input(&pAVFormatContext, rtspUrl.toUtf8().data(), 0, 0);
    if(ret)
    {
        LOG << "Failed";
        return;
    }
    // 步驟三:探測流媒體資訊
    ret = avformat_find_stream_info(pAVFormatContext, 0);
    if(ret < 0)
    {
        LOG << "Failed to avformat_find_stream_info(pAVFormatContext, 0)";
        return;
    }
    // 步驟四:提取流資訊,提取視訊資訊
    for(int index = 0; index < pAVFormatContext->nb_streams; index++)
    {
        pAVCodecContext = pAVFormatContext->streams[index]->codec;
        pAVStream = pAVFormatContext->streams[index];
        switch (pAVCodecContext->codec_type)
        {
        case AVMEDIA_TYPE_UNKNOWN:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_UNKNOWN";
            break;
        case AVMEDIA_TYPE_VIDEO:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_VIDEO";
            videoIndex = index;
            LOG;
            break;
        case AVMEDIA_TYPE_AUDIO:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_AUDIO";
            break;
        case AVMEDIA_TYPE_DATA:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_DATA";
            break;
        case AVMEDIA_TYPE_SUBTITLE:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_SUBTITLE";
            break;
        case AVMEDIA_TYPE_ATTACHMENT:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_ATTACHMENT";
            break;
        case AVMEDIA_TYPE_NB:
            LOG << "流序號:" << index << "型別為:" << "AVMEDIA_TYPE_NB";
            break;
        default:
            break;
        }
        // 已經找打視訊品流
        if(videoIndex != -1)
        {
            break;
        }
    }

    if(videoIndex == -1 || !pAVCodecContext)
    {
        LOG << "Failed to find video stream";
        return;
    }

    // 步驟五:對找到的視訊流尋解碼器
    pAVCodec = avcodec_find_decoder(pAVCodecContext->codec_id);
    if(!pAVCodec)
    {
        LOG << "Fialed to avcodec_find_decoder(pAVCodecContext->codec_id):"
            << pAVCodecContext->codec_id;
        return;
    }
    // 步驟六:開啟解碼器
    // 設定快取大小 1024000byte
    av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0);
    // 設定超時時間 20s
    av_dict_set(&pAVDictionary, "stimeout", "20000000", 0);
    // 設定最大延時 3s
    av_dict_set(&pAVDictionary, "max_delay", "30000000", 0);
    // 設定開啟方式 tcp/udp
    av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0);
    ret = avcodec_open2(pAVCodecContext, pAVCodec, &pAVDictionary);
    if(ret)
    {
        LOG << "Failed to avcodec_open2(pAVCodecContext, pAVCodec, pAVDictionary)";
        return;
    }

    // 顯示視訊相關的引數資訊(編碼上下文)
    LOG << "位元率:" << pAVCodecContext->bit_rate;

    LOG << "寬高:" << pAVCodecContext->width << "x" << pAVCodecContext->height;
    LOG << "格式:" << pAVCodecContext->pix_fmt;  // AV_PIX_FMT_YUV420P 0
    LOG << "幀率分母:" << pAVCodecContext->time_base.den;
    LOG << "幀率分子:" << pAVCodecContext->time_base.num;
    LOG << "幀率分母:" << pAVStream->avg_frame_rate.den;
    LOG << "幀率分子:" << pAVStream->avg_frame_rate.num;
    LOG << "總時長:" << pAVStream->duration / 10000.0 << "s";
    LOG << "總幀數:" << pAVStream->nb_frames;
    // 有總時長的時候計算幀率(較為準確)
//    fps = pAVStream->nb_frames / (pAVStream->duration / 10000.0);
//    interval = pAVStream->duration / 10.0 / pAVStream->nb_frames;
    // 沒有總時長的時候,使用分子和分母計算
    fps = pAVStream->avg_frame_rate.num * 1.0f / pAVStream->avg_frame_rate.den;
    interval = 1 * 1000 / fps;
    LOG << "平均幀率:" << fps;
    LOG << "幀間隔:" << interval << "ms";
    // 步驟七:對拿到的原始資料格式進行縮放轉換為指定的格式高寬大小
    pSwsContext = sws_getContext(pAVCodecContext->width,
                                 pAVCodecContext->height,
                                 pAVCodecContext->pix_fmt,
                                 pAVCodecContext->width,
                                 pAVCodecContext->height,
                                 AV_PIX_FMT_RGBA,
                                 SWS_FAST_BILINEAR,
                                 0,
                                 0,
                                 0);
    numBytes = avpicture_get_size(AV_PIX_FMT_RGBA,
                                  pAVCodecContext->width,
                                  pAVCodecContext->height);
    outBuffer = (uchar *)av_malloc(numBytes);
    // pAVFrame32的data指標指向了outBuffer
    avpicture_fill((AVPicture *)pAVFrameRGB32,
                   outBuffer,
                   AV_PIX_FMT_RGBA,
                   pAVCodecContext->width,
                   pAVCodecContext->height);

    ret = SDL_Init(SDL_INIT_VIDEO);
    if(ret)
    {
        LOG << "Failed";
        return;
    }
    pSDLWindow = SDL_CreateWindow(rtspUrl.toUtf8().data(),
                                  0,
                                  0,
                                  pAVCodecContext->width,
                                  pAVCodecContext->height,
                                  SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if(!pSDLWindow)
    {
        LOG << "Failed";
        return;
    }
    pSDLRenderer = SDL_CreateRenderer(pSDLWindow, -1, 0);
    if(!pSDLRenderer)
    {
        LOG << "Failed";
        return;
    }

    startTime = QDateTime::currentDateTime().toMSecsSinceEpoch();
    currentFrame = 0;


    pSDLTexture = SDL_CreateTexture(pSDLRenderer,
//                                  SDL_PIXELFORMAT_IYUV,
                                    SDL_PIXELFORMAT_YV12,
                                    SDL_TEXTUREACCESS_STREAMING,
                                    pAVCodecContext->width,
                                    pAVCodecContext->height);
    if(!pSDLTexture)
    {
        LOG << "Failed";
        return;
    }
    // 步驟八:讀取一幀資料的資料包
    while(av_read_frame(pAVFormatContext, pAVPacket) >= 0)
    {
        if(pAVPacket->stream_index == videoIndex)
        {
            // 步驟八:對讀取的資料包進行解碼
            ret = avcodec_send_packet(pAVCodecContext, pAVPacket);
            if(ret)
            {
                LOG << "Failed to avcodec_send_packet(pAVCodecContext, pAVPacket) ,ret =" << ret;
                break;
            }
            while(!avcodec_receive_frame(pAVCodecContext, pAVFrame))
            {
                sws_scale(pSwsContext,
                          (const uint8_t * const *)pAVFrame->data,
                          pAVFrame->linesize,
                          0,
                          pAVCodecContext->height,
                          pAVFrameRGB32->data,
                          pAVFrameRGB32->linesize);
                // 格式為RGBA=8:8:8:8”
                // rmask 應為 0xFF000000  但是顏色不對 改為 0x000000FF 對了
                // gmask     0x00FF0000                  0x0000FF00
                // bmask     0x0000FF00                  0x00FF0000
                // amask     0x000000FF                  0xFF000000
                // 測試了ARGB,也是相反的,而QImage是可以正確載入的
                // 暫時只能說這個地方標記下,可能有什麼設定不對什麼的
                qDebug() << __FILE__ << __LINE__  << pSDLTexture;
                SDL_UpdateYUVTexture(pSDLTexture,
                                     NULL,
                                     pAVFrame->data[0], pAVFrame->linesize[0],
                                     pAVFrame->data[1], pAVFrame->linesize[1],
                                     pAVFrame->data[2], pAVFrame->linesize[2]);
                qDebug() << __FILE__ << __LINE__  << pSDLTexture;

                SDL_RenderClear(pSDLRenderer);
                // Texture複製到Renderer
                SDL_Rect        sdlRect;
                sdlRect.x = 0;
                sdlRect.y = 0;
                sdlRect.w = pAVFrame->width;
                sdlRect.h = pAVFrame->height;
                qDebug() << __FILE__ << __LINE__ << SDL_RenderCopy(pSDLRenderer, pSDLTexture, 0, &sdlRect) << pSDLTexture;
                // 更新Renderer顯示
                SDL_RenderPresent(pSDLRenderer);
                // 事件處理
                SDL_PollEvent(&event);
            }
            // 下一幀
            currentFrame++;
            while(QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime < currentFrame * interval)
            {
                SDL_Delay(1);
            }
            LOG << "current:" << currentFrame <<"," << time << (QDateTime::currentDateTime().toMSecsSinceEpoch() - startTime);
        }
    }
    LOG << "釋放回收資源";
    if(outBuffer)
    {
        av_free(outBuffer);
        outBuffer = 0;
    }
    if(pSwsContext)
    {
        sws_freeContext(pSwsContext);
        pSwsContext = 0;
        LOG << "sws_freeContext(pSwsContext)";
    }
    if(pAVFrameRGB32)
    {
        av_frame_free(&pAVFrameRGB32);
        pAVFrame = 0;
        LOG << "av_frame_free(pAVFrameRGB888)";
    }
    if(pAVFrame)
    {
        av_frame_free(&pAVFrame);
        pAVFrame = 0;
        LOG << "av_frame_free(pAVFrame)";
    }
    if(pAVPacket)
    {
        av_free_packet(pAVPacket);
        pAVPacket = 0;
        LOG << "av_free_packet(pAVPacket)";
    }
    if(pAVCodecContext)
    {
        avcodec_close(pAVCodecContext);
        pAVCodecContext = 0;
        LOG << "avcodec_close(pAVCodecContext);";
    }
    if(pAVFormatContext)
    {
        avformat_close_input(&pAVFormatContext);
        avformat_free_context(pAVFormatContext);
        pAVFormatContext = 0;
        LOG << "avformat_free_context(pAVFormatContext)";
    }

    // 步驟五:銷燬渲染器
    SDL_DestroyRenderer(pSDLRenderer);
    // 步驟六:銷燬視窗
    SDL_DestroyWindow(pSDLWindow);
    // 步驟七:退出SDL
    SDL_Quit();
}
 

工程模板v1.5.0

  對應工程模板v1.5.0:增加播放rtsp使用SDL播放Demo

 

相關文章