supervision 0.2.0-next.1 → 0.2.0-next.2

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.
Files changed (63) hide show
  1. package/dist/index.d.ts +3 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +508 -127
  4. package/dist/index.js.map +1 -1
  5. package/dist/mask-preparation.worker.js.map +1 -1
  6. package/dist/media/media-frame-clock.d.ts +4 -0
  7. package/dist/media/media-frame-clock.d.ts.map +1 -0
  8. package/dist/media/media-source.d.ts +6 -0
  9. package/dist/media/media-source.d.ts.map +1 -1
  10. package/dist/media/video-engine-media-source.d.ts.map +1 -1
  11. package/dist/media/video-engine-media-source.js +1 -1
  12. package/dist/render-preparation/prepared-window-timeline.d.ts +1 -0
  13. package/dist/render-preparation/prepared-window-timeline.d.ts.map +1 -1
  14. package/dist/renderers/media-frame-navigation.d.ts +17 -0
  15. package/dist/renderers/media-frame-navigation.d.ts.map +1 -0
  16. package/dist/renderers/media-renderer-core.d.ts.map +1 -1
  17. package/dist/renderers/media-renderer-scene.d.ts +1 -0
  18. package/dist/renderers/media-renderer-scene.d.ts.map +1 -1
  19. package/dist/renderers/media-renderer-transport.d.ts +4 -1
  20. package/dist/renderers/media-renderer-transport.d.ts.map +1 -1
  21. package/dist/renderers/pixi-media-scene.d.ts.map +1 -1
  22. package/dist/renderers/prepared-annotation-window.d.ts.map +1 -1
  23. package/dist/renderers/presented-frame-channel.d.ts +8 -6
  24. package/dist/renderers/presented-frame-channel.d.ts.map +1 -1
  25. package/dist/sessions/media-session-defaults.d.ts.map +1 -1
  26. package/dist/sessions/media-session-detections.d.ts.map +1 -1
  27. package/dist/sessions/media-session.d.ts.map +1 -1
  28. package/dist/tracking.worker.js.map +1 -1
  29. package/dist/types/media-frame-clock.d.ts +16 -0
  30. package/dist/types/media-frame-clock.d.ts.map +1 -0
  31. package/dist/types/media-frame-navigation.d.ts +28 -0
  32. package/dist/types/media-frame-navigation.d.ts.map +1 -0
  33. package/dist/types/media-renderer-display.d.ts +12 -0
  34. package/dist/types/media-renderer-display.d.ts.map +1 -0
  35. package/dist/types/media-renderer.d.ts +13 -0
  36. package/dist/types/media-renderer.d.ts.map +1 -1
  37. package/dist/types/media-session.d.ts +15 -5
  38. package/dist/types/media-session.d.ts.map +1 -1
  39. package/dist/types/render-preparation.d.ts +8 -5
  40. package/dist/types/render-preparation.d.ts.map +1 -1
  41. package/dist/{video-engine-media-source-CUXnOJOV.js → video-engine-media-source-DR-x1UfK.js} +35 -2
  42. package/dist/video-engine-media-source-DR-x1UfK.js.map +1 -0
  43. package/dist/web-video-engine/analysis.js +16 -0
  44. package/dist/web-video-engine/canvas-sink-scrub-cursor.d.ts +6 -1
  45. package/dist/web-video-engine/decode-scheduler.d.ts +8 -2
  46. package/dist/web-video-engine/decode-session.d.ts +5 -2
  47. package/dist/web-video-engine/decode-source.d.ts +2 -0
  48. package/dist/web-video-engine/engine-core.d.ts +9 -0
  49. package/dist/web-video-engine/engine.js +103 -11
  50. package/dist/web-video-engine/engine.worker.js +492 -31
  51. package/dist/web-video-engine/frame-cache.d.ts +2 -0
  52. package/dist/web-video-engine/pausable-deadline.d.ts +11 -0
  53. package/dist/web-video-engine/scrub-controller.d.ts +8 -4
  54. package/dist/web-video-engine/scrub-cursor.d.ts +3 -0
  55. package/dist/web-video-engine/video-engine.d.ts +12 -1
  56. package/dist/web-video-engine/worker-protocol.d.ts +9 -1
  57. package/node_modules/supervision-js-core/dist/detections/buffered-detection-timeline.d.ts.map +1 -1
  58. package/node_modules/supervision-js-core/dist/index.js +77 -41
  59. package/node_modules/supervision-js-core/dist/index.js.map +1 -1
  60. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts +8 -3
  61. package/node_modules/supervision-js-core/dist/types/detection-timeline.d.ts.map +1 -1
  62. package/package.json +1 -1
  63. package/dist/video-engine-media-source-CUXnOJOV.js.map +0 -1
@@ -89,6 +89,7 @@ export interface FrameCacheOptions {
89
89
  readonly minExactSlots?: number;
90
90
  }
91
91
  export declare class FrameCache {
92
+ private readonly options;
92
93
  private readonly exact;
93
94
  private readonly preview;
94
95
  private exactHits;
@@ -97,6 +98,7 @@ export declare class FrameCache {
97
98
  private readonly bucketMs;
98
99
  private readonly exactBudgetBytes;
99
100
  constructor(options: FrameCacheOptions);
101
+ resized(width: number, height: number): FrameCache;
100
102
  /**
101
103
  * Store a crisp full-resolution frame for the exact (scrub) tier under
102
104
  * `frame`, the timeline's name for it. `timestampMs` is the decoded
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A timeout whose budget advances only while its owner is visible. The owner
3
+ * still decides which work is eligible: this helper never pauses the work
4
+ * itself, nor any unrelated watchdog.
5
+ */
6
+ export interface PausableDeadline {
7
+ cancel(): void;
8
+ pause(): void;
9
+ resume(): void;
10
+ }
11
+ export declare function createPausableDeadline(timeoutMs: number, onExpire: () => void): PausableDeadline;
@@ -349,6 +349,8 @@ export declare class ScrubController {
349
349
  * walk. Detaches the iterator and drops the read-ahead so paused ticks
350
350
  * never paint a stale play frame. */
351
351
  endPlay(): void;
352
+ holdOutput(): void;
353
+ releaseOutput(): void;
352
354
  /**
353
355
  * Cache lookup helper used by the engine on scrub. Asks the cursor for a
354
356
  * cached frame near the target and, if it is far enough from what the canvas
@@ -356,12 +358,14 @@ export declare class ScrubController {
356
358
  * calls cursor.seekTo to land the full-res frame; the cursor's emit listener
357
359
  * stashes it for the next tick.
358
360
  *
359
- * The skip-near-current check rejects hits whose timestamp falls within
360
- * cacheSkipNearMs of the currently-displayed frame. This is the knob
361
- * consumers tune to control single-frame-step UX: a higher value forces a
362
- * fresh decode on tighter movements, a value of 0 always serves the cache.
361
+ * Nearby approximate hits respect cacheSkipNearMs. An exact hit for the
362
+ * requested frame can advance immediately, without waiting for a decode.
363
363
  */
364
364
  tryPaintFromCache(timestampMs: number): boolean;
365
+ /** A new commit needs its own presentation even when its exact pixels were
366
+ * already shown by the preceding scrub. Approximate neighbors cannot answer it. */
367
+ presentExactFromCache(timestampMs: number): boolean;
368
+ private paintCached;
365
369
  private start;
366
370
  private runTick;
367
371
  private paint;
@@ -1,4 +1,5 @@
1
1
  import type { CreateScrubCursorOptions } from "./create-scrub-cursor";
2
+ import type { DecodeDimensions } from "./decode-resolution";
2
3
  import type { FrameCacheStats } from "./frame-cache";
3
4
  import type { FrameId, FrameTimeline } from "./frame-timeline";
4
5
  import type { GopStats } from "./keyframe-index";
@@ -283,6 +284,8 @@ export interface ScrubTrackInfo {
283
284
  * - close is final; reusing a closed cursor is a programming error.
284
285
  */
285
286
  export interface ScrubCursor {
287
+ /** Drains old output and changes frame/cache size without reopening decode. */
288
+ resizeOutput?(dimensions: DecodeDimensions): Promise<boolean>;
286
289
  readonly state: ScrubCursorState;
287
290
  readonly track: ScrubTrackInfo;
288
291
  readonly isIdle: boolean;
@@ -1,4 +1,4 @@
1
- import type { DecodeResolutionStrategy } from "./decode-resolution";
1
+ import type { DecodeResolutionStrategy, DisplayBoxResolutionOptions } from "./decode-resolution";
2
2
  import type { DiagnosticsSnapshot, EngineDiagnostics } from "./diagnostics";
3
3
  import { type FrameId, type FrameLanding } from "./frame-timeline";
4
4
  import type { SeekIntent } from "./scrub-cursor";
@@ -123,6 +123,9 @@ export declare class WebVideoEngine {
123
123
  private lastTrace;
124
124
  private traceArmed;
125
125
  private readonly pending;
126
+ private presentationVisible;
127
+ private listeningForPresentationVisibility;
128
+ private readonly onPresentationVisibilityChange;
126
129
  private nextRequestId;
127
130
  private metadata;
128
131
  /** The loaded source's frame table, held here so a gesture is resolved to a
@@ -156,6 +159,10 @@ export declare class WebVideoEngine {
156
159
  */
157
160
  bindCanvas: (el: HTMLCanvasElement | null) => void;
158
161
  play: () => Promise<void>;
162
+ /** Changes frames-mode display output without reopening the video. Resolves
163
+ * with true after worker delivery, or false for unchanged output dimensions.
164
+ * The host still acknowledges its actual presentation. */
165
+ setDisplay: (display: DisplayBoxResolutionOptions) => Promise<boolean>;
159
166
  pause: () => void;
160
167
  togglePlayback: () => void;
161
168
  /**
@@ -334,6 +341,9 @@ export declare class WebVideoEngine {
334
341
  private request;
335
342
  private post;
336
343
  private rejectAllPending;
344
+ private startPresentationVisibilityTracking;
345
+ private stopPresentationVisibilityTracking;
346
+ private syncPresentationVisibility;
337
347
  /**
338
348
  * The frame a target names.
339
349
  *
@@ -363,6 +373,7 @@ export type PresentedFrameHandler = (presented: PresentedFrame) => void;
363
373
  * getPlaybackState.
364
374
  */
365
375
  export interface WebVideoEngineHandle {
376
+ setDisplay(display: DisplayBoxResolutionOptions): Promise<boolean>;
366
377
  play(): Promise<void>;
367
378
  pause(): void;
368
379
  togglePlayback(): void;
@@ -1,6 +1,6 @@
1
1
  import type { FrameId, FrameLanding } from "./frame-timeline";
2
2
  import type { FrameQuality, SeekIntent } from "./scrub-cursor";
3
- import type { DecodeResolutionStrategy } from "./decode-resolution";
3
+ import type { DecodeResolutionStrategy, DisplayBoxResolutionOptions } from "./decode-resolution";
4
4
  import type { DiagnosticsSnapshot, EngineDiagnostics } from "./diagnostics";
5
5
  import type { Rotation } from "./rotation";
6
6
  import type { EngineTrace } from "./trace-recorder";
@@ -79,6 +79,9 @@ export interface BindCanvasCommand {
79
79
  }
80
80
  export type FireAndForgetCommand = BindCanvasCommand | {
81
81
  readonly type: "unbindCanvas";
82
+ } | {
83
+ readonly type: "setPresentationVisibility";
84
+ readonly visible: boolean;
82
85
  } | {
83
86
  readonly type: "scrub";
84
87
  readonly frameIndex: number;
@@ -109,6 +112,10 @@ export type FireAndForgetCommand = BindCanvasCommand | {
109
112
  readonly type: "traceDisarm";
110
113
  };
111
114
  export type AwaitableCommand = {
115
+ readonly type: "setDisplay";
116
+ readonly requestId: RequestId;
117
+ readonly display: DisplayBoxResolutionOptions;
118
+ } | {
112
119
  readonly type: "load";
113
120
  readonly requestId: RequestId;
114
121
  readonly config: EngineLoadConfig;
@@ -200,6 +207,7 @@ export type ResponseEvent = {
200
207
  readonly type: "ack";
201
208
  readonly requestId: RequestId;
202
209
  readonly landing?: FrameLanding;
210
+ readonly outputChanged?: boolean;
203
211
  } | {
204
212
  readonly type: "stats";
205
213
  readonly requestId: RequestId;
@@ -1 +1 @@
1
- {"version":3,"file":"buffered-detection-timeline.d.ts","sourceRoot":"","sources":["../../src/detections/buffered-detection-timeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAE3B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAG1B,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAuDxD;;;;;GAKG;AACH,wBAAgB,yCAAyC,CACvD,QAAQ,EAAE,yBAAyB,GAClC,SAAS,cAAc,EAAE,CAI3B;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE;IACP,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC,GAAG,sBAAsB,GACzB,yBAAyB,CA+3B3B;AAED,wBAAgB,8BAA8B,IAAI,oBAAoB,CAWrE"}
1
+ {"version":3,"file":"buffered-detection-timeline.d.ts","sourceRoot":"","sources":["../../src/detections/buffered-detection-timeline.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,yBAAyB,EAC9B,KAAK,sBAAsB,EAE3B,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EAG1B,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAuDxD;;;;;GAKG;AACH,wBAAgB,yCAAyC,CACvD,QAAQ,EAAE,yBAAyB,GAClC,SAAS,cAAc,EAAE,CAI3B;AAED,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE;IACP,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;CACvC,GAAG,sBAAsB,GACzB,yBAAyB,CA07B3B;AAED,wBAAgB,8BAA8B,IAAI,oBAAoB,CAWrE"}
@@ -615,6 +615,7 @@ function createBufferedDetectionTimeline(options) {
615
615
  duration: null,
616
616
  loop: false,
617
617
  };
618
+ let timelineFirstTimestamp = 0;
618
619
  let inFlight;
619
620
  let incrementalRefresh;
620
621
  /** Source version the gate last gave up on, or null while it still waits. */
@@ -754,14 +755,15 @@ function createBufferedDetectionTimeline(options) {
754
755
  state.bufferEndTime === null) {
755
756
  return;
756
757
  }
757
- const laps = Math.round((getComparableMediaTime(mediaTime) - mediaTime) / duration);
758
+ const loopSpan = duration - timelineFirstTimestamp;
759
+ const laps = Math.round((getComparableMediaTime(mediaTime) - mediaTime) / loopSpan);
758
760
  if (laps === 0) {
759
761
  return;
760
762
  }
761
763
  state = {
762
764
  ...state,
763
- bufferEndTime: state.bufferEndTime - laps * duration,
764
- bufferStartTime: state.bufferStartTime - laps * duration,
765
+ bufferEndTime: state.bufferEndTime - laps * loopSpan,
766
+ bufferStartTime: state.bufferStartTime - laps * loopSpan,
765
767
  };
766
768
  notifyBufferChanged();
767
769
  };
@@ -864,21 +866,27 @@ function createBufferedDetectionTimeline(options) {
864
866
  if (destroyed || !isLoadingEnabled()) {
865
867
  return;
866
868
  }
869
+ adoptFirstTimestamp(prepareOptions?.firstTimestamp);
870
+ const navigationMediaTime = getNavigationMediaTime(mediaTime);
867
871
  if (shouldWaitForPlaybackGate(prepareOptions)) {
868
- await waitForPlaybackGate(mediaTime, prepareOptions);
872
+ await waitForPlaybackGate(navigationMediaTime, prepareOptions);
869
873
  if (destroyed) {
870
874
  return;
871
875
  }
872
876
  }
873
- await refreshBuffer(mediaTime);
874
- anchorWindowToPlayhead(mediaTime);
877
+ await refreshBuffer(navigationMediaTime);
878
+ anchorWindowToPlayhead(navigationMediaTime);
875
879
  },
876
880
  needsBufferPrepare(mediaTime) {
881
+ const navigationMediaTime = getNavigationMediaTime(mediaTime);
877
882
  return (!destroyed &&
878
883
  isLoadingEnabled() &&
879
- (!isBuffered(mediaTime) || !inFlightCovers(mediaTime)));
884
+ (!isInsideBufferedRange(navigationMediaTime) ||
885
+ !inFlightCovers(navigationMediaTime)));
880
886
  },
881
887
  needsPlaybackGateWait(mediaTime, prepareOptions) {
888
+ adoptFirstTimestamp(prepareOptions?.firstTimestamp);
889
+ const navigationMediaTime = getNavigationMediaTime(mediaTime);
882
890
  if (destroyed ||
883
891
  !isLoadingEnabled() ||
884
892
  !playbackGate?.enabled ||
@@ -888,28 +896,31 @@ function createBufferedDetectionTimeline(options) {
888
896
  return false;
889
897
  }
890
898
  const availableRanges = options.source.getAvailableRanges();
891
- return !createPlaybackGateCoveragePlan(mediaTime, prepareOptions).sourceRanges.every((range) => isRangeCovered(range, availableRanges));
899
+ return !createPlaybackGateCoveragePlan(navigationMediaTime, prepareOptions).sourceRanges.every((range) => isRangeCovered(range, availableRanges));
892
900
  },
893
901
  prefetch(mediaTime) {
894
902
  if (destroyed || !isLoadingEnabled()) {
895
903
  return;
896
904
  }
897
- anchorWindowToPlayhead(mediaTime);
898
- supersedeLoadOutside(mediaTime);
899
- if (!shouldPrefetch(mediaTime)) {
905
+ const navigationMediaTime = getNavigationMediaTime(mediaTime);
906
+ anchorWindowToPlayhead(navigationMediaTime);
907
+ supersedeLoadOutside(navigationMediaTime);
908
+ if (!shouldPrefetch(navigationMediaTime)) {
900
909
  return;
901
910
  }
902
- pendingPrefetch = { loadId, mediaTime };
911
+ pendingPrefetch = { loadId, mediaTime: navigationMediaTime };
903
912
  pumpPrefetchQueue();
904
913
  },
905
914
  selectFrame(mediaTime) {
906
- if (!isBuffered(mediaTime)) {
915
+ const navigationMediaTime = getNavigationMediaTime(mediaTime);
916
+ if (!isInsideBufferedRange(navigationMediaTime)) {
907
917
  return undefined;
908
918
  }
909
- return selectDetectionFrame(buffer, getSourceMediaTime(mediaTime), options);
919
+ return selectDetectionFrame(buffer, getSourceMediaTime(navigationMediaTime), options);
910
920
  },
911
921
  setTimelineContext(context) {
912
922
  timelineContext = context;
923
+ adoptFirstTimestamp(context.firstTimestamp);
913
924
  bufferedSourceVersion = null;
914
925
  bufferedVersionRange = null;
915
926
  },
@@ -1045,12 +1056,12 @@ function createBufferedDetectionTimeline(options) {
1045
1056
  }
1046
1057
  function createPlaybackGateCoveragePlan(mediaTime, prepareOptions) {
1047
1058
  const requiredAheadSeconds = Math.max(0, playbackGate?.requiredAheadSeconds ?? 0);
1048
- const comparableMediaTime = getComparableMediaTime(mediaTime);
1059
+ const comparableMediaTime = Math.max(timelineFirstTimestamp, getComparableMediaTime(mediaTime));
1049
1060
  const endTime = getRequiredCoverageEndTime({
1050
1061
  // A looping window counts past the end of media and wraps into the
1051
1062
  // replay, so clamping it to duration would ask for less than it needs.
1052
1063
  duration: isLoopingTimeline() ? null : prepareOptions?.duration,
1053
- firstTimestamp: prepareOptions?.firstTimestamp,
1064
+ firstTimestamp: timelineFirstTimestamp,
1054
1065
  mediaTime: comparableMediaTime,
1055
1066
  requiredAheadSeconds,
1056
1067
  });
@@ -1137,21 +1148,23 @@ function createBufferedDetectionTimeline(options) {
1137
1148
  * a looping timeline. A plan is stated on the first lap and the window on the
1138
1149
  * playhead's, so across the loop point the two state the same edge a whole lap
1139
1150
  * apart; measured straight, that reads as a window a full lap stale and
1140
- * rebuilds it on every frame until the plan stops reaching behind zero.
1151
+ * rebuilds it on every frame until the plan stops reaching behind the first
1152
+ * playable timestamp.
1141
1153
  */
1142
1154
  function getWindowDrift(planTime, bufferTime) {
1143
1155
  const drift = Math.abs(planTime - bufferTime);
1144
1156
  if (!isLoopingTimeline() || timelineContext.duration === null) {
1145
1157
  return drift;
1146
1158
  }
1147
- const wrappedDrift = modulo(drift, timelineContext.duration);
1148
- return Math.min(wrappedDrift, timelineContext.duration - wrappedDrift);
1159
+ const loopSpan = timelineContext.duration - timelineFirstTimestamp;
1160
+ const wrappedDrift = modulo(drift, loopSpan);
1161
+ return Math.min(wrappedDrift, loopSpan - wrappedDrift);
1149
1162
  }
1150
1163
  function createLoadPlan(requestedStartTime, requestedEndTime) {
1151
1164
  const startTime = Math.min(requestedStartTime, requestedEndTime);
1152
1165
  const endTime = Math.max(startTime, requestedEndTime);
1153
1166
  if (!isLoopingTimeline()) {
1154
- const clampedStartTime = Math.max(0, startTime);
1167
+ const clampedStartTime = Math.max(timelineFirstTimestamp, startTime);
1155
1168
  const clampedEndTime = Math.max(clampedStartTime, endTime);
1156
1169
  return {
1157
1170
  endTime: clampedEndTime,
@@ -1165,22 +1178,25 @@ function createBufferedDetectionTimeline(options) {
1165
1178
  };
1166
1179
  }
1167
1180
  const duration = timelineContext.duration ?? 0;
1168
- if (endTime - startTime >= duration) {
1181
+ const loopSpan = duration - timelineFirstTimestamp;
1182
+ if (endTime - startTime >= loopSpan) {
1169
1183
  return {
1170
1184
  endTime: duration,
1171
- sourceRanges: [{ endTime: duration, startTime: 0 }],
1172
- startTime: 0,
1185
+ sourceRanges: [
1186
+ { endTime: duration, startTime: timelineFirstTimestamp },
1187
+ ],
1188
+ startTime: timelineFirstTimestamp,
1173
1189
  };
1174
1190
  }
1175
1191
  // Both ends move by the same whole number of laps, so the window keeps its
1176
1192
  // span and its source ranges while its start reads on the media clock: a
1177
1193
  // window counted in the laps playback accumulated is one no host can hold
1178
1194
  // against a current time.
1179
- const laps = Math.floor(startTime / duration);
1195
+ const laps = Math.floor((startTime - timelineFirstTimestamp) / loopSpan);
1180
1196
  return {
1181
- endTime: endTime - laps * duration,
1182
- sourceRanges: getLoopingSourceRanges(startTime, endTime, duration),
1183
- startTime: startTime - laps * duration,
1197
+ endTime: endTime - laps * loopSpan,
1198
+ sourceRanges: getLoopingSourceRanges(startTime, endTime, duration, timelineFirstTimestamp),
1199
+ startTime: startTime - laps * loopSpan,
1184
1200
  };
1185
1201
  }
1186
1202
  function getBufferedSourceRanges() {
@@ -1192,7 +1208,7 @@ function createBufferedDetectionTimeline(options) {
1192
1208
  function isLoopingTimeline() {
1193
1209
  return (timelineContext.loop &&
1194
1210
  timelineContext.duration !== null &&
1195
- timelineContext.duration > 0);
1211
+ timelineContext.duration > timelineFirstTimestamp);
1196
1212
  }
1197
1213
  function getComparableMediaTime(mediaTime) {
1198
1214
  if (!isLoopingTimeline() ||
@@ -1201,23 +1217,42 @@ function createBufferedDetectionTimeline(options) {
1201
1217
  timelineContext.duration === null) {
1202
1218
  return mediaTime;
1203
1219
  }
1204
- const duration = timelineContext.duration;
1205
- // The representative of mediaTime (mod duration) anchored at the window
1220
+ const loopSpan = timelineContext.duration - timelineFirstTimestamp;
1221
+ // The representative of mediaTime (mod the loop span) anchored at the window
1206
1222
  // start. The mapping must depend only on where the window sits, never on
1207
1223
  // how many laps playback has accumulated, or membership drifts away from
1208
1224
  // what the buffer actually holds. A time less than one lap past the
1209
1225
  // anchor never wraps, so ordinary forward playback cannot ratchet.
1210
1226
  return (state.bufferStartTime +
1211
- modulo(mediaTime - state.bufferStartTime, duration));
1227
+ modulo(mediaTime - state.bufferStartTime, loopSpan));
1212
1228
  }
1213
1229
  function getSourceMediaTime(mediaTime) {
1214
1230
  if (!isLoopingTimeline() || timelineContext.duration === null) {
1215
1231
  return mediaTime;
1216
1232
  }
1217
- if (mediaTime >= 0 && mediaTime <= timelineContext.duration) {
1233
+ if (mediaTime >= timelineFirstTimestamp &&
1234
+ mediaTime <= timelineContext.duration) {
1218
1235
  return mediaTime;
1219
1236
  }
1220
- return modulo(mediaTime, timelineContext.duration);
1237
+ const loopSpan = timelineContext.duration - timelineFirstTimestamp;
1238
+ return (timelineFirstTimestamp +
1239
+ modulo(mediaTime - timelineFirstTimestamp, loopSpan));
1240
+ }
1241
+ function getNavigationMediaTime(mediaTime) {
1242
+ return Math.max(timelineFirstTimestamp, mediaTime);
1243
+ }
1244
+ function adoptFirstTimestamp(firstTimestamp) {
1245
+ if (firstTimestamp === undefined ||
1246
+ !Number.isFinite(firstTimestamp) ||
1247
+ firstTimestamp === timelineFirstTimestamp) {
1248
+ return;
1249
+ }
1250
+ timelineFirstTimestamp = firstTimestamp;
1251
+ loadId += 1;
1252
+ inFlight = undefined;
1253
+ pendingPrefetch = undefined;
1254
+ bufferedSourceVersion = null;
1255
+ bufferedVersionRange = null;
1221
1256
  }
1222
1257
  }
1223
1258
  function createIdleDetectionBufferState() {
@@ -1248,13 +1283,14 @@ function getRequiredCoverageEndTime(options) {
1248
1283
  if (options.duration === null || options.duration === undefined) {
1249
1284
  return requestedEndTime;
1250
1285
  }
1251
- return Math.min(requestedEndTime, (options.firstTimestamp ?? 0) + Math.max(options.duration, 0));
1286
+ return Math.min(requestedEndTime, Math.max(options.firstTimestamp ?? 0, options.duration));
1252
1287
  }
1253
- function getLoopingSourceRanges(startTime, endTime, duration) {
1254
- const normalizedStartTime = modulo(startTime, duration);
1255
- const normalizedEndTime = modulo(endTime, duration);
1256
- const startCycle = Math.floor(startTime / duration);
1257
- const endCycle = Math.floor(endTime / duration);
1288
+ function getLoopingSourceRanges(startTime, endTime, duration, firstTimestamp = 0) {
1289
+ const loopSpan = duration - firstTimestamp;
1290
+ const normalizedStartTime = firstTimestamp + modulo(startTime - firstTimestamp, loopSpan);
1291
+ const normalizedEndTime = firstTimestamp + modulo(endTime - firstTimestamp, loopSpan);
1292
+ const startCycle = Math.floor((startTime - firstTimestamp) / loopSpan);
1293
+ const endCycle = Math.floor((endTime - firstTimestamp) / loopSpan);
1258
1294
  if (startCycle === endCycle) {
1259
1295
  return [{ endTime: normalizedEndTime, startTime: normalizedStartTime }];
1260
1296
  }
@@ -1262,8 +1298,8 @@ function getLoopingSourceRanges(startTime, endTime, duration) {
1262
1298
  if (normalizedStartTime < duration) {
1263
1299
  ranges.push({ endTime: duration, startTime: normalizedStartTime });
1264
1300
  }
1265
- if (normalizedEndTime > 0) {
1266
- ranges.push({ endTime: normalizedEndTime, startTime: 0 });
1301
+ if (normalizedEndTime > firstTimestamp) {
1302
+ ranges.push({ endTime: normalizedEndTime, startTime: firstTimestamp });
1267
1303
  }
1268
1304
  return ranges;
1269
1305
  }