見出し画像

配信の音声をブラウザで聞いてみた REALITY Advent Calendar 2023

この記事は REALITY AdventCalendar 2023 の 20日目の記事です。

こんにちは、Unityエンジニアのyaegakiです。
今回はブラウザで配信の音声を聞く方法について実験したのでその紹介をします。
MacのChromeでのみ動作を確認しています。

配信の音声について

配信の音声はOpusというフォーマットでエンコードされており、それをサーバーから受け取っています。
クライアントではOpusでエンコードされた音声をデコードして元に戻してあげる必要があります。

Opusをデコードする方法について

Opusをデコードする方法は2つ考えられます。
1つはデコードするプログラムをブラウザ上で動かす方法、もう1つはブラウザに元々ある機能を使用してデコードする方法です。
合宿では前者の方法で実装し、その後に後者の方法も試してみました。

デコードするプログラムをブラウザ上で動かす方法

この方法ではOpusをデコードするプログラムを作成し、デコードした結果を Web Audio API を使用して音声を再生します。
Opusをデコードするプログラムを最初から全部自分で書くのは難しいのでオープンソースの以下のライブラリを使用しました。

https://github.com/eshaz/wasm-audio-decoders

このライブラリはCで書かれたプログラムをEmscriptenでビルドしてブラウザで使用できるようにしてくれています。
これを使用すると以下のようなコードでデコードができます。

const decoder = new window["opus-decoder"].OpusDecoder();
await decoder.ready;
// 2ch, 48kHz
var player = new PCMPlayer(2, 48000);

const decode = (arrayBuffer) => {
    var data = new Uint8Array(arrayBuffer);
    // decodeFrameでデコードする
    const { channelData, samplesDecoded, sampleRate } = decoder.decodeFrame(data);

    // channelData[0]にL、channelData[1]にRのチャンネルの情報が入っている
    return channelData;
};

デコードできたらあとはこのデータを Web Audio API を使用して再生します。

const channels = 2;
const sampleRate = 48000;
const audioContext = new AudioContext({
    latencyHint: 'interactive',
    sampleRate: sampleRate,
});

let startTime = audioContext.currentTime;

const play = (channnelData) => {
    const bufferSource = audioContext.createBufferSource();
    const audioBuffer = audioContext.createBuffer(channels, channnelData[0].length, sampleRate);

    audioBuffer.getChannelData(0).set(channnelData[0]);
    audioBuffer.getChannelData(1).set(channnelData[1]);

    bufferSource.buffer = audioBuffer;
    bufferSource.connect(audioContext.destination);
    bufferSource.start(startTime);
    startTime += audioBuffer.duration;
}

この方法で再生することはできましたが、音声の遅延や新しくデータが入ってきた時に綺麗に繋がらないという課題がありました。

ブラウザに元々ある機能をつかってデコードする方法

ブラウザによっては WebCodecs APIを使用してOpusをデコードすることができます。
WebCodecs APIのAudioDecoderを使うと以下のようにデコードすることができます。

// AudioDecoderはブラウザによっては使用できない
const audioDecoder = new AudioDecoder({
    async output(audioData) {
        // audioDataにデコードされたデータが入っている

        audioData.close();
    },
    error(err) {
        console.error(err)
    }
});

audioDecoder.configure({
    codec: 'opus',
    numberOfChannels: 2,
    sampleRate: 48000,
});

const decode = (arrayBuffer) => {
    var data = new Uint8Array(arrayBuffer);
    const chunk = new EncodedAudioChunk({
        type: 'key',
        timestamp: 0,
        data: data,
    })
    audioDecoder.decode(chunk);
};

デコードしたデータをWebRTC関連のAPI(MediaStreamAPI, Insertable Streams)を使用して再生します。

// 再生時に使用するAudio要素
const audioElement = document.getElementById('sampleAudio');

// MediaStreamを作成してTrackGeneratorを追加する
const mediaStream = new MediaStream();
const audioTrackGenerator = new MediaStreamTrackGenerator('audio');
const audioTrackGeneratorWriter = audioTrackGenerator.writable.getWriter();
mediaStream.addTrack(audioTrackGenerator);

// 作成したMediaStreamをAudio要素に設定する
audioElement.srcObject = mediaStream;

const audioDecoder = new AudioDecoder({
    async output(audioData) {
        // デコードしたデータをTrackGeneratorに書き込む
        audioTrackGeneratorWriter.write(audioData);
        audioData.close();
    },
    error(err) {
        console.error(err)
    }
});

この方法で比較的手軽に音声を再生することができました。
音声の途切れも感じられませんでしたが特に何も対策してないと遅延は発生してしまうようです。
また、この方法は一部のブラウザでは動かないという欠点もあります。

さいごに

ブラウザでREALITY配信の音声を聞くことはできましたが課題はまだたくさんあります。
今回はサーバー側を変更せずにクライアント側の工夫だけでなんとかしましたが、サーバー側でWebRTCを使って再生できるデータを送信する方がいいかもしれません。
とりあえず当初の目的は達成できたのでヨシ!とします。

明日はるいさんで「配信一覧を改善したい!」です、お楽しみに!