Skip to content

Commit

Permalink
feat(player): add media session api support
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Jan 8, 2024
1 parent d3f6699 commit da82b35
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 4 deletions.
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ All notable changes to this project will be documented in this file.
- new `crossOrigin` prop on thumbnail components ([72b8056](https://github.com/vidstack/player/commit/72b805680be2726c9d2286e925e7492f7c32ace9))
- support new thumbnail src types ([698e575](https://github.com/vidstack/player/commit/698e5756ace9b0a85fffaa33083f567c9f127945))
- new keyboard action display in default layout ([52890b0](https://github.com/vidstack/player/commit/52890b0373a99d32a9620d46b5e5d7734ca85d16))
- new airplay button ([0448950](https://github.com/vidstack/player/commit/0448950968e1bb9702e10b843f51e066ff661467))
- airplay support ([0f7df2f](https://github.com/vidstack/player/commit/0f7df2fa9ece6a9923c1b4c2f611e6362a8eb28e))
- google cast support ([d08d630](https://github.com/vidstack/player/commit/d08d63044a3cbb54fc278e72ef4dd3ab1a28dd0b))
- add media session api support ([2817694](https://github.com/vidstack/player/commit/2817694e10700f4a0cb0eb884cc5bd5272b345c9))

#### Player (React)

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

#### Player (React)

Expand Down
8 changes: 7 additions & 1 deletion packages/vidstack/mangle.json
Original file line number Diff line number Diff line change
Expand Up @@ -736,5 +736,11 @@
"_syncActiveIds": "Im",
"_syncLocalTracks": "Lm",
"_syncRemoteActiveIds": "Km",
"_syncRemoteTracks": "Mm"
"_syncRemoteTracks": "Mm",
"_actions": "Nm",
"_createGoogleCastContainer": "Sm",
"_handleAction": "Rm",
"_onMetadataChange": "Pm",
"_onPlaybackStateChange": "Qm",
"_watchArtist": "Om"
}
2 changes: 2 additions & 0 deletions packages/vidstack/src/components/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { MediaPlayerDelegate } from '../core/state/media-player-delegate';
import { MediaRequestContext, MediaRequestManager } from '../core/state/media-request-manager';
import { MediaStateManager } from '../core/state/media-state-manager';
import { MediaStateSync } from '../core/state/media-state-sync';
import { NavigatorMediaSession } from '../core/state/navigator-media-session';
import { MediaStorage } from '../core/storage';
import { TextTrackSymbol } from '../core/tracks/text/symbols';
import { canFullscreen } from '../foundation/fullscreen/controller';
Expand Down Expand Up @@ -191,6 +192,7 @@ export class MediaPlayer
context,
);

new NavigatorMediaSession();
new MediaLoadController('load', this.startLoading.bind(this));
new MediaLoadController('posterLoad', this.startLoadingPoster.bind(this));
}
Expand Down
2 changes: 2 additions & 0 deletions packages/vidstack/src/core/api/player-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { MediaState } from './player-state';
import type { MediaLoadingStrategy, MediaPosterLoadingStrategy, MediaResource } from './types';

export const mediaPlayerProps: MediaPlayerProps = {
artist: '',
autoplay: false,
clipStartTime: 0,
clipEndTime: 0,
Expand Down Expand Up @@ -54,6 +55,7 @@ export interface MediaPlayerProps
// Prefer picking off the `MediaStore` type to ensure docs are kept in-sync.
extends Pick<
MediaState,
| 'artist'
| 'autoplay'
| 'clipStartTime'
| 'clipEndTime'
Expand Down
8 changes: 7 additions & 1 deletion packages/vidstack/src/core/api/player-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ import type {
export interface MediaPlayerState extends MediaState {}

export const mediaState = new State<MediaState>({
audioTracks: [],
artist: '',
audioTrack: null,
audioTracks: [],
autoplay: false,
autoplayError: null,
buffered: new TimeRange(),
Expand Down Expand Up @@ -718,6 +719,11 @@ export interface MediaState {
* The title of the current media.
*/
readonly title: string;
/**
* The artist or channel name for which this content belongs to. This can be used in your
* layout and it will be included in the Media Session API.
*/
artist: string;
/**
* The list of all available text tracks.
*/
Expand Down
6 changes: 6 additions & 0 deletions packages/vidstack/src/core/state/media-state-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class MediaStateSync extends MediaPlayerController {

if (__DEV__) effect(this._watchLogLevel.bind(this));
effect(this._watchProvidedTypes.bind(this));
effect(this._watchArtist.bind(this));
effect(this._watchTitle.bind(this));
effect(this._watchAutoplay.bind(this));
effect(this._watchPoster.bind(this));
Expand Down Expand Up @@ -65,6 +66,11 @@ export class MediaStateSync extends MediaPlayerController {
this.$state.logLevel.set(this.$props.logLevel());
}

private _watchArtist() {
const { artist } = this.$props;
this.$state.artist.set(artist());
}

private _watchTitle() {
const { title } = this.$state;
this.dispatch('title-change', { detail: title() });
Expand Down
66 changes: 66 additions & 0 deletions packages/vidstack/src/core/state/navigator-media-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { effect, onDispose } from 'maverick.js';
import { DOMEvent, isNumber } from 'maverick.js/std';

import { MediaPlayerController } from '../api/player-controller';

export class NavigatorMediaSession extends MediaPlayerController {
protected static _actions = ['play', 'pause', 'seekforward', 'seekbackward', 'seekto'] as const;

constructor() {
super();
}

protected override onConnect() {
effect(this._onMetadataChange.bind(this));
effect(this._onPlaybackStateChange.bind(this));

const handleAction = this._handleAction.bind(this);
for (const action of NavigatorMediaSession._actions) {
navigator.mediaSession.setActionHandler(action, handleAction);
}

onDispose(this._onDisconnect.bind(this));
}

protected _onDisconnect() {
for (const action of NavigatorMediaSession._actions) {
navigator.mediaSession.setActionHandler(action, null);
}
}

protected _onMetadataChange() {
const { title, artist, poster } = this.$state;
navigator.mediaSession.metadata = new MediaMetadata({
title: title(),
artist: artist(),
artwork: [{ src: poster() }],
});
}

protected _onPlaybackStateChange() {
const { canPlay, paused } = this.$state;
navigator.mediaSession.playbackState = !canPlay() ? 'none' : paused() ? 'paused' : 'playing';
}

protected _handleAction(details: MediaSessionActionDetails) {
const trigger = new DOMEvent(`media-session-action`, { detail: details });
switch (details.action) {
case 'play':
this.dispatch('media-play-request', { trigger });
break;
case 'pause':
this.dispatch('media-pause-request', { trigger });
break;
case 'seekto':
case 'seekforward':
case 'seekbackward':
this.dispatch('media-seek-request', {
detail: isNumber(details.seekTime)
? details.seekTime
: this.$state.currentTime() + (details.seekOffset ?? 10),
trigger,
});
break;
}
}
}

0 comments on commit da82b35

Please sign in to comment.