@homebridge-plugins/homebridge-eufy-security 4.7.2 → 5.0.0-beta.0

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.
Files changed (471) hide show
  1. package/AUTHORS +4 -0
  2. package/CODE_OF_CONDUCT.md +30 -0
  3. package/CONTRIBUTING.md +77 -0
  4. package/README.md +152 -49
  5. package/SECURITY.md +30 -0
  6. package/config.schema.json +92 -3
  7. package/dist/account/ownership.js +290 -0
  8. package/dist/account/ownership.js.map +1 -0
  9. package/dist/account/persistence.js +274 -0
  10. package/dist/account/persistence.js.map +1 -0
  11. package/dist/account/temporary-authentication.js +348 -0
  12. package/dist/account/temporary-authentication.js.map +1 -0
  13. package/dist/configuration.js +167 -0
  14. package/dist/configuration.js.map +1 -0
  15. package/dist/device/member-evidence.js +66 -0
  16. package/dist/device/member-evidence.js.map +1 -0
  17. package/dist/device/member-trust.js +114 -0
  18. package/dist/device/member-trust.js.map +1 -0
  19. package/dist/device/snapshot.js +190 -0
  20. package/dist/device/snapshot.js.map +1 -0
  21. package/dist/diagnostics.js +2218 -0
  22. package/dist/diagnostics.js.map +1 -0
  23. package/dist/homekit/adapter.js +2 -0
  24. package/dist/homekit/adapter.js.map +1 -0
  25. package/dist/homekit/adapters/battery.js +208 -0
  26. package/dist/homekit/adapters/battery.js.map +1 -0
  27. package/dist/homekit/adapters/camera-controls.js +527 -0
  28. package/dist/homekit/adapters/camera-controls.js.map +1 -0
  29. package/dist/homekit/adapters/camera-streaming.js +1515 -0
  30. package/dist/homekit/adapters/camera-streaming.js.map +1 -0
  31. package/dist/homekit/adapters/contact.js +138 -0
  32. package/dist/homekit/adapters/contact.js.map +1 -0
  33. package/dist/homekit/adapters/doorbell.js +57 -0
  34. package/dist/homekit/adapters/doorbell.js.map +1 -0
  35. package/dist/homekit/adapters/information.js +55 -0
  36. package/dist/homekit/adapters/information.js.map +1 -0
  37. package/dist/homekit/adapters/lock.js +489 -0
  38. package/dist/homekit/adapters/lock.js.map +1 -0
  39. package/dist/homekit/adapters/motion.js +100 -0
  40. package/dist/homekit/adapters/motion.js.map +1 -0
  41. package/dist/homekit/adapters/registry.js +26 -0
  42. package/dist/homekit/adapters/registry.js.map +1 -0
  43. package/dist/homekit/adapters/security-system.js +425 -0
  44. package/dist/homekit/adapters/security-system.js.map +1 -0
  45. package/dist/homekit/adapters/siren.js +136 -0
  46. package/dist/homekit/adapters/siren.js.map +1 -0
  47. package/dist/homekit/adapters/smart-light.js +662 -0
  48. package/dist/homekit/adapters/smart-light.js.map +1 -0
  49. package/dist/homekit/coverage-matrix.js +119 -0
  50. package/dist/homekit/coverage-matrix.js.map +1 -0
  51. package/dist/homekit/device-control.js +175 -0
  52. package/dist/homekit/device-control.js.map +1 -0
  53. package/dist/homekit/reconciler.js +319 -0
  54. package/dist/homekit/reconciler.js.map +1 -0
  55. package/dist/homekit/representation.js +23 -0
  56. package/dist/homekit/representation.js.map +1 -0
  57. package/dist/index.js +4 -9
  58. package/dist/index.js.map +1 -1
  59. package/dist/media/contracts.js +2 -0
  60. package/dist/media/contracts.js.map +1 -0
  61. package/dist/media/last-successful-image.js +208 -0
  62. package/dist/media/last-successful-image.js.map +1 -0
  63. package/dist/media/live-stream.js +1003 -0
  64. package/dist/media/live-stream.js.map +1 -0
  65. package/dist/media/recording.js +560 -0
  66. package/dist/media/recording.js.map +1 -0
  67. package/dist/media/session-budget.js +39 -0
  68. package/dist/media/session-budget.js.map +1 -0
  69. package/dist/media/snapshot.js +406 -0
  70. package/dist/media/snapshot.js.map +1 -0
  71. package/dist/media/station-live-sessions.js +101 -0
  72. package/dist/media/station-live-sessions.js.map +1 -0
  73. package/dist/platform.js +97 -809
  74. package/dist/platform.js.map +1 -1
  75. package/dist/runtime/channel.js +375 -0
  76. package/dist/runtime/channel.js.map +1 -0
  77. package/dist/runtime/owner.js +682 -0
  78. package/dist/runtime/owner.js.map +1 -0
  79. package/dist/runtime/sdk-client.js +211 -0
  80. package/dist/runtime/sdk-client.js.map +1 -0
  81. package/dist/runtime/tracker.js +211 -0
  82. package/dist/runtime/tracker.js.map +1 -0
  83. package/dist/settings.js +1 -43
  84. package/dist/settings.js.map +1 -1
  85. package/dist/storage.js +59 -0
  86. package/dist/storage.js.map +1 -0
  87. package/dist/ui/dashboard.js +181 -0
  88. package/dist/ui/dashboard.js.map +1 -0
  89. package/dist/ui/device-image.js +22 -0
  90. package/dist/ui/device-image.js.map +1 -0
  91. package/dist/ui/runtime-channel-client.js +305 -0
  92. package/dist/ui/runtime-channel-client.js.map +1 -0
  93. package/dist/ui/server.js +259 -0
  94. package/dist/ui/server.js.map +1 -0
  95. package/homebridge-ui/public/app.css +1902 -0
  96. package/homebridge-ui/public/assets/devices/clean/clean-T1250.webp +0 -0
  97. package/homebridge-ui/public/assets/devices/clean/clean-T2070.webp +0 -0
  98. package/homebridge-ui/public/assets/devices/clean/clean-T2080.webp +0 -0
  99. package/homebridge-ui/public/assets/devices/clean/clean-T2081.webp +0 -0
  100. package/homebridge-ui/public/assets/devices/clean/clean-T2103.webp +0 -0
  101. package/homebridge-ui/public/assets/devices/clean/clean-T2117.webp +0 -0
  102. package/homebridge-ui/public/assets/devices/clean/clean-T2118.webp +0 -0
  103. package/homebridge-ui/public/assets/devices/clean/clean-T2119.webp +0 -0
  104. package/homebridge-ui/public/assets/devices/clean/clean-T211A.webp +0 -0
  105. package/homebridge-ui/public/assets/devices/clean/clean-T2120.webp +0 -0
  106. package/homebridge-ui/public/assets/devices/clean/clean-T2123.webp +0 -0
  107. package/homebridge-ui/public/assets/devices/clean/clean-T2128.webp +0 -0
  108. package/homebridge-ui/public/assets/devices/clean/clean-T2130.webp +0 -0
  109. package/homebridge-ui/public/assets/devices/clean/clean-T2132.webp +0 -0
  110. package/homebridge-ui/public/assets/devices/clean/clean-T2150.webp +0 -0
  111. package/homebridge-ui/public/assets/devices/clean/clean-T2181.webp +0 -0
  112. package/homebridge-ui/public/assets/devices/clean/clean-T2182.webp +0 -0
  113. package/homebridge-ui/public/assets/devices/clean/clean-T2190.webp +0 -0
  114. package/homebridge-ui/public/assets/devices/clean/clean-T2192.webp +0 -0
  115. package/homebridge-ui/public/assets/devices/clean/clean-T2193.webp +0 -0
  116. package/homebridge-ui/public/assets/devices/clean/clean-T2194.webp +0 -0
  117. package/homebridge-ui/public/assets/devices/clean/clean-T2210.webp +0 -0
  118. package/homebridge-ui/public/assets/devices/clean/clean-T2250.webp +0 -0
  119. package/homebridge-ui/public/assets/devices/clean/clean-T2251.webp +0 -0
  120. package/homebridge-ui/public/assets/devices/clean/clean-T2252.webp +0 -0
  121. package/homebridge-ui/public/assets/devices/clean/clean-T2253.webp +0 -0
  122. package/homebridge-ui/public/assets/devices/clean/clean-T2254.webp +0 -0
  123. package/homebridge-ui/public/assets/devices/clean/clean-T2255.webp +0 -0
  124. package/homebridge-ui/public/assets/devices/clean/clean-T2256.webp +0 -0
  125. package/homebridge-ui/public/assets/devices/clean/clean-T2257.webp +0 -0
  126. package/homebridge-ui/public/assets/devices/clean/clean-T2258.webp +0 -0
  127. package/homebridge-ui/public/assets/devices/clean/clean-T2259.webp +0 -0
  128. package/homebridge-ui/public/assets/devices/clean/clean-T2261.webp +0 -0
  129. package/homebridge-ui/public/assets/devices/clean/clean-T2262.webp +0 -0
  130. package/homebridge-ui/public/assets/devices/clean/clean-T2266.webp +0 -0
  131. package/homebridge-ui/public/assets/devices/clean/clean-T2267.webp +0 -0
  132. package/homebridge-ui/public/assets/devices/clean/clean-T2268.webp +0 -0
  133. package/homebridge-ui/public/assets/devices/clean/clean-T2270.webp +0 -0
  134. package/homebridge-ui/public/assets/devices/clean/clean-T2272.webp +0 -0
  135. package/homebridge-ui/public/assets/devices/clean/clean-T2273.webp +0 -0
  136. package/homebridge-ui/public/assets/devices/clean/clean-T2276.webp +0 -0
  137. package/homebridge-ui/public/assets/devices/clean/clean-T2277.webp +0 -0
  138. package/homebridge-ui/public/assets/devices/clean/clean-T2278.webp +0 -0
  139. package/homebridge-ui/public/assets/devices/clean/clean-T2280.webp +0 -0
  140. package/homebridge-ui/public/assets/devices/clean/clean-T2292.webp +0 -0
  141. package/homebridge-ui/public/assets/devices/clean/clean-T2320.webp +0 -0
  142. package/homebridge-ui/public/assets/devices/clean/clean-T2351.webp +0 -0
  143. package/homebridge-ui/public/assets/devices/clean/clean-T2352.webp +0 -0
  144. package/homebridge-ui/public/assets/devices/clean/clean-T2353.webp +0 -0
  145. package/homebridge-ui/public/assets/devices/life/life-T8L00.webp +0 -0
  146. package/homebridge-ui/public/assets/devices/life/life-T8L01.webp +0 -0
  147. package/homebridge-ui/public/assets/devices/life/life-T8L02.webp +0 -0
  148. package/homebridge-ui/public/assets/devices/life/life-T8L04.webp +0 -0
  149. package/homebridge-ui/public/assets/devices/life/life-T8L10.webp +0 -0
  150. package/homebridge-ui/public/assets/devices/life/life-T8L20.webp +0 -0
  151. package/homebridge-ui/public/assets/devices/life/life-T8L30.webp +0 -0
  152. package/homebridge-ui/public/assets/devices/life/life-T8L40.webp +0 -0
  153. package/homebridge-ui/public/assets/devices/mower/mower-T2801.webp +0 -0
  154. package/homebridge-ui/public/assets/devices/mower/mower-T280B.webp +0 -0
  155. package/homebridge-ui/public/assets/devices/mower/mower-T2880.webp +0 -0
  156. package/homebridge-ui/public/assets/devices/security/security-T7400.webp +0 -0
  157. package/homebridge-ui/public/assets/devices/security/security-T7401.webp +0 -0
  158. package/homebridge-ui/public/assets/devices/security/security-T8001.webp +0 -0
  159. package/homebridge-ui/public/assets/devices/security/security-T8002.webp +0 -0
  160. package/homebridge-ui/public/assets/devices/security/security-T8010.webp +0 -0
  161. package/homebridge-ui/public/assets/devices/security/security-T8021.webp +0 -0
  162. package/homebridge-ui/public/assets/devices/security/security-T8023.webp +0 -0
  163. package/homebridge-ui/public/assets/devices/security/security-T8025.webp +0 -0
  164. package/homebridge-ui/public/assets/devices/security/security-T8030.webp +0 -0
  165. package/homebridge-ui/public/assets/devices/security/security-T8110.webp +0 -0
  166. package/homebridge-ui/public/assets/devices/security/security-T8111.webp +0 -0
  167. package/homebridge-ui/public/assets/devices/security/security-T8112.webp +0 -0
  168. package/homebridge-ui/public/assets/devices/security/security-T8113.webp +0 -0
  169. package/homebridge-ui/public/assets/devices/security/security-T8114.webp +0 -0
  170. package/homebridge-ui/public/assets/devices/security/security-T8122.webp +0 -0
  171. package/homebridge-ui/public/assets/devices/security/security-T8123.webp +0 -0
  172. package/homebridge-ui/public/assets/devices/security/security-T8124.webp +0 -0
  173. package/homebridge-ui/public/assets/devices/security/security-T8130.webp +0 -0
  174. package/homebridge-ui/public/assets/devices/security/security-T8131.webp +0 -0
  175. package/homebridge-ui/public/assets/devices/security/security-T8134.webp +0 -0
  176. package/homebridge-ui/public/assets/devices/security/security-T8140.webp +0 -0
  177. package/homebridge-ui/public/assets/devices/security/security-T8141.webp +0 -0
  178. package/homebridge-ui/public/assets/devices/security/security-T8150.webp +0 -0
  179. package/homebridge-ui/public/assets/devices/security/security-T8151.webp +0 -0
  180. package/homebridge-ui/public/assets/devices/security/security-T8152.webp +0 -0
  181. package/homebridge-ui/public/assets/devices/security/security-T8153.webp +0 -0
  182. package/homebridge-ui/public/assets/devices/security/security-T8160.webp +0 -0
  183. package/homebridge-ui/public/assets/devices/security/security-T8161.webp +0 -0
  184. package/homebridge-ui/public/assets/devices/security/security-T8162.webp +0 -0
  185. package/homebridge-ui/public/assets/devices/security/security-T8170.webp +0 -0
  186. package/homebridge-ui/public/assets/devices/security/security-T8171.webp +0 -0
  187. package/homebridge-ui/public/assets/devices/security/security-T8172.webp +0 -0
  188. package/homebridge-ui/public/assets/devices/security/security-T8173.webp +0 -0
  189. package/homebridge-ui/public/assets/devices/security/security-T8200.webp +0 -0
  190. package/homebridge-ui/public/assets/devices/security/security-T8200X.webp +0 -0
  191. package/homebridge-ui/public/assets/devices/security/security-T8201.webp +0 -0
  192. package/homebridge-ui/public/assets/devices/security/security-T8203.webp +0 -0
  193. package/homebridge-ui/public/assets/devices/security/security-T8210.webp +0 -0
  194. package/homebridge-ui/public/assets/devices/security/security-T8213.webp +0 -0
  195. package/homebridge-ui/public/assets/devices/security/security-T8214.webp +0 -0
  196. package/homebridge-ui/public/assets/devices/security/security-T8222.webp +0 -0
  197. package/homebridge-ui/public/assets/devices/security/security-T8400.webp +0 -0
  198. package/homebridge-ui/public/assets/devices/security/security-T8401.webp +0 -0
  199. package/homebridge-ui/public/assets/devices/security/security-T8410.webp +0 -0
  200. package/homebridge-ui/public/assets/devices/security/security-T8411.webp +0 -0
  201. package/homebridge-ui/public/assets/devices/security/security-T8414.webp +0 -0
  202. package/homebridge-ui/public/assets/devices/security/security-T8416.webp +0 -0
  203. package/homebridge-ui/public/assets/devices/security/security-T8417.webp +0 -0
  204. package/homebridge-ui/public/assets/devices/security/security-T8419.webp +0 -0
  205. package/homebridge-ui/public/assets/devices/security/security-T8419N.webp +0 -0
  206. package/homebridge-ui/public/assets/devices/security/security-T8420.webp +0 -0
  207. package/homebridge-ui/public/assets/devices/security/security-T8420X.webp +0 -0
  208. package/homebridge-ui/public/assets/devices/security/security-T8422.webp +0 -0
  209. package/homebridge-ui/public/assets/devices/security/security-T8423.webp +0 -0
  210. package/homebridge-ui/public/assets/devices/security/security-T8424.webp +0 -0
  211. package/homebridge-ui/public/assets/devices/security/security-T8425.webp +0 -0
  212. package/homebridge-ui/public/assets/devices/security/security-T8441.webp +0 -0
  213. package/homebridge-ui/public/assets/devices/security/security-T8442.webp +0 -0
  214. package/homebridge-ui/public/assets/devices/security/security-T8452.webp +0 -0
  215. package/homebridge-ui/public/assets/devices/security/security-T84A0.webp +0 -0
  216. package/homebridge-ui/public/assets/devices/security/security-T84A1.webp +0 -0
  217. package/homebridge-ui/public/assets/devices/security/security-T8500.webp +0 -0
  218. package/homebridge-ui/public/assets/devices/security/security-T8502.webp +0 -0
  219. package/homebridge-ui/public/assets/devices/security/security-T8503.webp +0 -0
  220. package/homebridge-ui/public/assets/devices/security/security-T8504.webp +0 -0
  221. package/homebridge-ui/public/assets/devices/security/security-T8506.webp +0 -0
  222. package/homebridge-ui/public/assets/devices/security/security-T8510.webp +0 -0
  223. package/homebridge-ui/public/assets/devices/security/security-T8510P.webp +0 -0
  224. package/homebridge-ui/public/assets/devices/security/security-T8520.webp +0 -0
  225. package/homebridge-ui/public/assets/devices/security/security-T8520P.webp +0 -0
  226. package/homebridge-ui/public/assets/devices/security/security-T8530.webp +0 -0
  227. package/homebridge-ui/public/assets/devices/security/security-T8531.webp +0 -0
  228. package/homebridge-ui/public/assets/devices/security/security-T85D0.webp +0 -0
  229. package/homebridge-ui/public/assets/devices/security/security-T85L0.webp +0 -0
  230. package/homebridge-ui/public/assets/devices/security/security-T85P0.webp +0 -0
  231. package/homebridge-ui/public/assets/devices/security/security-T85V0.webp +0 -0
  232. package/homebridge-ui/public/assets/devices/security/security-T8600.webp +0 -0
  233. package/homebridge-ui/public/assets/devices/security/security-T86P2.webp +0 -0
  234. package/homebridge-ui/public/assets/devices/security/security-T8790.webp +0 -0
  235. package/homebridge-ui/public/assets/devices/security/security-T87B0.webp +0 -0
  236. package/homebridge-ui/public/assets/devices/security/security-T87B2.webp +0 -0
  237. package/homebridge-ui/public/assets/devices/security/security-T8900.webp +0 -0
  238. package/homebridge-ui/public/assets/devices/security/security-T8910.webp +0 -0
  239. package/homebridge-ui/public/assets/devices/security/security-T8920.webp +0 -0
  240. package/homebridge-ui/public/assets/devices/security/security-T8960.webp +0 -0
  241. package/homebridge-ui/public/assets/devices/security/security-T8B0.webp +0 -0
  242. package/homebridge-ui/public/assets/devices/security/security-T8E00.webp +0 -0
  243. package/homebridge-ui/public/assets/devices/security/security-T8N00.webp +0 -0
  244. package/homebridge-ui/public/assets/devices/security/security-T8W11C.webp +0 -0
  245. package/homebridge-ui/public/assets/devices/security/security-T90F0.webp +0 -0
  246. package/homebridge-ui/public/assets/devices/security/security-T90M0.webp +0 -0
  247. package/homebridge-ui/public/assets/devices/security/security-T90R0.webp +0 -0
  248. package/homebridge-ui/public/assets/devices/security/security-T90S0.webp +0 -0
  249. package/homebridge-ui/public/assets/icons/inventory.svg +1 -1
  250. package/homebridge-ui/public/assets/icons/troubleshoot.svg +1 -0
  251. package/homebridge-ui/public/assets/icons/warning.svg +1 -1
  252. package/homebridge-ui/public/assets/logo-dark.svg +10 -0
  253. package/homebridge-ui/public/assets/logo.svg +10 -0
  254. package/homebridge-ui/public/i18n/en.json +218 -0
  255. package/homebridge-ui/public/i18n/fr.json +218 -0
  256. package/homebridge-ui/public/index.html +377 -28
  257. package/homebridge-ui/public/js/app.js +1065 -0
  258. package/homebridge-ui/public/js/dashboard.js +385 -0
  259. package/homebridge-ui/public/js/legacy-settings.js +37 -0
  260. package/homebridge-ui/public/js/profile-wizard.js +68 -0
  261. package/homebridge-ui/server.js +2 -1393
  262. package/i18n/runtime/en.json +100 -0
  263. package/media/Snapshot-Unavailable.jpg +0 -0
  264. package/media/Snapshot-black.jpg +0 -0
  265. package/media/camera-disabled.jpg +0 -0
  266. package/media/camera-offline.jpg +0 -0
  267. package/package.json +29 -24
  268. package/.claude/CLAUDE.md +0 -175
  269. package/.claude/PRD.md +0 -241
  270. package/.claude/docs/debug-recording-lessons.md +0 -91
  271. package/.claude/docs/hksv-recording-fix.md +0 -160
  272. package/.claude/skills/architect/SKILL.md +0 -76
  273. package/.claude/skills/developer/SKILL.md +0 -59
  274. package/.claude/skills/new-device-support/SKILL.md +0 -212
  275. package/.claude/skills/new-device-support/check-device.mjs +0 -363
  276. package/.claude/skills/new-device-support/map-properties.mjs +0 -450
  277. package/.claude/skills/new-device-support/verify-device.mjs +0 -272
  278. package/.claude/skills/planner/SKILL.md +0 -100
  279. package/.claude/skills/qa/SKILL.md +0 -79
  280. package/.claude/skills/support/SKILL.md +0 -175
  281. package/.prettierignore +0 -7
  282. package/.prettierrc.json +0 -7
  283. package/FUNDING.yml +0 -1
  284. package/dist/accessories/AutoSyncStationAccessory.js +0 -157
  285. package/dist/accessories/AutoSyncStationAccessory.js.map +0 -1
  286. package/dist/accessories/BaseAccessory.js +0 -261
  287. package/dist/accessories/BaseAccessory.js.map +0 -1
  288. package/dist/accessories/CameraAccessory.js +0 -459
  289. package/dist/accessories/CameraAccessory.js.map +0 -1
  290. package/dist/accessories/Device.js +0 -65
  291. package/dist/accessories/Device.js.map +0 -1
  292. package/dist/accessories/EntrySensorAccessory.js +0 -48
  293. package/dist/accessories/EntrySensorAccessory.js.map +0 -1
  294. package/dist/accessories/GarageDoorAccessory.js +0 -160
  295. package/dist/accessories/GarageDoorAccessory.js.map +0 -1
  296. package/dist/accessories/LockAccessory.js +0 -142
  297. package/dist/accessories/LockAccessory.js.map +0 -1
  298. package/dist/accessories/MotionSensorAccessory.js +0 -48
  299. package/dist/accessories/MotionSensorAccessory.js.map +0 -1
  300. package/dist/accessories/SmartDropAccessory.js +0 -141
  301. package/dist/accessories/SmartDropAccessory.js.map +0 -1
  302. package/dist/accessories/StationAccessory.js +0 -373
  303. package/dist/accessories/StationAccessory.js.map +0 -1
  304. package/dist/config.js +0 -26
  305. package/dist/config.js.map +0 -1
  306. package/dist/controller/DebugRecordingManager.js +0 -402
  307. package/dist/controller/DebugRecordingManager.js.map +0 -1
  308. package/dist/controller/LocalLivestreamManager.js +0 -371
  309. package/dist/controller/LocalLivestreamManager.js.map +0 -1
  310. package/dist/controller/recordingDelegate.js +0 -252
  311. package/dist/controller/recordingDelegate.js.map +0 -1
  312. package/dist/controller/snapshotDelegate.js +0 -348
  313. package/dist/controller/snapshotDelegate.js.map +0 -1
  314. package/dist/controller/streamingDelegate.js +0 -389
  315. package/dist/controller/streamingDelegate.js.map +0 -1
  316. package/dist/interfaces.js +0 -2
  317. package/dist/interfaces.js.map +0 -1
  318. package/dist/media/Snapshot-Unavailable.png +0 -0
  319. package/dist/media/Snapshot-Unavailable.xcf +0 -0
  320. package/dist/media/Snapshot-black.png +0 -0
  321. package/dist/media/camera-disabled.png +0 -0
  322. package/dist/media/camera-offline.png +0 -0
  323. package/dist/media/media/Snapshot-Unavailable.png +0 -0
  324. package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
  325. package/dist/media/media/Snapshot-black.png +0 -0
  326. package/dist/media/media/camera-disabled.png +0 -0
  327. package/dist/media/media/camera-offline.png +0 -0
  328. package/dist/utils/Talkback.js +0 -162
  329. package/dist/utils/Talkback.js.map +0 -1
  330. package/dist/utils/accessoriesStore.js +0 -210
  331. package/dist/utils/accessoriesStore.js.map +0 -1
  332. package/dist/utils/configTypes.js +0 -38
  333. package/dist/utils/configTypes.js.map +0 -1
  334. package/dist/utils/ffmpeg.js +0 -1111
  335. package/dist/utils/ffmpeg.js.map +0 -1
  336. package/dist/utils/interfaces.js +0 -8
  337. package/dist/utils/interfaces.js.map +0 -1
  338. package/dist/utils/utils.js +0 -224
  339. package/dist/utils/utils.js.map +0 -1
  340. package/dist/version.js +0 -2
  341. package/dist/version.js.map +0 -1
  342. package/homebridge-eufy-security.png +0 -0
  343. package/homebridge-ui/diagnosticsCrypto.js +0 -112
  344. package/homebridge-ui/public/app.js +0 -237
  345. package/homebridge-ui/public/assets/devices/4g_lte_cam_s330_large.png +0 -0
  346. package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
  347. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
  348. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
  349. package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
  350. package/homebridge-ui/public/assets/devices/batterydoorbell2k_large.png +0 -0
  351. package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
  352. package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
  353. package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
  354. package/homebridge-ui/public/assets/devices/eufyCamS4_large.png +0 -0
  355. package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
  356. package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
  357. package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
  358. package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
  359. package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
  360. package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
  361. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  362. package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
  363. package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
  364. package/homebridge-ui/public/assets/devices/flood_freeze_sensor_e20_t90F0_large.png +0 -0
  365. package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
  366. package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
  367. package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
  368. package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
  369. package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.png +0 -0
  370. package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
  371. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  372. package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
  373. package/homebridge-ui/public/assets/devices/homebasemini_large.png +0 -0
  374. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  375. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  376. package/homebridge-ui/public/assets/devices/indoorcamC220_t8419N_large.png +0 -0
  377. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  378. package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
  379. package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
  380. package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
  381. package/homebridge-ui/public/assets/devices/indoorcams350_large.png +0 -0
  382. package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
  383. package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
  384. package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
  385. package/homebridge-ui/public/assets/devices/nvr_s4_max_T8N00_large.png +0 -0
  386. package/homebridge-ui/public/assets/devices/pir_sensor_e20_large.png +0 -0
  387. package/homebridge-ui/public/assets/devices/poe_bullet_ptz_cam_s4_T8E00_large.png +0 -0
  388. package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
  389. package/homebridge-ui/public/assets/devices/siren_sensor_e20_large.png +0 -0
  390. package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
  391. package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
  392. package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
  393. package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
  394. package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
  395. package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
  396. package/homebridge-ui/public/assets/devices/smartlock_t85D0_large.png +0 -0
  397. package/homebridge-ui/public/assets/devices/smartlock_t85L0_large.png +0 -0
  398. package/homebridge-ui/public/assets/devices/smartlock_t85P0_large.png +0 -0
  399. package/homebridge-ui/public/assets/devices/smartlock_t85V0_large.png +0 -0
  400. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
  401. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
  402. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
  403. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
  404. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
  405. package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
  406. package/homebridge-ui/public/assets/devices/smartlock_video_t8531_large.png +0 -0
  407. package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
  408. package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
  409. package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
  410. package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
  411. package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
  412. package/homebridge-ui/public/assets/devices/smoke_alarm_e10_t90S0_large.png +0 -0
  413. package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
  414. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  415. package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
  416. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  417. package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
  418. package/homebridge-ui/public/assets/devices/solocame42_large.png +0 -0
  419. package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
  420. package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
  421. package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
  422. package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
  423. package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
  424. package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
  425. package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
  426. package/homebridge-ui/public/assets/devices/unknown.png +0 -0
  427. package/homebridge-ui/public/assets/devices/walllight_s100_large.png +0 -0
  428. package/homebridge-ui/public/assets/devices/walllight_s120_large.png +0 -0
  429. package/homebridge-ui/public/assets/devices/water_freeze_sensor_t8920_large.png +0 -0
  430. package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
  431. package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
  432. package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
  433. package/homebridge-ui/public/assets/icons/attach.svg +0 -1
  434. package/homebridge-ui/public/assets/icons/battery_0.svg +0 -1
  435. package/homebridge-ui/public/assets/icons/battery_1.svg +0 -1
  436. package/homebridge-ui/public/assets/icons/battery_2.svg +0 -1
  437. package/homebridge-ui/public/assets/icons/battery_3.svg +0 -1
  438. package/homebridge-ui/public/assets/icons/battery_4.svg +0 -1
  439. package/homebridge-ui/public/assets/icons/battery_5.svg +0 -1
  440. package/homebridge-ui/public/assets/icons/battery_6.svg +0 -1
  441. package/homebridge-ui/public/assets/icons/copy.svg +0 -1
  442. package/homebridge-ui/public/assets/icons/delete.svg +0 -1
  443. package/homebridge-ui/public/assets/icons/download.svg +0 -1
  444. package/homebridge-ui/public/assets/icons/info.svg +0 -1
  445. package/homebridge-ui/public/assets/icons/refresh.svg +0 -1
  446. package/homebridge-ui/public/assets/icons/satellite_alt.svg +0 -1
  447. package/homebridge-ui/public/assets/icons/solar_power.svg +0 -1
  448. package/homebridge-ui/public/components/device-card.js +0 -174
  449. package/homebridge-ui/public/components/guard-modes.js +0 -88
  450. package/homebridge-ui/public/components/number-input.js +0 -121
  451. package/homebridge-ui/public/components/select.js +0 -73
  452. package/homebridge-ui/public/components/toggle.js +0 -68
  453. package/homebridge-ui/public/services/api.js +0 -250
  454. package/homebridge-ui/public/services/config.js +0 -144
  455. package/homebridge-ui/public/style.css +0 -818
  456. package/homebridge-ui/public/utils/countries.js +0 -73
  457. package/homebridge-ui/public/utils/device-images.js +0 -102
  458. package/homebridge-ui/public/utils/helpers.js +0 -115
  459. package/homebridge-ui/public/views/dashboard.js +0 -226
  460. package/homebridge-ui/public/views/device-detail.js +0 -654
  461. package/homebridge-ui/public/views/diagnostics.js +0 -402
  462. package/homebridge-ui/public/views/login.js +0 -650
  463. package/homebridge-ui/public/views/recordings.js +0 -319
  464. package/homebridge-ui/public/views/settings.js +0 -197
  465. package/homebridge-ui/public/views/unsupported-detail.js +0 -296
  466. package/media/Snapshot-Unavailable.png +0 -0
  467. package/media/Snapshot-Unavailable.xcf +0 -0
  468. package/media/Snapshot-black.png +0 -0
  469. package/media/camera-disabled.png +0 -0
  470. package/media/camera-offline.png +0 -0
  471. package/scripts/decrypt-diagnostics.mjs +0 -257
@@ -0,0 +1,1003 @@
1
+ import { LiveStreamStartError } from '@mega-yfue/eufy-sdk';
2
+ import { createSocket } from 'node:dgram';
3
+ import { execFile, spawn } from 'node:child_process';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ /** How long a deliberately stopped adaptation process is given to exit before it is killed. */
6
+ export const PROCESS_STOP_GRACE_MS = 2_000;
7
+ /**
8
+ * Whether an adaptation opened for `running` can code what the source announced as `announced`.
9
+ *
10
+ * Only the codec decides it. An adaptation's input format is fixed at spawn, so an H.265 access unit fed to
11
+ * a process opened for H.264 does not parse; a changed geometry is absorbed, because the decoder
12
+ * reinitialises on the new parameter sets and the scale filter follows it. Measured through this plugin's
13
+ * bundled FFmpeg on its own argument list: 60 of 60 frames across 1280x720 to 640x360, 60 of 60 in reverse,
14
+ * 90 of 90 across 640 to 1280 to 640, no warnings; and a codec change decoding only the frames before it.
15
+ *
16
+ * The geometry never reached the argument list either — the output geometry is HomeKit's selection — so
17
+ * rebuilding on it spawned a byte-identical command, discarded the frames in flight, and waited for the
18
+ * next keyframe. A camera ramps its ladder during the first seconds of a session, four rungs inside 3.9 s
19
+ * on a mains-powered camera measured on the fleet, so all of that was paid before any video reached the
20
+ * controller.
21
+ */
22
+ function canCode(running, announced) {
23
+ return running.codec === announced.codec;
24
+ }
25
+ /**
26
+ * How many times one live session replaces its adaptation because the source's input changed, before it
27
+ * fails the session instead.
28
+ *
29
+ * A replacement costs a process, FFmpeg's stream analysis, the encoder's state, and the wait for another
30
+ * source keyframe, withholding every access unit in between. A source alternating between two inputs pays
31
+ * that at every keyframe and produces nothing watchable, so this bound sits above the changes a source
32
+ * settling on one input makes and below an alternation. Only a changed codec reaches it: {@link canCode}
33
+ * absorbs a changed geometry.
34
+ */
35
+ const MAX_SOURCE_INPUT_REPLACEMENTS = 3;
36
+ const INITIAL_RTCP_GRACE_MS = 15_000;
37
+ const SOURCE_ACQUISITION_DEADLINE_MS = 10_000;
38
+ const RETURN_AUDIO_BIND_GRACE_MS = 250;
39
+ /**
40
+ * Backstop for a started media session that never produces adapted output. The SDK source owns the
41
+ * warm-up window, retries the start inside it, and fails its consumers with a typed `error` event, which
42
+ * is the primary failure signal for both live and recording sessions; that window is an SDK-internal
43
+ * default the plugin cannot read, so this bound sits strictly above it and only catches an SDK that
44
+ * reports nothing at all.
45
+ */
46
+ export const SOURCE_START_BACKSTOP_MS = 30_000;
47
+ const SOURCE_ACQUISITION_TIMEOUT = Symbol('source-acquisition-timeout');
48
+ /**
49
+ * The bounded reason one SDK source failure reports.
50
+ *
51
+ * The SDK stages a start that produced no keyframe by what the source delivered, and a start that carried
52
+ * audio and never a single video frame is the one signature worth naming apart: measured on a real camera
53
+ * that had been switched off, the source accepted the start, delivered audio for the whole warm-up window,
54
+ * and reported `audio-only`. Every other stage is a source error, because nothing else distinguishes it
55
+ * from a transport that failed.
56
+ */
57
+ function sourceFailure(error) {
58
+ if (error instanceof Error && error.name === 'StationBusyError') {
59
+ return 'station-busy';
60
+ }
61
+ return error instanceof LiveStreamStartError && error.stage === 'audio-only' ? 'source-audio-only' : 'source-error';
62
+ }
63
+ /** How many diagnostic lines one adaptation retains, which is what a failure is attributed from. */
64
+ const ADAPTATION_STDERR_TAIL_LINES = 8;
65
+ /** The longest partial line held while waiting for its terminator, so a silent process cannot grow one. */
66
+ const ADAPTATION_STDERR_PARTIAL_BYTES = 512;
67
+ /** RFC 3550 fixed RTP header, RFC 3711 HMAC-SHA1-80 tag, and the RFC 6184 FU-A fragmentation header. */
68
+ const PACKET_OVERHEAD_BYTES = 12 + 10 + 2;
69
+ /**
70
+ * How far under the negotiated MTU the RTP muxer keeps a datagram. Measured on the bundled encoder at two
71
+ * MTUs: `pkt_size` 1200 emitted datagrams of at most 1196 bytes and 1378 emitted at most 1374.
72
+ */
73
+ const PACKET_SIZE_HEADROOM_BYTES = 4;
74
+ /**
75
+ * The encoder budget one negotiated video selection leaves after paying for its own transport, in kbps.
76
+ *
77
+ * The negotiated bit rate bounds what the accessory transmits, not what it codes, so the RTP and SRTP bytes
78
+ * this session will add are reserved out of the ceiling instead of being spent beside it. Every packet costs
79
+ * a header, an authentication tag and a fragmentation header, and the packet count is the coded media divided
80
+ * by what one full packet can carry, plus one partial packet per frame.
81
+ *
82
+ * The negotiated frame rate is the figure used because it bounds packets per second exactly as it bounds
83
+ * pictures per second. A camera delivering below its selection therefore produces fewer packets than were
84
+ * reserved for, which spends the ceiling rather than exceeding it.
85
+ *
86
+ * A ceiling too small to pay for its own transport still yields a positive budget, because an encoder given
87
+ * a non-positive rate refuses to start and a refused session is worse than an overshooting one.
88
+ */
89
+ function videoBudgetInsideCeiling(selection) {
90
+ const ceilingBytesPerSecond = selection.maxBitRate * 125;
91
+ const mediaBytesPerPacket = selection.mtu - PACKET_SIZE_HEADROOM_BYTES - PACKET_OVERHEAD_BYTES;
92
+ const packetsPerSecond = ceilingBytesPerSecond / mediaBytesPerPacket + selection.fps;
93
+ const reserved = (PACKET_OVERHEAD_BYTES * packetsPerSecond * 8) / 1_000;
94
+ return Math.max(1, Math.floor(selection.maxBitRate - reserved));
95
+ }
96
+ /**
97
+ * Splits one adaptation process's stderr into whole lines and retains a bounded tail of the diagnostic
98
+ * ones.
99
+ *
100
+ * Both consumers of that pipe read it: `-progress pipe:2` writes the key/value block that says adaptation
101
+ * reached its output, and everything else on it is the only account of why a process refused or stopped.
102
+ * Progress lines are excluded from the tail because they are emitted on a timer and would otherwise be the
103
+ * whole of it by the time anything went wrong.
104
+ */
105
+ export class AdaptationStderr {
106
+ partial = '';
107
+ retained = [];
108
+ /** Consumes one chunk and returns the lines it completed, in the order they were written. */
109
+ observe(chunk) {
110
+ const lines = `${this.partial}${chunk.toString()}`.split(/\r?\n/);
111
+ this.partial = lines.pop()?.slice(-ADAPTATION_STDERR_PARTIAL_BYTES) ?? '';
112
+ for (const line of lines) {
113
+ if (line.trim() === '' || line.startsWith('progress=')) {
114
+ continue;
115
+ }
116
+ this.retained.push(line);
117
+ if (this.retained.length > ADAPTATION_STDERR_TAIL_LINES) {
118
+ this.retained.shift();
119
+ }
120
+ }
121
+ return lines;
122
+ }
123
+ /** The retained diagnostic tail, oldest first. */
124
+ tail() {
125
+ return [...this.retained];
126
+ }
127
+ }
128
+ /** How long the version probe is allowed to take before the binary counts as not having answered. */
129
+ const FFMPEG_VERSION_PROBE_TIMEOUT_MS = 5_000;
130
+ /** How much of the banner is kept: enough for the build and version, and no more. */
131
+ const FFMPEG_VERSION_BANNER_LENGTH = 120;
132
+ /**
133
+ * Runs `-version` on one path and returns its first banner line, or nothing where it produced none.
134
+ *
135
+ * Every failure answers the same way, because none of them can be told apart by a caller that only needs
136
+ * to know whether this binary can be asked to encode: a missing file, a file that is not executable, one
137
+ * that is not FFmpeg, and one that hangs all mean the same thing to the media that would have used it.
138
+ */
139
+ const probeFfmpegVersion = (path) => new Promise((resolve) => {
140
+ execFile(path, ['-hide_banner', '-version'], { timeout: FFMPEG_VERSION_PROBE_TIMEOUT_MS, maxBuffer: 64 * 1024, windowsHide: true }, (error, stdout) => {
141
+ const banner = error ? undefined : stdout.split(/\r?\n/, 1)[0]?.trim().slice(0, FFMPEG_VERSION_BANNER_LENGTH);
142
+ resolve(banner === undefined || banner === '' ? undefined : banner);
143
+ });
144
+ });
145
+ /**
146
+ * Resolves the complete identity of one adaptation binary, without opening any media.
147
+ *
148
+ * Reading the banner spawns the binary, so this is asked once at launch and its answer is not waited on: a
149
+ * support archive is worth one process at startup, and no camera is worth delaying for one.
150
+ */
151
+ export async function resolveFfmpegIdentity(path, source, probe = probeFfmpegVersion) {
152
+ const version = await probe(path).catch(() => undefined);
153
+ return { path, source, ...(version === undefined ? {} : { version }) };
154
+ }
155
+ /** Adapts separate SDK elementary streams into independently failing HomeKit SRTP outputs. */
156
+ export class FfmpegLiveMedia {
157
+ executable;
158
+ adaptationDiagnostics;
159
+ createProcess;
160
+ reservePort;
161
+ createReturnAudioProcess;
162
+ constructor(executable, adaptationDiagnostics, createProcess = spawnLiveMediaProcess, reservePort = reserveMediaPort, createReturnAudioProcess = spawnReturnAudioProcess) {
163
+ this.executable = executable;
164
+ this.adaptationDiagnostics = adaptationDiagnostics;
165
+ this.createProcess = createProcess;
166
+ this.reservePort = reservePort;
167
+ this.createReturnAudioProcess = createReturnAudioProcess;
168
+ }
169
+ /**
170
+ * Reserves the negotiated output ports so `SetupEndpoints` can be answered with them, and returns a
171
+ * session that holds nothing else: no SDK handle, adaptation process, or device session exists until
172
+ * `start`. The reservation therefore lives for as long as its HomeKit consumer keeps the session, and
173
+ * `stop` releases every reservation exactly once however the session ended.
174
+ */
175
+ async prepare(transport) {
176
+ const videoPort = await this.reservePort(transport.addressVersion);
177
+ const targetAddress = transport.addressVersion === 'ipv6' ? `[${transport.targetAddress}]` : transport.targetAddress;
178
+ let audioPort;
179
+ try {
180
+ audioPort = transport.audio ? await this.reservePort(transport.addressVersion) : undefined;
181
+ }
182
+ catch (error) {
183
+ videoPort.close();
184
+ throw error;
185
+ }
186
+ let source;
187
+ let videoProcess;
188
+ let audioProcess;
189
+ let returnAudioProcess;
190
+ let talkbackHandle;
191
+ let talkbackSink;
192
+ let talkbackStarting = false;
193
+ let talkbackEnded = false;
194
+ let negotiated;
195
+ let stopped = false;
196
+ let receivedVideoKeyframe = false;
197
+ let reconfigurationPending = false;
198
+ /**
199
+ * The coded configuration the source last announced, and the one the running adaptation was opened for.
200
+ *
201
+ * Both, rather than a flag, because only a difference between them is a reason to replace an adaptation:
202
+ * announcements arrive before the first keyframe has opened one, and a configuration that moves away and
203
+ * back leaves the running adaptation still able to code what follows. What counts as a difference is
204
+ * {@link canCode}.
205
+ */
206
+ let videoConfig;
207
+ let adaptationConfig;
208
+ /** How many adaptation replacements the source's own changes have asked this session for. */
209
+ let sourceInputReplacements = 0;
210
+ let audioInputCodec;
211
+ let rtcpDeadline;
212
+ let initialRtcpGrace;
213
+ let videoStartBackstop;
214
+ let videoFailed = false;
215
+ let rtcpObserved = false;
216
+ let streaming = false;
217
+ const stoppingProcesses = new WeakSet();
218
+ const congested = new Set();
219
+ const { adaptationDiagnostics } = this;
220
+ /**
221
+ * Releases one adaptation input's claim on the source, whether it drained or stopped existing.
222
+ *
223
+ * An adaptation that is replaced or exits while full never emits the `drain` its claim is waiting for, so
224
+ * discharging the claim is part of ending the process rather than something only a successful drain does:
225
+ * otherwise one stalled process that a geometry change already replaced would hold the source for the
226
+ * rest of the session. It is idempotent and claimed from both directions — the process ending, and its
227
+ * input closing — because an adaptation can stop either way and neither alone covers both.
228
+ */
229
+ const releaseAdaptation = (sink) => {
230
+ if (congested.delete(sink) && !stopped && congested.size === 0) {
231
+ source?.resume();
232
+ }
233
+ };
234
+ /**
235
+ * Writes one media payload to an adaptation, holding the SDK source while that input stays full.
236
+ *
237
+ * A discarded write result relocates a keyframe-aware bounded queue in the SDK into a byte-blind pipe
238
+ * buffer, which then grows in plugin heap for the life of the session and stalls the event loop with it.
239
+ * Holding the source instead arms the SDK's own drop-to-keyframe policy, so a session that cannot keep up
240
+ * resynchronises on decodable media rather than replaying a stale backlog.
241
+ *
242
+ * One source feeds both adaptations, so it is held while *any* of them is full and released only once
243
+ * every one has drained. That keeps a single source of truth for backpressure on this path: no queue,
244
+ * threshold, or drop rule is invented here.
245
+ */
246
+ const writeToAdaptation = (sink, data) => {
247
+ if (sink.write(data) || congested.has(sink)) {
248
+ return;
249
+ }
250
+ congested.add(sink);
251
+ if (congested.size === 1) {
252
+ source?.pause();
253
+ }
254
+ sink.once('drain', () => releaseAdaptation(sink));
255
+ };
256
+ /**
257
+ * Reports what one adaptation process did, whether or not the session it belonged to fails for it.
258
+ *
259
+ * An audio adaptation is restarted rather than failed and a return-audio one fails only talkback, so
260
+ * without a report of their own those two exit with no account anywhere of why they stopped. An `output`
261
+ * report carries nothing but the tail, so it is made only when the process actually wrote something:
262
+ * a silent process that ended as intended has nothing to attribute.
263
+ */
264
+ const reportAdaptation = (role, event, stderr, code, signal) => {
265
+ const tail = stderr.tail();
266
+ if (event === 'output' && tail.length === 0) {
267
+ return;
268
+ }
269
+ adaptationDiagnostics?.report({
270
+ role,
271
+ event,
272
+ ...(typeof code === 'number' ? { code } : {}),
273
+ ...(typeof signal === 'string' ? { signal } : {}),
274
+ ...(tail.length ? { stderr: tail } : {}),
275
+ });
276
+ };
277
+ const stopProcess = (process) => {
278
+ if (!process) {
279
+ return;
280
+ }
281
+ stoppingProcesses.add(process);
282
+ process.stdin.destroy();
283
+ releaseAdaptation(process.stdin);
284
+ process.kill('SIGTERM');
285
+ const killDeadline = setTimeout(() => process.kill('SIGKILL'), PROCESS_STOP_GRACE_MS);
286
+ killDeadline.unref?.();
287
+ process.on('exit', () => clearTimeout(killDeadline));
288
+ };
289
+ /** Stop a caller-owned SDK handle without allowing synchronous or asynchronous faults to abort cleanup. */
290
+ const stopTalkbackHandle = (handle) => {
291
+ try {
292
+ void handle.stop().catch(() => undefined);
293
+ }
294
+ catch { }
295
+ };
296
+ const stopTalkback = (stopHandle = true) => {
297
+ if (talkbackEnded) {
298
+ return;
299
+ }
300
+ talkbackEnded = true;
301
+ stopProcess(returnAudioProcess);
302
+ returnAudioProcess = undefined;
303
+ talkbackSink?.destroy();
304
+ talkbackSink = undefined;
305
+ const handle = talkbackHandle;
306
+ talkbackHandle = undefined;
307
+ if (stopHandle && handle) {
308
+ stopTalkbackHandle(handle);
309
+ }
310
+ };
311
+ const failTalkback = (reason) => {
312
+ if (stopped || talkbackEnded) {
313
+ return;
314
+ }
315
+ transport.onTalkbackOutcome?.({ outcome: 'failed', reason });
316
+ stopTalkback();
317
+ };
318
+ const stop = () => {
319
+ if (stopped) {
320
+ return;
321
+ }
322
+ stopped = true;
323
+ clearTimeout(rtcpDeadline);
324
+ clearTimeout(initialRtcpGrace);
325
+ clearTimeout(videoStartBackstop);
326
+ stopTalkback();
327
+ stopProcess(videoProcess);
328
+ stopProcess(audioProcess);
329
+ if (source) {
330
+ source.stop();
331
+ transport.onSessionReleased?.();
332
+ }
333
+ videoPort.close();
334
+ audioPort?.close();
335
+ };
336
+ const failVideo = (reason) => {
337
+ if (stopped || videoFailed) {
338
+ return;
339
+ }
340
+ videoFailed = true;
341
+ stop();
342
+ const stage = reason === 'source-acquisition-timeout' || source === undefined
343
+ ? 'sdk-source-acquisition'
344
+ : reason === 'rtcp-timeout'
345
+ ? 'controller-rtcp'
346
+ : !receivedVideoKeyframe
347
+ ? 'first-source-keyframe'
348
+ : 'first-adapted-output';
349
+ transport.onSessionOutcome?.({ outcome: 'failed', reason, stage });
350
+ transport.onVideoFailure?.();
351
+ };
352
+ /**
353
+ * RTCP liveness bounds a session that is already sending media. Before adaptation reaches the
354
+ * negotiated output the start backstop owns the bound, so an early datagram only records that the
355
+ * controller is present; arming the recurring deadline here would fail a source that legitimately
356
+ * warms for longer than one RTCP interval.
357
+ */
358
+ const resetRtcpDeadline = () => {
359
+ rtcpObserved = true;
360
+ clearTimeout(initialRtcpGrace);
361
+ clearTimeout(rtcpDeadline);
362
+ if (!streaming) {
363
+ return;
364
+ }
365
+ const interval = Math.max(negotiated?.video.rtcpInterval ?? 1, 1) * 5_000;
366
+ rtcpDeadline = setTimeout(() => failVideo('rtcp-timeout'), interval);
367
+ rtcpDeadline.unref?.();
368
+ };
369
+ videoPort.onMessage(resetRtcpDeadline);
370
+ /**
371
+ * Adaptation reached the negotiated output, so the session is bounded from here by RTCP liveness
372
+ * rather than by the start backstop. The initial grace is armed from this point because media may
373
+ * legitimately start well after the session does.
374
+ *
375
+ * A selection HomeKit has already replaced does not discharge the deadline for its replacement, however
376
+ * much it keeps reporting: FFmpeg reports progress on a timer whether or not new media reaches it, so
377
+ * only the adaptation that carries the current selection can say that selection is being served.
378
+ */
379
+ const observeAdaptationProgress = (reporter) => {
380
+ if (stopped || (reconfigurationPending && reporter === videoProcess)) {
381
+ return;
382
+ }
383
+ clearTimeout(videoStartBackstop);
384
+ videoStartBackstop = undefined;
385
+ if (streaming) {
386
+ return;
387
+ }
388
+ streaming = true;
389
+ if (rtcpObserved) {
390
+ resetRtcpDeadline();
391
+ }
392
+ else {
393
+ initialRtcpGrace = setTimeout(() => failVideo('rtcp-timeout'), INITIAL_RTCP_GRACE_MS);
394
+ initialRtcpGrace.unref?.();
395
+ }
396
+ transport.onSessionOutcome?.({ outcome: 'streaming' });
397
+ };
398
+ /**
399
+ * Records the coded configuration the source is about to deliver. Nothing is replaced here: a replacement
400
+ * can only start on a keyframe, and this arrives ahead of the frame carrying the new configuration.
401
+ */
402
+ const observeVideoConfig = (config) => {
403
+ videoConfig = config;
404
+ };
405
+ /**
406
+ * Writes one source access unit to the adaptation that is entitled to code it.
407
+ *
408
+ * A source configuration the running adaptation was not opened for cannot be coded by it at all, so those
409
+ * frames wait for the keyframe that lets a replacement start. A reconfigured HomeKit selection changes
410
+ * only the output, so the current process keeps coding and keeps the previous selection on the wire until
411
+ * that same keyframe arrives; a controller reconfigures precisely when it is unhappy with what it
412
+ * receives, and the source is then often the very thing not producing keyframes.
413
+ *
414
+ * Source-driven replacement is bounded by {@link MAX_SOURCE_INPUT_REPLACEMENTS}, past which the session
415
+ * fails rather than spawn again. A controller-driven one is not: the controller bounds how often it asks.
416
+ */
417
+ const writeVideo = (frame) => {
418
+ if (stopped || !negotiated || !videoConfig) {
419
+ return;
420
+ }
421
+ if (!receivedVideoKeyframe) {
422
+ if (!frame.keyframe) {
423
+ return;
424
+ }
425
+ receivedVideoKeyframe = true;
426
+ }
427
+ const inputChanged = adaptationConfig !== undefined && !canCode(adaptationConfig, videoConfig);
428
+ if (videoProcess && inputChanged && !frame.keyframe) {
429
+ return;
430
+ }
431
+ if (videoProcess && frame.keyframe && (inputChanged || reconfigurationPending)) {
432
+ if (inputChanged) {
433
+ sourceInputReplacements += 1;
434
+ }
435
+ if (sourceInputReplacements > MAX_SOURCE_INPUT_REPLACEMENTS) {
436
+ failVideo('source-input-unstable');
437
+ return;
438
+ }
439
+ reconfigurationPending = false;
440
+ stopProcess(videoProcess);
441
+ videoProcess = undefined;
442
+ }
443
+ if (!videoProcess) {
444
+ adaptationConfig = videoConfig;
445
+ const child = this.createProcess(this.executable, videoArguments(videoConfig, negotiated.video, targetAddress, transport.video));
446
+ videoProcess = child;
447
+ adaptationDiagnostics?.report({ role: 'live-video', event: 'started' });
448
+ const stderr = new AdaptationStderr();
449
+ let producedOutput = false;
450
+ child.stderr.on('data', (chunk) => {
451
+ if (stderr.observe(chunk).some((line) => line.startsWith('progress='))) {
452
+ producedOutput = true;
453
+ observeAdaptationProgress(child);
454
+ }
455
+ });
456
+ child.stdin.on('error', () => {
457
+ if (!stoppingProcesses.has(child)) {
458
+ reportAdaptation('live-video', 'input-failed', stderr);
459
+ failVideo('adaptation-failed');
460
+ }
461
+ });
462
+ child.stdin.on('close', () => releaseAdaptation(child.stdin));
463
+ child.on('error', () => {
464
+ if (!stoppingProcesses.has(child)) {
465
+ reportAdaptation('live-video', producedOutput ? 'exited-while-streaming' : 'spawn-failed', stderr);
466
+ failVideo(producedOutput ? 'adaptation-exited-while-streaming' : 'adaptation-spawn-failed');
467
+ }
468
+ });
469
+ child.on('exit', (code, signal) => {
470
+ if (stopped || stoppingProcesses.has(child)) {
471
+ reportAdaptation('live-video', 'output', stderr, code, signal);
472
+ return;
473
+ }
474
+ reportAdaptation('live-video', producedOutput ? 'exited-while-streaming' : 'exited-before-output', stderr, code, signal);
475
+ failVideo(producedOutput ? 'adaptation-exited-while-streaming' : 'adaptation-exited-before-output');
476
+ });
477
+ }
478
+ writeToAdaptation(videoProcess.stdin, frame.data);
479
+ };
480
+ const writeAudio = (frame) => {
481
+ if (stopped || !negotiated?.audio || !transport.audio || !audioPort) {
482
+ return;
483
+ }
484
+ if (!audioProcess) {
485
+ audioInputCodec = frame.codec;
486
+ const child = this.createProcess(this.executable, audioArguments(frame, negotiated.audio, targetAddress, transport.audio));
487
+ audioProcess = child;
488
+ adaptationDiagnostics?.report({ role: 'live-audio', event: 'started' });
489
+ const stderr = new AdaptationStderr();
490
+ let producedOutput = false;
491
+ child.stderr.on('data', (chunk) => {
492
+ if (stderr.observe(chunk).some((line) => line.startsWith('progress='))) {
493
+ producedOutput = true;
494
+ }
495
+ });
496
+ /**
497
+ * An audio adaptation is dropped rather than failed, so its own end has to discharge whatever claim
498
+ * it held on the source. Nothing else would if its input outlives it: the `drain` a full input is
499
+ * waiting for never arrives, and the video beside it would starve.
500
+ */
501
+ const clearChild = () => {
502
+ releaseAdaptation(child.stdin);
503
+ if (audioProcess === child) {
504
+ audioProcess = undefined;
505
+ }
506
+ };
507
+ child.stdin.on('close', () => releaseAdaptation(child.stdin));
508
+ child.stdin.on('error', () => {
509
+ if (!stoppingProcesses.has(child) && audioProcess === child) {
510
+ reportAdaptation('live-audio', 'input-failed', stderr);
511
+ }
512
+ stopProcess(child);
513
+ clearChild();
514
+ });
515
+ child.on('error', () => {
516
+ if (!stoppingProcesses.has(child)) {
517
+ reportAdaptation('live-audio', producedOutput ? 'exited-while-streaming' : 'spawn-failed', stderr);
518
+ }
519
+ stopProcess(child);
520
+ clearChild();
521
+ });
522
+ child.on('exit', (code, signal) => {
523
+ reportAdaptation('live-audio', stopped || stoppingProcesses.has(child)
524
+ ? 'output'
525
+ : producedOutput
526
+ ? 'exited-while-streaming'
527
+ : 'exited-before-output', stderr, code, signal);
528
+ clearChild();
529
+ });
530
+ }
531
+ if (audioInputCodec !== frame.codec) {
532
+ stopProcess(audioProcess);
533
+ audioProcess = undefined;
534
+ audioInputCodec = undefined;
535
+ writeAudio(frame);
536
+ return;
537
+ }
538
+ writeToAdaptation(audioProcess.stdin, frame.data);
539
+ };
540
+ /**
541
+ * Write one decoded return-audio chunk through the SDK writable that owns ADTS frame recovery,
542
+ * validation, pacing, and transport backpressure. The first chunk opens exactly one SDK handle;
543
+ * stdout stays paused while that handle is acquired so acquisition cannot create an unbounded queue.
544
+ */
545
+ const writeReturnAudio = (camera, child, chunk) => {
546
+ if (stopped || talkbackEnded) {
547
+ return;
548
+ }
549
+ if (talkbackSink) {
550
+ try {
551
+ if (!talkbackSink.write(chunk)) {
552
+ child.stdout.pause();
553
+ talkbackSink.once('drain', () => {
554
+ if (!stopped && !talkbackEnded) {
555
+ child.stdout.resume();
556
+ }
557
+ });
558
+ }
559
+ }
560
+ catch {
561
+ failTalkback('device-audio-failed');
562
+ }
563
+ return;
564
+ }
565
+ if (talkbackStarting || !camera.talkback) {
566
+ return;
567
+ }
568
+ talkbackStarting = true;
569
+ child.stdout.pause();
570
+ let acquisition;
571
+ try {
572
+ acquisition = camera.talkback();
573
+ }
574
+ catch {
575
+ failTalkback('source-unavailable');
576
+ return;
577
+ }
578
+ void acquisition.then((handle) => {
579
+ if (stopped || talkbackEnded) {
580
+ stopTalkbackHandle(handle);
581
+ return;
582
+ }
583
+ talkbackHandle = handle;
584
+ try {
585
+ const sink = handle.writable();
586
+ talkbackSink = sink;
587
+ handle.on('budget', (notice) => {
588
+ if (!stopped && !talkbackEnded) {
589
+ notice.extend();
590
+ }
591
+ });
592
+ handle.on('error', () => failTalkback('device-audio-failed'));
593
+ handle.on('stop', () => {
594
+ if (!stopped && !talkbackEnded && talkbackHandle === handle) {
595
+ failTalkback('device-audio-failed');
596
+ }
597
+ });
598
+ sink.on('error', () => failTalkback('device-audio-failed'));
599
+ const accepted = sink.write(chunk);
600
+ if (talkbackEnded) {
601
+ return;
602
+ }
603
+ transport.onTalkbackOutcome?.({ outcome: 'talking' });
604
+ if (accepted) {
605
+ child.stdout.resume();
606
+ }
607
+ else {
608
+ sink.once('drain', () => {
609
+ if (!stopped && !talkbackEnded) {
610
+ child.stdout.resume();
611
+ }
612
+ });
613
+ }
614
+ }
615
+ catch {
616
+ failTalkback('device-audio-failed');
617
+ }
618
+ }, () => failTalkback('source-unavailable'));
619
+ };
620
+ /**
621
+ * Hand the accessory audio endpoint to FFmpeg and give the spawned process a bounded bind grace before
622
+ * stream start is acknowledged. FFmpeg owns SRTP authentication, RTP AAC-ELD depacketization, decoding,
623
+ * and AAC-LC ADTS encoding; the SDK handle remains unopened until the process produces decoded audio.
624
+ */
625
+ const startReturnAudio = async (camera, selection) => {
626
+ if (!camera.talkback || !selection || !transport.audio || !audioPort) {
627
+ return;
628
+ }
629
+ if (selection.sampleRate !== 16 || selection.channels !== 1) {
630
+ failTalkback('unsupported-selection');
631
+ return;
632
+ }
633
+ try {
634
+ await audioPort.close();
635
+ if (stopped || talkbackEnded) {
636
+ return;
637
+ }
638
+ const child = this.createReturnAudioProcess(this.executable, returnAudioArguments());
639
+ returnAudioProcess = child;
640
+ const stderr = new AdaptationStderr();
641
+ let producedOutput = false;
642
+ child.stderr.on('data', (chunk) => stderr.observe(chunk));
643
+ child.stdin.on('error', () => {
644
+ if (!stopped && !talkbackEnded && !stoppingProcesses.has(child)) {
645
+ reportAdaptation('return-audio', 'input-failed', stderr);
646
+ failTalkback('adaptation-failed');
647
+ }
648
+ });
649
+ child.stdout.on('data', (chunk) => {
650
+ producedOutput = true;
651
+ writeReturnAudio(camera, child, chunk);
652
+ });
653
+ child.stdout.on('error', () => {
654
+ if (!stopped && !talkbackEnded && !stoppingProcesses.has(child)) {
655
+ reportAdaptation('return-audio', 'input-failed', stderr);
656
+ failTalkback('adaptation-failed');
657
+ }
658
+ });
659
+ child.on('error', () => {
660
+ if (!stoppingProcesses.has(child)) {
661
+ reportAdaptation('return-audio', producedOutput ? 'exited-while-streaming' : 'spawn-failed', stderr);
662
+ failTalkback('adaptation-failed');
663
+ }
664
+ });
665
+ child.on('exit', (code, signal) => {
666
+ if (stopped || talkbackEnded || stoppingProcesses.has(child)) {
667
+ reportAdaptation('return-audio', 'output', stderr, code, signal);
668
+ return;
669
+ }
670
+ reportAdaptation('return-audio', producedOutput ? 'exited-while-streaming' : 'exited-before-output', stderr, code, signal);
671
+ failTalkback('adaptation-failed');
672
+ });
673
+ child.stdin.end(returnAudioSdp(audioPort.port, selection, transport.audio, transport.addressVersion));
674
+ await delay(RETURN_AUDIO_BIND_GRACE_MS);
675
+ }
676
+ catch {
677
+ failTalkback('adaptation-failed');
678
+ }
679
+ };
680
+ return {
681
+ videoPort: videoPort.port,
682
+ ...(audioPort ? { audioPort: audioPort.port } : {}),
683
+ async start(camera, selection) {
684
+ if (stopped) {
685
+ throw new Error('live media session is already stopped');
686
+ }
687
+ negotiated = selection;
688
+ const returnAudioReady = startReturnAudio(camera, selection.audio);
689
+ let sourcePromise;
690
+ let acquisitionDeadline;
691
+ try {
692
+ sourcePromise = camera.live();
693
+ void sourcePromise.then((lateSource) => {
694
+ if (stopped && source !== lateSource) {
695
+ lateSource.stop();
696
+ }
697
+ }, () => undefined);
698
+ source = await Promise.race([
699
+ sourcePromise,
700
+ new Promise((_, reject) => {
701
+ acquisitionDeadline = setTimeout(() => reject(SOURCE_ACQUISITION_TIMEOUT), SOURCE_ACQUISITION_DEADLINE_MS);
702
+ acquisitionDeadline.unref?.();
703
+ }),
704
+ ]);
705
+ }
706
+ catch (error) {
707
+ failVideo(error === SOURCE_ACQUISITION_TIMEOUT ? 'source-acquisition-timeout' : sourceFailure(error));
708
+ throw error === SOURCE_ACQUISITION_TIMEOUT ? new Error('live media source acquisition timed out') : error;
709
+ }
710
+ finally {
711
+ clearTimeout(acquisitionDeadline);
712
+ }
713
+ if (stopped) {
714
+ source.stop();
715
+ return;
716
+ }
717
+ videoStartBackstop = setTimeout(() => failVideo('no-video-within-backstop'), SOURCE_START_BACKSTOP_MS);
718
+ videoStartBackstop.unref?.();
719
+ source.on('video-config', observeVideoConfig);
720
+ source.on('video', writeVideo);
721
+ source.on('audio', writeAudio);
722
+ source.on('budget', (notice) => {
723
+ if (!stopped) {
724
+ notice.extend();
725
+ }
726
+ });
727
+ source.on('error', (error) => failVideo(sourceFailure(error)));
728
+ source.on('stop', () => failVideo('source-stopped'));
729
+ await returnAudioReady;
730
+ },
731
+ /**
732
+ * Acknowledges a reconfigured selection and applies it at the next source keyframe.
733
+ *
734
+ * Nothing is torn down here: the current adaptation keeps the previous selection on the wire until the
735
+ * replacement has a keyframe to start from. The deferral is still bounded, because a session that never
736
+ * applies the selection HomeKit asked for must end and be renegotiated rather than silently serve the
737
+ * old one forever.
738
+ */
739
+ reconfigure(video) {
740
+ if (!negotiated) {
741
+ return;
742
+ }
743
+ negotiated = { ...negotiated, video };
744
+ reconfigurationPending = videoProcess !== undefined;
745
+ clearTimeout(videoStartBackstop);
746
+ videoStartBackstop = setTimeout(() => failVideo('no-video-within-backstop'), SOURCE_START_BACKSTOP_MS);
747
+ videoStartBackstop.unref?.();
748
+ },
749
+ stop,
750
+ };
751
+ }
752
+ }
753
+ /**
754
+ * Options every adapted elementary stream applies to its own input.
755
+ *
756
+ * A piped elementary stream carries no container, so FFmpeg's initial stream analysis is the only thing
757
+ * standing between the first written access unit and the first coded frame. The caller already declares the
758
+ * format, so that analysis has nothing left to discover and is bounded to its minimum; leaving it at the
759
+ * default delays first output by seconds and scales that delay with the source keyframe interval.
760
+ *
761
+ * Every such input is read on one clock: when its media arrived. A pipe carries no timeline of its own, so
762
+ * arrival is the only timeline there is, and two adapted streams of one session can be presented together
763
+ * only while both are read on it.
764
+ *
765
+ * Nothing asks FFmpeg to discard or reinterpret what it read. Discarding the analysed packets throws away the
766
+ * leading keyframe the caller waited for, and a raw A-law demuxer stops emitting timestamps entirely, so both
767
+ * cost media that was already in hand.
768
+ */
769
+ function commonArguments(inputFormat, inputOptions = []) {
770
+ return [
771
+ '-hide_banner',
772
+ '-loglevel',
773
+ 'warning',
774
+ '-probesize',
775
+ '32',
776
+ '-analyzeduration',
777
+ '1',
778
+ '-progress',
779
+ 'pipe:2',
780
+ '-nostats',
781
+ '-f',
782
+ inputFormat,
783
+ ...inputOptions,
784
+ '-use_wallclock_as_timestamps',
785
+ '1',
786
+ '-i',
787
+ 'pipe:0',
788
+ ];
789
+ }
790
+ function outputArguments(targetAddress, target, payloadType, ssrc, packetSize) {
791
+ return [
792
+ '-payload_type',
793
+ String(payloadType),
794
+ '-ssrc',
795
+ String(ssrc),
796
+ '-f',
797
+ 'rtp',
798
+ '-srtp_out_suite',
799
+ target.srtpCryptoSuite,
800
+ '-srtp_out_params',
801
+ Buffer.concat([target.srtpKey, target.srtpSalt]).toString('base64'),
802
+ `srtp://${targetAddress}:${target.port}?rtcpport=${target.port}&pkt_size=${packetSize}`,
803
+ ];
804
+ }
805
+ /**
806
+ * Raw SDK frames cannot prove profile, level, frame rate, and bitrate, so negotiated video is transcoded.
807
+ *
808
+ * Asking FFmpeg to generate presentation timestamps rather than read the arrival ones every adapted input
809
+ * carries makes it interpolate them from a frame rate a bare Annex-B pipe never states, which collapses the
810
+ * whole session onto one instant; the constant-rate output then resolves the collision by discarding almost
811
+ * every frame it was given.
812
+ *
813
+ * A negotiated frame rate is a CEILING, not a cadence, so the rate is bounded rather than pinned. Pinning it
814
+ * makes the encoder duplicate frames the source never sent, and each duplicate costs a full encode and a share
815
+ * of the negotiated bit rate for a picture carrying nothing new. Measured on a 1600x1200 doorbell delivering
816
+ * about 15 fps against a 30 fps selection: 264 of 267 emitted frames were duplicates and the adaptation ran at
817
+ * 0.32x real time, so it fell further behind every second and the session died on its backstop with nothing
818
+ * watchable reaching the controller.
819
+ *
820
+ * A negotiated selection states no refresh cadence, so the keyframe interval is plugin policy: `-g` and
821
+ * `-keyint_min` are the negotiated rate doubled and scene-cut detection is off, which is a refresh every two
822
+ * negotiated seconds and nothing between them. It is the only repair a live session has, because this output is
823
+ * unretransmitted SRTP over UDP and nothing here asks the encoder for a picture out of cadence, so one lost in
824
+ * transit persists until the next refresh. What the interval costs is in docs/architecture.md.
825
+ *
826
+ * A negotiated bit rate is a ceiling on what the accessory TRANSMITS, so the encoder is given the ceiling less
827
+ * the transport its own packetization adds, and its VBV buffer holds one second of that budget rather than
828
+ * two. A buffer is an allowance a window may exceed the rate by, so a two-second buffer owes a 45-second
829
+ * session 4.4 percent over its ceiling on arithmetic alone, before any transport is counted. An encoder coding
830
+ * inside its ceiling still transmits over it whenever the transport is unreserved, which is what the measured
831
+ * overshoot was; the evidence is in docs/architecture.md.
832
+ *
833
+ * `superfast` is the cheapest `libx264` preset that retains CABAC, and therefore the cheapest one whose
834
+ * coded stream can carry a negotiated Main or High profile; `ultrafast` drops CABAC and codes Constrained
835
+ * Baseline whatever `-profile:v` asks for. `-tune zerolatency` pins the same `sliced_threads`, `bframes`
836
+ * and `rc_lookahead` at either preset, so this costs computation rather than frame delay.
837
+ */
838
+ function videoArguments(input, selection, targetAddress, target) {
839
+ const budget = videoBudgetInsideCeiling(selection);
840
+ return [
841
+ ...commonArguments(input.codec === 'h265' ? 'hevc' : input.codec),
842
+ '-an',
843
+ '-c:v',
844
+ 'libx264',
845
+ '-preset',
846
+ 'superfast',
847
+ '-tune',
848
+ 'zerolatency',
849
+ '-profile:v',
850
+ selection.profile,
851
+ '-level:v',
852
+ selection.level,
853
+ '-pix_fmt',
854
+ 'yuv420p',
855
+ '-vf',
856
+ `scale=${selection.width}:${selection.height}:force_original_aspect_ratio=decrease,pad=${selection.width}:${selection.height}:(ow-iw)/2:(oh-ih)/2`,
857
+ '-fpsmax',
858
+ String(selection.fps),
859
+ '-g',
860
+ String(selection.fps * 2),
861
+ '-keyint_min',
862
+ String(selection.fps * 2),
863
+ '-sc_threshold',
864
+ '0',
865
+ '-b:v',
866
+ `${budget}k`,
867
+ '-maxrate',
868
+ `${budget}k`,
869
+ '-bufsize',
870
+ `${budget}k`,
871
+ ...outputArguments(targetAddress, target, selection.payloadType, selection.ssrc, selection.mtu),
872
+ ];
873
+ }
874
+ /**
875
+ * Adapts one source audio elementary stream to the negotiated AAC-ELD output.
876
+ *
877
+ * The SDK reports no sample rate or channel count, because a station sends neither; 16 kHz mono is the
878
+ * assumption every Eufy client applies. Raw A-law carries nothing at all and must be told that assumption,
879
+ * while an ADTS input states its own rate in every frame header and rejects the option outright, which fails
880
+ * the process before it reads a byte.
881
+ *
882
+ * Asynchronous resampling is what makes this output honour the arrival clock its input is read on. An encoder
883
+ * advances its output timeline by the samples in each coded frame it is handed, whatever timestamp arrived
884
+ * with them, so the arrival stamp alone would move only where the timeline starts. The resampler resolves
885
+ * every frame against that stamp instead, which bounds the output to real time however much content a source
886
+ * hands over at once.
887
+ *
888
+ * It resolves an excess by DISCARDING it, so a source that hands over more audio than it has been running for
889
+ * loses the excess permanently rather than being played back late. That is this session's policy and not an
890
+ * accident of the filter: a live view owes its controller the newest audio, and audio which is already
891
+ * seconds stale by the time it arrives cannot be presented beside a picture timestamped on arrival.
892
+ *
893
+ * `libfdk_aac` selects its transport from the requested output framing, and AAC-ELD cannot be carried in
894
+ * ADTS, so without an explicit global header the encoder refuses to initialise at all.
895
+ */
896
+ function audioArguments(frame, selection, targetAddress, target) {
897
+ const rawAlaw = frame.codec === 'g711a';
898
+ return [
899
+ ...commonArguments(rawAlaw ? 'alaw' : 'aac', rawAlaw ? ['-ar', '16k', '-ac', '1'] : []),
900
+ '-vn',
901
+ '-c:a',
902
+ 'libfdk_aac',
903
+ '-profile:a',
904
+ 'aac_eld',
905
+ '-flags',
906
+ '+global_header',
907
+ '-af',
908
+ 'aresample=async=1',
909
+ '-ar',
910
+ `${selection.sampleRate}k`,
911
+ '-ac',
912
+ String(selection.channels),
913
+ '-b:a',
914
+ `${selection.maxBitRate}k`,
915
+ ...outputArguments(targetAddress, target, selection.payloadType, selection.ssrc, 188),
916
+ ];
917
+ }
918
+ /**
919
+ * Decode HomeKit's controller-to-accessory AAC-ELD SRTP and emit the SDK's exact talkback input: 16 kHz
920
+ * mono AAC-LC in ADTS. The SDK owns complete-frame recovery, the 640-byte frame limit, and 64 ms pacing,
921
+ * so FFmpeg must produce a byte stream rather than impose another clock.
922
+ */
923
+ function returnAudioArguments() {
924
+ return [
925
+ '-hide_banner',
926
+ '-loglevel',
927
+ 'warning',
928
+ '-nostats',
929
+ '-protocol_whitelist',
930
+ 'pipe,udp,rtp,crypto',
931
+ '-f',
932
+ 'sdp',
933
+ '-c:a',
934
+ 'libfdk_aac',
935
+ '-i',
936
+ 'pipe:0',
937
+ '-map',
938
+ '0:a:0',
939
+ '-vn',
940
+ '-c:a',
941
+ 'libfdk_aac',
942
+ '-profile:a',
943
+ 'aac_low',
944
+ '-ar',
945
+ '16k',
946
+ '-ac',
947
+ '1',
948
+ '-b:a',
949
+ '32k',
950
+ '-f',
951
+ 'adts',
952
+ 'pipe:1',
953
+ ];
954
+ }
955
+ /**
956
+ * Describe the one return-audio RTP stream HomeKit negotiated. AAC-ELD at 16 kHz mono uses the
957
+ * established MPEG4-GENERIC AAC-hbr configuration below; the crypto material is the audio endpoint key
958
+ * HomeKit supplied during SetupEndpoints and no media or identity enters diagnostics.
959
+ */
960
+ function returnAudioSdp(port, selection, target, addressVersion) {
961
+ const network = addressVersion === 'ipv6' ? 'IP6' : 'IP4';
962
+ const address = addressVersion === 'ipv6' ? '::' : '0.0.0.0';
963
+ const key = Buffer.concat([target.srtpKey, target.srtpSalt]).toString('base64');
964
+ return [
965
+ 'v=0',
966
+ `o=- 0 0 IN ${network} ${address}`,
967
+ 's=HomeKit Return Audio',
968
+ `c=IN ${network} ${address}`,
969
+ 't=0 0',
970
+ `m=audio ${port} RTP/AVP ${selection.payloadType}`,
971
+ `b=AS:${selection.maxBitRate}`,
972
+ `a=rtpmap:${selection.payloadType} MPEG4-GENERIC/16000/1`,
973
+ `a=fmtp:${selection.payloadType} profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=F8F0212C00BC00`,
974
+ `a=crypto:1 ${target.srtpCryptoSuite} inline:${key}`,
975
+ 'a=rtcp-mux',
976
+ '',
977
+ ].join('\r\n');
978
+ }
979
+ function spawnLiveMediaProcess(executable, args) {
980
+ return spawn(executable, args, { stdio: ['pipe', 'ignore', 'pipe'] });
981
+ }
982
+ function spawnReturnAudioProcess(executable, args) {
983
+ return spawn(executable, args, { stdio: ['pipe', 'pipe', 'pipe'] });
984
+ }
985
+ function reserveMediaPort(addressVersion) {
986
+ return new Promise((resolve, reject) => {
987
+ const socket = createSocket(addressVersion === 'ipv6' ? 'udp6' : 'udp4');
988
+ socket.once('error', reject);
989
+ socket.bind(0, () => {
990
+ const address = socket.address();
991
+ socket.removeListener('error', reject);
992
+ let closing;
993
+ resolve({
994
+ port: address.port,
995
+ onMessage: (listener) => socket.on('message', listener),
996
+ close: () => (closing ??= new Promise((done) => {
997
+ socket.close(done);
998
+ })),
999
+ });
1000
+ });
1001
+ });
1002
+ }
1003
+ //# sourceMappingURL=live-stream.js.map