@flighthq/mediasession 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ export * from './mediasession';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './mediasession';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { MediaSessionAction, MediaSessionActionDetails, MediaSessionBackend, MediaSessionMetadata, MediaSessionPlaybackState, MediaSessionPositionState } from '@flighthq/types';
2
+ export declare function clearMediaSessionActionHandler(action: MediaSessionAction): void;
3
+ export declare function clearMediaSessionMetadata(): void;
4
+ export declare function clearMediaSessionPositionState(): void;
5
+ export declare function createWebMediaSessionBackend(): MediaSessionBackend;
6
+ export declare function getMediaSessionBackend(): MediaSessionBackend;
7
+ export declare function setMediaSessionActionHandler(action: MediaSessionAction, handler: (details: Readonly<MediaSessionActionDetails>) => void): void;
8
+ export declare function setMediaSessionBackend(backend: MediaSessionBackend | null): void;
9
+ export declare function setMediaSessionMetadata(metadata: Readonly<MediaSessionMetadata>): void;
10
+ export declare function setMediaSessionPlaybackState(state: MediaSessionPlaybackState): void;
11
+ export declare function setMediaSessionPositionState(state: Readonly<MediaSessionPositionState>): void;
12
+ //# sourceMappingURL=mediasession.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediasession.d.ts","sourceRoot":"","sources":["../src/mediasession.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,yBAAyB,EACzB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,yBAAyB,EAC1B,MAAM,iBAAiB,CAAC;AAGzB,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI,CAE/E;AAGD,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAGD,wBAAgB,8BAA8B,IAAI,IAAI,CAErD;AAKD,wBAAgB,4BAA4B,IAAI,mBAAmB,CAuClE;AAGD,wBAAgB,sBAAsB,IAAI,mBAAmB,CAG5D;AAID,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,yBAAyB,CAAC,KAAK,IAAI,GAC9D,IAAI,CAEN;AAGD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAEhF;AAGD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAEtF;AAGD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,yBAAyB,GAAG,IAAI,CAEnF;AAGD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,QAAQ,CAAC,yBAAyB,CAAC,GAAG,IAAI,CAE7F"}
@@ -0,0 +1,95 @@
1
+ // Clears the handler for a transport action; the OS drops the corresponding button.
2
+ export function clearMediaSessionActionHandler(action) {
3
+ getMediaSessionBackend().setActionHandler(action, null);
4
+ }
5
+ // Clears the now-playing card from the OS media UI.
6
+ export function clearMediaSessionMetadata() {
7
+ getMediaSessionBackend().setMetadata(null);
8
+ }
9
+ // Clears the scrubber position/duration the OS shows.
10
+ export function clearMediaSessionPositionState() {
11
+ getMediaSessionBackend().setPositionState(null);
12
+ }
13
+ // Builds the default web backend over navigator.mediaSession. Every method is a no-op when the API
14
+ // (or a specific capability such as setPositionState / MediaMetadata) is absent — jsdom, older
15
+ // browsers, non-secure contexts — rather than throwing.
16
+ export function createWebMediaSessionBackend() {
17
+ return {
18
+ setMetadata(metadata) {
19
+ const session = getWebMediaSession();
20
+ if (session === null)
21
+ return;
22
+ if (metadata === null) {
23
+ session.metadata = null;
24
+ return;
25
+ }
26
+ if (typeof MediaMetadata === 'undefined')
27
+ return;
28
+ session.metadata = new MediaMetadata({
29
+ title: metadata.title,
30
+ artist: metadata.artist,
31
+ album: metadata.album,
32
+ artwork: [...metadata.artwork],
33
+ });
34
+ },
35
+ setPlaybackState(state) {
36
+ const session = getWebMediaSession();
37
+ if (session === null)
38
+ return;
39
+ session.playbackState = state;
40
+ },
41
+ setPositionState(state) {
42
+ const session = getWebMediaSession();
43
+ if (session === null || typeof session.setPositionState !== 'function')
44
+ return;
45
+ // A null position clears the OS scrubber; the web API spells "clear" as an omitted argument.
46
+ session.setPositionState(state ?? undefined);
47
+ },
48
+ setActionHandler(action, handler) {
49
+ const session = getWebMediaSession();
50
+ if (session === null)
51
+ return;
52
+ try {
53
+ // Some browsers throw for an action they do not support; treat that as a no-op.
54
+ session.setActionHandler(action, handler ? (details) => handler(details) : null);
55
+ }
56
+ catch {
57
+ // Unsupported action — leave it unregistered.
58
+ }
59
+ },
60
+ };
61
+ }
62
+ // The active media-session backend, or a lazily-created web default. There is always a backend.
63
+ export function getMediaSessionBackend() {
64
+ if (_backend === null)
65
+ _backend = createWebMediaSessionBackend();
66
+ return _backend;
67
+ }
68
+ // Registers a handler the OS invokes when the user presses the corresponding transport button
69
+ // (play/pause/next/seek/…). Pass null via clearMediaSessionActionHandler to remove it.
70
+ export function setMediaSessionActionHandler(action, handler) {
71
+ getMediaSessionBackend().setActionHandler(action, handler);
72
+ }
73
+ // Installs a native host media-session backend; pass null to fall back to the web default.
74
+ export function setMediaSessionBackend(backend) {
75
+ _backend = backend;
76
+ }
77
+ // Publishes the now-playing card (title/artist/album/artwork) to the OS media UI.
78
+ export function setMediaSessionMetadata(metadata) {
79
+ getMediaSessionBackend().setMetadata(metadata);
80
+ }
81
+ // Reports whether media is playing/paused/absent so the OS shows the right play/pause affordance.
82
+ export function setMediaSessionPlaybackState(state) {
83
+ getMediaSessionBackend().setPlaybackState(state);
84
+ }
85
+ // Publishes the scrubber position/duration/playbackRate so the OS can render an accurate seek bar.
86
+ export function setMediaSessionPositionState(state) {
87
+ getMediaSessionBackend().setPositionState(state);
88
+ }
89
+ let _backend = null;
90
+ function getWebMediaSession() {
91
+ if (typeof navigator === 'undefined' || !('mediaSession' in navigator))
92
+ return null;
93
+ return navigator.mediaSession;
94
+ }
95
+ //# sourceMappingURL=mediasession.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mediasession.js","sourceRoot":"","sources":["../src/mediasession.ts"],"names":[],"mappings":"AASA,oFAAoF;AACpF,MAAM,UAAU,8BAA8B,CAAC,MAA0B;IACvE,sBAAsB,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAC1D,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,yBAAyB;IACvC,sBAAsB,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,8BAA8B;IAC5C,sBAAsB,EAAE,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC;AAClD,CAAC;AAED,mGAAmG;AACnG,+FAA+F;AAC/F,wDAAwD;AACxD,MAAM,UAAU,4BAA4B;IAC1C,OAAO;QACL,WAAW,CAAC,QAAQ;YAClB,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;YACrC,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO;YAC7B,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;gBACtB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;gBACxB,OAAO;YACT,CAAC;YACD,IAAI,OAAO,aAAa,KAAK,WAAW;gBAAE,OAAO;YACjD,OAAO,CAAC,QAAQ,GAAG,IAAI,aAAa,CAAC;gBACnC,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,MAAM,EAAE,QAAQ,CAAC,MAAM;gBACvB,KAAK,EAAE,QAAQ,CAAC,KAAK;gBACrB,OAAO,EAAE,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC;aAC/B,CAAC,CAAC;QACL,CAAC;QACD,gBAAgB,CAAC,KAAK;YACpB,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;YACrC,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO;YAC7B,OAAO,CAAC,aAAa,GAAG,KAAK,CAAC;QAChC,CAAC;QACD,gBAAgB,CAAC,KAAK;YACpB,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;YACrC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,CAAC,gBAAgB,KAAK,UAAU;gBAAE,OAAO;YAC/E,6FAA6F;YAC7F,OAAO,CAAC,gBAAgB,CAAC,KAAK,IAAI,SAAS,CAAC,CAAC;QAC/C,CAAC;QACD,gBAAgB,CAAC,MAAM,EAAE,OAAO;YAC9B,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;YACrC,IAAI,OAAO,KAAK,IAAI;gBAAE,OAAO;YAC7B,IAAI,CAAC;gBACH,gFAAgF;gBAChF,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAoC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAChH,CAAC;YAAC,MAAM,CAAC;gBACP,8CAA8C;YAChD,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,MAAM,UAAU,sBAAsB;IACpC,IAAI,QAAQ,KAAK,IAAI;QAAE,QAAQ,GAAG,4BAA4B,EAAE,CAAC;IACjE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,8FAA8F;AAC9F,uFAAuF;AACvF,MAAM,UAAU,4BAA4B,CAC1C,MAA0B,EAC1B,OAA+D;IAE/D,sBAAsB,EAAE,CAAC,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC7D,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,sBAAsB,CAAC,OAAmC;IACxE,QAAQ,GAAG,OAAO,CAAC;AACrB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,uBAAuB,CAAC,QAAwC;IAC9E,sBAAsB,EAAE,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AACjD,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,4BAA4B,CAAC,KAAgC;IAC3E,sBAAsB,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACnD,CAAC;AAED,mGAAmG;AACnG,MAAM,UAAU,4BAA4B,CAAC,KAA0C;IACrF,sBAAsB,EAAE,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;AACnD,CAAC;AAED,IAAI,QAAQ,GAA+B,IAAI,CAAC;AAEhD,SAAS,kBAAkB;IACzB,IAAI,OAAO,SAAS,KAAK,WAAW,IAAI,CAAC,CAAC,cAAc,IAAI,SAAS,CAAC;QAAE,OAAO,IAAI,CAAC;IACpF,OAAO,SAAS,CAAC,YAAY,CAAC;AAChC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/mediasession",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "OS media-session integration (now-playing metadata, playback/position state, transport-action handlers) over a swappable web/native backend wrapping navigator.mediaSession",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,295 @@
1
+ import type { MediaSessionActionDetails, MediaSessionBackend } from '@flighthq/types';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import {
5
+ clearMediaSessionActionHandler,
6
+ clearMediaSessionMetadata,
7
+ clearMediaSessionPositionState,
8
+ createWebMediaSessionBackend,
9
+ getMediaSessionBackend,
10
+ setMediaSessionActionHandler,
11
+ setMediaSessionBackend,
12
+ setMediaSessionMetadata,
13
+ setMediaSessionPlaybackState,
14
+ setMediaSessionPositionState,
15
+ } from './mediasession';
16
+
17
+ interface FakeMediaSession {
18
+ metadata: unknown;
19
+ playbackState: string;
20
+ positionCalls: unknown[];
21
+ handlers: Map<string, ((details: MediaSessionActionDetails) => void) | null>;
22
+ setPositionState?: (state?: unknown) => void;
23
+ setActionHandler(action: string, handler: ((details: MediaSessionActionDetails) => void) | null): void;
24
+ }
25
+
26
+ function createFakeBackend(): MediaSessionBackend & { calls: string[]; args: unknown[][] } {
27
+ const calls: string[] = [];
28
+ const args: unknown[][] = [];
29
+ return {
30
+ calls,
31
+ args,
32
+ setMetadata(metadata) {
33
+ calls.push('setMetadata');
34
+ args.push([metadata]);
35
+ },
36
+ setPlaybackState(state) {
37
+ calls.push('setPlaybackState');
38
+ args.push([state]);
39
+ },
40
+ setPositionState(state) {
41
+ calls.push('setPositionState');
42
+ args.push([state]);
43
+ },
44
+ setActionHandler(action, handler) {
45
+ calls.push('setActionHandler');
46
+ args.push([action, handler]);
47
+ },
48
+ };
49
+ }
50
+
51
+ function installFakeMediaSession(unsupportedAction?: string): FakeMediaSession {
52
+ const session: FakeMediaSession = {
53
+ metadata: undefined,
54
+ playbackState: 'none',
55
+ positionCalls: [],
56
+ handlers: new Map(),
57
+ setPositionState(state?: unknown) {
58
+ this.positionCalls.push(state);
59
+ },
60
+ setActionHandler(action, handler) {
61
+ if (action === unsupportedAction) throw new Error('unsupported action');
62
+ this.handlers.set(action, handler);
63
+ },
64
+ };
65
+ Object.defineProperty(navigator, 'mediaSession', { value: session, configurable: true });
66
+ return session;
67
+ }
68
+
69
+ function removeMediaSession(): void {
70
+ if ('mediaSession' in navigator) {
71
+ Object.defineProperty(navigator, 'mediaSession', { value: undefined, configurable: true });
72
+ delete (navigator as { mediaSession?: unknown }).mediaSession;
73
+ }
74
+ }
75
+
76
+ class FakeMediaMetadata {
77
+ title: string;
78
+ artist: string;
79
+ album: string;
80
+ artwork: readonly unknown[];
81
+ constructor(init: { title: string; artist: string; album: string; artwork: readonly unknown[] }) {
82
+ this.title = init.title;
83
+ this.artist = init.artist;
84
+ this.album = init.album;
85
+ this.artwork = init.artwork;
86
+ }
87
+ }
88
+
89
+ beforeEach(() => {
90
+ setMediaSessionBackend(null);
91
+ removeMediaSession();
92
+ (globalThis as { MediaMetadata?: unknown }).MediaMetadata = FakeMediaMetadata;
93
+ });
94
+
95
+ afterEach(() => {
96
+ setMediaSessionBackend(null);
97
+ removeMediaSession();
98
+ delete (globalThis as { MediaMetadata?: unknown }).MediaMetadata;
99
+ });
100
+
101
+ describe('clearMediaSessionActionHandler', () => {
102
+ it('routes a null action handler through the active backend', () => {
103
+ const fake = createFakeBackend();
104
+ setMediaSessionBackend(fake);
105
+ clearMediaSessionActionHandler('play');
106
+ expect(fake.calls).toEqual(['setActionHandler']);
107
+ expect(fake.args[0]).toEqual(['play', null]);
108
+ });
109
+
110
+ it('unregisters the web handler for the action', () => {
111
+ const session = installFakeMediaSession();
112
+ setMediaSessionActionHandler('pause', () => {});
113
+ clearMediaSessionActionHandler('pause');
114
+ expect(session.handlers.get('pause')).toBeNull();
115
+ });
116
+ });
117
+
118
+ describe('clearMediaSessionMetadata', () => {
119
+ it('routes a null metadata set through the active backend', () => {
120
+ const fake = createFakeBackend();
121
+ setMediaSessionBackend(fake);
122
+ clearMediaSessionMetadata();
123
+ expect(fake.calls).toEqual(['setMetadata']);
124
+ expect(fake.args[0]).toEqual([null]);
125
+ });
126
+
127
+ it('assigns null to the web session metadata', () => {
128
+ const session = installFakeMediaSession();
129
+ setMediaSessionMetadata({ title: 'A', artist: 'B', album: 'C', artwork: [] });
130
+ clearMediaSessionMetadata();
131
+ expect(session.metadata).toBeNull();
132
+ });
133
+ });
134
+
135
+ describe('clearMediaSessionPositionState', () => {
136
+ it('routes a null position through the active backend', () => {
137
+ const fake = createFakeBackend();
138
+ setMediaSessionBackend(fake);
139
+ clearMediaSessionPositionState();
140
+ expect(fake.calls).toEqual(['setPositionState']);
141
+ expect(fake.args[0]).toEqual([null]);
142
+ });
143
+
144
+ it('clears the web position by passing undefined', () => {
145
+ const session = installFakeMediaSession();
146
+ clearMediaSessionPositionState();
147
+ expect(session.positionCalls).toEqual([undefined]);
148
+ });
149
+ });
150
+
151
+ describe('createWebMediaSessionBackend', () => {
152
+ it('sets metadata via a MediaMetadata instance', () => {
153
+ const session = installFakeMediaSession();
154
+ const backend = createWebMediaSessionBackend();
155
+ backend.setMetadata({ title: 'Song', artist: 'Artist', album: 'Album', artwork: [{ src: 'a.png' }] });
156
+ expect(session.metadata).toBeInstanceOf(FakeMediaMetadata);
157
+ expect((session.metadata as FakeMediaMetadata).title).toBe('Song');
158
+ expect((session.metadata as FakeMediaMetadata).artwork).toEqual([{ src: 'a.png' }]);
159
+ });
160
+
161
+ it('does not construct MediaMetadata when it is absent', () => {
162
+ const session = installFakeMediaSession();
163
+ delete (globalThis as { MediaMetadata?: unknown }).MediaMetadata;
164
+ const backend = createWebMediaSessionBackend();
165
+ session.metadata = 'unchanged';
166
+ backend.setMetadata({ title: 'Song', artist: 'Artist', album: 'Album', artwork: [] });
167
+ expect(session.metadata).toBe('unchanged');
168
+ });
169
+
170
+ it('assigns the playback state', () => {
171
+ const session = installFakeMediaSession();
172
+ const backend = createWebMediaSessionBackend();
173
+ backend.setPlaybackState('playing');
174
+ expect(session.playbackState).toBe('playing');
175
+ });
176
+
177
+ it('forwards the position state to setPositionState', () => {
178
+ const session = installFakeMediaSession();
179
+ const backend = createWebMediaSessionBackend();
180
+ backend.setPositionState({ duration: 100, playbackRate: 1, position: 10 });
181
+ expect(session.positionCalls).toEqual([{ duration: 100, playbackRate: 1, position: 10 }]);
182
+ });
183
+
184
+ it('no-ops setPositionState when the method is absent', () => {
185
+ const session = installFakeMediaSession();
186
+ delete session.setPositionState;
187
+ const backend = createWebMediaSessionBackend();
188
+ expect(() => backend.setPositionState({ duration: 1, playbackRate: 1, position: 0 })).not.toThrow();
189
+ });
190
+
191
+ it('registers an action handler and maps fired details to the caller', () => {
192
+ const session = installFakeMediaSession();
193
+ const backend = createWebMediaSessionBackend();
194
+ const handler = vi.fn();
195
+ backend.setActionHandler('seekto', handler);
196
+ const registered = session.handlers.get('seekto');
197
+ expect(registered).toBeTypeOf('function');
198
+ registered?.({ action: 'seekto', seekTime: 42, fastSeek: true });
199
+ expect(handler).toHaveBeenCalledWith({ action: 'seekto', seekTime: 42, fastSeek: true });
200
+ });
201
+
202
+ it('swallows an unsupported-action throw from setActionHandler', () => {
203
+ const session = installFakeMediaSession('skipad');
204
+ const backend = createWebMediaSessionBackend();
205
+ expect(() => backend.setActionHandler('skipad', () => {})).not.toThrow();
206
+ expect(session.handlers.has('skipad')).toBe(false);
207
+ });
208
+
209
+ it('is a no-op in an environment without navigator.mediaSession', () => {
210
+ removeMediaSession();
211
+ const backend = createWebMediaSessionBackend();
212
+ expect(() => {
213
+ backend.setMetadata({ title: 'A', artist: 'B', album: 'C', artwork: [] });
214
+ backend.setMetadata(null);
215
+ backend.setPlaybackState('paused');
216
+ backend.setPositionState({ duration: 1, playbackRate: 1, position: 0 });
217
+ backend.setPositionState(null);
218
+ backend.setActionHandler('play', () => {});
219
+ backend.setActionHandler('play', null);
220
+ }).not.toThrow();
221
+ });
222
+ });
223
+
224
+ describe('getMediaSessionBackend', () => {
225
+ it('lazily creates a web default backend when none is set', () => {
226
+ const backend = getMediaSessionBackend();
227
+ expect(backend).toBeTypeOf('object');
228
+ expect(backend.setMetadata).toBeTypeOf('function');
229
+ });
230
+
231
+ it('returns the same lazily-created backend on repeat calls', () => {
232
+ expect(getMediaSessionBackend()).toBe(getMediaSessionBackend());
233
+ });
234
+
235
+ it('returns an installed backend', () => {
236
+ const fake = createFakeBackend();
237
+ setMediaSessionBackend(fake);
238
+ expect(getMediaSessionBackend()).toBe(fake);
239
+ });
240
+ });
241
+
242
+ describe('setMediaSessionActionHandler', () => {
243
+ it('routes the handler through the active backend', () => {
244
+ const fake = createFakeBackend();
245
+ setMediaSessionBackend(fake);
246
+ const handler = () => {};
247
+ setMediaSessionActionHandler('nexttrack', handler);
248
+ expect(fake.calls).toEqual(['setActionHandler']);
249
+ expect(fake.args[0]).toEqual(['nexttrack', handler]);
250
+ });
251
+ });
252
+
253
+ describe('setMediaSessionBackend', () => {
254
+ it('installs a backend and null restores the lazy web default', () => {
255
+ const fake = createFakeBackend();
256
+ setMediaSessionBackend(fake);
257
+ expect(getMediaSessionBackend()).toBe(fake);
258
+ setMediaSessionBackend(null);
259
+ const restored = getMediaSessionBackend();
260
+ expect(restored).not.toBe(fake);
261
+ expect(restored.setMetadata).toBeTypeOf('function');
262
+ });
263
+ });
264
+
265
+ describe('setMediaSessionMetadata', () => {
266
+ it('routes the metadata through the active backend', () => {
267
+ const fake = createFakeBackend();
268
+ setMediaSessionBackend(fake);
269
+ const metadata = { title: 'A', artist: 'B', album: 'C', artwork: [] };
270
+ setMediaSessionMetadata(metadata);
271
+ expect(fake.calls).toEqual(['setMetadata']);
272
+ expect(fake.args[0]).toEqual([metadata]);
273
+ });
274
+ });
275
+
276
+ describe('setMediaSessionPlaybackState', () => {
277
+ it('routes the playback state through the active backend', () => {
278
+ const fake = createFakeBackend();
279
+ setMediaSessionBackend(fake);
280
+ setMediaSessionPlaybackState('playing');
281
+ expect(fake.calls).toEqual(['setPlaybackState']);
282
+ expect(fake.args[0]).toEqual(['playing']);
283
+ });
284
+ });
285
+
286
+ describe('setMediaSessionPositionState', () => {
287
+ it('routes the position state through the active backend', () => {
288
+ const fake = createFakeBackend();
289
+ setMediaSessionBackend(fake);
290
+ const state = { duration: 200, playbackRate: 1, position: 50 };
291
+ setMediaSessionPositionState(state);
292
+ expect(fake.calls).toEqual(['setPositionState']);
293
+ expect(fake.args[0]).toEqual([state]);
294
+ });
295
+ });