Files
Haven/test/appVoiceScreenPip.test.js
Bernardo Costa c3ade67336 Screen sharing: drop the native GStreamer path, add a bitrate setting, fix black tiles on GPU encoders (#5672)
The native GStreamer screen share path is gone: in a peer-to-peer mesh every
hardware and OS combination behaved differently, and keeping them in step cost
more than it saved. Screen sharing uses Chromium's own capture everywhere.

- Screen share bitrate is a setting: a stepper from 300 to 10,000 Kbps or
  unlimited, with press-and-hold repeat and typed entry. Changes apply live,
  including to a share going through the voice relay (debounced, so a hold
  becomes one update). The default stays 1080p at 30 fps with an 8,000 Kbps cap.
- Black screen share tiles on GPU encoders: the sharer asks for a fresh
  keyframe after negotiation, and the watchdog only re-adopts tracks a tile is
  not already showing.
- Relay screen publishing is serialized and tied to the share it was started
  for, so a stop, a new share or a relay restart can never publish a stale
  producer, and a failed first publish retries on its own.
2026-10-02 12:12:31 -04:00

188 lines
6.1 KiB
JavaScript

'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
// The voice UI is spread over four modules that the app merges into one
// object (the screen PiP code lives in app-stream-tiles.js); load them the
// same way.
const FILES = ['app-voice.js', 'app-screen-webcam.js', 'app-stream-tiles.js', 'app-music.js'];
const SOURCES = FILES.map(name => fs.readFileSync(path.join(__dirname, '..', 'public/js/modules', name), 'utf8'));
function loadMethods(globals = {}) {
const methods = {};
SOURCES.forEach((source, i) => {
const context = vm.createContext({ module: { exports: {} }, t: key => key, ...globals });
vm.runInContext(source.replace(/^export default/m, 'module.exports ='), context, {
filename: FILES[i],
});
Object.assign(methods, context.module.exports);
});
return methods;
}
function createClassList() {
const values = new Set();
return {
add: value => values.add(value),
remove: value => values.delete(value),
contains: value => values.has(value),
};
}
test('a reset invalidates a pending screen PiP continuation', async () => {
let resolvePictureInPicture;
let exitCalls = 0;
const button = { textContent: '', title: '' };
const video = {
srcObject: { id: 'old-stream' },
disablePictureInPicture: false,
requestPictureInPicture: () => new Promise(resolve => {
resolvePictureInPicture = resolve;
}),
addEventListener() {},
};
const tile = {
classList: createClassList(),
contains: element => element === video,
querySelector: selector => selector === 'video' ? video : button,
};
const document = {
pictureInPictureEnabled: true,
pictureInPictureElement: null,
getElementById: id => id === 'screen-tile-7' ? tile : null,
exitPictureInPicture: async () => {
exitCalls++;
document.pictureInPictureElement = null;
},
querySelector: () => null,
};
const methods = loadMethods({ document });
const app = Object.assign({ _updateStreamContainerCollapse() {} }, methods);
app._popOutStream(tile, 7);
app._removeScreenSharePiP(7);
document.pictureInPictureElement = video;
resolvePictureInPicture();
await Promise.resolve();
await Promise.resolve();
assert.equal(exitCalls, 1);
assert.equal(tile.classList.contains('stream-popped-out'), false);
assert.equal(button.title, 'media.pop_out_stream');
});
test('screen PiP cleanup restores the previous track onended handler', () => {
let previousCalls = 0;
let pipCalls = 0;
const previousOnEnded = () => { previousCalls++; };
const track = { onended: previousOnEnded };
const document = {
pictureInPictureElement: null,
getElementById: () => null,
querySelector: () => null,
};
const methods = loadMethods({ document });
const app = Object.assign({ _updateStreamContainerCollapse() {} }, methods);
app._bindScreenPipTrack(7, track, () => { pipCalls++; });
const wrappedOnEnded = track.onended;
wrappedOnEnded();
assert.equal(previousCalls, 1);
assert.equal(pipCalls, 1);
app._removeScreenSharePiP(7);
assert.equal(track.onended, previousOnEnded);
assert.equal(app._screenPipTrackCleanups.has(7), false);
});
test('an old PiP completion does not close a newer request on the same video', async () => {
const resolvers = [];
let exitCalls = 0;
const button = { textContent: '', title: '' };
const video = {
srcObject: { id: 'current-stream' },
disablePictureInPicture: false,
requestPictureInPicture: () => new Promise(resolve => { resolvers.push(resolve); }),
addEventListener() {},
};
const tile = {
classList: createClassList(),
contains: element => element === video,
querySelector: selector => selector === 'video' ? video : button,
};
const document = {
pictureInPictureEnabled: true,
pictureInPictureElement: null,
getElementById: id => id === 'screen-tile-7' ? tile : null,
exitPictureInPicture: async () => { exitCalls++; },
querySelector: () => null,
};
const methods = loadMethods({ document });
const app = Object.assign({ _updateStreamContainerCollapse() {} }, methods);
app._popOutStream(tile, 7);
app._removeScreenSharePiP(7);
app._popOutStream(tile, 7);
document.pictureInPictureElement = video;
resolvers[1]();
await Promise.resolve();
await Promise.resolve();
resolvers[0]();
await Promise.resolve();
await Promise.resolve();
assert.equal(exitCalls, 0);
assert.equal(tile.classList.contains('stream-popped-out'), true);
assert.equal(button.title, 'media.pop_in_stream');
});
test('a rejected replacement adopts an older PiP that finished while it was pending', async () => {
const requests = [];
let exitCalls = 0;
let fallbackCalls = 0;
const button = { textContent: '', title: '' };
const video = {
srcObject: { id: 'current-stream' },
disablePictureInPicture: false,
requestPictureInPicture: () => new Promise((resolve, reject) => {
requests.push({ resolve, reject });
}),
addEventListener() {},
};
const tile = {
classList: createClassList(),
contains: element => element === video,
querySelector: selector => selector === 'video' ? video : button,
};
const document = {
pictureInPictureEnabled: true,
pictureInPictureElement: null,
getElementById: id => id === 'screen-tile-7' ? tile : null,
exitPictureInPicture: async () => { exitCalls++; },
querySelector: () => null,
};
const methods = loadMethods({ document });
const app = Object.assign({ _updateStreamContainerCollapse() {} }, methods);
app._popOutStreamWindow = () => { fallbackCalls++; };
app._popOutStream(tile, 7);
app._removeScreenSharePiP(7);
app._popOutStream(tile, 7);
document.pictureInPictureElement = video;
requests[0].resolve();
await Promise.resolve();
await Promise.resolve();
requests[1].reject(new Error('replacement rejected'));
await Promise.resolve();
await Promise.resolve();
assert.equal(exitCalls, 0);
assert.equal(fallbackCalls, 0);
assert.equal(tile.classList.contains('stream-popped-out'), true);
assert.equal(button.title, 'media.pop_in_stream');
});