Posted at

L-Smash を使って無圧縮動画を作成する


L-Smash とは何か

L-Smash とは mp4 及び mov コンテナを読み書きできる C99 で書かれたライブラリである。名前で見て気づいた人はわかると思うが、動画編集アプリケーションのひとつ AviUtl において事実上必須となっている L-Smash Works の土台となっている。

あくまでコンテナを読み書きするライブラリなので、音声や動画のエンコード及びデコードはできないが後述の方法により L-Smash ライブラリ単体で無圧縮動画を作成することが可能である。


どうやって格納するか

L-Smash は mp4 及び mov コンテナ両方対応しているが、そのうち無圧縮動画を作成できるのは mov コンテナなので、mov コンテナを使って動画形式を RGB24 (RGB それぞれ 8bit で格納) かつ音声形式を LPCM (Linear PCM) で入れることにする。


L-Smash を使って無圧縮動画を作成する

以下 L-Smash を使って無圧縮動画を作成する方法を手順を踏む形で解説する。解説で使う言語は C++ (C++11) であるが、C++ でなければならない要素はなく、C++ 以外の言語、たとえば Swift や Kotlin Native でも原理的には応用できるとみている。


各構造体を初期化

lsmash_root_t は外向き中身の見えない構造体のため、 lsmash_create_root で初期化する。

以下の構造体は専用の初期化関数があるのでそれで初期化する


  • lsmash_movie_parameters_t

  • lsmash_track_parameters_t

  • lsmash_media_parameters_t

lsmash_file_t はあとで初期化するためここでは nullptr を指定する。

lsmash_file_t *file = nullptr;

lsmash_root_t *root = lsmash_create_root();
lsmash_movie_parameters_t movieParameters;
lsmash_track_parameters_t audioTrackParameters;
lsmash_track_parameters_t videoTrackParameters;
lsmash_media_parameters_t audioMediaParameters;
lsmash_media_parameters_t videoMediaParameters;
lsmash_initialize_movie_parameters(&movieParameters);
lsmash_initialize_track_parameters(&audioTrackParameters);
lsmash_initialize_track_parameters(&videoTrackParameters);
lsmash_initialize_media_parameters(&audioMediaParameters);
lsmash_initialize_media_parameters(&videoMediaParameters);


トラック有効化

各トラックを有効化する。動画トラックには ISOTRACK_IN_MOVIE を付加する。movieParameters.timescale は動画のフレームレートを指定する。

uint32_t audioTrackFlags = ISOTRACK_ENABLED;

uint32_t videoTrackFlags = ISOTRACK_ENABLED | ISOTRACK_IN_MOVIE;
// ここでは 60FPS とする
movieParameters.timescale = 60;
audioTrackParameters.mode = static_cast<lsmash_track_mode>(audioTrackFlags);
videoTrackParameters.mode = static_cast<lsmash_track_mode>(videoTrackFlags);


lsmash_file_parameters_t を初期化

I/O 関連及びコールバックを管理する構造体であるが、今回は特にコールバック指定は行わずファイル上でやりとりする。原理的にはコールバック指定を用いてメモリ上で読み書きすることが可能。

lsmash_file_parameters_t fileParameters;

memset(&fileParameters, 0, sizeof(fileParameters));
fileParameters.mode = LSMASH_FILE_MODE_WRITE;
fileParameters.major_brand = ISOBRAND_TYPE_QT;


lsmash_audio_summary_t を初期化

lsmash_create_summary を呼び出して各種パラメータを設定する。 lsmash_create_summary は返る値の型が lsmash_summary_t なので、lsmash_audio_summary_t にアップキャストしている。

mov コンテナかつ LPCM の場合は lsmash_qt_audio_format_specific_flags_t の設定が必要なので、 lsmash_create_codec_specific_data を使ってlsmash_qt_audio_format_specific_flags_t を設定して lsmash_add_codec_specific_data で追加する。

lsmash_qt_audio_format_specific_flags_t の設定がないとサンプル格納時にエラーになる

lsmash_audio_summary_t *audioSummary = reinterpret_cast<lsmash_audio_summary_t *>(lsmash_create_summary(LSMASH_SUMMARY_TYPE_AUDIO));

audioSummary->sample_type = QT_CODEC_TYPE_LPCAUDIO;
// ここではサンプルレート 44100Hz チャンネル数 2 サンプルビット数 16 を指定
audioSummary->frequency = 44100;
audioSummary->channels = 2;
audioSummary->sample_size = 16;
{
lsmash_codec_specific_t *spec = lsmash_create_codec_specific_data(LSMASH_CODEC_SPECIFIC_DATA_TYPE_QT_AUDIO_FORMAT_SPECIFIC_FLAGS, LSMASH_CODEC_SPECIFIC_FORMAT_STRUCTURED);
lsmash_qt_audio_format_specific_flags_t *format = static_cast<lsmash_qt_audio_format_specific_flags_t *>(spec->data.structured);
uint32_t flags = QT_AUDIO_FORMAT_FLAG_SIGNED_INTEGER | QT_AUDIO_FORMAT_FLAG_PACKED;
format->format_flags = static_cast<lsmash_qt_audio_format_specific_flag>(flags);
lsmash_add_codec_specific_data(reinterpret_cast<lsmash_summary_t *>(audioSummary), spec);
lsmash_destroy_codec_specific_data(spec);
}


lsmash_video_summary_t を初期化

lsmash_audio_summary_t と同じように lsmash_create_summary を呼び出して各種パラメータを設定する。

lsmash_video_summary_t *videoSummary = reinterpret_cast<lsmash_video_summary_t *>(lsmash_create_summary(LSMASH_SUMMARY_TYPE_VIDEO));

videoSummary->sample_type = QT_CODEC_TYPE_RAW_VIDEO;
videoSummary->depth = QT_VIDEO_DEPTH_COLOR_24;
// ここでは幅 960 高さ 640 で指定する
videoSummary->width = 960;
videoSummary->height = 640;


ファイルを開く

準備ができたので lsmash_open_file を使ってファイルを開く。

lsmash_create_tracklsmash_add_sample_entry をそれぞれ呼び出し、トラック及びメディアパラメータを設定する。lsmash_add_sample_entry が返すサマリインデックスはサンプル追加に必須なので保持する。

videoMediaParameters.timescalemovieParameters.timescale をそのまま割り当てる。audioSummary->samples_in_frame は音源の周波数 (= audioSummary->frequency) を動画のフレームレート (= movieParameters.timescale) で割った値を、audioMediaParameters.timescale は音源の周波数を指定する。

uint32 audioTrackID = 0, videoTrackID = 0, audioSummaryIndex = 0, videoSummaryIndex = 0;

int result = lsmash_open_file(path, 0, &fileParameters);
// 成功時は 0 を返す
if (result == 0) {
file = lsmash_set_file(root, &fileParameters);
// 上のコードでは既定値を指定しているためここでの分岐は無意味だが、もし音源が必要ない場合は追加させないようにする必要がある。
// 音源がないのに音源のエントリを追加すると書き出しでエラーになるため。
if (audioSummary->frequency > 0 && audioSummary->channels > 0 && audioSummary->sample_size > 0) {
audioSummary->samples_in_frame = audioSummary->frequency / movieParameters.timescale;
audioMediaParameters.timescale = audioSummary->frequency;
audioTrackID = lsmash_create_track(root, ISOMEDIA_HANDLER_TYPE_AUDIO_TRACK);
audioSummaryIndex = lsmash_add_sample_entry(root, audioTrackID, audioSummary);
lsmash_set_track_parameters(root, audioTrackID, &audioTrackParameters);
lsmash_set_media_parameters(root, audioTrackID, &audioMediaParameters);
}
videoMediaParameters.timescale = movieParameters.timescale;
videoTrackID = lsmash_create_track(root, ISOMEDIA_HANDLER_TYPE_VIDEO_TRACK);
videoSummaryIndex = lsmash_add_sample_entry(root, videoTrackID, videoSummary);
lsmash_set_movie_parameters(root, &movieParameters);
lsmash_set_track_parameters(root, videoTrackID, &videoTrackParameters);
lsmash_set_media_parameters(root, videoTrackID, &videoMediaParameters);
}


オーディオフレームの追加

以下の手順を踏む形でフレームを追加する。オーディオの場合フレームは audioSummary->samples_in_frame 分のサンプルを持ったデータとみなす。



  • lsmash_create_sample を使ってサンプルを作成


  • lsmash_sample_alloc でサンプル格納に必要なデータを確保


  • memcpy でサンプルデータをコピー


  • sample->cts および sample->dtscurrent_frame_indexaudioSummary->samples_in_frame をかけた値を代入


  • sample->indexsummaryIndex を設定する


    • 設定し忘れると lsmash_append_sample でエラーになる




  • lsmash_append_sample でサンプルをコンテナに追加

外部から渡す必要のある引数は以下


  • data



    • audioSummary->samples_in_frame のサンプルを持つフレームのデータ

    • 16bit 符号ありのリトルエンディアンである必要がある



  • size


    • フレームデータのサイズ


    • audioSummary->samples_in_frame * audioSummary->channels * (audioSummary->sample_size / 8) でなければならない



  • current_frame_index


    • 現在のフレーム位置



lsmash_sample_t *sample = lsmash_create_sample(size);

lsmash_sample_alloc(sample, size);
memcpy(sample->data, data, size);
sample->cts = sample->dts = current_frame_index * audioSummary->samples_in_frame;
sample->index = audioSummaryIndex;
int result = lsmash_append_sample(root, audioTrackID, sample);
if (result != 0) {
// エラー処理
}


ビデオフレームの追加

オーディオフレームの追加と以下の違いがあることを除いて基本的に同じ。ビデオにおいてフレームはサンプルと同一。



  • cts 及び dts には current_frame_index を直接代入する


  • index には videoSummaryIndex を入れる

外部から渡す必要のある引数は以下


  • data


    • フレームデータ


    • RGB 形式で並ぶ必要がある



  • size


    • フレームデータのサイズ


    • videoSummary->width * videoSummary->height * 3 でなければならない



  • current_frame_index


    • 現在のフレーム位置



lsmash_sample_t *sample = lsmash_create_sample(size);

lsmash_sample_alloc(sample, size);
memcpy(sample->data, data, size);
sample->cts = sample->dts = current_frame_index;
sample->index = videoSummaryIndex;
int result = lsmash_append_sample(root, videoTrackID, sample);
if (result != 0) {
// エラー処理
}


動画の書き出し

全てのフレームを追加し終わったら lsmash_flush_pooled_samples を読んで全てのサンプルを書き出すように指示する。

lsmash_finish_movie を呼び出すとコンテナとして完全な状態で書き出される。

lsmash_flush_pooled_samples(root, videoTrackID, 0);

if (audioTrackID > 0) {
lsmash_flush_pooled_samples(root, audioTrackID, 0);
}
int result = lsmash_finish_movie(root, NULL);
if (result != 0) {
// エラー処理
}


終了処理

以下の関数を呼び出してクリーンアップする


  • lsmash_close_file

  • lsmash_cleanup_summary

  • lsmash_destroy_root

lsmash_cleanup_summarylsmash_summary_t にする必要があるのでダウンキャストする。

lsmash_close_file(&fileParameters);

lsmash_cleanup_summary(reinterpret_cast<lsmash_summary_t *>(audioSummary));
lsmash_cleanup_summary(reinterpret_cast<lsmash_summary_t *>(videoSummary));
lsmash_destroy_root(root);


その他


LSMASH_CODEC_SPECIFIC_DATA_TYPE_QT_AUDIO_CHANNEL_LAYOUT の設定

{

lsmash_codec_specific_t *spec = lsmash_create_codec_specific_data(LSMASH_CODEC_SPECIFIC_DATA_TYPE_QT_AUDIO_CHANNEL_LAYOUT, LSMASH_CODEC_SPECIFIC_FORMAT_STRUCTURED);
lsmash_qt_audio_channel_layout_t *layout = static_cast<lsmash_qt_audio_channel_layout_t *>(spec->data.structured);
layout->channelLayoutTag = QT_CHANNEL_LAYOUT_STEREO;
lsmash_add_codec_specific_data(reinterpret_cast<lsmash_summary_t *>(audioSummary), spec);
lsmash_destroy_codec_specific_data(spec);
}


エンコーダ名の設定

パスカル文字列である必要があるので、最初の1byte目に長さ、2byte目以降に実体を32byte以内に収める必要がある。

// 必須ではないがエンコーダを指定する場合は指定する

const char name[] = "some encoder";
videoSummary->compressorname[0] = static_cast<uint8_t>(sizeof(name) - 1);
memcpy(videoSummary->compressorname + 1, name, sizeof(videoSummary->compressorname) - 1);