Compare commits
742 Commits
v1.9.6
...
fix-net-dots
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fc40aa7db | |||
| dc1685e9cf | |||
| 65af579770 | |||
| d9af354de8 | |||
| 31c19a0e7d | |||
| 01c7451556 | |||
| f68c602a7d | |||
| 7f36033bff | |||
| 6304987a26 | |||
| 2735bafd86 | |||
| 44f6f111c4 | |||
| e5bb03349b | |||
| e90f159c68 | |||
| be4b6c3271 | |||
| f3ad4ad977 | |||
| 7083afe9b2 | |||
| d01b99d105 | |||
| d1d7846aed | |||
| 8d58cc7f97 | |||
| f1d61bf15a | |||
| d2c4e44844 | |||
| 5bf5327a45 | |||
| 4bf0aef124 | |||
| 8e01d5cfa9 | |||
| 6085c8aabe | |||
| 8d4869fd08 | |||
| 5970dd2fe4 | |||
| a41185c22a | |||
| c793855800 | |||
| a556bca7bd | |||
| 051aa664cd | |||
| 6374366da5 | |||
| de157eb144 | |||
| 72ec880300 | |||
| 6eeba1ae4c | |||
| 8f68f80bff | |||
| 2f9d138692 | |||
| 366cbd200d | |||
| 6f75b2f75e | |||
| 75bd1f1b2b | |||
| 7793a55853 | |||
| 06cbbe5543 | |||
| 861bc9728b | |||
| ae0646ea9e | |||
| c8412eb067 | |||
| a37fdf38d8 | |||
| b5948cfb25 | |||
| c64fcc55a5 | |||
| ba9fb70929 | |||
| 1d9bb27f58 | |||
| 55c1f5890b | |||
| d98059f069 | |||
| fc22b20896 | |||
| af819952e8 | |||
| 159fb4675c | |||
| 54eafe9d0a | |||
| 38cc05c22d | |||
| 8c457710bd | |||
| 514188201a | |||
| b65ee278cd | |||
| 66225973ae | |||
| d40f6064d9 | |||
| 9365fef7b3 | |||
| b220959e41 | |||
| c493087876 | |||
| af90b4c12c | |||
| 679454aedb | |||
| 160695857e | |||
| 6a5deecfcc | |||
| 572f07fcce | |||
| 9d1e4b11d7 | |||
| f85cfdc214 | |||
| 5780bf3720 | |||
| 338a3a6f03 | |||
| 6796bdabe2 | |||
| ebe454e3be | |||
| c03cd9f156 | |||
| 1ec40f2fc3 | |||
| 790fdfbf7a | |||
| 6d77b175d8 | |||
| 8e6b8b32d6 | |||
| 425fcffbe1 | |||
| 9a1eac8ef4 | |||
| 4ffdeb06d0 | |||
| 6b4eb8ffb6 | |||
| cea524074a | |||
| 7498d0fba5 | |||
| 50d9aab0d7 | |||
| 3983ce3f4f | |||
| 0066da94f7 | |||
| 4dbf53122e | |||
| f96a074957 | |||
| dbd04cb972 | |||
| 0d035e5bce | |||
| 7241759fea | |||
| b067c408c0 | |||
| a99590823b | |||
| 3bf2b316d7 | |||
| 2f43bfe5dc | |||
| 59161fcef2 | |||
| 9ca9f96ea2 | |||
| bc0c8d5577 | |||
| fd68107940 | |||
| b19c081642 | |||
| 039e916030 | |||
| 3a587c9cee | |||
| 25e3125a89 | |||
| 439dccf4bd | |||
| 5fcb33c0cd | |||
| c5311cdd94 | |||
| 3dcb6dfc48 | |||
| 406159cce5 | |||
| 659a042c42 | |||
| e614513b97 | |||
| f9f22cdd0b | |||
| dab9efb7d0 | |||
| eb39b80883 | |||
| ff04a0d4b2 | |||
| c4b32e3a0b | |||
| 58d8a86a92 | |||
| 29f966f280 | |||
| cbaa147469 | |||
| c55fa87827 | |||
| 84e13d9d22 | |||
| c6733bf4f1 | |||
| e960f90a97 | |||
| 3207f9e783 | |||
| 263579fa01 | |||
| 90c0b513e9 | |||
| cfbba5a52c | |||
| c74a39a30d | |||
| d4dc670cb5 | |||
| 4cff72c9a3 | |||
| f923487546 | |||
| f47a041ece | |||
| e77210f916 | |||
| 44da81774c | |||
| 7d4b3fe65b | |||
| a42ab88dbd | |||
| 4dae65a535 | |||
| a09e1b2f90 | |||
| d183b99a44 | |||
| 654e78b7c5 | |||
| 1cd5517026 | |||
| 07fb78d661 | |||
| 86f9f114b5 | |||
| 7e38b4fe89 | |||
| 0fd2217bd2 | |||
| 76bdc7e065 | |||
| 3bd433c950 | |||
| 4c50a2c00c | |||
| 0e65bd05c4 | |||
| 05fd0c5cca | |||
| a3a4276c15 | |||
| 0f4607a070 | |||
| 11ad1129ed | |||
| da0b19026c | |||
| c880c37d37 | |||
| c4fb66a818 | |||
| 9b618f45d0 | |||
| 3fd1fe2622 | |||
| 2f470fa518 | |||
| 212def9ceb | |||
| 79698365bb | |||
| 5b1da84ae2 | |||
| 247d4063ed | |||
| 7ada6d81eb | |||
| eda92afffe | |||
| ff19885790 | |||
| bbb9466845 | |||
| 079d404ed0 | |||
| 753d6617ab | |||
| dfe47559d1 | |||
| eabd7d60cd | |||
| dfda4b11ff | |||
| 353262307b | |||
| d734140eaf | |||
| 2409bb56d7 | |||
| df484cc904 | |||
| 7e0c7a8173 | |||
| 7eaa4a1b55 | |||
| 03941a5691 | |||
| 28821c41e0 | |||
| 8636e96379 | |||
| 7119384184 | |||
| 57b0ace802 | |||
| b0f46bc919 | |||
| a4d4598a13 | |||
| d041c89c5c | |||
| ce9138b354 | |||
| 1e5def35c9 | |||
| 17c1f69f66 | |||
| fb31a251b8 | |||
| c5277daa45 | |||
| 76a5e160c2 | |||
| 494daed937 | |||
| a86e10446a | |||
| 209b73a0f1 | |||
| 54473ff1de | |||
| 7eb5fe0355 | |||
| fbd5215669 | |||
| 3ebc115cc5 | |||
| 31962181cb | |||
| 6d1a95a4e3 | |||
| 4ec2849008 | |||
| 4ef6a147a6 | |||
| 94df080bf7 | |||
| 86edd814c9 | |||
| 5a259993d8 | |||
| b6fe8871df | |||
| b47a2ba73c | |||
| 4934fa4cc1 | |||
| 61f74820bc | |||
| 248fc7a11a | |||
| aa0ece2d1e | |||
| 68036b68c1 | |||
| 319dbf2c63 | |||
| 4b419309a8 | |||
| 42e7a03534 | |||
| 290e8fcfda | |||
| 6c78b5cb53 | |||
| c72b205d87 | |||
| 2cd009646a | |||
| 8f5fce4d73 | |||
| b705cadc04 | |||
| 2523a5ac38 | |||
| e246e2e756 | |||
| 2dc0d58ba7 | |||
| cb22ae7833 | |||
| c98b0a83c4 | |||
| 0bae158e41 | |||
| e2b63a4f6c | |||
| 3897f10a4d | |||
| ac80f1470e | |||
| 1fe602679e | |||
| e2c7d06730 | |||
| 2133f5323c | |||
| c10a06d199 | |||
| d053d88ce9 | |||
| 2ce38b4486 | |||
| 44d59b1696 | |||
| 15ec995ecc | |||
| 231cab36b2 | |||
| 640db3029e | |||
| 2836fdae13 | |||
| 964bb225fa | |||
| 5cc32197b8 | |||
| bc1a4ac8e4 | |||
| 158f9d3a08 | |||
| 81cfcf877a | |||
| 96919bf9e3 | |||
| e4359ac217 | |||
| 56d7a6fee4 | |||
| 292b32af99 | |||
| 33e4527042 | |||
| 62a9046f01 | |||
| 25e7ac531e | |||
| 0f27bb1124 | |||
| 0ff3bf67e1 | |||
| 6d3d45e337 | |||
| c6940eb0f3 | |||
| 8142d2fc43 | |||
| 721ed98afb | |||
| fb8c6e1b1b | |||
| 80ab32379c | |||
| 863174839c | |||
| ff18283d11 | |||
| 994e0dc526 | |||
| d16d260679 | |||
| d4190290b7 | |||
| 5660311bb2 | |||
| 7254bd4fbc | |||
| 9f407a754d | |||
| cc97bc33c4 | |||
| bd8e4fa298 | |||
| 6db4dda535 | |||
| be80eb1ac9 | |||
| a8d2312cb0 | |||
| 5bbc2aaf2e | |||
| 7abc963a50 | |||
| f8c88cfbe0 | |||
| cf4acd5a8d | |||
| 19226df7d6 | |||
| c415c8f2af | |||
| 2751f41afb | |||
| d59cb99f0d | |||
| 007e8dbc75 | |||
| dae396a1ed | |||
| d894483166 | |||
| 4924cf2256 | |||
| 60ef52f44b | |||
| 240e1960f8 | |||
| a107d13e61 | |||
| f911936d79 | |||
| ea23957f2a | |||
| f1971cec7c | |||
| 7291c03cea | |||
| fdb3116027 | |||
| c87885be48 | |||
| 98f88d037e | |||
| fde1fdc592 | |||
| cca216ace5 | |||
| e953e949ef | |||
| fe2cc4b525 | |||
| 6a67fc3712 | |||
| 94b7c33485 | |||
| 887f0f4890 | |||
| ec08dfee9c | |||
| 54b95dced4 | |||
| 56bf53208e | |||
| 37d7409e82 | |||
| 4dd1f73a18 | |||
| 22cc8ac2c4 | |||
| a667acad07 | |||
| ee5e31d3b3 | |||
| c196b82a72 | |||
| 670370056c | |||
| 0c5a2bf02b | |||
| 8f9e78be0c | |||
| 50d5fa93b6 | |||
| 3a0e4078fd | |||
| 549da0257e | |||
| 2b5f9429a8 | |||
| c7119f4403 | |||
| 7d9862202a | |||
| dd7130d2d4 | |||
| d697bdcf05 | |||
| abd61919cf | |||
| ad2383de80 | |||
| 9f3ff98951 | |||
| d286980548 | |||
| 87533ac5a1 | |||
| d05451416d | |||
| 5c01cbad9e | |||
| df95ce39d0 | |||
| 26f16e392f | |||
| fcc837e570 | |||
| fd682306e7 | |||
| e072842b7a | |||
| 3b976c6812 | |||
| 40269328fb | |||
| 45cbbaf1cf | |||
| 3f542a642c | |||
| 8b4df5f02c | |||
| 788afb7189 | |||
| cd7fa5d09c | |||
| beb82045ff | |||
| 33f0fd5fe6 | |||
| 850051a299 | |||
| 8f7cbdf60a | |||
| 4577390130 | |||
| f2242e31c8 | |||
| 975a43d392 | |||
| 3d38e5e567 | |||
| 7d2ad92c4b | |||
| b82023bc32 | |||
| a92e04b6e0 | |||
| 56e61a85ee | |||
| 7c3d97b0a2 | |||
| 88a2f33b71 | |||
| b7557d750e | |||
| ce4bee3be7 | |||
| 5e746e3367 | |||
| 7d83b0d6c8 | |||
| 708277230a | |||
| 06e6e31443 | |||
| 8474f2f571 | |||
| 9ddea7d9d6 | |||
| c2fbd372b3 | |||
| e9611769be | |||
| c5dfa84ff2 | |||
| da68101c09 | |||
| 30c418542c | |||
| b58c1a7ed6 | |||
| 47e87281a1 | |||
| 60b6b93ff8 | |||
| 3149b6f750 | |||
| e44f1ad53e | |||
| c38c8a7fce | |||
| 9efc717633 | |||
| a2d422f5cb | |||
| 3036dd7cfe | |||
| 5be5d9247c | |||
| 42b7eea852 | |||
| 7ee3f6e4f7 | |||
| fbd8d995ed | |||
| 998c85d6f5 | |||
| 67dfc942a0 | |||
| 1cc8b373de | |||
| f045f3fccd | |||
| 691f6d9cdd | |||
| 8a8fb66eeb | |||
| e7044a93f6 | |||
| a9bcb46f38 | |||
| 16a812c8b8 | |||
| 27fe2622ec | |||
| 3d222136f9 | |||
| 524cdb7176 | |||
| 499dc10390 | |||
| e7bd3d401f | |||
| 5ec942cb5e | |||
| 6c255cd2f2 | |||
| 4f969d750a | |||
| bd2cbe20e0 | |||
| 6d8d6a91ef | |||
| 05c12b34e5 | |||
| 43b7a662c1 | |||
| a7e76db464 | |||
| b797a2fcd1 | |||
| 6e35f1a389 | |||
| 30d48e139c | |||
| e74fc6f198 | |||
| 34b103bbcb | |||
| 6734492a2d | |||
| 90217d61d0 | |||
| ba27a7f68c | |||
| 22fbd8bc9e | |||
| d175213369 | |||
| e00d211619 | |||
| c68e3cafe4 | |||
| 5a34483513 | |||
| 7bb0f0d2e6 | |||
| 96d7066085 | |||
| ae49946740 | |||
| fcc91c9b8a | |||
| 994fd41826 | |||
| 3282b38900 | |||
| 647b2acf48 | |||
| ef318f663e | |||
| 5771454400 | |||
| 6732e726d5 | |||
| 230c80c70e | |||
| 45503aa8c5 | |||
| a4d7fd0d95 | |||
| b6579122d1 | |||
| 42a67f8ad5 | |||
| 91eeefec68 | |||
| 8ab7aeb8b2 | |||
| 493fa1ef6a | |||
| 020549ef60 | |||
| dfc1f45f97 | |||
| 641e65ee95 | |||
| 24ca87e00d | |||
| 859cd1cbe6 | |||
| 79656d1344 | |||
| 759f979182 | |||
| 7c17e64090 | |||
| bf45f64a7e | |||
| 72890d5983 | |||
| e8e798d955 | |||
| 8a8b379bfc | |||
| ca491def83 | |||
| 5a597277a9 | |||
| 6f2afdc97b | |||
| 6e2e0d353a | |||
| ae8145f266 | |||
| c90fcd1ce1 | |||
| e0687db9e2 | |||
| 24310e2f7a | |||
| a1f0b86ab3 | |||
| 7f87c6e478 | |||
| a0145b4b24 | |||
| 2fcbb1d836 | |||
| a2beea1bbd | |||
| e5e55b7a50 | |||
| 0830d8342e | |||
| adb1b21e81 | |||
| 4788011249 | |||
| edfa09bb9f | |||
| 2eef7bdbd3 | |||
| 124556f4db | |||
| f1faa95897 | |||
| b84df84e53 | |||
| d528e167db | |||
| f151969fe1 | |||
| 7107508286 | |||
| cd2f90a7a1 | |||
| e1577b5ad3 | |||
| 251686608a | |||
| 993aa613fd | |||
| 3c1f7e4181 | |||
| 2ed67648c3 | |||
| 2fdfec6f21 | |||
| a0d8f3ae81 | |||
| 6d37cceb91 | |||
| fce41f4fc1 | |||
| c50e894a42 | |||
| 890fd78a6a | |||
| 518cae1476 | |||
| 545a105ba0 | |||
| 70b4bf779e | |||
| 7cf672da84 | |||
| fd5746a954 | |||
| 2c3813deb9 | |||
| 80f57a0292 | |||
| 3b7309d9f7 | |||
| 6df1e68a5f | |||
| df2e982090 | |||
| 902af5e5d7 | |||
| 7fe23c7bc5 | |||
| d0c3cb066c | |||
| 5666943559 | |||
| 1b41f61247 | |||
| e1342f06b7 | |||
| f535595d1f | |||
| 7415776e4d | |||
| bad7caa187 | |||
| 2473eee66b | |||
| a8b51ad619 | |||
| 49c4d45731 | |||
| f45fef29d8 | |||
| 699a995e8c | |||
| ce02b03a73 | |||
| 3e1b01073b | |||
| 3e4dce2413 | |||
| fef3091ecc | |||
| af7509ebaf | |||
| 487527f5a5 | |||
| bfd26560b1 | |||
| be3a1c5b5f | |||
| 1282b23c57 | |||
| 54afd0b50b | |||
| 0669cfbebf | |||
| d99bf122ea | |||
| 6ee748a87a | |||
| 0ebda76cc8 | |||
| ed5581d1d9 | |||
| 71c59cfe50 | |||
| 68b3dc6f37 | |||
| 3c83102e91 | |||
| 0e49a066ba | |||
| fcb786cf60 | |||
| c56b2cdd62 | |||
| 6309d323dc | |||
| db2937d4a3 | |||
| fe10a7e55f | |||
| dd77c3e1f0 | |||
| e5e1f6bd05 | |||
| c52f3ebdd6 | |||
| ac96b64c64 | |||
| fa8fd60ac6 | |||
| 9c9393e0cf | |||
| 47f32a5f55 | |||
| 8b26f9309f | |||
| 4027809f32 | |||
| 60250a32c2 | |||
| 6a4c73db03 | |||
| b28ffa9543 | |||
| 7836f2e47f | |||
| 648873978c | |||
| c51c13b4b8 | |||
| 9a7c7d2a4b | |||
| 5f17474ff4 | |||
| 3376bf8b99 | |||
| ad1bea088e | |||
| fa580c516e | |||
| 7f4c450553 | |||
| 761ff7ed5a | |||
| 117d767f05 | |||
| 8405bfe6f9 | |||
| ccdb1479f7 | |||
| c8f68f44af | |||
| 3954a555f8 | |||
| b6934922fa | |||
| 944e6f5569 | |||
| d51b36e80d | |||
| c9724e2024 | |||
| 830e476120 | |||
| fe2e372997 | |||
| a15deedf0d | |||
| 22bf8163cd | |||
| b8390331af | |||
| 47b740ff35 | |||
| 39c14e6556 | |||
| 57cd791348 | |||
| 3c612e284e | |||
| 8cd1ab5c8f | |||
| a6c22cadb8 | |||
| a628ecf72b | |||
| 8d70233d83 | |||
| ae89600201 | |||
| 934d43b525 | |||
| 858c04bacf | |||
| 2a5355b1f8 | |||
| 5cf2ac4c3e | |||
| 71173da5ad | |||
| e304f4f34f | |||
| 7d37f645ba | |||
| c50738005d | |||
| effff6f88d | |||
| 45b223a2ef | |||
| 90544ba713 | |||
| e55c2e9598 | |||
| 6ee52474e1 | |||
| 4bf9f0b96c | |||
| 79e2fa89df | |||
| 2ad0ded73f | |||
| 1ab05e5c3b | |||
| 4b4a1644ff | |||
| 7fd0ec8ce6 | |||
| 2d1e08b50e | |||
| b881c52118 | |||
| 6fb59949a2 | |||
| 7d41dc21c1 | |||
| 6365968dc3 | |||
| 1abb3c8c22 | |||
| 33f4bb45d1 | |||
| 4897994b35 | |||
| 0a773c82af | |||
| b34d970076 | |||
| 19cf781431 | |||
| 637e65e5a0 | |||
| b3f83fd363 | |||
| 02ac3a6814 | |||
| 97891d36ab | |||
| 65c87d5e0f | |||
| ae3b53540e | |||
| 0e9009b0de | |||
| be2864c34b | |||
| c9bdac2e03 | |||
| d0ac99fc69 | |||
| 040de3d973 | |||
| 1703380ebc | |||
| e935885cd3 | |||
| da809bb9d7 | |||
| c39c9aa1da | |||
| ad61662cc4 | |||
| e42bcd0115 | |||
| ad8c025393 | |||
| 1b0db3c8b0 | |||
| f9a8c1969c | |||
| 645c11f0bd | |||
| ece49a158e | |||
| b139b8fdd6 | |||
| b14aa4f0dc | |||
| 9b392a22e1 | |||
| 36547a7343 | |||
| 876390aa68 | |||
| 297ecfbae3 | |||
| eeb0012e7f | |||
| 35cf82f11c | |||
| 82f6c2c550 | |||
| 3e3988a67f | |||
| 2f4694dc95 | |||
| f072dab07b | |||
| fc02e6f4a5 | |||
| 0651a09a3c | |||
| 2c5f1e0417 | |||
| c9682ca64d | |||
| bceb024588 | |||
| 17bba4d4a2 | |||
| 485448cbc7 | |||
| 244dad447b | |||
| 22e63a7367 | |||
| 83907132b5 | |||
| 7dc9beb171 | |||
| 0664e46a4b | |||
| 2ca97a42c5 | |||
| 773e415dff | |||
| 9e673559c4 | |||
| 879ef603fe | |||
| 7e0a163f12 | |||
| 8e4088e08f | |||
| 59161c663b | |||
| 93252fc5d2 | |||
| e4b8d1807d | |||
| 33e0ccdd10 | |||
| d59139a2ab | |||
| df831833b1 | |||
| c065db6da1 | |||
| a55be809f3 | |||
| a9e1ebc0a8 | |||
| 55af09a350 | |||
| 199fdd6728 | |||
| 4035e91672 | |||
| bc9194d740 | |||
| f601c47218 | |||
| 066d559377 | |||
| 2c3219ffcb | |||
| cf88bf9c23 | |||
| b8303b9a22 | |||
| a3f084dcde | |||
| 0d6b8fc6fc | |||
| 261a936bb8 | |||
| 159d9425a7 | |||
| 3a50b3678d | |||
| 6fa352f407 | |||
| 4b80b2c233 | |||
| d881755503 | |||
| fd125ecc68 | |||
| 29f7f1a57d | |||
| 8ecaabfce9 | |||
| 1797ff67c0 | |||
| f1ba5e95ec | |||
| d8c0f9d1d9 | |||
| df4b5fc87d | |||
| d7cdc8b3b0 | |||
| 5b53ca7cf1 | |||
| 194d1dae51 | |||
| 61322ede6c | |||
| a8edaedc8b | |||
| 25145f72e5 | |||
| 3ddf8b5922 | |||
| dbe9e4aade | |||
| 715be4dad0 | |||
| 570b7d0d97 | |||
| 80ac0ab17f | |||
| 9ee8174d5f | |||
| 831aa03c9f | |||
| d372597bdb | |||
| 172437b6fc | |||
| 7640a42bfc | |||
| fde04bd625 | |||
| ad14a5ccba | |||
| 2348d12e9d | |||
| 5cafc05e13 | |||
| e982257271 | |||
| 340fd81778 | |||
| 223f94077f | |||
| f13aa21d0f | |||
| 2c34a17d88 | |||
| 6b005a666e | |||
| 1d1bcb0a63 | |||
| 3f5f1328e7 | |||
| 23e8f7e0aa | |||
| c81caa4d2c | |||
| 13dd3084c2 | |||
| e1021a96af | |||
| 5b0781253f | |||
| a04b7eed28 | |||
| c47427633c | |||
| 56e2c6650d | |||
| 82f0fb8a79 | |||
| 0e5b293b1f | |||
| 2b69eb2fd0 | |||
| ac798d9d6d | |||
| e46fc13fea | |||
| bce0b4a8a0 | |||
| 562046c278 | |||
| 4cc28977cb | |||
| 3ce4624aee | |||
| 2b8ced9c59 |
+69
-13
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with: { go-version: '1.22' }
|
||||
with: { go-version: '1.25' }
|
||||
|
||||
- name: Build go2rtc_win64
|
||||
env: { GOOS: windows, GOARCH: amd64 }
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||
|
||||
- name: Build go2rtc_win32
|
||||
env: { GOOS: windows, GOARCH: 386, GOTOOLCHAIN: go1.20.14 }
|
||||
env: { GOOS: windows, GOARCH: 386 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_win32
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_mac_amd64
|
||||
env: { GOOS: darwin, GOARCH: amd64, GOTOOLCHAIN: go1.20.14 }
|
||||
env: { GOOS: darwin, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_mac_amd64
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -102,14 +102,14 @@ jobs:
|
||||
env: { GOOS: freebsd, GOARCH: amd64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_amd64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_freebsd_amd64, path: go2rtc }
|
||||
|
||||
- name: Build go2rtc_freebsd_arm64
|
||||
env: { GOOS: freebsd, GOARCH: arm64 }
|
||||
run: go build -ldflags "-s -w" -trimpath
|
||||
- name: Upload go2rtc_freebsd_arm64
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
|
||||
|
||||
docker-master:
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
@@ -138,14 +138,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -156,6 +156,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/386
|
||||
@@ -180,7 +181,7 @@ jobs:
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ github.repository }}
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-hardware,onlatest=true
|
||||
@@ -197,14 +198,14 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -215,10 +216,65 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
file: docker/hardware.Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-hw.outputs.tags }}
|
||||
labels: ${{ steps.meta-hw.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
docker-rockchip:
|
||||
name: Build docker rockchip
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
id: meta-rk
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||
ghcr.io/${{ github.repository }}
|
||||
flavor: |
|
||||
suffix=-rockchip,onlatest=true
|
||||
latest=auto
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}},enable=false
|
||||
type=match,pattern=v(.*),group=1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: github.event_name == 'push' && github.event.repository.fork == false
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/rockchip.Dockerfile
|
||||
platforms: linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta-rk.outputs.tags }}
|
||||
labels: ${{ steps.meta-rk.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -17,21 +17,37 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false
|
||||
- name: Install dependencies
|
||||
run: npm install --no-package-lock
|
||||
- name: Build docs
|
||||
run: npm run docs:build
|
||||
- name: Copy docs into website
|
||||
run: rsync -a --exclude '.vitepress/' --exclude 'README.md' website/ website/.vitepress/dist/
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: website/.vitepress/dist
|
||||
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './website'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
go-version: '1.24'
|
||||
|
||||
- name: Build Go binary
|
||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||
@@ -79,6 +79,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/${{ matrix.platform }}
|
||||
push: false
|
||||
load: true
|
||||
@@ -92,7 +93,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: hardware.Dockerfile
|
||||
file: docker/hardware.Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
|
||||
+11
@@ -4,10 +4,21 @@
|
||||
go2rtc.yaml
|
||||
go2rtc.json
|
||||
|
||||
go2rtc_freebsd*
|
||||
go2rtc_linux*
|
||||
go2rtc_mac*
|
||||
go2rtc_win*
|
||||
|
||||
/go2rtc
|
||||
/go2rtc.exe
|
||||
|
||||
0_test.go
|
||||
|
||||
.DS_Store
|
||||
|
||||
website/.vitepress/cache
|
||||
website/.vitepress/dist
|
||||
|
||||
node_modules
|
||||
package-lock.json
|
||||
CLAUDE.md
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
# API
|
||||
|
||||
Fill free to make any API design proposals.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Interactive [OpenAPI](https://alexxit.github.io/go2rtc/api/).
|
||||
|
||||
`www/stream.html` - universal viewer with support params in URL:
|
||||
|
||||
- multiple streams on page `src=camera1&src=camera2...`
|
||||
- stream technology autoselection `mode=webrtc,webrtc/tcp,mse,hls,mp4,mjpeg`
|
||||
- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`
|
||||
- player width setting in pixels `width=320px` or percents `width=50%`
|
||||
|
||||
`www/webrtc.html` - WebRTC viewer with support two way audio and params in URL:
|
||||
|
||||
- `media=video+audio` - simple viewer
|
||||
- `media=video+audio+microphone` - two way audio from camera
|
||||
- `media=camera+microphone` - stream from browser
|
||||
- `media=display+speaker` - stream from desktop
|
||||
|
||||
## JavaScript API
|
||||
|
||||
- You can write your viewer from the scratch
|
||||
- You can extend the built-in viewer - `www/video-rtc.js`
|
||||
- Check example - `www/video-stream.js`
|
||||
- Check example - https://github.com/AlexxIT/WebRTC
|
||||
|
||||
`video-rtc.js` features:
|
||||
|
||||
- support technologies:
|
||||
- WebRTC over UDP or TCP
|
||||
- MSE or HLS or MP4 or MJPEG over WebSocket
|
||||
- automatic selection best technology according on:
|
||||
- codecs inside your stream
|
||||
- current browser capabilities
|
||||
- current network configuration
|
||||
- automatic stop stream while browser or page not active
|
||||
- automatic stop stream while player not inside page viewport
|
||||
- automatic reconnection
|
||||
|
||||
Technology selection based on priorities:
|
||||
|
||||
1. Video and Audio better than just Video
|
||||
2. H265 better than H264
|
||||
3. WebRTC better than MSE, than HLS, than MJPEG
|
||||
|
||||
## WebSocket API
|
||||
|
||||
Endpoint: `/api/ws`
|
||||
|
||||
Query parameters:
|
||||
|
||||
- `src` (required) - Stream name
|
||||
|
||||
### WebRTC
|
||||
|
||||
Request SDP:
|
||||
|
||||
```json
|
||||
{"type":"webrtc/offer","value":"v=0\r\n..."}
|
||||
```
|
||||
|
||||
Response SDP:
|
||||
|
||||
```json
|
||||
{"type":"webrtc/answer","value":"v=0\r\n..."}
|
||||
```
|
||||
|
||||
Request/response candidate:
|
||||
|
||||
- empty value also allowed and optional
|
||||
|
||||
```json
|
||||
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
|
||||
```
|
||||
|
||||
### MSE
|
||||
|
||||
Request:
|
||||
|
||||
- codecs list optional
|
||||
|
||||
```json
|
||||
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
|
||||
```
|
||||
|
||||
### HLS
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
|
||||
|
||||
```json
|
||||
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
|
||||
```
|
||||
|
||||
### MJPEG
|
||||
|
||||
Request/response:
|
||||
|
||||
```json
|
||||
{"type":"mjpeg"}
|
||||
```
|
||||
@@ -1,537 +0,0 @@
|
||||
openapi: 3.1.0
|
||||
|
||||
info:
|
||||
title: go2rtc
|
||||
license: { name: MIT,url: https://opensource.org/licenses/MIT }
|
||||
version: 1.0.0
|
||||
contact: { url: https://github.com/AlexxIT/go2rtc }
|
||||
description: |
|
||||
Ultimate camera streaming application with support RTSP, RTMP, HTTP-FLV, WebRTC, MSE, HLS, MP4, MJPEG, HomeKit, FFmpeg, etc.
|
||||
|
||||
servers:
|
||||
- url: http://localhost:1984
|
||||
|
||||
components:
|
||||
parameters:
|
||||
stream_src_path:
|
||||
name: src
|
||||
in: path
|
||||
description: Source stream name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
stream_dst_path:
|
||||
name: dst
|
||||
in: path
|
||||
description: Destination stream name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
stream_src_query:
|
||||
name: src
|
||||
in: query
|
||||
description: Source stream name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
mp4_filter:
|
||||
name: mp4
|
||||
in: query
|
||||
description: MP4 codecs filter
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
enum: [ "", flac, all ]
|
||||
example: flac
|
||||
video_filter:
|
||||
name: video
|
||||
in: query
|
||||
description: Video codecs filter
|
||||
schema:
|
||||
type: string
|
||||
enum: [ "", all, h264, h265, mjpeg ]
|
||||
example: h264,h265
|
||||
audio_filter:
|
||||
name: audio
|
||||
in: query
|
||||
description: Audio codecs filter
|
||||
schema:
|
||||
type: string
|
||||
enum: [ "", all, aac, opus, pcm, pcmu, pcma ]
|
||||
example: aac
|
||||
responses:
|
||||
discovery:
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
example: { streams: [ { "name": "Camera 1","url": "..." } ] }
|
||||
webtorrent:
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
example: { share: AKDypPy4zz, pwd: H0Km1HLTTP }
|
||||
|
||||
tags:
|
||||
- name: Application
|
||||
description: "[Module: API](https://github.com/AlexxIT/go2rtc#module-api)"
|
||||
- name: Config
|
||||
description: "[Configuration](https://github.com/AlexxIT/go2rtc#configuration)"
|
||||
- name: Streams list
|
||||
description: "[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)"
|
||||
- name: Consume stream
|
||||
- name: Snapshot
|
||||
- name: Produce stream
|
||||
- name: Discovery
|
||||
- name: ONVIF
|
||||
- name: RTSPtoWebRTC
|
||||
- name: WebTorrent
|
||||
description: "[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)"
|
||||
- name: Debug
|
||||
|
||||
paths:
|
||||
/api:
|
||||
get:
|
||||
summary: Get application info
|
||||
tags: [ Application ]
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
example: { config_path: "/config/go2rtc.yaml",host: "192.168.1.123:1984",rtsp: { listen: ":8554",default_query: "video&audio" },version: "1.5.0" }
|
||||
|
||||
/api/exit:
|
||||
post:
|
||||
summary: Close application
|
||||
tags: [ Application ]
|
||||
parameters:
|
||||
- name: code
|
||||
in: query
|
||||
description: Application exit code
|
||||
required: false
|
||||
schema: { type: integer }
|
||||
example: 100
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/restart:
|
||||
post:
|
||||
summary: Restart Daemon
|
||||
description: Restarts the daemon.
|
||||
tags: [ Application ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/config:
|
||||
get:
|
||||
summary: Get main config file content
|
||||
tags: [ Config ]
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/yaml: { example: "streams:..." }
|
||||
post:
|
||||
summary: Rewrite main config file
|
||||
tags: [ Config ]
|
||||
requestBody:
|
||||
content:
|
||||
"*/*": { example: "streams:..." }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
patch:
|
||||
summary: Merge changes to main config file
|
||||
tags: [ Config ]
|
||||
requestBody:
|
||||
content:
|
||||
"*/*": { example: "streams:..." }
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/api/streams:
|
||||
get:
|
||||
summary: Get all streams info
|
||||
tags: [ Streams list ]
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json: { example: { camera1: { producers: [ ],consumers: [ ] } } }
|
||||
put:
|
||||
summary: Create new stream
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (URI)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
|
||||
- name: name
|
||||
in: query
|
||||
description: Stream name
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
patch:
|
||||
summary: Update stream source
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (URI)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0"
|
||||
- name: name
|
||||
in: query
|
||||
description: Stream name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
delete:
|
||||
summary: Delete stream
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
post:
|
||||
summary: Send stream from source to destination
|
||||
description: "[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)"
|
||||
tags: [ Streams list ]
|
||||
parameters:
|
||||
- name: src
|
||||
in: query
|
||||
description: Stream source (URI)
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: "ffmpeg:http://example.com/song.mp3#audio=pcma#input=file"
|
||||
- name: dst
|
||||
in: query
|
||||
description: Destination stream name
|
||||
required: true
|
||||
schema: { type: string }
|
||||
example: camera1
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/api/streams?src={src}:
|
||||
get:
|
||||
summary: Get stream info in JSON format
|
||||
tags: [ Consume stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
"200":
|
||||
description: ""
|
||||
content:
|
||||
application/json:
|
||||
example: { producers: [ { url: "rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0" } ], consumers: [ ] }
|
||||
|
||||
/api/webrtc?src={src}:
|
||||
post:
|
||||
summary: Get stream in WebRTC format (WHEP)
|
||||
description: "[Module: WebRTC](https://github.com/AlexxIT/go2rtc#module-webrtc)"
|
||||
tags: [ Consume stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
requestBody:
|
||||
description: |
|
||||
Support:
|
||||
- JSON format (`Content-Type: application/json`)
|
||||
- WHEP standard (`Content-Type: application/sdp`)
|
||||
- raw SDP (`Content-Type: anything`)
|
||||
required: true
|
||||
content:
|
||||
application/json: { example: { type: offer, sdp: "v=0..." } }
|
||||
"application/sdp": { example: "v=0..." }
|
||||
"*/*": { example: "v=0..." }
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: "Response on JSON or raw SDP"
|
||||
content:
|
||||
application/json: { example: { type: answer, sdp: "v=0..." } }
|
||||
application/sdp: { example: "v=0..." }
|
||||
"201":
|
||||
description: "Response on `Content-Type: application/sdp`"
|
||||
content:
|
||||
application/sdp: { example: "v=0..." }
|
||||
|
||||
/api/stream.mp4?src={src}:
|
||||
get:
|
||||
summary: Get stream in MP4 format (HTTP progressive)
|
||||
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
|
||||
tags: [ Consume stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
- name: duration
|
||||
in: query
|
||||
description: Limit the length of the stream in seconds
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: 15
|
||||
- name: filename
|
||||
in: query
|
||||
description: Download as a file with this name
|
||||
required: false
|
||||
schema: { type: string }
|
||||
example: camera1.mp4
|
||||
- $ref: "#/components/parameters/mp4_filter"
|
||||
- $ref: "#/components/parameters/video_filter"
|
||||
- $ref: "#/components/parameters/audio_filter"
|
||||
responses:
|
||||
200:
|
||||
description: ""
|
||||
content: { video/mp4: { example: "" } }
|
||||
|
||||
/api/stream.m3u8?src={src}:
|
||||
get:
|
||||
summary: Get stream in HLS format
|
||||
description: "[Module: HLS](https://github.com/AlexxIT/go2rtc#module-hls)"
|
||||
tags: [ Consume stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
- $ref: "#/components/parameters/mp4_filter"
|
||||
- $ref: "#/components/parameters/video_filter"
|
||||
- $ref: "#/components/parameters/audio_filter"
|
||||
responses:
|
||||
200:
|
||||
description: ""
|
||||
content: { application/vnd.apple.mpegurl: { example: "" } }
|
||||
|
||||
/api/stream.mjpeg?src={src}:
|
||||
get:
|
||||
summary: Get stream in MJPEG format
|
||||
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
|
||||
tags: [ Consume stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
200:
|
||||
description: ""
|
||||
content: { multipart/x-mixed-replace: { example: "" } }
|
||||
|
||||
|
||||
|
||||
/api/frame.jpeg?src={src}:
|
||||
get:
|
||||
summary: Get snapshot in JPEG format
|
||||
description: "[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)"
|
||||
tags: [ Snapshot ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
200:
|
||||
description: ""
|
||||
content: { image/jpeg: { example: "" } }
|
||||
/api/frame.mp4?src={src}:
|
||||
get:
|
||||
summary: Get snapshot in MP4 format
|
||||
description: "[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)"
|
||||
tags: [ Snapshot ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
200:
|
||||
description: ""
|
||||
content: { video/mp4: { example: "" } }
|
||||
|
||||
|
||||
|
||||
/api/webrtc?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in WebRTC format
|
||||
description: "[Incoming: WebRTC/WHIP](https://github.com/AlexxIT/go2rtc#incoming-webrtcwhip)"
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.flv?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in FLV format
|
||||
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.ts?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in MPEG-TS format
|
||||
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/stream.mjpeg?dst={dst}:
|
||||
post:
|
||||
summary: Post stream in MJPEG format
|
||||
description: "[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)"
|
||||
tags: [ Produce stream ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_dst_path"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/api/dvrip:
|
||||
get:
|
||||
summary: DVRIP cameras discovery
|
||||
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/ffmpeg/devices:
|
||||
get:
|
||||
summary: FFmpeg USB devices discovery
|
||||
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/ffmpeg/hardware:
|
||||
get:
|
||||
summary: FFmpeg hardware transcoding discovery
|
||||
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/hass:
|
||||
get:
|
||||
summary: Home Assistant cameras discovery
|
||||
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/homekit:
|
||||
get:
|
||||
summary: HomeKit cameras discovery
|
||||
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/nest:
|
||||
get:
|
||||
summary: Nest cameras discovery
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/onvif:
|
||||
get:
|
||||
summary: ONVIF cameras discovery
|
||||
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
/api/roborock:
|
||||
get:
|
||||
summary: Roborock vacuums discovery
|
||||
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
|
||||
tags: [ Discovery ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/onvif/:
|
||||
get:
|
||||
summary: ONVIF server implementation
|
||||
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
|
||||
tags: [ ONVIF ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/stream/:
|
||||
get:
|
||||
summary: RTSPtoWebRTC server implementation
|
||||
description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration
|
||||
tags: [ RTSPtoWebRTC ]
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
|
||||
|
||||
/api/webtorrent?src={src}:
|
||||
get:
|
||||
summary: Get WebTorrent share info
|
||||
tags: [ WebTorrent ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
200: { $ref: "#/components/responses/webtorrent" }
|
||||
post:
|
||||
summary: Add WebTorrent share
|
||||
tags: [ WebTorrent ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
200: { $ref: "#/components/responses/webtorrent" }
|
||||
|
||||
delete:
|
||||
summary: Delete WebTorrent share
|
||||
tags: [ WebTorrent ]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/stream_src_path"
|
||||
responses:
|
||||
default:
|
||||
description: Default response
|
||||
|
||||
/api/webtorrent:
|
||||
get:
|
||||
summary: Get all WebTorrent shares info
|
||||
tags: [ WebTorrent ]
|
||||
responses:
|
||||
200: { $ref: "#/components/responses/discovery" }
|
||||
|
||||
|
||||
|
||||
/api/stack:
|
||||
get:
|
||||
summary: Show list unknown goroutines
|
||||
tags: [ Debug ]
|
||||
responses:
|
||||
200:
|
||||
description: ""
|
||||
content: { text/plain: { example: "" } }
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,20 +1,11 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.11"
|
||||
ARG GO_VERSION="1.22"
|
||||
ARG PYTHON_VERSION="3.13"
|
||||
ARG GO_VERSION="1.25"
|
||||
|
||||
|
||||
# 1. Download ngrok binary (for support arm/v6)
|
||||
FROM alpine AS ngrok
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
ADD https://bin.equinox.io/c/bNyj1mQVY4c/ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz /
|
||||
RUN tar -xzf /ngrok-v3-stable-${TARGETOS}-${TARGETARCH}.tgz -C /bin
|
||||
|
||||
|
||||
# 2. Build go2rtc binary
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
@@ -35,14 +26,14 @@ COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 3. Final image
|
||||
# 2. Final image
|
||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||
# font-droid for FFmpeg drawtext filter (+2MB)
|
||||
RUN apk add --no-cache tini ffmpeg bash curl jq alsa-plugins-pulse font-droid
|
||||
RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
|
||||
|
||||
# Hardware Acceleration for Intel CPU (+50MB)
|
||||
ARG TARGETARCH
|
||||
@@ -55,8 +46,8 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
EXPOSE 1984 8554 8555 8555/udp
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
@@ -0,0 +1,54 @@
|
||||
# Docker
|
||||
|
||||
Images are built automatically via [GitHub actions](https://github.com/AlexxIT/go2rtc/actions) and published on [Docker Hub](https://hub.docker.com/r/alexxit/go2rtc) and [GitHub](https://github.com/AlexxIT/go2rtc/pkgs/container/go2rtc).
|
||||
|
||||
## Versions
|
||||
|
||||
- `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support for hardware transcoding for Intel iGPU and Raspberry
|
||||
- `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support for hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU
|
||||
- `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support for hardware transcoding for Rockchip RK35xx
|
||||
- `alexxit/go2rtc:master` - latest unstable version based on `alpine`
|
||||
- `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`)
|
||||
- `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`)
|
||||
|
||||
## Docker compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
go2rtc:
|
||||
image: alexxit/go2rtc
|
||||
network_mode: host # important for WebRTC, HomeKit, UDP cameras
|
||||
privileged: true # only for FFmpeg hardware transcoding
|
||||
restart: unless-stopped # autorestart on fail or config change from WebUI
|
||||
environment:
|
||||
- TZ=Atlantic/Bermuda # timezone in logs
|
||||
volumes:
|
||||
- "~/go2rtc:/config" # folder for go2rtc.yaml file (edit from WebUI)
|
||||
```
|
||||
|
||||
## Basic Deployment
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name go2rtc \
|
||||
--network host \
|
||||
--privileged \
|
||||
--restart unless-stopped \
|
||||
-e TZ=Atlantic/Bermuda \
|
||||
-v ~/go2rtc:/config \
|
||||
alexxit/go2rtc
|
||||
```
|
||||
|
||||
## Deployment with GPU Acceleration
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name go2rtc \
|
||||
--network host \
|
||||
--privileged \
|
||||
--restart unless-stopped \
|
||||
-e TZ=Atlantic/Bermuda \
|
||||
--gpus all \
|
||||
-v ~/go2rtc:/config \
|
||||
alexxit/go2rtc:latest-hardware
|
||||
```
|
||||
@@ -4,16 +4,11 @@
|
||||
# only debian 13 (trixie) has latest ffmpeg
|
||||
# https://packages.debian.org/trixie/ffmpeg
|
||||
ARG DEBIAN_VERSION="trixie-slim"
|
||||
ARG GO_VERSION="1.22-bookworm"
|
||||
ARG NGROK_VERSION="3"
|
||||
|
||||
FROM debian:${DEBIAN_VERSION} AS base
|
||||
FROM golang:${GO_VERSION} AS go
|
||||
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
|
||||
ARG GO_VERSION="1.25-bookworm"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM go AS build
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
@@ -31,35 +26,30 @@ COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Collect all files
|
||||
FROM scratch AS rootfs
|
||||
# 2. Final image
|
||||
FROM debian:${DEBIAN_VERSION}
|
||||
|
||||
COPY --link --from=build /build/go2rtc /usr/local/bin/
|
||||
COPY --link --from=ngrok /bin/ngrok /usr/local/bin/
|
||||
|
||||
# 3. Final image
|
||||
FROM base
|
||||
# Prepare apt for buildkit cache
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
|
||||
# Install ffmpeg, bash (for run.sh), tini (for signal handling),
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||
# mesa-va-drivers for AMD APU
|
||||
# libasound2-plugins for ALSA support
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
echo 'deb http://deb.debian.org/debian trixie non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||
apt-get -y update && apt-get -y install tini ffmpeg \
|
||||
apt-get -y update && apt-get -y install ffmpeg tini \
|
||||
python3 curl jq \
|
||||
intel-media-va-driver-non-free \
|
||||
mesa-va-drivers \
|
||||
libasound2-plugins && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --link --from=rootfs / /
|
||||
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
|
||||
EXPOSE 1984 8554 8555 8555/udp
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
@@ -0,0 +1,51 @@
|
||||
# syntax=docker/dockerfile:labs
|
||||
|
||||
# 0. Prepare images
|
||||
ARG PYTHON_VERSION="3.13-slim-bookworm"
|
||||
ARG GO_VERSION="1.25-bookworm"
|
||||
|
||||
|
||||
# 1. Build go2rtc binary
|
||||
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GOOS=${TARGETOS}
|
||||
ENV GOARCH=${TARGETARCH}
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Cache dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||
|
||||
COPY . .
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||
|
||||
|
||||
# 2. Final image
|
||||
FROM python:${PYTHON_VERSION}
|
||||
|
||||
# Prepare apt for buildkit cache
|
||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
||||
&& echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' >/etc/apt/apt.conf.d/keep-cache
|
||||
|
||||
# Install ffmpeg, tini (for signal handling),
|
||||
# and other common tools for the echo source.
|
||||
# libasound2-plugins for ALSA support
|
||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||
apt-get -y update && apt-get -y install tini \
|
||||
curl jq \
|
||||
libasound2-plugins && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||
ADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin
|
||||
|
||||
EXPOSE 1984 8554 8555 8555/udp
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
VOLUME /config
|
||||
WORKDIR /config
|
||||
|
||||
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||
@@ -0,0 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/mjpeg"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/internal/v4l2"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app.Init()
|
||||
streams.Init()
|
||||
|
||||
api.Init()
|
||||
ws.Init()
|
||||
|
||||
ffmpeg.Init()
|
||||
mjpeg.Init()
|
||||
v4l2.Init()
|
||||
|
||||
shell.RunUntilSignal()
|
||||
}
|
||||
@@ -54,7 +54,7 @@ var chars = map[string]string{
|
||||
"21C": "Third Party Camera Active",
|
||||
"21D": "Camera Operating Mode Indicator",
|
||||
"11B": "Night Vision",
|
||||
"129": "Supported Data Stream Transport Configuration",
|
||||
//"129": "Supported Data Stream Transport Configuration",
|
||||
"37": "Version",
|
||||
"131": "Setup Data Stream Transport",
|
||||
"130": "Supported Data Stream Transport Configuration",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module pinggy
|
||||
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 // indirect
|
||||
golang.org/x/crypto v0.8.0 // indirect
|
||||
golang.org/x/sys v0.7.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 h1:lzZ00JK6BUGQXnpkJZ+cVj8kIkXsmiVBUci9uEkSwEY=
|
||||
github.com/Pinggy-io/pinggy-go/pinggy v0.6.9/go.mod h1:V1Sxb+4zyr36o9atZiqtT4XhsKtW1RSb2GvsbTbTJYw=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/Pinggy-io/pinggy-go/pinggy"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tunType := os.Args[1]
|
||||
address := os.Args[2]
|
||||
|
||||
log.SetFlags(log.Llongfile | log.LstdFlags)
|
||||
|
||||
config := pinggy.Config{
|
||||
Type: pinggy.TunnelType(tunType),
|
||||
TcpForwardingAddr: address,
|
||||
|
||||
//SshOverSsl: true,
|
||||
//Stdout: os.Stderr,
|
||||
//Stderr: os.Stderr,
|
||||
}
|
||||
|
||||
if tunType == "http" {
|
||||
hman := pinggy.CreateHeaderManipulationAndAuthConfig()
|
||||
//hman.SetReverseProxy(address)
|
||||
//hman.SetPassPreflight(true)
|
||||
//hman.SetNoReverseProxy()
|
||||
config.HeaderManipulationAndAuth = hman
|
||||
}
|
||||
|
||||
pl, err := pinggy.ConnectWithConfig(config)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
log.Println("Addrs: ", pl.RemoteUrls())
|
||||
//err = pl.InitiateWebDebug("localhost:3424")
|
||||
//log.Println(err)
|
||||
pl.StartForwarding()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
## ONVIF Client
|
||||
|
||||
```shell
|
||||
go run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations
|
||||
```
|
||||
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/onvif"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var rawURL = os.Args[1]
|
||||
var operation = os.Args[2]
|
||||
var token string
|
||||
if len(os.Args) > 3 {
|
||||
token = os.Args[3]
|
||||
}
|
||||
|
||||
client, err := onvif.NewClient(rawURL)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
switch operation {
|
||||
case onvif.ServiceGetServiceCapabilities:
|
||||
b, err = client.MediaRequest(operation)
|
||||
case onvif.DeviceGetCapabilities,
|
||||
onvif.DeviceGetDeviceInformation,
|
||||
onvif.DeviceGetDiscoveryMode,
|
||||
onvif.DeviceGetDNS,
|
||||
onvif.DeviceGetHostname,
|
||||
onvif.DeviceGetNetworkDefaultGateway,
|
||||
onvif.DeviceGetNetworkInterfaces,
|
||||
onvif.DeviceGetNetworkProtocols,
|
||||
onvif.DeviceGetNTP,
|
||||
onvif.DeviceGetScopes,
|
||||
onvif.DeviceGetServices,
|
||||
onvif.DeviceGetSystemDateAndTime,
|
||||
onvif.DeviceSystemReboot:
|
||||
b, err = client.DeviceRequest(operation)
|
||||
case onvif.MediaGetProfiles,
|
||||
onvif.MediaGetVideoEncoderConfigurations,
|
||||
onvif.MediaGetVideoSources,
|
||||
onvif.MediaGetVideoSourceConfigurations,
|
||||
onvif.MediaGetAudioEncoderConfigurations,
|
||||
onvif.MediaGetAudioSources,
|
||||
onvif.MediaGetAudioSourceConfigurations:
|
||||
b, err = client.MediaRequest(operation)
|
||||
case onvif.MediaGetProfile:
|
||||
b, err = client.GetProfile(token)
|
||||
case onvif.MediaGetVideoSourceConfiguration:
|
||||
b, err = client.GetVideoSourceConfiguration(token)
|
||||
case onvif.MediaGetStreamUri:
|
||||
b, err = client.GetStreamUri(token)
|
||||
case onvif.MediaGetSnapshotUri:
|
||||
b, err = client.GetSnapshotUri(token)
|
||||
default:
|
||||
log.Printf("unknown action\n")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err = os.WriteFile(u.Hostname()+"_"+operation+".xml", b, 0644); err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# tutk_decoder
|
||||
|
||||
1. Wireshark > Select any packet > Follow > UDP Stream
|
||||
2. Wireshark > File > Export Packet Dissections > As JSON > Displayed, Values
|
||||
3. `tutk_decoder wireshark.json decoded.txt`
|
||||
@@ -0,0 +1,82 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/tutk"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Println("Usage: tutk_decoder wireshark.json decoded.txt")
|
||||
return
|
||||
}
|
||||
|
||||
src, err := os.Open(os.Args[1])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
var items []item
|
||||
if err = json.NewDecoder(src).Decode(&items); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
for _, v := range items {
|
||||
if v.Source.Layers.Data.DataData == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
s := strings.ReplaceAll(v.Source.Layers.Data.DataData, ":", "")
|
||||
b, err = hex.DecodeString(s)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tutk.ReverseTransCodePartial(b, b)
|
||||
|
||||
ts := v.Source.Layers.Frame.FrameTimeRelative
|
||||
|
||||
_, _ = fmt.Fprintf(dst, "%8s: %s -> %s [%4d] %x\n",
|
||||
ts[:len(ts)-6],
|
||||
v.Source.Layers.Ip.IpSrc, v.Source.Layers.Ip.IpDst,
|
||||
len(b), b)
|
||||
}
|
||||
}
|
||||
|
||||
type item struct {
|
||||
Source struct {
|
||||
Layers struct {
|
||||
Frame struct {
|
||||
FrameTimeRelative string `json:"frame.time_relative"`
|
||||
FrameNumber string `json:"frame.number"`
|
||||
} `json:"frame"`
|
||||
Ip struct {
|
||||
IpSrc string `json:"ip.src"`
|
||||
IpDst string `json:"ip.dst"`
|
||||
} `json:"ip"`
|
||||
Udp struct {
|
||||
UdpSrcport string `json:"udp.srcport"`
|
||||
UdpDstport string `json:"udp.dstport"`
|
||||
} `json:"udp"`
|
||||
Data struct {
|
||||
DataData string `json:"data.data"`
|
||||
DataLen string `json:"data.len"`
|
||||
} `json:"data"`
|
||||
} `json:"layers"`
|
||||
} `json:"_source"`
|
||||
}
|
||||
@@ -1,49 +1,52 @@
|
||||
module github.com/AlexxIT/go2rtc
|
||||
|
||||
go 1.20
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/expr-lang/expr v1.16.9
|
||||
github.com/asticode/go-astits v1.14.0
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||
github.com/expr-lang/expr v1.17.7
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/pion/ice/v2 v2.3.36
|
||||
github.com/pion/interceptor v0.1.37
|
||||
github.com/pion/rtcp v1.2.14
|
||||
github.com/pion/rtp v1.8.9
|
||||
github.com/pion/sdp/v3 v3.0.9
|
||||
github.com/pion/srtp/v2 v2.0.20
|
||||
github.com/pion/stun v0.6.1
|
||||
github.com/pion/webrtc/v3 v3.3.4
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/miekg/dns v1.1.70
|
||||
github.com/pion/dtls/v3 v3.0.10
|
||||
github.com/pion/ice/v4 v4.2.0
|
||||
github.com/pion/interceptor v0.1.43
|
||||
github.com/pion/rtcp v1.2.16
|
||||
github.com/pion/rtp v1.10.0
|
||||
github.com/pion/sdp/v3 v3.0.17
|
||||
github.com/pion/srtp/v3 v3.0.10
|
||||
github.com/pion/stun/v3 v3.1.1
|
||||
github.com/pion/webrtc/v4 v4.2.3
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/crypto v0.47.0
|
||||
golang.org/x/net v0.49.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asticode/go-astikit v0.45.0 // indirect
|
||||
github.com/asticode/go-astikit v0.57.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/pion/datachannel v1.5.9 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/pion/datachannel v1.6.0 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.33 // indirect
|
||||
github.com/pion/transport/v2 v2.2.10 // indirect
|
||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
||||
github.com/pion/sctp v1.9.2 // indirect
|
||||
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
github.com/pion/turn/v4 v4.1.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/mod v0.18.0 // indirect
|
||||
golang.org/x/net v0.27.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,77 +1,97 @@
|
||||
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astikit v0.45.0 h1:08to/jrbod9tchF2bJ9moW+RTDK7DBUxLdIeSE7v7Sw=
|
||||
github.com/asticode/go-astikit v0.45.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||
github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=
|
||||
github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=
|
||||
github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||
github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=
|
||||
github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
|
||||
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/pion/datachannel v1.5.9 h1:LpIWAOYPyDrXtU+BW7X0Yt/vGtYxtXQ8ql7dFfYUVZA=
|
||||
github.com/pion/datachannel v1.5.9/go.mod h1:kDUuk4CU4Uxp82NH4LQZbISULkX/HtzKa4P7ldf9izE=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
|
||||
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
|
||||
github.com/pion/ice/v2 v2.3.36 h1:SopeXiVbbcooUg2EIR8sq4b13RQ8gzrkkldOVg+bBsc=
|
||||
github.com/pion/ice/v2 v2.3.36/go.mod h1:mBF7lnigdqgtB+YHkaY/Y6s6tsyRyo4u4rPGRuOjUBQ=
|
||||
github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI=
|
||||
github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
||||
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
||||
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||
github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=
|
||||
github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=
|
||||
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE=
|
||||
github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.9 h1:E2HX740TZKaqdcPmf4pw6ZZuG8u5RlMMt+l3dxeu6Wk=
|
||||
github.com/pion/rtp v1.8.9/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.33 h1:dSE4wX6uTJBcNm8+YlMg7lw1wqyKHggsP5uKbdj+NZw=
|
||||
github.com/pion/sctp v1.8.33/go.mod h1:beTnqSzewI53KWoG3nqB282oDMGrhNxBdb+JZnkCwRM=
|
||||
github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY=
|
||||
github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M=
|
||||
github.com/pion/srtp/v2 v2.0.20 h1:HNNny4s+OUmG280ETrCdgFndp4ufx3/uy85EawYEhTk=
|
||||
github.com/pion/srtp/v2 v2.0.20/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
|
||||
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=
|
||||
github.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.3.4 h1:v2heQVnXTSqNRXcaFQVOhIOYkLMxOu1iJG8uy1djvkk=
|
||||
github.com/pion/webrtc/v3 v3.3.4/go.mod h1:liNa+E1iwyzyXqNUwvoMRNQ10x8h8FOeJKL8RkIbamE=
|
||||
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
|
||||
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
|
||||
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
|
||||
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
|
||||
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
|
||||
github.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4=
|
||||
github.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||
@@ -79,97 +99,54 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Modules
|
||||
|
||||
go2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.
|
||||
Some formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.
|
||||
|
||||
- The [`echo`], [`expr`], [`hass`] and [`onvif`] modules receive a link to a stream. They don't know the protocol in advance.
|
||||
- The [`exec`] and [`ffmpeg`] modules support many formats. They are identical to the [`http`] module.
|
||||
- The [`api`], [`app`], [`debug`], [`ngrok`], [`pinggy`], [`srtp`], [`streams`] are supporting modules.
|
||||
|
||||
**Modules** implement communication APIs: authorization, encryption, command set, structure of media packets.
|
||||
|
||||
**Formats** describe the structure of the data being transmitted.
|
||||
|
||||
**Protocols** implement transport for data transmission.
|
||||
|
||||
| module | formats | protocols | input | output | ingest | two-way |
|
||||
|----------------|-----------------|------------------|-------|--------|--------|---------|
|
||||
| [`alsa`] | `pcm` | `ioctl` | yes | | | |
|
||||
| [`bubble`] | - | `http` | yes | | | |
|
||||
| [`doorbird`] | `mulaw` | `http` | yes | | | yes |
|
||||
| [`dvrip`] | - | `tcp` | yes | | | yes |
|
||||
| [`echo`] | * | * | yes | | | |
|
||||
| [`eseecloud`] | `rtp` | `http` | yes | | | |
|
||||
| [`exec`] | * | `pipe`, `rtsp` | yes | | | yes |
|
||||
| [`expr`] | * | * | yes | | | |
|
||||
| [`ffmpeg`] | * | `pipe`, `rtsp` | yes | | | |
|
||||
| [`flussonic`] | `mp4` | `ws` | yes | | | |
|
||||
| [`gopro`] | `mpegts` | `udp` | yes | | | |
|
||||
| [`hass`] | * | * | yes | | | |
|
||||
| [`hls`] | `mpegts`, `mp4` | `http` | | yes | | |
|
||||
| [`homekit`] | `srtp` | `hap` | yes | yes | | no |
|
||||
| [`http`] | `adts` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `flv` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `h264` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `hevc` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `hls` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `mjpeg` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `mpjpeg` | `http` | yes | | | |
|
||||
| [`http`] | `mpegts` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `wav` | `http`, `tcp` | yes | | | |
|
||||
| [`http`] | `yuv4mpegpipe` | `http`, `tcp` | yes | | | |
|
||||
| [`isapi`] | `alaw`, `mulaw` | `http` | | | | yes |
|
||||
| [`ivideon`] | `mp4` | `ws` | yes | | | |
|
||||
| [`kasa`] | `h264`, `mulaw` | `http` | yes | | | |
|
||||
| [`mjpeg`] | `ascii` | `http` | | yes | | |
|
||||
| [`mjpeg`] | `jpeg` | `http` | | yes | | |
|
||||
| [`mjpeg`] | `mpjpeg` | `http` | | yes | yes | |
|
||||
| [`mjpeg`] | `yuv4mpegpipe` | `http` | | yes | | |
|
||||
| [`mp4`] | `mp4` | `http`, `ws` | | yes | | |
|
||||
| [`mpeg`] | `adts` | `http` | | yes | | |
|
||||
| [`mpeg`] | `mpegts` | `http` | | yes | yes | |
|
||||
| [`multitrans`] | `rtp` | `tcp` | | | | yes |
|
||||
| [`nest`] | `srtp` | `rtsp`, `webrtc` | yes | | | no |
|
||||
| [`onvif`] | `rtp` | * | yes | yes | | |
|
||||
| [`ring`] | `srtp` | `webrtc` | yes | | | yes |
|
||||
| [`roborock`] | `srtp` | `webrtc` | yes | | | yes |
|
||||
| [`rtmp`] | `flv` | `rtmp` | yes | yes | yes | |
|
||||
| [`rtmp`] | `flv` | `http` | | yes | yes | |
|
||||
| [`rtsp`] | `rtsp` | `rtsp` | yes | yes | yes | yes |
|
||||
| [`tapo`] | `mpegts` | `http` | yes | | | yes |
|
||||
| [`tuya`] | `srtp` | `webrtc` | yes | | | yes |
|
||||
| [`v4l2`] | `rawvideo` | `ioctl` | yes | | | |
|
||||
| [`webrtc`] | `srtp` | `webrtc` | yes | yes | yes | yes |
|
||||
| [`webtorrent`] | `srtp` | `webrtc` | yes | yes | | |
|
||||
| [`wyoming`] | `pcm` | `tcp` | | yes | | |
|
||||
| [`wyze`] | - | `tutk` | yes | | | yes |
|
||||
| [`xiaomi`] | - | `cs2`, `tutk` | yes | | | yes |
|
||||
| [`yandex`] | `srtp` | `webrtc` | yes | | | |
|
||||
|
||||
[`alsa`]: alsa/README.md
|
||||
[`api`]: api/README.md
|
||||
[`app`]: app/README.md
|
||||
[`bubble`]: bubble/README.md
|
||||
[`debug`]: debug/README.md
|
||||
[`doorbird`]: doorbird/README.md
|
||||
[`dvrip`]: dvrip/README.md
|
||||
[`echo`]: echo/README.md
|
||||
[`eseecloud`]: eseecloud/README.md
|
||||
[`exec`]: exec/README.md
|
||||
[`expr`]: expr/README.md
|
||||
[`ffmpeg`]: ffmpeg/README.md
|
||||
[`flussonic`]: flussonic/README.md
|
||||
[`gopro`]: gopro/README.md
|
||||
[`hass`]: hass/README.md
|
||||
[`hls`]: hls/README.md
|
||||
[`homekit`]: homekit/README.md
|
||||
[`http`]: http/README.md
|
||||
[`isapi`]: isapi/README.md
|
||||
[`ivideon`]: ivideon/README.md
|
||||
[`kasa`]: kasa/README.md
|
||||
[`mjpeg`]: mjpeg/README.md
|
||||
[`mp4`]: mp4/README.md
|
||||
[`mpeg`]: mpeg/README.md
|
||||
[`multitrans`]: multitrans/README.md
|
||||
[`nest`]: nest/README.md
|
||||
[`ngrok`]: ngrok/README.md
|
||||
[`onvif`]: onvif/README.md
|
||||
[`pinggy`]: pinggy/README.md
|
||||
[`ring`]: ring/README.md
|
||||
[`roborock`]: roborock/README.md
|
||||
[`rtmp`]: rtmp/README.md
|
||||
[`rtsp`]: rtsp/README.md
|
||||
[`srtp`]: srtp/README.md
|
||||
[`streams`]: streams/README.md
|
||||
[`tapo`]: tapo/README.md
|
||||
[`tuya`]: tuya/README.md
|
||||
[`v4l2`]: v4l2/README.md
|
||||
[`webrtc`]: webrtc/README.md
|
||||
[`webtorrent`]: webtorrent/README.md
|
||||
[`wyoming`]: wyze/README.md
|
||||
[`wyze`]: wyze/README.md
|
||||
[`xiaomi`]: xiaomi/README.md
|
||||
[`yandex`]: yandex/README.md
|
||||
@@ -0,0 +1,12 @@
|
||||
# ALSA
|
||||
|
||||
[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)
|
||||
|
||||
> [!WARNING]
|
||||
> This source is under development and does not always work well.
|
||||
|
||||
[Advanced Linux Sound Architecture](https://en.wikipedia.org/wiki/Advanced_Linux_Sound_Architecture) - a framework for receiving audio from devices on Linux OS.
|
||||
|
||||
Easy to add via **WebUI > add > ALSA**.
|
||||
|
||||
Alternatively, you can use FFmpeg source.
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))
|
||||
|
||||
package alsa
|
||||
|
||||
func Init() {
|
||||
// not supported
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//go:build linux && (386 || amd64 || arm || arm64 || mipsle)
|
||||
|
||||
package alsa
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa"
|
||||
"github.com/AlexxIT/go2rtc/pkg/alsa/device"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("alsa", alsa.Open)
|
||||
|
||||
api.HandleFunc("api/alsa", apiAlsa)
|
||||
}
|
||||
|
||||
func apiAlsa(w http.ResponseWriter, r *http.Request) {
|
||||
files, err := os.ReadDir("/dev/snd/")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sources []*api.Source
|
||||
|
||||
for _, file := range files {
|
||||
if !strings.HasPrefix(file.Name(), "pcm") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := "/dev/snd/" + file.Name()
|
||||
|
||||
dev, err := device.Open(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := dev.Info()
|
||||
if err == nil {
|
||||
formats := formatsToString(dev.ListFormats())
|
||||
r1, r2 := dev.RangeRates()
|
||||
c1, c2 := dev.RangeChannels()
|
||||
source := &api.Source{
|
||||
Name: info.ID,
|
||||
Info: fmt.Sprintf("Formats: %s, Rates: %d-%d, Channels: %d-%d", formats, r1, r2, c1, c2),
|
||||
URL: "alsa:device?audio=" + path,
|
||||
}
|
||||
if !strings.Contains(source.Name, info.Name) {
|
||||
source.Name += ", " + info.Name
|
||||
}
|
||||
sources = append(sources, source)
|
||||
}
|
||||
|
||||
_ = dev.Close()
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
}
|
||||
|
||||
func formatsToString(formats []byte) string {
|
||||
var s string
|
||||
for i, format := range formats {
|
||||
if i > 0 {
|
||||
s += " "
|
||||
}
|
||||
switch format {
|
||||
case 2:
|
||||
s += "s16le"
|
||||
case 10:
|
||||
s += "s32le"
|
||||
default:
|
||||
s += strconv.Itoa(int(format))
|
||||
}
|
||||
|
||||
}
|
||||
return s
|
||||
}
|
||||
+44
-3
@@ -1,4 +1,45 @@
|
||||
## Exit codes
|
||||
# HTTP API
|
||||
|
||||
- https://tldp.org/LDP/abs/html/exitcodes.html
|
||||
- https://komodor.com/learn/exit-codes-in-containers-and-kubernetes-the-complete-guide/
|
||||
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||
|
||||
The HTTP API is described in [OpenAPI](../../website/api/openapi.yaml) format. It can be explored in [interactive viewer](https://go2rtc.org/api/). WebSocket API described [here](ws/README.md).
|
||||
|
||||
The project's static HTML and JS files are located in the [www](../../www/README.md) folder. An external developer can use them as a basis for integrating go2rtc into their project or for developing a custom web interface for go2rtc.
|
||||
|
||||
The contents of `www` folder are built into go2rtc when building, but you can use configuration to specify an external folder as the source of static files.
|
||||
|
||||
## Configuration
|
||||
|
||||
**Important!** go2rtc passes requests from localhost and Unix sockets without HTTP authorization, even if you have it configured. It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.
|
||||
|
||||
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||
- you can change the API `base_path` and host go2rtc on your main app webserver suburl
|
||||
- all files from `static_dir` hosted on root path: `/`
|
||||
- you can use raw TLS cert/key content or path to files
|
||||
|
||||
```yaml
|
||||
api:
|
||||
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
|
||||
username: "admin" # default "", Basic auth for WebUI
|
||||
password: "pass" # default "", Basic auth for WebUI
|
||||
local_auth: true # default false, Enable auth check for localhost requests
|
||||
base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api)
|
||||
static_dir: "www" # default "", folder for static files (custom web interface)
|
||||
origin: "*" # default "", allow CORS requests (only * supported)
|
||||
tls_listen: ":443" # default "", enable HTTPS server
|
||||
tls_cert: | # default "", PEM-encoded fullchain certificate for HTTPS
|
||||
-----BEGIN CERTIFICATE-----
|
||||
...
|
||||
-----END CERTIFICATE-----
|
||||
tls_key: | # default "", PEM-encoded private key for HTTPS
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
...
|
||||
-----END PRIVATE KEY-----
|
||||
unix_listen: "/tmp/go2rtc.sock" # default "", unix socket listener for API
|
||||
```
|
||||
|
||||
**PS:**
|
||||
|
||||
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
|
||||
- MP4 over WebSocket was created only for Apple iOS because it doesn't support file streaming
|
||||
|
||||
+19
-7
@@ -7,6 +7,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -23,6 +24,7 @@ func Init() {
|
||||
Listen string `yaml:"listen"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
LocalAuth bool `yaml:"local_auth"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
StaticDir string `yaml:"static_dir"`
|
||||
Origin string `yaml:"origin"`
|
||||
@@ -30,6 +32,8 @@ func Init() {
|
||||
TLSCert string `yaml:"tls_cert"`
|
||||
TLSKey string `yaml:"tls_key"`
|
||||
UnixListen string `yaml:"unix_listen"`
|
||||
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"api"`
|
||||
}
|
||||
|
||||
@@ -43,6 +47,7 @@ func Init() {
|
||||
return
|
||||
}
|
||||
|
||||
allowPaths = cfg.Mod.AllowPaths
|
||||
basePath = cfg.Mod.BasePath
|
||||
log = app.GetLogger("api")
|
||||
|
||||
@@ -61,7 +66,7 @@ func Init() {
|
||||
}
|
||||
|
||||
if cfg.Mod.Username != "" {
|
||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
|
||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd
|
||||
}
|
||||
|
||||
if log.Trace().Enabled() {
|
||||
@@ -69,6 +74,8 @@ func Init() {
|
||||
}
|
||||
|
||||
if cfg.Mod.Listen != "" {
|
||||
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||
Port, _ = strconv.Atoi(port)
|
||||
go listen("tcp", cfg.Mod.Listen)
|
||||
}
|
||||
|
||||
@@ -92,10 +99,6 @@ func listen(network, address string) {
|
||||
|
||||
log.Info().Str("addr", address).Msg("[api] listen")
|
||||
|
||||
if network == "tcp" {
|
||||
Port = ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
server := http.Server{
|
||||
Handler: Handler,
|
||||
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||
@@ -154,6 +157,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||
if len(pattern) == 0 || pattern[0] != '/' {
|
||||
pattern = basePath + "/" + pattern
|
||||
}
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, pattern) {
|
||||
log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths")
|
||||
return
|
||||
}
|
||||
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||
http.HandleFunc(pattern, handler)
|
||||
}
|
||||
@@ -187,6 +194,7 @@ func Response(w http.ResponseWriter, body any, contentType string) {
|
||||
|
||||
const StreamNotFound = "stream not found"
|
||||
|
||||
var allowPaths []string
|
||||
var basePath string
|
||||
var log zerolog.Logger
|
||||
|
||||
@@ -197,9 +205,13 @@ func middlewareLog(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func middlewareAuth(username, password string, next http.Handler) http.Handler {
|
||||
func isLoopback(remoteAddr string) bool {
|
||||
return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@"
|
||||
}
|
||||
|
||||
func middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") && r.RemoteAddr != "@" {
|
||||
if localAuth || !isLoopback(r.RemoteAddr) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok || user != username || pass != password {
|
||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/www"
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/www"
|
||||
)
|
||||
|
||||
func initStatic(staticDir string) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# WebSocket
|
||||
|
||||
Endpoint: `/api/ws`
|
||||
|
||||
Query parameters:
|
||||
|
||||
- `src` (required) - Stream name
|
||||
|
||||
### WebRTC
|
||||
|
||||
Request SDP:
|
||||
|
||||
```json
|
||||
{"type":"webrtc/offer","value":"v=0\r\n..."}
|
||||
```
|
||||
|
||||
Response SDP:
|
||||
|
||||
```json
|
||||
{"type":"webrtc/answer","value":"v=0\r\n..."}
|
||||
```
|
||||
|
||||
Request/response candidate:
|
||||
|
||||
- empty value also allowed and optional
|
||||
|
||||
```json
|
||||
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
|
||||
```
|
||||
|
||||
### MSE
|
||||
|
||||
Request:
|
||||
|
||||
- codecs list optional
|
||||
|
||||
```json
|
||||
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
|
||||
```
|
||||
|
||||
### HLS
|
||||
|
||||
Request:
|
||||
|
||||
```json
|
||||
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
|
||||
|
||||
```json
|
||||
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
|
||||
```
|
||||
|
||||
### MJPEG
|
||||
|
||||
Request/response:
|
||||
|
||||
```json
|
||||
{"type":"mjpeg"}
|
||||
```
|
||||
+13
-11
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -37,16 +38,21 @@ var log zerolog.Logger
|
||||
type Message struct {
|
||||
Type string `json:"type"`
|
||||
Value any `json:"value,omitempty"`
|
||||
Raw []byte `json:"-"`
|
||||
}
|
||||
|
||||
func (m *Message) String() (value string) {
|
||||
_ = json.Unmarshal(m.Raw, &value)
|
||||
if s, ok := m.Value.(string); ok {
|
||||
return s
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Message) Unmarshal(v any) error {
|
||||
return json.Unmarshal(m.Raw, v)
|
||||
b, err := json.Marshal(m.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(b, v)
|
||||
}
|
||||
|
||||
type WSHandler func(tr *Transport, msg *Message) error
|
||||
@@ -113,11 +119,8 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
for {
|
||||
var raw struct {
|
||||
Type string `json:"type"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
}
|
||||
if err = ws.ReadJSON(&raw); err != nil {
|
||||
msg := new(Message)
|
||||
if err = ws.ReadJSON(msg); err != nil {
|
||||
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
|
||||
log.Trace().Err(err).Caller().Send()
|
||||
}
|
||||
@@ -125,14 +128,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
|
||||
msg := &Message{Type: raw.Type, Raw: raw.Value}
|
||||
|
||||
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||
|
||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||
go func() {
|
||||
if err = handler(tr, msg); err != nil {
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
|
||||
errMsg := creds.SecretString(err.Error())
|
||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
+46
-19
@@ -1,17 +1,23 @@
|
||||
- By default go2rtc will search config file `go2rtc.yaml` in current work directory
|
||||
- go2rtc support multiple config files:
|
||||
- `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml`
|
||||
- go2rtc support inline config as multiple formats from command line:
|
||||
# App
|
||||
|
||||
The application module is responsible for reading configuration files, running other modules and setting up [logs](#log).
|
||||
|
||||
The configuration can be edited through the application's WebUI with code highlighting, syntax and specification checking.
|
||||
|
||||
- By default, go2rtc will search for the `go2rtc.yaml` config file in the current working directory
|
||||
- go2rtc supports multiple config files:
|
||||
- `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml`
|
||||
- go2rtc supports inline config in multiple formats from the command line:
|
||||
- **YAML**: `go2rtc -c '{log: {format: text}}'`
|
||||
- **JSON**: `go2rtc -c '{"log":{"format":"text"}}'`
|
||||
- **key=value**: `go2rtc -c log.format=text`
|
||||
- Every next config will overwrite previous (but only defined params)
|
||||
- Each subsequent config will overwrite the previous one (but only for defined params)
|
||||
|
||||
```
|
||||
go2rtc -config "{log: {format: text}}" -config /config/go2rtc.yaml -config "{rtsp: {listen: ''}}" -config /usr/local/go2rtc/go2rtc.yaml
|
||||
```
|
||||
|
||||
or simple version
|
||||
or a simpler version
|
||||
|
||||
```
|
||||
go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml
|
||||
@@ -19,23 +25,29 @@ go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local
|
||||
|
||||
## Environment variables
|
||||
|
||||
Also go2rtc support templates for using environment variables in any part of config:
|
||||
There is support for loading external variables into the config. First, they will be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||
|
||||
rtsp:
|
||||
username: ${RTSP_USER:admin} # "admin" if env "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if env "RTSP_PASS" not set
|
||||
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
|
||||
password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set
|
||||
```
|
||||
|
||||
## JSON Schema
|
||||
|
||||
Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) supports autocomplete and syntax validation.
|
||||
Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) support autocomplete and syntax validation.
|
||||
|
||||
```yaml
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/website/schema.json
|
||||
# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/www/schema.json
|
||||
```
|
||||
|
||||
or from a running go2rtc:
|
||||
|
||||
```yaml
|
||||
# yaml-language-server: $schema=http://localhost:1984/schema.json
|
||||
```
|
||||
|
||||
## Defaults
|
||||
@@ -45,26 +57,41 @@ Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.
|
||||
|
||||
```yaml
|
||||
api:
|
||||
listen: ":1984"
|
||||
listen: ":1984" # default public port for WebUI and HTTP API
|
||||
|
||||
ffmpeg:
|
||||
bin: "ffmpeg"
|
||||
bin: "ffmpeg" # default binary path for FFmpeg
|
||||
|
||||
log:
|
||||
format: "color"
|
||||
level: "info"
|
||||
level: "info" # default log level
|
||||
output: "stdout"
|
||||
time: "UNIXMS"
|
||||
|
||||
rtsp:
|
||||
listen: ":8554"
|
||||
listen: ":8554" # default public port for RTSP server
|
||||
default_query: "video&audio"
|
||||
|
||||
srtp:
|
||||
listen: ":8443"
|
||||
listen: ":8443" # default public port for SRTP server (used for HomeKit)
|
||||
|
||||
webrtc:
|
||||
listen: ":8555/tcp"
|
||||
listen: ":8555" # default public port for WebRTC server (TCP and UDP)
|
||||
ice_servers:
|
||||
- urls: [ "stun:stun.l.google.com:19302" ]
|
||||
- urls: [ "stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302" ]
|
||||
```
|
||||
|
||||
## Log
|
||||
|
||||
You can set different log levels for different modules.
|
||||
|
||||
```yaml
|
||||
log:
|
||||
format: "" # empty (default, autodetect color support), color, json, text
|
||||
level: "info" # disabled, trace, debug, info (default), warn, error
|
||||
output: "stdout" # empty (only to memory), stderr, stdout (default)
|
||||
time: "UNIXMS" # empty (disable timestamp), UNIXMS (default), UNIXMICRO, UNIXNANO
|
||||
|
||||
api: trace # module name: log level
|
||||
```
|
||||
|
||||
Modules: `api`, `streams`, `rtsp`, `webrtc`, `mp4`, `hls`, `mjpeg`, `hass`, `homekit`, `onvif`, `rtmp`, `webtorrent`, `wyoming`, `echo`, `exec`, `expr`, `ffmpeg`, `wyze`, `xiaomi`.
|
||||
|
||||
+22
-1
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
var (
|
||||
Version string
|
||||
Modules []string
|
||||
UserAgent string
|
||||
ConfigPath string
|
||||
Info = make(map[string]any)
|
||||
@@ -76,6 +77,16 @@ func Init() {
|
||||
if ConfigPath != "" {
|
||||
Logger.Info().Str("path", ConfigPath).Msg("config")
|
||||
}
|
||||
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Modules []string `yaml:"modules"`
|
||||
} `yaml:"app"`
|
||||
}
|
||||
|
||||
LoadConfig(&cfg)
|
||||
|
||||
Modules = cfg.Mod.Modules
|
||||
}
|
||||
|
||||
func readRevisionTime() (revision, vcsTime string) {
|
||||
@@ -92,10 +103,20 @@ func readRevisionTime() (revision, vcsTime string) {
|
||||
vcsTime = setting.Value
|
||||
case "vcs.modified":
|
||||
if setting.Value == "true" {
|
||||
revision = "mod." + revision
|
||||
revision += ".dirty"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check version from -buildvcs info
|
||||
// Format for tagged version : v1.9.13
|
||||
// Format for modified code: v1.9.14-0.20251215184105-753d6617ab58+dirty
|
||||
if info.Main.Version != "v"+Version {
|
||||
// Format: 1.9.13+dev.753d661[.dirty]
|
||||
// Compatible with "awesomeversion" and "packaging.version" from python.
|
||||
// Version will be larger than the previous release, but smaller than the next release.
|
||||
Version += "+dev." + revision
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
+12
-4
@@ -5,8 +5,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
)
|
||||
|
||||
@@ -18,15 +19,20 @@ func LoadConfig(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
func PatchConfig(key string, value any, path ...string) error {
|
||||
var configMu sync.Mutex
|
||||
|
||||
func PatchConfig(path []string, value any) error {
|
||||
if ConfigPath == "" {
|
||||
return errors.New("config file disabled")
|
||||
}
|
||||
|
||||
configMu.Lock()
|
||||
defer configMu.Unlock()
|
||||
|
||||
// empty config is OK
|
||||
b, _ := os.ReadFile(ConfigPath)
|
||||
|
||||
b, err := yaml.Patch(b, key, value, path...)
|
||||
b, err := yaml.Patch(b, path, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,13 +71,15 @@ func initConfig(confs flagConfig) {
|
||||
// config as file
|
||||
if ConfigPath == "" {
|
||||
ConfigPath = conf
|
||||
initStorage()
|
||||
}
|
||||
|
||||
if data, _ = os.ReadFile(conf); data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
||||
loadEnv(data)
|
||||
data = creds.ReplaceVars(data)
|
||||
configs = append(configs, data)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-15
@@ -3,14 +3,19 @@ package app
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var MemoryLog = newBuffer(16)
|
||||
var MemoryLog = newBuffer()
|
||||
|
||||
func GetLogger(module string) zerolog.Logger {
|
||||
Logger.Trace().Str("module", module).Msgf("[log] init")
|
||||
|
||||
if s, ok := modules[module]; ok {
|
||||
lvl, err := zerolog.ParseLevel(s)
|
||||
if err == nil {
|
||||
@@ -38,11 +43,17 @@ func initLogger() {
|
||||
|
||||
var writer io.Writer
|
||||
|
||||
switch modules["output"] {
|
||||
switch output, path, _ := strings.Cut(modules["output"], ":"); output {
|
||||
case "stderr":
|
||||
writer = os.Stderr
|
||||
case "stdout":
|
||||
writer = os.Stdout
|
||||
case "file":
|
||||
if path == "" {
|
||||
path = "go2rtc.log"
|
||||
}
|
||||
// if fail - only MemoryLog will be available
|
||||
writer, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
}
|
||||
|
||||
timeFormat := modules["time"]
|
||||
@@ -80,6 +91,8 @@ func initLogger() {
|
||||
writer = MemoryLog
|
||||
}
|
||||
|
||||
writer = creds.SecretWriter(writer)
|
||||
|
||||
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||
Logger = zerolog.New(writer).Level(lvl)
|
||||
|
||||
@@ -99,15 +112,19 @@ var modules = map[string]string{
|
||||
"time": zerolog.TimeFormatUnixMs,
|
||||
}
|
||||
|
||||
const chunkSize = 1 << 16
|
||||
const (
|
||||
chunkCount = 16
|
||||
chunkSize = 1 << 16
|
||||
)
|
||||
|
||||
type circularBuffer struct {
|
||||
chunks [][]byte
|
||||
r, w int
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newBuffer(chunks int) *circularBuffer {
|
||||
b := &circularBuffer{chunks: make([][]byte, 0, chunks)}
|
||||
func newBuffer() *circularBuffer {
|
||||
b := &circularBuffer{chunks: make([][]byte, 0, chunkCount)}
|
||||
// create first chunk
|
||||
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||
return b
|
||||
@@ -116,16 +133,17 @@ func newBuffer(chunks int) *circularBuffer {
|
||||
func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
|
||||
b.mu.Lock()
|
||||
// check if chunk has size
|
||||
if len(b.chunks[b.w])+n > chunkSize {
|
||||
// increase write chunk index
|
||||
if b.w++; b.w == cap(b.chunks) {
|
||||
if b.w++; b.w == chunkCount {
|
||||
b.w = 0
|
||||
}
|
||||
// check overflow
|
||||
if b.r == b.w {
|
||||
// increase read chunk index
|
||||
if b.r++; b.r == cap(b.chunks) {
|
||||
if b.r++; b.r == chunkCount {
|
||||
b.r = 0
|
||||
}
|
||||
}
|
||||
@@ -140,29 +158,34 @@ func (b *circularBuffer) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
b.chunks[b.w] = append(b.chunks[b.w], p...)
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {
|
||||
for i := b.r; ; {
|
||||
var nn int
|
||||
if nn, err = w.Write(b.chunks[i]); err != nil {
|
||||
return
|
||||
}
|
||||
n += int64(nn)
|
||||
buf := make([]byte, 0, chunkCount*chunkSize)
|
||||
|
||||
// use temp buffer inside mutex because w.Write can take some time
|
||||
b.mu.Lock()
|
||||
for i := b.r; ; {
|
||||
buf = append(buf, b.chunks[i]...)
|
||||
if i == b.w {
|
||||
break
|
||||
}
|
||||
if i++; i == cap(b.chunks) {
|
||||
if i++; i == chunkCount {
|
||||
i = 0
|
||||
}
|
||||
}
|
||||
return
|
||||
b.mu.Unlock()
|
||||
|
||||
nn, err := w.Write(buf)
|
||||
return int64(nn), err
|
||||
}
|
||||
|
||||
func (b *circularBuffer) Reset() {
|
||||
b.mu.Lock()
|
||||
b.chunks[0] = b.chunks[0][:0]
|
||||
b.r = 0
|
||||
b.w = 0
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||
)
|
||||
|
||||
func initStorage() {
|
||||
storage = &envStorage{data: make(map[string]string)}
|
||||
creds.SetStorage(storage)
|
||||
}
|
||||
|
||||
func loadEnv(data []byte) {
|
||||
var cfg struct {
|
||||
Env map[string]string `yaml:"env"`
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
storage.mu.Lock()
|
||||
for name, value := range cfg.Env {
|
||||
storage.data[name] = value
|
||||
creds.AddSecret(value)
|
||||
}
|
||||
storage.mu.Unlock()
|
||||
}
|
||||
|
||||
var storage *envStorage
|
||||
|
||||
type envStorage struct {
|
||||
data map[string]string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *envStorage) SetValue(name, value string) error {
|
||||
if err := PatchConfig([]string{"env", name}, value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.data[name] = value
|
||||
s.mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *envStorage) GetValue(name string) (value string, ok bool) {
|
||||
s.mu.Lock()
|
||||
value, ok = s.data[name]
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Bubble
|
||||
|
||||
[`new in v1.6.1`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)
|
||||
|
||||
Private format in some cameras from [dvr163.com](http://help.dvr163.com/) and [eseecloud.com](http://www.eseecloud.com/).
|
||||
|
||||
## Configuration
|
||||
|
||||
- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default
|
||||
- set up separate streams for different channels and streams
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
# Debug
|
||||
|
||||
This module provides `GET /api/stack`, with which you can find hanging goroutines
|
||||
@@ -29,8 +29,8 @@ var stackSkip = [][]byte{
|
||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
|
||||
|
||||
// webrtc/api.go
|
||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v4.NewTCPMuxDefault"),
|
||||
[]byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
|
||||
}
|
||||
|
||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Doorbird
|
||||
|
||||
[`new in v1.9.8`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.8)
|
||||
|
||||
This source type supports [Doorbird](https://www.doorbird.com/) devices including MJPEG stream, audio stream as well as two-way audio.
|
||||
|
||||
It is recommended to create a separate user within your doorbird setup for go2rtc. Minimum permissions for the user are:
|
||||
|
||||
- Watch always
|
||||
- API operator
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
doorbird1:
|
||||
- rtsp://admin:password@192.168.1.123:8557/mpeg/720p/media.amp # RTSP stream
|
||||
- doorbird://admin:password@192.168.1.123?media=video # MJPEG stream
|
||||
- doorbird://admin:password@192.168.1.123?media=audio # audio stream
|
||||
- doorbird://admin:password@192.168.1.123 # two-way audio
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
package doorbird
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/doorbird"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.RedirectFunc("doorbird", func(rawURL string) (string, error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// https://www.doorbird.com/downloads/api_lan.pdf
|
||||
switch u.Query().Get("media") {
|
||||
case "video":
|
||||
u.Path = "/bha-api/video.cgi"
|
||||
case "audio":
|
||||
u.Path = "/bha-api/audio-receive.cgi"
|
||||
default:
|
||||
return "", nil
|
||||
}
|
||||
|
||||
u.Scheme = "http"
|
||||
|
||||
return u.String(), nil
|
||||
})
|
||||
|
||||
streams.HandleFunc("doorbird", func(source string) (core.Producer, error) {
|
||||
return doorbird.Dial(source)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# DVR-IP
|
||||
|
||||
[`new in v1.2.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)
|
||||
|
||||
Private format from DVR-IP NVR, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
|
||||
|
||||
## Configuration
|
||||
|
||||
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
|
||||
- set up separate streams for different channels
|
||||
- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream
|
||||
- only the TCP protocol is supported
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
two_way_audio:
|
||||
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||
- dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# Echo
|
||||
|
||||
Some sources may have a dynamic link. And you will need to get it using a Bash or Python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the supported sources.
|
||||
|
||||
**Docker** and **Home Assistant add-on** users have preinstalled `python3`, `curl`, `jq`.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||
```
|
||||
|
||||
## Install python libraries
|
||||
|
||||
**Docker** and **Hass Add-on** users have preinstalled `python3` without any additional libraries, like [requests](https://requests.readthedocs.io/) or others. If you need some additional libraries - you need to install them to folder with your script:
|
||||
|
||||
1. Install [SSH & Web Terminal](https://github.com/hassio-addons/addon-ssh)
|
||||
2. Goto Add-on Web UI
|
||||
3. Install library: `pip install requests -t /config/echo`
|
||||
4. Add your script to `/config/echo/myscript.py`
|
||||
5. Use your script as source `echo:python3 /config/echo/myscript.py`
|
||||
|
||||
## Example: Apple HLS
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html
|
||||
```
|
||||
|
||||
**hls.py**
|
||||
|
||||
```python
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import urlopen
|
||||
|
||||
html = urlopen(sys.argv[1]).read().decode("utf-8")
|
||||
url = re.search(r"https.+?m3u8", html)[0]
|
||||
|
||||
html = urlopen(url).read().decode("utf-8")
|
||||
m = re.search(r"^[a-z0-1/_]+\.m3u8$", html, flags=re.MULTILINE)
|
||||
url = urljoin(url, m[0])
|
||||
|
||||
# ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8#video=copy
|
||||
print("ffmpeg:" + url + "#video=copy")
|
||||
```
|
||||
@@ -2,7 +2,9 @@ package echo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os/exec"
|
||||
"slices"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
@@ -10,11 +12,25 @@ import (
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"echo"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
allowPaths := cfg.Mod.AllowPaths
|
||||
|
||||
log := app.GetLogger("echo")
|
||||
|
||||
streams.RedirectFunc("echo", func(url string) (string, error) {
|
||||
args := shell.QuoteSplit(url[5:])
|
||||
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, args[0]) {
|
||||
return "", errors.New("echo: bin not in allow_paths: " + args[0])
|
||||
}
|
||||
|
||||
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -26,4 +42,5 @@ func Init() {
|
||||
|
||||
return string(b), nil
|
||||
})
|
||||
streams.MarkInsecure("echo")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# EseeCloud
|
||||
|
||||
[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)
|
||||
|
||||
This source is for cameras with a link like this `http://admin:@192.168.1.123:80/livestream/12`. Related [issue](https://github.com/AlexxIT/go2rtc/issues/1690).
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1: eseecloud://user:pass@192.168.1.123:80/livestream/12
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
package eseecloud
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/eseecloud"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("eseecloud", eseecloud.Dial)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Exec
|
||||
|
||||
Exec source can run any external application and expect data from it. Two transports are supported - **pipe** ([`new in v1.5.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)) and **RTSP**.
|
||||
|
||||
If you want to use **RTSP** transport, the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.
|
||||
|
||||
**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**.
|
||||
|
||||
The source can be used with:
|
||||
|
||||
- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source is just a shortcut to exec source
|
||||
- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server
|
||||
- [GStreamer](https://gstreamer.freedesktop.org/)
|
||||
- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)
|
||||
- any of your own software
|
||||
|
||||
## Configuration
|
||||
|
||||
Pipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):
|
||||
|
||||
- `killsignal` - signal which will be sent to stop the process (numeric form)
|
||||
- `killtimeout` - time in seconds for forced termination with sigkill
|
||||
- `backchannel` - enable backchannel for two-way audio
|
||||
- `starttimeout` - time in seconds for waiting first byte from RTSP
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
|
||||
picam_h264: exec:libcamera-vid -t 0 --inline -o -
|
||||
picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -
|
||||
pi5cam_h264: exec:libcamera-vid -t 0 --libav-format h264 -o -
|
||||
canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5
|
||||
play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1
|
||||
play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1
|
||||
```
|
||||
|
||||
## Backchannel
|
||||
|
||||
- You can check audio card names in the **Go2rtc > WebUI > Add**
|
||||
- You can specify multiple backchannel lines with different codecs
|
||||
|
||||
```yaml
|
||||
sources:
|
||||
two_way_audio_win:
|
||||
- exec:ffmpeg -hide_banner -f dshow -i "audio=Microphone (High Definition Audio Device)" -c pcm_s16le -ar 16000 -ac 1 -f wav -
|
||||
- exec:ffplay -nodisp -probesize 32 -f s16le -ar 16000 -#backchannel=1#audio=s16le/16000
|
||||
- exec:ffplay -nodisp -probesize 32 -f alaw -ar 8000 -#backchannel=1#audio=alaw/8000
|
||||
```
|
||||
@@ -1,39 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
)
|
||||
|
||||
// closer support custom killsignal with custom killtimeout
|
||||
type closer struct {
|
||||
cmd *exec.Cmd
|
||||
query url.Values
|
||||
}
|
||||
|
||||
func (c *closer) Close() (err error) {
|
||||
sig := os.Kill
|
||||
if s := c.query.Get("killsignal"); s != "" {
|
||||
sig = syscall.Signal(core.Atoi(s))
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[exec] kill with signal=%d", sig)
|
||||
err = c.cmd.Process.Signal(sig)
|
||||
|
||||
if s := c.query.Get("killtimeout"); s != "" {
|
||||
timeout := time.Duration(core.Atoi(s)) * time.Second
|
||||
timer := time.AfterFunc(timeout, func() {
|
||||
log.Trace().Msgf("[exec] kill after timeout=%s", s)
|
||||
_ = c.cmd.Process.Kill()
|
||||
})
|
||||
defer timer.Stop() // stop timer if Wait ends before timeout
|
||||
}
|
||||
|
||||
return errors.Join(err, c.cmd.Wait())
|
||||
}
|
||||
+65
-25
@@ -9,9 +9,10 @@ import (
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
@@ -19,13 +20,23 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||
"github.com/AlexxIT/go2rtc/pkg/stdin"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
AllowPaths []string `yaml:"allow_paths"`
|
||||
} `yaml:"exec"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
allowPaths = cfg.Mod.AllowPaths
|
||||
|
||||
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||
waitersMu.Lock()
|
||||
waiter := waiters[conn.URL.Path]
|
||||
@@ -45,11 +56,14 @@ func Init() {
|
||||
})
|
||||
|
||||
streams.HandleFunc("exec", execHandle)
|
||||
streams.MarkInsecure("exec")
|
||||
|
||||
log = app.GetLogger("exec")
|
||||
}
|
||||
|
||||
func execHandle(rawURL string) (core.Producer, error) {
|
||||
var allowPaths []string
|
||||
|
||||
func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
|
||||
@@ -67,39 +81,67 @@ func execHandle(rawURL string) (core.Producer, error) {
|
||||
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||
}
|
||||
|
||||
args := shell.QuoteSplit(rawURL[5:]) // remove `exec:`
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
|
||||
cmd.Stderr = &logWriter{
|
||||
buf: make([]byte, 512),
|
||||
debug: log.Debug().Enabled(),
|
||||
}
|
||||
|
||||
if query.Get("backchannel") == "1" {
|
||||
return stdin.NewClient(cmd)
|
||||
if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {
|
||||
_ = cmd.Close()
|
||||
return nil, errors.New("exec: bin not in allow_paths: " + cmd.Args[0])
|
||||
}
|
||||
|
||||
cl := &closer{cmd: cmd, query: query}
|
||||
if s := query.Get("killsignal"); s != "" {
|
||||
sig := syscall.Signal(core.Atoi(s))
|
||||
cmd.Cancel = func() error {
|
||||
log.Debug().Msgf("[exec] kill with signal=%d", sig)
|
||||
return cmd.Process.Signal(sig)
|
||||
}
|
||||
}
|
||||
|
||||
if s := query.Get("killtimeout"); s != "" {
|
||||
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
|
||||
}
|
||||
|
||||
if query.Get("backchannel") == "1" {
|
||||
return pcm.NewBackchannel(cmd, query.Get("audio"))
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
if s := query.Get("starttimeout"); s != "" {
|
||||
timeout = time.Duration(core.Atoi(s)) * time.Second
|
||||
} else {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return handlePipe(rawURL, cmd, cl)
|
||||
prod, err = handlePipe(rawURL, cmd)
|
||||
} else {
|
||||
prod, err = handleRTSP(rawURL, cmd, path, timeout)
|
||||
}
|
||||
|
||||
return handleRTSP(rawURL, cmd, cl, path)
|
||||
if err != nil {
|
||||
_ = cmd.Close()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, error) {
|
||||
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rc := struct {
|
||||
rd := struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
}{
|
||||
// add buffer for pipe reader to reduce syscall
|
||||
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||
cl,
|
||||
// stop cmd on close pipe call
|
||||
cmd,
|
||||
}
|
||||
|
||||
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||
@@ -110,9 +152,8 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prod, err := magic.Open(rc)
|
||||
prod, err := magic.Open(rd)
|
||||
if err != nil {
|
||||
_ = rc.Close()
|
||||
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||
}
|
||||
|
||||
@@ -126,7 +167,7 @@ func handlePipe(source string, cmd *exec.Cmd, cl io.Closer) (core.Producer, erro
|
||||
return prod, nil
|
||||
}
|
||||
|
||||
func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.Producer, error) {
|
||||
func handleRTSP(source string, cmd *shell.Command, path string, timeout time.Duration) (core.Producer, error) {
|
||||
if log.Trace().Enabled() {
|
||||
cmd.Stdout = os.Stdout
|
||||
}
|
||||
@@ -152,23 +193,22 @@ func handleRTSP(source string, cmd *exec.Cmd, cl io.Closer, path string) (core.P
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
timer := time.NewTimer(timeout)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-time.After(time.Minute):
|
||||
case <-timer.C:
|
||||
// haven't received data from app in timeout
|
||||
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||
_ = cl.Close()
|
||||
return nil, errors.New("exec: timeout")
|
||||
case <-done:
|
||||
// limit message size
|
||||
case <-cmd.Done():
|
||||
// app fail before we receive any data
|
||||
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||
case prod := <-waiter:
|
||||
// app started successfully
|
||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||
setRemoteInfo(prod, source, cmd.Args)
|
||||
prod.OnClose = cl.Close
|
||||
prod.OnClose = cmd.Close
|
||||
return prod, nil
|
||||
}
|
||||
}
|
||||
|
||||
+74
-12
@@ -1,5 +1,7 @@
|
||||
# Expr
|
||||
|
||||
[`new in v1.8.2`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)
|
||||
|
||||
[Expr](https://github.com/antonmedv/expr) - expression language and expression evaluation for Go.
|
||||
|
||||
- [language definition](https://expr.medv.io/docs/Language-Definition) - takes best from JS, Python, Jinja2 syntax
|
||||
@@ -12,34 +14,94 @@
|
||||
- `fetch` - JS-like HTTP requests
|
||||
- `match` - JS-like RegExp queries
|
||||
|
||||
## Examples
|
||||
## Fetch examples
|
||||
|
||||
Multiple fetch requests are executed within a single session. They share the same cookie.
|
||||
|
||||
**HTTP GET**
|
||||
|
||||
```js
|
||||
var r = fetch('https://example.org/products.json');
|
||||
```
|
||||
|
||||
**HTTP POST JSON**
|
||||
|
||||
```js
|
||||
var r = fetch('https://example.org/post', {
|
||||
method: 'POST',
|
||||
// Content-Type: application/json will be set automatically
|
||||
json: {username: 'example'}
|
||||
});
|
||||
```
|
||||
|
||||
**HTTP POST Form**
|
||||
|
||||
```js
|
||||
var r = fetch('https://example.org/post', {
|
||||
method: 'POST',
|
||||
// Content-Type: application/x-www-form-urlencoded will be set automatically
|
||||
data: {username: 'example', password: 'password'}
|
||||
});
|
||||
```
|
||||
|
||||
## Script examples
|
||||
|
||||
**Two way audio for Dahua VTO**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua_vto: |
|
||||
expr: let host = "admin:password@192.168.1.123";
|
||||
fetch("http://"+host+"/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000").ok
|
||||
? "rtsp://"+host+"/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif" : ""
|
||||
expr:
|
||||
let host = 'admin:password@192.168.1.123';
|
||||
|
||||
var r = fetch('http://' + host + '/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000');
|
||||
|
||||
'rtsp://' + host + '/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif'
|
||||
```
|
||||
|
||||
**dom.ru**
|
||||
|
||||
You can get credentials via:
|
||||
|
||||
- https://github.com/alexmorbo/domru (file `/share/domru/accounts`)
|
||||
- https://github.com/ad/domru
|
||||
You can get credentials from https://github.com/ad/domru
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dom_ru: |
|
||||
expr: let camera = "99999999"; let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; let operator = 99;
|
||||
fetch("https://myhome.novotelecom.ru/rest/v1/forpost/cameras/"+camera+"/video", {
|
||||
headers: {Authorization: "Bearer "+token, Operator: operator}
|
||||
expr:
|
||||
let camera = '***';
|
||||
let token = '***';
|
||||
let operator = '***';
|
||||
|
||||
fetch('https://myhome.proptech.ru/rest/v1/forpost/cameras/' + camera + '/video', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'User-Agent': 'Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010) | 0 | 0 | 0',
|
||||
'Operator': operator
|
||||
}
|
||||
}).json().data.URL
|
||||
```
|
||||
|
||||
**dom.ufanet.ru**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
ufanet_ru: |
|
||||
expr:
|
||||
let username = '***';
|
||||
let password = '***';
|
||||
let cameraid = '***';
|
||||
|
||||
let r1 = fetch('https://ucams.ufanet.ru/api/internal/login/', {
|
||||
method: 'POST',
|
||||
data: {username: username, password: password}
|
||||
});
|
||||
let r2 = fetch('https://ucams.ufanet.ru/api/v0/cameras/this/?lang=ru', {
|
||||
method: 'POST',
|
||||
json: {'fields': ['token_l', 'server'], 'token_l_ttl': 3600, 'numbers': [cameraid]},
|
||||
}).json().results[0];
|
||||
|
||||
'rtsp://' + r2.server.domain + '/' + r2.number + '?token=' + r2.token_l
|
||||
```
|
||||
|
||||
**Parse HLS files from Apple**
|
||||
|
||||
Same example in two languages - python and expr.
|
||||
@@ -76,7 +138,7 @@ streams:
|
||||
"ffmpeg:" + url3 + "#video=copy"
|
||||
```
|
||||
|
||||
## Comparsion
|
||||
## Comparison
|
||||
|
||||
| expr | python | js |
|
||||
|------------------------------|----------------------------|--------------------------------|
|
||||
|
||||
@@ -12,7 +12,7 @@ func Init() {
|
||||
log := app.GetLogger("expr")
|
||||
|
||||
streams.RedirectFunc("expr", func(url string) (string, error) {
|
||||
v, err := expr.Run(url[5:])
|
||||
v, err := expr.Eval(url[5:], nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -25,4 +25,5 @@ func Init() {
|
||||
|
||||
return url, nil
|
||||
})
|
||||
streams.MarkInsecure("expr")
|
||||
}
|
||||
|
||||
+52
-58
@@ -1,68 +1,62 @@
|
||||
## FFplay output
|
||||
# FFmpeg
|
||||
|
||||
[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V: 0.003 fd= 1 aq= 21KB vq= 321KB sq= 0B f=0/0`:
|
||||
You can get any stream, file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.
|
||||
|
||||
- `7.11` - master clock, is the time from start of the stream/video
|
||||
- `A-V` - av_diff, difference between audio and video timestamps
|
||||
- `fd` - frames dropped
|
||||
- `aq` - audio queue (0 - no delay)
|
||||
- `vq` - video queue (0 - no delay)
|
||||
- `sq` - subtitle queue
|
||||
- `f` - timestamp error correction rate (Not 100% sure)
|
||||
- FFmpeg preinstalled for **Docker** and **Home Assistant add-on** users
|
||||
- **Home Assistant add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
|
||||
|
||||
`M-V`, `M-A` means video stream only, audio stream only respectively.
|
||||
## Configuration
|
||||
|
||||
## Devices Windows
|
||||
|
||||
```
|
||||
>ffmpeg -hide_banner -f dshow -list_options true -i video="VMware Virtual USB Video Device"
|
||||
[dshow @ 0000025695e52900] DirectShow video device options (from video devices)
|
||||
[dshow @ 0000025695e52900] Pin "Record" (alternative pin name "0")
|
||||
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10
|
||||
[dshow @ 0000025695e52900] pixel_format=yuyv422 min s=1280x720 fps=1 max s=1280x720 fps=10 (tv, bt470bg/bt709/unknown, topleft)
|
||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23
|
||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)
|
||||
```
|
||||
|
||||
## Devices Mac
|
||||
|
||||
```
|
||||
% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i ""
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone
|
||||
[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)
|
||||
```
|
||||
|
||||
## Devices Linux
|
||||
|
||||
```
|
||||
# ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0
|
||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Raw : yuyv422 : YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||
```
|
||||
|
||||
## TTS
|
||||
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i "flite=text='1 2 3 4 5 6 7 8 9 0'"#audio=pcma
|
||||
# [FILE] all tracks will be copied without transcoding codecs
|
||||
file1: ffmpeg:/media/BigBuckBunny.mp4
|
||||
|
||||
# [FILE] video will be transcoded to H264, audio will be skipped
|
||||
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
|
||||
|
||||
# [FILE] video will be copied, audio will be transcoded to PCMU
|
||||
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
|
||||
|
||||
# [HLS] video will be copied, audio will be skipped
|
||||
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
|
||||
|
||||
# [MJPEG] video will be transcoded to H264
|
||||
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
|
||||
|
||||
# [RTSP] video with rotation, should be transcoded, so select H264
|
||||
rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||
```
|
||||
|
||||
## Useful links
|
||||
All transcoding formats have [built-in templates](ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.
|
||||
|
||||
- https://superuser.com/questions/564402/explanation-of-x264-tune
|
||||
- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264
|
||||
- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования
|
||||
- https://html5test.com/
|
||||
- https://trac.ffmpeg.org/wiki/Capture/Webcam
|
||||
- https://trac.ffmpeg.org/wiki/DirectShow
|
||||
- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table
|
||||
- https://github.com/tuupola/esp_video/blob/master/README.md
|
||||
- https://github.com/leandromoreira/ffmpeg-libav-tutorial
|
||||
- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/
|
||||
- https://slhck.info/video/2017/02/24/vbr-settings.html
|
||||
- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)
|
||||
But you can override them via YAML config. You can also add your own formats to the config and use them with source params.
|
||||
|
||||
```yaml
|
||||
ffmpeg:
|
||||
bin: ffmpeg # path to ffmpeg binary
|
||||
global: "-hide_banner"
|
||||
timeout: 5 # default timeout in seconds for rtsp inputs
|
||||
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
|
||||
mycodec: "-any args that supported by ffmpeg..."
|
||||
myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}"
|
||||
myraw: "-ss 00:00:20"
|
||||
```
|
||||
|
||||
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
|
||||
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
|
||||
- You can use `rotate` param with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
|
||||
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
|
||||
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
|
||||
- This will greatly increase the CPU of the server, even with hardware acceleration
|
||||
- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)
|
||||
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
|
||||
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
|
||||
- You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`)
|
||||
- You can add your own input templates
|
||||
|
||||
Read more about [hardware acceleration](hardware/README.md).
|
||||
|
||||
**PS.** It is recommended to check the available hardware in the WebUI add page.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# FFmpeg Device
|
||||
|
||||
You can get video from any USB camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.
|
||||
|
||||
- check available devices in web interface
|
||||
- `video_size` and `framerate` must be supported by your camera!
|
||||
- for Linux supported only video for now
|
||||
- for macOS you can stream FaceTime camera or whole desktop!
|
||||
- for macOS important to set right framerate
|
||||
|
||||
## Configuration
|
||||
|
||||
Format: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
linux_usbcam: ffmpeg:device?video=0&video_size=1280x720#video=h264
|
||||
windows_webcam: ffmpeg:device?video=0#video=h264
|
||||
macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma
|
||||
```
|
||||
|
||||
**PS.** It is recommended to check the available devices in the WebUI add page.
|
||||
+24
-10
@@ -58,15 +58,15 @@ func Init() {
|
||||
}
|
||||
|
||||
var defaults = map[string]string{
|
||||
"bin": "ffmpeg",
|
||||
"global": "-hide_banner",
|
||||
"bin": "ffmpeg",
|
||||
"global": "-hide_banner",
|
||||
"timeout": "5",
|
||||
|
||||
// inputs
|
||||
"file": "-re -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
|
||||
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
||||
"file": "-re -i {input}",
|
||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}",
|
||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -i {input}",
|
||||
|
||||
// output
|
||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||
@@ -80,7 +80,7 @@ var defaults = map[string]string{
|
||||
// `-profile high -level 4.1` - most used streaming profile
|
||||
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"h265": "-c:v libx265 -g 50 -profile:v main -x265-params level=5.1:high-tier=0 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p",
|
||||
"mjpeg": "-c:v mjpeg",
|
||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||
|
||||
@@ -113,6 +113,7 @@ var defaults = map[string]string{
|
||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
||||
"pcml": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||
"pcml/8000": "-c:a pcm_s16le -ar:a 8000 -ac:a 1",
|
||||
"pcml/16000": "-c:a pcm_s16le -ar:a 16000 -ac:a 1",
|
||||
"pcml/44100": "-c:a pcm_s16le -ar:a 44100 -ac:a 1",
|
||||
|
||||
// hardware Intel and AMD on Linux
|
||||
@@ -129,8 +130,9 @@ var defaults = map[string]string{
|
||||
// hardware Rockchip
|
||||
// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768
|
||||
// hevc - doesn't have a profile setting
|
||||
"h264/rkmpp": "-c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||
"h265/rkmpp": "-c:v hevc_rkmpp_encoder -g 50 -bf 0 -level:v 5.1",
|
||||
"h264/rkmpp": "-c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||
"h265/rkmpp": "-c:v hevc_rkmpp -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||
"mjpeg/rkmpp": "-c:v mjpeg_rkmpp",
|
||||
|
||||
// hardware NVidia on Linux and Windows
|
||||
// preset=p2 - faster, tune=ll - low latency
|
||||
@@ -167,6 +169,13 @@ func inputTemplate(name, s string, query url.Values) string {
|
||||
} else {
|
||||
template = defaults[name]
|
||||
}
|
||||
if strings.Contains(template, "{timeout}") {
|
||||
timeout := query.Get("timeout")
|
||||
if timeout == "" {
|
||||
timeout = defaults["timeout"]
|
||||
}
|
||||
template = strings.Replace(template, "{timeout}", timeout+"000000", 1)
|
||||
}
|
||||
return strings.Replace(template, "{input}", s, 1)
|
||||
}
|
||||
|
||||
@@ -179,6 +188,7 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
Version: verAV,
|
||||
}
|
||||
|
||||
var source = s
|
||||
var query url.Values
|
||||
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||
query = streams.ParseQuery(s[i+1:])
|
||||
@@ -221,6 +231,10 @@ func parseArgs(s string) *ffmpeg.Args {
|
||||
default:
|
||||
s += "?video&audio"
|
||||
}
|
||||
s += "&source=ffmpeg:" + url.QueryEscape(source)
|
||||
for _, v := range query["query"] {
|
||||
s += "&" + v
|
||||
}
|
||||
args.Input = inputTemplate("rtsp", s, query)
|
||||
} else if i = strings.Index(s, "?"); i > 0 {
|
||||
switch s[:i] {
|
||||
|
||||
@@ -123,6 +123,11 @@ func TestParseArgsIpCam(t *testing.T) {
|
||||
source: "rtmp://example.com#input=rtsp/udp",
|
||||
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[RTSP] custom timeout",
|
||||
source: "rtsp://example.com#timeout=10",
|
||||
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -251,15 +256,33 @@ func _TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseArgsHwRKMPP(t *testing.T) {
|
||||
// [HTTP-MJPEG] video will be transcoded to H264
|
||||
args := parseArgs("http://example.com#video=h264#hardware=rkmpp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http://example.com#video=h264#rotate=180#hardware=rkmpp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "transpose=1,transpose=1" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
|
||||
args = parseArgs("http://example.com#video=h264#height=320#hardware=rkmpp")
|
||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v h264_rkmpp_encoder -g 50 -bf 0 -profile:v high -level:v 4.1 -height 320 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
name: "[FILE] transcoding to H264",
|
||||
source: "bbb.mp4#video=h264#hardware=rkmpp",
|
||||
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] transcoding with rotation",
|
||||
source: "bbb.mp4#video=h264#rotate=180#hardware=rkmpp",
|
||||
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,vpp_rkrga=transpose=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
{
|
||||
name: "[FILE] transcoding with scaling",
|
||||
source: "bbb.mp4#video=h264#height=320#hardware=rkmpp",
|
||||
expect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf "format=drm_prime|nv12,hwupload,scale_rkrga=-1:320:force_original_aspect_ratio=0" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
args := parseArgs(test.source)
|
||||
require.Equal(t, test.expect, args.String())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func _TestParseArgsHwCuda(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# Hardware
|
||||
|
||||
You **DON'T** need hardware acceleration if:
|
||||
|
||||
- you're not using the [FFmpeg source](../README.md)
|
||||
- you're using only `#video=copy` for the FFmpeg source
|
||||
- you're using only `#audio=...` (any audio) transcoding for the FFmpeg source
|
||||
|
||||
You **NEED** hardware acceleration if you're using `#video=h264`, `#video=h265`, `#video=mjpeg` (video) transcoding.
|
||||
|
||||
## Important
|
||||
|
||||
- Acceleration is disabled by default because it can be unstable (this may change in the future)
|
||||
- go2rtc can automatically detect supported hardware acceleration if enabled
|
||||
- go2rtc will enable hardware decoding only if hardware encoding is supported
|
||||
- go2rtc will use the same GPU for decoder and encoder
|
||||
- Intel and AMD will switch to a software decoder if the input codec isn't supported by the hardware decoder
|
||||
- NVIDIA will fail if the input codec isn't supported by the hardware decoder
|
||||
- Raspberry Pi always uses a software decoder
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# auto select hardware encoder
|
||||
camera1_hw: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware
|
||||
|
||||
# manual select hardware encoder (vaapi, cuda, v4l2m2m, dxva2, videotoolbox)
|
||||
camera1_vaapi: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware=vaapi
|
||||
```
|
||||
|
||||
## Docker and Hass Addon
|
||||
|
||||
There are two versions of the Docker container and Hass Add-on:
|
||||
|
||||
- Latest (Alpine) supports hardware acceleration for Intel iGPU (CPU with graphics) and Raspberry Pi.
|
||||
- Hardware (Debian 12) supports Intel iGPU, AMD GPU, NVIDIA GPU.
|
||||
|
||||
## Intel iGPU
|
||||
|
||||
**Supported on:** Windows binary, Linux binary, Docker, Hass Addon.
|
||||
|
||||
If you have an Intel Sandy Bridge (2011) CPU with graphics, you already have hardware decoding/encoding support for `AVC/H.264`.
|
||||
|
||||
If you have an Intel Skylake (2015) CPU with graphics, you already have hardware decoding/encoding support for `AVC/H.264`, `HEVC/H.265` and `MJPEG`.
|
||||
|
||||
Read more [here](https://en.wikipedia.org/wiki/Intel_Quick_Sync_Video#Hardware_decoding_and_encoding) and [here](https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)).
|
||||
|
||||
Linux and Docker:
|
||||
|
||||
- It may be important to have a recent OS and Linux kernel. For example, on my **Debian 10 (kernel 4.19)** it did not work, but after updating to **Debian 11 (kernel 5.10)** everything was fine.
|
||||
- If you run into trouble, check that you have the `/dev/dri/` folder on your host.
|
||||
|
||||
Docker users should add the `--privileged` option to the container for access to the hardware.
|
||||
|
||||
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine on Linux and [DXVA2+QSV](https://trac.ffmpeg.org/wiki/Hardware/QuickSync) engine on Windows.
|
||||
|
||||
## AMD GPU
|
||||
|
||||
*I don't have the hardware to test this!!!*
|
||||
|
||||
**Supported on:** Linux binary, Docker, Hass Addon.
|
||||
|
||||
Docker users should install: `alexxit/go2rtc:master-hardware`. Docker users should add the `--privileged` option to the container for access to the hardware.
|
||||
|
||||
Hass Addon users should install **go2rtc master hardware** version.
|
||||
|
||||
**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine.
|
||||
|
||||
## NVIDIA GPU
|
||||
|
||||
**Supported on:** Windows binary, Linux binary, Docker.
|
||||
|
||||
Docker users should install: `alexxit/go2rtc:master-hardware`.
|
||||
|
||||
Read more [here](https://docs.frigate.video/configuration/hardware_acceleration) and [here](https://jellyfin.org/docs/general/administration/hardware-acceleration/#nvidia-hardware-acceleration-on-docker-linux).
|
||||
|
||||
**PS.** Supported via [CUDA](https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC) engine.
|
||||
|
||||
## Raspberry Pi 3
|
||||
|
||||
**Supported on:** Linux binary, Docker, Hass Addon.
|
||||
|
||||
I don't recommend using transcoding on the Raspberry Pi 3. It's extremely slow, even with hardware acceleration. Also, it may fail when transcoding a 2K+ stream.
|
||||
|
||||
## Raspberry Pi 4
|
||||
|
||||
*I don't have the hardware to test this!!!*
|
||||
|
||||
**Supported on:** Linux binary, Docker, Hass Addon.
|
||||
|
||||
**PS.** Supported via [v4l2m2m](https://lalitm.com/hw-encoding-raspi/) engine.
|
||||
|
||||
## macOS
|
||||
|
||||
In my tests, transcoding is faster on the M1 CPU than on the M1 GPU. Transcoding time on the M1 CPU is better than any Intel iGPU and comparable to an NVIDIA RTX 2070.
|
||||
|
||||
**PS.** Supported via [videotoolbox](https://trac.ffmpeg.org/wiki/HWAccelIntro#VideoToolbox) engine.
|
||||
|
||||
## Rockchip
|
||||
|
||||
- It's important to use a custom FFmpeg build with Rockchip support from [@nyanmisaka](https://github.com/nyanmisaka/ffmpeg-rockchip)
|
||||
- Static binaries from [@MarcA711](https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/)
|
||||
- It's important to have Linux kernel 5.10 or 6.1
|
||||
|
||||
**Tested**
|
||||
|
||||
- [Orange Pi 3B](https://www.armbian.com/orangepi3b/) with Armbian 6.1, supports transcoding H.264, H.265, MJPEG
|
||||
@@ -128,19 +128,32 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
||||
case EngineRKMPP:
|
||||
args.Codecs[i] = defaults[name+"/"+engine]
|
||||
|
||||
for j, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters = append(args.Filters[:j], args.Filters[j+1:]...)
|
||||
if !args.HasFilters("drawtext=") {
|
||||
args.Input = "-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga " + args.Input
|
||||
|
||||
width, height, _ := strings.Cut(filter[6:], ":")
|
||||
if width != "-1" {
|
||||
args.Codecs[i] += " -width " + width
|
||||
for i, filter := range args.Filters {
|
||||
if strings.HasPrefix(filter, "scale=") {
|
||||
args.Filters[i] = "scale_rkrga=" + filter[6:] + ":force_original_aspect_ratio=0"
|
||||
}
|
||||
if height != "-1" {
|
||||
args.Codecs[i] += " -height " + height
|
||||
if strings.HasPrefix(filter, "transpose=") {
|
||||
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
|
||||
args.Filters[i] = "vpp_rkrga=transpose=4" // reversal
|
||||
} else {
|
||||
args.Filters[i] = "vpp_rkrga=transpose=" + filter[10:]
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if len(args.Filters) > 0 {
|
||||
// fix if input doesn't support hwaccel, do nothing when support
|
||||
// insert as first filter before hardware scale and transpose
|
||||
args.InsertFilter("format=drm_prime|nv12,hwupload")
|
||||
}
|
||||
} else {
|
||||
// enable software pixel for drawtext, scale and transpose
|
||||
args.Input = "-hwaccel rkmpp -hwaccel_output_format nv12 -afbc rga " + args.Input
|
||||
|
||||
args.AddFilter("hwupload")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@ import (
|
||||
const (
|
||||
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -"
|
||||
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -"
|
||||
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp -f null -"
|
||||
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp -f null -"
|
||||
ProbeRKMPPJPEG = "-f lavfi -i testsrc2 -t 1 -c mjpeg_rkmpp -f null -"
|
||||
ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
||||
ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
||||
ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
||||
@@ -39,6 +40,10 @@ func ProbeAll(bin string) []*api.Source {
|
||||
Name: runToString(bin, ProbeRKMPPH265),
|
||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
|
||||
},
|
||||
{
|
||||
Name: runToString(bin, ProbeRKMPPJPEG),
|
||||
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineRKMPP,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +88,10 @@ func ProbeHardware(bin, name string) string {
|
||||
if run(bin, ProbeRKMPPH265) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
case "mjpeg":
|
||||
if run(bin, ProbeRKMPPJPEG) {
|
||||
return EngineRKMPP
|
||||
}
|
||||
}
|
||||
|
||||
return EngineSoftware
|
||||
|
||||
@@ -42,9 +42,11 @@ func NewProducer(url string) (core.Producer, error) {
|
||||
Codecs: []*core.Codec{
|
||||
// OPUS will always marked as OPUS/48000/2
|
||||
{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},
|
||||
{Name: core.CodecPCML, ClockRate: 16000},
|
||||
{Name: core.CodecPCM, ClockRate: 16000},
|
||||
{Name: core.CodecPCMA, ClockRate: 16000},
|
||||
{Name: core.CodecPCMU, ClockRate: 16000},
|
||||
{Name: core.CodecPCML, ClockRate: 8000},
|
||||
{Name: core.CodecPCM, ClockRate: 8000},
|
||||
{Name: core.CodecPCMA, ClockRate: 8000},
|
||||
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||
@@ -94,9 +96,11 @@ func (p *Producer) newURL() string {
|
||||
codec := receiver.Codec
|
||||
switch codec.Name {
|
||||
case core.CodecOpus:
|
||||
s += "#audio=opus"
|
||||
s += "#audio=opus/16000"
|
||||
case core.CodecAAC:
|
||||
s += "#audio=aac/16000"
|
||||
case core.CodecPCML:
|
||||
s += "#audio=pcml/" + strconv.Itoa(int(codec.ClockRate))
|
||||
case core.CodecPCM:
|
||||
s += "#audio=pcm/" + strconv.Itoa(int(codec.ClockRate))
|
||||
case core.CodecPCMA:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Flussonic
|
||||
|
||||
[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)
|
||||
|
||||
Support streams from [Flussonic](https://flussonic.com/) server. Related [issue](https://github.com/AlexxIT/go2rtc/issues/1678).
|
||||
@@ -0,0 +1,10 @@
|
||||
package flussonic
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/flussonic"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("flussonic", flussonic.Dial)
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
# GoPro
|
||||
|
||||
[`new in v1.8.3`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)
|
||||
|
||||
Support streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows.
|
||||
|
||||
Supported models: HERO9, HERO10, HERO11, HERO12.
|
||||
Supported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)
|
||||
|
||||
The other camera models have different APIs. I will try to add them in the next versions.
|
||||
Other camera models have different APIs. I will try to add them in future versions.
|
||||
|
||||
## Config
|
||||
## Configuration
|
||||
|
||||
- USB-connected cameras create a new network interface in the system
|
||||
- Linux users do not need to install anything
|
||||
- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)
|
||||
- if the camera is detected but the stream does not start - you need to disable firewall
|
||||
- if the camera is detected but the stream does not start, you need to disable the firewall
|
||||
|
||||
1. Discover camera address: WebUI > Add > GoPro
|
||||
2. Add camera to config
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Hass
|
||||
|
||||
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files:
|
||||
|
||||
- [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
|
||||
- [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)
|
||||
- [ONVIF](https://www.home-assistant.io/integrations/onvif/)
|
||||
- [Roborock](https://github.com/humbertogontijo/homeassistant-roborock) vacuums with camera
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
hass:
|
||||
config: "/config" # skip this setting if you are a Home Assistant add-on user
|
||||
|
||||
streams:
|
||||
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
|
||||
aqara_g3: hass:Camera-Hub-G3-AB12
|
||||
```
|
||||
|
||||
### WebRTC Cameras
|
||||
|
||||
[`new in v1.6.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)
|
||||
|
||||
Any cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this format.
|
||||
|
||||
**Important.** The Nest API only allows you to get a link to a stream for 5 minutes.
|
||||
Do not use this with Frigate! If the stream expires, Frigate will consume all available RAM on your machine within seconds.
|
||||
It's recommended to use [Nest source](../nest/README.md) - it supports extending the stream.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# link to Home Assistant Supervised
|
||||
hass-webrtc1: hass://supervisor?entity_id=camera.nest_doorbell
|
||||
# link to external Home Assistant with Long-Lived Access Tokens
|
||||
hass-webrtc2: hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=eyXYZ...
|
||||
```
|
||||
|
||||
### RTSP Cameras
|
||||
|
||||
By default, the Home Assistant API does not allow you to get a dynamic RTSP link to a camera stream. [This method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-cameras-from-home-assistant-to-go2rtc-or-frigate) can work around it.
|
||||
@@ -30,10 +30,10 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
||||
// 2. static link to Hass camera
|
||||
// 3. dynamic link to Hass camera
|
||||
if streams.Patch(v.Name, v.Channels.First.Url) != nil {
|
||||
if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {
|
||||
apiOK(w, r)
|
||||
} else {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// /stream/{id}/channel/0/webrtc
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -37,8 +38,13 @@ func Init() {
|
||||
api.HandleFunc("/streams", apiOK)
|
||||
api.HandleFunc("/stream/", apiStream)
|
||||
|
||||
streams.RedirectFunc("hass", func(url string) (string, error) {
|
||||
if location := entities[url[5:]]; location != "" {
|
||||
streams.RedirectFunc("hass", func(rawURL string) (string, error) {
|
||||
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||
|
||||
if location := entities[rawURL[5:]]; location != "" {
|
||||
if rawQuery != "" {
|
||||
return location + "#" + rawQuery, nil
|
||||
}
|
||||
return location, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
# HLS
|
||||
|
||||
[`new in v1.1.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)
|
||||
|
||||
[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming.
|
||||
It can only be useful on devices that do not support more modern technology, like [WebRTC](../webrtc/README.md), [MP4](../mp4/README.md).
|
||||
|
||||
The go2rtc implementation differs from the standards and may not work with all players.
|
||||
|
||||
API examples:
|
||||
|
||||
- HLS/TS stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1` (H264)
|
||||
- HLS/fMP4 stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` (H264, H265, AAC)
|
||||
|
||||
Read more about [codecs filters](../../README.md#codecs-filters).
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://walterebert.com/playground/video/hls/
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# Apple HomeKit
|
||||
|
||||
This module supports both client and server for the [Apple HomeKit](https://www.apple.com/home-app/accessories/) protocol.
|
||||
|
||||
## HomeKit Client
|
||||
|
||||
**Important:**
|
||||
|
||||
- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol
|
||||
- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home), you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc, you can't pair it with an iPhone
|
||||
- HomeKit device should be on the same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between the device and go2rtc
|
||||
|
||||
go2rtc supports importing paired HomeKit devices from [Home Assistant](../hass/README.md).
|
||||
So you can use HomeKit camera with Home Assistant and go2rtc simultaneously.
|
||||
If you are using Home Assistant, I recommend pairing devices with it; it will give you more options.
|
||||
|
||||
You can pair device with go2rtc on the HomeKit page. If you can't see your devices, reload the page.
|
||||
Also, try rebooting your HomeKit device (power off). If you still can't see it, you have a problem with mDNS.
|
||||
|
||||
If you see a device but it does not have a pairing button, it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge, etc.). You need to delete the device from that ecosystem, and it will be available for pairing. If you cannot unpair the device, you will have to reset it.
|
||||
|
||||
**Important:**
|
||||
|
||||
- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violations
|
||||
- Audio can't be played in `VLC` and probably any other player
|
||||
- Audio should be transcoded for use with MSE, WebRTC, etc.
|
||||
|
||||
### Client Configuration
|
||||
|
||||
Recommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
aqara_g3:
|
||||
- hass:Camera-Hub-G3-AB12
|
||||
- ffmpeg:aqara_g3#audio=aac#audio=opus
|
||||
```
|
||||
|
||||
RTSP link with "normal" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`
|
||||
|
||||
**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).
|
||||
|
||||
## HomeKit Server
|
||||
|
||||
[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)
|
||||
|
||||
HomeKit module can work in two modes:
|
||||
|
||||
- export any H264 camera to Apple HomeKit
|
||||
- transparent proxy any Apple HomeKit camera (Aqara, Eve, Eufy, etc.) back to Apple HomeKit, so you will have all camera features in Apple Home and also will have RTSP/WebRTC/MP4/etc. from your HomeKit camera
|
||||
|
||||
**Important**
|
||||
|
||||
- HomeKit cameras support only H264 video and OPUS audio
|
||||
|
||||
### Server Configuration
|
||||
|
||||
**Minimal config**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
||||
homekit:
|
||||
dahua1: # same stream ID from streams list, default PIN - 19550224
|
||||
```
|
||||
|
||||
**Full config**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua1:
|
||||
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0
|
||||
- ffmpeg:dahua1#video=h264#hardware # if your camera doesn't support H264, important for HomeKit
|
||||
- ffmpeg:dahua1#audio=opus # only OPUS audio supported by HomeKit
|
||||
|
||||
homekit:
|
||||
dahua1: # same stream ID from streams list
|
||||
pin: 12345678 # custom PIN, default: 19550224
|
||||
name: Dahua camera # custom camera name, default: generated from stream ID
|
||||
device_id: dahua1 # custom ID, default: generated from stream ID
|
||||
device_private: dahua1 # custom key, default: generated from stream ID
|
||||
```
|
||||
|
||||
**Proxy HomeKit camera**
|
||||
|
||||
- Video stream from HomeKit camera to Apple device (iPhone, Apple TV) will be transmitted directly
|
||||
- Video stream from HomeKit camera to RTSP/WebRTC/MP4/etc. will be transmitted via go2rtc
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
aqara1:
|
||||
- homekit://...
|
||||
- ffmpeg:aqara1#audio=aac#audio=opus # optional audio transcoding
|
||||
|
||||
homekit:
|
||||
aqara1: # same stream ID from streams list
|
||||
```
|
||||
+85
-43
@@ -3,6 +3,7 @@ package homekit
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -14,56 +15,97 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
)
|
||||
|
||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func apiDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
sources, err := discovery()
|
||||
if err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
urls := findHomeKitURLs()
|
||||
for id, u := range urls {
|
||||
deviceID := u.Query().Get("device_id")
|
||||
for _, source := range sources {
|
||||
if strings.Contains(source.URL, deviceID) {
|
||||
source.Location = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if source.Location == "" {
|
||||
source.Location = " "
|
||||
}
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
}
|
||||
|
||||
func apiHomekit(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
sources, err := discovery()
|
||||
if err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
urls := findHomeKitURLs()
|
||||
for id, u := range urls {
|
||||
deviceID := u.Query().Get("device_id")
|
||||
for _, source := range sources {
|
||||
if strings.Contains(source.URL, deviceID) {
|
||||
source.Location = id
|
||||
break
|
||||
}
|
||||
if id := r.Form.Get("id"); id != "" {
|
||||
if srv := servers[id]; srv != nil {
|
||||
api.ResponsePrettyJSON(w, srv)
|
||||
} else {
|
||||
http.Error(w, "server not found", http.StatusNotFound)
|
||||
}
|
||||
} else {
|
||||
api.ResponsePrettyJSON(w, servers)
|
||||
}
|
||||
|
||||
for _, source := range sources {
|
||||
if source.Location == "" {
|
||||
source.Location = " "
|
||||
}
|
||||
}
|
||||
|
||||
api.ResponseSources(w, sources)
|
||||
|
||||
case "POST":
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apiPair(r.Form.Get("id"), r.Form.Get("url")); err != nil {
|
||||
api.Error(w, err)
|
||||
id := r.Form.Get("id")
|
||||
rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin")
|
||||
if err := apiPair(id, rawURL); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
case "DELETE":
|
||||
if err := r.ParseMultipartForm(1024); err != nil {
|
||||
api.Error(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := apiUnpair(r.Form.Get("id")); err != nil {
|
||||
api.Error(w, err)
|
||||
id := r.Form.Get("id")
|
||||
if err := apiUnpair(id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
stream := streams.Get(id)
|
||||
if stream == nil {
|
||||
http.Error(w, "", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
rawURL := findHomeKitURL(stream.Sources())
|
||||
if rawURL == "" {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := hap.Dial(rawURL)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
res, err := client.Get(hap.PathAccessories)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", api.MimeJSON)
|
||||
_, _ = io.Copy(w, res.Body)
|
||||
}
|
||||
|
||||
func discovery() ([]*api.Source, error) {
|
||||
var sources []*api.Source
|
||||
|
||||
@@ -103,7 +145,7 @@ func apiPair(id, url string) error {
|
||||
|
||||
streams.New(id, conn.URL())
|
||||
|
||||
return app.PatchConfig(id, conn.URL(), "streams")
|
||||
return app.PatchConfig([]string{"streams", id}, conn.URL())
|
||||
}
|
||||
|
||||
func apiUnpair(id string) error {
|
||||
@@ -112,7 +154,7 @@ func apiUnpair(id string) error {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
rawURL := findHomeKitURL(stream)
|
||||
rawURL := findHomeKitURL(stream.Sources())
|
||||
if rawURL == "" {
|
||||
return errors.New("not homekit source")
|
||||
}
|
||||
@@ -123,15 +165,15 @@ func apiUnpair(id string) error {
|
||||
|
||||
streams.Delete(id)
|
||||
|
||||
return app.PatchConfig(id, nil, "streams")
|
||||
return app.PatchConfig([]string{"streams", id}, nil)
|
||||
}
|
||||
|
||||
func findHomeKitURLs() map[string]*url.URL {
|
||||
urls := map[string]*url.URL{}
|
||||
for id, stream := range streams.Streams() {
|
||||
if rawURL := findHomeKitURL(stream); rawURL != "" {
|
||||
for name, sources := range streams.GetAllSources() {
|
||||
if rawURL := findHomeKitURL(sources); rawURL != "" {
|
||||
if u, err := url.Parse(rawURL); err == nil {
|
||||
urls[id] = u
|
||||
urls[name] = u
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+51
-69
@@ -2,8 +2,6 @@ package homekit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -26,6 +24,7 @@ func Init() {
|
||||
Name string `yaml:"name"`
|
||||
DeviceID string `yaml:"device_id"`
|
||||
DevicePrivate string `yaml:"device_private"`
|
||||
CategoryID string `yaml:"category_id"`
|
||||
Pairings []string `yaml:"pairings"`
|
||||
} `yaml:"homekit"`
|
||||
}
|
||||
@@ -35,12 +34,15 @@ func Init() {
|
||||
|
||||
streams.HandleFunc("homekit", streamHandler)
|
||||
|
||||
api.HandleFunc("api/homekit", apiHandler)
|
||||
api.HandleFunc("api/homekit", apiHomekit)
|
||||
api.HandleFunc("api/homekit/accessories", apiHomekitAccessories)
|
||||
api.HandleFunc("api/discovery/homekit", apiDiscovery)
|
||||
|
||||
if cfg.Mod == nil {
|
||||
return
|
||||
}
|
||||
|
||||
hosts = map[string]*server{}
|
||||
servers = map[string]*server{}
|
||||
var entries []*mdns.ServiceEntry
|
||||
|
||||
@@ -63,36 +65,19 @@ func Init() {
|
||||
|
||||
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
|
||||
name := calcName(conf.Name, deviceID)
|
||||
setupID := calcSetupID(id)
|
||||
|
||||
srv := &server{
|
||||
stream: id,
|
||||
srtp: srtp.Server,
|
||||
pairings: conf.Pairings,
|
||||
setupID: setupID,
|
||||
}
|
||||
|
||||
srv.hap = &hap.Server{
|
||||
Pin: pin,
|
||||
DeviceID: deviceID,
|
||||
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
|
||||
GetPair: srv.GetPair,
|
||||
AddPair: srv.AddPair,
|
||||
Handler: homekit.ServerHandler(srv),
|
||||
}
|
||||
|
||||
if url := findHomeKitURL(stream); url != "" {
|
||||
// 1. Act as transparent proxy for HomeKit camera
|
||||
dial := func() (net.Conn, error) {
|
||||
client, err := homekit.Dial(url, srtp.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client.Conn(), nil
|
||||
}
|
||||
srv.hap.Handler = homekit.ProxyHandler(srv, dial)
|
||||
} else {
|
||||
// 2. Act as basic HomeKit camera
|
||||
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
|
||||
srv.hap.Handler = homekit.ServerHandler(srv)
|
||||
Pin: pin,
|
||||
DeviceID: deviceID,
|
||||
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
|
||||
GetClientPublic: srv.GetPair,
|
||||
}
|
||||
|
||||
srv.mdns = &mdns.ServiceEntry{
|
||||
@@ -106,22 +91,31 @@ func Init() {
|
||||
hap.TXTProtoVersion: "1.1",
|
||||
hap.TXTStateNumber: "1",
|
||||
hap.TXTStatusFlags: hap.StatusNotPaired,
|
||||
hap.TXTCategory: hap.CategoryCamera,
|
||||
hap.TXTSetupHash: srv.hap.SetupHash(),
|
||||
hap.TXTCategory: calcCategoryID(conf.CategoryID),
|
||||
hap.TXTSetupHash: hap.SetupHash(setupID, deviceID),
|
||||
},
|
||||
}
|
||||
entries = append(entries, srv.mdns)
|
||||
|
||||
srv.UpdateStatus()
|
||||
|
||||
if url := findHomeKitURL(stream.Sources()); url != "" {
|
||||
// 1. Act as transparent proxy for HomeKit camera
|
||||
srv.proxyURL = url
|
||||
} else {
|
||||
// 2. Act as basic HomeKit camera
|
||||
srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", app.Version)
|
||||
}
|
||||
|
||||
host := srv.mdns.Host(mdns.ServiceHAP)
|
||||
servers[host] = srv
|
||||
hosts[host] = srv
|
||||
servers[id] = srv
|
||||
|
||||
log.Trace().Msgf("[homekit] new server: %s", srv.mdns)
|
||||
}
|
||||
|
||||
api.HandleFunc(hap.PathPairSetup, hapPairSetup)
|
||||
api.HandleFunc(hap.PathPairVerify, hapPairVerify)
|
||||
|
||||
log.Trace().Msgf("[homekit] mdns: %s", entries)
|
||||
api.HandleFunc(hap.PathPairSetup, hapHandler)
|
||||
api.HandleFunc(hap.PathPairVerify, hapHandler)
|
||||
|
||||
go func() {
|
||||
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
||||
@@ -131,6 +125,7 @@ func Init() {
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
var hosts map[string]*server
|
||||
var servers map[string]*server
|
||||
|
||||
func streamHandler(rawURL string) (core.Producer, error) {
|
||||
@@ -142,52 +137,39 @@ func streamHandler(rawURL string) (core.Producer, error) {
|
||||
client, err := homekit.Dial(rawURL, srtp.Server)
|
||||
if client != nil && rawQuery != "" {
|
||||
query := streams.ParseQuery(rawQuery)
|
||||
client.MaxWidth = core.Atoi(query.Get("maxwidth"))
|
||||
client.MaxHeight = core.Atoi(query.Get("maxheight"))
|
||||
client.Bitrate = parseBitrate(query.Get("bitrate"))
|
||||
}
|
||||
|
||||
return client, err
|
||||
}
|
||||
|
||||
func hapPairSetup(w http.ResponseWriter, r *http.Request) {
|
||||
srv, ok := servers[r.Host]
|
||||
if !ok {
|
||||
func resolve(host string) *server {
|
||||
if len(hosts) == 1 {
|
||||
for _, srv := range hosts {
|
||||
return srv
|
||||
}
|
||||
}
|
||||
if srv, ok := hosts[host]; ok {
|
||||
return srv
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hapHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Can support multiple HomeKit cameras on single port ONLY for Apple devices.
|
||||
// Doesn't support Home Assistant and any other open source projects
|
||||
// because they don't send the host header in requests.
|
||||
srv := resolve(r.Host)
|
||||
if srv == nil {
|
||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if err = srv.hap.PairSetup(r, rw, conn); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
srv.Handle(w, r)
|
||||
}
|
||||
|
||||
func hapPairVerify(w http.ResponseWriter, r *http.Request) {
|
||||
srv, ok := servers[r.Host]
|
||||
if !ok {
|
||||
log.Error().Msg("[homekit] unknown host: " + r.Host)
|
||||
return
|
||||
}
|
||||
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
if err = srv.hap.PairVerify(r, rw, conn); err != nil && err != io.EOF {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
|
||||
func findHomeKitURL(stream *streams.Stream) string {
|
||||
sources := stream.Sources()
|
||||
func findHomeKitURL(sources []string) string {
|
||||
if len(sources) == 0 {
|
||||
return ""
|
||||
}
|
||||
@@ -200,7 +182,7 @@ func findHomeKitURL(stream *streams.Stream) string {
|
||||
if strings.HasPrefix(url, "hass") {
|
||||
location, _ := streams.Location(url)
|
||||
if strings.HasPrefix(location, "homekit") {
|
||||
return url
|
||||
return location
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+237
-97
@@ -4,10 +4,16 @@ import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg"
|
||||
@@ -16,23 +22,142 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/hds"
|
||||
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
stream string // stream name from YAML
|
||||
hap *hap.Server // server for HAP connection and encryption
|
||||
mdns *mdns.ServiceEntry
|
||||
srtp *srtp.Server
|
||||
accessory *hap.Accessory // HAP accessory
|
||||
pairings []string // pairings list
|
||||
hap *hap.Server // server for HAP connection and encryption
|
||||
mdns *mdns.ServiceEntry
|
||||
|
||||
streams map[string]*homekit.Consumer
|
||||
consumer *homekit.Consumer
|
||||
pairings []string // pairings list
|
||||
conns []any
|
||||
mu sync.Mutex
|
||||
|
||||
accessory *hap.Accessory // HAP accessory
|
||||
consumer *homekit.Consumer
|
||||
proxyURL string
|
||||
setupID string
|
||||
stream string // stream name from YAML
|
||||
}
|
||||
|
||||
func (s *server) MarshalJSON() ([]byte, error) {
|
||||
v := struct {
|
||||
Name string `json:"name"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Paired int `json:"paired,omitempty"`
|
||||
CategoryID string `json:"category_id,omitempty"`
|
||||
SetupCode string `json:"setup_code,omitempty"`
|
||||
SetupID string `json:"setup_id,omitempty"`
|
||||
Conns []any `json:"connections,omitempty"`
|
||||
}{
|
||||
Name: s.mdns.Name,
|
||||
DeviceID: s.mdns.Info[hap.TXTDeviceID],
|
||||
CategoryID: s.mdns.Info[hap.TXTCategory],
|
||||
Paired: len(s.pairings),
|
||||
Conns: s.conns,
|
||||
}
|
||||
if v.Paired == 0 {
|
||||
v.SetupCode = s.hap.Pin
|
||||
v.SetupID = s.setupID
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func (s *server) Handle(w http.ResponseWriter, r *http.Request) {
|
||||
conn, rw, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
// Fix reading from Body after Hijack.
|
||||
r.Body = io.NopCloser(rw)
|
||||
|
||||
switch r.RequestURI {
|
||||
case hap.PathPairSetup:
|
||||
id, key, err := s.hap.PairSetup(r, rw)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
s.AddPair(id, key, hap.PermissionAdmin)
|
||||
|
||||
case hap.PathPairVerify:
|
||||
id, key, err := s.hap.PairVerify(r, rw)
|
||||
if err != nil {
|
||||
log.Debug().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[homekit] %s: new conn", conn.RemoteAddr())
|
||||
|
||||
controller, err := hap.NewConn(conn, rw, key, false)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
|
||||
s.AddConn(controller)
|
||||
defer s.DelConn(controller)
|
||||
|
||||
var handler homekit.HandlerFunc
|
||||
|
||||
switch {
|
||||
case s.accessory != nil:
|
||||
handler = homekit.ServerHandler(s)
|
||||
case s.proxyURL != "":
|
||||
client, err := hap.Dial(s.proxyURL)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
handler = homekit.ProxyHandler(s, client.Conn)
|
||||
}
|
||||
|
||||
// If your iPhone goes to sleep, it will be an EOF error.
|
||||
if err = handler(controller); err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type logger struct {
|
||||
v any
|
||||
}
|
||||
|
||||
func (l logger) String() string {
|
||||
switch v := l.v.(type) {
|
||||
case *hap.Conn:
|
||||
return "hap " + v.RemoteAddr().String()
|
||||
case *hds.Conn:
|
||||
return "hds " + v.RemoteAddr().String()
|
||||
case *homekit.Consumer:
|
||||
return "rtp " + v.RemoteAddr
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (s *server) AddConn(v any) {
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] add conn %s", logger{v})
|
||||
s.mu.Lock()
|
||||
s.conns = append(s.conns, v)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) DelConn(v any) {
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] del conn %s", logger{v})
|
||||
s.mu.Lock()
|
||||
if i := slices.Index(s.conns, v); i >= 0 {
|
||||
s.conns = slices.Delete(s.conns, i, i+1)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) UpdateStatus() {
|
||||
@@ -44,12 +169,68 @@ func (s *server) UpdateStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) pairIndex(id string) int {
|
||||
id = "client_id=" + id
|
||||
for i, pairing := range s.pairings {
|
||||
if strings.HasPrefix(pairing, id) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (s *server) GetPair(id string) []byte {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if i := s.pairIndex(id); i >= 0 {
|
||||
query, _ := url.ParseQuery(s.pairings[i])
|
||||
b, _ := hex.DecodeString(query.Get("client_public"))
|
||||
return b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) AddPair(id string, public []byte, permissions byte) {
|
||||
log.Debug().Str("stream", s.stream).Msgf("[homekit] add pair id=%s public=%x perm=%d", id, public, permissions)
|
||||
|
||||
s.mu.Lock()
|
||||
if s.pairIndex(id) < 0 {
|
||||
s.pairings = append(s.pairings, fmt.Sprintf(
|
||||
"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
|
||||
))
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) DelPair(id string) {
|
||||
log.Debug().Str("stream", s.stream).Msgf("[homekit] del pair id=%s", id)
|
||||
|
||||
s.mu.Lock()
|
||||
if i := s.pairIndex(id); i >= 0 {
|
||||
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *server) PatchConfig() {
|
||||
if err := app.PatchConfig([]string{"homekit", s.stream, "pairings"}, s.pairings); err != nil {
|
||||
log.Error().Err(err).Msgf(
|
||||
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {
|
||||
return []*hap.Accessory{s.accessory}
|
||||
}
|
||||
|
||||
func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
|
||||
log.Trace().Msgf("[homekit] %s: get char aid=%d iid=0x%x", conn.RemoteAddr(), aid, iid)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] get char aid=%d iid=0x%x", aid, iid)
|
||||
|
||||
char := s.accessory.GetCharacterByID(iid)
|
||||
if char == nil {
|
||||
@@ -59,11 +240,12 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
|
||||
|
||||
switch char.Type {
|
||||
case camera.TypeSetupEndpoints:
|
||||
if s.consumer == nil {
|
||||
consumer := s.consumer
|
||||
if consumer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
answer := s.consumer.GetAnswer()
|
||||
answer := consumer.GetAnswer()
|
||||
v, err := tlv8.MarshalBase64(answer)
|
||||
if err != nil {
|
||||
return nil
|
||||
@@ -76,7 +258,7 @@ func (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
|
||||
}
|
||||
|
||||
func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
|
||||
log.Trace().Msgf("[homekit] %s: set char aid=%d iid=0x%x value=%v", conn.RemoteAddr(), aid, iid, value)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] set char aid=%d iid=0x%x value=%v", aid, iid, value)
|
||||
|
||||
char := s.accessory.GetCharacterByID(iid)
|
||||
if char == nil {
|
||||
@@ -86,61 +268,64 @@ func (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
|
||||
|
||||
switch char.Type {
|
||||
case camera.TypeSetupEndpoints:
|
||||
var offer camera.SetupEndpoints
|
||||
if err := tlv8.UnmarshalBase64(value.(string), &offer); err != nil {
|
||||
var offer camera.SetupEndpointsRequest
|
||||
if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
s.consumer = homekit.NewConsumer(conn, srtp2.Server)
|
||||
s.consumer.SetOffer(&offer)
|
||||
consumer := homekit.NewConsumer(conn, srtp2.Server)
|
||||
consumer.SetOffer(&offer)
|
||||
s.consumer = consumer
|
||||
|
||||
case camera.TypeSelectedStreamConfiguration:
|
||||
var conf camera.SelectedStreamConfig
|
||||
if err := tlv8.UnmarshalBase64(value.(string), &conf); err != nil {
|
||||
var conf camera.SelectedStreamConfiguration
|
||||
if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[homekit] %s stream id=%x cmd=%d", conn.RemoteAddr(), conf.Control.SessionID, conf.Control.Command)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)
|
||||
|
||||
switch conf.Control.Command {
|
||||
case camera.SessionCommandEnd:
|
||||
if consumer := s.streams[conf.Control.SessionID]; consumer != nil {
|
||||
_ = consumer.Stop()
|
||||
for _, consumer := range s.conns {
|
||||
if consumer, ok := consumer.(*homekit.Consumer); ok {
|
||||
if consumer.SessionID() == conf.Control.SessionID {
|
||||
_ = consumer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case camera.SessionCommandStart:
|
||||
if s.consumer == nil {
|
||||
consumer := s.consumer
|
||||
if consumer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.consumer.SetConfig(&conf) {
|
||||
if !consumer.SetConfig(&conf) {
|
||||
log.Warn().Msgf("[homekit] wrong config")
|
||||
return
|
||||
}
|
||||
|
||||
if s.streams == nil {
|
||||
s.streams = map[string]*homekit.Consumer{}
|
||||
}
|
||||
|
||||
s.streams[conf.Control.SessionID] = s.consumer
|
||||
s.AddConn(consumer)
|
||||
|
||||
stream := streams.Get(s.stream)
|
||||
if err := stream.AddConsumer(s.consumer); err != nil {
|
||||
if err := stream.AddConsumer(consumer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
_, _ = s.consumer.WriteTo(nil)
|
||||
stream.RemoveConsumer(s.consumer)
|
||||
_, _ = consumer.WriteTo(nil)
|
||||
stream.RemoveConsumer(consumer)
|
||||
|
||||
delete(s.streams, conf.Control.SessionID)
|
||||
s.DelConn(consumer)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) GetImage(conn net.Conn, width, height int) []byte {
|
||||
log.Trace().Msgf("[homekit] %s: get image width=%d height=%d", conn.RemoteAddr(), width, height)
|
||||
log.Trace().Str("stream", s.stream).Msgf("[homekit] get image width=%d height=%d", width, height)
|
||||
|
||||
stream := streams.Get(s.stream)
|
||||
cons := magic.NewKeyframe()
|
||||
@@ -166,69 +351,6 @@ func (s *server) GetImage(conn net.Conn, width, height int) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
func (s *server) GetPair(conn net.Conn, id string) []byte {
|
||||
log.Trace().Msgf("[homekit] %s: get pair id=%s", conn.RemoteAddr(), id)
|
||||
|
||||
for _, pairing := range s.pairings {
|
||||
if !strings.Contains(pairing, id) {
|
||||
continue
|
||||
}
|
||||
|
||||
query, err := url.ParseQuery(pairing)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if query.Get("client_id") != id {
|
||||
continue
|
||||
}
|
||||
|
||||
s := query.Get("client_public")
|
||||
b, _ := hex.DecodeString(s)
|
||||
return b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) AddPair(conn net.Conn, id string, public []byte, permissions byte) {
|
||||
log.Trace().Msgf("[homekit] %s: add pair id=%s public=%x perm=%d", conn.RemoteAddr(), id, public, permissions)
|
||||
|
||||
query := url.Values{
|
||||
"client_id": []string{id},
|
||||
"client_public": []string{hex.EncodeToString(public)},
|
||||
"permissions": []string{string('0' + permissions)},
|
||||
}
|
||||
if s.GetPair(conn, id) == nil {
|
||||
s.pairings = append(s.pairings, query.Encode())
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) DelPair(conn net.Conn, id string) {
|
||||
log.Trace().Msgf("[homekit] %s: del pair id=%s", conn.RemoteAddr(), id)
|
||||
|
||||
id = "client_id=" + id
|
||||
for i, pairing := range s.pairings {
|
||||
if !strings.Contains(pairing, id) {
|
||||
continue
|
||||
}
|
||||
|
||||
s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
|
||||
s.UpdateStatus()
|
||||
s.PatchConfig()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) PatchConfig() {
|
||||
if err := app.PatchConfig("pairings", s.pairings, "homekit", s.stream); err != nil {
|
||||
log.Error().Err(err).Msgf(
|
||||
"[homekit] can't save %s pairings=%v", s.stream, s.pairings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func calcName(name, seed string) string {
|
||||
if name != "" {
|
||||
return name
|
||||
@@ -263,3 +385,21 @@ func calcDevicePrivate(private, seed string) []byte {
|
||||
b := sha512.Sum512([]byte(seed))
|
||||
return ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])
|
||||
}
|
||||
|
||||
func calcSetupID(seed string) string {
|
||||
b := sha512.Sum512([]byte(seed))
|
||||
return fmt.Sprintf("%02X%02X", b[44], b[46])
|
||||
}
|
||||
|
||||
func calcCategoryID(categoryID string) string {
|
||||
switch categoryID {
|
||||
case "bridge":
|
||||
return hap.CategoryBridge
|
||||
case "doorbell":
|
||||
return hap.CategoryDoorbell
|
||||
}
|
||||
if core.Atoi(categoryID) > 0 {
|
||||
return categoryID
|
||||
}
|
||||
return hap.CategoryCamera
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# HTTP
|
||||
|
||||
This source supports receiving a stream via an HTTP link.
|
||||
|
||||
It can determine the source format from the`Content-Type` HTTP header:
|
||||
|
||||
- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream
|
||||
- **HTTP-MJPEG** (`multipart/x-mixed-replace`) - A continuous sequence of JPEG frames (with HTTP headers).
|
||||
- **HLS** (`application/vnd.apple.mpegurl`) - A popular [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) (HLS) format, which is not designed for real-time media transmission.
|
||||
|
||||
> [!WARNING]
|
||||
> The HLS format is not designed for real time and is supported quite poorly. It is recommended to use it via ffmpeg source with buffering enabled (disabled by default).
|
||||
|
||||
## TCP
|
||||
|
||||
Source also supports HTTP and TCP streams with autodetection for different formats:
|
||||
|
||||
- `adts` - Audio stream in [AAC](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) codec with Audio Data Transport Stream (ADTS) headers.
|
||||
- `flv` - The legacy but still used [Flash Video](https://en.wikipedia.org/wiki/Flash_Video) format.
|
||||
- `h264` - AVC/H.264 bitstream.
|
||||
- `hevc` - HEVC/H.265 bitstream.
|
||||
- `mjpeg` - A continuous sequence of JPEG frames (without HTTP headers).
|
||||
- `mpegts` - The legacy [MPEG transport stream](https://en.wikipedia.org/wiki/MPEG_transport_stream) format.
|
||||
- `wav` - Audio stream in [WAV](https://en.wikipedia.org/wiki/WAV) format.
|
||||
- `yuv4mpegpipe` - Raw YUV frame stream with YUV4MPEG header.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
# [HTTP-FLV] stream in video/x-flv format
|
||||
http_flv: http://192.168.1.123:20880/api/camera/stream/780900131155/657617
|
||||
|
||||
# [JPEG] snapshots from Dahua camera, will be converted to MJPEG stream
|
||||
dahua_snap: http://admin:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1
|
||||
|
||||
# [MJPEG] stream will be proxied without modification
|
||||
http_mjpeg: https://mjpeg.sanford.io/count.mjpeg
|
||||
|
||||
# [MJPEG or H.264/H.265 bitstream or MPEG-TS]
|
||||
tcp_magic: tcp://192.168.1.123:12345
|
||||
|
||||
# Add custom header
|
||||
custom_header: "https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX"
|
||||
```
|
||||
|
||||
**PS.** Dahua camera has a bug: if you select MJPEG codec for RTSP second stream, snapshot won't work.
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/AlexxIT/go2rtc/pkg/image"
|
||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpjpeg"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
||||
)
|
||||
|
||||
@@ -87,6 +88,9 @@ func do(req *http.Request) (core.Producer, error) {
|
||||
return image.Open(res)
|
||||
case ct == "multipart/x-mixed-replace":
|
||||
return mpjpeg.Open(res.Body)
|
||||
//https://www.iana.org/assignments/media-types/audio/basic
|
||||
case ct == "audio/basic":
|
||||
return pcm.Open(res.Body)
|
||||
}
|
||||
|
||||
return magic.Open(res.Body)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Hikvision ISAPI
|
||||
|
||||
[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)
|
||||
|
||||
This source type supports only backchannel audio for the [Hikvision ISAPI](https://tpp.hikvision.com/download/ISAPI_OTAP) protocol. So it should be used as a second source in addition to the RTSP protocol.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
hikvision1:
|
||||
- rtsp://admin:password@192.168.1.123:554/Streaming/Channels/101
|
||||
- isapi://admin:password@192.168.1.123:80/
|
||||
```
|
||||
@@ -0,0 +1,10 @@
|
||||
# Ivideon
|
||||
|
||||
Support public cameras from the service [Ivideon](https://tv.ivideon.com/).
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0
|
||||
```
|
||||
@@ -2,12 +2,9 @@ package ivideon
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ivideon", func(source string) (core.Producer, error) {
|
||||
return ivideon.Dial(source)
|
||||
})
|
||||
streams.HandleFunc("ivideon", ivideon.Dial)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# TP-Link Kasa
|
||||
|
||||
[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)
|
||||
|
||||
[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).
|
||||
|
||||
- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`
|
||||
- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed
|
||||
```
|
||||
|
||||
Tested: KD110, KC200, KC401, KC420WS, EC71.
|
||||
@@ -0,0 +1,13 @@
|
||||
package kasa
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/kasa"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("kasa", func(source string) (core.Producer, error) {
|
||||
return kasa.Dial(source)
|
||||
})
|
||||
}
|
||||
@@ -1,15 +1,71 @@
|
||||
## Stream as ASCII to Terminal
|
||||
# Motion JPEG
|
||||
|
||||
- This module can provide and receive streams in MJPEG format.
|
||||
- This module is also responsible for receiving snapshots in JPEG format.
|
||||
- This module also supports streaming to the server console (terminal) in the **animated ASCII art** format.
|
||||
|
||||
## MJPEG Client
|
||||
|
||||
**Important.** For a stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has the MJPEG codec, you can receive an **MJPEG stream** or **JPEG snapshots** via the API.
|
||||
|
||||
You can receive an MJPEG stream in several ways:
|
||||
|
||||
- some cameras support MJPEG codec inside [RTSP stream](../rtsp/README.md) (ex. second stream for Dahua cameras)
|
||||
- some cameras have an HTTP link with [MJPEG stream](../http/README.md)
|
||||
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](../http/README.md)
|
||||
- you can convert an H264/H265 stream from your camera via [FFmpeg integration](../ffmpeg/README.md)
|
||||
|
||||
With this example, your stream will have both H264 and MJPEG codecs:
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
camera1:
|
||||
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||
- ffmpeg:camera1#video=mjpeg
|
||||
```
|
||||
|
||||
## MJPEG Server
|
||||
|
||||
### mpjpeg
|
||||
|
||||
Output a stream in [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) format. In [FFmpeg](https://ffmpeg.org/), this format is called `mpjpeg` because it contains HTTP headers.
|
||||
|
||||
```
|
||||
ffplay http://192.168.1.123:1984/api/stream.mjpeg?src=camera1
|
||||
```
|
||||
|
||||
### jpeg
|
||||
|
||||
Receiving a JPEG snapshot.
|
||||
|
||||
```
|
||||
curl http://192.168.1.123:1984/api/frame.jpeg?src=camera1
|
||||
```
|
||||
|
||||
- You can use `width`/`w` and/or `height`/`h` parameters.
|
||||
- You can use `rotate` param with `90`, `180`, `270` or `-90` values.
|
||||
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
|
||||
- You can use `cache` param (`1m`, `10s`, etc.) to get a cached snapshot.
|
||||
- The snapshot is cached only when requested with the `cache` parameter.
|
||||
- A cached snapshot will be used if its time is not older than the time specified in the `cache` parameter.
|
||||
- The `cache` parameter does not check the image dimensions from the cache and those specified in the query.
|
||||
|
||||
### ascii
|
||||
|
||||
Stream as ASCII to Terminal. This format is just for fun. You can boast to your friends that you can stream cameras even to the server console without a GUI.
|
||||
|
||||
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
||||
|
||||
> The demo video features a combination of several settings for this format with added audio. Of course, the format doesn't support audio out of the box.
|
||||
|
||||
**Tips**
|
||||
|
||||
- this feature works only with MJPEG codec (use transcoding)
|
||||
- choose a low frame rate (FPS)
|
||||
- choose the width and height to fit in your terminal
|
||||
- different terminals support different numbers of colours (8, 256, rgb)
|
||||
- escape text param with urlencode
|
||||
- you can stream any camera or file from a disc
|
||||
- different terminals support different numbers of colors (8, 256, rgb)
|
||||
- URL-encode the `text` parameter
|
||||
- you can stream any camera or file from disk
|
||||
|
||||
**go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10
|
||||
|
||||
@@ -24,7 +80,7 @@ streams:
|
||||
- example: `30` (black), `37` (white), `38;5;226` (yellow)
|
||||
- `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
- example: `40` (black), `47` (white), `48;5;226` (yellow)
|
||||
- `text` - character set, values: empty, one char, `block`, list of chars (in order of brightness)
|
||||
- `text` - character set, values: empty, one character, `block`, list of chars (in order of brightness)
|
||||
- example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)
|
||||
|
||||
**Examples**
|
||||
@@ -36,3 +92,17 @@ streams:
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20"
|
||||
% curl "http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld"
|
||||
```
|
||||
|
||||
### yuv4mpegpipe
|
||||
|
||||
Raw [YUV](https://en.wikipedia.org/wiki/Y%E2%80%B2UV) frame stream with [YUV4MPEG](https://manned.org/yuv4mpeg) header.
|
||||
|
||||
```
|
||||
ffplay http://192.168.1.123:1984/api/stream.y4m?src=camera1
|
||||
```
|
||||
|
||||
## Streaming ingest
|
||||
|
||||
```shell
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c mjpeg -f mpjpeg http://localhost:1984/api/stream.mjpeg?dst=camera1
|
||||
```
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -36,13 +37,44 @@ func Init() {
|
||||
var log zerolog.Logger
|
||||
|
||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
query := r.URL.Query()
|
||||
stream, _ := streams.GetOrPatch(query)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
if s := query.Get("cache"); s != "" {
|
||||
if timeout, err := time.ParseDuration(s); err == nil {
|
||||
src := query.Get("src")
|
||||
|
||||
cacheMu.Lock()
|
||||
entry, found := cache[src]
|
||||
cacheMu.Unlock()
|
||||
|
||||
if found && time.Since(entry.timestamp) < timeout {
|
||||
writeJPEGResponse(w, entry.payload)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
entry = cacheEntry{payload: b, timestamp: time.Now()}
|
||||
cacheMu.Lock()
|
||||
if cache == nil {
|
||||
cache = map[string]cacheEntry{src: entry}
|
||||
} else {
|
||||
cache[src] = entry
|
||||
}
|
||||
cacheMu.Unlock()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
cons := magic.NewKeyframe()
|
||||
cons.WithRequest(r)
|
||||
|
||||
@@ -53,7 +85,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
once := &core.OnceBuffer{} // init and first frame
|
||||
_, _ = cons.WriteTo(once)
|
||||
b := once.Buffer()
|
||||
b = once.Buffer()
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
|
||||
@@ -61,7 +93,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
case core.CodecH264, core.CodecH265:
|
||||
ts := time.Now()
|
||||
var err error
|
||||
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
|
||||
if b, err = ffmpeg.JPEGWithQuery(b, query); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -70,6 +102,19 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||
b = mjpeg.FixJPEG(b)
|
||||
}
|
||||
|
||||
writeJPEGResponse(w, b)
|
||||
}
|
||||
|
||||
var cache map[string]cacheEntry
|
||||
var cacheMu sync.Mutex
|
||||
|
||||
// cacheEntry represents a cached keyframe with its timestamp
|
||||
type cacheEntry struct {
|
||||
payload []byte
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
func writeJPEGResponse(w http.ResponseWriter, b []byte) {
|
||||
h := w.Header()
|
||||
h.Set("Content-Type", "image/jpeg")
|
||||
h.Set("Content-Length", strconv.Itoa(len(b)))
|
||||
@@ -146,7 +191,7 @@ func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func handlerWS(tr *ws.Transport, _ *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
# MP4
|
||||
|
||||
This module provides several features:
|
||||
|
||||
1. MSE stream (fMP4 over WebSocket)
|
||||
2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](#snapshot-to-telegram)
|
||||
3. HTTP progressive streaming (MP4 file stream) - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 in this case.
|
||||
|
||||
## API examples
|
||||
|
||||
- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265)
|
||||
- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC)
|
||||
- MP4 file: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM)
|
||||
- You can use `mp4`, `mp4=flac` and `mp4=all` param for codec filters
|
||||
- You can use `duration` param in seconds (ex. `duration=15`)
|
||||
- You can use `filename` param (ex. `filename=record.mp4`)
|
||||
- You can use `rotate` param with `90`, `180` or `270` values
|
||||
- You can use `scale` param with positive integer values (ex. `scale=4:3`)
|
||||
|
||||
Read more about [codecs filters](../../README.md#codecs-filters).
|
||||
|
||||
**PS.** Rotate and scale params don't use transcoding and change video using metadata.
|
||||
|
||||
## Snapshot to Telegram
|
||||
|
||||
This examples for Home Assistant [Telegram Bot](https://www.home-assistant.io/integrations/telegram_bot/) integration.
|
||||
|
||||
- change `url` to your go2rtc web API (`http://localhost:1984/` for most users)
|
||||
- change `target` to your Telegram chat ID (support list)
|
||||
- change `src=camera1` to your stream name from go2rtc config
|
||||
|
||||
**Important.** Snapshot will be near instant for most cameras and many sources, except `ffmpeg` source. Because it takes a long time for ffmpeg to start streaming with video, even when you use `#video=copy`. Also the delay can be with cameras that do not start the stream with a keyframe.
|
||||
|
||||
### Snapshot from H264 or H265 camera
|
||||
|
||||
```yaml
|
||||
service: telegram_bot.send_video
|
||||
data:
|
||||
url: http://localhost:1984/api/frame.mp4?src=camera1
|
||||
target: 123456789
|
||||
```
|
||||
|
||||
### Record from H264 or H265 camera
|
||||
|
||||
Record from service call to the future. Doesn't support loopback.
|
||||
|
||||
- `mp4=flac` - adds support PCM audio family
|
||||
- `filename=record.mp4` - set name for downloaded file
|
||||
|
||||
```yaml
|
||||
service: telegram_bot.send_video
|
||||
data:
|
||||
url: http://localhost:1984/api/stream.mp4?src=camera1&mp4=flac&duration=5&filename=record.mp4 # duration in seconds
|
||||
target: 123456789
|
||||
```
|
||||
|
||||
### Snapshot from JPEG or MJPEG camera
|
||||
|
||||
This example works via the [mjpeg](../mjpeg/README.md) module.
|
||||
|
||||
```yaml
|
||||
service: telegram_bot.send_photo
|
||||
data:
|
||||
url: http://localhost:1984/api/frame.jpeg?src=camera1
|
||||
target: 123456789
|
||||
```
|
||||
+1
-1
@@ -91,7 +91,7 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
stream := streams.GetOrPatch(query)
|
||||
stream, _ := streams.GetOrPatch(query)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ func handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {
|
||||
}
|
||||
|
||||
func handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {
|
||||
stream := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||
if stream == nil {
|
||||
return errors.New(api.StreamNotFound)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# MPEG
|
||||
|
||||
This module provides an [HTTP API](../api/README.md) for:
|
||||
|
||||
- Streaming output in `mpegts` format.
|
||||
- Streaming output in `adts` format.
|
||||
- Streaming ingest in `mpegts` format.
|
||||
|
||||
## MPEG-TS Server
|
||||
|
||||
```shell
|
||||
ffplay http://localhost:1984/api/stream.ts?src=camera1
|
||||
```
|
||||
|
||||
## ADTS Server
|
||||
|
||||
```shell
|
||||
ffplay http://localhost:1984/api/stream.aac?src=camera1
|
||||
```
|
||||
|
||||
## Streaming ingest
|
||||
|
||||
```shell
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f mpegts http://localhost:1984/api/stream.ts?dst=camera1
|
||||
```
|
||||
@@ -1,10 +1,11 @@
|
||||
package mpegts
|
||||
package mpeg
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||
)
|
||||
|
||||
@@ -66,3 +67,26 @@ func inputMpegTS(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := aac.NewConsumer()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "audio/aac")
|
||||
|
||||
_, _ = cons.WriteTo(w)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package mpegts
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||
)
|
||||
|
||||
func apiStreamAAC(w http.ResponseWriter, r *http.Request) {
|
||||
src := r.URL.Query().Get("src")
|
||||
stream := streams.Get(src)
|
||||
if stream == nil {
|
||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
cons := aac.NewConsumer()
|
||||
cons.WithRequest(r)
|
||||
|
||||
if err := stream.AddConsumer(cons); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "audio/aac")
|
||||
|
||||
_, _ = cons.WriteTo(w)
|
||||
|
||||
stream.RemoveConsumer(cons)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
# TP-Link MULTITRANS
|
||||
|
||||
[`new in v1.9.14`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.14) by [@forrestsocool](https://github.com/forrestsocool)
|
||||
|
||||
Two-way audio support for Chinese version of [TP-Link](https://www.tp-link.com.cn/) cameras.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
tplink_cam:
|
||||
# video uses standard RTSP
|
||||
- rtsp://admin:admin@192.168.1.202:554/stream1
|
||||
# two-way audio uses MULTITRANS schema
|
||||
- multitrans://admin:admin@192.168.1.202:554
|
||||
```
|
||||
|
||||
## Useful links
|
||||
|
||||
- https://www.tp-link.com.cn/list_2549.html
|
||||
- https://github.com/AlexxIT/go2rtc/issues/1724
|
||||
- https://github.com/bingooo/hass-tplink-ipc/
|
||||
@@ -0,0 +1,10 @@
|
||||
package multitrans
|
||||
|
||||
import (
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/multitrans"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("multitrans", multitrans.Dial)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Google Nest
|
||||
|
||||
[`new in v1.6.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)
|
||||
|
||||
For simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](../hass/README.md).
|
||||
But if you can somehow get the below parameters, Nest/WebRTC source will work without Home Assistant.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***
|
||||
```
|
||||
@@ -2,6 +2,7 @@ package nest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
@@ -38,11 +39,12 @@ func apiNest(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var items []*api.Source
|
||||
|
||||
for name, deviceID := range devices {
|
||||
query.Set("device_id", deviceID)
|
||||
for _, device := range devices {
|
||||
query.Set("device_id", device.DeviceID)
|
||||
query.Set("protocols", strings.Join(device.Protocols, ","))
|
||||
|
||||
items = append(items, &api.Source{
|
||||
Name: name, URL: "nest:?" + query.Encode(),
|
||||
Name: device.Name, URL: "nest:?" + query.Encode(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# ngrok
|
||||
|
||||
With the ngrok integration, you can get external access to your streams when your Internet connection is behind a private IP address.
|
||||
|
||||
- you may need external access for two different things:
|
||||
- WebRTC streams (tunnel the WebRTC TCP port, e.g. 8555)
|
||||
- go2rtc web interface (tunnel the API HTTP port, e.g. 1984)
|
||||
- ngrok supports authorization for your web interface
|
||||
- ngrok automatically adds HTTPS to your web interface
|
||||
|
||||
The ngrok free subscription has the following limitations:
|
||||
|
||||
- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and will change with each restart of the ngrok agent (not a problem for WebRTC streams)
|
||||
- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan
|
||||
|
||||
go2rtc will automatically get your external TCP address (if you enable it in the ngrok config) and use it for WebRTC connections (if you enable it in the WebRTC config).
|
||||
|
||||
You need to manually download the [ngrok agent](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).
|
||||
|
||||
**Tunnel for only WebRTC Stream**
|
||||
|
||||
You need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
```
|
||||
|
||||
**Tunnel for WebRTC and Web interface**
|
||||
|
||||
You need to create `ngrok.yaml` config file and add it to the go2rtc config:
|
||||
|
||||
```yaml
|
||||
ngrok:
|
||||
command: ngrok start --all --config ngrok.yaml
|
||||
```
|
||||
|
||||
ngrok config example:
|
||||
|
||||
```yaml
|
||||
version: "2"
|
||||
authtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
|
||||
tunnels:
|
||||
api:
|
||||
addr: 1984 # use the same port as in the go2rtc config
|
||||
proto: http
|
||||
basic_auth:
|
||||
- admin:password # you can set login/pass for your web interface
|
||||
webrtc:
|
||||
addr: 8555 # use the same port as in the go2rtc config
|
||||
proto: tcp
|
||||
```
|
||||
|
||||
See the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.
|
||||
@@ -0,0 +1,42 @@
|
||||
# ONVIF
|
||||
|
||||
## ONVIF Client
|
||||
|
||||
[`new in v1.5.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)
|
||||
|
||||
The source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.
|
||||
|
||||
**WebUI > Add** webpage supports ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use Docker, you must use "network host".
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
dahua1: onvif://admin:password@192.168.1.123
|
||||
reolink1: onvif://admin:password@192.168.1.123:8000
|
||||
tapo1: onvif://admin:password@192.168.1.123:2020
|
||||
```
|
||||
|
||||
## ONVIF Server
|
||||
|
||||
A regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).
|
||||
|
||||
Go2rtc has one video source and one profile per stream.
|
||||
|
||||
## Tested clients
|
||||
|
||||
Go2rtc works as ONVIF server:
|
||||
|
||||
- Happytime onvif client (windows)
|
||||
- Home Assistant ONVIF integration (linux)
|
||||
- Onvier (android)
|
||||
- ONVIF Device Manager (windows)
|
||||
|
||||
PS. Supports only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.
|
||||
|
||||
## Tested cameras
|
||||
|
||||
Go2rtc works as ONVIF client:
|
||||
|
||||
- Dahua IPC-K42
|
||||
- OpenIPC
|
||||
- Reolink RLC-520A
|
||||
- TP-Link Tapo TC60
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
@@ -43,8 +44,17 @@ func streamOnvif(rawURL string) (core.Producer, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Append hash-based arguments to the retrieved URI
|
||||
if i := strings.IndexByte(rawURL, '#'); i > 0 {
|
||||
uri += rawURL[i:]
|
||||
}
|
||||
|
||||
log.Debug().Msgf("[onvif] new uri=%s", uri)
|
||||
|
||||
if err = streams.Validate(uri); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return streams.GetProducer(uri)
|
||||
}
|
||||
|
||||
@@ -55,66 +65,95 @@ func onvifDeviceService(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
action := onvif.GetRequestAction(b)
|
||||
if action == "" {
|
||||
operation := onvif.GetRequestAction(b)
|
||||
if operation == "" {
|
||||
http.Error(w, "malformed request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] %s", action)
|
||||
log.Trace().Msgf("[onvif] server request %s %s:\n%s", r.Method, r.RequestURI, b)
|
||||
|
||||
var res string
|
||||
switch operation {
|
||||
case onvif.ServiceGetServiceCapabilities, // important for Hass
|
||||
onvif.DeviceGetNetworkInterfaces, // important for Hass
|
||||
onvif.DeviceGetSystemDateAndTime, // important for Hass
|
||||
onvif.DeviceSetSystemDateAndTime, // return just OK
|
||||
onvif.DeviceGetDiscoveryMode,
|
||||
onvif.DeviceGetDNS,
|
||||
onvif.DeviceGetHostname,
|
||||
onvif.DeviceGetNetworkDefaultGateway,
|
||||
onvif.DeviceGetNetworkProtocols,
|
||||
onvif.DeviceGetNTP,
|
||||
onvif.DeviceGetScopes,
|
||||
onvif.MediaGetVideoEncoderConfiguration,
|
||||
onvif.MediaGetVideoEncoderConfigurations,
|
||||
onvif.MediaGetAudioEncoderConfigurations,
|
||||
onvif.MediaGetVideoEncoderConfigurationOptions,
|
||||
onvif.MediaGetAudioSources,
|
||||
onvif.MediaGetAudioSourceConfigurations:
|
||||
b = onvif.StaticResponse(operation)
|
||||
|
||||
switch action {
|
||||
case onvif.ActionGetCapabilities:
|
||||
case onvif.DeviceGetCapabilities:
|
||||
// important for Hass: Media section
|
||||
res = onvif.GetCapabilitiesResponse(r.Host)
|
||||
b = onvif.GetCapabilitiesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetSystemDateAndTime:
|
||||
// important for Hass
|
||||
res = onvif.GetSystemDateAndTimeResponse()
|
||||
case onvif.DeviceGetServices:
|
||||
b = onvif.GetServicesResponse(r.Host)
|
||||
|
||||
case onvif.ActionGetNetworkInterfaces:
|
||||
// important for Hass: none
|
||||
res = onvif.GetNetworkInterfacesResponse()
|
||||
|
||||
case onvif.ActionGetDeviceInformation:
|
||||
case onvif.DeviceGetDeviceInformation:
|
||||
// important for Hass: SerialNumber (unique server ID)
|
||||
res = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
b = onvif.GetDeviceInformationResponse("", "go2rtc", app.Version, r.Host)
|
||||
|
||||
case onvif.ActionGetServiceCapabilities:
|
||||
// important for Hass
|
||||
res = onvif.GetServiceCapabilitiesResponse()
|
||||
|
||||
case onvif.ActionSystemReboot:
|
||||
res = onvif.SystemRebootResponse()
|
||||
case onvif.DeviceSystemReboot:
|
||||
b = onvif.StaticResponse(operation)
|
||||
|
||||
time.AfterFunc(time.Second, func() {
|
||||
os.Exit(0)
|
||||
})
|
||||
|
||||
case onvif.ActionGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
res = onvif.GetProfilesResponse(streams.GetAll())
|
||||
case onvif.MediaGetVideoSources:
|
||||
b = onvif.GetVideoSourcesResponse(streams.GetAllNames())
|
||||
|
||||
case onvif.ActionGetStreamUri:
|
||||
case onvif.MediaGetProfiles:
|
||||
// important for Hass: H264 codec, width, height
|
||||
b = onvif.GetProfilesResponse(streams.GetAllNames())
|
||||
|
||||
case onvif.MediaGetProfile:
|
||||
token := onvif.FindTagValue(b, "ProfileToken")
|
||||
b = onvif.GetProfileResponse(token)
|
||||
|
||||
case onvif.MediaGetVideoSourceConfigurations:
|
||||
// important for Happytime Onvif Client
|
||||
b = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())
|
||||
|
||||
case onvif.MediaGetVideoSourceConfiguration:
|
||||
token := onvif.FindTagValue(b, "ConfigurationToken")
|
||||
b = onvif.GetVideoSourceConfigurationResponse(token)
|
||||
|
||||
case onvif.MediaGetStreamUri:
|
||||
host, _, err := net.SplitHostPort(r.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
host = r.Host // in case of Host without port
|
||||
}
|
||||
|
||||
uri := "rtsp://" + host + ":" + rtsp.Port + "/" + onvif.FindTagValue(b, "ProfileToken")
|
||||
res = onvif.GetStreamUriResponse(uri)
|
||||
b = onvif.GetStreamUriResponse(uri)
|
||||
|
||||
case onvif.MediaGetSnapshotUri:
|
||||
uri := "http://" + r.Host + "/api/frame.jpeg?src=" + onvif.FindTagValue(b, "ProfileToken")
|
||||
b = onvif.GetSnapshotUriResponse(uri)
|
||||
|
||||
default:
|
||||
http.Error(w, "unsupported action", http.StatusBadRequest)
|
||||
http.Error(w, "unsupported operation", http.StatusBadRequest)
|
||||
log.Warn().Msgf("[onvif] unsupported operation: %s", operation)
|
||||
log.Debug().Msgf("[onvif] unsupported request:\n%s", b)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace().Msgf("[onvif] server response:\n%s", b)
|
||||
|
||||
w.Header().Set("Content-Type", "application/soap+xml; charset=utf-8")
|
||||
if _, err = w.Write([]byte(res)); err != nil {
|
||||
if _, err = w.Write(b); err != nil {
|
||||
log.Error().Err(err).Caller().Send()
|
||||
}
|
||||
}
|
||||
@@ -125,21 +164,21 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||
var items []*api.Source
|
||||
|
||||
if src == "" {
|
||||
urls, err := onvif.DiscoveryStreamingURLs()
|
||||
devices, err := onvif.DiscoveryStreamingDevices()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, rawURL := range urls {
|
||||
u, err := url.Parse(rawURL)
|
||||
for _, device := range devices {
|
||||
u, err := url.Parse(device.URL)
|
||||
if err != nil {
|
||||
log.Warn().Str("url", rawURL).Msg("[onvif] broken")
|
||||
log.Warn().Str("url", device.URL).Msg("[onvif] broken")
|
||||
continue
|
||||
}
|
||||
|
||||
if u.Scheme != "http" {
|
||||
log.Warn().Str("url", rawURL).Msg("[onvif] unsupported")
|
||||
log.Warn().Str("url", device.URL).Msg("[onvif] unsupported")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -150,7 +189,11 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||
u.Path = ""
|
||||
}
|
||||
|
||||
items = append(items, &api.Source{Name: u.Host, URL: u.String()})
|
||||
items = append(items, &api.Source{
|
||||
Name: u.Host,
|
||||
URL: u.String(),
|
||||
Info: device.Name + " " + device.Hardware,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
client, err := onvif.NewClient(src)
|
||||
@@ -160,7 +203,7 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if l := log.Trace(); l.Enabled() {
|
||||
b, _ := client.GetProfiles()
|
||||
b, _ := client.MediaRequest(onvif.MediaGetProfiles)
|
||||
l.Msgf("[onvif] src=%s profiles:\n%s", src, b)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# Pinggy
|
||||
|
||||
[Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services.
|
||||
|
||||
**Features:**
|
||||
|
||||
- A free account does not require registration.
|
||||
- It does not require downloading third-party binaries and works over the SSH protocol.
|
||||
- Works with HTTP, TCP and UDP protocols.
|
||||
- Creates HTTPS for your HTTP services.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY.
|
||||
|
||||
> [!CAUTION]
|
||||
> Public access to go2rtc without authorization puts your entire home network at risk. Use with caution.
|
||||
|
||||
**Why:**
|
||||
|
||||
- It's easy to set up HTTPS for testing two-way audio.
|
||||
- It's easy to check whether external access via WebRTC technology will work.
|
||||
- It's easy to share direct access to your RTSP or HTTP camera with the go2rtc developer. If such access is necessary to debug your problem.
|
||||
|
||||
## Configuration
|
||||
|
||||
You will find public links in the go2rtc log after startup.
|
||||
|
||||
**Tunnel to go2rtc WebUI.**
|
||||
|
||||
```yaml
|
||||
pinggy:
|
||||
tunnel: http://localhost:1984
|
||||
```
|
||||
|
||||
**Tunnel to RTSP camera.**
|
||||
|
||||
For example, you have camera: `rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0`
|
||||
|
||||
```yaml
|
||||
pinggy:
|
||||
tunnel: tcp://192.168.10.91:554
|
||||
```
|
||||
|
||||
In go2rtc logs you will get similar output:
|
||||
|
||||
```
|
||||
16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345
|
||||
```
|
||||
|
||||
Now you have a working stream:
|
||||
|
||||
```
|
||||
rtsp://admin:password@abcde-123-123-123-123.a.free.pinggy.link:12345/cam/realmonitor?channel=1&subtype=0
|
||||
```
|
||||
@@ -0,0 +1,60 @@
|
||||
package pinggy
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/app"
|
||||
"github.com/AlexxIT/go2rtc/pkg/pinggy"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
var cfg struct {
|
||||
Mod struct {
|
||||
Tunnel string `yaml:"tunnel"`
|
||||
} `yaml:"pinggy"`
|
||||
}
|
||||
|
||||
app.LoadConfig(&cfg)
|
||||
|
||||
if cfg.Mod.Tunnel == "" {
|
||||
return
|
||||
}
|
||||
|
||||
log = app.GetLogger("pinggy")
|
||||
|
||||
u, err := url.Parse(cfg.Mod.Tunnel)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
go proxy(u.Scheme, u.Host)
|
||||
}
|
||||
|
||||
var log zerolog.Logger
|
||||
|
||||
func proxy(proto, address string) {
|
||||
client, err := pinggy.NewClient(proto)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
urls, err := client.GetURLs()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range urls {
|
||||
log.Info().Str("url", s).Msgf("[pinggy] proxy")
|
||||
}
|
||||
|
||||
err = client.Proxy(address)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# Ring
|
||||
|
||||
[`new in v1.9.13`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13) by [@seydx](https://github.com/seydx)
|
||||
|
||||
This source type supports Ring cameras with [two-way audio](../../README.md#two-way-audio) support.
|
||||
|
||||
## Configuration
|
||||
|
||||
If you have a `refresh_token` and `device_id`, you can use them in the `go2rtc.yaml` config file.
|
||||
|
||||
Otherwise, you can use the go2rtc web interface and add your Ring account (WebUI > Add > Ring). Once added, it will list all your Ring cameras.
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
ring: ring:?device_id=XXX&refresh_token=XXX
|
||||
ring_snapshot: ring:?device_id=XXX&refresh_token=XXX&snapshot
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/AlexxIT/go2rtc/internal/api"
|
||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||
"github.com/AlexxIT/go2rtc/pkg/ring"
|
||||
)
|
||||
|
||||
func Init() {
|
||||
streams.HandleFunc("ring", func(source string) (core.Producer, error) {
|
||||
return ring.Dial(source)
|
||||
})
|
||||
|
||||
api.HandleFunc("api/ring", apiRing)
|
||||
}
|
||||
|
||||
func apiRing(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
var ringAPI *ring.RingApi
|
||||
|
||||
// Check auth method
|
||||
if email := query.Get("email"); email != "" {
|
||||
// Email/Password Flow
|
||||
password := query.Get("password")
|
||||
code := query.Get("code")
|
||||
|
||||
var err error
|
||||
ringAPI, err = ring.NewRestClient(ring.EmailAuth{
|
||||
Email: email,
|
||||
Password: password,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Try authentication (this will trigger 2FA if needed)
|
||||
if _, err = ringAPI.GetAuth(code); err != nil {
|
||||
if ringAPI.Using2FA {
|
||||
// Return 2FA prompt
|
||||
api.ResponseJSON(w, map[string]interface{}{
|
||||
"needs_2fa": true,
|
||||
"prompt": ringAPI.PromptFor2FA,
|
||||
})
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else if refreshToken := query.Get("refresh_token"); refreshToken != "" {
|
||||
// Refresh Token Flow
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "either email/password or refresh_token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
ringAPI, err = ring.NewRestClient(ring.RefreshTokenAuth{
|
||||
RefreshToken: refreshToken,
|
||||
}, nil)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "either email/password or refresh token is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := ringAPI.FetchRingDevices()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cleanQuery := url.Values{}
|
||||
cleanQuery.Set("refresh_token", ringAPI.RefreshToken)
|
||||
|
||||
var items []*api.Source
|
||||
for _, camera := range devices.AllCameras {
|
||||
cleanQuery.Set("camera_id", fmt.Sprint(camera.ID))
|
||||
cleanQuery.Set("device_id", camera.DeviceID)
|
||||
|
||||
// Stream source
|
||||
items = append(items, &api.Source{
|
||||
Name: camera.Description,
|
||||
URL: "ring:?" + cleanQuery.Encode(),
|
||||
})
|
||||
|
||||
// Snapshot source
|
||||
items = append(items, &api.Source{
|
||||
Name: camera.Description + " Snapshot",
|
||||
URL: "ring:?" + cleanQuery.Encode() + "&snapshot",
|
||||
})
|
||||
}
|
||||
|
||||
api.ResponseSources(w, items)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# Roborock
|
||||
|
||||
[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)
|
||||
|
||||
This source type supports Roborock vacuums with cameras. Known working models:
|
||||
|
||||
- **Roborock S6 MaxV** - only video (the vacuum has no microphone)
|
||||
- **Roborock S7 MaxV** - video and two-way audio
|
||||
- **Roborock Qrevo MaxV** - video and two-way audio
|
||||
|
||||
## Configuration
|
||||
|
||||
This source supports loading Roborock credentials from the Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to go2rtc WebUI > Add webpage. Copy the `roborock://...` source for your vacuum and paste it into your `go2rtc.yaml` config.
|
||||
|
||||
If you have a pattern PIN for your vacuum, add it as a numeric PIN (lines: 123, 456, 789) to the end of the `roborock` link.
|
||||
@@ -0,0 +1,118 @@
|
||||
# Real-Time Messaging Protocol
|
||||
|
||||
This module provides the following features for the RTMP protocol:
|
||||
|
||||
- Streaming input - [RTMP client](#rtmp-client)
|
||||
- Streaming output and ingest in `rtmp` format - [RTMP server](#rtmp-server)
|
||||
- Streaming output and ingest in `flv` format - [FLV server](#flv-server)
|
||||
|
||||
## RTMP Client
|
||||
|
||||
You can get a stream from an RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).
|
||||
|
||||
### Client Configuration
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
rtmp_stream: rtmp://192.168.1.123/live/camera1
|
||||
```
|
||||
|
||||
## RTMP Server
|
||||
|
||||
[`new in v1.8.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)
|
||||
|
||||
Streaming output stream in `rtmp` format:
|
||||
|
||||
```shell
|
||||
ffplay rtmp://localhost:1935/camera1
|
||||
```
|
||||
|
||||
Streaming ingest stream in `rtmp` format:
|
||||
|
||||
```shell
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f flv rtmp://localhost:1935/camera1
|
||||
```
|
||||
|
||||
### Server Configuration
|
||||
|
||||
By default, the RTMP server is disabled.
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
listen: ":1935" # by default - disabled!
|
||||
```
|
||||
|
||||
## FLV Server
|
||||
|
||||
Streaming output in `flv` format.
|
||||
|
||||
```shell
|
||||
ffplay http://localhost:1984/stream.flv?src=camera1
|
||||
```
|
||||
|
||||
Streaming ingest in `flv` format.
|
||||
|
||||
```shell
|
||||
ffmpeg -re -i BigBuckBunny.mp4 -c copy -f flv http://localhost:1984/api/stream.flv?dst=camera1
|
||||
```
|
||||
|
||||
## Tested clients
|
||||
|
||||
| From | To | Comment |
|
||||
|--------|---------------------------------|---------|
|
||||
| go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK |
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
streams:
|
||||
rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password
|
||||
rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password
|
||||
rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password
|
||||
```
|
||||
|
||||
## Tested server
|
||||
|
||||
| From | To | Comment |
|
||||
|------------------------|--------|---------------------|
|
||||
| OBS 31.0.2 | go2rtc | OK |
|
||||
| OpenIPC 2.5.03.02-lite | go2rtc | OK |
|
||||
| FFmpeg 6.1 | go2rtc | OK |
|
||||
| GoPro Black 12 | go2rtc | OK, 1080p, 5000kbps |
|
||||
|
||||
**go2rtc.yaml**
|
||||
|
||||
```yaml
|
||||
rtmp:
|
||||
listen: :1935
|
||||
streams:
|
||||
tmp:
|
||||
```
|
||||
|
||||
**OBS**
|
||||
|
||||
Settings > Stream:
|
||||
|
||||
- Service: Custom
|
||||
- Server: rtmp://192.168.10.101/tmp
|
||||
- Stream Key: `<empty>`
|
||||
- Use auth: `<disabled>`
|
||||
|
||||
**OpenIPC**
|
||||
|
||||
WebUI > Majestic > Settings > Outgoing
|
||||
|
||||
- Enable
|
||||
- Address: rtmp://192.168.10.101/tmp
|
||||
- Save
|
||||
- Restart
|
||||
|
||||
**FFmpeg**
|
||||
|
||||
```shell
|
||||
ffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp
|
||||
```
|
||||
|
||||
**GoPro**
|
||||
|
||||
GoPro Quik > Camera > Translation > Other
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user