@camstack/addon-pipeline 1.2.285 → 1.2.286

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.
@@ -26692,6 +26692,14 @@ var ServerMessageSchema = require_dist.discriminatedUnion("t", [
26692
26692
  clipId: require_dist.string().min(1),
26693
26693
  durationMs: require_dist.number().positive()
26694
26694
  }),
26695
+ require_dist.object({
26696
+ t: require_dist.literal("clipTransport"),
26697
+ clipId: require_dist.string().min(1),
26698
+ /** `forward` = only ahead of the playhead. `free` = anywhere. */
26699
+ seek: require_dist._enum(["forward", "free"]),
26700
+ /** Frame-step BACKWARD is meaningful. Forward always is. */
26701
+ stepBack: require_dist.boolean()
26702
+ }),
26695
26703
  require_dist.object({
26696
26704
  t: require_dist.literal("clipAudio"),
26697
26705
  clipId: require_dist.string().min(1),
@@ -26722,11 +26730,49 @@ function serializeServerMessage(msg) {
26722
26730
  //#endregion
26723
26731
  //#region src/stream-broker/recorded/feed-clock.ts
26724
26732
  /**
26733
+ * The ORIGIN two tracks are measured from — the base and the source anchor,
26734
+ * shared, so the offset between video and audio survives a discontinuity.
26735
+ *
26736
+ * ## Why this exists
26737
+ *
26738
+ * `markDiscontinuity` re-bases on the last output. With one instance per
26739
+ * track, that last output is the TRACK'S own, and the two differ by however
26740
+ * far apart the interleave left them — so every seek replaced the real A/V
26741
+ * offset with that gap, in the same direction each time. Measured on the
26742
+ * clock alone it grows by a fixed step each time and never comes back; how
26743
+ * big that step is, is the interleave's business — 10–23 ms a seek on a
26744
+ * synthetic fixture, **900 ms over three seeks on a real clip**.
26745
+ *
26746
+ * Calling `markSeek()` on both instances "together" did not help and could
26747
+ * not: it is not the CALL that has to be shared, it is the ORIGIN. With one,
26748
+ * `out_video − out_audio` is `(vPts − aPts) / rate` — exactly the source
26749
+ * offset, which is the entire content of lip sync — and there is nothing left
26750
+ * to keep in step.
26751
+ *
26752
+ * `lastOutMs` stays per-track: monotonicity is a promise to ONE receiver
26753
+ * track, and clamping video against audio's last stamp would move video.
26754
+ */
26755
+ var FeedOrigin = class {
26756
+ /** Output ms the current run is measured from. */
26757
+ outBaseMs = 0;
26758
+ /** Source ms the current run is measured from; `null` until the next stamp. */
26759
+ srcAnchorMs = null;
26760
+ /**
26761
+ * The furthest EITHER track has been stamped. What a discontinuity clears,
26762
+ * so the gap is opened past both — and so two clocks marking the same
26763
+ * discontinuity in a row produce one gap, not two.
26764
+ */
26765
+ highWaterMs = 0;
26766
+ };
26767
+ /**
26725
26768
  * Maps arbitrary, possibly-resetting source PTS (ms) onto a strictly increasing
26726
26769
  * output clock at `rateHz` (default 90 kHz video; pass 8000 for PCMU audio). On
26727
26770
  * seek the output keeps advancing while the source anchor re-bases, so
26728
- * per-segment PTS resets never rewind the track. Video and audio use SEPARATE
26729
- * instances that `markSeek()` together to stay A/V aligned across boundaries.
26771
+ * per-segment PTS resets never rewind the track.
26772
+ *
26773
+ * Video and audio are separate instances SHARING one {@link FeedOrigin} — see
26774
+ * there for what happens when they do not. An instance constructed without one
26775
+ * gets its own, so a lone clock behaves exactly as it always did.
26730
26776
  *
26731
26777
  * Playback rate: `setRate(r)` divides SOURCE elapsed time by `r` on the output
26732
26778
  * timeline (2× → half the output spacing → the receiver plays frames twice as
@@ -26736,20 +26782,21 @@ function serializeServerMessage(msg) {
26736
26782
  */
26737
26783
  var MonotonicClock = class {
26738
26784
  rateHz;
26739
- srcAnchorMs = null;
26740
26785
  lastSrcMs = null;
26741
- outBaseMs = 0;
26742
26786
  lastOutMs = 0;
26743
26787
  rate = 1;
26744
- constructor(rateHz = 9e4) {
26788
+ origin;
26789
+ constructor(rateHz = 9e4, origin) {
26745
26790
  this.rateHz = rateHz;
26791
+ this.origin = origin ?? new FeedOrigin();
26746
26792
  }
26747
26793
  stamp(srcPtsMs) {
26748
- if (this.srcAnchorMs === null) this.srcAnchorMs = srcPtsMs;
26794
+ if (this.origin.srcAnchorMs === null) this.origin.srcAnchorMs = srcPtsMs;
26749
26795
  this.lastSrcMs = srcPtsMs;
26750
- const elapsed = Math.max(0, (srcPtsMs - this.srcAnchorMs) / this.rate);
26751
- const outMs = this.outBaseMs + elapsed;
26796
+ const elapsed = Math.max(0, (srcPtsMs - this.origin.srcAnchorMs) / this.rate);
26797
+ const outMs = this.origin.outBaseMs + elapsed;
26752
26798
  this.lastOutMs = Math.max(this.lastOutMs, outMs);
26799
+ this.origin.highWaterMs = Math.max(this.origin.highWaterMs, this.lastOutMs);
26753
26800
  return Math.round(this.lastOutMs * this.rateHz / 1e3);
26754
26801
  }
26755
26802
  /**
@@ -26773,8 +26820,8 @@ var MonotonicClock = class {
26773
26820
  * rate-scaled.
26774
26821
  */
26775
26822
  markDiscontinuity(gapMs) {
26776
- this.outBaseMs = this.lastOutMs + gapMs;
26777
- this.srcAnchorMs = null;
26823
+ this.origin.outBaseMs = this.origin.highWaterMs + gapMs;
26824
+ this.origin.srcAnchorMs = null;
26778
26825
  this.lastSrcMs = null;
26779
26826
  }
26780
26827
  /**
@@ -26786,9 +26833,9 @@ var MonotonicClock = class {
26786
26833
  */
26787
26834
  setRate(rate) {
26788
26835
  if (!(rate > 0) || rate === this.rate) return;
26789
- if (this.srcAnchorMs !== null && this.lastSrcMs !== null) {
26790
- this.outBaseMs = this.lastOutMs;
26791
- this.srcAnchorMs = this.lastSrcMs;
26836
+ if (this.origin.srcAnchorMs !== null && this.lastSrcMs !== null) {
26837
+ this.origin.outBaseMs = this.lastOutMs;
26838
+ this.origin.srcAnchorMs = this.lastSrcMs;
26792
26839
  }
26793
26840
  this.rate = rate;
26794
26841
  }
@@ -27627,7 +27674,14 @@ function emptyPlayCensus() {
27627
27674
  }
27628
27675
  var RecordedFeeder = class {
27629
27676
  deps;
27630
- clock = new MonotonicClock();
27677
+ /**
27678
+ * ONE origin for both tracks. Two independent ones replaced the real A/V
27679
+ * offset with the interleave gap at every seek, in the same direction each
27680
+ * time — measured at 23 ms per seek on the clock alone and 900 ms over
27681
+ * three seeks on a real clip. See `FeedOrigin`.
27682
+ */
27683
+ feedOrigin = new FeedOrigin();
27684
+ clock = new MonotonicClock(9e4, this.feedOrigin);
27631
27685
  /** Audio clock (PCMU 8 kHz / Opus 48 kHz); re-anchored together with video. */
27632
27686
  audioClock;
27633
27687
  clockHasAdvanced = false;
@@ -27810,7 +27864,7 @@ var RecordedFeeder = class {
27810
27864
  lastAnnouncedState = null;
27811
27865
  constructor(deps) {
27812
27866
  this.deps = deps;
27813
- this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3);
27867
+ this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3, this.feedOrigin);
27814
27868
  this.scrubCache = deps.fragmentCache ?? new ScrubFragmentCache();
27815
27869
  }
27816
27870
  /**
@@ -30077,9 +30131,46 @@ var TimelineSession = class {
30077
30131
  this.lastPositionMs = null;
30078
30132
  const feeder = this.enterRecorded();
30079
30133
  await feeder.seek(offsetMs);
30134
+ this.reportClipTransport(clipId, feeder);
30080
30135
  await Promise.all([this.reportClipDuration(clipId, feeder), this.reportClipAudio(clipId, feeder)]);
30081
30136
  }
30082
30137
  /**
30138
+ * Tell the viewer what this clip can be driven to do.
30139
+ *
30140
+ * A clip served from the provider's stream is forward-only; one fetched as
30141
+ * a file is not. Without the answer the viewer draws the same progress bar
30142
+ * for both, and on the stream a backward drag is refused and the position
30143
+ * re-sent — a control that visibly undoes the operator's gesture, which
30144
+ * reads as broken rather than as unavailable (D62).
30145
+ *
30146
+ * A feeder that does not say sends nothing: silence is "the freedom you
30147
+ * always had", and claiming `free` for a feeder that never answered would
30148
+ * be this surface inventing a capability.
30149
+ */
30150
+ reportClipTransport(clipId, feeder) {
30151
+ const report = feeder.transport;
30152
+ if (report === void 0) {
30153
+ this.deps.onLog?.("clip playback: transport not reported — this feeder cannot say", {
30154
+ deviceId: this.deps.deviceId,
30155
+ clipId
30156
+ });
30157
+ return;
30158
+ }
30159
+ if (this.clip?.clipId !== clipId) return;
30160
+ this.deps.onLog?.("clip playback: transport reported", {
30161
+ deviceId: this.deps.deviceId,
30162
+ clipId,
30163
+ seek: report.seek,
30164
+ stepBack: report.stepBack
30165
+ });
30166
+ this.deps.session.sendControl(serializeServerMessage({
30167
+ t: "clipTransport",
30168
+ clipId,
30169
+ seek: report.seek,
30170
+ stepBack: report.stepBack
30171
+ }));
30172
+ }
30173
+ /**
30083
30174
  * Tell the viewer whether this clip has sound.
30084
30175
  *
30085
30176
  * Silence has three causes the operator cannot tell apart by listening —
@@ -31617,6 +31708,16 @@ function createClipStreamFeeder(deps) {
31617
31708
  }
31618
31709
  var ClipStreamFeeder = class {
31619
31710
  deps;
31711
+ /**
31712
+ * Forward-only, and no backward frame-step: the bytes behind the head were
31713
+ * never kept, and the camera offers no replay-from-offset. Stated here so
31714
+ * the viewer can DISABLE those verbs rather than offer them and have every
31715
+ * use refused — the whole content of this report.
31716
+ */
31717
+ transport = {
31718
+ seek: "forward",
31719
+ stepBack: false
31720
+ };
31620
31721
  phase = "idle";
31621
31722
  connection = null;
31622
31723
  demuxer;
@@ -26688,6 +26688,14 @@ var ServerMessageSchema = discriminatedUnion("t", [
26688
26688
  clipId: string().min(1),
26689
26689
  durationMs: number().positive()
26690
26690
  }),
26691
+ object({
26692
+ t: literal("clipTransport"),
26693
+ clipId: string().min(1),
26694
+ /** `forward` = only ahead of the playhead. `free` = anywhere. */
26695
+ seek: _enum(["forward", "free"]),
26696
+ /** Frame-step BACKWARD is meaningful. Forward always is. */
26697
+ stepBack: boolean()
26698
+ }),
26691
26699
  object({
26692
26700
  t: literal("clipAudio"),
26693
26701
  clipId: string().min(1),
@@ -26718,11 +26726,49 @@ function serializeServerMessage(msg) {
26718
26726
  //#endregion
26719
26727
  //#region src/stream-broker/recorded/feed-clock.ts
26720
26728
  /**
26729
+ * The ORIGIN two tracks are measured from — the base and the source anchor,
26730
+ * shared, so the offset between video and audio survives a discontinuity.
26731
+ *
26732
+ * ## Why this exists
26733
+ *
26734
+ * `markDiscontinuity` re-bases on the last output. With one instance per
26735
+ * track, that last output is the TRACK'S own, and the two differ by however
26736
+ * far apart the interleave left them — so every seek replaced the real A/V
26737
+ * offset with that gap, in the same direction each time. Measured on the
26738
+ * clock alone it grows by a fixed step each time and never comes back; how
26739
+ * big that step is, is the interleave's business — 10–23 ms a seek on a
26740
+ * synthetic fixture, **900 ms over three seeks on a real clip**.
26741
+ *
26742
+ * Calling `markSeek()` on both instances "together" did not help and could
26743
+ * not: it is not the CALL that has to be shared, it is the ORIGIN. With one,
26744
+ * `out_video − out_audio` is `(vPts − aPts) / rate` — exactly the source
26745
+ * offset, which is the entire content of lip sync — and there is nothing left
26746
+ * to keep in step.
26747
+ *
26748
+ * `lastOutMs` stays per-track: monotonicity is a promise to ONE receiver
26749
+ * track, and clamping video against audio's last stamp would move video.
26750
+ */
26751
+ var FeedOrigin = class {
26752
+ /** Output ms the current run is measured from. */
26753
+ outBaseMs = 0;
26754
+ /** Source ms the current run is measured from; `null` until the next stamp. */
26755
+ srcAnchorMs = null;
26756
+ /**
26757
+ * The furthest EITHER track has been stamped. What a discontinuity clears,
26758
+ * so the gap is opened past both — and so two clocks marking the same
26759
+ * discontinuity in a row produce one gap, not two.
26760
+ */
26761
+ highWaterMs = 0;
26762
+ };
26763
+ /**
26721
26764
  * Maps arbitrary, possibly-resetting source PTS (ms) onto a strictly increasing
26722
26765
  * output clock at `rateHz` (default 90 kHz video; pass 8000 for PCMU audio). On
26723
26766
  * seek the output keeps advancing while the source anchor re-bases, so
26724
- * per-segment PTS resets never rewind the track. Video and audio use SEPARATE
26725
- * instances that `markSeek()` together to stay A/V aligned across boundaries.
26767
+ * per-segment PTS resets never rewind the track.
26768
+ *
26769
+ * Video and audio are separate instances SHARING one {@link FeedOrigin} — see
26770
+ * there for what happens when they do not. An instance constructed without one
26771
+ * gets its own, so a lone clock behaves exactly as it always did.
26726
26772
  *
26727
26773
  * Playback rate: `setRate(r)` divides SOURCE elapsed time by `r` on the output
26728
26774
  * timeline (2× → half the output spacing → the receiver plays frames twice as
@@ -26732,20 +26778,21 @@ function serializeServerMessage(msg) {
26732
26778
  */
26733
26779
  var MonotonicClock = class {
26734
26780
  rateHz;
26735
- srcAnchorMs = null;
26736
26781
  lastSrcMs = null;
26737
- outBaseMs = 0;
26738
26782
  lastOutMs = 0;
26739
26783
  rate = 1;
26740
- constructor(rateHz = 9e4) {
26784
+ origin;
26785
+ constructor(rateHz = 9e4, origin) {
26741
26786
  this.rateHz = rateHz;
26787
+ this.origin = origin ?? new FeedOrigin();
26742
26788
  }
26743
26789
  stamp(srcPtsMs) {
26744
- if (this.srcAnchorMs === null) this.srcAnchorMs = srcPtsMs;
26790
+ if (this.origin.srcAnchorMs === null) this.origin.srcAnchorMs = srcPtsMs;
26745
26791
  this.lastSrcMs = srcPtsMs;
26746
- const elapsed = Math.max(0, (srcPtsMs - this.srcAnchorMs) / this.rate);
26747
- const outMs = this.outBaseMs + elapsed;
26792
+ const elapsed = Math.max(0, (srcPtsMs - this.origin.srcAnchorMs) / this.rate);
26793
+ const outMs = this.origin.outBaseMs + elapsed;
26748
26794
  this.lastOutMs = Math.max(this.lastOutMs, outMs);
26795
+ this.origin.highWaterMs = Math.max(this.origin.highWaterMs, this.lastOutMs);
26749
26796
  return Math.round(this.lastOutMs * this.rateHz / 1e3);
26750
26797
  }
26751
26798
  /**
@@ -26769,8 +26816,8 @@ var MonotonicClock = class {
26769
26816
  * rate-scaled.
26770
26817
  */
26771
26818
  markDiscontinuity(gapMs) {
26772
- this.outBaseMs = this.lastOutMs + gapMs;
26773
- this.srcAnchorMs = null;
26819
+ this.origin.outBaseMs = this.origin.highWaterMs + gapMs;
26820
+ this.origin.srcAnchorMs = null;
26774
26821
  this.lastSrcMs = null;
26775
26822
  }
26776
26823
  /**
@@ -26782,9 +26829,9 @@ var MonotonicClock = class {
26782
26829
  */
26783
26830
  setRate(rate) {
26784
26831
  if (!(rate > 0) || rate === this.rate) return;
26785
- if (this.srcAnchorMs !== null && this.lastSrcMs !== null) {
26786
- this.outBaseMs = this.lastOutMs;
26787
- this.srcAnchorMs = this.lastSrcMs;
26832
+ if (this.origin.srcAnchorMs !== null && this.lastSrcMs !== null) {
26833
+ this.origin.outBaseMs = this.lastOutMs;
26834
+ this.origin.srcAnchorMs = this.lastSrcMs;
26788
26835
  }
26789
26836
  this.rate = rate;
26790
26837
  }
@@ -27623,7 +27670,14 @@ function emptyPlayCensus() {
27623
27670
  }
27624
27671
  var RecordedFeeder = class {
27625
27672
  deps;
27626
- clock = new MonotonicClock();
27673
+ /**
27674
+ * ONE origin for both tracks. Two independent ones replaced the real A/V
27675
+ * offset with the interleave gap at every seek, in the same direction each
27676
+ * time — measured at 23 ms per seek on the clock alone and 900 ms over
27677
+ * three seeks on a real clip. See `FeedOrigin`.
27678
+ */
27679
+ feedOrigin = new FeedOrigin();
27680
+ clock = new MonotonicClock(9e4, this.feedOrigin);
27627
27681
  /** Audio clock (PCMU 8 kHz / Opus 48 kHz); re-anchored together with video. */
27628
27682
  audioClock;
27629
27683
  clockHasAdvanced = false;
@@ -27806,7 +27860,7 @@ var RecordedFeeder = class {
27806
27860
  lastAnnouncedState = null;
27807
27861
  constructor(deps) {
27808
27862
  this.deps = deps;
27809
- this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3);
27863
+ this.audioClock = new MonotonicClock(deps.audioClockRateHz ?? 8e3, this.feedOrigin);
27810
27864
  this.scrubCache = deps.fragmentCache ?? new ScrubFragmentCache();
27811
27865
  }
27812
27866
  /**
@@ -30073,9 +30127,46 @@ var TimelineSession = class {
30073
30127
  this.lastPositionMs = null;
30074
30128
  const feeder = this.enterRecorded();
30075
30129
  await feeder.seek(offsetMs);
30130
+ this.reportClipTransport(clipId, feeder);
30076
30131
  await Promise.all([this.reportClipDuration(clipId, feeder), this.reportClipAudio(clipId, feeder)]);
30077
30132
  }
30078
30133
  /**
30134
+ * Tell the viewer what this clip can be driven to do.
30135
+ *
30136
+ * A clip served from the provider's stream is forward-only; one fetched as
30137
+ * a file is not. Without the answer the viewer draws the same progress bar
30138
+ * for both, and on the stream a backward drag is refused and the position
30139
+ * re-sent — a control that visibly undoes the operator's gesture, which
30140
+ * reads as broken rather than as unavailable (D62).
30141
+ *
30142
+ * A feeder that does not say sends nothing: silence is "the freedom you
30143
+ * always had", and claiming `free` for a feeder that never answered would
30144
+ * be this surface inventing a capability.
30145
+ */
30146
+ reportClipTransport(clipId, feeder) {
30147
+ const report = feeder.transport;
30148
+ if (report === void 0) {
30149
+ this.deps.onLog?.("clip playback: transport not reported — this feeder cannot say", {
30150
+ deviceId: this.deps.deviceId,
30151
+ clipId
30152
+ });
30153
+ return;
30154
+ }
30155
+ if (this.clip?.clipId !== clipId) return;
30156
+ this.deps.onLog?.("clip playback: transport reported", {
30157
+ deviceId: this.deps.deviceId,
30158
+ clipId,
30159
+ seek: report.seek,
30160
+ stepBack: report.stepBack
30161
+ });
30162
+ this.deps.session.sendControl(serializeServerMessage({
30163
+ t: "clipTransport",
30164
+ clipId,
30165
+ seek: report.seek,
30166
+ stepBack: report.stepBack
30167
+ }));
30168
+ }
30169
+ /**
30079
30170
  * Tell the viewer whether this clip has sound.
30080
30171
  *
30081
30172
  * Silence has three causes the operator cannot tell apart by listening —
@@ -31613,6 +31704,16 @@ function createClipStreamFeeder(deps) {
31613
31704
  }
31614
31705
  var ClipStreamFeeder = class {
31615
31706
  deps;
31707
+ /**
31708
+ * Forward-only, and no backward frame-step: the bytes behind the head were
31709
+ * never kept, and the camera offers no replay-from-offset. Stated here so
31710
+ * the viewer can DISABLE those verbs rather than offer them and have every
31711
+ * use refused — the whole content of this report.
31712
+ */
31713
+ transport = {
31714
+ seek: "forward",
31715
+ stepBack: false
31716
+ };
31616
31717
  phase = "idle";
31617
31718
  connection = null;
31618
31719
  demuxer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-pipeline",
3
- "version": "1.2.285",
3
+ "version": "1.2.286",
4
4
  "description": "Pipeline bundle — runner, detection, motion, audio + stream broker. Multi-entry npm package shipping pipeline addons under a single bundle.",
5
5
  "keywords": [
6
6
  "camstack",