Compare commits
1991 Commits
v0.1-beta.10
...
master
| 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 | |||
| b5d40caffc | |||
| 1e0952be86 | |||
| d5fa933772 | |||
| 73bf96e123 | |||
| 4ea5a22eda | |||
| a79fe6041d | |||
| 07440f359e | |||
| 01ef67153e | |||
| fded87aa33 | |||
| 52a4fc329c | |||
| ce61d5759c | |||
| 39cc4610e3 | |||
| 67b25015df | |||
| f0d627fa55 | |||
| 9809f41117 | |||
| 2ce72dbcca | |||
| ddfeb6fae6 | |||
| 19130a4858 | |||
| 51b494b193 | |||
| fd3b3c9bf1 | |||
| fa763399c2 | |||
| af2398c072 | |||
| 19b0bc5f44 | |||
| f94cd16cb7 | |||
| 3246e7284c | |||
| 9339957c13 | |||
| 4ca397da3d | |||
| f6936f7cee | |||
| bdafaef7dc | |||
| 209d7b47d9 | |||
| 4283ae1022 | |||
| c2a398211c | |||
| 6c2f883f9e | |||
| c34f9ae2b7 | |||
| c29dd8c4e3 | |||
| 9e65f18e08 | |||
| db3fb72ac8 | |||
| 90cdfafcf5 | |||
| fa8d4e4807 | |||
| 37abe2ce0d | |||
| 1c3835f2a8 | |||
| bc6e4f40bf | |||
| ac5bcda492 | |||
| 7bd42eb55f | |||
| e4c7ffd1b4 | |||
| d31cf5521b | |||
| 9de980a63c | |||
| 74cef13479 | |||
| 887a491077 | |||
| 253fc4c915 | |||
| 3a51fa2397 | |||
| 306451f94f | |||
| 39811d121b | |||
| 99b962e7bb | |||
| 3dd14a826c | |||
| a99d7097b9 | |||
| 4f97e119ac | |||
| 44ee0066a5 | |||
| e5e899450f | |||
| 05a2f53b67 | |||
| 63bcaa836a | |||
| ba68bcb89e | |||
| 4a162c9a55 | |||
| c2f5f37f40 | |||
| 11201790d2 | |||
| 64804cbc87 | |||
| 75818d6967 | |||
| 14bb4b40f7 | |||
| 0fdb0b128b | |||
| fe28c32400 | |||
| 888159d2b6 | |||
| 397eb0b6ee | |||
| ffeb473918 | |||
| 966bedd38c | |||
| 0e270081fe | |||
| 1612f9c81e | |||
| bff9b06d5d | |||
| 59555cfe1d | |||
| c94d1e237d | |||
| 82a8e07b66 | |||
| e29307125c | |||
| 1eaacdb217 | |||
| c09438d3d0 | |||
| 8b126c0d37 | |||
| 3139189975 | |||
| 4fe078c7c0 | |||
| 083ec127fd | |||
| bcb9756aca | |||
| 981974eac9 | |||
| 5b29306d4f | |||
| e89c5cb429 | |||
| 04f263aa15 | |||
| da92256910 | |||
| 035b824645 | |||
| 2a91c4625a | |||
| 23dd5b450c | |||
| f617c148cd | |||
| b5f4c7f75b | |||
| d44efb84a0 | |||
| 03968d2f2e | |||
| 3c371e7046 | |||
| 4656086985 | |||
| e78f9fa69d | |||
| 2e8be342ef | |||
| 5387e88fe3 | |||
| 1746f55eda | |||
| 4d53889519 | |||
| 6d9d89bbe3 | |||
| c1923627c0 | |||
| 95ca5f5fe1 | |||
| 4bbd3a1cd2 | |||
| 9c8a1d8b19 | |||
| 53967fc72a | |||
| 31f870e950 | |||
| c7d228daff | |||
| 378f071e2c | |||
| 75f61b38ac | |||
| bc770f1a85 | |||
| d276311fcf | |||
| 1e14dc9ab2 | |||
| 8dbaa4ba93 | |||
| f0893bd78b | |||
| 6247746177 | |||
| a20de73ab2 | |||
| 813c8b3b3d | |||
| 63d9c6c2b7 | |||
| 2610f15eb6 | |||
| 9268acf1ca | |||
| 55fdf1a647 | |||
| 5fe07aeea0 | |||
| e8b22bca99 | |||
| 5926c1deb9 | |||
| dd98edc48e | |||
| fb1cc7dfc2 | |||
| 7626a09c1c | |||
| db85533e74 | |||
| 5939c8acba | |||
| e985ad23a2 | |||
| 7452eb5e05 | |||
| 5f9788209d | |||
| c07ddb8309 | |||
| 79f1dcfea3 | |||
| 3feaf852af | |||
| 76ec70d2a0 | |||
| 6cef5faf27 | |||
| edb4e6eaad | |||
| 116319f876 | |||
| a0e6005598 | |||
| fd580b6f2c | |||
| 1837e7c86c | |||
| 235f2fde0d | |||
| 35087e0812 | |||
| da08d8e973 | |||
| 757091e43d | |||
| a5c4854aeb | |||
| 4b4deaaaf2 | |||
| 553f5ff0d8 | |||
| 25dc3664fd | |||
| 8dd9991268 | |||
| d633d331bb | |||
| 7d3fbf2ee0 | |||
| af717b2172 | |||
| c44aaebd65 | |||
| d6259fc0e9 | |||
| 5c657d557a | |||
| 93be5cd92f | |||
| cf6a35d0c7 | |||
| af79e6054b | |||
| 9f3d5e7460 | |||
| abbf180b1b | |||
| 696588e52e | |||
| 3e97ce8b2a | |||
| 722b2827a1 | |||
| 69598b508c | |||
| f49fcc4f68 | |||
| 59347a409e | |||
| 45b25d29b7 | |||
| 49e861d1b0 | |||
| b1701e856a | |||
| a6260d0f56 | |||
| 693d41be87 | |||
| 222dc6a5c2 | |||
| 8fde2b6fe5 | |||
| 15e205cc01 | |||
| 1db9ed4946 | |||
| fd83d151d2 | |||
| 71051e7dcf | |||
| cdb3ee45cf | |||
| ae99c1da03 | |||
| 863cc0c1d7 | |||
| 40494ab87c | |||
| bffe5f0aa2 | |||
| 8241af8b9d | |||
| 5c164de393 | |||
| 8bf5c85b79 | |||
| a42c3e21c9 | |||
| 7016289f14 | |||
| 54302d3bda | |||
| af6b8a400d | |||
| a1b5eae653 | |||
| bb3c64598c | |||
| 3002d5f4f1 | |||
| cca4f0500e | |||
| b087be9c56 | |||
| 2d5a0e4822 | |||
| acf5ec5256 | |||
| e1e8abc334 | |||
| d84efd1238 | |||
| 7c79c1ff26 | |||
| 43840576ea | |||
| bd79b24db3 | |||
| e728643aad | |||
| 12a7b96289 | |||
| 2146ea470b | |||
| d4d91e4920 | |||
| a6393da956 | |||
| d686d4f691 | |||
| 58849fd1e5 | |||
| 31c86272bb | |||
| 0382fbf8a9 | |||
| 0b714a59e5 | |||
| 13c426e2a9 | |||
| d6d21286c1 | |||
| ce2898ac3a | |||
| e0320b8ead | |||
| a960b9b9ee | |||
| 0b4ebb4e21 | |||
| a1dd941814 | |||
| 146fb62b8e | |||
| 53e8fed0b0 | |||
| 3d34854387 | |||
| 77842643c8 | |||
| f9fe22569c | |||
| e17645ac02 | |||
| 1fc2cf3175 | |||
| 775b1818d1 | |||
| 1e83dc85f7 | |||
| 03a4393ce3 | |||
| 5f2368b0f9 | |||
| 59b35f2501 | |||
| 9ab9412c95 | |||
| d805d560b9 | |||
| 5aa20f0845 | |||
| c2cdf60ffc | |||
| c70c3a58f1 | |||
| df0ab77791 | |||
| 402df50b65 | |||
| 5c084c9989 | |||
| 1703e0dce8 | |||
| 7301f55e4a | |||
| 06fc9717df | |||
| 4e19c54467 | |||
| a0e04fb70e | |||
| 218eea6806 | |||
| ad3c5440fe | |||
| f5892e4cfc | |||
| 4328d2a573 | |||
| 3fb917f00f | |||
| eca79f1c0b | |||
| bd9b69d0d5 | |||
| 676ec25a7f | |||
| 12d10ae14e | |||
| 1912a43679 | |||
| eb1f423da3 | |||
| 5846cbd989 | |||
| ab1b3932ac | |||
| 91a7b5be27 | |||
| eca311717a | |||
| d3b2b8fdae | |||
| 3b9a0059df | |||
| 1fe21bb300 | |||
| 41cdcb69c6 | |||
| 6b00134575 | |||
| 5519f3e061 | |||
| e312d0b46b | |||
| eff7b27293 | |||
| e3f6c459c7 | |||
| 91399d3194 | |||
| 338da2a747 | |||
| bb5df24ecf | |||
| adb424033f | |||
| 70c415a1d8 | |||
| 9fd783793e | |||
| 665545903c | |||
| 830baafffe | |||
| a22c33fd4e | |||
| 1ad09f48cc | |||
| 5b1ec08341 | |||
| 3f22c010ce | |||
| df5f585064 | |||
| c1b810a5fe | |||
| e43b1e4ab6 | |||
| a8612fca43 | |||
| 0d18c23cc2 | |||
| c22ede2396 | |||
| 6b3a2652b2 | |||
| 4bf5034ce7 | |||
| b57027441c | |||
| d3b62d82cf | |||
| 836701cb68 | |||
| 3aee438e37 | |||
| 116e2f739b | |||
| 47371fbdcf | |||
| 6e62c442f8 | |||
| 57b49d735e | |||
| a72fa7fb23 | |||
| b2029d1004 | |||
| 3f338c83b7 | |||
| 00b445a170 | |||
| 4cbbb5407c | |||
| 80f77d28c8 | |||
| f60b55b6fa | |||
| c42413866d | |||
| b137eb66d0 | |||
| 6a40039645 | |||
| 2e4b28d871 | |||
| 58146b7e7e | |||
| 23db40220b | |||
| 557aac185d | |||
| 9ed4d4cedb | |||
| b05cbdf3d3 | |||
| 497594f53f | |||
| 73cdb39335 | |||
| a388002b12 | |||
| 6d1c0a2459 | |||
| da3137b6f0 | |||
| d21ce3d27d | |||
| 8cee4179f2 | |||
| 1153ee3652 | |||
| 3240301f27 | |||
| 2a20251dbd | |||
| 5a2d7de56b | |||
| 38ea8b56b8 | |||
| 08c2174e94 | |||
| b48f1c1a0b | |||
| cf58a6f952 | |||
| 350e677838 | |||
| 7b3505f4f4 | |||
| 98af8c3dbf | |||
| 762edf157a | |||
| 4a633cd9b5 | |||
| f4d2c801f0 | |||
| fb4b609914 | |||
| 56633229ed | |||
| 2d49cfd4b6 | |||
| 0f934be9b6 | |||
| c1d6adc189 | |||
| 500b8720d5 | |||
| b7391f58a5 | |||
| bef8e6454d | |||
| 5243aca8e9 | |||
| 69dd4d26ec | |||
| e93d89ec96 | |||
| ec56227900 | |||
| decd3af941 | |||
| e8e43f9d68 | |||
| a1fec1c6f6 | |||
| 073acdfec9 | |||
| d05ab79f88 | |||
| e295bc4eaf | |||
| 2f436bba4e | |||
| 0e28b0c797 | |||
| 3acea1ed5a | |||
| 3fb8d9af66 | |||
| 9bbaf41d54 | |||
| c43530fbd3 | |||
| 15777a3d94 | |||
| 6e61ac6d2f | |||
| 6d7d5f53d8 | |||
| d2bca8d461 | |||
| 94b089d1e3 | |||
| b3d16c9fcc | |||
| a36359f3dd | |||
| f0def68482 | |||
| 9ddbb326b4 | |||
| a2e58d928e | |||
| 3c48fb8bea | |||
| 4b0cbb5a73 | |||
| e28b49ea86 | |||
| 5c17d8fcb6 | |||
| e040fb591f | |||
| 140014f2a6 | |||
| 23f72d111e | |||
| f9d5ab9d0a | |||
| 8628c48db8 | |||
| 6e49d51c33 | |||
| 6a61b5234e | |||
| 7a0091777d | |||
| d23d2a7eff | |||
| cecbe4166c | |||
| dcb457235c | |||
| bc4e032830 | |||
| 8218cda149 | |||
| d1e56feeb6 | |||
| 463d05dfd3 | |||
| a1a73f7b45 | |||
| 39662e10af | |||
| 1c830d6e60 | |||
| 2039aa60b3 | |||
| b7016e798f | |||
| 0b291f5185 | |||
| 395304654a | |||
| e472397705 | |||
| 7c1f48e0ad | |||
| f4346a104f | |||
| 030972b436 | |||
| efddefa123 | |||
| 3c1bdd0dab | |||
| 7e7e15d7c8 | |||
| a1a9f77535 | |||
| a06462729d | |||
| 331c5bbcad | |||
| 58a76efc8a | |||
| 5e0f010885 | |||
| 4ae733aa11 | |||
| 27d8b33b62 | |||
| ff8b0fbb9c | |||
| c6ad7ac39f | |||
| 7a3adf17be | |||
| 94f6c07b28 | |||
| 7b326d4753 | |||
| 5407a3bc4b | |||
| 6b24421722 | |||
| d12775a2d7 | |||
| 6151593c08 | |||
| dba0989c54 | |||
| ba0c7d911d | |||
| 09fefca712 | |||
| b3f177e2ec | |||
| 228abb8fbe | |||
| eee70c07b7 | |||
| d92b0f29af | |||
| fca6c87b2c | |||
| 0601091772 | |||
| 89eb653d67 | |||
| 0e49ffdfff | |||
| bd2fc1252d | |||
| 78ac88448c | |||
| 4cd9757e53 | |||
| f9cb6fd670 | |||
| 57fa6a5530 | |||
| 6906b56524 | |||
| c9b0806c84 | |||
| a9d1e64f88 | |||
| 9e9f07f3f7 | |||
| b51aabd3d9 | |||
| 368562c540 | |||
| 6d6e7010b4 | |||
| 4157a53dd8 | |||
| bdf5654c01 | |||
| 66f729aa0e | |||
| 96d1ef2d2c | |||
| 9739f7f416 | |||
| 654fa32b3a | |||
| db2263c7fe | |||
| e6c36f1cf7 | |||
| 110f90cb34 | |||
| aca3bab238 | |||
| 4df44645d7 | |||
| 097fdfbbb8 | |||
| dc21a04da7 | |||
| db255b476a | |||
| 464ea417ef | |||
| c1fac66329 | |||
| a6057a2eca | |||
| 7c69ba13b0 | |||
| 2b8bfe8bd9 | |||
| 0bd54da456 | |||
| 9f6af1c9e4 | |||
| c9dd0e37e4 | |||
| 562872beb8 | |||
| 46a278c067 | |||
| 270fc7c1b6 | |||
| 6feb635522 | |||
| 6f48131e4d | |||
| f120db71a3 | |||
| 72823af9d0 | |||
| 15d9d4ebf4 | |||
| b09bbd79c4 | |||
| 1830273f02 | |||
| 07f3972794 | |||
| 4c2ebd20bc | |||
| 440c7bd6e1 | |||
| 74c3510a10 | |||
| c2748fc77b | |||
| d334551591 | |||
| cfe20925ac | |||
| 5b39f78ace | |||
| b965c191b7 | |||
| 7057b4846f | |||
| a746b96adc | |||
| b7718b33b8 | |||
| 69b17230f3 | |||
| e2ecd909ab | |||
| ea79da0d53 | |||
| e64919838c | |||
| 162b11213d | |||
| d27acbd7e3 | |||
| a692ecd7c1 | |||
| 98c5366ba9 | |||
| 3eaaa3fcfa | |||
| 7409b32836 | |||
| e2cfdf8419 | |||
| 4c0929d854 | |||
| 258a0ffb91 | |||
| 999e81c2dd | |||
| 8c6729027b | |||
| d3bd5eeab5 | |||
| dbbf2ea310 | |||
| b8234e0c76 | |||
| 96cd753e27 | |||
| c522e5bb08 | |||
| a16d8acc30 | |||
| 684878b4b1 | |||
| 140a742cee | |||
| 1b518b94fd | |||
| 5678121c50 | |||
| 4915f12bde | |||
| bb91240b95 | |||
| b1d5d53832 | |||
| 31fbbf91bb | |||
| a3f72fbab9 | |||
| fae59c7992 | |||
| aff34f1d21 | |||
| 65e7efa775 | |||
| 3c3e9d282b | |||
| bd51069086 | |||
| 1ddf7f1a6c | |||
| 0e281e36d3 | |||
| 3d6472cfb1 | |||
| 7c31fa2ffd | |||
| 0ed9d2410a | |||
| 1c89e7945e | |||
| 48635ae341 | |||
| fdb316910f | |||
| e29f2594fa | |||
| c3da7584b0 | |||
| 1e247cba92 | |||
| 01631d9eb0 | |||
| 4b27d119f0 | |||
| dd55c03dc2 | |||
| a4eab1944a | |||
| eea413a36c | |||
| cdd42a8ed2 | |||
| 4815ce1baf | |||
| e6d3939c78 | |||
| 220b9ca318 | |||
| d625620dfd | |||
| dd503f3410 | |||
| 3e8e87bfcc | |||
| 64d218886e | |||
| e91ccc211e | |||
| 9f8a219483 | |||
| b617796941 | |||
| 77888fe086 | |||
| 7bc3534bcb | |||
| 77bc0630d6 | |||
| 2f68711405 | |||
| b8cab5db60 | |||
| eae01be71f | |||
| 0127115180 | |||
| aef84cef6b | |||
| d478436758 | |||
| f77db44529 | |||
| 149d1bf235 | |||
| b650475b10 | |||
| 16e5406156 | |||
| 49f6233bde | |||
| 78c5c70c73 | |||
| 32651c74ab | |||
| 5c64d1f847 | |||
| 717af29630 | |||
| ea18475d31 | |||
| 701a9c69ec | |||
| c06253c8b2 | |||
| 3a07e9fa03 | |||
| e1bc30fab3 | |||
| d16ae0972f | |||
| 8b93c97e69 | |||
| d8158bc1e3 | |||
| f4f588d2c6 | |||
| e287b52808 | |||
| ff96257252 | |||
| 909f21b7e4 | |||
| 7d6a5b44f8 | |||
| 278f7696b6 | |||
| 3cbf2465ae | |||
| e9ea7a0b1f | |||
| 0231fc3a90 | |||
| 9ef2633840 | |||
| 5a8df3e90a | |||
| a31cbec3eb | |||
| 54f547977e | |||
| 65d91e02bd | |||
| 7fc3f0f641 | |||
| 7725d5ed31 | |||
| 6c1b9daa8b | |||
| 6d432574bf | |||
| 616f69c88b | |||
| f72440712b | |||
| ceed146fb8 | |||
| f17dadbbbf | |||
| 3d4514eab9 | |||
| 2629dccb81 |
@@ -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,59 +0,0 @@
|
|||||||
# https://github.com/home-assistant/builder
|
|
||||||
name: 'Builder'
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags: [ 'v*' ]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
hassio:
|
|
||||||
name: Hassio Addon
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout the repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Branch name
|
|
||||||
run: |
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
||||||
echo "REPO=alexxit/go2rtc" >> $GITHUB_ENV
|
|
||||||
echo "TAG=${VERSION}" >> $GITHUB_ENV
|
|
||||||
echo "IMAGE=alexxit/go2rtc:${VERSION}" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build amd64
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --amd64 --target build/hassio --version $TAG-amd64 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Build i386
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --i386 --target build/hassio --version $TAG-i386 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Build aarch64
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --aarch64 --target build/hassio --version $TAG-aarch64 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Build armv7
|
|
||||||
uses: home-assistant/builder@master
|
|
||||||
with:
|
|
||||||
args: --armv7 --target build/hassio --version $TAG-armv7 --no-latest --docker-hub-check
|
|
||||||
|
|
||||||
- name: Docker manifest
|
|
||||||
run: |
|
|
||||||
# thanks to https://github.com/aler9/rtsp-simple-server/blob/main/Makefile
|
|
||||||
docker manifest create "${IMAGE}" \
|
|
||||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
|
||||||
docker manifest push "${IMAGE}"
|
|
||||||
|
|
||||||
docker manifest create "${REPO}:latest" \
|
|
||||||
"${IMAGE}-amd64" "${IMAGE}-i386" "${IMAGE}-aarch64" "${IMAGE}-armv7"
|
|
||||||
docker manifest push "${REPO}:latest"
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Simple workflow for deploying static content to GitHub Pages
|
||||||
|
name: Deploy static content to Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# Allow one concurrent deployment
|
||||||
|
concurrency:
|
||||||
|
group: "pages"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
package-manager-cache: false
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --no-package-lock
|
||||||
|
- name: Build docs
|
||||||
|
run: npm run docs:build
|
||||||
|
- name: Copy docs into website
|
||||||
|
run: rsync -a --exclude '.vitepress/' --exclude 'README.md' website/ website/.vitepress/dist/
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: website/.vitepress/dist
|
||||||
|
|
||||||
|
# Single deploy job since we're just deploying
|
||||||
|
deploy:
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
name: Test Build and Run
|
||||||
|
|
||||||
|
on:
|
||||||
|
# push:
|
||||||
|
# branches:
|
||||||
|
# - '*'
|
||||||
|
# pull_request:
|
||||||
|
# merge_group:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||||
|
arch: [amd64, arm64]
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GOARCH: ${{ matrix.arch }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.24'
|
||||||
|
|
||||||
|
- name: Build Go binary
|
||||||
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
|
|
||||||
|
- name: Test Go binary on linux
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.arch }}" = "amd64" ]; then
|
||||||
|
./go2rtc -version
|
||||||
|
else
|
||||||
|
sudo apt-get update && sudo apt-get install -y qemu-user-static
|
||||||
|
sudo cp /usr/bin/qemu-aarch64-static .
|
||||||
|
sudo chown $USER:$USER ./qemu-aarch64-static
|
||||||
|
qemu-aarch64-static ./go2rtc -version
|
||||||
|
fi
|
||||||
|
- name: Test Go binary on macos
|
||||||
|
if: matrix.os == 'macos-latest'
|
||||||
|
run: |
|
||||||
|
if [ "${{ matrix.arch }}" = "amd64" ]; then
|
||||||
|
./go2rtc -version
|
||||||
|
else
|
||||||
|
echo "ARM64 architecture is not yet supported on macOS"
|
||||||
|
fi
|
||||||
|
- name: Test Go binary on windows
|
||||||
|
if: matrix.os == 'windows-latest'
|
||||||
|
run: |
|
||||||
|
if ("${{ matrix.arch }}" -eq "amd64") {
|
||||||
|
.\go2rtc* -version
|
||||||
|
} else {
|
||||||
|
Write-Host "ARM64 architecture is not yet supported on Windows"
|
||||||
|
}
|
||||||
|
docker-test:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- amd64
|
||||||
|
- "386"
|
||||||
|
- arm/v7
|
||||||
|
- arm64/v8
|
||||||
|
continue-on-error: true
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
platforms: linux/${{ matrix.platform }}
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: go2rtc-${{ matrix.platform }}
|
||||||
|
- name: test run
|
||||||
|
run: |
|
||||||
|
docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }} go2rtc -version
|
||||||
|
|
||||||
|
- name: Build and push Hardware
|
||||||
|
if: matrix.platform == 'amd64'
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/hardware.Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: false
|
||||||
|
load: true
|
||||||
|
tags: go2rtc-${{ matrix.platform }}-hardware
|
||||||
|
- name: test run
|
||||||
|
if: matrix.platform == 'amd64'
|
||||||
|
run: |
|
||||||
|
docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }}-hardware go2rtc -version
|
||||||
+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
|
||||||
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,41 +0,0 @@
|
|||||||
ARG BUILD_FROM
|
|
||||||
|
|
||||||
FROM $BUILD_FROM as build
|
|
||||||
|
|
||||||
# 1. Build go2rtc
|
|
||||||
RUN apk add --no-cache git go
|
|
||||||
|
|
||||||
RUN git clone https://github.com/AlexxIT/go2rtc \
|
|
||||||
&& cd go2rtc \
|
|
||||||
&& CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
|
||||||
|
|
||||||
# 2. Download ngrok
|
|
||||||
ARG BUILD_ARCH
|
|
||||||
|
|
||||||
# https://github.com/home-assistant/docker-base/blob/master/alpine/Dockerfile
|
|
||||||
RUN if [ "${BUILD_ARCH}" = "aarch64" ]; then BUILD_ARCH="arm64"; \
|
|
||||||
elif [ "${BUILD_ARCH}" = "armv7" ]; then BUILD_ARCH="arm"; fi \
|
|
||||||
&& cd go2rtc \
|
|
||||||
&& curl $(curl -s "https://raw.githubusercontent.com/ngrok/docker-ngrok/main/releases.json" | jq -r ".${BUILD_ARCH}.url") -o ngrok.zip \
|
|
||||||
&& unzip ngrok
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# https://devopscube.com/reduce-docker-image-size/
|
|
||||||
FROM $BUILD_FROM
|
|
||||||
|
|
||||||
# 3. Copy go2rtc and ngrok to release
|
|
||||||
COPY --from=build /go2rtc/go2rtc /usr/local/bin
|
|
||||||
COPY --from=build /go2rtc/ngrok /usr/local/bin
|
|
||||||
|
|
||||||
# 4. Install ffmpeg
|
|
||||||
# apk base OK: 22 MiB in 40 packages
|
|
||||||
# ffmpeg OK: 113 MiB in 110 packages
|
|
||||||
# python3 OK: 161 MiB in 114 packages
|
|
||||||
RUN apk add --no-cache ffmpeg python3
|
|
||||||
|
|
||||||
# 5. Copy run to release
|
|
||||||
COPY run.sh /
|
|
||||||
RUN chmod a+x /run.sh
|
|
||||||
|
|
||||||
CMD [ "/run.sh" ]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# https://github.com/home-assistant/builder/blob/master/builder.sh
|
|
||||||
name: go2rtc
|
|
||||||
description: Ultimate camera streaming application
|
|
||||||
url: https://github.com/AlexxIT/go2rtc
|
|
||||||
image: alexxit/go2rtc
|
|
||||||
arch: [ amd64, aarch64, i386, armv7 ]
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#!/usr/bin/with-contenv bashio
|
|
||||||
|
|
||||||
set +e
|
|
||||||
|
|
||||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
|
||||||
cd /config
|
|
||||||
|
|
||||||
# add the feature to override go2rtc binary from Hass config folder
|
|
||||||
export PATH="/config:$PATH"
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
go2rtc
|
|
||||||
sleep 5
|
|
||||||
done
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
**Project layout**
|
|
||||||
|
|
||||||
- https://github.com/golang-standards/project-layout
|
|
||||||
- https://github.com/micro/micro
|
|
||||||
-139
@@ -1,139 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod struct {
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
BasePath string `yaml:"base_path"`
|
|
||||||
StaticDir string `yaml:"static_dir"`
|
|
||||||
} `yaml:"api"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// default config
|
|
||||||
cfg.Mod.Listen = ":1984"
|
|
||||||
|
|
||||||
// load config from YAML
|
|
||||||
app.LoadConfig(&cfg)
|
|
||||||
|
|
||||||
if cfg.Mod.Listen == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
basePath = cfg.Mod.BasePath
|
|
||||||
log = app.GetLogger("api")
|
|
||||||
|
|
||||||
initStatic(cfg.Mod.StaticDir)
|
|
||||||
initWS()
|
|
||||||
|
|
||||||
HandleFunc("api/streams", streamsHandler)
|
|
||||||
HandleFunc("api/ws", apiWS)
|
|
||||||
|
|
||||||
// ensure we can listen without errors
|
|
||||||
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")
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
s := http.Server{}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Trace().Stringer("url", r.URL).Msgf("[api] %s", r.Method)
|
|
||||||
http.DefaultServeMux.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = s.Serve(listener); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleFunc handle pattern with relative path:
|
|
||||||
// - "api/streams" => "{basepath}/api/streams"
|
|
||||||
// - "/streams" => "/streams"
|
|
||||||
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
|
||||||
if len(pattern) == 0 || pattern[0] != '/' {
|
|
||||||
pattern = basePath + "/" + pattern
|
|
||||||
}
|
|
||||||
log.Trace().Str("path", pattern).Msg("[api] register path")
|
|
||||||
http.HandleFunc(pattern, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func HandleWS(msgType string, handler WSHandler) {
|
|
||||||
wsHandlers[msgType] = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
var basePath string
|
|
||||||
var log zerolog.Logger
|
|
||||||
var wsHandlers = make(map[string]WSHandler)
|
|
||||||
|
|
||||||
func streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
name = src
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.Method {
|
|
||||||
case "PUT":
|
|
||||||
streams.New(name, src)
|
|
||||||
return
|
|
||||||
case "DELETE":
|
|
||||||
streams.Delete(src)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var v interface{}
|
|
||||||
if src != "" {
|
|
||||||
v = streams.Get(src)
|
|
||||||
} else {
|
|
||||||
v = streams.All()
|
|
||||||
}
|
|
||||||
|
|
||||||
e := json.NewEncoder(w)
|
|
||||||
e.SetIndent("", " ")
|
|
||||||
_ = e.Encode(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiWS(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := new(Context)
|
|
||||||
if err := ctx.Upgrade(w, r); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.ws] upgrade")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer ctx.Close()
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg := new(streamer.Message)
|
|
||||||
if err := ctx.Conn.ReadJSON(msg); err != nil {
|
|
||||||
if websocket.IsUnexpectedCloseError(
|
|
||||||
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
|
|
||||||
) {
|
|
||||||
log.Error().Err(err).Msg("[api.ws] readJSON")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handler := wsHandlers[msg.Type]
|
|
||||||
if handler != nil {
|
|
||||||
handler(ctx, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initWS() {
|
|
||||||
wsUp = &websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 512000,
|
|
||||||
}
|
|
||||||
wsUp.CheckOrigin = func(r *http.Request) bool {
|
|
||||||
origin := r.Header["Origin"]
|
|
||||||
if len(origin) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
o, err := url.Parse(origin[0])
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if o.Host == r.Host {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
log.Trace().Msgf("[api.ws] origin: %s, host: %s", o.Host, r.Host)
|
|
||||||
// some users change Nginx external port using Docker port
|
|
||||||
// so origin will be with a port and host without
|
|
||||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
|
||||||
return o.Host[:i] == r.Host
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var wsUp *websocket.Upgrader
|
|
||||||
|
|
||||||
type WSHandler func(ctx *Context, msg *streamer.Message)
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
Conn *websocket.Conn
|
|
||||||
Request *http.Request
|
|
||||||
Consumer interface{} // TODO: rewrite
|
|
||||||
|
|
||||||
onClose []func()
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
|
|
||||||
ctx.Conn, err = wsUp.Upgrade(w, r, nil)
|
|
||||||
ctx.Request = r
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) Close() {
|
|
||||||
for _, f := range ctx.onClose {
|
|
||||||
f()
|
|
||||||
}
|
|
||||||
_ = ctx.Conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) Write(msg interface{}) {
|
|
||||||
ctx.mu.Lock()
|
|
||||||
defer ctx.mu.Unlock()
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *streamer.Message:
|
|
||||||
err = ctx.Conn.WriteJSON(msg)
|
|
||||||
case []byte:
|
|
||||||
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
//panic(err) // TODO: fix panic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) Error(err error) {
|
|
||||||
ctx.Write(&streamer.Message{
|
|
||||||
Type: "error", Value: err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *Context) OnClose(f func()) {
|
|
||||||
ctx.onClose = append(ctx.onClose, f)
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
config := flag.String(
|
|
||||||
"config",
|
|
||||||
"go2rtc.yaml",
|
|
||||||
"Path to go2rtc configuration file",
|
|
||||||
)
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
data, _ = os.ReadFile(*config)
|
|
||||||
|
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]string `yaml:"log"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != nil {
|
|
||||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
|
||||||
println("ERROR: " + err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var writer io.Writer = os.Stdout
|
|
||||||
|
|
||||||
// styles
|
|
||||||
format := cfg.Mod["format"]
|
|
||||||
if format != "json" {
|
|
||||||
writer = zerolog.ConsoleWriter{
|
|
||||||
Out: writer, TimeFormat: "15:04:05.000",
|
|
||||||
NoColor: writer != os.Stdout || format == "text",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
|
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(cfg.Mod["level"])
|
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
|
||||||
lvl = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
|
||||||
|
|
||||||
modules = cfg.Mod
|
|
||||||
|
|
||||||
path, _ := os.Getwd()
|
|
||||||
log.Debug().Str("os", runtime.GOOS).Str("arch", runtime.GOARCH).
|
|
||||||
Str("cwd", path).Int("conf_size", len(data)).Msgf("[app]")
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(v interface{}) {
|
|
||||||
if data != nil {
|
|
||||||
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 {
|
|
||||||
log.Warn().Err(err).Msg("[log]")
|
|
||||||
return log
|
|
||||||
}
|
|
||||||
|
|
||||||
return log.Level(lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
return log
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal
|
|
||||||
|
|
||||||
// data - config content
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
// log - main logger
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
// modules log levels
|
|
||||||
var modules map[string]string
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const name = "go2rtc.json"
|
|
||||||
|
|
||||||
var store map[string]interface{}
|
|
||||||
|
|
||||||
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]interface{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() error {
|
|
||||||
data, err := json.Marshal(store)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(name, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRaw(key string) interface{} {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
return store[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDict(key string) map[string]interface{} {
|
|
||||||
raw := GetRaw(key)
|
|
||||||
if raw != nil {
|
|
||||||
return raw.(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
return make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(key string, v interface{}) error {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
store[key] = v
|
|
||||||
|
|
||||||
return save()
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package debug
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
api.HandleFunc("api/stack", stackHandler)
|
|
||||||
api.HandleFunc("api/exit", exitHandler)
|
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func exitHandler(_ http.ResponseWriter, r *http.Request) {
|
|
||||||
s := r.URL.Query().Get("code")
|
|
||||||
code, _ := strconv.Atoi(s)
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullHandler(string) (streamer.Producer, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package echo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
log := app.GetLogger("echo")
|
|
||||||
|
|
||||||
streams.HandleFunc("echo", func(url string) (streamer.Producer, error) {
|
|
||||||
args := shell.QuoteSplit(url[5:])
|
|
||||||
|
|
||||||
b, err := exec.Command(args[0], args[1:]...).Output()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
b = bytes.TrimSpace(b)
|
|
||||||
|
|
||||||
log.Debug().Str("url", url).Msgf("[echo] %s", b)
|
|
||||||
|
|
||||||
return streams.GetProducer(string(b))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package exec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
// depends on RTSP server
|
|
||||||
if rtsp.Port == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rtsp.OnProducer = func(prod streamer.Producer) bool {
|
|
||||||
if conn := prod.(*pkg.Conn); conn != nil {
|
|
||||||
if waiter := waiters[conn.URL.Path]; waiter != nil {
|
|
||||||
waiter <- prod
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.HandleFunc("exec", Handle)
|
|
||||||
|
|
||||||
log = app.GetLogger("exec")
|
|
||||||
|
|
||||||
// TODO: add sync.Mutex
|
|
||||||
waiters = map[string]chan streamer.Producer{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Handle(url string) (streamer.Producer, error) {
|
|
||||||
sum := md5.Sum([]byte(url))
|
|
||||||
path := "/" + hex.EncodeToString(sum[:])
|
|
||||||
|
|
||||||
url = strings.Replace(
|
|
||||||
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
|
|
||||||
)
|
|
||||||
|
|
||||||
// remove `exec:`
|
|
||||||
args := shell.QuoteSplit(url[5:])
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan streamer.Producer)
|
|
||||||
|
|
||||||
waiters[path] = ch
|
|
||||||
defer delete(waiters, path)
|
|
||||||
|
|
||||||
log.Debug().Str("url", url).Msg("[exec] run")
|
|
||||||
|
|
||||||
ts := time.Now()
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Error().Err(err).Str("url", url).Msg("[exec]")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Second * 15):
|
|
||||||
_ = cmd.Process.Kill()
|
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
|
||||||
return nil, errors.New("timeout")
|
|
||||||
case prod := <-ch:
|
|
||||||
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run")
|
|
||||||
return prod, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
var waiters map[string]chan streamer.Producer
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
|
||||||
const deviceInputPrefix = "-f avfoundation"
|
|
||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|
||||||
video := findMedia(streamer.KindVideo, videoIdx)
|
|
||||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
|
||||||
switch {
|
|
||||||
case video != nil && audio != nil:
|
|
||||||
return `"` + video.Title + `:` + audio.Title + `"`
|
|
||||||
case video != nil:
|
|
||||||
return `"` + video.Title + `"`
|
|
||||||
case audio != nil:
|
|
||||||
return `"` + audio.Title + `"`
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedias() {
|
|
||||||
cmd := exec.Command(
|
|
||||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "dummy",
|
|
||||||
)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
cmd.Stderr = &buf
|
|
||||||
_ = cmd.Run()
|
|
||||||
|
|
||||||
var kind string
|
|
||||||
|
|
||||||
lines := strings.Split(buf.String(), "\n")
|
|
||||||
process:
|
|
||||||
for _, line := range lines {
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(line, "video devices:"):
|
|
||||||
kind = streamer.KindVideo
|
|
||||||
continue
|
|
||||||
case strings.HasSuffix(line, "audio devices:"):
|
|
||||||
kind = streamer.KindAudio
|
|
||||||
continue
|
|
||||||
case strings.HasPrefix(line, "dummy"):
|
|
||||||
break process
|
|
||||||
}
|
|
||||||
|
|
||||||
// [AVFoundation indev @ 0x7fad54604380] [0] FaceTime HD Camera
|
|
||||||
name := line[42:]
|
|
||||||
media := loadMedia(kind, name)
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedia(kind, name string) *streamer.Media {
|
|
||||||
return &streamer.Media{
|
|
||||||
Kind: kind, Title: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"io/ioutil"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
|
||||||
const deviceInputPrefix = "-f v4l2"
|
|
||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|
||||||
video := findMedia(streamer.KindVideo, videoIdx)
|
|
||||||
return video.Title
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedias() {
|
|
||||||
files, err := ioutil.ReadDir("/dev")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
log.Trace().Msg("[ffmpeg] " + file.Name())
|
|
||||||
if strings.HasPrefix(file.Name(), streamer.KindVideo) {
|
|
||||||
media := loadMedia(streamer.KindVideo, "/dev/"+file.Name())
|
|
||||||
if media != nil {
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedia(kind, name string) *streamer.Media {
|
|
||||||
cmd := exec.Command(
|
|
||||||
Bin, "-hide_banner", "-f", "v4l2", "-list_formats", "all", "-i", name,
|
|
||||||
)
|
|
||||||
var buf bytes.Buffer
|
|
||||||
cmd.Stderr = &buf
|
|
||||||
_ = cmd.Run()
|
|
||||||
|
|
||||||
if !bytes.Contains(buf.Bytes(), []byte("Raw")) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &streamer.Media{
|
|
||||||
Kind: kind, Title: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://trac.ffmpeg.org/wiki/DirectShow
|
|
||||||
const deviceInputPrefix = "-f dshow"
|
|
||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|
||||||
video := findMedia(streamer.KindVideo, videoIdx)
|
|
||||||
audio := findMedia(streamer.KindAudio, audioIdx)
|
|
||||||
switch {
|
|
||||||
case video != nil && audio != nil:
|
|
||||||
return `video="` + video.Title + `":audio=` + audio.Title + `"`
|
|
||||||
case video != nil:
|
|
||||||
return `video="` + video.Title + `"`
|
|
||||||
case audio != nil:
|
|
||||||
return `audio="` + audio.Title + `"`
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedias() {
|
|
||||||
cmd := exec.Command(
|
|
||||||
Bin, "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "",
|
|
||||||
)
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
cmd.Stderr = &buf
|
|
||||||
_ = cmd.Run()
|
|
||||||
|
|
||||||
lines := strings.Split(buf.String(), "\r\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
var kind string
|
|
||||||
if strings.HasSuffix(line, "(video)") {
|
|
||||||
kind = streamer.KindVideo
|
|
||||||
} else if strings.HasSuffix(line, "(audio)") {
|
|
||||||
kind = streamer.KindAudio
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// hope we have constant prefix and suffix sizes
|
|
||||||
// [dshow @ 00000181e8d028c0] "VMware Virtual USB Video Device" (video)
|
|
||||||
name := line[28 : len(line)-9]
|
|
||||||
media := loadMedia(kind, name)
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedia(kind, name string) *streamer.Media {
|
|
||||||
return &streamer.Media{
|
|
||||||
Kind: kind, Title: name,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
log = app.GetLogger("exec")
|
|
||||||
|
|
||||||
api.HandleFunc("api/devices", handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetInput(src string) (string, error) {
|
|
||||||
if medias == nil {
|
|
||||||
loadMedias()
|
|
||||||
}
|
|
||||||
|
|
||||||
input := deviceInputPrefix
|
|
||||||
|
|
||||||
var videoIdx, audioIdx int
|
|
||||||
if i := strings.IndexByte(src, '?'); i > 0 {
|
|
||||||
query, err := url.ParseQuery(src[i+1:])
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for key, value := range query {
|
|
||||||
switch key {
|
|
||||||
case "video":
|
|
||||||
videoIdx, _ = strconv.Atoi(value[0])
|
|
||||||
case "audio":
|
|
||||||
audioIdx, _ = strconv.Atoi(value[0])
|
|
||||||
case "framerate":
|
|
||||||
input += " -framerate " + value[0]
|
|
||||||
case "resolution":
|
|
||||||
input += " -video_size " + value[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input += " -i " + deviceInputSuffix(videoIdx, audioIdx)
|
|
||||||
|
|
||||||
return input, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var Bin string
|
|
||||||
var log zerolog.Logger
|
|
||||||
var medias []*streamer.Media
|
|
||||||
|
|
||||||
func findMedia(kind string, index int) *streamer.Media {
|
|
||||||
for _, media := range medias {
|
|
||||||
if media.Kind != kind {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if index == 0 {
|
|
||||||
return media
|
|
||||||
}
|
|
||||||
index--
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if medias == nil {
|
|
||||||
loadMedias()
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(medias)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.ffmpeg]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.ffmpeg]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]string `yaml:"ffmpeg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaults
|
|
||||||
|
|
||||||
cfg.Mod = map[string]string{
|
|
||||||
"bin": "ffmpeg",
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
"file": "-re -stream_loop -1 -i {input}",
|
|
||||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
|
||||||
"rtsp": "-fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
|
|
||||||
|
|
||||||
// output
|
|
||||||
"output": "-rtsp_transport tcp -f rtsp {output}",
|
|
||||||
|
|
||||||
// `-g 30` - group of picture, GOP, keyframe interval
|
|
||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
|
||||||
// `-tune zerolatency` - for minimal latency
|
|
||||||
// `-profile main -level 4.1` - most used streaming profile
|
|
||||||
// `-pix_fmt yuv420p` - if input pix format 4:2:2
|
|
||||||
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1 -pix_fmt yuv420p",
|
|
||||||
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
|
|
||||||
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
|
|
||||||
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
|
|
||||||
"mjpeg": "-codec:v mjpeg -force_duplicated_matrix 1 -huffman 0 -pix_fmt yuvj420p",
|
|
||||||
"opus": "-codec:a libopus -ar 48000 -ac 2",
|
|
||||||
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
|
|
||||||
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
|
|
||||||
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1",
|
|
||||||
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1",
|
|
||||||
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1",
|
|
||||||
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1",
|
|
||||||
"aac/16000": "-codec:a aac -ar 16000 -ac 1",
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
|
||||||
|
|
||||||
tpl := cfg.Mod
|
|
||||||
|
|
||||||
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
|
|
||||||
s = s[7:] // remove `ffmpeg:`
|
|
||||||
|
|
||||||
var query url.Values
|
|
||||||
var queryVideo, queryAudio bool
|
|
||||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
|
||||||
query = parseQuery(s[i+1:])
|
|
||||||
queryVideo = query["video"] != nil
|
|
||||||
queryAudio = query["audio"] != nil
|
|
||||||
s = s[:i]
|
|
||||||
} else {
|
|
||||||
// by default query both video and audio
|
|
||||||
queryVideo = true
|
|
||||||
queryAudio = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var input string
|
|
||||||
if i := strings.IndexByte(s, ':'); i > 0 {
|
|
||||||
switch s[:i] {
|
|
||||||
case "http", "https", "rtmp":
|
|
||||||
input = strings.Replace(tpl["http"], "{input}", s, 1)
|
|
||||||
case "rtsp", "rtsps":
|
|
||||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
|
||||||
// skip unnecessary input tracks
|
|
||||||
switch {
|
|
||||||
case queryVideo && queryAudio:
|
|
||||||
input = "-allowed_media_types video+audio "
|
|
||||||
case queryVideo:
|
|
||||||
input = "-allowed_media_types video "
|
|
||||||
case queryAudio:
|
|
||||||
input = "-allowed_media_types audio "
|
|
||||||
}
|
|
||||||
|
|
||||||
input += strings.Replace(tpl["rtsp"], "{input}", s, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if input == "" {
|
|
||||||
if strings.HasPrefix(s, "device?") {
|
|
||||||
var err error
|
|
||||||
input, err = device.GetInput(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input = strings.Replace(tpl["file"], "{input}", s, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s = "exec:" + tpl["bin"] + " -hide_banner " + input
|
|
||||||
|
|
||||||
if query != nil {
|
|
||||||
for _, raw := range query["raw"] {
|
|
||||||
s += " " + raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: multiple codecs via -map
|
|
||||||
// s += fmt.Sprintf(" -map 0:v:0 -c:v:%d copy", i)
|
|
||||||
|
|
||||||
for _, video := range query["video"] {
|
|
||||||
if video == "copy" {
|
|
||||||
s += " -codec:v copy"
|
|
||||||
} else {
|
|
||||||
s += " " + tpl[video]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, audio := range query["audio"] {
|
|
||||||
if audio == "copy" {
|
|
||||||
s += " -codec:a copy"
|
|
||||||
} else {
|
|
||||||
s += " " + tpl[audio]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case queryVideo && !queryAudio:
|
|
||||||
s += " -an"
|
|
||||||
case queryAudio && !queryVideo:
|
|
||||||
s += " -vn"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s += " -c copy"
|
|
||||||
}
|
|
||||||
|
|
||||||
s += " " + tpl["output"]
|
|
||||||
|
|
||||||
return exec.Handle(s)
|
|
||||||
})
|
|
||||||
|
|
||||||
device.Bin = cfg.Mod["bin"]
|
|
||||||
device.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseQuery(s string) map[string][]string {
|
|
||||||
query := map[string][]string{}
|
|
||||||
for _, key := range strings.Split(s, "#") {
|
|
||||||
var value string
|
|
||||||
i := strings.IndexByte(key, '=')
|
|
||||||
if i > 0 {
|
|
||||||
key, value = key[:i], key[i+1:]
|
|
||||||
}
|
|
||||||
query[key] = append(query[key], value)
|
|
||||||
}
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
-149
@@ -1,149 +0,0 @@
|
|||||||
package hass
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/webrtc"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initAPI() {
|
|
||||||
ok := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_, _ = w.Write([]byte(`{"status":1,"payload":{}}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
// support https://www.home-assistant.io/integrations/rtsp_to_webrtc/
|
|
||||||
api.HandleFunc("/static", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
api.HandleFunc("/streams", ok)
|
|
||||||
|
|
||||||
api.HandleFunc("/stream/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch {
|
|
||||||
// /stream/{id}/add
|
|
||||||
case strings.HasSuffix(r.RequestURI, "/add"):
|
|
||||||
var v addJSON
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&v); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// we can get three types of links:
|
|
||||||
// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}
|
|
||||||
// 2. static link to Hass camera
|
|
||||||
// 3. dynamic link to Hass camera
|
|
||||||
stream := streams.Get(v.Name)
|
|
||||||
if stream == nil {
|
|
||||||
// check if it is rtsp link to go2rtc
|
|
||||||
stream = rtspStream(v.Channels.First.Url)
|
|
||||||
if stream != nil {
|
|
||||||
streams.New(v.Name, stream)
|
|
||||||
} else {
|
|
||||||
stream = streams.New(v.Name, "{input}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.SetSource(v.Channels.First.Url)
|
|
||||||
|
|
||||||
ok(w, r)
|
|
||||||
|
|
||||||
// /stream/{id}/channel/0/webrtc
|
|
||||||
default:
|
|
||||||
i := strings.IndexByte(r.RequestURI[8:], '/')
|
|
||||||
name := r.RequestURI[8 : 8+i]
|
|
||||||
|
|
||||||
stream := streams.Get(name)
|
|
||||||
if stream == nil {
|
|
||||||
w.WriteHeader(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] parse form")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s := r.FormValue("data")
|
|
||||||
offer, err := base64.StdEncoding.DecodeString(s)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] sdp64 decode")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.hass] exchange SDP")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s = base64.StdEncoding.EncodeToString([]byte(s))
|
|
||||||
_, _ = w.Write([]byte(s))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// api from RTSPtoWebRTC
|
|
||||||
api.HandleFunc("/stream", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if err := r.ParseForm(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
str := r.FormValue("sdp64")
|
|
||||||
offer, err := base64.StdEncoding.DecodeString(str)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src := r.FormValue("url")
|
|
||||||
src, err = url.QueryUnescape(src)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream := streams.Get(src)
|
|
||||||
if stream == nil {
|
|
||||||
if stream = rtspStream(src); stream != nil {
|
|
||||||
streams.New(src, stream)
|
|
||||||
} else {
|
|
||||||
stream = streams.New(src, src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err = webrtc.ExchangeSDP(stream, string(offer), r.UserAgent())
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
v := struct {
|
|
||||||
Answer string `json:"sdp64"`
|
|
||||||
}{
|
|
||||||
Answer: base64.StdEncoding.EncodeToString([]byte(str)),
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func rtspStream(url string) *streams.Stream {
|
|
||||||
if strings.HasPrefix(url, "rtsp://") {
|
|
||||||
if i := strings.IndexByte(url[7:], '/'); i > 0 {
|
|
||||||
return streams.Get(url[8+i:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type addJSON struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Channels struct {
|
|
||||||
First struct {
|
|
||||||
//Name string `json:"name"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
} `json:"0"`
|
|
||||||
} `json:"channels"`
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package hass
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var conf struct {
|
|
||||||
Mod struct {
|
|
||||||
Config string `yaml:"config"`
|
|
||||||
} `yaml:"hass"`
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LoadConfig(&conf)
|
|
||||||
|
|
||||||
log = app.GetLogger("hass")
|
|
||||||
|
|
||||||
initAPI()
|
|
||||||
|
|
||||||
// support load cameras from Hass config file
|
|
||||||
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
storage := new(entries)
|
|
||||||
if err = json.Unmarshal(data, storage); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
urls := map[string]string{}
|
|
||||||
|
|
||||||
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
|
|
||||||
if hurl := urls[url[5:]]; hurl != "" {
|
|
||||||
return streams.GetProducer(hurl)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("can't get url: %s", url)
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, entrie := range storage.Data.Entries {
|
|
||||||
switch entrie.Domain {
|
|
||||||
case "generic":
|
|
||||||
if entrie.Options.StreamSource == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
urls[entrie.Title] = entrie.Options.StreamSource
|
|
||||||
|
|
||||||
case "homekit_controller":
|
|
||||||
if entrie.Data.ClientID == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
urls[entrie.Title] = fmt.Sprintf(
|
|
||||||
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
|
|
||||||
entrie.Data.DeviceHost, entrie.Data.DevicePort,
|
|
||||||
entrie.Data.ClientID, entrie.Data.ClientPrivate, entrie.Data.ClientPublic,
|
|
||||||
entrie.Data.DeviceID, entrie.Data.DevicePublic,
|
|
||||||
)
|
|
||||||
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
|
|
||||||
//streams.Get("hass:" + entrie.Title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
type entries struct {
|
|
||||||
Data struct {
|
|
||||||
Entries []struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
Data struct {
|
|
||||||
ClientID string `json:"iOSPairingId"`
|
|
||||||
ClientPrivate string `json:"iOSDeviceLTSK"`
|
|
||||||
ClientPublic string `json:"iOSDeviceLTPK"`
|
|
||||||
DeviceID string `json:"AccessoryPairingID"`
|
|
||||||
DevicePublic string `json:"AccessoryLTPK"`
|
|
||||||
DeviceHost string `json:"AccessoryIP"`
|
|
||||||
DevicePort uint16 `json:"AccessoryPort"`
|
|
||||||
} `json:"data"`
|
|
||||||
Options struct {
|
|
||||||
StreamSource string `json:"stream_source"`
|
|
||||||
}
|
|
||||||
} `json:"entries"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
package homekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit/mdns"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
items := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for name, src := range store.GetDict("streams") {
|
|
||||||
if src := src.(string); strings.HasPrefix(src, "homekit") {
|
|
||||||
u, err := url.Parse(src)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
device := Device{
|
|
||||||
Name: name,
|
|
||||||
Addr: u.Host,
|
|
||||||
Paired: true,
|
|
||||||
}
|
|
||||||
items = append(items, device)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for info := range mdns.GetAll() {
|
|
||||||
if !strings.HasSuffix(info.Name, mdns.Suffix) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name := info.Name[:len(info.Name)-len(mdns.Suffix)]
|
|
||||||
device := Device{
|
|
||||||
Name: strings.ReplaceAll(name, "\\", ""),
|
|
||||||
Addr: fmt.Sprintf("%s:%d", info.AddrV4, info.Port),
|
|
||||||
}
|
|
||||||
for _, field := range info.InfoFields {
|
|
||||||
switch field[:2] {
|
|
||||||
case "id":
|
|
||||||
device.ID = field[3:]
|
|
||||||
case "md":
|
|
||||||
device.Model = field[3:]
|
|
||||||
case "sf":
|
|
||||||
device.Paired = field[3] == '0'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items = append(items, device)
|
|
||||||
}
|
|
||||||
|
|
||||||
_= json.NewEncoder(w).Encode(items)
|
|
||||||
|
|
||||||
case "POST":
|
|
||||||
// TODO: post params...
|
|
||||||
|
|
||||||
id := r.URL.Query().Get("id")
|
|
||||||
pin := r.URL.Query().Get("pin")
|
|
||||||
|
|
||||||
client, err := homekit.Pair(id, pin)
|
|
||||||
if err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] pair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
dict := store.GetDict("streams")
|
|
||||||
dict[name] = client.URL()
|
|
||||||
if err = store.Set("streams", dict); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] save to store")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.New(name, client.URL())
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
dict := store.GetDict("streams")
|
|
||||||
for name, rawURL := range dict {
|
|
||||||
if name != src {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := homekit.NewClient(rawURL.(string))
|
|
||||||
if err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] new client")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Dial(); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] client dial")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go client.Handle()
|
|
||||||
|
|
||||||
if err = client.ListPairings(); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.DeletePairing(client.ClientID); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] unpair")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(dict, name)
|
|
||||||
|
|
||||||
if err = store.Set("streams", dict); err != nil {
|
|
||||||
// log error
|
|
||||||
log.Error().Err(err).Msg("[api.homekit] store set")
|
|
||||||
// response error
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Device struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Addr string `json:"addr"`
|
|
||||||
Model string `json:"model"`
|
|
||||||
Paired bool `json:"paired"`
|
|
||||||
//Type string `json:"type"`
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package homekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
log = app.GetLogger("homekit")
|
|
||||||
|
|
||||||
streams.HandleFunc("homekit", streamHandler)
|
|
||||||
|
|
||||||
api.HandleFunc("api/homekit", apiHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
func streamHandler(url string) (streamer.Producer, error) {
|
|
||||||
client, err := homekit.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = client.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// start gorutine for reading responses from camera
|
|
||||||
go func() {
|
|
||||||
if err = client.Handle(); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[homekit] client")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return &Producer{client: client}, nil
|
|
||||||
}
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
package homekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit/camera"
|
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/srtp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/brutella/hap/characteristic"
|
|
||||||
"github.com/brutella/hap/rtp"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Producer struct {
|
|
||||||
streamer.Element
|
|
||||||
|
|
||||||
client *homekit.Client
|
|
||||||
medias []*streamer.Media
|
|
||||||
tracks []*streamer.Track
|
|
||||||
|
|
||||||
sessions []*pkg.Session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) GetMedias() []*streamer.Media {
|
|
||||||
if c.medias == nil {
|
|
||||||
c.medias = c.getMedias()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.medias
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
|
||||||
for _, track := range c.tracks {
|
|
||||||
if track.Codec == codec {
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
track := &streamer.Track{Codec: codec, Direction: media.Direction}
|
|
||||||
c.tracks = append(c.tracks, track)
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) Start() error {
|
|
||||||
if c.tracks == nil {
|
|
||||||
return errors.New("producer without tracks")
|
|
||||||
}
|
|
||||||
|
|
||||||
// get our server local IP-address
|
|
||||||
host, _, err := net.SplitHostPort(c.client.LocalAddr())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// get our server SRTP port
|
|
||||||
port, err := strconv.Atoi(srtp.Port)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup HomeKit stream session
|
|
||||||
hkSession := camera.NewSession()
|
|
||||||
hkSession.SetLocalEndpoint(host, uint16(port))
|
|
||||||
|
|
||||||
// create client for processing camera accessory
|
|
||||||
cam := camera.NewClient(c.client)
|
|
||||||
// try to start HomeKit stream
|
|
||||||
if err = cam.StartStream2(hkSession); err != nil {
|
|
||||||
panic(err) // TODO: fixme
|
|
||||||
}
|
|
||||||
|
|
||||||
// SRTP Video Session
|
|
||||||
vs := &pkg.Session{
|
|
||||||
LocalSSRC: hkSession.Config.Video.RTP.Ssrc,
|
|
||||||
RemoteSSRC: hkSession.Answer.SsrcVideo,
|
|
||||||
Track: c.tracks[0],
|
|
||||||
}
|
|
||||||
if err = vs.SetKeys(
|
|
||||||
hkSession.Offer.Video.MasterKey, hkSession.Offer.Video.MasterSalt,
|
|
||||||
hkSession.Answer.Video.MasterKey, hkSession.Answer.Video.MasterSalt,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SRTP Audio Session
|
|
||||||
as := &pkg.Session{
|
|
||||||
LocalSSRC: hkSession.Config.Audio.RTP.Ssrc,
|
|
||||||
RemoteSSRC: hkSession.Answer.SsrcAudio,
|
|
||||||
Track: &streamer.Track{},
|
|
||||||
}
|
|
||||||
if err = as.SetKeys(
|
|
||||||
hkSession.Offer.Audio.MasterKey, hkSession.Offer.Audio.MasterSalt,
|
|
||||||
hkSession.Answer.Audio.MasterKey, hkSession.Answer.Audio.MasterSalt,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
srtp.AddSession(vs)
|
|
||||||
srtp.AddSession(as)
|
|
||||||
|
|
||||||
c.sessions = []*pkg.Session{vs, as}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) Stop() error {
|
|
||||||
err := c.client.Close()
|
|
||||||
|
|
||||||
for _, session := range c.sessions {
|
|
||||||
srtp.RemoveSession(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Producer) getMedias() []*streamer.Media {
|
|
||||||
var medias []*streamer.Media
|
|
||||||
|
|
||||||
accs, err := c.client.GetAccessories()
|
|
||||||
acc := accs[0]
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get supported video config (not really necessary)
|
|
||||||
char := acc.GetCharacter(characteristic.TypeSupportedVideoStreamConfiguration)
|
|
||||||
v1 := &rtp.VideoStreamConfiguration{}
|
|
||||||
if err = char.ReadTLV8(v1); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, hkCodec := range v1.Codecs {
|
|
||||||
codec := &streamer.Codec{ClockRate: 90000}
|
|
||||||
|
|
||||||
switch hkCodec.Type {
|
|
||||||
case rtp.VideoCodecType_H264:
|
|
||||||
codec.Name = streamer.CodecH264
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &streamer.Media{
|
|
||||||
Kind: streamer.KindVideo, Direction: streamer.DirectionSendonly,
|
|
||||||
Codecs: []*streamer.Codec{codec},
|
|
||||||
}
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
char = acc.GetCharacter(characteristic.TypeSupportedAudioStreamConfiguration)
|
|
||||||
v2 := &rtp.AudioStreamConfiguration{}
|
|
||||||
if err = char.ReadTLV8(v2); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, hkCodec := range v2.Codecs {
|
|
||||||
codec := &streamer.Codec{
|
|
||||||
Channels: uint16(hkCodec.Parameters.Channels),
|
|
||||||
}
|
|
||||||
|
|
||||||
switch hkCodec.Type {
|
|
||||||
case rtp.AudioCodecType_AAC_ELD:
|
|
||||||
codec.Name = streamer.CodecAAC
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown codec: %d", hkCodec.Type))
|
|
||||||
}
|
|
||||||
|
|
||||||
switch hkCodec.Parameters.Samplerate {
|
|
||||||
case rtp.AudioCodecSampleRate8Khz:
|
|
||||||
codec.ClockRate = 8000
|
|
||||||
case rtp.AudioCodecSampleRate16Khz:
|
|
||||||
codec.ClockRate = 16000
|
|
||||||
case rtp.AudioCodecSampleRate24Khz:
|
|
||||||
codec.ClockRate = 24000
|
|
||||||
default:
|
|
||||||
panic(fmt.Sprintf("unknown clockrate: %d", hkCodec.Parameters.Samplerate))
|
|
||||||
}
|
|
||||||
|
|
||||||
media := &streamer.Media{
|
|
||||||
Kind: streamer.KindAudio, Direction: streamer.DirectionSendonly,
|
|
||||||
Codecs: []*streamer.Codec{codec},
|
|
||||||
}
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
return medias
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package ivideon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("ivideon", func(url string) (streamer.Producer, error) {
|
|
||||||
id := strings.Replace(url[8:], "/", ":", 1)
|
|
||||||
prod := ivideon.NewClient(id)
|
|
||||||
if err := prod.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return prod, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package mjpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
api.HandleFunc("api/stream.mjpeg", handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
|
||||||
|
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := make(chan struct{})
|
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{}
|
|
||||||
cons.Listen(func(msg interface{}) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case []byte:
|
|
||||||
data := []byte(header + strconv.Itoa(len(msg)))
|
|
||||||
data = append(data, 0x0D, 0x0A, 0x0D, 0x0A)
|
|
||||||
data = append(data, msg...)
|
|
||||||
data = append(data, 0x0D, 0x0A)
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
exit <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", `multipart/x-mixed-replace; boundary=frame`)
|
|
||||||
|
|
||||||
<-exit
|
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
//log.Trace().Msg("[api.mjpeg] close")
|
|
||||||
}
|
|
||||||
-138
@@ -1,138 +0,0 @@
|
|||||||
package mp4
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
log = app.GetLogger("mp4")
|
|
||||||
|
|
||||||
api.HandleWS(MsgTypeMSE, handlerWS)
|
|
||||||
|
|
||||||
api.HandleFunc("api/frame.mp4", handlerKeyframe)
|
|
||||||
api.HandleFunc("api/stream.mp4", handlerMP4)
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if isChromeFirst(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := make(chan []byte)
|
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
|
||||||
cons.Listen(func(msg interface{}) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case []byte:
|
|
||||||
exit <- msg
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.keyframe] add consumer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType())
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.keyframe] init")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data = append(data, <-exit...)
|
|
||||||
|
|
||||||
// Apple Safari won't show frame without length
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.keyframe] add consumer")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if isChromeFirst(w, r) || isSafari(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Trace().Msgf("[api.mp4] %+v", r)
|
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := make(chan struct{})
|
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
|
||||||
cons.Listen(func(msg interface{}) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case []byte:
|
|
||||||
if _, err := w.Write(msg); err != nil {
|
|
||||||
exit <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.mp4] add consumer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType())
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.mp4] init")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.mp4] write")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
<-exit
|
|
||||||
|
|
||||||
log.Trace().Msg("[api.mp4] close")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isChromeFirst(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
|
|
||||||
if strings.Contains(r.UserAgent(), " Chrome/") {
|
|
||||||
if r.Header.Values("Range") == nil {
|
|
||||||
w.Header().Set("Content-Type", "video/mp4")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSafari(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
if r.Header.Get("Range") == "bytes=0-1" {
|
|
||||||
handlerKeyframe(w, r)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
package mp4
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
)
|
|
||||||
|
|
||||||
const MsgTypeMSE = "mse" // fMP4
|
|
||||||
|
|
||||||
func handlerWS(ctx *api.Context, msg *streamer.Message) {
|
|
||||||
src := ctx.Request.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cons := &mp4.Consumer{}
|
|
||||||
cons.UserAgent = ctx.Request.UserAgent()
|
|
||||||
cons.RemoteAddr = ctx.Request.RemoteAddr
|
|
||||||
|
|
||||||
cons.Listen(func(msg interface{}) {
|
|
||||||
switch msg.(type) {
|
|
||||||
case *streamer.Message, []byte:
|
|
||||||
ctx.Write(msg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[api.mse] add consumer")
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.OnClose(func() {
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{
|
|
||||||
Type: MsgTypeMSE, Value: cons.MimeType(),
|
|
||||||
})
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[api.mse] init")
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Write(data)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package rtmp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("rtmp", handle)
|
|
||||||
// RTMPT (flv over HTTP)
|
|
||||||
streams.HandleFunc("http", handle)
|
|
||||||
streams.HandleFunc("https", handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(url string) (streamer.Producer, error) {
|
|
||||||
conn := rtmp.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
package rtsp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var conf struct {
|
|
||||||
Mod struct {
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
} `yaml:"rtsp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// default config
|
|
||||||
conf.Mod.Listen = ":8554"
|
|
||||||
|
|
||||||
app.LoadConfig(&conf)
|
|
||||||
|
|
||||||
log = app.GetLogger("rtsp")
|
|
||||||
|
|
||||||
// RTSP client support
|
|
||||||
streams.HandleFunc("rtsp", rtspHandler)
|
|
||||||
streams.HandleFunc("rtsps", rtspHandler)
|
|
||||||
streams.HandleFunc("rtspx", rtspHandler)
|
|
||||||
|
|
||||||
// RTSP server support
|
|
||||||
address := conf.Mod.Listen
|
|
||||||
if address != "" {
|
|
||||||
_, Port, _ = net.SplitHostPort(address)
|
|
||||||
|
|
||||||
go worker(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var Port string
|
|
||||||
|
|
||||||
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
|
|
||||||
|
|
||||||
// internal
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
func rtspHandler(url string) (streamer.Producer, error) {
|
|
||||||
backchannel := true
|
|
||||||
|
|
||||||
if i := strings.IndexByte(url, '#'); i > 0 {
|
|
||||||
if url[i+1:] == "backchannel=0" {
|
|
||||||
backchannel = false
|
|
||||||
}
|
|
||||||
url = url[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := rtsp.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
conn.Listen(func(msg interface{}) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *tcp.Request:
|
|
||||||
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
|
|
||||||
case *tcp.Response:
|
|
||||||
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Backchannel = backchannel
|
|
||||||
if err = conn.Describe(); err != nil {
|
|
||||||
if !backchannel {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// second try without backchannel, we need to reconnect
|
|
||||||
conn.Backchannel = false
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = conn.Describe(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func worker(address string) {
|
|
||||||
srv, err := tcp.NewServer(address)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[rtsp] listen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", address).Msg("[rtsp] listen")
|
|
||||||
|
|
||||||
srv.Listen(func(msg interface{}) {
|
|
||||||
switch msg.(type) {
|
|
||||||
case net.Conn:
|
|
||||||
var name string
|
|
||||||
var onDisconnect func()
|
|
||||||
|
|
||||||
trace := log.Trace().Enabled()
|
|
||||||
|
|
||||||
conn := rtsp.NewServer(msg.(net.Conn))
|
|
||||||
conn.Listen(func(msg interface{}) {
|
|
||||||
if trace {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *tcp.Request:
|
|
||||||
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
|
|
||||||
case *tcp.Response:
|
|
||||||
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg {
|
|
||||||
case rtsp.MethodDescribe:
|
|
||||||
name = conn.URL.Path[1:]
|
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
|
|
||||||
|
|
||||||
stream := streams.Get(name) // TODO: rewrite
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initMedias(conn)
|
|
||||||
|
|
||||||
if err = stream.AddConsumer(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
onDisconnect = func() {
|
|
||||||
stream.RemoveConsumer(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
case rtsp.MethodAnnounce:
|
|
||||||
if OnProducer != nil {
|
|
||||||
if OnProducer(conn) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
name = conn.URL.Path[1:]
|
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
|
|
||||||
|
|
||||||
stream := streams.Get(name)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddProducer(conn)
|
|
||||||
|
|
||||||
onDisconnect = func() {
|
|
||||||
stream.RemoveProducer(conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
case streamer.StatePlaying:
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] start")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = conn.Accept(); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[rtsp] accept")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.Handle(); err != nil {
|
|
||||||
//log.Warn().Err(err).Msg("[rtsp] handle server")
|
|
||||||
}
|
|
||||||
|
|
||||||
if onDisconnect != nil {
|
|
||||||
onDisconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
srv.Serve()
|
|
||||||
}
|
|
||||||
|
|
||||||
func initMedias(conn *rtsp.Conn) {
|
|
||||||
// set media candidates from query list
|
|
||||||
for key, value := range conn.URL.Query() {
|
|
||||||
switch key {
|
|
||||||
case streamer.KindVideo, streamer.KindAudio:
|
|
||||||
for _, value := range value {
|
|
||||||
media := &streamer.Media{
|
|
||||||
Kind: key, Direction: streamer.DirectionRecvonly,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch value {
|
|
||||||
case "", "copy": // pass empty codecs list
|
|
||||||
default:
|
|
||||||
codec := streamer.NewCodec(value)
|
|
||||||
media.Codecs = append(media.Codecs, codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Medias = append(conn.Medias, media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set default media candidates if query is empty
|
|
||||||
if conn.Medias == nil {
|
|
||||||
conn.Medias = []*streamer.Media{
|
|
||||||
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
|
|
||||||
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package srtp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod struct {
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
} `yaml:"srtp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// default config
|
|
||||||
cfg.Mod.Listen = ":8443"
|
|
||||||
|
|
||||||
// load config from YAML
|
|
||||||
app.LoadConfig(&cfg)
|
|
||||||
|
|
||||||
if cfg.Mod.Listen == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log = app.GetLogger("srtp")
|
|
||||||
|
|
||||||
// create SRTP server (endpoint) for receiving video from HomeKit camera
|
|
||||||
conn, err := net.ListenPacket("udp", cfg.Mod.Listen)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[srtp] listen")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
|
|
||||||
|
|
||||||
_, Port, _ = net.SplitHostPort(cfg.Mod.Listen)
|
|
||||||
|
|
||||||
// run server
|
|
||||||
go func() {
|
|
||||||
server = &srtp.Server{}
|
|
||||||
if err = server.Serve(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[srtp] serve")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
var server *srtp.Server
|
|
||||||
|
|
||||||
var Port string
|
|
||||||
|
|
||||||
func AddSession(session *srtp.Session) {
|
|
||||||
server.AddSession(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveSession(session *srtp.Session) {
|
|
||||||
server.RemoveSession(session)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Handler func(url string) (streamer.Producer, error)
|
|
||||||
|
|
||||||
var handlers map[string]Handler
|
|
||||||
|
|
||||||
func HandleFunc(scheme string, handler Handler) {
|
|
||||||
if handlers == nil {
|
|
||||||
handlers = make(map[string]Handler)
|
|
||||||
}
|
|
||||||
handlers[scheme] = handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func HasProducer(url string) bool {
|
|
||||||
i := strings.IndexByte(url, ':')
|
|
||||||
if i <= 0 { // TODO: i < 4 ?
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return handlers[url[:i]] != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetProducer(url string) (streamer.Producer, error) {
|
|
||||||
i := strings.IndexByte(url, ':')
|
|
||||||
handler := handlers[url[:i]]
|
|
||||||
if handler == nil {
|
|
||||||
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
|
||||||
}
|
|
||||||
return handler(url)
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type state byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
stateNone state = iota
|
|
||||||
stateMedias
|
|
||||||
stateTracks
|
|
||||||
stateStart
|
|
||||||
)
|
|
||||||
|
|
||||||
type Producer struct {
|
|
||||||
streamer.Element
|
|
||||||
|
|
||||||
url string
|
|
||||||
template string
|
|
||||||
|
|
||||||
element streamer.Producer
|
|
||||||
tracks []*streamer.Track
|
|
||||||
|
|
||||||
state state
|
|
||||||
mx sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Producer) SetSource(s string) {
|
|
||||||
if p.template == "" {
|
|
||||||
p.template = p.url
|
|
||||||
}
|
|
||||||
p.url = strings.Replace(p.template, "{input}", s, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Producer) GetMedias() []*streamer.Media {
|
|
||||||
p.mx.Lock()
|
|
||||||
defer p.mx.Unlock()
|
|
||||||
|
|
||||||
if p.state == stateNone {
|
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
p.element, err = GetProducer(p.url)
|
|
||||||
if err != nil || p.element == nil {
|
|
||||||
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.state = stateMedias
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.element.GetMedias()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
|
|
||||||
p.mx.Lock()
|
|
||||||
defer p.mx.Unlock()
|
|
||||||
|
|
||||||
if p.state == stateMedias {
|
|
||||||
p.state = stateTracks
|
|
||||||
}
|
|
||||||
|
|
||||||
track := p.element.GetTrack(media, codec)
|
|
||||||
if track == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, t := range p.tracks {
|
|
||||||
if track == t {
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p.tracks = append(p.tracks, track)
|
|
||||||
|
|
||||||
return track
|
|
||||||
}
|
|
||||||
|
|
||||||
// internals
|
|
||||||
|
|
||||||
func (p *Producer) start() {
|
|
||||||
p.mx.Lock()
|
|
||||||
defer p.mx.Unlock()
|
|
||||||
|
|
||||||
if p.state != stateTracks {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] start producer")
|
|
||||||
|
|
||||||
p.state = stateStart
|
|
||||||
go func() {
|
|
||||||
if err := p.element.Start(); err != nil {
|
|
||||||
log.Warn().Err(err).Str("url", p.url).Msg("[streams] start")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Producer) stop() {
|
|
||||||
p.mx.Lock()
|
|
||||||
|
|
||||||
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
|
|
||||||
|
|
||||||
if p.element != nil {
|
|
||||||
_ = p.element.Stop()
|
|
||||||
p.element = nil
|
|
||||||
} else {
|
|
||||||
log.Warn().Str("url", p.url).Msg("[streams] stop empty producer")
|
|
||||||
}
|
|
||||||
p.tracks = nil
|
|
||||||
p.state = stateNone
|
|
||||||
|
|
||||||
p.mx.Unlock()
|
|
||||||
}
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Consumer struct {
|
|
||||||
element streamer.Consumer
|
|
||||||
tracks []*streamer.Track
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stream struct {
|
|
||||||
producers []*Producer
|
|
||||||
consumers []*Consumer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStream(source interface{}) *Stream {
|
|
||||||
switch source := source.(type) {
|
|
||||||
case string:
|
|
||||||
s := new(Stream)
|
|
||||||
prod := &Producer{url: source}
|
|
||||||
s.producers = append(s.producers, prod)
|
|
||||||
return s
|
|
||||||
case []interface{}:
|
|
||||||
s := new(Stream)
|
|
||||||
for _, source := range source {
|
|
||||||
prod := &Producer{url: source.(string)}
|
|
||||||
s.producers = append(s.producers, prod)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
case *Stream:
|
|
||||||
return source
|
|
||||||
case map[string]interface{}:
|
|
||||||
return NewStream(source["url"])
|
|
||||||
case nil:
|
|
||||||
return new(Stream)
|
|
||||||
default:
|
|
||||||
panic("wrong source type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) SetSource(source string) {
|
|
||||||
for _, prod := range s.producers {
|
|
||||||
prod.SetSource(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
|
|
||||||
ic := len(s.consumers)
|
|
||||||
|
|
||||||
consumer := &Consumer{element: cons}
|
|
||||||
|
|
||||||
// Step 1. Get consumer medias
|
|
||||||
for icc, consMedia := range cons.GetMedias() {
|
|
||||||
log.Trace().Stringer("media", consMedia).
|
|
||||||
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
|
|
||||||
|
|
||||||
producers:
|
|
||||||
for ip, prod := range s.producers {
|
|
||||||
// Step 2. Get producer medias (not tracks yet)
|
|
||||||
for ipc, prodMedia := range prod.GetMedias() {
|
|
||||||
log.Trace().Stringer("media", prodMedia).
|
|
||||||
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
|
|
||||||
|
|
||||||
// Step 3. Match consumer/producer codecs list
|
|
||||||
prodCodec := prodMedia.MatchMedia(consMedia)
|
|
||||||
if prodCodec != nil {
|
|
||||||
log.Trace().Stringer("codec", prodCodec).
|
|
||||||
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
|
|
||||||
|
|
||||||
// Step 4. Get producer track
|
|
||||||
prodTrack := prod.GetTrack(prodMedia, prodCodec)
|
|
||||||
if prodTrack == nil {
|
|
||||||
log.Warn().Msg("[stream] can't get track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 5. Add track to consumer and get new track
|
|
||||||
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
|
|
||||||
|
|
||||||
consumer.tracks = append(consumer.tracks, consTrack)
|
|
||||||
break producers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// can't match tracks for consumer
|
|
||||||
if len(consumer.tracks) == 0 {
|
|
||||||
return errors.New("couldn't find the matching tracks")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.consumers = append(s.consumers, consumer)
|
|
||||||
|
|
||||||
for _, prod := range s.producers {
|
|
||||||
prod.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
|
|
||||||
for i, consumer := range s.consumers {
|
|
||||||
if consumer == nil {
|
|
||||||
log.Warn().Msgf("empty consumer: %+v\n", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if consumer.element == cons {
|
|
||||||
// remove consumer pads from all producers
|
|
||||||
for _, track := range consumer.tracks {
|
|
||||||
track.Unbind()
|
|
||||||
}
|
|
||||||
// remove consumer from slice
|
|
||||||
s.removeConsumer(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, producer := range s.producers {
|
|
||||||
if producer == nil {
|
|
||||||
log.Warn().Msgf("empty producer: %+v\n", s)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var sink bool
|
|
||||||
for _, track := range producer.tracks {
|
|
||||||
if len(track.Sink) > 0 {
|
|
||||||
sink = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !sink {
|
|
||||||
producer.stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) AddProducer(prod streamer.Producer) {
|
|
||||||
producer := &Producer{element: prod, state: stateTracks}
|
|
||||||
s.producers = append(s.producers, producer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) RemoveProducer(prod streamer.Producer) {
|
|
||||||
for i, producer := range s.producers {
|
|
||||||
if producer.element == prod {
|
|
||||||
s.removeProducer(i)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) Active() bool {
|
|
||||||
if len(s.consumers) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, prod := range s.producers {
|
|
||||||
if prod.element != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
|
||||||
var v []interface{}
|
|
||||||
for _, prod := range s.producers {
|
|
||||||
if prod.element != nil {
|
|
||||||
v = append(v, prod.element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, cons := range s.consumers {
|
|
||||||
// cons.element always not nil
|
|
||||||
v = append(v, cons.element)
|
|
||||||
}
|
|
||||||
if len(v) == 0 {
|
|
||||||
v = nil
|
|
||||||
}
|
|
||||||
return json.Marshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) removeConsumer(i int) {
|
|
||||||
switch {
|
|
||||||
case len(s.consumers) == 1: // only one element
|
|
||||||
s.consumers = nil
|
|
||||||
case i == 0: // first element
|
|
||||||
s.consumers = s.consumers[1:]
|
|
||||||
case i == len(s.consumers)-1: // last element
|
|
||||||
s.consumers = s.consumers[:i]
|
|
||||||
default: // middle element
|
|
||||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) removeProducer(i int) {
|
|
||||||
switch {
|
|
||||||
case len(s.producers) == 1: // only one element
|
|
||||||
s.producers = nil
|
|
||||||
case i == 0: // first element
|
|
||||||
s.producers = s.producers[1:]
|
|
||||||
case i == len(s.producers)-1: // last element
|
|
||||||
s.producers = s.producers[:i]
|
|
||||||
default: // middle element
|
|
||||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/fake"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Google Chrome 104.0.5112.79
|
|
||||||
const chrome = `v=0
|
|
||||||
o=- 0 0 IN IP4 0.0.0.0
|
|
||||||
s=-
|
|
||||||
t=0 0
|
|
||||||
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 110 112 113 126
|
|
||||||
a=sendrecv
|
|
||||||
a=rtpmap:111 opus/48000/2
|
|
||||||
a=rtpmap:63 red/48000/2
|
|
||||||
a=rtpmap:103 ISAC/16000
|
|
||||||
a=rtpmap:104 ISAC/32000
|
|
||||||
a=rtpmap:9 G722/8000
|
|
||||||
a=rtpmap:0 PCMU/8000
|
|
||||||
a=rtpmap:8 PCMA/8000
|
|
||||||
a=rtpmap:110 telephone-event/48000
|
|
||||||
a=rtpmap:112 telephone-event/32000
|
|
||||||
a=rtpmap:113 telephone-event/16000
|
|
||||||
a=rtpmap:126 telephone-event/8000
|
|
||||||
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 35 36 37 38 39 40 41 42 114 115 116 117 118 43
|
|
||||||
a=recvonly
|
|
||||||
a=rtpmap:96 VP8/90000
|
|
||||||
a=rtpmap:97 rtx/90000
|
|
||||||
a=rtpmap:98 VP9/90000
|
|
||||||
a=rtpmap:99 rtx/90000
|
|
||||||
a=rtpmap:100 VP9/90000
|
|
||||||
a=rtpmap:101 rtx/90000
|
|
||||||
a=rtpmap:102 VP9/90000
|
|
||||||
a=rtpmap:122 rtx/90000
|
|
||||||
a=rtpmap:127 H264/90000
|
|
||||||
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
|
|
||||||
a=rtpmap:121 rtx/90000
|
|
||||||
a=rtpmap:125 H264/90000
|
|
||||||
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
|
|
||||||
a=rtpmap:107 rtx/90000
|
|
||||||
a=rtpmap:108 H264/90000
|
|
||||||
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
|
||||||
a=rtpmap:109 rtx/90000
|
|
||||||
a=rtpmap:124 H264/90000
|
|
||||||
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
|
|
||||||
a=rtpmap:120 rtx/90000
|
|
||||||
a=rtpmap:123 H264/90000
|
|
||||||
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
|
|
||||||
a=rtpmap:119 rtx/90000
|
|
||||||
a=rtpmap:35 H264/90000
|
|
||||||
a=fmtp:35 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
|
|
||||||
a=rtpmap:36 rtx/90000
|
|
||||||
a=rtpmap:37 H264/90000
|
|
||||||
a=fmtp:37 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=f4001f
|
|
||||||
a=rtpmap:38 rtx/90000
|
|
||||||
a=rtpmap:39 H264/90000
|
|
||||||
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=f4001f
|
|
||||||
a=rtpmap:40 rtx/90000
|
|
||||||
a=rtpmap:41 AV1/90000
|
|
||||||
a=rtpmap:42 rtx/90000
|
|
||||||
a=rtpmap:114 H264/90000
|
|
||||||
a=fmtp:114 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
|
|
||||||
a=rtpmap:115 rtx/90000
|
|
||||||
a=rtpmap:116 red/90000
|
|
||||||
a=rtpmap:117 rtx/90000
|
|
||||||
a=rtpmap:118 ulpfec/90000
|
|
||||||
a=rtpmap:43 flexfec-03/90000
|
|
||||||
`
|
|
||||||
|
|
||||||
const dahuaSimple = `v=0
|
|
||||||
o=- 0 0 IN IP4 0.0.0.0
|
|
||||||
s=-
|
|
||||||
t=0 0
|
|
||||||
m=video 0 RTP/AVP 96
|
|
||||||
a=control:trackID=0
|
|
||||||
a=rtpmap:96 H264/90000
|
|
||||||
a=fmtp:96 packetization-mode=1;profile-level-id=42401E;sprop-parameter-sets=Z0JAHqaAoD2QAA==,aM48gAA=
|
|
||||||
a=recvonly
|
|
||||||
m=audio 0 RTP/AVP 97
|
|
||||||
a=control:trackID=1
|
|
||||||
a=rtpmap:97 MPEG4-GENERIC/16000
|
|
||||||
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
|
|
||||||
a=recvonly
|
|
||||||
m=audio 0 RTP/AVP 8
|
|
||||||
a=control:trackID=5
|
|
||||||
a=rtpmap:8 PCMA/8000
|
|
||||||
a=sendonly
|
|
||||||
`
|
|
||||||
|
|
||||||
const ffmpegPCMU48000 = `v=0
|
|
||||||
o=- 0 0 IN IP4 127.0.0.1
|
|
||||||
s=-
|
|
||||||
t=0 0
|
|
||||||
m=audio 0 RTP/AVP 96
|
|
||||||
b=AS:384
|
|
||||||
a=rtpmap:96 PCMU/48000/1
|
|
||||||
a=control:streamid=0
|
|
||||||
`
|
|
||||||
|
|
||||||
func TestRouting(t *testing.T) {
|
|
||||||
prod := &fake.Producer{}
|
|
||||||
prod.Medias, _ = rtsp.UnmarshalSDP([]byte(dahuaSimple))
|
|
||||||
assert.Len(t, prod.Medias, 3)
|
|
||||||
|
|
||||||
HandleFunc("fake", func(url string) (streamer.Producer, error) {
|
|
||||||
return prod, nil
|
|
||||||
})
|
|
||||||
|
|
||||||
cons := &fake.Consumer{}
|
|
||||||
cons.Medias, _ = streamer.UnmarshalSDP([]byte(chrome))
|
|
||||||
assert.Len(t, cons.Medias, 3)
|
|
||||||
|
|
||||||
// setup stream with one producer
|
|
||||||
stream := NewStream("fake:")
|
|
||||||
|
|
||||||
// main check:
|
|
||||||
err := stream.AddConsumer(cons)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
assert.Len(t, prod.Tracks, 2)
|
|
||||||
assert.Len(t, cons.Tracks, 2)
|
|
||||||
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
assert.Greater(t, prod.SendPackets,0)
|
|
||||||
assert.Greater(t, cons.RecvPackets,0)
|
|
||||||
|
|
||||||
assert.Greater(t, prod.RecvPackets,0)
|
|
||||||
assert.Greater(t, cons.SendPackets,0)
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]interface{} `yaml:"streams"`
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
|
||||||
|
|
||||||
log = app.GetLogger("streams")
|
|
||||||
|
|
||||||
for name, item := range cfg.Mod {
|
|
||||||
streams[name] = NewStream(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, item := range store.GetDict("streams") {
|
|
||||||
streams[name] = NewStream(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get(name string) *Stream {
|
|
||||||
return streams[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name string, source interface{}) *Stream {
|
|
||||||
stream := NewStream(source)
|
|
||||||
streams[name] = stream
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetOrNew(src string) *Stream {
|
|
||||||
if stream, ok := streams[src]; ok {
|
|
||||||
return stream
|
|
||||||
}
|
|
||||||
|
|
||||||
if !HasProducer(src) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("url", src).Msg("[streams] create new stream")
|
|
||||||
|
|
||||||
return New(src, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(name string) {
|
|
||||||
delete(streams, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func All() map[string]interface{} {
|
|
||||||
all := map[string]interface{}{}
|
|
||||||
for name, stream := range streams {
|
|
||||||
all[name] = stream
|
|
||||||
//if stream.Active() {
|
|
||||||
// all[name] = stream
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
return all
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
var streams = map[string]*Stream{}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package webrtc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
|
||||||
"github.com/pion/sdp/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var candidates []string
|
|
||||||
|
|
||||||
func AddCandidate(address string) {
|
|
||||||
candidates = append(candidates, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCanditates(answer string) (string, error) {
|
|
||||||
if len(candidates) == 0 {
|
|
||||||
return answer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sd := &sdp.SessionDescription{}
|
|
||||||
if err := sd.Unmarshal([]byte(answer)); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
md := sd.MediaDescriptions[0]
|
|
||||||
|
|
||||||
_, end := md.Attribute("end-of-candidates")
|
|
||||||
if end {
|
|
||||||
md.Attributes = md.Attributes[:len(md.Attributes)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, address := range candidates {
|
|
||||||
var err error
|
|
||||||
address, err = webrtc.LookupIP(address)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
cand, err := webrtc.NewCandidate(address)
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[webrtc] candidate")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
md.WithPropertyAttribute(cand)
|
|
||||||
}
|
|
||||||
|
|
||||||
if end {
|
|
||||||
md.WithPropertyAttribute("end-of-candidates")
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := sd.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
|
|
||||||
if ctx.Consumer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
|
|
||||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] remote")
|
|
||||||
conn.Push(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
package webrtc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/streamer"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
|
||||||
pion "github.com/pion/webrtc/v3"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod struct {
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
Candidates []string `yaml:"candidates"`
|
|
||||||
IceServers []pion.ICEServer `yaml:"ice_servers"`
|
|
||||||
} `yaml:"webrtc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Mod.IceServers = []pion.ICEServer{
|
|
||||||
{URLs: []string{"stun:stun.l.google.com:19302"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
|
||||||
|
|
||||||
log = app.GetLogger("webrtc")
|
|
||||||
|
|
||||||
address := cfg.Mod.Listen
|
|
||||||
pionAPI, err := webrtc.NewAPI(address)
|
|
||||||
if pionAPI == nil {
|
|
||||||
log.Error().Err(err).Msg("[webrtc] init API")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[webrtc] listen")
|
|
||||||
} else if address != "" {
|
|
||||||
log.Info().Str("addr", address).Msg("[webrtc] listen")
|
|
||||||
_, Port, _ = net.SplitHostPort(address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pionConf := pion.Configuration{
|
|
||||||
ICEServers: cfg.Mod.IceServers,
|
|
||||||
SDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,
|
|
||||||
}
|
|
||||||
|
|
||||||
NewPConn = func() (*pion.PeerConnection, error) {
|
|
||||||
return pionAPI.NewPeerConnection(pionConf)
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates = cfg.Mod.Candidates
|
|
||||||
|
|
||||||
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
|
|
||||||
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
var Port string
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
var NewPConn func() (*pion.PeerConnection, error)
|
|
||||||
|
|
||||||
func offerHandler(ctx *api.Context, msg *streamer.Message) {
|
|
||||||
src := ctx.Request.URL.Query().Get("src")
|
|
||||||
stream := streams.Get(src)
|
|
||||||
if stream == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug().Str("url", src).Msg("[webrtc] new consumer")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// create new webrtc instance
|
|
||||||
conn := new(webrtc.Conn)
|
|
||||||
conn.Conn, err = NewPConn()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[webrtc] new conn")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.UserAgent = ctx.Request.UserAgent()
|
|
||||||
conn.Listen(func(msg interface{}) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case pion.PeerConnectionState:
|
|
||||||
if msg == pion.PeerConnectionStateClosed {
|
|
||||||
stream.RemoveConsumer(conn)
|
|
||||||
}
|
|
||||||
case *streamer.Message:
|
|
||||||
// subscribe on webrtc server candidates
|
|
||||||
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local")
|
|
||||||
ctx.Write(msg)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 1. SetOffer, so we can get remote client codecs
|
|
||||||
offer := msg.Value.(string)
|
|
||||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
|
||||||
|
|
||||||
if err = conn.SetOffer(offer); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[api.webrtc] set offer")
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. AddConsumer, so we get new tracks
|
|
||||||
if err = stream.AddConsumer(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
|
||||||
_ = conn.Conn.Close()
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Init()
|
|
||||||
|
|
||||||
// exchange sdp without waiting all candidates
|
|
||||||
//answer, err := conn.ExchangeSDP(offer, false)
|
|
||||||
//answer, err := conn.GetAnswer()
|
|
||||||
answer, err := conn.GetCompleteAnswer()
|
|
||||||
if err == nil {
|
|
||||||
answer, err = addCanditates(answer)
|
|
||||||
}
|
|
||||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[webrtc] get answer")
|
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Write(&streamer.Message{
|
|
||||||
Type: webrtc.MsgTypeAnswer, Value: answer,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx.Consumer = conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExchangeSDP(
|
|
||||||
stream *streams.Stream, offer string, userAgent string,
|
|
||||||
) (answer string, err error) {
|
|
||||||
// create new webrtc instance
|
|
||||||
conn := new(webrtc.Conn)
|
|
||||||
conn.Conn, err = NewPConn()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[webrtc] new conn")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.UserAgent = userAgent
|
|
||||||
conn.Listen(func(msg interface{}) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case pion.PeerConnectionState:
|
|
||||||
if msg == pion.PeerConnectionStateClosed {
|
|
||||||
stream.RemoveConsumer(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 1. SetOffer, so we can get remote client codecs
|
|
||||||
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
|
|
||||||
|
|
||||||
if err = conn.SetOffer(offer); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[api.webrtc] set offer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. AddConsumer, so we get new tracks
|
|
||||||
if err = stream.AddConsumer(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
|
|
||||||
_ = conn.Conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.Init()
|
|
||||||
|
|
||||||
// exchange sdp without waiting all candidates
|
|
||||||
//answer, err := conn.ExchangeSDP(offer, false)
|
|
||||||
answer, err = conn.GetCompleteAnswer()
|
|
||||||
if err == nil {
|
|
||||||
answer, err = addCanditates(answer)
|
|
||||||
}
|
|
||||||
log.Trace().Msgf("[webrtc] answer\n%s", answer)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("[webrtc] get answer")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# syntax=docker/dockerfile:labs
|
||||||
|
|
||||||
|
# 0. Prepare images
|
||||||
|
ARG PYTHON_VERSION="3.13"
|
||||||
|
ARG GO_VERSION="1.25"
|
||||||
|
|
||||||
|
|
||||||
|
# 1. Build go2rtc binary
|
||||||
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
ENV GOOS=${TARGETOS}
|
||||||
|
ENV GOARCH=${TARGETARCH}
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
|
# 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}-alpine AS base
|
||||||
|
|
||||||
|
# Install ffmpeg, tini (for signal handling),
|
||||||
|
# and other common tools for the echo source.
|
||||||
|
# alsa-plugins-pulse for ALSA support (+0MB)
|
||||||
|
# font-droid for FFmpeg drawtext filter (+2MB)
|
||||||
|
RUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid
|
||||||
|
|
||||||
|
# Hardware Acceleration for Intel CPU (+50MB)
|
||||||
|
ARG TARGETARCH
|
||||||
|
|
||||||
|
RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi
|
||||||
|
|
||||||
|
# Hardware: AMD and NVidia VAAPI (not sure about this)
|
||||||
|
# RUN libva-glx mesa-va-gallium
|
||||||
|
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||||
|
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||||
|
|
||||||
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 1984 8554 8555 8555/udp
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
|
VOLUME /config
|
||||||
|
WORKDIR /config
|
||||||
|
|
||||||
|
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# syntax=docker/dockerfile:labs
|
||||||
|
|
||||||
|
# 0. Prepare images
|
||||||
|
# only debian 13 (trixie) has latest ffmpeg
|
||||||
|
# https://packages.debian.org/trixie/ffmpeg
|
||||||
|
ARG DEBIAN_VERSION="trixie-slim"
|
||||||
|
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 debian:${DEBIAN_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.
|
||||||
|
# non-free for Intel QSV support (not used by go2rtc, just for tests)
|
||||||
|
# mesa-va-drivers for AMD APU
|
||||||
|
# libasound2-plugins for ALSA support
|
||||||
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
|
echo 'deb http://deb.debian.org/debian trixie non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
||||||
|
apt-get -y update && apt-get -y install ffmpeg tini \
|
||||||
|
python3 curl jq \
|
||||||
|
intel-media-va-driver-non-free \
|
||||||
|
mesa-va-drivers \
|
||||||
|
libasound2-plugins && \
|
||||||
|
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 1984 8554 8555 8555/udp
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||||
|
VOLUME /config
|
||||||
|
WORKDIR /config
|
||||||
|
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
||||||
|
ENV NVIDIA_VISIBLE_DEVICES all
|
||||||
|
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
|
||||||
|
|
||||||
|
CMD ["go2rtc", "-config", "/config/go2rtc.yaml"]
|
||||||
@@ -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,20 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/hass"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.Init()
|
||||||
|
streams.Init()
|
||||||
|
|
||||||
|
api.Init()
|
||||||
|
|
||||||
|
hass.Init()
|
||||||
|
|
||||||
|
shell.RunUntilSignal()
|
||||||
|
}
|
||||||
@@ -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,17 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.Init()
|
||||||
|
streams.Init()
|
||||||
|
|
||||||
|
rtsp.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtsp"
|
|
||||||
"github.com/pion/rtp"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
client, err := rtsp.NewClient(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Dial(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err = client.Describe(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, media := range client.GetMedias() {
|
|
||||||
fmt.Printf("Media: %v\n", media)
|
|
||||||
|
|
||||||
if media.AV() {
|
|
||||||
track := client.GetTrack(media, media.Codecs[0])
|
|
||||||
fmt.Printf("Track: %v, %v\n", track, track.Codec)
|
|
||||||
|
|
||||||
track.Bind(func(packet *rtp.Packet) error {
|
|
||||||
nalUnitType := packet.Payload[0] & 0x1F
|
|
||||||
fmt.Printf(
|
|
||||||
"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
|
|
||||||
track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
|
|
||||||
packet.PayloadType, packet.SSRC,
|
|
||||||
)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Play(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.AfterFunc(time.Second*5, func() {
|
|
||||||
if err = client.Close(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err = client.Handle(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("The End")
|
|
||||||
}
|
|
||||||
@@ -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.17
|
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/hashicorp/mdns v1.0.5
|
github.com/google/uuid v1.6.0
|
||||||
github.com/pion/ice/v2 v2.2.6
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/pion/interceptor v0.1.11
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/pion/rtcp v1.2.9
|
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.5
|
github.com/pion/ice/v4 v4.2.0
|
||||||
github.com/pion/srtp/v2 v2.0.10
|
github.com/pion/interceptor v0.1.43
|
||||||
github.com/pion/stun v0.3.5
|
github.com/pion/rtcp v1.2.16
|
||||||
github.com/pion/webrtc/v3 v3.1.43
|
github.com/pion/rtp v1.10.0
|
||||||
github.com/rs/zerolog v1.27.0
|
github.com/pion/sdp/v3 v3.0.17
|
||||||
github.com/stretchr/testify v1.7.1
|
github.com/pion/srtp/v3 v3.0.10
|
||||||
|
github.com/pion/stun/v3 v3.1.1
|
||||||
|
github.com/pion/webrtc/v4 v4.2.3
|
||||||
|
github.com/rs/zerolog v1.34.0
|
||||||
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1
|
||||||
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f
|
||||||
|
github.com/stretchr/testify v1.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.3 // 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/mattn/go-colorable v0.1.12 // indirect
|
github.com/pion/datachannel v1.6.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/miekg/dns v1.1.50 // indirect
|
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||||
github.com/pion/datachannel v1.5.2 // indirect
|
|
||||||
github.com/pion/dtls/v2 v2.1.5 // indirect
|
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
|
||||||
github.com/pion/mdns v0.0.5 // indirect
|
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.2 // indirect
|
github.com/pion/sctp v1.9.2 // indirect
|
||||||
github.com/pion/transport v0.13.1 // indirect
|
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||||
github.com/pion/turn/v2 v2.0.8 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/pion/udp v0.1.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.0.0-20220516162934-403b01795ae8 // indirect
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
golang.org/x/mod v0.4.2 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
replace (
|
|
||||||
// windows support: https://github.com/brutella/dnssd/pull/35
|
|
||||||
github.com/brutella/dnssd v1.2.2 => github.com/rblenkinsopp/dnssd v1.2.3-0.20220516082132-0923f3c787a1
|
|
||||||
// RTP tlv8 fix
|
|
||||||
github.com/brutella/hap v0.0.17 => github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,252 +1,152 @@
|
|||||||
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657 h1:FUzXAJfm6sRLJ8T6vfzvy/Hm3aioX8+fbxgx2VZoI78=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/AlexxIT/hap v0.0.15-0.20220823033740-ce7d1564e657/go.mod h1:c2vEL5pzjRWEx07sa32kTVjzI9bBVlstrwBwKe3DlJ0=
|
github.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=
|
||||||
github.com/brutella/dnssd v1.2.3 h1:4fBLjZjPH7SbcHhEcIJhZcC9nOhIDZ0m3rn9bjl1/i0=
|
github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||||
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=
|
||||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/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/deepch/vdk v0.0.19 h1:r6xYyBTtXEIEh+csO0XHT00sI7xLF+hQFkJE9/go5II=
|
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||||
github.com/deepch/vdk v0.0.19/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
github.com/expr-lang/expr v1.17.7/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/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/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||||
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||||
github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA=
|
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
|
||||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
|
||||||
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
|
|
||||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
|
||||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
|
||||||
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
|
|
||||||
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
|
|
||||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
|
||||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
|
||||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
|
||||||
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
|
|
||||||
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
|
|
||||||
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
|
|
||||||
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
|
||||||
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
|
|
||||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
|
||||||
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
|
|
||||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
|
||||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
|
||||||
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
|
|
||||||
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
|
||||||
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.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
|
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||||
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
|
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
|
||||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
|
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
|
||||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
|
||||||
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
|
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||||
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
|
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||||
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
|
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
|
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||||
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
|
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||||
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
|
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||||
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
|
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||||
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||||
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
|
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||||
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
|
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
|
||||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
|
||||||
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
|
github.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4=
|
||||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
github.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc=
|
||||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
|
|
||||||
github.com/pion/webrtc/v3 v3.1.42/go.mod h1:ffD9DulDrPxyWvDPUIPAOSAWx9GUlOExiJPf7cCcMLA=
|
|
||||||
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
|
|
||||||
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
|
|
||||||
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.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
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/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=
|
||||||
|
github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=
|
||||||
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=
|
||||||
|
github.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
|
||||||
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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
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=
|
||||||
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
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-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
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-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
|
|
||||||
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
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-20190228124157-a34e9553db1e/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-20200724161237-0e2f3a69832c/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-20210303074136-134d130e1a04/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-20211216021012-1d35b9e2eb4e/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-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
|
|
||||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/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/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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
|
||||||
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 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0=
|
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
|
||||||
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.1/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.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.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
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# HTTP API
|
||||||
|
|
||||||
|
The HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.
|
||||||
|
|
||||||
|
The HTTP API is described in [OpenAPI](../../website/api/openapi.yaml) format. It can be explored in [interactive viewer](https://go2rtc.org/api/). WebSocket API described [here](ws/README.md).
|
||||||
|
|
||||||
|
The project's static HTML and JS files are located in the [www](../../www/README.md) folder. An external developer can use them as a basis for integrating go2rtc into their project or for developing a custom web interface for go2rtc.
|
||||||
|
|
||||||
|
The contents of `www` folder are built into go2rtc when building, but you can use configuration to specify an external folder as the source of static files.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
**Important!** go2rtc passes requests from localhost and Unix sockets without HTTP authorization, even if you have it configured. It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.
|
||||||
|
|
||||||
|
- you can disable HTTP API with `listen: ""` and use, for example, only RTSP client/server protocol
|
||||||
|
- you can enable HTTP API only on localhost with `listen: "127.0.0.1:1984"` setting
|
||||||
|
- you can change the API `base_path` and host go2rtc on your main app webserver suburl
|
||||||
|
- all files from `static_dir` hosted on root path: `/`
|
||||||
|
- you can use raw TLS cert/key content or path to files
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api:
|
||||||
|
listen: ":1984" # default ":1984", HTTP API port ("" - disabled)
|
||||||
|
username: "admin" # default "", Basic auth for WebUI
|
||||||
|
password: "pass" # default "", Basic auth for WebUI
|
||||||
|
local_auth: true # default false, Enable auth check for localhost requests
|
||||||
|
base_path: "/rtc" # default "", API prefix for serving on suburl (/api => /rtc/api)
|
||||||
|
static_dir: "www" # default "", folder for static files (custom web interface)
|
||||||
|
origin: "*" # default "", allow CORS requests (only * supported)
|
||||||
|
tls_listen: ":443" # default "", enable HTTPS server
|
||||||
|
tls_cert: | # default "", PEM-encoded fullchain certificate for HTTPS
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
...
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
tls_key: | # default "", PEM-encoded private key for HTTPS
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
...
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
unix_listen: "/tmp/go2rtc.sock" # default "", unix socket listener for API
|
||||||
|
```
|
||||||
|
|
||||||
|
**PS:**
|
||||||
|
|
||||||
|
- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)
|
||||||
|
- MP4 over WebSocket was created only for Apple iOS because it doesn't support file streaming
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod struct {
|
||||||
|
Listen string `yaml:"listen"`
|
||||||
|
Username string `yaml:"username"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
|
LocalAuth bool `yaml:"local_auth"`
|
||||||
|
BasePath string `yaml:"base_path"`
|
||||||
|
StaticDir string `yaml:"static_dir"`
|
||||||
|
Origin string `yaml:"origin"`
|
||||||
|
TLSListen string `yaml:"tls_listen"`
|
||||||
|
TLSCert string `yaml:"tls_cert"`
|
||||||
|
TLSKey string `yaml:"tls_key"`
|
||||||
|
UnixListen string `yaml:"unix_listen"`
|
||||||
|
|
||||||
|
AllowPaths []string `yaml:"allow_paths"`
|
||||||
|
} `yaml:"api"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// default config
|
||||||
|
cfg.Mod.Listen = ":1984"
|
||||||
|
|
||||||
|
// load config from YAML
|
||||||
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
|
if cfg.Mod.Listen == "" && cfg.Mod.UnixListen == "" && cfg.Mod.TLSListen == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
allowPaths = cfg.Mod.AllowPaths
|
||||||
|
basePath = cfg.Mod.BasePath
|
||||||
|
log = app.GetLogger("api")
|
||||||
|
|
||||||
|
initStatic(cfg.Mod.StaticDir)
|
||||||
|
|
||||||
|
HandleFunc("api", apiHandler)
|
||||||
|
HandleFunc("api/config", configHandler)
|
||||||
|
HandleFunc("api/exit", exitHandler)
|
||||||
|
HandleFunc("api/restart", restartHandler)
|
||||||
|
HandleFunc("api/log", logHandler)
|
||||||
|
|
||||||
|
Handler = http.DefaultServeMux // 4th
|
||||||
|
|
||||||
|
if cfg.Mod.Origin == "*" {
|
||||||
|
Handler = middlewareCORS(Handler) // 3rd
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Mod.Username != "" {
|
||||||
|
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.Trace().Enabled() {
|
||||||
|
Handler = middlewareLog(Handler) // 1st
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Mod.Listen != "" {
|
||||||
|
_, port, _ := net.SplitHostPort(cfg.Mod.Listen)
|
||||||
|
Port, _ = strconv.Atoi(port)
|
||||||
|
go listen("tcp", cfg.Mod.Listen)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Mod.UnixListen != "" {
|
||||||
|
_ = syscall.Unlink(cfg.Mod.UnixListen)
|
||||||
|
go listen("unix", cfg.Mod.UnixListen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the HTTPS server
|
||||||
|
if cfg.Mod.TLSListen != "" && cfg.Mod.TLSCert != "" && cfg.Mod.TLSKey != "" {
|
||||||
|
go tlsListen("tcp", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
MimeJSON = "application/json"
|
||||||
|
MimeText = "text/plain"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Handler http.Handler
|
||||||
|
|
||||||
|
// HandleFunc handle pattern with relative path:
|
||||||
|
// - "api/streams" => "{basepath}/api/streams"
|
||||||
|
// - "/streams" => "/streams"
|
||||||
|
func HandleFunc(pattern string, handler http.HandlerFunc) {
|
||||||
|
if len(pattern) == 0 || pattern[0] != '/' {
|
||||||
|
pattern = basePath + "/" + pattern
|
||||||
|
}
|
||||||
|
if allowPaths != nil && !slices.Contains(allowPaths, pattern) {
|
||||||
|
log.Trace().Str("path", pattern).Msg("[api] ignore path not in allow_paths")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Trace().Str("path", pattern).Msg("[api] register path")
|
||||||
|
http.HandleFunc(pattern, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseJSON important always add Content-Type
|
||||||
|
// so go won't need to call http.DetectContentType
|
||||||
|
func ResponseJSON(w http.ResponseWriter, v any) {
|
||||||
|
w.Header().Set("Content-Type", MimeJSON)
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResponsePrettyJSON(w http.ResponseWriter, v any) {
|
||||||
|
w.Header().Set("Content-Type", MimeJSON)
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
_ = enc.Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Response(w http.ResponseWriter, body any, contentType string) {
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
|
||||||
|
switch v := body.(type) {
|
||||||
|
case []byte:
|
||||||
|
_, _ = w.Write(v)
|
||||||
|
case string:
|
||||||
|
_, _ = w.Write([]byte(v))
|
||||||
|
default:
|
||||||
|
_, _ = fmt.Fprint(w, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const StreamNotFound = "stream not found"
|
||||||
|
|
||||||
|
var allowPaths []string
|
||||||
|
var basePath string
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
|
func middlewareLog(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr)
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLoopback(remoteAddr string) bool {
|
||||||
|
return strings.HasPrefix(remoteAddr, "127.") || strings.HasPrefix(remoteAddr, "[::1]") || remoteAddr == "@"
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if localAuth || !isLoopback(r.RemoteAddr) {
|
||||||
|
user, pass, ok := r.BasicAuth()
|
||||||
|
if !ok || user != username || pass != password {
|
||||||
|
w.Header().Set("Www-Authenticate", `Basic realm="go2rtc"`)
|
||||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func middlewareCORS(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
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-Headers", "Authorization, Content-Type")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
mu.Lock()
|
||||||
|
app.Info["host"] = r.Host
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
ResponseJSON(w, app.Info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s := r.URL.Query().Get("code")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func restartHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != "POST" {
|
||||||
|
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 logHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = struct {
|
||||||
|
Sources []*Source `json:"sources"`
|
||||||
|
}{
|
||||||
|
Sources: sources,
|
||||||
|
}
|
||||||
|
ResponseJSON(w, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(w http.ResponseWriter, err error) {
|
||||||
|
log.Error().Err(err).Caller(1).Send()
|
||||||
|
|
||||||
|
http.Error(w, err.Error(), http.StatusInsufficientStorage)
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if app.ConfigPath == "" {
|
||||||
|
http.Error(w, "", http.StatusGone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
case "GET":
|
||||||
|
data, err := os.ReadFile(app.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
|
||||||
|
Response(w, data, "application/yaml")
|
||||||
|
|
||||||
|
case "POST", "PATCH":
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "PATCH" {
|
||||||
|
// no need to validate after merge
|
||||||
|
data, err = mergeYAML(app.ConfigPath, data)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// validate config
|
||||||
|
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {
|
||||||
|
// Read the contents of the first YAML file
|
||||||
|
data1, err := os.ReadFile(file1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the first YAML file into a map
|
||||||
|
var config1 map[string]any
|
||||||
|
if err = yaml.Unmarshal(data1, &config1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal the second YAML document into a map
|
||||||
|
var config2 map[string]any
|
||||||
|
if err = yaml.Unmarshal(yaml2, &config2); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge the two maps
|
||||||
|
config1 = merge(config1, config2)
|
||||||
|
|
||||||
|
// Marshal the merged map into YAML
|
||||||
|
return yaml.Marshal(&config1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func merge(dst, src map[string]any) map[string]any {
|
||||||
|
for k, v := range src {
|
||||||
|
if vv, ok := dst[k]; ok {
|
||||||
|
switch vv := vv.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
v := v.(map[string]any)
|
||||||
|
dst[k] = merge(vv, v)
|
||||||
|
case []any:
|
||||||
|
v := v.([]any)
|
||||||
|
dst[k] = v
|
||||||
|
default:
|
||||||
|
dst[k] = v
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dst[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
@@ -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"}
|
||||||
|
```
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
package ws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"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() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod struct {
|
||||||
|
Origin string `yaml:"origin"`
|
||||||
|
} `yaml:"api"`
|
||||||
|
}
|
||||||
|
|
||||||
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
|
log = app.GetLogger("api")
|
||||||
|
|
||||||
|
initWS(cfg.Mod.Origin)
|
||||||
|
|
||||||
|
api.HandleFunc("api/ws", apiWS)
|
||||||
|
}
|
||||||
|
|
||||||
|
var log zerolog.Logger
|
||||||
|
|
||||||
|
// Message - struct for data exchange in Web API
|
||||||
|
type Message struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value any `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) String() (value string) {
|
||||||
|
if s, ok := m.Value.(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Message) Unmarshal(v any) error {
|
||||||
|
b, err := json.Marshal(m.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.Unmarshal(b, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSHandler func(tr *Transport, msg *Message) error
|
||||||
|
|
||||||
|
func HandleFunc(msgType string, handler WSHandler) {
|
||||||
|
wsHandlers[msgType] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
var wsHandlers = make(map[string]WSHandler)
|
||||||
|
|
||||||
|
func initWS(origin string) {
|
||||||
|
wsUp = &websocket.Upgrader{
|
||||||
|
ReadBufferSize: 4096, // for SDP
|
||||||
|
WriteBufferSize: 512 * 1024, // 512K
|
||||||
|
}
|
||||||
|
|
||||||
|
switch origin {
|
||||||
|
case "":
|
||||||
|
// same origin + ignore port
|
||||||
|
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||||
|
origin := r.Header["Origin"]
|
||||||
|
if len(origin) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
o, err := url.Parse(origin[0])
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if o.Host == r.Host {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
|
||||||
|
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||||
|
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||||
|
return o.Host[:i] == r.Host
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case "*":
|
||||||
|
// any origin
|
||||||
|
wsUp.CheckOrigin = func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiWS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ws, err := wsUp.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
log.Error().Err(err).Caller().Msgf("host=%s origin=%s", r.Host, origin)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := &Transport{Request: r}
|
||||||
|
tr.OnWrite(func(msg any) error {
|
||||||
|
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||||
|
|
||||||
|
if data, ok := msg.([]byte); ok {
|
||||||
|
return ws.WriteMessage(websocket.BinaryMessage, data)
|
||||||
|
} else {
|
||||||
|
return ws.WriteJSON(msg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg := new(Message)
|
||||||
|
if err = ws.ReadJSON(msg); err != nil {
|
||||||
|
if !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {
|
||||||
|
log.Trace().Err(err).Caller().Send()
|
||||||
|
}
|
||||||
|
_ = ws.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||||
|
|
||||||
|
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||||
|
go func() {
|
||||||
|
if err = handler(tr, msg); err != nil {
|
||||||
|
errMsg := creds.SecretString(err.Error())
|
||||||
|
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var wsUp *websocket.Upgrader
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
Request *http.Request
|
||||||
|
|
||||||
|
ctx map[any]any
|
||||||
|
|
||||||
|
closed bool
|
||||||
|
mx sync.Mutex
|
||||||
|
wrmx sync.Mutex
|
||||||
|
|
||||||
|
onChange func()
|
||||||
|
onWrite func(msg any) error
|
||||||
|
onClose []func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) OnWrite(f func(msg any) error) {
|
||||||
|
t.mx.Lock()
|
||||||
|
if t.onChange != nil {
|
||||||
|
t.onChange()
|
||||||
|
}
|
||||||
|
t.onWrite = f
|
||||||
|
t.mx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Write(msg any) {
|
||||||
|
t.wrmx.Lock()
|
||||||
|
_ = t.onWrite(msg)
|
||||||
|
t.wrmx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Close() {
|
||||||
|
t.mx.Lock()
|
||||||
|
for _, f := range t.onClose {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
t.closed = true
|
||||||
|
t.mx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) OnChange(f func()) {
|
||||||
|
t.mx.Lock()
|
||||||
|
t.onChange = f
|
||||||
|
t.mx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) OnClose(f func()) {
|
||||||
|
t.mx.Lock()
|
||||||
|
if t.closed {
|
||||||
|
f()
|
||||||
|
} else {
|
||||||
|
t.onClose = append(t.onClose, f)
|
||||||
|
}
|
||||||
|
t.mx.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext - run function with Context variable
|
||||||
|
func (t *Transport) WithContext(f func(ctx map[any]any)) {
|
||||||
|
t.mx.Lock()
|
||||||
|
if t.ctx == nil {
|
||||||
|
t.ctx = map[any]any{}
|
||||||
|
}
|
||||||
|
f(t.ctx)
|
||||||
|
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`.
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"runtime/debug"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version string
|
||||||
|
Modules []string
|
||||||
|
UserAgent string
|
||||||
|
ConfigPath string
|
||||||
|
Info = make(map[string]any)
|
||||||
|
)
|
||||||
|
|
||||||
|
const usage = `Usage of go2rtc:
|
||||||
|
|
||||||
|
-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() {
|
||||||
|
var config flagConfig
|
||||||
|
var daemon bool
|
||||||
|
var version bool
|
||||||
|
|
||||||
|
flag.Var(&config, "config", "")
|
||||||
|
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()
|
||||||
|
|
||||||
|
revision, vcsTime := readRevisionTime()
|
||||||
|
|
||||||
|
if version {
|
||||||
|
fmt.Printf("go2rtc version %s (%s) %s/%s\n", Version, revision, runtime.GOOS, runtime.GOARCH)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if daemon && os.Getppid() != 1 {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
fmt.Println("Daemon mode is not supported on Windows")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 != "" {
|
||||||
|
Logger.Info().Str("path", ConfigPath).Msg("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg struct {
|
||||||
|
Mod struct {
|
||||||
|
Modules []string `yaml:"modules"`
|
||||||
|
} `yaml:"app"`
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadConfig(&cfg)
|
||||||
|
|
||||||
|
Modules = cfg.Mod.Modules
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRevisionTime() (revision, vcsTime string) {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check version from -buildvcs info
|
||||||
|
// Format for tagged version : v1.9.13
|
||||||
|
// Format for modified code: v1.9.14-0.20251215184105-753d6617ab58+dirty
|
||||||
|
if info.Main.Version != "v"+Version {
|
||||||
|
// Format: 1.9.13+dev.753d661[.dirty]
|
||||||
|
// Compatible with "awesomeversion" and "packaging.version" from python.
|
||||||
|
// Version will be larger than the previous release, but smaller than the next release.
|
||||||
|
Version += "+dev." + revision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
api.HandleFunc("api/stack", stackHandler)
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var stackSkip = [][]byte{
|
var stackSkip = [][]byte{
|
||||||
@@ -13,17 +15,22 @@ var stackSkip = [][]byte{
|
|||||||
[]byte("created by os/signal.Notify"),
|
[]byte("created by os/signal.Notify"),
|
||||||
|
|
||||||
// api/stack.go
|
// api/stack.go
|
||||||
[]byte("github.com/AlexxIT/go2rtc/cmd/api.stackHandler"),
|
[]byte("github.com/AlexxIT/go2rtc/internal/api.stackHandler"),
|
||||||
|
|
||||||
// api/api.go
|
// api/api.go
|
||||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/api.Init"),
|
||||||
[]byte("created by net/http.(*connReader).startBackgroundRead"),
|
[]byte("created by net/http.(*connReader).startBackgroundRead"),
|
||||||
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
[]byte("created by net/http.(*Server).Serve"), // TODO: why two?
|
||||||
|
|
||||||
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/rtsp.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/v4.NewUDPMuxDefault"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -49,7 +56,5 @@ func stackHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
|
"Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
|
||||||
)
|
)
|
||||||
|
|
||||||
if _, err := w.Write(buf[:i]); err != nil {
|
api.Response(w, buf[:i], api.MimeText)
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Doorbird
|
||||||
|
|
||||||
|
[`new in v1.9.8`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.8)
|
||||||
|
|
||||||
|
This source type supports [Doorbird](https://www.doorbird.com/) devices including MJPEG stream, audio stream as well as two-way audio.
|
||||||
|
|
||||||
|
It is recommended to create a separate user within your doorbird setup for go2rtc. Minimum permissions for the user are:
|
||||||
|
|
||||||
|
- Watch always
|
||||||
|
- API operator
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
doorbird1:
|
||||||
|
- rtsp://admin:password@192.168.1.123:8557/mpeg/720p/media.amp # RTSP stream
|
||||||
|
- doorbird://admin:password@192.168.1.123?media=video # MJPEG stream
|
||||||
|
- doorbird://admin:password@192.168.1.123?media=audio # audio stream
|
||||||
|
- doorbird://admin:password@192.168.1.123 # two-way audio
|
||||||
|
```
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package doorbird
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/doorbird"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.RedirectFunc("doorbird", func(rawURL string) (string, error) {
|
||||||
|
u, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://www.doorbird.com/downloads/api_lan.pdf
|
||||||
|
switch u.Query().Get("media") {
|
||||||
|
case "video":
|
||||||
|
u.Path = "/bha-api/video.cgi"
|
||||||
|
case "audio":
|
||||||
|
u.Path = "/bha-api/audio-receive.cgi"
|
||||||
|
default:
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Scheme = "http"
|
||||||
|
|
||||||
|
return u.String(), nil
|
||||||
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("doorbird", func(source string) (core.Producer, error) {
|
||||||
|
return doorbird.Dial(source)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# DVR-IP
|
||||||
|
|
||||||
|
[`new in v1.2.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)
|
||||||
|
|
||||||
|
Private format from DVR-IP NVR, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default
|
||||||
|
- set up separate streams for different channels
|
||||||
|
- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream
|
||||||
|
- only the TCP protocol is supported
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||||
|
only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||||
|
two_way_audio:
|
||||||
|
- dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0
|
||||||
|
- dvrip://username:password@192.168.1.123:34567?backchannel=1
|
||||||
|
```
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package dvrip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
streams.HandleFunc("dvrip", dvrip.Dial)
|
||||||
|
|
||||||
|
// DVRIP client autodiscovery
|
||||||
|
api.HandleFunc("api/dvrip", apiDvrip)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Port = 34569 // UDP port number for dvrip discovery
|
||||||
|
|
||||||
|
func apiDvrip(w http.ResponseWriter, r *http.Request) {
|
||||||
|
items, err := discover()
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
api.ResponseSources(w, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
func discover() ([]*api.Source, error) {
|
||||||
|
addr := &net.UDPAddr{
|
||||||
|
Port: Port,
|
||||||
|
IP: net.IP{239, 255, 255, 250},
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.ListenUDP("udp4", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
go sendBroadcasts(conn)
|
||||||
|
|
||||||
|
var items []*api.Source
|
||||||
|
|
||||||
|
for _, info := range getResponses(conn) {
|
||||||
|
if info.HostIP == "" || info.HostName == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
host, err := hexToDecimalBytes(info.HostIP)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, &api.Source{
|
||||||
|
Name: info.HostName,
|
||||||
|
URL: "dvrip://user:pass@" + host + "?channel=0&subtype=0",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendBroadcasts(conn *net.UDPConn) {
|
||||||
|
// broadcasting the same multiple times because the devies some times don't answer
|
||||||
|
data, err := hex.DecodeString("ff00000000000000000000000000fa0500000000")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := &net.UDPAddr{
|
||||||
|
Port: Port,
|
||||||
|
IP: net.IP{255, 255, 255, 255},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
_, _ = conn.WriteToUDP(data, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
NetCommon NetCommon `json:"NetWork.NetCommon"`
|
||||||
|
Ret int `json:"Ret"`
|
||||||
|
SessionID string `json:"SessionID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetCommon struct {
|
||||||
|
BuildDate string `json:"BuildDate"`
|
||||||
|
ChannelNum int `json:"ChannelNum"`
|
||||||
|
DeviceType int `json:"DeviceType"`
|
||||||
|
GateWay string `json:"GateWay"`
|
||||||
|
HostIP string `json:"HostIP"`
|
||||||
|
HostName string `json:"HostName"`
|
||||||
|
HttpPort int `json:"HttpPort"`
|
||||||
|
MAC string `json:"MAC"`
|
||||||
|
MonMode string `json:"MonMode"`
|
||||||
|
NetConnectState int `json:"NetConnectState"`
|
||||||
|
OtherFunction string `json:"OtherFunction"`
|
||||||
|
SN string `json:"SN"`
|
||||||
|
SSLPort int `json:"SSLPort"`
|
||||||
|
Submask string `json:"Submask"`
|
||||||
|
TCPMaxConn int `json:"TCPMaxConn"`
|
||||||
|
TCPPort int `json:"TCPPort"`
|
||||||
|
UDPPort int `json:"UDPPort"`
|
||||||
|
UseHSDownLoad bool `json:"UseHSDownLoad"`
|
||||||
|
Version string `json:"Version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResponses(conn *net.UDPConn) (infos []*NetCommon) {
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var ips []net.IP // processed IPs
|
||||||
|
|
||||||
|
b := make([]byte, 4096)
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
n, addr, err := conn.ReadFromUDP(b)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.Equal(addr.IP) {
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if n <= 20+1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg Message
|
||||||
|
|
||||||
|
if err = json.Unmarshal(b[20:n-1], &msg); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
infos = append(infos, &msg.NetCommon)
|
||||||
|
ips = append(ips, addr.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexToDecimalBytes(hexIP string) (string, error) {
|
||||||
|
b, err := hex.DecodeString(hexIP[2:]) // remove the '0x' prefix
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d.%d.%d.%d", b[3], b[2], b[1], b[0]), nil
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package echo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod struct {
|
||||||
|
AllowPaths []string `yaml:"allow_paths"`
|
||||||
|
} `yaml:"echo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
|
allowPaths := cfg.Mod.AllowPaths
|
||||||
|
|
||||||
|
log := app.GetLogger("echo")
|
||||||
|
|
||||||
|
streams.RedirectFunc("echo", func(url string) (string, error) {
|
||||||
|
args := shell.QuoteSplit(url[5:])
|
||||||
|
|
||||||
|
if allowPaths != nil && !slices.Contains(allowPaths, args[0]) {
|
||||||
|
return "", errors.New("echo: bin not in allow_paths: " + args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := exec.Command(args[0], args[1:]...).Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b = bytes.TrimSpace(b)
|
||||||
|
|
||||||
|
log.Debug().Str("url", url).Msgf("[echo] %s", 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
|
||||||
|
```
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
package exec
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/streams"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/magic"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/pcm"
|
||||||
|
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/shell"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
var cfg struct {
|
||||||
|
Mod struct {
|
||||||
|
AllowPaths []string `yaml:"allow_paths"`
|
||||||
|
} `yaml:"exec"`
|
||||||
|
}
|
||||||
|
|
||||||
|
app.LoadConfig(&cfg)
|
||||||
|
|
||||||
|
allowPaths = cfg.Mod.AllowPaths
|
||||||
|
|
||||||
|
rtsp.HandleFunc(func(conn *pkg.Conn) bool {
|
||||||
|
waitersMu.Lock()
|
||||||
|
waiter := waiters[conn.URL.Path]
|
||||||
|
waitersMu.Unlock()
|
||||||
|
|
||||||
|
if waiter == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// unblocking write to channel
|
||||||
|
select {
|
||||||
|
case waiter <- conn:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
streams.HandleFunc("exec", execHandle)
|
||||||
|
streams.MarkInsecure("exec")
|
||||||
|
|
||||||
|
log = app.GetLogger("exec")
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowPaths []string
|
||||||
|
|
||||||
|
func execHandle(rawURL string) (prod core.Producer, err error) {
|
||||||
|
rawURL, rawQuery, _ := strings.Cut(rawURL, "#")
|
||||||
|
query := streams.ParseQuery(rawQuery)
|
||||||
|
|
||||||
|
var path string
|
||||||
|
|
||||||
|
// RTSP flow should have `{output}` inside URL
|
||||||
|
// pipe flow may have `#{params}` inside URL
|
||||||
|
if i := strings.Index(rawURL, "{output}"); i > 0 {
|
||||||
|
if rtsp.Port == "" {
|
||||||
|
return nil, errors.New("exec: rtsp module disabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := md5.Sum([]byte(rawURL))
|
||||||
|
path = "/" + hex.EncodeToString(sum[:])
|
||||||
|
rawURL = rawURL[:i] + "rtsp://127.0.0.1:" + rtsp.Port + path + rawURL[i+8:]
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s := query.Get("killtimeout"); s != "" {
|
||||||
|
cmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Get("backchannel") == "1" {
|
||||||
|
return pcm.NewBackchannel(cmd, query.Get("audio"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout time.Duration
|
||||||
|
if s := query.Get("starttimeout"); s != "" {
|
||||||
|
timeout = time.Duration(core.Atoi(s)) * time.Second
|
||||||
|
} else {
|
||||||
|
timeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if path == "" {
|
||||||
|
prod, err = handlePipe(rawURL, cmd)
|
||||||
|
} else {
|
||||||
|
prod, err = handleRTSP(rawURL, cmd, path, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
_ = cmd.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlePipe(source string, cmd *shell.Command) (core.Producer, error) {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
prod, err := magic.Open(rd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("exec/pipe: %w\n%s", err, cmd.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if info, ok := prod.(core.Info); ok {
|
||||||
|
info.SetProtocol("pipe")
|
||||||
|
setRemoteInfo(info, source, cmd.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run pipe")
|
||||||
|
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleRTSP(source string, cmd *shell.Command, path string, timeout time.Duration) (core.Producer, error) {
|
||||||
|
if log.Trace().Enabled() {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
waiter := make(chan *pkg.Conn, 1)
|
||||||
|
|
||||||
|
waitersMu.Lock()
|
||||||
|
waiters[path] = waiter
|
||||||
|
waitersMu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
waitersMu.Lock()
|
||||||
|
delete(waiters, path)
|
||||||
|
waitersMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Debug().Strs("args", cmd.Args).Msg("[exec] run rtsp")
|
||||||
|
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Error().Err(err).Str("source", source).Msg("[exec]")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timer := time.NewTimer(timeout)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
// haven't received data from app in timeout
|
||||||
|
log.Error().Str("source", source).Msg("[exec] timeout")
|
||||||
|
return nil, errors.New("exec: timeout")
|
||||||
|
case <-cmd.Done():
|
||||||
|
// app fail before we receive any data
|
||||||
|
return nil, fmt.Errorf("exec/rtsp\n%s", cmd.Stderr)
|
||||||
|
case prod := <-waiter:
|
||||||
|
// app started successfully
|
||||||
|
log.Debug().Stringer("launch", time.Since(ts)).Msg("[exec] run rtsp")
|
||||||
|
setRemoteInfo(prod, source, cmd.Args)
|
||||||
|
prod.OnClose = cmd.Close
|
||||||
|
return prod, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// internal
|
||||||
|
|
||||||
|
var (
|
||||||
|
log zerolog.Logger
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# FFmpeg
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
- FFmpeg preinstalled for **Docker** and **Home Assistant add-on** users
|
||||||
|
- **Home Assistant add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Format: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
# [FILE] all tracks will be copied without transcoding codecs
|
||||||
|
file1: ffmpeg:/media/BigBuckBunny.mp4
|
||||||
|
|
||||||
|
# [FILE] video will be transcoded to H264, audio will be skipped
|
||||||
|
file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264
|
||||||
|
|
||||||
|
# [FILE] video will be copied, audio will be transcoded to PCMU
|
||||||
|
file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu
|
||||||
|
|
||||||
|
# [HLS] video will be copied, audio will be skipped
|
||||||
|
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
|
||||||
|
|
||||||
|
# [MJPEG] video will be transcoded to H264
|
||||||
|
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264
|
||||||
|
|
||||||
|
# [RTSP] video with rotation, should be transcoded, so select H264
|
||||||
|
rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ffmpeg:
|
||||||
|
bin: ffmpeg # path to ffmpeg binary
|
||||||
|
global: "-hide_banner"
|
||||||
|
timeout: 5 # default timeout in seconds for rtsp inputs
|
||||||
|
h264: "-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1"
|
||||||
|
mycodec: "-any args that supported by ffmpeg..."
|
||||||
|
myinput: "-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}"
|
||||||
|
myraw: "-ss 00:00:20"
|
||||||
|
```
|
||||||
|
|
||||||
|
- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)
|
||||||
|
- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)
|
||||||
|
- You can use `rotate` param with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)
|
||||||
|
- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)
|
||||||
|
- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)
|
||||||
|
- This will greatly increase the CPU of the server, even with hardware acceleration
|
||||||
|
- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)
|
||||||
|
- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)
|
||||||
|
- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)
|
||||||
|
- You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`)
|
||||||
|
- You can add your own input templates
|
||||||
|
|
||||||
|
Read more about [hardware acceleration](hardware/README.md).
|
||||||
|
|
||||||
|
**PS.** It is recommended to check the available hardware in the WebUI add page.
|
||||||
@@ -0,0 +1,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//go:build darwin || ios
|
||||||
|
|
||||||
|
package device
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func queryToInput(query url.Values) string {
|
||||||
|
video := query.Get("video")
|
||||||
|
audio := query.Get("audio")
|
||||||
|
|
||||||
|
if video == "" && audio == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://ffmpeg.org/ffmpeg-devices.html#avfoundation
|
||||||
|
input := "-f avfoundation"
|
||||||
|
|
||||||
|
if video != "" {
|
||||||
|
video = indexToItem(videos, video)
|
||||||
|
|
||||||
|
for key, value := range query {
|
||||||
|
switch key {
|
||||||
|
case "resolution":
|
||||||
|
input += " -video_size " + value[0]
|
||||||
|
case "pixel_format", "framerate", "video_size", "capture_cursor", "capture_mouse_clicks", "capture_raw_data":
|
||||||
|
input += " -" + key + " " + value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if audio != "" {
|
||||||
|
audio = indexToItem(audios, audio)
|
||||||
|
}
|
||||||
|
|
||||||
|
return input + ` -i "` + video + `:` + audio + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDevices() {
|
||||||
|
// [AVFoundation indev @ 0x147f04510] AVFoundation video devices:
|
||||||
|
// [AVFoundation indev @ 0x147f04510] [0] FaceTime HD Camera
|
||||||
|
// [AVFoundation indev @ 0x147f04510] [1] Capture screen 0
|
||||||
|
// [AVFoundation indev @ 0x147f04510] AVFoundation audio devices:
|
||||||
|
// [AVFoundation indev @ 0x147f04510] [0] MacBook Pro Microphone
|
||||||
|
cmd := exec.Command(
|
||||||
|
Bin, "-hide_banner", "-list_devices", "true", "-f", "avfoundation", "-i", "",
|
||||||
|
)
|
||||||
|
b, _ := cmd.CombinedOutput()
|
||||||
|
|
||||||
|
re := regexp.MustCompile(`\[\d+] (.+)`)
|
||||||
|
|
||||||
|
var kind string
|
||||||
|
for _, line := range strings.Split(string(b), "\n") {
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(line, "video devices:"):
|
||||||
|
kind = core.KindVideo
|
||||||
|
continue
|
||||||
|
case strings.HasSuffix(line, "audio devices:"):
|
||||||
|
kind = core.KindAudio
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m := re.FindStringSubmatch(line)
|
||||||
|
if m == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := m[1]
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case core.KindVideo:
|
||||||
|
videos = append(videos, name)
|
||||||
|
case core.KindAudio:
|
||||||
|
audios = append(audios, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
streams = append(streams, &api.Source{
|
||||||
|
Name: name, URL: "ffmpeg:device?" + kind + "=" + name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user