@mega-yfue/eufy-sdk 0.5.0-beta.82 → 0.5.0-beta.84

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.
@@ -680,10 +680,9 @@ export interface LiveStreamConsumer extends LiveStreamHandle {
680
680
  * consumer receives, so a consumer holding media it has not been told the configuration of is not a state
681
681
  * it can reach.
682
682
  *
683
- * Per consumer, against what THIS consumer was last given: a consumer that joins mid-session is primed
684
- * with a cached keyframe it did not witness arriving, and one that crosses its bound resynchronises onto
685
- * a later IDR having skipped the frame the source saw the change on. Announcing what the source saw
686
- * would leave both holding an encoder built for media they never received.
683
+ * Per consumer, against what this consumer was last given: a mid-session join begins at its next live
684
+ * keyframe, and a consumer crossing its queue bound resynchronises onto a later IDR. Each receives
685
+ * the configuration of the media it is delivered, even when another consumer has already seen it.
687
686
  *
688
687
  * Only a consumer announces this, never a bare {@link LiveStreamHandle}: it is read from the parameter
689
688
  * sets a shared source watches every frame for, which the raw pull underneath it does not do. That is
package/dist/index.js CHANGED
@@ -19128,10 +19128,9 @@ async function openLiveStream(session, opts = {}) {
19128
19128
  async function captureSnapshotFromShared(source, opts = {}) {
19129
19129
  const timeoutMs = opts.timeoutMs ?? 2e4;
19130
19130
  const collectMs = opts.collectMs ?? 1500;
19131
- const skip = opts.skipKeyframes ?? 1;
19132
19131
  opts.signal?.throwIfAborted();
19133
19132
  const consumer = source.attach();
19134
- const primed = consumer.primed;
19133
+ const skip = opts.skipKeyframes ?? (source.state === "live" ? 0 : 1);
19135
19134
  let burst;
19136
19135
  try {
19137
19136
  burst = await new Promise((resolve, reject) => {
@@ -19148,8 +19147,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
19148
19147
  if (fr.keyframe)
19149
19148
  keyCount++;
19150
19149
  if (!capturing) {
19151
- const threshold = primed ? 0 : skip;
19152
- if (!fr.keyframe || keyCount <= threshold)
19150
+ if (!fr.keyframe || keyCount <= skip)
19153
19151
  return;
19154
19152
  capturing = true;
19155
19153
  codec = fr.codec;
@@ -19159,7 +19157,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
19159
19157
  settle = setTimeout(() => {
19160
19158
  cleanup();
19161
19159
  resolve({ h264: Buffer.concat(bufs), codec, sets });
19162
- }, primed ? 0 : collectMs);
19160
+ }, collectMs);
19163
19161
  };
19164
19162
  const onError = (err) => {
19165
19163
  cleanup();
@@ -19549,30 +19547,24 @@ var ConsumerImpl = class extends EventEmitter4 {
19549
19547
  queue = [];
19550
19548
  paused = false;
19551
19549
  detached = false;
19552
- primed = false;
19553
19550
  awaitingKeyframe = false;
19554
19551
  /** The last coded configuration announced to THIS consumer — see {@link flush}. */
19555
19552
  config;
19556
- /** Cached keyframe to replay, held until a "video" listener actually subscribes (see below). */
19557
- pendingPrime;
19558
- constructor(onDetach, maxQueue) {
19553
+ /** Re-arm an ordinary join when its first video-bearing subscription starts, including after an async gap. */
19554
+ constructor(onDetach, maxQueue, waitForKeyframe) {
19559
19555
  super();
19560
19556
  this.onDetach = onDetach;
19561
19557
  this.maxQueue = maxQueue;
19562
- this.on("newListener", (event) => {
19563
- if (event !== "video" || this.listenerCount("video") !== 0)
19564
- return;
19565
- const kf = this.pendingPrime;
19566
- if (!kf)
19567
- return;
19568
- this.pendingPrime = void 0;
19569
- queueMicrotask(() => this.deliverVideo(kf));
19570
- });
19571
- }
19572
- /** Stage a keyframe to replay when the first video listener subscribes. */
19573
- prime(item) {
19574
- this.primed = true;
19575
- this.pendingPrime = item;
19558
+ if (waitForKeyframe) {
19559
+ this.on("newListener", (event) => {
19560
+ if (event !== "video" && event !== "media")
19561
+ return;
19562
+ if (this.listenerCount("video") > 0 || this.listenerCount("media") > 0)
19563
+ return;
19564
+ this.awaitingKeyframe = true;
19565
+ this.config = void 0;
19566
+ });
19567
+ }
19576
19568
  }
19577
19569
  /** Attached-by-construction — `start()` is a no-op so a Consumer satisfies LiveStreamHandle. */
19578
19570
  start() {
@@ -19623,14 +19615,7 @@ var ConsumerImpl = class extends EventEmitter4 {
19623
19615
  this.flush(q[i]);
19624
19616
  }
19625
19617
  }
19626
- /**
19627
- * Source → consumer video, honouring resync-to-keyframe and the bounded queue.
19628
- *
19629
- * Takes the whole item rather than a frame, because a keyframe-prime replays a RETAINED one and has to
19630
- * arrive by the same route: a primed keyframe is exactly the IDR a resynchronising consumer is waiting
19631
- * for, so it must clear that wait as a live keyframe does, and it carries the configuration its own media
19632
- * was coded under. While that wait is unsatisfied a delta frame is dropped — nothing can begin at it.
19633
- */
19618
+ /** Source → consumer video, dropping dependent pictures until a subscribed consumer receives a keyframe. */
19634
19619
  deliverVideo(item) {
19635
19620
  if (this.detached)
19636
19621
  return;
@@ -19675,9 +19660,8 @@ var ConsumerImpl = class extends EventEmitter4 {
19675
19660
  * about ahead of the frame that carries it.
19676
19661
  *
19677
19662
  * The single delivery point is where the announcement belongs, because every way media reaches a
19678
- * consumer passes through here: a live frame, a replayed keyframe-prime, and a backlog drained by
19679
- * {@link resume}. Comparing against what this consumer was last given rather than what the source last
19680
- * saw is what makes a primed join and a post-overflow resynchronisation correct.
19663
+ * consumer passes through here, including a backlog drained by {@link resume}. The comparison is
19664
+ * against what this consumer was last given rather than what the source last saw.
19681
19665
  */
19682
19666
  flush(item) {
19683
19667
  if (item.kind === "video") {
@@ -19731,8 +19715,8 @@ var SharedLiveSource = class {
19731
19715
  * {@link LiveStreamStartError}.
19732
19716
  */
19733
19717
  delivered = { keyframe: false, video: false, audio: false };
19734
- /** Last keyframe access unit seen — replayed to a joining consumer (keyframe-prime). */
19735
- lastKeyframe;
19718
+ /** Whether the upstream has delivered a keyframe since its last teardown. */
19719
+ hasKeyframe = false;
19736
19720
  /** Last parameter sets the stream announced — see {@link parameterSets}. */
19737
19721
  lastParamSets;
19738
19722
  /**
@@ -19750,14 +19734,7 @@ var SharedLiveSource = class {
19750
19734
  warmRetryTimer;
19751
19735
  /** Whether this warm-up has already asked its owner to replace the session. */
19752
19736
  sessionReplacementAsked = false;
19753
- /**
19754
- * Whether the pending watch is a REUSE watch, which any frame settles.
19755
- *
19756
- * A cold warm-up needs a keyframe: nothing can be decoded without one. A join already holds the retained
19757
- * keyframe, so what its watch is missing is evidence the stream is still being served — and a delta frame is
19758
- * that evidence. Requiring a keyframe there let the deadline outlive an actively delivering stream whose
19759
- * group of pictures is longer than the window, and the timeout fails EVERY consumer.
19760
- */
19737
+ /** A reuse watch settles on upstream media independently of each consumer's keyframe gate. */
19761
19738
  reuseWatch = false;
19762
19739
  /** This source's opaque handle for tracing — see {@link SharedLiveSource.trace}. */
19763
19740
  traceId;
@@ -19765,8 +19742,8 @@ var SharedLiveSource = class {
19765
19742
  * How many re-issues this watch has spent with nothing arriving since it was armed.
19766
19743
  *
19767
19744
  * What a stream delivered BEFORE the current watch is no evidence about now — a reused stream's upstream may
19768
- * have served plenty and since been dropped by the station, and the retained keyframe replayed to a joining
19769
- * consumer says nothing either. Only a frame arriving after the watch was armed does, and {@link delivered}
19745
+ * have served plenty and since been dropped by the station. Only a frame arriving after the watch was
19746
+ * armed proves current delivery, and {@link delivered}
19770
19747
  * is reset to track exactly that.
19771
19748
  *
19772
19749
  * The first re-issue is therefore a keepalive: a reuse cannot yet know which case it is in, and a keepalive
@@ -19825,40 +19802,38 @@ var SharedLiveSource = class {
19825
19802
  }
19826
19803
  /**
19827
19804
  * Attach a new consumer. Warms the stream on the first attach (or cancels a pending linger teardown
19828
- * and reuses the warm stream), then replays the cached keyframe so the consumer can decode at once.
19805
+ * and reuses the warm stream). Delivery starts at the next live keyframe so every dependent
19806
+ * picture follows its reference pictures without a gap from a cached keyframe.
19829
19807
  */
19830
19808
  attach() {
19831
19809
  return this.attachConsumer(true);
19832
19810
  }
19833
19811
  /**
19834
19812
  * Attach at the same instant a keyframe-aligned prebuffer snapshot is taken. The returned consumer
19835
- * is not separately keyframe-primed, so replaying `buffered` followed by its live events neither
19836
- * duplicates the newest IDR nor leaves a gap at the handoff.
19813
+ * continues after `buffered` without waiting for another keyframe or duplicating the newest IDR.
19837
19814
  */
19838
19815
  attachWithPrebuffer(seconds) {
19839
19816
  const consumer = this.attachConsumer(false);
19840
19817
  return { consumer, buffered: this.bufferedMedia(seconds) };
19841
19818
  }
19842
- attachConsumer(prime) {
19819
+ attachConsumer(waitForKeyframe) {
19843
19820
  if (this.disposed)
19844
19821
  throw new Error("SharedLiveSource is disposed");
19845
19822
  const wasEmpty = this.consumers.size === 0;
19846
- const consumer = new ConsumerImpl((c) => this.onDetach(c), this.maxQueue);
19823
+ const consumer = new ConsumerImpl((c) => this.onDetach(c), this.maxQueue, waitForKeyframe);
19847
19824
  this.consumers.add(consumer);
19848
19825
  if (wasEmpty)
19849
19826
  this.opts.onActive?.();
19850
19827
  if (this.lingerTimer.pending) {
19851
19828
  this.lingerTimer.cancel();
19852
19829
  if (this._state === "lingering")
19853
- this._state = this.lastKeyframe ? "live" : "warming";
19830
+ this._state = this.hasKeyframe ? "live" : "warming";
19854
19831
  this.logger.debug(`${this.tag} re-attach in linger window \u2014 reusing warm stream (consumers=${this.consumers.size})`);
19855
19832
  }
19856
19833
  if (!this.stream)
19857
19834
  this.warm();
19858
19835
  else
19859
19836
  this.watchReusedStream();
19860
- if (prime && this.lastKeyframe)
19861
- consumer.prime(this.lastKeyframe);
19862
19837
  return consumer;
19863
19838
  }
19864
19839
  /**
@@ -19990,24 +19965,9 @@ var SharedLiveSource = class {
19990
19965
  current.nudge(!arrivedSinceWatch && this.fruitlessReissues > 1);
19991
19966
  }
19992
19967
  /**
19993
- * Watch a stream this consumer joined rather than warmed, so a dead one cannot pass for a live one.
19994
- *
19995
- * A reused stream hands a joining consumer the retained keyframe at once, which is evidence about the past:
19996
- * it says the stream WAS being served, not that it still is. A caller commits to media on that frame — a
19997
- * process, a negotiated session — so a stream the station has quietly stopped serving strands it with no
19998
- * deadline, because warming is what arms one and a reuse skips warming by definition.
19999
- *
20000
- * The watch is the warm-up's own, deadline and retry alike, and the first frame to arrive AFTER the join
20001
- * clears it, that being the only frame which says the stream is still being served. A stream still serving
20002
- * clears it long before the deadline fires; one that is not fails its consumers exactly as a cold start that
20003
- * never delivered would.
20004
- *
20005
- * The start is re-issued at once and then on the retry's cadence, because a station that stopped serving a
20006
- * channel when its last consumer left is the very case the retry recovers: waiting the whole window to
20007
- * report what one re-issued start can fix is a timeout where a stream was available.
20008
- *
20009
- * Nothing is armed while a watch is already pending, so several consumers joining one reused stream share the
20010
- * watch the first of them started.
19968
+ * Watch an existing upstream with the warm-up retry and deadline. Any video or audio received after
19969
+ * the watch begins confirms delivery and settles it. Each consumer's keyframe gate remains separate.
19970
+ * Concurrent joins share a pending watch; the first join reissues the start immediately.
20011
19971
  */
20012
19972
  watchReusedStream() {
20013
19973
  if (this.warmDeadlineTimer.pending || this.warmRetryTimer !== void 0)
@@ -20056,7 +20016,7 @@ var SharedLiveSource = class {
20056
20016
  this.clearBudget();
20057
20017
  this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
20058
20018
  }
20059
- /** Settle a reuse watch on any frame — the join already holds a decodable picture. */
20019
+ /** Settle a reuse watch on any upstream frame without changing consumer keyframe gates. */
20060
20020
  settleReuseWatch() {
20061
20021
  if (!this.reuseWatch)
20062
20022
  return;
@@ -20118,7 +20078,7 @@ var SharedLiveSource = class {
20118
20078
  this.lastParamSets = updatedParamSets(frame.data, this.lastParamSets);
20119
20079
  const item = { kind: "video", frame, timestampMs: Date.now(), config: this.configOf(frame) };
20120
20080
  if (frame.keyframe) {
20121
- this.lastKeyframe = item;
20081
+ this.hasKeyframe = true;
20122
20082
  const wasWarming = this.warmRetryTimer !== void 0 || this.warmDeadlineTimer.pending;
20123
20083
  this.delivered.keyframe = true;
20124
20084
  if (this._state === "warming")
@@ -20251,7 +20211,7 @@ var SharedLiveSource = class {
20251
20211
  this.lingerTimer.arm(this.lingerMs, () => this.teardown("stopped"));
20252
20212
  }
20253
20213
  /**
20254
- * Stop + drop the underlying stream and clear the prime/ring caches. Rebuildable via attach().
20214
+ * Stop + drop the underlying stream and clear the keyframe/ring caches. Rebuildable via attach().
20255
20215
  *
20256
20216
  * The stream reference is dropped BEFORE stopping it, because `stop()` emits `"stop"` synchronously and
20257
20217
  * this source listens for that — so stopping re-enters `teardown` through {@link onUpstreamEnd}. Clearing
@@ -20271,7 +20231,7 @@ var SharedLiveSource = class {
20271
20231
  stream?.stop();
20272
20232
  } catch {
20273
20233
  }
20274
- this.lastKeyframe = void 0;
20234
+ this.hasKeyframe = false;
20275
20235
  this.lastParamSets = void 0;
20276
20236
  this.declaredGeometry = void 0;
20277
20237
  this.configuredFrom = void 0;
@@ -20674,8 +20634,6 @@ function openReadableFromConsumer(consumer, opts = {}) {
20674
20634
  if (!readable.push(value))
20675
20635
  consumer.pause();
20676
20636
  });
20677
- consumer.on("video", () => {
20678
- });
20679
20637
  consumer.on("stop", () => readable.push(null));
20680
20638
  consumer.on("error", (err) => readable.destroy(err));
20681
20639
  return readable;
@@ -22187,7 +22145,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
22187
22145
  * A {@link MediaProvider} bound to one serial — resolves the session then calls `p2p/media`.
22188
22146
  *
22189
22147
  * Every live egress here is a consumer of the SAME shared pull, so N `live()` calls collapse to one
22190
- * PPCS session and a live snapshot against a warm, keyframe-primed source costs no extra pull at all; a
22148
+ * PPCS session and a live snapshot against a warm shared source costs no extra pull at all; a
22191
22149
  * cold source warms one and waits for a clean keyframe. Each shared egress passes its complete options
22192
22150
  * through because any of them may create the source, whose power and retention hints are fixed for
22193
22151
  * everyone who joins later. The bounded {@link MediaProvider.record} clip is the exception: it opens its