fix: keep autoplay fallback from saving mute

(cherry picked from commit f373bb9ce7)
This commit is contained in:
thePR0M3TH3AN committed 2026-07-26 12:34:15 -04:00
1 parent 2cda5025d4
commit 2f3028f2e4
7 files changed
+96 -10

No files matched your search

+12 -1
View File
@@ -232,7 +232,18 @@ export function createAuthSessionCoordinator(deps) {
} }
this.applyAuthenticatedUiState(); 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" }); this.updateShareNostrAuthState({ reason: "auth-login" });
if (typeof this.refreshUnreadDmIndicator === "function") { if (typeof this.refreshUnreadDmIndicator === "function") {
void this.refreshUnreadDmIndicator({ reason: "auth-login" }); void this.refreshUnreadDmIndicator({ reason: "auth-login" });
+1 -1
View File
@@ -237,7 +237,7 @@ export function createPlaybackCoordinator(deps) {
this.log("Autoplay failed:", err); this.log("Autoplay failed:", err);
if (!this.modalVideo.muted) { if (!this.modalVideo.muted) {
this.log("Falling back to muted autoplay."); this.log("Falling back to muted autoplay.");
this.modalVideo.muted = true; this.playbackService.muteForAutoplayFallback(this.modalVideo);
this.modalVideo.play().catch((err2) => { this.modalVideo.play().catch((err2) => {
this.log("Muted autoplay also failed:", err2); this.log("Muted autoplay also failed:", err2);
}); });
+15 -1
View File
@@ -1,6 +1,10 @@
import { userLogger } from "../utils/logger.js"; import { userLogger } from "../utils/logger.js";
import { PLAYBACK_START_TIMEOUT } from "../constants.js"; import { PLAYBACK_START_TIMEOUT } from "../constants.js";
import { getCurrentVideo, setCurrentVideo } from "../state/appState.js"; import { getCurrentVideo, setCurrentVideo } from "../state/appState.js";
import {
consumeAutoplayFallbackMute,
muteForAutoplayFallback,
} from "../utils/autoplayPreference.js";
import { import {
SimpleEventEmitter, SimpleEventEmitter,
extractWebSeedsFromMagnet, extractWebSeedsFromMagnet,
@@ -330,11 +334,17 @@ export class PlaybackService {
return; return;
} }
const storedUnmuted = localStorage.getItem("unmutedAutoplay"); 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; videoElement.muted = !userWantsUnmuted;
if (!videoElement.dataset.autoplayBound) { if (!videoElement.dataset.autoplayBound) {
videoElement.addEventListener("volumechange", () => { videoElement.addEventListener("volumechange", () => {
if (consumeAutoplayFallbackMute(videoElement)) {
return;
}
localStorage.setItem( localStorage.setItem(
"unmutedAutoplay", "unmutedAutoplay",
(!videoElement.muted).toString() (!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 * Registers a set of event listeners ("watchdogs") on the video element to detect
* when playback has stalled or failed. * when playback has stalled or failed.
+27
View File
@@ -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;
}
+2 -1
View File
@@ -30,6 +30,7 @@ import {
waitForActiveController as waitForActiveControllerHelper, waitForActiveController as waitForActiveControllerHelper,
} from "./webtorrentHelpers.js"; } from "./webtorrentHelpers.js";
import { devLogger, userLogger } from "./utils/logger.js"; import { devLogger, userLogger } from "./utils/logger.js";
import { muteForAutoplayFallback } from "./utils/autoplayPreference.js";
import { emit } from "./embedDiagnostics.js"; import { emit } from "./embedDiagnostics.js";
const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]); const DEFAULT_PROBE_TRACKERS = Object.freeze([...WSS_TRACKERS]);
@@ -561,7 +562,7 @@ export class TorrentClient {
return; return;
} }
this.log(`Retrying with muted autoplay (${context} path).`); this.log(`Retrying with muted autoplay (${context} path).`);
videoElement.muted = true; muteForAutoplayFallback(videoElement);
videoElement.play().catch((err2) => { videoElement.play().catch((err2) => {
this.log(`Muted autoplay also failed (${context} path):`, err2); this.log(`Muted autoplay also failed (${context} path):`, err2);
}); });
+27
View File
@@ -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", "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]);
});
+12 -6
View File
@@ -44,21 +44,27 @@ describe("PlaybackService", () => {
const service = new PlaybackService(); const service = new PlaybackService();
const video = document.createElement("video"); const video = document.createElement("video");
// Default: muted (unless localStorage says unmuted) // Default: sound on unless the viewer explicitly chose mute.
localStorage.removeItem("unmutedAutoplay"); localStorage.removeItem("unmutedAutoplay");
service.prepareVideoElement(video); service.prepareVideoElement(video);
assert.equal(video.muted, true); assert.equal(video.muted, false);
assert.equal(video.dataset.autoplayBound, "true"); assert.equal(video.dataset.autoplayBound, "true");
// Simulate volume change // Simulate an explicit mute choice.
video.muted = false; video.muted = true;
video.dispatchEvent(new window.Event("volumechange")); video.dispatchEvent(new window.Event("volumechange"));
assert.equal(localStorage.getItem("unmutedAutoplay"), "true"); assert.equal(localStorage.getItem("unmutedAutoplay"), "false");
// Test respecting stored value // Test respecting stored value
const video2 = document.createElement("video"); const video2 = document.createElement("video");
service.prepareVideoElement(video2); 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", () => { test("registerUrlPlaybackWatchdogs triggers onFallback on error", () => {