Google Meetに座長としてタイムキーパーを表示する方法（Linux版）

## 概要

Google Meetでは、自分自身が発表者として画面共有している場合には、Chromeプラグインを利用するなどの方法により簡単にタイムキーパーを表示することができる。しかし、座長（Google Meet的には座長は区別されず、単なる参加者に過ぎない）として接続しているGoogle Meetにタイムキーパーを表示させるには、少し厄介な手順が必要である。本稿では、そのための手順を述べる。

基本的な手順は、以下の通りである。

* ブラウザで[HTML5で学会タイマー](https://ichiro-maruta.blogspot.com/2011/09/html5.html)を表示しておく
* OBS-Studioで仮想カメラを作成し、学会タイマーを表示しているブラウザのウインドウを仮想カメラに転送する
* PipeWireで仮想マイクを作成し、学会タイマーを表示しているブラウザの音声信号を仮想マイクに転送する
* 仮想カメラと仮想マイクを入力として指定してGoogle Meetに接続する

## OBS-Studioで仮想カメラを用意する

### ループバックカメラを用意する

基本的な手順は、「[obs-v4l2sinkを使ってLinux上のOBS Studioで合成した映像をSkypeやZoomに流す](http://www.nofuture.tv/diary/20200413.html)」の通り。

```sh
$ sudo apt install v4l2loopback-dkms v4l2loopback-utils
$ sudo modprobe v4l2loopback card_label="OBS Virtual Camera"
```

なお、OBS-Studio 26.1 の[仮想カメラプラグイン](https://github.com/CatxFish/obs-virtual-cam)は、`card_label` で指定した名前を手がかりにループバックカメラを探している。そのため、`card_label` で指定する名前は変更しないこと。

v4l2loopback カーネルモジュールを組み込もうとする時、UEFI secure boot 環境では、以下のようなエラーになる。

```
modprobe: ERROR: could not insert 'v4l2loopback': Required key not available
```

その場合は、[カーネルモジュールに署名](https://gist.github.com/reillysiemens/ac6bea1e6c7684d62f544bd79b2182a4)が必要である。

### OBS-Studioをインストール・設定する

```
$ sudo apt install obs-studio
```

学会タイマーを表示しているウインドウをキャプチャするように設定する。設定方法は、https://note.com/kulo/n/nb540797ecabd#QteCf を参考にした。

OBS-Studio の仮想カメラの使い方は、[仮想カメラを使った高解像度・高フレームレートのテスト方法](https://knowledge.sakura.ad.jp/30186/)に記載がある通り。OBS-Studio の右下にある「仮想カメラ開始」というボタンを押すと、問題なければ「仮想カメラ停止」と表示が切り替わって、仮想カメラが動き始める。

## PipeWireで仮想マイクを用意する

Debian も Bookworm からは、PulseAudio に代えて PipeWire を使うようになっている。最初に、従来の PulseAudio との互換性を維持するコマンド `pactl` をインストールする。

```
$ sudo apt install pipewire-pulse
```

基本的には、[ArchLinuxによるPulseAudioの設定例](https://wiki.archlinux.org/index.php/PulseAudio/Examples#Mixing_additional_audio_into_the_microphone's_audio)にしたがって設定する。PulseAudioについての設定は、[UbuntuのForum](https://forums.ubuntulinux.jp/viewtopic.php?pid=75533)が参考になる。

最初に、`virtualspeaker` という名前の仮想スピーカーを作成する。

```
$ pactl load-module module-null-sink sink_name=virtualspeaker sink_properties=device.description=virtualspeaker
```

仮想スピーカーに入った信号をモニタするための `virtualspeaker.monitor` という出力[^source]を、`virtualmic` という名前の仮想マイクに転送する。

```
$ pactl load-module module-remap-source master=virtualspeaker.monitor source_name=virtualmic source_properties="device.description=virtualmic"
```

次に、`pavucontrol` コマンドを用いて、学会タイマーを表示しているブラウザの音出力を、仮想スピーカー `virtualspeaker` を使うようにする。下図では、学会タイマーを表示しているブラウザとして、Firefox を用いている。

![virtualspeaker-screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79826/81227ec5-4227-34ba-b980-ce586a219d0d.png)

[^source]: PulseAudio用語では、出力のことをsourceと言う。どのような出力が存在しているかは、`pactl list sources` というコマンドで調べることができる。 逆に、入力のことをsinkと言う。どのような入力が存在しているかは、`pactl list sinks` というコマンドで調べることができる。 

また、音出力を全て捨てるための `nullspeaker` という名前の仮想スピーカーも作成しておく。

```
$ pactl load-module module-null-sink sink_name=nullspeaker sink_properties=device.description=nullspeaker
```
 
## 学会タイマーをGoogle Meetに表示する

別のブラウザでGoogle Meetに接続する。マイクとして `virtualmic` 仮想マイクを、スピーカーとして `nullspeaker` 仮想スピーカーを、カメラとして `OBS Virtual Camera` 仮想カメラを指定する。画面が反転して表示されるが、他の参加者には正しい向きで表示されるので問題ない。

![googlemeet-virtualio-screenshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79826/5f7e299a-0869-4713-f1da-8e879d1efdad.png)

## 後片付け

利用した仮想マイクや仮想スピーカーのindexを、以下のコマンドで調べる。この結果では、131,132,133 の3つである。

```
$ pactl list modules
(snip)
    index: 131
	name: <module-null-sink>
	argument: <sink_name=virtualspeaker sink_properties=device.description=virtualspeaker>
	used: 1
	load once: no
	properties:
		module.author = "Lennart Poettering"
		module.description = "クロック付き NULL シンク"
		module.version = "12.2"
    index: 132
	name: <module-remap-source>
	argument: <master=virtualspeaker.monitor source_name=virtualmic source_properties=device.description=virtualmic>
	used: 0
	load once: no
	properties:
		module.author = "Stefan Huber"
		module.description = "Virtual channel remapping source"
		module.version = "12.2"
    index: 133
	name: <module-null-sink>
	argument: <sink_name=nullspeaker sink_properties=device.description=nullspeaker>
	used: 0
	load once: no
	properties:
		module.author = "Lennart Poettering"
		module.description = "クロック付き NULL シンク"
		module.version = "12.2"
```

これら3つのモジュールを削除する。

```
$ pactl unload-module 131
$ pactl unload-module 132
$ pactl unload-module 133
```