tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 95e4c34b2164a01edcf582eb662da439ec7eea9b
parent f1135d926ea4d8979664d02f42ab38953cc258c4
Author: Jan-Ivar Bruaroey <jib@mozilla.com>
Date:   Tue,  4 Nov 2025 14:48:59 +0000

Bug 1994562 - Interventions for some websites relying on legacy MSTP or MSTG on window. r=twisniewski,webcompat-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D271165

Diffstat:
Mbrowser/extensions/webcompat/data/interventions.json | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/extensions/webcompat/injections/js/bug1994562-shim-mstp-mstg-on-window.js | 323+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmodules/libpref/init/StaticPrefList.yaml | 2+-
3 files changed, 392 insertions(+), 1 deletion(-)

diff --git a/browser/extensions/webcompat/data/interventions.json b/browser/extensions/webcompat/data/interventions.json @@ -5273,6 +5273,64 @@ } ] }, + "1994562": { + "label": "MSTP+MSTG on window shim test-page", + "bugs": { + "1994562": { + "issue": "broken-meetings", + "matches": ["https://jan-ivar.github.io/*"] + } + }, + "interventions": [ + { + "platforms": ["all"], + "only_channels": ["nightly"], + "content_scripts": { + "all_frames": true, + "js": ["bug1994562-shim-mstp-mstg-on-window.js"] + } + } + ] + }, + "1899831": { + "label": "zencastr.com", + "bugs": { + "1899831": { + "issue": "broken-meetings", + "matches": ["https://*.zencastr.com/*"] + } + }, + "interventions": [ + { + "platforms": ["all"], + "only_channels": ["nightly"], + "ua_string": ["Chrome_with_FxQuantum"], + "content_scripts": { + "all_frames": true, + "js": ["bug1994562-shim-mstp-mstg-on-window.js"] + } + } + ] + }, + "1943289": { + "label": "livestorm.co", + "bugs": { + "1943289": { + "issue": "broken-meetings", + "matches": ["https://*.livestorm.co/*"] + } + }, + "interventions": [ + { + "platforms": ["all"], + "only_channels": ["nightly"], + "content_scripts": { + "all_frames": true, + "js": ["bug1994562-shim-mstp-mstg-on-window.js"] + } + } + ] + }, "1913599": { "label": "createEncodedStreams shim test-page", "bugs": { @@ -5344,6 +5402,16 @@ { "platforms": ["all"], "only_channels": ["nightly"], + "ua_string": ["Chrome_with_FxQuantum"], + "content_scripts": { + "all_frames": true, + "js": ["bug1994562-shim-mstp-mstg-on-window.js"] + } + }, + { + "platforms": ["all"], + "only_channels": ["nightly"], + "ua_string": ["Chrome_with_FxQuantum"], "content_scripts": { "all_frames": true, "js": ["bug1913599-shim-createencodedstreams.js"] diff --git a/browser/extensions/webcompat/injections/js/bug1994562-shim-mstp-mstg-on-window.js b/browser/extensions/webcompat/injections/js/bug1994562-shim-mstp-mstg-on-window.js @@ -0,0 +1,323 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Bug 1994562 - Sites that depend on legacy (main-thread) + * MediaStreamTrackProcessor or MediaStreamTrackGenerator + * + * Several websites that offer real-time media processing in Chrome fail + * to work in Firefox, either ghosting the button that offers this + * feature or erroring with a message like "Voice/Video processing is + * not supported in this browser". + * + * These webpages rely on the older Chrome-only MSTP or MSTG APIs on + * window instead of the standard MSTP and VTG (VideoTrackGenerator) + * implemented in Safari (and soon Firefox). The following shims the + * former APIs using existing technology on window (canvas for video + * and AudioWorklets for audio). + * + * Note: this shim has inherent performance limitations being on + * main thread. Websites are encouraged to upgrade to the standard + * worker-based APIs directly for optimal performance in Firefox. + */ + +/* globals exportFunction, cloneInto */ + +console.info( + "Nonstandard MediaStreamTrackProcessor and MediaStreamTrackGenerator are being shimmed for compatibility reasons. Please consider updating to the standard equivalents available in workers for optimal performance! See https://bugzil.la/1994562 for details." +); + +if (!window.MediaStreamTrackProcessor) { + const win = window.wrappedJSObject; + const f = func => exportFunction(func, window); + const o = obj => Object.assign(new win.Object(), obj); + + function MediaStreamTrackProcessor(options) { + if (!(options?.track instanceof win.MediaStreamTrack)) { + throw new TypeError("Missing track"); + } + const { track } = options; + if (track.kind == "video") { + const src = o({ + start: f(function start(controller) { + return win.Promise.resolve() + .then( + f(() => { + track.addEventListener( + "ended", + f(() => controller.close()), + o({ once: true }) + ); + src.video = win.document.createElement("video"); + const tracks = new win.Array(); + tracks.push(track); + src.video.srcObject = new win.MediaStream(tracks); + src.video.play(); + return new win.Promise( + f(r => (src.video.onloadedmetadata = r)) + ); + }) + ) + .then( + f(() => { + src.track = track; + src.canvas = new win.OffscreenCanvas( + src.video.videoWidth, + src.video.videoHeight + ); + src.ctx = src.canvas.getContext( + "2d", + o({ desynchronized: true }) + ); + src.t1 = performance.now(); + }) + ); + }), + pull: f(function pull(controller) { + if (track.readyState == "ended") { + controller.close(); + return Promise.resolve(); + } + const fps = track.getSettings().frameRate || 30; + return new win.Promise( + f(r => { + const waitUntil = () => { + if ( + track.readyState == "ended" || + performance.now() - src.t1 >= 1000 / fps + ) { + r(); + return; + } + requestAnimationFrame(waitUntil); + }; + requestAnimationFrame(waitUntil); + }) + ).then( + f(() => { + if (track.readyState == "ended") { + controller.close(); + return; + } + src.t1 = performance.now(); + src.ctx.drawImage(src.video, 0, 0); + const frame = new win.VideoFrame( + src.canvas, + o({ timestamp: src.t1 }) + ); + controller.enqueue(frame); + }) + ); + }), + }); + return o({ readable: new win.ReadableStream(src) }); + } else if (track.kind == "audio") { + const src = o({ + start: f(function start(controller) { + return win.Promise.resolve() + .then( + f(() => { + track.addEventListener( + "ended", + f(() => controller.close()), + o({ once: true }) + ); + src.ac = new win.AudioContext(); + src.arrays = new win.Array(); + function worklet() { + registerProcessor( + "mstp-shim", + class Processor extends AudioWorkletProcessor { + process(input) { + this.port.postMessage(input); + return true; + } + } + ); + } + return src.ac.audioWorklet.addModule( + `data:text/javascript,(${worklet.toString()})()` + ); + }) + ) + .then( + f(() => { + src.node = new win.AudioWorkletNode(src.ac, "mstp-shim"); + const tracks = new win.Array(); + tracks.push(track); + src.ac + .createMediaStreamSource(new win.MediaStream(tracks)) + .connect(src.node); + src.node.port.addEventListener( + "message", + f(({ data }) => data[0][0] && src.arrays.push(data)) + ); + }) + ); + }), + pull: f(function pull(controller) { + return win.Promise.resolve() + .then( + f(() => { + if (track.readyState == "ended") { + controller.close(); + return Promise.resolve(); + } + return src.arrays.length + ? win.Promise.resolve() + : new win.Promise(f(r => (src.node.port.onmessage = r))).then( + f(function loop() { + if (track.readyState == "ended") { + return Promise.resolve(); + } + if (!src.arrays.length) { + return new win.Promise( + f(r => (src.node.port.onmessage = r)) + ).then(f(loop)); + } + return win.Promise.resolve(); + }) + ); + }) + ) + .then( + f(() => { + if (track.readyState == "ended") { + return; + } + const [channels] = src.arrays.shift(); + const joined = new win.Float32Array( + channels.reduce(f((a, b) => a + b.length, 0)) + ); + channels.reduce( + f((offset, a) => { + joined.set(a, offset); + return offset + a.length; + }, 0) + ); + const transfer = new win.Array(); + transfer.push(joined.buffer); + const data = new win.AudioData( + o({ + format: "f32-planar", + sampleRate: src.ac.sampleRate, + numberOfFrames: channels[0].length, + numberOfChannels: channels.length, + timestamp: (src.ac.currentTime * 1e6) | 0, + data: joined, + transfer, + }) + ); + controller.enqueue(data); + }) + ); + }), + }); + return o({ readable: new win.ReadableStream(src) }); + } + } + win.MediaStreamTrackProcessor = exportFunction( + MediaStreamTrackProcessor, + window, + { allowCrossOriginArguments: true } + ); +} + +if (!window.MediaStreamTrackGenerator) { + const win = window.wrappedJSObject; + const f = func => exportFunction(func, window); + const o = obj => Object.assign(new win.Object(), obj); + + function MediaStreamTrackGenerator(options) { + if (options?.kind != "video" && options?.kind != "audio") { + throw new TypeError("Invalid kind"); + } + if (options.kind == "video") { + const canvas = win.document.createElement("canvas"); + const ctx = canvas.getContext("2d", o({ desynchronized: true })); + const [track] = canvas.captureStream().getVideoTracks(); + const sink = o({ + write: f(function write(frame) { + canvas.width = frame.displayWidth; + canvas.height = frame.displayHeight; + ctx.drawImage(frame, 0, 0, canvas.width, canvas.height); + frame.close(); + }), + }); + track.writable = new win.WritableStream(sink); + return track; + } else if (options.kind == "audio") { + const ac = new win.AudioContext(); + const dest = ac.createMediaStreamDestination(); + const [track] = dest.stream.getAudioTracks(); + const sink = o({ + start: f(function start() { + return win.Promise.resolve() + .then( + f(() => { + sink.arrays = new win.Array(); + function worklet() { + registerProcessor( + "mstg-shim", + class Processor extends AudioWorkletProcessor { + constructor() { + super(); + this.arrays = []; + this.arrayOffset = 0; + this.port.onmessage = ({ data }) => + this.arrays.push(data); + this.emptyArray = new Float32Array(0); + } + process(inputs, [[output]]) { + for (let i = 0; i < output.length; i++) { + if ( + !this.array || + this.arrayOffset >= this.array.length + ) { + this.array = this.arrays.shift() || this.emptyArray; + this.arrayOffset = 0; + } + output[i] = this.array[this.arrayOffset++] || 0; + } + return true; + } + } + ); + } + return ac.audioWorklet.addModule( + `data:text/javascript,(${worklet.toString()})()` + ); + }) + ) + .then( + f(() => { + sink.node = new win.AudioWorkletNode(ac, "mstg-shim"); + sink.node.connect(dest); + return track; + }) + ); + }), + write: f(function write(audioData) { + const array = new win.Float32Array( + audioData.numberOfFrames * audioData.numberOfChannels + ); + audioData.copyTo(array, o({ planeIndex: 0 })); + const transfer = new win.Array(); + transfer.push(array.buffer); + sink.node.port.postMessage(array, o({ transfer })); + audioData.close(); + }), + }); + track.writable = new win.WritableStream(sink); + return track; + } + } + win.MediaStreamTrackGenerator = exportFunction( + MediaStreamTrackGenerator, + window, + { allowCrossOriginArguments: true } + ); +} diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -12534,7 +12534,7 @@ # calls only for certain domains (ignored if above pref is true). - name: media.devices.enumerate.legacy.allowlist type: String - value: "slack.com,*.slack.com" + value: "slack.com,*.slack.com,riverside.fm,*.riverside.fm" mirror: never # WebRTC prefs follow