@mega-yfue/eufy-sdk 0.5.0-beta.82 → 0.5.0-beta.83
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/core/contracts.d.ts +3 -4
- package/dist/index.js +90 -86
- package/dist/index.js.map +2 -2
- package/dist/transport/p2p/command-router.d.ts +1 -1
- package/dist/transport/p2p/media.d.ts +3 -5
- package/dist/transport/p2p/p2p-session.d.ts +21 -0
- package/dist/transport/p2p/readable-egress.d.ts +1 -2
- package/dist/transport/p2p/shared-live-source.d.ts +15 -42
- package/package.json +1 -1
package/dist/core/contracts.d.ts
CHANGED
|
@@ -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
|
|
684
|
-
*
|
|
685
|
-
*
|
|
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
|
@@ -16227,6 +16227,10 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16227
16227
|
* change (the level-2 key arrived after an initial level-1 start), otherwise sends the 1139 nudge.
|
|
16228
16228
|
*/
|
|
16229
16229
|
liveStartedChannels = /* @__PURE__ */ new Map();
|
|
16230
|
+
/** The `cipher_id` this connection's `CMD_GATEWAYINFO` reply named, once its negotiation has read it. */
|
|
16231
|
+
gatewayCipherId;
|
|
16232
|
+
/** The `n` of the last `[0x01, n]` header this connection sent in the clear — see {@link nextClearCommandBytes}. */
|
|
16233
|
+
clearCommandCount = 0;
|
|
16230
16234
|
unackedLiveStarts = /* @__PURE__ */ new Map();
|
|
16231
16235
|
liveStartRetransmitTimer;
|
|
16232
16236
|
logger;
|
|
@@ -16424,6 +16428,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16424
16428
|
this.level2Negotiating = true;
|
|
16425
16429
|
const generation = this.connectionGeneration;
|
|
16426
16430
|
const cipherId = gatewayInfoCipherId(gwPayload);
|
|
16431
|
+
this.gatewayCipherId = cipherId;
|
|
16427
16432
|
this.trace({ phase: "level2-negotiating", cipherId });
|
|
16428
16433
|
void (async () => {
|
|
16429
16434
|
try {
|
|
@@ -16530,6 +16535,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16530
16535
|
this.level2Pending = this.cfg.resolveCipherKey !== void 0;
|
|
16531
16536
|
this.level2Negotiating = false;
|
|
16532
16537
|
this.level2Reprompted = false;
|
|
16538
|
+
this.gatewayCipherId = void 0;
|
|
16539
|
+
this.clearCommandCount = 0;
|
|
16533
16540
|
}
|
|
16534
16541
|
const socket = dgram.createSocket("udp4");
|
|
16535
16542
|
this.socket = socket;
|
|
@@ -16820,7 +16827,7 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16820
16827
|
this.sendMediaPayloadLevel2(CMD_START_REALTIME_MEDIA, channel, accountId, {});
|
|
16821
16828
|
return;
|
|
16822
16829
|
}
|
|
16823
|
-
const want = this.level2Key ? "l2" : "l1";
|
|
16830
|
+
const want = this.level2Key ? "l2" : this.sendsInTheClear() ? "clear" : "l1";
|
|
16824
16831
|
if (opts?.force) {
|
|
16825
16832
|
for (const pending of this.unackedLiveStarts.values())
|
|
16826
16833
|
if (pending.channel === channel)
|
|
@@ -16850,8 +16857,17 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16850
16857
|
* `video_type` is 12, and `transaction` is the millisecond timestamp as a string. The device answers a
|
|
16851
16858
|
* start carrying these values with a keyframe; `encryptkey` is the modulus it RSA-wraps each keyframe's
|
|
16852
16859
|
* AES media key with (unwrapped by {@link decodeVideoFrame}).
|
|
16860
|
+
*
|
|
16861
|
+
* With `clearMsgId`, the body of a start sent in the clear: `{accountId, encryptkey, msg_id, streamtype: 1}`
|
|
16862
|
+
* only, `msg_id` being `clearMsgId`, which changes from one start to the next.
|
|
16853
16863
|
*/
|
|
16854
|
-
startLiveJson(channel, accountId) {
|
|
16864
|
+
startLiveJson(channel, accountId, clearMsgId) {
|
|
16865
|
+
if (clearMsgId !== void 0) {
|
|
16866
|
+
return Buffer.from(JSON.stringify({
|
|
16867
|
+
commandType: CMD_START_LIVE,
|
|
16868
|
+
data: { accountId, encryptkey: this.rsaModulus(), msg_id: clearMsgId, streamtype: 1 }
|
|
16869
|
+
}), "utf-8");
|
|
16870
|
+
}
|
|
16855
16871
|
const now2 = Date.now();
|
|
16856
16872
|
return Buffer.from(JSON.stringify({
|
|
16857
16873
|
commandType: CMD_START_LIVE,
|
|
@@ -16882,25 +16898,31 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16882
16898
|
* (GCM, signCode 8, frame type 10) when the session has a negotiated key, else level-1 (AES-128-ECB,
|
|
16883
16899
|
* signCode 1, frame type 11) — chosen by the session key, not the device family. The camera RSA-wraps
|
|
16884
16900
|
* each keyframe's AES media key with the `encryptkey` modulus (unwrapped by {@link decodeVideoFrame}).
|
|
16901
|
+
* A session that {@link sendsInTheClear} sends it unencrypted instead: signCode 0, frame type 0, header
|
|
16902
|
+
* bytes `[0x01, n]` from {@link nextClearCommandBytes}, and `n` as the body's `msg_id`.
|
|
16885
16903
|
*/
|
|
16886
16904
|
sendStartLiveOwnSession(channel, accountId) {
|
|
16887
16905
|
if (!this.connectAddress)
|
|
16888
16906
|
return;
|
|
16889
|
-
const plain = this.startLiveJson(channel, accountId);
|
|
16890
16907
|
let payload;
|
|
16891
16908
|
let signCode;
|
|
16892
16909
|
let magic;
|
|
16893
16910
|
let frameType;
|
|
16894
16911
|
if (this.level2Key) {
|
|
16895
|
-
const enc2 = this.encryptLevel2(
|
|
16912
|
+
const enc2 = this.encryptLevel2(this.startLiveJson(channel, accountId));
|
|
16896
16913
|
if (!enc2)
|
|
16897
16914
|
return;
|
|
16898
16915
|
payload = enc2;
|
|
16899
16916
|
signCode = 8;
|
|
16900
16917
|
magic = [8, 0];
|
|
16901
16918
|
frameType = START_LIVE_FRAME_TYPE_L2;
|
|
16919
|
+
} else if (this.sendsInTheClear()) {
|
|
16920
|
+
magic = this.nextClearCommandBytes();
|
|
16921
|
+
payload = this.startLiveJson(channel, accountId, magic[1]);
|
|
16922
|
+
signCode = 0;
|
|
16923
|
+
frameType = 0;
|
|
16902
16924
|
} else {
|
|
16903
|
-
payload = encryptP2PData(paddingP2PData(
|
|
16925
|
+
payload = encryptP2PData(paddingP2PData(this.startLiveJson(channel, accountId)), this.level1Key);
|
|
16904
16926
|
signCode = 1;
|
|
16905
16927
|
magic = [1, 0];
|
|
16906
16928
|
frameType = START_LIVE_FRAME_TYPE_L1;
|
|
@@ -16920,6 +16942,21 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
16920
16942
|
this.armLiveStartRetransmit();
|
|
16921
16943
|
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
16922
16944
|
}
|
|
16945
|
+
/**
|
|
16946
|
+
* Whether this session's own-session start and stop go in the clear: its `CMD_GATEWAYINFO` reply named
|
|
16947
|
+
* `cipher_id` 0.
|
|
16948
|
+
*/
|
|
16949
|
+
sendsInTheClear() {
|
|
16950
|
+
return this.gatewayCipherId === 0;
|
|
16951
|
+
}
|
|
16952
|
+
/**
|
|
16953
|
+
* Header bytes 10-11 of this connection's next command sent in the clear: `[0x01, n]`, with `n` counting
|
|
16954
|
+
* those commands from 1 and never 0. A retransmit repeats its `n`, and the device's reply echoes it.
|
|
16955
|
+
*/
|
|
16956
|
+
nextClearCommandBytes() {
|
|
16957
|
+
this.clearCommandCount = this.clearCommandCount % 255 + 1;
|
|
16958
|
+
return [1, this.clearCommandCount];
|
|
16959
|
+
}
|
|
16923
16960
|
/**
|
|
16924
16961
|
* Repeat unacknowledged own-session live starts until the device takes one. Runs only while a start is
|
|
16925
16962
|
* outstanding.
|
|
@@ -17082,6 +17119,8 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17082
17119
|
* `uint32`, the same 4-byte body as the attached talkback frames. The app sends this frame on closing
|
|
17083
17120
|
* an own-session camera's live view; the camera stops streaming on it within a second, and keeps
|
|
17084
17121
|
* streaming through the 1350-wrapped form.
|
|
17122
|
+
* - own-session on a session that {@link sendsInTheClear}: the same 4-byte body unencrypted, signCode 0,
|
|
17123
|
+
* header bytes from {@link nextClearCommandBytes}.
|
|
17085
17124
|
* - without a level-2 key: the bare 1004 command.
|
|
17086
17125
|
*/
|
|
17087
17126
|
stopLiveMedia(channel = STATION_CHANNEL3, accountId = "", homeBaseAttached = false) {
|
|
@@ -17094,12 +17133,19 @@ var P2PSession = class _P2PSession extends EventEmitter2 {
|
|
|
17094
17133
|
clearInterval(this.liveStartRetransmitTimer);
|
|
17095
17134
|
this.liveStartRetransmitTimer = void 0;
|
|
17096
17135
|
}
|
|
17136
|
+
const body = Buffer.allocUnsafe(4);
|
|
17137
|
+
body.writeUInt32LE(channel >>> 0, 0);
|
|
17097
17138
|
if (this.level2Key && !homeBaseAttached) {
|
|
17098
|
-
const body = Buffer.allocUnsafe(4);
|
|
17099
|
-
body.writeUInt32LE(channel >>> 0, 0);
|
|
17100
17139
|
this.sendRawLevel2Bytes(body, channel, CMD_STOP_REALTIME_MEDIA, 8);
|
|
17101
17140
|
} else if (this.level2Key) {
|
|
17102
17141
|
this.sendMediaPayloadLevel2(CMD_STOP_REALTIME_MEDIA, channel, accountId, {});
|
|
17142
|
+
} else if (!homeBaseAttached && this.sendsInTheClear() && this.connectAddress) {
|
|
17143
|
+
const data = Buffer.concat([
|
|
17144
|
+
buildCommandHeader(this.seqNumber, CMD_STOP_REALTIME_MEDIA),
|
|
17145
|
+
buildRawCommandPayload(body, channel, 0, this.nextClearCommandBytes(), 0)
|
|
17146
|
+
]);
|
|
17147
|
+
this.seqNumber = this.seqNumber + 1 & 65535;
|
|
17148
|
+
this.send(this.connectAddress, RequestMessageType.DATA, data);
|
|
17103
17149
|
} else {
|
|
17104
17150
|
this.sendCommand(CMD_STOP_REALTIME_MEDIA, channel);
|
|
17105
17151
|
}
|
|
@@ -19128,10 +19174,9 @@ async function openLiveStream(session, opts = {}) {
|
|
|
19128
19174
|
async function captureSnapshotFromShared(source, opts = {}) {
|
|
19129
19175
|
const timeoutMs = opts.timeoutMs ?? 2e4;
|
|
19130
19176
|
const collectMs = opts.collectMs ?? 1500;
|
|
19131
|
-
const skip = opts.skipKeyframes ?? 1;
|
|
19132
19177
|
opts.signal?.throwIfAborted();
|
|
19133
19178
|
const consumer = source.attach();
|
|
19134
|
-
const
|
|
19179
|
+
const skip = opts.skipKeyframes ?? (source.state === "live" ? 0 : 1);
|
|
19135
19180
|
let burst;
|
|
19136
19181
|
try {
|
|
19137
19182
|
burst = await new Promise((resolve, reject) => {
|
|
@@ -19148,8 +19193,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
|
|
|
19148
19193
|
if (fr.keyframe)
|
|
19149
19194
|
keyCount++;
|
|
19150
19195
|
if (!capturing) {
|
|
19151
|
-
|
|
19152
|
-
if (!fr.keyframe || keyCount <= threshold)
|
|
19196
|
+
if (!fr.keyframe || keyCount <= skip)
|
|
19153
19197
|
return;
|
|
19154
19198
|
capturing = true;
|
|
19155
19199
|
codec = fr.codec;
|
|
@@ -19159,7 +19203,7 @@ async function captureSnapshotFromShared(source, opts = {}) {
|
|
|
19159
19203
|
settle = setTimeout(() => {
|
|
19160
19204
|
cleanup();
|
|
19161
19205
|
resolve({ h264: Buffer.concat(bufs), codec, sets });
|
|
19162
|
-
},
|
|
19206
|
+
}, collectMs);
|
|
19163
19207
|
};
|
|
19164
19208
|
const onError = (err) => {
|
|
19165
19209
|
cleanup();
|
|
@@ -19549,30 +19593,24 @@ var ConsumerImpl = class extends EventEmitter4 {
|
|
|
19549
19593
|
queue = [];
|
|
19550
19594
|
paused = false;
|
|
19551
19595
|
detached = false;
|
|
19552
|
-
primed = false;
|
|
19553
19596
|
awaitingKeyframe = false;
|
|
19554
19597
|
/** The last coded configuration announced to THIS consumer — see {@link flush}. */
|
|
19555
19598
|
config;
|
|
19556
|
-
/**
|
|
19557
|
-
|
|
19558
|
-
constructor(onDetach, maxQueue) {
|
|
19599
|
+
/** Re-arm an ordinary join when its first video-bearing subscription starts, including after an async gap. */
|
|
19600
|
+
constructor(onDetach, maxQueue, waitForKeyframe) {
|
|
19559
19601
|
super();
|
|
19560
19602
|
this.onDetach = onDetach;
|
|
19561
19603
|
this.maxQueue = maxQueue;
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
|
|
19569
|
-
|
|
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;
|
|
19604
|
+
if (waitForKeyframe) {
|
|
19605
|
+
this.on("newListener", (event) => {
|
|
19606
|
+
if (event !== "video" && event !== "media")
|
|
19607
|
+
return;
|
|
19608
|
+
if (this.listenerCount("video") > 0 || this.listenerCount("media") > 0)
|
|
19609
|
+
return;
|
|
19610
|
+
this.awaitingKeyframe = true;
|
|
19611
|
+
this.config = void 0;
|
|
19612
|
+
});
|
|
19613
|
+
}
|
|
19576
19614
|
}
|
|
19577
19615
|
/** Attached-by-construction — `start()` is a no-op so a Consumer satisfies LiveStreamHandle. */
|
|
19578
19616
|
start() {
|
|
@@ -19623,14 +19661,7 @@ var ConsumerImpl = class extends EventEmitter4 {
|
|
|
19623
19661
|
this.flush(q[i]);
|
|
19624
19662
|
}
|
|
19625
19663
|
}
|
|
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
|
-
*/
|
|
19664
|
+
/** Source → consumer video, dropping dependent pictures until a subscribed consumer receives a keyframe. */
|
|
19634
19665
|
deliverVideo(item) {
|
|
19635
19666
|
if (this.detached)
|
|
19636
19667
|
return;
|
|
@@ -19675,9 +19706,8 @@ var ConsumerImpl = class extends EventEmitter4 {
|
|
|
19675
19706
|
* about ahead of the frame that carries it.
|
|
19676
19707
|
*
|
|
19677
19708
|
* The single delivery point is where the announcement belongs, because every way media reaches a
|
|
19678
|
-
* consumer passes through here
|
|
19679
|
-
*
|
|
19680
|
-
* saw is what makes a primed join and a post-overflow resynchronisation correct.
|
|
19709
|
+
* consumer passes through here, including a backlog drained by {@link resume}. The comparison is
|
|
19710
|
+
* against what this consumer was last given rather than what the source last saw.
|
|
19681
19711
|
*/
|
|
19682
19712
|
flush(item) {
|
|
19683
19713
|
if (item.kind === "video") {
|
|
@@ -19731,8 +19761,8 @@ var SharedLiveSource = class {
|
|
|
19731
19761
|
* {@link LiveStreamStartError}.
|
|
19732
19762
|
*/
|
|
19733
19763
|
delivered = { keyframe: false, video: false, audio: false };
|
|
19734
|
-
/**
|
|
19735
|
-
|
|
19764
|
+
/** Whether the upstream has delivered a keyframe since its last teardown. */
|
|
19765
|
+
hasKeyframe = false;
|
|
19736
19766
|
/** Last parameter sets the stream announced — see {@link parameterSets}. */
|
|
19737
19767
|
lastParamSets;
|
|
19738
19768
|
/**
|
|
@@ -19750,14 +19780,7 @@ var SharedLiveSource = class {
|
|
|
19750
19780
|
warmRetryTimer;
|
|
19751
19781
|
/** Whether this warm-up has already asked its owner to replace the session. */
|
|
19752
19782
|
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
|
-
*/
|
|
19783
|
+
/** A reuse watch settles on upstream media independently of each consumer's keyframe gate. */
|
|
19761
19784
|
reuseWatch = false;
|
|
19762
19785
|
/** This source's opaque handle for tracing — see {@link SharedLiveSource.trace}. */
|
|
19763
19786
|
traceId;
|
|
@@ -19765,8 +19788,8 @@ var SharedLiveSource = class {
|
|
|
19765
19788
|
* How many re-issues this watch has spent with nothing arriving since it was armed.
|
|
19766
19789
|
*
|
|
19767
19790
|
* 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
|
|
19769
|
-
*
|
|
19791
|
+
* have served plenty and since been dropped by the station. Only a frame arriving after the watch was
|
|
19792
|
+
* armed proves current delivery, and {@link delivered}
|
|
19770
19793
|
* is reset to track exactly that.
|
|
19771
19794
|
*
|
|
19772
19795
|
* The first re-issue is therefore a keepalive: a reuse cannot yet know which case it is in, and a keepalive
|
|
@@ -19825,40 +19848,38 @@ var SharedLiveSource = class {
|
|
|
19825
19848
|
}
|
|
19826
19849
|
/**
|
|
19827
19850
|
* Attach a new consumer. Warms the stream on the first attach (or cancels a pending linger teardown
|
|
19828
|
-
* and reuses the warm stream)
|
|
19851
|
+
* and reuses the warm stream). Delivery starts at the next live keyframe so every dependent
|
|
19852
|
+
* picture follows its reference pictures without a gap from a cached keyframe.
|
|
19829
19853
|
*/
|
|
19830
19854
|
attach() {
|
|
19831
19855
|
return this.attachConsumer(true);
|
|
19832
19856
|
}
|
|
19833
19857
|
/**
|
|
19834
19858
|
* Attach at the same instant a keyframe-aligned prebuffer snapshot is taken. The returned consumer
|
|
19835
|
-
*
|
|
19836
|
-
* duplicates the newest IDR nor leaves a gap at the handoff.
|
|
19859
|
+
* continues after `buffered` without waiting for another keyframe or duplicating the newest IDR.
|
|
19837
19860
|
*/
|
|
19838
19861
|
attachWithPrebuffer(seconds) {
|
|
19839
19862
|
const consumer = this.attachConsumer(false);
|
|
19840
19863
|
return { consumer, buffered: this.bufferedMedia(seconds) };
|
|
19841
19864
|
}
|
|
19842
|
-
attachConsumer(
|
|
19865
|
+
attachConsumer(waitForKeyframe) {
|
|
19843
19866
|
if (this.disposed)
|
|
19844
19867
|
throw new Error("SharedLiveSource is disposed");
|
|
19845
19868
|
const wasEmpty = this.consumers.size === 0;
|
|
19846
|
-
const consumer = new ConsumerImpl((c) => this.onDetach(c), this.maxQueue);
|
|
19869
|
+
const consumer = new ConsumerImpl((c) => this.onDetach(c), this.maxQueue, waitForKeyframe);
|
|
19847
19870
|
this.consumers.add(consumer);
|
|
19848
19871
|
if (wasEmpty)
|
|
19849
19872
|
this.opts.onActive?.();
|
|
19850
19873
|
if (this.lingerTimer.pending) {
|
|
19851
19874
|
this.lingerTimer.cancel();
|
|
19852
19875
|
if (this._state === "lingering")
|
|
19853
|
-
this._state = this.
|
|
19876
|
+
this._state = this.hasKeyframe ? "live" : "warming";
|
|
19854
19877
|
this.logger.debug(`${this.tag} re-attach in linger window \u2014 reusing warm stream (consumers=${this.consumers.size})`);
|
|
19855
19878
|
}
|
|
19856
19879
|
if (!this.stream)
|
|
19857
19880
|
this.warm();
|
|
19858
19881
|
else
|
|
19859
19882
|
this.watchReusedStream();
|
|
19860
|
-
if (prime && this.lastKeyframe)
|
|
19861
|
-
consumer.prime(this.lastKeyframe);
|
|
19862
19883
|
return consumer;
|
|
19863
19884
|
}
|
|
19864
19885
|
/**
|
|
@@ -19990,24 +20011,9 @@ var SharedLiveSource = class {
|
|
|
19990
20011
|
current.nudge(!arrivedSinceWatch && this.fruitlessReissues > 1);
|
|
19991
20012
|
}
|
|
19992
20013
|
/**
|
|
19993
|
-
* Watch
|
|
19994
|
-
*
|
|
19995
|
-
*
|
|
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.
|
|
20014
|
+
* Watch an existing upstream with the warm-up retry and deadline. Any video or audio received after
|
|
20015
|
+
* the watch begins confirms delivery and settles it. Each consumer's keyframe gate remains separate.
|
|
20016
|
+
* Concurrent joins share a pending watch; the first join reissues the start immediately.
|
|
20011
20017
|
*/
|
|
20012
20018
|
watchReusedStream() {
|
|
20013
20019
|
if (this.warmDeadlineTimer.pending || this.warmRetryTimer !== void 0)
|
|
@@ -20056,7 +20062,7 @@ var SharedLiveSource = class {
|
|
|
20056
20062
|
this.clearBudget();
|
|
20057
20063
|
this.budgetTimer.arm(timerMs(ms, this.batteryBudgetMs), () => this.onBudgetExpire());
|
|
20058
20064
|
}
|
|
20059
|
-
/** Settle a reuse watch on any frame
|
|
20065
|
+
/** Settle a reuse watch on any upstream frame without changing consumer keyframe gates. */
|
|
20060
20066
|
settleReuseWatch() {
|
|
20061
20067
|
if (!this.reuseWatch)
|
|
20062
20068
|
return;
|
|
@@ -20118,7 +20124,7 @@ var SharedLiveSource = class {
|
|
|
20118
20124
|
this.lastParamSets = updatedParamSets(frame.data, this.lastParamSets);
|
|
20119
20125
|
const item = { kind: "video", frame, timestampMs: Date.now(), config: this.configOf(frame) };
|
|
20120
20126
|
if (frame.keyframe) {
|
|
20121
|
-
this.
|
|
20127
|
+
this.hasKeyframe = true;
|
|
20122
20128
|
const wasWarming = this.warmRetryTimer !== void 0 || this.warmDeadlineTimer.pending;
|
|
20123
20129
|
this.delivered.keyframe = true;
|
|
20124
20130
|
if (this._state === "warming")
|
|
@@ -20251,7 +20257,7 @@ var SharedLiveSource = class {
|
|
|
20251
20257
|
this.lingerTimer.arm(this.lingerMs, () => this.teardown("stopped"));
|
|
20252
20258
|
}
|
|
20253
20259
|
/**
|
|
20254
|
-
* Stop + drop the underlying stream and clear the
|
|
20260
|
+
* Stop + drop the underlying stream and clear the keyframe/ring caches. Rebuildable via attach().
|
|
20255
20261
|
*
|
|
20256
20262
|
* The stream reference is dropped BEFORE stopping it, because `stop()` emits `"stop"` synchronously and
|
|
20257
20263
|
* this source listens for that — so stopping re-enters `teardown` through {@link onUpstreamEnd}. Clearing
|
|
@@ -20271,7 +20277,7 @@ var SharedLiveSource = class {
|
|
|
20271
20277
|
stream?.stop();
|
|
20272
20278
|
} catch {
|
|
20273
20279
|
}
|
|
20274
|
-
this.
|
|
20280
|
+
this.hasKeyframe = false;
|
|
20275
20281
|
this.lastParamSets = void 0;
|
|
20276
20282
|
this.declaredGeometry = void 0;
|
|
20277
20283
|
this.configuredFrom = void 0;
|
|
@@ -20674,8 +20680,6 @@ function openReadableFromConsumer(consumer, opts = {}) {
|
|
|
20674
20680
|
if (!readable.push(value))
|
|
20675
20681
|
consumer.pause();
|
|
20676
20682
|
});
|
|
20677
|
-
consumer.on("video", () => {
|
|
20678
|
-
});
|
|
20679
20683
|
consumer.on("stop", () => readable.push(null));
|
|
20680
20684
|
consumer.on("error", (err) => readable.destroy(err));
|
|
20681
20685
|
return readable;
|
|
@@ -22187,7 +22191,7 @@ var P2PCommandRouter = class _P2PCommandRouter {
|
|
|
22187
22191
|
* A {@link MediaProvider} bound to one serial — resolves the session then calls `p2p/media`.
|
|
22188
22192
|
*
|
|
22189
22193
|
* 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
|
|
22194
|
+
* PPCS session and a live snapshot against a warm shared source costs no extra pull at all; a
|
|
22191
22195
|
* cold source warms one and waits for a clean keyframe. Each shared egress passes its complete options
|
|
22192
22196
|
* through because any of them may create the source, whose power and retention hints are fixed for
|
|
22193
22197
|
* everyone who joins later. The bounded {@link MediaProvider.record} clip is the exception: it opens its
|