rivmux 0.1.0 → 0.3.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 CHANGED
@@ -1,3 +1,202 @@
1
1
  # Rivmux Player
2
2
 
3
3
  Public browser player facade for Rivmux low-latency live playback.
4
+
5
+ `rivmux` loads HTTP-FLV streams in a dedicated worker, transmuxes supported
6
+ audio/video into fragmented MP4, and attaches the resulting media stream to a
7
+ browser `<video>` element.
8
+
9
+ M1 supports only the dedicated-worker MSE runtime path. Main-thread MSE and
10
+ custom precompiled WASM module injection are not exposed runtime capabilities.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ pnpm add rivmux
16
+ ```
17
+
18
+ ```sh
19
+ npm install rivmux
20
+ ```
21
+
22
+ ## Basic Usage
23
+
24
+ Always wait for `attach()` before calling `start()`. `attach()` initializes the
25
+ worker and connects the internal `MediaSourceHandle` to the video element.
26
+
27
+ ```ts
28
+ import { RivmuxPlayer } from 'rivmux'
29
+
30
+ const video = document.querySelector<HTMLVideoElement>('#player')
31
+
32
+ if (!video) {
33
+ throw new Error('Missing video element')
34
+ }
35
+
36
+ const player = new RivmuxPlayer('https://example.com/live.flv', {
37
+ playback: {
38
+ muted: true,
39
+ },
40
+ })
41
+
42
+ player.on('mediaInfo', (info) => {
43
+ console.log('media info', info)
44
+ })
45
+
46
+ player.on('error', (error) => {
47
+ console.error(`[${error.code}] ${error.message}`)
48
+ })
49
+
50
+ await player.attach(video)
51
+ await player.start()
52
+
53
+ // Later:
54
+ await player.stop()
55
+ await player.destroy()
56
+ ```
57
+
58
+ ## Player Lifecycle
59
+
60
+ ```ts
61
+ const player = new RivmuxPlayer(url, options)
62
+ ```
63
+
64
+ | Call | Description |
65
+ | --------------------------------- | -------------------------------------------------------------------------------------------------------- |
66
+ | `new RivmuxPlayer(url, options?)` | Creates a player instance for one stream URL. |
67
+ | `await player.attach(video)` | Attaches the player to a `<video>` element and prepares the worker/MSE pipeline. |
68
+ | `await player.start()` | Starts loading, transmuxing, buffering, and playback control. Call after `attach()`. |
69
+ | `await player.stop()` | Stops loading, detaches the media source, and keeps the instance reusable. |
70
+ | `await player.destroy()` | Releases the worker, timers, listeners, and video source. The instance cannot be reused after this call. |
71
+ | `player.on(type, listener)` | Subscribes to a player event. |
72
+ | `player.off(type, listener)` | Removes a previously registered listener. |
73
+
74
+ ## Options
75
+
76
+ All options are optional. Missing fields are merged with
77
+ `DEFAULT_RIVMUX_PLAYER_OPTIONS`.
78
+
79
+ ```ts
80
+ import { RivmuxPlayer } from 'rivmux'
81
+
82
+ const player = new RivmuxPlayer('https://example.com/live.flv', {
83
+ playback: {
84
+ autoPlay: true,
85
+ muted: true,
86
+ },
87
+ latency: {
88
+ startupBuffer: 0.35,
89
+ target: 1.2,
90
+ max: 2.5,
91
+ maxForwardBuffer: 4,
92
+ backwardBuffer: 1.5,
93
+ },
94
+ network: {
95
+ credentials: 'include',
96
+ headers: {
97
+ Authorization: 'Bearer token',
98
+ },
99
+ retry: {
100
+ maxAttempts: 5,
101
+ backoffMs: 500,
102
+ },
103
+ },
104
+ runtime: {
105
+ workerUrl: '/assets/rivmux-runtime-worker.js',
106
+ wasmUrl: '/assets/rivmux-transmux-core.wasm',
107
+ },
108
+ diagnostics: {
109
+ statsIntervalMs: 1000,
110
+ debug: false,
111
+ },
112
+ })
113
+ ```
114
+
115
+ ### `playback`
116
+
117
+ | Option | Default | Description |
118
+ | ---------- | ------- | --------------------------------------------------------------------------------- |
119
+ | `autoPlay` | `true` | Lets the runtime request `video.play()` when enough startup buffer is available. |
120
+ | `muted` | `false` | Sets `video.muted`. Many browsers require muted playback for autoplay with audio. |
121
+
122
+ ### `latency`
123
+
124
+ Values are in seconds.
125
+
126
+ | Option | Default | Description |
127
+ | ------------------ | ------- | ---------------------------------------------------------------------------------------------- |
128
+ | `startupBuffer` | `0.35` | Buffered duration required before automatic playback starts. |
129
+ | `target` | `1.2` | Desired live latency. The runtime uses this when resuming fetches and restoring playback rate. |
130
+ | `max` | `2.5` | Maximum tolerated live latency before the runtime seeks closer to the live edge. |
131
+ | `maxForwardBuffer` | `4` | Forward buffer threshold where the loader may pause to avoid excessive buffering. |
132
+ | `backwardBuffer` | `1.5` | Amount of buffer to keep behind the current playhead during cleanup. |
133
+
134
+ ### `network`
135
+
136
+ | Option | Default | Description |
137
+ | ------------------- | --------------- | ----------------------------------------------------------------- |
138
+ | `headers` | `{}` | Extra request headers for the HTTP-FLV stream. |
139
+ | `credentials` | `'same-origin'` | Fetch credentials mode for stream requests. |
140
+ | `retry.maxAttempts` | `3` | Maximum stream request attempts before reporting a network error. |
141
+ | `retry.backoffMs` | `500` | Base retry delay in milliseconds. |
142
+
143
+ ### `runtime`
144
+
145
+ | Option | Default | Description |
146
+ | ----------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `preferWorkerMse` | `true` | Must remain `true` in M1. Main-thread MSE fallback is not implemented. |
148
+ | `workerUrl` | bundled worker URL | Overrides the worker script URL. Use this when serving worker assets from a fixed public path. |
149
+ | `wasmUrl` | bundled WASM module | Overrides the WASM asset URL. If provided, the matching wasm-bindgen JS glue file must be available at the same path with `.js` instead of `.wasm`. |
150
+
151
+ ### `diagnostics`
152
+
153
+ | Option | Default | Description |
154
+ | ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
155
+ | `statsIntervalMs` | `1000` | Requested stats interval in milliseconds. Runtime reporting is clamped internally for stable video-state feedback. |
156
+ | `debug` | `false` | Enables debug-oriented behavior where supported by the runtime. |
157
+
158
+ ## Events
159
+
160
+ ```ts
161
+ import type { MediaInfo, PlayerError, PlayerStats, PlayerWarning } from 'rivmux'
162
+
163
+ player.on('ready', () => {})
164
+ player.on('mediaInfo', (info: MediaInfo) => {})
165
+ player.on('stats', (stats: PlayerStats) => {})
166
+ player.on('warning', (warning: PlayerWarning) => {})
167
+ player.on('error', (error: PlayerError) => {})
168
+ player.on('stopped', () => {})
169
+ player.on('destroyed', () => {})
170
+ ```
171
+
172
+ | Event | Payload | Description |
173
+ | ----------- | --------------- | ----------------------------------------------------------------------- |
174
+ | `ready` | `undefined` | The worker/runtime is initialized. |
175
+ | `mediaInfo` | `MediaInfo` | Stream container and codec metadata has been detected. |
176
+ | `stats` | `PlayerStats` | Runtime diagnostics such as bytes, buffer, latency, and playback state. |
177
+ | `warning` | `PlayerWarning` | Recoverable issue reported by the runtime. |
178
+ | `error` | `PlayerError` | Runtime, network, demux, codec, mux, MSE, or unsupported-feature error. |
179
+ | `stopped` | `undefined` | The stream has stopped and the media source has been detached. |
180
+ | `destroyed` | `undefined` | The worker/runtime has been destroyed. |
181
+
182
+ ## Type Imports
183
+
184
+ The player package re-exports the key public types from `@rivmux/protocol`:
185
+
186
+ ```ts
187
+ import type {
188
+ DiagnosticsOptions,
189
+ LatencyOptions,
190
+ MediaInfo,
191
+ NetworkOptions,
192
+ PlaybackOptions,
193
+ PlayerError,
194
+ PlayerStats,
195
+ PlayerWarning,
196
+ RivmuxPlayerOptions,
197
+ RuntimeOptions,
198
+ } from 'rivmux'
199
+ ```
200
+
201
+ Use `normalizePlayerOptions(options)` if you need to inspect a fully populated
202
+ options object with defaults applied.
package/dist/index.d.ts CHANGED
@@ -7,8 +7,16 @@ type RivmuxPlayerInternals = {
7
7
  detectRuntime?: () => PlayerError$1 | undefined;
8
8
  idFactory?: () => string;
9
9
  };
10
+ /**
11
+ * Public browser player facade for one HTTP-FLV stream.
12
+ *
13
+ * Create one instance per stream URL, call `attach(video)` first, then
14
+ * `start()`. Call `destroy()` when the instance is no longer needed.
15
+ */
10
16
  declare class RivmuxPlayer {
17
+ /** Original stream URL passed to the constructor. */
11
18
  readonly url: string;
19
+ /** Fully normalized options with defaults applied. */
12
20
  readonly options: NormalizedRivmuxPlayerOptions$1;
13
21
  private readonly id;
14
22
  private readonly events;
@@ -18,12 +26,41 @@ declare class RivmuxPlayer {
18
26
  private video?;
19
27
  private videoStateTimer?;
20
28
  private state;
29
+ /**
30
+ * Creates a player instance for one stream URL.
31
+ *
32
+ * The instance does not start network loading until `start()` is called.
33
+ */
21
34
  constructor(url: string, options?: RivmuxPlayerOptions$1, internals?: RivmuxPlayerInternals);
35
+ /**
36
+ * Attaches this player to a video element and prepares the worker/MSE pipe.
37
+ *
38
+ * Await this method before calling `start()`.
39
+ */
22
40
  attach(video: HTMLVideoElement): Promise<void>;
41
+ /**
42
+ * Starts loading, transmuxing, buffering, and playback control.
43
+ *
44
+ * Requires a successful `attach(video)` call first.
45
+ */
23
46
  start(): Promise<void>;
47
+ /**
48
+ * Stops loading and detaches the current media source.
49
+ *
50
+ * The instance remains reusable; call `start()` again to restart the same
51
+ * stream after the player has stopped.
52
+ */
24
53
  stop(): Promise<void>;
54
+ /**
55
+ * Releases worker resources, timers, listeners, and the attached video source.
56
+ *
57
+ * The instance is terminal after destroy and cannot be attached or started
58
+ * again.
59
+ */
25
60
  destroy(): Promise<void>;
61
+ /** Registers an event listener for a typed player event. */
26
62
  on<T extends PlayerEventType$1>(type: T, listener: PlayerEventListener$1<T>): void;
63
+ /** Removes a previously registered event listener. */
27
64
  off<T extends PlayerEventType$1>(type: T, listener: PlayerEventListener$1<T>): void;
28
65
  private ensureWorkerClient;
29
66
  private handleWorkerMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/player.ts","../src/options.ts","../src/errors.ts"],"mappings":";;;;KAmBY,qBAAA;EACV,aAAA,GAAgB,oBAAA;EAChB,aAAA,SAAsB,aAAW;EACjC,SAAA;AAAA;AAAA,cAGW,YAAA;EAAA,SACF,GAAA;EAAA,SACA,OAAA,EAAS,+BAAA;EAAA,iBACD,EAAA;EAAA,iBACA,MAAA;EAAA,iBACA,aAAA;EAAA,iBACA,aAAA;EAAA,QACT,YAAA;EAAA,QACA,KAAA;EAAA,QACA,eAAA;EAAA,QACA,KAAA;cAEI,GAAA,UAAa,OAAA,GAAU,qBAAA,EAAqB,SAAA,GAAW,qBAAA;EAQ7D,MAAA,CAAO,KAAA,EAAO,gBAAA,GAAmB,OAAA;EAoBjC,KAAA,IAAS,OAAA;EAqBT,IAAA,IAAQ,OAAA;EAiBR,OAAA,IAAW,OAAA;EA4BjB,EAAA,WAAa,iBAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,QAAA,EAAU,qBAAA,CAAoB,CAAA;EAIrE,GAAA,WAAc,iBAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,QAAA,EAAU,qBAAA,CAAoB,CAAA;EAAA,QAI9D,kBAAA;EAAA,QAaA,mBAAA;EAAA,QAoCA,uBAAA;EAAA,QAeA,oBAAA;EAAA,QAKA,wBAAA;EAAA,QAUA,uBAAA;EAAA,QASA,cAAA;EAAA,QAoBM,oBAAA;EAAA,QAkCN,iBAAA;EAAA,QAYA,kBAAA;AAAA;;;cCnSG,6BAAA,EAA+B,+BA2B3C;AAAA,iBAEe,sBAAA,CAAuB,OAAA,GAAS,qBAAA,GAA2B,+BAA6B;;;iBC7BxF,iBAAA,CAAkB,IAAA,EAAM,iBAAA,EAAiB,IAAA,UAAc,OAAA,UAAiB,QAAA,WAAmB,KAAA,aAAkB,aAAW"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/player.ts","../src/options.ts","../src/errors.ts"],"mappings":";;;;KAmBY,qBAAA;EACV,aAAA,GAAgB,oBAAA;EAChB,aAAA,SAAsB,aAAW;EACjC,SAAA;AAAA;;;;;;;cASW,YAAA;EATF;EAAA,SAWA,GAAA;EAFc;EAAA,SAKd,OAAA,EAAS,+BAAA;EAAA,iBACD,EAAA;EAAA,iBACA,MAAA;EAAA,iBACA,aAAA;EAAA,iBACA,aAAA;EAAA,QACT,YAAA;EAAA,QACA,KAAA;EAAA,QACA,eAAA;EAAA,QACA,KAAA;EA4HK;;;;;cArHD,GAAA,UAAa,OAAA,GAAU,qBAAA,EAAqB,SAAA,GAAW,qBAAA;EA0HG;;;;;EA7GhE,MAAA,CAAO,KAAA,EAAO,gBAAA,GAAmB,OAAA;EA5BrB;;;;;EAqDZ,KAAA,IAAS,OAAA;EA/CP;;;;;;EA0EF,IAAA,IAAQ,OAAA;EAjEqD;;;;;;EAwF7D,OAAA,IAAW,OAAA;EAlDF;EA+Ef,EAAA,WAAa,iBAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,QAAA,EAAU,qBAAA,CAAoB,CAAA;EApDvD;EAyDd,GAAA,WAAc,iBAAA,EAAiB,IAAA,EAAM,CAAA,EAAG,QAAA,EAAU,qBAAA,CAAoB,CAAA;EAAA,QAI9D,kBAAA;EAAA,QAaA,mBAAA;EAAA,QAoCA,uBAAA;EAAA,QAeA,oBAAA;EAAA,QAKA,wBAAA;EAAA,QAUA,uBAAA;EAAA,QASA,cAAA;EAAA,QAoBM,oBAAA;EAAA,QAkCN,iBAAA;EAAA,QAYA,kBAAA;AAAA;;;cCvUG,6BAAA,EAA+B,+BA2B3C;AAAA,iBAEe,sBAAA,CAAuB,OAAA,GAAS,qBAAA,GAA2B,+BAA6B;;;iBC/BxF,iBAAA,CAAkB,IAAA,EAAM,iBAAA,EAAiB,IAAA,UAAc,OAAA,UAAiB,QAAA,WAAmB,KAAA,aAAkB,aAAW"}
package/dist/index.js CHANGED
@@ -1,9 +1,7 @@
1
1
  import { WorkerClient, createRuntimeWorker } from "@rivmux/runtime-worker";
2
2
  //#region src/events.ts
3
3
  var PlayerEventEmitter = class {
4
- constructor() {
5
- this.listeners = /* @__PURE__ */ new Map();
6
- }
4
+ listeners = /* @__PURE__ */ new Map();
7
5
  on(type, listener) {
8
6
  const typedListener = listener;
9
7
  const listeners = this.listeners.get(type);
@@ -49,13 +47,27 @@ function playerErrorToException(error) {
49
47
  }
50
48
  //#endregion
51
49
  //#region src/feature-detect.ts
52
- const M1_VIDEO_MIME = "video/mp4; codecs=\"avc1.42C01E\"";
50
+ const REQUIRED_MSE_MIME_TYPES = [{
51
+ mediaType: "video",
52
+ mimeType: createMp4Mime("video", "avc1.42C01E"),
53
+ unsupportedCode: "RIVMUX_UNSUPPORTED_MSE_VIDEO_MIME"
54
+ }, {
55
+ mediaType: "audio",
56
+ mimeType: createMp4Mime("audio", "mp4a.40.2"),
57
+ unsupportedCode: "RIVMUX_UNSUPPORTED_MSE_AUDIO_MIME"
58
+ }];
53
59
  function detectMainThreadRuntime() {
54
60
  if (typeof Worker === "undefined") return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_WORKER", "Dedicated Worker is not available in this runtime.", true);
61
+ if (typeof fetch !== "function") return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_FETCH", "Fetch is not available in this runtime.", true);
62
+ if (typeof ReadableStream === "undefined") return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_READABLE_STREAM", "ReadableStream is not available in this runtime.", true);
55
63
  if (typeof WebAssembly === "undefined") return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_WASM", "WebAssembly is not available in this runtime.", true);
56
64
  if (typeof MediaSource === "undefined") return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_MSE", "MediaSource is not available in this runtime.", true);
57
65
  if (MediaSource.canConstructInDedicatedWorker !== true) return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_WORKER_MSE", "MediaSource cannot be constructed in a dedicated worker in this runtime.", true);
58
- if (!MediaSource.isTypeSupported("video/mp4; codecs=\"avc1.42C01E\"")) return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_M1_VIDEO_MIME", `MSE does not support ${M1_VIDEO_MIME}.`, true);
66
+ if (typeof MediaSource.isTypeSupported !== "function") return createPlayerError("unsupported", "RIVMUX_UNSUPPORTED_MSE_TYPE_CHECK", "MediaSource.isTypeSupported is not available in this runtime.", true);
67
+ for (const requirement of REQUIRED_MSE_MIME_TYPES) if (!MediaSource.isTypeSupported(requirement.mimeType)) return createPlayerError("unsupported", requirement.unsupportedCode, `MSE does not support ${requirement.mimeType}.`, true);
68
+ }
69
+ function createMp4Mime(mediaType, codec) {
70
+ return `${mediaType}/mp4; codecs="${codec}"`;
59
71
  }
60
72
  //#endregion
61
73
  //#region src/options.ts
@@ -86,7 +98,7 @@ const DEFAULT_RIVMUX_PLAYER_OPTIONS = {
86
98
  }
87
99
  };
88
100
  function normalizePlayerOptions(options = {}) {
89
- return {
101
+ const normalizedOptions = {
90
102
  playback: {
91
103
  ...DEFAULT_RIVMUX_PLAYER_OPTIONS.playback,
92
104
  ...options.playback
@@ -107,28 +119,86 @@ function normalizePlayerOptions(options = {}) {
107
119
  ...options.network?.retry
108
120
  }
109
121
  },
110
- runtime: {
111
- ...DEFAULT_RIVMUX_PLAYER_OPTIONS.runtime,
112
- ...options.runtime
113
- },
122
+ runtime: normalizeRuntimeOptions(options),
114
123
  diagnostics: {
115
124
  ...DEFAULT_RIVMUX_PLAYER_OPTIONS.diagnostics,
116
125
  ...options.diagnostics
117
126
  }
118
127
  };
128
+ validateNormalizedOptions(normalizedOptions);
129
+ return normalizedOptions;
130
+ }
131
+ function validateNormalizedOptions(options) {
132
+ validateLatencyOptions(options);
133
+ validateRuntimeOptions(options);
134
+ }
135
+ function validateLatencyOptions(options) {
136
+ const latency = options.latency;
137
+ assertFiniteNonNegativeLatency(latency.startupBuffer, "latency.startupBuffer");
138
+ assertFinitePositiveLatency(latency.target, "latency.target");
139
+ assertFiniteNonNegativeLatency(latency.max, "latency.max");
140
+ assertFiniteNonNegativeLatency(latency.maxForwardBuffer, "latency.maxForwardBuffer");
141
+ assertFiniteNonNegativeLatency(latency.backwardBuffer, "latency.backwardBuffer");
142
+ if (latency.max < latency.target) throwOptionError("runtime", "RIVMUX_INVALID_LATENCY_OPTION", "latency.max must be greater than or equal to latency.target.");
143
+ if (latency.maxForwardBuffer < latency.target) throwOptionError("runtime", "RIVMUX_INVALID_LATENCY_OPTION", "latency.maxForwardBuffer must be greater than or equal to latency.target.");
144
+ }
145
+ function validateRuntimeOptions(options) {
146
+ if (options.runtime.preferWorkerMse !== true) throwOptionError("unsupported", "RIVMUX_UNSUPPORTED_MAIN_THREAD_MSE_FALLBACK", "Main-thread MSE fallback is not implemented; runtime.preferWorkerMse must remain true.");
147
+ }
148
+ function normalizeRuntimeOptions(options) {
149
+ return {
150
+ preferWorkerMse: options.runtime?.preferWorkerMse ?? DEFAULT_RIVMUX_PLAYER_OPTIONS.runtime.preferWorkerMse,
151
+ ...options.runtime?.workerUrl === void 0 ? {} : { workerUrl: options.runtime.workerUrl },
152
+ ...options.runtime?.wasmUrl === void 0 ? {} : { wasmUrl: options.runtime.wasmUrl }
153
+ };
154
+ }
155
+ function assertFiniteNonNegativeLatency(value, field) {
156
+ if (!Number.isFinite(value) || value < 0) throwOptionError("runtime", "RIVMUX_INVALID_LATENCY_OPTION", `${field} must be a finite number greater than or equal to 0.`);
157
+ }
158
+ function assertFinitePositiveLatency(value, field) {
159
+ if (!Number.isFinite(value) || value <= 0) throwOptionError("runtime", "RIVMUX_INVALID_LATENCY_OPTION", `${field} must be a finite number greater than 0.`);
160
+ }
161
+ function throwOptionError(kind, code, message) {
162
+ throw playerErrorToException(createPlayerError(kind, code, message, true));
119
163
  }
120
164
  //#endregion
121
165
  //#region src/player.ts
166
+ /**
167
+ * Public browser player facade for one HTTP-FLV stream.
168
+ *
169
+ * Create one instance per stream URL, call `attach(video)` first, then
170
+ * `start()`. Call `destroy()` when the instance is no longer needed.
171
+ */
122
172
  var RivmuxPlayer = class {
173
+ /** Original stream URL passed to the constructor. */
174
+ url;
175
+ /** Fully normalized options with defaults applied. */
176
+ options;
177
+ id;
178
+ events = new PlayerEventEmitter();
179
+ workerFactory;
180
+ detectRuntime;
181
+ workerClient;
182
+ video;
183
+ videoStateTimer;
184
+ state = "idle";
185
+ /**
186
+ * Creates a player instance for one stream URL.
187
+ *
188
+ * The instance does not start network loading until `start()` is called.
189
+ */
123
190
  constructor(url, options, internals = {}) {
124
- this.events = new PlayerEventEmitter();
125
- this.state = "idle";
126
191
  this.url = url;
127
192
  this.options = normalizePlayerOptions(options);
128
193
  this.id = internals.idFactory?.() ?? createPlayerId();
129
194
  this.workerFactory = internals.workerFactory ?? createRuntimeWorker;
130
195
  this.detectRuntime = internals.detectRuntime ?? detectMainThreadRuntime;
131
196
  }
197
+ /**
198
+ * Attaches this player to a video element and prepares the worker/MSE pipe.
199
+ *
200
+ * Await this method before calling `start()`.
201
+ */
132
202
  async attach(video) {
133
203
  this.assertNotDestroyed("attach");
134
204
  if (this.video !== void 0 && this.video !== video) throw playerErrorToException(createPlayerError("runtime", "RIVMUX_ALREADY_ATTACHED", "This player is already attached to a video element.", false));
@@ -143,6 +213,11 @@ var RivmuxPlayer = class {
143
213
  await this.workerClient?.waitForMediaSourceHandle({ type: "attach-media-source" });
144
214
  this.state = "attached";
145
215
  }
216
+ /**
217
+ * Starts loading, transmuxing, buffering, and playback control.
218
+ *
219
+ * Requires a successful `attach(video)` call first.
220
+ */
146
221
  async start() {
147
222
  this.assertNotDestroyed("start");
148
223
  if (this.video === void 0 || this.workerClient === void 0) throw playerErrorToException(createPlayerError("runtime", "RIVMUX_START_REQUIRES_ATTACH", "start() requires a previously attached video element.", false));
@@ -155,6 +230,12 @@ var RivmuxPlayer = class {
155
230
  this.state = "started";
156
231
  this.startVideoStateReporting();
157
232
  }
233
+ /**
234
+ * Stops loading and detaches the current media source.
235
+ *
236
+ * The instance remains reusable; call `start()` again to restart the same
237
+ * stream after the player has stopped.
238
+ */
158
239
  async stop() {
159
240
  if (this.state === "destroyed") return;
160
241
  if (this.workerClient === void 0) {
@@ -167,6 +248,12 @@ var RivmuxPlayer = class {
167
248
  this.detachVideoSource();
168
249
  this.state = "stopped";
169
250
  }
251
+ /**
252
+ * Releases worker resources, timers, listeners, and the attached video source.
253
+ *
254
+ * The instance is terminal after destroy and cannot be attached or started
255
+ * again.
256
+ */
170
257
  async destroy() {
171
258
  if (this.state === "destroyed") return;
172
259
  const workerClient = this.workerClient;
@@ -185,9 +272,11 @@ var RivmuxPlayer = class {
185
272
  this.state = "destroyed";
186
273
  this.events.clear();
187
274
  }
275
+ /** Registers an event listener for a typed player event. */
188
276
  on(type, listener) {
189
277
  this.events.on(type, listener);
190
278
  }
279
+ /** Removes a previously registered event listener. */
191
280
  off(type, listener) {
192
281
  this.events.off(type, listener);
193
282
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/events.ts","../src/errors.ts","../src/feature-detect.ts","../src/options.ts","../src/player.ts"],"sourcesContent":["import type { PlayerEventListener, PlayerEventMap, PlayerEventType } from '@rivmux/protocol'\n\nexport class PlayerEventEmitter {\n private readonly listeners = new Map<PlayerEventType, Set<PlayerEventListener<PlayerEventType>>>()\n\n on<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n const typedListener = listener as PlayerEventListener<PlayerEventType>\n const listeners = this.listeners.get(type)\n\n if (listeners === undefined) {\n this.listeners.set(type, new Set([typedListener]))\n return\n }\n\n listeners.add(typedListener)\n }\n\n off<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n this.listeners.get(type)?.delete(listener as PlayerEventListener<PlayerEventType>)\n }\n\n emit<T extends PlayerEventType>(type: T, payload: PlayerEventMap[T]): void {\n for (const listener of this.listeners.get(type) ?? []) {\n listener(payload)\n }\n }\n\n clear(): void {\n this.listeners.clear()\n }\n}\n","import type { PlayerError, PlayerErrorKind } from '@rivmux/protocol'\n\nexport function createPlayerError(kind: PlayerErrorKind, code: string, message: string, terminal: boolean, cause?: unknown): PlayerError {\n return cause === undefined ? { kind, code, message, terminal } : { kind, code, message, terminal, cause }\n}\n\nexport function toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value))\n}\n\nexport function playerErrorToException(error: PlayerError): Error {\n const exception = toError(error.cause ?? error.message)\n exception.name = error.code\n return exception\n}\n","import { createPlayerError } from './errors'\n\nimport type { PlayerError } from '@rivmux/protocol'\n\nexport const M1_VIDEO_MIME = 'video/mp4; codecs=\"avc1.42C01E\"'\n\nexport function detectMainThreadRuntime(): PlayerError | undefined {\n if (typeof Worker === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_WORKER', 'Dedicated Worker is not available in this runtime.', true)\n }\n\n if (typeof WebAssembly === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_WASM', 'WebAssembly is not available in this runtime.', true)\n }\n\n if (typeof MediaSource === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_MSE', 'MediaSource is not available in this runtime.', true)\n }\n\n if (MediaSource.canConstructInDedicatedWorker !== true) {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_WORKER_MSE', 'MediaSource cannot be constructed in a dedicated worker in this runtime.', true)\n }\n\n if (!MediaSource.isTypeSupported(M1_VIDEO_MIME)) {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_M1_VIDEO_MIME', `MSE does not support ${M1_VIDEO_MIME}.`, true)\n }\n\n return undefined\n}\n","import type { NormalizedRivmuxPlayerOptions, RivmuxPlayerOptions } from '@rivmux/protocol'\n\nexport const DEFAULT_RIVMUX_PLAYER_OPTIONS: NormalizedRivmuxPlayerOptions = {\n playback: {\n autoPlay: true,\n muted: false,\n },\n latency: {\n startupBuffer: 0.35,\n target: 1.2,\n max: 2.5,\n maxForwardBuffer: 4,\n backwardBuffer: 1.5,\n },\n network: {\n headers: {},\n credentials: 'same-origin',\n retry: {\n maxAttempts: 3,\n backoffMs: 500,\n },\n },\n runtime: {\n preferWorkerMse: true,\n },\n diagnostics: {\n statsIntervalMs: 1000,\n debug: false,\n },\n}\n\nexport function normalizePlayerOptions(options: RivmuxPlayerOptions = {}): NormalizedRivmuxPlayerOptions {\n return {\n playback: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.playback,\n ...options.playback,\n },\n latency: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.latency,\n ...options.latency,\n },\n network: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.network,\n ...options.network,\n headers: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.network.headers,\n ...options.network?.headers,\n },\n retry: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.network.retry,\n ...options.network?.retry,\n },\n },\n runtime: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.runtime,\n ...options.runtime,\n },\n diagnostics: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.diagnostics,\n ...options.diagnostics,\n },\n }\n}\n","import { PlayerEventEmitter } from './events'\nimport { createPlayerError, playerErrorToException } from './errors'\nimport { detectMainThreadRuntime } from './feature-detect'\nimport { normalizePlayerOptions } from './options'\nimport { createRuntimeWorker, WorkerClient } from '@rivmux/runtime-worker'\n\nimport type {\n NormalizedRivmuxPlayerOptions,\n PlaybackControlAction,\n PlayerError,\n PlayerEventListener,\n PlayerEventType,\n RivmuxPlayerOptions,\n WorkerMessage,\n} from '@rivmux/protocol'\nimport type { RuntimeWorkerFactory } from '@rivmux/runtime-worker'\n\ntype PlayerState = 'idle' | 'attached' | 'started' | 'stopped' | 'destroyed'\n\nexport type RivmuxPlayerInternals = {\n workerFactory?: RuntimeWorkerFactory\n detectRuntime?: () => PlayerError | undefined\n idFactory?: () => string\n}\n\nexport class RivmuxPlayer {\n readonly url: string\n readonly options: NormalizedRivmuxPlayerOptions\n private readonly id: string\n private readonly events = new PlayerEventEmitter()\n private readonly workerFactory: RuntimeWorkerFactory\n private readonly detectRuntime: () => PlayerError | undefined\n private workerClient?: WorkerClient\n private video?: HTMLVideoElement\n private videoStateTimer?: ReturnType<typeof setInterval>\n private state: PlayerState = 'idle'\n\n constructor(url: string, options?: RivmuxPlayerOptions, internals: RivmuxPlayerInternals = {}) {\n this.url = url\n this.options = normalizePlayerOptions(options)\n this.id = internals.idFactory?.() ?? createPlayerId()\n this.workerFactory = internals.workerFactory ?? createRuntimeWorker\n this.detectRuntime = internals.detectRuntime ?? detectMainThreadRuntime\n }\n\n async attach(video: HTMLVideoElement): Promise<void> {\n this.assertNotDestroyed('attach')\n\n if (this.video !== undefined && this.video !== video) {\n throw playerErrorToException(createPlayerError('runtime', 'RIVMUX_ALREADY_ATTACHED', 'This player is already attached to a video element.', false))\n }\n\n const runtimeError = this.detectRuntime()\n if (runtimeError !== undefined) {\n this.events.emit('error', runtimeError)\n throw playerErrorToException(runtimeError)\n }\n\n this.video = video\n this.applyPlaybackOptions(video)\n this.ensureWorkerClient()\n await this.workerClient?.waitForMediaSourceHandle({ type: 'attach-media-source' })\n this.state = 'attached'\n }\n\n async start(): Promise<void> {\n this.assertNotDestroyed('start')\n\n if (this.video === undefined || this.workerClient === undefined) {\n throw playerErrorToException(createPlayerError('runtime', 'RIVMUX_START_REQUIRES_ATTACH', 'start() requires a previously attached video element.', false))\n }\n\n if (this.state === 'started') {\n return\n }\n\n if (this.state === 'stopped') {\n await this.workerClient.waitForMediaSourceHandle({ type: 'attach-media-source' })\n this.state = 'attached'\n }\n\n this.workerClient.post({ type: 'start' })\n this.state = 'started'\n this.startVideoStateReporting()\n }\n\n async stop(): Promise<void> {\n if (this.state === 'destroyed') {\n return\n }\n\n if (this.workerClient === undefined) {\n this.state = 'stopped'\n this.events.emit('stopped', undefined)\n return\n }\n\n this.stopVideoStateReporting()\n await this.workerClient.waitForStopped({ type: 'stop' })\n this.detachVideoSource()\n this.state = 'stopped'\n }\n\n async destroy(): Promise<void> {\n if (this.state === 'destroyed') {\n return\n }\n\n const workerClient = this.workerClient\n this.workerClient = undefined\n this.stopVideoStateReporting()\n\n if (workerClient !== undefined) {\n try {\n await workerClient.waitForDestroyed({ type: 'destroy' })\n } finally {\n workerClient.dispose()\n }\n } else {\n this.events.emit('destroyed', undefined)\n }\n\n if (this.video !== undefined) {\n this.detachVideoSource()\n this.video = undefined\n }\n\n this.state = 'destroyed'\n this.events.clear()\n }\n\n on<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n this.events.on(type, listener)\n }\n\n off<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n this.events.off(type, listener)\n }\n\n private ensureWorkerClient(): void {\n if (this.workerClient !== undefined) {\n return\n }\n\n const worker = this.workerFactory(this.options)\n this.workerClient = new WorkerClient(worker, {\n onMessage: (message) => this.handleWorkerMessage(message),\n onError: (error) => this.events.emit('error', error),\n })\n this.workerClient.post({ type: 'init', id: this.id, url: this.url, options: this.options })\n }\n\n private handleWorkerMessage(message: WorkerMessage): void {\n if (this.state === 'destroyed') {\n return\n }\n\n switch (message.type) {\n case 'ready':\n this.events.emit('ready', undefined)\n return\n case 'media-source-handle':\n this.attachMediaSourceHandle(message.handle)\n return\n case 'media-info':\n this.events.emit('mediaInfo', message.mediaInfo)\n return\n case 'stats':\n this.events.emit('stats', message.stats)\n return\n case 'warning':\n this.events.emit('warning', message.warning)\n return\n case 'error':\n this.events.emit('error', message.error)\n return\n case 'playback-control':\n void this.applyPlaybackControl(message.action)\n return\n case 'stopped':\n this.events.emit('stopped', undefined)\n return\n case 'destroyed':\n this.events.emit('destroyed', undefined)\n return\n }\n }\n\n private attachMediaSourceHandle(handle: MediaSourceHandle): void {\n if (this.video === undefined) {\n const error = createPlayerError(\n 'runtime',\n 'RIVMUX_ATTACH_HANDLE_WITHOUT_VIDEO',\n 'Worker returned a MediaSourceHandle before a video element was attached.',\n true\n )\n this.events.emit('error', error)\n throw playerErrorToException(error)\n }\n\n Reflect.set(this.video, 'srcObject', handle)\n }\n\n private applyPlaybackOptions(video: HTMLVideoElement): void {\n video.muted = this.options.playback.muted\n video.autoplay = this.options.playback.autoPlay\n }\n\n private startVideoStateReporting(): void {\n this.stopVideoStateReporting()\n this.postVideoState()\n\n const intervalMs = Math.max(100, Math.min(this.options.diagnostics.statsIntervalMs, 250))\n this.videoStateTimer = setInterval(() => {\n this.postVideoState()\n }, intervalMs)\n }\n\n private stopVideoStateReporting(): void {\n if (this.videoStateTimer === undefined) {\n return\n }\n\n clearInterval(this.videoStateTimer)\n this.videoStateTimer = undefined\n }\n\n private postVideoState(): void {\n const video = this.video\n const workerClient = this.workerClient\n if (video === undefined || workerClient === undefined || this.state !== 'started') {\n return\n }\n\n const droppedFrames = getDroppedFrames(video)\n workerClient.post({\n type: 'video-state',\n state: {\n currentTime: Number.isFinite(video.currentTime) ? video.currentTime : 0,\n readyState: video.readyState,\n playbackRate: video.playbackRate,\n paused: video.paused,\n ...(droppedFrames === undefined ? {} : { droppedFrames }),\n },\n })\n }\n\n private async applyPlaybackControl(action: PlaybackControlAction): Promise<void> {\n const video = this.video\n const workerClient = this.workerClient\n if (video === undefined || workerClient === undefined || this.state !== 'started') {\n return\n }\n\n try {\n switch (action.type) {\n case 'play':\n await video.play()\n break\n case 'set-playback-rate':\n video.playbackRate = action.playbackRate\n break\n case 'seek':\n video.currentTime = action.targetTime\n break\n }\n\n workerClient.post({ type: 'playback-control-result', result: { type: action.type, accepted: true } })\n this.postVideoState()\n } catch (cause) {\n workerClient.post({\n type: 'playback-control-result',\n result: {\n type: action.type,\n accepted: false,\n message: cause instanceof Error ? cause.message : String(cause),\n },\n })\n }\n }\n\n private detachVideoSource(): void {\n if (this.video === undefined) {\n return\n }\n\n this.video.pause()\n this.video.playbackRate = 1\n this.video.removeAttribute('src')\n this.video.srcObject = null\n this.video.load()\n }\n\n private assertNotDestroyed(method: string): void {\n if (this.state !== 'destroyed') {\n return\n }\n\n throw playerErrorToException(createPlayerError('runtime', 'RIVMUX_PLAYER_DESTROYED', `RivmuxPlayer.${method}() cannot be called after destroy().`, true))\n }\n}\n\nfunction createPlayerId(): string {\n const random = globalThis.crypto?.randomUUID?.()\n return random ?? `rivmux-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction getDroppedFrames(video: HTMLVideoElement): number | undefined {\n const quality = video.getVideoPlaybackQuality?.()\n if (quality !== undefined && Number.isFinite(quality.droppedVideoFrames)) {\n return quality.droppedVideoFrames\n }\n\n const webkitDroppedFrameCount = (video as HTMLVideoElement & { webkitDroppedFrameCount?: number }).webkitDroppedFrameCount\n return Number.isFinite(webkitDroppedFrameCount) ? webkitDroppedFrameCount : undefined\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/events.ts","../src/errors.ts","../src/feature-detect.ts","../src/options.ts","../src/player.ts"],"sourcesContent":["import type { PlayerEventListener, PlayerEventMap, PlayerEventType } from '@rivmux/protocol'\n\nexport class PlayerEventEmitter {\n private readonly listeners = new Map<PlayerEventType, Set<PlayerEventListener<PlayerEventType>>>()\n\n on<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n const typedListener = listener as PlayerEventListener<PlayerEventType>\n const listeners = this.listeners.get(type)\n\n if (listeners === undefined) {\n this.listeners.set(type, new Set([typedListener]))\n return\n }\n\n listeners.add(typedListener)\n }\n\n off<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n this.listeners.get(type)?.delete(listener as PlayerEventListener<PlayerEventType>)\n }\n\n emit<T extends PlayerEventType>(type: T, payload: PlayerEventMap[T]): void {\n for (const listener of this.listeners.get(type) ?? []) {\n listener(payload)\n }\n }\n\n clear(): void {\n this.listeners.clear()\n }\n}\n","import type { PlayerError, PlayerErrorKind } from '@rivmux/protocol'\n\nexport function createPlayerError(kind: PlayerErrorKind, code: string, message: string, terminal: boolean, cause?: unknown): PlayerError {\n return cause === undefined ? { kind, code, message, terminal } : { kind, code, message, terminal, cause }\n}\n\nexport function toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value))\n}\n\nexport function playerErrorToException(error: PlayerError): Error {\n const exception = toError(error.cause ?? error.message)\n exception.name = error.code\n return exception\n}\n","import { createPlayerError } from './errors'\n\nimport type { PlayerError } from '@rivmux/protocol'\n\nexport type RequiredMseMimeType = {\n readonly mediaType: 'video' | 'audio'\n readonly mimeType: string\n readonly unsupportedCode: string\n}\n\nexport const REQUIRED_MSE_MIME_TYPES = [\n {\n mediaType: 'video',\n mimeType: createMp4Mime('video', 'avc1.42C01E'),\n unsupportedCode: 'RIVMUX_UNSUPPORTED_MSE_VIDEO_MIME',\n },\n {\n mediaType: 'audio',\n mimeType: createMp4Mime('audio', 'mp4a.40.2'),\n unsupportedCode: 'RIVMUX_UNSUPPORTED_MSE_AUDIO_MIME',\n },\n] as const satisfies readonly RequiredMseMimeType[]\n\nexport function detectMainThreadRuntime(): PlayerError | undefined {\n if (typeof Worker === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_WORKER', 'Dedicated Worker is not available in this runtime.', true)\n }\n\n if (typeof fetch !== 'function') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_FETCH', 'Fetch is not available in this runtime.', true)\n }\n\n if (typeof ReadableStream === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_READABLE_STREAM', 'ReadableStream is not available in this runtime.', true)\n }\n\n if (typeof WebAssembly === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_WASM', 'WebAssembly is not available in this runtime.', true)\n }\n\n if (typeof MediaSource === 'undefined') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_MSE', 'MediaSource is not available in this runtime.', true)\n }\n\n if (MediaSource.canConstructInDedicatedWorker !== true) {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_WORKER_MSE', 'MediaSource cannot be constructed in a dedicated worker in this runtime.', true)\n }\n\n if (typeof MediaSource.isTypeSupported !== 'function') {\n return createPlayerError('unsupported', 'RIVMUX_UNSUPPORTED_MSE_TYPE_CHECK', 'MediaSource.isTypeSupported is not available in this runtime.', true)\n }\n\n for (const requirement of REQUIRED_MSE_MIME_TYPES) {\n if (!MediaSource.isTypeSupported(requirement.mimeType)) {\n return createPlayerError('unsupported', requirement.unsupportedCode, `MSE does not support ${requirement.mimeType}.`, true)\n }\n }\n\n return undefined\n}\n\nfunction createMp4Mime(mediaType: RequiredMseMimeType['mediaType'], codec: string): string {\n return `${mediaType}/mp4; codecs=\"${codec}\"`\n}\n","import { createPlayerError, playerErrorToException } from './errors'\n\nimport type { NormalizedRivmuxPlayerOptions, PlayerErrorKind, RivmuxPlayerOptions } from '@rivmux/protocol'\n\nexport const DEFAULT_RIVMUX_PLAYER_OPTIONS: NormalizedRivmuxPlayerOptions = {\n playback: {\n autoPlay: true,\n muted: false,\n },\n latency: {\n startupBuffer: 0.35,\n target: 1.2,\n max: 2.5,\n maxForwardBuffer: 4,\n backwardBuffer: 1.5,\n },\n network: {\n headers: {},\n credentials: 'same-origin',\n retry: {\n maxAttempts: 3,\n backoffMs: 500,\n },\n },\n runtime: {\n preferWorkerMse: true,\n },\n diagnostics: {\n statsIntervalMs: 1000,\n debug: false,\n },\n}\n\nexport function normalizePlayerOptions(options: RivmuxPlayerOptions = {}): NormalizedRivmuxPlayerOptions {\n const normalizedOptions = {\n playback: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.playback,\n ...options.playback,\n },\n latency: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.latency,\n ...options.latency,\n },\n network: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.network,\n ...options.network,\n headers: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.network.headers,\n ...options.network?.headers,\n },\n retry: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.network.retry,\n ...options.network?.retry,\n },\n },\n runtime: normalizeRuntimeOptions(options),\n diagnostics: {\n ...DEFAULT_RIVMUX_PLAYER_OPTIONS.diagnostics,\n ...options.diagnostics,\n },\n }\n\n validateNormalizedOptions(normalizedOptions)\n return normalizedOptions\n}\n\nfunction validateNormalizedOptions(options: NormalizedRivmuxPlayerOptions): void {\n validateLatencyOptions(options)\n validateRuntimeOptions(options)\n}\n\nfunction validateLatencyOptions(options: NormalizedRivmuxPlayerOptions): void {\n const latency = options.latency\n\n assertFiniteNonNegativeLatency(latency.startupBuffer, 'latency.startupBuffer')\n assertFinitePositiveLatency(latency.target, 'latency.target')\n assertFiniteNonNegativeLatency(latency.max, 'latency.max')\n assertFiniteNonNegativeLatency(latency.maxForwardBuffer, 'latency.maxForwardBuffer')\n assertFiniteNonNegativeLatency(latency.backwardBuffer, 'latency.backwardBuffer')\n\n if (latency.max < latency.target) {\n throwOptionError('runtime', 'RIVMUX_INVALID_LATENCY_OPTION', 'latency.max must be greater than or equal to latency.target.')\n }\n\n if (latency.maxForwardBuffer < latency.target) {\n throwOptionError('runtime', 'RIVMUX_INVALID_LATENCY_OPTION', 'latency.maxForwardBuffer must be greater than or equal to latency.target.')\n }\n}\n\nfunction validateRuntimeOptions(options: NormalizedRivmuxPlayerOptions): void {\n if (options.runtime.preferWorkerMse !== true) {\n throwOptionError(\n 'unsupported',\n 'RIVMUX_UNSUPPORTED_MAIN_THREAD_MSE_FALLBACK',\n 'Main-thread MSE fallback is not implemented; runtime.preferWorkerMse must remain true.'\n )\n }\n}\n\nfunction normalizeRuntimeOptions(options: RivmuxPlayerOptions): NormalizedRivmuxPlayerOptions['runtime'] {\n return {\n preferWorkerMse: options.runtime?.preferWorkerMse ?? DEFAULT_RIVMUX_PLAYER_OPTIONS.runtime.preferWorkerMse,\n ...(options.runtime?.workerUrl === undefined ? {} : { workerUrl: options.runtime.workerUrl }),\n ...(options.runtime?.wasmUrl === undefined ? {} : { wasmUrl: options.runtime.wasmUrl }),\n }\n}\n\nfunction assertFiniteNonNegativeLatency(value: number, field: string): void {\n if (!Number.isFinite(value) || value < 0) {\n throwOptionError('runtime', 'RIVMUX_INVALID_LATENCY_OPTION', `${field} must be a finite number greater than or equal to 0.`)\n }\n}\n\nfunction assertFinitePositiveLatency(value: number, field: string): void {\n if (!Number.isFinite(value) || value <= 0) {\n throwOptionError('runtime', 'RIVMUX_INVALID_LATENCY_OPTION', `${field} must be a finite number greater than 0.`)\n }\n}\n\nfunction throwOptionError(kind: PlayerErrorKind, code: string, message: string): never {\n throw playerErrorToException(createPlayerError(kind, code, message, true))\n}\n","import { PlayerEventEmitter } from './events'\nimport { createPlayerError, playerErrorToException } from './errors'\nimport { detectMainThreadRuntime } from './feature-detect'\nimport { normalizePlayerOptions } from './options'\nimport { createRuntimeWorker, WorkerClient } from '@rivmux/runtime-worker'\n\nimport type {\n NormalizedRivmuxPlayerOptions,\n PlaybackControlAction,\n PlayerError,\n PlayerEventListener,\n PlayerEventType,\n RivmuxPlayerOptions,\n WorkerMessage,\n} from '@rivmux/protocol'\nimport type { RuntimeWorkerFactory } from '@rivmux/runtime-worker'\n\ntype PlayerState = 'idle' | 'attached' | 'started' | 'stopped' | 'destroyed'\n\nexport type RivmuxPlayerInternals = {\n workerFactory?: RuntimeWorkerFactory\n detectRuntime?: () => PlayerError | undefined\n idFactory?: () => string\n}\n\n/**\n * Public browser player facade for one HTTP-FLV stream.\n *\n * Create one instance per stream URL, call `attach(video)` first, then\n * `start()`. Call `destroy()` when the instance is no longer needed.\n */\nexport class RivmuxPlayer {\n /** Original stream URL passed to the constructor. */\n readonly url: string\n\n /** Fully normalized options with defaults applied. */\n readonly options: NormalizedRivmuxPlayerOptions\n private readonly id: string\n private readonly events = new PlayerEventEmitter()\n private readonly workerFactory: RuntimeWorkerFactory\n private readonly detectRuntime: () => PlayerError | undefined\n private workerClient?: WorkerClient\n private video?: HTMLVideoElement\n private videoStateTimer?: ReturnType<typeof setInterval>\n private state: PlayerState = 'idle'\n\n /**\n * Creates a player instance for one stream URL.\n *\n * The instance does not start network loading until `start()` is called.\n */\n constructor(url: string, options?: RivmuxPlayerOptions, internals: RivmuxPlayerInternals = {}) {\n this.url = url\n this.options = normalizePlayerOptions(options)\n this.id = internals.idFactory?.() ?? createPlayerId()\n this.workerFactory = internals.workerFactory ?? createRuntimeWorker\n this.detectRuntime = internals.detectRuntime ?? detectMainThreadRuntime\n }\n\n /**\n * Attaches this player to a video element and prepares the worker/MSE pipe.\n *\n * Await this method before calling `start()`.\n */\n async attach(video: HTMLVideoElement): Promise<void> {\n this.assertNotDestroyed('attach')\n\n if (this.video !== undefined && this.video !== video) {\n throw playerErrorToException(createPlayerError('runtime', 'RIVMUX_ALREADY_ATTACHED', 'This player is already attached to a video element.', false))\n }\n\n const runtimeError = this.detectRuntime()\n if (runtimeError !== undefined) {\n this.events.emit('error', runtimeError)\n throw playerErrorToException(runtimeError)\n }\n\n this.video = video\n this.applyPlaybackOptions(video)\n this.ensureWorkerClient()\n await this.workerClient?.waitForMediaSourceHandle({ type: 'attach-media-source' })\n this.state = 'attached'\n }\n\n /**\n * Starts loading, transmuxing, buffering, and playback control.\n *\n * Requires a successful `attach(video)` call first.\n */\n async start(): Promise<void> {\n this.assertNotDestroyed('start')\n\n if (this.video === undefined || this.workerClient === undefined) {\n throw playerErrorToException(createPlayerError('runtime', 'RIVMUX_START_REQUIRES_ATTACH', 'start() requires a previously attached video element.', false))\n }\n\n if (this.state === 'started') {\n return\n }\n\n if (this.state === 'stopped') {\n await this.workerClient.waitForMediaSourceHandle({ type: 'attach-media-source' })\n this.state = 'attached'\n }\n\n this.workerClient.post({ type: 'start' })\n this.state = 'started'\n this.startVideoStateReporting()\n }\n\n /**\n * Stops loading and detaches the current media source.\n *\n * The instance remains reusable; call `start()` again to restart the same\n * stream after the player has stopped.\n */\n async stop(): Promise<void> {\n if (this.state === 'destroyed') {\n return\n }\n\n if (this.workerClient === undefined) {\n this.state = 'stopped'\n this.events.emit('stopped', undefined)\n return\n }\n\n this.stopVideoStateReporting()\n await this.workerClient.waitForStopped({ type: 'stop' })\n this.detachVideoSource()\n this.state = 'stopped'\n }\n\n /**\n * Releases worker resources, timers, listeners, and the attached video source.\n *\n * The instance is terminal after destroy and cannot be attached or started\n * again.\n */\n async destroy(): Promise<void> {\n if (this.state === 'destroyed') {\n return\n }\n\n const workerClient = this.workerClient\n this.workerClient = undefined\n this.stopVideoStateReporting()\n\n if (workerClient !== undefined) {\n try {\n await workerClient.waitForDestroyed({ type: 'destroy' })\n } finally {\n workerClient.dispose()\n }\n } else {\n this.events.emit('destroyed', undefined)\n }\n\n if (this.video !== undefined) {\n this.detachVideoSource()\n this.video = undefined\n }\n\n this.state = 'destroyed'\n this.events.clear()\n }\n\n /** Registers an event listener for a typed player event. */\n on<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n this.events.on(type, listener)\n }\n\n /** Removes a previously registered event listener. */\n off<T extends PlayerEventType>(type: T, listener: PlayerEventListener<T>): void {\n this.events.off(type, listener)\n }\n\n private ensureWorkerClient(): void {\n if (this.workerClient !== undefined) {\n return\n }\n\n const worker = this.workerFactory(this.options)\n this.workerClient = new WorkerClient(worker, {\n onMessage: (message) => this.handleWorkerMessage(message),\n onError: (error) => this.events.emit('error', error),\n })\n this.workerClient.post({ type: 'init', id: this.id, url: this.url, options: this.options })\n }\n\n private handleWorkerMessage(message: WorkerMessage): void {\n if (this.state === 'destroyed') {\n return\n }\n\n switch (message.type) {\n case 'ready':\n this.events.emit('ready', undefined)\n return\n case 'media-source-handle':\n this.attachMediaSourceHandle(message.handle)\n return\n case 'media-info':\n this.events.emit('mediaInfo', message.mediaInfo)\n return\n case 'stats':\n this.events.emit('stats', message.stats)\n return\n case 'warning':\n this.events.emit('warning', message.warning)\n return\n case 'error':\n this.events.emit('error', message.error)\n return\n case 'playback-control':\n void this.applyPlaybackControl(message.action)\n return\n case 'stopped':\n this.events.emit('stopped', undefined)\n return\n case 'destroyed':\n this.events.emit('destroyed', undefined)\n return\n }\n }\n\n private attachMediaSourceHandle(handle: MediaSourceHandle): void {\n if (this.video === undefined) {\n const error = createPlayerError(\n 'runtime',\n 'RIVMUX_ATTACH_HANDLE_WITHOUT_VIDEO',\n 'Worker returned a MediaSourceHandle before a video element was attached.',\n true\n )\n this.events.emit('error', error)\n throw playerErrorToException(error)\n }\n\n Reflect.set(this.video, 'srcObject', handle)\n }\n\n private applyPlaybackOptions(video: HTMLVideoElement): void {\n video.muted = this.options.playback.muted\n video.autoplay = this.options.playback.autoPlay\n }\n\n private startVideoStateReporting(): void {\n this.stopVideoStateReporting()\n this.postVideoState()\n\n const intervalMs = Math.max(100, Math.min(this.options.diagnostics.statsIntervalMs, 250))\n this.videoStateTimer = setInterval(() => {\n this.postVideoState()\n }, intervalMs)\n }\n\n private stopVideoStateReporting(): void {\n if (this.videoStateTimer === undefined) {\n return\n }\n\n clearInterval(this.videoStateTimer)\n this.videoStateTimer = undefined\n }\n\n private postVideoState(): void {\n const video = this.video\n const workerClient = this.workerClient\n if (video === undefined || workerClient === undefined || this.state !== 'started') {\n return\n }\n\n const droppedFrames = getDroppedFrames(video)\n workerClient.post({\n type: 'video-state',\n state: {\n currentTime: Number.isFinite(video.currentTime) ? video.currentTime : 0,\n readyState: video.readyState,\n playbackRate: video.playbackRate,\n paused: video.paused,\n ...(droppedFrames === undefined ? {} : { droppedFrames }),\n },\n })\n }\n\n private async applyPlaybackControl(action: PlaybackControlAction): Promise<void> {\n const video = this.video\n const workerClient = this.workerClient\n if (video === undefined || workerClient === undefined || this.state !== 'started') {\n return\n }\n\n try {\n switch (action.type) {\n case 'play':\n await video.play()\n break\n case 'set-playback-rate':\n video.playbackRate = action.playbackRate\n break\n case 'seek':\n video.currentTime = action.targetTime\n break\n }\n\n workerClient.post({ type: 'playback-control-result', result: { type: action.type, accepted: true } })\n this.postVideoState()\n } catch (cause) {\n workerClient.post({\n type: 'playback-control-result',\n result: {\n type: action.type,\n accepted: false,\n message: cause instanceof Error ? cause.message : String(cause),\n },\n })\n }\n }\n\n private detachVideoSource(): void {\n if (this.video === undefined) {\n return\n }\n\n this.video.pause()\n this.video.playbackRate = 1\n this.video.removeAttribute('src')\n this.video.srcObject = null\n this.video.load()\n }\n\n private assertNotDestroyed(method: string): void {\n if (this.state !== 'destroyed') {\n return\n }\n\n throw playerErrorToException(createPlayerError('runtime', 'RIVMUX_PLAYER_DESTROYED', `RivmuxPlayer.${method}() cannot be called after destroy().`, true))\n }\n}\n\nfunction createPlayerId(): string {\n const random = globalThis.crypto?.randomUUID?.()\n return random ?? `rivmux-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n}\n\nfunction getDroppedFrames(video: HTMLVideoElement): number | undefined {\n const quality = video.getVideoPlaybackQuality?.()\n if (quality !== undefined && Number.isFinite(quality.droppedVideoFrames)) {\n return quality.droppedVideoFrames\n }\n\n const webkitDroppedFrameCount = (video as HTMLVideoElement & { webkitDroppedFrameCount?: number }).webkitDroppedFrameCount\n return Number.isFinite(webkitDroppedFrameCount) ? webkitDroppedFrameCount : undefined\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "rivmux",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Public player package for Rivmux.",
5
+ "homepage": "https://www.npmjs.com/package/rivmux",
6
+ "bugs": {
7
+ "url": "https://github.com/yangxu52/Rivmux/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/yangxu52/Rivmux.git",
12
+ "directory": "packages/player"
13
+ },
5
14
  "license": "Apache-2.0",
6
15
  "type": "module",
7
16
  "sideEffects": false,
@@ -21,12 +30,12 @@
21
30
  "module": "./dist/index.js",
22
31
  "types": "./dist/index.d.ts",
23
32
  "dependencies": {
24
- "@rivmux/runtime-worker": "0.1.0",
25
- "@rivmux/protocol": "0.1.0"
33
+ "@rivmux/runtime-worker": "0.3.0",
34
+ "@rivmux/protocol": "0.3.0"
26
35
  },
27
36
  "devDependencies": {
28
37
  "tsdown": "^0.22.3",
29
- "typescript": "~5.9.3",
38
+ "typescript": "~6.0.3",
30
39
  "vitest": "^4.1.9"
31
40
  },
32
41
  "engines": {