• スポンサードリンク

Webカメラからh264動画を取得した〜い〜その5〜

Android Camera MediaCodec NDK USB

前回の最後に

これでとりあえずは多重化されているフレームデータを取り出せたんで、このままネットに投げるとかなら簡単やねんけど、表示しようとか録画しようとかとなると、まだまだ色々する必要があります。

なぁーんて事を書いてしまったので続きを書きます^^;

せんとあかんこと

そや言うてもせんとあかんことは大きくは1つだけ。ずばり(^^)/
MediaCodecのデコーダーに突っ込んでデコードしてもらう
デコードした後はSurface経由で受け取ってSurfaceViewなりTextuewViewなりなんなりとするなり。

そんなんチョロいやんって思ったそこのあなた

そう、「MediaCodecをh.264のデコード用に初期化してByteBufferに放り込んだデータをqueueInputBufferで突っ込めばええんやろ」と思ったそこのあなたです。世の中そんなに甘くないのです。

おさらい: h.264で圧縮された動画の入ったファイルを再生する時の手順

おさらいのために、MediaCodecのデコーダー(とMediaExtractorとか)を使ってh.264で圧縮された動画の入ったファイルを再生する時の手順を書いておきましょう。

  1. MediaExtractorインスタンスを生成する
  2. MediaExtractor#setDataSourceで動画ソースをセットする
  3. MediaExtractor#getTrackCount/#getTrackFormatとかを使って映像トラックを探す
  4. 映像トラックが見つかればMediaExtractor#getTrackFormatでMediaFormatインスタンスを取得
  5. MediaCodec#createDecoderByTypeを呼び出してMediaCodecをデコーダー用に生成
  6. 先程取得したMediaFormatインスタンスと表示用のSurfaceを使ってMediaCodec#configureを呼び出して初期化する
  7. MediaCodec#startを呼び出してデコード開始
  8. MediaExtractor#readSampleDataを呼び出してフレームデータを取得
  9. 取得したフレームデータをMediaCodec#queueInputBufferへ渡す
  10. コールバック呼び出しを使うかまたは別スレッド上でMediaCodec#dequeueOutputBufferを呼び出しデコード済みのデータを取得してMediaCodec#releaseOutputBufferを呼び出す

こないな感じですね。あとは自動的に指定したSurfaceへ映像が書き込まれます。

ではいざUVCカメラからのh.264フレームデータをデコード

ってまずつまずくのはMediaCodec#configureを呼び出してデコーダー用に初期化するときでしょう。
まぁ普通は「MediaFormat#createVideoFormatで生成すりゃいいんやろ」って思うでしょう。思わんかった?
いや絶対思ったはずや。
でもMediaFormat#createVideoFormatで生成したMediaFormatをそのままMediaCodec#configureへ渡してもh.264デコード用にMediaCodecを初期化することは出来ないのですキッパリ。

以前の記事にも書きましたがh.264用のデコーダー/エンコーダー用にMediaCodecを初期化するには特別なデータが必要なのです。エンコード用に初期化する際には、MediaCodec自身が1番最初のフレームの前に
MediaCodec#dequeueOutputBufferの返り値がMediaCodec#INFO_OUTPUT_FORMAT_CHANGEDとなりこの時にMediaCodec#getOutputFormatを呼び出すことでMediaCodecエンコーダーが生成したMediaFormatインスタンスを取得することが出来ました。
また先程おさらいしたデコード用に初期化する際にはMediaExtractorが必要なデータを含んだMediaFormatインスタンスを生成してくれます。

ではUVCカメラからのh.264フレームデータをデコードする際には誰がMediaCodecを初期化するのに必要な特別なデータを含むMediaForamtインスタンスを生成してくれるのでしょう?

そりゃぁもちろんあなた(の作ったコード)です。当然でしょう。

特別なデータって何やねん

特別なデータはSPSとPPSという情報になります。SPSはSequence Parameter Setの略、PPSはPicture Parameter Setの略です。ちなみに自分はSPSと聞くとどちらかと言うと過硫酸ナトリウムを思ってしまいますが、本記事とは全く関係ありません(*ノω・*)テヘ

SPSとPPSに含まれる情報が何かは規格書を読むなりぐるぐる先生に聞いてみるなりしてもらうことにして、どないすれば生成することが出来るのでしょう?

実は…UVCカメラから届くh.264のフレームデータには既にSPSとPPSが含まれているのです\(^o^)/
なので、届いたh.264フレームデータをMediaCodecへ書き込むだけで…はダメです。MediaCodecへ書き込むためにはまずMediaFormatを使って初期化しないとダメでなので、MediaCodecのデコーダーに自分で解析してやとはいえないのです。

h.264フレームデータからSPS/PPSを取得する

前回MJPEGペイロードに多重化されたh.264フレームデータは、APP4マーカーを探すことで抜き出すことが出来ました。

実はh.264フレームデータも似たようなフォーマットになっているのです。
まず大きなフォーマットとしてUVCカメラから届くh.264フレームデータはByte Stream formatというフォーマットになっています。Annex Bと呼ばれることも多いです。

前回のMJPEGデータをパースする際にはFFxxな2バイトのJFIFマーカーを検索しました。一方、h.264の場合にはN[00]000001(N>=0)の3バイト以上がマーカー(スタートコード)となります。

指定したバイト配列がAnnex Bのマーカーで始まったいるかどうかをチェックするコードは例えばこんな感じ。

まぁ大したことはしとりません。32ビット整数とか64ビット整数とかに読み込んでパターンマッチングするとか、高速化する方法は多少はありますが、SPS/PPSは先頭についていると決まっているのでこの関数が呼ばれるのは1フレームにつき1-2回…凝ったことをしてもあまり効果がないので単純なリニア探索になっています。

無事先頭がAnnex Bマーカーから始まっている事がわかればマーカー直後にNALU(Network Abstraction Layer Unit)と呼ばれる一区切りのデータがあり、場合よってはその後に再度Annex Bマーカー+NALU…が繰り返します。
正規表現風に書くとこんな感じかな?

前の方の0とか1は1文字じゃなくて2つで1バイトやからね。

NALUの内部の構造については自分で調べてもらうことにして先頭の1バイトの下5ビットにNALUの種類が入っています。

もしかすると古いかもしれんけど、こんだけ種類があります。と言うか5ビットなんやから32種類しかあらへんけどな。

RESERVEDとUNSPECIFIEDの違いって…(。・_・。)

この全部がh.264と関係があるわけではありません。とりあえず、上の列挙型で言うとNAL_UNIT_CODEC_SLICE_IDR, NAL_UNIT_SEQUENCE_PARAM_SET, NAL_UNIT_PICTURE_PARAM_SET, NAL_UNIT_PICTURE_DELIMITERだけは特別扱いしてそれ以外は何もせずにMediaCodecのデコーダーへ突っ込みます。簡単に説明すれば、NAL_UNIT_CODEC_SLICE_IDRはI-Frame, NAL_UNIT_SEQUENCE_PARAM_SETはそのまんまSPS, NAL_UNIT_PICTURE_PARAM_SETはPPSで, NAL_UNIT_PICTURE_DELIMITERはI-Frameじゃ無いけどこれだけで1フレームを生成できるNALUの集まりの区切りなのでI-Frameとみなしても実用上は問題ありません。
もっとも自分がテストした限りではNAL_UNIT_CODEC_SLICE_IDRを送ってくるカメラはありませんでした。たいした数テストしたわけではないですが、NAL_UNIT_SEQUENCE_PARAM_SET + NAL_UNIT_PICTURE_PARAM_SETを送ってくるか、NAL_UNIT_PICTURE_DELIMITERだけがくるかのどちらかでした。

ところで御存知の通りh.264はMJPEGよりも圧倒的に高圧縮を実現しています(1/2から1/4サイズになります)。その大きな違いの1つがh.264ではフレーム間での圧縮が可能になっている点にあります。まぁぶっちゃけ1つ前の映像との差分を使ってゴニョゴニョしよるってことです。ですが差分って事はあるフレームを生成するには1つ前のフレームが必要になるということです。つまり算数での数列 [latex] a_1 = 1, a_2 = 1, a_{n+2} = a_n + a_{n+1}(n\geq 1) [/latex] みたいなやつと同じで初期値がないとそれ以降のフレームを生成することが出来ません。

h.264の場合はプロファイルといってどのような機能をサポートしているかが異なり、それによってどのようなフレームがどのような順に来るかが変わりますが、少なくとも2種類、1つはI-Frame(もしくはキーフレーム)でもう1つはP-Frame(映像の差分的なデータ)の2種類が来ます。
先程の数列での例えでわかるように、I-Frameは前のフレームに依存しておらず単独で1フレームを生成出来るデータです。1フレームを生成するにはSPSやPPSが必要になるのでI-Frameには必ずSPS/PPSがくっついています。
また、I-Frame以外は1フレーム生成するのに直前のフレームが必要=1フレームでも抜けてしまうと生成した映像が乱れるということなので、MediaCodecへ投入する1フレーム目も当然I-Frame(またはNAL_UNIT_PICTURE_DELIMITER)でなければなりません。

ということで、兎にも角にも次の3つのうち1つが来るまでずっと我慢です。

  • NAL_UNIT_CODEC_SLICE_IDR
  • NAL_UNIT_SEQUENCE_PARAM_SET + NAL_UNIT_PICTURE_PARAM_SET
  • NAL_UNIT_PICTURE_DELIMITER

ずっと我慢したかいあってI-FrameまたはI-Frame相当のフレームを受信できれば、ようやくSPS/PPSの切り出しが出来ます。

Javaだと例えばこんな感じにしてSPS/PPSを切り出してMediaFormat#setByteBufferを使ってセットします。

どや?思ってたよりも大変やったやろ?というか知らんかったらでけんやろ。

でもここまででけたら後はもう分かるよな?MP4のファイルから動画を再生するときみたいに、上で作ったMediaFormatインスタンスをMediaCodec#configureに食わせて初期化してstartしたら、データをポイポイぽーいと投入するだけやで。

でも気をつけんとあかんのは、途中でエラーフレームが来たときで、再度I-Frameが来るのを待機しないとぐちゃぐちゃな画面表示がしばらく続きます。

今日はコードの量が少ないから文字数は少ないもの、文章が長くて行数のわりに疲れたぁ(´・ω・`)

次回は意外と苦労したUVC1.5でのh.264ストリームやでぇ〜(ぶっちゃけRICOH THETA Sな)

お疲れ様でした
(^.^)/~~~

« »

  • スポンサードリンク

コメント

  • Sienna より:

    Appreciate it for this post, I am a big big fan of this web
    site would like to go along updated. https://en.gravatar.com/barrysmeithg