@camstack/addon-provider-reolink 1.2.155 → 1.2.159
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/addon.js +1518 -13
- package/dist/addon.mjs +1518 -13
- package/package.json +2 -2
package/dist/addon.js
CHANGED
|
@@ -5390,7 +5390,7 @@ var ZodIssueCode = {
|
|
|
5390
5390
|
var ZodFirstPartyTypeKind;
|
|
5391
5391
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5392
5392
|
//#endregion
|
|
5393
|
-
//#region ../types/dist/sleep-
|
|
5393
|
+
//#region ../types/dist/sleep-CUmLTud7.mjs
|
|
5394
5394
|
/**
|
|
5395
5395
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5396
5396
|
* window to float samples (D455).
|
|
@@ -25639,6 +25639,23 @@ var videoclipsCapability = {
|
|
|
25639
25639
|
deviceId: number(),
|
|
25640
25640
|
clipId: string().min(1),
|
|
25641
25641
|
/**
|
|
25642
|
+
* WHICH provider holds the bytes — the `addonId` a
|
|
25643
|
+
* {@link ClipSourceSchema} row carries. **Required**, for the reason
|
|
25644
|
+
* `listClips` states one method above and for a second one this method
|
|
25645
|
+
* learned the hard way.
|
|
25646
|
+
*
|
|
25647
|
+
* The caller is the stream broker, and the broker lives in
|
|
25648
|
+
* `addon-pipeline` — the same addon that registers the RECORDER's
|
|
25649
|
+
* videoclips provider. With no provider named, the in-process
|
|
25650
|
+
* resolution handed it its own sibling, which implements this method
|
|
25651
|
+
* solely to explain that an analytics visit has no file behind it. So
|
|
25652
|
+
* every onboard clip answered with that sentence, a flash of video and
|
|
25653
|
+
* nothing more, while the SAME call from outside the addon reached the
|
|
25654
|
+
* right provider and returned the bytes (measured on 3628,
|
|
25655
|
+
* 2026-09-21).
|
|
25656
|
+
*/
|
|
25657
|
+
provider: string().min(1),
|
|
25658
|
+
/**
|
|
25642
25659
|
* Which twin to fetch, on the mapping D549 15 already fixed:
|
|
25643
25660
|
* `low | mid` → the sub file, `high` → the main twin. `auto` is not
|
|
25644
25661
|
* accepted here for the same reason it is not accepted by
|
|
@@ -33385,7 +33402,7 @@ method(object({
|
|
|
33385
33402
|
auth: "admin"
|
|
33386
33403
|
}), method(object({ deviceId: number() }), RecordingConfigSchema, {
|
|
33387
33404
|
kind: "query",
|
|
33388
|
-
auth: "
|
|
33405
|
+
auth: "protected"
|
|
33389
33406
|
}), method(object({
|
|
33390
33407
|
deviceId: number(),
|
|
33391
33408
|
profile: string(),
|
|
@@ -38153,6 +38170,12 @@ function createRuntimeStateBridge(params) {
|
|
|
38153
38170
|
};
|
|
38154
38171
|
}
|
|
38155
38172
|
Object.freeze({
|
|
38173
|
+
"accessories.getStatus": {
|
|
38174
|
+
capName: "accessories",
|
|
38175
|
+
capScope: "device",
|
|
38176
|
+
addonId: null,
|
|
38177
|
+
access: "view"
|
|
38178
|
+
},
|
|
38156
38179
|
"accessories.setChildHidden": {
|
|
38157
38180
|
capName: "accessories",
|
|
38158
38181
|
capScope: "device",
|
|
@@ -38435,6 +38458,12 @@ Object.freeze({
|
|
|
38435
38458
|
addonId: null,
|
|
38436
38459
|
access: "view"
|
|
38437
38460
|
},
|
|
38461
|
+
"airQualitySensor.getStatus": {
|
|
38462
|
+
capName: "air-quality-sensor",
|
|
38463
|
+
capScope: "device",
|
|
38464
|
+
addonId: null,
|
|
38465
|
+
access: "view"
|
|
38466
|
+
},
|
|
38438
38467
|
"alarmPanel.arm": {
|
|
38439
38468
|
capName: "alarm-panel",
|
|
38440
38469
|
capScope: "device",
|
|
@@ -38447,6 +38476,12 @@ Object.freeze({
|
|
|
38447
38476
|
addonId: null,
|
|
38448
38477
|
access: "create"
|
|
38449
38478
|
},
|
|
38479
|
+
"alarmPanel.getStatus": {
|
|
38480
|
+
capName: "alarm-panel",
|
|
38481
|
+
capScope: "device",
|
|
38482
|
+
addonId: null,
|
|
38483
|
+
access: "view"
|
|
38484
|
+
},
|
|
38450
38485
|
"alarmPanel.trigger": {
|
|
38451
38486
|
capName: "alarm-panel",
|
|
38452
38487
|
capScope: "device",
|
|
@@ -38495,6 +38530,30 @@ Object.freeze({
|
|
|
38495
38530
|
addonId: null,
|
|
38496
38531
|
access: "create"
|
|
38497
38532
|
},
|
|
38533
|
+
"ambientLightSensor.getStatus": {
|
|
38534
|
+
capName: "ambient-light-sensor",
|
|
38535
|
+
capScope: "device",
|
|
38536
|
+
addonId: null,
|
|
38537
|
+
access: "view"
|
|
38538
|
+
},
|
|
38539
|
+
"audioAnalysis.applyDeviceSettingsPatch": {
|
|
38540
|
+
capName: "audio-analysis",
|
|
38541
|
+
capScope: "device",
|
|
38542
|
+
addonId: null,
|
|
38543
|
+
access: "create"
|
|
38544
|
+
},
|
|
38545
|
+
"audioAnalysis.getDeviceLiveContribution": {
|
|
38546
|
+
capName: "audio-analysis",
|
|
38547
|
+
capScope: "device",
|
|
38548
|
+
addonId: null,
|
|
38549
|
+
access: "view"
|
|
38550
|
+
},
|
|
38551
|
+
"audioAnalysis.getDeviceSettingsContribution": {
|
|
38552
|
+
capName: "audio-analysis",
|
|
38553
|
+
capScope: "device",
|
|
38554
|
+
addonId: null,
|
|
38555
|
+
access: "view"
|
|
38556
|
+
},
|
|
38498
38557
|
"audioAnalysis.resolveDeviceSettings": {
|
|
38499
38558
|
capName: "audio-analysis",
|
|
38500
38559
|
capScope: "device",
|
|
@@ -38657,6 +38716,12 @@ Object.freeze({
|
|
|
38657
38716
|
addonId: null,
|
|
38658
38717
|
access: "create"
|
|
38659
38718
|
},
|
|
38719
|
+
"automationControl.getStatus": {
|
|
38720
|
+
capName: "automation-control",
|
|
38721
|
+
capScope: "device",
|
|
38722
|
+
addonId: null,
|
|
38723
|
+
access: "view"
|
|
38724
|
+
},
|
|
38660
38725
|
"automationControl.trigger": {
|
|
38661
38726
|
capName: "automation-control",
|
|
38662
38727
|
capScope: "device",
|
|
@@ -38753,12 +38818,30 @@ Object.freeze({
|
|
|
38753
38818
|
addonId: null,
|
|
38754
38819
|
access: "create"
|
|
38755
38820
|
},
|
|
38821
|
+
"battery.getStatus": {
|
|
38822
|
+
capName: "battery",
|
|
38823
|
+
capScope: "device",
|
|
38824
|
+
addonId: null,
|
|
38825
|
+
access: "view"
|
|
38826
|
+
},
|
|
38756
38827
|
"battery.wakeForStream": {
|
|
38757
38828
|
capName: "battery",
|
|
38758
38829
|
capScope: "device",
|
|
38759
38830
|
addonId: null,
|
|
38760
38831
|
access: "create"
|
|
38761
38832
|
},
|
|
38833
|
+
"binary.getStatus": {
|
|
38834
|
+
capName: "binary",
|
|
38835
|
+
capScope: "device",
|
|
38836
|
+
addonId: null,
|
|
38837
|
+
access: "view"
|
|
38838
|
+
},
|
|
38839
|
+
"brightness.getStatus": {
|
|
38840
|
+
capName: "brightness",
|
|
38841
|
+
capScope: "device",
|
|
38842
|
+
addonId: null,
|
|
38843
|
+
access: "view"
|
|
38844
|
+
},
|
|
38762
38845
|
"brightness.setBrightness": {
|
|
38763
38846
|
capName: "brightness",
|
|
38764
38847
|
capScope: "device",
|
|
@@ -38873,6 +38956,30 @@ Object.freeze({
|
|
|
38873
38956
|
addonId: null,
|
|
38874
38957
|
access: "view"
|
|
38875
38958
|
},
|
|
38959
|
+
"cameraCredentials.getStatus": {
|
|
38960
|
+
capName: "camera-credentials",
|
|
38961
|
+
capScope: "device",
|
|
38962
|
+
addonId: null,
|
|
38963
|
+
access: "view"
|
|
38964
|
+
},
|
|
38965
|
+
"cameraGridLayout.applyDeviceSettingsPatch": {
|
|
38966
|
+
capName: "camera-grid-layout",
|
|
38967
|
+
capScope: "device",
|
|
38968
|
+
addonId: null,
|
|
38969
|
+
access: "create"
|
|
38970
|
+
},
|
|
38971
|
+
"cameraGridLayout.getDeviceLiveContribution": {
|
|
38972
|
+
capName: "camera-grid-layout",
|
|
38973
|
+
capScope: "device",
|
|
38974
|
+
addonId: null,
|
|
38975
|
+
access: "view"
|
|
38976
|
+
},
|
|
38977
|
+
"cameraGridLayout.getDeviceSettingsContribution": {
|
|
38978
|
+
capName: "camera-grid-layout",
|
|
38979
|
+
capScope: "device",
|
|
38980
|
+
addonId: null,
|
|
38981
|
+
access: "view"
|
|
38982
|
+
},
|
|
38876
38983
|
"cameraGridLayout.getLayout": {
|
|
38877
38984
|
capName: "camera-grid-layout",
|
|
38878
38985
|
capScope: "device",
|
|
@@ -38885,6 +38992,24 @@ Object.freeze({
|
|
|
38885
38992
|
addonId: null,
|
|
38886
38993
|
access: "create"
|
|
38887
38994
|
},
|
|
38995
|
+
"cameraPipelineConfig.applyDeviceSettingsPatch": {
|
|
38996
|
+
capName: "camera-pipeline-config",
|
|
38997
|
+
capScope: "device",
|
|
38998
|
+
addonId: null,
|
|
38999
|
+
access: "create"
|
|
39000
|
+
},
|
|
39001
|
+
"cameraPipelineConfig.getDeviceLiveContribution": {
|
|
39002
|
+
capName: "camera-pipeline-config",
|
|
39003
|
+
capScope: "device",
|
|
39004
|
+
addonId: null,
|
|
39005
|
+
access: "view"
|
|
39006
|
+
},
|
|
39007
|
+
"cameraPipelineConfig.getDeviceSettingsContribution": {
|
|
39008
|
+
capName: "camera-pipeline-config",
|
|
39009
|
+
capScope: "device",
|
|
39010
|
+
addonId: null,
|
|
39011
|
+
access: "view"
|
|
39012
|
+
},
|
|
38888
39013
|
"cameraStreams.getBrokerStreams": {
|
|
38889
39014
|
capName: "camera-streams",
|
|
38890
39015
|
capScope: "device",
|
|
@@ -38915,6 +39040,18 @@ Object.freeze({
|
|
|
38915
39040
|
addonId: null,
|
|
38916
39041
|
access: "view"
|
|
38917
39042
|
},
|
|
39043
|
+
"carbonMonoxide.getStatus": {
|
|
39044
|
+
capName: "carbon-monoxide",
|
|
39045
|
+
capScope: "device",
|
|
39046
|
+
addonId: null,
|
|
39047
|
+
access: "view"
|
|
39048
|
+
},
|
|
39049
|
+
"climateControl.getStatus": {
|
|
39050
|
+
capName: "climate-control",
|
|
39051
|
+
capScope: "device",
|
|
39052
|
+
addonId: null,
|
|
39053
|
+
access: "view"
|
|
39054
|
+
},
|
|
38918
39055
|
"climateControl.setFanMode": {
|
|
38919
39056
|
capName: "climate-control",
|
|
38920
39057
|
capScope: "device",
|
|
@@ -38963,6 +39100,12 @@ Object.freeze({
|
|
|
38963
39100
|
addonId: null,
|
|
38964
39101
|
access: "create"
|
|
38965
39102
|
},
|
|
39103
|
+
"color.getStatus": {
|
|
39104
|
+
capName: "color",
|
|
39105
|
+
capScope: "device",
|
|
39106
|
+
addonId: null,
|
|
39107
|
+
access: "view"
|
|
39108
|
+
},
|
|
38966
39109
|
"color.setColor": {
|
|
38967
39110
|
capName: "color",
|
|
38968
39111
|
capScope: "device",
|
|
@@ -38981,12 +39124,36 @@ Object.freeze({
|
|
|
38981
39124
|
addonId: null,
|
|
38982
39125
|
access: "create"
|
|
38983
39126
|
},
|
|
39127
|
+
"connectivity.getStatus": {
|
|
39128
|
+
capName: "connectivity",
|
|
39129
|
+
capScope: "device",
|
|
39130
|
+
addonId: null,
|
|
39131
|
+
access: "view"
|
|
39132
|
+
},
|
|
39133
|
+
"consumables.getStatus": {
|
|
39134
|
+
capName: "consumables",
|
|
39135
|
+
capScope: "device",
|
|
39136
|
+
addonId: null,
|
|
39137
|
+
access: "view"
|
|
39138
|
+
},
|
|
38984
39139
|
"consumables.reset": {
|
|
38985
39140
|
capName: "consumables",
|
|
38986
39141
|
capScope: "device",
|
|
38987
39142
|
addonId: null,
|
|
38988
39143
|
access: "delete"
|
|
38989
39144
|
},
|
|
39145
|
+
"contact.getStatus": {
|
|
39146
|
+
capName: "contact",
|
|
39147
|
+
capScope: "device",
|
|
39148
|
+
addonId: null,
|
|
39149
|
+
access: "view"
|
|
39150
|
+
},
|
|
39151
|
+
"control.getStatus": {
|
|
39152
|
+
capName: "control",
|
|
39153
|
+
capScope: "device",
|
|
39154
|
+
addonId: null,
|
|
39155
|
+
access: "view"
|
|
39156
|
+
},
|
|
38990
39157
|
"control.setValue": {
|
|
38991
39158
|
capName: "control",
|
|
38992
39159
|
capScope: "device",
|
|
@@ -39053,6 +39220,12 @@ Object.freeze({
|
|
|
39053
39220
|
addonId: null,
|
|
39054
39221
|
access: "create"
|
|
39055
39222
|
},
|
|
39223
|
+
"cover.getStatus": {
|
|
39224
|
+
capName: "cover",
|
|
39225
|
+
capScope: "device",
|
|
39226
|
+
addonId: null,
|
|
39227
|
+
access: "view"
|
|
39228
|
+
},
|
|
39056
39229
|
"cover.open": {
|
|
39057
39230
|
capName: "cover",
|
|
39058
39231
|
capScope: "device",
|
|
@@ -39185,6 +39358,12 @@ Object.freeze({
|
|
|
39185
39358
|
addonId: null,
|
|
39186
39359
|
access: "view"
|
|
39187
39360
|
},
|
|
39361
|
+
"dayNight.getStatus": {
|
|
39362
|
+
capName: "day-night",
|
|
39363
|
+
capScope: "device",
|
|
39364
|
+
addonId: null,
|
|
39365
|
+
access: "view"
|
|
39366
|
+
},
|
|
39188
39367
|
"dayNight.setSettings": {
|
|
39189
39368
|
capName: "day-night",
|
|
39190
39369
|
capScope: "device",
|
|
@@ -39275,6 +39454,24 @@ Object.freeze({
|
|
|
39275
39454
|
addonId: null,
|
|
39276
39455
|
access: "view"
|
|
39277
39456
|
},
|
|
39457
|
+
"detectionPipeline.applyDeviceSettingsPatch": {
|
|
39458
|
+
capName: "detection-pipeline",
|
|
39459
|
+
capScope: "device",
|
|
39460
|
+
addonId: null,
|
|
39461
|
+
access: "create"
|
|
39462
|
+
},
|
|
39463
|
+
"detectionPipeline.getDeviceLiveContribution": {
|
|
39464
|
+
capName: "detection-pipeline",
|
|
39465
|
+
capScope: "device",
|
|
39466
|
+
addonId: null,
|
|
39467
|
+
access: "view"
|
|
39468
|
+
},
|
|
39469
|
+
"detectionPipeline.getDeviceSettingsContribution": {
|
|
39470
|
+
capName: "detection-pipeline",
|
|
39471
|
+
capScope: "device",
|
|
39472
|
+
addonId: null,
|
|
39473
|
+
access: "view"
|
|
39474
|
+
},
|
|
39278
39475
|
"deviceAdoption.adopt": {
|
|
39279
39476
|
capName: "device-adoption",
|
|
39280
39477
|
capScope: "system",
|
|
@@ -39287,6 +39484,12 @@ Object.freeze({
|
|
|
39287
39484
|
addonId: null,
|
|
39288
39485
|
access: "view"
|
|
39289
39486
|
},
|
|
39487
|
+
"deviceAdoption.getStatus": {
|
|
39488
|
+
capName: "device-adoption",
|
|
39489
|
+
capScope: "system",
|
|
39490
|
+
addonId: null,
|
|
39491
|
+
access: "view"
|
|
39492
|
+
},
|
|
39290
39493
|
"deviceAdoption.listCandidateFilters": {
|
|
39291
39494
|
capName: "device-adoption",
|
|
39292
39495
|
capScope: "system",
|
|
@@ -39323,6 +39526,12 @@ Object.freeze({
|
|
|
39323
39526
|
addonId: null,
|
|
39324
39527
|
access: "create"
|
|
39325
39528
|
},
|
|
39529
|
+
"deviceDiscovery.getStatus": {
|
|
39530
|
+
capName: "device-discovery",
|
|
39531
|
+
capScope: "device",
|
|
39532
|
+
addonId: null,
|
|
39533
|
+
access: "view"
|
|
39534
|
+
},
|
|
39326
39535
|
"deviceDiscovery.listDiscovered": {
|
|
39327
39536
|
capName: "device-discovery",
|
|
39328
39537
|
capScope: "device",
|
|
@@ -39341,12 +39550,30 @@ Object.freeze({
|
|
|
39341
39550
|
addonId: null,
|
|
39342
39551
|
access: "create"
|
|
39343
39552
|
},
|
|
39553
|
+
"deviceExport.applyDeviceSettingsPatch": {
|
|
39554
|
+
capName: "device-export",
|
|
39555
|
+
capScope: "system",
|
|
39556
|
+
addonId: null,
|
|
39557
|
+
access: "create"
|
|
39558
|
+
},
|
|
39344
39559
|
"deviceExport.exposeDevice": {
|
|
39345
39560
|
capName: "device-export",
|
|
39346
39561
|
capScope: "system",
|
|
39347
39562
|
addonId: null,
|
|
39348
39563
|
access: "create"
|
|
39349
39564
|
},
|
|
39565
|
+
"deviceExport.getDeviceLiveContribution": {
|
|
39566
|
+
capName: "device-export",
|
|
39567
|
+
capScope: "system",
|
|
39568
|
+
addonId: null,
|
|
39569
|
+
access: "view"
|
|
39570
|
+
},
|
|
39571
|
+
"deviceExport.getDeviceSettingsContribution": {
|
|
39572
|
+
capName: "device-export",
|
|
39573
|
+
capScope: "system",
|
|
39574
|
+
addonId: null,
|
|
39575
|
+
access: "view"
|
|
39576
|
+
},
|
|
39350
39577
|
"deviceExport.getStatus": {
|
|
39351
39578
|
capName: "device-export",
|
|
39352
39579
|
capScope: "system",
|
|
@@ -39443,6 +39670,12 @@ Object.freeze({
|
|
|
39443
39670
|
addonId: null,
|
|
39444
39671
|
access: "create"
|
|
39445
39672
|
},
|
|
39673
|
+
"deviceManager.applyDeviceSettingsPatch": {
|
|
39674
|
+
capName: "device-manager",
|
|
39675
|
+
capScope: "system",
|
|
39676
|
+
addonId: null,
|
|
39677
|
+
access: "create"
|
|
39678
|
+
},
|
|
39446
39679
|
"deviceManager.applyInitialMeta": {
|
|
39447
39680
|
capName: "device-manager",
|
|
39448
39681
|
capScope: "system",
|
|
@@ -39545,6 +39778,12 @@ Object.freeze({
|
|
|
39545
39778
|
addonId: null,
|
|
39546
39779
|
access: "view"
|
|
39547
39780
|
},
|
|
39781
|
+
"deviceManager.getDeviceLiveContribution": {
|
|
39782
|
+
capName: "device-manager",
|
|
39783
|
+
capScope: "system",
|
|
39784
|
+
addonId: null,
|
|
39785
|
+
access: "view"
|
|
39786
|
+
},
|
|
39548
39787
|
"deviceManager.getDeviceLiveInfoAggregate": {
|
|
39549
39788
|
capName: "device-manager",
|
|
39550
39789
|
capScope: "system",
|
|
@@ -39557,6 +39796,12 @@ Object.freeze({
|
|
|
39557
39796
|
addonId: null,
|
|
39558
39797
|
access: "view"
|
|
39559
39798
|
},
|
|
39799
|
+
"deviceManager.getDeviceSettingsContribution": {
|
|
39800
|
+
capName: "device-manager",
|
|
39801
|
+
capScope: "system",
|
|
39802
|
+
addonId: null,
|
|
39803
|
+
access: "view"
|
|
39804
|
+
},
|
|
39560
39805
|
"deviceManager.getDeviceStatusAggregate": {
|
|
39561
39806
|
capName: "device-manager",
|
|
39562
39807
|
capScope: "system",
|
|
@@ -40049,6 +40294,36 @@ Object.freeze({
|
|
|
40049
40294
|
addonId: null,
|
|
40050
40295
|
access: "create"
|
|
40051
40296
|
},
|
|
40297
|
+
"deviceStatus.getStatus": {
|
|
40298
|
+
capName: "device-status",
|
|
40299
|
+
capScope: "device",
|
|
40300
|
+
addonId: null,
|
|
40301
|
+
access: "view"
|
|
40302
|
+
},
|
|
40303
|
+
"doorbell.applyDeviceSettingsPatch": {
|
|
40304
|
+
capName: "doorbell",
|
|
40305
|
+
capScope: "device",
|
|
40306
|
+
addonId: null,
|
|
40307
|
+
access: "create"
|
|
40308
|
+
},
|
|
40309
|
+
"doorbell.getDeviceLiveContribution": {
|
|
40310
|
+
capName: "doorbell",
|
|
40311
|
+
capScope: "device",
|
|
40312
|
+
addonId: null,
|
|
40313
|
+
access: "view"
|
|
40314
|
+
},
|
|
40315
|
+
"doorbell.getDeviceSettingsContribution": {
|
|
40316
|
+
capName: "doorbell",
|
|
40317
|
+
capScope: "device",
|
|
40318
|
+
addonId: null,
|
|
40319
|
+
access: "view"
|
|
40320
|
+
},
|
|
40321
|
+
"doorbell.getStatus": {
|
|
40322
|
+
capName: "doorbell",
|
|
40323
|
+
capScope: "device",
|
|
40324
|
+
addonId: null,
|
|
40325
|
+
access: "view"
|
|
40326
|
+
},
|
|
40052
40327
|
"embeddingEncoder.encode": {
|
|
40053
40328
|
capName: "embedding-encoder",
|
|
40054
40329
|
capScope: "system",
|
|
@@ -40067,6 +40342,18 @@ Object.freeze({
|
|
|
40067
40342
|
addonId: null,
|
|
40068
40343
|
access: "view"
|
|
40069
40344
|
},
|
|
40345
|
+
"enumSensor.getStatus": {
|
|
40346
|
+
capName: "enum-sensor",
|
|
40347
|
+
capScope: "device",
|
|
40348
|
+
addonId: null,
|
|
40349
|
+
access: "view"
|
|
40350
|
+
},
|
|
40351
|
+
"eventEmitter.getStatus": {
|
|
40352
|
+
capName: "event-emitter",
|
|
40353
|
+
capScope: "device",
|
|
40354
|
+
addonId: null,
|
|
40355
|
+
access: "view"
|
|
40356
|
+
},
|
|
40070
40357
|
"events.getEventClipUrl": {
|
|
40071
40358
|
capName: "events",
|
|
40072
40359
|
capScope: "device",
|
|
@@ -40187,6 +40474,12 @@ Object.freeze({
|
|
|
40187
40474
|
addonId: null,
|
|
40188
40475
|
access: "view"
|
|
40189
40476
|
},
|
|
40477
|
+
"fanControl.getStatus": {
|
|
40478
|
+
capName: "fan-control",
|
|
40479
|
+
capScope: "device",
|
|
40480
|
+
addonId: null,
|
|
40481
|
+
access: "view"
|
|
40482
|
+
},
|
|
40190
40483
|
"fanControl.setDirection": {
|
|
40191
40484
|
capName: "fan-control",
|
|
40192
40485
|
capScope: "device",
|
|
@@ -40211,6 +40504,12 @@ Object.freeze({
|
|
|
40211
40504
|
addonId: null,
|
|
40212
40505
|
access: "create"
|
|
40213
40506
|
},
|
|
40507
|
+
"featureProbe.getStatus": {
|
|
40508
|
+
capName: "feature-probe",
|
|
40509
|
+
capScope: "device",
|
|
40510
|
+
addonId: null,
|
|
40511
|
+
access: "view"
|
|
40512
|
+
},
|
|
40214
40513
|
"filesystemBrowse.browse": {
|
|
40215
40514
|
capName: "filesystem-browse",
|
|
40216
40515
|
capScope: "system",
|
|
@@ -40229,6 +40528,24 @@ Object.freeze({
|
|
|
40229
40528
|
addonId: null,
|
|
40230
40529
|
access: "view"
|
|
40231
40530
|
},
|
|
40531
|
+
"flood.getStatus": {
|
|
40532
|
+
capName: "flood",
|
|
40533
|
+
capScope: "device",
|
|
40534
|
+
addonId: null,
|
|
40535
|
+
access: "view"
|
|
40536
|
+
},
|
|
40537
|
+
"gas.getStatus": {
|
|
40538
|
+
capName: "gas",
|
|
40539
|
+
capScope: "device",
|
|
40540
|
+
addonId: null,
|
|
40541
|
+
access: "view"
|
|
40542
|
+
},
|
|
40543
|
+
"humidifier.getStatus": {
|
|
40544
|
+
capName: "humidifier",
|
|
40545
|
+
capScope: "device",
|
|
40546
|
+
addonId: null,
|
|
40547
|
+
access: "view"
|
|
40548
|
+
},
|
|
40232
40549
|
"humidifier.setMode": {
|
|
40233
40550
|
capName: "humidifier",
|
|
40234
40551
|
capScope: "device",
|
|
@@ -40247,12 +40564,30 @@ Object.freeze({
|
|
|
40247
40564
|
addonId: null,
|
|
40248
40565
|
access: "create"
|
|
40249
40566
|
},
|
|
40567
|
+
"humiditySensor.getStatus": {
|
|
40568
|
+
capName: "humidity-sensor",
|
|
40569
|
+
capScope: "device",
|
|
40570
|
+
addonId: null,
|
|
40571
|
+
access: "view"
|
|
40572
|
+
},
|
|
40573
|
+
"image.getStatus": {
|
|
40574
|
+
capName: "image",
|
|
40575
|
+
capScope: "device",
|
|
40576
|
+
addonId: null,
|
|
40577
|
+
access: "view"
|
|
40578
|
+
},
|
|
40250
40579
|
"imageSettings.getOptions": {
|
|
40251
40580
|
capName: "image-settings",
|
|
40252
40581
|
capScope: "device",
|
|
40253
40582
|
addonId: null,
|
|
40254
40583
|
access: "view"
|
|
40255
40584
|
},
|
|
40585
|
+
"imageSettings.getStatus": {
|
|
40586
|
+
capName: "image-settings",
|
|
40587
|
+
capScope: "device",
|
|
40588
|
+
addonId: null,
|
|
40589
|
+
access: "view"
|
|
40590
|
+
},
|
|
40256
40591
|
"imageSettings.setSettings": {
|
|
40257
40592
|
capName: "image-settings",
|
|
40258
40593
|
capScope: "device",
|
|
@@ -40325,6 +40660,12 @@ Object.freeze({
|
|
|
40325
40660
|
addonId: null,
|
|
40326
40661
|
access: "create"
|
|
40327
40662
|
},
|
|
40663
|
+
"intercom.getStatus": {
|
|
40664
|
+
capName: "intercom",
|
|
40665
|
+
capScope: "device",
|
|
40666
|
+
addonId: null,
|
|
40667
|
+
access: "view"
|
|
40668
|
+
},
|
|
40328
40669
|
"intercom.handleAnswer": {
|
|
40329
40670
|
capName: "intercom",
|
|
40330
40671
|
capScope: "device",
|
|
@@ -40361,6 +40702,12 @@ Object.freeze({
|
|
|
40361
40702
|
addonId: null,
|
|
40362
40703
|
access: "create"
|
|
40363
40704
|
},
|
|
40705
|
+
"lawnMowerControl.getStatus": {
|
|
40706
|
+
capName: "lawn-mower-control",
|
|
40707
|
+
capScope: "device",
|
|
40708
|
+
addonId: null,
|
|
40709
|
+
access: "view"
|
|
40710
|
+
},
|
|
40364
40711
|
"lawnMowerControl.pause": {
|
|
40365
40712
|
capName: "lawn-mower-control",
|
|
40366
40713
|
capScope: "device",
|
|
@@ -40643,6 +40990,12 @@ Object.freeze({
|
|
|
40643
40990
|
addonId: null,
|
|
40644
40991
|
access: "create"
|
|
40645
40992
|
},
|
|
40993
|
+
"lockControl.getStatus": {
|
|
40994
|
+
capName: "lock-control",
|
|
40995
|
+
capScope: "device",
|
|
40996
|
+
addonId: null,
|
|
40997
|
+
access: "view"
|
|
40998
|
+
},
|
|
40646
40999
|
"lockControl.lock": {
|
|
40647
41000
|
capName: "lock-control",
|
|
40648
41001
|
capScope: "device",
|
|
@@ -40691,6 +41044,12 @@ Object.freeze({
|
|
|
40691
41044
|
addonId: null,
|
|
40692
41045
|
access: "view"
|
|
40693
41046
|
},
|
|
41047
|
+
"mediaPlayer.getStatus": {
|
|
41048
|
+
capName: "media-player",
|
|
41049
|
+
capScope: "device",
|
|
41050
|
+
addonId: null,
|
|
41051
|
+
access: "view"
|
|
41052
|
+
},
|
|
40694
41053
|
"mediaPlayer.next": {
|
|
40695
41054
|
capName: "media-player",
|
|
40696
41055
|
capScope: "device",
|
|
@@ -40883,6 +41242,12 @@ Object.freeze({
|
|
|
40883
41242
|
addonId: null,
|
|
40884
41243
|
access: "create"
|
|
40885
41244
|
},
|
|
41245
|
+
"motion.getStatus": {
|
|
41246
|
+
capName: "motion",
|
|
41247
|
+
capScope: "device",
|
|
41248
|
+
addonId: null,
|
|
41249
|
+
access: "view"
|
|
41250
|
+
},
|
|
40886
41251
|
"motion.isDetected": {
|
|
40887
41252
|
capName: "motion",
|
|
40888
41253
|
capScope: "device",
|
|
@@ -40895,6 +41260,24 @@ Object.freeze({
|
|
|
40895
41260
|
addonId: null,
|
|
40896
41261
|
access: "create"
|
|
40897
41262
|
},
|
|
41263
|
+
"motionDetection.applyDeviceSettingsPatch": {
|
|
41264
|
+
capName: "motion-detection",
|
|
41265
|
+
capScope: "device",
|
|
41266
|
+
addonId: null,
|
|
41267
|
+
access: "create"
|
|
41268
|
+
},
|
|
41269
|
+
"motionDetection.getDeviceLiveContribution": {
|
|
41270
|
+
capName: "motion-detection",
|
|
41271
|
+
capScope: "device",
|
|
41272
|
+
addonId: null,
|
|
41273
|
+
access: "view"
|
|
41274
|
+
},
|
|
41275
|
+
"motionDetection.getDeviceSettingsContribution": {
|
|
41276
|
+
capName: "motion-detection",
|
|
41277
|
+
capScope: "device",
|
|
41278
|
+
addonId: null,
|
|
41279
|
+
access: "view"
|
|
41280
|
+
},
|
|
40898
41281
|
"motionDetection.removeCamera": {
|
|
40899
41282
|
capName: "motion-detection",
|
|
40900
41283
|
capScope: "device",
|
|
@@ -40907,6 +41290,12 @@ Object.freeze({
|
|
|
40907
41290
|
addonId: null,
|
|
40908
41291
|
access: "delete"
|
|
40909
41292
|
},
|
|
41293
|
+
"motionTrigger.getStatus": {
|
|
41294
|
+
capName: "motion-trigger",
|
|
41295
|
+
capScope: "device",
|
|
41296
|
+
addonId: null,
|
|
41297
|
+
access: "view"
|
|
41298
|
+
},
|
|
40910
41299
|
"motionTrigger.setMotionTrigger": {
|
|
40911
41300
|
capName: "motion-trigger",
|
|
40912
41301
|
capScope: "device",
|
|
@@ -40919,6 +41308,12 @@ Object.freeze({
|
|
|
40919
41308
|
addonId: null,
|
|
40920
41309
|
access: "view"
|
|
40921
41310
|
},
|
|
41311
|
+
"motionZones.getStatus": {
|
|
41312
|
+
capName: "motion-zones",
|
|
41313
|
+
capScope: "device",
|
|
41314
|
+
addonId: null,
|
|
41315
|
+
access: "view"
|
|
41316
|
+
},
|
|
40922
41317
|
"motionZones.setZone": {
|
|
40923
41318
|
capName: "motion-zones",
|
|
40924
41319
|
capScope: "device",
|
|
@@ -40979,6 +41374,12 @@ Object.freeze({
|
|
|
40979
41374
|
addonId: null,
|
|
40980
41375
|
access: "view"
|
|
40981
41376
|
},
|
|
41377
|
+
"nativeObjectDetection.getStatus": {
|
|
41378
|
+
capName: "native-object-detection",
|
|
41379
|
+
capScope: "device",
|
|
41380
|
+
addonId: null,
|
|
41381
|
+
access: "view"
|
|
41382
|
+
},
|
|
40982
41383
|
"nativeObjectDetection.setEnabled": {
|
|
40983
41384
|
capName: "native-object-detection",
|
|
40984
41385
|
capScope: "device",
|
|
@@ -40997,6 +41398,12 @@ Object.freeze({
|
|
|
40997
41398
|
addonId: null,
|
|
40998
41399
|
access: "view"
|
|
40999
41400
|
},
|
|
41401
|
+
"navigation.getStatus": {
|
|
41402
|
+
capName: "navigation",
|
|
41403
|
+
capScope: "device",
|
|
41404
|
+
addonId: null,
|
|
41405
|
+
access: "view"
|
|
41406
|
+
},
|
|
41000
41407
|
"navigation.goToPoint": {
|
|
41001
41408
|
capName: "navigation",
|
|
41002
41409
|
capScope: "device",
|
|
@@ -41081,6 +41488,12 @@ Object.freeze({
|
|
|
41081
41488
|
addonId: null,
|
|
41082
41489
|
access: "create"
|
|
41083
41490
|
},
|
|
41491
|
+
"networkLink.getStatus": {
|
|
41492
|
+
capName: "network-link",
|
|
41493
|
+
capScope: "device",
|
|
41494
|
+
addonId: null,
|
|
41495
|
+
access: "view"
|
|
41496
|
+
},
|
|
41084
41497
|
"networkQuality.getAllStats": {
|
|
41085
41498
|
capName: "network-quality",
|
|
41086
41499
|
capScope: "system",
|
|
@@ -41339,18 +41752,36 @@ Object.freeze({
|
|
|
41339
41752
|
addonId: null,
|
|
41340
41753
|
access: "create"
|
|
41341
41754
|
},
|
|
41755
|
+
"notifier.getStatus": {
|
|
41756
|
+
capName: "notifier",
|
|
41757
|
+
capScope: "device",
|
|
41758
|
+
addonId: null,
|
|
41759
|
+
access: "view"
|
|
41760
|
+
},
|
|
41342
41761
|
"notifier.send": {
|
|
41343
41762
|
capName: "notifier",
|
|
41344
41763
|
capScope: "device",
|
|
41345
41764
|
addonId: null,
|
|
41346
41765
|
access: "create"
|
|
41347
41766
|
},
|
|
41767
|
+
"numericSensor.getStatus": {
|
|
41768
|
+
capName: "numeric-sensor",
|
|
41769
|
+
capScope: "device",
|
|
41770
|
+
addonId: null,
|
|
41771
|
+
access: "view"
|
|
41772
|
+
},
|
|
41348
41773
|
"oauthIntegration.getDescriptor": {
|
|
41349
41774
|
capName: "oauth-integration",
|
|
41350
41775
|
capScope: "system",
|
|
41351
41776
|
addonId: null,
|
|
41352
41777
|
access: "view"
|
|
41353
41778
|
},
|
|
41779
|
+
"osd.getStatus": {
|
|
41780
|
+
capName: "osd",
|
|
41781
|
+
capScope: "device",
|
|
41782
|
+
addonId: null,
|
|
41783
|
+
access: "view"
|
|
41784
|
+
},
|
|
41354
41785
|
"osd.setOverlay": {
|
|
41355
41786
|
capName: "osd",
|
|
41356
41787
|
capScope: "device",
|
|
@@ -41423,6 +41854,12 @@ Object.freeze({
|
|
|
41423
41854
|
addonId: null,
|
|
41424
41855
|
access: "create"
|
|
41425
41856
|
},
|
|
41857
|
+
"petFeeder.getStatus": {
|
|
41858
|
+
capName: "pet-feeder",
|
|
41859
|
+
capScope: "device",
|
|
41860
|
+
addonId: null,
|
|
41861
|
+
access: "view"
|
|
41862
|
+
},
|
|
41426
41863
|
"petFeeder.markFoodReplenished": {
|
|
41427
41864
|
capName: "pet-feeder",
|
|
41428
41865
|
capScope: "device",
|
|
@@ -41465,6 +41902,12 @@ Object.freeze({
|
|
|
41465
41902
|
addonId: null,
|
|
41466
41903
|
access: "create"
|
|
41467
41904
|
},
|
|
41905
|
+
"pipelineAnalytics.applyDeviceSettingsPatch": {
|
|
41906
|
+
capName: "pipeline-analytics",
|
|
41907
|
+
capScope: "device",
|
|
41908
|
+
addonId: null,
|
|
41909
|
+
access: "create"
|
|
41910
|
+
},
|
|
41468
41911
|
"pipelineAnalytics.cancelRelocateMedia": {
|
|
41469
41912
|
capName: "pipeline-analytics",
|
|
41470
41913
|
capScope: "device",
|
|
@@ -41531,6 +41974,18 @@ Object.freeze({
|
|
|
41531
41974
|
addonId: null,
|
|
41532
41975
|
access: "view"
|
|
41533
41976
|
},
|
|
41977
|
+
"pipelineAnalytics.getDeviceLiveContribution": {
|
|
41978
|
+
capName: "pipeline-analytics",
|
|
41979
|
+
capScope: "device",
|
|
41980
|
+
addonId: null,
|
|
41981
|
+
access: "view"
|
|
41982
|
+
},
|
|
41983
|
+
"pipelineAnalytics.getDeviceSettingsContribution": {
|
|
41984
|
+
capName: "pipeline-analytics",
|
|
41985
|
+
capScope: "device",
|
|
41986
|
+
addonId: null,
|
|
41987
|
+
access: "view"
|
|
41988
|
+
},
|
|
41534
41989
|
"pipelineAnalytics.getEventDensity": {
|
|
41535
41990
|
capName: "pipeline-analytics",
|
|
41536
41991
|
capScope: "device",
|
|
@@ -42101,6 +42556,12 @@ Object.freeze({
|
|
|
42101
42556
|
addonId: null,
|
|
42102
42557
|
access: "view"
|
|
42103
42558
|
},
|
|
42559
|
+
"pipelineOrchestrator.applyDeviceSettingsPatch": {
|
|
42560
|
+
capName: "pipeline-orchestrator",
|
|
42561
|
+
capScope: "system",
|
|
42562
|
+
addonId: null,
|
|
42563
|
+
access: "create"
|
|
42564
|
+
},
|
|
42104
42565
|
"pipelineOrchestrator.assignAudio": {
|
|
42105
42566
|
capName: "pipeline-orchestrator",
|
|
42106
42567
|
capScope: "system",
|
|
@@ -42191,6 +42652,18 @@ Object.freeze({
|
|
|
42191
42652
|
addonId: null,
|
|
42192
42653
|
access: "view"
|
|
42193
42654
|
},
|
|
42655
|
+
"pipelineOrchestrator.getDeviceLiveContribution": {
|
|
42656
|
+
capName: "pipeline-orchestrator",
|
|
42657
|
+
capScope: "system",
|
|
42658
|
+
addonId: null,
|
|
42659
|
+
access: "view"
|
|
42660
|
+
},
|
|
42661
|
+
"pipelineOrchestrator.getDeviceSettingsContribution": {
|
|
42662
|
+
capName: "pipeline-orchestrator",
|
|
42663
|
+
capScope: "system",
|
|
42664
|
+
addonId: null,
|
|
42665
|
+
access: "view"
|
|
42666
|
+
},
|
|
42194
42667
|
"pipelineOrchestrator.getGlobalMetrics": {
|
|
42195
42668
|
capName: "pipeline-orchestrator",
|
|
42196
42669
|
capScope: "system",
|
|
@@ -42599,12 +43072,36 @@ Object.freeze({
|
|
|
42599
43072
|
addonId: null,
|
|
42600
43073
|
access: "view"
|
|
42601
43074
|
},
|
|
43075
|
+
"powerMeter.getStatus": {
|
|
43076
|
+
capName: "power-meter",
|
|
43077
|
+
capScope: "device",
|
|
43078
|
+
addonId: null,
|
|
43079
|
+
access: "view"
|
|
43080
|
+
},
|
|
43081
|
+
"presence.getStatus": {
|
|
43082
|
+
capName: "presence",
|
|
43083
|
+
capScope: "device",
|
|
43084
|
+
addonId: null,
|
|
43085
|
+
access: "view"
|
|
43086
|
+
},
|
|
43087
|
+
"pressureSensor.getStatus": {
|
|
43088
|
+
capName: "pressure-sensor",
|
|
43089
|
+
capScope: "device",
|
|
43090
|
+
addonId: null,
|
|
43091
|
+
access: "view"
|
|
43092
|
+
},
|
|
42602
43093
|
"privacyMask.getOptions": {
|
|
42603
43094
|
capName: "privacy-mask",
|
|
42604
43095
|
capScope: "device",
|
|
42605
43096
|
addonId: null,
|
|
42606
43097
|
access: "view"
|
|
42607
43098
|
},
|
|
43099
|
+
"privacyMask.getStatus": {
|
|
43100
|
+
capName: "privacy-mask",
|
|
43101
|
+
capScope: "device",
|
|
43102
|
+
addonId: null,
|
|
43103
|
+
access: "view"
|
|
43104
|
+
},
|
|
42608
43105
|
"privacyMask.setAudioEnabled": {
|
|
42609
43106
|
capName: "privacy-mask",
|
|
42610
43107
|
capScope: "device",
|
|
@@ -42665,6 +43162,12 @@ Object.freeze({
|
|
|
42665
43162
|
addonId: null,
|
|
42666
43163
|
access: "view"
|
|
42667
43164
|
},
|
|
43165
|
+
"ptz.getStatus": {
|
|
43166
|
+
capName: "ptz",
|
|
43167
|
+
capScope: "device",
|
|
43168
|
+
addonId: null,
|
|
43169
|
+
access: "view"
|
|
43170
|
+
},
|
|
42668
43171
|
"ptz.goHome": {
|
|
42669
43172
|
capName: "ptz",
|
|
42670
43173
|
capScope: "device",
|
|
@@ -42743,6 +43246,12 @@ Object.freeze({
|
|
|
42743
43246
|
addonId: null,
|
|
42744
43247
|
access: "create"
|
|
42745
43248
|
},
|
|
43249
|
+
"recording.applyDeviceSettingsPatch": {
|
|
43250
|
+
capName: "recording",
|
|
43251
|
+
capScope: "system",
|
|
43252
|
+
addonId: null,
|
|
43253
|
+
access: "create"
|
|
43254
|
+
},
|
|
42746
43255
|
"recording.cancelRelocateJob": {
|
|
42747
43256
|
capName: "recording",
|
|
42748
43257
|
capScope: "system",
|
|
@@ -42791,6 +43300,18 @@ Object.freeze({
|
|
|
42791
43300
|
addonId: null,
|
|
42792
43301
|
access: "view"
|
|
42793
43302
|
},
|
|
43303
|
+
"recording.getDeviceLiveContribution": {
|
|
43304
|
+
capName: "recording",
|
|
43305
|
+
capScope: "system",
|
|
43306
|
+
addonId: null,
|
|
43307
|
+
access: "view"
|
|
43308
|
+
},
|
|
43309
|
+
"recording.getDeviceSettingsContribution": {
|
|
43310
|
+
capName: "recording",
|
|
43311
|
+
capScope: "system",
|
|
43312
|
+
addonId: null,
|
|
43313
|
+
access: "view"
|
|
43314
|
+
},
|
|
42794
43315
|
"recording.getPlacement": {
|
|
42795
43316
|
capName: "recording",
|
|
42796
43317
|
capScope: "system",
|
|
@@ -42809,6 +43330,12 @@ Object.freeze({
|
|
|
42809
43330
|
addonId: null,
|
|
42810
43331
|
access: "view"
|
|
42811
43332
|
},
|
|
43333
|
+
"recording.getStatus": {
|
|
43334
|
+
capName: "recording",
|
|
43335
|
+
capScope: "system",
|
|
43336
|
+
addonId: null,
|
|
43337
|
+
access: "view"
|
|
43338
|
+
},
|
|
42812
43339
|
"recording.getStorageMigrationMoveStatus": {
|
|
42813
43340
|
capName: "recording",
|
|
42814
43341
|
capScope: "system",
|
|
@@ -43013,6 +43540,12 @@ Object.freeze({
|
|
|
43013
43540
|
addonId: null,
|
|
43014
43541
|
access: "delete"
|
|
43015
43542
|
},
|
|
43543
|
+
"sceneMonitor.getStatus": {
|
|
43544
|
+
capName: "scene-monitor",
|
|
43545
|
+
capScope: "device",
|
|
43546
|
+
addonId: null,
|
|
43547
|
+
access: "view"
|
|
43548
|
+
},
|
|
43016
43549
|
"sceneMonitor.listScenes": {
|
|
43017
43550
|
capName: "scene-monitor",
|
|
43018
43551
|
capScope: "device",
|
|
@@ -43043,6 +43576,12 @@ Object.freeze({
|
|
|
43043
43576
|
addonId: null,
|
|
43044
43577
|
access: "create"
|
|
43045
43578
|
},
|
|
43579
|
+
"scriptRunner.getStatus": {
|
|
43580
|
+
capName: "script-runner",
|
|
43581
|
+
capScope: "device",
|
|
43582
|
+
addonId: null,
|
|
43583
|
+
access: "view"
|
|
43584
|
+
},
|
|
43046
43585
|
"scriptRunner.run": {
|
|
43047
43586
|
capName: "script-runner",
|
|
43048
43587
|
capScope: "device",
|
|
@@ -43175,6 +43714,12 @@ Object.freeze({
|
|
|
43175
43714
|
addonId: null,
|
|
43176
43715
|
access: "create"
|
|
43177
43716
|
},
|
|
43717
|
+
"smoke.getStatus": {
|
|
43718
|
+
capName: "smoke",
|
|
43719
|
+
capScope: "device",
|
|
43720
|
+
addonId: null,
|
|
43721
|
+
access: "view"
|
|
43722
|
+
},
|
|
43178
43723
|
"smtpProvider.getStatus": {
|
|
43179
43724
|
capName: "smtp-provider",
|
|
43180
43725
|
capScope: "system",
|
|
@@ -43193,12 +43738,30 @@ Object.freeze({
|
|
|
43193
43738
|
addonId: null,
|
|
43194
43739
|
access: "view"
|
|
43195
43740
|
},
|
|
43741
|
+
"snapshot.applyDeviceSettingsPatch": {
|
|
43742
|
+
capName: "snapshot",
|
|
43743
|
+
capScope: "device",
|
|
43744
|
+
addonId: null,
|
|
43745
|
+
access: "create"
|
|
43746
|
+
},
|
|
43196
43747
|
"snapshot.getDebugState": {
|
|
43197
43748
|
capName: "snapshot",
|
|
43198
43749
|
capScope: "device",
|
|
43199
43750
|
addonId: null,
|
|
43200
43751
|
access: "view"
|
|
43201
43752
|
},
|
|
43753
|
+
"snapshot.getDeviceLiveContribution": {
|
|
43754
|
+
capName: "snapshot",
|
|
43755
|
+
capScope: "device",
|
|
43756
|
+
addonId: null,
|
|
43757
|
+
access: "view"
|
|
43758
|
+
},
|
|
43759
|
+
"snapshot.getDeviceSettingsContribution": {
|
|
43760
|
+
capName: "snapshot",
|
|
43761
|
+
capScope: "device",
|
|
43762
|
+
addonId: null,
|
|
43763
|
+
access: "view"
|
|
43764
|
+
},
|
|
43202
43765
|
"snapshot.getSnapshot": {
|
|
43203
43766
|
capName: "snapshot",
|
|
43204
43767
|
capScope: "device",
|
|
@@ -43217,6 +43780,12 @@ Object.freeze({
|
|
|
43217
43780
|
addonId: null,
|
|
43218
43781
|
access: "view"
|
|
43219
43782
|
},
|
|
43783
|
+
"snapshot.getStatus": {
|
|
43784
|
+
capName: "snapshot",
|
|
43785
|
+
capScope: "device",
|
|
43786
|
+
addonId: null,
|
|
43787
|
+
access: "view"
|
|
43788
|
+
},
|
|
43220
43789
|
"snapshot.invalidateCache": {
|
|
43221
43790
|
capName: "snapshot",
|
|
43222
43791
|
capScope: "device",
|
|
@@ -43553,6 +44122,12 @@ Object.freeze({
|
|
|
43553
44122
|
addonId: null,
|
|
43554
44123
|
access: "create"
|
|
43555
44124
|
},
|
|
44125
|
+
"streamBroker.applyDeviceSettingsPatch": {
|
|
44126
|
+
capName: "stream-broker",
|
|
44127
|
+
capScope: "system",
|
|
44128
|
+
addonId: null,
|
|
44129
|
+
access: "create"
|
|
44130
|
+
},
|
|
43556
44131
|
"streamBroker.assignProfile": {
|
|
43557
44132
|
capName: "stream-broker",
|
|
43558
44133
|
capScope: "system",
|
|
@@ -43589,6 +44164,18 @@ Object.freeze({
|
|
|
43589
44164
|
addonId: null,
|
|
43590
44165
|
access: "view"
|
|
43591
44166
|
},
|
|
44167
|
+
"streamBroker.getDeviceLiveContribution": {
|
|
44168
|
+
capName: "stream-broker",
|
|
44169
|
+
capScope: "system",
|
|
44170
|
+
addonId: null,
|
|
44171
|
+
access: "view"
|
|
44172
|
+
},
|
|
44173
|
+
"streamBroker.getDeviceSettingsContribution": {
|
|
44174
|
+
capName: "stream-broker",
|
|
44175
|
+
capScope: "system",
|
|
44176
|
+
addonId: null,
|
|
44177
|
+
access: "view"
|
|
44178
|
+
},
|
|
43592
44179
|
"streamBroker.getPreBufferInfo": {
|
|
43593
44180
|
capName: "stream-broker",
|
|
43594
44181
|
capScope: "system",
|
|
@@ -43787,6 +44374,12 @@ Object.freeze({
|
|
|
43787
44374
|
addonId: null,
|
|
43788
44375
|
access: "view"
|
|
43789
44376
|
},
|
|
44377
|
+
"streamParams.getStatus": {
|
|
44378
|
+
capName: "stream-params",
|
|
44379
|
+
capScope: "device",
|
|
44380
|
+
addonId: null,
|
|
44381
|
+
access: "view"
|
|
44382
|
+
},
|
|
43790
44383
|
"streamParams.setProfile": {
|
|
43791
44384
|
capName: "stream-params",
|
|
43792
44385
|
capScope: "device",
|
|
@@ -43799,6 +44392,12 @@ Object.freeze({
|
|
|
43799
44392
|
addonId: null,
|
|
43800
44393
|
access: "view"
|
|
43801
44394
|
},
|
|
44395
|
+
"switch.getStatus": {
|
|
44396
|
+
capName: "switch",
|
|
44397
|
+
capScope: "device",
|
|
44398
|
+
addonId: null,
|
|
44399
|
+
access: "view"
|
|
44400
|
+
},
|
|
43802
44401
|
"switch.setState": {
|
|
43803
44402
|
capName: "switch",
|
|
43804
44403
|
capScope: "device",
|
|
@@ -43895,6 +44494,18 @@ Object.freeze({
|
|
|
43895
44494
|
addonId: null,
|
|
43896
44495
|
access: "create"
|
|
43897
44496
|
},
|
|
44497
|
+
"tamper.getStatus": {
|
|
44498
|
+
capName: "tamper",
|
|
44499
|
+
capScope: "device",
|
|
44500
|
+
addonId: null,
|
|
44501
|
+
access: "view"
|
|
44502
|
+
},
|
|
44503
|
+
"temperatureSensor.getStatus": {
|
|
44504
|
+
capName: "temperature-sensor",
|
|
44505
|
+
capScope: "device",
|
|
44506
|
+
addonId: null,
|
|
44507
|
+
access: "view"
|
|
44508
|
+
},
|
|
43898
44509
|
"terminalSession.adoptLegacyMonitor": {
|
|
43899
44510
|
capName: "terminal-session",
|
|
43900
44511
|
capScope: "system",
|
|
@@ -43991,6 +44602,12 @@ Object.freeze({
|
|
|
43991
44602
|
addonId: null,
|
|
43992
44603
|
access: "view"
|
|
43993
44604
|
},
|
|
44605
|
+
"update.getStatus": {
|
|
44606
|
+
capName: "update",
|
|
44607
|
+
capScope: "device",
|
|
44608
|
+
addonId: null,
|
|
44609
|
+
access: "view"
|
|
44610
|
+
},
|
|
43994
44611
|
"update.installUpdate": {
|
|
43995
44612
|
capName: "update",
|
|
43996
44613
|
capScope: "device",
|
|
@@ -44213,6 +44830,12 @@ Object.freeze({
|
|
|
44213
44830
|
addonId: null,
|
|
44214
44831
|
access: "create"
|
|
44215
44832
|
},
|
|
44833
|
+
"vacuumControl.getStatus": {
|
|
44834
|
+
capName: "vacuum-control",
|
|
44835
|
+
capScope: "device",
|
|
44836
|
+
addonId: null,
|
|
44837
|
+
access: "view"
|
|
44838
|
+
},
|
|
44216
44839
|
"vacuumControl.locate": {
|
|
44217
44840
|
capName: "vacuum-control",
|
|
44218
44841
|
capScope: "device",
|
|
@@ -44255,6 +44878,12 @@ Object.freeze({
|
|
|
44255
44878
|
addonId: null,
|
|
44256
44879
|
access: "create"
|
|
44257
44880
|
},
|
|
44881
|
+
"valve.getStatus": {
|
|
44882
|
+
capName: "valve",
|
|
44883
|
+
capScope: "device",
|
|
44884
|
+
addonId: null,
|
|
44885
|
+
access: "view"
|
|
44886
|
+
},
|
|
44258
44887
|
"valve.open": {
|
|
44259
44888
|
capName: "valve",
|
|
44260
44889
|
capScope: "device",
|
|
@@ -44327,6 +44956,12 @@ Object.freeze({
|
|
|
44327
44956
|
addonId: null,
|
|
44328
44957
|
access: "create"
|
|
44329
44958
|
},
|
|
44959
|
+
"vibration.getStatus": {
|
|
44960
|
+
capName: "vibration",
|
|
44961
|
+
capScope: "device",
|
|
44962
|
+
addonId: null,
|
|
44963
|
+
access: "view"
|
|
44964
|
+
},
|
|
44330
44965
|
"videoclips.getClipPlayback": {
|
|
44331
44966
|
capName: "videoclips",
|
|
44332
44967
|
capScope: "device",
|
|
@@ -44363,6 +44998,12 @@ Object.freeze({
|
|
|
44363
44998
|
addonId: null,
|
|
44364
44999
|
access: "view"
|
|
44365
45000
|
},
|
|
45001
|
+
"waterHeater.getStatus": {
|
|
45002
|
+
capName: "water-heater",
|
|
45003
|
+
capScope: "device",
|
|
45004
|
+
addonId: null,
|
|
45005
|
+
access: "view"
|
|
45006
|
+
},
|
|
44366
45007
|
"waterHeater.setAway": {
|
|
44367
45008
|
capName: "water-heater",
|
|
44368
45009
|
capScope: "device",
|
|
@@ -44381,6 +45022,12 @@ Object.freeze({
|
|
|
44381
45022
|
addonId: null,
|
|
44382
45023
|
access: "create"
|
|
44383
45024
|
},
|
|
45025
|
+
"weather.getStatus": {
|
|
45026
|
+
capName: "weather",
|
|
45027
|
+
capScope: "device",
|
|
45028
|
+
addonId: null,
|
|
45029
|
+
access: "view"
|
|
45030
|
+
},
|
|
44384
45031
|
"webrtcSession.addIceCandidate": {
|
|
44385
45032
|
capName: "webrtc-session",
|
|
44386
45033
|
capScope: "device",
|
|
@@ -76168,7 +76815,7 @@ var require_lz4 = /* @__PURE__ */ require_chunk.__commonJSMin(((exports) => {
|
|
|
76168
76815
|
};
|
|
76169
76816
|
}));
|
|
76170
76817
|
//#endregion
|
|
76171
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
76818
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-GNC5U3YA.js
|
|
76172
76819
|
var import_fxp = require_fxp();
|
|
76173
76820
|
var import_lz4 = /* @__PURE__ */ require_chunk.__toESM(require_lz4(), 1);
|
|
76174
76821
|
function encodeHeader(h) {
|
|
@@ -80333,6 +80980,7 @@ var BaichuanClient = class _BaichuanClient extends events.EventEmitter {
|
|
|
80333
80980
|
lockedChannelId = frame.header.channelId;
|
|
80334
80981
|
lockedStreamType = frame.header.streamType;
|
|
80335
80982
|
}
|
|
80983
|
+
params.onChunk?.(decrypted);
|
|
80336
80984
|
chunks.push(decrypted);
|
|
80337
80985
|
armIdleFinish();
|
|
80338
80986
|
if (frame.header.responseCode === 201) finish(Buffer.concat(chunks));
|
|
@@ -92339,7 +92987,8 @@ ${stderr}`));
|
|
|
92339
92987
|
payloadXml,
|
|
92340
92988
|
streamType: 0,
|
|
92341
92989
|
timeoutMs,
|
|
92342
|
-
...params.idleTimeoutMs != null ? { idleTimeoutMs: params.idleTimeoutMs } : {}
|
|
92990
|
+
...params.idleTimeoutMs != null ? { idleTimeoutMs: params.idleTimeoutMs } : {},
|
|
92991
|
+
...params.onChunk ? { onChunk: params.onChunk } : {}
|
|
92343
92992
|
});
|
|
92344
92993
|
} catch (e) {
|
|
92345
92994
|
trace(`download failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -148403,6 +149052,123 @@ function createClipPlaybackHandler(deps) {
|
|
|
148403
149052
|
}).pipe(res);
|
|
148404
149053
|
};
|
|
148405
149054
|
}
|
|
149055
|
+
/**
|
|
149056
|
+
* `/addon/provider-reolink/clip-stream/<deviceId>/<clipKey>.mp4` — the same
|
|
149057
|
+
* recording as `clip/…`, served as it ARRIVES from the camera.
|
|
149058
|
+
*
|
|
149059
|
+
* **Not `Range`-able, and it says so.** The response has no length: it is a
|
|
149060
|
+
* fragmented MP4 written by an ffmpeg that is still reading the camera when
|
|
149061
|
+
* the first byte leaves. So there is no `content-length`, `accept-ranges` is
|
|
149062
|
+
* `none`, a `Range` header is IGNORED (a 206 over something with no length
|
|
149063
|
+
* would be a lie), and the peer may not cache it (`no-store`) — a stream is
|
|
149064
|
+
* not an immutable artifact, the file route is. The status line is written
|
|
149065
|
+
* on the first muxed byte, never before: until then every refusal still owns
|
|
149066
|
+
* it, and a camera that serves nothing answers a status with a reason, not an
|
|
149067
|
+
* empty 200 (D391).
|
|
149068
|
+
*
|
|
149069
|
+
* `HEAD` answers the headers and touches no camera.
|
|
149070
|
+
*/
|
|
149071
|
+
var CLIP_STREAM_PREFIX = "clip-stream";
|
|
149072
|
+
var STREAM_HEADERS = {
|
|
149073
|
+
"content-type": "video/mp4",
|
|
149074
|
+
"accept-ranges": "none",
|
|
149075
|
+
"cache-control": "no-store",
|
|
149076
|
+
["x-camstack-clip-transport"]: "forward-only"
|
|
149077
|
+
};
|
|
149078
|
+
/** The status a refusal answers. `404` = nothing here to serve; `409` = a
|
|
149079
|
+
* clip that exists and cannot be served right now; `502` = the camera. */
|
|
149080
|
+
function streamRefusalStatus(code) {
|
|
149081
|
+
switch (code) {
|
|
149082
|
+
case "unknown-device":
|
|
149083
|
+
case "no-catalog-row": return 404;
|
|
149084
|
+
case "sleeping":
|
|
149085
|
+
case "uid-missing":
|
|
149086
|
+
case "clip-in-progress": return 409;
|
|
149087
|
+
case "camera-refused":
|
|
149088
|
+
case "fetch-failed": return 502;
|
|
149089
|
+
}
|
|
149090
|
+
}
|
|
149091
|
+
function createClipStreamHandler(deps) {
|
|
149092
|
+
return async (req, res) => {
|
|
149093
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
149094
|
+
res.writeHead(405, { allow: "GET, HEAD" });
|
|
149095
|
+
res.end();
|
|
149096
|
+
return;
|
|
149097
|
+
}
|
|
149098
|
+
const match = MEDIA_PATH.exec(pathnameOf(req));
|
|
149099
|
+
if (match === null) {
|
|
149100
|
+
res.writeHead(400);
|
|
149101
|
+
res.end();
|
|
149102
|
+
return;
|
|
149103
|
+
}
|
|
149104
|
+
const deviceId = Number(match[1]);
|
|
149105
|
+
const clipKey = match[2] ?? "";
|
|
149106
|
+
if (req.method === "HEAD") {
|
|
149107
|
+
res.writeHead(200, STREAM_HEADERS);
|
|
149108
|
+
res.end();
|
|
149109
|
+
return;
|
|
149110
|
+
}
|
|
149111
|
+
if (req.headers.range !== void 0) deps.logger.debug("videoclips: a Range header on the clip stream route is ignored", {
|
|
149112
|
+
tags: { deviceId },
|
|
149113
|
+
meta: {
|
|
149114
|
+
clipKey,
|
|
149115
|
+
range: req.headers.range
|
|
149116
|
+
}
|
|
149117
|
+
});
|
|
149118
|
+
const sink = {
|
|
149119
|
+
begin: () => {
|
|
149120
|
+
res.writeHead(200, STREAM_HEADERS);
|
|
149121
|
+
},
|
|
149122
|
+
write: (chunk) => res.write(chunk),
|
|
149123
|
+
onceDrain: (listener) => {
|
|
149124
|
+
res.once("drain", listener);
|
|
149125
|
+
},
|
|
149126
|
+
onceClose: (listener) => {
|
|
149127
|
+
res.once("close", listener);
|
|
149128
|
+
},
|
|
149129
|
+
end: () => {
|
|
149130
|
+
res.end();
|
|
149131
|
+
},
|
|
149132
|
+
destroy: () => {
|
|
149133
|
+
res.destroy();
|
|
149134
|
+
}
|
|
149135
|
+
};
|
|
149136
|
+
let outcome;
|
|
149137
|
+
try {
|
|
149138
|
+
outcome = await deps.stream(deviceId, clipKey, sink);
|
|
149139
|
+
} catch (err) {
|
|
149140
|
+
deps.logger.warn("videoclips: the clip stream route threw", {
|
|
149141
|
+
tags: { deviceId },
|
|
149142
|
+
meta: {
|
|
149143
|
+
clipKey,
|
|
149144
|
+
branch: "stream-threw",
|
|
149145
|
+
error: err instanceof Error ? err.message : String(err)
|
|
149146
|
+
}
|
|
149147
|
+
});
|
|
149148
|
+
if (res.headersSent) res.destroy();
|
|
149149
|
+
else {
|
|
149150
|
+
res.writeHead(500);
|
|
149151
|
+
res.end();
|
|
149152
|
+
}
|
|
149153
|
+
return;
|
|
149154
|
+
}
|
|
149155
|
+
if (outcome.kind !== "refused") return;
|
|
149156
|
+
deps.logger.info("videoclips: clip stream not served", {
|
|
149157
|
+
tags: { deviceId },
|
|
149158
|
+
meta: {
|
|
149159
|
+
clipKey,
|
|
149160
|
+
code: outcome.code,
|
|
149161
|
+
reason: outcome.detail,
|
|
149162
|
+
status: streamRefusalStatus(outcome.code)
|
|
149163
|
+
}
|
|
149164
|
+
});
|
|
149165
|
+
res.writeHead(streamRefusalStatus(outcome.code), {
|
|
149166
|
+
[CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
|
|
149167
|
+
[CLIP_STILL_REASON_CODE_HEADER]: outcome.code
|
|
149168
|
+
});
|
|
149169
|
+
res.end();
|
|
149170
|
+
};
|
|
149171
|
+
}
|
|
148406
149172
|
//#endregion
|
|
148407
149173
|
//#region src/clips/clip-mux.ts
|
|
148408
149174
|
/** A clip that has not transferred by here is not going to. */
|
|
@@ -148560,6 +149326,94 @@ function describeMuxFailure(run) {
|
|
|
148560
149326
|
if (run.code !== 0) return `mux-failed (exit ${String(run.code)}): ${run.stderr.slice(0, 300)}`;
|
|
148561
149327
|
return null;
|
|
148562
149328
|
}
|
|
149329
|
+
/** The fragmented container a stream is muxed into — see the file header. */
|
|
149330
|
+
var CLIP_STREAM_MOVFLAGS = "frag_keyframe+empty_moov+default_base_moof";
|
|
149331
|
+
/**
|
|
149332
|
+
* The rate the probe measured, through the library's one derivation.
|
|
149333
|
+
*
|
|
149334
|
+
* `estimateVideoTiming` falls back to the info header when the timestamps
|
|
149335
|
+
* cannot say; that fallback is REFUSED here (`fpsSource: 'unmeasured'`), as
|
|
149336
|
+
* D567 says, so a guess is never handed to `-r` under a measurement's name.
|
|
149337
|
+
*/
|
|
149338
|
+
function probeClipStreamRate(timestampsUs) {
|
|
149339
|
+
const timing = estimateVideoTiming({ timestampsUs });
|
|
149340
|
+
if (timing.fpsSource !== "timestamps" || timing.fps === null) return {
|
|
149341
|
+
fps: null,
|
|
149342
|
+
fpsSource: "unmeasured",
|
|
149343
|
+
accessUnits: timestampsUs.length
|
|
149344
|
+
};
|
|
149345
|
+
return {
|
|
149346
|
+
fps: timing.fps,
|
|
149347
|
+
fpsSource: "timestamps",
|
|
149348
|
+
accessUnits: timestampsUs.length
|
|
149349
|
+
};
|
|
149350
|
+
}
|
|
149351
|
+
/**
|
|
149352
|
+
* One ffmpeg, stdin to stdout, video only, fragmented MP4.
|
|
149353
|
+
*
|
|
149354
|
+
* Mirrors `buildClipMuxArgs` where a stream can: the demuxer is named from
|
|
149355
|
+
* the DETECTED codec (the BcMedia header lies on a hub child), `-r` is an
|
|
149356
|
+
* input option before its own `-i`, and the video is copied. Where it
|
|
149357
|
+
* cannot — `+faststart`, the audio input — the file header says why.
|
|
149358
|
+
*/
|
|
149359
|
+
function buildClipStreamMuxArgs(input) {
|
|
149360
|
+
const demuxer = input.videoType === "H265" ? "hevc" : "h264";
|
|
149361
|
+
return [
|
|
149362
|
+
"-hide_banner",
|
|
149363
|
+
"-loglevel",
|
|
149364
|
+
"error",
|
|
149365
|
+
"-nostdin",
|
|
149366
|
+
...input.fps !== null && Number.isFinite(input.fps) && input.fps > 0 ? ["-r", String(input.fps)] : [],
|
|
149367
|
+
"-f",
|
|
149368
|
+
demuxer,
|
|
149369
|
+
"-i",
|
|
149370
|
+
"pipe:0",
|
|
149371
|
+
"-c:v",
|
|
149372
|
+
"copy",
|
|
149373
|
+
"-movflags",
|
|
149374
|
+
CLIP_STREAM_MOVFLAGS,
|
|
149375
|
+
"-f",
|
|
149376
|
+
"mp4",
|
|
149377
|
+
"pipe:1"
|
|
149378
|
+
];
|
|
149379
|
+
}
|
|
149380
|
+
/**
|
|
149381
|
+
* The D447 rule as a state machine, so it can be proved without a socket.
|
|
149382
|
+
*
|
|
149383
|
+
* A `false` write ARMS the timer; further `false` writes while it is armed do
|
|
149384
|
+
* not push the deadline out. `drain` — the hop's own word that it took what
|
|
149385
|
+
* it held — disarms it. Stall fires at most once, and a disposed gate is
|
|
149386
|
+
* silent for ever.
|
|
149387
|
+
*/
|
|
149388
|
+
function createDrainGate(deps) {
|
|
149389
|
+
let handle = null;
|
|
149390
|
+
let stalled = false;
|
|
149391
|
+
const disarm = () => {
|
|
149392
|
+
if (handle !== null) clearTimeout(handle);
|
|
149393
|
+
handle = null;
|
|
149394
|
+
};
|
|
149395
|
+
return {
|
|
149396
|
+
wrote(accepted) {
|
|
149397
|
+
if (accepted || handle !== null || stalled) return;
|
|
149398
|
+
handle = setTimeout(() => {
|
|
149399
|
+
handle = null;
|
|
149400
|
+
if (stalled) return;
|
|
149401
|
+
stalled = true;
|
|
149402
|
+
deps.onStall();
|
|
149403
|
+
}, deps.timeoutMs);
|
|
149404
|
+
},
|
|
149405
|
+
drained() {
|
|
149406
|
+
disarm();
|
|
149407
|
+
},
|
|
149408
|
+
waiting() {
|
|
149409
|
+
return handle !== null;
|
|
149410
|
+
},
|
|
149411
|
+
dispose() {
|
|
149412
|
+
disarm();
|
|
149413
|
+
stalled = true;
|
|
149414
|
+
}
|
|
149415
|
+
};
|
|
149416
|
+
}
|
|
148563
149417
|
//#endregion
|
|
148564
149418
|
//#region src/clips/clip-truncation.ts
|
|
148565
149419
|
/**
|
|
@@ -148606,6 +149460,500 @@ function judgeClipDuration(input) {
|
|
|
148606
149460
|
};
|
|
148607
149461
|
}
|
|
148608
149462
|
//#endregion
|
|
149463
|
+
//#region src/clips/clip-stream.ts
|
|
149464
|
+
var defaultDecoder = (handlers) => new require_chunk_ZBACPERX.BcMediaAnnexBDecoder({
|
|
149465
|
+
strict: false,
|
|
149466
|
+
onVideoAccessUnit: handlers.onVideoAccessUnit,
|
|
149467
|
+
onAudioFrame: () => handlers.onAudioFrame()
|
|
149468
|
+
});
|
|
149469
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
149470
|
+
function createClipStreamProducer(deps) {
|
|
149471
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
149472
|
+
const createDecoder = deps.createDecoder ?? defaultDecoder;
|
|
149473
|
+
const drainTimeoutMs = deps.drainTimeoutMs ?? 1e4;
|
|
149474
|
+
const flushTimeoutMs = deps.flushTimeoutMs ?? 15e3;
|
|
149475
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
149476
|
+
const activeByHost = /* @__PURE__ */ new Map();
|
|
149477
|
+
const runAttempt = (input, attempt) => new ClipStreamRun({
|
|
149478
|
+
logger: deps.logger,
|
|
149479
|
+
spawnMux: deps.spawnMux,
|
|
149480
|
+
createDecoder,
|
|
149481
|
+
drainTimeoutMs,
|
|
149482
|
+
flushTimeoutMs
|
|
149483
|
+
}, input, attempt).run();
|
|
149484
|
+
const runAttempts = async (input) => {
|
|
149485
|
+
const { deviceId } = input.device;
|
|
149486
|
+
let lastDetail = "no reason given";
|
|
149487
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
149488
|
+
if (attempt > 1) await sleep(500 * 2 ** (attempt - 2));
|
|
149489
|
+
const result = await runAttempt(input, attempt);
|
|
149490
|
+
if (result.kind !== "no-media") return result;
|
|
149491
|
+
lastDetail = result.detail;
|
|
149492
|
+
deps.logger.warn("videoclips: a clip stream produced no media — retrying", {
|
|
149493
|
+
tags: { deviceId },
|
|
149494
|
+
meta: {
|
|
149495
|
+
clipKey: input.row.clipKey,
|
|
149496
|
+
twin: input.twin,
|
|
149497
|
+
branch: "stream-no-media",
|
|
149498
|
+
attempt,
|
|
149499
|
+
error: lastDetail
|
|
149500
|
+
}
|
|
149501
|
+
});
|
|
149502
|
+
}
|
|
149503
|
+
deps.logger.warn("videoclips: the camera never delivered this clip stream", {
|
|
149504
|
+
tags: { deviceId },
|
|
149505
|
+
meta: {
|
|
149506
|
+
clipKey: input.row.clipKey,
|
|
149507
|
+
twin: input.twin,
|
|
149508
|
+
branch: "camera-refused",
|
|
149509
|
+
attempts: 3,
|
|
149510
|
+
error: lastDetail
|
|
149511
|
+
}
|
|
149512
|
+
});
|
|
149513
|
+
return {
|
|
149514
|
+
kind: "refused",
|
|
149515
|
+
code: "camera-refused",
|
|
149516
|
+
detail: `clip stream never landed after 3 attempts — ${lastDetail}`,
|
|
149517
|
+
attempts: 3
|
|
149518
|
+
};
|
|
149519
|
+
};
|
|
149520
|
+
return {
|
|
149521
|
+
active: (hostKey) => activeByHost.get(hostKey) ?? 0,
|
|
149522
|
+
stream: (input) => {
|
|
149523
|
+
const { hostKey, deviceId } = input.device;
|
|
149524
|
+
const ahead = activeByHost.get(hostKey) ?? 0;
|
|
149525
|
+
if (ahead > 0) deps.logger.info("videoclips: a clip stream is queued behind another on this host", {
|
|
149526
|
+
tags: { deviceId },
|
|
149527
|
+
meta: {
|
|
149528
|
+
clipKey: input.row.clipKey,
|
|
149529
|
+
twin: input.twin,
|
|
149530
|
+
branch: "stream-queued",
|
|
149531
|
+
ahead
|
|
149532
|
+
}
|
|
149533
|
+
});
|
|
149534
|
+
activeByHost.set(hostKey, ahead + 1);
|
|
149535
|
+
const run = (lanes.get(hostKey) ?? Promise.resolve()).then(() => runAttempts(input)).finally(() => {
|
|
149536
|
+
const left = (activeByHost.get(hostKey) ?? 1) - 1;
|
|
149537
|
+
if (left <= 0) activeByHost.delete(hostKey);
|
|
149538
|
+
else activeByHost.set(hostKey, left);
|
|
149539
|
+
});
|
|
149540
|
+
lanes.set(hostKey, run.then(() => void 0, () => void 0));
|
|
149541
|
+
return run;
|
|
149542
|
+
}
|
|
149543
|
+
};
|
|
149544
|
+
}
|
|
149545
|
+
/**
|
|
149546
|
+
* One transfer: camera → decoder → ffmpeg → peer, with a cut on every hop.
|
|
149547
|
+
*
|
|
149548
|
+
* A class because the run is a state machine with a dozen fields the chunk
|
|
149549
|
+
* callback and the download promise both touch; passing them around as
|
|
149550
|
+
* closures over `let`s was less readable, not more immutable.
|
|
149551
|
+
*/
|
|
149552
|
+
var ClipStreamRun = class {
|
|
149553
|
+
deps;
|
|
149554
|
+
input;
|
|
149555
|
+
attempt;
|
|
149556
|
+
accessUnits = 0;
|
|
149557
|
+
audioFrames = 0;
|
|
149558
|
+
timestampsUs = [];
|
|
149559
|
+
/** Access units held until the rate is probed — the frames in flight. */
|
|
149560
|
+
probe = [];
|
|
149561
|
+
mux = null;
|
|
149562
|
+
muxStderr = "";
|
|
149563
|
+
muxClosed = null;
|
|
149564
|
+
rate = {
|
|
149565
|
+
fps: null,
|
|
149566
|
+
fpsSource: "unmeasured",
|
|
149567
|
+
accessUnits: 0
|
|
149568
|
+
};
|
|
149569
|
+
videoType = null;
|
|
149570
|
+
begun = false;
|
|
149571
|
+
ended = false;
|
|
149572
|
+
bytesOut = 0;
|
|
149573
|
+
cut = null;
|
|
149574
|
+
stdinGate;
|
|
149575
|
+
peerGate;
|
|
149576
|
+
tags;
|
|
149577
|
+
constructor(deps, input, attempt) {
|
|
149578
|
+
this.deps = deps;
|
|
149579
|
+
this.input = input;
|
|
149580
|
+
this.attempt = attempt;
|
|
149581
|
+
this.tags = { deviceId: input.device.deviceId };
|
|
149582
|
+
this.stdinGate = createDrainGate({
|
|
149583
|
+
timeoutMs: deps.drainTimeoutMs,
|
|
149584
|
+
onStall: () => this.sever("mux-stalled", "ffmpeg stopped taking access units")
|
|
149585
|
+
});
|
|
149586
|
+
this.peerGate = createDrainGate({
|
|
149587
|
+
timeoutMs: deps.drainTimeoutMs,
|
|
149588
|
+
onStall: () => this.sever("peer-stalled", "the peer stopped taking bytes")
|
|
149589
|
+
});
|
|
149590
|
+
}
|
|
149591
|
+
async run() {
|
|
149592
|
+
const { device, row, fileName, twin, sink } = this.input;
|
|
149593
|
+
sink.onceClose(() => {
|
|
149594
|
+
if (!this.ended) this.sever("peer-closed", "the peer closed the connection");
|
|
149595
|
+
});
|
|
149596
|
+
const decoder = this.deps.createDecoder({
|
|
149597
|
+
onVideoAccessUnit: (unit) => this.onAccessUnit(unit),
|
|
149598
|
+
onAudioFrame: () => {
|
|
149599
|
+
this.audioFrames += 1;
|
|
149600
|
+
}
|
|
149601
|
+
});
|
|
149602
|
+
this.deps.logger.info("videoclips: streaming a clip file from the camera", {
|
|
149603
|
+
tags: this.tags,
|
|
149604
|
+
meta: {
|
|
149605
|
+
clipKey: row.clipKey,
|
|
149606
|
+
twin,
|
|
149607
|
+
fileName,
|
|
149608
|
+
startMs: row.startMs,
|
|
149609
|
+
endMs: row.endMs,
|
|
149610
|
+
attempt: this.attempt,
|
|
149611
|
+
idleTimeoutMs: 300
|
|
149612
|
+
}
|
|
149613
|
+
});
|
|
149614
|
+
let api;
|
|
149615
|
+
try {
|
|
149616
|
+
api = await device.getApi();
|
|
149617
|
+
} catch (err) {
|
|
149618
|
+
return {
|
|
149619
|
+
kind: "no-media",
|
|
149620
|
+
detail: describe(err)
|
|
149621
|
+
};
|
|
149622
|
+
}
|
|
149623
|
+
try {
|
|
149624
|
+
await api.fileInfoListReplayBinaryDownload({
|
|
149625
|
+
channel: device.channel,
|
|
149626
|
+
...device.uid !== void 0 ? { uid: device.uid } : {},
|
|
149627
|
+
fileName,
|
|
149628
|
+
idleTimeoutMs: 300,
|
|
149629
|
+
onChunk: (chunk) => this.onChunk(decoder, chunk)
|
|
149630
|
+
});
|
|
149631
|
+
} catch (err) {
|
|
149632
|
+
return this.onTransferFailed(err);
|
|
149633
|
+
}
|
|
149634
|
+
return this.onTransferEnded();
|
|
149635
|
+
}
|
|
149636
|
+
/** The camera's bytes, as they arrive. A cut run THROWS here: the only stop. */
|
|
149637
|
+
onChunk(decoder, chunk) {
|
|
149638
|
+
if (this.cut !== null) throw new Error(`clip stream cut: ${this.cut.reason}`);
|
|
149639
|
+
try {
|
|
149640
|
+
decoder.push(chunk);
|
|
149641
|
+
} catch (err) {
|
|
149642
|
+
this.sever("decode-failed", describe(err));
|
|
149643
|
+
throw err;
|
|
149644
|
+
}
|
|
149645
|
+
}
|
|
149646
|
+
onAccessUnit(unit) {
|
|
149647
|
+
if (this.cut !== null) return;
|
|
149648
|
+
this.accessUnits += 1;
|
|
149649
|
+
this.timestampsUs.push(unit.microseconds);
|
|
149650
|
+
if (this.mux === null) {
|
|
149651
|
+
this.probe = [...this.probe, unit];
|
|
149652
|
+
if (this.probe.length >= 16) this.startMux();
|
|
149653
|
+
return;
|
|
149654
|
+
}
|
|
149655
|
+
this.feed(unit.annexB);
|
|
149656
|
+
}
|
|
149657
|
+
/** Spawn ffmpeg with the probed rate and hand it what was held. */
|
|
149658
|
+
startMux() {
|
|
149659
|
+
const first = this.probe[0];
|
|
149660
|
+
if (first === void 0 || this.mux !== null) return;
|
|
149661
|
+
this.videoType = first.videoType;
|
|
149662
|
+
this.rate = probeClipStreamRate(this.timestampsUs);
|
|
149663
|
+
const { row, twin } = this.input;
|
|
149664
|
+
if (this.rate.fpsSource !== "timestamps") this.deps.logger.warn("videoclips: no measured frame rate for this clip stream", {
|
|
149665
|
+
tags: this.tags,
|
|
149666
|
+
meta: {
|
|
149667
|
+
clipKey: row.clipKey,
|
|
149668
|
+
twin,
|
|
149669
|
+
branch: "fps-unmeasured",
|
|
149670
|
+
probeAccessUnits: this.rate.accessUnits
|
|
149671
|
+
}
|
|
149672
|
+
});
|
|
149673
|
+
let mux;
|
|
149674
|
+
try {
|
|
149675
|
+
mux = this.deps.spawnMux(buildClipStreamMuxArgs({
|
|
149676
|
+
videoType: this.videoType,
|
|
149677
|
+
fps: this.rate.fps
|
|
149678
|
+
}));
|
|
149679
|
+
} catch (err) {
|
|
149680
|
+
this.sever("mux-failed", `ffmpeg could not be spawned: ${describe(err)}`);
|
|
149681
|
+
return;
|
|
149682
|
+
}
|
|
149683
|
+
this.mux = mux;
|
|
149684
|
+
this.muxClosed = new Promise((resolve) => {
|
|
149685
|
+
mux.on("close", (code) => resolve(code));
|
|
149686
|
+
mux.on("error", (err) => {
|
|
149687
|
+
this.sever("mux-failed", `ffmpeg failed: ${err.message}`);
|
|
149688
|
+
resolve(null);
|
|
149689
|
+
});
|
|
149690
|
+
});
|
|
149691
|
+
mux.stderr?.on("data", (chunk) => {
|
|
149692
|
+
if (this.muxStderr.length < 2e3) this.muxStderr += chunk.toString("utf8");
|
|
149693
|
+
});
|
|
149694
|
+
mux.stdin?.on("error", (err) => this.sever("mux-failed", `ffmpeg stdin: ${err.message}`));
|
|
149695
|
+
mux.stdin?.on("drain", () => this.stdinGate.drained());
|
|
149696
|
+
mux.stdout?.on("data", (chunk) => this.onMuxOut(chunk));
|
|
149697
|
+
mux.stdout?.on("end", () => this.finishSink());
|
|
149698
|
+
this.deps.logger.info("videoclips: a clip stream is muxing", {
|
|
149699
|
+
tags: this.tags,
|
|
149700
|
+
meta: {
|
|
149701
|
+
clipKey: row.clipKey,
|
|
149702
|
+
twin,
|
|
149703
|
+
videoType: this.videoType,
|
|
149704
|
+
fps: this.rate.fps,
|
|
149705
|
+
fpsSource: this.rate.fpsSource,
|
|
149706
|
+
probeAccessUnits: this.rate.accessUnits
|
|
149707
|
+
}
|
|
149708
|
+
});
|
|
149709
|
+
const held = this.probe;
|
|
149710
|
+
this.probe = [];
|
|
149711
|
+
for (const unit of held) this.feed(unit.annexB);
|
|
149712
|
+
}
|
|
149713
|
+
feed(annexB) {
|
|
149714
|
+
if (this.cut !== null || this.mux?.stdin === null || this.mux?.stdin === void 0) return;
|
|
149715
|
+
this.stdinGate.wrote(this.mux.stdin.write(annexB));
|
|
149716
|
+
}
|
|
149717
|
+
/** ffmpeg's output, to the peer, honouring `write()`'s return (D447). */
|
|
149718
|
+
onMuxOut(chunk) {
|
|
149719
|
+
if (this.cut !== null) return;
|
|
149720
|
+
const { sink } = this.input;
|
|
149721
|
+
if (!this.begun) {
|
|
149722
|
+
this.begun = true;
|
|
149723
|
+
sink.begin();
|
|
149724
|
+
}
|
|
149725
|
+
const accepted = sink.write(chunk);
|
|
149726
|
+
this.bytesOut += chunk.byteLength;
|
|
149727
|
+
this.peerGate.wrote(accepted);
|
|
149728
|
+
if (accepted) return;
|
|
149729
|
+
const stdout = this.mux?.stdout;
|
|
149730
|
+
stdout?.pause();
|
|
149731
|
+
sink.onceDrain(() => {
|
|
149732
|
+
this.peerGate.drained();
|
|
149733
|
+
stdout?.resume();
|
|
149734
|
+
});
|
|
149735
|
+
}
|
|
149736
|
+
finishSink() {
|
|
149737
|
+
if (this.cut !== null || this.ended) return;
|
|
149738
|
+
this.ended = true;
|
|
149739
|
+
this.input.sink.end();
|
|
149740
|
+
}
|
|
149741
|
+
/** Sever every hop, once, and say why. */
|
|
149742
|
+
sever(reason, detail) {
|
|
149743
|
+
if (this.cut !== null) return;
|
|
149744
|
+
this.cut = {
|
|
149745
|
+
reason,
|
|
149746
|
+
detail
|
|
149747
|
+
};
|
|
149748
|
+
this.stdinGate.dispose();
|
|
149749
|
+
this.peerGate.dispose();
|
|
149750
|
+
this.mux?.kill("SIGKILL");
|
|
149751
|
+
this.deps.logger.warn("videoclips: a clip stream was cut", {
|
|
149752
|
+
tags: this.tags,
|
|
149753
|
+
meta: {
|
|
149754
|
+
clipKey: this.input.row.clipKey,
|
|
149755
|
+
twin: this.input.twin,
|
|
149756
|
+
branch: reason,
|
|
149757
|
+
detail,
|
|
149758
|
+
accessUnits: this.accessUnits,
|
|
149759
|
+
bytesOut: this.bytesOut,
|
|
149760
|
+
begun: this.begun
|
|
149761
|
+
}
|
|
149762
|
+
});
|
|
149763
|
+
if (this.begun) this.input.sink.destroy();
|
|
149764
|
+
}
|
|
149765
|
+
cutOutcome(cut) {
|
|
149766
|
+
if (this.begun) return {
|
|
149767
|
+
kind: "cut",
|
|
149768
|
+
reason: cut.reason,
|
|
149769
|
+
detail: cut.detail,
|
|
149770
|
+
bytesOut: this.bytesOut
|
|
149771
|
+
};
|
|
149772
|
+
return {
|
|
149773
|
+
kind: "refused",
|
|
149774
|
+
code: "fetch-failed",
|
|
149775
|
+
detail: `${cut.reason}: ${cut.detail}`,
|
|
149776
|
+
attempts: this.attempt
|
|
149777
|
+
};
|
|
149778
|
+
}
|
|
149779
|
+
onTransferFailed(err) {
|
|
149780
|
+
if (this.cut !== null) return this.cutOutcome(this.cut);
|
|
149781
|
+
if (this.accessUnits === 0) return {
|
|
149782
|
+
kind: "no-media",
|
|
149783
|
+
detail: describe(err)
|
|
149784
|
+
};
|
|
149785
|
+
this.sever("camera-failed", describe(err));
|
|
149786
|
+
return this.cutOutcome({
|
|
149787
|
+
reason: "camera-failed",
|
|
149788
|
+
detail: describe(err)
|
|
149789
|
+
});
|
|
149790
|
+
}
|
|
149791
|
+
async onTransferEnded() {
|
|
149792
|
+
if (this.cut !== null) return this.cutOutcome(this.cut);
|
|
149793
|
+
if (this.accessUnits === 0) return {
|
|
149794
|
+
kind: "no-media",
|
|
149795
|
+
detail: "the camera answered the transfer with no access unit"
|
|
149796
|
+
};
|
|
149797
|
+
if (this.mux === null) this.startMux();
|
|
149798
|
+
const cutAtStart = this.cut;
|
|
149799
|
+
if (cutAtStart !== null) return this.cutOutcome(cutAtStart);
|
|
149800
|
+
const mux = this.mux;
|
|
149801
|
+
if (mux === null || this.muxClosed === null) return this.cutOutcome({
|
|
149802
|
+
reason: "mux-failed",
|
|
149803
|
+
detail: "no mux after the transfer ended"
|
|
149804
|
+
});
|
|
149805
|
+
mux.stdin?.end();
|
|
149806
|
+
const flush = setTimeout(() => this.sever("mux-hung", `ffmpeg did not exit ${this.deps.flushTimeoutMs} ms after its input ended`), this.deps.flushTimeoutMs);
|
|
149807
|
+
const code = await this.muxClosed;
|
|
149808
|
+
clearTimeout(flush);
|
|
149809
|
+
const cutAfter = this.cut;
|
|
149810
|
+
if (cutAfter !== null) return this.cutOutcome(cutAfter);
|
|
149811
|
+
if (code !== 0) {
|
|
149812
|
+
this.sever("mux-failed", `ffmpeg exited ${String(code)}: ${this.muxStderr.slice(0, 300)}`);
|
|
149813
|
+
return this.cutOutcome({
|
|
149814
|
+
reason: "mux-failed",
|
|
149815
|
+
detail: this.muxStderr.slice(0, 300)
|
|
149816
|
+
});
|
|
149817
|
+
}
|
|
149818
|
+
this.finishSink();
|
|
149819
|
+
return this.served();
|
|
149820
|
+
}
|
|
149821
|
+
/** The stream landed: judge it against the row, out loud, and answer. */
|
|
149822
|
+
served() {
|
|
149823
|
+
const { row, twin } = this.input;
|
|
149824
|
+
const timing = estimateVideoTiming({ timestampsUs: this.timestampsUs });
|
|
149825
|
+
const durationSeconds = timing.fpsSource === "timestamps" ? timing.durationSeconds : null;
|
|
149826
|
+
const verdict = judgeClipDuration({
|
|
149827
|
+
expectedMs: row.endMs - row.startMs,
|
|
149828
|
+
decodedSeconds: durationSeconds,
|
|
149829
|
+
...row.inProgress === true ? { inProgress: true } : {}
|
|
149830
|
+
});
|
|
149831
|
+
const meta = {
|
|
149832
|
+
clipKey: row.clipKey,
|
|
149833
|
+
twin,
|
|
149834
|
+
attempt: this.attempt,
|
|
149835
|
+
accessUnits: this.accessUnits,
|
|
149836
|
+
bytesOut: this.bytesOut,
|
|
149837
|
+
durationSeconds,
|
|
149838
|
+
expectedMs: row.endMs - row.startMs,
|
|
149839
|
+
fps: this.rate.fps,
|
|
149840
|
+
fpsSource: this.rate.fpsSource,
|
|
149841
|
+
videoType: this.videoType
|
|
149842
|
+
};
|
|
149843
|
+
if (verdict.kind === "short") this.deps.logger.warn("videoclips: a clip stream came up short against its row", {
|
|
149844
|
+
tags: this.tags,
|
|
149845
|
+
meta: {
|
|
149846
|
+
...meta,
|
|
149847
|
+
branch: "stream-short",
|
|
149848
|
+
shortfallSeconds: verdict.shortfallSeconds,
|
|
149849
|
+
retried: false
|
|
149850
|
+
}
|
|
149851
|
+
});
|
|
149852
|
+
else if (verdict.kind === "unknown") this.deps.logger.warn("videoclips: a clip stream could not be checked for truncation", {
|
|
149853
|
+
tags: this.tags,
|
|
149854
|
+
meta: {
|
|
149855
|
+
...meta,
|
|
149856
|
+
branch: "completeness-unverified",
|
|
149857
|
+
reason: verdict.reason
|
|
149858
|
+
}
|
|
149859
|
+
});
|
|
149860
|
+
else this.deps.logger.info("videoclips: a clip stream landed whole", {
|
|
149861
|
+
tags: this.tags,
|
|
149862
|
+
meta: {
|
|
149863
|
+
...meta,
|
|
149864
|
+
branch: "stream-served"
|
|
149865
|
+
}
|
|
149866
|
+
});
|
|
149867
|
+
if (this.audioFrames > 0) this.deps.logger.warn("videoclips: a clip stream was served without sound", {
|
|
149868
|
+
tags: this.tags,
|
|
149869
|
+
meta: {
|
|
149870
|
+
clipKey: row.clipKey,
|
|
149871
|
+
twin,
|
|
149872
|
+
branch: "stream-video-only",
|
|
149873
|
+
audioFramesDropped: this.audioFrames
|
|
149874
|
+
}
|
|
149875
|
+
});
|
|
149876
|
+
return {
|
|
149877
|
+
kind: "served",
|
|
149878
|
+
accessUnits: this.accessUnits,
|
|
149879
|
+
audioFramesDropped: this.audioFrames,
|
|
149880
|
+
bytesOut: this.bytesOut,
|
|
149881
|
+
durationSeconds,
|
|
149882
|
+
verdict,
|
|
149883
|
+
rate: this.rate,
|
|
149884
|
+
attempts: this.attempt
|
|
149885
|
+
};
|
|
149886
|
+
}
|
|
149887
|
+
};
|
|
149888
|
+
function describe(err) {
|
|
149889
|
+
return err instanceof Error ? err.message : String(err);
|
|
149890
|
+
}
|
|
149891
|
+
//#endregion
|
|
149892
|
+
//#region src/clips/clip-stream-route.ts
|
|
149893
|
+
function createClipStreamRoute(deps) {
|
|
149894
|
+
return async (deviceId, clipKey, sink) => {
|
|
149895
|
+
const twin = "sub";
|
|
149896
|
+
const gate = await deps.gate({
|
|
149897
|
+
deviceId,
|
|
149898
|
+
clipKey,
|
|
149899
|
+
twin,
|
|
149900
|
+
path: "stream"
|
|
149901
|
+
});
|
|
149902
|
+
if (gate.kind === "refused") return {
|
|
149903
|
+
kind: "refused",
|
|
149904
|
+
code: gate.branch,
|
|
149905
|
+
detail: gate.detail
|
|
149906
|
+
};
|
|
149907
|
+
const row = deps.rowFor(deviceId, clipKey);
|
|
149908
|
+
if (row === null) {
|
|
149909
|
+
deps.logger.warn("videoclips: no catalog row for this clip stream", {
|
|
149910
|
+
tags: { deviceId },
|
|
149911
|
+
meta: {
|
|
149912
|
+
clipKey,
|
|
149913
|
+
twin,
|
|
149914
|
+
branch: "no-catalog-row"
|
|
149915
|
+
}
|
|
149916
|
+
});
|
|
149917
|
+
return {
|
|
149918
|
+
kind: "refused",
|
|
149919
|
+
code: "no-catalog-row",
|
|
149920
|
+
detail: "no-catalog-row: list this day before asking for its stream"
|
|
149921
|
+
};
|
|
149922
|
+
}
|
|
149923
|
+
if (row.inProgress === true) {
|
|
149924
|
+
deps.logger.warn("videoclips: refusing to stream a clip the camera is still writing", {
|
|
149925
|
+
tags: { deviceId },
|
|
149926
|
+
meta: {
|
|
149927
|
+
clipKey,
|
|
149928
|
+
twin,
|
|
149929
|
+
branch: "clip-in-progress"
|
|
149930
|
+
}
|
|
149931
|
+
});
|
|
149932
|
+
return {
|
|
149933
|
+
kind: "refused",
|
|
149934
|
+
code: "clip-in-progress",
|
|
149935
|
+
detail: "clip-in-progress: the camera is still writing this file; it has no end to stream to"
|
|
149936
|
+
};
|
|
149937
|
+
}
|
|
149938
|
+
const outcome = await deps.producer.stream({
|
|
149939
|
+
device: gate.device,
|
|
149940
|
+
row,
|
|
149941
|
+
fileName: row.subFileName,
|
|
149942
|
+
twin,
|
|
149943
|
+
sink
|
|
149944
|
+
});
|
|
149945
|
+
switch (outcome.kind) {
|
|
149946
|
+
case "served": return { kind: "served" };
|
|
149947
|
+
case "cut": return { kind: "cut" };
|
|
149948
|
+
case "refused": return {
|
|
149949
|
+
kind: "refused",
|
|
149950
|
+
code: outcome.code,
|
|
149951
|
+
detail: outcome.detail
|
|
149952
|
+
};
|
|
149953
|
+
}
|
|
149954
|
+
};
|
|
149955
|
+
}
|
|
149956
|
+
//#endregion
|
|
148609
149957
|
//#region src/clips/clip-thumb-queue.ts
|
|
148610
149958
|
/**
|
|
148611
149959
|
* The provider's OWN CoverPreview lane — two queues, one in flight per host.
|
|
@@ -148785,6 +150133,59 @@ function decideClipPrefetch(input) {
|
|
|
148785
150133
|
return { kind: "run" };
|
|
148786
150134
|
}
|
|
148787
150135
|
//#endregion
|
|
150136
|
+
//#region src/clips/clip-window.ts
|
|
150137
|
+
/**
|
|
150138
|
+
* Does this row overlap the window?
|
|
150139
|
+
*
|
|
150140
|
+
* Half-open on both ends (`start < until`, `end > since`), so a clip that ends
|
|
150141
|
+
* exactly when the window begins is OUT and a clip that begins exactly when it
|
|
150142
|
+
* ends is OUT — otherwise two adjacent windows both claim the same boundary
|
|
150143
|
+
* clip and a surface paging through them renders it twice.
|
|
150144
|
+
*
|
|
150145
|
+
* A row whose `endMs` is not after its `startMs` is treated as an INSTANT at
|
|
150146
|
+
* `startMs`. That is not defensive padding: a clip still being written carries
|
|
150147
|
+
* a frozen `endMs` from the camera's index (D563), and on this fleet that has
|
|
150148
|
+
* been observed equal to — and once behind — its own start. Judging such a row
|
|
150149
|
+
* by a bad end would drop a recording that is happening right now.
|
|
150150
|
+
*/
|
|
150151
|
+
function clipOverlapsWindow(row, window) {
|
|
150152
|
+
const end = row.endMs > row.startMs ? row.endMs : row.startMs;
|
|
150153
|
+
return row.startMs < window.until && end > window.since;
|
|
150154
|
+
}
|
|
150155
|
+
/**
|
|
150156
|
+
* What the provider ANSWERS with — the one gate, so there is a single place
|
|
150157
|
+
* that decides what leaves this listing and a single line that reports it.
|
|
150158
|
+
*
|
|
150159
|
+
* ## Why a clip still being written is not listed
|
|
150160
|
+
*
|
|
150161
|
+
* The operator, 2026-09-22: *"Niente clips in preparazione, non ha senso
|
|
150162
|
+
* visualizzarle."* He is right, and the reasons had been accumulating:
|
|
150163
|
+
*
|
|
150164
|
+
* - its `endMs` is a frozen guess — the camera's index keeps whatever it held
|
|
150165
|
+
* when the file was opened, measured on 592 as the same `endTime` for eight
|
|
150166
|
+
* minutes (D563) — so the row's duration, its position on the axis and its
|
|
150167
|
+
* height are all wrong and all move;
|
|
150168
|
+
* - `readClipBytes` already refuses it by name (`clip-in-progress`): it has no
|
|
150169
|
+
* end to export against, so the completion rule has nothing to prove the
|
|
150170
|
+
* fetch finished (D568);
|
|
150171
|
+
* - it cannot be looped, and the surface has to explain why (`resolveClipLoop`);
|
|
150172
|
+
* - the player route would file a TRUNCATED copy under an immutable key, so
|
|
150173
|
+
* the first seconds become permanently what that clip is.
|
|
150174
|
+
*
|
|
150175
|
+
* Four different places bending around a row that cannot be played properly.
|
|
150176
|
+
* Not listing it deletes all four questions instead of answering them: the
|
|
150177
|
+
* clip appears, whole, on the next listing after the camera closes the file.
|
|
150178
|
+
*/
|
|
150179
|
+
function listableClips(rows, window) {
|
|
150180
|
+
const inWindow = rows.filter((row) => clipOverlapsWindow(row, window));
|
|
150181
|
+
const kept = inWindow.filter((row) => row.inProgress !== true);
|
|
150182
|
+
return {
|
|
150183
|
+
kept,
|
|
150184
|
+
outOfWindow: rows.length - inWindow.length,
|
|
150185
|
+
inProgress: inWindow.length - kept.length
|
|
150186
|
+
};
|
|
150187
|
+
}
|
|
150188
|
+
//#endregion
|
|
148788
150189
|
//#region src/clips/clip-identity.ts
|
|
148789
150190
|
/**
|
|
148790
150191
|
* A Reolink clip's IDENTITY — the id the operator's surface carries around and
|
|
@@ -148901,7 +150302,8 @@ var CLASS_BY_NATIVE = {
|
|
|
148901
150302
|
var KIND_BY_NATIVE = {
|
|
148902
150303
|
md: "motion",
|
|
148903
150304
|
pir: "motion",
|
|
148904
|
-
cry: "audio"
|
|
150305
|
+
cry: "audio",
|
|
150306
|
+
other: "motion"
|
|
148905
150307
|
};
|
|
148906
150308
|
/** What the subject DID — carried beside a class, never as one. */
|
|
148907
150309
|
var BEHAVIOUR_BY_NATIVE = {
|
|
@@ -148914,8 +150316,9 @@ var BEHAVIOUR_BY_NATIVE = {
|
|
|
148914
150316
|
/**
|
|
148915
150317
|
* Recording REASONS — why the file exists, not what is in it. They get no
|
|
148916
150318
|
* label and no behaviour; the row still lists, showing the reason raw.
|
|
148917
|
-
* `
|
|
148918
|
-
*
|
|
150319
|
+
* `none` is the camera declining to classify, which is not the same as the
|
|
150320
|
+
* camera saying "nothing". `other` used to live here and is now a motion KIND
|
|
150321
|
+
* — see {@link KIND_BY_NATIVE}.
|
|
148919
150322
|
*/
|
|
148920
150323
|
var RECORDING_REASONS = new Set([
|
|
148921
150324
|
"sched",
|
|
@@ -148927,7 +150330,6 @@ var RECORDING_REASONS = new Set([
|
|
|
148927
150330
|
"answer",
|
|
148928
150331
|
"tamper",
|
|
148929
150332
|
"lmsg",
|
|
148930
|
-
"other",
|
|
148931
150333
|
"none"
|
|
148932
150334
|
]);
|
|
148933
150335
|
/**
|
|
@@ -149503,6 +150905,18 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149503
150905
|
timeZone: device.timeZone,
|
|
149504
150906
|
day
|
|
149505
150907
|
});
|
|
150908
|
+
const newest = result.rows[result.rows.length - 1] ?? result.rows[0];
|
|
150909
|
+
deps.logger.info("videoclips: the camera answered a day search", {
|
|
150910
|
+
tags,
|
|
150911
|
+
meta: {
|
|
150912
|
+
day,
|
|
150913
|
+
rows: result.rows.length,
|
|
150914
|
+
newestFile: newest?.subFileName ?? null,
|
|
150915
|
+
newestStartMs: newest?.startMs ?? null,
|
|
150916
|
+
oldestStartMs: result.rows[0]?.startMs ?? null,
|
|
150917
|
+
askedAtMs: Date.now()
|
|
150918
|
+
}
|
|
150919
|
+
});
|
|
149506
150920
|
if (result.mainSearchFailed !== void 0) deps.logger.warn("videoclips: the main-stream search refused — no high twin today", {
|
|
149507
150921
|
tags,
|
|
149508
150922
|
meta: {
|
|
@@ -149544,7 +150958,24 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149544
150958
|
state: "ok",
|
|
149545
150959
|
catalogAsOf: deps.now()
|
|
149546
150960
|
});
|
|
149547
|
-
|
|
150961
|
+
for (const row of rows) rowsByKey.set(`${device.deviceId}|${row.clipKey}`, row);
|
|
150962
|
+
const narrowed = listableClips(rows, {
|
|
150963
|
+
since,
|
|
150964
|
+
until
|
|
150965
|
+
});
|
|
150966
|
+
if (narrowed.outOfWindow > 0 || narrowed.inProgress > 0) deps.logger.info("videoclips: narrowed a day listing to what it can answer", {
|
|
150967
|
+
tags,
|
|
150968
|
+
meta: {
|
|
150969
|
+
branch: "listing-narrowed",
|
|
150970
|
+
listed: rows.length,
|
|
150971
|
+
kept: narrowed.kept.length,
|
|
150972
|
+
outOfWindow: narrowed.outOfWindow,
|
|
150973
|
+
inProgress: narrowed.inProgress,
|
|
150974
|
+
since,
|
|
150975
|
+
until
|
|
150976
|
+
}
|
|
150977
|
+
});
|
|
150978
|
+
return narrowed.kept;
|
|
149548
150979
|
};
|
|
149549
150980
|
/**
|
|
149550
150981
|
* Give a day's rows a class when the FILES carried none.
|
|
@@ -150452,9 +151883,15 @@ function createClipService(deps) {
|
|
|
150452
151883
|
* clip-export vocabulary, so the provider can hand the engine a token it
|
|
150453
151884
|
* believes rather than prose it has to guess at (D572).
|
|
150454
151885
|
*/
|
|
150455
|
-
|
|
150456
|
-
|
|
150457
|
-
|
|
151886
|
+
/**
|
|
151887
|
+
* The gates every clip byte path runs, in ONE place: the device, the
|
|
151888
|
+
* sleeping standalone, the uid. The file producer and the stream producer
|
|
151889
|
+
* both call this and neither decides any of it again — a second copy of
|
|
151890
|
+
* the sleeping gate is a second chance to wake a camera nobody authorised.
|
|
151891
|
+
* Every refusal is logged HERE, with its branch, so a caller cannot forget.
|
|
151892
|
+
*/
|
|
151893
|
+
const gateClipDevice = async (input) => {
|
|
151894
|
+
const { deviceId, clipKey, twin, path } = input;
|
|
150458
151895
|
const tags = { deviceId };
|
|
150459
151896
|
const device = await deps.resolveDevice(deviceId);
|
|
150460
151897
|
if (device === null) {
|
|
@@ -150463,11 +151900,13 @@ function createClipService(deps) {
|
|
|
150463
151900
|
meta: {
|
|
150464
151901
|
clipKey,
|
|
150465
151902
|
twin,
|
|
151903
|
+
path,
|
|
150466
151904
|
branch: "unknown-device"
|
|
150467
151905
|
}
|
|
150468
151906
|
});
|
|
150469
151907
|
return {
|
|
150470
151908
|
kind: "refused",
|
|
151909
|
+
branch: "unknown-device",
|
|
150471
151910
|
code: "catalog-unreachable",
|
|
150472
151911
|
detail: `device ${deviceId} is not a Reolink camera on this node`
|
|
150473
151912
|
};
|
|
@@ -150479,11 +151918,13 @@ function createClipService(deps) {
|
|
|
150479
151918
|
meta: {
|
|
150480
151919
|
clipKey,
|
|
150481
151920
|
twin,
|
|
151921
|
+
path,
|
|
150482
151922
|
branch: "sleeping"
|
|
150483
151923
|
}
|
|
150484
151924
|
});
|
|
150485
151925
|
return {
|
|
150486
151926
|
kind: "refused",
|
|
151927
|
+
branch: "sleeping",
|
|
150487
151928
|
code: "sleeping",
|
|
150488
151929
|
detail: `device ${deviceId} is a standalone battery camera and is sleeping — fetching a clip would wake it, and no operator authorisation was carried`
|
|
150489
151930
|
};
|
|
@@ -150493,6 +151934,7 @@ function createClipService(deps) {
|
|
|
150493
151934
|
meta: {
|
|
150494
151935
|
clipKey,
|
|
150495
151936
|
twin,
|
|
151937
|
+
path,
|
|
150496
151938
|
branch: "wake-authorised"
|
|
150497
151939
|
}
|
|
150498
151940
|
});
|
|
@@ -150503,16 +151945,50 @@ function createClipService(deps) {
|
|
|
150503
151945
|
meta: {
|
|
150504
151946
|
clipKey,
|
|
150505
151947
|
twin,
|
|
151948
|
+
path,
|
|
150506
151949
|
branch: "uid-missing"
|
|
150507
151950
|
}
|
|
150508
151951
|
});
|
|
150509
151952
|
return {
|
|
150510
151953
|
kind: "refused",
|
|
151954
|
+
branch: "uid-missing",
|
|
150511
151955
|
code: "catalog-unreachable",
|
|
150512
151956
|
detail: UID_MISSING_REASON
|
|
150513
151957
|
};
|
|
150514
151958
|
}
|
|
151959
|
+
return {
|
|
151960
|
+
kind: "ok",
|
|
151961
|
+
device
|
|
151962
|
+
};
|
|
151963
|
+
};
|
|
151964
|
+
const produceTwin = async (input) => {
|
|
151965
|
+
const { deviceId, row, fileName, twin } = input;
|
|
151966
|
+
const clipKey = row.clipKey;
|
|
151967
|
+
const tags = { deviceId };
|
|
151968
|
+
const gate = await gateClipDevice({
|
|
151969
|
+
deviceId,
|
|
151970
|
+
clipKey,
|
|
151971
|
+
twin,
|
|
151972
|
+
path: "file",
|
|
151973
|
+
...input.wake !== void 0 ? { wake: input.wake } : {}
|
|
151974
|
+
});
|
|
151975
|
+
if (gate.kind === "refused") return {
|
|
151976
|
+
kind: "refused",
|
|
151977
|
+
code: gate.code,
|
|
151978
|
+
detail: gate.detail
|
|
151979
|
+
};
|
|
151980
|
+
const { device } = gate;
|
|
150515
151981
|
let downloaded;
|
|
151982
|
+
deps.logger.info("videoclips: fetching a clip file from the camera", {
|
|
151983
|
+
tags,
|
|
151984
|
+
meta: {
|
|
151985
|
+
clipKey,
|
|
151986
|
+
twin,
|
|
151987
|
+
fileName,
|
|
151988
|
+
startMs: row.startMs,
|
|
151989
|
+
endMs: row.endMs
|
|
151990
|
+
}
|
|
151991
|
+
});
|
|
150516
151992
|
try {
|
|
150517
151993
|
downloaded = await fetchVerifiedClipBytes(device, row, clipKey, fileName);
|
|
150518
151994
|
} catch (err) {
|
|
@@ -150679,6 +152155,23 @@ function createClipService(deps) {
|
|
|
150679
152155
|
if (outcome.kind === "ok" && twin === "sub") await media.put(deviceId, row.clipKey, outcome.bytes);
|
|
150680
152156
|
return outcome;
|
|
150681
152157
|
};
|
|
152158
|
+
/**
|
|
152159
|
+
* The STREAM's path (`clip-stream-route.ts`): the same gates, the same row,
|
|
152160
|
+
* nothing written. Beside the file path, not in its place.
|
|
152161
|
+
*/
|
|
152162
|
+
const streamClip = createClipStreamRoute({
|
|
152163
|
+
logger: deps.logger,
|
|
152164
|
+
gate: gateClipDevice,
|
|
152165
|
+
rowFor,
|
|
152166
|
+
producer: createClipStreamProducer({
|
|
152167
|
+
logger: deps.logger,
|
|
152168
|
+
spawnMux: (args) => (0, node_child_process.spawn)(deps.ffmpegBinaryPath ?? "ffmpeg", [...args], { stdio: [
|
|
152169
|
+
"pipe",
|
|
152170
|
+
"pipe",
|
|
152171
|
+
"pipe"
|
|
152172
|
+
] })
|
|
152173
|
+
})
|
|
152174
|
+
});
|
|
150682
152175
|
const provider = createReolinkVideoclipsProvider({
|
|
150683
152176
|
now,
|
|
150684
152177
|
cache,
|
|
@@ -150708,6 +152201,11 @@ function createClipService(deps) {
|
|
|
150708
152201
|
hostBusy: (hostKey) => queue.busy(hostKey),
|
|
150709
152202
|
thumbPrefix: CLIP_THUMB_PREFIX,
|
|
150710
152203
|
mediaPrefix: CLIP_MEDIA_PREFIX,
|
|
152204
|
+
streamPrefix: CLIP_STREAM_PREFIX,
|
|
152205
|
+
streamHandler: createClipStreamHandler({
|
|
152206
|
+
stream: streamClip,
|
|
152207
|
+
logger: deps.logger
|
|
152208
|
+
}),
|
|
150711
152209
|
thumbHandler: createClipThumbHandler({
|
|
150712
152210
|
readCached: (deviceId, clipKey) => thumbs.read(deviceId, clipKey),
|
|
150713
152211
|
mint: (deviceId, clipKey) => mintThumbnail(deviceId, clipKey, "interactive"),
|
|
@@ -164947,11 +166445,18 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
164947
166445
|
access: "authenticated",
|
|
164948
166446
|
handler: service.playbackHandler
|
|
164949
166447
|
});
|
|
166448
|
+
const stream = await this.ctx.dataPlane?.serve({
|
|
166449
|
+
prefix: service.streamPrefix,
|
|
166450
|
+
access: "authenticated",
|
|
166451
|
+
handler: service.streamHandler
|
|
166452
|
+
});
|
|
164950
166453
|
this.ctx.logger.info("Reolink clip data planes served", { meta: {
|
|
164951
166454
|
thumbPath: `/addon/${REOLINK_ADDON_ID}/${service.thumbPrefix}`,
|
|
164952
166455
|
mediaPath: `/addon/${REOLINK_ADDON_ID}/${service.mediaPrefix}`,
|
|
166456
|
+
streamPath: `/addon/${REOLINK_ADDON_ID}/${service.streamPrefix}`,
|
|
164953
166457
|
thumbServed: thumb !== void 0,
|
|
164954
|
-
mediaServed: media !== void 0
|
|
166458
|
+
mediaServed: media !== void 0,
|
|
166459
|
+
streamServed: stream !== void 0
|
|
164955
166460
|
} });
|
|
164956
166461
|
} catch (err) {
|
|
164957
166462
|
this.ctx.logger.warn("Reolink clip data planes failed to serve — clips will not render", { meta: { error: err instanceof Error ? err.message : String(err) } });
|