11const WASM_BASE64 = '__WASM__'
22
3+ // biome-ignore lint/style/noUnusedTemplateLiteral: audio_processor.js 内で文字列を扱いたいので `` で囲んでいる
4+ const AUDIO_WORKLET_PROCESSOR_CODE = `__AUDIO_PROCESSOR__`
5+ const AUDIO_WORKLET_PROCESSOR_NAME = 'mp4-media-stream-audio-worklet-processor'
6+
37/**
48 * {@link Mp4MediaStream.play } に指定可能なオプション
59 */
@@ -36,18 +40,13 @@ class Mp4MediaStream {
3640 * 実行環境が必要な機能をサポートしているかどうかを判定します
3741 *
3842 * 以下のクラスが利用可能である必要があります:
39- * - MediaStreamTrackGenerator
4043 * - AudioDecoder
4144 * - VideoDecoder
4245 *
4346 * @returns サポートされているかどうか
4447 */
4548 static isSupported ( ) : boolean {
46- return ! (
47- typeof MediaStreamTrackGenerator === 'undefined' ||
48- typeof AudioDecoder === 'undefined' ||
49- typeof VideoDecoder === 'undefined'
50- )
49+ return typeof AudioDecoder !== 'undefined' && typeof VideoDecoder !== 'undefined'
5150 }
5251
5352 /**
@@ -141,7 +140,7 @@ class Mp4MediaStream {
141140 * ようにしないと、WebRTC の受信側で映像のフレームレートが極端に下がったり、止まったりする現象が確認されています。
142141 * なお、VideoElement はミュートかつ hidden visibility でも問題ありません。
143142 */
144- play ( options : PlayOptions = { } ) : MediaStream {
143+ play ( options : PlayOptions = { } ) : Promise < MediaStream > {
145144 if ( this . info === undefined ) {
146145 // ここには来ないはず
147146 throw new Error ( 'bug' )
@@ -150,7 +149,7 @@ class Mp4MediaStream {
150149 const playerId = this . nextPlayerId
151150 this . nextPlayerId += 1
152151
153- const player = new Player ( this . info . audioConfigs . length > 0 , this . info . videoConfigs . length > 0 )
152+ const player = new Player ( this . info . audioConfigs , this . info . videoConfigs )
154153 this . players . set ( playerId , player )
155154 ; ( this . wasm . exports . play as CallableFunction ) (
156155 this . engine ,
@@ -242,26 +241,17 @@ class Mp4MediaStream {
242241
243242 const init = {
244243 output : async ( frame : VideoFrame ) => {
245- if ( player . videoWriter === undefined ) {
246- // writer の出力先がすでに閉じられている場合などにここに来る可能性がある
244+ if ( player . canvas === undefined || player . canvasCtx === undefined ) {
247245 return
248246 }
249247
250248 try {
251- await player . videoWriter . write ( frame )
249+ player . canvas . width = frame . displayWidth
250+ player . canvas . height = frame . displayHeight
251+ player . canvasCtx . drawImage ( frame , 0 , 0 )
252+ frame . close ( )
252253 } catch ( error ) {
253- // 書き込みエラーが発生した場合には再生を停止する
254-
255- if ( error instanceof DOMException && error . name === 'InvalidStateError' ) {
256- // 出力先の MediaStreamTrack が停止済み、などの理由で write() が失敗した場合にここに来る。
257- // このケースは普通に発生し得るので正常系の一部。
258- // writer はすでに閉じているので、重複 close() による警告ログ出力を避けるために undefined に設定する。
259- player . videoWriter = undefined
260- await this . stopPlayer ( playerId )
261- return
262- }
263-
264- // 想定外のエラーの場合は再送する
254+ // エラーが発生した場合には再生を停止する
265255 await this . stopPlayer ( playerId )
266256 throw error
267257 }
@@ -296,26 +286,25 @@ class Mp4MediaStream {
296286 const config = this . wasmJsonToValue ( configWasmJson ) as AudioDecoderConfig
297287 const init = {
298288 output : async ( data : AudioData ) => {
299- if ( player . audioWriter === undefined ) {
300- // writer の出力先がすでに閉じられている場合などにここに来る可能性がある
289+ if ( player . audioInputNode === undefined ) {
301290 return
302291 }
303292
304293 try {
305- await player . audioWriter . write ( data )
306- } catch ( e ) {
307- // 書き込みエラーが発生した場合には再生を停止する
308-
309- if ( e instanceof DOMException && e . name === 'InvalidStateError' ) {
310- // 出力先の MediaStreamTrack が停止済み、などの理由で write() が失敗した場合にここに来る。
311- // このケースは普通に発生し得るので正常系の一部。
312- // writer はすでに閉じているので、重複 close() による警告ログ出力を避けるために undefined に設定する。
313- player . audioWriter = undefined
314- await this . stopPlayer ( playerId )
315- return
294+ if ( data . format !== 'f32' ) {
295+ // フォーマットは f32 だけが来る想定。
296+ // もし他のフォーマットが来ることがあれば、その都度対応すること。
297+ throw Error ( `Unsupported audio data format: ${ data . format } "` )
316298 }
317299
318- // 想定外のエラーの場合は再送する
300+ const samples = new Float32Array ( data . numberOfFrames * data . numberOfChannels )
301+ data . copyTo ( samples , { planeIndex : 0 } )
302+ data . close ( )
303+
304+ const timestamp = data . timestamp
305+ player . audioInputNode . port . postMessage ( { timestamp, samples } , [ samples . buffer ] )
306+ } catch ( e ) {
307+ // エラーが発生した場合には再生を停止する
319308 await this . stopPlayer ( playerId )
320309 throw e
321310 }
@@ -433,27 +422,49 @@ type Mp4Info = {
433422class Player {
434423 private audio : boolean
435424 private video : boolean
425+ private numberOfChannels = 1
426+ private sampleRate = 48000
436427 audioDecoder ?: AudioDecoder
437428 videoDecoder ?: VideoDecoder
438- audioWriter ?: WritableStreamDefaultWriter
439- videoWriter ?: WritableStreamDefaultWriter
440-
441- constructor ( audio : boolean , video : boolean ) {
442- this . audio = audio
443- this . video = video
429+ canvas ?: HTMLCanvasElement
430+ canvasCtx ?: CanvasRenderingContext2D
431+ audioContext ?: AudioContext
432+ audioInputNode ?: AudioWorkletNode
433+
434+ constructor ( audioConfigs : AudioDecoderConfig [ ] , videoConfigs : VideoDecoderConfig [ ] ) {
435+ this . audio = audioConfigs . length > 0
436+ this . video = videoConfigs . length > 0
437+
438+ if ( audioConfigs . length > 0 ) {
439+ // [NOTE] 今は複数音声入力トラックには未対応なので、最初の一つに決め打ちでいい
440+ this . numberOfChannels = audioConfigs [ 0 ] . numberOfChannels
441+ this . sampleRate = audioConfigs [ 0 ] . sampleRate
442+ }
444443 }
445444
446- createMediaStream ( ) : MediaStream {
445+ async createMediaStream ( ) : Promise < MediaStream > {
447446 const tracks = [ ]
448447 if ( this . audio ) {
449- const generator = new MediaStreamTrackGenerator ( { kind : 'audio' } )
450- tracks . push ( generator )
451- this . audioWriter = generator . writable . getWriter ( )
448+ const blob = new Blob ( [ AUDIO_WORKLET_PROCESSOR_CODE ] , { type : 'application/javascript' } )
449+ this . audioContext = new AudioContext ( { sampleRate : this . sampleRate } )
450+ await this . audioContext . audioWorklet . addModule ( URL . createObjectURL ( blob ) )
451+
452+ this . audioInputNode = new AudioWorkletNode ( this . audioContext , AUDIO_WORKLET_PROCESSOR_NAME , {
453+ outputChannelCount : [ this . numberOfChannels ] ,
454+ } )
455+
456+ const destination = this . audioContext . createMediaStreamDestination ( )
457+ this . audioInputNode . connect ( destination )
458+ tracks . push ( destination . stream . getAudioTracks ( ) [ 0 ] )
452459 }
453460 if ( this . video ) {
454- const generator = new MediaStreamTrackGenerator ( { kind : 'video' } )
455- tracks . push ( generator )
456- this . videoWriter = generator . writable . getWriter ( )
461+ this . canvas = document . createElement ( 'canvas' )
462+ const canvasCtx = this . canvas . getContext ( '2d' )
463+ if ( canvasCtx === null ) {
464+ throw Error ( 'Failed to create 2D canvas context' )
465+ }
466+ this . canvasCtx = canvasCtx
467+ tracks . push ( this . canvas . captureStream ( ) . getVideoTracks ( ) [ 0 ] )
457468 }
458469 return new MediaStream ( tracks )
459470 }
@@ -512,25 +523,14 @@ class Player {
512523 await this . closeAudioDecoder ( )
513524 await this . closeVideoDecoder ( )
514525
515- if ( this . audioWriter !== undefined ) {
516- try {
517- await this . audioWriter . close ( )
518- } catch ( e ) {
519- // writer がエラー状態になっている場合などには close() に失敗する模様
520- // 特に対処法も実害もなさそうなので、ログだけ出して無視しておく
521- console . log ( `[WARNING] ${ e } ` )
522- }
523- this . audioWriter = undefined
526+ if ( this . audioContext !== undefined ) {
527+ await this . audioContext . close ( )
528+ this . audioContext = undefined
529+ this . audioInputNode = undefined
524530 }
525- if ( this . videoWriter !== undefined ) {
526- try {
527- await this . videoWriter . close ( )
528- } catch ( e ) {
529- // writer がエラー状態になっている場合などには close() に失敗する模様
530- // 特に対処法も実害もなさそうなので、ログだけ出して無視しておく
531- console . log ( `[WARNING] ${ e } ` )
532- }
533- this . videoWriter = undefined
531+ if ( this . canvas !== undefined ) {
532+ this . canvas = undefined
533+ this . canvasCtx = undefined
534534 }
535535 }
536536}
0 commit comments