From f373bb9ce77d7e9b5896eb55838364e5469f3878 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sun, 26 Jul 2026 08:58:58 -0400 Subject: [PATCH] fix: keep autoplay fallback from saving mute --- js/app/authSessionCoordinator.js | 13 +++++++++++- js/app/playbackCoordinator.js | 2 +- js/services/playbackService.js | 16 ++++++++++++++- js/utils/autoplayPreference.js | 27 +++++++++++++++++++++++++ js/webtorrent.js | 3 ++- tests/app/login-defers-dms.test.mjs | 27 +++++++++++++++++++++++++ tests/services/playbackService.test.mjs | 18 +++++++++++------ 7 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 js/utils/autoplayPreference.js diff --git a/js/app/authSessionCoordinator.js b/js/app/authSessionCoordinator.js index 94986672..1c50994d 100644 --- a/js/app/authSessionCoordinator.js +++ b/js/app/authSessionCoordinator.js @@ -232,7 +232,18 @@ export function createAuthSessionCoordinator(deps) { } this.applyAuthenticatedUiState(); - this.commentController?.refreshAuthState?.(); + const activeModalVideo = + typeof this.videoModal?.getCurrentVideo === "function" + ? this.videoModal.getCurrentVideo() + : this.commentController?.currentVideo || null; + if (this.commentController && activeModalVideo) { + // Login can reconfigure relay-backed services. Reload the active thread + // instead of only enabling its composer so readable comments survive + // the transition from guest to authenticated state. + this.commentController.load(activeModalVideo); + } else { + this.commentController?.refreshAuthState?.(); + } this.updateShareNostrAuthState({ reason: "auth-login" }); if (typeof this.refreshUnreadDmIndicator === "function") { void this.refreshUnreadDmIndicator({ reason: "auth-login" }); diff --git a/js/app/playbackCoordinator.js b/js/app/playbackCoordinator.js index 6a4e2afe..f1ccf4ed 100644 --- a/js/app/playbackCoordinator.js +++ b/js/app/playbackCoordinator.js @@ -237,7 +237,7 @@ export function createPlaybackCoordinator(deps) { this.log("Autoplay failed:", err); if (!this.modalVideo.muted) { this.log("Falling back to muted autoplay."); - this.modalVideo.muted = true; + this.playbackService.muteForAutoplayFallback(this.modalVideo); this.modalVideo.play().catch((err2) => { this.log("Muted autoplay also failed:", err2); }); diff --git a/js/services/playbackService.js b/js/services/playbackService.js index e6b74bc6..f9adf2de 100644 --- a/js/services/playbackService.js +++ b/js/services/playbackService.js @@ -1,6 +1,10 @@ import { userLogger } from "../utils/logger.js"; import { PLAYBACK_START_TIMEOUT } from "../constants.js"; import { getCurrentVideo, setCurrentVideo } from "../state/appState.js"; +import { + consumeAutoplayFallbackMute, + muteForAutoplayFallback, +} from "../utils/autoplayPreference.js"; import { SimpleEventEmitter, extractWebSeedsFromMagnet, @@ -330,11 +334,17 @@ export class PlaybackService { return; } const storedUnmuted = localStorage.getItem("unmutedAutoplay"); - const userWantsUnmuted = storedUnmuted === "true"; + // Sound is the default for an explicit watch action. Browsers may still + // reject unmuted autoplay, in which case the coordinator retries muted. + // Preserve a viewer's explicit mute choice across subsequent videos. + const userWantsUnmuted = storedUnmuted !== "false"; videoElement.muted = !userWantsUnmuted; if (!videoElement.dataset.autoplayBound) { videoElement.addEventListener("volumechange", () => { + if (consumeAutoplayFallbackMute(videoElement)) { + return; + } localStorage.setItem( "unmutedAutoplay", (!videoElement.muted).toString() @@ -344,6 +354,10 @@ export class PlaybackService { } } + muteForAutoplayFallback(videoElement) { + return muteForAutoplayFallback(videoElement); + } + /** * Registers a set of event listeners ("watchdogs") on the video element to detect * when playback has stalled or failed. diff --git a/js/utils/autoplayPreference.js b/js/utils/autoplayPreference.js new file mode 100644 index 00000000..f5cce19f --- /dev/null +++ b/js/utils/autoplayPreference.js @@ -0,0 +1,27 @@ +const FALLBACK_MUTE_DATASET_KEY = "bitvidAutoplayFallbackMute"; + +/** + * Mute playback after an unmuted autoplay rejection without recording the + * browser-policy fallback as a viewer preference. + */ +export function muteForAutoplayFallback(videoElement) { + if (!videoElement || videoElement.muted) { + return false; + } + + if (videoElement.dataset) { + videoElement.dataset[FALLBACK_MUTE_DATASET_KEY] = "true"; + } + videoElement.muted = true; + return true; +} + +/** Consume the marker attached to the next fallback-generated volume change. */ +export function consumeAutoplayFallbackMute(videoElement) { + if (videoElement?.dataset?.[FALLBACK_MUTE_DATASET_KEY] !== "true") { + return false; + } + + delete videoElement.dataset[FALLBACK_MUTE_DATASET_KEY]; + return true; +} diff --git a/js/webtorrent.js b/js/webtorrent.js index 5264177c..6c5ee499 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -30,6 +30,7 @@ import { waitForActiveController as waitForActiveControllerHelper, } from "./webtorrentHelpers.js"; import { devLogger, userLogger } from "./utils/logger.js"; +import { muteForAutoplayFallback } from "./utils/autoplayPreference.js"; import { emit } from "./embedDiagnostics.js"; const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]); @@ -561,7 +562,7 @@ export class TorrentClient { return; } this.log(`Retrying with muted autoplay (${context} path).`); - videoElement.muted = true; + muteForAutoplayFallback(videoElement); videoElement.play().catch((err2) => { this.log(`Muted autoplay also failed (${context} path):`, err2); }); diff --git a/tests/app/login-defers-dms.test.mjs b/tests/app/login-defers-dms.test.mjs index 6f37003d..68620d15 100644 --- a/tests/app/login-defers-dms.test.mjs +++ b/tests/app/login-defers-dms.test.mjs @@ -151,3 +151,30 @@ test("login does not eagerly load DM history; it is deferred", async () => { "the lightweight unread DM indicator should still run so the badge stays accurate", ); }); + +test("login reloads the open video comment thread", async () => { + globalThis.window.location = { search: "", href: "http://localhost/" }; + + const coord = makeCoordinator({ + loadDirectMessages: async () => ({ ok: true }), + refreshUnreadDmIndicator: async () => {}, + }); + const activeVideo = { id: "b".repeat(64), title: "Open video" }; + const loadedVideos = []; + coord.videoModal = { getCurrentVideo: () => activeVideo }; + coord.commentController = { + load: (video) => loadedVideos.push(video), + refreshAuthState: () => { + throw new Error("an open thread should be reloaded, not only refreshed"); + }, + }; + + await coord.handleAuthLogin({ + pubkey: ACTIVE_PUBKEY, + identityChanged: true, + postLogin: { profile: { name: "tester" } }, + postLoginPromise: Promise.resolve({ profile: { name: "tester" } }), + }); + + assert.deepEqual(loadedVideos, [activeVideo]); +}); diff --git a/tests/services/playbackService.test.mjs b/tests/services/playbackService.test.mjs index 115ce348..a6dcf57d 100644 --- a/tests/services/playbackService.test.mjs +++ b/tests/services/playbackService.test.mjs @@ -44,21 +44,27 @@ describe("PlaybackService", () => { const service = new PlaybackService(); const video = document.createElement("video"); - // Default: muted (unless localStorage says unmuted) + // Default: sound on unless the viewer explicitly chose mute. localStorage.removeItem("unmutedAutoplay"); service.prepareVideoElement(video); - assert.equal(video.muted, true); + assert.equal(video.muted, false); assert.equal(video.dataset.autoplayBound, "true"); - // Simulate volume change - video.muted = false; + // Simulate an explicit mute choice. + video.muted = true; video.dispatchEvent(new window.Event("volumechange")); - assert.equal(localStorage.getItem("unmutedAutoplay"), "true"); + assert.equal(localStorage.getItem("unmutedAutoplay"), "false"); // Test respecting stored value const video2 = document.createElement("video"); service.prepareVideoElement(video2); - assert.equal(video2.muted, false); + assert.equal(video2.muted, true); + + // A browser-policy fallback is temporary and must not become a saved mute. + localStorage.setItem("unmutedAutoplay", "true"); + video2.muted = false; + service.muteForAutoplayFallback(video2); + assert.equal(localStorage.getItem("unmutedAutoplay"), "true"); }); test("registerUrlPlaybackWatchdogs triggers onFallback on error", () => {