ステートメント
コードを書くのは簡単ではありません!
ディレクトリ
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のデカプセル化とカプセル化プロセスは、比較的単純な、主にビデオ編集の背面には、エンコード、パッケージ化の準備ができての使用です。