@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.mjs
CHANGED
|
@@ -5385,7 +5385,7 @@ var ZodIssueCode = {
|
|
|
5385
5385
|
var ZodFirstPartyTypeKind;
|
|
5386
5386
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5387
5387
|
//#endregion
|
|
5388
|
-
//#region ../types/dist/sleep-
|
|
5388
|
+
//#region ../types/dist/sleep-CUmLTud7.mjs
|
|
5389
5389
|
/**
|
|
5390
5390
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5391
5391
|
* window to float samples (D455).
|
|
@@ -25634,6 +25634,23 @@ var videoclipsCapability = {
|
|
|
25634
25634
|
deviceId: number(),
|
|
25635
25635
|
clipId: string().min(1),
|
|
25636
25636
|
/**
|
|
25637
|
+
* WHICH provider holds the bytes — the `addonId` a
|
|
25638
|
+
* {@link ClipSourceSchema} row carries. **Required**, for the reason
|
|
25639
|
+
* `listClips` states one method above and for a second one this method
|
|
25640
|
+
* learned the hard way.
|
|
25641
|
+
*
|
|
25642
|
+
* The caller is the stream broker, and the broker lives in
|
|
25643
|
+
* `addon-pipeline` — the same addon that registers the RECORDER's
|
|
25644
|
+
* videoclips provider. With no provider named, the in-process
|
|
25645
|
+
* resolution handed it its own sibling, which implements this method
|
|
25646
|
+
* solely to explain that an analytics visit has no file behind it. So
|
|
25647
|
+
* every onboard clip answered with that sentence, a flash of video and
|
|
25648
|
+
* nothing more, while the SAME call from outside the addon reached the
|
|
25649
|
+
* right provider and returned the bytes (measured on 3628,
|
|
25650
|
+
* 2026-09-21).
|
|
25651
|
+
*/
|
|
25652
|
+
provider: string().min(1),
|
|
25653
|
+
/**
|
|
25637
25654
|
* Which twin to fetch, on the mapping D549 15 already fixed:
|
|
25638
25655
|
* `low | mid` → the sub file, `high` → the main twin. `auto` is not
|
|
25639
25656
|
* accepted here for the same reason it is not accepted by
|
|
@@ -33380,7 +33397,7 @@ method(object({
|
|
|
33380
33397
|
auth: "admin"
|
|
33381
33398
|
}), method(object({ deviceId: number() }), RecordingConfigSchema, {
|
|
33382
33399
|
kind: "query",
|
|
33383
|
-
auth: "
|
|
33400
|
+
auth: "protected"
|
|
33384
33401
|
}), method(object({
|
|
33385
33402
|
deviceId: number(),
|
|
33386
33403
|
profile: string(),
|
|
@@ -38148,6 +38165,12 @@ function createRuntimeStateBridge(params) {
|
|
|
38148
38165
|
};
|
|
38149
38166
|
}
|
|
38150
38167
|
Object.freeze({
|
|
38168
|
+
"accessories.getStatus": {
|
|
38169
|
+
capName: "accessories",
|
|
38170
|
+
capScope: "device",
|
|
38171
|
+
addonId: null,
|
|
38172
|
+
access: "view"
|
|
38173
|
+
},
|
|
38151
38174
|
"accessories.setChildHidden": {
|
|
38152
38175
|
capName: "accessories",
|
|
38153
38176
|
capScope: "device",
|
|
@@ -38430,6 +38453,12 @@ Object.freeze({
|
|
|
38430
38453
|
addonId: null,
|
|
38431
38454
|
access: "view"
|
|
38432
38455
|
},
|
|
38456
|
+
"airQualitySensor.getStatus": {
|
|
38457
|
+
capName: "air-quality-sensor",
|
|
38458
|
+
capScope: "device",
|
|
38459
|
+
addonId: null,
|
|
38460
|
+
access: "view"
|
|
38461
|
+
},
|
|
38433
38462
|
"alarmPanel.arm": {
|
|
38434
38463
|
capName: "alarm-panel",
|
|
38435
38464
|
capScope: "device",
|
|
@@ -38442,6 +38471,12 @@ Object.freeze({
|
|
|
38442
38471
|
addonId: null,
|
|
38443
38472
|
access: "create"
|
|
38444
38473
|
},
|
|
38474
|
+
"alarmPanel.getStatus": {
|
|
38475
|
+
capName: "alarm-panel",
|
|
38476
|
+
capScope: "device",
|
|
38477
|
+
addonId: null,
|
|
38478
|
+
access: "view"
|
|
38479
|
+
},
|
|
38445
38480
|
"alarmPanel.trigger": {
|
|
38446
38481
|
capName: "alarm-panel",
|
|
38447
38482
|
capScope: "device",
|
|
@@ -38490,6 +38525,30 @@ Object.freeze({
|
|
|
38490
38525
|
addonId: null,
|
|
38491
38526
|
access: "create"
|
|
38492
38527
|
},
|
|
38528
|
+
"ambientLightSensor.getStatus": {
|
|
38529
|
+
capName: "ambient-light-sensor",
|
|
38530
|
+
capScope: "device",
|
|
38531
|
+
addonId: null,
|
|
38532
|
+
access: "view"
|
|
38533
|
+
},
|
|
38534
|
+
"audioAnalysis.applyDeviceSettingsPatch": {
|
|
38535
|
+
capName: "audio-analysis",
|
|
38536
|
+
capScope: "device",
|
|
38537
|
+
addonId: null,
|
|
38538
|
+
access: "create"
|
|
38539
|
+
},
|
|
38540
|
+
"audioAnalysis.getDeviceLiveContribution": {
|
|
38541
|
+
capName: "audio-analysis",
|
|
38542
|
+
capScope: "device",
|
|
38543
|
+
addonId: null,
|
|
38544
|
+
access: "view"
|
|
38545
|
+
},
|
|
38546
|
+
"audioAnalysis.getDeviceSettingsContribution": {
|
|
38547
|
+
capName: "audio-analysis",
|
|
38548
|
+
capScope: "device",
|
|
38549
|
+
addonId: null,
|
|
38550
|
+
access: "view"
|
|
38551
|
+
},
|
|
38493
38552
|
"audioAnalysis.resolveDeviceSettings": {
|
|
38494
38553
|
capName: "audio-analysis",
|
|
38495
38554
|
capScope: "device",
|
|
@@ -38652,6 +38711,12 @@ Object.freeze({
|
|
|
38652
38711
|
addonId: null,
|
|
38653
38712
|
access: "create"
|
|
38654
38713
|
},
|
|
38714
|
+
"automationControl.getStatus": {
|
|
38715
|
+
capName: "automation-control",
|
|
38716
|
+
capScope: "device",
|
|
38717
|
+
addonId: null,
|
|
38718
|
+
access: "view"
|
|
38719
|
+
},
|
|
38655
38720
|
"automationControl.trigger": {
|
|
38656
38721
|
capName: "automation-control",
|
|
38657
38722
|
capScope: "device",
|
|
@@ -38748,12 +38813,30 @@ Object.freeze({
|
|
|
38748
38813
|
addonId: null,
|
|
38749
38814
|
access: "create"
|
|
38750
38815
|
},
|
|
38816
|
+
"battery.getStatus": {
|
|
38817
|
+
capName: "battery",
|
|
38818
|
+
capScope: "device",
|
|
38819
|
+
addonId: null,
|
|
38820
|
+
access: "view"
|
|
38821
|
+
},
|
|
38751
38822
|
"battery.wakeForStream": {
|
|
38752
38823
|
capName: "battery",
|
|
38753
38824
|
capScope: "device",
|
|
38754
38825
|
addonId: null,
|
|
38755
38826
|
access: "create"
|
|
38756
38827
|
},
|
|
38828
|
+
"binary.getStatus": {
|
|
38829
|
+
capName: "binary",
|
|
38830
|
+
capScope: "device",
|
|
38831
|
+
addonId: null,
|
|
38832
|
+
access: "view"
|
|
38833
|
+
},
|
|
38834
|
+
"brightness.getStatus": {
|
|
38835
|
+
capName: "brightness",
|
|
38836
|
+
capScope: "device",
|
|
38837
|
+
addonId: null,
|
|
38838
|
+
access: "view"
|
|
38839
|
+
},
|
|
38757
38840
|
"brightness.setBrightness": {
|
|
38758
38841
|
capName: "brightness",
|
|
38759
38842
|
capScope: "device",
|
|
@@ -38868,6 +38951,30 @@ Object.freeze({
|
|
|
38868
38951
|
addonId: null,
|
|
38869
38952
|
access: "view"
|
|
38870
38953
|
},
|
|
38954
|
+
"cameraCredentials.getStatus": {
|
|
38955
|
+
capName: "camera-credentials",
|
|
38956
|
+
capScope: "device",
|
|
38957
|
+
addonId: null,
|
|
38958
|
+
access: "view"
|
|
38959
|
+
},
|
|
38960
|
+
"cameraGridLayout.applyDeviceSettingsPatch": {
|
|
38961
|
+
capName: "camera-grid-layout",
|
|
38962
|
+
capScope: "device",
|
|
38963
|
+
addonId: null,
|
|
38964
|
+
access: "create"
|
|
38965
|
+
},
|
|
38966
|
+
"cameraGridLayout.getDeviceLiveContribution": {
|
|
38967
|
+
capName: "camera-grid-layout",
|
|
38968
|
+
capScope: "device",
|
|
38969
|
+
addonId: null,
|
|
38970
|
+
access: "view"
|
|
38971
|
+
},
|
|
38972
|
+
"cameraGridLayout.getDeviceSettingsContribution": {
|
|
38973
|
+
capName: "camera-grid-layout",
|
|
38974
|
+
capScope: "device",
|
|
38975
|
+
addonId: null,
|
|
38976
|
+
access: "view"
|
|
38977
|
+
},
|
|
38871
38978
|
"cameraGridLayout.getLayout": {
|
|
38872
38979
|
capName: "camera-grid-layout",
|
|
38873
38980
|
capScope: "device",
|
|
@@ -38880,6 +38987,24 @@ Object.freeze({
|
|
|
38880
38987
|
addonId: null,
|
|
38881
38988
|
access: "create"
|
|
38882
38989
|
},
|
|
38990
|
+
"cameraPipelineConfig.applyDeviceSettingsPatch": {
|
|
38991
|
+
capName: "camera-pipeline-config",
|
|
38992
|
+
capScope: "device",
|
|
38993
|
+
addonId: null,
|
|
38994
|
+
access: "create"
|
|
38995
|
+
},
|
|
38996
|
+
"cameraPipelineConfig.getDeviceLiveContribution": {
|
|
38997
|
+
capName: "camera-pipeline-config",
|
|
38998
|
+
capScope: "device",
|
|
38999
|
+
addonId: null,
|
|
39000
|
+
access: "view"
|
|
39001
|
+
},
|
|
39002
|
+
"cameraPipelineConfig.getDeviceSettingsContribution": {
|
|
39003
|
+
capName: "camera-pipeline-config",
|
|
39004
|
+
capScope: "device",
|
|
39005
|
+
addonId: null,
|
|
39006
|
+
access: "view"
|
|
39007
|
+
},
|
|
38883
39008
|
"cameraStreams.getBrokerStreams": {
|
|
38884
39009
|
capName: "camera-streams",
|
|
38885
39010
|
capScope: "device",
|
|
@@ -38910,6 +39035,18 @@ Object.freeze({
|
|
|
38910
39035
|
addonId: null,
|
|
38911
39036
|
access: "view"
|
|
38912
39037
|
},
|
|
39038
|
+
"carbonMonoxide.getStatus": {
|
|
39039
|
+
capName: "carbon-monoxide",
|
|
39040
|
+
capScope: "device",
|
|
39041
|
+
addonId: null,
|
|
39042
|
+
access: "view"
|
|
39043
|
+
},
|
|
39044
|
+
"climateControl.getStatus": {
|
|
39045
|
+
capName: "climate-control",
|
|
39046
|
+
capScope: "device",
|
|
39047
|
+
addonId: null,
|
|
39048
|
+
access: "view"
|
|
39049
|
+
},
|
|
38913
39050
|
"climateControl.setFanMode": {
|
|
38914
39051
|
capName: "climate-control",
|
|
38915
39052
|
capScope: "device",
|
|
@@ -38958,6 +39095,12 @@ Object.freeze({
|
|
|
38958
39095
|
addonId: null,
|
|
38959
39096
|
access: "create"
|
|
38960
39097
|
},
|
|
39098
|
+
"color.getStatus": {
|
|
39099
|
+
capName: "color",
|
|
39100
|
+
capScope: "device",
|
|
39101
|
+
addonId: null,
|
|
39102
|
+
access: "view"
|
|
39103
|
+
},
|
|
38961
39104
|
"color.setColor": {
|
|
38962
39105
|
capName: "color",
|
|
38963
39106
|
capScope: "device",
|
|
@@ -38976,12 +39119,36 @@ Object.freeze({
|
|
|
38976
39119
|
addonId: null,
|
|
38977
39120
|
access: "create"
|
|
38978
39121
|
},
|
|
39122
|
+
"connectivity.getStatus": {
|
|
39123
|
+
capName: "connectivity",
|
|
39124
|
+
capScope: "device",
|
|
39125
|
+
addonId: null,
|
|
39126
|
+
access: "view"
|
|
39127
|
+
},
|
|
39128
|
+
"consumables.getStatus": {
|
|
39129
|
+
capName: "consumables",
|
|
39130
|
+
capScope: "device",
|
|
39131
|
+
addonId: null,
|
|
39132
|
+
access: "view"
|
|
39133
|
+
},
|
|
38979
39134
|
"consumables.reset": {
|
|
38980
39135
|
capName: "consumables",
|
|
38981
39136
|
capScope: "device",
|
|
38982
39137
|
addonId: null,
|
|
38983
39138
|
access: "delete"
|
|
38984
39139
|
},
|
|
39140
|
+
"contact.getStatus": {
|
|
39141
|
+
capName: "contact",
|
|
39142
|
+
capScope: "device",
|
|
39143
|
+
addonId: null,
|
|
39144
|
+
access: "view"
|
|
39145
|
+
},
|
|
39146
|
+
"control.getStatus": {
|
|
39147
|
+
capName: "control",
|
|
39148
|
+
capScope: "device",
|
|
39149
|
+
addonId: null,
|
|
39150
|
+
access: "view"
|
|
39151
|
+
},
|
|
38985
39152
|
"control.setValue": {
|
|
38986
39153
|
capName: "control",
|
|
38987
39154
|
capScope: "device",
|
|
@@ -39048,6 +39215,12 @@ Object.freeze({
|
|
|
39048
39215
|
addonId: null,
|
|
39049
39216
|
access: "create"
|
|
39050
39217
|
},
|
|
39218
|
+
"cover.getStatus": {
|
|
39219
|
+
capName: "cover",
|
|
39220
|
+
capScope: "device",
|
|
39221
|
+
addonId: null,
|
|
39222
|
+
access: "view"
|
|
39223
|
+
},
|
|
39051
39224
|
"cover.open": {
|
|
39052
39225
|
capName: "cover",
|
|
39053
39226
|
capScope: "device",
|
|
@@ -39180,6 +39353,12 @@ Object.freeze({
|
|
|
39180
39353
|
addonId: null,
|
|
39181
39354
|
access: "view"
|
|
39182
39355
|
},
|
|
39356
|
+
"dayNight.getStatus": {
|
|
39357
|
+
capName: "day-night",
|
|
39358
|
+
capScope: "device",
|
|
39359
|
+
addonId: null,
|
|
39360
|
+
access: "view"
|
|
39361
|
+
},
|
|
39183
39362
|
"dayNight.setSettings": {
|
|
39184
39363
|
capName: "day-night",
|
|
39185
39364
|
capScope: "device",
|
|
@@ -39270,6 +39449,24 @@ Object.freeze({
|
|
|
39270
39449
|
addonId: null,
|
|
39271
39450
|
access: "view"
|
|
39272
39451
|
},
|
|
39452
|
+
"detectionPipeline.applyDeviceSettingsPatch": {
|
|
39453
|
+
capName: "detection-pipeline",
|
|
39454
|
+
capScope: "device",
|
|
39455
|
+
addonId: null,
|
|
39456
|
+
access: "create"
|
|
39457
|
+
},
|
|
39458
|
+
"detectionPipeline.getDeviceLiveContribution": {
|
|
39459
|
+
capName: "detection-pipeline",
|
|
39460
|
+
capScope: "device",
|
|
39461
|
+
addonId: null,
|
|
39462
|
+
access: "view"
|
|
39463
|
+
},
|
|
39464
|
+
"detectionPipeline.getDeviceSettingsContribution": {
|
|
39465
|
+
capName: "detection-pipeline",
|
|
39466
|
+
capScope: "device",
|
|
39467
|
+
addonId: null,
|
|
39468
|
+
access: "view"
|
|
39469
|
+
},
|
|
39273
39470
|
"deviceAdoption.adopt": {
|
|
39274
39471
|
capName: "device-adoption",
|
|
39275
39472
|
capScope: "system",
|
|
@@ -39282,6 +39479,12 @@ Object.freeze({
|
|
|
39282
39479
|
addonId: null,
|
|
39283
39480
|
access: "view"
|
|
39284
39481
|
},
|
|
39482
|
+
"deviceAdoption.getStatus": {
|
|
39483
|
+
capName: "device-adoption",
|
|
39484
|
+
capScope: "system",
|
|
39485
|
+
addonId: null,
|
|
39486
|
+
access: "view"
|
|
39487
|
+
},
|
|
39285
39488
|
"deviceAdoption.listCandidateFilters": {
|
|
39286
39489
|
capName: "device-adoption",
|
|
39287
39490
|
capScope: "system",
|
|
@@ -39318,6 +39521,12 @@ Object.freeze({
|
|
|
39318
39521
|
addonId: null,
|
|
39319
39522
|
access: "create"
|
|
39320
39523
|
},
|
|
39524
|
+
"deviceDiscovery.getStatus": {
|
|
39525
|
+
capName: "device-discovery",
|
|
39526
|
+
capScope: "device",
|
|
39527
|
+
addonId: null,
|
|
39528
|
+
access: "view"
|
|
39529
|
+
},
|
|
39321
39530
|
"deviceDiscovery.listDiscovered": {
|
|
39322
39531
|
capName: "device-discovery",
|
|
39323
39532
|
capScope: "device",
|
|
@@ -39336,12 +39545,30 @@ Object.freeze({
|
|
|
39336
39545
|
addonId: null,
|
|
39337
39546
|
access: "create"
|
|
39338
39547
|
},
|
|
39548
|
+
"deviceExport.applyDeviceSettingsPatch": {
|
|
39549
|
+
capName: "device-export",
|
|
39550
|
+
capScope: "system",
|
|
39551
|
+
addonId: null,
|
|
39552
|
+
access: "create"
|
|
39553
|
+
},
|
|
39339
39554
|
"deviceExport.exposeDevice": {
|
|
39340
39555
|
capName: "device-export",
|
|
39341
39556
|
capScope: "system",
|
|
39342
39557
|
addonId: null,
|
|
39343
39558
|
access: "create"
|
|
39344
39559
|
},
|
|
39560
|
+
"deviceExport.getDeviceLiveContribution": {
|
|
39561
|
+
capName: "device-export",
|
|
39562
|
+
capScope: "system",
|
|
39563
|
+
addonId: null,
|
|
39564
|
+
access: "view"
|
|
39565
|
+
},
|
|
39566
|
+
"deviceExport.getDeviceSettingsContribution": {
|
|
39567
|
+
capName: "device-export",
|
|
39568
|
+
capScope: "system",
|
|
39569
|
+
addonId: null,
|
|
39570
|
+
access: "view"
|
|
39571
|
+
},
|
|
39345
39572
|
"deviceExport.getStatus": {
|
|
39346
39573
|
capName: "device-export",
|
|
39347
39574
|
capScope: "system",
|
|
@@ -39438,6 +39665,12 @@ Object.freeze({
|
|
|
39438
39665
|
addonId: null,
|
|
39439
39666
|
access: "create"
|
|
39440
39667
|
},
|
|
39668
|
+
"deviceManager.applyDeviceSettingsPatch": {
|
|
39669
|
+
capName: "device-manager",
|
|
39670
|
+
capScope: "system",
|
|
39671
|
+
addonId: null,
|
|
39672
|
+
access: "create"
|
|
39673
|
+
},
|
|
39441
39674
|
"deviceManager.applyInitialMeta": {
|
|
39442
39675
|
capName: "device-manager",
|
|
39443
39676
|
capScope: "system",
|
|
@@ -39540,6 +39773,12 @@ Object.freeze({
|
|
|
39540
39773
|
addonId: null,
|
|
39541
39774
|
access: "view"
|
|
39542
39775
|
},
|
|
39776
|
+
"deviceManager.getDeviceLiveContribution": {
|
|
39777
|
+
capName: "device-manager",
|
|
39778
|
+
capScope: "system",
|
|
39779
|
+
addonId: null,
|
|
39780
|
+
access: "view"
|
|
39781
|
+
},
|
|
39543
39782
|
"deviceManager.getDeviceLiveInfoAggregate": {
|
|
39544
39783
|
capName: "device-manager",
|
|
39545
39784
|
capScope: "system",
|
|
@@ -39552,6 +39791,12 @@ Object.freeze({
|
|
|
39552
39791
|
addonId: null,
|
|
39553
39792
|
access: "view"
|
|
39554
39793
|
},
|
|
39794
|
+
"deviceManager.getDeviceSettingsContribution": {
|
|
39795
|
+
capName: "device-manager",
|
|
39796
|
+
capScope: "system",
|
|
39797
|
+
addonId: null,
|
|
39798
|
+
access: "view"
|
|
39799
|
+
},
|
|
39555
39800
|
"deviceManager.getDeviceStatusAggregate": {
|
|
39556
39801
|
capName: "device-manager",
|
|
39557
39802
|
capScope: "system",
|
|
@@ -40044,6 +40289,36 @@ Object.freeze({
|
|
|
40044
40289
|
addonId: null,
|
|
40045
40290
|
access: "create"
|
|
40046
40291
|
},
|
|
40292
|
+
"deviceStatus.getStatus": {
|
|
40293
|
+
capName: "device-status",
|
|
40294
|
+
capScope: "device",
|
|
40295
|
+
addonId: null,
|
|
40296
|
+
access: "view"
|
|
40297
|
+
},
|
|
40298
|
+
"doorbell.applyDeviceSettingsPatch": {
|
|
40299
|
+
capName: "doorbell",
|
|
40300
|
+
capScope: "device",
|
|
40301
|
+
addonId: null,
|
|
40302
|
+
access: "create"
|
|
40303
|
+
},
|
|
40304
|
+
"doorbell.getDeviceLiveContribution": {
|
|
40305
|
+
capName: "doorbell",
|
|
40306
|
+
capScope: "device",
|
|
40307
|
+
addonId: null,
|
|
40308
|
+
access: "view"
|
|
40309
|
+
},
|
|
40310
|
+
"doorbell.getDeviceSettingsContribution": {
|
|
40311
|
+
capName: "doorbell",
|
|
40312
|
+
capScope: "device",
|
|
40313
|
+
addonId: null,
|
|
40314
|
+
access: "view"
|
|
40315
|
+
},
|
|
40316
|
+
"doorbell.getStatus": {
|
|
40317
|
+
capName: "doorbell",
|
|
40318
|
+
capScope: "device",
|
|
40319
|
+
addonId: null,
|
|
40320
|
+
access: "view"
|
|
40321
|
+
},
|
|
40047
40322
|
"embeddingEncoder.encode": {
|
|
40048
40323
|
capName: "embedding-encoder",
|
|
40049
40324
|
capScope: "system",
|
|
@@ -40062,6 +40337,18 @@ Object.freeze({
|
|
|
40062
40337
|
addonId: null,
|
|
40063
40338
|
access: "view"
|
|
40064
40339
|
},
|
|
40340
|
+
"enumSensor.getStatus": {
|
|
40341
|
+
capName: "enum-sensor",
|
|
40342
|
+
capScope: "device",
|
|
40343
|
+
addonId: null,
|
|
40344
|
+
access: "view"
|
|
40345
|
+
},
|
|
40346
|
+
"eventEmitter.getStatus": {
|
|
40347
|
+
capName: "event-emitter",
|
|
40348
|
+
capScope: "device",
|
|
40349
|
+
addonId: null,
|
|
40350
|
+
access: "view"
|
|
40351
|
+
},
|
|
40065
40352
|
"events.getEventClipUrl": {
|
|
40066
40353
|
capName: "events",
|
|
40067
40354
|
capScope: "device",
|
|
@@ -40182,6 +40469,12 @@ Object.freeze({
|
|
|
40182
40469
|
addonId: null,
|
|
40183
40470
|
access: "view"
|
|
40184
40471
|
},
|
|
40472
|
+
"fanControl.getStatus": {
|
|
40473
|
+
capName: "fan-control",
|
|
40474
|
+
capScope: "device",
|
|
40475
|
+
addonId: null,
|
|
40476
|
+
access: "view"
|
|
40477
|
+
},
|
|
40185
40478
|
"fanControl.setDirection": {
|
|
40186
40479
|
capName: "fan-control",
|
|
40187
40480
|
capScope: "device",
|
|
@@ -40206,6 +40499,12 @@ Object.freeze({
|
|
|
40206
40499
|
addonId: null,
|
|
40207
40500
|
access: "create"
|
|
40208
40501
|
},
|
|
40502
|
+
"featureProbe.getStatus": {
|
|
40503
|
+
capName: "feature-probe",
|
|
40504
|
+
capScope: "device",
|
|
40505
|
+
addonId: null,
|
|
40506
|
+
access: "view"
|
|
40507
|
+
},
|
|
40209
40508
|
"filesystemBrowse.browse": {
|
|
40210
40509
|
capName: "filesystem-browse",
|
|
40211
40510
|
capScope: "system",
|
|
@@ -40224,6 +40523,24 @@ Object.freeze({
|
|
|
40224
40523
|
addonId: null,
|
|
40225
40524
|
access: "view"
|
|
40226
40525
|
},
|
|
40526
|
+
"flood.getStatus": {
|
|
40527
|
+
capName: "flood",
|
|
40528
|
+
capScope: "device",
|
|
40529
|
+
addonId: null,
|
|
40530
|
+
access: "view"
|
|
40531
|
+
},
|
|
40532
|
+
"gas.getStatus": {
|
|
40533
|
+
capName: "gas",
|
|
40534
|
+
capScope: "device",
|
|
40535
|
+
addonId: null,
|
|
40536
|
+
access: "view"
|
|
40537
|
+
},
|
|
40538
|
+
"humidifier.getStatus": {
|
|
40539
|
+
capName: "humidifier",
|
|
40540
|
+
capScope: "device",
|
|
40541
|
+
addonId: null,
|
|
40542
|
+
access: "view"
|
|
40543
|
+
},
|
|
40227
40544
|
"humidifier.setMode": {
|
|
40228
40545
|
capName: "humidifier",
|
|
40229
40546
|
capScope: "device",
|
|
@@ -40242,12 +40559,30 @@ Object.freeze({
|
|
|
40242
40559
|
addonId: null,
|
|
40243
40560
|
access: "create"
|
|
40244
40561
|
},
|
|
40562
|
+
"humiditySensor.getStatus": {
|
|
40563
|
+
capName: "humidity-sensor",
|
|
40564
|
+
capScope: "device",
|
|
40565
|
+
addonId: null,
|
|
40566
|
+
access: "view"
|
|
40567
|
+
},
|
|
40568
|
+
"image.getStatus": {
|
|
40569
|
+
capName: "image",
|
|
40570
|
+
capScope: "device",
|
|
40571
|
+
addonId: null,
|
|
40572
|
+
access: "view"
|
|
40573
|
+
},
|
|
40245
40574
|
"imageSettings.getOptions": {
|
|
40246
40575
|
capName: "image-settings",
|
|
40247
40576
|
capScope: "device",
|
|
40248
40577
|
addonId: null,
|
|
40249
40578
|
access: "view"
|
|
40250
40579
|
},
|
|
40580
|
+
"imageSettings.getStatus": {
|
|
40581
|
+
capName: "image-settings",
|
|
40582
|
+
capScope: "device",
|
|
40583
|
+
addonId: null,
|
|
40584
|
+
access: "view"
|
|
40585
|
+
},
|
|
40251
40586
|
"imageSettings.setSettings": {
|
|
40252
40587
|
capName: "image-settings",
|
|
40253
40588
|
capScope: "device",
|
|
@@ -40320,6 +40655,12 @@ Object.freeze({
|
|
|
40320
40655
|
addonId: null,
|
|
40321
40656
|
access: "create"
|
|
40322
40657
|
},
|
|
40658
|
+
"intercom.getStatus": {
|
|
40659
|
+
capName: "intercom",
|
|
40660
|
+
capScope: "device",
|
|
40661
|
+
addonId: null,
|
|
40662
|
+
access: "view"
|
|
40663
|
+
},
|
|
40323
40664
|
"intercom.handleAnswer": {
|
|
40324
40665
|
capName: "intercom",
|
|
40325
40666
|
capScope: "device",
|
|
@@ -40356,6 +40697,12 @@ Object.freeze({
|
|
|
40356
40697
|
addonId: null,
|
|
40357
40698
|
access: "create"
|
|
40358
40699
|
},
|
|
40700
|
+
"lawnMowerControl.getStatus": {
|
|
40701
|
+
capName: "lawn-mower-control",
|
|
40702
|
+
capScope: "device",
|
|
40703
|
+
addonId: null,
|
|
40704
|
+
access: "view"
|
|
40705
|
+
},
|
|
40359
40706
|
"lawnMowerControl.pause": {
|
|
40360
40707
|
capName: "lawn-mower-control",
|
|
40361
40708
|
capScope: "device",
|
|
@@ -40638,6 +40985,12 @@ Object.freeze({
|
|
|
40638
40985
|
addonId: null,
|
|
40639
40986
|
access: "create"
|
|
40640
40987
|
},
|
|
40988
|
+
"lockControl.getStatus": {
|
|
40989
|
+
capName: "lock-control",
|
|
40990
|
+
capScope: "device",
|
|
40991
|
+
addonId: null,
|
|
40992
|
+
access: "view"
|
|
40993
|
+
},
|
|
40641
40994
|
"lockControl.lock": {
|
|
40642
40995
|
capName: "lock-control",
|
|
40643
40996
|
capScope: "device",
|
|
@@ -40686,6 +41039,12 @@ Object.freeze({
|
|
|
40686
41039
|
addonId: null,
|
|
40687
41040
|
access: "view"
|
|
40688
41041
|
},
|
|
41042
|
+
"mediaPlayer.getStatus": {
|
|
41043
|
+
capName: "media-player",
|
|
41044
|
+
capScope: "device",
|
|
41045
|
+
addonId: null,
|
|
41046
|
+
access: "view"
|
|
41047
|
+
},
|
|
40689
41048
|
"mediaPlayer.next": {
|
|
40690
41049
|
capName: "media-player",
|
|
40691
41050
|
capScope: "device",
|
|
@@ -40878,6 +41237,12 @@ Object.freeze({
|
|
|
40878
41237
|
addonId: null,
|
|
40879
41238
|
access: "create"
|
|
40880
41239
|
},
|
|
41240
|
+
"motion.getStatus": {
|
|
41241
|
+
capName: "motion",
|
|
41242
|
+
capScope: "device",
|
|
41243
|
+
addonId: null,
|
|
41244
|
+
access: "view"
|
|
41245
|
+
},
|
|
40881
41246
|
"motion.isDetected": {
|
|
40882
41247
|
capName: "motion",
|
|
40883
41248
|
capScope: "device",
|
|
@@ -40890,6 +41255,24 @@ Object.freeze({
|
|
|
40890
41255
|
addonId: null,
|
|
40891
41256
|
access: "create"
|
|
40892
41257
|
},
|
|
41258
|
+
"motionDetection.applyDeviceSettingsPatch": {
|
|
41259
|
+
capName: "motion-detection",
|
|
41260
|
+
capScope: "device",
|
|
41261
|
+
addonId: null,
|
|
41262
|
+
access: "create"
|
|
41263
|
+
},
|
|
41264
|
+
"motionDetection.getDeviceLiveContribution": {
|
|
41265
|
+
capName: "motion-detection",
|
|
41266
|
+
capScope: "device",
|
|
41267
|
+
addonId: null,
|
|
41268
|
+
access: "view"
|
|
41269
|
+
},
|
|
41270
|
+
"motionDetection.getDeviceSettingsContribution": {
|
|
41271
|
+
capName: "motion-detection",
|
|
41272
|
+
capScope: "device",
|
|
41273
|
+
addonId: null,
|
|
41274
|
+
access: "view"
|
|
41275
|
+
},
|
|
40893
41276
|
"motionDetection.removeCamera": {
|
|
40894
41277
|
capName: "motion-detection",
|
|
40895
41278
|
capScope: "device",
|
|
@@ -40902,6 +41285,12 @@ Object.freeze({
|
|
|
40902
41285
|
addonId: null,
|
|
40903
41286
|
access: "delete"
|
|
40904
41287
|
},
|
|
41288
|
+
"motionTrigger.getStatus": {
|
|
41289
|
+
capName: "motion-trigger",
|
|
41290
|
+
capScope: "device",
|
|
41291
|
+
addonId: null,
|
|
41292
|
+
access: "view"
|
|
41293
|
+
},
|
|
40905
41294
|
"motionTrigger.setMotionTrigger": {
|
|
40906
41295
|
capName: "motion-trigger",
|
|
40907
41296
|
capScope: "device",
|
|
@@ -40914,6 +41303,12 @@ Object.freeze({
|
|
|
40914
41303
|
addonId: null,
|
|
40915
41304
|
access: "view"
|
|
40916
41305
|
},
|
|
41306
|
+
"motionZones.getStatus": {
|
|
41307
|
+
capName: "motion-zones",
|
|
41308
|
+
capScope: "device",
|
|
41309
|
+
addonId: null,
|
|
41310
|
+
access: "view"
|
|
41311
|
+
},
|
|
40917
41312
|
"motionZones.setZone": {
|
|
40918
41313
|
capName: "motion-zones",
|
|
40919
41314
|
capScope: "device",
|
|
@@ -40974,6 +41369,12 @@ Object.freeze({
|
|
|
40974
41369
|
addonId: null,
|
|
40975
41370
|
access: "view"
|
|
40976
41371
|
},
|
|
41372
|
+
"nativeObjectDetection.getStatus": {
|
|
41373
|
+
capName: "native-object-detection",
|
|
41374
|
+
capScope: "device",
|
|
41375
|
+
addonId: null,
|
|
41376
|
+
access: "view"
|
|
41377
|
+
},
|
|
40977
41378
|
"nativeObjectDetection.setEnabled": {
|
|
40978
41379
|
capName: "native-object-detection",
|
|
40979
41380
|
capScope: "device",
|
|
@@ -40992,6 +41393,12 @@ Object.freeze({
|
|
|
40992
41393
|
addonId: null,
|
|
40993
41394
|
access: "view"
|
|
40994
41395
|
},
|
|
41396
|
+
"navigation.getStatus": {
|
|
41397
|
+
capName: "navigation",
|
|
41398
|
+
capScope: "device",
|
|
41399
|
+
addonId: null,
|
|
41400
|
+
access: "view"
|
|
41401
|
+
},
|
|
40995
41402
|
"navigation.goToPoint": {
|
|
40996
41403
|
capName: "navigation",
|
|
40997
41404
|
capScope: "device",
|
|
@@ -41076,6 +41483,12 @@ Object.freeze({
|
|
|
41076
41483
|
addonId: null,
|
|
41077
41484
|
access: "create"
|
|
41078
41485
|
},
|
|
41486
|
+
"networkLink.getStatus": {
|
|
41487
|
+
capName: "network-link",
|
|
41488
|
+
capScope: "device",
|
|
41489
|
+
addonId: null,
|
|
41490
|
+
access: "view"
|
|
41491
|
+
},
|
|
41079
41492
|
"networkQuality.getAllStats": {
|
|
41080
41493
|
capName: "network-quality",
|
|
41081
41494
|
capScope: "system",
|
|
@@ -41334,18 +41747,36 @@ Object.freeze({
|
|
|
41334
41747
|
addonId: null,
|
|
41335
41748
|
access: "create"
|
|
41336
41749
|
},
|
|
41750
|
+
"notifier.getStatus": {
|
|
41751
|
+
capName: "notifier",
|
|
41752
|
+
capScope: "device",
|
|
41753
|
+
addonId: null,
|
|
41754
|
+
access: "view"
|
|
41755
|
+
},
|
|
41337
41756
|
"notifier.send": {
|
|
41338
41757
|
capName: "notifier",
|
|
41339
41758
|
capScope: "device",
|
|
41340
41759
|
addonId: null,
|
|
41341
41760
|
access: "create"
|
|
41342
41761
|
},
|
|
41762
|
+
"numericSensor.getStatus": {
|
|
41763
|
+
capName: "numeric-sensor",
|
|
41764
|
+
capScope: "device",
|
|
41765
|
+
addonId: null,
|
|
41766
|
+
access: "view"
|
|
41767
|
+
},
|
|
41343
41768
|
"oauthIntegration.getDescriptor": {
|
|
41344
41769
|
capName: "oauth-integration",
|
|
41345
41770
|
capScope: "system",
|
|
41346
41771
|
addonId: null,
|
|
41347
41772
|
access: "view"
|
|
41348
41773
|
},
|
|
41774
|
+
"osd.getStatus": {
|
|
41775
|
+
capName: "osd",
|
|
41776
|
+
capScope: "device",
|
|
41777
|
+
addonId: null,
|
|
41778
|
+
access: "view"
|
|
41779
|
+
},
|
|
41349
41780
|
"osd.setOverlay": {
|
|
41350
41781
|
capName: "osd",
|
|
41351
41782
|
capScope: "device",
|
|
@@ -41418,6 +41849,12 @@ Object.freeze({
|
|
|
41418
41849
|
addonId: null,
|
|
41419
41850
|
access: "create"
|
|
41420
41851
|
},
|
|
41852
|
+
"petFeeder.getStatus": {
|
|
41853
|
+
capName: "pet-feeder",
|
|
41854
|
+
capScope: "device",
|
|
41855
|
+
addonId: null,
|
|
41856
|
+
access: "view"
|
|
41857
|
+
},
|
|
41421
41858
|
"petFeeder.markFoodReplenished": {
|
|
41422
41859
|
capName: "pet-feeder",
|
|
41423
41860
|
capScope: "device",
|
|
@@ -41460,6 +41897,12 @@ Object.freeze({
|
|
|
41460
41897
|
addonId: null,
|
|
41461
41898
|
access: "create"
|
|
41462
41899
|
},
|
|
41900
|
+
"pipelineAnalytics.applyDeviceSettingsPatch": {
|
|
41901
|
+
capName: "pipeline-analytics",
|
|
41902
|
+
capScope: "device",
|
|
41903
|
+
addonId: null,
|
|
41904
|
+
access: "create"
|
|
41905
|
+
},
|
|
41463
41906
|
"pipelineAnalytics.cancelRelocateMedia": {
|
|
41464
41907
|
capName: "pipeline-analytics",
|
|
41465
41908
|
capScope: "device",
|
|
@@ -41526,6 +41969,18 @@ Object.freeze({
|
|
|
41526
41969
|
addonId: null,
|
|
41527
41970
|
access: "view"
|
|
41528
41971
|
},
|
|
41972
|
+
"pipelineAnalytics.getDeviceLiveContribution": {
|
|
41973
|
+
capName: "pipeline-analytics",
|
|
41974
|
+
capScope: "device",
|
|
41975
|
+
addonId: null,
|
|
41976
|
+
access: "view"
|
|
41977
|
+
},
|
|
41978
|
+
"pipelineAnalytics.getDeviceSettingsContribution": {
|
|
41979
|
+
capName: "pipeline-analytics",
|
|
41980
|
+
capScope: "device",
|
|
41981
|
+
addonId: null,
|
|
41982
|
+
access: "view"
|
|
41983
|
+
},
|
|
41529
41984
|
"pipelineAnalytics.getEventDensity": {
|
|
41530
41985
|
capName: "pipeline-analytics",
|
|
41531
41986
|
capScope: "device",
|
|
@@ -42096,6 +42551,12 @@ Object.freeze({
|
|
|
42096
42551
|
addonId: null,
|
|
42097
42552
|
access: "view"
|
|
42098
42553
|
},
|
|
42554
|
+
"pipelineOrchestrator.applyDeviceSettingsPatch": {
|
|
42555
|
+
capName: "pipeline-orchestrator",
|
|
42556
|
+
capScope: "system",
|
|
42557
|
+
addonId: null,
|
|
42558
|
+
access: "create"
|
|
42559
|
+
},
|
|
42099
42560
|
"pipelineOrchestrator.assignAudio": {
|
|
42100
42561
|
capName: "pipeline-orchestrator",
|
|
42101
42562
|
capScope: "system",
|
|
@@ -42186,6 +42647,18 @@ Object.freeze({
|
|
|
42186
42647
|
addonId: null,
|
|
42187
42648
|
access: "view"
|
|
42188
42649
|
},
|
|
42650
|
+
"pipelineOrchestrator.getDeviceLiveContribution": {
|
|
42651
|
+
capName: "pipeline-orchestrator",
|
|
42652
|
+
capScope: "system",
|
|
42653
|
+
addonId: null,
|
|
42654
|
+
access: "view"
|
|
42655
|
+
},
|
|
42656
|
+
"pipelineOrchestrator.getDeviceSettingsContribution": {
|
|
42657
|
+
capName: "pipeline-orchestrator",
|
|
42658
|
+
capScope: "system",
|
|
42659
|
+
addonId: null,
|
|
42660
|
+
access: "view"
|
|
42661
|
+
},
|
|
42189
42662
|
"pipelineOrchestrator.getGlobalMetrics": {
|
|
42190
42663
|
capName: "pipeline-orchestrator",
|
|
42191
42664
|
capScope: "system",
|
|
@@ -42594,12 +43067,36 @@ Object.freeze({
|
|
|
42594
43067
|
addonId: null,
|
|
42595
43068
|
access: "view"
|
|
42596
43069
|
},
|
|
43070
|
+
"powerMeter.getStatus": {
|
|
43071
|
+
capName: "power-meter",
|
|
43072
|
+
capScope: "device",
|
|
43073
|
+
addonId: null,
|
|
43074
|
+
access: "view"
|
|
43075
|
+
},
|
|
43076
|
+
"presence.getStatus": {
|
|
43077
|
+
capName: "presence",
|
|
43078
|
+
capScope: "device",
|
|
43079
|
+
addonId: null,
|
|
43080
|
+
access: "view"
|
|
43081
|
+
},
|
|
43082
|
+
"pressureSensor.getStatus": {
|
|
43083
|
+
capName: "pressure-sensor",
|
|
43084
|
+
capScope: "device",
|
|
43085
|
+
addonId: null,
|
|
43086
|
+
access: "view"
|
|
43087
|
+
},
|
|
42597
43088
|
"privacyMask.getOptions": {
|
|
42598
43089
|
capName: "privacy-mask",
|
|
42599
43090
|
capScope: "device",
|
|
42600
43091
|
addonId: null,
|
|
42601
43092
|
access: "view"
|
|
42602
43093
|
},
|
|
43094
|
+
"privacyMask.getStatus": {
|
|
43095
|
+
capName: "privacy-mask",
|
|
43096
|
+
capScope: "device",
|
|
43097
|
+
addonId: null,
|
|
43098
|
+
access: "view"
|
|
43099
|
+
},
|
|
42603
43100
|
"privacyMask.setAudioEnabled": {
|
|
42604
43101
|
capName: "privacy-mask",
|
|
42605
43102
|
capScope: "device",
|
|
@@ -42660,6 +43157,12 @@ Object.freeze({
|
|
|
42660
43157
|
addonId: null,
|
|
42661
43158
|
access: "view"
|
|
42662
43159
|
},
|
|
43160
|
+
"ptz.getStatus": {
|
|
43161
|
+
capName: "ptz",
|
|
43162
|
+
capScope: "device",
|
|
43163
|
+
addonId: null,
|
|
43164
|
+
access: "view"
|
|
43165
|
+
},
|
|
42663
43166
|
"ptz.goHome": {
|
|
42664
43167
|
capName: "ptz",
|
|
42665
43168
|
capScope: "device",
|
|
@@ -42738,6 +43241,12 @@ Object.freeze({
|
|
|
42738
43241
|
addonId: null,
|
|
42739
43242
|
access: "create"
|
|
42740
43243
|
},
|
|
43244
|
+
"recording.applyDeviceSettingsPatch": {
|
|
43245
|
+
capName: "recording",
|
|
43246
|
+
capScope: "system",
|
|
43247
|
+
addonId: null,
|
|
43248
|
+
access: "create"
|
|
43249
|
+
},
|
|
42741
43250
|
"recording.cancelRelocateJob": {
|
|
42742
43251
|
capName: "recording",
|
|
42743
43252
|
capScope: "system",
|
|
@@ -42786,6 +43295,18 @@ Object.freeze({
|
|
|
42786
43295
|
addonId: null,
|
|
42787
43296
|
access: "view"
|
|
42788
43297
|
},
|
|
43298
|
+
"recording.getDeviceLiveContribution": {
|
|
43299
|
+
capName: "recording",
|
|
43300
|
+
capScope: "system",
|
|
43301
|
+
addonId: null,
|
|
43302
|
+
access: "view"
|
|
43303
|
+
},
|
|
43304
|
+
"recording.getDeviceSettingsContribution": {
|
|
43305
|
+
capName: "recording",
|
|
43306
|
+
capScope: "system",
|
|
43307
|
+
addonId: null,
|
|
43308
|
+
access: "view"
|
|
43309
|
+
},
|
|
42789
43310
|
"recording.getPlacement": {
|
|
42790
43311
|
capName: "recording",
|
|
42791
43312
|
capScope: "system",
|
|
@@ -42804,6 +43325,12 @@ Object.freeze({
|
|
|
42804
43325
|
addonId: null,
|
|
42805
43326
|
access: "view"
|
|
42806
43327
|
},
|
|
43328
|
+
"recording.getStatus": {
|
|
43329
|
+
capName: "recording",
|
|
43330
|
+
capScope: "system",
|
|
43331
|
+
addonId: null,
|
|
43332
|
+
access: "view"
|
|
43333
|
+
},
|
|
42807
43334
|
"recording.getStorageMigrationMoveStatus": {
|
|
42808
43335
|
capName: "recording",
|
|
42809
43336
|
capScope: "system",
|
|
@@ -43008,6 +43535,12 @@ Object.freeze({
|
|
|
43008
43535
|
addonId: null,
|
|
43009
43536
|
access: "delete"
|
|
43010
43537
|
},
|
|
43538
|
+
"sceneMonitor.getStatus": {
|
|
43539
|
+
capName: "scene-monitor",
|
|
43540
|
+
capScope: "device",
|
|
43541
|
+
addonId: null,
|
|
43542
|
+
access: "view"
|
|
43543
|
+
},
|
|
43011
43544
|
"sceneMonitor.listScenes": {
|
|
43012
43545
|
capName: "scene-monitor",
|
|
43013
43546
|
capScope: "device",
|
|
@@ -43038,6 +43571,12 @@ Object.freeze({
|
|
|
43038
43571
|
addonId: null,
|
|
43039
43572
|
access: "create"
|
|
43040
43573
|
},
|
|
43574
|
+
"scriptRunner.getStatus": {
|
|
43575
|
+
capName: "script-runner",
|
|
43576
|
+
capScope: "device",
|
|
43577
|
+
addonId: null,
|
|
43578
|
+
access: "view"
|
|
43579
|
+
},
|
|
43041
43580
|
"scriptRunner.run": {
|
|
43042
43581
|
capName: "script-runner",
|
|
43043
43582
|
capScope: "device",
|
|
@@ -43170,6 +43709,12 @@ Object.freeze({
|
|
|
43170
43709
|
addonId: null,
|
|
43171
43710
|
access: "create"
|
|
43172
43711
|
},
|
|
43712
|
+
"smoke.getStatus": {
|
|
43713
|
+
capName: "smoke",
|
|
43714
|
+
capScope: "device",
|
|
43715
|
+
addonId: null,
|
|
43716
|
+
access: "view"
|
|
43717
|
+
},
|
|
43173
43718
|
"smtpProvider.getStatus": {
|
|
43174
43719
|
capName: "smtp-provider",
|
|
43175
43720
|
capScope: "system",
|
|
@@ -43188,12 +43733,30 @@ Object.freeze({
|
|
|
43188
43733
|
addonId: null,
|
|
43189
43734
|
access: "view"
|
|
43190
43735
|
},
|
|
43736
|
+
"snapshot.applyDeviceSettingsPatch": {
|
|
43737
|
+
capName: "snapshot",
|
|
43738
|
+
capScope: "device",
|
|
43739
|
+
addonId: null,
|
|
43740
|
+
access: "create"
|
|
43741
|
+
},
|
|
43191
43742
|
"snapshot.getDebugState": {
|
|
43192
43743
|
capName: "snapshot",
|
|
43193
43744
|
capScope: "device",
|
|
43194
43745
|
addonId: null,
|
|
43195
43746
|
access: "view"
|
|
43196
43747
|
},
|
|
43748
|
+
"snapshot.getDeviceLiveContribution": {
|
|
43749
|
+
capName: "snapshot",
|
|
43750
|
+
capScope: "device",
|
|
43751
|
+
addonId: null,
|
|
43752
|
+
access: "view"
|
|
43753
|
+
},
|
|
43754
|
+
"snapshot.getDeviceSettingsContribution": {
|
|
43755
|
+
capName: "snapshot",
|
|
43756
|
+
capScope: "device",
|
|
43757
|
+
addonId: null,
|
|
43758
|
+
access: "view"
|
|
43759
|
+
},
|
|
43197
43760
|
"snapshot.getSnapshot": {
|
|
43198
43761
|
capName: "snapshot",
|
|
43199
43762
|
capScope: "device",
|
|
@@ -43212,6 +43775,12 @@ Object.freeze({
|
|
|
43212
43775
|
addonId: null,
|
|
43213
43776
|
access: "view"
|
|
43214
43777
|
},
|
|
43778
|
+
"snapshot.getStatus": {
|
|
43779
|
+
capName: "snapshot",
|
|
43780
|
+
capScope: "device",
|
|
43781
|
+
addonId: null,
|
|
43782
|
+
access: "view"
|
|
43783
|
+
},
|
|
43215
43784
|
"snapshot.invalidateCache": {
|
|
43216
43785
|
capName: "snapshot",
|
|
43217
43786
|
capScope: "device",
|
|
@@ -43548,6 +44117,12 @@ Object.freeze({
|
|
|
43548
44117
|
addonId: null,
|
|
43549
44118
|
access: "create"
|
|
43550
44119
|
},
|
|
44120
|
+
"streamBroker.applyDeviceSettingsPatch": {
|
|
44121
|
+
capName: "stream-broker",
|
|
44122
|
+
capScope: "system",
|
|
44123
|
+
addonId: null,
|
|
44124
|
+
access: "create"
|
|
44125
|
+
},
|
|
43551
44126
|
"streamBroker.assignProfile": {
|
|
43552
44127
|
capName: "stream-broker",
|
|
43553
44128
|
capScope: "system",
|
|
@@ -43584,6 +44159,18 @@ Object.freeze({
|
|
|
43584
44159
|
addonId: null,
|
|
43585
44160
|
access: "view"
|
|
43586
44161
|
},
|
|
44162
|
+
"streamBroker.getDeviceLiveContribution": {
|
|
44163
|
+
capName: "stream-broker",
|
|
44164
|
+
capScope: "system",
|
|
44165
|
+
addonId: null,
|
|
44166
|
+
access: "view"
|
|
44167
|
+
},
|
|
44168
|
+
"streamBroker.getDeviceSettingsContribution": {
|
|
44169
|
+
capName: "stream-broker",
|
|
44170
|
+
capScope: "system",
|
|
44171
|
+
addonId: null,
|
|
44172
|
+
access: "view"
|
|
44173
|
+
},
|
|
43587
44174
|
"streamBroker.getPreBufferInfo": {
|
|
43588
44175
|
capName: "stream-broker",
|
|
43589
44176
|
capScope: "system",
|
|
@@ -43782,6 +44369,12 @@ Object.freeze({
|
|
|
43782
44369
|
addonId: null,
|
|
43783
44370
|
access: "view"
|
|
43784
44371
|
},
|
|
44372
|
+
"streamParams.getStatus": {
|
|
44373
|
+
capName: "stream-params",
|
|
44374
|
+
capScope: "device",
|
|
44375
|
+
addonId: null,
|
|
44376
|
+
access: "view"
|
|
44377
|
+
},
|
|
43785
44378
|
"streamParams.setProfile": {
|
|
43786
44379
|
capName: "stream-params",
|
|
43787
44380
|
capScope: "device",
|
|
@@ -43794,6 +44387,12 @@ Object.freeze({
|
|
|
43794
44387
|
addonId: null,
|
|
43795
44388
|
access: "view"
|
|
43796
44389
|
},
|
|
44390
|
+
"switch.getStatus": {
|
|
44391
|
+
capName: "switch",
|
|
44392
|
+
capScope: "device",
|
|
44393
|
+
addonId: null,
|
|
44394
|
+
access: "view"
|
|
44395
|
+
},
|
|
43797
44396
|
"switch.setState": {
|
|
43798
44397
|
capName: "switch",
|
|
43799
44398
|
capScope: "device",
|
|
@@ -43890,6 +44489,18 @@ Object.freeze({
|
|
|
43890
44489
|
addonId: null,
|
|
43891
44490
|
access: "create"
|
|
43892
44491
|
},
|
|
44492
|
+
"tamper.getStatus": {
|
|
44493
|
+
capName: "tamper",
|
|
44494
|
+
capScope: "device",
|
|
44495
|
+
addonId: null,
|
|
44496
|
+
access: "view"
|
|
44497
|
+
},
|
|
44498
|
+
"temperatureSensor.getStatus": {
|
|
44499
|
+
capName: "temperature-sensor",
|
|
44500
|
+
capScope: "device",
|
|
44501
|
+
addonId: null,
|
|
44502
|
+
access: "view"
|
|
44503
|
+
},
|
|
43893
44504
|
"terminalSession.adoptLegacyMonitor": {
|
|
43894
44505
|
capName: "terminal-session",
|
|
43895
44506
|
capScope: "system",
|
|
@@ -43986,6 +44597,12 @@ Object.freeze({
|
|
|
43986
44597
|
addonId: null,
|
|
43987
44598
|
access: "view"
|
|
43988
44599
|
},
|
|
44600
|
+
"update.getStatus": {
|
|
44601
|
+
capName: "update",
|
|
44602
|
+
capScope: "device",
|
|
44603
|
+
addonId: null,
|
|
44604
|
+
access: "view"
|
|
44605
|
+
},
|
|
43989
44606
|
"update.installUpdate": {
|
|
43990
44607
|
capName: "update",
|
|
43991
44608
|
capScope: "device",
|
|
@@ -44208,6 +44825,12 @@ Object.freeze({
|
|
|
44208
44825
|
addonId: null,
|
|
44209
44826
|
access: "create"
|
|
44210
44827
|
},
|
|
44828
|
+
"vacuumControl.getStatus": {
|
|
44829
|
+
capName: "vacuum-control",
|
|
44830
|
+
capScope: "device",
|
|
44831
|
+
addonId: null,
|
|
44832
|
+
access: "view"
|
|
44833
|
+
},
|
|
44211
44834
|
"vacuumControl.locate": {
|
|
44212
44835
|
capName: "vacuum-control",
|
|
44213
44836
|
capScope: "device",
|
|
@@ -44250,6 +44873,12 @@ Object.freeze({
|
|
|
44250
44873
|
addonId: null,
|
|
44251
44874
|
access: "create"
|
|
44252
44875
|
},
|
|
44876
|
+
"valve.getStatus": {
|
|
44877
|
+
capName: "valve",
|
|
44878
|
+
capScope: "device",
|
|
44879
|
+
addonId: null,
|
|
44880
|
+
access: "view"
|
|
44881
|
+
},
|
|
44253
44882
|
"valve.open": {
|
|
44254
44883
|
capName: "valve",
|
|
44255
44884
|
capScope: "device",
|
|
@@ -44322,6 +44951,12 @@ Object.freeze({
|
|
|
44322
44951
|
addonId: null,
|
|
44323
44952
|
access: "create"
|
|
44324
44953
|
},
|
|
44954
|
+
"vibration.getStatus": {
|
|
44955
|
+
capName: "vibration",
|
|
44956
|
+
capScope: "device",
|
|
44957
|
+
addonId: null,
|
|
44958
|
+
access: "view"
|
|
44959
|
+
},
|
|
44325
44960
|
"videoclips.getClipPlayback": {
|
|
44326
44961
|
capName: "videoclips",
|
|
44327
44962
|
capScope: "device",
|
|
@@ -44358,6 +44993,12 @@ Object.freeze({
|
|
|
44358
44993
|
addonId: null,
|
|
44359
44994
|
access: "view"
|
|
44360
44995
|
},
|
|
44996
|
+
"waterHeater.getStatus": {
|
|
44997
|
+
capName: "water-heater",
|
|
44998
|
+
capScope: "device",
|
|
44999
|
+
addonId: null,
|
|
45000
|
+
access: "view"
|
|
45001
|
+
},
|
|
44361
45002
|
"waterHeater.setAway": {
|
|
44362
45003
|
capName: "water-heater",
|
|
44363
45004
|
capScope: "device",
|
|
@@ -44376,6 +45017,12 @@ Object.freeze({
|
|
|
44376
45017
|
addonId: null,
|
|
44377
45018
|
access: "create"
|
|
44378
45019
|
},
|
|
45020
|
+
"weather.getStatus": {
|
|
45021
|
+
capName: "weather",
|
|
45022
|
+
capScope: "device",
|
|
45023
|
+
addonId: null,
|
|
45024
|
+
access: "view"
|
|
45025
|
+
},
|
|
44379
45026
|
"webrtcSession.addIceCandidate": {
|
|
44380
45027
|
capName: "webrtc-session",
|
|
44381
45028
|
capScope: "device",
|
|
@@ -76163,7 +76810,7 @@ var require_lz4 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
|
76163
76810
|
};
|
|
76164
76811
|
}));
|
|
76165
76812
|
//#endregion
|
|
76166
|
-
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-
|
|
76813
|
+
//#region node_modules/@apocaliss92/nodelink-js/dist/chunk-GNC5U3YA.js
|
|
76167
76814
|
var import_fxp = require_fxp();
|
|
76168
76815
|
var import_lz4 = /* @__PURE__ */ __toESM(require_lz4(), 1);
|
|
76169
76816
|
function encodeHeader(h) {
|
|
@@ -80328,6 +80975,7 @@ var BaichuanClient = class _BaichuanClient extends EventEmitter {
|
|
|
80328
80975
|
lockedChannelId = frame.header.channelId;
|
|
80329
80976
|
lockedStreamType = frame.header.streamType;
|
|
80330
80977
|
}
|
|
80978
|
+
params.onChunk?.(decrypted);
|
|
80331
80979
|
chunks.push(decrypted);
|
|
80332
80980
|
armIdleFinish();
|
|
80333
80981
|
if (frame.header.responseCode === 201) finish(Buffer.concat(chunks));
|
|
@@ -92334,7 +92982,8 @@ ${stderr}`));
|
|
|
92334
92982
|
payloadXml,
|
|
92335
92983
|
streamType: 0,
|
|
92336
92984
|
timeoutMs,
|
|
92337
|
-
...params.idleTimeoutMs != null ? { idleTimeoutMs: params.idleTimeoutMs } : {}
|
|
92985
|
+
...params.idleTimeoutMs != null ? { idleTimeoutMs: params.idleTimeoutMs } : {},
|
|
92986
|
+
...params.onChunk ? { onChunk: params.onChunk } : {}
|
|
92338
92987
|
});
|
|
92339
92988
|
} catch (e) {
|
|
92340
92989
|
trace(`download failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -148398,6 +149047,123 @@ function createClipPlaybackHandler(deps) {
|
|
|
148398
149047
|
}).pipe(res);
|
|
148399
149048
|
};
|
|
148400
149049
|
}
|
|
149050
|
+
/**
|
|
149051
|
+
* `/addon/provider-reolink/clip-stream/<deviceId>/<clipKey>.mp4` — the same
|
|
149052
|
+
* recording as `clip/…`, served as it ARRIVES from the camera.
|
|
149053
|
+
*
|
|
149054
|
+
* **Not `Range`-able, and it says so.** The response has no length: it is a
|
|
149055
|
+
* fragmented MP4 written by an ffmpeg that is still reading the camera when
|
|
149056
|
+
* the first byte leaves. So there is no `content-length`, `accept-ranges` is
|
|
149057
|
+
* `none`, a `Range` header is IGNORED (a 206 over something with no length
|
|
149058
|
+
* would be a lie), and the peer may not cache it (`no-store`) — a stream is
|
|
149059
|
+
* not an immutable artifact, the file route is. The status line is written
|
|
149060
|
+
* on the first muxed byte, never before: until then every refusal still owns
|
|
149061
|
+
* it, and a camera that serves nothing answers a status with a reason, not an
|
|
149062
|
+
* empty 200 (D391).
|
|
149063
|
+
*
|
|
149064
|
+
* `HEAD` answers the headers and touches no camera.
|
|
149065
|
+
*/
|
|
149066
|
+
var CLIP_STREAM_PREFIX = "clip-stream";
|
|
149067
|
+
var STREAM_HEADERS = {
|
|
149068
|
+
"content-type": "video/mp4",
|
|
149069
|
+
"accept-ranges": "none",
|
|
149070
|
+
"cache-control": "no-store",
|
|
149071
|
+
["x-camstack-clip-transport"]: "forward-only"
|
|
149072
|
+
};
|
|
149073
|
+
/** The status a refusal answers. `404` = nothing here to serve; `409` = a
|
|
149074
|
+
* clip that exists and cannot be served right now; `502` = the camera. */
|
|
149075
|
+
function streamRefusalStatus(code) {
|
|
149076
|
+
switch (code) {
|
|
149077
|
+
case "unknown-device":
|
|
149078
|
+
case "no-catalog-row": return 404;
|
|
149079
|
+
case "sleeping":
|
|
149080
|
+
case "uid-missing":
|
|
149081
|
+
case "clip-in-progress": return 409;
|
|
149082
|
+
case "camera-refused":
|
|
149083
|
+
case "fetch-failed": return 502;
|
|
149084
|
+
}
|
|
149085
|
+
}
|
|
149086
|
+
function createClipStreamHandler(deps) {
|
|
149087
|
+
return async (req, res) => {
|
|
149088
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
149089
|
+
res.writeHead(405, { allow: "GET, HEAD" });
|
|
149090
|
+
res.end();
|
|
149091
|
+
return;
|
|
149092
|
+
}
|
|
149093
|
+
const match = MEDIA_PATH.exec(pathnameOf(req));
|
|
149094
|
+
if (match === null) {
|
|
149095
|
+
res.writeHead(400);
|
|
149096
|
+
res.end();
|
|
149097
|
+
return;
|
|
149098
|
+
}
|
|
149099
|
+
const deviceId = Number(match[1]);
|
|
149100
|
+
const clipKey = match[2] ?? "";
|
|
149101
|
+
if (req.method === "HEAD") {
|
|
149102
|
+
res.writeHead(200, STREAM_HEADERS);
|
|
149103
|
+
res.end();
|
|
149104
|
+
return;
|
|
149105
|
+
}
|
|
149106
|
+
if (req.headers.range !== void 0) deps.logger.debug("videoclips: a Range header on the clip stream route is ignored", {
|
|
149107
|
+
tags: { deviceId },
|
|
149108
|
+
meta: {
|
|
149109
|
+
clipKey,
|
|
149110
|
+
range: req.headers.range
|
|
149111
|
+
}
|
|
149112
|
+
});
|
|
149113
|
+
const sink = {
|
|
149114
|
+
begin: () => {
|
|
149115
|
+
res.writeHead(200, STREAM_HEADERS);
|
|
149116
|
+
},
|
|
149117
|
+
write: (chunk) => res.write(chunk),
|
|
149118
|
+
onceDrain: (listener) => {
|
|
149119
|
+
res.once("drain", listener);
|
|
149120
|
+
},
|
|
149121
|
+
onceClose: (listener) => {
|
|
149122
|
+
res.once("close", listener);
|
|
149123
|
+
},
|
|
149124
|
+
end: () => {
|
|
149125
|
+
res.end();
|
|
149126
|
+
},
|
|
149127
|
+
destroy: () => {
|
|
149128
|
+
res.destroy();
|
|
149129
|
+
}
|
|
149130
|
+
};
|
|
149131
|
+
let outcome;
|
|
149132
|
+
try {
|
|
149133
|
+
outcome = await deps.stream(deviceId, clipKey, sink);
|
|
149134
|
+
} catch (err) {
|
|
149135
|
+
deps.logger.warn("videoclips: the clip stream route threw", {
|
|
149136
|
+
tags: { deviceId },
|
|
149137
|
+
meta: {
|
|
149138
|
+
clipKey,
|
|
149139
|
+
branch: "stream-threw",
|
|
149140
|
+
error: err instanceof Error ? err.message : String(err)
|
|
149141
|
+
}
|
|
149142
|
+
});
|
|
149143
|
+
if (res.headersSent) res.destroy();
|
|
149144
|
+
else {
|
|
149145
|
+
res.writeHead(500);
|
|
149146
|
+
res.end();
|
|
149147
|
+
}
|
|
149148
|
+
return;
|
|
149149
|
+
}
|
|
149150
|
+
if (outcome.kind !== "refused") return;
|
|
149151
|
+
deps.logger.info("videoclips: clip stream not served", {
|
|
149152
|
+
tags: { deviceId },
|
|
149153
|
+
meta: {
|
|
149154
|
+
clipKey,
|
|
149155
|
+
code: outcome.code,
|
|
149156
|
+
reason: outcome.detail,
|
|
149157
|
+
status: streamRefusalStatus(outcome.code)
|
|
149158
|
+
}
|
|
149159
|
+
});
|
|
149160
|
+
res.writeHead(streamRefusalStatus(outcome.code), {
|
|
149161
|
+
[CLIP_STILL_REASON_HEADER]: outcome.detail.slice(0, 500),
|
|
149162
|
+
[CLIP_STILL_REASON_CODE_HEADER]: outcome.code
|
|
149163
|
+
});
|
|
149164
|
+
res.end();
|
|
149165
|
+
};
|
|
149166
|
+
}
|
|
148401
149167
|
//#endregion
|
|
148402
149168
|
//#region src/clips/clip-mux.ts
|
|
148403
149169
|
/** A clip that has not transferred by here is not going to. */
|
|
@@ -148555,6 +149321,94 @@ function describeMuxFailure(run) {
|
|
|
148555
149321
|
if (run.code !== 0) return `mux-failed (exit ${String(run.code)}): ${run.stderr.slice(0, 300)}`;
|
|
148556
149322
|
return null;
|
|
148557
149323
|
}
|
|
149324
|
+
/** The fragmented container a stream is muxed into — see the file header. */
|
|
149325
|
+
var CLIP_STREAM_MOVFLAGS = "frag_keyframe+empty_moov+default_base_moof";
|
|
149326
|
+
/**
|
|
149327
|
+
* The rate the probe measured, through the library's one derivation.
|
|
149328
|
+
*
|
|
149329
|
+
* `estimateVideoTiming` falls back to the info header when the timestamps
|
|
149330
|
+
* cannot say; that fallback is REFUSED here (`fpsSource: 'unmeasured'`), as
|
|
149331
|
+
* D567 says, so a guess is never handed to `-r` under a measurement's name.
|
|
149332
|
+
*/
|
|
149333
|
+
function probeClipStreamRate(timestampsUs) {
|
|
149334
|
+
const timing = estimateVideoTiming({ timestampsUs });
|
|
149335
|
+
if (timing.fpsSource !== "timestamps" || timing.fps === null) return {
|
|
149336
|
+
fps: null,
|
|
149337
|
+
fpsSource: "unmeasured",
|
|
149338
|
+
accessUnits: timestampsUs.length
|
|
149339
|
+
};
|
|
149340
|
+
return {
|
|
149341
|
+
fps: timing.fps,
|
|
149342
|
+
fpsSource: "timestamps",
|
|
149343
|
+
accessUnits: timestampsUs.length
|
|
149344
|
+
};
|
|
149345
|
+
}
|
|
149346
|
+
/**
|
|
149347
|
+
* One ffmpeg, stdin to stdout, video only, fragmented MP4.
|
|
149348
|
+
*
|
|
149349
|
+
* Mirrors `buildClipMuxArgs` where a stream can: the demuxer is named from
|
|
149350
|
+
* the DETECTED codec (the BcMedia header lies on a hub child), `-r` is an
|
|
149351
|
+
* input option before its own `-i`, and the video is copied. Where it
|
|
149352
|
+
* cannot — `+faststart`, the audio input — the file header says why.
|
|
149353
|
+
*/
|
|
149354
|
+
function buildClipStreamMuxArgs(input) {
|
|
149355
|
+
const demuxer = input.videoType === "H265" ? "hevc" : "h264";
|
|
149356
|
+
return [
|
|
149357
|
+
"-hide_banner",
|
|
149358
|
+
"-loglevel",
|
|
149359
|
+
"error",
|
|
149360
|
+
"-nostdin",
|
|
149361
|
+
...input.fps !== null && Number.isFinite(input.fps) && input.fps > 0 ? ["-r", String(input.fps)] : [],
|
|
149362
|
+
"-f",
|
|
149363
|
+
demuxer,
|
|
149364
|
+
"-i",
|
|
149365
|
+
"pipe:0",
|
|
149366
|
+
"-c:v",
|
|
149367
|
+
"copy",
|
|
149368
|
+
"-movflags",
|
|
149369
|
+
CLIP_STREAM_MOVFLAGS,
|
|
149370
|
+
"-f",
|
|
149371
|
+
"mp4",
|
|
149372
|
+
"pipe:1"
|
|
149373
|
+
];
|
|
149374
|
+
}
|
|
149375
|
+
/**
|
|
149376
|
+
* The D447 rule as a state machine, so it can be proved without a socket.
|
|
149377
|
+
*
|
|
149378
|
+
* A `false` write ARMS the timer; further `false` writes while it is armed do
|
|
149379
|
+
* not push the deadline out. `drain` — the hop's own word that it took what
|
|
149380
|
+
* it held — disarms it. Stall fires at most once, and a disposed gate is
|
|
149381
|
+
* silent for ever.
|
|
149382
|
+
*/
|
|
149383
|
+
function createDrainGate(deps) {
|
|
149384
|
+
let handle = null;
|
|
149385
|
+
let stalled = false;
|
|
149386
|
+
const disarm = () => {
|
|
149387
|
+
if (handle !== null) clearTimeout(handle);
|
|
149388
|
+
handle = null;
|
|
149389
|
+
};
|
|
149390
|
+
return {
|
|
149391
|
+
wrote(accepted) {
|
|
149392
|
+
if (accepted || handle !== null || stalled) return;
|
|
149393
|
+
handle = setTimeout(() => {
|
|
149394
|
+
handle = null;
|
|
149395
|
+
if (stalled) return;
|
|
149396
|
+
stalled = true;
|
|
149397
|
+
deps.onStall();
|
|
149398
|
+
}, deps.timeoutMs);
|
|
149399
|
+
},
|
|
149400
|
+
drained() {
|
|
149401
|
+
disarm();
|
|
149402
|
+
},
|
|
149403
|
+
waiting() {
|
|
149404
|
+
return handle !== null;
|
|
149405
|
+
},
|
|
149406
|
+
dispose() {
|
|
149407
|
+
disarm();
|
|
149408
|
+
stalled = true;
|
|
149409
|
+
}
|
|
149410
|
+
};
|
|
149411
|
+
}
|
|
148558
149412
|
//#endregion
|
|
148559
149413
|
//#region src/clips/clip-truncation.ts
|
|
148560
149414
|
/**
|
|
@@ -148601,6 +149455,500 @@ function judgeClipDuration(input) {
|
|
|
148601
149455
|
};
|
|
148602
149456
|
}
|
|
148603
149457
|
//#endregion
|
|
149458
|
+
//#region src/clips/clip-stream.ts
|
|
149459
|
+
var defaultDecoder = (handlers) => new BcMediaAnnexBDecoder({
|
|
149460
|
+
strict: false,
|
|
149461
|
+
onVideoAccessUnit: handlers.onVideoAccessUnit,
|
|
149462
|
+
onAudioFrame: () => handlers.onAudioFrame()
|
|
149463
|
+
});
|
|
149464
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
149465
|
+
function createClipStreamProducer(deps) {
|
|
149466
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
149467
|
+
const createDecoder = deps.createDecoder ?? defaultDecoder;
|
|
149468
|
+
const drainTimeoutMs = deps.drainTimeoutMs ?? 1e4;
|
|
149469
|
+
const flushTimeoutMs = deps.flushTimeoutMs ?? 15e3;
|
|
149470
|
+
const lanes = /* @__PURE__ */ new Map();
|
|
149471
|
+
const activeByHost = /* @__PURE__ */ new Map();
|
|
149472
|
+
const runAttempt = (input, attempt) => new ClipStreamRun({
|
|
149473
|
+
logger: deps.logger,
|
|
149474
|
+
spawnMux: deps.spawnMux,
|
|
149475
|
+
createDecoder,
|
|
149476
|
+
drainTimeoutMs,
|
|
149477
|
+
flushTimeoutMs
|
|
149478
|
+
}, input, attempt).run();
|
|
149479
|
+
const runAttempts = async (input) => {
|
|
149480
|
+
const { deviceId } = input.device;
|
|
149481
|
+
let lastDetail = "no reason given";
|
|
149482
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
149483
|
+
if (attempt > 1) await sleep(500 * 2 ** (attempt - 2));
|
|
149484
|
+
const result = await runAttempt(input, attempt);
|
|
149485
|
+
if (result.kind !== "no-media") return result;
|
|
149486
|
+
lastDetail = result.detail;
|
|
149487
|
+
deps.logger.warn("videoclips: a clip stream produced no media — retrying", {
|
|
149488
|
+
tags: { deviceId },
|
|
149489
|
+
meta: {
|
|
149490
|
+
clipKey: input.row.clipKey,
|
|
149491
|
+
twin: input.twin,
|
|
149492
|
+
branch: "stream-no-media",
|
|
149493
|
+
attempt,
|
|
149494
|
+
error: lastDetail
|
|
149495
|
+
}
|
|
149496
|
+
});
|
|
149497
|
+
}
|
|
149498
|
+
deps.logger.warn("videoclips: the camera never delivered this clip stream", {
|
|
149499
|
+
tags: { deviceId },
|
|
149500
|
+
meta: {
|
|
149501
|
+
clipKey: input.row.clipKey,
|
|
149502
|
+
twin: input.twin,
|
|
149503
|
+
branch: "camera-refused",
|
|
149504
|
+
attempts: 3,
|
|
149505
|
+
error: lastDetail
|
|
149506
|
+
}
|
|
149507
|
+
});
|
|
149508
|
+
return {
|
|
149509
|
+
kind: "refused",
|
|
149510
|
+
code: "camera-refused",
|
|
149511
|
+
detail: `clip stream never landed after 3 attempts — ${lastDetail}`,
|
|
149512
|
+
attempts: 3
|
|
149513
|
+
};
|
|
149514
|
+
};
|
|
149515
|
+
return {
|
|
149516
|
+
active: (hostKey) => activeByHost.get(hostKey) ?? 0,
|
|
149517
|
+
stream: (input) => {
|
|
149518
|
+
const { hostKey, deviceId } = input.device;
|
|
149519
|
+
const ahead = activeByHost.get(hostKey) ?? 0;
|
|
149520
|
+
if (ahead > 0) deps.logger.info("videoclips: a clip stream is queued behind another on this host", {
|
|
149521
|
+
tags: { deviceId },
|
|
149522
|
+
meta: {
|
|
149523
|
+
clipKey: input.row.clipKey,
|
|
149524
|
+
twin: input.twin,
|
|
149525
|
+
branch: "stream-queued",
|
|
149526
|
+
ahead
|
|
149527
|
+
}
|
|
149528
|
+
});
|
|
149529
|
+
activeByHost.set(hostKey, ahead + 1);
|
|
149530
|
+
const run = (lanes.get(hostKey) ?? Promise.resolve()).then(() => runAttempts(input)).finally(() => {
|
|
149531
|
+
const left = (activeByHost.get(hostKey) ?? 1) - 1;
|
|
149532
|
+
if (left <= 0) activeByHost.delete(hostKey);
|
|
149533
|
+
else activeByHost.set(hostKey, left);
|
|
149534
|
+
});
|
|
149535
|
+
lanes.set(hostKey, run.then(() => void 0, () => void 0));
|
|
149536
|
+
return run;
|
|
149537
|
+
}
|
|
149538
|
+
};
|
|
149539
|
+
}
|
|
149540
|
+
/**
|
|
149541
|
+
* One transfer: camera → decoder → ffmpeg → peer, with a cut on every hop.
|
|
149542
|
+
*
|
|
149543
|
+
* A class because the run is a state machine with a dozen fields the chunk
|
|
149544
|
+
* callback and the download promise both touch; passing them around as
|
|
149545
|
+
* closures over `let`s was less readable, not more immutable.
|
|
149546
|
+
*/
|
|
149547
|
+
var ClipStreamRun = class {
|
|
149548
|
+
deps;
|
|
149549
|
+
input;
|
|
149550
|
+
attempt;
|
|
149551
|
+
accessUnits = 0;
|
|
149552
|
+
audioFrames = 0;
|
|
149553
|
+
timestampsUs = [];
|
|
149554
|
+
/** Access units held until the rate is probed — the frames in flight. */
|
|
149555
|
+
probe = [];
|
|
149556
|
+
mux = null;
|
|
149557
|
+
muxStderr = "";
|
|
149558
|
+
muxClosed = null;
|
|
149559
|
+
rate = {
|
|
149560
|
+
fps: null,
|
|
149561
|
+
fpsSource: "unmeasured",
|
|
149562
|
+
accessUnits: 0
|
|
149563
|
+
};
|
|
149564
|
+
videoType = null;
|
|
149565
|
+
begun = false;
|
|
149566
|
+
ended = false;
|
|
149567
|
+
bytesOut = 0;
|
|
149568
|
+
cut = null;
|
|
149569
|
+
stdinGate;
|
|
149570
|
+
peerGate;
|
|
149571
|
+
tags;
|
|
149572
|
+
constructor(deps, input, attempt) {
|
|
149573
|
+
this.deps = deps;
|
|
149574
|
+
this.input = input;
|
|
149575
|
+
this.attempt = attempt;
|
|
149576
|
+
this.tags = { deviceId: input.device.deviceId };
|
|
149577
|
+
this.stdinGate = createDrainGate({
|
|
149578
|
+
timeoutMs: deps.drainTimeoutMs,
|
|
149579
|
+
onStall: () => this.sever("mux-stalled", "ffmpeg stopped taking access units")
|
|
149580
|
+
});
|
|
149581
|
+
this.peerGate = createDrainGate({
|
|
149582
|
+
timeoutMs: deps.drainTimeoutMs,
|
|
149583
|
+
onStall: () => this.sever("peer-stalled", "the peer stopped taking bytes")
|
|
149584
|
+
});
|
|
149585
|
+
}
|
|
149586
|
+
async run() {
|
|
149587
|
+
const { device, row, fileName, twin, sink } = this.input;
|
|
149588
|
+
sink.onceClose(() => {
|
|
149589
|
+
if (!this.ended) this.sever("peer-closed", "the peer closed the connection");
|
|
149590
|
+
});
|
|
149591
|
+
const decoder = this.deps.createDecoder({
|
|
149592
|
+
onVideoAccessUnit: (unit) => this.onAccessUnit(unit),
|
|
149593
|
+
onAudioFrame: () => {
|
|
149594
|
+
this.audioFrames += 1;
|
|
149595
|
+
}
|
|
149596
|
+
});
|
|
149597
|
+
this.deps.logger.info("videoclips: streaming a clip file from the camera", {
|
|
149598
|
+
tags: this.tags,
|
|
149599
|
+
meta: {
|
|
149600
|
+
clipKey: row.clipKey,
|
|
149601
|
+
twin,
|
|
149602
|
+
fileName,
|
|
149603
|
+
startMs: row.startMs,
|
|
149604
|
+
endMs: row.endMs,
|
|
149605
|
+
attempt: this.attempt,
|
|
149606
|
+
idleTimeoutMs: 300
|
|
149607
|
+
}
|
|
149608
|
+
});
|
|
149609
|
+
let api;
|
|
149610
|
+
try {
|
|
149611
|
+
api = await device.getApi();
|
|
149612
|
+
} catch (err) {
|
|
149613
|
+
return {
|
|
149614
|
+
kind: "no-media",
|
|
149615
|
+
detail: describe(err)
|
|
149616
|
+
};
|
|
149617
|
+
}
|
|
149618
|
+
try {
|
|
149619
|
+
await api.fileInfoListReplayBinaryDownload({
|
|
149620
|
+
channel: device.channel,
|
|
149621
|
+
...device.uid !== void 0 ? { uid: device.uid } : {},
|
|
149622
|
+
fileName,
|
|
149623
|
+
idleTimeoutMs: 300,
|
|
149624
|
+
onChunk: (chunk) => this.onChunk(decoder, chunk)
|
|
149625
|
+
});
|
|
149626
|
+
} catch (err) {
|
|
149627
|
+
return this.onTransferFailed(err);
|
|
149628
|
+
}
|
|
149629
|
+
return this.onTransferEnded();
|
|
149630
|
+
}
|
|
149631
|
+
/** The camera's bytes, as they arrive. A cut run THROWS here: the only stop. */
|
|
149632
|
+
onChunk(decoder, chunk) {
|
|
149633
|
+
if (this.cut !== null) throw new Error(`clip stream cut: ${this.cut.reason}`);
|
|
149634
|
+
try {
|
|
149635
|
+
decoder.push(chunk);
|
|
149636
|
+
} catch (err) {
|
|
149637
|
+
this.sever("decode-failed", describe(err));
|
|
149638
|
+
throw err;
|
|
149639
|
+
}
|
|
149640
|
+
}
|
|
149641
|
+
onAccessUnit(unit) {
|
|
149642
|
+
if (this.cut !== null) return;
|
|
149643
|
+
this.accessUnits += 1;
|
|
149644
|
+
this.timestampsUs.push(unit.microseconds);
|
|
149645
|
+
if (this.mux === null) {
|
|
149646
|
+
this.probe = [...this.probe, unit];
|
|
149647
|
+
if (this.probe.length >= 16) this.startMux();
|
|
149648
|
+
return;
|
|
149649
|
+
}
|
|
149650
|
+
this.feed(unit.annexB);
|
|
149651
|
+
}
|
|
149652
|
+
/** Spawn ffmpeg with the probed rate and hand it what was held. */
|
|
149653
|
+
startMux() {
|
|
149654
|
+
const first = this.probe[0];
|
|
149655
|
+
if (first === void 0 || this.mux !== null) return;
|
|
149656
|
+
this.videoType = first.videoType;
|
|
149657
|
+
this.rate = probeClipStreamRate(this.timestampsUs);
|
|
149658
|
+
const { row, twin } = this.input;
|
|
149659
|
+
if (this.rate.fpsSource !== "timestamps") this.deps.logger.warn("videoclips: no measured frame rate for this clip stream", {
|
|
149660
|
+
tags: this.tags,
|
|
149661
|
+
meta: {
|
|
149662
|
+
clipKey: row.clipKey,
|
|
149663
|
+
twin,
|
|
149664
|
+
branch: "fps-unmeasured",
|
|
149665
|
+
probeAccessUnits: this.rate.accessUnits
|
|
149666
|
+
}
|
|
149667
|
+
});
|
|
149668
|
+
let mux;
|
|
149669
|
+
try {
|
|
149670
|
+
mux = this.deps.spawnMux(buildClipStreamMuxArgs({
|
|
149671
|
+
videoType: this.videoType,
|
|
149672
|
+
fps: this.rate.fps
|
|
149673
|
+
}));
|
|
149674
|
+
} catch (err) {
|
|
149675
|
+
this.sever("mux-failed", `ffmpeg could not be spawned: ${describe(err)}`);
|
|
149676
|
+
return;
|
|
149677
|
+
}
|
|
149678
|
+
this.mux = mux;
|
|
149679
|
+
this.muxClosed = new Promise((resolve) => {
|
|
149680
|
+
mux.on("close", (code) => resolve(code));
|
|
149681
|
+
mux.on("error", (err) => {
|
|
149682
|
+
this.sever("mux-failed", `ffmpeg failed: ${err.message}`);
|
|
149683
|
+
resolve(null);
|
|
149684
|
+
});
|
|
149685
|
+
});
|
|
149686
|
+
mux.stderr?.on("data", (chunk) => {
|
|
149687
|
+
if (this.muxStderr.length < 2e3) this.muxStderr += chunk.toString("utf8");
|
|
149688
|
+
});
|
|
149689
|
+
mux.stdin?.on("error", (err) => this.sever("mux-failed", `ffmpeg stdin: ${err.message}`));
|
|
149690
|
+
mux.stdin?.on("drain", () => this.stdinGate.drained());
|
|
149691
|
+
mux.stdout?.on("data", (chunk) => this.onMuxOut(chunk));
|
|
149692
|
+
mux.stdout?.on("end", () => this.finishSink());
|
|
149693
|
+
this.deps.logger.info("videoclips: a clip stream is muxing", {
|
|
149694
|
+
tags: this.tags,
|
|
149695
|
+
meta: {
|
|
149696
|
+
clipKey: row.clipKey,
|
|
149697
|
+
twin,
|
|
149698
|
+
videoType: this.videoType,
|
|
149699
|
+
fps: this.rate.fps,
|
|
149700
|
+
fpsSource: this.rate.fpsSource,
|
|
149701
|
+
probeAccessUnits: this.rate.accessUnits
|
|
149702
|
+
}
|
|
149703
|
+
});
|
|
149704
|
+
const held = this.probe;
|
|
149705
|
+
this.probe = [];
|
|
149706
|
+
for (const unit of held) this.feed(unit.annexB);
|
|
149707
|
+
}
|
|
149708
|
+
feed(annexB) {
|
|
149709
|
+
if (this.cut !== null || this.mux?.stdin === null || this.mux?.stdin === void 0) return;
|
|
149710
|
+
this.stdinGate.wrote(this.mux.stdin.write(annexB));
|
|
149711
|
+
}
|
|
149712
|
+
/** ffmpeg's output, to the peer, honouring `write()`'s return (D447). */
|
|
149713
|
+
onMuxOut(chunk) {
|
|
149714
|
+
if (this.cut !== null) return;
|
|
149715
|
+
const { sink } = this.input;
|
|
149716
|
+
if (!this.begun) {
|
|
149717
|
+
this.begun = true;
|
|
149718
|
+
sink.begin();
|
|
149719
|
+
}
|
|
149720
|
+
const accepted = sink.write(chunk);
|
|
149721
|
+
this.bytesOut += chunk.byteLength;
|
|
149722
|
+
this.peerGate.wrote(accepted);
|
|
149723
|
+
if (accepted) return;
|
|
149724
|
+
const stdout = this.mux?.stdout;
|
|
149725
|
+
stdout?.pause();
|
|
149726
|
+
sink.onceDrain(() => {
|
|
149727
|
+
this.peerGate.drained();
|
|
149728
|
+
stdout?.resume();
|
|
149729
|
+
});
|
|
149730
|
+
}
|
|
149731
|
+
finishSink() {
|
|
149732
|
+
if (this.cut !== null || this.ended) return;
|
|
149733
|
+
this.ended = true;
|
|
149734
|
+
this.input.sink.end();
|
|
149735
|
+
}
|
|
149736
|
+
/** Sever every hop, once, and say why. */
|
|
149737
|
+
sever(reason, detail) {
|
|
149738
|
+
if (this.cut !== null) return;
|
|
149739
|
+
this.cut = {
|
|
149740
|
+
reason,
|
|
149741
|
+
detail
|
|
149742
|
+
};
|
|
149743
|
+
this.stdinGate.dispose();
|
|
149744
|
+
this.peerGate.dispose();
|
|
149745
|
+
this.mux?.kill("SIGKILL");
|
|
149746
|
+
this.deps.logger.warn("videoclips: a clip stream was cut", {
|
|
149747
|
+
tags: this.tags,
|
|
149748
|
+
meta: {
|
|
149749
|
+
clipKey: this.input.row.clipKey,
|
|
149750
|
+
twin: this.input.twin,
|
|
149751
|
+
branch: reason,
|
|
149752
|
+
detail,
|
|
149753
|
+
accessUnits: this.accessUnits,
|
|
149754
|
+
bytesOut: this.bytesOut,
|
|
149755
|
+
begun: this.begun
|
|
149756
|
+
}
|
|
149757
|
+
});
|
|
149758
|
+
if (this.begun) this.input.sink.destroy();
|
|
149759
|
+
}
|
|
149760
|
+
cutOutcome(cut) {
|
|
149761
|
+
if (this.begun) return {
|
|
149762
|
+
kind: "cut",
|
|
149763
|
+
reason: cut.reason,
|
|
149764
|
+
detail: cut.detail,
|
|
149765
|
+
bytesOut: this.bytesOut
|
|
149766
|
+
};
|
|
149767
|
+
return {
|
|
149768
|
+
kind: "refused",
|
|
149769
|
+
code: "fetch-failed",
|
|
149770
|
+
detail: `${cut.reason}: ${cut.detail}`,
|
|
149771
|
+
attempts: this.attempt
|
|
149772
|
+
};
|
|
149773
|
+
}
|
|
149774
|
+
onTransferFailed(err) {
|
|
149775
|
+
if (this.cut !== null) return this.cutOutcome(this.cut);
|
|
149776
|
+
if (this.accessUnits === 0) return {
|
|
149777
|
+
kind: "no-media",
|
|
149778
|
+
detail: describe(err)
|
|
149779
|
+
};
|
|
149780
|
+
this.sever("camera-failed", describe(err));
|
|
149781
|
+
return this.cutOutcome({
|
|
149782
|
+
reason: "camera-failed",
|
|
149783
|
+
detail: describe(err)
|
|
149784
|
+
});
|
|
149785
|
+
}
|
|
149786
|
+
async onTransferEnded() {
|
|
149787
|
+
if (this.cut !== null) return this.cutOutcome(this.cut);
|
|
149788
|
+
if (this.accessUnits === 0) return {
|
|
149789
|
+
kind: "no-media",
|
|
149790
|
+
detail: "the camera answered the transfer with no access unit"
|
|
149791
|
+
};
|
|
149792
|
+
if (this.mux === null) this.startMux();
|
|
149793
|
+
const cutAtStart = this.cut;
|
|
149794
|
+
if (cutAtStart !== null) return this.cutOutcome(cutAtStart);
|
|
149795
|
+
const mux = this.mux;
|
|
149796
|
+
if (mux === null || this.muxClosed === null) return this.cutOutcome({
|
|
149797
|
+
reason: "mux-failed",
|
|
149798
|
+
detail: "no mux after the transfer ended"
|
|
149799
|
+
});
|
|
149800
|
+
mux.stdin?.end();
|
|
149801
|
+
const flush = setTimeout(() => this.sever("mux-hung", `ffmpeg did not exit ${this.deps.flushTimeoutMs} ms after its input ended`), this.deps.flushTimeoutMs);
|
|
149802
|
+
const code = await this.muxClosed;
|
|
149803
|
+
clearTimeout(flush);
|
|
149804
|
+
const cutAfter = this.cut;
|
|
149805
|
+
if (cutAfter !== null) return this.cutOutcome(cutAfter);
|
|
149806
|
+
if (code !== 0) {
|
|
149807
|
+
this.sever("mux-failed", `ffmpeg exited ${String(code)}: ${this.muxStderr.slice(0, 300)}`);
|
|
149808
|
+
return this.cutOutcome({
|
|
149809
|
+
reason: "mux-failed",
|
|
149810
|
+
detail: this.muxStderr.slice(0, 300)
|
|
149811
|
+
});
|
|
149812
|
+
}
|
|
149813
|
+
this.finishSink();
|
|
149814
|
+
return this.served();
|
|
149815
|
+
}
|
|
149816
|
+
/** The stream landed: judge it against the row, out loud, and answer. */
|
|
149817
|
+
served() {
|
|
149818
|
+
const { row, twin } = this.input;
|
|
149819
|
+
const timing = estimateVideoTiming({ timestampsUs: this.timestampsUs });
|
|
149820
|
+
const durationSeconds = timing.fpsSource === "timestamps" ? timing.durationSeconds : null;
|
|
149821
|
+
const verdict = judgeClipDuration({
|
|
149822
|
+
expectedMs: row.endMs - row.startMs,
|
|
149823
|
+
decodedSeconds: durationSeconds,
|
|
149824
|
+
...row.inProgress === true ? { inProgress: true } : {}
|
|
149825
|
+
});
|
|
149826
|
+
const meta = {
|
|
149827
|
+
clipKey: row.clipKey,
|
|
149828
|
+
twin,
|
|
149829
|
+
attempt: this.attempt,
|
|
149830
|
+
accessUnits: this.accessUnits,
|
|
149831
|
+
bytesOut: this.bytesOut,
|
|
149832
|
+
durationSeconds,
|
|
149833
|
+
expectedMs: row.endMs - row.startMs,
|
|
149834
|
+
fps: this.rate.fps,
|
|
149835
|
+
fpsSource: this.rate.fpsSource,
|
|
149836
|
+
videoType: this.videoType
|
|
149837
|
+
};
|
|
149838
|
+
if (verdict.kind === "short") this.deps.logger.warn("videoclips: a clip stream came up short against its row", {
|
|
149839
|
+
tags: this.tags,
|
|
149840
|
+
meta: {
|
|
149841
|
+
...meta,
|
|
149842
|
+
branch: "stream-short",
|
|
149843
|
+
shortfallSeconds: verdict.shortfallSeconds,
|
|
149844
|
+
retried: false
|
|
149845
|
+
}
|
|
149846
|
+
});
|
|
149847
|
+
else if (verdict.kind === "unknown") this.deps.logger.warn("videoclips: a clip stream could not be checked for truncation", {
|
|
149848
|
+
tags: this.tags,
|
|
149849
|
+
meta: {
|
|
149850
|
+
...meta,
|
|
149851
|
+
branch: "completeness-unverified",
|
|
149852
|
+
reason: verdict.reason
|
|
149853
|
+
}
|
|
149854
|
+
});
|
|
149855
|
+
else this.deps.logger.info("videoclips: a clip stream landed whole", {
|
|
149856
|
+
tags: this.tags,
|
|
149857
|
+
meta: {
|
|
149858
|
+
...meta,
|
|
149859
|
+
branch: "stream-served"
|
|
149860
|
+
}
|
|
149861
|
+
});
|
|
149862
|
+
if (this.audioFrames > 0) this.deps.logger.warn("videoclips: a clip stream was served without sound", {
|
|
149863
|
+
tags: this.tags,
|
|
149864
|
+
meta: {
|
|
149865
|
+
clipKey: row.clipKey,
|
|
149866
|
+
twin,
|
|
149867
|
+
branch: "stream-video-only",
|
|
149868
|
+
audioFramesDropped: this.audioFrames
|
|
149869
|
+
}
|
|
149870
|
+
});
|
|
149871
|
+
return {
|
|
149872
|
+
kind: "served",
|
|
149873
|
+
accessUnits: this.accessUnits,
|
|
149874
|
+
audioFramesDropped: this.audioFrames,
|
|
149875
|
+
bytesOut: this.bytesOut,
|
|
149876
|
+
durationSeconds,
|
|
149877
|
+
verdict,
|
|
149878
|
+
rate: this.rate,
|
|
149879
|
+
attempts: this.attempt
|
|
149880
|
+
};
|
|
149881
|
+
}
|
|
149882
|
+
};
|
|
149883
|
+
function describe(err) {
|
|
149884
|
+
return err instanceof Error ? err.message : String(err);
|
|
149885
|
+
}
|
|
149886
|
+
//#endregion
|
|
149887
|
+
//#region src/clips/clip-stream-route.ts
|
|
149888
|
+
function createClipStreamRoute(deps) {
|
|
149889
|
+
return async (deviceId, clipKey, sink) => {
|
|
149890
|
+
const twin = "sub";
|
|
149891
|
+
const gate = await deps.gate({
|
|
149892
|
+
deviceId,
|
|
149893
|
+
clipKey,
|
|
149894
|
+
twin,
|
|
149895
|
+
path: "stream"
|
|
149896
|
+
});
|
|
149897
|
+
if (gate.kind === "refused") return {
|
|
149898
|
+
kind: "refused",
|
|
149899
|
+
code: gate.branch,
|
|
149900
|
+
detail: gate.detail
|
|
149901
|
+
};
|
|
149902
|
+
const row = deps.rowFor(deviceId, clipKey);
|
|
149903
|
+
if (row === null) {
|
|
149904
|
+
deps.logger.warn("videoclips: no catalog row for this clip stream", {
|
|
149905
|
+
tags: { deviceId },
|
|
149906
|
+
meta: {
|
|
149907
|
+
clipKey,
|
|
149908
|
+
twin,
|
|
149909
|
+
branch: "no-catalog-row"
|
|
149910
|
+
}
|
|
149911
|
+
});
|
|
149912
|
+
return {
|
|
149913
|
+
kind: "refused",
|
|
149914
|
+
code: "no-catalog-row",
|
|
149915
|
+
detail: "no-catalog-row: list this day before asking for its stream"
|
|
149916
|
+
};
|
|
149917
|
+
}
|
|
149918
|
+
if (row.inProgress === true) {
|
|
149919
|
+
deps.logger.warn("videoclips: refusing to stream a clip the camera is still writing", {
|
|
149920
|
+
tags: { deviceId },
|
|
149921
|
+
meta: {
|
|
149922
|
+
clipKey,
|
|
149923
|
+
twin,
|
|
149924
|
+
branch: "clip-in-progress"
|
|
149925
|
+
}
|
|
149926
|
+
});
|
|
149927
|
+
return {
|
|
149928
|
+
kind: "refused",
|
|
149929
|
+
code: "clip-in-progress",
|
|
149930
|
+
detail: "clip-in-progress: the camera is still writing this file; it has no end to stream to"
|
|
149931
|
+
};
|
|
149932
|
+
}
|
|
149933
|
+
const outcome = await deps.producer.stream({
|
|
149934
|
+
device: gate.device,
|
|
149935
|
+
row,
|
|
149936
|
+
fileName: row.subFileName,
|
|
149937
|
+
twin,
|
|
149938
|
+
sink
|
|
149939
|
+
});
|
|
149940
|
+
switch (outcome.kind) {
|
|
149941
|
+
case "served": return { kind: "served" };
|
|
149942
|
+
case "cut": return { kind: "cut" };
|
|
149943
|
+
case "refused": return {
|
|
149944
|
+
kind: "refused",
|
|
149945
|
+
code: outcome.code,
|
|
149946
|
+
detail: outcome.detail
|
|
149947
|
+
};
|
|
149948
|
+
}
|
|
149949
|
+
};
|
|
149950
|
+
}
|
|
149951
|
+
//#endregion
|
|
148604
149952
|
//#region src/clips/clip-thumb-queue.ts
|
|
148605
149953
|
/**
|
|
148606
149954
|
* The provider's OWN CoverPreview lane — two queues, one in flight per host.
|
|
@@ -148780,6 +150128,59 @@ function decideClipPrefetch(input) {
|
|
|
148780
150128
|
return { kind: "run" };
|
|
148781
150129
|
}
|
|
148782
150130
|
//#endregion
|
|
150131
|
+
//#region src/clips/clip-window.ts
|
|
150132
|
+
/**
|
|
150133
|
+
* Does this row overlap the window?
|
|
150134
|
+
*
|
|
150135
|
+
* Half-open on both ends (`start < until`, `end > since`), so a clip that ends
|
|
150136
|
+
* exactly when the window begins is OUT and a clip that begins exactly when it
|
|
150137
|
+
* ends is OUT — otherwise two adjacent windows both claim the same boundary
|
|
150138
|
+
* clip and a surface paging through them renders it twice.
|
|
150139
|
+
*
|
|
150140
|
+
* A row whose `endMs` is not after its `startMs` is treated as an INSTANT at
|
|
150141
|
+
* `startMs`. That is not defensive padding: a clip still being written carries
|
|
150142
|
+
* a frozen `endMs` from the camera's index (D563), and on this fleet that has
|
|
150143
|
+
* been observed equal to — and once behind — its own start. Judging such a row
|
|
150144
|
+
* by a bad end would drop a recording that is happening right now.
|
|
150145
|
+
*/
|
|
150146
|
+
function clipOverlapsWindow(row, window) {
|
|
150147
|
+
const end = row.endMs > row.startMs ? row.endMs : row.startMs;
|
|
150148
|
+
return row.startMs < window.until && end > window.since;
|
|
150149
|
+
}
|
|
150150
|
+
/**
|
|
150151
|
+
* What the provider ANSWERS with — the one gate, so there is a single place
|
|
150152
|
+
* that decides what leaves this listing and a single line that reports it.
|
|
150153
|
+
*
|
|
150154
|
+
* ## Why a clip still being written is not listed
|
|
150155
|
+
*
|
|
150156
|
+
* The operator, 2026-09-22: *"Niente clips in preparazione, non ha senso
|
|
150157
|
+
* visualizzarle."* He is right, and the reasons had been accumulating:
|
|
150158
|
+
*
|
|
150159
|
+
* - its `endMs` is a frozen guess — the camera's index keeps whatever it held
|
|
150160
|
+
* when the file was opened, measured on 592 as the same `endTime` for eight
|
|
150161
|
+
* minutes (D563) — so the row's duration, its position on the axis and its
|
|
150162
|
+
* height are all wrong and all move;
|
|
150163
|
+
* - `readClipBytes` already refuses it by name (`clip-in-progress`): it has no
|
|
150164
|
+
* end to export against, so the completion rule has nothing to prove the
|
|
150165
|
+
* fetch finished (D568);
|
|
150166
|
+
* - it cannot be looped, and the surface has to explain why (`resolveClipLoop`);
|
|
150167
|
+
* - the player route would file a TRUNCATED copy under an immutable key, so
|
|
150168
|
+
* the first seconds become permanently what that clip is.
|
|
150169
|
+
*
|
|
150170
|
+
* Four different places bending around a row that cannot be played properly.
|
|
150171
|
+
* Not listing it deletes all four questions instead of answering them: the
|
|
150172
|
+
* clip appears, whole, on the next listing after the camera closes the file.
|
|
150173
|
+
*/
|
|
150174
|
+
function listableClips(rows, window) {
|
|
150175
|
+
const inWindow = rows.filter((row) => clipOverlapsWindow(row, window));
|
|
150176
|
+
const kept = inWindow.filter((row) => row.inProgress !== true);
|
|
150177
|
+
return {
|
|
150178
|
+
kept,
|
|
150179
|
+
outOfWindow: rows.length - inWindow.length,
|
|
150180
|
+
inProgress: inWindow.length - kept.length
|
|
150181
|
+
};
|
|
150182
|
+
}
|
|
150183
|
+
//#endregion
|
|
148783
150184
|
//#region src/clips/clip-identity.ts
|
|
148784
150185
|
/**
|
|
148785
150186
|
* A Reolink clip's IDENTITY — the id the operator's surface carries around and
|
|
@@ -148896,7 +150297,8 @@ var CLASS_BY_NATIVE = {
|
|
|
148896
150297
|
var KIND_BY_NATIVE = {
|
|
148897
150298
|
md: "motion",
|
|
148898
150299
|
pir: "motion",
|
|
148899
|
-
cry: "audio"
|
|
150300
|
+
cry: "audio",
|
|
150301
|
+
other: "motion"
|
|
148900
150302
|
};
|
|
148901
150303
|
/** What the subject DID — carried beside a class, never as one. */
|
|
148902
150304
|
var BEHAVIOUR_BY_NATIVE = {
|
|
@@ -148909,8 +150311,9 @@ var BEHAVIOUR_BY_NATIVE = {
|
|
|
148909
150311
|
/**
|
|
148910
150312
|
* Recording REASONS — why the file exists, not what is in it. They get no
|
|
148911
150313
|
* label and no behaviour; the row still lists, showing the reason raw.
|
|
148912
|
-
* `
|
|
148913
|
-
*
|
|
150314
|
+
* `none` is the camera declining to classify, which is not the same as the
|
|
150315
|
+
* camera saying "nothing". `other` used to live here and is now a motion KIND
|
|
150316
|
+
* — see {@link KIND_BY_NATIVE}.
|
|
148914
150317
|
*/
|
|
148915
150318
|
var RECORDING_REASONS = new Set([
|
|
148916
150319
|
"sched",
|
|
@@ -148922,7 +150325,6 @@ var RECORDING_REASONS = new Set([
|
|
|
148922
150325
|
"answer",
|
|
148923
150326
|
"tamper",
|
|
148924
150327
|
"lmsg",
|
|
148925
|
-
"other",
|
|
148926
150328
|
"none"
|
|
148927
150329
|
]);
|
|
148928
150330
|
/**
|
|
@@ -149498,6 +150900,18 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149498
150900
|
timeZone: device.timeZone,
|
|
149499
150901
|
day
|
|
149500
150902
|
});
|
|
150903
|
+
const newest = result.rows[result.rows.length - 1] ?? result.rows[0];
|
|
150904
|
+
deps.logger.info("videoclips: the camera answered a day search", {
|
|
150905
|
+
tags,
|
|
150906
|
+
meta: {
|
|
150907
|
+
day,
|
|
150908
|
+
rows: result.rows.length,
|
|
150909
|
+
newestFile: newest?.subFileName ?? null,
|
|
150910
|
+
newestStartMs: newest?.startMs ?? null,
|
|
150911
|
+
oldestStartMs: result.rows[0]?.startMs ?? null,
|
|
150912
|
+
askedAtMs: Date.now()
|
|
150913
|
+
}
|
|
150914
|
+
});
|
|
149501
150915
|
if (result.mainSearchFailed !== void 0) deps.logger.warn("videoclips: the main-stream search refused — no high twin today", {
|
|
149502
150916
|
tags,
|
|
149503
150917
|
meta: {
|
|
@@ -149539,7 +150953,24 @@ function createReolinkVideoclipsProvider(deps) {
|
|
|
149539
150953
|
state: "ok",
|
|
149540
150954
|
catalogAsOf: deps.now()
|
|
149541
150955
|
});
|
|
149542
|
-
|
|
150956
|
+
for (const row of rows) rowsByKey.set(`${device.deviceId}|${row.clipKey}`, row);
|
|
150957
|
+
const narrowed = listableClips(rows, {
|
|
150958
|
+
since,
|
|
150959
|
+
until
|
|
150960
|
+
});
|
|
150961
|
+
if (narrowed.outOfWindow > 0 || narrowed.inProgress > 0) deps.logger.info("videoclips: narrowed a day listing to what it can answer", {
|
|
150962
|
+
tags,
|
|
150963
|
+
meta: {
|
|
150964
|
+
branch: "listing-narrowed",
|
|
150965
|
+
listed: rows.length,
|
|
150966
|
+
kept: narrowed.kept.length,
|
|
150967
|
+
outOfWindow: narrowed.outOfWindow,
|
|
150968
|
+
inProgress: narrowed.inProgress,
|
|
150969
|
+
since,
|
|
150970
|
+
until
|
|
150971
|
+
}
|
|
150972
|
+
});
|
|
150973
|
+
return narrowed.kept;
|
|
149543
150974
|
};
|
|
149544
150975
|
/**
|
|
149545
150976
|
* Give a day's rows a class when the FILES carried none.
|
|
@@ -150447,9 +151878,15 @@ function createClipService(deps) {
|
|
|
150447
151878
|
* clip-export vocabulary, so the provider can hand the engine a token it
|
|
150448
151879
|
* believes rather than prose it has to guess at (D572).
|
|
150449
151880
|
*/
|
|
150450
|
-
|
|
150451
|
-
|
|
150452
|
-
|
|
151881
|
+
/**
|
|
151882
|
+
* The gates every clip byte path runs, in ONE place: the device, the
|
|
151883
|
+
* sleeping standalone, the uid. The file producer and the stream producer
|
|
151884
|
+
* both call this and neither decides any of it again — a second copy of
|
|
151885
|
+
* the sleeping gate is a second chance to wake a camera nobody authorised.
|
|
151886
|
+
* Every refusal is logged HERE, with its branch, so a caller cannot forget.
|
|
151887
|
+
*/
|
|
151888
|
+
const gateClipDevice = async (input) => {
|
|
151889
|
+
const { deviceId, clipKey, twin, path } = input;
|
|
150453
151890
|
const tags = { deviceId };
|
|
150454
151891
|
const device = await deps.resolveDevice(deviceId);
|
|
150455
151892
|
if (device === null) {
|
|
@@ -150458,11 +151895,13 @@ function createClipService(deps) {
|
|
|
150458
151895
|
meta: {
|
|
150459
151896
|
clipKey,
|
|
150460
151897
|
twin,
|
|
151898
|
+
path,
|
|
150461
151899
|
branch: "unknown-device"
|
|
150462
151900
|
}
|
|
150463
151901
|
});
|
|
150464
151902
|
return {
|
|
150465
151903
|
kind: "refused",
|
|
151904
|
+
branch: "unknown-device",
|
|
150466
151905
|
code: "catalog-unreachable",
|
|
150467
151906
|
detail: `device ${deviceId} is not a Reolink camera on this node`
|
|
150468
151907
|
};
|
|
@@ -150474,11 +151913,13 @@ function createClipService(deps) {
|
|
|
150474
151913
|
meta: {
|
|
150475
151914
|
clipKey,
|
|
150476
151915
|
twin,
|
|
151916
|
+
path,
|
|
150477
151917
|
branch: "sleeping"
|
|
150478
151918
|
}
|
|
150479
151919
|
});
|
|
150480
151920
|
return {
|
|
150481
151921
|
kind: "refused",
|
|
151922
|
+
branch: "sleeping",
|
|
150482
151923
|
code: "sleeping",
|
|
150483
151924
|
detail: `device ${deviceId} is a standalone battery camera and is sleeping — fetching a clip would wake it, and no operator authorisation was carried`
|
|
150484
151925
|
};
|
|
@@ -150488,6 +151929,7 @@ function createClipService(deps) {
|
|
|
150488
151929
|
meta: {
|
|
150489
151930
|
clipKey,
|
|
150490
151931
|
twin,
|
|
151932
|
+
path,
|
|
150491
151933
|
branch: "wake-authorised"
|
|
150492
151934
|
}
|
|
150493
151935
|
});
|
|
@@ -150498,16 +151940,50 @@ function createClipService(deps) {
|
|
|
150498
151940
|
meta: {
|
|
150499
151941
|
clipKey,
|
|
150500
151942
|
twin,
|
|
151943
|
+
path,
|
|
150501
151944
|
branch: "uid-missing"
|
|
150502
151945
|
}
|
|
150503
151946
|
});
|
|
150504
151947
|
return {
|
|
150505
151948
|
kind: "refused",
|
|
151949
|
+
branch: "uid-missing",
|
|
150506
151950
|
code: "catalog-unreachable",
|
|
150507
151951
|
detail: UID_MISSING_REASON
|
|
150508
151952
|
};
|
|
150509
151953
|
}
|
|
151954
|
+
return {
|
|
151955
|
+
kind: "ok",
|
|
151956
|
+
device
|
|
151957
|
+
};
|
|
151958
|
+
};
|
|
151959
|
+
const produceTwin = async (input) => {
|
|
151960
|
+
const { deviceId, row, fileName, twin } = input;
|
|
151961
|
+
const clipKey = row.clipKey;
|
|
151962
|
+
const tags = { deviceId };
|
|
151963
|
+
const gate = await gateClipDevice({
|
|
151964
|
+
deviceId,
|
|
151965
|
+
clipKey,
|
|
151966
|
+
twin,
|
|
151967
|
+
path: "file",
|
|
151968
|
+
...input.wake !== void 0 ? { wake: input.wake } : {}
|
|
151969
|
+
});
|
|
151970
|
+
if (gate.kind === "refused") return {
|
|
151971
|
+
kind: "refused",
|
|
151972
|
+
code: gate.code,
|
|
151973
|
+
detail: gate.detail
|
|
151974
|
+
};
|
|
151975
|
+
const { device } = gate;
|
|
150510
151976
|
let downloaded;
|
|
151977
|
+
deps.logger.info("videoclips: fetching a clip file from the camera", {
|
|
151978
|
+
tags,
|
|
151979
|
+
meta: {
|
|
151980
|
+
clipKey,
|
|
151981
|
+
twin,
|
|
151982
|
+
fileName,
|
|
151983
|
+
startMs: row.startMs,
|
|
151984
|
+
endMs: row.endMs
|
|
151985
|
+
}
|
|
151986
|
+
});
|
|
150511
151987
|
try {
|
|
150512
151988
|
downloaded = await fetchVerifiedClipBytes(device, row, clipKey, fileName);
|
|
150513
151989
|
} catch (err) {
|
|
@@ -150674,6 +152150,23 @@ function createClipService(deps) {
|
|
|
150674
152150
|
if (outcome.kind === "ok" && twin === "sub") await media.put(deviceId, row.clipKey, outcome.bytes);
|
|
150675
152151
|
return outcome;
|
|
150676
152152
|
};
|
|
152153
|
+
/**
|
|
152154
|
+
* The STREAM's path (`clip-stream-route.ts`): the same gates, the same row,
|
|
152155
|
+
* nothing written. Beside the file path, not in its place.
|
|
152156
|
+
*/
|
|
152157
|
+
const streamClip = createClipStreamRoute({
|
|
152158
|
+
logger: deps.logger,
|
|
152159
|
+
gate: gateClipDevice,
|
|
152160
|
+
rowFor,
|
|
152161
|
+
producer: createClipStreamProducer({
|
|
152162
|
+
logger: deps.logger,
|
|
152163
|
+
spawnMux: (args) => spawn$1(deps.ffmpegBinaryPath ?? "ffmpeg", [...args], { stdio: [
|
|
152164
|
+
"pipe",
|
|
152165
|
+
"pipe",
|
|
152166
|
+
"pipe"
|
|
152167
|
+
] })
|
|
152168
|
+
})
|
|
152169
|
+
});
|
|
150677
152170
|
const provider = createReolinkVideoclipsProvider({
|
|
150678
152171
|
now,
|
|
150679
152172
|
cache,
|
|
@@ -150703,6 +152196,11 @@ function createClipService(deps) {
|
|
|
150703
152196
|
hostBusy: (hostKey) => queue.busy(hostKey),
|
|
150704
152197
|
thumbPrefix: CLIP_THUMB_PREFIX,
|
|
150705
152198
|
mediaPrefix: CLIP_MEDIA_PREFIX,
|
|
152199
|
+
streamPrefix: CLIP_STREAM_PREFIX,
|
|
152200
|
+
streamHandler: createClipStreamHandler({
|
|
152201
|
+
stream: streamClip,
|
|
152202
|
+
logger: deps.logger
|
|
152203
|
+
}),
|
|
150706
152204
|
thumbHandler: createClipThumbHandler({
|
|
150707
152205
|
readCached: (deviceId, clipKey) => thumbs.read(deviceId, clipKey),
|
|
150708
152206
|
mint: (deviceId, clipKey) => mintThumbnail(deviceId, clipKey, "interactive"),
|
|
@@ -164942,11 +166440,18 @@ var ReolinkProviderAddon = class extends BaseDeviceProvider {
|
|
|
164942
166440
|
access: "authenticated",
|
|
164943
166441
|
handler: service.playbackHandler
|
|
164944
166442
|
});
|
|
166443
|
+
const stream = await this.ctx.dataPlane?.serve({
|
|
166444
|
+
prefix: service.streamPrefix,
|
|
166445
|
+
access: "authenticated",
|
|
166446
|
+
handler: service.streamHandler
|
|
166447
|
+
});
|
|
164945
166448
|
this.ctx.logger.info("Reolink clip data planes served", { meta: {
|
|
164946
166449
|
thumbPath: `/addon/${REOLINK_ADDON_ID}/${service.thumbPrefix}`,
|
|
164947
166450
|
mediaPath: `/addon/${REOLINK_ADDON_ID}/${service.mediaPrefix}`,
|
|
166451
|
+
streamPath: `/addon/${REOLINK_ADDON_ID}/${service.streamPrefix}`,
|
|
164948
166452
|
thumbServed: thumb !== void 0,
|
|
164949
|
-
mediaServed: media !== void 0
|
|
166453
|
+
mediaServed: media !== void 0,
|
|
166454
|
+
streamServed: stream !== void 0
|
|
164950
166455
|
} });
|
|
164951
166456
|
} catch (err) {
|
|
164952
166457
|
this.ctx.logger.warn("Reolink clip data planes failed to serve — clips will not render", { meta: { error: err instanceof Error ? err.message : String(err) } });
|