From 1bb3c0e262997f4ef70fff277abb29d1a8ff7607 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:42:23 +0200 Subject: [PATCH] Avoid opaque canvases in case of Firefox (fixes #117) --- src/media-sink.ts | 13 ++++++++++--- src/media-source.ts | 23 +++++++++++++++++++---- src/misc.ts | 9 +++++++++ src/sample.ts | 8 ++++++-- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/media-sink.ts b/src/media-sink.ts index 989d9b2..335dfcc 100644 --- a/src/media-sink.ts +++ b/src/media-sink.ts @@ -18,6 +18,7 @@ import { getInt24, getUint24, insertSorted, + isFirefox, isSafari, last, mapAsyncGenerator, @@ -1199,14 +1200,20 @@ export class CanvasSink { this._nextCanvasIndex = (this._nextCanvasIndex + 1) % this._canvasPool.length; } - const context - = canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; + const context = canvas.getContext('2d', { + alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases + }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; assert(context); context.resetTransform(); if (!canvasIsNew) { - context.clearRect(0, 0, this._width, this._height); + if (isFirefox()) { + context.fillStyle = 'black'; + context.fillRect(0, 0, this._width, this._height); + } else { + context.clearRect(0, 0, this._width, this._height); + } } sample.drawWithFit(context, { diff --git a/src/media-source.ts b/src/media-source.ts index b72c791..44a7d42 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -18,7 +18,16 @@ import { VideoCodec, } from './codec'; import { OutputAudioTrack, OutputSubtitleTrack, OutputTrack, OutputVideoTrack } from './output'; -import { assert, assertNever, CallSerializer, clamp, promiseWithResolvers, setInt24, setUint24 } from './misc'; +import { + assert, + assertNever, + CallSerializer, + clamp, + isFirefox, + promiseWithResolvers, + setInt24, + setUint24, +} from './misc'; import { Muxer } from './muxer'; import { SubtitleParser } from './subtitles'; import { toAlaw, toUlaw } from './pcm'; @@ -248,12 +257,18 @@ class VideoEncoderWrapper { canvasIsNew = true; } - const context = this.resizeCanvas.getContext('2d', { alpha: false }) as - CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; + const context = this.resizeCanvas.getContext('2d', { + alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases + }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D; assert(context); if (!canvasIsNew) { - context.clearRect(0, 0, this.codedWidth, this.codedHeight); + if (isFirefox()) { + context.fillStyle = 'black'; + context.fillRect(0, 0, this.codedWidth, this.codedHeight); + } else { + context.clearRect(0, 0, this.codedWidth, this.codedHeight); + } } videoSample.drawWithFit(context, { fit: sizeChangeBehavior }); diff --git a/src/misc.ts b/src/misc.ts index 3126f03..1c842f2 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -642,6 +642,15 @@ export const isSafari = () => { return result; }; +let isFirefoxCache: boolean | null = null; +export const isFirefox = () => { + if (isFirefoxCache !== null) { + return isFirefoxCache; + } + + return isFirefoxCache = typeof navigator !== 'undefined' && navigator.userAgent?.includes('Firefox'); +}; + /** * T or a promise that resolves to T. * @group Miscellaneous diff --git a/src/sample.ts b/src/sample.ts index 1f2e074..c713aa8 100644 --- a/src/sample.ts +++ b/src/sample.ts @@ -15,6 +15,7 @@ import { toDataView, toUint8Array, SetRequired, + isFirefox, } from './misc'; /** @@ -228,7 +229,10 @@ export class VideoSample { } const canvas = new OffscreenCanvas(width, height); - const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true }); + const context = canvas.getContext('2d', { + alpha: isFirefox(), // Firefox has VideoFrame glitches with opaque canvases + willReadFrequently: true, + }); assert(context); // Draw it to a canvas @@ -343,7 +347,7 @@ export class VideoSample { dest.set(this._data); } else { const canvas = this._data; - const context = canvas.getContext('2d', { alpha: false }); + const context = canvas.getContext('2d'); assert(context); const imageData = context.getImageData(0, 0, this.codedWidth, this.codedHeight);