diff --git a/web/src/components/player/dynamic/DynamicVideoController.ts b/web/src/components/player/dynamic/DynamicVideoController.ts index e9da0064d..151ea4022 100644 --- a/web/src/components/player/dynamic/DynamicVideoController.ts +++ b/web/src/components/player/dynamic/DynamicVideoController.ts @@ -6,6 +6,7 @@ import { calculateInpointOffset, calculateSeekPosition, } from "@/utils/videoUtil"; +import { playWithTemporaryMuteFallback } from "@/utils/videoUtil.ts"; type PlayerMode = "playback" | "scrubbing"; @@ -107,7 +108,7 @@ export class DynamicVideoController { return new Promise((resolve) => { const onSeekedHandler = () => { this.playerController.removeEventListener("seeked", onSeekedHandler); - this.playerController.play(); + playWithTemporaryMuteFallback(this.playerController); resolve(undefined); }; diff --git a/web/src/utils/videoUtil.ts b/web/src/utils/videoUtil.ts index 0b09ac061..d6ab203e9 100644 --- a/web/src/utils/videoUtil.ts +++ b/web/src/utils/videoUtil.ts @@ -78,3 +78,20 @@ export function calculateSeekPosition( return seekSeconds >= 0 ? seekSeconds : undefined; } + +/** + * Attempts to play the video, and if it fails due to a NotAllowedError (often caused by browser autoplay restrictions), + * it temporarily mutes the video and tries to play again. + * @param video - The HTMLVideoElement to play + */ +export function playWithTemporaryMuteFallback(video: HTMLVideoElement) { + return video.play().catch((error: { name?: string }) => { + if (error.name === "NotAllowedError" && !video.muted) { + video.muted = true; + + return video.play().catch(() => undefined); + } + + throw error; + }); +}