【Android 音視頻開發打怪升級:FFmpeg音視頻編解碼篇】六、FFmpeg簡單合成MP4:視屏解封與重新封裝

聲 明

首先,這一系列文章均基于自己的理解和實踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網上也有許許多多的博文供大家學習了。
最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。

碼字不易,轉載請注明出處!

教程代碼:【Github傳送門

目錄

一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
三、Android FFmpeg音視頻解碼篇

本文你可以了解到

利用 FFmpeg 對音視頻進行簡單的解封和重新封裝,不涉及解碼和編碼,為下一篇講解如何對編輯好的視頻進行重編碼和封裝做好鋪墊。

一、前言

前面的文章中,對 FFmpg 視頻的解碼,以及如何利用 OpenGL 對視頻進行編輯和渲染,做了詳細的講解,接來非常重要的,就是對編輯好的視頻進行編碼和保存。

當然了,在了解如何編碼之前,先了解如何對編碼好的音視頻進行封裝,會有事半功倍的效果。

在《音視頻解封和封裝:生成一個MP4》中使用了 Android 的原生功能,實現了對音視頻的重打包。FFmpeg 也是同樣的,只不過流程更為繁瑣一些。

二、初始化封裝參數

我們知道,將編碼數據封裝到 Mp4 中,需要知道音視頻編碼相關的參數,比如編碼格式,視頻的寬高,音頻通道數,幀率,比特率等,下面就先看看如何初始化它們。

首先,定義一個打包器 FFRepacker

// ff_repack.h

class FFRepack {
private:
    const char *TAG = "FFRepack";

    AVFormatContext *m_in_format_cxt;

    AVFormatContext *m_out_format_cxt;

    int OpenSrcFile(char *srcPath);

    int InitMuxerParams(char *destPath);

public:
    FFRepack(JNIEnv *env,jstring in_path, jstring out_path);
};

初始化過程分為兩個步驟:打開原視頻文件、初始化打包參數。

// ff_repack.cpp

FFRepack::FFRepack(JNIEnv *env, jstring in_path, jstring out_path) {
    const char *srcPath = env->GetStringUTFChars(in_path, NULL);
    const char *destPath = env->GetStringUTFChars(out_path, NULL);

    // 打開原視頻文件,并獲取相關參數
    if (OpenSrcFile(srcPath) >= 0) {
        // 初始化打包參數
        if (InitMuxerParams(destPath)) {
            LOGE(TAG, "Init muxer params fail")
        }
    } else {
        LOGE(TAG, "Open src file fail")
    }
}

打開原視頻,獲取原視頻參數

代碼很簡單,在使用 FFMpeg 解碼的文章中就已經講解過。如下:

// ff_repack.cpp

int FFRepack::OpenSrcFile(const char *srcPath) {
    // 打開文件
    if ((avformat_open_input(&m_in_format_cxt, srcPath, 0, 0)) < 0) {
        LOGE(TAG, "Fail to open input file")
        return -1;
    }

    // 獲取音視頻參數
    if ((avformat_find_stream_info(m_in_format_cxt, 0)) < 0) {
        LOGE(TAG, "Fail to retrieve input stream information")
        return -1;
    }

    return 0;
}

初始化打包參數

初始化打包參數稍微復雜一些,主要過程是:

查找原視頻中有哪些音視頻流,并為目標視頻(即重打包視頻文件)添加對應的流通道和初始化對應的編碼參數。

接著,使用已經初始化完畢的上下文,打開目標存儲文件。

最后,往目標文件中,寫入視頻頭部信息。

代碼如下, 主要流程請查看注釋:

//ff_repack.cpp

int FFRepack::InitMuxerParams(const char *destPath) {
    // 初始化輸出上下文
    if (avformat_alloc_output_context2(&m_out_format_cxt, NULL, NULL, destPath) < 0) {
        return -1;
    }

    // 查找原視頻所有媒體流
    for (int i = 0; i < m_in_format_cxt->nb_streams; ++i) {
        // 獲取媒體流
        AVStream *in_stream = m_in_format_cxt->streams[i];

        // 為目標文件創建輸出流
        AVStream *out_stream = avformat_new_stream(m_out_format_cxt, NULL);
        if (!out_stream) {
            LOGE(TAG, "Fail to allocate output stream")
            return -1;
        }

        // 復制原視頻數據流參數到目標輸出流
        if (avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar) < 0) {
            LOGE(TAG, "Fail to copy input context to output stream")
            return -1;
        }
    }

    // 打開目標文件
    if (avio_open(&m_out_format_cxt->pb, destPath, AVIO_FLAG_WRITE) < 0) {
        LOGE(TAG, "Could not open output file %s ", destPath);
        return -1;
    }

    // 寫入文件頭信息
    if (avformat_write_header(m_out_format_cxt, NULL) < 0) {
        LOGE(TAG, "Error occurred when opening output file");
        return -1;
    } else {
        LOGE(TAG, "Write file header success");
    }

    return 0;
}

以上,初始化已經完畢,下面就可以進行音視頻的解封和重新封裝了。

三、原視頻解封裝

新增一個 Start 方法,用于開啟重打包

// ff_repack.h

class FFRepack {
    // 省略其他...

public:
    // 省略其他...
    
    void Start();
};

具體實現如下:

// ff_repack.cpp

void FFRepack::Start() {
    LOGE(TAG, "Start repacking ....")
    AVPacket pkt;
    while (1) {
        // 讀取數據
        if (av_read_frame(m_in_format_cxt, &pkt)) {
            LOGE(TAG, "End of video,write trailer")

            // 釋放數據幀
            av_packet_unref(&pkt);

            // 讀取完畢,寫入結尾信息
            av_write_trailer(m_out_format_cxt);

            break;
        }

        // 寫入一幀數據
        Write(pkt);
    }

    // 釋放資源
    Release();
}

解封依然很簡單,在之前的解碼文章同樣介紹過,主要是將數據讀取到 AVPacket 中。然后調用 Write 方法,將幀數據寫入目標文件中。下面就來看看 Write 方法。

四、目標視頻封裝

增加一個 Write 方法。

// ff_repack.h

class FFRepack {
    // 省略其他...

public:
    // 省略其他...
    
    void Write(AVPacket pkt);
};
// ff_repacker.cpp

void FFRepack::Write(AVPacket pkt) {

    // 獲取數據對應的輸入/輸出流
    AVStream *in_stream = m_in_format_cxt->streams[pkt.stream_index];
    AVStream *out_stream = m_out_format_cxt->streams[pkt.stream_index];

    // 轉換時間基對應的 PTS/DTS
    int rounding = (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
    pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
                               (AVRounding)rounding);
    pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
                               (AVRounding)rounding);

    pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);

    pkt.pos = -1;

    // 將數據寫入目標文件
    if (av_interleaved_write_frame(m_out_format_cxt, &pkt) < 0) {
        LOGE(TAG, "Error to muxing packet: %x", ret)
    }
}

流程很簡單,將該幀數據的 ptsdtsduration 進行轉換以后,將數據寫入即可。

在寫入數據之前,先獲取了該幀數據所在的流和寫入的數據流。這是因為,在寫入之前,需要對數據的時間進行轉換。

FFmpeg 中的時間單位

我們知道,每一幀音視頻數據都有其對應的時間戳,根據這個時間戳就可以實現對音視頻播放的控制。

FFmpeg 中的時間戳并不是我們實際中的時間,它是一個特別的數值。并且在 FFmpeg 中,還有一個叫 時間基 的概念,時間基FFmpeg 中的時間單位。

[時間戳的值] 乘以 [時間基],才是[實際的時間],并且單位為秒。

換而言之,FFmpeg 的時間戳的值,是隨著 時間基 的不同而變化的。

FFmpeg 在不同的階段和不同的封裝格式下也有著不同的時間基,因此,在進行幀數據的封裝時,需要根據各自的時間基進行 “時間戳” 轉換,以保證最終計算得到的實際時間是一致的。

當然了,為了方便轉換 FFmpeg 為我們提供了轉換的方法,并且處理了數據的溢出和取整問題。

av_rescale_q_rnd(int64_t a, AVRational bq,AVRational cq,enum AVRounding rnd)

其內部原理很簡單:return (a × bq / cq)。

即:

x(目標時間戳值) * cq(目標時間基)= a(原時間戳值) * bq(原時間基)

=》=》=》=》=》=》

x = a * bq / cq

當所有數據幀都讀取完畢之后,需要通過 av_write_trailer 寫入結尾信息,這樣文件才算完整,視頻才能正常播放。

五、釋放資源

最后,需要將之前打開的資源進行關閉,避免內存泄漏。

增加一個 Release 方法:

// ff_repack.h

class FFRepack {
    // 省略其他...

public:
    // 省略其他...
    
    void Release();
};
//ff_repack.cpp

void FFRepack::Release() {
    LOGE(TAG, "Finish repacking, release resources")
    // 關閉輸入
    if (m_in_format_cxt) {
        avformat_close_input(&m_in_format_cxt);
    }

    // 關閉輸出
    if (m_out_format_cxt) {
        avio_close(m_out_format_cxt->pb);
        avformat_free_context(m_out_format_cxt);
    }
}

六、調用重打包

新增 JNI 接口

native-lib.cpp 中,新增 JNI 接口

// native-lib.cpp

extern "C" {

    // 省略其他 ...
    
    JNIEXPORT jint JNICALL
    Java_com_cxp_learningvideo_FFRepackActivity_createRepack(JNIEnv *env,
                                                           jobject  /* this */,
                                                           jstring srcPath,
                                                           jstring destPath) {
        FFRepack *repack = new FFRepack(env, srcPath, destPath);
        return (jint) repack;
    }
    
    JNIEXPORT void JNICALL
    Java_com_cxp_learningvideo_FFRepackActivity_startRepack(JNIEnv *env,
                                                           jobject  /* this */,
                                                           jint repack) {
        FFRepack *ffRepack = (FFRepack *) repack;
        ffRepack->Start();
    }
}

新增頁面

// FFRepackActivity.kt

class FFRepackActivity: AppCompatActivity() {

    private var ffRepack: Int = 0

    private val srcPath = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
    private val destPath = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_repack.mp4"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ff_repack)

        ffRepack = createRepack(srcPath, destPath)
    }

    fun onStartClick(view: View) {
        if (ffRepack != 0) {
            thread {
                startRepack(ffRepack)
            }
        }
    }

    private external fun createRepack(srcPath: String, destPath: String): Int

    private external fun startRepack(repack: Int)

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

以上,就是使用 FFmpeg 解封和封裝的過程,比較簡單,主要是為后面視頻編輯、編碼、封裝做好準備。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374