ChromeとcanvasでTVの放送用字幕を作っているお話2023

以前書いた記事は2019年時点の事例を出していましたが、実は昨日含め今年はすでに数回出番があったので、アップデートも兼ねてアドベントカレンダーにまとめてみます。機材のお話が多めになりますが、よろしければお付き合いください🙇🏻‍♂️

以前の記事は↓

https://qiita.com/Kaitou/items/d23e1900661e991b9001

## 共通

僕が MacBook Pro を使っているため、映像を HDMI で出力し、SDI（Serial Digital Interface）という放送機材用のものに変換してあげる必要があります。

僕は Blackmagic Design の Mini Converter UpDownCross HD というものを使って変換しています。

https://www.blackmagicdesign.com/jp/store/converters-encoders/mini-converters/W-CONM-28

具体的には下記のように接続しています。

```plantuml
@startmindmap
* MacBook Pro
** HDMI 1080p（60fps）
*** Mini Converter UpDownCross HD
**** SDI 1080i（59.94fps）
***** 放送用ミキサー
**** HDMI 1080i（59.94fps）
***** 確認用ディスプレイ
@endmindmap
```

また、インターフェイスをHDMIからSDIにしているだけでなく、映像の形式も変換しています。具体的には Mini Converter UpDownCross HD には 1080p（60fps）で持ち込み、Converter内で 1080i（59.94fps）に変換して、放送用のミキサーと確認用ディスプレイに入れています。

ちなみに2019年時点ではコンバーターの前で分岐をしていましたが、それはどうもうまくいかないことが多く（おそらくHDMIの分岐に本来的には無理がある）、コンバーターからSDIとHDMIを出すということに落ち着いています。

この際、確認用ディスプレイは一般的なPC用のディスプレイを使用しているため、プログレッシブ（1080p）ではなく、インターレース（1080i）で入力されてしまうため相性が悪く、確認用ディスプレイ上でのちらつきみたいなのが発生しますが、それは仕方ないと考えています。

## 透過なしの場合

透過無しというのは、TVの画面全体、もしくはミキサー側でPiPやワイプ（要は画面の中に小さい四角い画面を乗せる）方法です。その場合は、単に表示させたい映像を送り出せていればよいので下記のような構成になります。

```plantuml
@startmindmap
* MBP
** HDMI
*** コンバーター
**** SDI
***** 放送用ミキサー
**** HDMI
***** 確認用ディスプレイ
@endmindmap
```

今のMBPにはデフォルトでHDMI出力がついているので、これならばすぐにできると思います。

## 透過ありの場合

透過ありというのは、仮にスポーツ中継だと試合の映像の上に選手名や過去の成績等が表示されていると思いますが、この試合の映像のところは透過させ、選手名や過去の成績はPCで生成したものを表示させたいという場合です。

### 透過させる場合の仕組み
仮にこんな漢字の後景が中継画像、前景の文字が字幕だとします。
<img width="320" alt="【期待する映像のサンプル】" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/7b49c77e-ec6e-6c5c-4876-be0365dd27e9.jpeg">

PCやウェブだと、こんな感じの画像を用意してあげればなんとなく映像に重ねると、うまく透過してくれそうな感じがしますが……。
<img width="320" alt="【アルファチャンネルを持っていそうな文字データのサンプル】" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/34676d39-e731-4c64-2f68-fb9e3ff8b2a2.png">

実際にはアルファチャンネルは持ち込めないので下記のようになってしまいます。
<img width="320" alt="【アルファチャンネルを持てないのでベタ地が敷かれる文字データのサンプル】" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/e5343e6a-293f-5ca6-ee26-176f253a863d.jpeg">

つまり表示したい部分と透過させたい部分の情報を放送用のミキサーに持ち込まなければなりません。具体的にはこんな感じの映像を作る必要があります。映像に映したい箇所を白、透過させたい箇所を黒にしたものです。（ちなみに半透過はグレーです。グレーの度合いによって、どの程度透過するのかが決まります。黒寄りであれば透過度が高く、白寄りであれば透過度が低くなります。）
<img width="320" alt="【映像に合成したい文字部分を白、透過させたい箇所を黒にしたサンプル】" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/867616d9-ff18-954b-bb0e-b882b16c363e.png">

つまり表示したい映像と、表示したい場所の映像を2つ同時に放送用のミキサーに送ることによって、初めて透過ありの字幕を送れるということになります。
<img width="320" alt="【映像、キー信号、フィル信号が重なっているイメージ】" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/be36a73b-6370-1614-3c1b-9fd7d68ae9dd.png">

ちなみに表示したい映像をフィル信号、表示したい場所の映像をキー信号と言います。

### 機材のつなぎ方

機材のつなぎ方は下記のようになります。つまりコンバーターも、ディスプレイも2セット必要になると言うわけです。
```plantuml
@startmindmap
* MBP
** HDMI（フィル信号）
*** コンバーター
**** SDI
***** 放送用ミキサー
**** HDMI
***** 確認用ディスプレイ
** HDMI（キー信号）
*** コンバーター
**** SDI
***** 放送用ミキサー
**** HDMI
***** 確認用ディスプレイ
@endmindmap
```
（放送用ミキサーは図上は2つあるように見えますが同じものにつなぎ込んでいます。）


## どこでcanvasを使っているのか？

延々と機材の話をしてきましたが、ここからどのように映像を生成するのか？というところになります。ここからは基本 [2019年までの記事](https://qiita.com/Kaitou/items/d23e1900661e991b9001) と同じ流れになります。

### 透過なしの場合
やっていることはシンプルで、ローカルにサーバーを立てて、Chromeのウィンドウを2つ開いて、操作＆処理用ウィンドウ、表示用ウィンドウに割り当てて、表示用ウィンドウはフルスクリーンで外部ディスプレイに映すイメージです。略式ですが考え方としては下記のような構成です。（下記の図においてコンバーター等々は外部ディスプレイの中に含まれていると考えていただければ。）
```plantuml
@startmindmap
* MBP（操作＆処理用ウィンドウ）
** 外部ディスプレイ（表示用ウィンドウ）
@endmindmap
```

表示用のウィンドウの表示にcanvasを使っています。

### 透過ありの場合
やっていることはこちらもシンプルで、ローカルにサーバーを立てて、Chromeのウィンドウを3つ開いて、操作＆処理用ウィンドウ、フィル信号用ウィンドウ、キー信号用ウィンドウを作り、フィル信号用とキー信号用のウィンドウは外部ディスプレイ扱いでフルスクリーン状態にしています。同様に略式の図だとこんな感じ。
```plantuml
@startmindmap
* MBP（操作＆処理用ウィンドウ）
** 外部ディスプレイ（フィル信号用ウィンドウ）
** 外部ディスプレイ（キー信号用ウィンドウ）
@endmindmap
```

### 処理の流れに関して
「操作＆処理用ウィンドウ」のJavaScriptで外部のAPIと接続する、もしくはデータを集めてきて処理し、表示に必要なデータがそろったところで、表示に関するデータのみそれぞれのウィンドウに送っています。

JSで別のウィンドウの関数を呼び出すにはこんな感じで書きます。

事前に操作＆処理用ウィンドウから表示用のウィンドウをこんな感じで呼び出します。下記の例では、フィル信号用ウィンドウと、キー信号用ウィンドウを開いているところです。
```JS:操作＆処理用ウィンドウ
const windowObject = [];
document.getElementById('button').onclick = function() {
  windowObject[0] = window.open('fill.html', 'fillWindow');
  windowObject[1] = window.open('key.html', 'keyWindow');
}
```

実際に操作＆処理用ウィンドウから表示用のウィンドウに処理をさせたり、データを送る場合は下記のように行います。`windowObject[0]`が、前の段落に書いた、操作＆処理用ウィンドウで開いたフィル信号用ウィンドウです。フィル信号用ウィンドウに書かれた`hogehoge`という関数を呼び出しているということですね。データの受け渡しも普通に引数で渡せます。
```JS:操作＆処理用ウィンドウ
windowObj[0].hogehoge(data);
```
```JS:フィル信号用ウィンドウ
function hogehoge(data) {

}
```

### 細かい注意点

この仕組みだと、「操作＆処理用ウィンドウ」がAPIを呼びに行かない限り、基本的にはローカルにあるファイルで賄えると思います。しかし、逆にこのローカルのファイルで賄えるというところはあまりやらないところなので、細かい注意点がいくつかあります。

#### ①ローカルの画像を読み込む場合でも読み込み時間がある

ついついローカルなので、フィル信号とかで画像を読み込む時間を忘れがちですが、やっぱりあります。ちらつきの原因をよくよく追求してみると都度ブラウザに読み込んでいる処理を書いていることがあります。例えばこんな感じです。

```JS:フィル信号用ウィンドウ
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

function hogehoge(data) {
  context.clearRect(0, 0, 1920, 1080);

  const gazou = new Image();
  gazou.src = 'img/gazou.png';
  context.drawImage(gazou, 0, 0);
}
```

こちらは、関数の外に読み込みの`new Image()`の処理を書いてあげれば大丈夫です。ファイルの存在確認や読み込み完了の監視等は、よっぽどのことがない限り不要なはずです。

```JS:フィル信号用ウィンドウ
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');

const gazou = new Image();
gazou.src = 'img/gazou.png';

function hogehoge(data) {
  context.clearRect(0, 0, 1920, 1080);

  context.drawImage(gazou, 0, 0);
}
```

#### ②フォントの読み込みも要注意

使いたいフォントがある場合も、いきなりcanvasで呼び出さず、事前にHTML側で呼び出しておいたほうが安心です。例えばこんな感じで書き込んで置けばOK。`.preload`をCSSで画面外に出しておきましょう。

```HTML:フィル信号用ウィンドウ
<div class="preload" style="font-family: 'RodinProN-B';color:#fff;">名前名前　名前名前</div>

<canvas id="canvas" width="1920" height="1080"></canvas>
```

実際にcanvas用にJSで呼び出す場合はこんな感じ。

```JS:フィル信号用ウィンドウ
contextText.fillStyle = '#000000';
contextText.font = '40px "RodinProN-B"';
contextText.textAlign = 'left';
contextText.textBaseline = 'middle';
contextText.fillText('テスト', 100, 100, 338);
```

あとローカルのフォント指定はPostScript名でやっています。PostScript名は、Macの場合Font Bookの識別子のところで確認することが可能です。
<img width="320" alt="【Font Bookでの識別子確認イメージ】" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620149/b8848889-b72c-4dc3-fb5f-e4847a776c0b.png">


## 現在の課題

### 透過ありの場合、表示切り替えの場合にちらつきが発生してしまうケースがある

2つの Mini Converter UpDownCross HD を仕様しており、その機材間のタイミングの同期ができない、もしくは MBP からの HDMI の出力タイミングが2系統で違ってしまっているため、キー信号が動く場合はちらつきがでるケースがあります。これはどうしようもないので、スイッチャーさん側でうまくやってもらっています😭

### 4K対応はできていない

これはひとえに機材問題で Mini Converter UpDownCross HD に相当するものを僕が持っていないという点です。ただ現在取引のある放送局はFullHDの機材なので辛うじて大丈夫という感じです。

## 最後に

実はHTML、CSS、JavaScriptで、こんなものも作れちゃいます。技術的なサンプルというわけではなくて、ちゃんと放送で使われているものなので、ウェブページを作るだけにしておくのはもったいないです😎

逆にこれやってみたいけれど、知り合いにエンジニアが居ないという方は、ぜひお声がけください👍🏻

友だちも大募集中ですのでお気軽にXまで↓
https://twitter.com/Kaitou1192

<br><br>

## 2023.12.4 追記
「昨日」と書きましたが、実は本編を書いたのはちょっと前でした。真の意味で昨日、放送の方たちに機材について教わったの補足です。

「現在の課題 - 透過ありの場合、表示切り替えの場合にちらつきが発生してしまうケースがある」のところは、リファレンス信号というのを使うと解消できるそうです。昨日の場合でいうと、コンバーターにミキサー側のリファレンス信号を入力（REF INというところに同軸ケーブルを接続）しました。そうするとフィル信号用のコンバーター、キー信号用のコンバーターもミキサー側のタイミングに同期されるらしくちらつきが抑えられるそうです。

Blackmagic Design Mini Converter UpDownCross HD 凄い👏🏻👏🏻👏🏻