🛠️

RTP Payloadのキーフレーム判定方法 VP8/VP9/H264/AV1 編

2021/07/29に公開

趣旨

Chrome の WebRTC で使える代表的なコーデックの RTP Payload をパースして RTP パケットのキーフレーム判定を行う方法についてまとめます。

RTP Payload とは

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |V=2|P|X|  CC   |M|     PT      |       sequence number         |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                           timestamp                           |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |           synchronization source (SSRC) identifier            |
     +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     |            contributing source (CSRC) identifiers             |
     |                             ....                              |
     +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     |                        RTP Payload                            |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RTP Payload は RTP Header の後に続く部分のことです RTP Payload の先頭にはコーデックの制御情報が入っていて、キーフレームの情報もここに入っています。本記事では RTP Payload の先頭構造をパースしてキーフレーム情報を取り出していきます。

VP8

VP8 の RTP Payload の先頭構造の仕様は ↓ の RFC で定義されています

RFC 7741 - RTP Payload Format for VP8 Video

構造について見ていきましょう

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |V=2|P|X|  CC   |M|     PT      |       sequence number         |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                           timestamp                           |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |           synchronization source (SSRC) identifier            |
     +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     |            contributing source (CSRC) identifiers             |
     |                             ....                              |
     +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     |            VP8 payload descriptor (integer #octets)           |
     :                                                               :
     |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                               : VP8 payload header (3 octets) |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     | VP8 pyld hdr  :                                               |
     +-+-+-+-+-+-+-+-+                                               |
     :                   Octets 4..N of VP8 payload                  :
     |                                                               |
     |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                               :    OPTIONAL RTP padding       |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

これは RTP Header を含めた RTP パケット全体の構造です。
VP8 payload descriptor 以下が RTP Payload にあたります。
VP8 のキーフレーム情報は VP8 payload header に入っています

キーフレーム判定方法

VP8 payload header にアクセスする前に、VP8 payload descriptor のパースをする必要があります。まず VP8 payload descriptor のパースをしていきます。

VP8 payload descriptor

         0 1 2 3 4 5 6 7                      0 1 2 3 4 5 6 7
        +-+-+-+-+-+-+-+-+                   +-+-+-+-+-+-+-+-+
        |X|R|N|S|R| PID | (REQUIRED)        |X|R|N|S|R| PID | (REQUIRED)
        +-+-+-+-+-+-+-+-+                   +-+-+-+-+-+-+-+-+
   X:   |I|L|T|K| RSV   | (OPTIONAL)   X:   |I|L|T|K| RSV   | (OPTIONAL)
        +-+-+-+-+-+-+-+-+                   +-+-+-+-+-+-+-+-+
   I:   |M| PictureID   | (OPTIONAL)   I:   |M| PictureID   | (OPTIONAL)
        +-+-+-+-+-+-+-+-+                   +-+-+-+-+-+-+-+-+
   L:   |   TL0PICIDX   | (OPTIONAL)        |   PictureID   |
        +-+-+-+-+-+-+-+-+                   +-+-+-+-+-+-+-+-+
   T/K: |TID|Y| KEYIDX  | (OPTIONAL)   L:   |   TL0PICIDX   | (OPTIONAL)
        +-+-+-+-+-+-+-+-+                   +-+-+-+-+-+-+-+-+
                                       T/K: |TID|Y| KEYIDX  | (OPTIONAL)
                                            +-+-+-+-+-+-+-+-+
                                 Figure 2

↑ の構造をパースするコードを書きます

const buf: Buffer = RTPPayload;
let offset = 0;

const x = getBit(buf[offset], 0);
const n = getBit(buf[offset], 2);
const s = getBit(buf[offset], 3);
const pid = getBit(buf[offset], 5, 3);

offset++;

if (x === 1) {
  const i = getBit(buf[offset], 0);
  const l = getBit(buf[offset], 1);
  const t = getBit(buf[offset], 2);
  const k = getBit(buf[offset], 3);

  offset++;

  if (i === 1) {
    const m = getBit(buf[offset], 0);

    if (m === 1) {
      const _7 = paddingByte(getBit(buf[offset], 1, 7));
      const _8 = paddingByte(buf[offset + 1]);
      const pictureId = parseInt(_7 + _8, 2);
      offset += 2;
    } else {
      const pictureId = getBit(buf[offset], 1, 7);
      offset++;
    }
  }
}

VP8 payload descriptor には OPTIONAL な要素があるので VP8 payload header にアクセスするためには動的にパースする必要があります

VP8 payload header

                             0 1 2 3 4 5 6 7
                            +-+-+-+-+-+-+-+-+
                            |Size0|H| VER |P|
                            +-+-+-+-+-+-+-+-+
                            |     Size1     |
                            +-+-+-+-+-+-+-+-+
                            |     Size2     |
                            +-+-+-+-+-+-+-+-+
                            | Octets 4..N of|
                            | VP8 payload   |
                            :               :
                            +-+-+-+-+-+-+-+-+
                            | OPTIONAL RTP  |
                            | padding       |
                            :               :
                            +-+-+-+-+-+-+-+-+

VP8 payload header は VP8 payload descriptor の後ろにあります。

P ビットフラグは逆キーフレームフラグという名前がつけられており、名前の通りキーフレームなら 0 が入っているビットフラグです。

先程のコードの最後に ↓ を付け足せば、キーフレームの判定が出来ます

if (s === 1 && pid === 0) {
  const size0 = getBit(buf[offset], 0, 3);
  const h = getBit(buf[offset], 3);
  const ver = getBit(buf[offset], 4, 3);
  const p = getBit(buf[offset], 7);

  if (p === 0) {
    console.log("このペイロードはキーフレームです");
  }
}

リンク

VP9

VP9 の RTP Payload の先頭構造の仕様は ↓ の Draft で定義されています

RTP Payload Format for VP9 Video draft-ietf-payload-vp9-16

構造について見ていきましょう

      0                   1                   2                   3
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |V=2|P|X|  CC   |M|     PT      |       sequence number         |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                           timestamp                           |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |           synchronization source (SSRC) identifier            |
     +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     |            contributing source (CSRC) identifiers             |
     |                             ....                              |
     +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
     |            VP9 payload descriptor (integer #octets)           |
     :                                                               :
     |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                               :                               |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
     |                                                               |
     +                                                               |
     :                          VP9 payload                          :
     |                                                               |
     |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     |                               :    OPTIONAL RTP padding       |
     +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

これは RTP Header を含めた RTP パケット全体の構造です。
VP9 payload descriptor 以下が RTP Payload にあたります。
VP9 のキーフレーム情報は VP9 payload descriptor に入っています

キーフレーム判定方法

VP9 の payload descriptor は次のようになっています

フレキシブルモード

         0 1 2 3 4 5 6 7
        +-+-+-+-+-+-+-+-+
        |I|P|L|F|B|E|V|Z| (REQUIRED)
        +-+-+-+-+-+-+-+-+
   I:   |M| PICTURE ID  | (REQUIRED)
        +-+-+-+-+-+-+-+-+
   M:   | EXTENDED PID  | (RECOMMENDED)
        +-+-+-+-+-+-+-+-+
   L:   | TID |U| SID |D| (Conditionally RECOMMENDED)
        +-+-+-+-+-+-+-+-+                             -\
   P,F: | P_DIFF      |N| (Conditionally REQUIRED)    - up to 3 times
        +-+-+-+-+-+-+-+-+                             -/
   V:   | SS            |
        | ..            |
        +-+-+-+-+-+-+-+-+

ノンフレキシブルモード

         0 1 2 3 4 5 6 7
        +-+-+-+-+-+-+-+-+
        |I|P|L|F|B|E|V|Z| (REQUIRED)
        +-+-+-+-+-+-+-+-+
   I:   |M| PICTURE ID  | (RECOMMENDED)
        +-+-+-+-+-+-+-+-+
   M:   | EXTENDED PID  | (RECOMMENDED)
        +-+-+-+-+-+-+-+-+
   L:   | TID |U| SID |D| (Conditionally RECOMMENDED)
        +-+-+-+-+-+-+-+-+
        |   TL0PICIDX   | (Conditionally REQUIRED)
        +-+-+-+-+-+-+-+-+
   V:   | SS            |
        | ..            |
        +-+-+-+-+-+-+-+-+

フレキシブルモードとノンフレキシブルモードで L 列より後の構造が違います。
ただキーフレーム判定は、L 列以前の情報で可能なので、ここでは構造の違いについては意識しないこととします

payload descriptor をパースしてキーフレームを判定するコードを書きます

const buf: Buffer = RTPPayload;
let offset = 0;

const i = getBit(buf[offset], 0);
const p = getBit(buf[offset], 1);
const l = getBit(buf[offset], 2);
const f = getBit(buf[offset], 3);
const b = getBit(buf[offset], 4);
const e = getBit(buf[offset], 5);
const v = getBit(buf[offset], 6);
const z = getBit(buf[offset], 7);

offset++;

let pictureId: number;

if (i === 1) {
  const m = getBit(buf[offset], 0);

  if (m === 1) {
    const _7 = paddingByte(getBit(buf[offset], 1, 7));
    const _8 = paddingByte(buf[offset + 1]);
    pictureId = parseInt(_7 + _8, 2);
    offset += 2;
  } else {
    pictureId = getBit(buf[offset], 1, 7);
    offset++;
  }
}

const tid = getBit(buf[offset], 0, 3);
const u = getBit(buf[offset], 3);
const sid = getBit(buf[offset], 4, 3);
const d = getBit(buf[offset], 7);

if ((!this.sid || !this.l) && !p && b) {
  console.log("このペイロードはキーフレームです");
}

VP9 の RTP Payload は
SID(空間レイヤー ID) または L ビット(レイヤーインデックスの存在)が 0 で
P ビット(画像間予測フレーム)が 0 かつ
B ビット(フレームの開始)が 1 ならキーフレームです

4.2. VP9 Payload Descriptor

A key picture is a picture whose base
spatial layer frame is a key frame, and which thus completely
resets the encoder state. This packet will have its P bit equal
to zero, SID or L bit (described below) equal to zero, and B bit
(described below) equal to 1.

リンク

H264

H264 の RTP Payload の先頭構造の仕様は ↓ の RFC で定義されています

RFC 6184 RTP Payload Format for H.264 Video

構造について見ていきましょう

RFC 6184 には VP8/VP9 にあった RTP パケット全体の図は出てこないので RTP パケット全体の図は書きません。
H264 の RTP ペイロードの先頭には NAL Unit Header というものがあります。この中を見るだけでキーフレームの判定が出来ます。

      +---------------+
      |0|1|2|3|4|5|6|7|
      +-+-+-+-+-+-+-+-+
      |F|NRI|  Type   |
      +---------------+

キーフレーム判定方法

NAL Unit Header をパースしてキーフレームを判定するコードを書きます。

const buf: Buffer = RTPPayload;
let offset = 0;

const f = getBit(buf[offset], 0);
const nri = getBit(buf[offset], 1, 2);
const type = getBit(buf[offset], 3, 5);

if (type === 5) {
  console.log("このペイロードはキーフレームです");
}

H264 の RTP ペイロードは type が 5 ならキーフレームです。

5.3. NAL Unit Header Usage

For coded slice NAL units of a primary
coded picture having nal_unit_type equal to 5 (indicating a
coded slice belonging to an IDR picture)

リンク

AV1

AV1 の RTP Payload の先頭構造の仕様は ↓ の 文書 で定義されています

RTP Payload Format For AV1

構造について見ていきましょう

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X|  CC   |M|     PT      |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           synchronization source (SSRC) identifier            |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|            contributing source (CSRC) identifiers             |
|                             ....                              |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|         0x100         |  0x0  |       extensions length       |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|      ID       |  hdr_length   |                               |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+                               |
|                                                               |
|          dependency descriptor (hdr_length #bytes)            |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               | Other rtp header extensions...|
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| AV1 aggr hdr  |                                               |
+-+-+-+-+-+-+-+-+                                               |
|                                                               |
|                   Bytes 2..N of AV1 payload                   |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               :    OPTIONAL RTP padding       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

AV1 の dependency descriptor は、なんと RTP ヘッダーの拡張領域の中に含まれています。そのため、RTP ペイロードをパースしなくても AV1 パケットの情報を RTP ヘッダー拡張から得ることが出来ます。
ただ、キーフレーム情報自体は RTP Payload 内部の AV1 aggr hdr (AV1 Aggregation Header)からでも取得できます。
今回はキーフレームかどうかだけを知りたいので実装が楽そうな AV1 aggr hdr のパーサーを書いてキーフレーム判定を行いました。

キーフレーム判定方法

AV1 aggr hdr の中は次のようになっています。

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|Z|Y| W |N|-|-|-|
+-+-+-+-+-+-+-+-+

AV1 aggr hdr をパースしてキーフレームを判定するコードを書きます。

const buf: Buffer = RTP Payload;
let offset = 0;

const z = getBit(buf[offset], 0);
const y = getBit(buf[offset], 1);
const w = getBit(buf[offset], 2, 2);
const n = getBit(buf[offset], 4);

if (n === 1) {
  console.log("このペイロードはキーフレームです");
}

AV1 の RTP ペイロードは N ビット が 1 ならキーフレームです。

4.4 AV1 Aggregation Header

N: MUST be set to 1 if the packet is the first packet of a coded video sequence, and MUST be set to 0 otherwise.

リンク

参考文献

Discussion