avbridge 2.2.0 → 2.2.1

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/dist/element.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkOE66B34H_cjs = require('./chunk-OE66B34H.cjs');
3
+ var chunkUF2N5L63_cjs = require('./chunk-UF2N5L63.cjs');
4
4
  require('./chunk-HZLQNKFN.cjs');
5
5
  require('./chunk-G4APZMCP.cjs');
6
6
  require('./chunk-NZU7W256.cjs');
@@ -226,7 +226,7 @@ var AvbridgeVideoElement = class extends HTMLElementCtor {
226
226
  this._dispatch("loadstart", {});
227
227
  let player;
228
228
  try {
229
- player = await chunkOE66B34H_cjs.createPlayer({
229
+ player = await chunkUF2N5L63_cjs.createPlayer({
230
230
  source,
231
231
  target: this._videoEl,
232
232
  // Honor the consumer's preferred initial strategy. "auto" means
@@ -1,4 +1,4 @@
1
- import { a as MediaInput, m as StrategyName, S as StrategyClass, U as UnifiedPlayer, d as AudioTrackInfo, n as SubtitleTrackInfo, D as DiagnosticsSnapshot } from './player-DUyvltvy.cjs';
1
+ import { a as MediaInput, m as StrategyName, S as StrategyClass, U as UnifiedPlayer, d as AudioTrackInfo, n as SubtitleTrackInfo, D as DiagnosticsSnapshot } from './player-U2NPmFvA.cjs';
2
2
 
3
3
  /**
4
4
  * `<avbridge-video>` — `HTMLMediaElement`-compatible primitive backed by the
package/dist/element.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as MediaInput, m as StrategyName, S as StrategyClass, U as UnifiedPlayer, d as AudioTrackInfo, n as SubtitleTrackInfo, D as DiagnosticsSnapshot } from './player-DUyvltvy.js';
1
+ import { a as MediaInput, m as StrategyName, S as StrategyClass, U as UnifiedPlayer, d as AudioTrackInfo, n as SubtitleTrackInfo, D as DiagnosticsSnapshot } from './player-U2NPmFvA.js';
2
2
 
3
3
  /**
4
4
  * `<avbridge-video>` — `HTMLMediaElement`-compatible primitive backed by the
package/dist/element.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createPlayer } from './chunk-C5VA5U5O.js';
1
+ import { createPlayer } from './chunk-DMWARSEF.js';
2
2
  import './chunk-ILKDNBSE.js';
3
3
  import './chunk-5DMTJVIU.js';
4
4
  import './chunk-J5MCMN3S.js';
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkOE66B34H_cjs = require('./chunk-OE66B34H.cjs');
3
+ var chunkUF2N5L63_cjs = require('./chunk-UF2N5L63.cjs');
4
4
  var chunkHZLQNKFN_cjs = require('./chunk-HZLQNKFN.cjs');
5
5
  var chunkL4NPOJ36_cjs = require('./chunk-L4NPOJ36.cjs');
6
6
  require('./chunk-G4APZMCP.cjs');
@@ -21,7 +21,7 @@ var MEDIABUNNY_CONTAINERS = /* @__PURE__ */ new Set([
21
21
  async function remux(source, options = {}) {
22
22
  const outputFormat = options.outputFormat ?? "mp4";
23
23
  options.signal?.throwIfAborted();
24
- const ctx = await chunkOE66B34H_cjs.probe(source);
24
+ const ctx = await chunkUF2N5L63_cjs.probe(source);
25
25
  options.signal?.throwIfAborted();
26
26
  validateRemuxEligibility(ctx, options.strict ?? false);
27
27
  if (MEDIABUNNY_CONTAINERS.has(ctx.container)) {
@@ -33,7 +33,7 @@ function validateRemuxEligibility(ctx, strict) {
33
33
  const video = ctx.videoTracks[0];
34
34
  const audio = ctx.audioTracks[0];
35
35
  if (video) {
36
- const mbCodec = chunkOE66B34H_cjs.avbridgeVideoToMediabunny(video.codec);
36
+ const mbCodec = chunkUF2N5L63_cjs.avbridgeVideoToMediabunny(video.codec);
37
37
  if (!mbCodec) {
38
38
  throw new Error(
39
39
  `Cannot remux: video codec "${video.codec}" is not supported for remuxing. Use transcode() to re-encode to a modern codec.`
@@ -41,7 +41,7 @@ function validateRemuxEligibility(ctx, strict) {
41
41
  }
42
42
  }
43
43
  if (audio) {
44
- const mbCodec = chunkOE66B34H_cjs.avbridgeAudioToMediabunny(audio.codec);
44
+ const mbCodec = chunkUF2N5L63_cjs.avbridgeAudioToMediabunny(audio.codec);
45
45
  if (!mbCodec) {
46
46
  throw new Error(
47
47
  `Cannot remux: audio codec "${audio.codec}" is not supported for remuxing. Use transcode() to re-encode to a modern codec.`
@@ -60,7 +60,7 @@ function validateRemuxEligibility(ctx, strict) {
60
60
  async function remuxViaMediAbunny(ctx, outputFormat, options) {
61
61
  const mb = await import('mediabunny');
62
62
  const input = new mb.Input({
63
- source: await chunkOE66B34H_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
63
+ source: await chunkUF2N5L63_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
64
64
  formats: mb.ALL_FORMATS
65
65
  });
66
66
  const target = new mb.BufferTarget();
@@ -146,8 +146,8 @@ async function doLibavRemux(libav, filename, ctx, outputFormat, options) {
146
146
  const audioStream = streams.find((s) => s.codec_type === libav.AVMEDIA_TYPE_AUDIO) ?? null;
147
147
  const videoTrackInfo = ctx.videoTracks[0];
148
148
  const audioTrackInfo = ctx.audioTracks[0];
149
- const mbVideoCodec = videoTrackInfo ? chunkOE66B34H_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec) : null;
150
- const mbAudioCodec = audioTrackInfo ? chunkOE66B34H_cjs.avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
149
+ const mbVideoCodec = videoTrackInfo ? chunkUF2N5L63_cjs.avbridgeVideoToMediabunny(videoTrackInfo.codec) : null;
150
+ const mbAudioCodec = audioTrackInfo ? chunkUF2N5L63_cjs.avbridgeAudioToMediabunny(audioTrackInfo.codec) : null;
151
151
  const target = new mb.BufferTarget();
152
152
  const output = new mb.Output({
153
153
  format: createOutputFormat(mb, outputFormat),
@@ -358,7 +358,7 @@ async function transcode(source, options = {}) {
358
358
  const quality = options.quality ?? "medium";
359
359
  validateCodecCompatibility(outputFormat, videoCodec, audioCodec);
360
360
  options.signal?.throwIfAborted();
361
- const ctx = await chunkOE66B34H_cjs.probe(source);
361
+ const ctx = await chunkUF2N5L63_cjs.probe(source);
362
362
  options.signal?.throwIfAborted();
363
363
  if (!MEDIABUNNY_CONTAINERS2.has(ctx.container)) {
364
364
  throw new Error(
@@ -370,7 +370,7 @@ async function transcode(source, options = {}) {
370
370
  async function attemptTranscode(ctx, outputFormat, videoCodec, audioCodec, quality, options) {
371
371
  const mb = await import('mediabunny');
372
372
  const input = new mb.Input({
373
- source: await chunkOE66B34H_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
373
+ source: await chunkUF2N5L63_cjs.buildMediabunnySourceFromInput(mb, ctx.source),
374
374
  formats: mb.ALL_FORMATS
375
375
  });
376
376
  const target = new mb.BufferTarget();
@@ -552,23 +552,23 @@ function qualityToMediabunny(mb, quality) {
552
552
 
553
553
  Object.defineProperty(exports, "UnifiedPlayer", {
554
554
  enumerable: true,
555
- get: function () { return chunkOE66B34H_cjs.UnifiedPlayer; }
555
+ get: function () { return chunkUF2N5L63_cjs.UnifiedPlayer; }
556
556
  });
557
557
  Object.defineProperty(exports, "classify", {
558
558
  enumerable: true,
559
- get: function () { return chunkOE66B34H_cjs.classifyContext; }
559
+ get: function () { return chunkUF2N5L63_cjs.classifyContext; }
560
560
  });
561
561
  Object.defineProperty(exports, "createPlayer", {
562
562
  enumerable: true,
563
- get: function () { return chunkOE66B34H_cjs.createPlayer; }
563
+ get: function () { return chunkUF2N5L63_cjs.createPlayer; }
564
564
  });
565
565
  Object.defineProperty(exports, "probe", {
566
566
  enumerable: true,
567
- get: function () { return chunkOE66B34H_cjs.probe; }
567
+ get: function () { return chunkUF2N5L63_cjs.probe; }
568
568
  });
569
569
  Object.defineProperty(exports, "srtToVtt", {
570
570
  enumerable: true,
571
- get: function () { return chunkOE66B34H_cjs.srtToVtt; }
571
+ get: function () { return chunkUF2N5L63_cjs.srtToVtt; }
572
572
  });
573
573
  exports.remux = remux;
574
574
  exports.transcode = transcode;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MediaContext, C as Classification, a as MediaInput, b as ConvertOptions, c as ConvertResult, T as TranscodeOptions } from './player-DUyvltvy.cjs';
2
- export { A as AudioCodec, d as AudioTrackInfo, e as ContainerKind, f as CreatePlayerOptions, D as DiagnosticsSnapshot, H as HardwareAccelerationHint, O as OutputAudioCodec, g as OutputFormat, h as OutputVideoCodec, P as PlaybackSession, i as PlayerEventMap, j as PlayerEventName, k as Plugin, l as ProgressInfo, S as StrategyClass, m as StrategyName, n as SubtitleTrackInfo, o as TranscodeQuality, U as UnifiedPlayer, V as VideoCodec, p as VideoTrackInfo, q as createPlayer } from './player-DUyvltvy.cjs';
1
+ import { M as MediaContext, C as Classification, a as MediaInput, b as ConvertOptions, c as ConvertResult, T as TranscodeOptions } from './player-U2NPmFvA.cjs';
2
+ export { A as AudioCodec, d as AudioTrackInfo, e as ContainerKind, f as CreatePlayerOptions, D as DiagnosticsSnapshot, H as HardwareAccelerationHint, O as OutputAudioCodec, g as OutputFormat, h as OutputVideoCodec, P as PlaybackSession, i as PlayerEventMap, j as PlayerEventName, k as Plugin, l as ProgressInfo, S as StrategyClass, m as StrategyName, n as SubtitleTrackInfo, o as TranscodeQuality, U as UnifiedPlayer, V as VideoCodec, p as VideoTrackInfo, q as createPlayer } from './player-U2NPmFvA.cjs';
3
3
 
4
4
  /**
5
5
  * Pure classification — no I/O, no async. Test-friendly.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MediaContext, C as Classification, a as MediaInput, b as ConvertOptions, c as ConvertResult, T as TranscodeOptions } from './player-DUyvltvy.js';
2
- export { A as AudioCodec, d as AudioTrackInfo, e as ContainerKind, f as CreatePlayerOptions, D as DiagnosticsSnapshot, H as HardwareAccelerationHint, O as OutputAudioCodec, g as OutputFormat, h as OutputVideoCodec, P as PlaybackSession, i as PlayerEventMap, j as PlayerEventName, k as Plugin, l as ProgressInfo, S as StrategyClass, m as StrategyName, n as SubtitleTrackInfo, o as TranscodeQuality, U as UnifiedPlayer, V as VideoCodec, p as VideoTrackInfo, q as createPlayer } from './player-DUyvltvy.js';
1
+ import { M as MediaContext, C as Classification, a as MediaInput, b as ConvertOptions, c as ConvertResult, T as TranscodeOptions } from './player-U2NPmFvA.js';
2
+ export { A as AudioCodec, d as AudioTrackInfo, e as ContainerKind, f as CreatePlayerOptions, D as DiagnosticsSnapshot, H as HardwareAccelerationHint, O as OutputAudioCodec, g as OutputFormat, h as OutputVideoCodec, P as PlaybackSession, i as PlayerEventMap, j as PlayerEventName, k as Plugin, l as ProgressInfo, S as StrategyClass, m as StrategyName, n as SubtitleTrackInfo, o as TranscodeQuality, U as UnifiedPlayer, V as VideoCodec, p as VideoTrackInfo, q as createPlayer } from './player-U2NPmFvA.js';
3
3
 
4
4
  /**
5
5
  * Pure classification — no I/O, no async. Test-friendly.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { probe, avbridgeVideoToMediabunny, avbridgeAudioToMediabunny, buildMediabunnySourceFromInput } from './chunk-C5VA5U5O.js';
2
- export { UnifiedPlayer, classifyContext as classify, createPlayer, probe, srtToVtt } from './chunk-C5VA5U5O.js';
1
+ import { probe, avbridgeVideoToMediabunny, avbridgeAudioToMediabunny, buildMediabunnySourceFromInput } from './chunk-DMWARSEF.js';
2
+ export { UnifiedPlayer, classifyContext as classify, createPlayer, probe, srtToVtt } from './chunk-DMWARSEF.js';
3
3
  import { normalizeSource } from './chunk-ILKDNBSE.js';
4
4
  import { prepareLibavInput } from './chunk-WD2ZNQA7.js';
5
5
  import './chunk-5DMTJVIU.js';
@@ -324,6 +324,7 @@ declare class UnifiedPlayer {
324
324
  private lastProgressTime;
325
325
  private lastProgressPosition;
326
326
  private errorListener;
327
+ private endedListener;
327
328
  private switchingPromise;
328
329
  private subtitleResources;
329
330
  /**
@@ -324,6 +324,7 @@ declare class UnifiedPlayer {
324
324
  private lastProgressTime;
325
325
  private lastProgressPosition;
326
326
  private errorListener;
327
+ private endedListener;
327
328
  private switchingPromise;
328
329
  private subtitleResources;
329
330
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "avbridge",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Play and convert arbitrary video files in the browser. Native, remux, hybrid, fallback, and transcode — one API.",
5
5
  "license": "MIT",
6
6
  "author": "Keishi Hattori",
package/src/player.ts CHANGED
@@ -34,6 +34,11 @@ export class UnifiedPlayer {
34
34
  private lastProgressPosition = -1;
35
35
  private errorListener: (() => void) | null = null;
36
36
 
37
+ // Bound so we can removeEventListener in destroy(); without this the
38
+ // listener outlives the player and accumulates on elements that swap
39
+ // source (e.g. <avbridge-video>).
40
+ private endedListener: (() => void) | null = null;
41
+
37
42
  // Serializes escalation / setStrategy calls
38
43
  private switchingPromise: Promise<void> = Promise.resolve();
39
44
 
@@ -121,12 +126,10 @@ export class UnifiedPlayer {
121
126
  // Try the primary strategy, falling through the chain on failure
122
127
  await this.startSession(decision.strategy, decision.reason);
123
128
 
124
- // Apply subtitles for non-canvas strategies. Awaited so fetch/parse
125
- // failures surface deterministically but per-track failures are
126
- // caught inside attachSubtitleTracks and logged via console.warn so
127
- // a single bad sidecar doesn't break bootstrap. Subtitles are not
128
- // load-bearing for playback. (Promoting this to a typed `subtitleerror`
129
- // event is a nice-to-have follow-up.)
129
+ // Apply subtitles for non-canvas strategies. Per-track failures are
130
+ // caught inside attachSubtitleTracks and logged via console.warn
131
+ // subtitles are not load-bearing, so a bad sidecar must not break
132
+ // bootstrap.
130
133
  if (this.session!.strategy !== "fallback" && this.session!.strategy !== "hybrid") {
131
134
  await attachSubtitleTracks(
132
135
  this.options.target,
@@ -146,7 +149,8 @@ export class UnifiedPlayer {
146
149
  });
147
150
 
148
151
  this.startTimeupdateLoop();
149
- this.options.target.addEventListener("ended", () => this.emitter.emit("ended", undefined));
152
+ this.endedListener = () => this.emitter.emit("ended", undefined);
153
+ this.options.target.addEventListener("ended", this.endedListener);
150
154
  this.emitter.emitSticky("ready", undefined);
151
155
  const bootstrapElapsed = performance.now() - bootstrapStart;
152
156
  dbg.info("bootstrap", `ready in ${bootstrapElapsed.toFixed(0)}ms`);
@@ -492,6 +496,10 @@ export class UnifiedPlayer {
492
496
  this.timeupdateInterval = null;
493
497
  }
494
498
  this.clearSupervisor();
499
+ if (this.endedListener) {
500
+ this.options.target.removeEventListener("ended", this.endedListener);
501
+ this.endedListener = null;
502
+ }
495
503
  if (this.session) {
496
504
  await this.session.destroy();
497
505
  this.session = null;
@@ -508,14 +516,12 @@ export async function createPlayer(options: CreatePlayerOptions): Promise<Unifie
508
516
  }
509
517
 
510
518
  /**
511
- * Build a synthetic classification when the consumer asked for a specific
512
- * initial strategy. We ask `classify()` for the natural decision so we can
513
- * inherit the correct fallback chain, then override the strategy + class.
514
- *
515
- * Why this matters: hard-coding `class: "NATIVE"` (the old behavior) made
516
- * diagnostics lie for every initialStrategy that wasn't actually native, and
517
- * any downstream logic that trusted `strategyClass` could make the wrong
518
- * decision. We now derive the class from the picked strategy.
519
+ * Build a synthetic classification for an explicit `initialStrategy` override.
520
+ * The `class` is derived from the chosen strategy so diagnostics and any
521
+ * downstream consumer of `strategyClass` see the real strategy. The fallback
522
+ * chain is inherited from the natural classification but must never contain
523
+ * `initial` itself otherwise `startSession` would retry the strategy that
524
+ * just failed before escalating.
519
525
  *
520
526
  * @internal — exported for unit tests; not part of the public API.
521
527
  */
@@ -525,11 +531,13 @@ export function buildInitialDecision(
525
531
  ): Classification {
526
532
  const natural = classify(ctx);
527
533
  const cls = strategyToClass(initial, natural);
534
+ const inherited = natural.fallbackChain ?? defaultFallbackChain(initial);
535
+ const fallbackChain = inherited.filter((s) => s !== initial);
528
536
  return {
529
537
  class: cls,
530
538
  strategy: initial,
531
539
  reason: `initial strategy "${initial}" requested via options.initialStrategy`,
532
- fallbackChain: natural.fallbackChain ?? defaultFallbackChain(initial),
540
+ fallbackChain,
533
541
  };
534
542
  }
535
543
 
@@ -93,6 +93,13 @@ export async function createFallbackSession(
93
93
  void doSeek(v);
94
94
  },
95
95
  });
96
+ // Mirror `paused` from the audio clock — the underlying <video> never
97
+ // has its own src, so its native `paused` is always true and would
98
+ // mislead doSetStrategy's wasPlaying capture on a backend switch.
99
+ Object.defineProperty(target, "paused", {
100
+ configurable: true,
101
+ get: () => !audio.isPlaying(),
102
+ });
96
103
  // Mirror duration so the demo's controls can use target.duration too.
97
104
  if (ctx.duration && Number.isFinite(ctx.duration)) {
98
105
  Object.defineProperty(target, "duration", {
@@ -244,6 +251,7 @@ export async function createFallbackSession(
244
251
  try {
245
252
  delete (target as unknown as Record<string, unknown>).currentTime;
246
253
  delete (target as unknown as Record<string, unknown>).duration;
254
+ delete (target as unknown as Record<string, unknown>).paused;
247
255
  } catch { /* ignore */ }
248
256
  },
249
257
 
@@ -51,8 +51,12 @@ export class VideoRenderer {
51
51
  });
52
52
 
53
53
  this.canvas = document.createElement("canvas");
54
+ // object-fit:contain letterboxes the canvas bitmap (sized to
55
+ // frame.displayWidth × displayHeight in paint()) inside the stage so
56
+ // portrait / non-stage-aspect content isn't stretched. Canvas is a
57
+ // replaced element, so object-fit applies.
54
58
  this.canvas.style.cssText =
55
- "position:absolute;left:0;top:0;width:100%;height:100%;background:black;";
59
+ "position:absolute;left:0;top:0;width:100%;height:100%;background:black;object-fit:contain;";
56
60
 
57
61
  // Attach the canvas next to the video. When the video lives inside an
58
62
  // `<avbridge-video>` shadow root, `target.parentElement` is the
@@ -45,12 +45,19 @@ export async function createHybridSession(
45
45
  throw err;
46
46
  }
47
47
 
48
- // Patch <video> element for the unified player layer
48
+ // Patch <video> element for the unified player layer. `paused` is
49
+ // mirrored from the audio clock so callers that inspect target.paused
50
+ // (notably doSetStrategy capturing wasPlaying) see the real play state
51
+ // — the underlying <video> never has its own src and stays paused.
49
52
  Object.defineProperty(target, "currentTime", {
50
53
  configurable: true,
51
54
  get: () => audio.now(),
52
55
  set: (v: number) => { void doSeek(v); },
53
56
  });
57
+ Object.defineProperty(target, "paused", {
58
+ configurable: true,
59
+ get: () => !audio.isPlaying(),
60
+ });
54
61
  if (ctx.duration && Number.isFinite(ctx.duration)) {
55
62
  Object.defineProperty(target, "duration", {
56
63
  configurable: true,
@@ -129,6 +136,7 @@ export async function createHybridSession(
129
136
  try {
130
137
  delete (target as unknown as Record<string, unknown>).currentTime;
131
138
  delete (target as unknown as Record<string, unknown>).duration;
139
+ delete (target as unknown as Record<string, unknown>).paused;
132
140
  } catch { /* ignore */ }
133
141
  },
134
142
 
@@ -36,7 +36,19 @@ export async function createRemuxSession(
36
36
  await pipeline.start(video.currentTime || 0, true);
37
37
  return;
38
38
  }
39
- await video.play();
39
+ // seek() may have already started the pump with autoPlay=false
40
+ // (strategy-switch flow calls seek before play). Flip the pipeline's
41
+ // pending autoPlay so the MseSink fires video.play() once buffered
42
+ // data lands, and also attempt an immediate video.play() in case the
43
+ // sink is already wired up. The immediate call can reject when
44
+ // video.src hasn't been set yet — that's fine, the deferred path will
45
+ // catch it.
46
+ pipeline.setAutoPlay(true);
47
+ try {
48
+ await video.play();
49
+ } catch {
50
+ /* sink not ready yet; setAutoPlay will handle playback on first buffered write */
51
+ }
40
52
  },
41
53
  pause() {
42
54
  wantPlay = false;
@@ -24,6 +24,8 @@ import {
24
24
  export interface RemuxPipeline {
25
25
  start(fromTime?: number, autoPlay?: boolean): Promise<void>;
26
26
  seek(time: number, autoPlay?: boolean): Promise<void>;
27
+ /** Update the autoplay intent mid-flight — used when play() arrives after seek() but before the MseSink has been constructed. */
28
+ setAutoPlay(autoPlay: boolean): void;
27
29
  destroy(): Promise<void>;
28
30
  stats(): Record<string, unknown>;
29
31
  }
@@ -248,6 +250,10 @@ export async function createRemuxPipeline(
248
250
  console.error("[avbridge] remux pipeline reseek failed:", err);
249
251
  });
250
252
  },
253
+ setAutoPlay(autoPlay) {
254
+ pendingAutoPlay = autoPlay;
255
+ if (sink) sink.setPlayOnSeek(autoPlay);
256
+ },
251
257
  async destroy() {
252
258
  destroyed = true;
253
259
  pumpToken++;