refactor(store): merge getSnapshot/subscribe into attach (#364)

This commit is contained in:
rahim
2026-02-01 19:00:46 +11:00
committed by GitHub
parent 9b36f3acbf
commit 6ec2e80b86
27 changed files with 520 additions and 715 deletions
+10 -7
View File
@@ -11,14 +11,17 @@ export const bufferFeature = defineFeature<HTMLMediaElement>()({
seekable: [] as [number, number][],
}),
getSnapshot: ({ target }) => ({
buffered: serializeTimeRanges(target.buffered),
seekable: serializeTimeRanges(target.seekable),
}),
attach({ target, signal, set }) {
const sync = () =>
set({
buffered: serializeTimeRanges(target.buffered),
seekable: serializeTimeRanges(target.seekable),
});
subscribe: ({ target, update, signal }) => {
listen(target, 'progress', update, { signal });
listen(target, 'emptied', update, { signal });
sync();
listen(target, 'progress', sync, { signal });
listen(target, 'emptied', sync, { signal });
},
});
@@ -36,19 +36,22 @@ export const playbackFeature = defineFeature<HTMLMediaElement>()({
},
}),
getSnapshot: ({ target }) => ({
paused: target.paused,
ended: target.ended,
started: !target.paused || target.currentTime > 0,
waiting: target.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !target.paused,
}),
attach({ target, signal, set }) {
const sync = () =>
set({
paused: target.paused,
ended: target.ended,
started: !target.paused || target.currentTime > 0,
waiting: target.readyState < HTMLMediaElement.HAVE_FUTURE_DATA && !target.paused,
});
subscribe: ({ target, update, signal }) => {
listen(target, 'play', update, { signal });
listen(target, 'pause', update, { signal });
listen(target, 'ended', update, { signal });
listen(target, 'playing', update, { signal });
listen(target, 'waiting', update, { signal });
sync();
listen(target, 'play', sync, { signal });
listen(target, 'pause', sync, { signal });
listen(target, 'ended', sync, { signal });
listen(target, 'playing', sync, { signal });
listen(target, 'waiting', sync, { signal });
},
});
+13 -9
View File
@@ -9,6 +9,7 @@ export const sourceFeature = defineFeature<HTMLMediaElement>()({
source: null as string | null,
/** Whether enough data is loaded to begin playback. */
canPlay: false,
/** Load a new media source. Cancels all pending operations. Returns the new source URL. */
loadSource(src: string) {
return task({
@@ -23,16 +24,19 @@ export const sourceFeature = defineFeature<HTMLMediaElement>()({
},
}),
getSnapshot: ({ target }) => ({
source: target.currentSrc || target.src || null,
canPlay: target.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA,
}),
attach({ target, signal, set }) {
const sync = () =>
set({
source: target.currentSrc || target.src || null,
canPlay: target.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA,
});
subscribe: ({ target, update, signal }) => {
listen(target, 'canplay', update, { signal });
listen(target, 'canplaythrough', update, { signal });
listen(target, 'loadstart', update, { signal });
listen(target, 'emptied', update, { signal });
sync();
listen(target, 'canplay', sync, { signal });
listen(target, 'canplaythrough', sync, { signal });
listen(target, 'loadstart', sync, { signal });
listen(target, 'emptied', sync, { signal });
},
});
@@ -1,25 +1,21 @@
import { describe, expect, it, vi } from 'vitest';
import { createStore } from '@videojs/store';
import { describe, expect, it } from 'vitest';
import { bufferFeature } from '../buffer';
describe('bufferFeature', () => {
describe('getSnapshot', () => {
it('captures buffered and seekable ranges from video element', () => {
describe('attach', () => {
it('syncs buffered and seekable ranges on attach', () => {
const video = createMockVideo({
buffered: createTimeRanges([[0, 60]]),
seekable: createTimeRanges([[0, 120]]),
});
const snapshot = bufferFeature.getSnapshot({
target: video,
get: () => ({ buffered: [], seekable: [] }),
initialState: { buffered: [], seekable: [] },
});
const store = createStore({ features: [bufferFeature] });
store.attach(video);
expect(snapshot).toEqual({
buffered: [[0, 60]],
seekable: [[0, 120]],
});
expect(store.state.buffered).toEqual([[0, 60]]);
expect(store.state.seekable).toEqual([[0, 120]]);
});
it('handles multiple ranges', () => {
@@ -31,56 +27,61 @@ describe('bufferFeature', () => {
seekable: createTimeRanges([[0, 120]]),
});
const snapshot = bufferFeature.getSnapshot({
target: video,
get: () => ({ buffered: [], seekable: [] }),
initialState: { buffered: [], seekable: [] },
});
const store = createStore({ features: [bufferFeature] });
store.attach(video);
expect(snapshot.buffered).toEqual([
expect(store.state.buffered).toEqual([
[0, 30],
[60, 90],
]);
});
});
describe('subscribe', () => {
it('calls update on progress event', () => {
it('updates on progress event', () => {
const video = createMockVideo({
buffered: createTimeRanges([[0, 50]]),
seekable: createTimeRanges([[0, 100]]),
});
const update = vi.fn();
const controller = new AbortController();
bufferFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: () => ({ buffered: [], seekable: [] }),
const store = createStore({ features: [bufferFeature] });
store.attach(video);
// Update the mock video's buffered range
Object.defineProperty(video, 'buffered', {
value: createTimeRanges([[0, 75]]),
writable: false,
configurable: true,
});
video.dispatchEvent(new Event('progress'));
expect(update).toHaveBeenCalled();
expect(store.state.buffered).toEqual([[0, 75]]);
});
it('calls update on emptied event', () => {
it('updates on emptied event', () => {
const video = createMockVideo({
buffered: createTimeRanges([]),
seekable: createTimeRanges([]),
buffered: createTimeRanges([[0, 50]]),
seekable: createTimeRanges([[0, 100]]),
});
const update = vi.fn();
const controller = new AbortController();
bufferFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: () => ({ buffered: [], seekable: [] }),
const store = createStore({ features: [bufferFeature] });
store.attach(video);
// Update the mock video to have no buffered content
Object.defineProperty(video, 'buffered', {
value: createTimeRanges([]),
writable: false,
configurable: true,
});
Object.defineProperty(video, 'seekable', {
value: createTimeRanges([]),
writable: false,
configurable: true,
});
video.dispatchEvent(new Event('emptied'));
expect(update).toHaveBeenCalled();
expect(store.state.buffered).toEqual([]);
expect(store.state.seekable).toEqual([]);
});
});
});
@@ -94,10 +95,10 @@ function createMockVideo(
const video = document.createElement('video');
if (overrides.buffered !== undefined) {
Object.defineProperty(video, 'buffered', { value: overrides.buffered, writable: false });
Object.defineProperty(video, 'buffered', { value: overrides.buffered, writable: false, configurable: true });
}
if (overrides.seekable !== undefined) {
Object.defineProperty(video, 'seekable', { value: overrides.seekable, writable: false });
Object.defineProperty(video, 'seekable', { value: overrides.seekable, writable: false, configurable: true });
}
return video;
@@ -1,23 +1,11 @@
import { createStore } from '@videojs/store';
import { noop } from '@videojs/utils/function';
import { describe, expect, it, vi } from 'vitest';
import type { PlaybackState } from '../playback';
import { playbackFeature } from '../playback';
const mockState = () =>
({
paused: true,
ended: false,
started: false,
waiting: false,
play: noop,
pause: noop,
}) as unknown as PlaybackState;
describe('playbackFeature', () => {
describe('getSnapshot', () => {
it('captures current playback state from video element', () => {
describe('attach', () => {
it('syncs playback state on attach', () => {
const video = createMockVideo({
paused: false,
ended: false,
@@ -25,18 +13,13 @@ describe('playbackFeature', () => {
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
});
const snapshot = playbackFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(snapshot).toEqual({
paused: false,
ended: false,
started: true,
waiting: false,
});
expect(store.state.paused).toBe(false);
expect(store.state.ended).toBe(false);
expect(store.state.started).toBe(true);
expect(store.state.waiting).toBe(false);
});
it('detects waiting state when buffering', () => {
@@ -45,13 +28,10 @@ describe('playbackFeature', () => {
readyState: HTMLMediaElement.HAVE_CURRENT_DATA,
});
const snapshot = playbackFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(snapshot.waiting).toBe(true);
expect(store.state.waiting).toBe(true);
});
it('detects started from currentTime', () => {
@@ -60,13 +40,10 @@ describe('playbackFeature', () => {
currentTime: 5,
});
const snapshot = playbackFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(snapshot.started).toBe(true);
expect(store.state.started).toBe(true);
});
it('detects started from playing state', () => {
@@ -75,80 +52,71 @@ describe('playbackFeature', () => {
currentTime: 0,
});
const snapshot = playbackFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(snapshot.started).toBe(true);
expect(store.state.started).toBe(true);
});
});
describe('subscribe', () => {
it('calls update on play event', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
it('updates on play event', () => {
const video = createMockVideo({ paused: true });
playbackFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(store.state.paused).toBe(true);
// Update mock to playing state
Object.defineProperty(video, 'paused', { value: false, writable: false, configurable: true });
video.dispatchEvent(new Event('play'));
expect(update).toHaveBeenCalled();
expect(store.state.paused).toBe(false);
});
it('calls update on pause event', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
it('updates on pause event', () => {
const video = createMockVideo({ paused: false });
playbackFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(store.state.paused).toBe(false);
// Update mock to paused state
Object.defineProperty(video, 'paused', { value: true, writable: false, configurable: true });
video.dispatchEvent(new Event('pause'));
expect(update).toHaveBeenCalled();
expect(store.state.paused).toBe(true);
});
it('calls update on ended event', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
it('updates on ended event', () => {
const video = createMockVideo({ ended: false });
playbackFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [playbackFeature] });
store.attach(video);
expect(store.state.ended).toBe(false);
// Update mock to ended state
Object.defineProperty(video, 'ended', { value: true, writable: false, configurable: true });
video.dispatchEvent(new Event('ended'));
expect(update).toHaveBeenCalled();
expect(store.state.ended).toBe(true);
});
it('unsubscribes when signal aborted', () => {
it('stops listening when store is destroyed', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
playbackFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
controller.abort();
const store = createStore({ features: [playbackFeature] });
store.attach(video);
store.destroy();
// Update mock to playing state
Object.defineProperty(video, 'paused', { value: false, writable: false, configurable: true });
video.dispatchEvent(new Event('play'));
expect(update).not.toHaveBeenCalled();
// State should not update after destroy
expect(store.state.paused).toBe(true);
});
});
@@ -190,16 +158,16 @@ function createMockVideo(
const video = document.createElement('video');
if (overrides.paused !== undefined) {
Object.defineProperty(video, 'paused', { value: overrides.paused, writable: false });
Object.defineProperty(video, 'paused', { value: overrides.paused, writable: false, configurable: true });
}
if (overrides.ended !== undefined) {
Object.defineProperty(video, 'ended', { value: overrides.ended, writable: false });
Object.defineProperty(video, 'ended', { value: overrides.ended, writable: false, configurable: true });
}
if (overrides.currentTime !== undefined) {
video.currentTime = overrides.currentTime;
}
if (overrides.readyState !== undefined) {
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false });
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false, configurable: true });
}
return video;
@@ -1,36 +1,22 @@
import { createStore } from '@videojs/store';
import { noop } from '@videojs/utils/function';
import { describe, expect, it, vi } from 'vitest';
import type { SourceState } from '../source';
import { sourceFeature } from '../source';
const mockState = () =>
({
source: null,
canPlay: false,
loadSource: noop,
}) as unknown as SourceState;
describe('sourceFeature', () => {
describe('getSnapshot', () => {
it('captures source state from video element', () => {
describe('attach', () => {
it('syncs source state on attach', () => {
const video = createMockVideo({
currentSrc: 'https://example.com/video.mp4',
src: 'https://example.com/video.mp4',
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
});
const snapshot = sourceFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [sourceFeature] });
store.attach(video);
expect(snapshot).toEqual({
source: 'https://example.com/video.mp4',
canPlay: true,
});
expect(store.state.source).toBe('https://example.com/video.mp4');
expect(store.state.canPlay).toBe(true);
});
it('returns null source when no source set', () => {
@@ -39,67 +25,78 @@ describe('sourceFeature', () => {
Object.defineProperty(video, 'currentSrc', { value: '', writable: false });
Object.defineProperty(video, 'readyState', { value: HTMLMediaElement.HAVE_NOTHING, writable: false });
const snapshot = sourceFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
const store = createStore({ features: [sourceFeature] });
store.attach(video);
expect(store.state.source).toBe(null);
expect(store.state.canPlay).toBe(false);
});
it('updates on canplay event', () => {
const video = createMockVideo({
currentSrc: '',
readyState: HTMLMediaElement.HAVE_NOTHING,
});
expect(snapshot.source).toBe(null);
expect(snapshot.canPlay).toBe(false);
});
});
const store = createStore({ features: [sourceFeature] });
store.attach(video);
describe('subscribe', () => {
it('calls update on canplay event', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
expect(store.state.canPlay).toBe(false);
sourceFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
// Update mock to ready state
Object.defineProperty(video, 'readyState', {
value: HTMLMediaElement.HAVE_ENOUGH_DATA,
writable: false,
configurable: true,
});
video.dispatchEvent(new Event('canplay'));
expect(update).toHaveBeenCalled();
expect(store.state.canPlay).toBe(true);
});
it('calls update on loadstart event', () => {
it('updates on loadstart event', () => {
const video = createMockVideo({
currentSrc: 'https://example.com/new.mp4',
src: 'https://example.com/new.mp4',
currentSrc: 'https://example.com/video.mp4',
});
const update = vi.fn();
const controller = new AbortController();
sourceFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
const store = createStore({ features: [sourceFeature] });
store.attach(video);
expect(store.state.source).toBe('https://example.com/video.mp4');
// Update mock with new source
Object.defineProperty(video, 'currentSrc', {
value: 'https://example.com/new.mp4',
writable: false,
configurable: true,
});
video.dispatchEvent(new Event('loadstart'));
expect(update).toHaveBeenCalled();
expect(store.state.source).toBe('https://example.com/new.mp4');
});
it('calls update on emptied event', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
it('updates on emptied event', () => {
const video = createMockVideo({
currentSrc: 'https://example.com/video.mp4',
readyState: HTMLMediaElement.HAVE_ENOUGH_DATA,
});
sourceFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
const store = createStore({ features: [sourceFeature] });
store.attach(video);
expect(store.state.canPlay).toBe(true);
// Update mock to empty state
Object.defineProperty(video, 'currentSrc', { value: '', writable: false, configurable: true });
Object.defineProperty(video, 'readyState', {
value: HTMLMediaElement.HAVE_NOTHING,
writable: false,
configurable: true,
});
video.dispatchEvent(new Event('emptied'));
expect(update).toHaveBeenCalled();
expect(store.state.source).toBe(null);
expect(store.state.canPlay).toBe(false);
});
});
@@ -132,13 +129,13 @@ function createMockVideo(
const video = document.createElement('video');
if (overrides.currentSrc !== undefined) {
Object.defineProperty(video, 'currentSrc', { value: overrides.currentSrc, writable: false });
Object.defineProperty(video, 'currentSrc', { value: overrides.currentSrc, writable: false, configurable: true });
}
if (overrides.src !== undefined) {
video.src = overrides.src;
}
if (overrides.readyState !== undefined) {
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false });
Object.defineProperty(video, 'readyState', { value: overrides.readyState, writable: false, configurable: true });
}
return video;
@@ -1,35 +1,21 @@
import { createStore } from '@videojs/store';
import { noop } from '@videojs/utils/function';
import { describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import type { TimeState } from '../time';
import { timeFeature } from '../time';
const mockState = () =>
({
currentTime: 0,
duration: 0,
seek: noop,
}) as unknown as TimeState;
describe('timeFeature', () => {
describe('getSnapshot', () => {
it('captures current time state from video element', () => {
describe('attach', () => {
it('syncs time state on attach', () => {
const video = createMockVideo({
currentTime: 30,
duration: 120,
});
const snapshot = timeFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [timeFeature] });
store.attach(video);
expect(snapshot).toEqual({
currentTime: 30,
duration: 120,
});
expect(store.state.currentTime).toBe(30);
expect(store.state.duration).toBe(120);
});
it('handles NaN duration', () => {
@@ -38,79 +24,71 @@ describe('timeFeature', () => {
duration: Number.NaN,
});
const snapshot = timeFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [timeFeature] });
store.attach(video);
expect(snapshot.duration).toBe(0);
expect(store.state.duration).toBe(0);
});
});
describe('subscribe', () => {
it('calls update on timeupdate event', () => {
const video = createMockVideo({ currentTime: 42 });
const update = vi.fn();
const controller = new AbortController();
it('updates on timeupdate event', () => {
const video = createMockVideo({ currentTime: 0 });
timeFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [timeFeature] });
store.attach(video);
expect(store.state.currentTime).toBe(0);
// Update mock currentTime
video.currentTime = 42;
video.dispatchEvent(new Event('timeupdate'));
expect(update).toHaveBeenCalled();
expect(store.state.currentTime).toBe(42);
});
it('calls update on durationchange event', () => {
const video = createMockVideo({ duration: 100 });
const update = vi.fn();
const controller = new AbortController();
it('updates on durationchange event', () => {
const video = createMockVideo({ duration: 0 });
timeFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [timeFeature] });
store.attach(video);
expect(store.state.duration).toBe(0);
// Update mock duration
Object.defineProperty(video, 'duration', { value: 100, writable: false, configurable: true });
video.dispatchEvent(new Event('durationchange'));
expect(update).toHaveBeenCalled();
expect(store.state.duration).toBe(100);
});
it('calls update on seeked event', () => {
const video = createMockVideo({ currentTime: 50 });
const update = vi.fn();
const controller = new AbortController();
it('updates on seeked event', () => {
const video = createMockVideo({ currentTime: 0 });
timeFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [timeFeature] });
store.attach(video);
// Update mock currentTime
video.currentTime = 50;
video.dispatchEvent(new Event('seeked'));
expect(update).toHaveBeenCalled();
expect(store.state.currentTime).toBe(50);
});
it('calls update on emptied event', () => {
const video = createMockVideo({});
const update = vi.fn();
const controller = new AbortController();
timeFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
it('updates on emptied event', () => {
const video = createMockVideo({
currentTime: 30,
duration: 120,
});
const store = createStore({ features: [timeFeature] });
store.attach(video);
// Update mock to empty state
video.currentTime = 0;
Object.defineProperty(video, 'duration', { value: Number.NaN, writable: false, configurable: true });
video.dispatchEvent(new Event('emptied'));
expect(update).toHaveBeenCalled();
expect(store.state.currentTime).toBe(0);
expect(store.state.duration).toBe(0);
});
});
@@ -147,7 +125,7 @@ function createMockVideo(
video.currentTime = overrides.currentTime;
}
if (overrides.duration !== undefined) {
Object.defineProperty(video, 'duration', { value: overrides.duration, writable: false });
Object.defineProperty(video, 'duration', { value: overrides.duration, writable: false, configurable: true });
}
return video;
@@ -1,54 +1,38 @@
import { createStore } from '@videojs/store';
import { noop } from '@videojs/utils/function';
import { describe, expect, it, vi } from 'vitest';
import { describe, expect, it } from 'vitest';
import type { VolumeState } from '../volume';
import { volumeFeature } from '../volume';
const mockState = () =>
({
volume: 1,
muted: false,
changeVolume: noop,
toggleMute: noop,
}) as unknown as VolumeState;
describe('volumeFeature', () => {
describe('getSnapshot', () => {
it('captures volume state from video element', () => {
describe('attach', () => {
it('syncs volume state on attach', () => {
const video = createMockVideo({
volume: 0.8,
muted: false,
});
const snapshot = volumeFeature.getSnapshot({
target: video,
get: mockState,
initialState: mockState(),
});
const store = createStore({ features: [volumeFeature] });
store.attach(video);
expect(snapshot).toEqual({
volume: 0.8,
muted: false,
});
expect(store.state.volume).toBe(0.8);
expect(store.state.muted).toBe(false);
});
});
describe('subscribe', () => {
it('calls update on volumechange event', () => {
const video = createMockVideo({ volume: 0.5, muted: true });
const update = vi.fn();
const controller = new AbortController();
it('updates on volumechange event', () => {
const video = createMockVideo({ volume: 1, muted: false });
volumeFeature.subscribe({
target: video,
update,
signal: controller.signal,
get: mockState,
});
const store = createStore({ features: [volumeFeature] });
store.attach(video);
expect(store.state.volume).toBe(1);
// Update mock volume
video.volume = 0.5;
video.muted = true;
video.dispatchEvent(new Event('volumechange'));
expect(update).toHaveBeenCalled();
expect(store.state.volume).toBe(0.5);
expect(store.state.muted).toBe(true);
});
});
+13 -10
View File
@@ -23,17 +23,20 @@ export const timeFeature = defineFeature<HTMLMediaElement>()({
},
}),
getSnapshot: ({ target }) => ({
currentTime: target.currentTime,
duration: target.duration || 0,
}),
attach({ target, signal, set }) {
const sync = () =>
set({
currentTime: target.currentTime,
duration: target.duration || 0,
});
subscribe: ({ target, update, signal }) => {
listen(target, 'timeupdate', update, { signal });
listen(target, 'durationchange', update, { signal });
listen(target, 'seeked', update, { signal });
listen(target, 'loadedmetadata', update, { signal });
listen(target, 'emptied', update, { signal });
sync();
listen(target, 'timeupdate', sync, { signal });
listen(target, 'durationchange', sync, { signal });
listen(target, 'seeked', sync, { signal });
listen(target, 'loadedmetadata', sync, { signal });
listen(target, 'emptied', sync, { signal });
},
});
@@ -33,13 +33,12 @@ export const volumeFeature = defineFeature<HTMLMediaElement>()({
},
}),
getSnapshot: ({ target }) => ({
volume: target.volume,
muted: target.muted,
}),
attach({ target, signal, set }) {
const sync = () => set({ volume: target.volume, muted: target.muted });
subscribe: ({ target, update, signal }) => {
listen(target, 'volumechange', update, { signal });
sync();
listen(target, 'volumechange', sync, { signal });
},
});