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

聲 明

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

碼字不易,轉(zhuǎn)載請注明出處!

教程代碼:【Github傳送門

目錄

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

本文你可以了解到

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

一、前言

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

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

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

二、初始化封裝參數(shù)

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

首先,定義一個打包器 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);
};

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

// 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);

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

打開原視頻,獲取原視頻參數(shù)

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

// 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;
    }

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

    return 0;
}

初始化打包參數(shù)

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

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

接著,使用已經(jīng)初始化完畢的上下文,打開目標(biāo)存儲文件。

最后,往目標(biāo)文件中,寫入視頻頭部信息。

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

//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];

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

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

    // 打開目標(biāo)文件
    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;
}

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

三、原視頻解封裝

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

// ff_repack.h

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

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

具體實(shí)現(xiàn)如下:

// ff_repack.cpp

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

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

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

            break;
        }

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

    // 釋放資源
    Release();
}

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

四、目標(biāo)視頻封裝

增加一個 Write 方法。

// ff_repack.h

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

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

void FFRepack::Write(AVPacket pkt) {

    // 獲取數(shù)據(jù)對應(yīng)的輸入/輸出流
    AVStream *in_stream = m_in_format_cxt->streams[pkt.stream_index];
    AVStream *out_stream = m_out_format_cxt->streams[pkt.stream_index];

    // 轉(zhuǎn)換時間基對應(yīng)的 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;

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

流程很簡單,將該幀數(shù)據(jù)的 ptsdts 、 duration 進(jìn)行轉(zhuǎn)換以后,將數(shù)據(jù)寫入即可。

在寫入數(shù)據(jù)之前,先獲取了該幀數(shù)據(jù)所在的流和寫入的數(shù)據(jù)流。這是因?yàn)椋趯懭胫?,需要對?shù)據(jù)的時間進(jìn)行轉(zhuǎn)換。

FFmpeg 中的時間單位

我們知道,每一幀音視頻數(shù)據(jù)都有其對應(yīng)的時間戳,根據(jù)這個時間戳就可以實(shí)現(xiàn)對音視頻播放的控制。

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

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

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

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

當(dāng)然了,為了方便轉(zhuǎn)換 FFmpeg 為我們提供了轉(zhuǎn)換的方法,并且處理了數(shù)據(jù)的溢出和取整問題。

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

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

即:

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

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

x = a * bq / cq

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

五、釋放資源

最后,需要將之前打開的資源進(jìn)行關(guān)閉,避免內(nèi)存泄漏。

增加一個 Release 方法:

// ff_repack.h

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

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

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

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

六、調(diào)用重打包

新增 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 解封和封裝的過程,比較簡單,主要是為后面視頻編輯、編碼、封裝做好準(zhǔn)備。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容