Skip to content

Commit 778ff6c

Browse files
committed
feat(player): new storage player prop and MediaStorage interface
1 parent da82b35 commit 778ff6c

9 files changed

Lines changed: 250 additions & 169 deletions

File tree

packages/vidstack/src/components/player.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
Component,
33
computed,
44
effect,
5-
getScope,
65
method,
76
onDispose,
87
peek,
@@ -16,6 +15,7 @@ import type { ElementAttributesRecord } from 'maverick.js/element';
1615
import {
1716
animationFrameThrottle,
1817
camelToKebabCase,
18+
isString,
1919
listenEvent,
2020
setAttribute,
2121
setStyle,
@@ -52,8 +52,8 @@ 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 { LocalMediaStorage, type MediaStorage } from '../core/state/media-storage';
5556
import { NavigatorMediaSession } from '../core/state/navigator-media-session';
56-
import { MediaStorage } from '../core/storage';
5757
import { TextTrackSymbol } from '../core/tracks/text/symbols';
5858
import { canFullscreen } from '../foundation/fullscreen/controller';
5959
import { Logger } from '../foundation/logger/controller';
@@ -144,14 +144,11 @@ export class MediaPlayer
144144

145145
new MediaStateSync();
146146

147-
const mediaStorageKey = computed(this._computeMediaKey.bind(this)),
148-
storage = new MediaStorage(this.$props.storageKey, mediaStorageKey);
149-
150147
const context = {
151148
player: this,
152149
qualities: new VideoQualityList(),
153150
audioTracks: new AudioTrackList(),
154-
storage,
151+
storage: null,
155152
$provider: signal<MediaProvider | null>(null),
156153
$providerSetup: signal(false),
157154
$props: this.$props,
@@ -169,7 +166,7 @@ export class MediaPlayer
169166
context.remote = new MediaRemoteControl(__DEV__ ? context.logger : undefined);
170167
context.remote.setPlayer(this);
171168
context.$iosControls = computed(this._isIOSControls.bind(this));
172-
context.textTracks = new TextTrackList(storage);
169+
context.textTracks = new TextTrackList();
173170
context.textTracks[TextTrackSymbol._crossOrigin] = this.$state.crossOrigin;
174171
context.textRenderers = new TextRenderers(context);
175172
context.ariaKeys = {};
@@ -213,6 +210,8 @@ export class MediaPlayer
213210
setAttributeIfEmpty(el, 'tabindex', '0');
214211
setAttributeIfEmpty(el, 'role', 'region');
215212

213+
effect(this._watchStorage.bind(this));
214+
216215
if (__SERVER__) this._watchTitle();
217216
else effect(this._watchTitle.bind(this));
218217

@@ -255,14 +254,6 @@ export class MediaPlayer
255254
this.canPlayQueue._reset();
256255
}
257256

258-
private _computeMediaKey() {
259-
const { storageKey, clipStartTime, clipEndTime } = this.$props,
260-
{ source } = this.$state;
261-
return storageKey() && source().src
262-
? `${storageKey()}:${source().src}:${clipStartTime()}:${clipEndTime()}`
263-
: null;
264-
}
265-
266257
private _skipTitleUpdate = false;
267258
private _watchTitle() {
268259
if (this._skipTitleUpdate) {
@@ -583,7 +574,7 @@ export class MediaPlayer
583574

584575
private _queuePlaybackRateUpdate(rate: number) {
585576
this.canPlayQueue._enqueue('rate', () => {
586-
if (this._provider) this._provider.setPlaybackRate?.(rate);
577+
if (this._provider) (this._provider as MediaProviderAdapter).setPlaybackRate?.(rate);
587578
});
588579
}
589580

@@ -597,6 +588,37 @@ export class MediaPlayer
597588
});
598589
}
599590

591+
private _watchStorage() {
592+
let storageValue = this.$props.storage(),
593+
storage: MediaStorage | null = isString(storageValue)
594+
? new LocalMediaStorage()
595+
: storageValue;
596+
597+
if (storage?.onChange) {
598+
const { source } = this.$state,
599+
playerId = isString(storageValue) ? storageValue : this.el?.id,
600+
mediaId = computed(this._computeMediaId.bind(this));
601+
602+
effect(() => storage!.onChange!(source(), mediaId(), playerId));
603+
}
604+
605+
this._media.storage = storage;
606+
this._media.textTracks.setStorage(storage);
607+
608+
onDispose(() => {
609+
storage?.onDestroy?.();
610+
this._media.storage = null;
611+
this._media.textTracks.setStorage(null);
612+
});
613+
}
614+
615+
private _computeMediaId() {
616+
const { clipStartTime, clipEndTime } = this.$props,
617+
{ source } = this.$state,
618+
src = source();
619+
return src.src ? `${src.src}:${clipStartTime()}:${clipEndTime()}` : null;
620+
}
621+
600622
/**
601623
* Begins/resumes playback of the media. If this method is called programmatically before the
602624
* user has interacted with the player, the promise may be rejected subject to the browser's

packages/vidstack/src/core/api/media-context.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import type { MediaProviderAdapter } from '../../providers/types';
1212
import type { MediaKeyShortcuts } from '../keyboard/types';
1313
import type { VideoQualityList } from '../quality/video-quality';
1414
import type { MediaPlayerDelegate } from '../state/media-player-delegate';
15+
import type { MediaStorage } from '../state/media-storage';
1516
import type { MediaRemoteControl } from '../state/remote-control';
16-
import type { MediaStorage } from '../storage';
1717
import type { AudioTrackList } from '../tracks/audio-tracks';
1818
import type { TextRenderers } from '../tracks/text/render/text-renderer';
1919
import type { TextTrackList } from '../tracks/text/text-tracks';
@@ -22,7 +22,7 @@ import type { PlayerStore } from './player-state';
2222

2323
export interface MediaContext {
2424
player: MediaPlayer;
25-
storage: MediaStorage;
25+
storage: MediaStorage | null;
2626
remote: MediaRemoteControl;
2727
delegate: MediaPlayerDelegate;
2828
qualities: VideoQualityList;

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ScreenOrientationLockType } from '../../foundation/orientation/typ
33
import type { GoogleCastOptions } from '../../providers/google-cast/types';
44
import { MEDIA_KEY_SHORTCUTS } from '../keyboard/controller';
55
import type { MediaKeyShortcuts, MediaKeyTarget } from '../keyboard/types';
6+
import type { MediaStorage } from '../state/media-storage';
67
import type { MediaState } from './player-state';
78
import type { MediaLoadingStrategy, MediaPosterLoadingStrategy, MediaResource } from './types';
89

@@ -40,7 +41,7 @@ export const mediaPlayerProps: MediaPlayerProps = {
4041
keyDisabled: false,
4142
keyTarget: 'player',
4243
keyShortcuts: MEDIA_KEY_SHORTCUTS,
43-
storageKey: null,
44+
storage: null,
4445
};
4546

4647
export interface MediaStateAccessors
@@ -207,8 +208,14 @@ export interface MediaPlayerProps
207208
*/
208209
keyShortcuts: MediaKeyShortcuts;
209210
/**
210-
* Determines whether volume, time, and captions settings should be saved to local storage
211-
* and used when initializing media.
211+
* Determines whether volume, time, and other player settings should be saved to storage
212+
* and used when initializing media. The two options for enabling storage are:
213+
*
214+
* 1. You can provide a string which will use our local storage solution and the given string as
215+
* a key prefix.
216+
*
217+
* 2. Or, you can provide your own storage solution (e.g., database) by implementing
218+
* the `MediaStorage` interface and providing the object/class.
212219
*/
213-
storageKey: string | null;
220+
storage: string | MediaStorage | null;
214221
}

packages/vidstack/src/core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export type { MediaPlayerProps, MediaStateAccessors, PlayerSrc } from './api/pla
99
export type { MediaPlayerEvents } from './api/player-events';
1010
export { MediaRemoteControl } from './state/remote-control';
1111
export { MediaControls } from './controls';
12-
export type { MediaStorage } from './storage';
12+
export { type MediaStorage, LocalMediaStorage } from './state/media-storage';
1313
export * from './tracks/text/render/text-renderer';
1414
export * from './tracks/text/render/libass-text-renderer';
1515
export * from './tracks/text/text-track';

packages/vidstack/src/core/state/media-player-delegate.ts

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { peek, tick } from 'maverick.js';
1+
import { tick, untrack } from 'maverick.js';
22
import { DOMEvent, type InferEventDetail } from 'maverick.js/std';
33

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

40-
const { $state, logger } = this._media;
40+
return untrack(async () => {
41+
const { logger } = this._media,
42+
{ autoplay, canPlay, started, duration, seekable, buffered, remotePlaybackInfo } =
43+
this._media.$state;
4144

42-
if (peek($state.canPlay)) return;
45+
if (canPlay()) return;
4346

44-
const detail = {
45-
duration: info?.duration ?? peek($state.duration),
46-
seekable: info?.seekable ?? peek($state.seekable),
47-
buffered: info?.buffered ?? peek($state.buffered),
48-
provider: peek(this._media.$provider)!,
49-
};
47+
const detail = {
48+
duration: info?.duration ?? duration(),
49+
seekable: info?.seekable ?? seekable(),
50+
buffered: info?.buffered ?? buffered(),
51+
provider: this._media.$provider()!,
52+
};
5053

51-
this._notify('can-play', detail, trigger);
54+
this._notify('can-play', detail, trigger);
5255

53-
tick();
56+
tick();
5457

55-
if (__DEV__) {
56-
logger
57-
?.infoGroup('-~-~-~-~-~-~- ✅ MEDIA READY -~-~-~-~-~-~-')
58-
.labelledLog('Media Store', { ...$state })
59-
.labelledLog('Trigger Event', trigger)
60-
.dispatch();
61-
}
58+
if (__DEV__) {
59+
logger
60+
?.infoGroup('-~-~-~-~-~-~- ✅ MEDIA READY -~-~-~-~-~-~-')
61+
.labelledLog('Media', this._media)
62+
.labelledLog('Trigger Event', trigger)
63+
.dispatch();
64+
}
6265

63-
const provider = peek(this._media.$provider),
64-
{ storage } = this._media,
65-
{ muted, volume, playsinline, clipStartTime } = this._media.$props,
66-
{ remotePlaybackInfo } = this._media.$state,
67-
remotePlaybackTime = remotePlaybackInfo()?.savedState?.currentTime,
68-
wasRemotePlaying = remotePlaybackInfo()?.savedState?.paused === false,
69-
startTime = remotePlaybackTime ?? storage.data.time ?? clipStartTime(),
70-
shouldAutoPlay = wasRemotePlaying || $state.autoplay();
71-
72-
if (provider) {
73-
provider.setVolume(storage.data.volume ?? peek(volume));
74-
provider.setMuted(storage.data.muted ?? peek(muted));
75-
provider.setPlaysinline?.(peek(playsinline));
76-
if (startTime > 0) provider.setCurrentTime(startTime);
77-
}
66+
let provider = this._media.$provider(),
67+
{ storage } = this._media,
68+
{ muted, volume, playsinline, clipStartTime } = this._media.$props;
7869

79-
if ($state.canPlay() && shouldAutoPlay && !$state.started()) {
80-
await this._attemptAutoplay(trigger);
81-
}
70+
const remotePlaybackTime = remotePlaybackInfo()?.savedState?.currentTime,
71+
wasRemotePlaying = remotePlaybackInfo()?.savedState?.paused === false,
72+
startTime = remotePlaybackTime ?? (await storage?.getTime()) ?? clipStartTime(),
73+
shouldAutoPlay = wasRemotePlaying || autoplay();
74+
75+
if (provider) {
76+
provider.setVolume((await storage?.getVolume()) ?? volume());
77+
provider.setMuted((await storage?.getMuted()) ?? muted());
78+
provider.setPlaysinline?.(playsinline());
79+
if (startTime > 0) provider.setCurrentTime(startTime);
80+
}
81+
82+
if (canPlay() && shouldAutoPlay && !started()) {
83+
await this._attemptAutoplay(trigger);
84+
}
8285

83-
remotePlaybackInfo.set(null);
86+
remotePlaybackInfo.set(null);
87+
});
8488
}
8589

8690
private async _attemptAutoplay(trigger?: Event) {
87-
const { player, $state } = this._media;
91+
const {
92+
player,
93+
$state: { autoPlaying, muted },
94+
} = this._media;
8895

89-
$state.autoPlaying.set(true);
96+
autoPlaying.set(true);
9097

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

93100
try {
94101
await player.play(attemptEvent);
95102
} catch (error) {
96103
if (__DEV__ && !seenAutoplayWarning) {
97-
const muteMsg = !$state.muted()
104+
const muteMsg = !muted()
98105
? ' Attempting with volume muted will most likely resolve the issue.'
99106
: '';
100107

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ export class MediaStateManager extends MediaPlayerController {
644644

645645
if (!canPlay()) return;
646646

647-
storage.time = realCurrentTime();
647+
storage?.setTime?.(realCurrentTime());
648648
}
649649

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

661-
storage.volume = volume();
662-
storage.muted = muted();
661+
storage?.setVolume?.(volume());
662+
storage?.setMuted?.(muted());
663663
}
664664

665665
['seeking'] = throttle(

0 commit comments

Comments
 (0)