Android 音視頻深入 十八 FFmpeg播放視頻,有聲音(附源碼下載)

来源:https://www.cnblogs.com/jianpanwuzhe/archive/2018/03/02/8494261.html
-Advertisement-
Play Games

項目地址https://github.com/979451341/AudioVideoStudyCodeTwo/tree/master/FFmpegv%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91%E6%9C%89%E5%A3%B0%E9%9F%B3%EF%BC%8C%E6 ...


項目地址
https://github.com/979451341/AudioVideoStudyCodeTwo/tree/master/FFmpegv%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91%E6%9C%89%E5%A3%B0%E9%9F%B3%EF%BC%8C%E6%9A%82%E5%81%9C%EF%BC%8C%E9%87%8A%E6%94%BE%E3%80%81%E5%BF%AB%E8%BF%9B%E3%80%81%E9%80%80%E5%90%8E

這個項目是簡書2012lc大神寫的,播放沒問題就是其他功能都有點卡過頭了。。。
哎,自己也沒能寫出一個優秀的播放器,

回到正題

首先這個代碼是生產者和消費者的模式,生成者就是不斷地解碼mp4將一幀的數據給消費者,消費者就是音頻播放類和視頻播放類,也就說生成者一個,消費者兩個,都是通過pthread開啟線程,通過互斥鎖和條件信息來維持這個關係鏈

1.生產者—輸出一幀幀的數據

 

開始就是初始化各類組件和測試視頻文件是否能夠打開,並獲得視頻相關信息為後來代碼做準備工作

void init() {
    LOGE("開啟解碼線程")
    //1.註冊組件
    av_register_all();
    avformat_network_init();
    //封裝格式上下文
    pFormatCtx = avformat_alloc_context();

    //2.打開輸入視頻文件
    if (avformat_open_input(&pFormatCtx, inputPath, NULL, NULL) != 0) {
        LOGE("%s", "打開輸入視頻文件失敗");
    }
    //3.獲取視頻信息
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGE("%s", "獲取視頻信息失敗");
    }

    //得到播放總時間
    if (pFormatCtx->duration != AV_NOPTS_VALUE) {
        duration = pFormatCtx->duration;//微秒
    }
}

 

初始化音頻類和視頻類,並將SurfaceView給視頻類

    ffmpegVideo = new FFmpegVideo;
    ffmpegMusic = new FFmpegMusic;
    ffmpegVideo->setPlayCall(call_video_play);

 

開啟生成者線程

pthread_create(&p_tid, NULL, begin, NULL);//開啟begin線程

 

從視頻信息里獲取視屏流和音頻流,將各自的解碼器上下文複製分別給與兩個消費者類,並將流在哪個位置、還有時間單位給與兩個消費者類

    //找到視頻流和音頻流
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        //獲取解碼器
        AVCodecContext *avCodecContext = pFormatCtx->streams[i]->codec;
        AVCodec *avCodec = avcodec_find_decoder(avCodecContext->codec_id);

        //copy一個解碼器,
        AVCodecContext *codecContext = avcodec_alloc_context3(avCodec);
        avcodec_copy_context(codecContext, avCodecContext);
        if (avcodec_open2(codecContext, avCodec, NULL) < 0) {
            LOGE("打開失敗")
            continue;
        }
        //如果是視頻流
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
            ffmpegVideo->index = i;
            ffmpegVideo->setAvCodecContext(codecContext);
            ffmpegVideo->time_base = pFormatCtx->streams[i]->time_base;
            if (window) {
                ANativeWindow_setBuffersGeometry(window, ffmpegVideo->codec->width,
                                                 ffmpegVideo->codec->height,
                                                 WINDOW_FORMAT_RGBA_8888);
            }
        }//如果是音頻流
        else if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {
            ffmpegMusic->index = i;
            ffmpegMusic->setAvCodecContext(codecContext);
            ffmpegMusic->time_base = pFormatCtx->streams[i]->time_base;
        }
    }

 

開啟兩個消費者類的線程

    ffmpegVideo->setFFmepegMusic(ffmpegMusic);
    ffmpegMusic->play();
    ffmpegVideo->play();

 

然後開始一幀一幀的解碼出數據給兩個消費者類的用來存儲數據的矢量,如果矢量里的數據還有那就沒有播放玩,繼續播放

    while (isPlay) {
        //
        ret = av_read_frame(pFormatCtx, packet);
        if (ret == 0) {
            if (ffmpegVideo && ffmpegVideo->isPlay && packet->stream_index == ffmpegVideo->index
               ) {
                //將視頻packet壓入隊列
                ffmpegVideo->put(packet);
            } else if (ffmpegMusic && ffmpegMusic->isPlay &&
                       packet->stream_index == ffmpegMusic->index) {
                ffmpegMusic->put(packet);
            }
            av_packet_unref(packet);
        } else if (ret == AVERROR_EOF) {
            // 讀完了
            //讀取完畢 但是不一定播放完畢
            while (isPlay) {
                if (ffmpegVideo->queue.empty() && ffmpegMusic->queue.empty()) {
                    break;
                }
                // LOGE("等待播放完成");
                av_usleep(10000);
            }
        }
    }

 

播放完了就停止兩個消費者類的線程,並釋放資源

    isPlay = 0;
    if (ffmpegMusic && ffmpegMusic->isPlay) {
        ffmpegMusic->stop();
    }
    if (ffmpegVideo && ffmpegVideo->isPlay) {
        ffmpegVideo->stop();
    }
    //釋放
    av_free_packet(packet);
    avformat_free_context(pFormatCtx);
    pthread_exit(0);

 

2.消費者—音頻類


開啟線程

      pthread_create(&playId, NULL, MusicPlay, this);//開啟begin線程

 

就下來就是配置OpenSL ES來播放音頻,而這個數據的來源是這一段代碼決定的

    (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);

 

我們再來看看bqPlayerCallback,數據是從getPcm函數得到的

    FFmpegMusic *musicplay = (FFmpegMusic *) context;
    int datasize = getPcm(musicplay);
    if(datasize>0){
        //第一針所需要時間採樣位元組/採樣率
        double time = datasize/(44100*2*2);
        //
        musicplay->clock=time+musicplay->clock;
        LOGE("當前一幀聲音時間%f   播放時間%f",time,musicplay->clock);

        (*bq)->Enqueue(bq,musicplay->out_buffer,datasize);
        LOGE("播放 %d ",musicplay->queue.size());
    }

 

然後這個getPcm函數里,通過get函數來完成獲取一幀數據

agrs->get(avPacket);

 

如果有矢量里有數據它將矢量里的數據取出,如果沒有就等待生產者通過條件變數

//將packet彈出隊列
int FFmpegMusic::get(AVPacket *avPacket) {
    LOGE("取出隊列")
    pthread_mutex_lock(&mutex);
    while (isPlay){
        LOGE("取出對壘 xxxxxx")
        if(!queue.empty()&&isPause){
            LOGE("ispause %d",isPause);
            //如果隊列中有數據可以拿出來
            if(av_packet_ref(avPacket,queue.front())){
                break;
            }
            //取成功了,彈出隊列,銷毀packet
            AVPacket *packet2 = queue.front();
            queue.erase(queue.begin());
            av_free(packet2);
            break;
        } else{
            LOGE("音頻執行wait")
            LOGE("ispause %d",isPause);
            pthread_cond_wait(&cond,&mutex);

        }
    }
    pthread_mutex_unlock(&mutex);
    return 0;
}

 

註意這個獲取的數據是AVPacket,我們需要將他解碼為AVFrame才行

        if (avPacket->pts != AV_NOPTS_VALUE) {
            agrs->clock = av_q2d(agrs->time_base) * avPacket->pts;
        }
        //            解碼  mp3   編碼格式frame----pcm   frame
        LOGE("解碼")
        avcodec_decode_audio4(agrs->codec, avFrame, &gotframe, avPacket);
        if (gotframe) {

            swr_convert(agrs->swrContext, &agrs->out_buffer, 44100 * 2, (const uint8_t **) avFrame->data, avFrame->nb_samples);
//                緩衝區的大小
            size = av_samples_get_buffer_size(NULL, agrs->out_channer_nb, avFrame->nb_samples,
                                              AV_SAMPLE_FMT_S16, 1);
            break;
        }

 

回到OpenSL ES的回調函數,取到數據後將數據壓入播放器里讓他播放

        //第一針所需要時間採樣位元組/採樣率
        double time = datasize/(44100*2*2);
        //
        musicplay->clock=time+musicplay->clock;
        LOGE("當前一幀聲音時間%f   播放時間%f",time,musicplay->clock);

        (*bq)->Enqueue(bq,musicplay->out_buffer,datasize);
        LOGE("播放 %d ",musicplay->queue.size());

 

3.消費者—視頻類


這兩者的運行過程很像,我這裡就省略的說說

開啟線程

    //申請AVFrame
    AVFrame *frame = av_frame_alloc();//分配一個AVFrame結構體,AVFrame結構體一般用於存儲原始數據,指向解碼後的原始幀
    AVFrame *rgb_frame = av_frame_alloc();//分配一個AVFrame結構體,指向存放轉換成rgb後的幀
    AVPacket *packet = (AVPacket *) av_mallocz(sizeof(AVPacket));
    //輸出文件
    //FILE *fp = fopen(outputPath,"wb");


    //緩存區
    uint8_t  *out_buffer= (uint8_t *)av_mallocz(avpicture_get_size(AV_PIX_FMT_RGBA,
                                                                  ffmpegVideo->codec->width,ffmpegVideo->codec->height));
    //與緩存區相關聯,設置rgb_frame緩存區
    avpicture_fill((AVPicture *)rgb_frame,out_buffer,AV_PIX_FMT_RGBA,ffmpegVideo->codec->width,ffmpegVideo->codec->height);


    LOGE("轉換成rgba格式")
    ffmpegVideo->swsContext = sws_getContext(ffmpegVideo->codec->width,ffmpegVideo->codec->height,ffmpegVideo->codec->pix_fmt,
                                            ffmpegVideo->codec->width,ffmpegVideo->codec->height,AV_PIX_FMT_RGBA,
                                            SWS_BICUBIC,NULL,NULL,NULL);

 

獲取一幀數據

ffmpegVideo->get(packet);

 

然後從矢量里得到數據

調節視頻和音頻的播放速度

        diff = ffmpegVideo->clock - audio_clock;
//        在合理範圍外  才會延遲  加快
        sync_threshold = (delay > 0.01 ? 0.01 : delay);

        if (fabs(diff) < 10) {
            if (diff <= -sync_threshold) {
                delay = 0;
            } else if (diff >=sync_threshold) {
                delay = 2 * delay;
            }
        }
        start_time += delay;
        actual_delay=start_time-av_gettime()/1000000.0;
        if (actual_delay < 0.01) {
            actual_delay = 0.01;
        }
        av_usleep(actual_delay*1000000.0+6000);

 

播放視頻

video_call(rgb_frame);

 

釋放資源並退出線程

    LOGE("free packet");
    av_free(packet);
    LOGE("free packet ok");
    LOGE("free packet");
    av_frame_free(&frame);
    av_frame_free(&rgb_frame);
    sws_freeContext(ffmpegVideo->swsContext);
    size_t size = ffmpegVideo->queue.size();
    for (int i = 0; i < size; ++i) {
        AVPacket *pkt = ffmpegVideo->queue.front();
        av_free(pkt);
        ffmpegVideo->queue.erase(ffmpegVideo->queue.begin());
    }
    LOGE("VIDEO EXIT");
    pthread_exit(0);

 

結束了,以後哎,儘量自己寫出一個播放器,要那種暫停不卡的

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • problem: Write a SQL query to rank scores. If there is a tie between two scores, both should have the same ranking. Note that after a tie, the next ra ...
  • 1. 安裝 1.1. 下載spark安裝包 下載地址spark官網:http://spark.apache.org/downloads.html 這裡我們使用 spark-1.6.2-bin-hadoop2.6版本. 1.2. 規劃安裝目錄 /opt/bigdata 1.3. 解壓安裝包 tar - ...
  • 1. 配置系統環境 主機名,ssh互信,環境變數等 本文略去jdk安裝,請將datanode的jdk安裝路徑與/etc/hadoop/hadoop-evn.sh中的java_home保持一致,版本hadoop2.7.5 修改/etc/sysconfig/network 然後執行命令hostname ...
  • Redis簡介Redis 是完全開源免費的,遵守BSD協議,是一個高性能的key-value資料庫。Redis 與其他 key - value 緩存產品有以下三個特點:Redis支持數據的持久化,可以將記憶體中的數據保存在磁碟中,重啟的時候可以再次載入進行使用。Redis不僅僅支持簡單的key-val ...
  • 1. 儲存引擎的概念 儲存引擎(儲存引擎也可以成為表類型)其實就是如何儲存數據,如何為儲存的數據建立索引和如何更新,查詢數據等技術的實現方法。mysql中的數據用各種不同的技術儲存在文件(或記憶體)中。這些技術中的每一種技術都使用不同的儲存機制,索引技巧,鎖定水平並且最終提供廣泛的,不同的功能和能力, ...
  • 例如查詢昨日新註冊用戶,寫法有如下兩種: register_time欄位是datetime類型,轉換為日期再匹配,需要查詢出所有行進行過濾。而第二種寫法,可以利用在register_time欄位上建立索引,查詢極快! 附上日期轉換函數 ...
  • mysql 中 innoDB 與 MyISAM 的特點 --ENGINE = innodb 1.提供事務處理,支持行鎖; 2.不加鎖讀取,增加併發讀的用戶數量和空間; 3. insert/update 優秀,不支持全文索引; 4.支持事務。 --ENGINE = myisam 1.mysql預設值 ...
  • 首先,在小程式中,是沒有DOM這個概念的,所以在數據綁定這方面,小程式和Vue是一個思想的,即數據優先。 綁定的方法其實非常之簡單,在Vue中,我們用{{ }}來做數據的單向綁定,等同於v-html,即腳本js指向html。在小程式中同樣如此,用{{ }}表示單向數據綁定,表示從js指向wxml。在 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...