はじめに
この記事はATOMTechのATOMCamをhackしているatomcam_toolsの内部処理のうちtimelapseの部分に関して記述したものです。
atomcam_toolsに関しては下記を参照してください。
ATOMCamのVer upに伴いメモリ容量が厳しくなってきており、atom_tools のtimelapseの終了時のH.264からMP4への変換処理の負荷で再起動するようになってしまいました。
ファイルが生成されないことが多々あるため、今回実装を見直してtimelapse処理を作り直しました。
すぐに検討した内容を忘れてしまいそうなので、何が問題でどう修正したのかをメモとして記述しておきます。
atomcam_toolsのtimelapse
正式版に機能があるのにtoolsでわざわざ同じ機能を用意しているのは2つ理由があります。
1つ目の理由はtimelapseを生成後NASに送って処理を自動化したかったためです。
正式版の場合はATOMCam内部に生成されるのはh264ファイルまでなので、一旦iOS/Androidのアプリで吸い上げ手動で保存しないとmp4ファイルが生成されません。
もう1つの理由は生成されるファイルの60秒という制限を無くしたかったためです。
ATOMCamの正式版のtimelapseの処理
実装しなおすにあたりAtomCam商品の動作を再確認してみました。
確認したのはATOMCam2 Ver.4.58.0.115です。
iOS/Androidのアプリからtimelapseを実行すると、 /media/mmc/time_lapse/time_Task_xxxxxxx/ (xxxxは開始時間のunix time)に record.h264 という名前のファイルに周期ごとのI-pictureをbyte stream formatのまま記録しています。
実行中のtimelapseの開始時間、終了時間、周期、ディレクトリ名は /media/mmc/time_lapse/.setup ファイルにテキストで記述されており、現在の記録枚数は /media/mmc/time_lapse/time_Task_xxxx/.start にバイナリで記録されています。
timelapseの記録処理が終わると /media/mmc/time_lapse/time_Task_xxxx/.end というからのファイルが生成され、 /media/mmc/time_lapse/.setup ファイルは削除されます。
この生成されたh264ファイルからmp4への変換処理はAtomCam内部では行われず、iOS/Androidのアプリに転送するときに行われています。
アプリ側の処理の負荷や転送時間等の問題もあるため、生成されるファイルは20fpsで最大60秒で1200frameの制限があります。
ちなみにh264ファイルのままだとPC等でそのまま開くことはできませんが、VLC Playerで見るかffmpegなどでmp4ファイルに変換すれば開けるようになります。
atomcam_tools Ver.1.x.xのtimelapse処理
toolsの内部動作ですが、ATOMCam内部のlinux上の正式版アプリであるiCamera_appの起動時のconstructorに追加する形でhookをかけてコマンドIFとtimelapse処理のthreadを動作させています。
timelapse threadは通常時はmutexでlockされて停止していますが、toolsでコマンドを投げるとtimelapse処理が定期的に動作するようになります。
ATOMCamの元々の処理と同様に /media/mmc/time_lapse/xxxxx.h264 という名前のファイルにWebUIで設定された周期でI-pictureをbyte stream formatのまま記録しています。
実行中の管理情報は/media/mmc/timelapse.infoにバイナリ形式で記録しています。
timelapseの期間が終わるとAtomCam内部でh264ファイルからmp4への変換処理を行い、 /media/mmc/time_lapse/xxxx.mp4 ファイルを生成します。
この処理はAtomCam内部で再びh264ファイルを1枚ずつ読み出し、 mp4write_video_frame というAtomCamの内部ライブラリを呼び出して変換、ファイルへの書き出しをしています。
mp4write_video_frame はかなりメモリを使う仕組みのようで、長時間のh264ファイルだとここの処理の負荷で再起動してしまいmp4ファイルが途中で途切れてしまうことが多々発生していました。
atomcam_tools Ver.2.0.0のtimelapse処理
今回timelapse記録の処理構造を全面的に見直して処理の負荷低減を行いました。
hookをかけてtimelapse処理のthreadを動作させているところまでは同じです。
timelapseの開始のコマンドを受けるとthreadが起きて、最初の初期処理として /media/mmc/time_lapse/xxxx._mp4 ファイルにMP4のmdat boxのheaderまでを記録、/media/mmc/time_lapse/xxxx.stszファイルに管理情報を記録します。(次項で説明)
timelapseの周期ごとにvideo_get_frameを呼び出して取得されるデータをbyte stream formatからNAL file formatに変換してxxxx._mp4ファイルに追記していきます。
timelapseの期間が終わると、moov boxを生成し xxxx._mp4 ファイルに追記、 xxxx.mp4 にrenameして完了します。(次項で説明)
入力の決まったtimelapseとしての定型処理なので通用する簡易的な処理ですがうまく機能しています。
修正後のコードは下記にあります。
mp4ファイルの構造
mp4のデータ構造は他の方の記事が詳しいのでここでは記述しません。
Qiita内で参考にさせていただいた記事を下記に示します。
また、mp4の元になったQuickTime Formatの説明は下記にあります。
今回timelapseとして生成するmp4ファイルの構造は下記のように再生できる最低限のboxに絞り込んでいます。
+ ftyp MP4のheader
+ mdat H264のtimelapse画像データ
+ moov MP4の管理情報
+ mvhd movie全体のtimescaleや時間の情報
+ trak
+ tkhd 画像のサイズや長さなどの情報
+ mdia
+ mdhd timescaleや再生時間などの情報
+ hdlr media handlerの情報
+ minf
+ vmhd video mediaの情報
+ dinf
+ dref data reference
+ stbl
+ stts 画像frameや枚数や周期の情報
+ stsd SPS/PPSや画像frame数、画像のサイズと解像度などの情報
+ stsc mdatの画像frameとchunkの構造の情報
+ stsz mdatの画像frameのサイズ情報
+ stco mdatのchunkのファイルオフセット情報
mp4 header部の生成
mp4のheader部ですが、今回は開始時に_mp4ファイルを作成するときに固定で下記の内容を書き込んでいます。
この中でoffset 0x00000020からの4byteにはmdat boxのサイズが入るので、frameの追記ごとに更新しています。
00000000 00 00 00 20 66 74 79 70 69 73 6f 6d 00 00 02 00 |... ftypisom....|
00000010 69 73 6f 6d 69 73 6f 32 61 76 63 31 6d 70 34 31 |isomiso2avc1mp41|
00000020 00 00 00 08 6d 64 61 74 |....mdat
mdat blockの生成
timelapseのthreadで指定された周期ごとにATOMCamの内部ライブラリのvideo_get_frameを呼び出してH.264でencodeされたデータを取得します。
このデータはbyte stream formatなので先頭から確認し、NALU(SPS), NALU(PPS), と残りのNALU(IDR frame)のスタートコードをそれぞれのNALUのサイズに置き換えてNAL file formatに変換します。
video_get_frameで取得されるデータの構造がSPS, PPS,IDR frameの順に固定されているためサイズの小さいSPS,PPSはスタートコードを確認してサイズを確定し、比較的サイズの大きなIDR frameは video_get_frame で返されるサイズからSPS,PPSを引いた値を使うことで負荷を下げています。
この変換後のデータを_mp4ファイルに追記し、終了処理のために各frameのファイル上のoffsetとsizeをxxxx.stszファイルに追記していきます。
実行中の管理情報は /media/mmc/time_lapse/xxxx.stszファイルの先頭にバイナリ形式で記録しています。こちらはtimelapseの途中で再起動してしまった時に継続処理をするために必要です。
moov blockの生成
timelapseの全周期が完了すると、stszファイルの管理情報からmdatのheaderのサイズ情報を更新し、_mp4ファイルの最後にmoov blockを生成して追記します。
上記のmp4ファイル構造の中のmoov headerからstsdの中のSPS直前までは雛形データをバイナリデータ(508bytes)として持っています。
雛形データは時間関連の情報を書き換えるoffsetがずれないようにサイズ可変であるSPS,PPS,stsc,stsz,stcoを最後に集めています。
これを一旦メモリ上に展開してmdat blockのSPS,PPSを追加します。
次にstszファイルのサイズからstsc, stsz, stcoのサイズを計算してこれらを含んでいるmoov以下stblまでの各blockのヘッダーにあるblock sizeを調整します。
timescaleや再生時間、周期などの情報は記録しているframe数などの管理情報から時間関連の情報を更新します。
ここまででメモリ上の stsc, stsz, stco データ以外のmoovデータを_mp4ファイルに追記します。
stsc, stsz, stco はframe記録時に一緒に生成しているstszファイルを読みながら_mp4ファイルに追記していきます。
stscに関してはchunkあたりのsample数を8固定にして計算を簡略化しています。
そのため、stcoも8 sampleおきにstszファイルを読み出すことで処理が簡単になっています。
moov blockの生成ではVer.1.x.xのように画像データを全部読み出すわけではなく簡単な計算とファイルの追記をしているだけなので、軽い処理にすることができています。
まとめ
正式版、atomcam_tools Ver.1.x.x、Ver.2.0.0のそれぞれのtimelapse処理の内容についてとVer.2.0.0で生成しているmp4ファイルのblock構造と生成シーケンスについて記述しました。
Ver.2.0.0ではtimelapse処理が軽くなり終了処理での再起動は発生しなくなりました。また、Ver.2.0.0ではroot fsをext4からsquash fsに変更してメモリ削減等をしています。
atomcam_toolsは正式版のカメラ内アプリの動作をできるだけ妨げないように機能を追加していますが、システム全体的にメモリが枯渇しているためiOS/Android側のアプリもtoolsのWebUIでも、不要な機能をoffに設定して使うことをお勧めします。
特にATOMSwingの方はAtomCam2に比べてiCamera_appのCPU負荷が10%程度重たいため色々と厳しいようです。