@landr/audio-player 0.2.0-feat-CORE-23079-shared-player-audio-10175.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.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @landr/audio-player
2
+
3
+ Headless shared audio playback orchestration for React apps.
4
+
5
+ The package owns player state, queue behavior, repeat/shuffle behavior, progress subscriptions, and normalized playback engine contracts. It does not own product UI, waveform rendering, stream fetching, analytics, notifications, or app-specific logging.
6
+
7
+ Browser builds provide an `HTMLAudioElement` engine by default. Native and default builds require `createEngine`.
8
+
9
+ ## Public API
10
+
11
+ ```tsx
12
+ import { PlayerProvider, usePlayer, useTrackProgress, usePlayerQueue } from '@landr/audio-player';
13
+ import type { IPlayerEngine, PlayerTrack } from '@landr/audio-player';
14
+ import { EngineState, RepeatMode } from '@landr/audio-player';
15
+ ```
16
+
17
+ The public package surface is intentionally React-first:
18
+
19
+ - `PlayerProvider`
20
+ - `usePlayer()`
21
+ - `useTrackProgress()`
22
+ - `usePlayerQueue()`
23
+ - `PlayerTrack<TMeta = unknown>`
24
+ - `EngineState`
25
+ - `RepeatMode`
26
+ - `IPlayerEngine`
27
+
28
+ The `Player` class, basic queue implementation, and browser engine are internal implementation details. Consumers should interact through the provider and hooks.
29
+
30
+ ## Provider Usage
31
+
32
+ Browser consumers can rely on the default `HTMLAudioElement` engine:
33
+
34
+ ```tsx
35
+ <PlayerProvider>
36
+ <App />
37
+ </PlayerProvider>
38
+ ```
39
+
40
+ Native consumers must provide a platform engine:
41
+
42
+ ```tsx
43
+ <PlayerProvider createEngine={() => new ExpoAudioPlayer()}>
44
+ <App />
45
+ </PlayerProvider>
46
+ ```
47
+
48
+ Apps can keep stream loading, analytics, notifications, and waveforms outside this package by using `resolveTrackUrl` and app-level effects:
49
+
50
+ ```tsx
51
+ <PlayerProvider resolveTrackUrl={(track) => loadStream(track.id)}>
52
+ <App />
53
+ </PlayerProvider>
54
+ ```
55
+
56
+ ## Conditional Exports
57
+
58
+ The package supports the same import path in every runtime:
59
+
60
+ ```ts
61
+ import { PlayerProvider } from '@landr/audio-player';
62
+ ```
63
+
64
+ The `browser` export creates a default `HTMLAudioElement` engine when `createEngine` is not provided. The `default` and `react-native` exports do not import DOM code and throw a clear error if `createEngine` is missing.
65
+
66
+ ## Engine Contract
67
+
68
+ Custom engines implement `IPlayerEngine` and emit normalized events:
69
+
70
+ - `state`
71
+ - `position`
72
+ - `remote`
73
+ - `error`
74
+
75
+ Durations and positions should be finite numbers. Browser playback normalizes `NaN` and `Infinity` to `0`.
76
+
77
+ ## Local Workflow
78
+
79
+ From `LANDR.Web.Libs/packages/audio-player`:
80
+
81
+ ```sh
82
+ pnpm watch:rollup
83
+ ```
84
+
85
+ From `LANDR.Web.Projects`, use the local dependency:
86
+
87
+ ```json
88
+ "@landr/audio-player": "../../../LANDR.Web.Libs/packages/audio-player"
89
+ ```
90
+
91
+ Then run:
92
+
93
+ ```sh
94
+ pnpm install
95
+ ```
96
+
97
+ From `LANDR.MobileApp`, use the local dependency:
98
+
99
+ ```json
100
+ "@landr/audio-player": "../LANDR.Web.Libs/packages/audio-player"
101
+ ```
102
+
103
+ Then run:
104
+
105
+ ```sh
106
+ bun i
107
+ ```
108
+
109
+ Metro, Jest, and Rsbuild must resolve `react`, `react-dom`, and `react-native` from the consuming app to avoid duplicate React instances when using the local linked package.
@@ -0,0 +1,47 @@
1
+ import { type IPlayer, type PlayerDependencies, type PlayerSnapshot, type PlayerTrack, RepeatMode } from './types';
2
+ export declare class Player implements IPlayer {
3
+ private static readonly PREVIOUS_TRACK_THRESHOLD_SECONDS;
4
+ private static readonly RELOAD_REQUIRED_STATES;
5
+ private readonly queue;
6
+ private readonly engine;
7
+ private readonly resolveTrackUrl?;
8
+ private readonly logger?;
9
+ private loadedTrackId;
10
+ private snapshot;
11
+ private position;
12
+ private readonly subscribers;
13
+ private readonly progressSubscribers;
14
+ private readonly unsubscribers;
15
+ constructor({ queue, engine, resolveTrackUrl, logger }: PlayerDependencies);
16
+ subscribe(listener: (snapshot: PlayerSnapshot) => void): () => void;
17
+ getSnapshot(): PlayerSnapshot;
18
+ getPosition(): number;
19
+ subscribeToProgress(listener: () => void): () => void;
20
+ play(track: PlayerTrack): Promise<void>;
21
+ pause(): Promise<void>;
22
+ toggle(): Promise<void>;
23
+ seek(position: number): Promise<void>;
24
+ skipNext(): Promise<void>;
25
+ skipPrevious(): Promise<void>;
26
+ addNext(items: PlayerTrack[]): void;
27
+ addToEnd(items: PlayerTrack[]): void;
28
+ removeFromQueue(trackId: PlayerTrack['id']): void;
29
+ setRepeatMode(mode: RepeatMode): void;
30
+ setShuffleMode(enabled: boolean): void;
31
+ replaceQueue(items: PlayerTrack[]): Promise<void>;
32
+ destroy(): Promise<void>;
33
+ private subscribeToEngineEvents;
34
+ private subscribeToQueueEvents;
35
+ private readonly handleEngineProgressEvent;
36
+ private readonly handleEngineStateEvent;
37
+ private readonly handleRemoteEvent;
38
+ private notifySubscribers;
39
+ private shouldReloadTrack;
40
+ private shouldMoveToPreviousTrack;
41
+ private resetPosition;
42
+ private createSnapshot;
43
+ private updateActiveLockScreenControls;
44
+ private getLockScreenControls;
45
+ private resolvePlayableTrack;
46
+ private ensureTrackIsInQueue;
47
+ }
@@ -0,0 +1,40 @@
1
+ import { EngineState, type IPlayerEngine, type LockScreenControls, type PlayerEngineEvent, type PlayerEngineEventMap, type PlayerTrack } from '../types';
2
+ export type HtmlAudioPlayerEngineOptions = {
3
+ audio?: HTMLAudioElement;
4
+ progressIntervalMs?: number;
5
+ };
6
+ export declare class HtmlAudioPlayerEngine implements IPlayerEngine {
7
+ private readonly audio;
8
+ private readonly progressIntervalMs;
9
+ private state;
10
+ private progressInterval;
11
+ private readonly listeners;
12
+ constructor({ audio, progressIntervalMs }?: HtmlAudioPlayerEngineOptions);
13
+ getState(): EngineState;
14
+ setup(): Promise<void>;
15
+ loadTrack(track: PlayerTrack, _controls: LockScreenControls): Promise<void>;
16
+ play(): Promise<void>;
17
+ pause(): Promise<void>;
18
+ seek(position: number): Promise<void>;
19
+ getPosition(): number;
20
+ getDuration(): number;
21
+ updateActiveLockScreenControls(_controls: LockScreenControls): void;
22
+ clearLockScreenControls(): void;
23
+ destroy(): Promise<void>;
24
+ on<TEvent extends PlayerEngineEvent>(event: TEvent, handler: (payload: PlayerEngineEventMap[TEvent]) => void): () => void;
25
+ private readonly handleLoadedMetadata;
26
+ private readonly handleCanPlay;
27
+ private readonly handleWaiting;
28
+ private readonly handlePlaying;
29
+ private readonly handlePause;
30
+ private readonly handleEnded;
31
+ private readonly handleTimeUpdate;
32
+ private readonly handleAudioError;
33
+ private subscribeToAudioEvents;
34
+ private unsubscribeFromAudioEvents;
35
+ private startProgressPolling;
36
+ private stopProgressPolling;
37
+ private emitPosition;
38
+ private setState;
39
+ private emit;
40
+ }