diff --git a/dev/demux.html b/dev/demux.html index 091f7dd..1e754ee 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -59,9 +59,9 @@ const decoderConfig = await audioTrack.getDecoderConfig(); const sampleSource = new Metamuxer.EncodedAudioSampleSource(await audioTrack.getCodec()); - const audioDataSource = new Metamuxer.AudioDataSource({ codec: 'pcm-s16', bitrate: 128e3 }); + const audioDataSource = new Metamuxer.AudioDataSource({ codec: 'vorbis', bitrate: 128e3 }); const videoSampleSource = new Metamuxer.EncodedVideoSampleSource(await videoTrack.getCodec()); - output.addAudioTrack(sampleSource); + output.addAudioTrack(audioDataSource ?? sampleSource); output.addVideoTrack(videoSampleSource); output.start(); @@ -71,6 +71,7 @@ + /* for await (const sample of drain.samples()) { //console.log(sample.timestamp); //sample.timestamp *= 2; @@ -78,13 +79,12 @@ //console.log(sample) await sampleSource.digest(sample, { decoderConfig }); } - /* + */ for await (const { data, timestamp } of new Metamuxer.AudioDataDrain(audioTrack).data()) { //console.log("wow", data.timestamp); //console.log(sample) await audioDataSource.digest(data); } - */ for await (const sample of new Metamuxer.EncodedVideoSampleDrain(videoTrack).samples()) { //console.log(sample) await videoSampleSource.digest(sample, { decoderConfig: videoDecoderConfig }); diff --git a/src/media-source.ts b/src/media-source.ts index 4867b34..a7f5f62 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -171,7 +171,7 @@ class VideoEncoderWrapper { validateVideoEncodingConfig(encodingConfig); } - async digest(videoFrame: VideoFrame, encodeOptions?: VideoEncoderEncodeOptions) { + async digest(videoFrame: VideoFrame, shouldClose: boolean, encodeOptions?: VideoEncoderEncodeOptions) { this.source._ensureValidDigest(); // Ensure video frame size remains constant @@ -187,7 +187,9 @@ class VideoEncoderWrapper { this.lastHeight = videoFrame.codedHeight; } - this.ensureEncoder(videoFrame); + if (!this.encoder) { + await this.ensureEncoder(videoFrame); + } assert(this.encoder); const keyFrameInterval = this.encodingConfig.keyFrameInterval ?? 5; @@ -201,6 +203,10 @@ class VideoEncoderWrapper { keyFrame: keyFrameInterval === 0 || multipleOfKeyFrameInterval !== this.lastMultipleOfKeyFrameInterval, }); + if (shouldClose) { + videoFrame.close(); + } + this.lastMultipleOfKeyFrameInterval = multipleOfKeyFrameInterval; // We need to do this after sending the frame to the encoder as the frame otherwise might be closed @@ -211,22 +217,16 @@ class VideoEncoderWrapper { await this.muxer!.mutex.currentPromise; // Allow the writer to apply backpressure } - private ensureEncoder(videoFrame: VideoFrame) { + private async ensureEncoder(videoFrame: VideoFrame) { if (this.encoder) { return; } - this.encoder = new VideoEncoder({ - output: (chunk, meta) => { - const sample = EncodedVideoSample.fromEncodedVideoChunk(chunk); + if (typeof VideoEncoder === 'undefined') { + throw new Error('VideoEncoder is not supported by this browser.'); + } - this.encodingConfig.onEncodedSample?.(sample, meta); - void this.muxer!.addEncodedVideoSample(this.source._connectedTrack!, sample, meta); - }, - error: this.encodingConfig.onEncodingError ?? (error => console.error('VideoEncoder error:', error)), - }); - - this.encoder.configure({ + const encoderConfig: VideoEncoderConfig = { codec: buildVideoCodecString( this.encodingConfig.codec, videoFrame.codedWidth, @@ -239,7 +239,25 @@ class VideoEncoderWrapper { framerate: this.source._connectedTrack?.metadata.frameRate, latencyMode: this.encodingConfig.latencyMode, ...getVideoEncoderConfigExtension(this.encodingConfig.codec), + }; + const support = await VideoEncoder.isConfigSupported(encoderConfig); + if (!support.supported) { + throw new Error( + 'This specific encoder configuration is not supported by this browser. Consider using another codec or' + + ' changing your video parameters.', + ); + } + + this.encoder = new VideoEncoder({ + output: (chunk, meta) => { + const sample = EncodedVideoSample.fromEncodedVideoChunk(chunk); + + this.encodingConfig.onEncodedSample?.(sample, meta); + void this.muxer!.addEncodedVideoSample(this.source._connectedTrack!, sample, meta); + }, + error: this.encodingConfig.onEncodingError ?? (error => console.error('VideoEncoder error:', error)), }); + this.encoder.configure(encoderConfig); assert(this.source._connectedTrack); this.muxer = this.source._connectedTrack.output._muxer; @@ -268,7 +286,7 @@ export class VideoFrameSource extends VideoSource { throw new TypeError('videoFrame must be a VideoFrame.'); } - return this._encoder.digest(videoFrame, encodeOptions); + return this._encoder.digest(videoFrame, false, encodeOptions); } /** @internal */ @@ -308,10 +326,7 @@ export class CanvasSource extends VideoSource { alpha: 'discard', }); - const promise = this._encoder.digest(frame, encodeOptions); - frame.close(); - - return promise; + return this._encoder.digest(frame, true, encodeOptions); } /** @internal */ @@ -355,8 +370,7 @@ export class MediaStreamVideoTrackSource extends VideoSource { const consumer = new WritableStream({ write: (videoFrame) => { // TODO: Drop frames if encoder overloaded - void this._encoder.digest(videoFrame); - videoFrame.close(); + void this._encoder.digest(videoFrame, true); }, }); @@ -458,7 +472,7 @@ class AudioEncoderWrapper { validateAudioEncodingConfig(encodingConfig); } - async digest(audioData: AudioData) { + async digest(audioData: AudioData, shouldClose: boolean) { this.source._ensureValidDigest(); // Ensure audio parameters remain constant @@ -478,15 +492,21 @@ class AudioEncoderWrapper { this.lastSampleRate = audioData.sampleRate; } - this.ensureEncoder(audioData); + if (!this.encoderInitialized) { + await this.ensureEncoder(audioData); + } assert(this.encoderInitialized); if (this.isPcmEncoder) { - await this.doPcmEncoding(audioData); + await this.doPcmEncoding(audioData, shouldClose); } else { assert(this.encoder); this.encoder.encode(audioData); + if (shouldClose) { + audioData.close(); + } + if (this.encoder.encodeQueueSize >= 4) { await new Promise(resolve => this.encoder!.addEventListener('dequeue', resolve, { once: true })); } @@ -495,11 +515,11 @@ class AudioEncoderWrapper { } } - private async doPcmEncoding(audioData: AudioData) { + private async doPcmEncoding(audioData: AudioData, shouldClose: boolean) { assert(this.outputSampleSize); assert(this.writeOutputValue); - // Need to extract data from the audio data before it's closed + // Need to extract data from the audio data before we close it const { numberOfChannels, numberOfFrames, sampleRate, timestamp } = audioData; const CHUNK_SIZE = 2048; @@ -538,6 +558,10 @@ class AudioEncoderWrapper { } } + if (shouldClose) { + audioData.close(); + } + const meta: EncodedAudioChunkMetadata = { decoderConfig: { codec: this.encodingConfig.codec, @@ -563,7 +587,7 @@ class AudioEncoderWrapper { } } - private ensureEncoder(audioData: AudioData) { + private async ensureEncoder(audioData: AudioData) { if (this.encoderInitialized) { return; } @@ -571,17 +595,11 @@ class AudioEncoderWrapper { if ((PCM_CODECS as readonly string[]).includes(this.encodingConfig.codec)) { this.initPcmEncoder(); } else { - this.encoder = new AudioEncoder({ - output: (chunk, meta) => { - const sample = EncodedAudioSample.fromEncodedAudioChunk(chunk); + if (typeof AudioEncoder === 'undefined') { + throw new Error('AudioEncoder is not supported by this browser.'); + } - this.encodingConfig.onEncodedSample?.(sample, meta); - void this.muxer!.addEncodedAudioSample(this.source._connectedTrack!, sample, meta); - }, - error: this.encodingConfig.onEncodingError ?? (error => console.error('AudioEncoder error:', error)), - }); - - this.encoder.configure({ + const encoderConfig: AudioEncoderConfig = { codec: buildAudioCodecString( this.encodingConfig.codec, audioData.numberOfChannels, @@ -591,7 +609,25 @@ class AudioEncoderWrapper { sampleRate: audioData.sampleRate, bitrate: this.encodingConfig.bitrate, ...getAudioEncoderConfigExtension(this.encodingConfig.codec), + }; + const support = await AudioEncoder.isConfigSupported(encoderConfig); + if (!support.supported) { + throw new Error( + 'This specific encoder configuration not supported by this browser. Consider using another codec or' + + ' changing your audio parameters.', + ); + } + + this.encoder = new AudioEncoder({ + output: (chunk, meta) => { + const sample = EncodedAudioSample.fromEncodedAudioChunk(chunk); + + this.encodingConfig.onEncodedSample?.(sample, meta); + void this.muxer!.addEncodedAudioSample(this.source._connectedTrack!, sample, meta); + }, + error: this.encodingConfig.onEncodingError ?? (error => console.error('AudioEncoder error:', error)), }); + this.encoder.configure(encoderConfig); } assert(this.source._connectedTrack); @@ -704,7 +740,7 @@ export class AudioDataSource extends AudioSource { throw new TypeError('audioData must be an AudioData.'); } - return this._encoder.digest(audioData); + return this._encoder.digest(audioData, false); } /** @internal */ @@ -764,8 +800,7 @@ export class AudioBufferSource extends AudioSource { data: chunkData, }); - promises.push(this._encoder.digest(audioData)); - audioData.close(); + promises.push(this._encoder.digest(audioData, true)); currentRelativeFrame += framesToCopy; remainingFrames -= framesToCopy; @@ -811,8 +846,7 @@ export class MediaStreamAudioTrackSource extends AudioSource { const consumer = new WritableStream({ write: (audioData) => { // TODO: Drop frames if encoder overloaded - void this._encoder.digest(audioData); - audioData.close(); + void this._encoder.digest(audioData, true); }, });