フレームレートとタイムコード、タイムスタンプ

**2020/12/22 修正**
「タイムコード」と「タイムスタンプ」の言葉の使い分けが曖昧だったので修正しました。
「タイムコード」というと多くの場合 SMPTE タイムコードのことを指すようなのでその旨を記載し、ドロップフレームに関する項目以外で「タイムコード」と書いていた部分は「タイムスタンプ」を使うように修正しました。
（Wowza ではミリ秒の経過時間を Timecode と呼んでおり、「タイムコード」と書いても間違いではないようにも思います。）

# はじめに

私は 2016 年の 1 月から動画配信の新規事業へ異動して動画関連の技術を扱うようになりました。
動画を扱い始めたばかりのころは特有の概念や用語、考え方に戸惑いました。
その中でも特に悩まされた事柄の 1 つが映像上の時間の取り扱いです。
本稿では映像上の時間の概念について簡単に整理したいと思います。

# ビデオのフレームレート

一般に動画は 1 秒あたり数十枚の静止画で構成されています。
その 1 枚 1 枚をフレーム、 1 秒あたりのフレーム数をフレームレートと呼びます。

用いられるフレームレートにはいくつかの系統があります。次の表は主な例です。

| 規格  | 用途                  | フレームレート                                   |
| ---- | -------------------- | -------------------------------------------- |
| NTSC | テレビ（日本・アメリカなど）   | 30000/1001(≒29.97)fps, 60000/1001(≒59.94)fps |
| NTSC | 映画・アニメ             | 24000/1001(≒23.976)fps                       |
| PAL  | テレビ（ヨーロッパ・中東など） | 25fps, 50fps                                 |

日本やアメリカのテレビでは NTSC と呼ばれる規格が用いられ、フレームレートは 29.97fps や 59.94fps です。
なお、一般に 29.97fps や 59.94fps と表記されることが多いですが正確には 30000/1001fps, 60000/1001fps のことを指します。
テレビ業界の経緯について詳しくはないのですが、かつて白黒からカラー化する際に色信号を乗せる都合で 30fps や 60fps ではなくその 1000/1001 倍の周波数が使われるようになったそうです。
映画やアニメは 29.97fps の 4/5 倍に相当する 23.976fps が使われます。
なお、 29.97fps, 59.94fps, 23.976fps のことを指して 30fps, 60fps, 24fps と表記するケースもあるようです。

インターネットの配信の場合、バックエンドがテレビ業界の設備やコンテンツに依存していると 29.97fps で配信することもありますし、アニメのコンテンツであれば 23.976fps が使われるでしょう。
しかし、動画の制作・送出から配信までがインターネットで完結している場合には 30fps や 60fps といったきりの良いフレームレートを使うことが多いようです。
なお、一般的なニュースやエンターテイメントは 30 fps で、より繊細な動きの滑らかさが要求されるコンテンツには 60fps が使われています。
（例えば、ゲームにこだわりのある方にとって 30fps と 60fps では全く違うという話を耳にします。）

# インターレース

テレビ用のコンテンツや機材を扱うにはテレビ業界で長らく使われてきたインターレース (interlace scan) について知る必要があります。
本稿では詳細に書きませんが、インターレースとは奇数番目のフレームと偶数番目のフレームに対してそれぞれ奇数番目の走査線と偶数番目の走査線の情報だけを抜き出す方式です。
これによって同じデータ量でも見かけのフレーム数を増やして動きを滑らかに見せることができます。
対して、全ての走査線を書き出す方式はプログレッシブ (progressive scan) と呼びます。

# フレームレート変換とインターレース解除

一般的なアニメは 23.976fps で作られていますが、 テレビで放送する際には 29.97fps へ変換 [^32pulldown] しなければいけません。
その他にも配信や編集の都合でフレームレートを変換しなければならないケースはいくつもあります。

[^32pulldown]: このケースでは「3:2 プルダウン」という変換が用いられます。

29.97fps から 59.94fps への変換など、整数倍のフレームレートへの変換であれば劣化は起こりません。
しかし、低いフレームレートへや非整数倍のフレームレートへの変換はもとの映像表現を 100% 保つことは不可能です。
そのため動画の権利元がフレームレート変換を許可していないというケースもあります。
業務で動画を取り扱う場合は、ときにそういった事柄にも配慮する必要があるでしょう。

また先述の通り、インターレースのコンテンツも多く存在しますが、インターネットで動画を配信する場合に必ずしもプレイヤーがインターレースに対応しているわけではありません。
従って、配信前にインターレース解除が必要になりますが、これも多少なりとももとの表現は失われてしまいますし、フレームの情報を補完するアルゴリズムによって仕上がりが変わってきます。

良い画質で配信するにはできるだけ制作時点のデータをそのまま使うことが望ましいですが、ストリーミングサーバーやプレイヤーがフレームレートや走査線方式、コーデック、ビットレートについてあらゆるものを取り扱えるわけではありません。
およそ動画の配信プラットフォームでは入稿の規定があり、配信のサーバーシステムへインジェストされる前に基準通りのデータに整えることが求められます。

# 可変フレームレート

放送用の機器は長時間稼働させてもフレームレートを微塵も変化させないように作られています。
しかし、一般の人が手軽に使えるソフトウェアエンコーダーでは必ずしもそうではありません。
30fps に設定していても映像の変化が激しく圧縮が効きにくくなると一時的にフレームレートが下がってしまうことは往々にして起こります。
動画の配信・制作において望んでフレームレートを変化させることはまずありませんが、特にインターネットのライブ配信ではフレームレートが一定とは限らないものとして扱う必要があります。

# オーディオのサンプルとフレーム

オーディオのサンプリングレートとは 1 秒間にいくつの標本を取るかを表したものです。
48000Hz や 44100Hz がよく使われます。
オーディオのサンプリングレートも変換をすれば多少劣化はします。
しかし、そこまで大きい変化は無い [^audioissue] ですし特定のサンプリングレートしか対応していない端末もまれにあります。
配信プラットフォーム上ではやはり入稿の規定で統一されることが望ましいでしょう。

[^audioissue]: オーディオの変換については多くの場合、ラウドネス変換が最も難しい問題です。

オーディオの 1sample は 1/48000 秒や 1/44100 秒ですから、時間的にはビデオのフレームより非常に細かい単位です。
通常はある程度の数のサンプルをまとめてフレームとして扱います。
AAC であれば 1024samples を 1frame とし、例えばサンプリングレート 48000Hz ならば 1 秒間を 48000/1024≒47frames で構成します。

# タイムコードとタイムスタンプ

前述の通り、動画のフレームが一定間隔であるとは限りません。
またビデオとオーディオはそれぞれ異なる粒度で標本化されますが、それらを正確に同期して記録・再生しなければなりません。
そこで、ビデオやオーディオのフレームには経過時間の情報が付与されて管理されます。
これを総称してタイムコード、あるいはタイムスタンプと呼びます。

特に「タイムコード」と言った場合は SMPTE タイムコードのことを指すことが多いようです。
SMPTE タイムコードについて本稿ではあまり触れません。（筆者があまり詳しくないので。）

タイムスタンプはシステムの内部的には数千分の一秒〜十万分の一秒を単位とする整数値で表されることが一般的です。
映像の時間管理に関して浮動小数点数は通常使われません。
時間の経過とともに誤差が累積してしまうからです。

# ノンドロップフレーム (NDF) とドロップフレーム (DF)

30fps の場合にはタイムコードを

```
HH(時):MM(分):SS(秒):FF(フレーム数 0~29)
```

の形式で表します。 
これは 30frames ごとに秒へ繰り上がる単純な仕組みです。

しかし、 29.97fps の場合に同じようにフレームを数えると 10 分あたり 18 フレームのずれが生じます。
そこで 10 分に 9 回、 2 フレームずつタイムコードを飛ばして数えます。
これをドロップフレーム (DF) と呼びます。

```
frame non-drop frame     drop frame
----- -------------- --------------
    0    00:00:00:00    00:00:00;00
    1    00:00:00:01    00:00:00;01
    2    00:00:00:02    00:00:00;02
    :              :              :
 1799    00:00:59:29    00:00:59;29
 1800    00:01:00:00    00:01:00;02 --- 0, 1 を飛ばす (秒 = 00 AND 分 mod 10 = 0)
 1801    00:01:00:01    00:01:00;03
    :              :              :
 3597    00:01:59:27    00:01:59;29
 3598    00:01:59:28    00:02:00;02 --- 0, 1 を飛ばす (秒 = 00 AND 分 mod 10 = 0)
 3599    00:01:59:29    00:02:00;03
    :              :              :
    :              :              :
    :              :              :
17981    00:09:59:11    00:09:59;29
17982    00:09:59:12    00:10:00;00
17983    00:09:59:13    00:10:00;01
    :              :              :
```

29.97fps のデータを扱う際に DF を記述するか NDF を記述するかは個々の編集ソフトなどの仕様によるようです。
また、使われる区切り文字もまちまち [^dfsep] なので注意が必要です。

[^dfsep]: NDF を全てコロン、 DF を最後のみまたは全てセミコロンで書くケースが比較的多いようです。

# RTMP のタイムスタンプ

RTMP は Macromedia (のちに Adobe が買収) により作られた伝送プロトコルで、現在でも動画の生配信でサーバーへの伝送にしばしば用いられます。
RTMP ではタイムスタンプをミリ秒で表し、最大で 32bit まで使うことができます。
およそ 50 日弱で表現可能な最大の時間を超え、値は 0 へと循環します。

動画配信でよく使われる 30fps において 1frame の間隔は 1/30=0.333... 秒と循環小数になり、ミリ秒ではこれを正確に表すことができません。
従って、 RTMP で 30fps の動画を送信すると 0.333 秒間隔と 0.334 秒間隔が交互に現れるような動作が見られます。

# MPEG2-TS の PTS と DTS

MPEG2-TS はそれ自身が伝送プロトコルとしても機能しますが、ファイルへ書き出したものを HLS で使うケースも非常に多く見られます。
MPEG2-TS では 1 秒を 90000 で表します。
規格文書では Clock Frequency を 27000000 Hz と定義していて、その 300 分の 1 である 90000 をフレームの単位と定義しています。
最小単位が 1 / 90000 秒ですから、 30fps はもちろんのこと 29.97(=30000/1001)fps も誤差なく表現できます。
また、 (1024/48000)*90000 = 1920 であることから、オーディオのサンプリングレート 48000Hz とも相性が良いと言えます。

MPEG2-TS では PTS (Presentation Time-Stamp) と DTS (Decoding Time-Stamp) の 2 種類の値を持つことができます。
PTS は必須の要素で、そのフレームが再生されるべき時間を表します。
DTS はオプショナルの要素で、そのフレームをデコードすべき時間を表します。

FFmpeg の ffprobe コマンドを使うと TS ファイルの各フレームの PTS を見ることができます。

```
$ ffprobe -show_frames -print_format json sample.ts
{
    "frames": [
        {
            "media_type": "audio",
                :
            "pkt_pts": 1301970436,
            "pkt_pts_time": "14466.338178",
            "pkt_dts": 1301970436,
            "pkt_dts_time": "14466.338178",
                :
        },
                :
        {
            "media_type": "video",
                :
            "pkt_pts": 1301970275,
            "pkt_pts_time": "14466.336389",
            "pkt_dts": 1301970275,
            "pkt_dts_time": "14466.336389",
                :
        },
                :
    ]
}
```

各フレームごとに pkt_pts とそれを 90000 で割った pkt_pts_time が示されています。
上図には一部のみを抜粋して書いていますが、このファイルでは全てのフレームで PTS と DTS の値は同一でした。
これは DTS が省略された場合に PTS の値を FFmpeg が出力しているだけで、実際のファイルの中身は DTS フラグに 0 が書かれており DTS の領域は存在していません。
なお、 MPEG2-TS には PTS の存在を示す PTS フラグもありますが、 1 以外の値を書くことは規格上許されていません。

# MP4 の Timescale と Composition Time

MP4 はトラックごとにタイムスケールを持つことができ、この値が 1 秒をいくつで表すのかを示します。
従ってどのようなフレームレートにも対応することが可能です。

次のコマンド実行例は [go-mp4](https://github.com/abema/go-mp4) を使って MP4 の中身を出力したものです。

```
$ mp4tool dump -full mdhd sample.mp4
          :
[moov] Size=6106
          :
  [trak] Size=3119
    [tkhd] Size=92 ... (use "-full tkhd" to show all)
    [mdia] Size=3019
      [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=3631337122 ModificationTimeV0=3631337122 Timescale=44100 DurationV0=665058 Pad=false Language="```" PreDefined=0
      [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=0 HandlerType="soun" Name="SoundHandle"
          :
  [trak] Size=2716
    [tkhd] Size=92 ... (use "-full tkhd" to show all)
    [mdia] Size=2616
      [mdhd] Size=32 Version=0 Flags=0x000000 CreationTimeV0=3631337122 ModificationTimeV0=3631337122 Timescale=90000 DurationV0=1358456 Pad=false Language="```" PreDefined=0
      [hdlr] Size=44 Version=0 Flags=0x000000 PreDefined=0 HandlerType="vide" Name="VideoHandle"
          :
```

MP4 は Box という単位のデータがツリー構造で並んでいます。
このファイルには 2 つのトラック (trak box) があり、その配下の hdlr box の HandlerType 値が `soun` 、 `vide` であることから、それぞれオーディオ、ビデオのトラックであることがわかります。
mdhd box が Timescale や Duration という値を持っていて、ビデオのトラックは Timescale=90000, Duration=1358456 となっています。
従って、ビデオでは 1 秒を 90000 で表し、このファイルのビデオの長さは 1358456/90000≒15 秒であることがわかります。
オーディオも同様に 1 秒を 44100 で表し、665058/44100≒15 秒の長さであるとわかります。

さらに、各フレームの CT (Composition Time, MPEG2-TS の PTS に相当する値) は以下の式で求められます。

```
通常の MP4 の場合
  最初のフレーム
    CT(0) = SampleOffset(0)
  第nフレーム
    CT(n) = SampleOffset(n) + Σ(k: 0 ~ n-1) SampleDelta(k)

Fragment 化された MP4 の場合
  最初のフレーム
    CT(0) = BaseMediaDecodeTime + SampleCompositionTimeOffset(0)
  第nフレーム
    CT(n) = BaseMediaDecodeTime + SampleCompositionTimeOffset(n) + Σ(k: 0 ~ n-1) SampleDuration(k)
```

（実際に MP4 の中を解析する際には、 SampleDuration が全て同一の場合にはフレームごとに値を持たずに DefaultSampleDuration が書かれることや elst box で示される編集情報などを加味する必要があります。）

これらの値を先程の Timescale で除算したものが秒に相当します。

# Flicks

タイムスタンプの取り扱いについて [Flicks](https://github.com/facebookarchive/Flicks) という単位も提案されています。
これは 1 秒を 705600000flicks として時間を表そうというものです。
705600000 という値が 30 や 60, 25, 50, 30000/1001, 24000/1001, 48000, 44100 などフレームレートとして使われている様々な値の整数倍になっているのです。

# おわりに

今回はフレームレートとタイムコード（タイムスタンプ）について簡単にまとめました。
この辺りが理解できると MPEG-DASH や MP4 の中身をそれなりに分かるようになるはずです。
動画に関する技術は全般的にテレビや映画、音楽で培われたものがベースになっていて、 IT から入ってきた人にはすんなりとは理解しにくいと思っています。
私は動画に関わり始めてから何年かかかってやっと色々なことが繋がって理解できるようになったのかなぁと感じています。
次はまた別の観点で動画に関する知識を記事にできたらと思っています。

# 参考文献

- [NTSC - Wikipedia](https://ja.wikipedia.org/wiki/NTSC)
- [PAL - Wikipedia](https://ja.wikipedia.org/wiki/PAL)
- [テレシネ - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%86%E3%83%AC%E3%82%B7%E3%83%8D)
- [Adobe’s Real Time Messaging Protocol](https://www.adobe.com/content/dam/acom/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf)
- [ISO/IEC 13818-1:2000 Generic coding of moving pictures and associated audio information: Systems - Part 1](https://www.iso.org/standard/31537.html)
- [ISO/IEC 14496-12:2015 Coding of audio-visual objects - Part 12: ISO base media file format](https://www.iso.org/standard/68960.html)
- [ISO/IEC 14496-12:2015/AMD 1:2017 Coding of audio-visual objects - Part 12: ISO base media file format - Amendment 1](https://www.iso.org/standard/70430.html)
- [ISO/IEC 14496-12:2015/AMD 2:2018 Coding of audio-visual objects - Part 12: ISO base media file format - Amendment 2](https://www.iso.org/standard/71851.html)
- [GitHub facebookarchive/Flicks](https://github.com/facebookarchive/Flicks)