一年ぶりー(^o^)/にWebカメラからh264動画を取得した〜い〜その4〜を(汗)
途中まではその3を書いたついでに書いてあったんやけど、放置している間に意外とアクセス数が増えとったんで^^;
まずはじめに1年経つ間に判明した新事実を。その2でこんなことを書きました。
Logicoolの専用ドライバを入れたPCでしかアクセス出来ない悪いやつなのか、いえいえそんな事はありません。実はUVC規格でのH.264対応方法には2種類あるのです。
- YUY2やMJPEGと同様にフォーマットディスクリプタとフレームディスクリプタを使う方法
UVC1.5規格でのH.264対応- MJPEGのペイロードに埋め込んで転送する方法
UVC1.1規格でのH.264対応
はい、これは正確ではありませんでしたm(_ _)m。
- YUY2やMJPEGと同様にフォーマットディスクリプタとフレームディスクリプタを使う方法
UVC1.5規格でのH.264対応…例えばリコーのTHETA Sがこのタイプです。 - YUY2やMJPEGと同様にフォーマットディスクリプタとフレームディスクリプタを使う方法
UVC1.1規格のフレームベースフォーマットでのH.264対応…例えばロジクールのC920rがこのタイプです。 - MJPEGのペイロードに埋め込んで転送する方法
UVC1.1規格でのH.264対応…例えばロジクールのC930eがこのタイプです。
つまりUVC規格でのh.264対応は3通りありました。前回までに載せたのはこのうち一番古くからある3番めの方法です。
備忘として前回(っていつやねん)も載せましたがMJPEGペイロードに多重化されたH.264映像の取得手順をもう一度載せておきます。
MJPEGペイロードに多重化されたH.264映像の取得手順
- カメラと接続(ファイルオープン)する
- デバイスディスクリプタを解析。フォーマットディスクリプタとフレームディスクリプタからカメラが対応している映像フォーマット、解像度、フレームインターバル(フレームレート)等を取得する
- デバイスディスクリプタを解析。エクステンションユニットディスクリプタが存在する場合にはそれがH.264エクステンションユニットかどうかをguidExtensionCodeを使って確認する。
- H.264エクステンションユニットが存在する場合には使用可能なH.24 configurationを問い合わせる
- H.264エクステンションユニットへネゴシエーションを行う
これによってMJPEGペイロードにH.264ペイロードが多重化されて転送されてくるようになります - 使用したい条件(MJPEG)をカメラとネゴシエーションする
- カメラから映像(MJPEG)を受け取る
- MJPEGペイロードを解析してH.264ペイロードを取り出す
取り出したH.264ペイロードはよきにはからいたもうれ。表示するならごにょごにょしてからMediaCodecのデコーダーに放り込んでSurfaceへ描画させればOKです - 必要なだけ映像を受け取ったらカメラからの映像ストリームを停止する
- H.264エクステンションユニットの設定をクリアする
- カメラから切断(ファイルクローズ)する
前回はUVC(1.1)機器がH.264に対応しているかどうか、また対応している場合の設定取得について書きました。
今回は取得した情報を使ってH.264エクステンションユニットとネゴシエーションを行う方法についてです。
H.264エクステンションユニットとネゴシエーションを行う方法
すること自体はプルーブ&コミットクエリというおなじみに手順ですが、これはっきり言って超めんどいです。1年も記事を書かずに放置したのはやることも説明することも超めんどいからですm(__)m
めんどいのでコードを載せてごまかすことにします(*ノω・*)テヘ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
#define MUX_ENABLED 0x01 // 外部ストリームを有効にするかどうかのビットマスク #define MUX_H264 0x02 // 外部ストリームとしてh264ストリームが多重化されている #define MUX_YUV2 0x04 // 外部ストリームとしてYUV2ストリームが多重化されている #define MUX_NV12 0x08 // 外部ストリームとしてNV12ストリームが多重化されている #define MUX_CONTAINER_MJPEG 0x40 // MJPEGストリームを単なるコンテナにするかどうか(onなら有効な映像データではなくなる) int UVCStream::find_stream_aux(const raw_frame &frame_type, const uint32_t &width, const uint32_t &height, const float &min_fps, const float &max_fps, const float &bandwidth_factor) { ENTER(); bStreamMuxOption = 0; if ((frame_type && 0xffff) != UVC_VS_FRAME_H264) RETURN(USB_ERROR_NOT_SUPPORTED, int); std::vector<configh264 *> *configs = descriptor->get_h264_configs(); if (!configs) RETURN(USB_ERROR_NOT_SUPPORTED, int); LOGD("h264 Configの数:%d", configs->size()); // ここで見つかるのはh264が多重化されている時だけ。 // h264が単独のインターフェースに実装されている時は通常のfind_streamで見つかるはず int result = USB_ERROR_NOT_SUPPORTED; for (auto iter: *configs) { result = iter->find_stream(currentH264Config, currentH264Config, width, height, min_fps, max_fps, bandwidth_factor); // c930eだとbStreamMuxOption=0x06なのでyuy2とh.264が多重化されている if (!result && (currentH264Config.bStreamMuxOption & MUX_H264)) { // h.264ストリームが多重化されている時 // h264のみを選択して外部ストリームを有効にする, MJPEGストリームデータ無し(コンテナとして使うだけ)が可能なら有効にする bStreamMuxOption = (currentH264Config.bStreamMuxOption & 0xf3) | MUX_ENABLED | MUX_CONTAINER_MJPEG; aux_host_frame_type = RAW_FRAME_MJPEG; // 常にMJPEG用の映像インターフェースを使う bAuxUnitNumber = iter->getUnitId(); if (currentH264Config.dwFrameInterval > 333333) { currentH264Config.dwFrameInterval = 333333; // 30fps } result = setup_h264_config(bAuxUnitNumber, bStreamMuxOption, width, height, false); // プルーブクエリ if (!result && (currentH264Config.wWidth) && (currentH264Config.wHeight)) { // 多重化されているのでh264と多重化されているストリームインターフェースに対して通常のfind_streamも行う result = find_stream(aux_host_frame_type, width, height, min_fps, max_fps, bandwidth_factor); if (result) { LOGW("多重化されているストリームが見つからない:err=%d", result); } } else { LOGW("プローブクエリ失敗:err=%d", result); } currentH264Config.dwFrameInterval = currentControl.dwFrameInterval; result = probe_ctrl_aux(bAuxUnitNumber, currentH264Config); if (!result && (currentH264Config.wWidth) && (currentH264Config.wHeight)) { LOGD("found"); this->bandwidth_factor = bandwidth_factor; goto found; } } } if (result) { LOGW("specific stream not found:result=%d", result); } found: RETURN(result, int); } </configh264> |
※currentH264Configは前回取得したuvcx_video_config_probe_commit_t構造体です。
ここでキーとなるのはbStreamMuxOptionとbAuxUnitNumberです。
bStreamMuxOptionはuvcx_video_config_probe_commit_tのフィールドで、H.264エキステンションユニットからの出力をどのようにして他のフレームデータ(MJPEGフレームデータ)と多重化させるかを指定します。これはネゴシエーション(プルーブ/コミットクエリ)中に必要になるのと、後でフレームデータを受け取った際に多重化されているかどうかをチェックして分岐させる時に使うので別途保持しておきます。
bAuxUnitNumberはH.264エクステンションユニットのユニットIDです。
まぁあえて別関数にするほどではないですが、H.264エクステンションユニットとのプルーブ/コミットクエリの部分はこんな感じにしています。
最終的に行き着く先はdevice->query_configを呼ぶだけなのでMJPEGストリームの場合と同じですが、MJPEGの場合にはMJPEGストリーミングインターフェースのIDを使ってネゴシエーションしますがその代わりにH.264エクステンションユニットのユニットIDを使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
/** * 外部映像ストリーム用プローブクエリ * 今はh264のみ */ /*protected*/ int UVCStream::probe_ctrl_aux(const uint8_t &unit_no, uvcx_video_config_probe_commit_t &_config) { ENTER(); LOGV("プローブクエリ:REQ_SET_CUR"); int err = device->query_config(_config, unit_no, true, REQ_SET_CUR); // probe query if (UNLIKELY(err)) { LOGE("query_ctrl(UVC_SET_CUR):err=%d", err); RETURN(err, int); } LOGV("プローブクエリ:REQ_GET_CUR"); err = device->query_config(_config, unit_no, true, REQ_GET_CUR); // probe query if (UNLIKELY(err)) { LOGE("query_ctrl(UVC_GET_CUR):err=%d", err); RETURN(err, int); } RETURN(USB_SUCCESS, int); } /** * 外部ストリームからの映像ストリーム用コミットクエリ */ /*protected*/ inline int UVCStream::commit_ctrl_aux(const uint8_t &unit_no, uvcx_video_config_probe_commit_t &_config) { ENTER(); int err = device->query_config(_config, unit_no, false, REQ_SET_CUR); if (UNLIKELY(err)) { LOGE("query_ctrl(UVC_SET_CUR):err=%d", err); RETURN(err, int); } // コミットクエリはREQ_SET_CURのみ使用可 RETURN(USB_SUCCESS, int); } /*protected*/ int UVCStream::setup_h264_config(const uint8_t &unit_no, const uint8_t &bStreamMuxOption, const uint32_t &width, const uint32_t &height, const bool &commit) { ENTER(); int result = USB_ERROR_NOT_SUPPORTED; currentH264Config.bStreamMuxOption = bStreamMuxOption; currentH264Config.bUsageType = 0x01; // realtime; // c930eはConstant QPにするのが一番いいみたい。APP4マーカーを見つけられない率が QP < CBR << VBRな感じ // CBRだと転送するデータが多すぎてどこかでフレームが飛んでいるのかも // 1280x720の場合CBRだと100KB弱/フレームなのがQPだと2〜4KB/フレームになる currentH264Config.bRateControlMode = 0x03; // 0x01: CBR, 0x02: VBR, 0x03: Constant QP currentH264Config.wIFramePeriod = 1000; // 1秒 currentH264Config.wSliceMode = 0; currentH264Config.wWidth = width; currentH264Config.wHeight = height; currentH264Config.bmHints = 0x8001; // 解像度,wIFramePeriodは固定, bRateControlMode, dwFrameIntervalは変更可 if (commit) { LOGD("コミットクエリ:REQ_SET_CUR"); result = commit_ctrl_aux(unit_no, currentH264Config); } else { LOGD("プローブクエリ:REQ_SET_CUR"); result = probe_ctrl_aux(unit_no, currentH264Config); } RETURN(result, int); } |
どやっ、参ったか^^
H.264エキステンションユニットに対するプルーブクエリと、対応するMJPEGストリームインターフェースに対するプルーブクエリの両方が成功すれば多重化H.264ストリームのネゴシエーションが半分ほど終わったことになります。
後はH.264エキステンションユニットに対するコミットクエリと、対応するMJPEGストリームインターフェースに対するコミットクエリを行って両方が成功すればいよいよフレームデータの受信です。
と言っても受信自体はMJPEGでアイソクロナス転送のペイロード受信するのと同じなので省略です。
違うのは、保存しておいたbStreamMuxOptionをチェックして多重化されていればMJPEGのフレームデータから多重化されているペイロードを抜き出す処理です。
MJPEGフレームデータに多重化されたH.264ペイロードを抜き出す
多重化されたH.264ペイロードはMJPEGのAPP4マーカーの直後のセグメントに入っています。なので最初はlibjpeg/libjpeg-turboのマーカーコールバックを試してみました…がうまく動きませんでした(´・ω・`)。設定が間違っているのかそもそもマーカーコールバックが呼ばれんのです。
という事でオレオレ実装してしまいました。
先ずはJPEGフレームデータからJFIFマーカーを探す処理。まぁこんな感じ。
線形探索するしかないのがボトルネックなのですが、JFIFの規格上APPxマーカーは必ずSOSマーカーよりも前に存在するのでSOSマーカー以降はスキップすることで速度向上を図っています。なお、MUX_CONTAINER_MJPEGに対応したカメラ(…が有るかどうかは知りませんが)であれば、MJPEG関係のフラグメントは殆ど入っていないはずなのでもっと速く探索できるかもしれません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
/** * JPEGデータから指定したJFIFマーカーを探してマーカー直後のデータへのポインタを返す * 見つからなければNULLを返す */ static const uint8_t *find_app_marker(const uint8_t &marker, const uint8_t *data, const uint32_t &size, uint32_t &len) { // ENTER(); uint8_t *result = NULL; // JFIFマーカーは2バイト(0xff+1バイト) // ffd0-ffd7(リスタートマーカー)とffd8(SOI), ffd9(EOI)はデータ部無し // それ以外のマーカーはマーカー直後の2バイトがデータ部のサイズが入る(ビッグエンディアン)。 // 実際のデータはデータ部のサイズ-2バイト for (int i = 0; i < size - 1; i++) { if (data[i] == 0xff) { LOGV("JFIFマーカーかも"); const uint8_t next = data[i + 1]; if (next == marker) { LOGV("指定したマーカーかも"); if (i + 4 >= size) { LOGD("not enough data space"); continue; } // データ部の長さを読み取る(2バイト), これはJPEGのデータなのでビッグエンディアン uint32_t data_len = (uint32_t)betoh16(*((uint16_t *)(data + i + 2))); if (i + data_len + 2 >= size) { // セグメントがデータの終わりよりも長いのはおかしい LOGD("not enough data space"); continue; } result = (uint8_t *)data + i; len = data_len; LOGV("指定したマーカーが見つかった:%p,len=%d,%s", result, data_len, bin2hex(result, data_len < 64 ? data_len : 64).c_str()); break; } else if (next == 0xda) { // APPxマーカーは必ずSOSマーカーよりも前に存在するのでSOSマーカー以降は解析しない LOGI("found SOSマーカー"); break; } else { switch (next) { // ここはこんな無駄な書き方せんと0xd0-0xd9にしとけばええで case 0xd0: // RST0 case 0xd1: // RST1 case 0xd2: // RST2 case 0xd3: // RST3 case 0xd4: // RST4 case 0xd5: // RST5 case 0xd6: // RST6 case 0xd7: // RST7 case 0xd8: // SOI case 0xd9: // EOI i += 2; // データ部の無いマーカーの場合 break; default: if (i + 4 >= size) { LOGD("not enough data space"); continue; } // データ部の長さを読み取る(2バイト), これはJPEGのデータなのでビッグエンディアン const uint32_t data_len = (uint32_t)betoh16(*((uint16_t *)(data + i + 2))); if (i + data_len + 2 >= size) { // セグメントがデータの終わりよりも長いのはおかしい LOGD("not enough data space"); continue; } i += (data_len + 2); break; } } } } finish: return result; // RETURN(result, const uint8_t *); } |
このfind_app_marker関数にMJPEGペイロードとAPP4マーカーコード(0xe4…実際のAPP4マーカーは0xff + 0xe4)を渡して呼び出して、APP4マーカーが見つかれば次のようにします。
- 一番最初のAPP4マーカーの時
- 最初に見つかったAPP4マーカーの直後にペイロードヘッダーがついているはずですのでそれを取り出します
- ペイロードヘッダーに含まれるFOURCCをチェックしてH.264かどうかを確認します
- APP4マーカーとペイロードヘッダー分を差し引いてセグメント内のデータをコピーします
- 2番目以降のAPPマーカーの場合はAPP4マーカー分を差し引いた残りをコピーします
コードにするとこないなことに。APP4マーカーは1つもMJPEGフレームデータ内に1つかもしれませんし複数存在するかもしれませんので、ループ中で繰り返し呼ぶことになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/** * フレームデータ内に埋め込まれた別のストリームデータを取り出す * 規格上は他の組み合わせもあるけど、MJPEGフレームに埋め込まれた最初のh264フレームデータのみに対応 */ int VideoFrame::extractAuxFrame(VideoFrame &dest, const raw_frame_t &target_frame_type) { ENTER(); int ret = USB_ERROR_NOT_FOUND; if (LIKELY(frame_type == RAW_FRAME_MJPEG)) { dest.clear(); const uint8_t *data = &frame[0]; const uint32_t size = frame.size(); const uint8_t *app4, *payload; uint32_t marker_len, copy_len, total_bytes = 0; uvc_payload_header_mux_t header; header.dwPayloadSize = 0; raw_frame_t frame_type = RAW_FRAME_UNKNOWN; bool first_app4 = true; for (int i = 0; i < size; i++) { if (app4 = find_app_marker(APP_MARKER_4, &data[i], size - i, marker_len)) { payload = NULL; if (first_app4) { // 最初に見つかったAPP4マーカーの後にヘッダーが付いているはず if (!getHeader(app4 + 4, marker_len - 2, header)) { frame_type = checkFOURCC((uint8_t *)&header.dwStreamType, 4); if (frame_type != target_frame_type) { continue; } first_app4 = false; LOGD("h264ペイロードが見つかった\(^o^)/"); payload = app4 + 4 + header.wLength + 4; copy_len = marker_len - 2 - header.wLength - 4; if (dest.resize(header.dwPayloadSize) < header.dwPayloadSize) { return USB_ERROR_NO_MEM; } dest.clear(); } else { continue; } } else if (marker_len > 4) { payload = app4 + 4; copy_len = marker_len - 2; } // ここでペイロードを取り出す LOGV("total_bytes=%d,dwPayloadSize=%d,copy_len=%d", total_bytes, header.dwPayloadSize, copy_len); appendAux(dest, payload, copy_len); total_bytes += copy_len; if (total_bytes >= header.dwPayloadSize) { // 本当は複数同時ストリーミングがあるので次のストリーム用に初期化するほうがいいのかも break; } i += marker_len - 1; // for文で1バイトインクリメントするのを考慮して-1しておく } else { // APP4マーカーが見つからなかった時 break; } } LOGV("total_bytes=%d,dwPayloadSize=%d", total_bytes, header.dwPayloadSize); if (LIKELY(dest.size() && (total_bytes == header.dwPayloadSize))) { if (dest.resize(total_bytes) >= total_bytes) { dest.sequence = sequence; dest.setFormat(header.wWidth, header.wHeight, target_frame_type); LOGV("ペイロード:%s", bin2hex(&dest[0], total_bytes < 64 ? total_bytes : 64).c_str()); ret = USB_SUCCESS; } else { ret = USB_ERROR_NO_MEM; } } else { LOGD("ペイロードの長さが違う!total_bytes=%d,dwPayloadSize=%d", total_bytes, header.dwPayloadSize); dest.clear(); } } RETURN(ret, int); } |
これでH.264ペイロードを抜き出すことが出来ました。ちなみにペイロードヘッダーはこのようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 |
// MJPEGフレーム中に埋めこまれたペイロードのヘッダー定義 typedef struct uvc_payload_header_mux { uint16_t wVersion; uint16_t wLength; uint32_t dwStreamType; uint16_t wWidth; uint16_t wHeight; uint32_t dwFrameIntervals; uint16_t wDelay; uint32_t dwPresentationTime; // 規格上はここまでがペイロードヘッダー uint32_t dwPayloadSize; // こっちは本来はペイロード自体に含まれるけどここに含めといたほうが使い勝手がいいので } __attribute__((__packed__)) uvc_payload_header_mux_t; |
多重化されている可能性のあるFOURCCは次の通り。みたまんまやな。それぞれが、「H.264エクステンションユニットとネゴシエーションを行う方法」のところのコードで#defineしてあるフラグに対応しています。
1 2 3 |
static const uint8_t FOURCC_H264[] = { 'H', '2', '6', '4' }; static const uint8_t FOURCC_YUY2[] = { 'Y', 'U', 'Y', '2' }; static const uint8_t FOURCC_NV12[] = { 'N', 'V', '1', '2' }; |
これでとりあえずは多重化されているフレームデータを取り出せたんで、このままネットに投げるとかなら簡単やねんけど、表示しようとか録画しようとかとなると、まだまだ色々する必要があります。
続く…かも
(^.^)/~~~