Handle media sessions

This commit is contained in:
2025-10-13 23:46:18 +02:00
parent c356fa10ec
commit bcc084e083
4 changed files with 163 additions and 12 deletions

View File

@@ -11,7 +11,8 @@ import {
import type { VideoPlayerBase } from "./types/VideoPlayerBase";
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
import { VideoPlayerEvents } from "./VideoPlayerEvents";
import { WebEventEmiter } from "./WebEventEmiter";
import { MediaSessionHandler } from "./web/MediaSession";
import { WebEventEmiter } from "./web/WebEventEmiter";
import videojs from "video.js";
type VideoJsPlayer = ReturnType<typeof videojs>;
@@ -33,6 +34,7 @@ type VideoJsTextTracks = {
class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
protected video: HTMLVideoElement;
public player: VideoJsPlayer;
private mediaSession: MediaSessionHandler;
constructor(source: VideoSource | VideoConfig | VideoPlayerSource) {
const video = document.createElement("video");
@@ -42,6 +44,7 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
this.video = video;
this.player = player;
this.mediaSession = new MediaSessionHandler(this.player);
this.replaceSourceAsync(source);
}
@@ -153,40 +156,48 @@ class VideoPlayer extends VideoPlayerEvents implements VideoPlayerBase {
return "auto";
}
set mixAudioMode(_: MixAudioMode) { }
set mixAudioMode(_: MixAudioMode) {}
// Ignore Silent Switch Mode
get ignoreSilentSwitchMode(): IgnoreSilentSwitchMode {
return "auto";
}
set ignoreSilentSwitchMode(_: IgnoreSilentSwitchMode) { }
set ignoreSilentSwitchMode(_: IgnoreSilentSwitchMode) {}
// Play In Background
get playInBackground(): boolean {
return true;
}
set playInBackground(_: boolean) { }
set playInBackground(_: boolean) {}
// Play When Inactive
get playWhenInactive(): boolean {
return true;
}
set playWhenInactive(_: boolean) { }
set playWhenInactive(_: boolean) {}
// Is Playing
get isPlaying(): boolean {
return !this.player.paused();
}
get showNotificationControls(): boolean {
return this.mediaSession.enabled;
}
set showNotificationControls(value: boolean) {
if (value) this.mediaSession.enable();
else this.mediaSession.disable();
}
async initialize(): Promise<void> {
// noop on web
}
async preload(): Promise<void> {
this.player.load()
this.player.load();
}
/**

View File

@@ -141,7 +141,7 @@ interface NativeDrmParams extends DrmParams {
type?: string;
}
interface CustomVideoMetadata {
export interface CustomVideoMetadata {
title?: string;
subtitle?: string;
description?: string;

View File

@@ -0,0 +1,140 @@
import type videojs from "video.js";
import type { CustomVideoMetadata } from "../types/VideoConfig";
type VideoJsPlayer = ReturnType<typeof videojs>;
const mediaSession = window.navigator.mediaSession;
export class MediaSessionHandler {
enabled: boolean = false;
constructor(private player: VideoJsPlayer) {}
enable() {
this.enabled = true;
const defaultSkipTime = 15;
const actionHandlers = [
[
"play",
() => {
this.player.play();
},
],
[
"pause",
() => {
this.player.pause();
},
],
[
"stop",
() => {
this.player.pause();
this.player.currentTime(0);
},
],
// videojs-contrib-ads
[
"seekbackward",
(details: MediaSessionActionDetails) => {
if (this.player.usingPlugin("ads") && this.player.ads.inAdBreak()) {
return;
}
this.player.currentTime(
Math.max(
0,
(this.player.currentTime() ?? 0) -
(details.seekOffset || defaultSkipTime),
),
);
},
],
[
"seekforward",
(details: MediaSessionActionDetails) => {
if (this.player.usingPlugin("ads") && this.player.ads.inAdBreak()) {
return;
}
this.player.currentTime(
Math.min(
this.player.duration() ?? 0,
(this.player.currentTime() ?? 0) +
(details.seekOffset || defaultSkipTime),
),
);
},
],
[
"seekto",
(details: MediaSessionActionDetails) => {
if (this.player.usingPlugin("ads") && this.player.ads.inAdBreak()) {
return;
}
this.player.currentTime(details.seekTime);
},
],
] as const;
for (const [action, handler] of actionHandlers) {
try {
mediaSession.setActionHandler(action, handler);
} catch {
this.player.log.debug(
`Couldn't register media session action "${action}".`,
);
}
}
const onPlaying = () => {
mediaSession.playbackState = "playing";
};
const onPaused = () => {
mediaSession.playbackState = "paused";
};
const onTimeUpdate = () => {
const dur = this.player.duration();
if (Number.isFinite(dur)) {
mediaSession.setPositionState({
duration: dur,
playbackRate: this.player.playbackRate(),
position: this.player.currentTime(),
});
}
};
this.player.on("playing", onPlaying);
this.player.on("paused", onPaused);
if ("setPositionState" in mediaSession) {
this.player.on("timeupdate", onTimeUpdate);
}
this.disable = () => {
this.enabled = false;
this.player.off("playing", onPlaying);
this.player.off("paused", onPaused);
if ("setPositionState" in mediaSession) {
this.player.off("timeupdate", onTimeUpdate);
}
mediaSession.metadata = null;
for (const [action, _] of actionHandlers) {
try {
mediaSession.setActionHandler(action, null);
} catch {}
}
};
}
disable() {}
updateMediaSession(metadata: CustomVideoMetadata) {
mediaSession.metadata = new window.MediaMetadata({
title: metadata.title,
album: metadata.subtitle,
artist: metadata.artist,
artwork: metadata.imageUri ? [{ src: metadata.imageUri }] : [],
});
}
}

View File

@@ -8,10 +8,10 @@ import type {
onVolumeChangeData,
AllPlayerEvents as PlayerEvents,
TimedMetadata,
} from "./types/Events";
import type { TextTrack } from "./types/TextTrack";
import type { VideoRuntimeError } from "./types/VideoError";
import type { VideoPlayerStatus } from "./types/VideoPlayerStatus";
} from "../types/Events";
import type { TextTrack } from "../types/TextTrack";
import type { VideoRuntimeError } from "../types/VideoError";
import type { VideoPlayerStatus } from "../types/VideoPlayerStatus";
type VideoJsPlayer = ReturnType<typeof videojs>;