openvibers/openvibe.live Commits
Channel player on a phone: a tap on the speaker pops the volume slider up above it (#374)
Changes
@@@ -1523,7 +1523,17 @@@media (max-width: 480px) {#page-channel .vc-volume-group .volume-slider { display: none !important; }#page-channel #uptime-badge { display: none; }++ /* …except above the speaker for a few seconds after a tap on it (stream-player.js popVolume, 2026-10-26): the++ volume stays adjustable on a phone without taking room in the bar. */++ #page-channel .vc-volume-group { position: relative; }++ #page-channel .vc-volume-group.is-popped .volume-slider {++ display: block !important; position: absolute; left: -4px; bottom: calc(100% + 10px); z-index: 6;++ width: 150px !important; height: 40px; padding: 0 14px; opacity: 1 !important; box-sizing: border-box;++ border-radius: 999px; background: rgba(12, 15, 24, 0.92); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);++ animation: vc-volume-pop 0.18s ease-out;++ }}++@keyframes vc-volume-pop { from { opacity: 0; transform: translateY(4px); } }/* ── The player takes the stream's shape (2026-10-26) ──────────────────────────────player-shape.js sets --video-ar (the picture's width / height) on #video-container. The box stays the column's width,@@@ -2766,7 +2766,20 @@ function setupVideoControls() {// Start showing pause icon since autoplaybtnPlay.innerHTML = '<i class="fa-solid fa-pause"></i>';++ // A phone under 480px has no room for the slider in the bar (2026-10-25): a tap on the speaker also shows it, above++ // the button, for a few seconds (the owner, 2026-10-26: "can't easily adjust volume" on a phone). Not on an iPhone,++ // whose Safari ignores a page setting the volume; there the speaker mutes, and the phone's buttons set the level.++ const volGroup = btnVol.closest('.vc-volume-group');++ const popVolume = () => {++ if (!volGroup || !window.matchMedia || !matchMedia('(max-width: 480px)').matches || !_volumeSettable()) return;++ volGroup.classList.add('is-popped');++ clearTimeout(popVolume.timer);++ popVolume.timer = setTimeout(() => volGroup.classList.remove('is-popped'), 3500);++ };++ volSlider.onpointerdown = popVolume;++btnVol.onclick = () => {++ popVolume();muted = !muted;btnVol.innerHTML = muted? '<i class="fa-solid fa-volume-xmark"></i>'@@@ -2781,6 +2794,7 @@ function setupVideoControls() {};volSlider.oninput = () => {++ popVolume();const v = volSlider.value / 100;setVolume(v, { muted: v === 0 });muted = v === 0;@@@ -3391,6 +3405,14 @@ function _buildClipTrimModal(clipId, dur, defaultTitle) {overlay.onclick = (e) => { if (e.target === overlay) close(); };}++/** Whether this browser lets a page set a media element's volume (an iPhone's Safari does not: only mute). */++function _volumeSettable() {++ if (_volumeSettable.value == null) {++ try { const a = document.createElement('audio'); a.volume = 0.5; _volumeSettable.value = Math.abs(a.volume - 0.5) < 0.01; } catch { _volumeSettable.value = false; }++ }++ return _volumeSettable.value;++}++function setVolume(v) {const options = arguments[1] || {};const volume = Math.max(0, Math.min(1, Number(v) || 0));nnew file mode 100644@@@ -0,0 +1,41 @@++/**++ * The volume stays adjustable on a phone (owner, 2026-10-26: "can't easily adjust volume" on mobile). Under 480px the++ * channel player's bar has no room for the slider (2026-10-25), so a tap on the speaker also pops the slider up above++ * it for a few seconds (stream-player.js popVolume, channel.css .is-popped). Not where a page cannot set the volume++ * (an iPhone's Safari): there the speaker mutes and the phone's own buttons set the level.++ *++ * node test/phone-volume.test.js++ */++'use strict';++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const vm = require('vm');++++const root = path.join(__dirname, '..');++const player = fs.readFileSync(path.join(root, 'public/js/stream-player.js'), 'utf8');++const css = fs.readFileSync(path.join(root, 'public/css/features/channel.css'), 'utf8');++++// _volumeSettable: true where an audio element takes a volume, false where it stays 1 (iOS Safari), asked once.++const fn = /\/\*\* Whether this browser lets a page set[\s\S]*?\nfunction _volumeSettable\(\) \{[\s\S]*?\n\}\n/.exec(player);++assert.ok(fn, '_volumeSettable() is in stream-player.js');++for (const [fixed, want] of [[false, true], [true, false]]) {++ let made = 0;++ const ctx = { Math, document: { createElement: () => { made++; let v = 1; return { get volume() { return v; }, set volume(x) { if (!fixed) v = x; } }; } } };++ vm.createContext(ctx);++ vm.runInContext(`${fn[0]}; this.f = _volumeSettable;`, ctx);++ assert.strictEqual(ctx.f(), want, fixed ? 'an iPhone: no slider' : 'Android, desktop: the slider');++ ctx.f();++ assert.strictEqual(made, 1, 'asked once');++}++++// The speaker and the slider pop it up; it only pops on a narrow screen that can set the volume.++assert.match(player, /btnVol\.onclick = \(\) => \{\n\s+popVolume\(\);/);++assert.match(player, /volSlider\.oninput = \(\) => \{\n\s+popVolume\(\);/);++assert.match(player, /matchMedia\('\(max-width: 480px\)'\)\.matches \|\| !_volumeSettable\(\)\) return;/);++assert.match(player, /setTimeout\(\(\) => volGroup\.classList\.remove\('is-popped'\), 3500\)/);++// The bar still hides the slider under 480px; the popped one shows above the speaker.++const narrow = /@media \(max-width: 480px\) \{([\s\S]*?)\n\}\n/.exec(css.slice(css.indexOf("The player's controls on a narrow phone")));++assert.ok(narrow && /\.vc-volume-group \.volume-slider \{ display: none !important; \}/.test(narrow[1]));++assert.match(narrow[1], /\.vc-volume-group\.is-popped \.volume-slider \{\s*display: block !important; position: absolute;[^}]*bottom: calc\(100% \+ 10px\)/);++console.log('✅ phone volume: the slider pops above the speaker on a narrow phone that can set the volume');
@@ -1523,7 +1523,17 @@
@@ -1523,7 +1523,17 @@
@media (max-width: 480px) {
@media (max-width: 480px) {
#page-channel .vc-volume-group .volume-slider { display: none !important; }
#page-channel .vc-volume-group .volume-slider { display: none !important; }
#page-channel #uptime-badge { display: none; }
#page-channel #uptime-badge { display: none; }
+ /* …except above the speaker for a few seconds after a tap on it (stream-player.js popVolume, 2026-10-26): the
+ volume stays adjustable on a phone without taking room in the bar. */
+ #page-channel .vc-volume-group { position: relative; }
+ #page-channel .vc-volume-group.is-popped .volume-slider {
+ display: block !important; position: absolute; left: -4px; bottom: calc(100% + 10px); z-index: 6;
+ width: 150px !important; height: 40px; padding: 0 14px; opacity: 1 !important; box-sizing: border-box;
+ border-radius: 999px; background: rgba(12, 15, 24, 0.92); box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
+ animation: vc-volume-pop 0.18s ease-out;
+ }
}
}
+@keyframes vc-volume-pop { from { opacity: 0; transform: translateY(4px); } }
/* ── The player takes the stream's shape (2026-10-26) ──────────────────────────────
/* ── The player takes the stream's shape (2026-10-26) ──────────────────────────────
player-shape.js sets --video-ar (the picture's width / height) on #video-container. The box stays the column's width,
player-shape.js sets --video-ar (the picture's width / height) on #video-container. The box stays the column's width,
@@ -2766,7 +2766,20 @@ function setupVideoControls() {
@@ -2766,7 +2766,20 @@ function setupVideoControls() {
// Start showing pause icon since autoplay
// Start showing pause icon since autoplay
btnPlay.innerHTML = '<i class="fa-solid fa-pause"></i>';
btnPlay.innerHTML = '<i class="fa-solid fa-pause"></i>';
+ // A phone under 480px has no room for the slider in the bar (2026-10-25): a tap on the speaker also shows it, above
+ // the button, for a few seconds (the owner, 2026-10-26: "can't easily adjust volume" on a phone). Not on an iPhone,
+ // whose Safari ignores a page setting the volume; there the speaker mutes, and the phone's buttons set the level.
+ const volGroup = btnVol.closest('.vc-volume-group');
+ const popVolume = () => {
+ if (!volGroup || !window.matchMedia || !matchMedia('(max-width: 480px)').matches || !_volumeSettable()) return;
+ volGroup.classList.add('is-popped');
+ clearTimeout(popVolume.timer);
+ popVolume.timer = setTimeout(() => volGroup.classList.remove('is-popped'), 3500);
+ };
+ volSlider.onpointerdown = popVolume;
+
btnVol.onclick = () => {
btnVol.onclick = () => {
+ popVolume();
muted = !muted;
muted = !muted;
btnVol.innerHTML = muted
btnVol.innerHTML = muted
? '<i class="fa-solid fa-volume-xmark"></i>'
? '<i class="fa-solid fa-volume-xmark"></i>'
@@ -2781,6 +2794,7 @@ function setupVideoControls() {
@@ -2781,6 +2794,7 @@ function setupVideoControls() {
};
};
volSlider.oninput = () => {
volSlider.oninput = () => {
+ popVolume();
const v = volSlider.value / 100;
const v = volSlider.value / 100;
setVolume(v, { muted: v === 0 });
setVolume(v, { muted: v === 0 });
muted = v === 0;
muted = v === 0;
@@ -3391,6 +3405,14 @@ function _buildClipTrimModal(clipId, dur, defaultTitle) {
@@ -3391,6 +3405,14 @@ function _buildClipTrimModal(clipId, dur, defaultTitle) {
overlay.onclick = (e) => { if (e.target === overlay) close(); };
overlay.onclick = (e) => { if (e.target === overlay) close(); };
}
}
+/** Whether this browser lets a page set a media element's volume (an iPhone's Safari does not: only mute). */
+function _volumeSettable() {
+ if (_volumeSettable.value == null) {
+ try { const a = document.createElement('audio'); a.volume = 0.5; _volumeSettable.value = Math.abs(a.volume - 0.5) < 0.01; } catch { _volumeSettable.value = false; }
+ }
+ return _volumeSettable.value;
+}
+
function setVolume(v) {
function setVolume(v) {
const options = arguments[1] || {};
const options = arguments[1] || {};
const volume = Math.max(0, Math.min(1, Number(v) || 0));
const volume = Math.max(0, Math.min(1, Number(v) || 0));
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+/**
+ * The volume stays adjustable on a phone (owner, 2026-10-26: "can't easily adjust volume" on mobile). Under 480px the
+ * channel player's bar has no room for the slider (2026-10-25), so a tap on the speaker also pops the slider up above
+ * it for a few seconds (stream-player.js popVolume, channel.css .is-popped). Not where a page cannot set the volume
+ * (an iPhone's Safari): there the speaker mutes and the phone's own buttons set the level.
+ *
+ * node test/phone-volume.test.js
+ */
+'use strict';
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const root = path.join(__dirname, '..');
+const player = fs.readFileSync(path.join(root, 'public/js/stream-player.js'), 'utf8');
+const css = fs.readFileSync(path.join(root, 'public/css/features/channel.css'), 'utf8');
+
+// _volumeSettable: true where an audio element takes a volume, false where it stays 1 (iOS Safari), asked once.
+const fn = /\/\*\* Whether this browser lets a page set[\s\S]*?\nfunction _volumeSettable\(\) \{[\s\S]*?\n\}\n/.exec(player);
+assert.ok(fn, '_volumeSettable() is in stream-player.js');
+for (const [fixed, want] of [[false, true], [true, false]]) {
+ let made = 0;
+ const ctx = { Math, document: { createElement: () => { made++; let v = 1; return { get volume() { return v; }, set volume(x) { if (!fixed) v = x; } }; } } };
+ vm.createContext(ctx);
+ vm.runInContext(`${fn[0]}; this.f = _volumeSettable;`, ctx);
+ assert.strictEqual(ctx.f(), want, fixed ? 'an iPhone: no slider' : 'Android, desktop: the slider');
+ ctx.f();
+ assert.strictEqual(made, 1, 'asked once');
+}
+
+// The speaker and the slider pop it up; it only pops on a narrow screen that can set the volume.
+assert.match(player, /btnVol\.onclick = \(\) => \{\n\s+popVolume\(\);/);
+assert.match(player, /volSlider\.oninput = \(\) => \{\n\s+popVolume\(\);/);
+assert.match(player, /matchMedia\('\(max-width: 480px\)'\)\.matches \|\| !_volumeSettable\(\)\) return;/);
+assert.match(player, /setTimeout\(\(\) => volGroup\.classList\.remove\('is-popped'\), 3500\)/);
+// The bar still hides the slider under 480px; the popped one shows above the speaker.
+const narrow = /@media \(max-width: 480px\) \{([\s\S]*?)\n\}\n/.exec(css.slice(css.indexOf("The player's controls on a narrow phone")));
+assert.ok(narrow && /\.vc-volume-group \.volume-slider \{ display: none !important; \}/.test(narrow[1]));
+assert.match(narrow[1], /\.vc-volume-group\.is-popped \.volume-slider \{\s*display: block !important; position: absolute;[^}]*bottom: calc\(100% \+ 10px\)/);
+console.log('✅ phone volume: the slider pops above the speaker on a narrow phone that can set the volume');