openvibers/openvibe.live Commits

Keep cross-site Live picture playing when sound autoplay is blocked (#835)

OP OpenVibers committed 2 days ago · 6706209bbe · 2 files, +37 −1

Changes

ddiff --git a/public/js/channel-embed.js b/public/js/channel-embed.js
iindex 7a319d63..9798a4bc 100644
---- a/public/js/channel-embed.js
++++ b/public/js/channel-embed.js
@@@ -7,6 +7,7 @@
let state = { live: false, muted: true, playing: false, title: '', username: user, viewers: 0 };
let currentStreamId = null;
let requestedSound = false;
++ let pausedByParent = false;
const approved18 = new Set();
const send = () => { if (parent !== window) parent.postMessage({ type: 'ov-embed', event: 'state', ...state }, '*'); };
const paint = () => { document.getElementById('live').hidden = !state.live; send(); };
@@@ -23,12 +24,14 @@
video.muted = true;
state.muted = true;
send();
++ video.play().catch(() => {});
});
state.muted = wanted;
send();
};
const setPlaying = (playing) => {
const wanted = !!playing;
++ pausedByParent = !wanted;
if (state.playing !== wanted) document.getElementById('btn-play-pause')?.click();
state.playing = wanted;
send();
@@@ -43,6 +46,19 @@
video.addEventListener('volumechange', () => { state.muted = video.muted; send(); });
video.addEventListener('playing', () => { state.playing = true; send(); });
video.addEventListener('pause', () => { state.playing = false; send(); });
++ video.addEventListener('canplay', () => {
++ const streamId = currentStreamId;
++ setTimeout(() => {
++ if (streamId === null || currentStreamId !== streamId || pausedByParent || !video.paused || video.muted || video.readyState < 2) return;
++ // An unmuted handoff may be blocked by autoplay. Keep the picture moving until the next tap for sound.
++ requestedSound = false;
++ window.OVChannelEmbedForceMuted = true;
++ video.muted = true;
++ state.muted = true;
++ send();
++ video.play().catch(() => {});
++ }, 1500);
++ });
document.getElementById('video-container').addEventListener('click', (event) => {
if (state.live && !event.target.closest('#gate')) setMuted(!state.muted);
});
ddiff --git a/test/channel-embed.test.js b/test/channel-embed.test.js
iindex 722a56ac..2ea07a77 100644
---- a/test/channel-embed.test.js
++++ b/test/channel-embed.test.js
@@@ -59,6 +59,13 @@ assert.strictEqual(video.muted, false, 'allowlisted parent can control playback'
const simulate = async (payload, preSound = false) => {
const elements = Object.fromEntries(['video-element', 'live', 'offline', 'gate', 'continue', 'video-container', 'btn-volume', 'btn-play-pause'].map((id) => [id, { hidden: false, style: {}, addEventListener() {}, click() {} }]));
++ const listeners = {};
++ const timers = [];
++ Object.assign(elements['video-element'], {
++ paused: true, readyState: 0,
++ play() { this.paused = false; return Promise.resolve(); },
++ addEventListener(type, fn) { (listeners[type] ||= []).push(fn); },
++ });
const playerCalls = [];
const sent = [];
let message;
@@@ -67,12 +74,13 @@ assert.strictEqual(video.muted, false, 'allowlisted parent can control playback'
parent: { postMessage(data, target) { sent.push({ data, target }); } }, location: { pathname: '/embed/alice' },
document: { getElementById(id) { return elements[id]; } }, addEventListener(type, fn) { if (type === 'message') message = fn; }, setInterval() {},
fetch: async () => ({ ok: true, json: async () => payload }), initPlayer(stream) { playerCalls.push({ stream, muted: elements['video-element'].muted, forceMuted: browser.window.OVChannelEmbedForceMuted }); },
++ setTimeout(fn) { timers.push(fn); },
};
browser.window.parent = browser.parent;
vm.runInNewContext(clientSource, browser);
if (preSound) message({ origin: 'https://openvibe.live', source: browser.parent, data: { type: 'ov-embed', cmd: 'unmute' } });
await new Promise((resolve) => setImmediate(resolve));
-- return { elements, playerCalls, sent };
++ return { elements, playerCalls, sent, listeners, timers, message, browser };
};
const liveResult = await simulate({ channel: { username: 'alice', display_name: 'Alice' }, streams: [{ id: 2, is_live: true, title: 'Public show' }] });
assert.strictEqual(liveResult.playerCalls.length, 1, 'live channel starts the shared player');
@@@ -80,6 +88,18 @@ assert.strictEqual(video.muted, false, 'allowlisted parent can control playback'
const carried = await simulate({ channel: { username: 'alice' }, streams: [{ id: 4, is_live: true }] }, true);
assert.strictEqual(carried.playerCalls[0].forceMuted, false, 'sound intent received before the new stream starts is preserved');
assert.strictEqual(carried.playerCalls[0].muted, false, 'the carried stream starts with sound when allowed');
++ const blocked = await simulate({ channel: { username: 'alice' }, streams: [{ id: 5, is_live: true }] }, true);
++ const blockedVideo = blocked.elements['video-element'];
++ blockedVideo.readyState = 4; blockedVideo.muted = false;
++ blocked.listeners.canplay[0](); blocked.timers.forEach((fn) => fn());
++ assert.strictEqual(blockedVideo.muted, true, 'blocked unmuted autoplay falls back to muted video');
++ assert.strictEqual(blockedVideo.paused, false, 'muted fallback starts playback');
++ assert.strictEqual(blocked.browser.window.OVChannelEmbedForceMuted, true, 'the fallback survives reconnects');
++ const paused = await simulate({ channel: { username: 'alice' }, streams: [{ id: 6, is_live: true }] }, true);
++ paused.message({ origin: 'https://openvibe.live', source: paused.browser.parent, data: { type: 'ov-embed', cmd: 'pause' } });
++ paused.elements['video-element'].readyState = 4; paused.elements['video-element'].muted = false;
++ paused.listeners.canplay[0](); paused.timers.forEach((fn) => fn());
++ assert.strictEqual(paused.elements['video-element'].paused, true, 'explicit parent pause is respected');
const offlineResult = await simulate({ channel: { username: 'alice', display_name: 'Alice' }, streams: [] });
assert.strictEqual(offlineResult.elements.offline.style.display, 'block', 'offline channel shows the offline card');
const gatedResult = await simulate({ channel: { username: 'alice' }, streams: [{ id: 3, is_live: true, is_nsfw: true }] });