@homebridge-plugins/homebridge-eufy-security 4.7.2 → 5.0.0-beta.1
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/AUTHORS +4 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +77 -0
- package/README.md +152 -49
- package/SECURITY.md +30 -0
- package/config.schema.json +92 -3
- package/dist/account/ownership.js +290 -0
- package/dist/account/ownership.js.map +1 -0
- package/dist/account/persistence.js +274 -0
- package/dist/account/persistence.js.map +1 -0
- package/dist/account/temporary-authentication.js +348 -0
- package/dist/account/temporary-authentication.js.map +1 -0
- package/dist/configuration.js +167 -0
- package/dist/configuration.js.map +1 -0
- package/dist/device/member-evidence.js +66 -0
- package/dist/device/member-evidence.js.map +1 -0
- package/dist/device/member-trust.js +114 -0
- package/dist/device/member-trust.js.map +1 -0
- package/dist/device/snapshot.js +190 -0
- package/dist/device/snapshot.js.map +1 -0
- package/dist/diagnostics.js +2218 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/homekit/adapter.js +2 -0
- package/dist/homekit/adapter.js.map +1 -0
- package/dist/homekit/adapters/battery.js +208 -0
- package/dist/homekit/adapters/battery.js.map +1 -0
- package/dist/homekit/adapters/camera-controls.js +527 -0
- package/dist/homekit/adapters/camera-controls.js.map +1 -0
- package/dist/homekit/adapters/camera-streaming.js +1515 -0
- package/dist/homekit/adapters/camera-streaming.js.map +1 -0
- package/dist/homekit/adapters/contact.js +138 -0
- package/dist/homekit/adapters/contact.js.map +1 -0
- package/dist/homekit/adapters/doorbell.js +57 -0
- package/dist/homekit/adapters/doorbell.js.map +1 -0
- package/dist/homekit/adapters/information.js +55 -0
- package/dist/homekit/adapters/information.js.map +1 -0
- package/dist/homekit/adapters/lock.js +489 -0
- package/dist/homekit/adapters/lock.js.map +1 -0
- package/dist/homekit/adapters/motion.js +100 -0
- package/dist/homekit/adapters/motion.js.map +1 -0
- package/dist/homekit/adapters/registry.js +26 -0
- package/dist/homekit/adapters/registry.js.map +1 -0
- package/dist/homekit/adapters/security-system.js +425 -0
- package/dist/homekit/adapters/security-system.js.map +1 -0
- package/dist/homekit/adapters/siren.js +136 -0
- package/dist/homekit/adapters/siren.js.map +1 -0
- package/dist/homekit/adapters/smart-light.js +662 -0
- package/dist/homekit/adapters/smart-light.js.map +1 -0
- package/dist/homekit/coverage-matrix.js +119 -0
- package/dist/homekit/coverage-matrix.js.map +1 -0
- package/dist/homekit/device-control.js +175 -0
- package/dist/homekit/device-control.js.map +1 -0
- package/dist/homekit/reconciler.js +319 -0
- package/dist/homekit/reconciler.js.map +1 -0
- package/dist/homekit/representation.js +23 -0
- package/dist/homekit/representation.js.map +1 -0
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/media/contracts.js +2 -0
- package/dist/media/contracts.js.map +1 -0
- package/dist/media/last-successful-image.js +208 -0
- package/dist/media/last-successful-image.js.map +1 -0
- package/dist/media/live-stream.js +1003 -0
- package/dist/media/live-stream.js.map +1 -0
- package/dist/media/recording.js +560 -0
- package/dist/media/recording.js.map +1 -0
- package/dist/media/session-budget.js +39 -0
- package/dist/media/session-budget.js.map +1 -0
- package/dist/media/snapshot.js +406 -0
- package/dist/media/snapshot.js.map +1 -0
- package/dist/media/station-live-sessions.js +101 -0
- package/dist/media/station-live-sessions.js.map +1 -0
- package/dist/platform.js +97 -809
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +375 -0
- package/dist/runtime/channel.js.map +1 -0
- package/dist/runtime/owner.js +682 -0
- package/dist/runtime/owner.js.map +1 -0
- package/dist/runtime/sdk-client.js +211 -0
- package/dist/runtime/sdk-client.js.map +1 -0
- package/dist/runtime/tracker.js +211 -0
- package/dist/runtime/tracker.js.map +1 -0
- package/dist/settings.js +1 -43
- package/dist/settings.js.map +1 -1
- package/dist/storage.js +59 -0
- package/dist/storage.js.map +1 -0
- package/dist/ui/dashboard.js +181 -0
- package/dist/ui/dashboard.js.map +1 -0
- package/dist/ui/device-image.js +22 -0
- package/dist/ui/device-image.js.map +1 -0
- package/dist/ui/runtime-channel-client.js +305 -0
- package/dist/ui/runtime-channel-client.js.map +1 -0
- package/dist/ui/server.js +259 -0
- package/dist/ui/server.js.map +1 -0
- package/homebridge-ui/public/app.css +1902 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T1250.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2070.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2080.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2081.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2103.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2117.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2118.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2119.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T211A.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2120.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2123.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2128.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2130.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2132.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2150.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2181.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2182.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2190.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2192.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2193.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2194.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2210.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2250.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2251.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2252.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2253.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2254.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2255.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2256.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2257.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2258.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2259.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2261.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2262.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2266.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2267.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2268.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2270.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2272.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2273.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2276.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2277.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2278.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2280.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2292.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2320.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2351.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2352.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2353.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L00.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L01.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L02.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L04.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L10.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L20.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L30.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L40.webp +0 -0
- package/homebridge-ui/public/assets/devices/mower/mower-T2801.webp +0 -0
- package/homebridge-ui/public/assets/devices/mower/mower-T280B.webp +0 -0
- package/homebridge-ui/public/assets/devices/mower/mower-T2880.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T7400.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T7401.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8001.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8002.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8010.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8021.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8023.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8025.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8030.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8110.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8111.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8112.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8113.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8114.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8122.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8123.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8124.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8130.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8131.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8134.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8140.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8141.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8150.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8151.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8152.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8153.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8160.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8161.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8162.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8170.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8171.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8172.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8173.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8200.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8200X.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8201.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8203.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8210.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8213.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8214.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8222.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8400.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8401.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8410.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8411.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8414.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8416.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8417.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8419.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8419N.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8420.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8420X.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8422.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8423.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8424.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8425.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8441.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8442.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8452.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T84A0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T84A1.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8500.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8502.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8503.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8504.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8506.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8510.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8510P.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8520.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8520P.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8530.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8531.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85D0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85L0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85P0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85V0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8600.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T86P2.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8790.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T87B0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T87B2.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8900.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8910.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8920.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8960.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8B0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8E00.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8N00.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8W11C.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90F0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90M0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90R0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90S0.webp +0 -0
- package/homebridge-ui/public/assets/icons/inventory.svg +1 -1
- package/homebridge-ui/public/assets/icons/troubleshoot.svg +1 -0
- package/homebridge-ui/public/assets/icons/warning.svg +1 -1
- package/homebridge-ui/public/assets/logo-dark.svg +10 -0
- package/homebridge-ui/public/assets/logo.svg +10 -0
- package/homebridge-ui/public/i18n/en.json +218 -0
- package/homebridge-ui/public/i18n/fr.json +218 -0
- package/homebridge-ui/public/index.html +377 -28
- package/homebridge-ui/public/js/app.js +1065 -0
- package/homebridge-ui/public/js/dashboard.js +385 -0
- package/homebridge-ui/public/js/legacy-settings.js +37 -0
- package/homebridge-ui/public/js/profile-wizard.js +68 -0
- package/homebridge-ui/server.js +2 -1393
- package/i18n/runtime/en.json +100 -0
- package/media/Snapshot-Unavailable.jpg +0 -0
- package/media/Snapshot-black.jpg +0 -0
- package/media/camera-disabled.jpg +0 -0
- package/media/camera-offline.jpg +0 -0
- package/package.json +29 -24
- package/.claude/CLAUDE.md +0 -175
- package/.claude/PRD.md +0 -241
- package/.claude/docs/debug-recording-lessons.md +0 -91
- package/.claude/docs/hksv-recording-fix.md +0 -160
- package/.claude/skills/architect/SKILL.md +0 -76
- package/.claude/skills/developer/SKILL.md +0 -59
- package/.claude/skills/new-device-support/SKILL.md +0 -212
- package/.claude/skills/new-device-support/check-device.mjs +0 -363
- package/.claude/skills/new-device-support/map-properties.mjs +0 -450
- package/.claude/skills/new-device-support/verify-device.mjs +0 -272
- package/.claude/skills/planner/SKILL.md +0 -100
- package/.claude/skills/qa/SKILL.md +0 -79
- package/.claude/skills/support/SKILL.md +0 -175
- package/.prettierignore +0 -7
- package/.prettierrc.json +0 -7
- package/FUNDING.yml +0 -1
- package/dist/accessories/AutoSyncStationAccessory.js +0 -157
- package/dist/accessories/AutoSyncStationAccessory.js.map +0 -1
- package/dist/accessories/BaseAccessory.js +0 -261
- package/dist/accessories/BaseAccessory.js.map +0 -1
- package/dist/accessories/CameraAccessory.js +0 -459
- package/dist/accessories/CameraAccessory.js.map +0 -1
- package/dist/accessories/Device.js +0 -65
- package/dist/accessories/Device.js.map +0 -1
- package/dist/accessories/EntrySensorAccessory.js +0 -48
- package/dist/accessories/EntrySensorAccessory.js.map +0 -1
- package/dist/accessories/GarageDoorAccessory.js +0 -160
- package/dist/accessories/GarageDoorAccessory.js.map +0 -1
- package/dist/accessories/LockAccessory.js +0 -142
- package/dist/accessories/LockAccessory.js.map +0 -1
- package/dist/accessories/MotionSensorAccessory.js +0 -48
- package/dist/accessories/MotionSensorAccessory.js.map +0 -1
- package/dist/accessories/SmartDropAccessory.js +0 -141
- package/dist/accessories/SmartDropAccessory.js.map +0 -1
- package/dist/accessories/StationAccessory.js +0 -373
- package/dist/accessories/StationAccessory.js.map +0 -1
- package/dist/config.js +0 -26
- package/dist/config.js.map +0 -1
- package/dist/controller/DebugRecordingManager.js +0 -402
- package/dist/controller/DebugRecordingManager.js.map +0 -1
- package/dist/controller/LocalLivestreamManager.js +0 -371
- package/dist/controller/LocalLivestreamManager.js.map +0 -1
- package/dist/controller/recordingDelegate.js +0 -252
- package/dist/controller/recordingDelegate.js.map +0 -1
- package/dist/controller/snapshotDelegate.js +0 -348
- package/dist/controller/snapshotDelegate.js.map +0 -1
- package/dist/controller/streamingDelegate.js +0 -389
- package/dist/controller/streamingDelegate.js.map +0 -1
- package/dist/interfaces.js +0 -2
- package/dist/interfaces.js.map +0 -1
- package/dist/media/Snapshot-Unavailable.png +0 -0
- package/dist/media/Snapshot-Unavailable.xcf +0 -0
- package/dist/media/Snapshot-black.png +0 -0
- package/dist/media/camera-disabled.png +0 -0
- package/dist/media/camera-offline.png +0 -0
- package/dist/media/media/Snapshot-Unavailable.png +0 -0
- package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
- package/dist/media/media/Snapshot-black.png +0 -0
- package/dist/media/media/camera-disabled.png +0 -0
- package/dist/media/media/camera-offline.png +0 -0
- package/dist/utils/Talkback.js +0 -162
- package/dist/utils/Talkback.js.map +0 -1
- package/dist/utils/accessoriesStore.js +0 -210
- package/dist/utils/accessoriesStore.js.map +0 -1
- package/dist/utils/configTypes.js +0 -38
- package/dist/utils/configTypes.js.map +0 -1
- package/dist/utils/ffmpeg.js +0 -1111
- package/dist/utils/ffmpeg.js.map +0 -1
- package/dist/utils/interfaces.js +0 -8
- package/dist/utils/interfaces.js.map +0 -1
- package/dist/utils/utils.js +0 -224
- package/dist/utils/utils.js.map +0 -1
- package/dist/version.js +0 -2
- package/dist/version.js.map +0 -1
- package/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/diagnosticsCrypto.js +0 -112
- package/homebridge-ui/public/app.js +0 -237
- package/homebridge-ui/public/assets/devices/4g_lte_cam_s330_large.png +0 -0
- package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
- package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell2k_large.png +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
- package/homebridge-ui/public/assets/devices/eufyCamS4_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/flood_freeze_sensor_e20_t90F0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/homebasemini_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC220_t8419N_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcams350_large.png +0 -0
- package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
- package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
- package/homebridge-ui/public/assets/devices/nvr_s4_max_T8N00_large.png +0 -0
- package/homebridge-ui/public/assets/devices/pir_sensor_e20_large.png +0 -0
- package/homebridge-ui/public/assets/devices/poe_bullet_ptz_cam_s4_T8E00_large.png +0 -0
- package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
- package/homebridge-ui/public/assets/devices/siren_sensor_e20_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85D0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85L0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85P0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85V0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_video_t8531_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smoke_alarm_e10_t90S0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocame42_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/unknown.png +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s100_large.png +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s120_large.png +0 -0
- package/homebridge-ui/public/assets/devices/water_freeze_sensor_t8920_large.png +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
- package/homebridge-ui/public/assets/icons/attach.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_0.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_1.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_2.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_3.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_4.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_5.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_6.svg +0 -1
- package/homebridge-ui/public/assets/icons/copy.svg +0 -1
- package/homebridge-ui/public/assets/icons/delete.svg +0 -1
- package/homebridge-ui/public/assets/icons/download.svg +0 -1
- package/homebridge-ui/public/assets/icons/info.svg +0 -1
- package/homebridge-ui/public/assets/icons/refresh.svg +0 -1
- package/homebridge-ui/public/assets/icons/satellite_alt.svg +0 -1
- package/homebridge-ui/public/assets/icons/solar_power.svg +0 -1
- package/homebridge-ui/public/components/device-card.js +0 -174
- package/homebridge-ui/public/components/guard-modes.js +0 -88
- package/homebridge-ui/public/components/number-input.js +0 -121
- package/homebridge-ui/public/components/select.js +0 -73
- package/homebridge-ui/public/components/toggle.js +0 -68
- package/homebridge-ui/public/services/api.js +0 -250
- package/homebridge-ui/public/services/config.js +0 -144
- package/homebridge-ui/public/style.css +0 -818
- package/homebridge-ui/public/utils/countries.js +0 -73
- package/homebridge-ui/public/utils/device-images.js +0 -102
- package/homebridge-ui/public/utils/helpers.js +0 -115
- package/homebridge-ui/public/views/dashboard.js +0 -226
- package/homebridge-ui/public/views/device-detail.js +0 -654
- package/homebridge-ui/public/views/diagnostics.js +0 -402
- package/homebridge-ui/public/views/login.js +0 -650
- package/homebridge-ui/public/views/recordings.js +0 -319
- package/homebridge-ui/public/views/settings.js +0 -197
- package/homebridge-ui/public/views/unsupported-detail.js +0 -296
- package/media/Snapshot-Unavailable.png +0 -0
- package/media/Snapshot-Unavailable.xcf +0 -0
- package/media/Snapshot-black.png +0 -0
- package/media/camera-disabled.png +0 -0
- package/media/camera-offline.png +0 -0
- package/scripts/decrypt-diagnostics.mjs +0 -257
package/dist/utils/ffmpeg.js
DELETED
|
@@ -1,1111 +0,0 @@
|
|
|
1
|
-
// Node built-ins
|
|
2
|
-
import { execFileSync, spawn } from 'child_process';
|
|
3
|
-
import EventEmitter from 'events';
|
|
4
|
-
import { mkdirSync } from 'fs';
|
|
5
|
-
import net from 'net';
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
// External packages
|
|
9
|
-
import ffmpegPath from 'ffmpeg-for-homebridge';
|
|
10
|
-
import { pickPort } from 'pick-port';
|
|
11
|
-
import { ffmpegLogger } from './utils.js';
|
|
12
|
-
/** Timeout for one-shot TCP servers waiting for a connection (ms) */
|
|
13
|
-
const TCP_SERVER_TIMEOUT_MS = 30_000;
|
|
14
|
-
/** Timeout for ffmpeg getResult() before force-killing the process (ms) */
|
|
15
|
-
const PROCESS_RESULT_TIMEOUT_MS = 15_000;
|
|
16
|
-
/** Grace period after SIGTERM before sending SIGKILL (ms) */
|
|
17
|
-
const KILL_GRACE_PERIOD_MS = 2_000;
|
|
18
|
-
/** Maximum allowed fMP4 box size (50 MB). Anything larger is likely corruption. */
|
|
19
|
-
const MAX_FRAG_BOX_SIZE = 50 * 1024 * 1024;
|
|
20
|
-
/** Returns true when the value is a non-empty string (guards `undefined | ''`). */
|
|
21
|
-
function isNonEmpty(value) {
|
|
22
|
-
return !!value && value !== '';
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Cached result of probing the ffmpeg binary for libfdk_aac support.
|
|
26
|
-
* `undefined` means we haven't checked yet.
|
|
27
|
-
*/
|
|
28
|
-
let _hasFdkAac;
|
|
29
|
-
/**
|
|
30
|
-
* Returns true when the ffmpeg binary on this system supports the libfdk_aac
|
|
31
|
-
* encoder. The result is cached after the first call.
|
|
32
|
-
*/
|
|
33
|
-
export function hasFdkAac() {
|
|
34
|
-
if (_hasFdkAac !== undefined) {
|
|
35
|
-
return _hasFdkAac;
|
|
36
|
-
}
|
|
37
|
-
const ffmpegExec = ffmpegPath || 'ffmpeg';
|
|
38
|
-
try {
|
|
39
|
-
const output = execFileSync(ffmpegExec, ['-encoders'], {
|
|
40
|
-
timeout: 5_000,
|
|
41
|
-
encoding: 'utf-8',
|
|
42
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
43
|
-
});
|
|
44
|
-
_hasFdkAac = output.includes('libfdk_aac');
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
_hasFdkAac = false;
|
|
48
|
-
}
|
|
49
|
-
if (_hasFdkAac) {
|
|
50
|
-
ffmpegLogger.info('libfdk_aac encoder available.');
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
ffmpegLogger.warn('libfdk_aac encoder is NOT available — falling back to the native aac encoder (AAC-LC instead of AAC-ELD). ' +
|
|
54
|
-
'This usually means the ffmpeg-for-homebridge binary could not be installed on your platform. ' +
|
|
55
|
-
'You can install a compatible ffmpeg manually (built with --enable-libfdk-aac --enable-nonfree) ' +
|
|
56
|
-
"and set the path in your camera's videoConfig.videoProcessor setting.");
|
|
57
|
-
}
|
|
58
|
-
return _hasFdkAac;
|
|
59
|
-
}
|
|
60
|
-
/** Cached result: `undefined` = not yet probed, `null` = no hardware encoder found. */
|
|
61
|
-
let _hwEncoder;
|
|
62
|
-
/**
|
|
63
|
-
* Probes for hardware H.264 encoders by running a real test encode.
|
|
64
|
-
* Result is cached after the first call. Call early at platform init.
|
|
65
|
-
*/
|
|
66
|
-
export function probeHardwareEncoder(hostSystem) {
|
|
67
|
-
if (_hwEncoder !== undefined)
|
|
68
|
-
return _hwEncoder;
|
|
69
|
-
const ffmpegExec = ffmpegPath || 'ffmpeg';
|
|
70
|
-
const candidates = [
|
|
71
|
-
{
|
|
72
|
-
systems: ['macOS.Apple', 'macOS.Intel'],
|
|
73
|
-
encoder: 'h264_videotoolbox',
|
|
74
|
-
hwaccel: 'videotoolbox',
|
|
75
|
-
options: '-allow_sw 1 -realtime 1',
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
systems: ['raspbian'],
|
|
79
|
-
encoder: 'h264_v4l2m2m',
|
|
80
|
-
options: '',
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
systems: ['generic', 'raspbian'],
|
|
84
|
-
encoder: 'h264_vaapi',
|
|
85
|
-
hwaccel: 'vaapi',
|
|
86
|
-
options: '',
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
systems: ['generic'],
|
|
90
|
-
encoder: 'h264_qsv',
|
|
91
|
-
hwaccel: 'qsv',
|
|
92
|
-
options: '-preset veryfast',
|
|
93
|
-
},
|
|
94
|
-
];
|
|
95
|
-
const toTry = candidates.filter((c) => c.systems.includes(hostSystem) || c.systems.includes('generic'));
|
|
96
|
-
for (const candidate of toTry) {
|
|
97
|
-
try {
|
|
98
|
-
// Include candidate-specific options (e.g. `-allow_sw 1` for VideoToolbox)
|
|
99
|
-
// so the probe reflects actual encoding conditions.
|
|
100
|
-
const extraArgs = candidate.options ? candidate.options.split(/\s+/) : [];
|
|
101
|
-
execFileSync(ffmpegExec, [
|
|
102
|
-
'-hide_banner',
|
|
103
|
-
'-loglevel',
|
|
104
|
-
'error',
|
|
105
|
-
'-f',
|
|
106
|
-
'lavfi',
|
|
107
|
-
'-i',
|
|
108
|
-
'color=black:s=64x64:d=0.1',
|
|
109
|
-
'-c:v',
|
|
110
|
-
candidate.encoder,
|
|
111
|
-
...extraArgs,
|
|
112
|
-
'-frames:v',
|
|
113
|
-
'1',
|
|
114
|
-
'-f',
|
|
115
|
-
'null',
|
|
116
|
-
'-',
|
|
117
|
-
], { timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
118
|
-
_hwEncoder = {
|
|
119
|
-
encoder: candidate.encoder,
|
|
120
|
-
decoder: candidate.decoder,
|
|
121
|
-
hwaccel: candidate.hwaccel,
|
|
122
|
-
customOptions: candidate.options,
|
|
123
|
-
};
|
|
124
|
-
ffmpegLogger.info(`Hardware encoder detected and validated: ${candidate.encoder}`);
|
|
125
|
-
return _hwEncoder;
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
// This encoder doesn't work — try next candidate
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
_hwEncoder = null;
|
|
132
|
-
ffmpegLogger.info('No hardware H.264 encoder available — using software encoding (libx264). ' +
|
|
133
|
-
'If running in Docker, make sure hardware devices are exposed to the container ' +
|
|
134
|
-
'(e.g. --device /dev/dri for VAAPI, --device /dev/video* for V4L2).');
|
|
135
|
-
return _hwEncoder;
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Returns the preferred AAC encoder and codec options for AAC-ELD output.
|
|
139
|
-
* Falls back to the built-in `aac` encoder when `libfdk_aac` is absent.
|
|
140
|
-
*/
|
|
141
|
-
function getAacEldCodecAndOptions() {
|
|
142
|
-
if (hasFdkAac()) {
|
|
143
|
-
return { codec: 'libfdk_aac', codecOptions: '-profile:a aac_eld' };
|
|
144
|
-
}
|
|
145
|
-
// The native aac encoder doesn't support ELD, but LC works with HomeKit.
|
|
146
|
-
return { codec: 'aac', codecOptions: '-profile:a aac_low' };
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Returns the preferred AAC encoder for a given recording codec type.
|
|
150
|
-
* Falls back to the built-in `aac` encoder when `libfdk_aac` is absent.
|
|
151
|
-
*/
|
|
152
|
-
function getAacRecordingCodecAndOptions(codecType) {
|
|
153
|
-
const isLC = codecType === 0 /* AudioRecordingCodecType.AAC_LC */;
|
|
154
|
-
if (hasFdkAac()) {
|
|
155
|
-
return {
|
|
156
|
-
codec: 'libfdk_aac',
|
|
157
|
-
codecOptions: isLC ? '-profile:a aac_low' : '-profile:a aac_eld',
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
codec: 'aac',
|
|
162
|
-
codecOptions: '-profile:a aac_low',
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
/** Map HomeKit audio samplerate enum values to kHz strings for ffmpeg `-ar`. */
|
|
166
|
-
const SAMPLERATE_MAP = new Map([
|
|
167
|
-
[0 /* AudioRecordingSamplerate.KHZ_8 */, '8'],
|
|
168
|
-
[1 /* AudioRecordingSamplerate.KHZ_16 */, '16'],
|
|
169
|
-
[2 /* AudioRecordingSamplerate.KHZ_24 */, '24'],
|
|
170
|
-
[3 /* AudioRecordingSamplerate.KHZ_32 */, '32'],
|
|
171
|
-
[4 /* AudioRecordingSamplerate.KHZ_44_1 */, '44.1'],
|
|
172
|
-
[5 /* AudioRecordingSamplerate.KHZ_48 */, '48'],
|
|
173
|
-
]);
|
|
174
|
-
/**
|
|
175
|
-
* Creates a TCP server that accepts exactly one connection, then auto-closes.
|
|
176
|
-
* If no connection arrives within the timeout, the server closes anyway.
|
|
177
|
-
* @returns The port the server is listening on.
|
|
178
|
-
*/
|
|
179
|
-
async function createOneShotTcpServer(onConnection, existingPort) {
|
|
180
|
-
const port = existingPort ?? (await pickPort({ type: 'tcp' }));
|
|
181
|
-
// eslint-disable-next-line prefer-const
|
|
182
|
-
let killTimeout;
|
|
183
|
-
const server = net.createServer((socket) => {
|
|
184
|
-
if (killTimeout) {
|
|
185
|
-
clearTimeout(killTimeout);
|
|
186
|
-
}
|
|
187
|
-
server.close();
|
|
188
|
-
socket.on('error', () => { }); // ignore — handled elsewhere
|
|
189
|
-
onConnection(socket);
|
|
190
|
-
});
|
|
191
|
-
server.on('error', () => { }); // ignore — handled elsewhere
|
|
192
|
-
killTimeout = setTimeout(() => {
|
|
193
|
-
server.close();
|
|
194
|
-
}, TCP_SERVER_TIMEOUT_MS);
|
|
195
|
-
server.listen(port);
|
|
196
|
-
return { port, server };
|
|
197
|
-
}
|
|
198
|
-
class FFmpegProgress extends EventEmitter {
|
|
199
|
-
started = false;
|
|
200
|
-
constructor() {
|
|
201
|
-
super();
|
|
202
|
-
}
|
|
203
|
-
static async create(port) {
|
|
204
|
-
const instance = new FFmpegProgress();
|
|
205
|
-
const { server } = await createOneShotTcpServer((socket) => {
|
|
206
|
-
socket.on('data', instance.analyzeProgress.bind(instance));
|
|
207
|
-
}, port);
|
|
208
|
-
server.on('close', () => instance.emit('progress stopped'));
|
|
209
|
-
return instance;
|
|
210
|
-
}
|
|
211
|
-
analyzeProgress(progressData) {
|
|
212
|
-
const progress = new Map();
|
|
213
|
-
progressData
|
|
214
|
-
.toString()
|
|
215
|
-
.split(/\r?\n/)
|
|
216
|
-
.forEach((line) => {
|
|
217
|
-
const split = line.split('=', 2);
|
|
218
|
-
if (split.length !== 2) {
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
progress.set(split[0], split[1]);
|
|
222
|
-
});
|
|
223
|
-
if (!this.started) {
|
|
224
|
-
if (progress.get('progress') !== undefined) {
|
|
225
|
-
this.started = true;
|
|
226
|
-
this.emit('progress started');
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Build an AAC AudioSpecificConfig hex string for the SDP fmtp line.
|
|
233
|
-
* This replaces the previously hardcoded `F8F0212C00BC00` which was
|
|
234
|
-
* only valid for AAC-ELD at 16 kHz mono.
|
|
235
|
-
*
|
|
236
|
-
* AudioSpecificConfig structure (ISO 14496-3):
|
|
237
|
-
* audioObjectType (5 bits) — 39 for AAC-ELD
|
|
238
|
-
* samplingFrequencyIndex (4 bits)
|
|
239
|
-
* channelConfiguration (4 bits)
|
|
240
|
-
* ELD-specific config follows
|
|
241
|
-
*/
|
|
242
|
-
function buildAacEldAudioSpecificConfig(sampleRateKHz, channels) {
|
|
243
|
-
// Sampling frequency index lookup (ISO 14496-3 Table 1.18)
|
|
244
|
-
const sampleRateHz = sampleRateKHz * 1000;
|
|
245
|
-
// AAC-ELD = audioObjectType 39 (5-bit overflow: write 31 then 8 more bits for 39-32=7)
|
|
246
|
-
// Bit layout: 11111 | 00000111 | freqIndex(4) | channels(4) | ELD-specific...
|
|
247
|
-
// The ELD-specific config for basic ELD (no SBR): frameLengthFlag=0, aacSectionDataResilienceFlag=0, etc.
|
|
248
|
-
// For simplicity and compatibility, use known-good configs per sample rate
|
|
249
|
-
const configs = {
|
|
250
|
-
8000: { 1: 'F8E85C00' },
|
|
251
|
-
16000: { 1: 'F8F0212C00BC00', 2: 'F8F0612C00BC00' },
|
|
252
|
-
24000: { 1: 'F8EC212C00BC00', 2: 'F8EC612C00BC00' },
|
|
253
|
-
};
|
|
254
|
-
const config = configs[sampleRateHz]?.[channels];
|
|
255
|
-
if (!config) {
|
|
256
|
-
ffmpegLogger.warn(`No AAC-ELD AudioSpecificConfig for ${sampleRateKHz} kHz / ${channels} ch — ` +
|
|
257
|
-
'falling back to 16 kHz mono. Return audio may not work correctly.');
|
|
258
|
-
}
|
|
259
|
-
return config ?? configs[16000][1];
|
|
260
|
-
}
|
|
261
|
-
/** Build an SDP session description for AAC-ELD return audio from HomeKit. */
|
|
262
|
-
function buildAacEldSdp(ipVer, sessionInfo, payloadType, sampleRateKHz, channels, maxBitrate, srtpSuite) {
|
|
263
|
-
const sampleRateHz = sampleRateKHz * 1000;
|
|
264
|
-
const config = buildAacEldAudioSpecificConfig(sampleRateKHz, channels);
|
|
265
|
-
return ('v=0\r\n' +
|
|
266
|
-
`o=- 0 0 IN ${ipVer} ${sessionInfo.address}\r\n` +
|
|
267
|
-
's=Talk\r\n' +
|
|
268
|
-
`c=IN ${ipVer} ${sessionInfo.address}\r\n` +
|
|
269
|
-
't=0 0\r\n' +
|
|
270
|
-
`m=audio ${sessionInfo.audioReturnPort} RTP/AVP ${payloadType}\r\n` +
|
|
271
|
-
`b=AS:${maxBitrate}\r\n` +
|
|
272
|
-
`a=rtpmap:${payloadType} MPEG4-GENERIC/${sampleRateHz}/${channels}\r\n` +
|
|
273
|
-
'a=rtcp-mux\r\n' +
|
|
274
|
-
`a=fmtp:${payloadType} ` +
|
|
275
|
-
`profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; ` +
|
|
276
|
-
`config=${config}\r\n` +
|
|
277
|
-
`a=crypto:1 ${srtpSuite} inline:${sessionInfo.audioSRTP.toString('base64')}\r\n`);
|
|
278
|
-
}
|
|
279
|
-
/** Build an SDP session description for Opus return audio from HomeKit. */
|
|
280
|
-
function buildOpusSdp(ipVer, sessionInfo, payloadType, sampleRateKHz, channels, maxBitrate, srtpSuite) {
|
|
281
|
-
// Opus in RTP always uses 48000 Hz clock rate (RFC 7587), regardless of actual sample rate
|
|
282
|
-
const clockRate = 48000;
|
|
283
|
-
return ('v=0\r\n' +
|
|
284
|
-
`o=- 0 0 IN ${ipVer} ${sessionInfo.address}\r\n` +
|
|
285
|
-
's=Talk\r\n' +
|
|
286
|
-
`c=IN ${ipVer} ${sessionInfo.address}\r\n` +
|
|
287
|
-
't=0 0\r\n' +
|
|
288
|
-
`m=audio ${sessionInfo.audioReturnPort} RTP/AVP ${payloadType}\r\n` +
|
|
289
|
-
`b=AS:${maxBitrate}\r\n` +
|
|
290
|
-
`a=rtpmap:${payloadType} opus/${clockRate}/${channels}\r\n` +
|
|
291
|
-
'a=rtcp-mux\r\n' +
|
|
292
|
-
`a=fmtp:${payloadType} minptime=10;useinbandfec=1;sprop-maxcapturerate=${sampleRateKHz * 1000}${channels === 2 ? ';sprop-stereo=1' : ''}\r\n` +
|
|
293
|
-
`a=crypto:1 ${srtpSuite} inline:${sessionInfo.audioSRTP.toString('base64')}\r\n`);
|
|
294
|
-
}
|
|
295
|
-
export class FFmpegParameters {
|
|
296
|
-
progressPort;
|
|
297
|
-
debug;
|
|
298
|
-
// default parameters
|
|
299
|
-
processor;
|
|
300
|
-
hideBanner = true;
|
|
301
|
-
useWallclockAsTimestamp = true;
|
|
302
|
-
inputSource = '-i pipe:';
|
|
303
|
-
protocolWhitelist;
|
|
304
|
-
inputCodec;
|
|
305
|
-
inputFormat;
|
|
306
|
-
output = 'pipe:1';
|
|
307
|
-
isVideo;
|
|
308
|
-
isAudio;
|
|
309
|
-
isSnapshot;
|
|
310
|
-
// generic options
|
|
311
|
-
analyzeDuration;
|
|
312
|
-
probeSize;
|
|
313
|
-
stimeout;
|
|
314
|
-
readrate;
|
|
315
|
-
codec = 'copy';
|
|
316
|
-
codecOptions;
|
|
317
|
-
bitrate;
|
|
318
|
-
// output options
|
|
319
|
-
payloadType;
|
|
320
|
-
ssrc;
|
|
321
|
-
srtpSuite;
|
|
322
|
-
srtpParams;
|
|
323
|
-
format;
|
|
324
|
-
// video options
|
|
325
|
-
fps;
|
|
326
|
-
pixFormat;
|
|
327
|
-
colorRange;
|
|
328
|
-
filters;
|
|
329
|
-
width;
|
|
330
|
-
height;
|
|
331
|
-
bufsize;
|
|
332
|
-
maxrate;
|
|
333
|
-
crop = false;
|
|
334
|
-
// audio options
|
|
335
|
-
sampleRate;
|
|
336
|
-
channels;
|
|
337
|
-
flagsGlobalHeader = false;
|
|
338
|
-
// snapshot options
|
|
339
|
-
numberFrames;
|
|
340
|
-
delaySnapshot = false;
|
|
341
|
-
// recording options / fragmented mp4
|
|
342
|
-
movflags;
|
|
343
|
-
maxMuxingQueueSize;
|
|
344
|
-
iFrameInterval;
|
|
345
|
-
processAudio = true;
|
|
346
|
-
// debug file recording (parallel copy of stream to disk)
|
|
347
|
-
fileRecordingPath;
|
|
348
|
-
constructor(port, isVideo, isAudio, isSnapshot, debug = false) {
|
|
349
|
-
this.progressPort = port;
|
|
350
|
-
this.isVideo = isVideo;
|
|
351
|
-
this.isAudio = isAudio;
|
|
352
|
-
this.isSnapshot = isSnapshot;
|
|
353
|
-
this.debug = debug;
|
|
354
|
-
}
|
|
355
|
-
/** Allocate a progress port and construct an instance. */
|
|
356
|
-
static async create(isVideo, isAudio, isSnapshot, debug) {
|
|
357
|
-
const port = await pickPort({ type: 'tcp' });
|
|
358
|
-
return new FFmpegParameters(port, isVideo, isAudio, isSnapshot, debug);
|
|
359
|
-
}
|
|
360
|
-
static async forAudio(debug = false) {
|
|
361
|
-
const ffmpeg = await FFmpegParameters.create(false, true, false, debug);
|
|
362
|
-
ffmpeg.useWallclockAsTimestamp = false;
|
|
363
|
-
ffmpeg.flagsGlobalHeader = true;
|
|
364
|
-
return ffmpeg;
|
|
365
|
-
}
|
|
366
|
-
static async forVideo(debug = false) {
|
|
367
|
-
return FFmpegParameters.create(true, false, false, debug);
|
|
368
|
-
}
|
|
369
|
-
static async forSnapshot(debug = false) {
|
|
370
|
-
const ffmpeg = await FFmpegParameters.create(false, false, true, debug);
|
|
371
|
-
ffmpeg.useWallclockAsTimestamp = false;
|
|
372
|
-
ffmpeg.numberFrames = 1;
|
|
373
|
-
ffmpeg.format = 'image2';
|
|
374
|
-
return ffmpeg;
|
|
375
|
-
}
|
|
376
|
-
static async forVideoRecording(debug = false) {
|
|
377
|
-
const ffmpeg = await FFmpegParameters.create(true, false, false, debug);
|
|
378
|
-
ffmpeg.useWallclockAsTimestamp = true;
|
|
379
|
-
return ffmpeg;
|
|
380
|
-
}
|
|
381
|
-
static async forAudioRecording(debug = false) {
|
|
382
|
-
return FFmpegParameters.create(false, true, false, debug);
|
|
383
|
-
}
|
|
384
|
-
setResolution(width, height) {
|
|
385
|
-
this.width = width;
|
|
386
|
-
this.height = height;
|
|
387
|
-
}
|
|
388
|
-
usesStdInAsInput() {
|
|
389
|
-
return this.inputSource === '-i pipe:';
|
|
390
|
-
}
|
|
391
|
-
setInputSource(value) {
|
|
392
|
-
this.inputSource = `-i ${value}`;
|
|
393
|
-
}
|
|
394
|
-
setInputFormat(value) {
|
|
395
|
-
this.inputFormat = value;
|
|
396
|
-
}
|
|
397
|
-
setInputCodec(value) {
|
|
398
|
-
this.inputCodec = value;
|
|
399
|
-
}
|
|
400
|
-
async setInputStream(input) {
|
|
401
|
-
const { port } = await createOneShotTcpServer((socket) => {
|
|
402
|
-
// Manual backpressure handling instead of pipe() — prevents
|
|
403
|
-
// unbounded memory growth when FFmpeg is slower than the camera
|
|
404
|
-
// (common on Raspberry Pi during HKSV transcoding).
|
|
405
|
-
socket.on('drain', () => input.resume());
|
|
406
|
-
input.on('data', (chunk) => {
|
|
407
|
-
if (!socket.write(chunk)) {
|
|
408
|
-
input.pause();
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
input.on('end', () => socket.end());
|
|
412
|
-
input.on('error', () => socket.destroy());
|
|
413
|
-
socket.on('error', () => {
|
|
414
|
-
if (!input.destroyed)
|
|
415
|
-
input.destroy();
|
|
416
|
-
});
|
|
417
|
-
socket.on('close', () => {
|
|
418
|
-
if (!input.destroyed)
|
|
419
|
-
input.destroy();
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
this.setInputSource(`tcp://127.0.0.1:${port}`);
|
|
423
|
-
}
|
|
424
|
-
setDelayedSnapshot() {
|
|
425
|
-
this.delaySnapshot = true;
|
|
426
|
-
}
|
|
427
|
-
setup(cameraConfig, request) {
|
|
428
|
-
const videoConfig = (cameraConfig.videoConfig ??= {});
|
|
429
|
-
if (isNonEmpty(videoConfig.videoProcessor)) {
|
|
430
|
-
this.processor = videoConfig.videoProcessor;
|
|
431
|
-
}
|
|
432
|
-
if (videoConfig.readRate) {
|
|
433
|
-
this.readrate = videoConfig.readRate;
|
|
434
|
-
}
|
|
435
|
-
if (videoConfig.stimeout) {
|
|
436
|
-
this.stimeout = videoConfig.stimeout;
|
|
437
|
-
}
|
|
438
|
-
if (videoConfig.probeSize) {
|
|
439
|
-
this.probeSize = videoConfig.probeSize;
|
|
440
|
-
}
|
|
441
|
-
if (videoConfig.analyzeDuration) {
|
|
442
|
-
this.analyzeDuration = videoConfig.analyzeDuration;
|
|
443
|
-
}
|
|
444
|
-
if (this.isVideo) {
|
|
445
|
-
const req = request;
|
|
446
|
-
if (isNonEmpty(videoConfig.vcodec)) {
|
|
447
|
-
this.codec = videoConfig.vcodec;
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
const hw = probeHardwareEncoder('generic');
|
|
451
|
-
this.codec = hw?.encoder ?? 'libx264';
|
|
452
|
-
}
|
|
453
|
-
if (this.codec !== 'copy') {
|
|
454
|
-
this.fps = videoConfig.maxFPS ?? req.video.fps;
|
|
455
|
-
const bitrate = videoConfig.maxBitrate ?? req.video.max_bit_rate;
|
|
456
|
-
this.bitrate = bitrate;
|
|
457
|
-
this.bufsize = bitrate * 2;
|
|
458
|
-
this.maxrate = bitrate;
|
|
459
|
-
// Resolve H.264 profile & level from the HomeKit request.
|
|
460
|
-
const startReq = request;
|
|
461
|
-
const profile = startReq.video.profile === 2 /* H264Profile.HIGH */
|
|
462
|
-
? 'high'
|
|
463
|
-
: startReq.video.profile === 1 /* H264Profile.MAIN */
|
|
464
|
-
? 'main'
|
|
465
|
-
: 'baseline';
|
|
466
|
-
const level = startReq.video.level === 2 /* H264Level.LEVEL4_0 */
|
|
467
|
-
? '4.0'
|
|
468
|
-
: startReq.video.level === 1 /* H264Level.LEVEL3_2 */
|
|
469
|
-
? '3.2'
|
|
470
|
-
: '3.1';
|
|
471
|
-
if (isNonEmpty(videoConfig.encoderOptions)) {
|
|
472
|
-
this.codecOptions = videoConfig.encoderOptions;
|
|
473
|
-
}
|
|
474
|
-
else if (this.codec === 'libx264') {
|
|
475
|
-
this.codecOptions = `-preset ultrafast -tune zerolatency -profile:v ${profile} -level:v ${level}`;
|
|
476
|
-
}
|
|
477
|
-
else {
|
|
478
|
-
const hw = probeHardwareEncoder('generic');
|
|
479
|
-
const hwOpts = hw && this.codec === hw.encoder ? hw.customOptions : '';
|
|
480
|
-
this.codecOptions = `${hwOpts} -profile:v ${profile} -level:v ${level}`.trim();
|
|
481
|
-
}
|
|
482
|
-
this.pixFormat = 'yuv420p';
|
|
483
|
-
this.colorRange = 'mpeg';
|
|
484
|
-
this.applyVisualConfig(req.video.width, req.video.height, videoConfig);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
if (this.isAudio) {
|
|
488
|
-
const req = request;
|
|
489
|
-
let codec;
|
|
490
|
-
let codecOptions;
|
|
491
|
-
switch (req.audio.codec) {
|
|
492
|
-
case "OPUS" /* AudioStreamingCodecType.OPUS */:
|
|
493
|
-
codec = 'libopus';
|
|
494
|
-
codecOptions = '-application lowdelay';
|
|
495
|
-
break;
|
|
496
|
-
default: {
|
|
497
|
-
const aac = getAacEldCodecAndOptions();
|
|
498
|
-
codec = aac.codec;
|
|
499
|
-
codecOptions = aac.codecOptions;
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
if (isNonEmpty(videoConfig.acodec)) {
|
|
504
|
-
codec = videoConfig.acodec;
|
|
505
|
-
codecOptions = '';
|
|
506
|
-
}
|
|
507
|
-
if (videoConfig.acodecOptions !== undefined) {
|
|
508
|
-
codecOptions = videoConfig.acodecOptions;
|
|
509
|
-
}
|
|
510
|
-
if (this.flagsGlobalHeader) {
|
|
511
|
-
if (codecOptions !== '') {
|
|
512
|
-
codecOptions += ' ';
|
|
513
|
-
}
|
|
514
|
-
codecOptions += '-flags +global_header';
|
|
515
|
-
}
|
|
516
|
-
this.codec = codec;
|
|
517
|
-
this.codecOptions = codecOptions;
|
|
518
|
-
if (this.codec !== 'copy') {
|
|
519
|
-
this.sampleRate = req.audio.sample_rate;
|
|
520
|
-
this.channels = req.audio.channel;
|
|
521
|
-
this.bitrate = videoConfig.audioBitrate ? videoConfig.audioBitrate : req.audio.max_bit_rate;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (this.isSnapshot) {
|
|
525
|
-
const req = request;
|
|
526
|
-
this.applyVisualConfig(req.width, req.height, videoConfig);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
setRTPTarget(sessionInfo, request) {
|
|
530
|
-
const isVideo = this.isVideo;
|
|
531
|
-
const mediaRequest = isVideo ? request.video : request.audio;
|
|
532
|
-
const port = isVideo ? sessionInfo.videoPort : sessionInfo.audioPort;
|
|
533
|
-
const pktSize = isVideo ? 1128 : 188;
|
|
534
|
-
this.payloadType = mediaRequest.pt;
|
|
535
|
-
this.ssrc = isVideo ? sessionInfo.videoSSRC : sessionInfo.audioSSRC;
|
|
536
|
-
this.srtpParams = (isVideo ? sessionInfo.videoSRTP : sessionInfo.audioSRTP).toString('base64');
|
|
537
|
-
this.srtpSuite = 'AES_CM_128_HMAC_SHA1_80';
|
|
538
|
-
this.format = 'rtp';
|
|
539
|
-
this.output = `srtp://${sessionInfo.address}:${port}?rtcpport=${port}&pkt_size=${pktSize}`;
|
|
540
|
-
}
|
|
541
|
-
setOutput(output) {
|
|
542
|
-
this.output = output;
|
|
543
|
-
}
|
|
544
|
-
setupForRecording(videoConfig, configuration) {
|
|
545
|
-
this.movflags = 'frag_keyframe+empty_moov+default_base_moof+omit_tfhd_offset';
|
|
546
|
-
this.maxMuxingQueueSize = 1024;
|
|
547
|
-
if (isNonEmpty(videoConfig.videoProcessor)) {
|
|
548
|
-
this.processor = videoConfig.videoProcessor;
|
|
549
|
-
}
|
|
550
|
-
if (this.isVideo) {
|
|
551
|
-
if (isNonEmpty(videoConfig.vcodec)) {
|
|
552
|
-
this.codec = videoConfig.vcodec;
|
|
553
|
-
}
|
|
554
|
-
else {
|
|
555
|
-
const hw = probeHardwareEncoder('generic');
|
|
556
|
-
this.codec = hw?.encoder ?? 'libx264';
|
|
557
|
-
}
|
|
558
|
-
const profile = configuration.videoCodec.parameters.profile === 2 /* H264Profile.HIGH */
|
|
559
|
-
? 'high'
|
|
560
|
-
: configuration.videoCodec.parameters.profile === 1 /* H264Profile.MAIN */
|
|
561
|
-
? 'main'
|
|
562
|
-
: 'baseline';
|
|
563
|
-
const level = configuration.videoCodec.parameters.level === 2 /* H264Level.LEVEL4_0 */
|
|
564
|
-
? '4.0'
|
|
565
|
-
: configuration.videoCodec.parameters.level === 1 /* H264Level.LEVEL3_2 */
|
|
566
|
-
? '3.2'
|
|
567
|
-
: '3.1';
|
|
568
|
-
if (this.codec === 'libx264') {
|
|
569
|
-
this.pixFormat = 'yuv420p';
|
|
570
|
-
this.codecOptions = `-preset ultrafast -tune zerolatency -profile:v ${profile} -level:v ${level}`;
|
|
571
|
-
}
|
|
572
|
-
else if (this.codec !== 'copy') {
|
|
573
|
-
this.pixFormat = 'yuv420p';
|
|
574
|
-
const hw = probeHardwareEncoder('generic');
|
|
575
|
-
const hwOpts = hw && this.codec === hw.encoder ? hw.customOptions : '';
|
|
576
|
-
this.codecOptions = `${hwOpts} -profile:v ${profile} -level:v ${level}`.trim();
|
|
577
|
-
}
|
|
578
|
-
if (this.codec !== 'copy') {
|
|
579
|
-
this.bitrate = videoConfig.maxBitrate ?? configuration.videoCodec.parameters.bitRate;
|
|
580
|
-
this.width = configuration.videoCodec.resolution[0];
|
|
581
|
-
this.height = configuration.videoCodec.resolution[1];
|
|
582
|
-
this.fps = videoConfig.maxFPS ?? configuration.videoCodec.resolution[2];
|
|
583
|
-
this.crop = videoConfig.crop !== false; // only false if 'crop: false' was specifically set
|
|
584
|
-
}
|
|
585
|
-
this.iFrameInterval = configuration.videoCodec.parameters.iFrameInterval;
|
|
586
|
-
}
|
|
587
|
-
if (this.isAudio) {
|
|
588
|
-
if (isNonEmpty(videoConfig.acodec)) {
|
|
589
|
-
this.codec = videoConfig.acodec;
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
const aac = getAacRecordingCodecAndOptions(configuration.audioCodec.type);
|
|
593
|
-
this.codec = aac.codec;
|
|
594
|
-
this.codecOptions = aac.codecOptions + ' -flags +global_header';
|
|
595
|
-
}
|
|
596
|
-
if (isNonEmpty(videoConfig.acodec) && (this.codec === 'libfdk_aac' || this.codec === 'aac')) {
|
|
597
|
-
this.codecOptions =
|
|
598
|
-
configuration.audioCodec.type === 0 /* AudioRecordingCodecType.AAC_LC */
|
|
599
|
-
? '-profile:a aac_low'
|
|
600
|
-
: '-profile:a aac_eld';
|
|
601
|
-
this.codecOptions += ' -flags +global_header';
|
|
602
|
-
}
|
|
603
|
-
if (this.codec !== 'copy') {
|
|
604
|
-
const samplerate = SAMPLERATE_MAP.get(configuration.audioCodec.samplerate);
|
|
605
|
-
if (!samplerate) {
|
|
606
|
-
throw new Error(`Unsupported audio samplerate: ${configuration.audioCodec.samplerate}`);
|
|
607
|
-
}
|
|
608
|
-
this.sampleRate = samplerate;
|
|
609
|
-
this.bitrate = configuration.audioCodec.bitrate;
|
|
610
|
-
this.channels = configuration.audioCodec.audioChannels;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
/**
|
|
615
|
-
* Configure this FFmpegParameters instance to receive return audio from
|
|
616
|
-
* HomeKit via SRTP, decode it, and re-encode to AAC ADTS for the eufy
|
|
617
|
-
* P2P talkback channel.
|
|
618
|
-
*
|
|
619
|
-
* The SDP is generated dynamically based on the codec and sample rate
|
|
620
|
-
* that HomeKit negotiated during the stream setup.
|
|
621
|
-
*/
|
|
622
|
-
async setTalkbackInput(sessionInfo, request) {
|
|
623
|
-
this.useWallclockAsTimestamp = false;
|
|
624
|
-
this.protocolWhitelist = 'pipe,udp,rtp,file,crypto,tcp';
|
|
625
|
-
this.inputFormat = 'sdp';
|
|
626
|
-
// Output: always AAC ADTS — this is what the eufy P2P layer expects
|
|
627
|
-
const outputCodec = hasFdkAac() ? 'libfdk_aac' : 'aac';
|
|
628
|
-
this.codec = outputCodec;
|
|
629
|
-
this.sampleRate = 16;
|
|
630
|
-
this.channels = 1;
|
|
631
|
-
this.bitrate = 20;
|
|
632
|
-
this.format = 'adts';
|
|
633
|
-
// Determine input codec from HomeKit's negotiated parameters
|
|
634
|
-
const audioCodec = request?.audio.codec ?? "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */;
|
|
635
|
-
const sampleRateKHz = request?.audio.sample_rate ?? 16;
|
|
636
|
-
const channels = request?.audio.channel ?? 1;
|
|
637
|
-
const payloadType = request?.audio.pt ?? 110;
|
|
638
|
-
const maxBitrate = request?.audio.max_bit_rate ?? 24;
|
|
639
|
-
const isOpus = audioCodec === "OPUS" /* AudioStreamingCodecType.OPUS */;
|
|
640
|
-
// Set the input decoder to match what HomeKit is sending
|
|
641
|
-
if (isOpus) {
|
|
642
|
-
this.inputCodec = 'libopus';
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
this.inputCodec = hasFdkAac() ? 'libfdk_aac' : 'aac';
|
|
646
|
-
}
|
|
647
|
-
const ipVer = sessionInfo.ipv6 ? 'IP6' : 'IP4';
|
|
648
|
-
const srtpSuite = sessionInfo.audioCryptoSuite === 1 /* SRTPCryptoSuites.AES_CM_256_HMAC_SHA1_80 */
|
|
649
|
-
? 'AES_CM_256_HMAC_SHA1_80'
|
|
650
|
-
: 'AES_CM_128_HMAC_SHA1_80';
|
|
651
|
-
const sdpInput = isOpus
|
|
652
|
-
? buildOpusSdp(ipVer, sessionInfo, payloadType, sampleRateKHz, channels, maxBitrate, srtpSuite)
|
|
653
|
-
: buildAacEldSdp(ipVer, sessionInfo, payloadType, sampleRateKHz, channels, maxBitrate, srtpSuite);
|
|
654
|
-
const { port } = await createOneShotTcpServer((socket) => {
|
|
655
|
-
socket.end(sdpInput);
|
|
656
|
-
});
|
|
657
|
-
this.setInputSource(`tcp://127.0.0.1:${port}`);
|
|
658
|
-
}
|
|
659
|
-
setTalkbackChannels(channels) {
|
|
660
|
-
this.channels = channels;
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* Enable parallel file recording. Generates a timestamped mp4
|
|
664
|
-
* filename inside `recordingDir`, creates the directory if needed,
|
|
665
|
-
* and configures this parameter set so the ffmpeg command will
|
|
666
|
-
* include an additional output that writes a copy of the encoded
|
|
667
|
-
* video to disk (mp4 container, fragmented).
|
|
668
|
-
*
|
|
669
|
-
* @returns The absolute path to the recording file.
|
|
670
|
-
*/
|
|
671
|
-
setFileRecording(recordingDir, serial, sessionId) {
|
|
672
|
-
const sanitizedSerial = serial.replace(/[^A-Za-z0-9_-]/g, '');
|
|
673
|
-
const ts = sessionId ?? new Date().toISOString().replace(/[:.]/g, '-');
|
|
674
|
-
mkdirSync(recordingDir, { recursive: true });
|
|
675
|
-
this.fileRecordingPath = path.join(recordingDir, `${sanitizedSerial}_${ts}_processed.mp4`);
|
|
676
|
-
return this.fileRecordingPath;
|
|
677
|
-
}
|
|
678
|
-
buildGenericParameters() {
|
|
679
|
-
const params = [];
|
|
680
|
-
if (this.hideBanner)
|
|
681
|
-
params.push('-hide_banner');
|
|
682
|
-
params.push('-loglevel level+verbose');
|
|
683
|
-
if (this.useWallclockAsTimestamp)
|
|
684
|
-
params.push('-use_wallclock_as_timestamps 1');
|
|
685
|
-
return params;
|
|
686
|
-
}
|
|
687
|
-
buildInputParameters() {
|
|
688
|
-
const params = [];
|
|
689
|
-
if (this.analyzeDuration)
|
|
690
|
-
params.push(`-analyzeduration ${this.analyzeDuration}`);
|
|
691
|
-
if (this.probeSize)
|
|
692
|
-
params.push(`-probesize ${this.probeSize}`);
|
|
693
|
-
if (this.stimeout)
|
|
694
|
-
params.push(`-stimeout ${this.stimeout * 10000000}`);
|
|
695
|
-
if (this.readrate)
|
|
696
|
-
params.push('-re');
|
|
697
|
-
if (this.protocolWhitelist)
|
|
698
|
-
params.push(`-protocol_whitelist ${this.protocolWhitelist}`);
|
|
699
|
-
if (this.inputFormat)
|
|
700
|
-
params.push(`-f ${this.inputFormat}`);
|
|
701
|
-
if (this.inputCodec)
|
|
702
|
-
params.push(`-c:a ${this.inputCodec}`);
|
|
703
|
-
params.push(this.inputSource);
|
|
704
|
-
if (this.isVideo)
|
|
705
|
-
params.push('-an -sn -dn');
|
|
706
|
-
if (this.isAudio)
|
|
707
|
-
params.push('-vn -sn -dn');
|
|
708
|
-
return params;
|
|
709
|
-
}
|
|
710
|
-
/** Clamp a requested dimension to an optional max from the video config. */
|
|
711
|
-
static clampDimension(requested, max) {
|
|
712
|
-
return max && max < requested ? max : requested;
|
|
713
|
-
}
|
|
714
|
-
/** Apply common visual settings (dimensions, filters, crop) from the video config. */
|
|
715
|
-
applyVisualConfig(width, height, videoConfig) {
|
|
716
|
-
this.width = FFmpegParameters.clampDimension(width, videoConfig.maxWidth);
|
|
717
|
-
this.height = FFmpegParameters.clampDimension(height, videoConfig.maxHeight);
|
|
718
|
-
if (isNonEmpty(videoConfig.videoFilter)) {
|
|
719
|
-
this.filters = videoConfig.videoFilter;
|
|
720
|
-
}
|
|
721
|
-
if (videoConfig.crop) {
|
|
722
|
-
this.crop = videoConfig.crop;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* Builds scale/crop video filter arguments based on current width, height, crop settings,
|
|
727
|
-
* and any user-specified filters. Shared between video and snapshot encoding.
|
|
728
|
-
*/
|
|
729
|
-
buildVideoFilterParams() {
|
|
730
|
-
const filters = this.filters ? this.filters.split(',') : [];
|
|
731
|
-
const noneFilter = filters.indexOf('none');
|
|
732
|
-
if (noneFilter >= 0) {
|
|
733
|
-
filters.splice(noneFilter, 1);
|
|
734
|
-
}
|
|
735
|
-
if (noneFilter < 0 && this.width && this.height) {
|
|
736
|
-
if (this.crop) {
|
|
737
|
-
filters.push(`scale=${this.width}:${this.height}:force_original_aspect_ratio=increase`);
|
|
738
|
-
filters.push(`crop=${this.width}:${this.height}`);
|
|
739
|
-
filters.push(`scale='trunc(${this.width}/2)*2:trunc(${this.height}/2)*2'`);
|
|
740
|
-
}
|
|
741
|
-
else {
|
|
742
|
-
filters.push(`scale='min(${this.width},iw)':'min(${this.height},ih)':force_original_aspect_ratio=decrease`);
|
|
743
|
-
filters.push("scale='trunc(iw/2)*2:trunc(ih/2)*2'");
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
if (filters.length > 0) {
|
|
747
|
-
return ['-filter:v ' + filters.join(',')];
|
|
748
|
-
}
|
|
749
|
-
return [];
|
|
750
|
-
}
|
|
751
|
-
buildEncodingParameters() {
|
|
752
|
-
const params = [];
|
|
753
|
-
if (this.isVideo) {
|
|
754
|
-
if (this.fps)
|
|
755
|
-
params.push(`-r ${this.fps}`);
|
|
756
|
-
params.push(`-vcodec ${this.codec}`);
|
|
757
|
-
if (this.pixFormat)
|
|
758
|
-
params.push(`-pix_fmt ${this.pixFormat}`);
|
|
759
|
-
if (this.colorRange)
|
|
760
|
-
params.push(`-color_range ${this.colorRange}`);
|
|
761
|
-
if (this.codecOptions)
|
|
762
|
-
params.push(this.codecOptions);
|
|
763
|
-
if (this.codec === 'libx264')
|
|
764
|
-
params.push('-sc_threshold 0');
|
|
765
|
-
params.push(...this.buildVideoFilterParams());
|
|
766
|
-
if (this.bitrate)
|
|
767
|
-
params.push(`-b:v ${this.bitrate}k`);
|
|
768
|
-
if (this.bufsize)
|
|
769
|
-
params.push(`-bufsize ${this.bufsize}k`);
|
|
770
|
-
if (this.maxrate)
|
|
771
|
-
params.push(`-maxrate ${this.maxrate}k`);
|
|
772
|
-
}
|
|
773
|
-
if (this.isAudio && this.processAudio) {
|
|
774
|
-
params.push(`-acodec ${this.codec}`);
|
|
775
|
-
if (this.codecOptions)
|
|
776
|
-
params.push(this.codecOptions);
|
|
777
|
-
if (this.bitrate)
|
|
778
|
-
params.push(`-b:a ${this.bitrate}k`);
|
|
779
|
-
if (this.sampleRate)
|
|
780
|
-
params.push(`-ar ${this.sampleRate}k`);
|
|
781
|
-
if (this.channels)
|
|
782
|
-
params.push(`-ac ${this.channels}`);
|
|
783
|
-
}
|
|
784
|
-
if (this.isSnapshot) {
|
|
785
|
-
if (this.numberFrames)
|
|
786
|
-
params.push(`-frames:v ${this.numberFrames}`);
|
|
787
|
-
if (this.delaySnapshot)
|
|
788
|
-
params.push('-ss 00:00:00.500');
|
|
789
|
-
params.push(...this.buildVideoFilterParams());
|
|
790
|
-
}
|
|
791
|
-
return params;
|
|
792
|
-
}
|
|
793
|
-
buildOutputParameters() {
|
|
794
|
-
const params = [];
|
|
795
|
-
if (this.payloadType)
|
|
796
|
-
params.push(`-payload_type ${this.payloadType}`);
|
|
797
|
-
if (this.ssrc)
|
|
798
|
-
params.push(`-ssrc ${this.ssrc}`);
|
|
799
|
-
if (this.format)
|
|
800
|
-
params.push(`-f ${this.format}`);
|
|
801
|
-
if (this.srtpSuite)
|
|
802
|
-
params.push(`-srtp_out_suite ${this.srtpSuite}`);
|
|
803
|
-
if (this.srtpParams)
|
|
804
|
-
params.push(`-srtp_out_params ${this.srtpParams}`);
|
|
805
|
-
params.push(this.output);
|
|
806
|
-
return params;
|
|
807
|
-
}
|
|
808
|
-
buildParameters() {
|
|
809
|
-
const params = [
|
|
810
|
-
...this.buildGenericParameters(),
|
|
811
|
-
...this.buildInputParameters(),
|
|
812
|
-
...this.buildEncodingParameters(),
|
|
813
|
-
...this.buildOutputParameters(),
|
|
814
|
-
`-progress tcp://127.0.0.1:${this.progressPort}`,
|
|
815
|
-
];
|
|
816
|
-
return params;
|
|
817
|
-
}
|
|
818
|
-
static getRecordingArguments(parameters) {
|
|
819
|
-
if (parameters.length === 0) {
|
|
820
|
-
return [];
|
|
821
|
-
}
|
|
822
|
-
const params = [...parameters[0].buildGenericParameters()];
|
|
823
|
-
// input
|
|
824
|
-
params.push(parameters[0].inputSource);
|
|
825
|
-
if (parameters.length > 1 && parameters[0].inputSource !== parameters[1].inputSource) {
|
|
826
|
-
if (parameters[1].processAudio) {
|
|
827
|
-
// Include audio input format/codec hints so FFmpeg can detect the
|
|
828
|
-
// raw AAC ADTS stream without slow probing. Cannot use full
|
|
829
|
-
// buildInputParameters() here because its -vn flag would suppress
|
|
830
|
-
// video in this combined (single-process) recording command.
|
|
831
|
-
if (parameters[1].inputFormat)
|
|
832
|
-
params.push(`-f ${parameters[1].inputFormat}`);
|
|
833
|
-
if (parameters[1].inputCodec)
|
|
834
|
-
params.push(`-c:a ${parameters[1].inputCodec}`);
|
|
835
|
-
params.push(parameters[1].inputSource);
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
params.push('-f lavfi -i anullsrc -shortest');
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
if (parameters.length === 1) {
|
|
842
|
-
params.push('-an');
|
|
843
|
-
}
|
|
844
|
-
params.push('-sn -dn');
|
|
845
|
-
// video encoding
|
|
846
|
-
params.push(...parameters[0].buildEncodingParameters());
|
|
847
|
-
if (parameters[0].iFrameInterval) {
|
|
848
|
-
params.push(`-force_key_frames expr:gte(t,n_forced*${parameters[0].iFrameInterval / 1000})`);
|
|
849
|
-
params.push('-sc_threshold 0');
|
|
850
|
-
}
|
|
851
|
-
// audio encoding
|
|
852
|
-
if (parameters.length > 1) {
|
|
853
|
-
if (parameters[1].processAudio) {
|
|
854
|
-
params.push('-bsf:a aac_adtstoasc');
|
|
855
|
-
}
|
|
856
|
-
params.push(...parameters[1].buildEncodingParameters());
|
|
857
|
-
}
|
|
858
|
-
// fragmented mp4 options
|
|
859
|
-
if (parameters[0].movflags)
|
|
860
|
-
params.push(`-movflags ${parameters[0].movflags}`);
|
|
861
|
-
if (parameters[0].maxMuxingQueueSize)
|
|
862
|
-
params.push(`-max_muxing_queue_size ${parameters[0].maxMuxingQueueSize}`);
|
|
863
|
-
// output
|
|
864
|
-
params.push('-f mp4');
|
|
865
|
-
params.push(parameters[0].output);
|
|
866
|
-
params.push(`-progress tcp://127.0.0.1:${parameters[0].progressPort}`);
|
|
867
|
-
return params;
|
|
868
|
-
}
|
|
869
|
-
static getCombinedArguments(parameters) {
|
|
870
|
-
if (parameters.length === 0) {
|
|
871
|
-
return [];
|
|
872
|
-
}
|
|
873
|
-
const params = [...parameters[0].buildGenericParameters()];
|
|
874
|
-
for (const p of parameters) {
|
|
875
|
-
params.push(...p.buildInputParameters());
|
|
876
|
-
params.push(...p.buildEncodingParameters());
|
|
877
|
-
params.push(...p.buildOutputParameters());
|
|
878
|
-
}
|
|
879
|
-
// Append a parallel file-recording output when configured.
|
|
880
|
-
// Video-only copy — for P2P streams video and audio use separate
|
|
881
|
-
// FFmpeg processes, so only the video track is available here.
|
|
882
|
-
const recPath = parameters[0].fileRecordingPath;
|
|
883
|
-
if (recPath) {
|
|
884
|
-
params.push('-map 0:v -c:v copy -f mp4 -movflags frag_keyframe+empty_moov');
|
|
885
|
-
params.push(recPath);
|
|
886
|
-
}
|
|
887
|
-
params.push(`-progress tcp://127.0.0.1:${parameters[0].progressPort}`);
|
|
888
|
-
return params;
|
|
889
|
-
}
|
|
890
|
-
getStreamStartText() {
|
|
891
|
-
if (this.isVideo) {
|
|
892
|
-
const detail = this.codec === 'copy' ? 'native' : `${this.width}x${this.height}, ${this.fps} fps, ${this.bitrate} kbps`;
|
|
893
|
-
return `Starting video stream: ${detail}`;
|
|
894
|
-
}
|
|
895
|
-
if (this.isAudio) {
|
|
896
|
-
const detail = this.codec === 'copy' ? 'native' : `${this.sampleRate} kHz, ${this.bitrate} kbps, codec: ${this.codec}`;
|
|
897
|
-
return `Starting audio stream: ${detail}`;
|
|
898
|
-
}
|
|
899
|
-
return 'Starting unknown stream';
|
|
900
|
-
}
|
|
901
|
-
}
|
|
902
|
-
export class FFmpeg extends EventEmitter {
|
|
903
|
-
process;
|
|
904
|
-
name;
|
|
905
|
-
progress;
|
|
906
|
-
parameters;
|
|
907
|
-
ffmpegExec = ffmpegPath || 'ffmpeg';
|
|
908
|
-
stdin;
|
|
909
|
-
stdout;
|
|
910
|
-
starttime;
|
|
911
|
-
killTimeout;
|
|
912
|
-
log;
|
|
913
|
-
constructor(name, parameters, logger) {
|
|
914
|
-
super();
|
|
915
|
-
this.name = name;
|
|
916
|
-
this.log = logger ?? ffmpegLogger;
|
|
917
|
-
if (Array.isArray(parameters)) {
|
|
918
|
-
if (parameters.length === 0) {
|
|
919
|
-
throw new Error('No ffmpeg parameters found.');
|
|
920
|
-
}
|
|
921
|
-
this.parameters = parameters;
|
|
922
|
-
}
|
|
923
|
-
else {
|
|
924
|
-
this.parameters = [parameters];
|
|
925
|
-
}
|
|
926
|
-
if (this.parameters[0].processor) {
|
|
927
|
-
this.ffmpegExec = this.parameters[0].processor;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
/**
|
|
931
|
-
* Shared initialisation: timestamps the start, creates the progress monitor,
|
|
932
|
-
* spawns the ffmpeg process, and wires up stderr logging.
|
|
933
|
-
*/
|
|
934
|
-
async spawnProcess(processArgs, label, opts) {
|
|
935
|
-
this.starttime = Date.now();
|
|
936
|
-
this.progress = await FFmpegProgress.create(this.parameters[0].progressPort);
|
|
937
|
-
this.progress.on('progress started', this.onProgressStarted.bind(this));
|
|
938
|
-
this.log.debug(this.name, `${label}: ${this.ffmpegExec} ${processArgs.join(' ')}`);
|
|
939
|
-
this.parameters.forEach((p) => this.log.info(this.name, p.getStreamStartText()));
|
|
940
|
-
const child = spawn(this.ffmpegExec, processArgs.join(' ').split(/\s+/), { env: process.env });
|
|
941
|
-
child.stderr.on('data', this.handleStderrData.bind(this));
|
|
942
|
-
if (opts?.attachLifecycle) {
|
|
943
|
-
child.on('error', this.onProcessError.bind(this));
|
|
944
|
-
child.on('exit', this.onProcessExit.bind(this));
|
|
945
|
-
}
|
|
946
|
-
this.process = child;
|
|
947
|
-
this.stdin = child.stdin;
|
|
948
|
-
this.stdout = child.stdout;
|
|
949
|
-
return child;
|
|
950
|
-
}
|
|
951
|
-
async start() {
|
|
952
|
-
const processArgs = FFmpegParameters.getCombinedArguments(this.parameters);
|
|
953
|
-
await this.spawnProcess(processArgs, 'Stream command', { attachLifecycle: true });
|
|
954
|
-
}
|
|
955
|
-
async getResult(input) {
|
|
956
|
-
const processArgs = FFmpegParameters.getCombinedArguments(this.parameters);
|
|
957
|
-
const child = await this.spawnProcess(processArgs, 'Process command');
|
|
958
|
-
return new Promise((resolve, reject) => {
|
|
959
|
-
const killTimeout = setTimeout(() => {
|
|
960
|
-
this.stop();
|
|
961
|
-
reject('ffmpeg process timed out.');
|
|
962
|
-
}, PROCESS_RESULT_TIMEOUT_MS);
|
|
963
|
-
child.on('error', (error) => {
|
|
964
|
-
reject(error);
|
|
965
|
-
this.onProcessError(error);
|
|
966
|
-
});
|
|
967
|
-
let resultBuffer = Buffer.alloc(0);
|
|
968
|
-
child.stdout.on('data', (data) => {
|
|
969
|
-
resultBuffer = Buffer.concat([resultBuffer, data]);
|
|
970
|
-
});
|
|
971
|
-
child.on('exit', () => {
|
|
972
|
-
if (killTimeout) {
|
|
973
|
-
clearTimeout(killTimeout);
|
|
974
|
-
}
|
|
975
|
-
if (resultBuffer.length > 0) {
|
|
976
|
-
resolve(resultBuffer);
|
|
977
|
-
}
|
|
978
|
-
else {
|
|
979
|
-
reject('Failed to fetch data.');
|
|
980
|
-
}
|
|
981
|
-
});
|
|
982
|
-
if (input) {
|
|
983
|
-
child.stdin.end(input);
|
|
984
|
-
}
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
async startFragmentedMP4Session() {
|
|
988
|
-
const port = await pickPort({ type: 'tcp' });
|
|
989
|
-
return new Promise((resolve) => {
|
|
990
|
-
const server = net.createServer((socket) => {
|
|
991
|
-
server.close();
|
|
992
|
-
resolve({
|
|
993
|
-
socket: socket,
|
|
994
|
-
process: this.process,
|
|
995
|
-
ffmpeg: this,
|
|
996
|
-
generator: this.parseFragmentedMP4(socket),
|
|
997
|
-
});
|
|
998
|
-
});
|
|
999
|
-
server.listen(port, async () => {
|
|
1000
|
-
this.parameters[0].setOutput(`tcp://127.0.0.1:${port}`);
|
|
1001
|
-
const processArgs = FFmpegParameters.getRecordingArguments(this.parameters);
|
|
1002
|
-
await this.spawnProcess(processArgs, 'Stream command', { attachLifecycle: true });
|
|
1003
|
-
});
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
async *parseFragmentedMP4(socket) {
|
|
1007
|
-
while (true) {
|
|
1008
|
-
const header = await this.readLength(socket, 8);
|
|
1009
|
-
const length = header.readInt32BE(0) - 8;
|
|
1010
|
-
const type = header.slice(4).toString();
|
|
1011
|
-
if (length > MAX_FRAG_BOX_SIZE) {
|
|
1012
|
-
throw new Error(`fMP4 box '${type}' reports size ${length} bytes, exceeding limit of ${MAX_FRAG_BOX_SIZE}. Stream likely corrupted.`);
|
|
1013
|
-
}
|
|
1014
|
-
const data = await this.readLength(socket, length);
|
|
1015
|
-
yield {
|
|
1016
|
-
header,
|
|
1017
|
-
length,
|
|
1018
|
-
type,
|
|
1019
|
-
data,
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
async readLength(socket, length) {
|
|
1024
|
-
if (length <= 0) {
|
|
1025
|
-
return Buffer.alloc(0);
|
|
1026
|
-
}
|
|
1027
|
-
const value = socket.read(length);
|
|
1028
|
-
if (value) {
|
|
1029
|
-
return value;
|
|
1030
|
-
}
|
|
1031
|
-
return new Promise((resolve, reject) => {
|
|
1032
|
-
const readHandler = () => {
|
|
1033
|
-
const value = socket.read(length);
|
|
1034
|
-
if (value) {
|
|
1035
|
-
cleanup();
|
|
1036
|
-
resolve(value);
|
|
1037
|
-
}
|
|
1038
|
-
};
|
|
1039
|
-
const endHandler = () => {
|
|
1040
|
-
cleanup();
|
|
1041
|
-
reject(new Error(`FFMPEG socket closed during read for ${length} bytes!`));
|
|
1042
|
-
};
|
|
1043
|
-
const cleanup = () => {
|
|
1044
|
-
socket.removeListener('readable', readHandler);
|
|
1045
|
-
socket.removeListener('close', endHandler);
|
|
1046
|
-
};
|
|
1047
|
-
if (!socket) {
|
|
1048
|
-
throw new Error('FFMPEG socket is closed now!');
|
|
1049
|
-
}
|
|
1050
|
-
socket.on('readable', readHandler);
|
|
1051
|
-
socket.on('close', endHandler);
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
stop() {
|
|
1055
|
-
const usesStdIn = this.parameters.some((p) => p.usesStdInAsInput());
|
|
1056
|
-
if (usesStdIn) {
|
|
1057
|
-
this.process?.stdin.destroy();
|
|
1058
|
-
this.process?.kill('SIGTERM');
|
|
1059
|
-
}
|
|
1060
|
-
else {
|
|
1061
|
-
this.process?.stdin.write('q' + os.EOL);
|
|
1062
|
-
}
|
|
1063
|
-
this.killTimeout = setTimeout(() => {
|
|
1064
|
-
this.process?.kill('SIGKILL');
|
|
1065
|
-
}, KILL_GRACE_PERIOD_MS);
|
|
1066
|
-
}
|
|
1067
|
-
onProgressStarted() {
|
|
1068
|
-
this.emit('started');
|
|
1069
|
-
const runtime = this.starttime ? (Date.now() - this.starttime) / 1000 : undefined;
|
|
1070
|
-
this.log.debug(this.name, `process started. Getting the first response took ${runtime} seconds.`);
|
|
1071
|
-
}
|
|
1072
|
-
handleStderrData(chunk) {
|
|
1073
|
-
const output = chunk.toString();
|
|
1074
|
-
const isError = output.includes('[panic]') || output.includes('[error]') || output.includes('[fatal]');
|
|
1075
|
-
if (isError) {
|
|
1076
|
-
this.log.error(this.name, 'ffmpeg log message:\n' + output);
|
|
1077
|
-
}
|
|
1078
|
-
else if (this.parameters[0].debug) {
|
|
1079
|
-
this.log.debug(this.name, 'ffmpeg log message:\n' + output);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
onProcessError(error) {
|
|
1083
|
-
this.emit('error', error);
|
|
1084
|
-
}
|
|
1085
|
-
onProcessExit(code, signal) {
|
|
1086
|
-
this.emit('exit');
|
|
1087
|
-
const wasStopRequested = !!this.killTimeout;
|
|
1088
|
-
if (this.killTimeout) {
|
|
1089
|
-
clearTimeout(this.killTimeout);
|
|
1090
|
-
}
|
|
1091
|
-
const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal;
|
|
1092
|
-
if (wasStopRequested) {
|
|
1093
|
-
// stop() was called — any exit code is expected (non-zero typically
|
|
1094
|
-
// means broken pipe from socket teardown during HKSV recording close).
|
|
1095
|
-
this.log.debug(this.name, message + ' (Expected)');
|
|
1096
|
-
}
|
|
1097
|
-
else if (code === null || code === 255) {
|
|
1098
|
-
if (this.process?.killed) {
|
|
1099
|
-
this.log.info(this.name, message + ' (Forced)');
|
|
1100
|
-
}
|
|
1101
|
-
else {
|
|
1102
|
-
this.log.error(this.name, message + ' (Unexpected)');
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
else {
|
|
1106
|
-
this.emit('error', message + ' (Error)');
|
|
1107
|
-
// this.log.error(this.name, message + ' (Error)');
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
//# sourceMappingURL=ffmpeg.js.map
|