(三)Android通過ffmpeg解碼視頻

播放一個視頻,都需要經(jīng)過解封裝衔瓮、視頻解碼浊猾、音頻解碼、音視頻同步热鞍、視頻輸出顯示和音頻輸出播放等過程葫慎。
先簡單介紹下視頻解碼的流程。

image.png

解協(xié)議:
就是將流媒體協(xié)議的數(shù)據(jù)薇宠,解析為標準的相應(yīng)的封裝格式數(shù)據(jù)偷办。音視頻在?絡(luò)上傳播的時候,常常采?各種流媒體協(xié)議澄港,例如HTTP椒涯,RTMP,或是MMS等等回梧。這些協(xié)議在傳輸視?頻數(shù)據(jù)的同時废岂,也會傳輸?些信令數(shù)據(jù)。這些信令數(shù)據(jù)包括對播放的控制(播放狱意,暫停湖苞,停?),或者對?絡(luò)狀態(tài)的描述等髓涯。解協(xié)議的過程中會去除掉信令數(shù)據(jù)?只保留視?頻數(shù)據(jù)袒啼。例如,采?RTMP協(xié)議傳輸?shù)臄?shù)據(jù)纬纪,經(jīng)過解協(xié)議操作后蚓再,輸出FLV格式的數(shù)據(jù)。
解封裝:
就是將輸?的封裝格式的數(shù)據(jù)包各,分離成為?頻流壓縮編碼數(shù)據(jù)和視頻流壓縮編碼數(shù)據(jù)摘仅。封裝格式種類很多,例如MP4问畅,MKV娃属,RMVB六荒,TS,F(xiàn)LV矾端,AVI等等掏击,它的作?就是將已經(jīng)壓縮編碼的視頻數(shù)據(jù)和?頻數(shù)據(jù)按照?定的格式放到?起。例如秩铆,F(xiàn)LV格式的數(shù)據(jù)砚亭,經(jīng)過解封裝操作后,輸出H.264編碼的視頻碼流和AAC編碼的?頻碼流殴玛。
解碼:
就是將視頻/?頻壓縮編碼數(shù)據(jù)捅膘,解碼成為?壓縮的視頻/?頻原始數(shù)據(jù)。?頻的壓縮編碼標準包含AAC滚粟,MP3寻仗,AC-3等等,視頻的壓縮編碼標準則包含H.264凡壤,MPEG2署尤,VC-1等等。解碼是整個系統(tǒng)中最重要也是最復(fù)雜的?個環(huán)節(jié)鲤遥。通過解碼沐寺,壓縮編碼的視頻數(shù)據(jù)輸出成為?壓縮的顏?數(shù)據(jù),例如YUV420P盖奈,RGB等等混坞;壓縮編碼的?頻數(shù)據(jù)輸出成為?壓縮的?頻抽樣數(shù)據(jù),例如PCM數(shù)據(jù)钢坦。

利用FFmpeg進行視頻解碼

ffmpeg視頻解碼.png
這里需要說明下一些FFMpeg的重要結(jié)構(gòu)體:
AVFormatContext:解封裝功能的結(jié)構(gòu)體究孕,包含文件名、音視頻流爹凹、時長厨诸、比特率等信息;
AVCodecContext:編解碼器上下文禾酱,編碼和解碼時必須用到的結(jié)構(gòu)體微酬,包含編解碼器類型、視頻寬高颤陶、音頻通道數(shù)和采樣率等信息颗管;
AVCodec:存儲編解碼器信息的結(jié)構(gòu)體;
AVStream:存儲音頻或視頻流信息的結(jié)構(gòu)體滓走;
AVPacket:存儲音頻或視頻編碼數(shù)據(jù)垦江;
AVFrame:存儲音頻或視頻解碼數(shù)據(jù)(原始數(shù)據(jù));

具體代碼實現(xiàn)

創(chuàng)建VideoDecoder類搅方,分別實現(xiàn)init比吭、start绽族、release等3個方法。
init為初始化衩藤,對一些重要的結(jié)構(gòu)體進行復(fù)制或者指向吧慢。
start為開始解碼循環(huán),讀取幀數(shù)據(jù)
release為釋放邏輯慷彤,在代碼結(jié)束時釋放娄蔼。

int videoDecoder::init(const char *url) {
    strcpy(m_url, url);
    int result = -1;
    //1.創(chuàng)建封裝格式上下文
    m_AVFormatContext = avformat_alloc_context();
    //2.打開文件
    if (avformat_open_input(&m_AVFormatContext, m_url, nullptr, nullptr) != 0) {
        LOGE("avformat_open_input fail");
    }
    //3.獲取音視頻流信息
    if (avformat_find_stream_info(m_AVFormatContext, nullptr) < 0) {

    }
    //4.獲取視頻流索引
    m_StreamIndex = av_find_best_stream(m_AVFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (m_StreamIndex == -1) {

    }
    //5.獲取解碼器參數(shù)
    AVCodecParameters *codecParameters = m_AVFormatContext->streams[m_StreamIndex]->codecpar;
    //6.獲取解碼器
    m_AVCodec = avcodec_find_decoder(codecParameters->codec_id);
    if (m_AVCodec == nullptr) {

    }
    //7.創(chuàng)建解碼器上下文
    m_AVCodecContext = avcodec_alloc_context3(m_AVCodec);
    //將流的參數(shù)(codecParameters)復(fù)制到解碼器中,否則某些流可能無法正常解碼
    if (avcodec_parameters_to_context(m_AVCodecContext, codecParameters) != 0) {

    }
    AVDictionary *pAVDictionary = nullptr;
    av_dict_set(&pAVDictionary, "buffer_size", "1024000", 0);
    av_dict_set(&pAVDictionary, "stimeout", "20000000", 0);
    av_dict_set(&pAVDictionary, "max_delay", "30000000", 0);
    av_dict_set(&pAVDictionary, "rtsp_transport", "tcp", 0);

    //8.打開解碼器
    result = avcodec_open2(m_AVCodecContext, m_AVCodec, &pAVDictionary);
    if (result < 0) {

    }
    m_Duration = m_AVFormatContext->duration / AV_TIME_BASE * 1000;//us to ms
    //創(chuàng)建 AVPacket 存放編碼數(shù)據(jù)
    m_Packet = av_packet_alloc();
    //創(chuàng)建 AVFrame 存放解碼后的數(shù)據(jù)
    m_Frame = av_frame_alloc();
    return result;
}

void videoDecoder::release() {
//釋放資源底哗,解碼完成
    if(m_Frame != nullptr) {
        av_frame_free(&m_Frame);
        m_Frame = nullptr;
    }

    if(m_Packet != nullptr) {
        av_packet_free(&m_Packet);
        m_Packet = nullptr;
    }

    if(m_AVCodecContext != nullptr) {
        avcodec_close(m_AVCodecContext);
        avcodec_free_context(&m_AVCodecContext);
        m_AVCodecContext = nullptr;
        m_AVCodec = nullptr;
    }

    if(m_AVFormatContext != nullptr) {
        avformat_close_input(&m_AVFormatContext);
        avformat_free_context(m_AVFormatContext);
        m_AVFormatContext = nullptr;
    }
}

int videoDecoder::start() {
    int result = av_read_frame(m_AVFormatContext, m_Packet);
    while (result >= 0) {
        //過濾數(shù)據(jù),找出對應(yīng)流锚沸,符合才進一步解碼
        if (m_Packet->stream_index == m_StreamIndex) {
            //解碼視頻跋选,將packet數(shù)據(jù)交給avcodec去處理
            if (avcodec_send_packet(m_AVCodecContext,m_Packet)!=0){
                return -1;
            }
            while (avcodec_receive_frame(m_AVCodecContext,m_Frame)==0){
                //獲取到 m_Frame 解碼數(shù)據(jù),在這里進行格式轉(zhuǎn)換哗蜈,然后進行渲染并且顯示
                LOGD("m_Frame pts is %ld",m_Frame->pts);
            }
        }
        av_packet_unref(m_Packet);
        result = av_read_frame(m_AVFormatContext, m_Packet);
    }
}

測試代碼

checkPermission(object : RequestPermissionListener {
            override fun permissionAllGranted() {
                PlayLocalVideo("/sdcard/Download/v1080.mp4")
            }

            override fun permissionDenied(list: MutableList<String>) {
            }

        })

    private external fun PlayLocalVideo(url: String)
videoDecoder *vDecoder = new videoDecoder();
    const char *url = env->GetStringUTFChars(jUrl, nullptr);
    vDecoder->init(url);
    vDecoder->start();
    vDecoder->release();

測試結(jié)果

image.png
到這里為止前标,就已經(jīng)是解碼成功了。接下來實現(xiàn)如何將視頻的數(shù)據(jù)距潘,顯示在手機上炼列。顯示的方法有很多種,OpenGL音比、ANativeWindow等俭尖,這里就先用Android自帶的演示一下。

首先在布局中增加一個surfaceView

    <SurfaceView
        android:id="@+id/surfaceVideo"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toBottomOf="@id/sample_text" />

android代碼中綁定窗口和傳遞參數(shù)

    binding.surfaceVideo.holder.addCallback(this)

    override fun surfaceCreated(holder: SurfaceHolder) {
        surface = holder.surface
        PlayLocalVideo("/sdcard/Download/v1080.mp4", surface!!)
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
    }

接下來需要做的就是洞翩,如何把上面解碼得到的數(shù)據(jù)稽犁,刷到這個SurfaceView中。在實現(xiàn)之前骚亿,需要知道顯示的控件支持什么類型的數(shù)據(jù)已亥。ANativeWindow 僅支持 RGB 類型的圖像數(shù)據(jù),所以我們還需要利用 libswscale 庫將解碼后的 YUV 數(shù)據(jù)轉(zhuǎn)成 RGB 来屠。

首先在初始化的方法里虑椎,加入ANativeWindow的初始化。

    //1. 利用 Java 層 SurfaceView 傳下來的 Surface 對象俱笛,獲取 ANativeWindow
    m_NativeWindow = ANativeWindow_fromSurface(env, surface);
    //顯示窗口初始化
    ANativeWindow_setBuffersGeometry(m_NativeWindow, m_RenderWidth, m_RenderHeight,
                                     WINDOW_FORMAT_RGBA_8888);


    m_VideoWidth = m_AVCodecContext->width;
    m_VideoHeight = m_AVCodecContext->height;
    m_RGBAFrame = av_frame_alloc();
    //計算 Buffer 的大小
    int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, m_VideoWidth, m_VideoHeight, 1);
    //為 m_RGBAFrame 分配空間
    m_FrameBuffer = static_cast<uint8_t *>(av_malloc(bufferSize * sizeof(uint8_t)));
    av_image_fill_arrays(m_RGBAFrame->data, m_RGBAFrame->linesize, m_FrameBuffer, AV_PIX_FMT_RGBA,
                         m_VideoWidth, m_VideoHeight, 1);
    //獲取轉(zhuǎn)換的上下文
    m_SwsContext = sws_getContext(m_VideoWidth, m_VideoHeight, m_AVCodecContext->pix_fmt,
                                  m_VideoWidth, m_VideoHeight, AV_PIX_FMT_RGBA,
                                  SWS_BICUBIC, nullptr, nullptr, nullptr);

然后在上面讀取幀數(shù)據(jù)的地方捆姜,加入轉(zhuǎn)換和顯示刷新的邏輯

sws_scale(m_SwsContext, m_Frame->data, m_Frame->linesize, 0,
              m_VideoHeight, m_RGBAFrame->data, m_RGBAFrame->linesize);
    if(m_NativeWindow == nullptr){
        LOGE("NativeWindow is null");
    return;
    }
    ANativeWindow_lock(m_NativeWindow, &m_NativeWindowBuffer, nullptr);
    uint8_t *dstBuffer = static_cast<uint8_t *>(m_NativeWindowBuffer.bits);

    int srcLineSize = m_RGBAFrame->linesize[0];//RGBA
    int dstLineSize = m_NativeWindowBuffer.stride * 4;

    for (int i = 0; i < m_VideoHeight; ++i) {
        memcpy(dstBuffer + i * dstLineSize,
               m_FrameBuffer + i * srcLineSize,
               srcLineSize);
    }

    ANativeWindow_unlockAndPost(m_NativeWindow);

這樣就可以看到視頻的播放了。但由于是demo代碼嫂粟,很多細節(jié)都沒有處理娇未,例如實際開發(fā)的時候,需要在異線程中進行解碼星虹、還有適當?shù)卦黾覮og提示零抬、指針的處理等镊讼,避免產(chǎn)生bug。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載平夜,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者蝶棋。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市忽妒,隨后出現(xiàn)的幾起案子玩裙,更是在濱河造成了極大的恐慌,老刑警劉巖段直,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吃溅,死亡現(xiàn)場離奇詭異,居然都是意外死亡鸯檬,警方通過查閱死者的電腦和手機决侈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喧务,“玉大人赖歌,你說我怎么就攤上這事」睿” “怎么了庐冯?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長坎穿。 經(jīng)常有香客問我展父,道長,這世上最難降的妖魔是什么赁酝? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任犯祠,我火速辦了婚禮,結(jié)果婚禮上酌呆,老公的妹妹穿的比我還像新娘衡载。我一直安慰自己,他們只是感情好隙袁,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布痰娱。 她就那樣靜靜地躺著,像睡著了一般菩收。 火紅的嫁衣襯著肌膚如雪梨睁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天娜饵,我揣著相機與錄音友绝,去河邊找鬼橄唬。 笑死前弯,一個胖子當著我的面吹牛诀蓉,可吹牛的內(nèi)容都是我干的衔统。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肺魁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤隔节,失蹤者是張志新(化名)和其女友劉穎鹅经,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體怎诫,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡瘾晃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了幻妓。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片酗捌。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖涌哲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尚镰,我是刑警寧澤阀圾,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站狗唉,受9級特大地震影響初烘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜分俯,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一肾筐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧缸剪,春花似錦吗铐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至奋渔,卻和暖如春镊逝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫉鲸。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工撑蒜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓座菠,卻偏偏與公主長得像狸眼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辈灼,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容