blog

Androidサウンド開発のアップグレード:FFmpegオーディオコーデック章VI、FFmpeg単純合成MP4:ビデオ画面のカプセル化と再パッケージ化

ステートメント\n\n\n\nカタログ\n\n\n\n1, OpenGL ES4の予備知識, OpenGL EGL5の深い理解, OpenGL FBOデータバッファ\n\n1, FFmpegライブラリ...

Nov 2, 2020 · 13 min. read
シェア

ステートメント

コードを書くのは簡単ではありません!

ディレクトリ

I. Androidのオーディオとビデオのハードデコードの章:
OpenGLを使ったビデオ・スクリーンショットのレンダリング
Android FFmpegオーディオ・ビデオデコード編

この記事では

FFmpegを使用して、デコードやエンコードなしで、オーディオとビデオを単純に解凍して再パッケージ化し、編集したビデオを再エンコードしてカプセル化する方法を説明する次の記事への道を開きます。

序文

前の記事では、FFmpgビデオのデコードだけでなく、OpenGLを使用してビデオを編集し、レンダリングする方法、詳細な説明を行い、非常に重要な、エンコードして保存するために編集したビデオです。

もちろん、エンコードの方法を理解する上で、まず優れたオーディオとビデオのカプセル化の方法を理解することで、半分の労力で2倍の効果が得られます。

、Androidのネイティブ機能を使ってオーディオ/ビデオを再パッケージしました。

パッケージパラメータの初期化

エンコードされたデータを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);
};

初期化プロセスは2つのステップに分かれています:元のビデオファイルを開き、パッキングパラメータを初期化します。

// 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メソッドを見てみましょう。

IV.ターゲット・ビデオのカプセル化

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)
    }
}

処理は非常に簡単で、フレームデータのpts、dts、durationを変換した後、データを書き込みます。

データを書き込む際には、まずフレームデータがあるストリームと、書き込む先のデータストリームを取得します。これは、書き込みの際にデータの時刻を変換する必要があるためです。

FFmpegの時間単位

オーディオとビデオデータの各フレームが対応するタイムスタンプを持っていることを知っていれば、このタイムスタンプに従ってオーディオとビデオの再生を制御することができます。

FFmpegのタイムスタンプは実際の時間ではなく、特別な値です。また、FFmpegにはタイムベースという概念があり、これはFFmpegにおける時間の単位です。

タイムスタンプの値]に[タイムベース]を掛けたものが[実際の時間]となり、単位は秒です。

つまり、FFmpegのタイムスタンプ値はタイムベースによって変化します。

また、FFmpegはステージごとにタイムベースが異なり、カプセル化フォーマットも異なるため、フレームデータをカプセル化する際には、最終的な計算で得られる実際の時刻が同じになるように、それぞれのタイムベースに従って「タイムスタンプ」を変換する必要があります。

もちろん、FFmpegは簡単に変換するための変換メソッドを提供し、データのオーバーフローや丸めを処理します。

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

内部原則は単純です。

つまり:

x * cq= a * bq
= = = = = = 
x = a * bq / cq

すべてのデータフレームが読み込まれた後、av_write_trailer を使ってエンディング情報を書き込み、ファイルが完成し、ビデオが正常に再生できるようにする必要があります。

V. リソースの解放

最後に、オープン・リソースはメモリ・リークを避けるためにクローズする必要があります。

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のデカプセル化とカプセル化プロセスは、比較的単純な、主にビデオ編集の背面には、エンコード、パッケージ化の準備ができての使用です。

Read next

BeanUtilsプロパティ変換ツールの使用を推奨しない理由

プロパティコピーツールの使用は推奨しない」と申し上げましたが、変換クラスや変換メソッドを直接定義し、IDEAプラグインでget/set関数をオートフィルすることを推奨します。 まず、実際のケースでプロパティコピー用の内部コモンズパッケージBeanUtilsのパフォーマンスが悪いことに遭遇し、その後、同僚がSpring BeanUtilsのパフォーマンスが良いことに置き換えました.

Nov 2, 2020 · 4 min read