diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx
index 9fc10bff..2a68ae31 100644
--- a/src/components/player/base/Container.tsx
+++ b/src/components/player/base/Container.tsx
@@ -4,6 +4,7 @@ import { OverlayDisplay } from "@/components/overlays/OverlayDisplay";
import { CastingInternal } from "@/components/player/internals/CastingInternal";
import { HeadUpdater } from "@/components/player/internals/HeadUpdater";
import { KeyboardEvents } from "@/components/player/internals/KeyboardEvents";
+import { MediaSession } from "@/components/player/internals/MediaSession";
import { MetaReporter } from "@/components/player/internals/MetaReporter";
import { ProgressSaver } from "@/components/player/internals/ProgressSaver";
import { ThumbnailScraper } from "@/components/player/internals/ThumbnailScraper";
@@ -91,6 +92,7 @@ export function Container(props: PlayerProps) {
+
diff --git a/src/components/player/internals/MediaSession.tsx b/src/components/player/internals/MediaSession.tsx
new file mode 100644
index 00000000..1dc7e737
--- /dev/null
+++ b/src/components/player/internals/MediaSession.tsx
@@ -0,0 +1,182 @@
+import { useCallback, useEffect, useRef } from "react";
+
+import { usePlayerStore } from "@/stores/player/store";
+
+import { usePlayerMeta } from "../hooks/usePlayerMeta";
+
+export function MediaSession() {
+ const { setDirectMeta } = usePlayerMeta();
+ const setShouldStartFromBeginning = usePlayerStore(
+ (s) => s.setShouldStartFromBeginning,
+ );
+
+ const shouldUpdatePositionState = useRef(false);
+ const lastPlaybackPosition = useRef(0);
+
+ const data = usePlayerStore.getState();
+
+ const changeEpisode = useCallback(
+ (change: number) => {
+ const nextEp = data.meta?.episodes?.find(
+ (v) => v.number === (data.meta?.episode?.number ?? 0) + change,
+ );
+
+ if (!data.meta || !nextEp) return;
+ const metaCopy = { ...data.meta };
+ metaCopy.episode = nextEp;
+ setShouldStartFromBeginning(true);
+ setDirectMeta(metaCopy);
+ },
+ [data.meta, setDirectMeta, setShouldStartFromBeginning],
+ );
+
+ const updatePositionState = useCallback(
+ (position: number) => {
+ // If the updated position needs to be buffered, queue an update
+ if (position > data.progress.buffered) {
+ shouldUpdatePositionState.current = true;
+ }
+ if (position > data.progress.duration) return;
+
+ lastPlaybackPosition.current = data.progress.time;
+ navigator.mediaSession.setPositionState({
+ duration: data.progress.duration,
+ playbackRate: data.mediaPlaying.playbackRate,
+ position,
+ });
+ },
+ [
+ data.mediaPlaying.playbackRate,
+ data.progress.buffered,
+ data.progress.duration,
+ data.progress.time,
+ ],
+ );
+
+ useEffect(() => {
+ if (!("mediaSession" in navigator)) return;
+
+ // If the media is paused, update the navigator
+ if (data.mediaPlaying.isPaused) {
+ navigator.mediaSession.playbackState = "paused";
+ } else {
+ navigator.mediaSession.playbackState = "playing";
+ }
+ }, [data.mediaPlaying.isPaused]);
+
+ useEffect(() => {
+ if (!("mediaSession" in navigator)) return;
+
+ updatePositionState(data.progress.time);
+ }, [data.progress.time, data.mediaPlaying.playbackRate, updatePositionState]);
+
+ useEffect(() => {
+ if (!("mediaSession" in navigator)) return;
+ // If not already updating the position state, and the media is loading, queue an update
+ if (!shouldUpdatePositionState.current && data.mediaPlaying.isLoading) {
+ shouldUpdatePositionState.current = true;
+ }
+
+ // If the user has skipped (or MediaSession desynced) by more than 5 seconds, queue an update
+ if (
+ Math.abs(data.progress.time - lastPlaybackPosition.current) >= 5 &&
+ !data.mediaPlaying.isLoading &&
+ !shouldUpdatePositionState.current
+ ) {
+ shouldUpdatePositionState.current = true;
+ }
+
+ // If not loading and the position state is queued, update it
+ if (shouldUpdatePositionState.current && !data.mediaPlaying.isLoading) {
+ shouldUpdatePositionState.current = false;
+ updatePositionState(data.progress.time);
+ }
+
+ lastPlaybackPosition.current = data.progress.time;
+ }, [updatePositionState, data.progress.time, data.mediaPlaying.isLoading]);
+
+ useEffect(() => {
+ if (
+ !("mediaSession" in navigator) ||
+ (!data.mediaPlaying.isLoading &&
+ data.mediaPlaying.isPlaying &&
+ !data.display)
+ )
+ return;
+
+ let title: string | undefined;
+ let artist: string | undefined;
+
+ if (data.meta?.type === "movie") {
+ title = data.meta?.title;
+ } else if (data.meta?.type === "show") {
+ artist = data.meta?.title;
+ title = `S${data.meta?.season?.number} E${data.meta?.episode?.number}: ${data.meta?.episode?.title}`;
+ }
+
+ navigator.mediaSession.metadata = new MediaMetadata({
+ title,
+ artist,
+ artwork: [
+ {
+ src: data.meta?.poster ?? "",
+ sizes: "342x513",
+ type: "image/png",
+ },
+ ],
+ });
+
+ navigator.mediaSession.setActionHandler("play", () => {
+ if (data.mediaPlaying.isLoading) return;
+ data.display?.play();
+
+ updatePositionState(data.progress.time);
+ });
+
+ navigator.mediaSession.setActionHandler("pause", () => {
+ if (data.mediaPlaying.isLoading) return;
+ data.display?.pause();
+
+ updatePositionState(data.progress.time);
+ });
+
+ navigator.mediaSession.setActionHandler("seekto", (e) => {
+ if (!e.seekTime) return;
+ data.display?.setTime(e.seekTime);
+ updatePositionState(e.seekTime);
+ });
+
+ if ((data.meta?.episode?.number ?? 1) !== 1) {
+ navigator.mediaSession.setActionHandler("previoustrack", () => {
+ changeEpisode(-1);
+ });
+ } else {
+ navigator.mediaSession.setActionHandler("previoustrack", null);
+ }
+
+ if (data.meta?.episode?.number !== data.meta?.episodes?.length) {
+ navigator.mediaSession.setActionHandler("nexttrack", () => {
+ changeEpisode(1);
+ });
+ } else {
+ navigator.mediaSession.setActionHandler("nexttrack", null);
+ }
+ }, [
+ changeEpisode,
+ updatePositionState,
+ data.mediaPlaying.hasPlayedOnce,
+ data.mediaPlaying.isLoading,
+ data.progress.duration,
+ data.progress.time,
+ data.meta?.episode?.number,
+ data.meta?.episodes?.length,
+ data.display,
+ data.mediaPlaying,
+ data.meta?.episode?.title,
+ data.meta?.title,
+ data.meta?.type,
+ data.meta?.poster,
+ data.meta?.season?.number,
+ ]);
+ return null;
+}