Skip to content

Commit da82b35

Browse files
committed
feat(player): add media session api support
1 parent d3f6699 commit da82b35

7 files changed

Lines changed: 94 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ All notable changes to this project will be documented in this file.
2323
- new `crossOrigin` prop on thumbnail components ([72b8056](https://github.com/vidstack/player/commit/72b805680be2726c9d2286e925e7492f7c32ace9))
2424
- support new thumbnail src types ([698e575](https://github.com/vidstack/player/commit/698e5756ace9b0a85fffaa33083f567c9f127945))
2525
- new keyboard action display in default layout ([52890b0](https://github.com/vidstack/player/commit/52890b0373a99d32a9620d46b5e5d7734ca85d16))
26-
- new airplay button ([0448950](https://github.com/vidstack/player/commit/0448950968e1bb9702e10b843f51e066ff661467))
26+
- airplay support ([0f7df2f](https://github.com/vidstack/player/commit/0f7df2fa9ece6a9923c1b4c2f611e6362a8eb28e))
27+
- google cast support ([d08d630](https://github.com/vidstack/player/commit/d08d63044a3cbb54fc278e72ef4dd3ab1a28dd0b))
28+
- add media session api support ([2817694](https://github.com/vidstack/player/commit/2817694e10700f4a0cb0eb884cc5bd5272b345c9))
2729

2830
#### Player (React)
2931

@@ -47,7 +49,7 @@ All notable changes to this project will be documented in this file.
4749
- catch false postive vimeo pro detection ([29d6fa0](https://github.com/vidstack/player/commit/29d6fa05fbcc373e47232cf5412f7f1f73446fae))
4850
- use intrisic duration for last vimeo chapter end time ([4dbe21e](https://github.com/vidstack/player/commit/4dbe21eafdba83ef5071f3be7f9eadbc8999e96d))
4951
- rename `crossorigin` prop to `crossOrigin` ([37513ea](https://github.com/vidstack/player/commit/37513ea12c761c65f1dbbfd8280d9635be4ffb50))
50-
- rework media request queue ([cd888a8](https://github.com/vidstack/player/commit/cd888a8025fbfdfd1a6d92aa083f2f18b9880f0a))
52+
- rework media request queue ([6f9c16b](https://github.com/vidstack/player/commit/6f9c16b1c9a41487233a033d2801bc1691aa5716))
5153

5254
#### Player (React)
5355

packages/vidstack/mangle.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,5 +736,11 @@
736736
"_syncActiveIds": "Im",
737737
"_syncLocalTracks": "Lm",
738738
"_syncRemoteActiveIds": "Km",
739-
"_syncRemoteTracks": "Mm"
739+
"_syncRemoteTracks": "Mm",
740+
"_actions": "Nm",
741+
"_createGoogleCastContainer": "Sm",
742+
"_handleAction": "Rm",
743+
"_onMetadataChange": "Pm",
744+
"_onPlaybackStateChange": "Qm",
745+
"_watchArtist": "Om"
740746
}

packages/vidstack/src/components/player.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { MediaPlayerDelegate } from '../core/state/media-player-delegate';
5252
import { MediaRequestContext, MediaRequestManager } from '../core/state/media-request-manager';
5353
import { MediaStateManager } from '../core/state/media-state-manager';
5454
import { MediaStateSync } from '../core/state/media-state-sync';
55+
import { NavigatorMediaSession } from '../core/state/navigator-media-session';
5556
import { MediaStorage } from '../core/storage';
5657
import { TextTrackSymbol } from '../core/tracks/text/symbols';
5758
import { canFullscreen } from '../foundation/fullscreen/controller';
@@ -191,6 +192,7 @@ export class MediaPlayer
191192
context,
192193
);
193194

195+
new NavigatorMediaSession();
194196
new MediaLoadController('load', this.startLoading.bind(this));
195197
new MediaLoadController('posterLoad', this.startLoadingPoster.bind(this));
196198
}

packages/vidstack/src/core/api/player-props.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { MediaState } from './player-state';
77
import type { MediaLoadingStrategy, MediaPosterLoadingStrategy, MediaResource } from './types';
88

99
export const mediaPlayerProps: MediaPlayerProps = {
10+
artist: '',
1011
autoplay: false,
1112
clipStartTime: 0,
1213
clipEndTime: 0,
@@ -54,6 +55,7 @@ export interface MediaPlayerProps
5455
// Prefer picking off the `MediaStore` type to ensure docs are kept in-sync.
5556
extends Pick<
5657
MediaState,
58+
| 'artist'
5759
| 'autoplay'
5860
| 'clipStartTime'
5961
| 'clipEndTime'

packages/vidstack/src/core/api/player-state.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import type {
2121
export interface MediaPlayerState extends MediaState {}
2222

2323
export const mediaState = new State<MediaState>({
24-
audioTracks: [],
24+
artist: '',
2525
audioTrack: null,
26+
audioTracks: [],
2627
autoplay: false,
2728
autoplayError: null,
2829
buffered: new TimeRange(),
@@ -718,6 +719,11 @@ export interface MediaState {
718719
* The title of the current media.
719720
*/
720721
readonly title: string;
722+
/**
723+
* The artist or channel name for which this content belongs to. This can be used in your
724+
* layout and it will be included in the Media Session API.
725+
*/
726+
artist: string;
721727
/**
722728
* The list of all available text tracks.
723729
*/

packages/vidstack/src/core/state/media-state-sync.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class MediaStateSync extends MediaPlayerController {
1414

1515
if (__DEV__) effect(this._watchLogLevel.bind(this));
1616
effect(this._watchProvidedTypes.bind(this));
17+
effect(this._watchArtist.bind(this));
1718
effect(this._watchTitle.bind(this));
1819
effect(this._watchAutoplay.bind(this));
1920
effect(this._watchPoster.bind(this));
@@ -65,6 +66,11 @@ export class MediaStateSync extends MediaPlayerController {
6566
this.$state.logLevel.set(this.$props.logLevel());
6667
}
6768

69+
private _watchArtist() {
70+
const { artist } = this.$props;
71+
this.$state.artist.set(artist());
72+
}
73+
6874
private _watchTitle() {
6975
const { title } = this.$state;
7076
this.dispatch('title-change', { detail: title() });
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { effect, onDispose } from 'maverick.js';
2+
import { DOMEvent, isNumber } from 'maverick.js/std';
3+
4+
import { MediaPlayerController } from '../api/player-controller';
5+
6+
export class NavigatorMediaSession extends MediaPlayerController {
7+
protected static _actions = ['play', 'pause', 'seekforward', 'seekbackward', 'seekto'] as const;
8+
9+
constructor() {
10+
super();
11+
}
12+
13+
protected override onConnect() {
14+
effect(this._onMetadataChange.bind(this));
15+
effect(this._onPlaybackStateChange.bind(this));
16+
17+
const handleAction = this._handleAction.bind(this);
18+
for (const action of NavigatorMediaSession._actions) {
19+
navigator.mediaSession.setActionHandler(action, handleAction);
20+
}
21+
22+
onDispose(this._onDisconnect.bind(this));
23+
}
24+
25+
protected _onDisconnect() {
26+
for (const action of NavigatorMediaSession._actions) {
27+
navigator.mediaSession.setActionHandler(action, null);
28+
}
29+
}
30+
31+
protected _onMetadataChange() {
32+
const { title, artist, poster } = this.$state;
33+
navigator.mediaSession.metadata = new MediaMetadata({
34+
title: title(),
35+
artist: artist(),
36+
artwork: [{ src: poster() }],
37+
});
38+
}
39+
40+
protected _onPlaybackStateChange() {
41+
const { canPlay, paused } = this.$state;
42+
navigator.mediaSession.playbackState = !canPlay() ? 'none' : paused() ? 'paused' : 'playing';
43+
}
44+
45+
protected _handleAction(details: MediaSessionActionDetails) {
46+
const trigger = new DOMEvent(`media-session-action`, { detail: details });
47+
switch (details.action) {
48+
case 'play':
49+
this.dispatch('media-play-request', { trigger });
50+
break;
51+
case 'pause':
52+
this.dispatch('media-pause-request', { trigger });
53+
break;
54+
case 'seekto':
55+
case 'seekforward':
56+
case 'seekbackward':
57+
this.dispatch('media-seek-request', {
58+
detail: isNumber(details.seekTime)
59+
? details.seekTime
60+
: this.$state.currentTime() + (details.seekOffset ?? 10),
61+
trigger,
62+
});
63+
break;
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)