@camstack/addon-pipeline 1.2.57 → 1.2.59
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/recorder/index.js +7 -5
- package/dist/recorder/index.mjs +7 -5
- package/dist/stream-broker/index.js +81 -41
- package/dist/stream-broker/index.mjs +81 -41
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-Bxm34WFr.js → MaskShapeCanvas-DI4BY7W2-BWAtF7b-.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-S8uFj2nP.js → MotionZonesSettings-NcxxQN8r-ByQWV87C.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-nhHVCSoL.js → PrivacyMaskSettings-APgPLF7p-B9uGNcFR.js} +1 -1
- package/embed-dist/assets/{index-hICPL_hd.js → index-Dcfe-5Ig.js} +4 -4
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
package/dist/recorder/index.js
CHANGED
|
@@ -1851,15 +1851,15 @@ async function readGopRange(file, args) {
|
|
|
1851
1851
|
bytesRead += chunk.length;
|
|
1852
1852
|
return chunk;
|
|
1853
1853
|
};
|
|
1854
|
-
const parsed = await parseTail(file, read);
|
|
1855
|
-
if (parsed === null) return null;
|
|
1856
|
-
const
|
|
1854
|
+
const parsed = args.seededIndex === void 0 ? await parseTail(file, read) : null;
|
|
1855
|
+
if (args.seededIndex === void 0 && parsed === null) return null;
|
|
1856
|
+
const index = args.seededIndex ?? parsed.index;
|
|
1857
1857
|
const firstMoof = index.samples[0]?.moofOffset ?? 0;
|
|
1858
1858
|
if (firstMoof <= 0 || firstMoof >= file.size) return null;
|
|
1859
1859
|
const head = await read(0, firstMoof);
|
|
1860
1860
|
const init = parseInitInfo(head);
|
|
1861
1861
|
if (init === null) return null;
|
|
1862
|
-
const videoIndex = index.trackId === init.videoTrackId ? index : reparseTail(tail, file.size, init.videoTrackId);
|
|
1862
|
+
const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
|
|
1863
1863
|
if (videoIndex === null) return null;
|
|
1864
1864
|
const { start, end, sampleIndex } = fragmentRangeFor(videoIndex, Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale);
|
|
1865
1865
|
if (start <= 0 || end <= start || end > file.size) return null;
|
|
@@ -2668,7 +2668,8 @@ function buildRecordingProvider(deps) {
|
|
|
2668
2668
|
const gop = await readGopRange(file, {
|
|
2669
2669
|
segmentStartMs: seg.startMs,
|
|
2670
2670
|
segmentDurMs: seg.durMs,
|
|
2671
|
-
epochMs
|
|
2671
|
+
epochMs,
|
|
2672
|
+
seededIndex: deps.mfraFor?.(deviceId, profile, startMs)
|
|
2672
2673
|
});
|
|
2673
2674
|
if (gop !== null) return {
|
|
2674
2675
|
data: gop.data,
|
|
@@ -5437,6 +5438,7 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
5437
5438
|
capacity: (root) => this.locationCapacity(root),
|
|
5438
5439
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
|
5439
5440
|
calendar: this.calendar,
|
|
5441
|
+
mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
|
|
5440
5442
|
hydrateWindow: (deviceId, fromMs, toMs) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger),
|
|
5441
5443
|
onConfigChanged: (deviceId) => this.controller?.onConfigChanged(deviceId) ?? Promise.resolve(),
|
|
5442
5444
|
dataDir: this.ctx.dataDir,
|
package/dist/recorder/index.mjs
CHANGED
|
@@ -1849,15 +1849,15 @@ async function readGopRange(file, args) {
|
|
|
1849
1849
|
bytesRead += chunk.length;
|
|
1850
1850
|
return chunk;
|
|
1851
1851
|
};
|
|
1852
|
-
const parsed = await parseTail(file, read);
|
|
1853
|
-
if (parsed === null) return null;
|
|
1854
|
-
const
|
|
1852
|
+
const parsed = args.seededIndex === void 0 ? await parseTail(file, read) : null;
|
|
1853
|
+
if (args.seededIndex === void 0 && parsed === null) return null;
|
|
1854
|
+
const index = args.seededIndex ?? parsed.index;
|
|
1855
1855
|
const firstMoof = index.samples[0]?.moofOffset ?? 0;
|
|
1856
1856
|
if (firstMoof <= 0 || firstMoof >= file.size) return null;
|
|
1857
1857
|
const head = await read(0, firstMoof);
|
|
1858
1858
|
const init = parseInitInfo(head);
|
|
1859
1859
|
if (init === null) return null;
|
|
1860
|
-
const videoIndex = index.trackId === init.videoTrackId ? index : reparseTail(tail, file.size, init.videoTrackId);
|
|
1860
|
+
const videoIndex = index.trackId === init.videoTrackId ? index : parsed === null ? null : reparseTail(parsed.tail, file.size, init.videoTrackId);
|
|
1861
1861
|
if (videoIndex === null) return null;
|
|
1862
1862
|
const { start, end, sampleIndex } = fragmentRangeFor(videoIndex, Math.max(0, args.epochMs - args.segmentStartMs) / 1e3 * init.videoTimescale);
|
|
1863
1863
|
if (start <= 0 || end <= start || end > file.size) return null;
|
|
@@ -2666,7 +2666,8 @@ function buildRecordingProvider(deps) {
|
|
|
2666
2666
|
const gop = await readGopRange(file, {
|
|
2667
2667
|
segmentStartMs: seg.startMs,
|
|
2668
2668
|
segmentDurMs: seg.durMs,
|
|
2669
|
-
epochMs
|
|
2669
|
+
epochMs,
|
|
2670
|
+
seededIndex: deps.mfraFor?.(deviceId, profile, startMs)
|
|
2670
2671
|
});
|
|
2671
2672
|
if (gop !== null) return {
|
|
2672
2673
|
data: gop.data,
|
|
@@ -5435,6 +5436,7 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
5435
5436
|
capacity: (root) => this.locationCapacity(root),
|
|
5436
5437
|
hydrateDevice: (deviceId, locations) => hydrateDeviceFromStorage(this.ctx.api, this.index, deviceId, locations, this.ctx.logger),
|
|
5437
5438
|
calendar: this.calendar,
|
|
5439
|
+
mfraFor: (deviceId, profile, startMs) => this.mfraTables.get(deviceId, profile, startMs),
|
|
5438
5440
|
hydrateWindow: (deviceId, fromMs, toMs) => hydrateWindowFromStorage(this.index, deviceId, fromMs, toMs, this.resolvedLocations, this.ctx.logger),
|
|
5439
5441
|
onConfigChanged: (deviceId) => this.controller?.onConfigChanged(deviceId) ?? Promise.resolve(),
|
|
5440
5442
|
dataDir: this.ctx.dataDir,
|
|
@@ -21718,21 +21718,32 @@ function clampPlaybackRate(rate) {
|
|
|
21718
21718
|
if (!Number.isFinite(rate) || rate <= 0) return 0;
|
|
21719
21719
|
return Math.min(4, Math.max(.25, rate));
|
|
21720
21720
|
}
|
|
21721
|
-
var ServerMessageSchema = require_dist.discriminatedUnion("t", [
|
|
21722
|
-
|
|
21723
|
-
|
|
21724
|
-
|
|
21725
|
-
|
|
21726
|
-
|
|
21727
|
-
|
|
21728
|
-
|
|
21729
|
-
|
|
21730
|
-
|
|
21731
|
-
|
|
21732
|
-
|
|
21733
|
-
|
|
21734
|
-
|
|
21735
|
-
|
|
21721
|
+
var ServerMessageSchema = require_dist.discriminatedUnion("t", [
|
|
21722
|
+
require_dist.object({
|
|
21723
|
+
t: require_dist.literal("state"),
|
|
21724
|
+
state: require_dist._enum([
|
|
21725
|
+
"live",
|
|
21726
|
+
"loading",
|
|
21727
|
+
"playing",
|
|
21728
|
+
"paused",
|
|
21729
|
+
"gap",
|
|
21730
|
+
"ended",
|
|
21731
|
+
"error"
|
|
21732
|
+
])
|
|
21733
|
+
}),
|
|
21734
|
+
require_dist.object({
|
|
21735
|
+
t: require_dist.literal("position"),
|
|
21736
|
+
epochMs: require_dist.number()
|
|
21737
|
+
}),
|
|
21738
|
+
require_dist.object({
|
|
21739
|
+
t: require_dist.literal("liveProfile"),
|
|
21740
|
+
profile: require_dist._enum([
|
|
21741
|
+
"high",
|
|
21742
|
+
"mid",
|
|
21743
|
+
"low"
|
|
21744
|
+
])
|
|
21745
|
+
})
|
|
21746
|
+
]);
|
|
21736
21747
|
function parseControlMessage(raw) {
|
|
21737
21748
|
try {
|
|
21738
21749
|
const parsed = ControlMessageSchema.safeParse(JSON.parse(raw));
|
|
@@ -22071,7 +22082,7 @@ var SEEK_DISCONTINUITY_MS = 250;
|
|
|
22071
22082
|
* sustainable there — cross-fragment moves remain paced by the demux + the
|
|
22072
22083
|
* one-in-flight guard, so this cannot spawn parallel demuxes.
|
|
22073
22084
|
*/
|
|
22074
|
-
var SCRUB_COALESCE_MS =
|
|
22085
|
+
var SCRUB_COALESCE_MS = 33;
|
|
22075
22086
|
/**
|
|
22076
22087
|
* How much forward slack two fragments may have and still count as ADJACENT
|
|
22077
22088
|
* for sweep continuity. Two real cases live inside it: the ±1 ms rounding of a
|
|
@@ -22426,13 +22437,7 @@ var RecordedFeeder = class {
|
|
|
22426
22437
|
const loadStartedAt = performance.now();
|
|
22427
22438
|
let prefixFed = false;
|
|
22428
22439
|
let gopFirst = false;
|
|
22429
|
-
|
|
22430
|
-
if (this.disposed || token !== this.seekToken) return;
|
|
22431
|
-
this.feedPrefix(loc, video, landMs);
|
|
22432
|
-
prefixFed = true;
|
|
22433
|
-
firstFrameMs = performance.now() - seekStartedAt;
|
|
22434
|
-
});
|
|
22435
|
-
fullLoad.catch(() => {});
|
|
22440
|
+
let stateAnnounced = false;
|
|
22436
22441
|
if (this.deps.readGop) {
|
|
22437
22442
|
const cached = this.scrubCache.covering(this.deps.profile, landMs);
|
|
22438
22443
|
let fragment;
|
|
@@ -22454,7 +22459,18 @@ var RecordedFeeder = class {
|
|
|
22454
22459
|
prefixFed = true;
|
|
22455
22460
|
gopFirst = true;
|
|
22456
22461
|
firstFrameMs = performance.now() - seekStartedAt;
|
|
22462
|
+
this.deps.onState(this.rate === 0 ? "paused" : "playing");
|
|
22463
|
+
this.deps.onPosition(this.cursorMs);
|
|
22464
|
+
this.startPositionReports();
|
|
22465
|
+
stateAnnounced = true;
|
|
22457
22466
|
}
|
|
22467
|
+
const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
|
|
22468
|
+
if (this.disposed || token !== this.seekToken) return;
|
|
22469
|
+
this.feedPrefix(loc, video, landMs);
|
|
22470
|
+
prefixFed = true;
|
|
22471
|
+
firstFrameMs = performance.now() - seekStartedAt;
|
|
22472
|
+
});
|
|
22473
|
+
fullLoad.catch(() => {});
|
|
22458
22474
|
const loaded = await fullLoad;
|
|
22459
22475
|
loadMs = performance.now() - loadStartedAt;
|
|
22460
22476
|
if (this.disposed || token !== this.seekToken || loaded === null) return;
|
|
@@ -22478,15 +22494,19 @@ var RecordedFeeder = class {
|
|
|
22478
22494
|
}
|
|
22479
22495
|
}
|
|
22480
22496
|
if (this.rate === 0) {
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22497
|
+
if (!stateAnnounced) {
|
|
22498
|
+
this.deps.onState("paused");
|
|
22499
|
+
this.deps.onPosition(this.cursorMs);
|
|
22500
|
+
this.startPositionReports();
|
|
22501
|
+
}
|
|
22484
22502
|
this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "paused");
|
|
22485
22503
|
return;
|
|
22486
22504
|
}
|
|
22487
|
-
|
|
22488
|
-
|
|
22489
|
-
|
|
22505
|
+
if (!stateAnnounced) {
|
|
22506
|
+
this.deps.onState("playing");
|
|
22507
|
+
this.deps.onPosition(this.cursorMs);
|
|
22508
|
+
this.startPositionReports();
|
|
22509
|
+
}
|
|
22490
22510
|
this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "playing");
|
|
22491
22511
|
const firstFullAu = loaded.video[0];
|
|
22492
22512
|
const resumeAfterPtsMs = gopFirst ? firstFullAu ? firstFullAu.ptsMs + Math.max(0, this.cursorMs - loc.startMs) : null : this.lastFedVideoIdx >= 0 ? loaded.video[this.lastFedVideoIdx]?.ptsMs ?? null : null;
|
|
@@ -23599,6 +23619,12 @@ var TimelineSession = class {
|
|
|
23599
23619
|
mode = "live";
|
|
23600
23620
|
feeder = null;
|
|
23601
23621
|
profile = "mid";
|
|
23622
|
+
/** Transport rate belongs to the session, not to one profile-specific feeder.
|
|
23623
|
+
* Scrub pins `low`; release restores the selected playback profile and may
|
|
23624
|
+
* therefore rebuild the feeder. Carrying the rate keeps a paused scrub
|
|
23625
|
+
* paused across that required quality transition. */
|
|
23626
|
+
rate = 1;
|
|
23627
|
+
liveProfileReported = false;
|
|
23602
23628
|
/**
|
|
23603
23629
|
* The last position the viewer was SHOWN (every `_emitPosition`). Handed to
|
|
23604
23630
|
* `seek` as `travelFromMs` so the seek travel animation works across feeder
|
|
@@ -23622,6 +23648,7 @@ var TimelineSession = class {
|
|
|
23622
23648
|
return this.mode;
|
|
23623
23649
|
}
|
|
23624
23650
|
async handleControl(msg) {
|
|
23651
|
+
this.emitLiveProfile();
|
|
23625
23652
|
switch (msg.t) {
|
|
23626
23653
|
case "playRecorded": {
|
|
23627
23654
|
const profileChanged = msg.profile !== this.profile;
|
|
@@ -23650,6 +23677,7 @@ var TimelineSession = class {
|
|
|
23650
23677
|
if (this.mode === "recorded") this.feeder?.scrubAck();
|
|
23651
23678
|
break;
|
|
23652
23679
|
case "setRate":
|
|
23680
|
+
this.rate = msg.rate;
|
|
23653
23681
|
this.feeder?.setRate(msg.rate);
|
|
23654
23682
|
break;
|
|
23655
23683
|
case "stepFrame":
|
|
@@ -23674,6 +23702,7 @@ var TimelineSession = class {
|
|
|
23674
23702
|
this.deps.session.setLiveFeedGate(true);
|
|
23675
23703
|
this.mode = "recorded";
|
|
23676
23704
|
const feeder = this.deps.makeFeeder(this.profile);
|
|
23705
|
+
if (this.rate !== 1) feeder.setRate(this.rate);
|
|
23677
23706
|
this.feeder = feeder;
|
|
23678
23707
|
return feeder;
|
|
23679
23708
|
}
|
|
@@ -23701,6 +23730,14 @@ var TimelineSession = class {
|
|
|
23701
23730
|
epochMs
|
|
23702
23731
|
}));
|
|
23703
23732
|
}
|
|
23733
|
+
emitLiveProfile() {
|
|
23734
|
+
if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
|
|
23735
|
+
this.liveProfileReported = true;
|
|
23736
|
+
this.deps.session.sendControl(serializeServerMessage({
|
|
23737
|
+
t: "liveProfile",
|
|
23738
|
+
profile: this.deps.initialLiveProfile
|
|
23739
|
+
}));
|
|
23740
|
+
}
|
|
23704
23741
|
dispose() {
|
|
23705
23742
|
this.feeder?.dispose();
|
|
23706
23743
|
this.feeder = null;
|
|
@@ -31360,8 +31397,8 @@ var ADAPTIVE_CONFIG = {
|
|
|
31360
31397
|
analyzeWindowMs: 15e3,
|
|
31361
31398
|
minFailPercent: .05,
|
|
31362
31399
|
waitAfterResetMs: 8e3,
|
|
31363
|
-
upgradeHealthyMs:
|
|
31364
|
-
minTimeBetweenChangesMs:
|
|
31400
|
+
upgradeHealthyMs: 2e4,
|
|
31401
|
+
minTimeBetweenChangesMs: 2e4,
|
|
31365
31402
|
maxUpgradeBackoffMs: 9e5
|
|
31366
31403
|
};
|
|
31367
31404
|
/**
|
|
@@ -31529,8 +31566,7 @@ function scoreBroker(tier, broker, hints) {
|
|
|
31529
31566
|
const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
|
|
31530
31567
|
const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
|
|
31531
31568
|
const srcPixels = fallback?.pixels ?? 1920 * 1080;
|
|
31532
|
-
const
|
|
31533
|
-
const targetPixels = (hints.viewportWidth ?? 1920) * dpr * ((hints.viewportHeight ?? 1080) * dpr);
|
|
31569
|
+
const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
|
|
31534
31570
|
const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
|
|
31535
31571
|
let bandwidthPenalty = 0;
|
|
31536
31572
|
if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
|
|
@@ -32275,6 +32311,7 @@ var BrokerWebrtcServer = class {
|
|
|
32275
32311
|
}
|
|
32276
32312
|
});
|
|
32277
32313
|
const scrubCache = new ScrubFragmentCache();
|
|
32314
|
+
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
32278
32315
|
const readGopAccessor = this.readRecordedGopBytes;
|
|
32279
32316
|
const readGop = readGopAccessor ? async (args) => {
|
|
32280
32317
|
const res = await readGopAccessor({
|
|
@@ -32294,6 +32331,7 @@ var BrokerWebrtcServer = class {
|
|
|
32294
32331
|
timeline = new TimelineSession({
|
|
32295
32332
|
session,
|
|
32296
32333
|
deviceId,
|
|
32334
|
+
...initialLiveProfile !== null ? { initialLiveProfile } : {},
|
|
32297
32335
|
warm: readGop ? (profile, epochMs) => {
|
|
32298
32336
|
warmFragment({
|
|
32299
32337
|
deviceId,
|
|
@@ -32522,9 +32560,9 @@ var BrokerWebrtcServer = class {
|
|
|
32522
32560
|
* server.
|
|
32523
32561
|
*
|
|
32524
32562
|
* The client-loss % is turned into a monotonic {@link LossSample} LOCALLY:
|
|
32525
|
-
*
|
|
32526
|
-
*
|
|
32527
|
-
*
|
|
32563
|
+
* per-controller cumulative sequence/loss counters advance for every
|
|
32564
|
+
* reported interval (including 0% loss), so a perfectly healthy LAN is real
|
|
32565
|
+
* evidence rather than silence, exactly like RTCP's cumulative fields.
|
|
32528
32566
|
* Both paths feed the same controller; the tracker naturally honours
|
|
32529
32567
|
* whichever produces the higher loss ratio across its window. The async
|
|
32530
32568
|
* poll is guarded against overlapping ticks via `tickInFlight`.
|
|
@@ -32544,7 +32582,8 @@ var BrokerWebrtcServer = class {
|
|
|
32544
32582
|
});
|
|
32545
32583
|
entry.session.onReceiverReport((s) => controller.onLoss(s, Date.now()));
|
|
32546
32584
|
entry.adaptiveController = controller;
|
|
32547
|
-
let
|
|
32585
|
+
let clientHighestSequence = 0;
|
|
32586
|
+
let clientPacketsLost = 0;
|
|
32548
32587
|
let tickInFlight = false;
|
|
32549
32588
|
const CLIENT_LOSS_SPAN = 1e3;
|
|
32550
32589
|
entry.adaptiveTimer = setInterval(() => {
|
|
@@ -32558,11 +32597,12 @@ var BrokerWebrtcServer = class {
|
|
|
32558
32597
|
}
|
|
32559
32598
|
const pct = this.clientLossPct ? await this.clientLossPct(deviceId) : null;
|
|
32560
32599
|
const now = Date.now();
|
|
32561
|
-
if (pct !== null && pct !== void 0
|
|
32562
|
-
|
|
32600
|
+
if (pct !== null && pct !== void 0) {
|
|
32601
|
+
clientHighestSequence += CLIENT_LOSS_SPAN;
|
|
32602
|
+
clientPacketsLost += Math.round(Math.max(0, Math.min(100, pct)) / 100 * CLIENT_LOSS_SPAN);
|
|
32563
32603
|
controller.onLoss({
|
|
32564
|
-
packetsLost:
|
|
32565
|
-
highestSequence:
|
|
32604
|
+
packetsLost: clientPacketsLost,
|
|
32605
|
+
highestSequence: clientHighestSequence
|
|
32566
32606
|
}, now);
|
|
32567
32607
|
}
|
|
32568
32608
|
controller.tick(now);
|
|
@@ -21712,21 +21712,32 @@ function clampPlaybackRate(rate) {
|
|
|
21712
21712
|
if (!Number.isFinite(rate) || rate <= 0) return 0;
|
|
21713
21713
|
return Math.min(4, Math.max(.25, rate));
|
|
21714
21714
|
}
|
|
21715
|
-
var ServerMessageSchema = discriminatedUnion("t", [
|
|
21716
|
-
|
|
21717
|
-
|
|
21718
|
-
|
|
21719
|
-
|
|
21720
|
-
|
|
21721
|
-
|
|
21722
|
-
|
|
21723
|
-
|
|
21724
|
-
|
|
21725
|
-
|
|
21726
|
-
|
|
21727
|
-
|
|
21728
|
-
|
|
21729
|
-
|
|
21715
|
+
var ServerMessageSchema = discriminatedUnion("t", [
|
|
21716
|
+
object({
|
|
21717
|
+
t: literal("state"),
|
|
21718
|
+
state: _enum([
|
|
21719
|
+
"live",
|
|
21720
|
+
"loading",
|
|
21721
|
+
"playing",
|
|
21722
|
+
"paused",
|
|
21723
|
+
"gap",
|
|
21724
|
+
"ended",
|
|
21725
|
+
"error"
|
|
21726
|
+
])
|
|
21727
|
+
}),
|
|
21728
|
+
object({
|
|
21729
|
+
t: literal("position"),
|
|
21730
|
+
epochMs: number()
|
|
21731
|
+
}),
|
|
21732
|
+
object({
|
|
21733
|
+
t: literal("liveProfile"),
|
|
21734
|
+
profile: _enum([
|
|
21735
|
+
"high",
|
|
21736
|
+
"mid",
|
|
21737
|
+
"low"
|
|
21738
|
+
])
|
|
21739
|
+
})
|
|
21740
|
+
]);
|
|
21730
21741
|
function parseControlMessage(raw) {
|
|
21731
21742
|
try {
|
|
21732
21743
|
const parsed = ControlMessageSchema.safeParse(JSON.parse(raw));
|
|
@@ -22065,7 +22076,7 @@ var SEEK_DISCONTINUITY_MS = 250;
|
|
|
22065
22076
|
* sustainable there — cross-fragment moves remain paced by the demux + the
|
|
22066
22077
|
* one-in-flight guard, so this cannot spawn parallel demuxes.
|
|
22067
22078
|
*/
|
|
22068
|
-
var SCRUB_COALESCE_MS =
|
|
22079
|
+
var SCRUB_COALESCE_MS = 33;
|
|
22069
22080
|
/**
|
|
22070
22081
|
* How much forward slack two fragments may have and still count as ADJACENT
|
|
22071
22082
|
* for sweep continuity. Two real cases live inside it: the ±1 ms rounding of a
|
|
@@ -22420,13 +22431,7 @@ var RecordedFeeder = class {
|
|
|
22420
22431
|
const loadStartedAt = performance.now();
|
|
22421
22432
|
let prefixFed = false;
|
|
22422
22433
|
let gopFirst = false;
|
|
22423
|
-
|
|
22424
|
-
if (this.disposed || token !== this.seekToken) return;
|
|
22425
|
-
this.feedPrefix(loc, video, landMs);
|
|
22426
|
-
prefixFed = true;
|
|
22427
|
-
firstFrameMs = performance.now() - seekStartedAt;
|
|
22428
|
-
});
|
|
22429
|
-
fullLoad.catch(() => {});
|
|
22434
|
+
let stateAnnounced = false;
|
|
22430
22435
|
if (this.deps.readGop) {
|
|
22431
22436
|
const cached = this.scrubCache.covering(this.deps.profile, landMs);
|
|
22432
22437
|
let fragment;
|
|
@@ -22448,7 +22453,18 @@ var RecordedFeeder = class {
|
|
|
22448
22453
|
prefixFed = true;
|
|
22449
22454
|
gopFirst = true;
|
|
22450
22455
|
firstFrameMs = performance.now() - seekStartedAt;
|
|
22456
|
+
this.deps.onState(this.rate === 0 ? "paused" : "playing");
|
|
22457
|
+
this.deps.onPosition(this.cursorMs);
|
|
22458
|
+
this.startPositionReports();
|
|
22459
|
+
stateAnnounced = true;
|
|
22451
22460
|
}
|
|
22461
|
+
const fullLoad = this.loadAus(loc.startMs, this.rate !== 0, this.deps.readGop ? void 0 : (video) => {
|
|
22462
|
+
if (this.disposed || token !== this.seekToken) return;
|
|
22463
|
+
this.feedPrefix(loc, video, landMs);
|
|
22464
|
+
prefixFed = true;
|
|
22465
|
+
firstFrameMs = performance.now() - seekStartedAt;
|
|
22466
|
+
});
|
|
22467
|
+
fullLoad.catch(() => {});
|
|
22452
22468
|
const loaded = await fullLoad;
|
|
22453
22469
|
loadMs = performance.now() - loadStartedAt;
|
|
22454
22470
|
if (this.disposed || token !== this.seekToken || loaded === null) return;
|
|
@@ -22472,15 +22488,19 @@ var RecordedFeeder = class {
|
|
|
22472
22488
|
}
|
|
22473
22489
|
}
|
|
22474
22490
|
if (this.rate === 0) {
|
|
22475
|
-
|
|
22476
|
-
|
|
22477
|
-
|
|
22491
|
+
if (!stateAnnounced) {
|
|
22492
|
+
this.deps.onState("paused");
|
|
22493
|
+
this.deps.onPosition(this.cursorMs);
|
|
22494
|
+
this.startPositionReports();
|
|
22495
|
+
}
|
|
22478
22496
|
this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "paused");
|
|
22479
22497
|
return;
|
|
22480
22498
|
}
|
|
22481
|
-
|
|
22482
|
-
|
|
22483
|
-
|
|
22499
|
+
if (!stateAnnounced) {
|
|
22500
|
+
this.deps.onState("playing");
|
|
22501
|
+
this.deps.onPosition(this.cursorMs);
|
|
22502
|
+
this.startPositionReports();
|
|
22503
|
+
}
|
|
22484
22504
|
this.logSeekReady(epochMs, seekStartedAt, locateMs, travelMs, loadMs, firstFrameMs, "playing");
|
|
22485
22505
|
const firstFullAu = loaded.video[0];
|
|
22486
22506
|
const resumeAfterPtsMs = gopFirst ? firstFullAu ? firstFullAu.ptsMs + Math.max(0, this.cursorMs - loc.startMs) : null : this.lastFedVideoIdx >= 0 ? loaded.video[this.lastFedVideoIdx]?.ptsMs ?? null : null;
|
|
@@ -23593,6 +23613,12 @@ var TimelineSession = class {
|
|
|
23593
23613
|
mode = "live";
|
|
23594
23614
|
feeder = null;
|
|
23595
23615
|
profile = "mid";
|
|
23616
|
+
/** Transport rate belongs to the session, not to one profile-specific feeder.
|
|
23617
|
+
* Scrub pins `low`; release restores the selected playback profile and may
|
|
23618
|
+
* therefore rebuild the feeder. Carrying the rate keeps a paused scrub
|
|
23619
|
+
* paused across that required quality transition. */
|
|
23620
|
+
rate = 1;
|
|
23621
|
+
liveProfileReported = false;
|
|
23596
23622
|
/**
|
|
23597
23623
|
* The last position the viewer was SHOWN (every `_emitPosition`). Handed to
|
|
23598
23624
|
* `seek` as `travelFromMs` so the seek travel animation works across feeder
|
|
@@ -23616,6 +23642,7 @@ var TimelineSession = class {
|
|
|
23616
23642
|
return this.mode;
|
|
23617
23643
|
}
|
|
23618
23644
|
async handleControl(msg) {
|
|
23645
|
+
this.emitLiveProfile();
|
|
23619
23646
|
switch (msg.t) {
|
|
23620
23647
|
case "playRecorded": {
|
|
23621
23648
|
const profileChanged = msg.profile !== this.profile;
|
|
@@ -23644,6 +23671,7 @@ var TimelineSession = class {
|
|
|
23644
23671
|
if (this.mode === "recorded") this.feeder?.scrubAck();
|
|
23645
23672
|
break;
|
|
23646
23673
|
case "setRate":
|
|
23674
|
+
this.rate = msg.rate;
|
|
23647
23675
|
this.feeder?.setRate(msg.rate);
|
|
23648
23676
|
break;
|
|
23649
23677
|
case "stepFrame":
|
|
@@ -23668,6 +23696,7 @@ var TimelineSession = class {
|
|
|
23668
23696
|
this.deps.session.setLiveFeedGate(true);
|
|
23669
23697
|
this.mode = "recorded";
|
|
23670
23698
|
const feeder = this.deps.makeFeeder(this.profile);
|
|
23699
|
+
if (this.rate !== 1) feeder.setRate(this.rate);
|
|
23671
23700
|
this.feeder = feeder;
|
|
23672
23701
|
return feeder;
|
|
23673
23702
|
}
|
|
@@ -23695,6 +23724,14 @@ var TimelineSession = class {
|
|
|
23695
23724
|
epochMs
|
|
23696
23725
|
}));
|
|
23697
23726
|
}
|
|
23727
|
+
emitLiveProfile() {
|
|
23728
|
+
if (this.liveProfileReported || this.deps.initialLiveProfile === void 0) return;
|
|
23729
|
+
this.liveProfileReported = true;
|
|
23730
|
+
this.deps.session.sendControl(serializeServerMessage({
|
|
23731
|
+
t: "liveProfile",
|
|
23732
|
+
profile: this.deps.initialLiveProfile
|
|
23733
|
+
}));
|
|
23734
|
+
}
|
|
23698
23735
|
dispose() {
|
|
23699
23736
|
this.feeder?.dispose();
|
|
23700
23737
|
this.feeder = null;
|
|
@@ -31351,8 +31388,8 @@ var ADAPTIVE_CONFIG = {
|
|
|
31351
31388
|
analyzeWindowMs: 15e3,
|
|
31352
31389
|
minFailPercent: .05,
|
|
31353
31390
|
waitAfterResetMs: 8e3,
|
|
31354
|
-
upgradeHealthyMs:
|
|
31355
|
-
minTimeBetweenChangesMs:
|
|
31391
|
+
upgradeHealthyMs: 2e4,
|
|
31392
|
+
minTimeBetweenChangesMs: 2e4,
|
|
31356
31393
|
maxUpgradeBackoffMs: 9e5
|
|
31357
31394
|
};
|
|
31358
31395
|
/**
|
|
@@ -31520,8 +31557,7 @@ function scoreBroker(tier, broker, hints) {
|
|
|
31520
31557
|
const fallback = tier ? LABEL_DEFAULTS[tier] : void 0;
|
|
31521
31558
|
const bitrateKbps = stats.bitrateKbps > 0 ? stats.bitrateKbps : fallback?.bitrateKbps ?? 2e3;
|
|
31522
31559
|
const srcPixels = fallback?.pixels ?? 1920 * 1080;
|
|
31523
|
-
const
|
|
31524
|
-
const targetPixels = (hints.viewportWidth ?? 1920) * dpr * ((hints.viewportHeight ?? 1080) * dpr);
|
|
31560
|
+
const targetPixels = (hints.viewportWidth ?? 1920) * (hints.viewportHeight ?? 1080);
|
|
31525
31561
|
const pixelDistance = Math.abs(srcPixels - targetPixels) / 1e6;
|
|
31526
31562
|
let bandwidthPenalty = 0;
|
|
31527
31563
|
if (hints.downlinkMbps && hints.downlinkMbps > 0 && bitrateKbps > 0) {
|
|
@@ -32266,6 +32302,7 @@ var BrokerWebrtcServer = class {
|
|
|
32266
32302
|
}
|
|
32267
32303
|
});
|
|
32268
32304
|
const scrubCache = new ScrubFragmentCache();
|
|
32305
|
+
const initialLiveProfile = this.profileTierResolver?.(brokerId) ?? null;
|
|
32269
32306
|
const readGopAccessor = this.readRecordedGopBytes;
|
|
32270
32307
|
const readGop = readGopAccessor ? async (args) => {
|
|
32271
32308
|
const res = await readGopAccessor({
|
|
@@ -32285,6 +32322,7 @@ var BrokerWebrtcServer = class {
|
|
|
32285
32322
|
timeline = new TimelineSession({
|
|
32286
32323
|
session,
|
|
32287
32324
|
deviceId,
|
|
32325
|
+
...initialLiveProfile !== null ? { initialLiveProfile } : {},
|
|
32288
32326
|
warm: readGop ? (profile, epochMs) => {
|
|
32289
32327
|
warmFragment({
|
|
32290
32328
|
deviceId,
|
|
@@ -32513,9 +32551,9 @@ var BrokerWebrtcServer = class {
|
|
|
32513
32551
|
* server.
|
|
32514
32552
|
*
|
|
32515
32553
|
* The client-loss % is turned into a monotonic {@link LossSample} LOCALLY:
|
|
32516
|
-
*
|
|
32517
|
-
*
|
|
32518
|
-
*
|
|
32554
|
+
* per-controller cumulative sequence/loss counters advance for every
|
|
32555
|
+
* reported interval (including 0% loss), so a perfectly healthy LAN is real
|
|
32556
|
+
* evidence rather than silence, exactly like RTCP's cumulative fields.
|
|
32519
32557
|
* Both paths feed the same controller; the tracker naturally honours
|
|
32520
32558
|
* whichever produces the higher loss ratio across its window. The async
|
|
32521
32559
|
* poll is guarded against overlapping ticks via `tickInFlight`.
|
|
@@ -32535,7 +32573,8 @@ var BrokerWebrtcServer = class {
|
|
|
32535
32573
|
});
|
|
32536
32574
|
entry.session.onReceiverReport((s) => controller.onLoss(s, Date.now()));
|
|
32537
32575
|
entry.adaptiveController = controller;
|
|
32538
|
-
let
|
|
32576
|
+
let clientHighestSequence = 0;
|
|
32577
|
+
let clientPacketsLost = 0;
|
|
32539
32578
|
let tickInFlight = false;
|
|
32540
32579
|
const CLIENT_LOSS_SPAN = 1e3;
|
|
32541
32580
|
entry.adaptiveTimer = setInterval(() => {
|
|
@@ -32549,11 +32588,12 @@ var BrokerWebrtcServer = class {
|
|
|
32549
32588
|
}
|
|
32550
32589
|
const pct = this.clientLossPct ? await this.clientLossPct(deviceId) : null;
|
|
32551
32590
|
const now = Date.now();
|
|
32552
|
-
if (pct !== null && pct !== void 0
|
|
32553
|
-
|
|
32591
|
+
if (pct !== null && pct !== void 0) {
|
|
32592
|
+
clientHighestSequence += CLIENT_LOSS_SPAN;
|
|
32593
|
+
clientPacketsLost += Math.round(Math.max(0, Math.min(100, pct)) / 100 * CLIENT_LOSS_SPAN);
|
|
32554
32594
|
controller.onLoss({
|
|
32555
|
-
packetsLost:
|
|
32556
|
-
highestSequence:
|
|
32595
|
+
packetsLost: clientPacketsLost,
|
|
32596
|
+
highestSequence: clientHighestSequence
|
|
32557
32597
|
}, now);
|
|
32558
32598
|
}
|
|
32559
32599
|
controller.tick(now);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-
|
|
1
|
+
import{d as e,f as t,g as n,h as r,m as i,p as a}from"./index-Dcfe-5Ig.js";var o=n(t()),s=n(e(),1),c=Math.PI/180;function l(){return typeof window<`u`&&({}.toString.call(window)===`[object Window]`||{}.toString.call(window)===`[object global]`)}var u=typeof global<`u`?global:typeof window<`u`?window:typeof WorkerGlobalScope<`u`?self:{},d={_global:u,version:`10.3.0`,isBrowser:l(),isUnminified:/param/.test(function(e){}.toString()),dblClickWindow:400,getAngle(e){return d.angleDeg?e*c:e},enableTrace:!1,pointerEventsEnabled:!0,autoDrawEnabled:!0,hitOnDragEnabled:!1,capturePointerEventsEnabled:!1,_mouseListenClick:!1,_touchListenClick:!1,_pointerListenClick:!1,_mouseInDblClickWindow:!1,_touchInDblClickWindow:!1,_pointerInDblClickWindow:!1,_mouseDblClickPointerId:null,_touchDblClickPointerId:null,_pointerDblClickPointerId:null,_renderBackend:`web`,legacyTextRendering:!1,pixelRatio:typeof window<`u`&&window.devicePixelRatio||1,dragDistance:3,angleDeg:!0,showWarnings:!0,dragButtons:[0,1],isDragging(){return d.DD.isDragging},isTransforming(){return d.Transformer?.isTransforming()??!1},isDragReady(){return!!d.DD.node},releaseCanvasOnDestroy:!0,document:u.document,_injectGlobal(e){u.Konva!==void 0&&console.error(`Several Konva instances detected. It is not recommended to use multiple Konva instances in the same environment.`),u.Konva=e}},f=e=>{d[e.prototype.getClassName()]=e};d._injectGlobal(d);var p=`Konva.js unsupported environment.
|
|
2
2
|
|
|
3
3
|
Looks like you are trying to use Konva.js in Node.js environment. because "document" object is undefined.
|
|
4
4
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,c as t,d as n,f as r,g as i,l as a,o,r as s,s as c,u as l}from"./index-
|
|
1
|
+
import{a as e,c as t,d as n,f as r,g as i,l as a,o,r as s,s as c,u as l}from"./index-Dcfe-5Ig.js";import{MaskShapeCanvas as u}from"./MaskShapeCanvas-DI4BY7W2-BWAtF7b-.js";var d=i(r(),1),f=i(n(),1),p=o(`grid-2x2`,[[`path`,{d:`M12 3v18`,key:`108xh3`}],[`path`,{d:`M3 12h18`,key:`1i2n21`}],[`rect`,{x:`3`,y:`3`,width:`18`,height:`18`,rx:`2`,key:`h1oib`}]]),m=110,h=`motion-zones`,g=0,_=[1,2,3],v=1,y=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,b=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function x(e,t,n){let r=t*n,i=Array.from({length:r});for(let t=0;t<r;t+=1)i[t]=e[t]===!0;return i}function S(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(e[n]!==t[n])return!1;return!0}function C(e,t){return Math.ceil(e/t)}function w(e,t,n){return Math.min(n-1,Math.floor((e+.5)/t*n))}function T(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:i*a},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)e[r*t+n]===!0&&(o[s*i+w(n,t,i)]=!0)}return o}function E(e,t,n,r){let i=C(t,r),a=C(n,r),o=Array.from({length:t*n},()=>!1);for(let r=0;r<n;r+=1){let s=w(r,n,a);for(let n=0;n<t;n+=1)o[r*t+n]=e[s*i+w(n,t,i)]===!0}return o}function D({deviceId:n}){let r=t(l().trpcClient,n),[i,o]=(0,d.useState)(null),[w,D]=(0,d.useState)(!1),[O,k]=(0,d.useState)(null),[A,j]=(0,d.useState)(null),[M,N]=(0,d.useState)(v),[P,F]=(0,d.useState)(!1),[I,L]=(0,d.useState)(!1),R=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(!r)return;let e=!1;return R.current=!1,o(null),D(!1),k(null),j(null),N(v),L(!1),(async()=>{try{let t=await r.motionZones?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(o(t),R.current)return;let n=await r.motionZones?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);R.current=!0;let i=n.regions.find(e=>e.shape.kind===`grid`),a=x(i?i.shape.cells:[],t.grid.width,t.grid.height);j(a),k(T(a,t.grid.width,t.grid.height,v))}catch(t){if(e)return;c(t)?D(!0):console.error(`Motion Zones load failed`,t)}})(),()=>{e=!0}},[r]);let z=i?i.grid.width:0,B=i?i.grid.height:0,V=C(z,M),H=C(B,M),U=(0,d.useMemo)(()=>O&&i?E(O,z,B,M):null,[O,i,z,B,M]),W=(0,d.useMemo)(()=>U!==null&&A!==null&&!S(U,A),[U,A]),G=(0,d.useMemo)(()=>U?U.reduce((e,t)=>t?e+1:e,0):0,[U]),K=z*B,q=V*H,J=(0,d.useCallback)(e=>{q>0&&k(Array.from({length:q},()=>e))},[q]),Y=(0,d.useCallback)(()=>{k(e=>e&&e.map(e=>!e))},[]),X=(0,d.useCallback)(()=>{A&&i&&k(T(A,z,B,M))},[A,i,z,B,M]),Z=(0,d.useCallback)(e=>{e===M||!i||k(t=>{if(!t)return N(e),t;let n=T(E(t,z,B,M),z,B,e);return N(e),n})},[M,i,z,B]),Q=(0,d.useMemo)(()=>i&&O?[{id:g,shape:{kind:`grid`,gridWidth:V,gridHeight:H,cells:[...O]}}]:[],[i,O,V,H]),$=(0,d.useCallback)((e,t)=>{t.kind===`grid`&&k(t.cells)},[]),ee=(0,d.useCallback)(async()=>{if(!(!r||!O||!i)){F(!0);try{let e=E(O,i.grid.width,i.grid.height,M),t={kind:`grid`,gridWidth:i.grid.width,gridHeight:i.grid.height,cells:e};await r.motionZones?.setZone({patch:{regions:[{id:g,enabled:!0,shape:t}]}});let n=await r.motionZones?.getStatus({});if(n){let e=n.regions.find(e=>e.shape.kind===`grid`),t=x(e?e.shape.cells:[],i.grid.width,i.grid.height);j(t),k(T(t,i.grid.width,i.grid.height,M))}}catch(e){console.error(`Motion Zones save failed`,e)}finally{F(!1)}}},[r,O,i,M]);a((0,d.useMemo)(()=>I&&!w&&i&&O?{id:h,order:m,node:(0,f.jsx)(u,{transparent:!0,items:Q,supportedShapes:[`grid`],grid:{width:V,height:H},selectedId:g,onSelect:()=>{},onShapeChange:$,onDrawComplete:()=>{},drawingKind:null})}:null,[I,w,i,O,Q,$,V,H]));let te=!w&&i!==null&&O!==null;return r?(0,f.jsx)(e,{title:`Motion Zones`,icon:(0,f.jsx)(p,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,f.jsx)(`div`,{className:`flex flex-col gap-3`,children:w?(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`This camera doesn't expose an on-board motion zones grid.`}):te?(0,f.jsxs)(f.Fragment,{children:[(0,f.jsxs)(`p`,{className:`${s} leading-relaxed`,children:[`Toggle `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Edit grid`}),` to paint the region directly on the live frame, then `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push the mask to the camera. Pick a bigger`,` `,(0,f.jsx)(`strong`,{className:`text-foreground`,children:`Cell size`}),` for quicker, broad-stroke painting — it's resampled to the camera's native `,i.grid.width,`×`,i.grid.height,` grid on save (×1 is the finest).`]}),(0,f.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,f.jsx)(`button`,{type:`button`,onClick:()=>L(e=>!e),disabled:P,"aria-pressed":I,className:I?b:y,children:I?`Done editing`:`Edit grid`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!0),disabled:P,className:y,children:`All on`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>J(!1),disabled:P,className:y,children:`All off`}),(0,f.jsx)(`button`,{type:`button`,onClick:Y,disabled:P,className:y,children:`Invert`}),(0,f.jsxs)(`div`,{className:`flex items-center gap-1 ml-1`,role:`group`,"aria-label":`Cell size`,children:[(0,f.jsx)(`span`,{className:`${s} mr-0.5`,children:`Cell size`}),_.map(e=>(0,f.jsxs)(`button`,{type:`button`,onClick:()=>Z(e),disabled:P,"aria-pressed":M===e,title:e===1?`Camera grid ${z}×${B} (finest)`:`${C(z,e)}×${C(B,e)} painting grid · cells ×${e} bigger`,className:M===e?b:y,children:[`×`,e]},e))]}),(0,f.jsxs)(`span`,{className:`${s} ml-1 tabular-nums`,children:[G,` / `,K,` cells · `,i.grid.width,`×`,i.grid.height,M===1?``:` · paint ${V}×${H}`]}),(0,f.jsx)(`span`,{className:`flex-1`}),(0,f.jsx)(`button`,{type:`button`,onClick:X,disabled:P||!W,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,f.jsx)(`button`,{type:`button`,onClick:()=>void ee(),disabled:P||!W,className:b,children:P?`Saving…`:`Save`})]})]}):(0,f.jsx)(`p`,{className:`${s} leading-relaxed`,children:`Loading the camera's grid…`})})}):null}export{D as MotionZonesSettings};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,c as t,d as n,f as r,g as i,i as a,l as o,n as s,o as c,r as l,s as u,t as d,u as ee}from"./index-
|
|
1
|
+
import{a as e,c as t,d as n,f as r,g as i,i as a,l as o,n as s,o as c,r as l,s as u,t as d,u as ee}from"./index-Dcfe-5Ig.js";import{MaskShapeCanvas as te}from"./MaskShapeCanvas-DI4BY7W2-BWAtF7b-.js";var f=i(r(),1),p=i(n(),1),m=c(`hexagon`,[[`path`,{d:`M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z`,key:`yt0hxn`}]]),h=120,g=`privacy-mask`,_=`rounded-md border border-border bg-surface px-2 py-1 text-[11px] font-medium text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,v=`rounded-md border border-primary/50 bg-primary/15 px-2.5 py-1 text-[11px] font-medium text-primary hover:bg-primary/25 disabled:opacity-40 transition-colors`;function y(e){return e.kind===`rect`||e.kind===`polygon`?e:null}function b(e){let t=new Set(e.map(e=>e.id)),n=0;for(;t.has(n);)n+=1;return n}function x(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1)if(JSON.stringify(e[n])!==JSON.stringify(t[n]))return!1;return!0}function S({deviceId:n}){let r=t(ee().trpcClient,n),[i,c]=(0,f.useState)(null),[S,C]=(0,f.useState)(!1),[w,T]=(0,f.useState)(null),[E,D]=(0,f.useState)(null),[O,k]=(0,f.useState)(!1),[A,j]=(0,f.useState)(!1),[M,N]=(0,f.useState)(null),[P,F]=(0,f.useState)(null),I=(0,f.useRef)(!1);(0,f.useEffect)(()=>{if(!r)return;let e=!1;return I.current=!1,c(null),C(!1),T(null),D(null),j(!1),N(null),F(null),(async()=>{try{let t=await r.privacyMask?.getOptions({});if(e)return;if(!t)throw Error(`device proxy not ready`);if(c(t),I.current)return;let n=await r.privacyMask?.getStatus({});if(e)return;if(!n)throw Error(`device proxy not ready`);I.current=!0;let i={enabled:n.enabled,regions:n.regions};D(i),T(i)}catch(t){if(e)return;u(t)?C(!0):console.error(`Privacy Mask load failed`,t)}})(),()=>{e=!0}},[r]);let L=(0,f.useMemo)(()=>w!==null&&E!==null&&(w.enabled!==E.enabled||!x(w.regions,E.regions)),[w,E]),R=w?w.regions.length:0,z=i?i.maxRegions:0,B=(0,f.useRef)(0);(0,f.useEffect)(()=>{B.current=z},[z]);let V=z>0&&R>=z,H=(0,f.useCallback)(()=>{T(e=>e&&{...e,enabled:!e.enabled})},[]),U=(0,f.useCallback)(e=>{N(typeof e==`number`?e:null)},[]),W=(0,f.useMemo)(()=>w?w.regions.map(e=>({id:e.id,shape:e.shape,enabled:e.enabled,label:`Zone ${String(e.id)}`})):[],[w]),G=(0,f.useCallback)((e,t)=>{let n=y(t);n&&T(t=>t&&{...t,regions:t.regions.map(t=>t.id===e?{...t,shape:n}:t)})},[]),K=(0,f.useCallback)(e=>{let t=y(e);t&&(F(null),T(e=>{if(!e)return e;let n=B.current;if(n>0&&e.regions.length>=n)return e;let r=b(e.regions),i={id:r,enabled:!0,shape:t};return N(r),{...e,regions:[...e.regions,i]}}))},[]),q=(0,f.useCallback)(e=>{j(!0),N(null),F(e)},[]),J=(0,f.useCallback)(e=>{j(!0),F(null),N(e)},[]),Y=(0,f.useCallback)(e=>{N(t=>t===e?null:t),T(t=>t&&{...t,regions:t.regions.filter(t=>t.id!==e)})},[]),X=(0,f.useCallback)(()=>{F(null),N(null),E&&T({enabled:E.enabled,regions:E.regions})},[E]),Z=(0,f.useCallback)(async()=>{if(!(!r||!w)){k(!0);try{await r.privacyMask?.setMask({patch:{enabled:w.enabled,regions:[...w.regions]}});let e=await r.privacyMask?.getStatus({});if(e){let t={enabled:e.enabled,regions:e.regions};D(t),T(t)}}catch(e){console.error(`Privacy Mask save failed`,e)}finally{k(!1)}}},[r,w]),ne=(0,f.useCallback)(()=>{j(e=>(e&&(F(null),N(null)),!e))},[]),Q=i?.supportedShapes??[];o((0,f.useMemo)(()=>A&&!S&&i&&w?{id:g,order:h,node:(0,p.jsx)(te,{transparent:!0,items:W,supportedShapes:Q,polygonVertices:i.polygonVertices,selectedId:M,onSelect:U,onShapeChange:G,onDrawComplete:K,drawingKind:P})}:null,[A,S,i,w,W,Q,M,U,G,K,P]));let re=i?.supportedShapes.includes(`rect`)??!1,ie=i?.supportedShapes.includes(`polygon`)??!1,$=i!==null&&(i.maxRegions<=0||i.supportedShapes.length===0),ae=!S&&!$&&i!==null&&w!==null;return r?(0,p.jsx)(e,{title:`Privacy Mask`,icon:(0,p.jsx)(d,{className:`h-3.5 w-3.5 text-foreground-subtle`}),children:(0,p.jsx)(`div`,{className:`flex flex-col gap-3`,children:S||$?(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`This camera doesn't support an on-board privacy mask.`}):ae?(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(`p`,{className:`${l} leading-relaxed`,children:[`Toggle `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Edit mask`}),` to draw blanked-out zones on the live frame, then `,(0,p.jsx)(`strong`,{className:`text-foreground`,children:`Save`}),` to push them to the camera. Drag a rectangle to move, its corner to resize; drag polygon vertices, click an edge midpoint to add one, or right-click a vertex to remove it.`]}),(0,p.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap`,children:[(0,p.jsx)(`button`,{type:`button`,onClick:ne,disabled:O,"aria-pressed":A,className:A?v:_,children:A?`Done editing`:`Edit mask`}),(0,p.jsx)(`button`,{type:`button`,onClick:H,disabled:O,"aria-pressed":w.enabled,className:w.enabled?v:_,children:w.enabled?`Mask on`:`Mask off`}),re&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`rect`),disabled:O||V,"aria-pressed":P===`rect`,className:P===`rect`?v:_,title:V?`Maximum zones reached`:`Add a rectangle zone`,children:`+ Rect`}),ie&&(0,p.jsx)(`button`,{type:`button`,onClick:()=>q(`polygon`),disabled:O||V,"aria-pressed":P===`polygon`,className:P===`polygon`?v:_,title:V?`Maximum zones reached`:`Add a polygon zone`,children:`+ Polygon`}),(0,p.jsxs)(`span`,{className:`${l} ml-1 tabular-nums`,children:[R,` / `,z,` zones`]}),(0,p.jsx)(`span`,{className:`flex-1`}),(0,p.jsx)(`button`,{type:`button`,onClick:X,disabled:O||!L,className:`rounded-md border border-border bg-surface px-2 py-1 text-[11px] text-foreground-subtle hover:bg-surface-hover disabled:opacity-40 transition-colors`,children:`Revert`}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>void Z(),disabled:O||!L,className:v,children:O?`Saving…`:`Save`})]}),R>0?(0,p.jsx)(`div`,{className:`flex flex-col gap-1`,children:w.regions.map(e=>{let t=M===e.id,n=e.shape.kind===`polygon`?m:s;return(0,p.jsxs)(`div`,{className:`flex items-center gap-2 rounded-md border px-2 py-1 transition-colors ${t?`border-primary/50 bg-primary/10`:`border-border bg-surface`}`,children:[(0,p.jsxs)(`button`,{type:`button`,onClick:()=>J(e.id),disabled:O,className:`flex flex-1 items-center gap-2 text-left text-[11px] font-medium text-foreground-subtle hover:text-foreground disabled:opacity-40 transition-colors`,children:[(0,p.jsx)(n,{className:`h-3.5 w-3.5 shrink-0`}),(0,p.jsxs)(`span`,{children:[`Zone `,e.id]}),(0,p.jsx)(`span`,{className:`text-foreground-faint capitalize`,children:e.shape.kind})]}),(0,p.jsx)(`button`,{type:`button`,onClick:()=>Y(e.id),disabled:O,"aria-label":`Delete zone ${String(e.id)}`,title:`Delete zone`,className:`inline-flex h-6 w-6 items-center justify-center rounded border border-border bg-surface text-foreground-subtle hover:border-red-400/40 hover:bg-red-500/10 hover:text-red-400 disabled:opacity-40 transition-colors`,children:(0,p.jsx)(a,{className:`h-3.5 w-3.5`})})]},e.id)})}):null]}):(0,p.jsx)(`p`,{className:`${l} leading-relaxed`,children:`Loading the camera's privacy mask…`})})}):null}export{S as PrivacyMaskSettings};
|