@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.
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
@@ -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