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


前言

  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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM