Skip to content

Commit

Permalink
feat(player): new storage player prop and MediaStorage interface
Browse files Browse the repository at this point in the history
  • Loading branch information
mihar-22 committed Jan 8, 2024
1 parent da82b35 commit 778ff6c
Show file tree
Hide file tree
Showing 9 changed files with 250 additions and 169 deletions.
54 changes: 38 additions & 16 deletions packages/vidstack/src/components/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Component,
computed,
effect,
getScope,
method,
onDispose,
peek,
Expand All @@ -16,6 +15,7 @@ import type { ElementAttributesRecord } from 'maverick.js/element';
import {
animationFrameThrottle,
camelToKebabCase,
isString,
listenEvent,
setAttribute,
setStyle,
Expand Down Expand Up @@ -52,8 +52,8 @@ 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 { LocalMediaStorage, type MediaStorage } from '../core/state/media-storage';
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';
import { Logger } from '../foundation/logger/controller';
Expand Down Expand Up @@ -144,14 +144,11 @@ export class MediaPlayer

new MediaStateSync();

const mediaStorageKey = computed(this._computeMediaKey.bind(this)),
storage = new MediaStorage(this.$props.storageKey, mediaStorageKey);

const context = {
player: this,
qualities: new VideoQualityList(),
audioTracks: new AudioTrackList(),
storage,
storage: null,
$provider: signal<MediaProvider | null>(null),
$providerSetup: signal(false),
$props: this.$props,
Expand All @@ -169,7 +166,7 @@ export class MediaPlayer
context.remote = new MediaRemoteControl(__DEV__ ? context.logger : undefined);
context.remote.setPlayer(this);
context.$iosControls = computed(this._isIOSControls.bind(this));
context.textTracks = new TextTrackList(storage);
context.textTracks = new TextTrackList();
context.textTracks[TextTrackSymbol._crossOrigin] = this.$state.crossOrigin;
context.textRenderers = new TextRenderers(context);
context.ariaKeys = {};
Expand Down Expand Up @@ -213,6 +210,8 @@ export class MediaPlayer
setAttributeIfEmpty(el, 'tabindex', '0');
setAttributeIfEmpty(el, 'role', 'region');

effect(this._watchStorage.bind(this));

if (__SERVER__) this._watchTitle();
else effect(this._watchTitle.bind(this));

Expand Down Expand Up @@ -255,14 +254,6 @@ export class MediaPlayer
this.canPlayQueue._reset();
}

private _computeMediaKey() {
const { storageKey, clipStartTime, clipEndTime } = this.$props,
{ source } = this.$state;
return storageKey() && source().src
? `${storageKey()}:${source().src}:${clipStartTime()}:${clipEndTime()}`
: null;
}

private _skipTitleUpdate = false;
private _watchTitle() {
if (this._skipTitleUpdate) {
Expand Down Expand Up @@ -583,7 +574,7 @@ export class MediaPlayer

private _queuePlaybackRateUpdate(rate: number) {
this.canPlayQueue._enqueue('rate', () => {
if (this._provider) this._provider.setPlaybackRate?.(rate);
if (this._provider) (this._provider as MediaProviderAdapter).setPlaybackRate?.(rate);
});
}

Expand All @@ -597,6 +588,37 @@ export class MediaPlayer
});
}

private _watchStorage() {
let storageValue = this.$props.storage(),
storage: MediaStorage | null = isString(storageValue)
? new LocalMediaStorage()
: storageValue;

if (storage?.onChange) {
const { source } = this.$state,
playerId = isString(storageValue) ? storageValue : this.el?.id,
mediaId = computed(this._computeMediaId.bind(this));

effect(() => storage!.onChange!(source(), mediaId(), playerId));
}

this._media.storage = storage;
this._media.textTracks.setStorage(storage);

onDispose(() => {
storage?.onDestroy?.();
this._media.storage = null;
this._media.textTracks.setStorage(null);
});
}

private _computeMediaId() {
const { clipStartTime, clipEndTime } = this.$props,
{ source } = this.$state,
src = source();
return src.src ? `${src.src}:${clipStartTime()}:${clipEndTime()}` : null;
}

/**
* Begins/resumes playback of the media. If this method is called programmatically before the
* user has interacted with the player, the promise may be rejected subject to the browser's
Expand Down
4 changes: 2 additions & 2 deletions packages/vidstack/src/core/api/media-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import type { MediaProviderAdapter } from '../../providers/types';
import type { MediaKeyShortcuts } from '../keyboard/types';
import type { VideoQualityList } from '../quality/video-quality';
import type { MediaPlayerDelegate } from '../state/media-player-delegate';
import type { MediaStorage } from '../state/media-storage';
import type { MediaRemoteControl } from '../state/remote-control';
import type { MediaStorage } from '../storage';
import type { AudioTrackList } from '../tracks/audio-tracks';
import type { TextRenderers } from '../tracks/text/render/text-renderer';
import type { TextTrackList } from '../tracks/text/text-tracks';
Expand All @@ -22,7 +22,7 @@ import type { PlayerStore } from './player-state';

export interface MediaContext {
player: MediaPlayer;
storage: MediaStorage;
storage: MediaStorage | null;
remote: MediaRemoteControl;
delegate: MediaPlayerDelegate;
qualities: VideoQualityList;
Expand Down
15 changes: 11 additions & 4 deletions packages/vidstack/src/core/api/player-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { ScreenOrientationLockType } from '../../foundation/orientation/typ
import type { GoogleCastOptions } from '../../providers/google-cast/types';
import { MEDIA_KEY_SHORTCUTS } from '../keyboard/controller';
import type { MediaKeyShortcuts, MediaKeyTarget } from '../keyboard/types';
import type { MediaStorage } from '../state/media-storage';
import type { MediaState } from './player-state';
import type { MediaLoadingStrategy, MediaPosterLoadingStrategy, MediaResource } from './types';

Expand Down Expand Up @@ -40,7 +41,7 @@ export const mediaPlayerProps: MediaPlayerProps = {
keyDisabled: false,
keyTarget: 'player',
keyShortcuts: MEDIA_KEY_SHORTCUTS,
storageKey: null,
storage: null,
};

export interface MediaStateAccessors
Expand Down Expand Up @@ -207,8 +208,14 @@ export interface MediaPlayerProps
*/
keyShortcuts: MediaKeyShortcuts;
/**
* Determines whether volume, time, and captions settings should be saved to local storage
* and used when initializing media.
* Determines whether volume, time, and other player settings should be saved to storage
* and used when initializing media. The two options for enabling storage are:
*
* 1. You can provide a string which will use our local storage solution and the given string as
* a key prefix.
*
* 2. Or, you can provide your own storage solution (e.g., database) by implementing
* the `MediaStorage` interface and providing the object/class.
*/
storageKey: string | null;
storage: string | MediaStorage | null;
}
2 changes: 1 addition & 1 deletion packages/vidstack/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type { MediaPlayerProps, MediaStateAccessors, PlayerSrc } from './api/pla
export type { MediaPlayerEvents } from './api/player-events';
export { MediaRemoteControl } from './state/remote-control';
export { MediaControls } from './controls';
export type { MediaStorage } from './storage';
export { type MediaStorage, LocalMediaStorage } from './state/media-storage';
export * from './tracks/text/render/text-renderer';
export * from './tracks/text/render/libass-text-renderer';
export * from './tracks/text/text-track';
Expand Down
87 changes: 47 additions & 40 deletions packages/vidstack/src/core/state/media-player-delegate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { peek, tick } from 'maverick.js';
import { tick, untrack } from 'maverick.js';
import { DOMEvent, type InferEventDetail } from 'maverick.js/std';

import type { MediaContext } from '../api/media-context';
Expand Down Expand Up @@ -37,64 +37,71 @@ export class MediaPlayerDelegate {
) {
if (__SERVER__) return;

const { $state, logger } = this._media;
return untrack(async () => {
const { logger } = this._media,
{ autoplay, canPlay, started, duration, seekable, buffered, remotePlaybackInfo } =
this._media.$state;

if (peek($state.canPlay)) return;
if (canPlay()) return;

const detail = {
duration: info?.duration ?? peek($state.duration),
seekable: info?.seekable ?? peek($state.seekable),
buffered: info?.buffered ?? peek($state.buffered),
provider: peek(this._media.$provider)!,
};
const detail = {
duration: info?.duration ?? duration(),
seekable: info?.seekable ?? seekable(),
buffered: info?.buffered ?? buffered(),
provider: this._media.$provider()!,
};

this._notify('can-play', detail, trigger);
this._notify('can-play', detail, trigger);

tick();
tick();

if (__DEV__) {
logger
?.infoGroup('-~-~-~-~-~-~- ✅ MEDIA READY -~-~-~-~-~-~-')
.labelledLog('Media Store', { ...$state })
.labelledLog('Trigger Event', trigger)
.dispatch();
}
if (__DEV__) {
logger
?.infoGroup('-~-~-~-~-~-~- ✅ MEDIA READY -~-~-~-~-~-~-')
.labelledLog('Media', this._media)
.labelledLog('Trigger Event', trigger)
.dispatch();
}

const provider = peek(this._media.$provider),
{ storage } = this._media,
{ muted, volume, playsinline, clipStartTime } = this._media.$props,
{ remotePlaybackInfo } = this._media.$state,
remotePlaybackTime = remotePlaybackInfo()?.savedState?.currentTime,
wasRemotePlaying = remotePlaybackInfo()?.savedState?.paused === false,
startTime = remotePlaybackTime ?? storage.data.time ?? clipStartTime(),
shouldAutoPlay = wasRemotePlaying || $state.autoplay();

if (provider) {
provider.setVolume(storage.data.volume ?? peek(volume));
provider.setMuted(storage.data.muted ?? peek(muted));
provider.setPlaysinline?.(peek(playsinline));
if (startTime > 0) provider.setCurrentTime(startTime);
}
let provider = this._media.$provider(),
{ storage } = this._media,
{ muted, volume, playsinline, clipStartTime } = this._media.$props;

if ($state.canPlay() && shouldAutoPlay && !$state.started()) {
await this._attemptAutoplay(trigger);
}
const remotePlaybackTime = remotePlaybackInfo()?.savedState?.currentTime,
wasRemotePlaying = remotePlaybackInfo()?.savedState?.paused === false,
startTime = remotePlaybackTime ?? (await storage?.getTime()) ?? clipStartTime(),
shouldAutoPlay = wasRemotePlaying || autoplay();

if (provider) {
provider.setVolume((await storage?.getVolume()) ?? volume());
provider.setMuted((await storage?.getMuted()) ?? muted());
provider.setPlaysinline?.(playsinline());
if (startTime > 0) provider.setCurrentTime(startTime);
}

if (canPlay() && shouldAutoPlay && !started()) {
await this._attemptAutoplay(trigger);
}

remotePlaybackInfo.set(null);
remotePlaybackInfo.set(null);
});
}

private async _attemptAutoplay(trigger?: Event) {
const { player, $state } = this._media;
const {
player,
$state: { autoPlaying, muted },
} = this._media;

$state.autoPlaying.set(true);
autoPlaying.set(true);

const attemptEvent = new DOMEvent<void>('autoplay-attempt', { trigger });

try {
await player.play(attemptEvent);
} catch (error) {
if (__DEV__ && !seenAutoplayWarning) {
const muteMsg = !$state.muted()
const muteMsg = !muted()
? ' Attempting with volume muted will most likely resolve the issue.'
: '';

Expand Down
6 changes: 3 additions & 3 deletions packages/vidstack/src/core/state/media-state-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ export class MediaStateManager extends MediaPlayerController {

if (!canPlay()) return;

storage.time = realCurrentTime();
storage?.setTime?.(realCurrentTime());
}

['volume-change'](event: ME.MediaVolumeChangeEvent) {
Expand All @@ -658,8 +658,8 @@ export class MediaStateManager extends MediaPlayerController {
this._satisfyRequest('media-volume-change-request', event);
this._satisfyRequest(detail.muted ? 'media-mute-request' : 'media-unmute-request', event);

storage.volume = volume();
storage.muted = muted();
storage?.setVolume?.(volume());
storage?.setMuted?.(muted());
}

['seeking'] = throttle(
Expand Down
Loading

0 comments on commit 778ff6c

Please sign in to comment.