ImageFlux Live Streamingで音声ミキシングのHLS配信をしてみた

こんにちは、テリーです。オリンピックの柔道とレスリングを毎日見ていました。ネットでテレビ番組が見られるのは本当にありがたい時代になりました。番組の中で、パリに駐在して現地の様子を伝えてくれるリポーターと、東京のスタジオのアナウンサーや解説者が会話しているシーンが度々ありました。国際ビデオ通話の様子を視聴者向けに放送するのは、今ではテレビで当たり前となっていますが、映像配信が高速で安定しているからできる技術です。今回はこの技術の実装方法を、特に音声のパートに絞って紹介したいと思います。

対象読者

  • ビデオ会議・音声会議をしている様子を多数の視聴者向けに配信したい人
  • ブラウザ上での音声ミキシング、映像キャプチャをしたい人

動作確認環境

  • Ubuntu 22.04 LTS (x64)
  • node.js 22.3.0
  • Nuxt 3.13
  • Sora JS SDK 2024.1.2
  • Chrome 128.0.6613.85

本サンプルはOS依存、CPU依存部分を複数含みます。異なるOSや異なるCPUの場合、動作しないことがあります。dockerで動作確認済みです。

音声ミキシングとは

ビデオ会議の場合、リモートの参加者と自分の音声がそれぞれ別のデータ(MediaStream)として、プログラムで取得されます。自分のパソコンでただ再生するだけならば、取得したMediaStreamを1つずつaudioタグ(またはvideoタグ)に割り当てていくだけです。

ビデオ会議参加者全員の音声を自分のパソコンで聞きつつ、それらの音声を別の多数の視聴者向けに再配信したいとします。もちろん自分の声も一緒に届けたい。WebRTCの場合は1つの映像と1つの音声しか配信できないため、自分を含めた全ての音声を合体させて、1つのMediaStreamにまとめた上で配信をする必要があります。複数のリアルタイムのオーディオデータを1つのオーディオデータにすることをミキシング、またはミキサーという言い方をします。

ミックスされたオーディオデータは複数の話者の声が入り混じった状態になっており、後から分離するのは実質できないので、個々の音量の調整や文字起こしはミキシング前に行う必要があります。ミキシングされた音声はHLSで片方向で配信するため、エコーキャンセラー等の処理を考える必要がありません。

ミキサークラスの実装

オーディオをミキシングするクラスそのものはブラウザには存在しないため、自分で実装するかライブラリを使う必要があります。下記に最もシンプルなミキサークラスを紹介します。

 1.export class Mixer {
 2.  context: AudioContext;
 3.  streams: Array<MediaStream> = [];
 4.  sources: Array<MediaStreamAudioSourceNode> = [];
 5.  destination: MediaStreamAudioDestinationNode;
 6.  constructor(audioContext: AudioContext) {
 7.    this.context = audioContext;
 8.    this.destination = this.context.createMediaStreamDestination();
 9.  }
10.
11.  public append(stream: MediaStream) {
12.    let source = this.context.createMediaStreamSource(stream);
13.    this.streams.push(stream);
14.    this.sources.push(source);
15.    source.connect(this.destination);
16.  }
17.
18.  public remove(stream: MediaStream) {
19.    const index = this.streams.findIndex(s => s === stream);
20.    if (index >= 0) {
21.      this.sources[index].disconnect();
22.      this.sources.splice(index, 1);
23.      this.streams.splice(index, 1);
24.    }
25.  }
26.
27.  public getMixedStream(): MediaStream {
28.    return this.destination.stream;
29.  }
30.}

重要なのは8行目、12行目、15行目、21行目、28行目です。
8行目では、ミックスされた音声データを取り出すためのノードを作成しています。

 8.    this.destination = this.context.createMediaStreamDestination();

12行目では、ミックスされた音声データを出力するMediaStreamオブジェクトを取得しています。

12.    let source = this.context.createMediaStreamSource(stream);

15行目では、個別の音声データを入力するためのノードを作成しています。

15.    source.connect(this.destination);

21行目では、個別の音声データをdestinationノードに接続します。この書き方をすることで、複数の音声が足し合わされます。

21.      this.sources[index].disconnect();

28行目では、21行目で接続した音声入力を切り離します。

28.    return this.destination.stream;

上記の実装は単純な足し合せになります。一般的なミキサーライブラリでは、sourceとdestinationの間にgainノードを差し込み、音量を調整できるようにする実装をします。またdestinationノードの先にもgainノードを入れ、全体の音量を上げ下げすることができるようにする実装も多いです。

ImageFlux Live Streamingでミキシングした映像・音声を配信するコツ

ImageFlux Live Streamingを使うと、WebRTCでビデオ会議をしつつ、その映像・音声を多数の視聴者に向けてライブ配信を行うことができます。

ポイントはチャンネルを2つ作成することです。
1つ目はビデオ会議用のチャンネルとして、CreateMultistreamChannel APIで作成します。
2つ目はHLS配信用のチャンネルとして、CreateMultistreamChannelWithHLS APIで作成します。

CreateMultistreamChannelWithHLS APIで作成したチャンネルの場合、視聴者の有無に関わらずHLS用のデータ変換料金が発生するため注意が必要です。HLSで配信する用途がないチャンネルは安い方のCreateMultistreamChannel APIを使用します。

この2つのチャンネルをDBで関連付けます。下記はサーバ側コードの一部です。

1.const response = await call_create_channel_api();
2.const response_mix = await call_create_hls_channel_api();
3.await hubDatabase().prepare('INSERT INTO channel(channel_id, sora_url, mix_channel_id, mix_sora_url, user_id, user_name) VALUES (?1, ?2, ?3, ?4, ?5, ?6)').bind(response.channel_id, response.sora_url, response_mix.channel_id, response_mix.sora_url, request.user_id, request.user_name).run();
4.return { channel_id: response.channel_id, sora_url: response.sora_url, mix_channel_id: response_mix.channel_id, mix_sora_url: response_mix.sora_url, user_id: request.user_id, user_name: request.user_name };

ビデオ会議に参加しているどのユーザーのパソコンでミキシングとHLS配信を行うかを決める必要があります。ただ1人となるように決めなかった場合、ビデオ会議に参加している人全員がHLS配信をしてしまい、人数分の料金が発生してしまいがちです。

ミキシング担当の決め方は「チャンネルを作成した人」とするのが無難です。上記のコードでもuser_idをDBに保存しています。このuser_idを持つビデオ会議の参加者が、ミキシングとHLS配信を行うようにブラウザ側のコードを実装します。

ミキサークラスの使い方

getUserMediaで取得した自分のカメラ・マイクと、WebRTCのtrackイベントで取得できるMediaStreamをmixerオブジェクトに追加します。

// 接続時
mixer = new Mixer(audioContext);
// 接続時
mixer.append(localStream);
// ビデオ会議に参加者が増えた時
mixer.append(stream);
// ビデオ会議から参加者が退室した時
mixer.remove(stream);

HLS配信を開始するタイミングは下記の3通りが考えられるでしょう。

  • 自分以外の誰かが会議に参加した時
  • 自分自身が接続開始した時
  • 任意のタイミングでボタン操作

そのタイミングを検知したら、下記のようにHLS対応のチャンネルで配信します。

const sora = Sora.connection(mix_sora_url, false);
pub = sora.sendonly(mix_channel_id, metadata, options);
await pub.connect(mixer.getMixedStream());

サンプル動画

最後にサンプル動画をお見せします。

音声通話をミキシングしたHLS配信のサンプル動画です。

ビデオ会議をミキシング・キャプチャしたHLS配信のサンプル動画です。

まとめ

ビデオ会議・音声通話のオーディオをミキシングする実装方法と、ミキシングした音声をImageFlux Live StreamingでHLS配信するコツを紹介しました。全体のソースコードはとても長いため記載しませんでしたが、より詳しい実装方法に興味がある方はお気軽にご相談ください。ビデオのミキシングは長くなるので次回以降に改めて記事にしたいと思います。