Compare commits
1387 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 8cca8decde | |||
| be5bbd3b9b | |||
| 3f94a754e4 | |||
| 780f378fb1 | |||
| b874c17bcb | |||
| 16e4831499 | |||
| 9d709f0db8 | |||
| a8d394efd7 | |||
| 95a5283c86 | |||
| ef7d898747 | |||
| 388c408080 | |||
| 7b77e41253 | |||
| c0bfebf3a4 | |||
| 6f9f1c3a35 | |||
| 8128edad43 | |||
| eb8a13d8c2 | |||
| 8399edce6a | |||
| 2311d5eabe | |||
| afc8f4fdf6 | |||
| 66de2f91b6 | |||
| bd88695e59 | |||
| 23e8f7e0aa | |||
| d559ec0208 | |||
| ed99025bd6 | |||
| 57d48f53e0 | |||
| 68fa42249e | |||
| c5bc761a52 | |||
| 3762bdbccd | |||
| c81caa4d2c | |||
| 13dd3084c2 | |||
| e1021a96af | |||
| 5b0781253f | |||
| a04b7eed28 | |||
| c47427633c | |||
| 56e2c6650d | |||
| 82f0fb8a79 | |||
| 0e5b293b1f | |||
| eaae7aee39 | |||
| a4885c2c3a | |||
| 2b69eb2fd0 | |||
| f5aaee006e | |||
| db6745e8ff | |||
| ba34855602 | |||
| e6fa97c738 | |||
| 5b481a27c6 | |||
| bdc7ff1035 | |||
| da5f060741 | |||
| a56d335380 | |||
| d8aed552bc | |||
| d7286fa06e | |||
| 906f554d74 | |||
| cb44d5431a | |||
| a69eb8a66e | |||
| 1b411b1fed | |||
| 5d57959608 | |||
| 31e57c2ff8 | |||
| 734393d638 | |||
| 96504e2fb0 | |||
| ecfe802065 | |||
| 1ac9d54dab | |||
| 72d7e8aaaa | |||
| 0395696866 | |||
| 0667683e4d | |||
| aca0781c4b | |||
| ac798d9d6d | |||
| b389d0eb9c | |||
| e46fc13fea | |||
| bce0b4a8a0 | |||
| bf303ed471 | |||
| cd777ba2b4 | |||
| e3188a0a6d | |||
| 2bab0a014d | |||
| a01da18018 | |||
| 9d5a5c1e45 | |||
| 8377ad1d05 | |||
| ec33796bd3 | |||
| 31e4ba2722 | |||
| e0b1a50356 | |||
| 9bb36ebb6c | |||
| 756be9801e | |||
| bd73b07ed8 | |||
| df1d44d24e | |||
| 79245eeff4 | |||
| aa86c1ec25 | |||
| 2ab1d9d774 | |||
| a9e7a73cc8 | |||
| ea17b420d6 | |||
| 660979dfda | |||
| a6b9b4993f | |||
| cc74504ed8 | |||
| 791239be12 | |||
| a79061c7c2 | |||
| 50ad3b20c4 | |||
| 649de0131c | |||
| 8cb513cb89 | |||
| 3932dbaa84 | |||
| 4534b4d8ca | |||
| 8e571a66e3 | |||
| 0ccfcb0ec0 | |||
| 8bae4631d2 | |||
| 268629f551 | |||
| 0bd2fcde54 | |||
| 6f34cf0c95 | |||
| f8bc25d0ae | |||
| 8749562c96 | |||
| d9d2bdff44 | |||
| 562046c278 | |||
| 4cc28977cb | |||
| 3ce4624aee | |||
| b3e9ed23ac | |||
| bf3f81ccac | |||
| ff39e2e496 | |||
| d2346a2aed | |||
| 8f57b1acb6 | |||
| 6fafd10482 | |||
| c726651b8b | |||
| 02af2e2849 | |||
| 6d9c7012b0 | |||
| 8a7712a4c8 | |||
| 82fa803a37 | |||
| 78a74da8d6 | |||
| 53242ea02f | |||
| af05083a1f | |||
| c41bddbbea | |||
| 54c8ca0112 | |||
| a518488289 | |||
| 99cc21aacb | |||
| bc8295baee | |||
| 50f9913c41 | |||
| 4c135b5a46 | |||
| 686fb374e9 | |||
| 2b3e6a2730 | |||
| 9143729042 | |||
| 3952f0ba0f | |||
| 7a131822db | |||
| b2399f3bb3 | |||
| 2a8a3f1cbf | |||
| b1ba5bab62 | |||
| 6878f05e57 | |||
| d428a8964a | |||
| f432e72dd0 | |||
| 2929db9cec | |||
| 6d967bc1f9 | |||
| 83c0053b2c | |||
| ecfd7404f5 | |||
| 41badbfb8e | |||
| 0cb013a7fd | |||
| 75020d4df7 | |||
| 69c288b154 | |||
| 0ea651db62 | |||
| 4823e60a92 | |||
| c4949eb81f | |||
| aa4c81c266 | |||
| 063fef5813 | |||
| d9fb734c85 | |||
| a51156cf18 | |||
| 32e0ee4a10 | |||
| e6bea97936 | |||
| 9776e09ca7 | |||
| ad273d3a98 | |||
| 69c301e79f | |||
| 8f2bb3f34b | |||
| e4ff6d224f | |||
| 00751459a2 | |||
| 874c07b887 | |||
| 152df3ef5d | |||
| c950bb0252 | |||
| dd7ea2657a | |||
| 5889791847 | |||
| 9160403b99 | |||
| 5ccbd7c1c2 | |||
| 778245dd1c | |||
| 205018c96a | |||
| eaba451a47 | |||
| b7c11db604 | |||
| f7b98044e6 | |||
| 1b1bdb37db | |||
| ab453d275e | |||
| ee387b79e1 | |||
| e71ed5e7eb | |||
| 122a550599 | |||
| f3f08afac8 | |||
| a0030194cb | |||
| f158ffb33e | |||
| a9f2b5158c | |||
| b9f984dad0 | |||
| 290e011061 | |||
| 2b8ced9c59 | |||
| 09109e783e | |||
| 8ac834bdd4 | |||
| 06d8503fd0 | |||
| 4c3de3bbf4 | |||
| 4933c1415b | |||
| 322c332170 | |||
| 5d9c254282 | |||
| a03db503c3 | |||
| 2ea66deb08 | |||
| b3c5ef8c86 | |||
| fb1e7613cb | |||
| 8a7ab63b00 | |||
| 07f51e6929 | |||
| f64d279672 | |||
| 4185202496 | |||
| edbcd3e736 | |||
| abe617a346 | |||
| 9c98f5e769 | |||
| b4a524f46d | |||
| 297096a93b | |||
| e23e64ab00 | |||
| 0698f90273 | |||
| bec792797d | |||
| fd6014c11f | |||
| b8b90aba51 | |||
| 652dc93e9a | |||
| 6f1cc94ea5 | |||
| 52832223f8 | |||
| e080eac204 | |||
| 7a0646fd5f | |||
| 732fe47836 | |||
| 4e0185cfe6 | |||
| 5f2d523242 | |||
| 64ac27d93d | |||
| d6774bbdb9 | |||
| a1983c725d | |||
| 070ea3892f | |||
| cf4f6468f3 | |||
| c7af5028be | |||
| 9527a2be2e | |||
| ee5c663467 | |||
| e304035f76 | |||
| d96701453d | |||
| 1682d18ba6 | |||
| fb756b7473 | |||
| 3bc5274461 | |||
| 5f0366ac32 | |||
| abda47045d | |||
| 51c5d51786 | |||
| c309bb83e7 | |||
| 0eeb3c7585 | |||
| ae29b8271f | |||
| ab405b35f3 | |||
| 8d6aabce7a | |||
| 8516f825e1 | |||
| bcfc64bef1 | |||
| 1d59c02745 | |||
| 12a75034c7 | |||
| fffb22dd1f | |||
| 65b5ca2dec | |||
| ef74fb8497 | |||
| 675476a8f6 | |||
| 2d86ffd18c | |||
| a1be812052 | |||
| 9c534b1df5 | |||
| 261feb5858 | |||
| e4d970233e | |||
| 7bd346c402 | |||
| 439319141b | |||
| a404c2c86c | |||
| 6cf3cd142a | |||
| 418cabb852 | |||
| 2ce8cec12f | |||
| 905ef9b1ba | |||
| 7dc9eaa543 | |||
| 215d55771c | |||
| ac3d931576 | |||
| fcfef3080a | |||
| e610081634 | |||
| 484d401021 | |||
| 55d95691c8 | |||
| 2d8ef99df2 | |||
| 01e2ed2306 | |||
| 166287ce1b | |||
| 8495c7350e | |||
| 40dd3907a0 | |||
| 621d2e017e | |||
| d0a9c7a126 | |||
| 2301d8d7b2 | |||
| d28ae5caea | |||
| 5cf343cb69 | |||
| de7326375d | |||
| 936e84f6e0 | |||
| e1ebed4859 | |||
| 0bda4d8308 | |||
| adf49b8475 | |||
| 8d825346ab | |||
| ef38468fa7 | |||
| ef54b04ffc | |||
| 51e20497ac | |||
| 4ddadc08cb | |||
| 801bb2d534 | |||
| 20dd16badf | |||
| 31398a7e6b | |||
| de70b0a861 | |||
| a50c99b8e5 | |||
| 63de86a409 | |||
| 9fc3d91a17 | |||
| 2ff7a20eba | |||
| 3fa481bdfc | |||
| 9f7448d255 | |||
| 3afe8d7c1d | |||
| 15c27e16cc | |||
| 14a9763c73 | |||
| 6fbd141576 | |||
| c0455a20aa | |||
| 6f9b8b732d | |||
| 5fa31fe4d6 | |||
| f237119b9a | |||
| b08b88357e | |||
| f73ee41d93 | |||
| 93dad05bde | |||
| b844722af1 | |||
| a4b212d906 | |||
| 152719441e | |||
| 4b62a6e34f | |||
| 48fabec431 | |||
| f8d9fccf74 | |||
| 8793c36364 | |||
| 59d25c10b3 | |||
| 3b3d5b033a | |||
| 249ae49b43 | |||
| 33eafd5691 | |||
| 2b9247d630 | |||
| cc6b8277c9 | |||
| f65b18842a | |||
| db190e69ed | |||
| bc516bce7d | |||
| ccec41a10f | |||
| 9feb98db3f | |||
| a724c5f3ce | |||
| c60767c8b0 | |||
| ae13a72fde | |||
| 458d5e7d0d | |||
| 89e15d9b57 | |||
| 0d2292c311 | |||
| 62343af009 | |||
| c8c3b22d19 | |||
| 853e98879b | |||
| bf5cb33385 | |||
| 7ad4d350f8 | |||
| c63fc6a2ad | |||
| 7036d196be | |||
| d3bc18c369 | |||
| 1f3a32023f | |||
| a46bad0522 | |||
| d0dfa1d3dd | |||
| fc5b36acd3 | |||
| 0a8ab9bbd1 | |||
| b60000ac34 | |||
| 39d87625d7 | |||
| 0da8b46148 | |||
| 8d9f87061c | |||
| 4bdfa62039 | |||
| 67ea2d9d02 | |||
| 39b614fb0f | |||
| 84469dcd25 | |||
| eceb4a476f | |||
| 051a4eabd7 | |||
| e68a304698 | |||
| 2e6c6b1d41 | |||
| 0def6f8de9 | |||
| 7ac5b4f114 | |||
| ab47d5718f | |||
| 94aced0fc0 | |||
| 66a4c3d06e | |||
| 8d382afa0f | |||
| 051c5ff913 | |||
| a87dafbbec | |||
| 742cb7699b | |||
| 43449e7b08 | |||
| 33512e73bd | |||
| b367ffee6d | |||
| 69447df6b3 | |||
| a6eac4ff02 | |||
| 1eaf879a76 | |||
| c9ae6dcc03 | |||
| befa6bd356 | |||
| 100ab62ab4 | |||
| a0f999d9c9 | |||
| 9bda2f7e60 | |||
| 54b19999c6 | |||
| aa3c081352 | |||
| 2d16ee8884 | |||
| ec96a14807 | |||
| af72548a43 | |||
| 6d85b36f47 | |||
| 28830a697d | |||
| 5d3953a948 | |||
| 4d6432d38d | |||
| bcbebd5a36 | |||
| 50e2a626a6 | |||
| f4fe8c3769 | |||
| e42085a237 | |||
| a060b3447c | |||
| d7784b24c6 | |||
| 39645cb3d8 | |||
| 36166caccc | |||
| 0f1dc73d55 | |||
| 6b29c37433 | |||
| 535bacf9d6 | |||
| e6fb4081f7 | |||
| eb04fafaa4 | |||
| b4ed738d17 | |||
| 6a9ae93fa1 | |||
| 2dd47654e6 | |||
| c27e735c17 | |||
| 8bc65e4c91 | |||
| 0a476a74b3 | |||
| b5be4ce03b | |||
| f291f1d827 | |||
| 041ce885c7 | |||
| df16f28825 | |||
| a8867bc3cb | |||
| b2b115ec9c | |||
| 95de3a1f3e | |||
| dd4376cd37 | |||
| 20d45bff92 | |||
| 4ad67e9f6f | |||
| e367940bd9 | |||
| 6f2af78392 | |||
| 548d8133eb | |||
| 36ee2b29fb | |||
| 05accb4555 | |||
| f949a278da | |||
| bfae16f3a0 | |||
| d09d21434b | |||
| 2b9926cedb | |||
| af24fd67aa | |||
| e2cd34ffe3 | |||
| ecdf5ba271 | |||
| 995ef5bb36 | |||
| 8165adcab1 | |||
| 91c4a3e7b5 | |||
| cb710ea2be | |||
| 843a3ae9c9 | |||
| de040fb160 | |||
| acec8a76aa | |||
| 6c07c59454 | |||
| 4d708b5385 | |||
| 2e9f3181d4 | |||
| 3ae15d8f80 | |||
| d016529030 | |||
| 09f1553e40 | |||
| 52e4bf1b35 | |||
| bbe6ae0059 | |||
| c02117e626 | |||
| b8fb3acbab | |||
| d4d0064220 | |||
| 855bbdeb60 | |||
| 05893c9203 | |||
| c9c8e73587 | |||
| c7b6eb5d5b | |||
| 96bc88d8ce | |||
| 9a2e9dd6d1 | |||
| b252fcaaa1 | |||
| c582b932c7 | |||
| c3f26c4db8 | |||
| f27f7d28bb | |||
| 0424b1a92a | |||
| 81fb8fc238 | |||
| 037970a4ea | |||
| 3f6e83e87c | |||
| aa5b23fa80 | |||
| 02bde2c8b7 | |||
| cb5e90cc3b | |||
| 209fe09806 | |||
| dca8279e0c | |||
| 8163c7a520 | |||
| 4dffceaf7e | |||
| 9f1e33e0c6 | |||
| 9a7d7e68e2 | |||
| ab18d5d1ca | |||
| 6e53e74742 | |||
| f910bd4fce | |||
| 93e475f3a4 | |||
| e5d8170037 | |||
| 861632f92b | |||
| 9cf75565b5 | |||
| 9368a6b85e | |||
| c8ac6b2271 | |||
| 28f5c2b974 | |||
| daa2522a52 | |||
| 863f8ec19b | |||
| 8f98fc4547 | |||
| 398afbe49f | |||
| ad8c0ab2fb | |||
| 37130576e9 | |||
| 486fea2227 | |||
| 6d7357b151 | |||
| 452d7577f8 | |||
| 124398115e | |||
| 541a7b28a7 | |||
| 947b0970ad | |||
| 447fd5b3eb | |||
| 064ffef462 | |||
| 05360ac284 | |||
| 08dabc7331 | |||
| d724df7db2 | |||
| fc1b6af436 | |||
| 88fb589d2e | |||
| 5c5357cd79 | |||
| 5ffd60c429 | |||
| 5645c73613 | |||
| 13a7957cf3 | |||
| c0d5a7c01a | |||
| d87cc9ddb6 | |||
| b1c4bcc508 | |||
| 6288c2a57f | |||
| 1c569e690d | |||
| 7fdc6b9472 | |||
| 60d7d525f2 | |||
| f6f2998e85 | |||
| 82d1f2cf0b | |||
| f00e646612 | |||
| a101387b26 | |||
| af31ab604d | |||
| ccdd6ed490 | |||
| 9f404d965f | |||
| 0621b82aff | |||
| 22787b979d | |||
| 7d65c60711 | |||
| 69da64a49c | |||
| 66c858e00e | |||
| ef63cec7a8 | |||
| 0ac505ba09 | |||
| d4444c6257 | |||
| c6d5bb4eeb | |||
| 7f232c5cf2 | |||
| dc2ab5fcc0 | |||
| 137b23da10 | |||
| 54e361e3b8 | |||
| c78da1a7a9 | |||
| 27673cb0c1 | |||
| c040a02fa8 | |||
| a664e3b838 | |||
| 317b3b5eeb | |||
| 9f14b30aae | |||
| 065a6f4f46 | |||
| 9f9dc7e844 | |||
| b1c0a28366 | |||
| fc963dfe5c | |||
| 6f5ba2ade6 | |||
| ea708bb606 | |||
| 0822326900 | |||
| 79fc0cd395 | |||
| 357e7c1b18 | |||
| 71f1e445e1 | |||
| 20efe22e60 | |||
| 75a3dad745 | |||
| f5cca50830 | |||
| 8cd977f7ad | |||
| 90f2a9e106 | |||
| e0ad358aa9 | |||
| 3db4002420 | |||
| bf248c49c3 | |||
| 69a3a30a0e | |||
| f80f179e4c | |||
| c1c1d84cef | |||
| c431d888f0 | |||
| 2ebb791eb7 | |||
| 00b818b4d7 | |||
| ce1b0d442c | |||
| 5283c9781c | |||
| 279d8bf799 | |||
| 7114d63ba6 | |||
| 120ae89578 | |||
| d1eb623fd6 | |||
| 873cf65317 | |||
| 2091dead3f | |||
| 2ffd859f0e | |||
| da02a97a00 | |||
| fb51dc781d | |||
| 32bf64028d | |||
| 2e4e75e386 | |||
| f67f6e5b9f | |||
| 24039218a1 | |||
| 1f447ef73c | |||
| 4509198eef | |||
| bc60cbefb8 | |||
| a9118562a9 | |||
| 24637be7c2 | |||
| d74be47696 | |||
| 76a00031cd | |||
| 063a192699 | |||
| b016b7dc2a | |||
| 42f6441512 | |||
| dd066ba040 | |||
| b3def6cfa2 | |||
| 4a82eb3503 | |||
| c3ba8db660 | |||
| 4e1a0e1ab9 | |||
| 1dd3dbbcd8 | |||
| e1be2d9e48 | |||
| 8fbfccd024 | |||
| de6bb33f01 | |||
| 3a40515a90 | |||
| 5d533338d0 | |||
| f412852d50 | |||
| 5fbec487e2 | |||
| 19c61e20c0 | |||
| 0b6fda2af5 | |||
| e9795e7521 | |||
| 3b8413a9dd | |||
| b2f9ad7efb | |||
| 4baa3f5588 | |||
| 9c5ae3260c | |||
| b7baef0a48 | |||
| 8778d7c9ab | |||
| d275997e54 | |||
| 2faea1bb69 | |||
| ba6c96412b | |||
| ed38122752 | |||
| 922587ed2e | |||
| 8e7c9d19e4 | |||
| 0f33ef0fc5 | |||
| a14c87ad60 | |||
| 6d82b1ce89 | |||
| d73e9f6bcf | |||
| e6a87fbd69 | |||
| 3defbd60db | |||
| 6e9574a1bd | |||
| 7005cd08f2 | |||
| e94f338b77 | |||
| d6172587b3 | |||
| f196d83a14 | |||
| 9d4f4e1509 | |||
| 7308652f6e | |||
| 870e9c3688 | |||
| 189f142fae | |||
| 6c0918662e | |||
| 2bc01c143a | |||
| f310b85ee6 | |||
| 97fef36f2f | |||
| a8526ae4eb | |||
| 966fbe7d61 | |||
| a77c2ef71f | |||
| 61a194e396 | |||
| ae25784d72 | |||
| 3343c78699 | |||
| 7928f54a95 | |||
| e4b68518e5 | |||
| 14ed1cdee8 | |||
| 72f159be88 | |||
| 144954b979 | |||
| 9e15391471 | |||
| d62b1e445a | |||
| ade4c035b7 | |||
| 13ca991c37 | |||
| e48459f49d | |||
| facf18e0df | |||
| 5c93dc62bd | |||
| d272d4b6c3 | |||
| 1b41edfc7e | |||
| d55270bd64 | |||
| 85225917f5 | |||
| eaef62a775 | |||
| f6c8d63658 | |||
| ea82d7ec2b | |||
| e8a7ba056c | |||
| 9fd40467f2 | |||
| c81e29fe54 | |||
| b9b7bb5489 | |||
| 8036278e29 | |||
| 39c25215ba | |||
| 490a48cd50 |
@@ -0,0 +1,280 @@
|
|||||||
|
name: Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-binaries:
|
||||||
|
name: Build binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env: { CGO_ENABLED: 0 }
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with: { go-version: '1.25' }
|
||||||
|
|
||||||
|
- name: Build go2rtc_win64
|
||||||
|
env: { GOOS: windows, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_win64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_win64, path: go2rtc.exe }
|
||||||
|
|
||||||
|
- name: Build go2rtc_win32
|
||||||
|
env: { GOOS: windows, GOARCH: 386 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_win32
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_win32, path: go2rtc.exe }
|
||||||
|
|
||||||
|
- name: Build go2rtc_win_arm64
|
||||||
|
env: { GOOS: windows, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_win_arm64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_win_arm64, path: go2rtc.exe }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_amd64
|
||||||
|
env: { GOOS: linux, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_amd64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_amd64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_i386
|
||||||
|
env: { GOOS: linux, GOARCH: 386 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_i386
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_i386, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_arm64
|
||||||
|
env: { GOOS: linux, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_arm64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_arm64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_arm
|
||||||
|
env: { GOOS: linux, GOARCH: arm, GOARM: 7 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_arm
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_arm, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_armv6
|
||||||
|
env: { GOOS: linux, GOARCH: arm, GOARM: 6 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_armv6
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_armv6, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_linux_mipsel
|
||||||
|
env: { GOOS: linux, GOARCH: mipsle }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_linux_mipsel
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_linux_mipsel, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_mac_amd64
|
||||||
|
env: { GOOS: darwin, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_mac_amd64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_mac_amd64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_mac_arm64
|
||||||
|
env: { GOOS: darwin, GOARCH: arm64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_mac_arm64
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with: { name: go2rtc_mac_arm64, path: go2rtc }
|
||||||
|
|
||||||
|
- name: Build go2rtc_freebsd_amd64
|
||||||
|
env: { GOOS: freebsd, GOARCH: amd64 }
|
||||||
|
run: go build -ldflags "-s -w" -trimpath
|
||||||
|
- name: Upload go2rtc_freebsd_amd64
|
||||||
|
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@v4
|
||||||
|
with: { name: go2rtc_freebsd_arm64, path: go2rtc }
|
||||||
|
|
||||||
|
docker-master:
|
||||||
|
name: Build docker master
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
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/Dockerfile
|
||||||
|
platforms: |
|
||||||
|
linux/amd64
|
||||||
|
linux/386
|
||||||
|
linux/arm/v6
|
||||||
|
linux/arm/v7
|
||||||
|
linux/arm64/v8
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
docker-hardware:
|
||||||
|
name: Build docker hardware
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta-hw
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}
|
||||||
|
ghcr.io/${{ github.repository }}
|
||||||
|
flavor: |
|
||||||
|
suffix=-hardware,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/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
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
name: docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'master'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ github.repository }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=semver,pattern={{version}},enable=false
|
|
||||||
type=match,pattern=v(.*),group=1
|
|
||||||
|
|
||||||
- name: Docker meta Hardware
|
|
||||||
id: meta-hw
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ github.repository }}
|
|
||||||
flavor: |
|
|
||||||
suffix=-hardware
|
|
||||||
latest=false
|
|
||||||
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@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/386
|
|
||||||
linux/arm/v7
|
|
||||||
linux/arm64/v8
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
- name: Build and push Hardware
|
|
||||||
uses: docker/build-push-action@v4
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: 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
|
|
||||||
@@ -17,21 +17,37 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
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
|
# Single deploy job since we're just deploying
|
||||||
deploy:
|
deploy:
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v3
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v1
|
|
||||||
with:
|
|
||||||
path: './website'
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v2
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# push:
|
|
||||||
# tags:
|
|
||||||
# - 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Generate changelog
|
|
||||||
run: |
|
|
||||||
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
|
|
||||||
- name: install lipo
|
|
||||||
run: |
|
|
||||||
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
|
|
||||||
chmod +x /tmp/lipo
|
|
||||||
mv /tmp/lipo /usr/local/bin
|
|
||||||
- name: Build Go binaries
|
|
||||||
run: |
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export CGO_ENABLED=0
|
|
||||||
|
|
||||||
mkdir -p artifacts
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=amd64
|
|
||||||
export FILENAME=artifacts/go2rtc_win64.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=386
|
|
||||||
export FILENAME=artifacts/go2rtc_win32.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=arm64
|
|
||||||
export FILENAME=artifacts/go2rtc_win_arm64.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=amd64
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_amd64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=386
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_i386
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm64
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_arm64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm
|
|
||||||
export GOARM=7
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_arm
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=mipsle
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_mipsel
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=darwin
|
|
||||||
export GOARCH=amd64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
|
|
||||||
|
|
||||||
export GOOS=darwin
|
|
||||||
export GOARCH=arm64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
|
|
||||||
|
|
||||||
export FILENAME=artifacts/go2rtc_mac_universal.zip
|
|
||||||
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
|
||||||
|
|
||||||
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
|
||||||
- name: Setup tmate session
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
if: ${{ failure() }}
|
|
||||||
- name: Set env
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Create GitHub release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
files: artifacts/*
|
|
||||||
generate_release_notes: true
|
|
||||||
name: Release ${{ env.RELEASE_VERSION }}
|
|
||||||
body_path: CHANGELOG.md
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
+15
-14
@@ -1,11 +1,11 @@
|
|||||||
name: Test Build and Run
|
name: Test Build and Run
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
# push:
|
||||||
branches:
|
# branches:
|
||||||
- '*'
|
# - '*'
|
||||||
pull_request:
|
# pull_request:
|
||||||
merge_group:
|
# merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.20'
|
go-version: '1.24'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
@@ -70,15 +70,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
platforms: linux/${{ matrix.platform }}
|
platforms: linux/${{ matrix.platform }}
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
@@ -89,10 +90,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Hardware
|
- name: Build and push Hardware
|
||||||
if: matrix.platform == 'amd64'
|
if: matrix.platform == 'amd64'
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: hardware.Dockerfile
|
file: docker/hardware.Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
|
|||||||
+19
-3
@@ -1,8 +1,24 @@
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
.tmp/
|
.tmp/
|
||||||
|
|
||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
|
|
||||||
go2rtc.json
|
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
|
||||||
@@ -1,486 +0,0 @@
|
|||||||
openapi: 3.0.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: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/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: { }
|
|
||||||
patch:
|
|
||||||
summary: Merge changes to main config file
|
|
||||||
tags: [ Config ]
|
|
||||||
requestBody:
|
|
||||||
content:
|
|
||||||
"*/*": { example: "streams:..." }
|
|
||||||
responses: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/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: { }
|
|
||||||
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: { }
|
|
||||||
delete:
|
|
||||||
summary: Delete stream
|
|
||||||
tags: [ Streams list ]
|
|
||||||
parameters:
|
|
||||||
- name: src
|
|
||||||
in: query
|
|
||||||
description: Stream name
|
|
||||||
required: true
|
|
||||||
schema: { type: string }
|
|
||||||
example: camera1
|
|
||||||
responses: { }
|
|
||||||
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: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/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: { }
|
|
||||||
/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: { }
|
|
||||||
/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: { }
|
|
||||||
/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: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/api/dvrip:
|
|
||||||
get:
|
|
||||||
summary: DVRIP cameras discovery
|
|
||||||
description: "[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
|
|
||||||
/api/ffmpeg/devices:
|
|
||||||
get:
|
|
||||||
summary: FFmpeg USB devices discovery
|
|
||||||
description: "[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
/api/ffmpeg/hardware:
|
|
||||||
get:
|
|
||||||
summary: FFmpeg hardware transcoding discovery
|
|
||||||
description: "[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
/api/hass:
|
|
||||||
get:
|
|
||||||
summary: Home Assistant cameras discovery
|
|
||||||
description: "[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
/api/homekit:
|
|
||||||
get:
|
|
||||||
summary: HomeKit cameras discovery
|
|
||||||
description: "[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
/api/nest:
|
|
||||||
get:
|
|
||||||
summary: Nest cameras discovery
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
/api/onvif:
|
|
||||||
get:
|
|
||||||
summary: ONVIF cameras discovery
|
|
||||||
description: "[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
/api/roborock:
|
|
||||||
get:
|
|
||||||
summary: Roborock vacuums discovery
|
|
||||||
description: "[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)"
|
|
||||||
tags: [ Discovery ]
|
|
||||||
responses: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/onvif/:
|
|
||||||
get:
|
|
||||||
summary: ONVIF server implementation
|
|
||||||
description: Simple realisation of the ONVIF protocol. Accepts any suburl requests
|
|
||||||
tags: [ ONVIF ]
|
|
||||||
responses: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/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: { }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/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: { }
|
|
||||||
|
|
||||||
/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 |
Binary file not shown.
|
Before Width: | Height: | Size: 295 KiB |
@@ -1,12 +1,8 @@
|
|||||||
# syntax=docker/dockerfile:labs
|
# syntax=docker/dockerfile:labs
|
||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.13"
|
||||||
ARG GO_VERSION="1.20"
|
ARG GO_VERSION="1.25"
|
||||||
ARG NGROK_VERSION="3"
|
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
|
||||||
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
|
||||||
|
|
||||||
|
|
||||||
# 1. Build go2rtc binary
|
# 1. Build go2rtc binary
|
||||||
@@ -20,6 +16,8 @@ ENV GOARCH=${TARGETARCH}
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
# Cache dependencies
|
# Cache dependencies
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||||
@@ -28,21 +26,14 @@ COPY . .
|
|||||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||||
|
|
||||||
|
|
||||||
# 2. Collect all files
|
# 2. Final image
|
||||||
FROM scratch AS rootfs
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
|
|
||||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
|
||||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
|
||||||
|
|
||||||
|
|
||||||
# 3. Final image
|
|
||||||
FROM base
|
|
||||||
|
|
||||||
# Install ffmpeg, tini (for signal handling),
|
# Install ffmpeg, tini (for signal handling),
|
||||||
# and other common tools for the echo source.
|
# and other common tools for the echo source.
|
||||||
# alsa-plugins-pulse for ALSA support (+0MB)
|
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||||
# font-droid for FFmpeg drawtext filter (+2MB)
|
# 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)
|
# Hardware Acceleration for Intel CPU (+50MB)
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@@ -54,8 +45,9 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
|||||||
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||||
|
|
||||||
COPY --from=rootfs / /
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 1984 8554 8555 8555/udp
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
WORKDIR /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
|
||||||
|
```
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
# syntax=docker/dockerfile:labs
|
# syntax=docker/dockerfile:labs
|
||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
# only debian 12 (bookworm) has latest ffmpeg
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
ARG DEBIAN_VERSION="bookworm-slim"
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
ARG GO_VERSION="1.20-buster"
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
ARG NGROK_VERSION="3"
|
ARG GO_VERSION="1.25-bookworm"
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
|
||||||
FROM golang:${GO_VERSION} AS go
|
|
||||||
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
|
|
||||||
|
|
||||||
|
|
||||||
# 1. Build go2rtc binary
|
# 1. Build go2rtc binary
|
||||||
FROM --platform=$BUILDPLATFORM go AS build
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build
|
||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@@ -30,32 +26,30 @@ COPY . .
|
|||||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||||
|
|
||||||
|
|
||||||
# 2. Collect all files
|
# 2. Final image
|
||||||
FROM scratch AS rootfs
|
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
|
# Prepare apt for buildkit cache
|
||||||
RUN rm -f /etc/apt/apt.conf.d/docker-clean \
|
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
|
&& 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.
|
# and other common tools for the echo source.
|
||||||
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||||
|
# mesa-va-drivers for AMD APU
|
||||||
# libasound2-plugins for ALSA support
|
# libasound2-plugins for ALSA support
|
||||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
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 bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
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 \
|
python3 curl jq \
|
||||||
intel-media-va-driver-non-free \
|
intel-media-va-driver-non-free \
|
||||||
libasound2-plugins
|
mesa-va-drivers \
|
||||||
|
libasound2-plugins && \
|
||||||
COPY --link --from=rootfs / /
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 1984 8554 8555 8555/udp
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
WORKDIR /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()
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
)
|
||||||
|
|
||||||
|
var servs = map[string]string{
|
||||||
|
"3E": "Accessory Information",
|
||||||
|
"7E": "Security System",
|
||||||
|
"85": "Motion Sensor",
|
||||||
|
"96": "Battery",
|
||||||
|
"A2": "Protocol Information",
|
||||||
|
"110": "Camera RTP Stream Management",
|
||||||
|
"112": "Microphone",
|
||||||
|
"113": "Speaker",
|
||||||
|
"121": "Doorbell",
|
||||||
|
"129": "Data Stream Transport Management",
|
||||||
|
"204": "Camera Recording Management",
|
||||||
|
"21A": "Camera Operating Mode",
|
||||||
|
"22A": "Wi-Fi Transport",
|
||||||
|
"239": "Accessory Runtime Information",
|
||||||
|
}
|
||||||
|
|
||||||
|
var chars = map[string]string{
|
||||||
|
"14": "Identify",
|
||||||
|
"20": "Manufacturer",
|
||||||
|
"21": "Model",
|
||||||
|
"23": "Name",
|
||||||
|
"30": "Serial Number",
|
||||||
|
"52": "Firmware Revision",
|
||||||
|
"53": "Hardware Revision",
|
||||||
|
"220": "Product Data",
|
||||||
|
"A6": "Accessory Flags",
|
||||||
|
|
||||||
|
"22": "Motion Detected",
|
||||||
|
"75": "Status Active",
|
||||||
|
|
||||||
|
"11A": "Mute",
|
||||||
|
"119": "Volume",
|
||||||
|
|
||||||
|
"B0": "Active",
|
||||||
|
"209": "Selected Camera Recording Configuration",
|
||||||
|
"207": "Supported Audio Recording Configuration",
|
||||||
|
"205": "Supported Camera Recording Configuration",
|
||||||
|
"206": "Supported Video Recording Configuration",
|
||||||
|
"226": "Recording Audio Active",
|
||||||
|
|
||||||
|
"223": "Event Snapshots Active",
|
||||||
|
"225": "Periodic Snapshots Active",
|
||||||
|
"21B": "HomeKit Camera Active",
|
||||||
|
"21C": "Third Party Camera Active",
|
||||||
|
"21D": "Camera Operating Mode Indicator",
|
||||||
|
"11B": "Night Vision",
|
||||||
|
//"129": "Supported Data Stream Transport Configuration",
|
||||||
|
"37": "Version",
|
||||||
|
"131": "Setup Data Stream Transport",
|
||||||
|
"130": "Supported Data Stream Transport Configuration",
|
||||||
|
|
||||||
|
"120": "Streaming Status",
|
||||||
|
"115": "Supported Audio Stream Configuration",
|
||||||
|
"116": "Supported RTP Configuration",
|
||||||
|
"114": "Supported Video Stream Configuration",
|
||||||
|
"117": "Selected RTP Stream Configuration",
|
||||||
|
"118": "Setup Endpoints",
|
||||||
|
|
||||||
|
"22B": "Current Transport",
|
||||||
|
"22C": "Wi-Fi Capabilities",
|
||||||
|
"22D": "Wi-Fi Configuration Control",
|
||||||
|
|
||||||
|
"23C": "Ping",
|
||||||
|
|
||||||
|
"68": "Battery Level",
|
||||||
|
"79": "Status Low Battery",
|
||||||
|
"8F": "Charging State",
|
||||||
|
|
||||||
|
"73": "Programmable Switch Event",
|
||||||
|
"232": "Operating State Response",
|
||||||
|
|
||||||
|
"66": "Security System Current State",
|
||||||
|
"67": "Security System Target State",
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
src := os.Args[1]
|
||||||
|
dst := os.Args[2]
|
||||||
|
|
||||||
|
f, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var v hap.JSONAccessories
|
||||||
|
if err = json.NewDecoder(f).Decode(&v); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, acc := range v.Value {
|
||||||
|
for _, srv := range acc.Services {
|
||||||
|
if srv.Desc == "" {
|
||||||
|
srv.Desc = servs[srv.Type]
|
||||||
|
}
|
||||||
|
for _, chr := range srv.Characters {
|
||||||
|
if chr.Desc == "" {
|
||||||
|
chr.Desc = chars[chr.Type]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err = os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
if err = enc.Encode(v); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var service = mdns.ServiceHAP
|
||||||
|
|
||||||
|
if len(os.Args) >= 2 {
|
||||||
|
service = os.Args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
onentry := func(entry *mdns.ServiceEntry) bool {
|
||||||
|
log.Printf("name=%s, addr=%s, info=%s\n", entry.Name, entry.Addr(), entry.Info)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(os.Args) >= 3 {
|
||||||
|
host := os.Args[2]
|
||||||
|
|
||||||
|
log.Printf("run discovery service=%s host=%s\n", service, host)
|
||||||
|
|
||||||
|
err = mdns.QueryOrDiscovery(host, service, onentry)
|
||||||
|
} else {
|
||||||
|
log.Printf("run discovery service=%s\n", service)
|
||||||
|
|
||||||
|
err = mdns.Discovery(service, onentry)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,39 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := rtsp.NewClient(os.Args[1])
|
||||||
|
if err := client.Dial(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
Codecs: []*core.Codec{
|
||||||
|
{Name: core.CodecPCMU, ClockRate: 8000},
|
||||||
|
},
|
||||||
|
ID: "streamid=0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.Announce(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if _, err := client.SetupMedia(client.Medias[0]); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
if err := client.Record(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.RunUntilSignal()
|
||||||
|
}
|
||||||
@@ -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,57 +1,52 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.20
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brutella/hap v0.0.17
|
github.com/asticode/go-astits v1.14.0
|
||||||
github.com/deepch/vdk v0.0.19
|
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/expr-lang/expr v1.17.7
|
||||||
github.com/miekg/dns v1.1.55
|
github.com/google/uuid v1.6.0
|
||||||
github.com/pion/ice/v2 v2.3.9
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/pion/interceptor v0.1.17
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/pion/rtcp v1.2.10
|
github.com/miekg/dns v1.1.70
|
||||||
github.com/pion/rtp v1.7.13
|
github.com/pion/dtls/v3 v3.0.10
|
||||||
github.com/pion/sdp/v3 v3.0.6
|
github.com/pion/ice/v4 v4.2.0
|
||||||
github.com/pion/srtp/v2 v2.0.15
|
github.com/pion/interceptor v0.1.43
|
||||||
github.com/pion/stun v0.6.1
|
github.com/pion/rtcp v1.2.16
|
||||||
github.com/pion/webrtc/v3 v3.2.12
|
github.com/pion/rtp v1.10.0
|
||||||
github.com/rs/zerolog v1.29.1
|
github.com/pion/sdp/v3 v3.0.17
|
||||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3
|
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/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9
|
||||||
|
golang.org/x/crypto v0.47.0
|
||||||
|
golang.org/x/net v0.49.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brutella/dnssd v1.2.9 // indirect
|
github.com/asticode/go-astikit v0.57.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-chi/chi v1.5.4 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/pion/datachannel v1.6.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.5 // indirect
|
|
||||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
|
||||||
github.com/pion/mdns v0.0.7 // indirect
|
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.7 // indirect
|
github.com/pion/sctp v1.9.2 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.2 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/crypto v0.11.0 // indirect
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
golang.org/x/mod v0.12.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/net v0.12.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/sys v0.10.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/text v0.11.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
golang.org/x/tools v0.11.0 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
replace (
|
|
||||||
// RTP tlv8 fix
|
|
||||||
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045
|
|
||||||
// fix reading AAC config bytes
|
|
||||||
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,253 +1,152 @@
|
|||||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
|
github.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=
|
||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
|
github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=
|
||||||
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/brutella/dnssd v1.2.9 h1:eUqO0qXZAMaFN4W4Ms1AAO/OtAbNoh9U87GAlN+1FCs=
|
|
||||||
github.com/brutella/dnssd v1.2.9/go.mod h1:yZ+GHHbGhtp5yJeKTnppdFGiy6OhiPoxs0WHW1KUcFA=
|
|
||||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
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.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||||
github.com/pion/ice/v2 v2.3.9 h1:7yZpHf3PhPxJGT4JkMj1Y8Rl5cQ6fB709iz99aeMd/U=
|
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
||||||
github.com/pion/ice/v2 v2.3.9/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4=
|
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
||||||
github.com/pion/interceptor v0.1.17 h1:prJtgwFh/gB8zMqGZoOgJPHivOwVAp61i2aG61Du/1w=
|
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||||
github.com/pion/interceptor v0.1.17/go.mod h1:SY8kpmfVBvrbUzvj2bsXz7OJt5JvmVNZ+4Kjq7FcwrI=
|
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=
|
||||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
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 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
|
||||||
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
|
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
|
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
|
||||||
github.com/pion/srtp/v2 v2.0.15 h1:+tqRtXGsGwHC0G0IUIAzRmdkHvriF79IHVfZGfHrQoA=
|
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
|
||||||
github.com/pion/srtp/v2 v2.0.15/go.mod h1:b/pQOlDrbB0HEH5EUAQXzSYxikFbNcNuKmF8tM0hCtw=
|
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||||
github.com/pion/transport/v2 v2.1.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||||
github.com/pion/transport/v2 v2.2.0/go.mod h1:AdSw4YBZVDkZm8fpoz+fclXyQwANWmZAlDuQdctTThQ=
|
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||||
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c=
|
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||||
github.com/pion/turn/v2 v2.1.2 h1:wj0cAoGKltaZ790XEGW9HwoUewqjliwmhtxCuB2ApyM=
|
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||||
github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU=
|
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||||
github.com/pion/webrtc/v3 v3.2.12 h1:pVqz5NdtTqyhKIhMcXR8bPp709kCf9blyAhDjoVRLvA=
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
github.com/pion/webrtc/v3 v3.2.12/go.mod h1:/Oz6K95CGWaN+3No+Z0NYvgOPOr3aY8UyTlMm/dec3A=
|
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/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
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 h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
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.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/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
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/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
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.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
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.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
|
||||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
|
||||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.4.0/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
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.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
|
||||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
|
||||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|
||||||
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.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
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.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
|
|
||||||
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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
|
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||||
- https://komodor.com/learn/exit-codes-in-containers-and-kubernetes-the-complete-guide/
|
|
||||||
|
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
|
||||||
|
|||||||
+156
-67
@@ -4,28 +4,36 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
BasePath string `yaml:"base_path"`
|
LocalAuth bool `yaml:"local_auth"`
|
||||||
StaticDir string `yaml:"static_dir"`
|
BasePath string `yaml:"base_path"`
|
||||||
Origin string `yaml:"origin"`
|
StaticDir string `yaml:"static_dir"`
|
||||||
TLSListen string `yaml:"tls_listen"`
|
Origin string `yaml:"origin"`
|
||||||
TLSCert string `yaml:"tls_cert"`
|
TLSListen string `yaml:"tls_listen"`
|
||||||
TLSKey string `yaml:"tls_key"`
|
TLSCert string `yaml:"tls_cert"`
|
||||||
|
TLSKey string `yaml:"tls_key"`
|
||||||
|
UnixListen string `yaml:"unix_listen"`
|
||||||
|
|
||||||
|
AllowPaths []string `yaml:"allow_paths"`
|
||||||
} `yaml:"api"`
|
} `yaml:"api"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +43,11 @@ func Init() {
|
|||||||
// load config from YAML
|
// load config from YAML
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
if cfg.Mod.Listen == "" {
|
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allowPaths = cfg.Mod.AllowPaths
|
||||||
basePath = cfg.Mod.BasePath
|
basePath = cfg.Mod.BasePath
|
||||||
log = app.GetLogger("api")
|
log = app.GetLogger("api")
|
||||||
|
|
||||||
@@ -47,15 +56,8 @@ func Init() {
|
|||||||
HandleFunc("api", apiHandler)
|
HandleFunc("api", apiHandler)
|
||||||
HandleFunc("api/config", configHandler)
|
HandleFunc("api/config", configHandler)
|
||||||
HandleFunc("api/exit", exitHandler)
|
HandleFunc("api/exit", exitHandler)
|
||||||
|
HandleFunc("api/restart", restartHandler)
|
||||||
// ensure we can listen without errors
|
HandleFunc("api/log", logHandler)
|
||||||
listener, err := net.Listen("tcp", cfg.Mod.Listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] listen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
|
|
||||||
|
|
||||||
Handler = http.DefaultServeMux // 4th
|
Handler = http.DefaultServeMux // 4th
|
||||||
|
|
||||||
@@ -64,52 +66,83 @@ func Init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Mod.Username != "" {
|
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() {
|
if log.Trace().Enabled() {
|
||||||
Handler = middlewareLog(Handler) // 1st
|
Handler = middlewareLog(Handler) // 1st
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if cfg.Mod.Listen != "" {
|
||||||
s := http.Server{}
|
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||||
s.Handler = Handler
|
Port, _ = strconv.Atoi(port)
|
||||||
if err = s.Serve(listener); err != nil {
|
go listen("tcp", cfg.Mod.Listen)
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
}
|
||||||
}
|
|
||||||
}()
|
if cfg.Mod.UnixListen != "" {
|
||||||
|
_ = syscall.Unlink(cfg.Mod.UnixListen)
|
||||||
|
go listen("unix", cfg.Mod.UnixListen)
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the HTTPS server
|
// Initialize the HTTPS server
|
||||||
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
||||||
cert, err := tls.X509KeyPair([]byte(cfg.Mod.TLSCert), []byte(cfg.Mod.TLSKey))
|
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsListener, err := net.Listen("tcp4", cfg.Mod.TLSListen)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.TLSListen).Msg("[api] tls listen")
|
|
||||||
|
|
||||||
tlsServer := &http.Server{
|
|
||||||
Handler: Handler,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := tlsServer.ServeTLS(tlsListener, "", ""); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] tls serve")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listen(network, address string) {
|
||||||
|
ln, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api] listen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("addr", address).Msg("[api] listen")
|
||||||
|
|
||||||
|
server := http.Server{
|
||||||
|
Handler: Handler,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds
|
||||||
|
}
|
||||||
|
if err = server.Serve(ln); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("[api] serve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tlsListen(network, address, certFile, keyFile string) {
|
||||||
|
var cert tls.Certificate
|
||||||
|
var err error
|
||||||
|
if strings.IndexByte(certFile, '\n') < 0 && strings.IndexByte(keyFile, '\n') < 0 {
|
||||||
|
// check if file path
|
||||||
|
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
} else {
|
||||||
|
// if text file content
|
||||||
|
cert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := net.Listen(network, address)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("[api] tls listen")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("addr", address).Msg("[api] tls listen")
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Handler: Handler,
|
||||||
|
TLSConfig: &tls.Config{Certificates: []tls.Certificate{cert}},
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
if err = server.ServeTLS(ln, "", ""); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("[api] tls serve")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Port int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MimeJSON = "application/json"
|
MimeJSON = "application/json"
|
||||||
MimeText = "text/plain"
|
MimeText = "text/plain"
|
||||||
@@ -124,6 +157,10 @@ func HandleFunc(pattern string, handler http.HandlerFunc) {
|
|||||||
if len(pattern) == 0 || pattern[0] != '/' {
|
if len(pattern) == 0 || pattern[0] != '/' {
|
||||||
pattern = basePath + "/" + pattern
|
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")
|
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||||
http.HandleFunc(pattern, handler)
|
http.HandleFunc(pattern, handler)
|
||||||
}
|
}
|
||||||
@@ -157,6 +194,7 @@ func Response(w http.ResponseWriter, body any, contentType string) {
|
|||||||
|
|
||||||
const StreamNotFound = "stream not found"
|
const StreamNotFound = "stream not found"
|
||||||
|
|
||||||
|
var allowPaths []string
|
||||||
var basePath string
|
var basePath string
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
@@ -167,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) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
|
if localAuth || !isLoopback(r.RemoteAddr) {
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if !ok || user != username || pass != password {
|
if !ok || user != username || pass != password {
|
||||||
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||||
@@ -186,7 +228,7 @@ func middlewareCORS(next http.Handler) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
|
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -208,25 +250,72 @@ func exitHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := r.URL.Query().Get("code")
|
s := r.URL.Query().Get("code")
|
||||||
code, _ := strconv.Atoi(s)
|
code, err := strconv.Atoi(s)
|
||||||
|
|
||||||
|
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
|
||||||
|
if err != nil || code < 0 || code > 125 {
|
||||||
|
http.Error(w, "Code must be in the range [0, 125]", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Stream struct {
|
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Name string `json:"name"`
|
if r.Method != "POST" {
|
||||||
URL string `json:"url"`
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("[api] restart %s", path)
|
||||||
|
|
||||||
|
go syscall.Exec(path, os.Args, os.Environ())
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResponseStreams(w http.ResponseWriter, streams []Stream) {
|
func logHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if len(streams) == 0 {
|
switch r.Method {
|
||||||
http.Error(w, "no streams", http.StatusNotFound)
|
case "GET":
|
||||||
|
// Send current state of the log file immediately
|
||||||
|
w.Header().Set("Content-Type", "application/jsonlines")
|
||||||
|
_, _ = app.MemoryLog.WriteTo(w)
|
||||||
|
case "DELETE":
|
||||||
|
app.MemoryLog.Reset()
|
||||||
|
Response(w, "OK", "text/plain")
|
||||||
|
default:
|
||||||
|
http.Error(w, "Method not allowed", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Source struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Info string `json:"info,omitempty"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Location string `json:"location,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResponseSources(w http.ResponseWriter, sources []*Source) {
|
||||||
|
if len(sources) == 0 {
|
||||||
|
http.Error(w, "no sources", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = struct {
|
var response = struct {
|
||||||
Streams []Stream `json:"streams"`
|
Sources []*Source `json:"sources"`
|
||||||
}{
|
}{
|
||||||
Streams: streams,
|
Sources: sources,
|
||||||
}
|
}
|
||||||
ResponseJSON(w, response)
|
ResponseJSON(w, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Error(w http.ResponseWriter, err error) {
|
||||||
|
log.Error().Err(err).Caller(1).Send()
|
||||||
|
|
||||||
|
http.Error(w, err.Error(), http.StatusInsufficientStorage)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configHandler(w http.ResponseWriter, r *http.Request) {
|
func configHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -40,8 +41,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// validate config
|
// validate config
|
||||||
var tmp struct{}
|
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
|
||||||
if err = yaml.Unmarshal(data, &tmp); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/www"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/www"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initStatic(staticDir string) {
|
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"}
|
||||||
|
```
|
||||||
+46
-21
@@ -1,15 +1,19 @@
|
|||||||
package ws
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"io"
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -21,31 +25,34 @@ func Init() {
|
|||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
|
log = app.GetLogger("api")
|
||||||
|
|
||||||
initWS(cfg.Mod.Origin)
|
initWS(cfg.Mod.Origin)
|
||||||
|
|
||||||
api.HandleFunc("api/ws", apiWS)
|
api.HandleFunc("api/ws", apiWS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
// Message - struct for data exchange in Web API
|
// Message - struct for data exchange in Web API
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Value any `json:"value,omitempty"`
|
Value any `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) String() string {
|
func (m *Message) String() (value string) {
|
||||||
if s, ok := m.Value.(string); ok {
|
if s, ok := m.Value.(string); ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) GetString(key string) string {
|
func (m *Message) Unmarshal(v any) error {
|
||||||
if v, ok := m.Value.(map[string]any); ok {
|
b, err := json.Marshal(m.Value)
|
||||||
if s, ok := v[key].(string); ok {
|
if err != nil {
|
||||||
return s
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
return json.Unmarshal(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WSHandler func(tr *Transport, msg *Message) error
|
type WSHandler func(tr *Transport, msg *Message) error
|
||||||
@@ -77,7 +84,7 @@ func initWS(origin string) {
|
|||||||
if o.Host == r.Host {
|
if o.Host == r.Host {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
|
||||||
// https://github.com/AlexxIT/go2rtc/issues/118
|
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||||
return o.Host[:i] == r.Host
|
return o.Host[:i] == r.Host
|
||||||
@@ -101,13 +108,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr := &Transport{Request: r}
|
tr := &Transport{Request: r}
|
||||||
tr.OnWrite(func(msg any) {
|
tr.OnWrite(func(msg any) error {
|
||||||
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||||
|
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
_ = ws.WriteMessage(websocket.BinaryMessage, data)
|
return ws.WriteMessage(websocket.BinaryMessage, data)
|
||||||
} else {
|
} else {
|
||||||
_ = ws.WriteJSON(msg)
|
return ws.WriteJSON(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -121,12 +128,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
|
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||||
|
|
||||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||||
go func() {
|
go func() {
|
||||||
if err = handler(tr, msg); err != nil {
|
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})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -147,11 +155,11 @@ type Transport struct {
|
|||||||
wrmx sync.Mutex
|
wrmx sync.Mutex
|
||||||
|
|
||||||
onChange func()
|
onChange func()
|
||||||
onWrite func(msg any)
|
onWrite func(msg any) error
|
||||||
onClose []func()
|
onClose []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) OnWrite(f func(msg any)) {
|
func (t *Transport) OnWrite(f func(msg any) error) {
|
||||||
t.mx.Lock()
|
t.mx.Lock()
|
||||||
if t.onChange != nil {
|
if t.onChange != nil {
|
||||||
t.onChange()
|
t.onChange()
|
||||||
@@ -162,7 +170,7 @@ func (t *Transport) OnWrite(f func(msg any)) {
|
|||||||
|
|
||||||
func (t *Transport) Write(msg any) {
|
func (t *Transport) Write(msg any) {
|
||||||
t.wrmx.Lock()
|
t.wrmx.Lock()
|
||||||
t.onWrite(msg)
|
_ = t.onWrite(msg)
|
||||||
t.wrmx.Unlock()
|
t.wrmx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,3 +208,20 @@ func (t *Transport) WithContext(f func(ctx map[any]any)) {
|
|||||||
f(t.ctx)
|
f(t.ctx)
|
||||||
t.mx.Unlock()
|
t.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Writer() io.Writer {
|
||||||
|
return &writer{t: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
type writer struct {
|
||||||
|
t *Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writer) Write(p []byte) (n int, err error) {
|
||||||
|
w.t.wrmx.Lock()
|
||||||
|
if err = w.t.onWrite(p); err == nil {
|
||||||
|
n = len(p)
|
||||||
|
}
|
||||||
|
w.t.wrmx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# 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`
|
||||||
|
- 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 a simpler version
|
||||||
|
|
||||||
|
```
|
||||||
|
go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
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 "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/) support autocomplete and syntax validation.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 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
|
||||||
|
|
||||||
|
- Default values may change in updates
|
||||||
|
- FFmpeg module has many presets, they are not listed here because they may also change in updates
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api:
|
||||||
|
listen: ":1984" # default public port for WebUI and HTTP API
|
||||||
|
|
||||||
|
ffmpeg:
|
||||||
|
bin: "ffmpeg" # default binary path for FFmpeg
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: "info" # default log level
|
||||||
|
output: "stdout"
|
||||||
|
time: "UNIXMS"
|
||||||
|
|
||||||
|
rtsp:
|
||||||
|
listen: ":8554" # default public port for RTSP server
|
||||||
|
default_query: "video&audio"
|
||||||
|
|
||||||
|
srtp:
|
||||||
|
listen: ":8443" # default public port for SRTP server (used for HomeKit)
|
||||||
|
|
||||||
|
webrtc:
|
||||||
|
listen: ":8555" # default public port for WebRTC server (TCP and UDP)
|
||||||
|
ice_servers:
|
||||||
|
- 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`.
|
||||||
+85
-105
@@ -3,140 +3,120 @@ package app
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"runtime/debug"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "1.6.0"
|
var (
|
||||||
var UserAgent = "go2rtc/" + Version
|
Version string
|
||||||
|
Modules []string
|
||||||
|
UserAgent string
|
||||||
|
ConfigPath string
|
||||||
|
Info = make(map[string]any)
|
||||||
|
)
|
||||||
|
|
||||||
var ConfigPath string
|
const usage = `Usage of go2rtc:
|
||||||
var Info = map[string]any{
|
|
||||||
"version": Version,
|
-c, --config Path to config file or config string as YAML or JSON, support multiple
|
||||||
}
|
-d, --daemon Run in background
|
||||||
|
-v, --version Print version and exit
|
||||||
|
`
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var confs Config
|
var config flagConfig
|
||||||
|
var daemon bool
|
||||||
var version bool
|
var version bool
|
||||||
|
|
||||||
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
flag.Var(&config, "config", "")
|
||||||
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
flag.Var(&config, "c", "")
|
||||||
|
flag.BoolVar(&daemon, "daemon", false, "")
|
||||||
|
flag.BoolVar(&daemon, "d", false, "")
|
||||||
|
flag.BoolVar(&version, "version", false, "")
|
||||||
|
flag.BoolVar(&version, "v", false, "")
|
||||||
|
|
||||||
|
flag.Usage = func() { fmt.Print(usage) }
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
revision, vcsTime := readRevisionTime()
|
||||||
|
|
||||||
if version {
|
if version {
|
||||||
fmt.Println("Current version: ", Version)
|
fmt.Printf("go2rtc version %s (%s) %s/%s\n", Version, revision, runtime.GOOS, runtime.GOARCH)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if confs == nil {
|
if daemon && os.Getppid() != 1 {
|
||||||
confs = []string{"go2rtc.yaml"}
|
if runtime.GOOS == "windows" {
|
||||||
}
|
fmt.Println("Daemon mode is not supported on Windows")
|
||||||
|
os.Exit(1)
|
||||||
for _, conf := range confs {
|
|
||||||
if conf[0] != '{' {
|
|
||||||
// config as file
|
|
||||||
if ConfigPath == "" {
|
|
||||||
ConfigPath = conf
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := os.ReadFile(conf)
|
|
||||||
if data == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
|
||||||
configs = append(configs, data)
|
|
||||||
} else {
|
|
||||||
// config as raw YAML
|
|
||||||
configs = append(configs, []byte(conf))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-run the program in background and exit
|
||||||
|
cmd := exec.Command(os.Args[0], os.Args[1:]...)
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
fmt.Println("Failed to start daemon:", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Running in daemon mode with PID:", cmd.Process.Pid)
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UserAgent = "go2rtc/" + Version
|
||||||
|
|
||||||
|
Info["version"] = Version
|
||||||
|
Info["revision"] = revision
|
||||||
|
|
||||||
|
initConfig(config)
|
||||||
|
initLogger()
|
||||||
|
|
||||||
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
Logger.Info().Str("version", Version).Str("platform", platform).Str("revision", revision).Msg("go2rtc")
|
||||||
|
Logger.Debug().Str("version", runtime.Version()).Str("vcs.time", vcsTime).Msg("build")
|
||||||
|
|
||||||
if ConfigPath != "" {
|
if ConfigPath != "" {
|
||||||
if !filepath.IsAbs(ConfigPath) {
|
Logger.Info().Str("path", ConfigPath).Msg("config")
|
||||||
if cwd, err := os.Getwd(); err == nil {
|
|
||||||
ConfigPath = filepath.Join(cwd, ConfigPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Info["config_path"] = ConfigPath
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]string `yaml:"log"`
|
Mod struct {
|
||||||
|
Modules []string `yaml:"modules"`
|
||||||
|
} `yaml:"app"`
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadConfig(&cfg)
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
Modules = cfg.Mod.Modules
|
||||||
|
|
||||||
modules = cfg.Mod
|
|
||||||
|
|
||||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
func readRevisionTime() (revision, vcsTime string) {
|
||||||
var writer io.Writer = os.Stdout
|
if info, ok := debug.ReadBuildInfo(); ok {
|
||||||
|
for _, setting := range info.Settings {
|
||||||
|
switch setting.Key {
|
||||||
|
case "vcs.revision":
|
||||||
|
if len(setting.Value) > 7 {
|
||||||
|
revision = setting.Value[:7]
|
||||||
|
} else {
|
||||||
|
revision = setting.Value
|
||||||
|
}
|
||||||
|
case "vcs.time":
|
||||||
|
vcsTime = setting.Value
|
||||||
|
case "vcs.modified":
|
||||||
|
if setting.Value == "true" {
|
||||||
|
revision += ".dirty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if format != "json" {
|
// Check version from -buildvcs info
|
||||||
writer = zerolog.ConsoleWriter{
|
// Format for tagged version : v1.9.13
|
||||||
Out: writer, TimeFormat: "15:04:05.000",
|
// Format for modified code: v1.9.14-0.20251215184105-753d6617ab58+dirty
|
||||||
NoColor: writer != os.Stdout || format == "text",
|
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
|
||||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
|
||||||
lvl = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig(v any) {
|
|
||||||
for _, data := range configs {
|
|
||||||
if err := yaml.Unmarshal(data, v); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[app] read config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
|
||||||
if s, ok := modules[module]; ok {
|
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
|
||||||
if err == nil {
|
|
||||||
return log.Level(lvl)
|
|
||||||
}
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
return log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal
|
|
||||||
|
|
||||||
type Config []string
|
|
||||||
|
|
||||||
func (c *Config) String() string {
|
|
||||||
return strings.Join(*c, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Set(value string) error {
|
|
||||||
*c = append(*c, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var configs [][]byte
|
|
||||||
|
|
||||||
// modules log levels
|
|
||||||
var modules map[string]string
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/yaml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadConfig(v any) {
|
||||||
|
for _, data := range configs {
|
||||||
|
if err := yaml.Unmarshal(data, v); err != nil {
|
||||||
|
Logger.Warn().Err(err).Send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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, path, value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(ConfigPath, b, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
type flagConfig []string
|
||||||
|
|
||||||
|
func (c *flagConfig) String() string {
|
||||||
|
return strings.Join(*c, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *flagConfig) Set(value string) error {
|
||||||
|
*c = append(*c, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var configs [][]byte
|
||||||
|
|
||||||
|
func initConfig(confs flagConfig) {
|
||||||
|
if confs == nil {
|
||||||
|
confs = []string{"go2rtc.yaml"}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conf := range confs {
|
||||||
|
if len(conf) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if conf[0] == '{' {
|
||||||
|
// config as raw YAML or JSON
|
||||||
|
configs = append(configs, []byte(conf))
|
||||||
|
} else if data := parseConfString(conf); data != nil {
|
||||||
|
configs = append(configs, data)
|
||||||
|
} else {
|
||||||
|
// config as file
|
||||||
|
if ConfigPath == "" {
|
||||||
|
ConfigPath = conf
|
||||||
|
initStorage()
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, _ = os.ReadFile(conf); data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
loadEnv(data)
|
||||||
|
data = creds.ReplaceVars(data)
|
||||||
|
configs = append(configs, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ConfigPath != "" {
|
||||||
|
if !filepath.IsAbs(ConfigPath) {
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
ConfigPath = filepath.Join(cwd, ConfigPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Info["config_path"] = ConfigPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfString(s string) []byte {
|
||||||
|
i := strings.IndexByte(s, '=')
|
||||||
|
if i < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
items := strings.Split(s[:i], ".")
|
||||||
|
if len(items) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// `log.level=trace` => `{log: {level: trace}}`
|
||||||
|
var pre string
|
||||||
|
var suf = s[i+1:]
|
||||||
|
for _, item := range items {
|
||||||
|
pre += "{" + item + ": "
|
||||||
|
suf += "}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(pre + suf)
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return Logger.Level(lvl)
|
||||||
|
}
|
||||||
|
Logger.Warn().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// initLogger support:
|
||||||
|
// - output: empty (only to memory), stderr, stdout
|
||||||
|
// - format: empty (autodetect color support), color, json, text
|
||||||
|
// - time: empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO
|
||||||
|
// - level: disabled, trace, debug, info, warn, error...
|
||||||
|
func initLogger() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod map[string]string `yaml:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Mod = modules // defaults
|
||||||
|
|
||||||
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
|
var writer io.Writer
|
||||||
|
|
||||||
|
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"]
|
||||||
|
|
||||||
|
if writer != nil {
|
||||||
|
if format := modules["format"]; format != "json" {
|
||||||
|
console := &zerolog.ConsoleWriter{Out: writer}
|
||||||
|
|
||||||
|
switch format {
|
||||||
|
case "text":
|
||||||
|
console.NoColor = true
|
||||||
|
case "color":
|
||||||
|
console.NoColor = false // useless, but anyway
|
||||||
|
default:
|
||||||
|
// autodetection if output support color
|
||||||
|
// go-isatty - dependency for go-colorable - dependency for ConsoleWriter
|
||||||
|
console.NoColor = !isatty.IsTerminal(writer.(*os.File).Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeFormat != "" {
|
||||||
|
console.TimeFormat = "15:04:05.000"
|
||||||
|
} else {
|
||||||
|
console.PartsOrder = []string{
|
||||||
|
zerolog.LevelFieldName,
|
||||||
|
zerolog.CallerFieldName,
|
||||||
|
zerolog.MessageFieldName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = console
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = zerolog.MultiLevelWriter(writer, MemoryLog)
|
||||||
|
} else {
|
||||||
|
writer = MemoryLog
|
||||||
|
}
|
||||||
|
|
||||||
|
writer = creds.SecretWriter(writer)
|
||||||
|
|
||||||
|
lvl, _ := zerolog.ParseLevel(modules["level"])
|
||||||
|
Logger = zerolog.New(writer).Level(lvl)
|
||||||
|
|
||||||
|
if timeFormat != "" {
|
||||||
|
zerolog.TimeFieldFormat = timeFormat
|
||||||
|
Logger = Logger.With().Timestamp().Logger()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Logger zerolog.Logger
|
||||||
|
|
||||||
|
// modules log levels
|
||||||
|
var modules = map[string]string{
|
||||||
|
"format": "", // useless, but anyway
|
||||||
|
"level": "info",
|
||||||
|
"output": "stdout", // TODO: change to stderr someday
|
||||||
|
"time": zerolog.TimeFormatUnixMs,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
chunkCount = 16
|
||||||
|
chunkSize = 1 << 16
|
||||||
|
)
|
||||||
|
|
||||||
|
type circularBuffer struct {
|
||||||
|
chunks [][]byte
|
||||||
|
r, w int
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBuffer() *circularBuffer {
|
||||||
|
b := &circularBuffer{chunks: make([][]byte, 0, chunkCount)}
|
||||||
|
// create first chunk
|
||||||
|
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
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 == chunkCount {
|
||||||
|
b.w = 0
|
||||||
|
}
|
||||||
|
// check overflow
|
||||||
|
if b.r == b.w {
|
||||||
|
// increase read chunk index
|
||||||
|
if b.r++; b.r == chunkCount {
|
||||||
|
b.r = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check if current chunk exists
|
||||||
|
if b.w == len(b.chunks) {
|
||||||
|
// allocate new chunk
|
||||||
|
b.chunks = append(b.chunks, make([]byte, 0, chunkSize))
|
||||||
|
} else {
|
||||||
|
// reset len of current chunk
|
||||||
|
b.chunks[b.w] = b.chunks[b.w][:0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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 == chunkCount {
|
||||||
|
i = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const name = "go2rtc.json"
|
|
||||||
|
|
||||||
var store map[string]any
|
|
||||||
|
|
||||||
func load() {
|
|
||||||
data, _ := os.ReadFile(name)
|
|
||||||
if data != nil {
|
|
||||||
if err := json.Unmarshal(data, &store); err != nil {
|
|
||||||
// TODO: log
|
|
||||||
log.Warn().Err(err).Msg("[app] read storage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if store == nil {
|
|
||||||
store = make(map[string]any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() error {
|
|
||||||
data, err := json.Marshal(store)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(name, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRaw(key string) any {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
return store[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDict(key string) map[string]any {
|
|
||||||
raw := GetRaw(key)
|
|
||||||
if raw != nil {
|
|
||||||
return raw.(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
return make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(key string, v any) error {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
store[key] = v
|
|
||||||
|
|
||||||
return save()
|
|
||||||
}
|
|
||||||
@@ -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,13 @@
|
|||||||
|
package bubble
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/bubble"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("bubble", func(source string) (core.Producer, error) {
|
||||||
|
return bubble.Dial(source)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Debug
|
||||||
|
|
||||||
|
This module provides `GET /api/stack`, with which you can find hanging goroutines
|
||||||
@@ -2,16 +2,8 @@ package debug
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
api.HandleFunc("api/stack", stackHandler)
|
api.HandleFunc("api/stack", stackHandler)
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullHandler(string) (core.Producer, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ package debug
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stackSkip = [][]byte{
|
var stackSkip = [][]byte{
|
||||||
@@ -24,9 +25,12 @@ var stackSkip = [][]byte{
|
|||||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.Init"),
|
||||||
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
|
||||||
|
|
||||||
|
// homekit
|
||||||
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
|
||||||
|
|
||||||
// webrtc/api.go
|
// webrtc/api.go
|
||||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v4.NewTCPMuxDefault"),
|
||||||
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
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
|
||||||
|
```
|
||||||
+6
-25
@@ -10,32 +10,16 @@ import (
|
|||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
streams.HandleFunc("dvrip", handle)
|
streams.HandleFunc("dvrip", dvrip.Dial)
|
||||||
|
|
||||||
// DVRIP client autodiscovery
|
// DVRIP client autodiscovery
|
||||||
api.HandleFunc("api/dvrip", apiDvrip)
|
api.HandleFunc("api/dvrip", apiDvrip)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn := dvrip.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Play(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Handle(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const Port = 34569 // UDP port number for dvrip discovery
|
const Port = 34569 // UDP port number for dvrip discovery
|
||||||
|
|
||||||
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -45,10 +29,10 @@ func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
func discover() ([]api.Stream, error) {
|
func discover() ([]*api.Source, error) {
|
||||||
addr := &net.UDPAddr{
|
addr := &net.UDPAddr{
|
||||||
Port: Port,
|
Port: Port,
|
||||||
IP: net.IP{239, 255, 255, 250},
|
IP: net.IP{239, 255, 255, 250},
|
||||||
@@ -63,7 +47,7 @@ func discover() ([]api.Stream, error) {
|
|||||||
|
|
||||||
go sendBroadcasts(conn)
|
go sendBroadcasts(conn)
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
|
|
||||||
for _, info := range getResponses(conn) {
|
for _, info := range getResponses(conn) {
|
||||||
if info.HostIP == "" || info.HostName == "" {
|
if info.HostIP == "" || info.HostName == "" {
|
||||||
@@ -75,7 +59,7 @@ func discover() ([]api.Stream, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
items = append(items, api.Stream{
|
items = append(items, &api.Source{
|
||||||
Name: info.HostName,
|
Name: info.HostName,
|
||||||
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
|
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
|
||||||
})
|
})
|
||||||
@@ -98,10 +82,7 @@ func sendBroadcasts(conn *net.UDPConn) {
|
|||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
_, _ = conn.WriteToUDP(data, addr)
|
||||||
if _, err = conn.WriteToUDP(data, addr); err != nil {
|
|
||||||
log.Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
```
|
||||||
+22
-5
@@ -2,28 +2,45 @@ package echo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
"os/exec"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
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")
|
log := app.GetLogger("echo")
|
||||||
|
|
||||||
streams.HandleFunc("echo", func(url string) (core.Producer, error) {
|
streams.RedirectFunc("echo", func(url string) (string, error) {
|
||||||
args := shell.QuoteSplit(url[5:])
|
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()
|
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
b = bytes.TrimSpace(b)
|
b = bytes.TrimSpace(b)
|
||||||
|
|
||||||
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
||||||
|
|
||||||
return streams.GetProducer(string(b))
|
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
|
||||||
|
```
|
||||||
+183
-55
@@ -1,25 +1,42 @@
|
|||||||
package exec
|
package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/magic"
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
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 {
|
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||||
waitersMu.Lock()
|
waitersMu.Lock()
|
||||||
waiter := waiters[conn.URL.Path]
|
waiter := waiters[conn.URL.Path]
|
||||||
@@ -39,69 +56,126 @@ func Init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
streams.HandleFunc("exec", execHandle)
|
streams.HandleFunc("exec", execHandle)
|
||||||
|
streams.MarkInsecure("exec")
|
||||||
|
|
||||||
log = app.GetLogger("exec")
|
log = app.GetLogger("exec")
|
||||||
}
|
}
|
||||||
|
|
||||||
func execHandle(url 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)
|
||||||
|
|
||||||
var path string
|
var path string
|
||||||
|
|
||||||
args := shell.QuoteSplit(url[5:]) // remove `exec:`
|
// RTSP flow should have `{output}` inside URL
|
||||||
for i, arg := range args {
|
// pipe flow may have `#{params}` inside URL
|
||||||
if arg == "{output}" {
|
if i := strings.Index(rawURL, "{output}"); i > 0 {
|
||||||
if rtsp.Port == "" {
|
if rtsp.Port == "" {
|
||||||
return nil, errors.New("rtsp module disabled")
|
return nil, errors.New("exec: rtsp module disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
sum := md5.Sum([]byte(url))
|
sum := md5.Sum([]byte(rawURL))
|
||||||
path = "/" + hex.EncodeToString(sum[:])
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
args[i] = "rtsp://127.0.0.1:" + rtsp.Port + path
|
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||||
break
|
}
|
||||||
|
|
||||||
|
cmd := shell.NewCommand(rawURL[5:]) // remove `exec:`
|
||||||
|
cmd.Stderr = &logWriter{
|
||||||
|
buf: make([]byte, 512),
|
||||||
|
debug: log.Debug().Enabled(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {
|
||||||
|
_ = cmd.Close()
|
||||||
|
return nil, errors.New("exec: bin not in allow_paths: " + cmd.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
if s := query.Get("killtimeout"); s != "" {
|
||||||
if log.Debug().Enabled() {
|
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
|
||||||
cmd.Stderr = os.Stderr
|
}
|
||||||
|
|
||||||
|
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 == "" {
|
if path == "" {
|
||||||
return handlePipe(url, cmd)
|
prod, err = handlePipe(rawURL, cmd)
|
||||||
|
} else {
|
||||||
|
prod, err = handleRTSP(rawURL, cmd, path, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleRTSP(url, path, cmd)
|
if err != nil {
|
||||||
|
_ = cmd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePipe(url string, cmd *exec.Cmd) (core.Producer, error) {
|
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
|
||||||
r, err := PipeCloser(cmd)
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rd := struct {
|
||||||
|
io.Reader
|
||||||
|
io.Closer
|
||||||
|
}{
|
||||||
|
// add buffer for pipe reader to reduce syscall
|
||||||
|
bufio.NewReaderSize(stdout, core.BufferSize),
|
||||||
|
// stop cmd on close pipe call
|
||||||
|
cmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
if err = cmd.Start(); err != nil {
|
if err = cmd.Start(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client := magic.NewClient(r)
|
prod, err := magic.Open(rd)
|
||||||
if err = client.Probe(); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
client.Desc = "exec active producer"
|
if info, ok := prod.(core.Info); ok {
|
||||||
client.URL = url
|
info.SetProtocol("pipe")
|
||||||
|
setRemoteInfo(info, source, cmd.Args)
|
||||||
|
}
|
||||||
|
|
||||||
return client, nil
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
func handleRTSP(source string, cmd *shell.Command, path string, timeout time.Duration) (core.Producer, error) {
|
||||||
if log.Trace().Enabled() {
|
if log.Trace().Enabled() {
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
ch := make(chan core.Producer)
|
waiter := make(chan *pkg.Conn, 1)
|
||||||
|
|
||||||
waitersMu.Lock()
|
waitersMu.Lock()
|
||||||
waiters[path] = ch
|
waiters[path] = waiter
|
||||||
waitersMu.Unlock()
|
waitersMu.Unlock()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -110,42 +184,96 @@ func handleRTSP(url, path string, cmd *exec.Cmd) (core.Producer, error) {
|
|||||||
waitersMu.Unlock()
|
waitersMu.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.Debug().Str("url", url).Msg("[exec] run")
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp")
|
||||||
|
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
log.Error().Err(err).Str("url", url).Msg("[exec]")
|
log.Error().Err(err).Str("source", source).Msg("[exec]")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
chErr := make(chan error)
|
timer := time.NewTimer(timeout)
|
||||||
|
defer timer.Stop()
|
||||||
go func() {
|
|
||||||
err := cmd.Wait()
|
|
||||||
// unblocking write to channel
|
|
||||||
select {
|
|
||||||
case chErr <- err:
|
|
||||||
default:
|
|
||||||
log.Trace().Str("url", url).Msg("[exec] close")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Second * 60):
|
case <-timer.C:
|
||||||
_ = cmd.Process.Kill()
|
// haven't received data from app in timeout
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||||
return nil, errors.New("timeout")
|
return nil, errors.New("exec: timeout")
|
||||||
case err := <-chErr:
|
case <-cmd.Done():
|
||||||
return nil, fmt.Errorf("exec: %s", err)
|
// app fail before we receive any data
|
||||||
case prod := <-ch:
|
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
case prod := <-waiter:
|
||||||
|
// app started successfully
|
||||||
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||||
|
setRemoteInfo(prod, source, cmd.Args)
|
||||||
|
prod.OnClose = cmd.Close
|
||||||
return prod, nil
|
return prod, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal
|
// internal
|
||||||
|
|
||||||
var log zerolog.Logger
|
var (
|
||||||
var waiters = map[string]chan core.Producer{}
|
log zerolog.Logger
|
||||||
var waitersMu sync.Mutex
|
waiters = make(map[string]chan *pkg.Conn)
|
||||||
|
waitersMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
type logWriter struct {
|
||||||
|
buf []byte
|
||||||
|
debug bool
|
||||||
|
n int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logWriter) String() string {
|
||||||
|
if l.n == len(l.buf) {
|
||||||
|
return string(l.buf) + "..."
|
||||||
|
}
|
||||||
|
return string(l.buf[:l.n])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *logWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if l.n < cap(l.buf) {
|
||||||
|
l.n += copy(l.buf[l.n:], p)
|
||||||
|
}
|
||||||
|
n = len(p)
|
||||||
|
if l.debug {
|
||||||
|
if p = trimSpace(p); p != nil {
|
||||||
|
log.Debug().Msgf("[exec] %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpace(b []byte) []byte {
|
||||||
|
start := 0
|
||||||
|
stop := len(b)
|
||||||
|
for ; start < stop; start++ {
|
||||||
|
if b[start] >= ' ' {
|
||||||
|
break // trim all ASCII before 0x20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; ; stop-- {
|
||||||
|
if stop == start {
|
||||||
|
return nil // skip empty output
|
||||||
|
}
|
||||||
|
if b[stop-1] > ' ' {
|
||||||
|
break // trim all ASCII before 0x21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[start:stop]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRemoteInfo(info core.Info, source string, args []string) {
|
||||||
|
info.SetSource(source)
|
||||||
|
|
||||||
|
if i := core.Index(args, "-i"); i > 0 && i < len(args)-1 {
|
||||||
|
rawURL := args[i+1]
|
||||||
|
if u, err := url.Parse(rawURL); err == nil && u.Host != "" {
|
||||||
|
info.SetRemoteAddr(u.Host)
|
||||||
|
info.SetURL(rawURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package exec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PipeCloser - return StdoutPipe that Kill cmd on Close call
|
|
||||||
func PipeCloser(cmd *exec.Cmd) (io.ReadCloser, error) {
|
|
||||||
stdout, err := cmd.StdoutPipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pipeCloser{stdout, cmd}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type pipeCloser struct {
|
|
||||||
io.ReadCloser
|
|
||||||
cmd *exec.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p pipeCloser) Close() error {
|
|
||||||
return core.Any(p.ReadCloser.Close(), p.cmd.Process.Kill(), p.cmd.Wait())
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
# 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
|
||||||
|
- your expression should return a link of any supported source
|
||||||
|
- expression supports multiple operation, but:
|
||||||
|
- all operations must be separated by a semicolon
|
||||||
|
- all operations, except the last one, must declare a new variable (`let s = "abc";`)
|
||||||
|
- the last operation should return a string
|
||||||
|
- go2rtc supports additional functions:
|
||||||
|
- `fetch` - JS-like HTTP requests
|
||||||
|
- `match` - JS-like RegExp queries
|
||||||
|
|
||||||
|
## 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';
|
||||||
|
|
||||||
|
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 from https://github.com/ad/domru
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
dom_ru: |
|
||||||
|
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.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
example_python: |
|
||||||
|
echo:python -c 'from urllib.request import urlopen; import re
|
||||||
|
|
||||||
|
# url1 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8"
|
||||||
|
html1 = urlopen("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").read().decode("utf-8")
|
||||||
|
url1 = re.search(r"https.+?m3u8", html1)[0]
|
||||||
|
|
||||||
|
# url2 = "gear1/prog_index.m3u8"
|
||||||
|
html2 = urlopen(url1).read().decode("utf-8")
|
||||||
|
url2 = re.search(r"^[a-z0-1/_]+\.m3u8$", html2, flags=re.MULTILINE)[0]
|
||||||
|
|
||||||
|
# url3 = "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8"
|
||||||
|
url3 = url1[:url1.rindex("/")+1] + url2
|
||||||
|
|
||||||
|
print("ffmpeg:" + url3 + "#video=copy")'
|
||||||
|
|
||||||
|
example_expr: |
|
||||||
|
expr:
|
||||||
|
|
||||||
|
let html1 = fetch("https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html").text;
|
||||||
|
let url1 = match(html1, "https.+?m3u8")[0];
|
||||||
|
|
||||||
|
let html2 = fetch(url1).text;
|
||||||
|
let url2 = match(html2, "^[a-z0-1/_]+\\.m3u8$", "m")[0];
|
||||||
|
|
||||||
|
let url3 = url1[:lastIndexOf(url1, "/")+1] + url2;
|
||||||
|
|
||||||
|
"ffmpeg:" + url3 + "#video=copy"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparison
|
||||||
|
|
||||||
|
| expr | python | js |
|
||||||
|
|------------------------------|----------------------------|--------------------------------|
|
||||||
|
| let x = 1; | x = 1 | let x = 1 |
|
||||||
|
| {a: 1, b: 2} | {"a": 1, "b": 2} | {a: 1, b: 2} |
|
||||||
|
| let r = fetch(url, {method}) | r = request(method, url) | r = await fetch(url, {method}) |
|
||||||
|
| r.ok | r.ok | r.ok |
|
||||||
|
| r.status | r.status_code | r.status |
|
||||||
|
| r.text | r.text | await r.text() |
|
||||||
|
| r.json() | r.json() | await r.json() |
|
||||||
|
| r.headers | r.headers | r.headers |
|
||||||
|
| let m = match(text, "abc") | m = re.search("abc", text) | let m = text.match(/abc/) |
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package expr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
log := app.GetLogger("expr")
|
||||||
|
|
||||||
|
streams.RedirectFunc("expr", func(url string) (string, error) {
|
||||||
|
v, err := expr.Eval(url[5:], nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("[expr] url=%s", url)
|
||||||
|
|
||||||
|
if url = v.(string); url == "" {
|
||||||
|
return "", errors.New("expr: result is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, nil
|
||||||
|
})
|
||||||
|
streams.MarkInsecure("expr")
|
||||||
|
}
|
||||||
+50
-49
@@ -1,61 +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
|
- FFmpeg preinstalled for **Docker** and **Home Assistant add-on** users
|
||||||
- `A-V` - av_diff, difference between audio and video timestamps
|
- **Home Assistant add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
|
||||||
- `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)
|
|
||||||
|
|
||||||
`M-V`, `M-A` means video stream only, audio stream only respectively.
|
## Configuration
|
||||||
|
|
||||||
## Devices Windows
|
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
>ffmpeg -hide_banner -f dshow -list_options true -i video="VMware Virtual USB Video Device"
|
streams:
|
||||||
[dshow @ 0000025695e52900] DirectShow video device options (from video devices)
|
# [FILE] all tracks will be copied without transcoding codecs
|
||||||
[dshow @ 0000025695e52900] Pin "Record" (alternative pin name "0")
|
file1: ffmpeg:/media/BigBuckBunny.mp4
|
||||||
[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)
|
# [FILE] video will be transcoded to H264, audio will be skipped
|
||||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23
|
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
|
||||||
[dshow @ 0000025695e52900] pixel_format=nv12 min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)
|
|
||||||
|
# [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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Devices Mac
|
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`.
|
||||||
|
|
||||||
```
|
But you can override them via YAML config. You can also add your own formats to the config and use them with source params.
|
||||||
% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i ""
|
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:
|
```yaml
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera
|
ffmpeg:
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0
|
bin: ffmpeg # path to ffmpeg binary
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:
|
global: "-hide_banner"
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)
|
timeout: 5 # default timeout in seconds for rtsp inputs
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone
|
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
|
||||||
[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)
|
mycodec: "-any args that supported by ffmpeg..."
|
||||||
|
myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}"
|
||||||
|
myraw: "-ss 00:00:20"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Devices Linux
|
- 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).
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
||||||
## Useful links
|
**PS.** It is recommended to check the available hardware in the WebUI add page.
|
||||||
|
|
||||||
- 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)
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
)
|
||||||
|
|
||||||
|
func apiFFmpeg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
query := r.URL.Query()
|
||||||
|
dst := query.Get("dst")
|
||||||
|
stream := streams.Get(dst)
|
||||||
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var src string
|
||||||
|
if s := query.Get("file"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto#input=file"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("live"); s != "" {
|
||||||
|
if streams.Validate(s) == nil {
|
||||||
|
src = "ffmpeg:" + s + "#audio=auto"
|
||||||
|
}
|
||||||
|
} else if s = query.Get("text"); s != "" {
|
||||||
|
if strings.IndexAny(s, `'"&%$`) < 0 {
|
||||||
|
src = "ffmpeg:tts?text=" + s
|
||||||
|
if s = query.Get("voice"); s != "" {
|
||||||
|
src += "&voice=" + s
|
||||||
|
}
|
||||||
|
src += "#audio=auto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if src == "" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stream.Play(src); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
//go:build freebsd || netbsd || openbsd || dragonfly
|
||||||
|
|
||||||
|
package device
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func queryToInput(query url.Values) string {
|
||||||
|
if video := query.Get("video"); video != "" {
|
||||||
|
// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2
|
||||||
|
input := "-f v4l2"
|
||||||
|
|
||||||
|
for key, value := range query {
|
||||||
|
switch key {
|
||||||
|
case "resolution":
|
||||||
|
input += " -video_size " + value[0]
|
||||||
|
case "video_size", "pixel_format", "input_format", "framerate", "use_libv4l2":
|
||||||
|
input += " -" + key + " " + value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return input + " -i " + indexToItem(videos, video)
|
||||||
|
}
|
||||||
|
|
||||||
|
if audio := query.Get("audio"); audio != "" {
|
||||||
|
input := "-f oss"
|
||||||
|
|
||||||
|
for key, value := range query {
|
||||||
|
switch key {
|
||||||
|
case "channels", "sample_rate":
|
||||||
|
input += " -" + key + " " + value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return input + " -i " + indexToItem(audios, audio)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDevices() {
|
||||||
|
files, err := os.ReadDir("/dev")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if !strings.HasPrefix(file.Name(), core.KindVideo) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "/dev/" + file.Name()
|
||||||
|
|
||||||
|
cmd := exec.Command(
|
||||||
|
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
||||||
|
)
|
||||||
|
b, _ := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
// [video4linux2,v4l2 @ 0x860b92280] 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 @ 0x860b92280] Compressed: mjpeg : Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960
|
||||||
|
re := regexp.MustCompile("(Raw *|Compressed): +(.+?) : +(.+?) : (.+)")
|
||||||
|
m := re.FindAllStringSubmatch(string(b), -1)
|
||||||
|
for _, i := range m {
|
||||||
|
size, _, _ := strings.Cut(i[4], " ")
|
||||||
|
stream := &api.Source{
|
||||||
|
Name: i[3],
|
||||||
|
Info: i[4],
|
||||||
|
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
||||||
|
}
|
||||||
|
|
||||||
|
if i[1] != "Compressed" {
|
||||||
|
stream.URL += "#video=h264#hardware"
|
||||||
|
}
|
||||||
|
|
||||||
|
videos = append(videos, name)
|
||||||
|
streams = append(streams, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = exec.Command(Bin, "-f", "oss", "-i", "/dev/dsp", "-t", "1", "-f", "null", "-").Run()
|
||||||
|
if err == nil {
|
||||||
|
stream := &api.Source{
|
||||||
|
Name: "OSS default",
|
||||||
|
Info: " ",
|
||||||
|
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
|
||||||
|
}
|
||||||
|
|
||||||
|
audios = append(audios, "default")
|
||||||
|
streams = append(streams, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
|
//go:build darwin || ios
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func queryToInput(query url.Values) string {
|
func queryToInput(query url.Values) string {
|
||||||
@@ -78,7 +81,7 @@ func initDevices() {
|
|||||||
audios = append(audios, name)
|
audios = append(audios, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
streams = append(streams, api.Stream{
|
streams = append(streams, &api.Source{
|
||||||
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func queryToInput(query url.Values) string {
|
func queryToInput(query url.Values) string {
|
||||||
@@ -28,8 +31,16 @@ func queryToInput(query url.Values) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if audio := query.Get("audio"); audio != "" {
|
if audio := query.Get("audio"); audio != "" {
|
||||||
|
// https://trac.ffmpeg.org/wiki/Capture/ALSA
|
||||||
input := "-f alsa"
|
input := "-f alsa"
|
||||||
|
|
||||||
|
for key, value := range query {
|
||||||
|
switch key {
|
||||||
|
case "channels", "sample_rate":
|
||||||
|
input += " -" + key + " " + value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return input + " -i " + indexToItem(audios, audio)
|
return input + " -i " + indexToItem(audios, audio)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,8 +72,9 @@ func initDevices() {
|
|||||||
m := re.FindAllStringSubmatch(string(b), -1)
|
m := re.FindAllStringSubmatch(string(b), -1)
|
||||||
for _, i := range m {
|
for _, i := range m {
|
||||||
size, _, _ := strings.Cut(i[4], " ")
|
size, _, _ := strings.Cut(i[4], " ")
|
||||||
stream := api.Stream{
|
stream := &api.Source{
|
||||||
Name: i[3] + " | " + i[4],
|
Name: i[3],
|
||||||
|
Info: i[4],
|
||||||
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
URL: "ffmpeg:device?video=" + name + "&input_format=" + i[2] + "&video_size=" + size,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +89,10 @@ func initDevices() {
|
|||||||
|
|
||||||
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
|
err = exec.Command(Bin, "-f", "alsa", "-i", "default", "-t", "1", "-f", "null", "-").Run()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
stream := api.Stream{
|
stream := &api.Source{
|
||||||
Name: "ALSA default",
|
Name: "ALSA default",
|
||||||
URL: "ffmpeg:device?audio=default#audio=opus",
|
Info: " ",
|
||||||
|
URL: "ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus",
|
||||||
}
|
}
|
||||||
|
|
||||||
audios = append(audios, "default")
|
audios = append(audios, "default")
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
func queryToInput(query url.Values) string {
|
func queryToInput(query url.Values) string {
|
||||||
@@ -44,30 +47,20 @@ func queryToInput(query url.Values) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if video != "" {
|
if video != "" {
|
||||||
input += ` -i video="` + video + `"`
|
input += ` -i "video=` + video
|
||||||
|
|
||||||
if audio != "" {
|
if audio != "" {
|
||||||
input += `:audio="` + audio + `"`
|
input += `:audio=` + audio
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input += `"`
|
||||||
} else {
|
} else {
|
||||||
input += ` -i audio="` + audio + `"`
|
input += ` -i "audio=` + audio + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceInputSuffix(video, audio string) string {
|
|
||||||
switch {
|
|
||||||
case video != "" && audio != "":
|
|
||||||
return `video="` + video + `":audio=` + audio + `"`
|
|
||||||
case video != "":
|
|
||||||
return `video="` + video + `"`
|
|
||||||
case audio != "":
|
|
||||||
return `audio="` + audio + `"`
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDevices() {
|
func initDevices() {
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
||||||
@@ -79,7 +72,7 @@ func initDevices() {
|
|||||||
name := m[1]
|
name := m[1]
|
||||||
kind := m[2]
|
kind := m[2]
|
||||||
|
|
||||||
stream := api.Stream{
|
stream := &api.Source{
|
||||||
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +82,7 @@ func initDevices() {
|
|||||||
stream.URL += "#video=h264#hardware"
|
stream.URL += "#video=h264#hardware"
|
||||||
case core.KindAudio:
|
case core.KindAudio:
|
||||||
audios = append(audios, name)
|
audios = append(audios, name)
|
||||||
|
stream.URL += "&channels=1&sample_rate=16000&audio_buffer_size=10"
|
||||||
}
|
}
|
||||||
|
|
||||||
streams = append(streams, stream)
|
streams = append(streams, stream)
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package device
|
package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(bin string) {
|
func Init(bin string) {
|
||||||
@@ -16,36 +15,27 @@ func Init(bin string) {
|
|||||||
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
api.HandleFunc("api/ffmpeg/devices", apiDevices)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
func GetInput(src string) string {
|
||||||
i := strings.IndexByte(src, '?')
|
query, err := url.ParseQuery(src)
|
||||||
if i < 0 {
|
|
||||||
return "", errors.New("empty query: " + src)
|
|
||||||
}
|
|
||||||
|
|
||||||
query, err := url.ParseQuery(src[i+1:])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
runonce.Do(initDevices)
|
runonce.Do(initDevices)
|
||||||
|
|
||||||
if input := queryToInput(query); input != "" {
|
return queryToInput(query)
|
||||||
return input, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("wrong query: " + src)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Bin string
|
var Bin string
|
||||||
|
|
||||||
var videos, audios []string
|
var videos, audios []string
|
||||||
var streams []api.Stream
|
var streams []*api.Source
|
||||||
var runonce sync.Once
|
var runonce sync.Once
|
||||||
|
|
||||||
func apiDevices(w http.ResponseWriter, r *http.Request) {
|
func apiDevices(w http.ResponseWriter, r *http.Request) {
|
||||||
runonce.Do(initDevices)
|
runonce.Do(initDevices)
|
||||||
|
|
||||||
api.ResponseStreams(w, streams)
|
api.ResponseSources(w, streams)
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexToItem(items []string, index string) string {
|
func indexToItem(items []string, index string) string {
|
||||||
|
|||||||
+123
-39
@@ -1,110 +1,156 @@
|
|||||||
package ffmpeg
|
package ffmpeg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/device"
|
||||||
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual"
|
||||||
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
Mod map[string]string `yaml:"ffmpeg"`
|
Mod map[string]string `yaml:"ffmpeg"`
|
||||||
|
Log struct {
|
||||||
|
Level string `yaml:"ffmpeg"`
|
||||||
|
} `yaml:"log"`
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Mod = defaults // will be overriden from yaml
|
cfg.Mod = defaults // will be overriden from yaml
|
||||||
|
cfg.Log.Level = "error"
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
log = app.GetLogger("ffmpeg")
|
||||||
defaults["global"] += " -v error"
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
|
// zerolog levels: trace debug info warn error fatal panic disabled
|
||||||
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
// FFmpeg levels: trace debug verbose info warning error fatal panic quiet
|
||||||
if args == nil {
|
if cfg.Log.Level == "warn" {
|
||||||
return nil, errors.New("can't generate ffmpeg command")
|
cfg.Log.Level = "warning"
|
||||||
|
}
|
||||||
|
defaults["global"] += " -v " + cfg.Log.Level
|
||||||
|
|
||||||
|
streams.RedirectFunc("ffmpeg", func(url string) (string, error) {
|
||||||
|
if _, err := Version(); err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
return streams.GetProducer("exec:" + args.String())
|
args := parseArgs(url[7:])
|
||||||
|
if core.Contains(args.Codecs, "auto") {
|
||||||
|
return "", nil // force call streams.HandleFunc("ffmpeg")
|
||||||
|
}
|
||||||
|
return "exec:" + args.String(), nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("ffmpeg", NewProducer)
|
||||||
|
|
||||||
|
api.HandleFunc("api/ffmpeg", apiFFmpeg)
|
||||||
|
|
||||||
device.Init(defaults["bin"])
|
device.Init(defaults["bin"])
|
||||||
hardware.Init(defaults["bin"])
|
hardware.Init(defaults["bin"])
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaults = map[string]string{
|
var defaults = map[string]string{
|
||||||
"bin": "ffmpeg",
|
"bin": "ffmpeg",
|
||||||
"global": "-hide_banner",
|
"global": "-hide_banner",
|
||||||
|
"timeout": "5",
|
||||||
|
|
||||||
// inputs
|
// inputs
|
||||||
"file": "-re -i {input}",
|
"file": "-re -i {input}",
|
||||||
"http": "-fflags nobuffer -flags low_delay -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": "-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}",
|
||||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
|
||||||
|
|
||||||
// output
|
// output
|
||||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
||||||
"output/mjpeg": "-f mjpeg -",
|
"output/mjpeg": "-f mjpeg -",
|
||||||
|
"output/raw": "-f yuv4mpegpipe -",
|
||||||
|
"output/aac": "-f adts -",
|
||||||
|
"output/wav": "-f wav -",
|
||||||
|
|
||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
||||||
// `-tune zerolatency` - for minimal latency
|
// `-tune zerolatency` - for minimal latency
|
||||||
// `-profile high -level 4.1` - most used streaming profile
|
// `-profile high -level 4.1` - most used streaming profile
|
||||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p",
|
// `-pix_fmt:v yuv420p` - important for Telegram
|
||||||
"h265": "-c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
"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 -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",
|
||||||
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
//"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
||||||
|
|
||||||
|
"raw": "-c:v rawvideo",
|
||||||
|
"raw/gray8": "-c:v rawvideo -pix_fmt:v gray8",
|
||||||
|
"raw/yuv420p": "-c:v rawvideo -pix_fmt:v yuv420p",
|
||||||
|
"raw/yuv422p": "-c:v rawvideo -pix_fmt:v yuv422p",
|
||||||
|
"raw/yuv444p": "-c:v rawvideo -pix_fmt:v yuv444p",
|
||||||
|
|
||||||
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1
|
||||||
// https://github.com/pion/webrtc/issues/1514
|
// https://github.com/pion/webrtc/issues/1514
|
||||||
// `-af adelay=0|0` - force frame_size=960, important for WebRTC audio quality
|
// https://ffmpeg.org/ffmpeg-resampler.html
|
||||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -af adelay=0|0",
|
// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality
|
||||||
|
"opus": "-c:a libopus -application:a lowdelay -min_comp 0",
|
||||||
|
"opus/16000": "-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1",
|
||||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||||
|
"pcmu/8000": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
||||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
||||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
||||||
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||||
|
"pcma/8000": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
||||||
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
||||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
||||||
"aac": "-c:a aac", // keep sample rate and channels
|
"aac": "-c:a aac", // keep sample rate and channels
|
||||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
||||||
"mp3": "-c:a libmp3lame -q:a 8",
|
"mp3": "-c:a libmp3lame -q:a 8",
|
||||||
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
"pcm": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||||
|
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
||||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
||||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
"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
|
// hardware Intel and AMD on Linux
|
||||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
||||||
// `-bf 0` - disable B-frames is very important
|
// `-bf 0` - disable B-frames is very important
|
||||||
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
||||||
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
|
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0",
|
||||||
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
||||||
|
|
||||||
// hardware Raspberry
|
// hardware Raspberry
|
||||||
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
||||||
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
||||||
|
|
||||||
|
// 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 -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
|
// hardware NVidia on Linux and Windows
|
||||||
// preset=p2 - faster, tune=ll - low latency
|
// preset=p2 - faster, tune=ll - low latency
|
||||||
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
"h264/cuda": "-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
||||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto",
|
"h265/cuda": "-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto",
|
||||||
|
|
||||||
// hardware Intel on Windows
|
// hardware Intel on Windows
|
||||||
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
||||||
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
|
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1",
|
||||||
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
"mjpeg/dxva2": "-c:v mjpeg_qsv",
|
||||||
|
|
||||||
// hardware macOS
|
// hardware macOS
|
||||||
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
||||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
|
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
// configTemplate - return template from config (defaults) if exist or return raw template
|
// configTemplate - return template from config (defaults) if exist or return raw template
|
||||||
func configTemplate(template string) string {
|
func configTemplate(template string) string {
|
||||||
if s := defaults[template]; s != "" {
|
if s := defaults[template]; s != "" {
|
||||||
@@ -123,19 +169,28 @@ func inputTemplate(name, s string, query url.Values) string {
|
|||||||
} else {
|
} else {
|
||||||
template = defaults[name]
|
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)
|
return strings.Replace(template, "{input}", s, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseArgs(s string) *ffmpeg.Args {
|
func parseArgs(s string) *ffmpeg.Args {
|
||||||
// init FFmpeg arguments
|
// init FFmpeg arguments
|
||||||
args := &ffmpeg.Args{
|
args := &ffmpeg.Args{
|
||||||
Bin: defaults["bin"],
|
Bin: defaults["bin"],
|
||||||
Global: defaults["global"],
|
Global: defaults["global"],
|
||||||
Output: defaults["output"],
|
Output: defaults["output"],
|
||||||
|
Version: verAV,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var source = s
|
||||||
var query url.Values
|
var query url.Values
|
||||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
if i := strings.IndexByte(s, '#'); i >= 0 {
|
||||||
query = streams.ParseQuery(s[i+1:])
|
query = streams.ParseQuery(s[i+1:])
|
||||||
args.Video = len(query["video"])
|
args.Video = len(query["video"])
|
||||||
args.Audio = len(query["audio"])
|
args.Audio = len(query["audio"])
|
||||||
@@ -176,12 +231,19 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
default:
|
default:
|
||||||
s += "?video&audio"
|
s += "?video&audio"
|
||||||
}
|
}
|
||||||
|
s += "&source=ffmpeg:" + url.QueryEscape(source)
|
||||||
|
for _, v := range query["query"] {
|
||||||
|
s += "&" + v
|
||||||
|
}
|
||||||
args.Input = inputTemplate("rtsp", s, query)
|
args.Input = inputTemplate("rtsp", s, query)
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
} else if i = strings.Index(s, "?"); i > 0 {
|
||||||
var err error
|
switch s[:i] {
|
||||||
args.Input, err = device.GetInput(s)
|
case "device":
|
||||||
if err != nil {
|
args.Input = device.GetInput(s[i+1:])
|
||||||
return nil
|
case "virtual":
|
||||||
|
args.Input = virtual.GetInput(s[i+1:])
|
||||||
|
case "tts":
|
||||||
|
args.Input = virtual.GetInputTTS(s[i+1:])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
args.Input = inputTemplate("file", s, query)
|
args.Input = inputTemplate("file", s, query)
|
||||||
@@ -264,6 +326,12 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query["bitrate"] != nil {
|
||||||
|
// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
|
||||||
|
b := query["bitrate"][0]
|
||||||
|
args.AddCodec("-b:v " + b + " -maxrate " + b + " -bufsize " + b)
|
||||||
|
}
|
||||||
|
|
||||||
// 4. Process audio codecs
|
// 4. Process audio codecs
|
||||||
if args.Audio > 0 {
|
if args.Audio > 0 {
|
||||||
for _, audio := range query["audio"] {
|
for _, audio := range query["audio"] {
|
||||||
@@ -293,11 +361,27 @@ func parseArgs(s string) *ffmpeg.Args {
|
|||||||
args.AddCodec("-an")
|
args.AddCodec("-an")
|
||||||
}
|
}
|
||||||
|
|
||||||
// transcoding to only mjpeg
|
// change otput from RTSP to some other pipe format
|
||||||
if (args.Video == 1 && args.Audio == 0 && query.Get("video") == "mjpeg") ||
|
switch {
|
||||||
// no transcoding from mjpeg input
|
case args.Video == 0 && args.Audio == 0:
|
||||||
(args.Video == 0 && args.Audio == 0 && strings.Contains(args.Input, " mjpeg ")) {
|
// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)
|
||||||
args.Output = defaults["output/mjpeg"]
|
if strings.Contains(args.Input, " mjpeg ") {
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
}
|
||||||
|
case args.Video == 1 && args.Audio == 0:
|
||||||
|
switch core.Before(query.Get("video"), "/") {
|
||||||
|
case "mjpeg":
|
||||||
|
args.Output = defaults["output/mjpeg"]
|
||||||
|
case "raw":
|
||||||
|
args.Output = defaults["output/raw"]
|
||||||
|
}
|
||||||
|
case args.Video == 0 && args.Audio == 1:
|
||||||
|
switch core.Before(query.Get("audio"), "/") {
|
||||||
|
case "aac":
|
||||||
|
args.Output = defaults["output/aac"]
|
||||||
|
case "pcma", "pcmu", "pcml":
|
||||||
|
args.Output = defaults["output/wav"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|||||||
+302
-115
@@ -3,136 +3,241 @@ package ffmpeg
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseArgsFile(t *testing.T) {
|
func TestParseArgsFile(t *testing.T) {
|
||||||
// [FILE] all tracks will be copied without transcoding codecs
|
tests := []struct {
|
||||||
args := parseArgs("/media/bbb.mp4")
|
name string
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source string
|
||||||
|
expect string
|
||||||
// [FILE] video will be transcoded to H264, audio will be skipped
|
}{
|
||||||
args = parseArgs("/media/bbb.mp4#video=h264")
|
{
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[FILE] all tracks will be copied without transcoding codecs",
|
||||||
|
source: "/media/bbb.mp4",
|
||||||
// [FILE] video will be copied, audio will be transcoded to pcmu
|
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
args = parseArgs("/media/bbb.mp4#video=copy#audio=pcmu")
|
},
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
{
|
||||||
|
name: "[FILE] video will be transcoded to H264, audio will be skipped",
|
||||||
// [FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped
|
source: "/media/bbb.mp4#video=h264",
|
||||||
args = parseArgs("/media/bbb.mp4#video=h265#rotate=-90")
|
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
},
|
||||||
|
{
|
||||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
name: "[FILE] video will be copied, audio will be transcoded to pcmu",
|
||||||
args = parseArgs("/media/bbb.mp4#video=mjpeg")
|
source: "/media/bbb.mp4#video=copy#audio=pcmu",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`, args.String())
|
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
// https://github.com/AlexxIT/go2rtc/issues/509
|
{
|
||||||
args = parseArgs("ffmpeg:test.mp4#raw=-ss 00:00:20")
|
name: "[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source: "/media/bbb.mp4#video=h265#rotate=-90",
|
||||||
|
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "transpose=2" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||||
|
source: "/media/bbb.mp4#video=mjpeg",
|
||||||
|
expect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https://github.com/AlexxIT/go2rtc/issues/509",
|
||||||
|
source: "ffmpeg:test.mp4#raw=-ss 00:00:20",
|
||||||
|
expect: `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -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 TestParseArgsDevice(t *testing.T) {
|
func TestParseArgsDevice(t *testing.T) {
|
||||||
// [DEVICE] video will be output for MJPEG to pipe, with size 1920x1080
|
tests := []struct {
|
||||||
args := parseArgs("device?video=0&video_size=1920x1080")
|
name string
|
||||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source string
|
||||||
|
expect string
|
||||||
// [DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped
|
}{
|
||||||
args = parseArgs("device?video=0&video_size=1280x720&framerate=20#video=h265#audio=pcma")
|
{
|
||||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1280x720 -framerate 20 -i video="0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -c:a pcm_alaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080",
|
||||||
|
source: "device?video=0&video_size=1920x1080",
|
||||||
|
expect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i "video=0" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped",
|
||||||
|
source: "device?video=0&framerate=20#video=h265",
|
||||||
|
expect: `ffmpeg -hide_banner -f dshow -framerate 20 -i "video=0" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[DEVICE] video/audio",
|
||||||
|
source: "device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)",
|
||||||
|
expect: `ffmpeg -hide_banner -f dshow -i "video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)" -c copy -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 TestParseArgsIpCam(t *testing.T) {
|
func TestParseArgsIpCam(t *testing.T) {
|
||||||
// [HTTP] video will be copied
|
tests := []struct {
|
||||||
args := parseArgs("http://example.com")
|
name string
|
||||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source string
|
||||||
|
expect string
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
}{
|
||||||
args = parseArgs("http://example.com#video=h264")
|
{
|
||||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuvj420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[HTTP] video will be copied",
|
||||||
|
source: "http://example.com",
|
||||||
// [HLS] video will be copied, audio will be skipped
|
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
args = parseArgs("https://example.com#video=copy")
|
},
|
||||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
{
|
||||||
|
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||||
// [RTSP] video will be copied without transcoding codecs
|
source: "http://example.com#video=h264",
|
||||||
args = parseArgs("rtsp://example.com")
|
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
},
|
||||||
|
{
|
||||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
name: "[HLS] video will be copied, audio will be skipped",
|
||||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720")
|
source: "https://example.com#video=copy",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
// [RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
{
|
||||||
args = parseArgs("rtsp://example.com#input=rtsp/udp")
|
name: "[RTSP] video will be copied without transcoding codecs",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source: "rtsp://example.com",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
// [RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP
|
},
|
||||||
args = parseArgs("rtmp://example.com#input=rtsp/udp")
|
{
|
||||||
require.Equal(t, `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}`, args.String())
|
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
|
||||||
|
source: "rtsp://example.com#video=h265#width=1280#height=720",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
|
||||||
|
source: "rtsp://example.com#input=rtsp/udp",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP",
|
||||||
|
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) {
|
||||||
|
args := parseArgs(test.source)
|
||||||
|
require.Equal(t, test.expect, args.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsAudio(t *testing.T) {
|
func TestParseArgsAudio(t *testing.T) {
|
||||||
// [AUDIO] audio will be transcoded to AAC, video will be skipped
|
tests := []struct {
|
||||||
args := parseArgs("rtsp:///example.com#audio=aac")
|
name string
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source string
|
||||||
|
expect string
|
||||||
// [AUDIO] audio will be transcoded to AAC/16000, video will be skipped
|
}{
|
||||||
args = parseArgs("rtsp:///example.com#audio=aac/16000")
|
{
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[AUDIO] audio will be transcoded to AAC, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=aac",
|
||||||
// [AUDIO] audio will be transcoded to OPUS, video will be skipped
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,
|
||||||
args = parseArgs("rtsp:///example.com#audio=opus")
|
},
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a libopus -ar:a 48000 -ac:a 2 -application:a voip -af adelay=0|0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to AAC/16000, video will be skipped",
|
||||||
// [AUDIO] audio will be transcoded to PCMU, video will be skipped
|
source: "rtsp://example.com#audio=aac/16000",
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcmu")
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
},
|
||||||
|
{
|
||||||
// [AUDIO] audio will be transcoded to PCMU/16000, video will be skipped
|
name: "[AUDIO] audio will be transcoded to OPUS, video will be skipped",
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcmu/16000")
|
source: "rtsp://example.com#audio=opus",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
// [AUDIO] audio will be transcoded to PCMU/48000, video will be skipped
|
{
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcmu/48000")
|
name: "[AUDIO] audio will be transcoded to PCMU, video will be skipped",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source: "rtsp://example.com#audio=pcmu",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
|
||||||
// [AUDIO] audio will be transcoded to PCMA, video will be skipped
|
},
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcma")
|
{
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=pcmu/16000",
|
||||||
// [AUDIO] audio will be transcoded to PCMA/16000, video will be skipped
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcma/16000")
|
},
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped",
|
||||||
// [AUDIO] audio will be transcoded to PCMA/48000, video will be skipped
|
source: "rtsp://example.com#audio=pcmu/48000",
|
||||||
args = parseArgs("rtsp:///example.com#audio=pcma/48000")
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||||
require.Equal(t, `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp:///example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
},
|
||||||
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to PCMA, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=pcma",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=pcma/16000",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped",
|
||||||
|
source: "rtsp://example.com#audio=pcma/48000",
|
||||||
|
expect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
args := parseArgs(test.source)
|
||||||
|
require.Equal(t, test.expect, args.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsHwVaapi(t *testing.T) {
|
func TestParseArgsHwVaapi(t *testing.T) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
tests := []struct {
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=vaapi")
|
name string
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
source string
|
||||||
|
expect string
|
||||||
// [RTSP] video with rotation, should be transcoded, so select H264
|
}{
|
||||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
{
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
name: "[HTTP-MJPEG] video will be transcoded to H264",
|
||||||
|
source: "http:///example.com#video=h264#hardware=vaapi",
|
||||||
// [RTSP] video with resize to 1280x720, should be transcoded, so select H265
|
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
args = parseArgs("rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi")
|
},
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
{
|
||||||
|
name: "[RTSP] video with rotation, should be transcoded, so select H264",
|
||||||
// [FILE] video will be output for MJPEG to pipe, audio will be skipped
|
source: "rtsp://example.com#video=h264#rotate=180#hardware=vaapi",
|
||||||
args = parseArgs("/media/bbb.mp4#video=mjpeg#hardware=vaapi")
|
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
},
|
||||||
|
{
|
||||||
// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265
|
name: "[RTSP] video with resize to 1280x720, should be transcoded, so select H265",
|
||||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=vaapi")
|
source: "rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi",
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=1280:720" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[FILE] video will be output for MJPEG to pipe, audio will be skipped",
|
||||||
|
source: "/media/bbb.mp4#video=mjpeg#hardware=vaapi",
|
||||||
|
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf "format=vaapi|nv12,hwupload" -f mjpeg -`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "[DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265",
|
||||||
|
source: "device?video=0&video_size=1920x1080#video=h265#hardware=vaapi",
|
||||||
|
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i "video=0" -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload" -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 TestParseArgsHwV4l2m2m(t *testing.T) {
|
func _TestParseArgsHwV4l2m2m(t *testing.T) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
|
args := parseArgs("http:///example.com#video=h264#hardware=v4l2m2m")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
@@ -150,7 +255,37 @@ func TestParseArgsHwV4l2m2m(t *testing.T) {
|
|||||||
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsHwCuda(t *testing.T) {
|
func TestParseArgsHwRKMPP(t *testing.T) {
|
||||||
|
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) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
|
args := parseArgs("http:///example.com#video=h264#hardware=cuda")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
@@ -168,7 +303,7 @@ func TestParseArgsHwCuda(t *testing.T) {
|
|||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsHwDxva2(t *testing.T) {
|
func _TestParseArgsHwDxva2(t *testing.T) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
|
args := parseArgs("http:///example.com#video=h264#hardware=dxva2")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
@@ -190,7 +325,7 @@ func TestParseArgsHwDxva2(t *testing.T) {
|
|||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf "hwmap=derive_device=qsv,format=qsv" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseArgsHwVideotoolbox(t *testing.T) {
|
func _TestParseArgsHwVideotoolbox(t *testing.T) {
|
||||||
// [HTTP-MJPEG] video will be transcoded to H264
|
// [HTTP-MJPEG] video will be transcoded to H264
|
||||||
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
|
args := parseArgs("http:///example.com#video=h264#hardware=videotoolbox")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
@@ -207,3 +342,55 @@ func TestParseArgsHwVideotoolbox(t *testing.T) {
|
|||||||
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
|
args = parseArgs("device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox")
|
||||||
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video="0" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeckLink(t *testing.T) {
|
||||||
|
args := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i "{input}"`)
|
||||||
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i "DeckLink SDI (2)" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDrawText(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: "http:///example.com#video=h264#drawtext=fontsize=12",
|
||||||
|
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12",
|
||||||
|
expect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}'" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi",
|
||||||
|
expect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf "scale=640:-1,drawtext=fontsize=12:text='%{localtime\:%Y-%m-%d %X}',hwupload" -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 TestVersion(t *testing.T) {
|
||||||
|
verAV = ffmpeg.Version61
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
source string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
source: "/media/bbb.mp4",
|
||||||
|
expect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -17,11 +16,12 @@ const (
|
|||||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
||||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
EngineDXVA2 = "dxva2" // Intel on Windows
|
||||||
EngineVideoToolbox = "videotoolbox" // macOS
|
EngineVideoToolbox = "videotoolbox" // macOS
|
||||||
|
EngineRKMPP = "rkmpp" // Rockchip
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init(bin string) {
|
func Init(bin string) {
|
||||||
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
|
api.HandleFunc("api/ffmpeg/hardware", func(w http.ResponseWriter, r *http.Request) {
|
||||||
api.ResponseStreams(w, ProbeAll(bin))
|
api.ResponseSources(w, ProbeAll(bin))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +58,11 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
args.Codecs[i] = defaults[name+"/"+engine]
|
args.Codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
if !args.HasFilters("drawtext=") {
|
if !args.HasFilters("drawtext=") {
|
||||||
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.Input
|
args.Input = "-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch " + args.Input
|
||||||
|
|
||||||
|
if name == "h264" {
|
||||||
|
fixPixelFormat(args)
|
||||||
|
}
|
||||||
|
|
||||||
for i, filter := range args.Filters {
|
for i, filter := range args.Filters {
|
||||||
if strings.HasPrefix(filter, "scale=") {
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
@@ -78,7 +82,7 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
args.InsertFilter("format=vaapi|nv12,hwupload")
|
||||||
} else {
|
} else {
|
||||||
// enable software pixel for drawtext, scale and transpose
|
// enable software pixel for drawtext, scale and transpose
|
||||||
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 " + args.Input
|
args.Input = "-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch " + args.Input
|
||||||
|
|
||||||
args.AddFilter("hwupload")
|
args.AddFilter("hwupload")
|
||||||
}
|
}
|
||||||
@@ -120,6 +124,37 @@ func MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string)
|
|||||||
|
|
||||||
case EngineV4L2M2M:
|
case EngineV4L2M2M:
|
||||||
args.Codecs[i] = defaults[name+"/"+engine]
|
args.Codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
case EngineRKMPP:
|
||||||
|
args.Codecs[i] = defaults[name+"/"+engine]
|
||||||
|
|
||||||
|
if !args.HasFilters("drawtext=") {
|
||||||
|
args.Input = "-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga " + args.Input
|
||||||
|
|
||||||
|
for i, filter := range args.Filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.Filters[i] = "scale_rkrga=" + filter[6:] + ":force_original_aspect_ratio=0"
|
||||||
|
}
|
||||||
|
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:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +163,6 @@ var cache = map[string]string{}
|
|||||||
|
|
||||||
func run(bin string, args string) bool {
|
func run(bin string, args string) bool {
|
||||||
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
err := exec.Command(bin, strings.Split(args, " ")...).Run()
|
||||||
log.Printf("%v %v", args, err)
|
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,3 +187,24 @@ func cut(s string, sep byte, pos int) string {
|
|||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fixPixelFormat:
|
||||||
|
// - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)
|
||||||
|
// - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)
|
||||||
|
// - bad jpeg pixel: yuvj422p(pc, bt470bg)
|
||||||
|
func fixPixelFormat(args *ffmpeg.Args) {
|
||||||
|
// in my tests this filters has same CPU/GPU load:
|
||||||
|
// - "hwupload"
|
||||||
|
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv"
|
||||||
|
// - "hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12"
|
||||||
|
const fixPixFmt = "out_color_matrix=bt709:out_range=tv:format=nv12"
|
||||||
|
|
||||||
|
for i, filter := range args.Filters {
|
||||||
|
if strings.HasPrefix(filter, "scale=") {
|
||||||
|
args.Filters[i] = filter + ":" + fixPixFmt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args.Filters = append(args.Filters, "scale="+fixPixFmt)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//go:build freebsd || netbsd || openbsd || dragonfly
|
||||||
|
|
||||||
|
package hardware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 -"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProbeAll(bin string) []*api.Source {
|
||||||
|
return []*api.Source{
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeV4L2M2MH264),
|
||||||
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeV4L2M2MH265),
|
||||||
|
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeRKMPPH264),
|
||||||
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeRKMPPH265),
|
||||||
|
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProbeHardware(bin, name string) string {
|
||||||
|
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||||
|
switch name {
|
||||||
|
case "h264":
|
||||||
|
if run(bin, ProbeV4L2M2MH264) {
|
||||||
|
return EngineV4L2M2M
|
||||||
|
}
|
||||||
|
if run(bin, ProbeRKMPPH264) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
|
case "h265":
|
||||||
|
if run(bin, ProbeV4L2M2MH265) {
|
||||||
|
return EngineV4L2M2M
|
||||||
|
}
|
||||||
|
if run(bin, ProbeRKMPPH265) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return EngineSoftware
|
||||||
|
}
|
||||||
|
|
||||||
|
return EngineSoftware
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build darwin || ios
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -7,8 +9,8 @@ import (
|
|||||||
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
|
const ProbeVideoToolboxH264 = "-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -"
|
||||||
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
|
const ProbeVideoToolboxH265 = "-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -"
|
||||||
|
|
||||||
func ProbeAll(bin string) []api.Stream {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeVideoToolboxH264),
|
Name: runToString(bin, ProbeVideoToolboxH264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineVideoToolbox,
|
||||||
|
|||||||
+41
-11
@@ -1,21 +1,29 @@
|
|||||||
|
//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
const (
|
||||||
const ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
ProbeV4L2M2MH264 = "-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -"
|
||||||
const ProbeVAAPIH264 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -"
|
ProbeV4L2M2MH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -"
|
||||||
const ProbeVAAPIH265 = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -"
|
ProbeRKMPPH264 = "-f lavfi -i testsrc2 -t 1 -c h264_rkmpp -f null -"
|
||||||
const ProbeVAAPIJPEG = "-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -"
|
ProbeRKMPPH265 = "-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp -f null -"
|
||||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
ProbeRKMPPJPEG = "-f lavfi -i testsrc2 -t 1 -c mjpeg_rkmpp -f null -"
|
||||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -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 -"
|
||||||
|
ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||||
|
ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||||
|
)
|
||||||
|
|
||||||
func ProbeAll(bin string) []api.Stream {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeV4L2M2MH264),
|
Name: runToString(bin, ProbeV4L2M2MH264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineV4L2M2M,
|
||||||
@@ -24,10 +32,22 @@ func ProbeAll(bin string) []api.Stream {
|
|||||||
Name: runToString(bin, ProbeV4L2M2MH265),
|
Name: runToString(bin, ProbeV4L2M2MH265),
|
||||||
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
URL: "ffmpeg:...#video=h265#hardware=" + EngineV4L2M2M,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeRKMPPH264),
|
||||||
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineRKMPP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeRKMPPH265),
|
||||||
|
URL: "ffmpeg:...#video=h265#hardware=" + EngineRKMPP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: runToString(bin, ProbeRKMPPJPEG),
|
||||||
|
URL: "ffmpeg:...#video=mjpeg#hardware=" + EngineRKMPP,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeVAAPIH264),
|
Name: runToString(bin, ProbeVAAPIH264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineVAAPI,
|
||||||
@@ -58,10 +78,20 @@ func ProbeHardware(bin, name string) string {
|
|||||||
if run(bin, ProbeV4L2M2MH264) {
|
if run(bin, ProbeV4L2M2MH264) {
|
||||||
return EngineV4L2M2M
|
return EngineV4L2M2M
|
||||||
}
|
}
|
||||||
|
if run(bin, ProbeRKMPPH264) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
case "h265":
|
case "h265":
|
||||||
if run(bin, ProbeV4L2M2MH265) {
|
if run(bin, ProbeV4L2M2MH265) {
|
||||||
return EngineV4L2M2M
|
return EngineV4L2M2M
|
||||||
}
|
}
|
||||||
|
if run(bin, ProbeRKMPPH265) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
|
case "mjpeg":
|
||||||
|
if run(bin, ProbeRKMPPJPEG) {
|
||||||
|
return EngineRKMPP
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return EngineSoftware
|
return EngineSoftware
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package hardware
|
package hardware
|
||||||
|
|
||||||
import "github.com/AlexxIT/go2rtc/internal/api"
|
import "github.com/AlexxIT/go2rtc/internal/api"
|
||||||
@@ -8,8 +10,8 @@ const ProbeDXVA2JPEG = "-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg
|
|||||||
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
const ProbeCUDAH264 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -"
|
||||||
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
const ProbeCUDAH265 = "-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -"
|
||||||
|
|
||||||
func ProbeAll(bin string) []api.Stream {
|
func ProbeAll(bin string) []*api.Source {
|
||||||
return []api.Stream{
|
return []*api.Source{
|
||||||
{
|
{
|
||||||
Name: runToString(bin, ProbeDXVA2H264),
|
Name: runToString(bin, ProbeDXVA2H264),
|
||||||
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
|
URL: "ffmpeg:...#video=h264#hardware=" + EngineDXVA2,
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TranscodeToJPEG(b []byte) ([]byte, error) {
|
|
||||||
cmd := exec.Command(defaults["bin"], "-hide_banner", "-i", "-", "-f", "mjpeg", "-")
|
|
||||||
cmd.Stdin = bytes.NewBuffer(b)
|
|
||||||
return cmd.Output()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {
|
||||||
|
args := parseQuery(query)
|
||||||
|
return transcode(b, args.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func JPEGWithScale(b []byte, width, height int) ([]byte, error) {
|
||||||
|
args := defaultArgs()
|
||||||
|
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
|
||||||
|
return transcode(b, args.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func transcode(b []byte, args string) ([]byte, error) {
|
||||||
|
cmdArgs := shell.QuoteSplit(args)
|
||||||
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||||
|
cmd.Stdin = bytes.NewBuffer(b)
|
||||||
|
return cmd.Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultArgs() *ffmpeg.Args {
|
||||||
|
return &ffmpeg.Args{
|
||||||
|
Bin: defaults["bin"],
|
||||||
|
Global: defaults["global"],
|
||||||
|
Input: "-i -",
|
||||||
|
Codecs: []string{defaults["mjpeg"]},
|
||||||
|
Output: defaults["output/mjpeg"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseQuery(query url.Values) *ffmpeg.Args {
|
||||||
|
args := defaultArgs()
|
||||||
|
|
||||||
|
var width = -1
|
||||||
|
var height = -1
|
||||||
|
var r, hw string
|
||||||
|
|
||||||
|
for k, v := range query {
|
||||||
|
switch k {
|
||||||
|
case "width", "w":
|
||||||
|
width = core.Atoi(v[0])
|
||||||
|
case "height", "h":
|
||||||
|
height = core.Atoi(v[0])
|
||||||
|
case "rotate":
|
||||||
|
r = v[0]
|
||||||
|
case "hardware", "hw":
|
||||||
|
hw = v[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if width > 0 || height > 0 {
|
||||||
|
args.AddFilter(fmt.Sprintf("scale=%d:%d", width, height))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r != "" {
|
||||||
|
switch r {
|
||||||
|
case "90":
|
||||||
|
args.AddFilter("transpose=1") // 90 degrees clockwise
|
||||||
|
case "180":
|
||||||
|
args.AddFilter("transpose=1,transpose=1")
|
||||||
|
case "-90", "270":
|
||||||
|
args.AddFilter("transpose=2") // 90 degrees counterclockwise
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hw != "" {
|
||||||
|
hardware.MakeHardware(args, hw, defaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseQuery(t *testing.T) {
|
||||||
|
args := parseQuery(nil)
|
||||||
|
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())
|
||||||
|
|
||||||
|
query, err := url.ParseQuery("h=480")
|
||||||
|
require.Nil(t, err)
|
||||||
|
args = parseQuery(query)
|
||||||
|
require.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf "scale=-1:480" -f mjpeg -`, args.String())
|
||||||
|
|
||||||
|
query, err = url.ParseQuery("hw=vaapi")
|
||||||
|
require.Nil(t, err)
|
||||||
|
args = parseQuery(query)
|
||||||
|
require.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf "format=vaapi|nv12,hwupload" -f mjpeg -`, args.String())
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/aac"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Producer struct {
|
||||||
|
core.Connection
|
||||||
|
url string
|
||||||
|
query url.Values
|
||||||
|
ffmpeg core.Producer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities
|
||||||
|
func NewProducer(url string) (core.Producer, error) {
|
||||||
|
p := &Producer{}
|
||||||
|
|
||||||
|
i := strings.IndexByte(url, '#')
|
||||||
|
p.url, p.query = url[:i], streams.ParseQuery(url[i+1:])
|
||||||
|
|
||||||
|
// ffmpeg.NewProducer support only one audio
|
||||||
|
if len(p.query["video"]) != 0 || len(p.query["audio"]) != 1 {
|
||||||
|
return nil, errors.New("ffmpeg: unsupported params: " + url[i:])
|
||||||
|
}
|
||||||
|
|
||||||
|
p.ID = core.NewID()
|
||||||
|
p.FormatName = "ffmpeg"
|
||||||
|
p.Medias = []*core.Media{
|
||||||
|
{
|
||||||
|
// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG
|
||||||
|
Kind: core.KindAudio,
|
||||||
|
Direction: core.DirectionRecvonly,
|
||||||
|
// codecs in order from best to worst
|
||||||
|
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},
|
||||||
|
// AAC has unknown problems on Dahua two way
|
||||||
|
{Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + "1408"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) Start() error {
|
||||||
|
var err error
|
||||||
|
if p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, media := range p.ffmpeg.GetMedias() {
|
||||||
|
track, err := p.ffmpeg.GetTrack(media, media.Codecs[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Receivers[i].Replace(track)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ffmpeg.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) Stop() error {
|
||||||
|
if p.ffmpeg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.ffmpeg.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) MarshalJSON() ([]byte, error) {
|
||||||
|
if p.ffmpeg == nil {
|
||||||
|
return json.Marshal(p.Connection)
|
||||||
|
}
|
||||||
|
return json.Marshal(p.ffmpeg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Producer) newURL() string {
|
||||||
|
s := p.url
|
||||||
|
// rewrite codecs in url from auto to known presets from defaults
|
||||||
|
for _, receiver := range p.Receivers {
|
||||||
|
codec := receiver.Codec
|
||||||
|
switch codec.Name {
|
||||||
|
case core.CodecOpus:
|
||||||
|
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:
|
||||||
|
s += "#audio=pcma/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
case core.CodecPCMU:
|
||||||
|
s += "#audio=pcmu/" + strconv.Itoa(int(codec.ClockRate))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add other params
|
||||||
|
for key, values := range p.query {
|
||||||
|
if key != "audio" {
|
||||||
|
for _, value := range values {
|
||||||
|
s += "#" + key + "=" + value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package ffmpeg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/ffmpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var verMu sync.Mutex
|
||||||
|
var verErr error
|
||||||
|
var verFF string
|
||||||
|
var verAV string
|
||||||
|
|
||||||
|
func Version() (string, error) {
|
||||||
|
verMu.Lock()
|
||||||
|
defer verMu.Unlock()
|
||||||
|
|
||||||
|
if verFF != "" {
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(defaults["bin"], "-version")
|
||||||
|
b, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
verFF = "-"
|
||||||
|
verErr = err
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
|
|
||||||
|
verFF, verAV = ffmpeg.ParseVersion(b)
|
||||||
|
|
||||||
|
if verFF == "" {
|
||||||
|
verFF = "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
// better to compare libavformat, because nightly/master builds
|
||||||
|
if verAV != "" && verAV < ffmpeg.Version50 {
|
||||||
|
verErr = errors.New("ffmpeg: unsupported version: " + verFF)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("version", verFF).Str("libavformat", verAV).Msgf("[ffmpeg] bin")
|
||||||
|
|
||||||
|
return verFF, verErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package virtual
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetInput(src string) string {
|
||||||
|
query, err := url.ParseQuery(src)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
input := "-re"
|
||||||
|
|
||||||
|
for _, video := range query["video"] {
|
||||||
|
// https://ffmpeg.org/ffmpeg-filters.html
|
||||||
|
sep := "=" // first separator
|
||||||
|
|
||||||
|
if video == "" {
|
||||||
|
video = "testsrc=decimals=2" // default video
|
||||||
|
sep = ":"
|
||||||
|
}
|
||||||
|
|
||||||
|
input += " -f lavfi -i " + video
|
||||||
|
|
||||||
|
// set defaults (using Add instead of Set)
|
||||||
|
query.Add("size", "1920x1080")
|
||||||
|
|
||||||
|
for key, values := range query {
|
||||||
|
value := values[0]
|
||||||
|
|
||||||
|
// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax
|
||||||
|
switch key {
|
||||||
|
case "color", "rate", "duration", "sar", "decimals":
|
||||||
|
case "size":
|
||||||
|
switch value {
|
||||||
|
case "720":
|
||||||
|
value = "1280x720" // crf=1 -> 12 Mbps
|
||||||
|
case "1080":
|
||||||
|
value = "1920x1080" // crf=1 -> 25 Mbps
|
||||||
|
case "2K":
|
||||||
|
value = "2560x1440" // crf=1 -> 43 Mbps
|
||||||
|
case "4K":
|
||||||
|
value = "3840x2160" // crf=1 -> 103 Mbps
|
||||||
|
case "8K":
|
||||||
|
value = "7680x4230" // https://reolink.com/blog/8k-resolution/
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
input += sep + key + "=" + value
|
||||||
|
sep = ":" // next separator
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := query.Get("format"); s != "" {
|
||||||
|
input += ",format=" + s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInputTTS(src string) string {
|
||||||
|
query, err := url.ParseQuery(src)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
input := `-re -f lavfi -i "flite=text='` + query.Get("text") + `'`
|
||||||
|
|
||||||
|
// ffmpeg -f lavfi -i flite=list_voices=1
|
||||||
|
// awb, kal, kal16, rms, slt
|
||||||
|
if voice := query.Get("voice"); voice != "" {
|
||||||
|
input += ":voice" + voice
|
||||||
|
}
|
||||||
|
|
||||||
|
return input + `"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package virtual
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetInput(t *testing.T) {
|
||||||
|
s := GetInput("video")
|
||||||
|
require.Equal(t, "-re -f lavfi -i testsrc=decimals=2:size=1920x1080", s)
|
||||||
|
|
||||||
|
s = GetInput("video=testsrc2&size=4K")
|
||||||
|
require.Equal(t, "-re -f lavfi -i testsrc2=size=3840x2160", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetInputTTS(t *testing.T) {
|
||||||
|
s := GetInputTTS("text=hello world&voice=slt")
|
||||||
|
require.Equal(t, `-re -f lavfi -i "flite=text='hello world':voiceslt"`, s)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 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/)
|
||||||
|
|
||||||
|
Other camera models have different APIs. I will try to add them in future versions.
|
||||||
|
|
||||||
|
## 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 the firewall
|
||||||
|
|
||||||
|
1. Discover camera address: WebUI > Add > GoPro
|
||||||
|
2. Add camera to config
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
hero12: gopro://172.20.100.51
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful links
|
||||||
|
|
||||||
|
- https://gopro.github.io/OpenGoPro/
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package gopro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/gopro"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("gopro", func(source string) (core.Producer, error) {
|
||||||
|
return gopro.Dial(source)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.HandleFunc("api/gopro", apiGoPro)
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiGoPro(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var items []*api.Source
|
||||||
|
|
||||||
|
for _, host := range gopro.Discovery() {
|
||||||
|
items = append(items, &api.Source{Name: host, URL: "gopro://" + host})
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponseSources(w, items)
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
+16
-17
@@ -3,12 +3,13 @@ package hass
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/webrtc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func apiOK(w http.ResponseWriter, r *http.Request) {
|
func apiOK(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -21,6 +22,7 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
case strings.HasSuffix(r.RequestURI, "/add"):
|
case strings.HasSuffix(r.RequestURI, "/add"):
|
||||||
var v addJSON
|
var v addJSON
|
||||||
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,45 +30,42 @@ func apiStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
||||||
// 2. static link to Hass camera
|
// 2. static link to Hass camera
|
||||||
// 3. dynamic link to Hass camera
|
// 3. dynamic link to Hass camera
|
||||||
stream := streams.Get(v.Name)
|
if _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {
|
||||||
if stream == nil {
|
apiOK(w, r)
|
||||||
stream = streams.NewTemplate(v.Name, v.Channels.First.Url)
|
} else {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.SetSource(v.Channels.First.Url)
|
|
||||||
|
|
||||||
apiOK(w, r)
|
|
||||||
|
|
||||||
// /stream/{id}/channel/0/webrtc
|
// /stream/{id}/channel/0/webrtc
|
||||||
default:
|
default:
|
||||||
i := strings.IndexByte(r.RequestURI[8:], '/')
|
i := strings.IndexByte(r.RequestURI[8:], '/')
|
||||||
if i <= 0 {
|
if i <= 0 {
|
||||||
log.Warn().Msgf("wrong request: %s", r.RequestURI)
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
name := r.RequestURI[8 : 8+i]
|
|
||||||
|
|
||||||
|
name := r.RequestURI[8 : 8+i]
|
||||||
stream := streams.Get(name)
|
stream := streams.Get(name)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
if err := r.ParseForm(); err != nil {
|
||||||
log.Error().Err(err).Msg("[api.hass] parse form")
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s := r.FormValue("data")
|
s := r.FormValue("data")
|
||||||
offer, err := base64.StdEncoding.DecodeString(s)
|
offer, err := base64.StdEncoding.DecodeString(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err = webrtc.ExchangeSDP(stream, string(offer), "WebRTC/Hass sync", r.UserAgent())
|
s, err = webrtc.ExchangeSDP(stream, string(offer), "hass/webrtc", r.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
-24
@@ -4,6 +4,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/roborock"
|
"github.com/AlexxIT/go2rtc/internal/roborock"
|
||||||
@@ -11,16 +17,12 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hass"
|
"github.com/AlexxIT/go2rtc/pkg/hass"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
var conf struct {
|
var conf struct {
|
||||||
API struct {
|
API struct {
|
||||||
Listen string `json:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
} `yaml:"api"`
|
} `yaml:"api"`
|
||||||
Mod struct {
|
Mod struct {
|
||||||
Config string `yaml:"config"`
|
Config string `yaml:"config"`
|
||||||
@@ -36,9 +38,27 @@ func Init() {
|
|||||||
api.HandleFunc("/streams", apiOK)
|
api.HandleFunc("/streams", apiOK)
|
||||||
api.HandleFunc("/stream/", apiStream)
|
api.HandleFunc("/stream/", apiStream)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("hass", func(source string) (core.Producer, error) {
|
||||||
|
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
||||||
|
return hass.NewClient(source)
|
||||||
|
})
|
||||||
|
|
||||||
// load static entries from Hass config
|
// load static entries from Hass config
|
||||||
if err := importConfig(conf.Mod.Config); err != nil {
|
if err := importConfig(conf.Mod.Config); err != nil {
|
||||||
log.Debug().Msgf("[hass] can't import config: %s", err)
|
log.Trace().Msgf("[hass] can't import config: %s", err)
|
||||||
|
|
||||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
http.Error(w, "no hass config", http.StatusNotFound)
|
http.Error(w, "no hass config", http.StatusNotFound)
|
||||||
@@ -56,26 +76,13 @@ func Init() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
var items []api.Stream
|
var items []*api.Source
|
||||||
for name, url := range entities {
|
for name, url := range entities {
|
||||||
items = append(items, api.Stream{Name: name, URL: url})
|
items = append(items, &api.Source{
|
||||||
|
Name: name, URL: "hass:" + name, Location: url,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
api.ResponseStreams(w, items)
|
api.ResponseSources(w, items)
|
||||||
})
|
|
||||||
|
|
||||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
|
||||||
// check entity by name
|
|
||||||
if url2 := entities[url[5:]]; url2 != "" {
|
|
||||||
return streams.GetProducer(url2)
|
|
||||||
}
|
|
||||||
|
|
||||||
// support hass://supervisor?entity_id=camera.driveway_doorbell
|
|
||||||
client, err := hass.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// for Addon listen on hassio interface, so WebUI feature will work
|
// for Addon listen on hassio interface, so WebUI feature will work
|
||||||
|
|||||||
@@ -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
|
## Useful links
|
||||||
|
|
||||||
- https://walterebert.com/playground/video/hls/
|
- https://walterebert.com/playground/video/hls/
|
||||||
|
|||||||
+23
-87
@@ -1,6 +1,10 @@
|
|||||||
package hls
|
package hls
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
@@ -8,12 +12,7 @@ import (
|
|||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
@@ -32,21 +31,12 @@ func Init() {
|
|||||||
ws.HandleFunc("hls", handlerWSHLS)
|
ws.HandleFunc("hls", handlerWSHLS)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Consumer interface {
|
|
||||||
core.Consumer
|
|
||||||
Listen(f core.EventFunc)
|
|
||||||
Init() ([]byte, error)
|
|
||||||
MimeCodecs() string
|
|
||||||
Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
const keepalive = 5 * time.Second
|
const keepalive = 5 * time.Second
|
||||||
|
|
||||||
var sessions = map[string]*Session{}
|
|
||||||
|
|
||||||
// once I saw 404 on MP4 segment, so better to use mutex
|
// once I saw 404 on MP4 segment, so better to use mutex
|
||||||
|
var sessions = map[string]*Session{}
|
||||||
var sessionsMu sync.RWMutex
|
var sessionsMu sync.RWMutex
|
||||||
|
|
||||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -66,22 +56,20 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var cons Consumer
|
var cons core.Consumer
|
||||||
|
|
||||||
// use fMP4 with codecs filter and TS without
|
// use fMP4 with codecs filter and TS without
|
||||||
medias := mp4.ParseQuery(r.URL.Query())
|
medias := mp4.ParseQuery(r.URL.Query())
|
||||||
if medias != nil {
|
if medias != nil {
|
||||||
cons = &mp4.Consumer{
|
c := mp4.NewConsumer(medias)
|
||||||
Desc: "HLS/HTTP",
|
c.FormatName = "hls/fmp4"
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
c.WithRequest(r)
|
||||||
UserAgent: r.UserAgent(),
|
cons = c
|
||||||
Medias: medias,
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
cons = &mpegts.Consumer{
|
c := mpegts.NewConsumer()
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
c.FormatName = "hls/mpegts"
|
||||||
UserAgent: r.UserAgent(),
|
c.WithRequest(r)
|
||||||
}
|
cons = c
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
@@ -89,64 +77,22 @@ func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session := &Session{cons: cons}
|
session := NewSession(cons)
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
session.mu.Lock()
|
|
||||||
session.buffer = append(session.buffer, data...)
|
|
||||||
session.mu.Unlock()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
sid := core.RandString(8, 62)
|
|
||||||
|
|
||||||
session.alive = time.AfterFunc(keepalive, func() {
|
session.alive = time.AfterFunc(keepalive, func() {
|
||||||
sessionsMu.Lock()
|
sessionsMu.Lock()
|
||||||
delete(sessions, sid)
|
delete(sessions, session.id)
|
||||||
sessionsMu.Unlock()
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
session.init, _ = cons.Init()
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
// two segments important for Chromecast
|
|
||||||
if medias != nil {
|
|
||||||
session.template = `#EXTM3U
|
|
||||||
#EXT-X-VERSION:6
|
|
||||||
#EXT-X-TARGETDURATION:1
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:%d
|
|
||||||
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d`
|
|
||||||
} else {
|
|
||||||
session.template = `#EXTM3U
|
|
||||||
#EXT-X-VERSION:3
|
|
||||||
#EXT-X-TARGETDURATION:1
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.ts?id=` + sid + `&n=%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.ts?id=` + sid + `&n=%d`
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionsMu.Lock()
|
sessionsMu.Lock()
|
||||||
sessions[sid] = session
|
sessions[session.id] = session
|
||||||
sessionsMu.Unlock()
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
|
go session.Run()
|
||||||
codecs := strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
|
|
||||||
|
|
||||||
// bandwidth important for Safari, codecs useful for smooth playback
|
if _, err := w.Write(session.Main()); err != nil {
|
||||||
data := []byte(`#EXTM3U
|
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
|
|
||||||
hls/playlist.m3u8?id=` + sid)
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +115,7 @@ func handlerPlaylist(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := w.Write([]byte(session.Playlist())); err != nil {
|
if _, err := w.Write(session.Playlist()); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -224,11 +170,8 @@ func handlerInit(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := session.init
|
data := session.Init()
|
||||||
session.init = nil
|
if data == nil {
|
||||||
|
|
||||||
session.segment0 = session.Segment()
|
|
||||||
if session.segment0 == nil {
|
|
||||||
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
|
log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
@@ -261,14 +204,7 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
session.alive.Reset(keepalive)
|
session.alive.Reset(keepalive)
|
||||||
|
|
||||||
var data []byte
|
data := session.Segment()
|
||||||
|
|
||||||
if query.Get("n") != "0" {
|
|
||||||
data = session.Segment()
|
|
||||||
} else {
|
|
||||||
data = session.segment0
|
|
||||||
}
|
|
||||||
|
|
||||||
if data == nil {
|
if data == nil {
|
||||||
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
|
log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery)
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|||||||
+92
-6
@@ -2,23 +2,105 @@ package hls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
cons Consumer
|
cons core.Consumer
|
||||||
|
id string
|
||||||
template string
|
template string
|
||||||
init []byte
|
init []byte
|
||||||
segment0 []byte
|
|
||||||
buffer []byte
|
buffer []byte
|
||||||
seq int
|
seq int
|
||||||
alive *time.Timer
|
alive *time.Timer
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Playlist() string {
|
func NewSession(cons core.Consumer) *Session {
|
||||||
return fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1)
|
s := &Session{
|
||||||
|
id: core.RandString(8, 62),
|
||||||
|
cons: cons,
|
||||||
|
}
|
||||||
|
|
||||||
|
// two segments important for Chromecast
|
||||||
|
if _, ok := cons.(*mp4.Consumer); ok {
|
||||||
|
s.template = `#EXTM3U
|
||||||
|
#EXT-X-VERSION:6
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:%d
|
||||||
|
#EXT-X-MAP:URI="init.mp4?id=` + s.id + `"
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.m4s?id=` + s.id + `&n=%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.m4s?id=` + s.id + `&n=%d`
|
||||||
|
} else {
|
||||||
|
s.template = `#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-TARGETDURATION:1
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.ts?id=` + s.id + `&n=%d
|
||||||
|
#EXTINF:0.500,
|
||||||
|
segment.ts?id=` + s.id + `&n=%d`
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Write(p []byte) (n int, err error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.init == nil {
|
||||||
|
s.init = p
|
||||||
|
} else {
|
||||||
|
s.buffer = append(s.buffer, p...)
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Run() {
|
||||||
|
_, _ = s.cons.(io.WriterTo).WriteTo(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Main() []byte {
|
||||||
|
type withCodecs interface {
|
||||||
|
Codecs() []*core.Codec
|
||||||
|
}
|
||||||
|
|
||||||
|
codecs := mp4.MimeCodecs(s.cons.(withCodecs).Codecs())
|
||||||
|
codecs = strings.Replace(codecs, mp4.MimeFlac, "fLaC", 1)
|
||||||
|
|
||||||
|
// bandwidth important for Safari, codecs useful for smooth playback
|
||||||
|
return []byte(`#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS="` + codecs + `"
|
||||||
|
hls/playlist.m3u8?id=` + s.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Playlist() []byte {
|
||||||
|
return []byte(fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Init() (init []byte) {
|
||||||
|
for i := 0; i < 60 && init == nil; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
// return init only when have some buffer
|
||||||
|
if len(s.buffer) > 0 {
|
||||||
|
init = s.init
|
||||||
|
}
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) Segment() (segment []byte) {
|
func (s *Session) Segment() (segment []byte) {
|
||||||
@@ -30,8 +112,12 @@ func (s *Session) Segment() (segment []byte) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if len(s.buffer) > 0 {
|
if len(s.buffer) > 0 {
|
||||||
segment = s.buffer
|
segment = s.buffer
|
||||||
// for TS important to start new segment with init
|
if _, ok := s.cons.(*mp4.Consumer); ok {
|
||||||
s.buffer = s.init
|
s.buffer = nil
|
||||||
|
} else {
|
||||||
|
// for TS important to start new segment with init
|
||||||
|
s.buffer = s.init
|
||||||
|
}
|
||||||
s.seq++
|
s.seq++
|
||||||
}
|
}
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|||||||
+16
-47
@@ -2,82 +2,51 @@ package hls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
"github.com/AlexxIT/go2rtc/internal/api/ws"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
func handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {
|
||||||
src := tr.Request.URL.Query().Get("src")
|
stream, _ := streams.GetOrPatch(tr.Request.URL.Query())
|
||||||
stream := streams.Get(src)
|
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
return errors.New(api.StreamNotFound)
|
return errors.New(api.StreamNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
codecs := msg.String()
|
codecs := msg.String()
|
||||||
|
medias := mp4.ParseCodecs(codecs, true)
|
||||||
|
cons := mp4.NewConsumer(medias)
|
||||||
|
cons.FormatName = "hls/fmp4"
|
||||||
|
cons.WithRequest(tr.Request)
|
||||||
|
|
||||||
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
log.Trace().Msgf("[hls] new ws consumer codecs=%s", codecs)
|
||||||
|
|
||||||
cons := &mp4.Consumer{
|
|
||||||
Desc: "HLS/WebSocket",
|
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
Medias: mp4.ParseCodecs(codecs, true),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
if err := stream.AddConsumer(cons); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
log.Error().Err(err).Caller().Send()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
session := &Session{cons: cons}
|
session := NewSession(cons)
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
session.mu.Lock()
|
|
||||||
session.buffer = append(session.buffer, data...)
|
|
||||||
session.mu.Unlock()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
session.alive = time.AfterFunc(keepalive, func() {
|
session.alive = time.AfterFunc(keepalive, func() {
|
||||||
|
sessionsMu.Lock()
|
||||||
|
delete(sessions, session.id)
|
||||||
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
})
|
})
|
||||||
session.init, _ = cons.Init()
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
sid := core.RandString(8, 62)
|
|
||||||
|
|
||||||
// two segments important for Chromecast
|
|
||||||
session.template = `#EXTM3U
|
|
||||||
#EXT-X-VERSION:6
|
|
||||||
#EXT-X-TARGETDURATION:1
|
|
||||||
#EXT-X-MEDIA-SEQUENCE:%d
|
|
||||||
#EXT-X-MAP:URI="init.mp4?id=` + sid + `"
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d
|
|
||||||
#EXTINF:0.500,
|
|
||||||
segment.m4s?id=` + sid + `&n=%d`
|
|
||||||
|
|
||||||
sessionsMu.Lock()
|
sessionsMu.Lock()
|
||||||
sessions[sid] = session
|
sessions[session.id] = session
|
||||||
sessionsMu.Unlock()
|
sessionsMu.Unlock()
|
||||||
|
|
||||||
// Apple Safari can play FLAC codec, but fail it it in m3u8 playlist
|
go session.Run()
|
||||||
codecs = strings.Replace(cons.MimeCodecs(), mp4.MimeFlac, mp4.MimeAAC, 1)
|
|
||||||
|
|
||||||
// bandwidth important for Safari, codecs useful for smooth playback
|
main := session.Main()
|
||||||
data := `#EXTM3U
|
tr.Write(&ws.Message{Type: "hls", Value: string(main)})
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS="` + codecs + `"
|
|
||||||
hls/playlist.m3u8?id=` + sid
|
|
||||||
|
|
||||||
tr.Write(&ws.Message{Type: "hls", Value: data})
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
+150
-105
@@ -1,136 +1,181 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"errors"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app/store"
|
"fmt"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"io"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
"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 {
|
switch r.Method {
|
||||||
case "GET":
|
case "GET":
|
||||||
items := make([]any, 0)
|
if id := r.Form.Get("id"); id != "" {
|
||||||
|
if srv := servers[id]; srv != nil {
|
||||||
for name, src := range store.GetDict("streams") {
|
api.ResponsePrettyJSON(w, srv)
|
||||||
if src := src.(string); strings.HasPrefix(src, "homekit") {
|
} else {
|
||||||
u, err := url.Parse(src)
|
http.Error(w, "server not found", http.StatusNotFound)
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
device := Device{
|
|
||||||
Name: name,
|
|
||||||
Addr: u.Host,
|
|
||||||
Paired: true,
|
|
||||||
}
|
|
||||||
items = append(items, device)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
api.ResponsePrettyJSON(w, servers)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
|
||||||
if entry.Complete() {
|
|
||||||
device := Device{
|
|
||||||
Name: entry.Name,
|
|
||||||
Addr: entry.Addr(),
|
|
||||||
ID: entry.Info["id"],
|
|
||||||
Model: entry.Info["md"],
|
|
||||||
Paired: entry.Info["sf"] == "0",
|
|
||||||
}
|
|
||||||
items = append(items, device)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.ResponseJSON(w, items)
|
|
||||||
|
|
||||||
case "POST":
|
case "POST":
|
||||||
// TODO: post params...
|
id := r.Form.Get("id")
|
||||||
|
rawURL := r.Form.Get("src") + "&pin=" + r.Form.Get("pin")
|
||||||
id := r.URL.Query().Get("id")
|
if err := apiPair(id, rawURL); err != nil {
|
||||||
pin := r.URL.Query().Get("pin")
|
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
if err := hkPair(id, pin, name); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
src := r.URL.Query().Get("src")
|
id := r.Form.Get("id")
|
||||||
if err := hkDelete(src); err != nil {
|
if err := apiUnpair(id); err != nil {
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func hkPair(deviceID, pin, name string) (err error) {
|
func apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {
|
||||||
var conn *hap.Conn
|
id := r.URL.Query().Get("id")
|
||||||
|
stream := streams.Get(id)
|
||||||
if conn, err = hap.Pair(deviceID, pin); err != nil {
|
if stream == nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
streams.New(name, conn.URL())
|
rawURL := findHomeKitURL(stream.Sources())
|
||||||
|
if rawURL == "" {
|
||||||
dict := store.GetDict("streams")
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
dict[name] = conn.URL()
|
return
|
||||||
|
|
||||||
return store.Set("streams", dict)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hkDelete(name string) (err error) {
|
|
||||||
dict := store.GetDict("streams")
|
|
||||||
for key, rawURL := range dict {
|
|
||||||
if key != name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var conn *hap.Conn
|
|
||||||
|
|
||||||
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err = conn.Handle(); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = conn.ListPairings(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(dict, name)
|
|
||||||
|
|
||||||
return store.Set("streams", dict)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
func discovery() ([]*api.Source, error) {
|
||||||
ID string `json:"id"`
|
var sources []*api.Source
|
||||||
Name string `json:"name"`
|
|
||||||
Addr string `json:"addr"`
|
// 1. Get streams from Discovery
|
||||||
Model string `json:"model"`
|
err := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {
|
||||||
Paired bool `json:"paired"`
|
log.Trace().Msgf("[homekit] mdns=%s", entry)
|
||||||
//Type string `json:"type"`
|
|
||||||
|
category := entry.Info[hap.TXTCategory]
|
||||||
|
if entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {
|
||||||
|
source := &api.Source{
|
||||||
|
Name: entry.Name,
|
||||||
|
Info: entry.Info[hap.TXTModel],
|
||||||
|
URL: fmt.Sprintf(
|
||||||
|
"homekit://%s:%d?device_id=%s&feature=%s&status=%s",
|
||||||
|
entry.IP, entry.Port, entry.Info[hap.TXTDeviceID],
|
||||||
|
entry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
sources = append(sources, source)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiPair(id, url string) error {
|
||||||
|
conn, err := hap.Pair(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.New(id, conn.URL())
|
||||||
|
|
||||||
|
return app.PatchConfig([]string{"streams", id}, conn.URL())
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiUnpair(id string) error {
|
||||||
|
stream := streams.Get(id)
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New(api.StreamNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawURL := findHomeKitURL(stream.Sources())
|
||||||
|
if rawURL == "" {
|
||||||
|
return errors.New("not homekit source")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hap.Unpair(rawURL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.Delete(id)
|
||||||
|
|
||||||
|
return app.PatchConfig([]string{"streams", id}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHomeKitURLs() map[string]*url.URL {
|
||||||
|
urls := map[string]*url.URL{}
|
||||||
|
for name, sources := range streams.GetAllSources() {
|
||||||
|
if rawURL := findHomeKitURL(sources); rawURL != "" {
|
||||||
|
if u, err := url.Parse(rawURL); err == nil {
|
||||||
|
urls[name] = u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return urls
|
||||||
}
|
}
|
||||||
|
|||||||
+187
-8
@@ -1,32 +1,211 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
"github.com/AlexxIT/go2rtc/internal/app"
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
"github.com/AlexxIT/go2rtc/internal/srtp"
|
"github.com/AlexxIT/go2rtc/internal/srtp"
|
||||||
"github.com/AlexxIT/go2rtc/internal/streams"
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/hap/camera"
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/mdns"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod map[string]struct {
|
||||||
|
Pin string `yaml:"pin"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
log = app.GetLogger("homekit")
|
log = app.GetLogger("homekit")
|
||||||
|
|
||||||
streams.HandleFunc("homekit", streamHandler)
|
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
|
||||||
|
|
||||||
|
for id, conf := range cfg.Mod {
|
||||||
|
stream := streams.Get(id)
|
||||||
|
if stream == nil {
|
||||||
|
log.Warn().Msgf("[homekit] missing stream: %s", id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Pin == "" {
|
||||||
|
conf.Pin = "19550224" // default PIN
|
||||||
|
}
|
||||||
|
|
||||||
|
pin, err := hap.SanitizePin(conf.Pin)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address
|
||||||
|
name := calcName(conf.Name, deviceID)
|
||||||
|
setupID := calcSetupID(id)
|
||||||
|
|
||||||
|
srv := &server{
|
||||||
|
stream: id,
|
||||||
|
pairings: conf.Pairings,
|
||||||
|
setupID: setupID,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.hap = &hap.Server{
|
||||||
|
Pin: pin,
|
||||||
|
DeviceID: deviceID,
|
||||||
|
DevicePrivate: calcDevicePrivate(conf.DevicePrivate, id),
|
||||||
|
GetClientPublic: srv.GetPair,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv.mdns = &mdns.ServiceEntry{
|
||||||
|
Name: name,
|
||||||
|
Port: uint16(api.Port),
|
||||||
|
Info: map[string]string{
|
||||||
|
hap.TXTConfigNumber: "1",
|
||||||
|
hap.TXTFeatureFlags: "0",
|
||||||
|
hap.TXTDeviceID: deviceID,
|
||||||
|
hap.TXTModel: app.UserAgent,
|
||||||
|
hap.TXTProtoVersion: "1.1",
|
||||||
|
hap.TXTStateNumber: "1",
|
||||||
|
hap.TXTStatusFlags: hap.StatusNotPaired,
|
||||||
|
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)
|
||||||
|
hosts[host] = srv
|
||||||
|
servers[id] = srv
|
||||||
|
|
||||||
|
log.Trace().Msgf("[homekit] new server: %s", srv.mdns)
|
||||||
|
}
|
||||||
|
|
||||||
|
api.HandleFunc(hap.PathPairSetup, hapHandler)
|
||||||
|
api.HandleFunc(hap.PathPairVerify, hapHandler)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {
|
||||||
|
log.Error().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
var hosts map[string]*server
|
||||||
|
var servers map[string]*server
|
||||||
|
|
||||||
func streamHandler(url string) (core.Producer, error) {
|
func streamHandler(rawURL string) (core.Producer, error) {
|
||||||
conn, err := homekit.NewClient(url, srtp.Server)
|
if srtp.Server == nil {
|
||||||
if err != nil {
|
return nil, errors.New("homekit: can't work without SRTP server")
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
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 conn, nil
|
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
srv.Handle(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findHomeKitURL(sources []string) string {
|
||||||
|
if len(sources) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
url := sources[0]
|
||||||
|
if strings.HasPrefix(url, "homekit") {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(url, "hass") {
|
||||||
|
location, _ := streams.Location(url)
|
||||||
|
if strings.HasPrefix(location, "homekit") {
|
||||||
|
return location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBitrate(s string) int {
|
||||||
|
n := len(s)
|
||||||
|
if n == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var k int
|
||||||
|
switch n--; s[n] {
|
||||||
|
case 'K':
|
||||||
|
k = 1024
|
||||||
|
s = s[:n]
|
||||||
|
case 'M':
|
||||||
|
k = 1024 * 1024
|
||||||
|
s = s[:n]
|
||||||
|
default:
|
||||||
|
k = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return k * core.Atoi(s)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user