從零開始仿寫一個抖音App——基於FFmpeg的極簡視頻播放器

本文首發於微信公衆號——世界上有意思的事,搬運轉載請註明出處,不然將追究版權責任。微信號:a1018998632,交流qq羣:859640274java

GitHub地址

很久不見,最近加班比較多因此第二篇音視頻方面的文章 delay 了一週,你們多包涵哈。本文預計閱讀時間二十分鐘。linux

本文分爲如下章節,讀者能夠按需閱讀android

  • 1.FFmpeg源碼食用——Clion中編譯、修改、引用FFmpeg源碼
  • 2.FFmpeg Api食用——FFmpeg 數據結構以及官方 demo 解析
  • 3.極簡視頻播放器——寫一個基於 FFmpeg 的極簡 Android 視頻播放器

1、FFmpeg源碼食用

注意事項:c++

  • 1.須要一些 git 的知識,git中文文檔
  • 2.個人FFmpeg:我 fork 的 FFmpeg 項目,源碼的編譯已經完成,編譯的 shell 腳本在根目錄下。
  • 3.FFmpeg-learing:本文章的示例代碼
  • 4.下面代碼塊中,使用 -----代碼塊x,本文發自簡書、掘金:什麼時候夕----- 來區分各個代碼塊,該文字不屬於代碼的一部分
  • 5.下面使用 project 指代 clone 下來的 FFmpeg 項目的路徑。
  • 6.下面的操做都是基於 Mac 平臺,linux 平臺應該也能順利運行,win 平臺的話筆者實在沒時間去折騰(靠大家本身啦)。
  • 7.開始前須要安裝一些前置軟件:Clion(百度)、make(mac 能夠用 brew 裝、linux 能夠用 apt 裝

1.開始

拿到一個項目,咱們通常有兩種方式可使用它:一個是使用它編譯打包後的產物,一個是本身引用他的項目集成到本身的項目中。咱們在這一章就來說講如何食用 FFmpeg 的源碼,將咱們的代碼寫入 FFmpeg項目中,而後編譯到 android 項目中。 FFmpeg-learing,強烈建議你們依照項目代碼進行文章的閱讀。git

  • 1.首先將 FFmpeg官方項目 fork 到咱們本身的 github 上,以便之後對這個項目的修改。
  • 2.clone 本身的 FFmpeg 項目到電腦上。
  • 3.之後個人代碼修改和編譯會基於 FFmpeg 3.3.8 這個版本(這個版本好編譯一點),因此咱們須要新建一個分支 local_build_base_on_3.3.8。而後使用 git reset --hard 18c9d5d3e80dc0b47e0a260b51f5230bdd499e8b 來到 FFmpeg 的 tag 爲 n3.3.8 這個 commit 上。
  • 4.如今咱們就能夠開始編譯代碼了。編譯的流程網上不少,我就簡單說一下。
    • 1.將 project/configure 文件中 3305-3308行,這四行代碼換成代碼塊1中的代碼。
    • 2.將代碼塊2中的代碼保存爲 project/build_android.sh 文件,而後執行 ./build_android.sh 命令。
-----代碼塊1,本文發自簡書、掘金:什麼時候夕-----

# SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
# LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
# SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
# SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR) $(SLIBNAME)'

SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB) "$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
複製代碼
-----代碼塊2,本文發自簡書、掘金:什麼時候夕-----
 #!/bin/bash
# 切換到 FFmpeg 的目錄
cd /Users/whensunset/AndroidStudioProjects/KSVideoProject/ffmpeg
 # NDK的路徑,根據本身的安裝位置進行設置
export NDK=/Users/whensunset/AndroidStudioProjects/KSVideoProject/android-ndk-r14b
export SYSROOT=$NDK/platforms/android-16/arch-arm/
export TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64
export CPU=arm
 # 配置編譯後的產物放置路徑
export PREFIX=$(pwd)/android/$CPU
export ADDI_CFLAGS="-marm"
 # 建立一個方法,這個方法使用 configure 這個文件傳入一些參數來對 FFmpeg 進行編譯,可使用 configure -help 命令來對參數進行了解
function build_one
{
./configure \
    --prefix=$PREFIX \
    --target-os=linux \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    --arch=arm \
    --sysroot=$SYSROOT \
    --extra-cflags="-Os -fpic $ADDI_CFLAGS" \
    --extra-ldflags="$ADDI_LDFLAGS" \
    --cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \
    --nm=$TOOLCHAIN/bin/arm-linux-androideabi-nm \
    --enable-shared \
    --enable-runtime-cpudetect \
    --enable-gpl \
    --enable-small \
    --enable-cross-compile \
    --disable-debug \
    --disable-static \
    --disable-doc \
    --disable-asm \
    --disable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-ffserver \
    --enable-postproc \
    --enable-avdevice \
    --disable-symver \
    --disable-stripping \
$ADDITIONAL_CONFIGURE_FLAG
sed -i '' 's/HAVE_LRINT 0/HAVE_LRINT 1/g' config.h
sed -i '' 's/HAVE_LRINTF 0/HAVE_LRINTF 1/g' config.h
sed -i '' 's/HAVE_ROUND 0/HAVE_ROUND 1/g' config.h
sed -i '' 's/HAVE_ROUNDF 0/HAVE_ROUNDF 1/g' config.h
sed -i '' 's/HAVE_TRUNC 0/HAVE_TRUNC 1/g' config.h
sed -i '' 's/HAVE_TRUNCF 0/HAVE_TRUNCF 1/g' config.h
sed -i '' 's/HAVE_CBRT 0/HAVE_CBRT 1/g' config.h
sed -i '' 's/HAVE_RINT 0/HAVE_RINT 1/g' config.h
make clean
make -j8
make install
}
 ## 運行前面建立的編譯 FFmpeg 的方法
build_one
複製代碼
  • 5.不出意外的話,咱們會在 project/android/arm 看見了 includelib這兩個文件夾。
    • 1.include:瞭解 c/c++ 的同窗知道,include 文件是 c/c++ 的接口定義文件,能夠比做 java 中的接口,用來將內部 api 暴露給外部。
    • 2.lib:這裏裏面就是 android 中可使用的 so 文件了。
    • 3.咱們能夠根據 include 文件中提供的函數定義,來調用 so 文件中被暴露到外部的 api。
  • 6.上面就是咱們整個 FFmpeg 的編譯過程。

2.修改FFmpeg源碼

本小節咱們來聊聊怎麼修改 FFmpeg 的源碼,而後自動化的在咱們的 android 項目中編譯和打包。程序員

在Clion 中編輯 FFmpeg 源碼:github

  • 1.首先咱們在上面一節已經得 FFmpeg 的源碼了,此時咱們須要打開 Clion,而後點擊 import project from sources 選擇 project 文件夾,按 Clion 的默認設置將源碼導入。
  • 2.這個時候咱們會看見 Clion 會自動生成 CmakeLists.txt 的文件,裏面引入了源碼中全部可編譯的文件。
  • 3.爲了有一個乾淨的 git 項目,因此須要在 .gitignore 裏面加上一些文件的過濾。如代碼塊3
----代碼塊3,本文發自簡書、掘金:什麼時候夕-----

*.version
*.ptx
*.ptx.c
/config.asm
/config.h
.idea
/.idea
/cmake-build-debug
/android
*.log
複製代碼
  • 4.導入完成以後,你們會發現不少文件裏面會報紅,而後一些被 include 的頭文件都找不到。這個是正常現象,由於咱們有專門的腳原本編譯代碼,Clion只是做爲一個編輯器來使用,因此報紅的地方不影響咱們接下來的操做。若是你實在看不順眼的話,能夠嘗試用 Clion 的 Auto Import 快捷鍵來看見一個就糾正一個。算法

  • 5.如今咱們就能愉快的編輯 FFmpeg 的源碼了。咱們在 project/libavcodec/allcodecs.c/avcodec_register_all 這個方法裏面加一行初學者的標配 av_log(NULL, AV_LOG_DEBUG, "hello world");shell

  • 6.如今能夠修改源碼了,也有腳本能編譯源碼了,一個簡單的將 so 文件引入 android 項目的方法就是手動編譯而後拷貝 so 文件到 android 項目中。但咱們是程序員,咱們須要方便一點的方式來構建這個流程。編程

    • 1.首先咱們在 從零開始仿寫一個抖音App——音視頻開篇 這篇文章中介紹了怎樣將 so 文件引入 android 項目而後在 jni 層調用,這裏我就不一一贅述了。
    • 2.那麼此時咱們只須要在咱們須要的時候編譯 FFmpeg 的源碼,而後將生成的 so 文件替換老的 so 文件就好了。如代碼塊4
    • 3.如今有了自動編譯拷貝的腳本了,咱們須要將這個腳本在 gradle 編譯項目的時候運行。如代碼塊5,咱們將裏面的代碼放到 app moudle 的 build.gradle 文件中。
    • 4.如今只要點擊一下 run,就會發現 Gradle Console 裏面會輸出 FFmpeg 編譯時的輸出 log。至此咱們就能愉快的修改和使用 FFmpeg 的源碼了。
    ----代碼塊4,本文發自簡書、掘金:什麼時候夕-----
     #!/usr/bin/env bash
    # exit 不註釋的時候,表示 android 項目編譯的時候不須要編譯 ffmepg,註釋的時候,表示 android 項目編譯的時候要編譯 ffmpeg。
    # exit
     # 執行 FFmpeg 源碼項目中的編譯腳本
    sh /Users/whensunset/AndroidStudioProjects/KSVideoProject/ffmpeg/build_android.sh
     # 當前項目的 so 文件的存放目錄,須要改爲本身的
    so_path="/Users/whensunset/AndroidStudioProjects/KSVideoProject/FFmpeglearning/app/src/main/jni/ffmpeg/armeabi/"
     # 全部 so 文件編譯生成後的默認命名
    libavcodec_name="libavcodec-57.so"
    libavdeivce_name="libavdevice-57.so"
    libavfilter_name="libavfilter-6.so"
    libavformat_name="libavformat-57.so"
    libavutil_name="libavutil-55.so"
    libpostproc_name="libpostproc-54.so"
    libswresample_name="libswresample-2.so"
    libseacale_name="libswscale-4.so"
     # 刪除當前項目中的老的 so 文件刪除
    rm ${so_path}${libavcodec_name}
    rm ${so_path}${libavdeivce_name}
    rm ${so_path}${libavfilter_name}
    rm ${so_path}${libavformat_name}
    rm ${so_path}${libavutil_name}
    rm ${so_path}${libpostproc_name}
    rm ${so_path}${libswresample_name}
    rm ${so_path}${libseacale_name}
     # FFmpeg 源碼項目中,編譯好的 so 文件的路徑,須要改爲本身的
    build_so_path="/Users/whensunset/AndroidStudioProjects/KSVideoProject/ffmpeg/android/arm/lib/"
     # 將新編譯的 so 文件拷貝到當前項目的 so 目錄下
    cd /Users/whensunset/AndroidStudioProjects/KSVideoProject/FFmpeglearning/app
    cp ${build_so_path}${libavcodec_name} ${so_path}${libavcodec_name}
    cp ${build_so_path}${libavdeivce_name} ${so_path}${libavdeivce_name}
    cp ${build_so_path}${libavfilter_name} ${so_path}${libavfilter_name}
    cp ${build_so_path}${libavformat_name} ${so_path}${libavformat_name}
    cp ${build_so_path}${libavutil_name} ${so_path}${libavutil_name}
    cp ${build_so_path}${libpostproc_name} ${so_path}${libpostproc_name}
    cp ${build_so_path}${libswresample_name} ${so_path}${libswresample_name}
    cp ${build_so_path}${libseacale_name} ${so_path}${libseacale_name}
    複製代碼
----代碼塊5,本文發自簡書、掘金:什麼時候夕-----
// 建立一個 build_ffmpeg 的 task,其負責運行shell 腳本
task build_ffmpeg {
    doLast {
        exec {
            commandLine 'sh', '/Users/whensunset/AndroidStudioProjects/KSVideoProject/FFmpeglearning/app/build_ffmpeg.sh'
        }
    }
}

// 將 build_ffmpeg 這個 task 做爲編譯的前置任務來執行。
tasks.whenTaskAdded { task ->
    task.dependsOn 'build_ffmpeg'
}

複製代碼

2、FFmpeg 解碼

上篇文章中咱們簡單分析了一個 FFmpeg 的官方 demo。幾周過去了,目前項目中已經有五個移植成功的官方 demo了,並且都是能夠運行的。因此這一章我就來分析解碼 demo。爲最後一章寫一個簡單的 android 視頻播放器打基礎。

FFmpeg-learing:本章示例項目。

從零開始仿寫一個抖音App——音視頻開篇:上一篇文章。

1.開始

  • 1.首先項目比較簡單,入口是 MainActivity,裏面有不少按鈕,每個功能都由一個按鈕觸發。
  • 2.點擊按鈕以後,會開啓一個線程來執行相應的代碼,這裏的代碼最終會進入到 c++ 代碼中使用 FFmpeg 的 Api 來進行視頻文件的處理。
  • 3.FFmpegPlayer 這個 java 類是用來調用 c++ 代碼的類。
  • 4.player.cpp 是 native 代碼的入口。
  • 5.同窗們應該還沒忘記上一章中咱們在 FFmpeg 中添加的 log 吧。可能有些人會問,那個 log 到底在哪裏能夠看見呢?在 c/c++ 中會有一個標準輸出流的概念,Ffmpeg 的 log 都是向標準輸出流中輸出的,這個標準輸出流通常會向控制檯之類的東西里面上面打印數據,咱們能夠將這裏 log 的輸出流重定向到 android 的日誌裏面,這樣咱們就能在 Android Studio 中的 Logcat 裏面看見它了。
    • 1.首先你們看 player.cpp 文件中有代碼塊6中的代碼,這裏咱們先定義了兩個宏,宏裏面分別是 ndk 中提供的 android 的日誌打印方法,咱們將日誌的 TAG 設置爲 「FFmpeg」。後面咱們只須要在 AS 的控制檯中過濾這個字段就能看見 FFmpeg 內部輸出的日誌了。
    • 2.而後咱們定義了一個方法,這個方法咱們指望能在 FFmpeg 打印 log 以後調用,而後將 FFmpeg 打印的 log 交給這個方法,從而將 log 輸出到 android 的日誌中。
    • 3.再看代碼塊7,這個代碼在 player.cpp 中,這裏 FFmpeg 提供了 av_log_set_callback 方法,他會將咱們剛剛定義的方法做爲一個函數指針傳入 FFmpeg 中進行持有,只要 FFmpeg 進行了 log 調用,那麼就會觸發咱們在2中定義的方法,從而將 FFmpeg 的日誌輸出流,重定向到咱們的 android 日誌系統中。
    • 4.固然咱們須要在 FFmpegPlayer 中定義 native 方法,而後在 MainActivity 中進行初始化調用。
-----代碼塊6,本文發自簡書、掘金:什麼時候夕-----------
#ifndef LOG_TAG
#define LOG_TAG "FFMPEG"
#endif

#define XLOGD(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
#define XLOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

static void log_callback_null(void *ptr, int level, const char *fmt, va_list vl) {
    static int print_prefix = 1;
    static char prev[1024];
    char line[1024];

    av_log_format_line(ptr, level, fmt, vl, line, sizeof(line), &print_prefix);

    strcpy(prev, line);

    if (level <= AV_LOG_WARNING)
    {
        XLOGE("%s", line);
    }
    else
    {
        XLOGD("%s", line);
    }
}

複製代碼
-----代碼塊7,本文發自簡書、掘金:什麼時候夕-----------
extern "C"
JNIEXPORT void JNICALL Java_com_example_whensunset_ffmpeg_1learning_FFmpegPlayer_initFfmpegLog(JNIEnv *env, jobject instance) {
    av_log_set_callback(log_callback_null);
}
複製代碼

2.解碼

  • 1.下面的代碼就是解碼的代碼,你們能夠在示例項目中找到 FFMPEG_純淨的解碼器 按鈕點擊觸發這個功能。
  • 2.注意在運行以前須要在將示例項目中的 c.mpeg4 文件拷貝到手機中的 **/storage/emulated/0/av_test/ **這個目錄下。
  • 3.有個前提知識咱們須要瞭解,一個 MP4 文件解析到屏幕上須要下面這些步驟:
    • 1.解封裝:解析 Mp4 文件的結構,而後讀取文件中的數據流。
    • 2.解碼:1中的數據流是通過編碼算法壓縮的,通常有 h26四、mpeg4等等編碼方式。這一步須要將數據流的每一幀都解碼成相似圖片的形式。
    • 3.顯示:將2中解碼出來的圖像繪製到屏幕上。
  • 4.下面的代碼主要用途是將咱們傳入的 c.mpeg4 文件直接解碼成 c.yuv 這種原始圖像數據,並無解封裝的過程。
----代碼塊8,本文發自簡書、掘金:什麼時候夕-----
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern "C" {
#include "libavcodec/avcodec.h"
}

#define INBUF_SIZE 4096

static void pgm_save(unsigned char *buf, int wrap, int xsize, int ysize, const char *filename) {
    FILE *f;
    int i;

    f = fopen(filename, "w");
    fprintf(f, "P5\n%d %d\n%d\n", xsize, ysize, 255);
    for (i = 0; i < ysize; i++)
        fwrite(buf + i * wrap, 1, xsize, f);
    fclose(f);
}

static int decode(AVCodecContext *dec_ctx, AVFrame *frame, AVPacket *pkt, const char *filename) {
    char buf[1024];
    int ret;

    // 將一幀壓縮圖像傳入解碼器中
    ret = avcodec_send_packet(dec_ctx, pkt);
    if (ret < 0) {
        return ret;
    }

    while (ret >= 0) {
        // 從解碼器中取出剛剛傳入的壓縮圖像被解碼出來的圖像,avcodec_send_packet 和 avcodec_receive_frame 通常是對應的。取出數據成功後,再去取時 ret 會小於0
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret < 0) {
            return ret;
        }

        av_log(NULL, AV_LOG_DEBUG, "saving frame %3d\n", dec_ctx->frame_number);
        fflush(stdout);

        /* the picture is allocated by the decoder. no need to free it */
        snprintf(buf, sizeof(buf), "%s-%d", filename, dec_ctx->frame_number);
        // ........**
        // ........**
        // ........**
        // ........**
        // ........**
        // ........**
        // ........**
        // 如上所示,點就是咱們平時看見的一幀圖像,*是無用數據。通常來講:width指的是一行點的數量,height指的是一列點的數量,linesize[0]指的是 width + *的數量。
        // data[0]中存放數據的方式則是這樣:........**........**........**........**........**........**........**將一幀圖像平鋪。
        // 最終咱們存到文件中的數據就是這樣:........ ........ ........ ........ ........ ........ ........ 中間的空格文件中不存在,只是爲了好看一點
        pgm_save(frame->data[0], frame->linesize[0],
                 frame->width, frame->height, filename);
    }
    return 0;
}

char *decode_video(char **argv) {
    const char *filename, *outfilename;
    const AVCodec *codec;
    AVCodecParserContext *parser;
    AVCodecContext *c = NULL;
    FILE *f;
    AVFrame *frame;
    uint8_t inbuf[INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    uint8_t *data;
    size_t data_size;
    int ret;
    AVPacket *pkt;

    // 輸入和輸出文件的名稱,輸入文件是 c.mpeg4,輸出文件是 c.yuv。
    filename = argv[0];
    outfilename = argv[1];


    // 註冊全部的編解碼器
    avcodec_register_all();

    // 爲 AVPacket 進行初始化,AVPacket 用於一幀壓縮後的圖像的數據結構
    pkt = av_packet_alloc();
    if (!pkt)
        exit(1);

    // 將 inbuf 從 INBUF_SIZE 到INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE 這一段的數據都設置爲0(這確保了對損壞的MPEG流不會發生過讀)
    /* set end of buffer to 0 (this ensures that no overreading happens for damaged MPEG streams) */
    memset(inbuf + INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);

    // 根據名稱來查找某個編解碼器,這裏咱們使用輸入文件的編解碼器 mpeg4
    codec = avcodec_find_decoder_by_name("mpeg4");
    if (!codec) {
        ret = -1111;
        goto end;
    }

    // 根據編解碼器的id,來找到一個 解析器,這個解析器能夠用來解析出 mpeg4 文件流中的一幀壓縮後的數據
    parser = av_parser_init(codec->id);
    if (!parser) {
        ret = -1112;
        goto end;
    }

    // 根據編解碼器初始化 編碼器的上下文 數據結構。
    c = avcodec_alloc_context3(codec);
    if (!c) {
        ret = -1113;
        goto end;
    }

    // 打來編解碼器
    if ((ret = avcodec_open2(c, codec, NULL)) < 0) {
        goto end;
    }

    // 打開文件
    f = fopen(filename, "rb");
    if (!f) {
        ret = -1114;
        goto end;
    }

    // 初始化 AV_Frame 這個數據結構,它是用來儲存一幀解碼後的圖像的數據結構
    frame = av_frame_alloc();
    if (!frame) {
        ret = -1115;
        goto end;
    }

    // 一直循環,直到輸入文件被讀到了最後
    while (!feof(f)) {
        // 從原文件中讀取4096個字節
        data_size = fread(inbuf, 1, INBUF_SIZE, f);
        if (!data_size)
            break;

        // 4096 的字節中可能會包含多幀壓縮後的圖像,因此這裏每次解析出一幀壓縮圖像數據,而後解碼成一幀解碼後圖像數據,而後再循環,直至4096個字節被讀取完畢。
        data = inbuf;
        while (data_size > 0) {
            // 從4096個字節中以 data 做爲起點,解析出一幀壓縮圖像數據到 AV_Packet 中。返回值是壓縮幀的byte大小
            if ((ret = av_parser_parse2(parser, c, &pkt->data, &pkt->size, data, data_size,
                                        AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0)) < 0) {
                goto end;
            }
            // 將 data 移動到新的起點
            data += ret;
            // 記錄 4096 字節中剩下的可用字節大小
            data_size -= ret;

            // 若是 size 大於0表示剛剛讀取數據成功
            if (pkt->size) {
                // 將一個 pkt 包解析成一個 frame
                decode(c, frame, pkt, outfilename);
            }

        }
    }

    /* flush the decoder */
    decode(c, frame, NULL, outfilename);

    fclose(f);

    end:
    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    if (ret < 0) {
        char buf2[500] = {0};
        if (ret == -1111) {
            return (char *) "codec not found";
        } else if (ret == -1112) {
            return (char *) "parser not found";
        } else if (ret == -1113) {
            return (char *) "could not allocate video codec context";
        } else if (ret == -1114) {
            return (char *) "could not open input file";
        } else if (ret == -1115) {
            return (char *) "could not allocate video frame";
        }
        av_strerror(ret, buf2, 1024);
        return buf2;
    } else {
        return (char *) "解碼成功";
    }
}
複製代碼

3、極簡視頻播放器

最後一章就來介紹一個用 FFmpeg 解碼的極簡視頻播放器。

  • 1.首先這個視頻播放器很是簡單,簡單到啥也沒有,只是將從文件中解碼出來的圖像繪製到 surface上面。
  • 2.示例程序的使用方法是:將須要播放的視頻以 /storage/emulated/0/av_test/b.mp4,這個命名拷貝到手機中去。
  • 3.更多的信息,你們能夠看代碼,裏面都有註釋。寫的有點累了,這篇文章就到這吧:)
----代碼塊9,本文發自簡書、掘金:什麼時候夕-----
extern "C"
{
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"

};

#include <sys/time.h>
#include <unistd.h>
#include <pthread.h>
static AVFormatContext *pFormatCtx;
static AVCodecContext *pCodecCtx;
static int video_stream_index = -1;
static AVCodec *pCodec;
static int64_t last_pts = AV_NOPTS_VALUE;


static long getCurrentTime() {
    struct timeval tv;
    gettimeofday(&tv,NULL);
    return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

struct timeval now;
struct timespec outtime;
pthread_cond_t cond;
pthread_mutex_t mutex;
static void sleep(int nHm) {
    gettimeofday(&now, NULL);
    now.tv_usec += 1000 * nHm;
    if (now.tv_usec > 1000000) {
        now.tv_sec += now.tv_usec / 1000000;
        now.tv_usec %= 1000000;
    }

    outtime.tv_sec = now.tv_sec;
    outtime.tv_nsec = now.tv_usec * 1000;

    pthread_cond_timedwait(&cond, &mutex, &outtime);
}

static int open_input_file(const char *filename) {

    int ret;

    // 打開文件,確認文件的封裝格式,而後將文件的信息寫入 AVFormatContext 中 
    if ((ret = avformat_open_input(&pFormatCtx, filename, NULL, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
        return ret;
    }

    // 從 AVFormatContext 中解析文件中的各類流的信息,好比音頻流、視頻流、字幕流等等 
    if ((ret = avformat_find_stream_info(pFormatCtx, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot find stream information\n");
        return ret;
    }

    // 找到根據傳入參數,找到最適合的數據流,和該數據流的編解碼器,這裏傳入 AVMEDIA_TYPE_VIDEO 表示須要找到視頻流
    ret = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &pCodec, 0);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot find a video stream in the input file\n");
        return ret;
    }
    // 將找到的視頻流,的 index 暫存 
    video_stream_index = ret;
    
    // 根據前面找到的視頻流的編解碼器,構造編解碼器上下文 
    pCodecCtx = avcodec_alloc_context3(pCodec);
    if (!pCodecCtx)
        return AVERROR(ENOMEM);
    // 使用視頻流的信息來編解碼器上下文的參數 
    avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[video_stream_index]->codecpar);

   // 打開編解碼器 
    if ((ret = avcodec_open2(pCodecCtx, pCodec, NULL)) < 0) {
        av_log(NULL, AV_LOG_ERROR, "Cannot open video decoder\n");
        return ret;
    }

    return 0;
}

int play(JNIEnv *env, jobject surface) {
    int ret;
    char filepath[] = "/storage/emulated/0/av_test/b.mp4";
    
    // 初始化 libavformat 而後 註冊全部的 封裝器,解封裝器 和 協議。
    av_register_all();
    if (open_input_file(filepath) < 0) {
        av_log(NULL, AV_LOG_ERROR, "can not open file");
        return 0;
    }

    // 初始化兩個 儲存解碼後視頻幀 的數據結構,pFrame 表示解碼後的視頻幀,pFrameRGBA 表示將 pFrame 轉換成 RGBA 格式的 視頻幀
    AVFrame *pFrame = av_frame_alloc();
    AVFrame *pFrameRGBA = av_frame_alloc();

    // 計算格式爲 RGBA 的視頻幀的 byte 大小,視頻幀的長和寬在解封裝的時候就肯定了 
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height, 1);
    // 初始化一塊內存,內存大小就是 格式爲 RGBA 的視頻幀的大小 
    uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    // 填充 buffer 
    av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, buffer, AV_PIX_FMT_RGBA,
                         pCodecCtx->width, pCodecCtx->height, 1);

    // 因爲解碼出來的幀格式不是RGBA的,在渲染以前須要進行格式轉換
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGBA, SWS_BILINEAR, NULL, NULL, NULL);

    // 獲取native window,即surface
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);

    // 獲取視頻寬高
    int videoWidth = pCodecCtx->width;
    int videoHeight = pCodecCtx->height;

    // 設置native window的buffer大小,可自動拉伸
    ANativeWindow_setBuffersGeometry(nativeWindow, videoWidth, videoHeight,
                                     WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer windowBuffer;

    av_dump_format(pFormatCtx, 0, filepath, 0);

    // 初始化 壓縮視頻幀 的數據結構 
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));

    while (1) {
        long start_time = getCurrentTime();

        // 從視頻流中讀取出一幀 壓縮幀 
        if ((ret = av_read_frame(pFormatCtx, packet)) < 0) {
            av_log(NULL, AV_LOG_DEBUG, "can not read frame");
            break;
        }

        // 若是 壓縮幀 是從是 視頻流中讀出來的,那麼就能夠被解碼
        if (packet->stream_index == video_stream_index) {
            // 解碼 
            ret = avcodec_send_packet(pCodecCtx, packet);
            if (ret < 0) {
                av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
                break;
            }

            while (ret >= 0) {
                // 解碼 
                ret = avcodec_receive_frame(pCodecCtx, pFrame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    break;
                } else if (ret < 0) {
                    av_log(NULL, AV_LOG_ERROR,
                           "Error while receiving a frame from the decoder\n");
                }

                ANativeWindow_lock(nativeWindow, &windowBuffer, 0);

                // 將 YUV 格式的數據轉換爲 RGBA 格式的數據 
                sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,
                          pFrame->linesize, 0, pCodecCtx->height,
                          pFrameRGBA->data, pFrameRGBA->linesize);

                // 獲取stride
                uint8_t *dst = (uint8_t *) windowBuffer.bits;
                int dstStride = windowBuffer.stride * 4;
                uint8_t *src = pFrameRGBA->data[0];
                int srcStride = pFrameRGBA->linesize[0];

                // 因爲window的stride和幀的stride不一樣,所以須要逐行復制,逐行將圖像幀的數據拷貝到 Surface 的緩衝流中。
                int h;
                for (h = 0; h < videoHeight; h++) {
                    memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
                }

                // 爲了保持 40毫秒一幀,若是解碼時間很快,那麼就 sleep一下子 
                int sleep_time = 40 - (getCurrentTime() - start_time);
                if (sleep_time > 0) {
                    sleep(sleep_time);
                }

                ANativeWindow_unlockAndPost(nativeWindow);
            }
        }

        av_packet_unref(packet);
    }

    if (sws_ctx) sws_freeContext(sws_ctx);
    av_frame_free(&pFrameRGBA);
    if (pFrame) av_frame_free(&pFrame);
    if (pCodecCtx) avcodec_close(pCodecCtx);
    if (pFormatCtx) avformat_close_input(&pFormatCtx);
    if (buffer) av_free(buffer);
    return 0;
}


複製代碼

4、尾巴

又是一篇文章結尾,最近公司加班太多了,不少計劃都沒有如期進行,但願過了這個月會好一點。不須要打賞,只但願你們能多評論點贊關注,也算是對個人支持和鼓勵。下篇文章見!

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、互聯網、程序員、計算機編程。下面是個人微信公衆號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事
相關文章
相關標籤/搜索