Compare commits
1576 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fc40aa7db | |||
| dc1685e9cf | |||
| 65af579770 | |||
| d9af354de8 | |||
| 31c19a0e7d | |||
| 01c7451556 | |||
| f68c602a7d | |||
| 7f36033bff | |||
| 6304987a26 | |||
| 2735bafd86 | |||
| 44f6f111c4 | |||
| e5bb03349b | |||
| e90f159c68 | |||
| be4b6c3271 | |||
| f3ad4ad977 | |||
| 7083afe9b2 | |||
| d01b99d105 | |||
| d1d7846aed | |||
| 8d58cc7f97 | |||
| f1d61bf15a | |||
| d2c4e44844 | |||
| 5bf5327a45 | |||
| 4bf0aef124 | |||
| 8e01d5cfa9 | |||
| 6085c8aabe | |||
| 8d4869fd08 | |||
| 5970dd2fe4 | |||
| a41185c22a | |||
| c793855800 | |||
| a556bca7bd | |||
| 051aa664cd | |||
| 6374366da5 | |||
| de157eb144 | |||
| 72ec880300 | |||
| 6eeba1ae4c | |||
| 8f68f80bff | |||
| 2f9d138692 | |||
| 366cbd200d | |||
| 6f75b2f75e | |||
| 75bd1f1b2b | |||
| 7793a55853 | |||
| 06cbbe5543 | |||
| 861bc9728b | |||
| ae0646ea9e | |||
| c8412eb067 | |||
| a37fdf38d8 | |||
| b5948cfb25 | |||
| c64fcc55a5 | |||
| ba9fb70929 | |||
| 1d9bb27f58 | |||
| 55c1f5890b | |||
| d98059f069 | |||
| fc22b20896 | |||
| af819952e8 | |||
| 159fb4675c | |||
| 54eafe9d0a | |||
| 38cc05c22d | |||
| 8c457710bd | |||
| 514188201a | |||
| b65ee278cd | |||
| 66225973ae | |||
| d40f6064d9 | |||
| 9365fef7b3 | |||
| b220959e41 | |||
| c493087876 | |||
| af90b4c12c | |||
| 679454aedb | |||
| 160695857e | |||
| 6a5deecfcc | |||
| 572f07fcce | |||
| 9d1e4b11d7 | |||
| f85cfdc214 | |||
| 5780bf3720 | |||
| 338a3a6f03 | |||
| 6796bdabe2 | |||
| ebe454e3be | |||
| c03cd9f156 | |||
| 1ec40f2fc3 | |||
| 790fdfbf7a | |||
| 6d77b175d8 | |||
| 8e6b8b32d6 | |||
| 425fcffbe1 | |||
| 9a1eac8ef4 | |||
| 4ffdeb06d0 | |||
| 6b4eb8ffb6 | |||
| cea524074a | |||
| 7498d0fba5 | |||
| 50d9aab0d7 | |||
| 3983ce3f4f | |||
| 0066da94f7 | |||
| 4dbf53122e | |||
| f96a074957 | |||
| dbd04cb972 | |||
| 0d035e5bce | |||
| 7241759fea | |||
| b067c408c0 | |||
| a99590823b | |||
| 3bf2b316d7 | |||
| 2f43bfe5dc | |||
| 59161fcef2 | |||
| 9ca9f96ea2 | |||
| bc0c8d5577 | |||
| fd68107940 | |||
| b19c081642 | |||
| 039e916030 | |||
| 3a587c9cee | |||
| 25e3125a89 | |||
| 439dccf4bd | |||
| 5fcb33c0cd | |||
| c5311cdd94 | |||
| 3dcb6dfc48 | |||
| 406159cce5 | |||
| 659a042c42 | |||
| e614513b97 | |||
| f9f22cdd0b | |||
| dab9efb7d0 | |||
| eb39b80883 | |||
| ff04a0d4b2 | |||
| c4b32e3a0b | |||
| 58d8a86a92 | |||
| 29f966f280 | |||
| cbaa147469 | |||
| c55fa87827 | |||
| 84e13d9d22 | |||
| c6733bf4f1 | |||
| e960f90a97 | |||
| 3207f9e783 | |||
| 263579fa01 | |||
| 90c0b513e9 | |||
| cfbba5a52c | |||
| c74a39a30d | |||
| d4dc670cb5 | |||
| 4cff72c9a3 | |||
| f923487546 | |||
| f47a041ece | |||
| e77210f916 | |||
| 44da81774c | |||
| 7d4b3fe65b | |||
| a42ab88dbd | |||
| 4dae65a535 | |||
| a09e1b2f90 | |||
| d183b99a44 | |||
| 654e78b7c5 | |||
| 1cd5517026 | |||
| 07fb78d661 | |||
| 86f9f114b5 | |||
| 7e38b4fe89 | |||
| 0fd2217bd2 | |||
| 76bdc7e065 | |||
| 3bd433c950 | |||
| 4c50a2c00c | |||
| 0e65bd05c4 | |||
| 05fd0c5cca | |||
| a3a4276c15 | |||
| 0f4607a070 | |||
| 11ad1129ed | |||
| da0b19026c | |||
| c880c37d37 | |||
| c4fb66a818 | |||
| 9b618f45d0 | |||
| 3fd1fe2622 | |||
| 2f470fa518 | |||
| 212def9ceb | |||
| 79698365bb | |||
| 5b1da84ae2 | |||
| 247d4063ed | |||
| 7ada6d81eb | |||
| eda92afffe | |||
| ff19885790 | |||
| bbb9466845 | |||
| 079d404ed0 | |||
| 753d6617ab | |||
| dfe47559d1 | |||
| eabd7d60cd | |||
| dfda4b11ff | |||
| 353262307b | |||
| d734140eaf | |||
| 2409bb56d7 | |||
| df484cc904 | |||
| 7e0c7a8173 | |||
| 7eaa4a1b55 | |||
| 03941a5691 | |||
| 28821c41e0 | |||
| 8636e96379 | |||
| 7119384184 | |||
| 57b0ace802 | |||
| b0f46bc919 | |||
| a4d4598a13 | |||
| d041c89c5c | |||
| ce9138b354 | |||
| 1e5def35c9 | |||
| 17c1f69f66 | |||
| fb31a251b8 | |||
| c5277daa45 | |||
| 76a5e160c2 | |||
| 494daed937 | |||
| a86e10446a | |||
| 209b73a0f1 | |||
| 54473ff1de | |||
| 7eb5fe0355 | |||
| fbd5215669 | |||
| 3ebc115cc5 | |||
| 31962181cb | |||
| 6d1a95a4e3 | |||
| 4ec2849008 | |||
| 4ef6a147a6 | |||
| 94df080bf7 | |||
| 86edd814c9 | |||
| 5a259993d8 | |||
| b6fe8871df | |||
| b47a2ba73c | |||
| 4934fa4cc1 | |||
| 61f74820bc | |||
| 248fc7a11a | |||
| aa0ece2d1e | |||
| 68036b68c1 | |||
| 319dbf2c63 | |||
| 4b419309a8 | |||
| 42e7a03534 | |||
| 290e8fcfda | |||
| 6c78b5cb53 | |||
| c72b205d87 | |||
| 2cd009646a | |||
| 8f5fce4d73 | |||
| b705cadc04 | |||
| 2523a5ac38 | |||
| e246e2e756 | |||
| 2dc0d58ba7 | |||
| cb22ae7833 | |||
| c98b0a83c4 | |||
| 0bae158e41 | |||
| e2b63a4f6c | |||
| 3897f10a4d | |||
| ac80f1470e | |||
| 1fe602679e | |||
| e2c7d06730 | |||
| 2133f5323c | |||
| c10a06d199 | |||
| d053d88ce9 | |||
| 2ce38b4486 | |||
| 44d59b1696 | |||
| 15ec995ecc | |||
| 231cab36b2 | |||
| 640db3029e | |||
| 2836fdae13 | |||
| 964bb225fa | |||
| 5cc32197b8 | |||
| bc1a4ac8e4 | |||
| 158f9d3a08 | |||
| 81cfcf877a | |||
| 96919bf9e3 | |||
| e4359ac217 | |||
| 56d7a6fee4 | |||
| 292b32af99 | |||
| 33e4527042 | |||
| 62a9046f01 | |||
| 25e7ac531e | |||
| 0f27bb1124 | |||
| 0ff3bf67e1 | |||
| 6d3d45e337 | |||
| c6940eb0f3 | |||
| 8142d2fc43 | |||
| 721ed98afb | |||
| fb8c6e1b1b | |||
| 80ab32379c | |||
| 863174839c | |||
| ff18283d11 | |||
| 994e0dc526 | |||
| d16d260679 | |||
| d4190290b7 | |||
| 5660311bb2 | |||
| 7254bd4fbc | |||
| 9f407a754d | |||
| cc97bc33c4 | |||
| bd8e4fa298 | |||
| 6db4dda535 | |||
| be80eb1ac9 | |||
| a8d2312cb0 | |||
| 5bbc2aaf2e | |||
| 7abc963a50 | |||
| f8c88cfbe0 | |||
| cf4acd5a8d | |||
| 19226df7d6 | |||
| c415c8f2af | |||
| 2751f41afb | |||
| d59cb99f0d | |||
| 007e8dbc75 | |||
| dae396a1ed | |||
| d894483166 | |||
| 4924cf2256 | |||
| 60ef52f44b | |||
| 240e1960f8 | |||
| a107d13e61 | |||
| f911936d79 | |||
| ea23957f2a | |||
| f1971cec7c | |||
| 7291c03cea | |||
| fdb3116027 | |||
| c87885be48 | |||
| 98f88d037e | |||
| fde1fdc592 | |||
| cca216ace5 | |||
| e953e949ef | |||
| fe2cc4b525 | |||
| 6a67fc3712 | |||
| 94b7c33485 | |||
| 887f0f4890 | |||
| ec08dfee9c | |||
| 54b95dced4 | |||
| 56bf53208e | |||
| 37d7409e82 | |||
| 4dd1f73a18 | |||
| 22cc8ac2c4 | |||
| a667acad07 | |||
| ee5e31d3b3 | |||
| c196b82a72 | |||
| 670370056c | |||
| 0c5a2bf02b | |||
| 8f9e78be0c | |||
| 50d5fa93b6 | |||
| 3a0e4078fd | |||
| 549da0257e | |||
| 2b5f9429a8 | |||
| c7119f4403 | |||
| 7d9862202a | |||
| dd7130d2d4 | |||
| d697bdcf05 | |||
| abd61919cf | |||
| ad2383de80 | |||
| 9f3ff98951 | |||
| d286980548 | |||
| 87533ac5a1 | |||
| d05451416d | |||
| 5c01cbad9e | |||
| df95ce39d0 | |||
| 26f16e392f | |||
| fcc837e570 | |||
| fd682306e7 | |||
| e072842b7a | |||
| 3b976c6812 | |||
| 40269328fb | |||
| 45cbbaf1cf | |||
| 3f542a642c | |||
| 8b4df5f02c | |||
| 788afb7189 | |||
| cd7fa5d09c | |||
| beb82045ff | |||
| 33f0fd5fe6 | |||
| 850051a299 | |||
| 8f7cbdf60a | |||
| 4577390130 | |||
| f2242e31c8 | |||
| 975a43d392 | |||
| 3d38e5e567 | |||
| 7d2ad92c4b | |||
| b82023bc32 | |||
| a92e04b6e0 | |||
| 56e61a85ee | |||
| 7c3d97b0a2 | |||
| 88a2f33b71 | |||
| b7557d750e | |||
| ce4bee3be7 | |||
| 5e746e3367 | |||
| 7d83b0d6c8 | |||
| 708277230a | |||
| 06e6e31443 | |||
| 8474f2f571 | |||
| 9ddea7d9d6 | |||
| c2fbd372b3 | |||
| e9611769be | |||
| c5dfa84ff2 | |||
| da68101c09 | |||
| 30c418542c | |||
| b58c1a7ed6 | |||
| 47e87281a1 | |||
| 60b6b93ff8 | |||
| 3149b6f750 | |||
| e44f1ad53e | |||
| c38c8a7fce | |||
| 9efc717633 | |||
| a2d422f5cb | |||
| 3036dd7cfe | |||
| 5be5d9247c | |||
| 42b7eea852 | |||
| 7ee3f6e4f7 | |||
| fbd8d995ed | |||
| 998c85d6f5 | |||
| 67dfc942a0 | |||
| 1cc8b373de | |||
| f045f3fccd | |||
| 691f6d9cdd | |||
| 8a8fb66eeb | |||
| e7044a93f6 | |||
| a9bcb46f38 | |||
| 16a812c8b8 | |||
| 27fe2622ec | |||
| 3d222136f9 | |||
| 524cdb7176 | |||
| 499dc10390 | |||
| e7bd3d401f | |||
| 5ec942cb5e | |||
| 6c255cd2f2 | |||
| 4f969d750a | |||
| bd2cbe20e0 | |||
| 6d8d6a91ef | |||
| 05c12b34e5 | |||
| 43b7a662c1 | |||
| a7e76db464 | |||
| b797a2fcd1 | |||
| 6e35f1a389 | |||
| 30d48e139c | |||
| e74fc6f198 | |||
| 34b103bbcb | |||
| 6734492a2d | |||
| 90217d61d0 | |||
| ba27a7f68c | |||
| 22fbd8bc9e | |||
| d175213369 | |||
| e00d211619 | |||
| c68e3cafe4 | |||
| 5a34483513 | |||
| 7bb0f0d2e6 | |||
| 96d7066085 | |||
| ae49946740 | |||
| fcc91c9b8a | |||
| 994fd41826 | |||
| 3282b38900 | |||
| 647b2acf48 | |||
| ef318f663e | |||
| 5771454400 | |||
| 6732e726d5 | |||
| 230c80c70e | |||
| 45503aa8c5 | |||
| a4d7fd0d95 | |||
| b6579122d1 | |||
| 42a67f8ad5 | |||
| 91eeefec68 | |||
| 8ab7aeb8b2 | |||
| 493fa1ef6a | |||
| 020549ef60 | |||
| dfc1f45f97 | |||
| 641e65ee95 | |||
| 24ca87e00d | |||
| 859cd1cbe6 | |||
| 79656d1344 | |||
| 759f979182 | |||
| 7c17e64090 | |||
| bf45f64a7e | |||
| 72890d5983 | |||
| e8e798d955 | |||
| 8a8b379bfc | |||
| ca491def83 | |||
| 5a597277a9 | |||
| 6f2afdc97b | |||
| 6e2e0d353a | |||
| ae8145f266 | |||
| c90fcd1ce1 | |||
| e0687db9e2 | |||
| 24310e2f7a | |||
| a1f0b86ab3 | |||
| 7f87c6e478 | |||
| a0145b4b24 | |||
| 2fcbb1d836 | |||
| a2beea1bbd | |||
| e5e55b7a50 | |||
| 0830d8342e | |||
| adb1b21e81 | |||
| 4788011249 | |||
| edfa09bb9f | |||
| 2eef7bdbd3 | |||
| 124556f4db | |||
| f1faa95897 | |||
| b84df84e53 | |||
| d528e167db | |||
| f151969fe1 | |||
| 7107508286 | |||
| cd2f90a7a1 | |||
| e1577b5ad3 | |||
| 251686608a | |||
| 993aa613fd | |||
| 3c1f7e4181 | |||
| 2ed67648c3 | |||
| 2fdfec6f21 | |||
| a0d8f3ae81 | |||
| 6d37cceb91 | |||
| fce41f4fc1 | |||
| c50e894a42 | |||
| 890fd78a6a | |||
| 518cae1476 | |||
| 545a105ba0 | |||
| 70b4bf779e | |||
| 7cf672da84 | |||
| fd5746a954 | |||
| 2c3813deb9 | |||
| 80f57a0292 | |||
| 3b7309d9f7 | |||
| 6df1e68a5f | |||
| df2e982090 | |||
| 902af5e5d7 | |||
| 7fe23c7bc5 | |||
| d0c3cb066c | |||
| 5666943559 | |||
| 1b41f61247 | |||
| e1342f06b7 | |||
| f535595d1f | |||
| 7415776e4d | |||
| bad7caa187 | |||
| 2473eee66b | |||
| a8b51ad619 | |||
| 49c4d45731 | |||
| f45fef29d8 | |||
| 699a995e8c | |||
| ce02b03a73 | |||
| 3e1b01073b | |||
| 3e4dce2413 | |||
| fef3091ecc | |||
| af7509ebaf | |||
| 487527f5a5 | |||
| bfd26560b1 | |||
| be3a1c5b5f | |||
| 1282b23c57 | |||
| 54afd0b50b | |||
| 0669cfbebf | |||
| d99bf122ea | |||
| 6ee748a87a | |||
| 0ebda76cc8 | |||
| ed5581d1d9 | |||
| 71c59cfe50 | |||
| 68b3dc6f37 | |||
| 3c83102e91 | |||
| 0e49a066ba | |||
| fcb786cf60 | |||
| c56b2cdd62 | |||
| 6309d323dc | |||
| db2937d4a3 | |||
| fe10a7e55f | |||
| dd77c3e1f0 | |||
| e5e1f6bd05 | |||
| c52f3ebdd6 | |||
| ac96b64c64 | |||
| fa8fd60ac6 | |||
| 9c9393e0cf | |||
| 47f32a5f55 | |||
| 8b26f9309f | |||
| 4027809f32 | |||
| 60250a32c2 | |||
| 6a4c73db03 | |||
| b28ffa9543 | |||
| 7836f2e47f | |||
| 648873978c | |||
| c51c13b4b8 | |||
| 9a7c7d2a4b | |||
| 5f17474ff4 | |||
| 3376bf8b99 | |||
| ad1bea088e | |||
| fa580c516e | |||
| 7f4c450553 | |||
| 761ff7ed5a | |||
| 117d767f05 | |||
| 8405bfe6f9 | |||
| ccdb1479f7 | |||
| c8f68f44af | |||
| 3954a555f8 | |||
| b6934922fa | |||
| 944e6f5569 | |||
| d51b36e80d | |||
| c9724e2024 | |||
| 830e476120 | |||
| fe2e372997 | |||
| a15deedf0d | |||
| 22bf8163cd | |||
| b8390331af | |||
| 47b740ff35 | |||
| 39c14e6556 | |||
| 57cd791348 | |||
| 3c612e284e | |||
| 8cd1ab5c8f | |||
| a6c22cadb8 | |||
| a628ecf72b | |||
| 8d70233d83 | |||
| ae89600201 | |||
| 934d43b525 | |||
| 858c04bacf | |||
| 2a5355b1f8 | |||
| 5cf2ac4c3e | |||
| 71173da5ad | |||
| e304f4f34f | |||
| 7d37f645ba | |||
| c50738005d | |||
| effff6f88d | |||
| 45b223a2ef | |||
| 90544ba713 | |||
| e55c2e9598 | |||
| 6ee52474e1 | |||
| 4bf9f0b96c | |||
| 79e2fa89df | |||
| 2ad0ded73f | |||
| 1ab05e5c3b | |||
| 4b4a1644ff | |||
| 7fd0ec8ce6 | |||
| 2d1e08b50e | |||
| b881c52118 | |||
| 6fb59949a2 | |||
| 7d41dc21c1 | |||
| 6365968dc3 | |||
| 1abb3c8c22 | |||
| 33f4bb45d1 | |||
| 4897994b35 | |||
| 0a773c82af | |||
| b34d970076 | |||
| 19cf781431 | |||
| 637e65e5a0 | |||
| b3f83fd363 | |||
| 02ac3a6814 | |||
| 97891d36ab | |||
| 65c87d5e0f | |||
| ae3b53540e | |||
| 0e9009b0de | |||
| be2864c34b | |||
| c9bdac2e03 | |||
| d0ac99fc69 | |||
| 040de3d973 | |||
| 1703380ebc | |||
| e935885cd3 | |||
| da809bb9d7 | |||
| c39c9aa1da | |||
| ad61662cc4 | |||
| e42bcd0115 | |||
| ad8c025393 | |||
| 1b0db3c8b0 | |||
| f9a8c1969c | |||
| 645c11f0bd | |||
| ece49a158e | |||
| b139b8fdd6 | |||
| b14aa4f0dc | |||
| 9b392a22e1 | |||
| 36547a7343 | |||
| 876390aa68 | |||
| 297ecfbae3 | |||
| eeb0012e7f | |||
| 35cf82f11c | |||
| 82f6c2c550 | |||
| 3e3988a67f | |||
| 2f4694dc95 | |||
| f072dab07b | |||
| fc02e6f4a5 | |||
| 0651a09a3c | |||
| 2c5f1e0417 | |||
| c9682ca64d | |||
| bceb024588 | |||
| 17bba4d4a2 | |||
| 485448cbc7 | |||
| 244dad447b | |||
| 22e63a7367 | |||
| 83907132b5 | |||
| 7dc9beb171 | |||
| 0664e46a4b | |||
| 2ca97a42c5 | |||
| 773e415dff | |||
| 9e673559c4 | |||
| 879ef603fe | |||
| 7e0a163f12 | |||
| 8e4088e08f | |||
| 59161c663b | |||
| 93252fc5d2 | |||
| e4b8d1807d | |||
| 33e0ccdd10 | |||
| d59139a2ab | |||
| df831833b1 | |||
| c065db6da1 | |||
| a55be809f3 | |||
| a9e1ebc0a8 | |||
| 55af09a350 | |||
| 199fdd6728 | |||
| 4035e91672 | |||
| bc9194d740 | |||
| f601c47218 | |||
| 066d559377 | |||
| 2c3219ffcb | |||
| cf88bf9c23 | |||
| b8303b9a22 | |||
| a3f084dcde | |||
| 0d6b8fc6fc | |||
| 261a936bb8 | |||
| 159d9425a7 | |||
| 3a50b3678d | |||
| 6fa352f407 | |||
| 4b80b2c233 | |||
| d881755503 | |||
| fd125ecc68 | |||
| 29f7f1a57d | |||
| 8ecaabfce9 | |||
| 1797ff67c0 | |||
| f1ba5e95ec | |||
| d8c0f9d1d9 | |||
| df4b5fc87d | |||
| d7cdc8b3b0 | |||
| 5b53ca7cf1 | |||
| 194d1dae51 | |||
| 61322ede6c | |||
| a8edaedc8b | |||
| 25145f72e5 | |||
| 3ddf8b5922 | |||
| dbe9e4aade | |||
| 715be4dad0 | |||
| 570b7d0d97 | |||
| 80ac0ab17f | |||
| 9ee8174d5f | |||
| 831aa03c9f | |||
| d372597bdb | |||
| 172437b6fc | |||
| 7640a42bfc | |||
| fde04bd625 | |||
| ad14a5ccba | |||
| 2348d12e9d | |||
| 5cafc05e13 | |||
| e982257271 | |||
| 340fd81778 | |||
| 223f94077f | |||
| f13aa21d0f | |||
| 2c34a17d88 | |||
| 6b005a666e | |||
| 1d1bcb0a63 | |||
| 3f5f1328e7 | |||
| 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 | |||
| 91a7b5be27 | |||
| a36359f3dd |
@@ -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,75 +0,0 @@
|
|||||||
name: docker
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'master'
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ github.repository }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=semver,pattern={{version}},enable=false
|
|
||||||
type=match,pattern=v(.*),group=1
|
|
||||||
|
|
||||||
- name: Docker meta Hardware
|
|
||||||
id: meta-hw
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: ${{ github.repository }}
|
|
||||||
flavor: |
|
|
||||||
suffix=-hardware
|
|
||||||
latest=false
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=semver,pattern={{version}},enable=false
|
|
||||||
type=match,pattern=v(.*),group=1
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Login to DockerHub
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: |
|
|
||||||
linux/amd64
|
|
||||||
linux/386
|
|
||||||
linux/arm/v7
|
|
||||||
linux/arm64/v8
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
||||||
- name: Build and push Hardware
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: hardware.Dockerfile
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta-hw.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-hw.outputs.labels }}
|
|
||||||
@@ -17,21 +17,37 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24
|
||||||
|
package-manager-cache: false
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install --no-package-lock
|
||||||
|
- name: Build docs
|
||||||
|
run: npm run docs:build
|
||||||
|
- name: Copy docs into website
|
||||||
|
run: rsync -a --exclude '.vitepress/' --exclude 'README.md' website/ website/.vitepress/dist/
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: website/.vitepress/dist
|
||||||
|
|
||||||
# Single deploy job since we're just deploying
|
# Single deploy job since we're just deploying
|
||||||
deploy:
|
deploy:
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v3
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v1
|
|
||||||
with:
|
|
||||||
path: './website'
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v1
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
name: release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
# push:
|
|
||||||
# tags:
|
|
||||||
# - 'v*'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Generate changelog
|
|
||||||
run: |
|
|
||||||
echo -e "$(git log $(git describe --tags --abbrev=0)..HEAD --oneline | awk '{print "- "$0}')" > CHANGELOG.md
|
|
||||||
- name: install lipo
|
|
||||||
run: |
|
|
||||||
curl -L -o /tmp/lipo https://github.com/konoui/lipo/releases/latest/download/lipo_Linux_amd64
|
|
||||||
chmod +x /tmp/lipo
|
|
||||||
mv /tmp/lipo /usr/local/bin
|
|
||||||
- name: Build Go binaries
|
|
||||||
run: |
|
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
export CGO_ENABLED=0
|
|
||||||
|
|
||||||
mkdir -p artifacts
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=amd64
|
|
||||||
export FILENAME=artifacts/go2rtc_win64.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=386
|
|
||||||
export FILENAME=artifacts/go2rtc_win32.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=windows
|
|
||||||
export GOARCH=arm64
|
|
||||||
export FILENAME=artifacts/go2rtc_win_arm64.zip
|
|
||||||
go build -ldflags "-s -w" -trimpath && 7z a -mx9 -sdel "$FILENAME" go2rtc.exe
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=amd64
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_amd64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=386
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_i386
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm64
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_arm64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm
|
|
||||||
export GOARM=7
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_arm
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=mipsle
|
|
||||||
export FILENAME=artifacts/go2rtc_linux_mipsel
|
|
||||||
go build -ldflags "-s -w" -trimpath -o "$FILENAME"
|
|
||||||
|
|
||||||
export GOOS=darwin
|
|
||||||
export GOARCH=amd64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o go2rtc.amd64
|
|
||||||
|
|
||||||
export GOOS=darwin
|
|
||||||
export GOARCH=arm64
|
|
||||||
go build -ldflags "-s -w" -trimpath -o go2rtc.arm64
|
|
||||||
|
|
||||||
export FILENAME=artifacts/go2rtc_mac_universal.zip
|
|
||||||
lipo -output go2rtc -create go2rtc.arm64 go2rtc.amd64 && 7z a -mx9 -sdel "$FILENAME" go2rtc
|
|
||||||
|
|
||||||
parallel --jobs $(nproc) "upx {}" ::: artifacts/go2rtc_linux_*
|
|
||||||
- name: Setup tmate session
|
|
||||||
uses: mxschmitt/action-tmate@v3
|
|
||||||
if: ${{ failure() }}
|
|
||||||
- name: Set env
|
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Create GitHub release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
files: artifacts/*
|
|
||||||
generate_release_notes: true
|
|
||||||
name: Release ${{ env.RELEASE_VERSION }}
|
|
||||||
body_path: CHANGELOG.md
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
+15
-14
@@ -1,11 +1,11 @@
|
|||||||
name: Test Build and Run
|
name: Test Build and Run
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
# push:
|
||||||
branches:
|
# branches:
|
||||||
- '*'
|
# - '*'
|
||||||
pull_request:
|
# pull_request:
|
||||||
merge_group:
|
# merge_group:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -21,12 +21,12 @@ jobs:
|
|||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.19'
|
go-version: '1.24'
|
||||||
|
|
||||||
- name: Build Go binary
|
- name: Build Go binary
|
||||||
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
run: go build -ldflags "-s -w" -trimpath -o ./go2rtc
|
||||||
@@ -70,15 +70,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
platforms: linux/${{ matrix.platform }}
|
platforms: linux/${{ matrix.platform }}
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
@@ -89,10 +90,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Hardware
|
- name: Build and push Hardware
|
||||||
if: matrix.platform == 'amd64'
|
if: matrix.platform == 'amd64'
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: hardware.Dockerfile
|
file: docker/hardware.Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: false
|
push: false
|
||||||
load: true
|
load: true
|
||||||
|
|||||||
+19
-3
@@ -1,8 +1,24 @@
|
|||||||
|
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
.tmp/
|
.tmp/
|
||||||
|
|
||||||
go2rtc.yaml
|
go2rtc.yaml
|
||||||
|
|
||||||
go2rtc.json
|
go2rtc.json
|
||||||
|
|
||||||
|
go2rtc_freebsd*
|
||||||
|
go2rtc_linux*
|
||||||
|
go2rtc_mac*
|
||||||
|
go2rtc_win*
|
||||||
|
|
||||||
|
/go2rtc
|
||||||
|
/go2rtc.exe
|
||||||
|
|
||||||
|
0_test.go
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
website/.vitepress/cache
|
||||||
|
website/.vitepress/dist
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
CLAUDE.md
|
||||||
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,19 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "Starting go2rtc..." >&2
|
|
||||||
|
|
||||||
readonly config_path="/config"
|
|
||||||
|
|
||||||
if [[ -x "${config_path}/go2rtc" ]]; then
|
|
||||||
readonly binary_path="${config_path}/go2rtc"
|
|
||||||
echo "Using go2rtc binary from '${binary_path}' instead of the embedded one" >&2
|
|
||||||
else
|
|
||||||
readonly binary_path="/usr/local/bin/go2rtc"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# set cwd for go2rtc (for config file, Hass integration, etc)
|
|
||||||
cd "${config_path}" || echo "Could not change working directory to '${config_path}'" >&2
|
|
||||||
|
|
||||||
exec "${binary_path}"
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
**Project layout**
|
|
||||||
|
|
||||||
- https://github.com/golang-standards/project-layout
|
|
||||||
- https://github.com/micro/micro
|
|
||||||
-169
@@ -1,169 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod struct {
|
|
||||||
Listen string `yaml:"listen"`
|
|
||||||
Username string `yaml:"username"`
|
|
||||||
Password string `yaml:"password"`
|
|
||||||
BasePath string `yaml:"base_path"`
|
|
||||||
StaticDir string `yaml:"static_dir"`
|
|
||||||
Origin string `yaml:"origin"`
|
|
||||||
} `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(cfg.Mod.Origin)
|
|
||||||
|
|
||||||
HandleFunc("api", apiHandler)
|
|
||||||
HandleFunc("api/config", configHandler)
|
|
||||||
HandleFunc("api/exit", exitHandler)
|
|
||||||
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")
|
|
||||||
|
|
||||||
Handler = http.DefaultServeMux // 4th
|
|
||||||
|
|
||||||
if cfg.Mod.Origin == "*" {
|
|
||||||
Handler = middlewareCORS(Handler) // 3rd
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Mod.Username != "" {
|
|
||||||
Handler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, Handler) // 2nd
|
|
||||||
}
|
|
||||||
|
|
||||||
if log.Trace().Enabled() {
|
|
||||||
Handler = middlewareLog(Handler) // 1st
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
s := http.Server{}
|
|
||||||
s.Handler = Handler
|
|
||||||
if err = s.Serve(listener); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("[api] serve")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
log.Trace().Str("path", pattern).Msg("[api] register path")
|
|
||||||
http.HandleFunc(pattern, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
const StreamNotFound = "stream not found"
|
|
||||||
|
|
||||||
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 middlewareAuth(username, password string, next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !strings.HasPrefix(r.RemoteAddr, "127.") && !strings.HasPrefix(r.RemoteAddr, "[::1]") {
|
|
||||||
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")
|
|
||||||
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()
|
|
||||||
|
|
||||||
if err := json.NewEncoder(w).Encode(app.Info); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, _ := strconv.Atoi(s)
|
|
||||||
os.Exit(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Stream struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResponseStreams(w http.ResponseWriter, streams []Stream) {
|
|
||||||
if len(streams) == 0 {
|
|
||||||
http.Error(w, "no streams", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var response struct {
|
|
||||||
Streams []Stream `json:"streams"`
|
|
||||||
}
|
|
||||||
response.Streams = streams
|
|
||||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Version = "1.3.1"
|
|
||||||
var UserAgent = "go2rtc/" + Version
|
|
||||||
|
|
||||||
var ConfigPath string
|
|
||||||
var Info = map[string]any{
|
|
||||||
"version": Version,
|
|
||||||
}
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var confs Config
|
|
||||||
var version bool
|
|
||||||
|
|
||||||
flag.Var(&confs, "config", "go2rtc config (path to file or raw text), support multiple")
|
|
||||||
flag.BoolVar(&version, "version", false, "Print the version of the application and exit")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if version {
|
|
||||||
fmt.Println("Current version: ", Version)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if confs == nil {
|
|
||||||
confs = []string{"go2rtc.yaml"}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, conf := range confs {
|
|
||||||
if conf[0] != '{' {
|
|
||||||
// config as file
|
|
||||||
if ConfigPath == "" {
|
|
||||||
ConfigPath = conf
|
|
||||||
}
|
|
||||||
|
|
||||||
data, _ := os.ReadFile(conf)
|
|
||||||
if data == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data = []byte(shell.ReplaceEnvVars(string(data)))
|
|
||||||
configs = append(configs, data)
|
|
||||||
} else {
|
|
||||||
// config as raw YAML
|
|
||||||
configs = append(configs, []byte(conf))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ConfigPath != "" {
|
|
||||||
if !filepath.IsAbs(ConfigPath) {
|
|
||||||
if cwd, err := os.Getwd(); err == nil {
|
|
||||||
ConfigPath = filepath.Join(cwd, ConfigPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Info["config_path"] = ConfigPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]string `yaml:"log"`
|
|
||||||
}
|
|
||||||
|
|
||||||
LoadConfig(&cfg)
|
|
||||||
|
|
||||||
log.Logger = NewLogger(cfg.Mod["format"], cfg.Mod["level"])
|
|
||||||
|
|
||||||
modules = cfg.Mod
|
|
||||||
|
|
||||||
log.Info().Msgf("go2rtc version %s %s/%s", Version, runtime.GOOS, runtime.GOARCH)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogger(format string, level string) zerolog.Logger {
|
|
||||||
var writer io.Writer = os.Stdout
|
|
||||||
|
|
||||||
if format != "json" {
|
|
||||||
writer = zerolog.ConsoleWriter{
|
|
||||||
Out: writer, TimeFormat: "15:04:05.000",
|
|
||||||
NoColor: writer != os.Stdout || format == "text",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
|
||||||
|
|
||||||
lvl, err := zerolog.ParseLevel(level)
|
|
||||||
if err != nil || lvl == zerolog.NoLevel {
|
|
||||||
lvl = zerolog.InfoLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
return zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadConfig(v any) {
|
|
||||||
for _, data := range configs {
|
|
||||||
if err := yaml.Unmarshal(data, v); err != nil {
|
|
||||||
log.Warn().Err(err).Msg("[app] read config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetLogger(module string) zerolog.Logger {
|
|
||||||
if s, ok := modules[module]; ok {
|
|
||||||
lvl, err := zerolog.ParseLevel(s)
|
|
||||||
if err == nil {
|
|
||||||
return log.Level(lvl)
|
|
||||||
}
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
return log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// internal
|
|
||||||
|
|
||||||
type Config []string
|
|
||||||
|
|
||||||
func (c *Config) String() string {
|
|
||||||
return strings.Join(*c, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Config) Set(value string) error {
|
|
||||||
*c = append(*c, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var configs [][]byte
|
|
||||||
|
|
||||||
// modules log levels
|
|
||||||
var modules map[string]string
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package store
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
const name = "go2rtc.json"
|
|
||||||
|
|
||||||
var store map[string]any
|
|
||||||
|
|
||||||
func load() {
|
|
||||||
data, _ := os.ReadFile(name)
|
|
||||||
if data != nil {
|
|
||||||
if err := json.Unmarshal(data, &store); err != nil {
|
|
||||||
// TODO: log
|
|
||||||
log.Warn().Err(err).Msg("[app] read storage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if store == nil {
|
|
||||||
store = make(map[string]any)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func save() error {
|
|
||||||
data, err := json.Marshal(store)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return os.WriteFile(name, data, 0644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRaw(key string) any {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
return store[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDict(key string) map[string]any {
|
|
||||||
raw := GetRaw(key)
|
|
||||||
if raw != nil {
|
|
||||||
return raw.(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
return make(map[string]any)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Set(key string, v any) error {
|
|
||||||
if store == nil {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
store[key] = v
|
|
||||||
|
|
||||||
return save()
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package debug
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
api.HandleFunc("api/stack", stackHandler)
|
|
||||||
|
|
||||||
streams.HandleFunc("null", nullHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func nullHandler(string) (core.Producer, error) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package dvrip
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/dvrip"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("dvrip", handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn := dvrip.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Play(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Handle(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -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/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"os/exec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
log := app.GetLogger("echo")
|
|
||||||
|
|
||||||
streams.HandleFunc("echo", func(url string) (core.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,120 +0,0 @@
|
|||||||
package exec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/shell"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
// depends on RTSP server
|
|
||||||
if rtsp.Port == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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", Handle)
|
|
||||||
|
|
||||||
log = app.GetLogger("exec")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Handle(url string) (core.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
|
|
||||||
}
|
|
||||||
if log.Debug().Enabled() {
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := make(chan core.Producer)
|
|
||||||
|
|
||||||
waitersMu.Lock()
|
|
||||||
waiters[path] = ch
|
|
||||||
waitersMu.Unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
waitersMu.Lock()
|
|
||||||
delete(waiters, path)
|
|
||||||
waitersMu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
chErr := make(chan error)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := cmd.Wait()
|
|
||||||
// unblocking write to channel
|
|
||||||
select {
|
|
||||||
case chErr <- err:
|
|
||||||
default:
|
|
||||||
log.Trace().Str("url", url).Msg("[exec] close")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Second * 60):
|
|
||||||
_ = cmd.Process.Kill()
|
|
||||||
log.Error().Str("url", url).Msg("[exec] timeout")
|
|
||||||
return nil, errors.New("timeout")
|
|
||||||
case err := <-chErr:
|
|
||||||
return nil, fmt.Errorf("exec: %s", err)
|
|
||||||
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 core.Producer{}
|
|
||||||
var waitersMu sync.Mutex
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
|
||||||
const deviceInputPrefix = "-f avfoundation"
|
|
||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|
||||||
video := findMedia(core.KindVideo, videoIdx)
|
|
||||||
audio := findMedia(core.KindAudio, audioIdx)
|
|
||||||
switch {
|
|
||||||
case video != nil && audio != nil:
|
|
||||||
return `"` + video.ID + `:` + audio.ID + `"`
|
|
||||||
case video != nil:
|
|
||||||
return `"` + video.ID + `"`
|
|
||||||
case audio != nil:
|
|
||||||
return `"` + audio.ID + `"`
|
|
||||||
}
|
|
||||||
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 = core.KindVideo
|
|
||||||
continue
|
|
||||||
case strings.HasSuffix(line, "audio devices:"):
|
|
||||||
kind = core.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) *core.Media {
|
|
||||||
return &core.Media{Kind: kind, ID: name}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"io/ioutil"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://trac.ffmpeg.org/wiki/Capture/Webcam
|
|
||||||
const deviceInputPrefix = "-f v4l2"
|
|
||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|
||||||
video := findMedia(core.KindVideo, videoIdx)
|
|
||||||
return video.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
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(), core.KindVideo) {
|
|
||||||
media := loadMedia(core.KindVideo, "/dev/"+file.Name())
|
|
||||||
if media != nil {
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadMedia(kind, name string) *core.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 &core.Media{Kind: kind, ID: name}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// https://trac.ffmpeg.org/wiki/DirectShow
|
|
||||||
const deviceInputPrefix = "-f dshow"
|
|
||||||
|
|
||||||
func deviceInputSuffix(videoIdx, audioIdx int) string {
|
|
||||||
video := findMedia(core.KindVideo, videoIdx)
|
|
||||||
audio := findMedia(core.KindAudio, audioIdx)
|
|
||||||
switch {
|
|
||||||
case video != nil && audio != nil:
|
|
||||||
return `video="` + video.ID + `":audio=` + audio.ID + `"`
|
|
||||||
case video != nil:
|
|
||||||
return `video="` + video.ID + `"`
|
|
||||||
case audio != nil:
|
|
||||||
return `audio="` + audio.ID + `"`
|
|
||||||
}
|
|
||||||
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 = core.KindVideo
|
|
||||||
} else if strings.HasSuffix(line, "(audio)") {
|
|
||||||
kind = core.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) *core.Media {
|
|
||||||
return &core.Media{Kind: kind, ID: name}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package device
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"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 []*core.Media
|
|
||||||
|
|
||||||
func findMedia(kind string, index int) *core.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()
|
|
||||||
}
|
|
||||||
|
|
||||||
var items []api.Stream
|
|
||||||
var iv, ia int
|
|
||||||
|
|
||||||
for _, media := range medias {
|
|
||||||
var source string
|
|
||||||
switch media.Kind {
|
|
||||||
case core.KindVideo:
|
|
||||||
source = "ffmpeg:device?video=" + strconv.Itoa(iv)
|
|
||||||
iv++
|
|
||||||
case core.KindAudio:
|
|
||||||
source = "ffmpeg:device?audio=" + strconv.Itoa(ia)
|
|
||||||
ia++
|
|
||||||
}
|
|
||||||
items = append(items, api.Stream{Name: media.ID, URL: source})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.ResponseStreams(w, items)
|
|
||||||
}
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/exec"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/ffmpeg/device"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/rtsp"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]string `yaml:"ffmpeg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.Mod = defaults // will be overriden from yaml
|
|
||||||
|
|
||||||
app.LoadConfig(&cfg)
|
|
||||||
|
|
||||||
if app.GetLogger("exec").GetLevel() >= 0 {
|
|
||||||
defaults["global"] += " -v error"
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.HandleFunc("ffmpeg", func(url string) (core.Producer, error) {
|
|
||||||
args := parseArgs(url[7:]) // remove `ffmpeg:`
|
|
||||||
if args == nil {
|
|
||||||
return nil, errors.New("can't generate ffmpeg command")
|
|
||||||
}
|
|
||||||
return exec.Handle("exec:" + args.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
device.Bin = defaults["bin"]
|
|
||||||
device.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaults = map[string]string{
|
|
||||||
"bin": "ffmpeg",
|
|
||||||
"global": "-hide_banner",
|
|
||||||
|
|
||||||
// inputs
|
|
||||||
"file": "-re -i {input}",
|
|
||||||
"http": "-fflags nobuffer -flags low_delay -i {input}",
|
|
||||||
"rtsp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i {input}",
|
|
||||||
|
|
||||||
"rtsp/udp": "-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}",
|
|
||||||
|
|
||||||
// output
|
|
||||||
"output": "-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}",
|
|
||||||
|
|
||||||
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
|
|
||||||
// `-tune zerolatency` - for minimal latency
|
|
||||||
// `-profile high -level 4.1` - most used streaming profile
|
|
||||||
"h264": "-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency",
|
|
||||||
"h265": "-c:v libx265 -g 50 -profile:v high -level:v 5.1 -preset:v superfast -tune:v zerolatency",
|
|
||||||
"mjpeg": "-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p",
|
|
||||||
|
|
||||||
"opus": "-c:a libopus -ar:a 48000 -ac:a 2",
|
|
||||||
"pcmu": "-c:a pcm_mulaw -ar:a 8000 -ac:a 1",
|
|
||||||
"pcmu/16000": "-c:a pcm_mulaw -ar:a 16000 -ac:a 1",
|
|
||||||
"pcmu/48000": "-c:a pcm_mulaw -ar:a 48000 -ac:a 1",
|
|
||||||
"pcma": "-c:a pcm_alaw -ar:a 8000 -ac:a 1",
|
|
||||||
"pcma/16000": "-c:a pcm_alaw -ar:a 16000 -ac:a 1",
|
|
||||||
"pcma/48000": "-c:a pcm_alaw -ar:a 48000 -ac:a 1",
|
|
||||||
"aac": "-c:a aac", // keep sample rate and channels
|
|
||||||
"aac/16000": "-c:a aac -ar:a 16000 -ac:a 1",
|
|
||||||
"mp3": "-c:a libmp3lame -q:a 8",
|
|
||||||
"pcm": "-c:a pcm_s16be",
|
|
||||||
"pcm/8000": "-c:a pcm_s16be -ar:a 8000 -ac:a 1",
|
|
||||||
"pcm/16000": "-c:a pcm_s16be -ar:a 16000 -ac:a 1",
|
|
||||||
"pcm/48000": "-c:a pcm_s16be -ar:a 48000 -ac:a 1",
|
|
||||||
|
|
||||||
// hardware Intel and AMD on Linux
|
|
||||||
// better not to set `-async_depth:v 1` like for QSV, because framedrops
|
|
||||||
// `-bf 0` - disable B-frames is very important
|
|
||||||
"h264/vaapi": "-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0",
|
|
||||||
"h265/vaapi": "-c:v hevc_vaapi -g 50 -bf 0 -profile:v high -level:v 5.1 -sei:v 0",
|
|
||||||
"mjpeg/vaapi": "-c:v mjpeg_vaapi",
|
|
||||||
|
|
||||||
// hardware Raspberry
|
|
||||||
"h264/v4l2m2m": "-c:v h264_v4l2m2m -g 50 -bf 0",
|
|
||||||
"h265/v4l2m2m": "-c:v hevc_v4l2m2m -g 50 -bf 0",
|
|
||||||
|
|
||||||
// hardware NVidia on Linux and Windows
|
|
||||||
// preset=p2 - faster, tune=ll - low latency
|
|
||||||
"h264/cuda": "-c:v h264_nvenc -g 50 -profile:v high -level:v auto -preset:v p2 -tune:v ll",
|
|
||||||
"h265/cuda": "-c:v hevc_nvenc -g 50 -profile:v high -level:v auto",
|
|
||||||
|
|
||||||
// hardware Intel on Windows
|
|
||||||
"h264/dxva2": "-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1",
|
|
||||||
"h265/dxva2": "-c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1",
|
|
||||||
"mjpeg/dxva2": "-c:v mjpeg_qsv -profile:v high -level:v 5.1",
|
|
||||||
|
|
||||||
// hardware macOS
|
|
||||||
"h264/videotoolbox": "-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1",
|
|
||||||
"h265/videotoolbox": "-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1",
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputTemplate - select input template from YAML config by template name
|
|
||||||
// if query has input param - select another tempalte by this name
|
|
||||||
// if there is no another template - use input param as template
|
|
||||||
func inputTemplate(name, s string, query url.Values) string {
|
|
||||||
var template string
|
|
||||||
if input := query.Get("input"); input != "" {
|
|
||||||
if template = defaults[input]; template == "" {
|
|
||||||
template = input
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
template = defaults[name]
|
|
||||||
}
|
|
||||||
return strings.Replace(template, "{input}", s, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseArgs(s string) *Args {
|
|
||||||
// init FFmpeg arguments
|
|
||||||
args := &Args{
|
|
||||||
bin: defaults["bin"],
|
|
||||||
global: defaults["global"],
|
|
||||||
output: defaults["output"],
|
|
||||||
}
|
|
||||||
|
|
||||||
var query url.Values
|
|
||||||
if i := strings.IndexByte(s, '#'); i > 0 {
|
|
||||||
query = parseQuery(s[i+1:])
|
|
||||||
args.video = len(query["video"])
|
|
||||||
args.audio = len(query["audio"])
|
|
||||||
s = s[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse input:
|
|
||||||
// 1. Input as xxxx:// link (http or rtsp or any other)
|
|
||||||
// 2. Input as stream name
|
|
||||||
// 3. Input as FFmpeg device (local USB camera)
|
|
||||||
if i := strings.Index(s, "://"); i > 0 {
|
|
||||||
switch s[:i] {
|
|
||||||
case "http", "https", "rtmp":
|
|
||||||
args.input = inputTemplate("http", s, query)
|
|
||||||
case "rtsp", "rtsps":
|
|
||||||
// https://ffmpeg.org/ffmpeg-protocols.html#rtsp
|
|
||||||
// skip unnecessary input tracks
|
|
||||||
switch {
|
|
||||||
case (args.video > 0 && args.audio > 0) || (args.video == 0 && args.audio == 0):
|
|
||||||
args.input = "-allowed_media_types video+audio "
|
|
||||||
case args.video > 0:
|
|
||||||
args.input = "-allowed_media_types video "
|
|
||||||
case args.audio > 0:
|
|
||||||
args.input = "-allowed_media_types audio "
|
|
||||||
}
|
|
||||||
|
|
||||||
args.input += inputTemplate("rtsp", s, query)
|
|
||||||
default:
|
|
||||||
args.input = "-i " + s
|
|
||||||
}
|
|
||||||
} else if streams.Get(s) != nil {
|
|
||||||
s = "rtsp://localhost:" + rtsp.Port + "/" + s
|
|
||||||
switch {
|
|
||||||
case args.video > 0 && args.audio == 0:
|
|
||||||
s += "?video"
|
|
||||||
case args.audio > 0 && args.video == 0:
|
|
||||||
s += "?audio"
|
|
||||||
default:
|
|
||||||
s += "?video&audio"
|
|
||||||
}
|
|
||||||
args.input = inputTemplate("rtsp", s, query)
|
|
||||||
} else if strings.HasPrefix(s, "device?") {
|
|
||||||
var err error
|
|
||||||
args.input, err = device.GetInput(s)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args.input = inputTemplate("file", s, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
if query["async"] != nil {
|
|
||||||
args.input = "-use_wallclock_as_timestamps 1 -async 1 " + args.input
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse query params:
|
|
||||||
// 1. `width`/`height` params
|
|
||||||
// 2. `rotate` param
|
|
||||||
// 3. `video` params (support multiple)
|
|
||||||
// 4. `audio` params (support multiple)
|
|
||||||
// 5. `hardware` param
|
|
||||||
if query != nil {
|
|
||||||
// 1. Process raw params for FFmpeg
|
|
||||||
for _, raw := range query["raw"] {
|
|
||||||
args.AddCodec(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Process video filters (resize and rotation)
|
|
||||||
if query["width"] != nil || query["height"] != nil {
|
|
||||||
filter := "scale="
|
|
||||||
if query["width"] != nil {
|
|
||||||
filter += query["width"][0]
|
|
||||||
} else {
|
|
||||||
filter += "-1"
|
|
||||||
}
|
|
||||||
filter += ":"
|
|
||||||
if query["height"] != nil {
|
|
||||||
filter += query["height"][0]
|
|
||||||
} else {
|
|
||||||
filter += "-1"
|
|
||||||
}
|
|
||||||
args.AddFilter(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if query["rotate"] != nil {
|
|
||||||
var filter string
|
|
||||||
switch query["rotate"][0] {
|
|
||||||
case "90":
|
|
||||||
filter = "transpose=1" // 90 degrees clockwise
|
|
||||||
case "180":
|
|
||||||
filter = "transpose=1,transpose=1"
|
|
||||||
case "-90", "270":
|
|
||||||
filter = "transpose=2" // 90 degrees counterclockwise
|
|
||||||
}
|
|
||||||
if filter != "" {
|
|
||||||
args.AddFilter(filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Process video codecs
|
|
||||||
if args.video > 0 {
|
|
||||||
for _, video := range query["video"] {
|
|
||||||
if video != "copy" {
|
|
||||||
if codec := defaults[video]; codec != "" {
|
|
||||||
args.AddCodec(codec)
|
|
||||||
} else {
|
|
||||||
args.AddCodec(video)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args.AddCodec("-c:v copy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args.AddCodec("-vn")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Process audio codecs
|
|
||||||
if args.audio > 0 {
|
|
||||||
for _, audio := range query["audio"] {
|
|
||||||
if audio != "copy" {
|
|
||||||
if codec := defaults[audio]; codec != "" {
|
|
||||||
args.AddCodec(codec)
|
|
||||||
} else {
|
|
||||||
args.AddCodec(audio)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args.AddCodec("-c:a copy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
args.AddCodec("-an")
|
|
||||||
}
|
|
||||||
|
|
||||||
if query["hardware"] != nil {
|
|
||||||
MakeHardware(args, query["hardware"][0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.codecs == nil {
|
|
||||||
args.AddCodec("-c copy")
|
|
||||||
}
|
|
||||||
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type Args struct {
|
|
||||||
bin string // ffmpeg
|
|
||||||
global string // -hide_banner -v error
|
|
||||||
input string // -re -stream_loop -1 -i /media/bunny.mp4
|
|
||||||
codecs []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency
|
|
||||||
filters []string // scale=1920:1080
|
|
||||||
output string // -f rtsp {output}
|
|
||||||
|
|
||||||
video, audio int // count of video and audio params
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Args) AddCodec(codec string) {
|
|
||||||
a.codecs = append(a.codecs, codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Args) AddFilter(filter string) {
|
|
||||||
a.filters = append(a.filters, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Args) InsertFilter(filter string) {
|
|
||||||
a.filters = append([]string{filter}, a.filters...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Args) String() string {
|
|
||||||
b := bytes.NewBuffer(make([]byte, 0, 512))
|
|
||||||
|
|
||||||
b.WriteString(a.bin)
|
|
||||||
|
|
||||||
if a.global != "" {
|
|
||||||
b.WriteByte(' ')
|
|
||||||
b.WriteString(a.global)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteByte(' ')
|
|
||||||
b.WriteString(a.input)
|
|
||||||
|
|
||||||
multimode := a.video > 1 || a.audio > 1
|
|
||||||
var iv, ia int
|
|
||||||
|
|
||||||
for _, codec := range a.codecs {
|
|
||||||
// support multiple video and/or audio codecs
|
|
||||||
if multimode && len(codec) >= 5 {
|
|
||||||
switch codec[:5] {
|
|
||||||
case "-c:v ":
|
|
||||||
codec = "-map 0:v:0? " + strings.ReplaceAll(codec, ":v ", ":v:"+strconv.Itoa(iv)+" ")
|
|
||||||
iv++
|
|
||||||
case "-c:a ":
|
|
||||||
codec = "-map 0:a:0? " + strings.ReplaceAll(codec, ":a ", ":a:"+strconv.Itoa(ia)+" ")
|
|
||||||
ia++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteByte(' ')
|
|
||||||
b.WriteString(codec)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.filters != nil {
|
|
||||||
for i, filter := range a.filters {
|
|
||||||
if i == 0 {
|
|
||||||
b.WriteString(" -vf ")
|
|
||||||
} else {
|
|
||||||
b.WriteByte(',')
|
|
||||||
}
|
|
||||||
b.WriteString(filter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteByte(' ')
|
|
||||||
b.WriteString(a.output)
|
|
||||||
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseArgs(t *testing.T) {
|
|
||||||
args := parseArgs("rtsp://example.com#video=h264#rotate=180")
|
|
||||||
assert.Equal(t, "ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -an -vf transpose=1,transpose=1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
|
||||||
|
|
||||||
args = parseArgs("rtsp://example.com#video=h264#rotate=180#hardware=vaapi")
|
|
||||||
assert.Equal(t, "ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_transport tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf format=vaapi|nv12,hwupload,transpose_vaapi=4 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}", args.String())
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
EngineSoftware = "software"
|
|
||||||
EngineVAAPI = "vaapi" // Intel iGPU and AMD GPU
|
|
||||||
EngineV4L2M2M = "v4l2m2m" // Raspberry Pi 3 and 4
|
|
||||||
EngineCUDA = "cuda" // NVidia on Windows and Linux
|
|
||||||
EngineDXVA2 = "dxva2" // Intel on Windows
|
|
||||||
EngineVideoToolbox = "videotoolbox" // macOS
|
|
||||||
)
|
|
||||||
|
|
||||||
var cache = map[string]string{}
|
|
||||||
|
|
||||||
// MakeHardware converts software FFmpeg args to hardware args
|
|
||||||
// empty engine for autoselect
|
|
||||||
func MakeHardware(args *Args, engine string) {
|
|
||||||
for i, codec := range args.codecs {
|
|
||||||
if len(codec) < 12 {
|
|
||||||
continue // skip short line (-c:v libx264...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get current codec name
|
|
||||||
name := cut(codec, ' ', 1)
|
|
||||||
switch name {
|
|
||||||
case "libx264":
|
|
||||||
name = "h264"
|
|
||||||
case "libx265":
|
|
||||||
name = "h265"
|
|
||||||
case "mjpeg":
|
|
||||||
default:
|
|
||||||
continue // skip unsupported codec
|
|
||||||
}
|
|
||||||
|
|
||||||
// temporary disable probe for H265 and MJPEG
|
|
||||||
if engine == "" && name == "h264" {
|
|
||||||
if engine = cache[name]; engine == "" {
|
|
||||||
engine = ProbeHardware(name)
|
|
||||||
cache[name] = engine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch engine {
|
|
||||||
case EngineVAAPI:
|
|
||||||
args.input = "-hwaccel vaapi -hwaccel_output_format vaapi " + args.input
|
|
||||||
args.codecs[i] = defaults[name+"/"+engine]
|
|
||||||
|
|
||||||
for i, filter := range args.filters {
|
|
||||||
if strings.HasPrefix(filter, "scale=") {
|
|
||||||
args.filters[i] = "scale_vaapi=" + filter[6:]
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(filter, "transpose=") {
|
|
||||||
if filter == "transpose=1,transpose=1" { // 180 degrees half-turn
|
|
||||||
args.filters[i] = "transpose_vaapi=4" // reversal
|
|
||||||
} else {
|
|
||||||
args.filters[i] = "transpose_vaapi=" + filter[10:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fix if input doesn't support hwaccel, do nothing when support
|
|
||||||
args.InsertFilter("format=vaapi|nv12,hwupload")
|
|
||||||
|
|
||||||
case EngineCUDA:
|
|
||||||
args.input = "-hwaccel cuda -hwaccel_output_format cuda -extra_hw_frames 2 " + args.input
|
|
||||||
args.codecs[i] = defaults[name+"/"+engine]
|
|
||||||
|
|
||||||
for i, filter := range args.filters {
|
|
||||||
if strings.HasPrefix(filter, "scale=") {
|
|
||||||
args.filters[i] = "scale_cuda=" + filter[6:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case EngineDXVA2:
|
|
||||||
args.input = "-hwaccel dxva2 -hwaccel_output_format dxva2_vld " + args.input
|
|
||||||
args.codecs[i] = defaults[name+"/"+engine]
|
|
||||||
|
|
||||||
for i, filter := range args.filters {
|
|
||||||
if strings.HasPrefix(filter, "scale=") {
|
|
||||||
args.filters[i] = "scale_qsv=" + filter[6:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args.InsertFilter("hwmap=derive_device=qsv,format=qsv")
|
|
||||||
|
|
||||||
case EngineVideoToolbox:
|
|
||||||
args.input = "-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld " + args.input
|
|
||||||
args.codecs[i] = defaults[name+"/"+engine]
|
|
||||||
|
|
||||||
case EngineV4L2M2M:
|
|
||||||
args.codecs[i] = defaults[name+"/"+engine]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(arg ...string) bool {
|
|
||||||
err := exec.Command(defaults["bin"], arg...).Run()
|
|
||||||
log.Printf("%v %v", arg, err)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cut(s string, sep byte, pos int) string {
|
|
||||||
for n := 0; n < pos; n++ {
|
|
||||||
if i := strings.IndexByte(s, sep); i > 0 {
|
|
||||||
s = s[i+1:]
|
|
||||||
} else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if i := strings.IndexByte(s, sep); i > 0 {
|
|
||||||
return s[:i]
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
func ProbeHardware(name string) string {
|
|
||||||
switch name {
|
|
||||||
case "h264":
|
|
||||||
if run(
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "h264_videotoolbox", "-f", "null", "-") {
|
|
||||||
return EngineVideoToolbox
|
|
||||||
}
|
|
||||||
|
|
||||||
case "h265":
|
|
||||||
if run(
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "hevc_videotoolbox", "-f", "null", "-") {
|
|
||||||
return EngineVideoToolbox
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EngineSoftware
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ProbeHardware(name string) string {
|
|
||||||
if runtime.GOARCH == "arm64" || runtime.GOARCH == "arm" {
|
|
||||||
switch name {
|
|
||||||
case "h264":
|
|
||||||
if run(
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "h264_v4l2m2m", "-f", "null", "-") {
|
|
||||||
return EngineV4L2M2M
|
|
||||||
}
|
|
||||||
|
|
||||||
case "h265":
|
|
||||||
if run(
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "hevc_v4l2m2m", "-f", "null", "-") {
|
|
||||||
return EngineV4L2M2M
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EngineSoftware
|
|
||||||
}
|
|
||||||
|
|
||||||
switch name {
|
|
||||||
case "h264":
|
|
||||||
if run("-init_hw_device", "cuda",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
|
||||||
return EngineCUDA
|
|
||||||
}
|
|
||||||
|
|
||||||
if run("-init_hw_device", "vaapi",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-vf", "format=nv12,hwupload",
|
|
||||||
"-c", "h264_vaapi", "-f", "null", "-") {
|
|
||||||
return EngineVAAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
case "h265":
|
|
||||||
if run("-init_hw_device", "cuda",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
|
||||||
return EngineCUDA
|
|
||||||
}
|
|
||||||
|
|
||||||
if run("-init_hw_device", "vaapi",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-vf", "format=nv12,hwupload",
|
|
||||||
"-c", "hevc_vaapi", "-f", "null", "-") {
|
|
||||||
return EngineVAAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mjpeg":
|
|
||||||
if run("-init_hw_device", "vaapi",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-vf", "format=nv12,hwupload",
|
|
||||||
"-c", "mjpeg_vaapi", "-f", "null", "-") {
|
|
||||||
return EngineVAAPI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EngineSoftware
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package ffmpeg
|
|
||||||
|
|
||||||
func ProbeHardware(name string) string {
|
|
||||||
switch name {
|
|
||||||
case "h264":
|
|
||||||
if run("-init_hw_device", "cuda",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "h264_nvenc", "-f", "null", "-") {
|
|
||||||
return EngineCUDA
|
|
||||||
}
|
|
||||||
|
|
||||||
if run("-init_hw_device", "dxva2",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "h264_qsv", "-f", "null", "-") {
|
|
||||||
return EngineDXVA2
|
|
||||||
}
|
|
||||||
|
|
||||||
case "h265":
|
|
||||||
if run("-init_hw_device", "cuda",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "hevc_nvenc", "-f", "null", "-") {
|
|
||||||
return EngineCUDA
|
|
||||||
}
|
|
||||||
|
|
||||||
if run("-init_hw_device", "dxva2",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "hevc_qsv", "-f", "null", "-") {
|
|
||||||
return EngineDXVA2
|
|
||||||
}
|
|
||||||
|
|
||||||
case "mjpeg":
|
|
||||||
if run("-init_hw_device", "dxva2",
|
|
||||||
"-f", "lavfi", "-i", "testsrc2", "-t", "1",
|
|
||||||
"-c", "mjpeg_qsv", "-f", "null", "-") {
|
|
||||||
return EngineDXVA2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return EngineSoftware
|
|
||||||
}
|
|
||||||
-173
@@ -1,173 +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"
|
|
||||||
"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:], '/')
|
|
||||||
if i <= 0 {
|
|
||||||
log.Warn().Msgf("wrong request: %s", r.RequestURI)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
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), "WebRTC/Hass sync", 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), "WebRTC/Hass sync", 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 HassioAddr() string {
|
|
||||||
ints, _ := net.Interfaces()
|
|
||||||
|
|
||||||
for _, i := range ints {
|
|
||||||
if i.Name != "hassio" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, _ := i.Addrs()
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if addr, ok := addr.(*net.IPNet); ok {
|
|
||||||
return addr.IP.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
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,145 +0,0 @@
|
|||||||
package hass
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/roborock"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var conf struct {
|
|
||||||
API struct {
|
|
||||||
Listen string `json:"listen"`
|
|
||||||
} `yaml:"api"`
|
|
||||||
Mod struct {
|
|
||||||
Config string `yaml:"config"`
|
|
||||||
} `yaml:"hass"`
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LoadConfig(&conf)
|
|
||||||
|
|
||||||
log = app.GetLogger("hass")
|
|
||||||
|
|
||||||
initAPI()
|
|
||||||
|
|
||||||
entries := importEntries(conf.Mod.Config)
|
|
||||||
if entries == nil {
|
|
||||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
http.Error(w, "no hass config", http.StatusNotFound)
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
api.HandleFunc("api/hass", func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
var items []api.Stream
|
|
||||||
for name, url := range entries {
|
|
||||||
items = append(items, api.Stream{Name: name, URL: url})
|
|
||||||
}
|
|
||||||
api.ResponseStreams(w, items)
|
|
||||||
})
|
|
||||||
|
|
||||||
streams.HandleFunc("hass", func(url string) (core.Producer, error) {
|
|
||||||
if hurl := entries[url[5:]]; hurl != "" {
|
|
||||||
return streams.GetProducer(hurl)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("can't get url: %s", url)
|
|
||||||
})
|
|
||||||
|
|
||||||
// for Addon listen on hassio interface, so WebUI feature will work
|
|
||||||
if conf.API.Listen == "127.0.0.1:1984" {
|
|
||||||
if addr := HassioAddr(); addr != "" {
|
|
||||||
addr += ":1984"
|
|
||||||
go func() {
|
|
||||||
log.Info().Str("addr", addr).Msg("[hass] listen")
|
|
||||||
if err := http.ListenAndServe(addr, api.Handler); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func importEntries(config string) map[string]string {
|
|
||||||
// support load cameras from Hass config file
|
|
||||||
filename := path.Join(config, ".storage/core.config_entries")
|
|
||||||
b, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var storage struct {
|
|
||||||
Data struct {
|
|
||||||
Entries []struct {
|
|
||||||
Title string `json:"title"`
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
Data json.RawMessage `json:"data"`
|
|
||||||
Options json.RawMessage `json:"options"`
|
|
||||||
} `json:"entries"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = json.Unmarshal(b, &storage); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
urls := map[string]string{}
|
|
||||||
|
|
||||||
for _, entrie := range storage.Data.Entries {
|
|
||||||
switch entrie.Domain {
|
|
||||||
case "generic":
|
|
||||||
var options struct {
|
|
||||||
StreamSource string `json:"stream_source"`
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(entrie.Options, &options); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
urls[entrie.Title] = options.StreamSource
|
|
||||||
|
|
||||||
case "homekit_controller":
|
|
||||||
if !bytes.Contains(entrie.Data, []byte("iOSPairingId")) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var 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"`
|
|
||||||
}
|
|
||||||
if err = json.Unmarshal(entrie.Data, &data); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
urls[entrie.Title] = fmt.Sprintf(
|
|
||||||
"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s",
|
|
||||||
data.DeviceHost, data.DevicePort,
|
|
||||||
data.ClientID, data.ClientPrivate, data.ClientPublic,
|
|
||||||
data.DeviceID, data.DevicePublic,
|
|
||||||
)
|
|
||||||
|
|
||||||
case "roborock":
|
|
||||||
_ = json.Unmarshal(entrie.Data, &roborock.Auth)
|
|
||||||
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("url", "hass:"+entrie.Title).Msg("[hass] load stream")
|
|
||||||
//streams.Get("hass:" + entrie.Title)
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
@@ -1,140 +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/hap"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/hap/mdns"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func apiHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
items := make([]any, 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")
|
|
||||||
name := r.URL.Query().Get("name")
|
|
||||||
if err := hkPair(id, pin, name); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
if err := hkDelete(src); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
_, err = w.Write([]byte(err.Error()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func hkPair(deviceID, pin, name string) (err error) {
|
|
||||||
var conn *hap.Conn
|
|
||||||
|
|
||||||
if conn, err = hap.Pair(deviceID, pin); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
streams.New(name, conn.URL())
|
|
||||||
|
|
||||||
dict := store.GetDict("streams")
|
|
||||||
dict[name] = conn.URL()
|
|
||||||
|
|
||||||
return store.Set("streams", dict)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hkDelete(name string) (err error) {
|
|
||||||
dict := store.GetDict("streams")
|
|
||||||
for key, rawURL := range dict {
|
|
||||||
if key != name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var conn *hap.Conn
|
|
||||||
|
|
||||||
if conn, err = hap.NewConn(rawURL.(string)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err = conn.Handle(); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = conn.ListPairings(); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = conn.DeletePairing(conn.ClientID); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(dict, name)
|
|
||||||
|
|
||||||
return store.Set("streams", dict)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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,32 +0,0 @@
|
|||||||
package homekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/srtp"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/homekit"
|
|
||||||
"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) (core.Producer, error) {
|
|
||||||
conn, err := homekit.NewClient(url, srtp.Server)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("http", handle)
|
|
||||||
streams.HandleFunc("https", handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
// first we get the Content-Type to define supported producer
|
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := tcp.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != http.StatusOK {
|
|
||||||
return nil, errors.New(res.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
ct := res.Header.Get("Content-Type")
|
|
||||||
if i := strings.IndexByte(ct, ';'); i > 0 {
|
|
||||||
ct = ct[:i]
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ct {
|
|
||||||
case "image/jpeg", "multipart/x-mixed-replace":
|
|
||||||
return mjpeg.NewClient(res), nil
|
|
||||||
|
|
||||||
case "video/x-flv":
|
|
||||||
var conn *rtmp.Client
|
|
||||||
if conn, err = rtmp.Accept(res); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = conn.Describe(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
|
|
||||||
case "video/mpeg":
|
|
||||||
client := mpegts.NewClient(res)
|
|
||||||
if err = client.Handle(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, fmt.Errorf("unsupported Content-Type: %s", ct)
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package isapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/isapi"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("isapi", handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn, err := isapi.NewClient(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err = conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package ivideon
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/ivideon"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("ivideon", func(url string) (core.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,171 +0,0 @@
|
|||||||
package mjpeg
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mjpeg"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
api.HandleFunc("api/frame.jpeg", handlerKeyframe)
|
|
||||||
api.HandleFunc("api/stream.mjpeg", handlerStream)
|
|
||||||
|
|
||||||
api.HandleWS("mjpeg", handlerWS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := make(chan []byte)
|
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{
|
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
|
||||||
UserAgent: r.UserAgent(),
|
|
||||||
}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case []byte:
|
|
||||||
exit <- msg
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := <-exit
|
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
h := w.Header()
|
|
||||||
h.Set("Content-Type", "image/jpeg")
|
|
||||||
h.Set("Content-Length", strconv.Itoa(len(data)))
|
|
||||||
h.Set("Cache-Control", "no-cache")
|
|
||||||
h.Set("Connection", "close")
|
|
||||||
h.Set("Pragma", "no-cache")
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const header = "--frame\r\nContent-Type: image/jpeg\r\nContent-Length: "
|
|
||||||
|
|
||||||
func handlerStream(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
outputMjpeg(w, r)
|
|
||||||
} else {
|
|
||||||
inputMjpeg(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func outputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
flusher := w.(http.Flusher)
|
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{
|
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
|
||||||
UserAgent: r.UserAgent(),
|
|
||||||
}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case []byte:
|
|
||||||
data := []byte(header + strconv.Itoa(len(msg)))
|
|
||||||
data = append(data, '\r', '\n', '\r', '\n')
|
|
||||||
data = append(data, msg...)
|
|
||||||
data = append(data, '\r', '\n')
|
|
||||||
|
|
||||||
// Chrome bug: mjpeg image always shows the second to last image
|
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
|
|
||||||
_, _ = w.Write(data)
|
|
||||||
flusher.Flush()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Msg("[api.mjpeg] add consumer")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h := w.Header()
|
|
||||||
h.Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
|
|
||||||
h.Set("Cache-Control", "no-cache")
|
|
||||||
h.Set("Connection", "close")
|
|
||||||
h.Set("Pragma", "no-cache")
|
|
||||||
|
|
||||||
<-r.Context().Done()
|
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
//log.Trace().Msg("[api.mjpeg] close")
|
|
||||||
}
|
|
||||||
|
|
||||||
func inputMjpeg(w http.ResponseWriter, r *http.Request) {
|
|
||||||
dst := r.URL.Query().Get("dst")
|
|
||||||
stream := streams.Get(dst)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Header: r.Header, Request: r}
|
|
||||||
res.Header.Set("Content-Type", "multipart/mixed;boundary=")
|
|
||||||
|
|
||||||
client := mjpeg.NewClient(res)
|
|
||||||
stream.AddProducer(client)
|
|
||||||
|
|
||||||
if err := client.Start(); err != nil && err != io.EOF {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.RemoveProducer(client)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerWS(tr *api.Transport, _ *api.Message) error {
|
|
||||||
src := tr.Request.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return errors.New(api.StreamNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
cons := &mjpeg.Consumer{
|
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
tr.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.Write(&api.Message{Type: "mjpeg"})
|
|
||||||
|
|
||||||
tr.OnClose(func() {
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
-162
@@ -1,162 +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/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
log = app.GetLogger("mp4")
|
|
||||||
|
|
||||||
api.HandleWS("mse", handlerWSMSE)
|
|
||||||
api.HandleWS("mp4", handlerWSMP4)
|
|
||||||
|
|
||||||
api.HandleFunc("api/frame.mp4", handlerKeyframe)
|
|
||||||
api.HandleFunc("api/stream.mp4", handlerMP4)
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Chrome 105 does two requests: without Range and with `Range: bytes=0-`
|
|
||||||
ua := r.UserAgent()
|
|
||||||
if strings.Contains(ua, " Chrome/") {
|
|
||||||
if r.Header.Values("Range") == nil {
|
|
||||||
w.Header().Set("Content-Type", "video/mp4")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := make(chan []byte)
|
|
||||||
|
|
||||||
cons := &mp4.Segment{OnlyKeyframe: true}
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok && exit != nil {
|
|
||||||
exit <- data
|
|
||||||
exit = nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := <-exit
|
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
// Apple Safari won't show frame without length
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType)
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerMP4(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Trace().Msgf("[mp4] %s %+v", r.Method, r.Header)
|
|
||||||
|
|
||||||
// Chrome has Safari in UA, so check first Chrome and later Safari
|
|
||||||
ua := r.UserAgent()
|
|
||||||
if strings.Contains(ua, " Chrome/") {
|
|
||||||
if r.Header.Values("Range") == nil {
|
|
||||||
w.Header().Set("Content-Type", "video/mp4")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if strings.Contains(ua, " Safari/") {
|
|
||||||
// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream
|
|
||||||
url := "stream.m3u8?" + r.URL.RawQuery
|
|
||||||
if !r.URL.Query().Has("mp4") {
|
|
||||||
url += "&mp4"
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(w, r, url, http.StatusMovedPermanently)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src := r.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
exit := make(chan error)
|
|
||||||
|
|
||||||
cons := &mp4.Consumer{
|
|
||||||
RemoteAddr: tcp.RemoteAddr(r),
|
|
||||||
UserAgent: r.UserAgent(),
|
|
||||||
Medias: core.ParseQuery(r.URL.Query()),
|
|
||||||
}
|
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
if _, err := w.Write(data); err != nil && exit != nil {
|
|
||||||
exit <- err
|
|
||||||
exit = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer stream.RemoveConsumer(cons)
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", cons.MimeType())
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = w.Write(data); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
var duration *time.Timer
|
|
||||||
if s := r.URL.Query().Get("duration"); s != "" {
|
|
||||||
if i, _ := strconv.Atoi(s); i > 0 {
|
|
||||||
duration = time.AfterFunc(time.Second*time.Duration(i), func() {
|
|
||||||
if exit != nil {
|
|
||||||
exit <- nil
|
|
||||||
exit = nil
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = <-exit
|
|
||||||
|
|
||||||
log.Trace().Err(err).Caller().Send()
|
|
||||||
|
|
||||||
if duration != nil {
|
|
||||||
duration.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-138
@@ -1,138 +0,0 @@
|
|||||||
package mp4
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mp4"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tcp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func handlerWSMSE(tr *api.Transport, msg *api.Message) error {
|
|
||||||
src := tr.Request.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return errors.New(api.StreamNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
cons := &mp4.Consumer{
|
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
}
|
|
||||||
|
|
||||||
if codecs := msg.String(); codecs != "" {
|
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MSE consumer")
|
|
||||||
cons.Medias = parseMedias(codecs, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
tr.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Debug().Err(err).Msg("[mp4] add consumer")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.OnClose(func() {
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
})
|
|
||||||
|
|
||||||
tr.Write(&api.Message{Type: "mse", Value: cons.MimeType()})
|
|
||||||
|
|
||||||
data, err := cons.Init()
|
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.Write(data)
|
|
||||||
|
|
||||||
cons.Start()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerWSMP4(tr *api.Transport, msg *api.Message) error {
|
|
||||||
src := tr.Request.URL.Query().Get("src")
|
|
||||||
stream := streams.GetOrNew(src)
|
|
||||||
if stream == nil {
|
|
||||||
return errors.New(api.StreamNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
cons := &mp4.Segment{
|
|
||||||
RemoteAddr: tcp.RemoteAddr(tr.Request),
|
|
||||||
UserAgent: tr.Request.UserAgent(),
|
|
||||||
OnlyKeyframe: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if codecs := msg.String(); codecs != "" {
|
|
||||||
log.Trace().Str("codecs", codecs).Msgf("[mp4] new WS/MP4 consumer")
|
|
||||||
cons.Medias = parseMedias(codecs, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
cons.Listen(func(msg any) {
|
|
||||||
if data, ok := msg.([]byte); ok {
|
|
||||||
tr.Write(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := stream.AddConsumer(cons); err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.Write(&api.Message{Type: "mp4", Value: cons.MimeType})
|
|
||||||
|
|
||||||
tr.OnClose(func() {
|
|
||||||
stream.RemoveConsumer(cons)
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMedias(codecs string, parseAudio bool) (medias []*core.Media) {
|
|
||||||
var videos []*core.Codec
|
|
||||||
var audios []*core.Codec
|
|
||||||
|
|
||||||
for _, name := range strings.Split(codecs, ",") {
|
|
||||||
switch name {
|
|
||||||
case mp4.MimeH264:
|
|
||||||
codec := &core.Codec{Name: core.CodecH264}
|
|
||||||
videos = append(videos, codec)
|
|
||||||
case mp4.MimeH265:
|
|
||||||
codec := &core.Codec{Name: core.CodecH265}
|
|
||||||
videos = append(videos, codec)
|
|
||||||
case mp4.MimeAAC:
|
|
||||||
codec := &core.Codec{Name: core.CodecAAC}
|
|
||||||
audios = append(audios, codec)
|
|
||||||
case mp4.MimeOpus:
|
|
||||||
codec := &core.Codec{Name: core.CodecOpus}
|
|
||||||
audios = append(audios, codec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if videos != nil {
|
|
||||||
media := &core.Media{
|
|
||||||
Kind: core.KindVideo,
|
|
||||||
Direction: core.DirectionSendonly,
|
|
||||||
Codecs: videos,
|
|
||||||
}
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
if audios != nil && parseAudio {
|
|
||||||
media := &core.Media{
|
|
||||||
Kind: core.KindAudio,
|
|
||||||
Direction: core.DirectionSendonly,
|
|
||||||
Codecs: audios,
|
|
||||||
}
|
|
||||||
medias = append(medias, media)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package mpegts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/mpegts"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
api.HandleFunc("api/stream.ts", apiHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := r.URL.Query().Get("dst")
|
|
||||||
stream := streams.Get(dst)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Request: r}
|
|
||||||
client := mpegts.NewClient(res)
|
|
||||||
|
|
||||||
if err := client.Handle(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddProducer(client)
|
|
||||||
|
|
||||||
if err := client.Handle(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.RemoveProducer(client)
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package rtmp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/rtmp"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("rtmp", streamsHandle)
|
|
||||||
|
|
||||||
api.HandleFunc("api/stream.flv", apiHandle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamsHandle(url string) (core.Producer, error) {
|
|
||||||
conn := rtmp.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := conn.Describe(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiHandle(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method != "POST" {
|
|
||||||
http.Error(w, "", http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dst := r.URL.Query().Get("dst")
|
|
||||||
stream := streams.Get(dst)
|
|
||||||
if stream == nil {
|
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := &http.Response{Body: r.Body, Request: r}
|
|
||||||
client, err := rtmp.Accept(res)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = client.Describe(); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.AddProducer(client)
|
|
||||||
|
|
||||||
if err = client.Start(); err != nil && err != io.EOF {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.RemoveProducer(client)
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package srtp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/srtp"
|
|
||||||
"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).Caller().Send()
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("addr", cfg.Mod.Listen).Msg("[srtp] listen")
|
|
||||||
|
|
||||||
// run server
|
|
||||||
go func() {
|
|
||||||
Server = &srtp.Server{}
|
|
||||||
if err = Server.Serve(conn); err != nil {
|
|
||||||
log.Warn().Err(err).Caller().Send()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
var Server *srtp.Server
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Handler func(url string) (core.Producer, error)
|
|
||||||
|
|
||||||
var handlers = map[string]Handler{}
|
|
||||||
var handlersMu sync.Mutex
|
|
||||||
|
|
||||||
func HandleFunc(scheme string, handler Handler) {
|
|
||||||
handlersMu.Lock()
|
|
||||||
handlers[scheme] = handler
|
|
||||||
handlersMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getHandler(url string) Handler {
|
|
||||||
i := strings.IndexByte(url, ':')
|
|
||||||
if i <= 0 { // TODO: i < 4 ?
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
handlersMu.Lock()
|
|
||||||
defer handlersMu.Unlock()
|
|
||||||
return handlers[url[:i]]
|
|
||||||
}
|
|
||||||
|
|
||||||
func HasProducer(url string) bool {
|
|
||||||
return getHandler(url) != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetProducer(url string) (core.Producer, error) {
|
|
||||||
handler := getHandler(url)
|
|
||||||
if handler == nil {
|
|
||||||
return nil, fmt.Errorf("unsupported scheme: %s", url)
|
|
||||||
}
|
|
||||||
return handler(url)
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app/store"
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
var cfg struct {
|
|
||||||
Mod map[string]any `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)
|
|
||||||
}
|
|
||||||
|
|
||||||
api.HandleFunc("api/streams", streamsHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get(name string) *Stream {
|
|
||||||
return streams[name]
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(name string, source any) *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 streamsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
query := r.URL.Query()
|
|
||||||
src := query.Get("src")
|
|
||||||
|
|
||||||
// without source - return all streams list
|
|
||||||
if src == "" && r.Method != "POST" {
|
|
||||||
_ = json.NewEncoder(w).Encode(streams)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not sure about all this API. Should be rewrited...
|
|
||||||
switch r.Method {
|
|
||||||
case "GET":
|
|
||||||
e := json.NewEncoder(w)
|
|
||||||
e.SetIndent("", " ")
|
|
||||||
_ = e.Encode(streams[src])
|
|
||||||
|
|
||||||
case "PUT":
|
|
||||||
name := query.Get("name")
|
|
||||||
if name == "" {
|
|
||||||
name = src
|
|
||||||
}
|
|
||||||
|
|
||||||
New(name, src)
|
|
||||||
|
|
||||||
case "PATCH":
|
|
||||||
name := query.Get("name")
|
|
||||||
if name == "" {
|
|
||||||
http.Error(w, "", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if stream := Get(name); stream != nil {
|
|
||||||
stream.SetSource(src)
|
|
||||||
} else {
|
|
||||||
New(name, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "POST":
|
|
||||||
// with dst - redirect source to dst
|
|
||||||
if dst := query.Get("dst"); dst != "" {
|
|
||||||
if stream := Get(dst); stream != nil {
|
|
||||||
if err := stream.Play(src); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
} else {
|
|
||||||
_ = json.NewEncoder(w).Encode(stream)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, "", http.StatusNotFound)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, "", http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
case "DELETE":
|
|
||||||
delete(streams, src)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var log zerolog.Logger
|
|
||||||
var streams = map[string]*Stream{}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
package streams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Stream struct {
|
|
||||||
producers []*Producer
|
|
||||||
consumers []core.Consumer
|
|
||||||
mu sync.Mutex
|
|
||||||
requests int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewStream(source any) *Stream {
|
|
||||||
switch source := source.(type) {
|
|
||||||
case string:
|
|
||||||
s := new(Stream)
|
|
||||||
prod := &Producer{url: source}
|
|
||||||
s.producers = append(s.producers, prod)
|
|
||||||
return s
|
|
||||||
case []any:
|
|
||||||
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]any:
|
|
||||||
return NewStream(source["url"])
|
|
||||||
case nil:
|
|
||||||
return new(Stream)
|
|
||||||
default:
|
|
||||||
panic(core.Caller())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) SetSource(source string) {
|
|
||||||
for _, prod := range s.producers {
|
|
||||||
prod.SetSource(source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) AddConsumer(cons core.Consumer) (err error) {
|
|
||||||
// support for multiple simultaneous requests from different consumers
|
|
||||||
atomic.AddInt32(&s.requests, 1)
|
|
||||||
|
|
||||||
var producers []*Producer // matched producers for consumer
|
|
||||||
|
|
||||||
var codecs string
|
|
||||||
|
|
||||||
// Step 1. Get consumer medias
|
|
||||||
for _, consMedia := range cons.GetMedias() {
|
|
||||||
|
|
||||||
producers:
|
|
||||||
for _, prod := range s.producers {
|
|
||||||
if err = prod.Dial(); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2. Get producer medias (not tracks yet)
|
|
||||||
for _, prodMedia := range prod.GetMedias() {
|
|
||||||
collectCodecs(prodMedia, &codecs)
|
|
||||||
|
|
||||||
// Step 3. Match consumer/producer codecs list
|
|
||||||
prodCodec, consCodec := prodMedia.MatchMedia(consMedia)
|
|
||||||
if prodCodec == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var track *core.Receiver
|
|
||||||
|
|
||||||
switch prodMedia.Direction {
|
|
||||||
case core.DirectionRecvonly:
|
|
||||||
// Step 4. Get recvonly track from producer
|
|
||||||
if track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't get track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Step 5. Add track to consumer
|
|
||||||
if err = cons.AddTrack(consMedia, consCodec, track); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't add track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
case core.DirectionSendonly:
|
|
||||||
// Step 4. Get recvonly track from consumer (backchannel)
|
|
||||||
if track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't get track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Step 5. Add track to producer
|
|
||||||
if err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {
|
|
||||||
log.Info().Err(err).Msg("[streams] can't add track")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
producers = append(producers, prod)
|
|
||||||
|
|
||||||
if !consMedia.MatchAll() {
|
|
||||||
break producers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop producers if they don't have readers
|
|
||||||
if atomic.AddInt32(&s.requests, -1) == 0 {
|
|
||||||
s.stopProducers()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(producers) == 0 {
|
|
||||||
if len(codecs) > 0 {
|
|
||||||
return errors.New("codecs not match: " + codecs)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, producer := range s.producers {
|
|
||||||
if producer.lastErr != nil {
|
|
||||||
return fmt.Errorf("source %d error: %w", i, producer.lastErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("sources unavailable: %d", len(s.producers))
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
s.consumers = append(s.consumers, cons)
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
// there may be duplicates, but that's not a problem
|
|
||||||
for _, prod := range producers {
|
|
||||||
prod.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) RemoveConsumer(cons core.Consumer) {
|
|
||||||
_ = cons.Stop()
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
for i, consumer := range s.consumers {
|
|
||||||
if consumer == cons {
|
|
||||||
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
s.stopProducers()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) AddProducer(prod core.Producer) {
|
|
||||||
producer := &Producer{conn: prod, state: stateExternal}
|
|
||||||
s.mu.Lock()
|
|
||||||
s.producers = append(s.producers, producer)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) RemoveProducer(prod core.Producer) {
|
|
||||||
s.mu.Lock()
|
|
||||||
for i, producer := range s.producers {
|
|
||||||
if producer.conn == prod {
|
|
||||||
s.producers = append(s.producers[:i], s.producers[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) stopProducers() {
|
|
||||||
s.mu.Lock()
|
|
||||||
producers:
|
|
||||||
for _, producer := range s.producers {
|
|
||||||
for _, track := range producer.receivers {
|
|
||||||
if len(track.Senders()) > 0 {
|
|
||||||
continue producers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
producer.stop()
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Stream) MarshalJSON() ([]byte, error) {
|
|
||||||
if !s.mu.TryLock() {
|
|
||||||
log.Warn().Msgf("[streams] json locked")
|
|
||||||
return json.Marshal(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
var info struct {
|
|
||||||
Producers []*Producer `json:"producers"`
|
|
||||||
Consumers []core.Consumer `json:"consumers"`
|
|
||||||
}
|
|
||||||
info.Producers = s.producers
|
|
||||||
info.Consumers = s.consumers
|
|
||||||
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
return json.Marshal(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectCodecs(media *core.Media, codecs *string) {
|
|
||||||
if media.Direction == core.DirectionRecvonly {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, codec := range media.Codecs {
|
|
||||||
name := codec.Name
|
|
||||||
if name == core.CodecAAC {
|
|
||||||
name = "AAC"
|
|
||||||
}
|
|
||||||
if strings.Contains(*codecs, name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(*codecs) > 0 {
|
|
||||||
*codecs += ","
|
|
||||||
}
|
|
||||||
*codecs += name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package tapo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/streams"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/tapo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Init() {
|
|
||||||
streams.HandleFunc("tapo", handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle(url string) (core.Producer, error) {
|
|
||||||
conn := tapo.NewClient(url)
|
|
||||||
if err := conn.Dial(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
## Userful links
|
|
||||||
|
|
||||||
- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html
|
|
||||||
- https://www.ietf.org/id/draft-murillo-whep-01.html
|
|
||||||
- https://github.com/Glimesh/broadcast-box/
|
|
||||||
- https://github.com/obsproject/obs-studio/pull/7926
|
|
||||||
- https://misi.github.io/webrtc-c0d3l4b/
|
|
||||||
- https://github.com/webtorrent/webtorrent/blob/master/docs/faq.md
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package webrtc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
|
||||||
"github.com/pion/sdp/v3"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Address struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
}
|
|
||||||
|
|
||||||
var addresses []Address
|
|
||||||
|
|
||||||
func AddCandidate(address string) {
|
|
||||||
var port int
|
|
||||||
|
|
||||||
// try to get port from address string
|
|
||||||
if i := strings.LastIndexByte(address, ':'); i > 0 {
|
|
||||||
if v, _ := strconv.Atoi(address[i+1:]); v != 0 {
|
|
||||||
address = address[:i]
|
|
||||||
port = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// use default WebRTC port
|
|
||||||
if port == 0 {
|
|
||||||
port, _ = strconv.Atoi(Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses = append(addresses, Address{Host: address, Port: port})
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetCandidates() (candidates []string) {
|
|
||||||
for _, address := range addresses {
|
|
||||||
// using stun server for receive public IP-address
|
|
||||||
if address.Host == "stun" {
|
|
||||||
ip, err := webrtc.GetCachedPublicIP()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// this is a copy, original host unchanged
|
|
||||||
address.Host = ip.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates = append(
|
|
||||||
candidates,
|
|
||||||
webrtc.CandidateManualHostUDP(address.Host, address.Port),
|
|
||||||
webrtc.CandidateManualHostTCPPassive(address.Host, address.Port),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func asyncCandidates(tr *api.Transport, cons *webrtc.Conn) {
|
|
||||||
tr.WithContext(func(ctx map[any]any) {
|
|
||||||
if candidates, ok := ctx["candidate"].([]string); ok {
|
|
||||||
// process candidates that receive before this moment
|
|
||||||
for _, candidate := range candidates {
|
|
||||||
_ = cons.AddCandidate(candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove already processed candidates
|
|
||||||
delete(ctx, "candidate")
|
|
||||||
}
|
|
||||||
|
|
||||||
// set variable for process candidates after this moment
|
|
||||||
ctx["webrtc"] = cons
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, candidate := range GetCandidates() {
|
|
||||||
log.Trace().Str("candidate", candidate).Msg("[webrtc] config")
|
|
||||||
tr.Write(&api.Message{Type: "webrtc/candidate", Value: candidate})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func syncCanditates(answer string) (string, error) {
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
return answer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sd := &sdp.SessionDescription{}
|
|
||||||
if err := sd.Unmarshal([]byte(answer)); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
md := sd.MediaDescriptions[0]
|
|
||||||
|
|
||||||
for _, candidate := range GetCandidates() {
|
|
||||||
md.WithPropertyAttribute(candidate)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := sd.Marshal()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func candidateHandler(tr *api.Transport, msg *api.Message) error {
|
|
||||||
// process incoming candidate in sync function
|
|
||||||
tr.WithContext(func(ctx map[any]any) {
|
|
||||||
candidate := msg.String()
|
|
||||||
log.Trace().Str("candidate", candidate).Msg("[webrtc] remote")
|
|
||||||
|
|
||||||
if cons, ok := ctx["webrtc"].(*webrtc.Conn); ok {
|
|
||||||
// if webrtc.Server already initialized - process candidate
|
|
||||||
_ = cons.AddCandidate(candidate)
|
|
||||||
} else {
|
|
||||||
// or collect candidate and process it later
|
|
||||||
list, _ := ctx["candidate"].([]string)
|
|
||||||
ctx["candidate"] = append(list, candidate)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
package webrtc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"github.com/AlexxIT/go2rtc/cmd/api"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/core"
|
|
||||||
"github.com/AlexxIT/go2rtc/pkg/webrtc"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
pion "github.com/pion/webrtc/v3"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func streamsHandler(url string) (core.Producer, error) {
|
|
||||||
url = url[7:]
|
|
||||||
if i := strings.Index(url, "://"); i > 0 {
|
|
||||||
switch url[:i] {
|
|
||||||
case "ws", "wss":
|
|
||||||
return asyncClient(url)
|
|
||||||
case "http", "https":
|
|
||||||
return syncClient(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errors.New("unsupported url: " + url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// asyncClient can connect only to go2rtc server
|
|
||||||
// ex: ws://localhost:1984/api/ws?src=camera1
|
|
||||||
func asyncClient(url string) (core.Producer, error) {
|
|
||||||
// 1. Connect to signalign server
|
|
||||||
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
_ = ws.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 2. Create PeerConnection
|
|
||||||
pc, err := PeerConnection(true)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendOffer core.Waiter
|
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
|
||||||
prod.Desc = "WebRTC/WebSocket async"
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
|
||||||
prod.Listen(func(msg any) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case pion.PeerConnectionState:
|
|
||||||
_ = ws.Close()
|
|
||||||
|
|
||||||
case *pion.ICECandidate:
|
|
||||||
sendOffer.Wait()
|
|
||||||
|
|
||||||
s := msg.ToJSON().Candidate
|
|
||||||
log.Trace().Str("candidate", s).Msg("[webrtc] local")
|
|
||||||
_ = ws.WriteJSON(&api.Message{Type: "webrtc/candidate", Value: s})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
medias := []*core.Media{
|
|
||||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
|
||||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
|
||||||
{Kind: core.KindAudio, Direction: core.DirectionSendonly},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Create offer
|
|
||||||
offer, err := prod.CreateOffer(medias)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Send offer
|
|
||||||
msg := &api.Message{Type: "webrtc/offer", Value: offer}
|
|
||||||
if err = ws.WriteJSON(msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sendOffer.Done()
|
|
||||||
|
|
||||||
// 5. Get answer
|
|
||||||
if err = ws.ReadJSON(msg); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if msg.Type != "webrtc/answer" {
|
|
||||||
return nil, errors.New("wrong answer: " + msg.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
answer := msg.String()
|
|
||||||
if err = prod.SetAnswer(answer); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Continue to receiving candidates
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
// receive data from remote
|
|
||||||
msg := new(api.Message)
|
|
||||||
if err = ws.ReadJSON(msg); err != nil {
|
|
||||||
if cerr, ok := err.(*websocket.CloseError); ok {
|
|
||||||
log.Trace().Err(err).Caller().Msgf("[webrtc] ws code=%d", cerr)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg.Type {
|
|
||||||
case "webrtc/candidate":
|
|
||||||
if msg.Value != nil {
|
|
||||||
_ = prod.AddCandidate(msg.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = ws.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return prod, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// syncClient - support WebRTC-HTTP Egress Protocol (WHEP)
|
|
||||||
// ex: http://localhost:1984/api/webrtc?src=camera1
|
|
||||||
func syncClient(url string) (core.Producer, error) {
|
|
||||||
// 2. Create PeerConnection
|
|
||||||
pc, err := PeerConnection(true)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Caller().Send()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
prod := webrtc.NewConn(pc)
|
|
||||||
prod.Desc = "WebRTC/WHEP sync"
|
|
||||||
prod.Mode = core.ModeActiveProducer
|
|
||||||
|
|
||||||
medias := []*core.Media{
|
|
||||||
{Kind: core.KindVideo, Direction: core.DirectionRecvonly},
|
|
||||||
{Kind: core.KindAudio, Direction: core.DirectionRecvonly},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Create offer
|
|
||||||
offer, err := prod.CreateCompleteOffer(medias)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", url, strings.NewReader(offer))
|
|
||||||
req.Header.Set("Content-Type", MimeSDP)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := http.Client{Timeout: time.Second * 5000}
|
|
||||||
defer client.CloseIdleConnections()
|
|
||||||
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
answer, err := io.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = prod.SetAnswer(string(answer)); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return prod, nil
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
# syntax=docker/dockerfile:labs
|
# syntax=docker/dockerfile:labs
|
||||||
|
|
||||||
# 0. Prepare images
|
# 0. Prepare images
|
||||||
ARG PYTHON_VERSION="3.11"
|
ARG PYTHON_VERSION="3.13"
|
||||||
ARG GO_VERSION="1.19"
|
ARG GO_VERSION="1.25"
|
||||||
ARG NGROK_VERSION="3"
|
|
||||||
|
|
||||||
FROM python:${PYTHON_VERSION}-alpine AS base
|
|
||||||
FROM ngrok/ngrok:${NGROK_VERSION}-alpine AS ngrok
|
|
||||||
|
|
||||||
|
|
||||||
# 1. Build go2rtc binary
|
# 1. Build go2rtc binary
|
||||||
@@ -20,6 +16,8 @@ ENV GOARCH=${TARGETARCH}
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
# Cache dependencies
|
# Cache dependencies
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
RUN --mount=type=cache,target=/root/.cache/go-build go mod download
|
||||||
@@ -28,19 +26,14 @@ COPY . .
|
|||||||
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
||||||
|
|
||||||
|
|
||||||
# 2. Collect all files
|
# 2. Final image
|
||||||
FROM scratch AS rootfs
|
FROM python:${PYTHON_VERSION}-alpine AS base
|
||||||
|
|
||||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
|
||||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
|
||||||
|
|
||||||
|
|
||||||
# 3. Final image
|
|
||||||
FROM base
|
|
||||||
|
|
||||||
# Install ffmpeg, tini (for signal handling),
|
# Install ffmpeg, tini (for signal handling),
|
||||||
# and other common tools for the echo source.
|
# and other common tools for the echo source.
|
||||||
RUN apk add --no-cache tini ffmpeg bash curl jq
|
# 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)
|
# Hardware Acceleration for Intel CPU (+50MB)
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
@@ -52,8 +45,9 @@ RUN if [ "${TARGETARCH}" = "amd64" ]; then apk add --no-cache libva-intel-driver
|
|||||||
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
# Hardware: AMD and NVidia VDPAU (not sure about this)
|
||||||
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)
|
||||||
|
|
||||||
COPY --from=rootfs / /
|
COPY --from=build /build/go2rtc /usr/local/bin/
|
||||||
|
|
||||||
|
EXPOSE 1984 8554 8555 8555/udp
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
ENTRYPOINT ["/sbin/tini", "--"]
|
||||||
VOLUME /config
|
VOLUME /config
|
||||||
WORKDIR /config
|
WORKDIR /config
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Docker
|
||||||
|
|
||||||
|
Images are built automatically via [GitHub actions](https://github.com/AlexxIT/go2rtc/actions) and published on [Docker Hub](https://hub.docker.com/r/alexxit/go2rtc) and [GitHub](https://github.com/AlexxIT/go2rtc/pkgs/container/go2rtc).
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
- `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support for hardware transcoding for Intel iGPU and Raspberry
|
||||||
|
- `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support for hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU
|
||||||
|
- `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support for hardware transcoding for Rockchip RK35xx
|
||||||
|
- `alexxit/go2rtc:master` - latest unstable version based on `alpine`
|
||||||
|
- `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`)
|
||||||
|
- `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`)
|
||||||
|
|
||||||
|
## Docker compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
go2rtc:
|
||||||
|
image: alexxit/go2rtc
|
||||||
|
network_mode: host # important for WebRTC, HomeKit, UDP cameras
|
||||||
|
privileged: true # only for FFmpeg hardware transcoding
|
||||||
|
restart: unless-stopped # autorestart on fail or config change from WebUI
|
||||||
|
environment:
|
||||||
|
- TZ=Atlantic/Bermuda # timezone in logs
|
||||||
|
volumes:
|
||||||
|
- "~/go2rtc:/config" # folder for go2rtc.yaml file (edit from WebUI)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name go2rtc \
|
||||||
|
--network host \
|
||||||
|
--privileged \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e TZ=Atlantic/Bermuda \
|
||||||
|
-v ~/go2rtc:/config \
|
||||||
|
alexxit/go2rtc
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment with GPU Acceleration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name go2rtc \
|
||||||
|
--network host \
|
||||||
|
--privileged \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-e TZ=Atlantic/Bermuda \
|
||||||
|
--gpus all \
|
||||||
|
-v ~/go2rtc:/config \
|
||||||
|
alexxit/go2rtc:latest-hardware
|
||||||
|
```
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,59 +1,52 @@
|
|||||||
module github.com/AlexxIT/go2rtc
|
module github.com/AlexxIT/go2rtc
|
||||||
|
|
||||||
go 1.20
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brutella/hap v0.0.17
|
github.com/asticode/go-astits v1.14.0
|
||||||
github.com/deepch/vdk v0.0.19
|
github.com/eclipse/paho.mqtt.golang v1.5.1
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/expr-lang/expr v1.17.7
|
||||||
github.com/hashicorp/mdns v1.0.5
|
github.com/google/uuid v1.6.0
|
||||||
github.com/pion/ice/v2 v2.3.1
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/pion/interceptor v0.1.12
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/pion/rtcp v1.2.10
|
github.com/miekg/dns v1.1.70
|
||||||
github.com/pion/rtp v1.7.13
|
github.com/pion/dtls/v3 v3.0.10
|
||||||
github.com/pion/sdp/v3 v3.0.6
|
github.com/pion/ice/v4 v4.2.0
|
||||||
github.com/pion/srtp/v2 v2.0.12
|
github.com/pion/interceptor v0.1.43
|
||||||
github.com/pion/stun v0.4.0
|
github.com/pion/rtcp v1.2.16
|
||||||
github.com/pion/webrtc/v3 v3.1.58
|
github.com/pion/rtp v1.10.0
|
||||||
github.com/rs/zerolog v1.29.0
|
github.com/pion/sdp/v3 v3.0.17
|
||||||
github.com/stretchr/testify v1.8.2
|
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.5 // indirect
|
github.com/asticode/go-astikit v0.57.1 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-chi/chi v1.5.4 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/google/uuid v1.3.0 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/kr/pretty v0.2.1 // indirect
|
github.com/pion/datachannel v1.6.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/pion/logging v0.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/pion/mdns/v2 v2.1.0 // indirect
|
||||||
github.com/miekg/dns v1.1.52 // indirect
|
|
||||||
github.com/pion/datachannel v1.5.5 // indirect
|
|
||||||
github.com/pion/dtls/v2 v2.2.6 // indirect
|
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
|
||||||
github.com/pion/mdns v0.0.7 // indirect
|
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/sctp v1.8.6 // indirect
|
github.com/pion/sctp v1.9.2 // indirect
|
||||||
github.com/pion/transport/v2 v2.0.2 // indirect
|
github.com/pion/transport/v3 v3.1.1 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.0 // indirect
|
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||||
github.com/pion/udp/v2 v2.0.1 // indirect
|
github.com/pion/turn/v4 v4.1.4 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
golang.org/x/crypto v0.7.0 // indirect
|
golang.org/x/mod v0.32.0 // indirect
|
||||||
golang.org/x/mod v0.9.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/net v0.8.0 // indirect
|
golang.org/x/sys v0.40.0 // indirect
|
||||||
golang.org/x/sys v0.6.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/text v0.8.0 // indirect
|
golang.org/x/tools v0.41.0 // indirect
|
||||||
golang.org/x/tools v0.7.0 // 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.20221108133010-d8a45b7a7045
|
|
||||||
// fix reading AAC config bytes
|
|
||||||
github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,235 +1,152 @@
|
|||||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045 h1:xJf3FxQJReJSDyYXJfI1NUWv8tUEAGNV9xigLqNtmrI=
|
github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
|
||||||
github.com/AlexxIT/hap v0.0.15-0.20221108133010-d8a45b7a7045/go.mod h1:QNA3sm16zE5uUyC8+E/gNkMvQWjqQLuxQKkU5PMi8N4=
|
github.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=
|
||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92 h1:cIeYMGaAirSZnrKRDTb5VgZDDYqPLhYiczElMg4sQW0=
|
github.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=
|
||||||
github.com/AlexxIT/vdk v0.0.18-0.20221108193131-6168555b4f92/go.mod h1:7ydHfSkflMZxBXfWR79dMjrT54xzvLxnPaByOa9Jpzg=
|
github.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=
|
||||||
github.com/brutella/dnssd v1.2.3/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
github.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=
|
||||||
github.com/brutella/dnssd v1.2.5 h1:b8syhho41/5ikw3X2X4baR9NWEBSlpZnfQgujsv7bk4=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/brutella/dnssd v1.2.5/go.mod h1:JoW2sJUrmVIef25G6lrLj7HS6Xdwh6q8WUIvMkkBYXs=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
|
||||||
github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs=
|
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=
|
||||||
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
|
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
|
||||||
|
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE=
|
|
||||||
github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
|
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||||
github.com/miekg/dns v1.1.52 h1:Bmlc/qsNNULOe6bpXcUTsuOajd0DzRHwup6D9k1An0c=
|
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||||
github.com/miekg/dns v1.1.52/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
github.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
|
||||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
|
||||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
|
||||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
|
||||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
|
||||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
|
||||||
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
|
github.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=
|
||||||
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
|
github.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=
|
||||||
github.com/pion/ice/v2 v2.3.1 h1:FQCmUfZe2Jpe7LYStVBOP6z1DiSzbIateih3TztgTjc=
|
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
|
||||||
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
|
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
|
||||||
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
github.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=
|
||||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
github.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=
|
||||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
|
||||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
|
||||||
github.com/pion/mdns v0.0.7 h1:P0UB4Sr6xDWEox0kTVxF0LmQihtCbSAdW0H2nEgkA3U=
|
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
|
||||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
|
||||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
|
||||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
|
||||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
|
||||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
github.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=
|
||||||
github.com/pion/sctp v1.8.6 h1:CUex11Vkt9YS++VhLf8b55O3VqKrWL6W3SDwX4jAqsI=
|
github.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
|
||||||
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
|
||||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
|
||||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
github.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=
|
||||||
github.com/pion/srtp/v2 v2.0.12 h1:WrmiVCubGMOAObBU1vwWjG0H3VSyQHawKeer2PVA5rY=
|
github.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=
|
||||||
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
|
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
|
||||||
github.com/pion/stun v0.4.0 h1:vgRrbBE2htWHy7l3Zsxckk7rkjnjOsSM7PHZnBwo8rk=
|
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
|
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
|
||||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
|
||||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
|
||||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
|
||||||
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
|
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
|
||||||
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
|
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
|
||||||
github.com/pion/turn/v2 v2.1.0 h1:5wGHSgGhJhP/RpabkUb/T9PdsAjkGLS6toYz5HNzoSI=
|
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
|
||||||
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
|
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
|
||||||
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
|
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
|
||||||
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
|
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
|
||||||
github.com/pion/webrtc/v3 v3.1.58 h1:husXqiKQuk6gbOqJlPHs185OskAyxUW6iAEgHghgCrc=
|
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
|
||||||
github.com/pion/webrtc/v3 v3.1.58/go.mod h1:jJdqoqGBlZiE3y8Z1tg1fjSkyEDCZLL+foypUBn0Lhk=
|
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
|
||||||
|
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
|
||||||
|
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
|
||||||
|
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
|
||||||
|
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
|
||||||
|
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
|
||||||
|
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
|
||||||
|
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
|
||||||
|
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
|
||||||
|
github.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4=
|
||||||
|
github.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
|
||||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=
|
||||||
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
github.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561 h1:SVoNK97S6JlaYlHcaC+79tg3JUlQABcc0dH2VQ4Y+9s=
|
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||||
github.com/xiam/to v0.0.0-20200126224905-d60d31e03561/go.mod h1:cqbG7phSzrbdg3aj+Kn63bpVruzwDZi58CpxlZkjwzw=
|
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
|
||||||
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
|
|
||||||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
|
||||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
|
||||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
|
||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
|
||||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
|
||||||
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
|
|
||||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
|
||||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
|
||||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
||||||
golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
|
|
||||||
golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
|
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
|
||||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
|
||||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
|
||||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
|
||||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
|
||||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# 0. Prepare images
|
|
||||||
# only debian 12 (bookworm) has latest ffmpeg
|
|
||||||
ARG DEBIAN_VERSION="bookworm-slim"
|
|
||||||
ARG GO_VERSION="1.19-buster"
|
|
||||||
ARG NGROK_VERSION="3"
|
|
||||||
|
|
||||||
FROM debian:${DEBIAN_VERSION} AS base
|
|
||||||
FROM golang:${GO_VERSION} AS go
|
|
||||||
FROM ngrok/ngrok:${NGROK_VERSION} AS ngrok
|
|
||||||
|
|
||||||
|
|
||||||
# 1. Build go2rtc binary
|
|
||||||
FROM go AS build
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# Cache dependencies
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath
|
|
||||||
|
|
||||||
|
|
||||||
# 2. Collect all files
|
|
||||||
FROM scratch AS rootfs
|
|
||||||
|
|
||||||
COPY --from=build /build/go2rtc /usr/local/bin/
|
|
||||||
COPY --from=ngrok /bin/ngrok /usr/local/bin/
|
|
||||||
COPY ./build/docker/run.sh /
|
|
||||||
|
|
||||||
|
|
||||||
# 3. Final image
|
|
||||||
FROM base
|
|
||||||
|
|
||||||
# Install ffmpeg, bash (for run.sh), 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)
|
|
||||||
RUN echo 'deb http://deb.debian.org/debian bookworm non-free' > /etc/apt/sources.list.d/debian-non-free.list && \
|
|
||||||
apt-get -y update && apt-get -y install tini ffmpeg python3 curl jq intel-media-va-driver-non-free
|
|
||||||
|
|
||||||
COPY --from=rootfs / /
|
|
||||||
|
|
||||||
RUN chmod a+x /run.sh && mkdir -p /config
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
|
||||||
|
|
||||||
# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)
|
|
||||||
ENV NVIDIA_VISIBLE_DEVICES all
|
|
||||||
ENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility
|
|
||||||
|
|
||||||
CMD ["/run.sh"]
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/cmd/app"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configHandler(w http.ResponseWriter, r *http.Request) {
|
func configHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -21,9 +22,8 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "", http.StatusNotFound)
|
http.Error(w, "", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err = w.Write(data); err != nil {
|
// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html
|
||||||
log.Warn().Err(err).Caller().Send()
|
Response(w, data, "application/yaml")
|
||||||
}
|
|
||||||
|
|
||||||
case "POST", "PATCH":
|
case "POST", "PATCH":
|
||||||
data, err := io.ReadAll(r.Body)
|
data, err := io.ReadAll(r.Body)
|
||||||
@@ -41,8 +41,7 @@ func configHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// validate config
|
// validate config
|
||||||
var tmp struct{}
|
if err = yaml.Unmarshal(data, map[string]any{}); err != nil {
|
||||||
if err = yaml.Unmarshal(data, &tmp); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AlexxIT/go2rtc/www"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/www"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initStatic(staticDir string) {
|
func initStatic(staticDir string) {
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# WebSocket
|
||||||
|
|
||||||
|
Endpoint: `/api/ws`
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
|
||||||
|
- `src` (required) - Stream name
|
||||||
|
|
||||||
|
### WebRTC
|
||||||
|
|
||||||
|
Request SDP:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"webrtc/offer","value":"v=0\r\n..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response SDP:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"webrtc/answer","value":"v=0\r\n..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
Request/response candidate:
|
||||||
|
|
||||||
|
- empty value also allowed and optional
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"webrtc/candidate","value":"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MSE
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
- codecs list optional
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"mse","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"mse","value":"video/mp4; codecs=\"avc1.64001F,mp4a.40.2\""}
|
||||||
|
```
|
||||||
|
|
||||||
|
### HLS
|
||||||
|
|
||||||
|
Request:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"hls","value":"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"hls","value":"#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\"avc1.64001F,mp4a.40.2\"\nhls/playlist.m3u8?id=DvmHdd9w"}
|
||||||
|
```
|
||||||
|
|
||||||
|
### MJPEG
|
||||||
|
|
||||||
|
Request/response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"mjpeg"}
|
||||||
|
```
|
||||||
@@ -1,39 +1,63 @@
|
|||||||
package api
|
package ws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gorilla/websocket"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
|
"github.com/AlexxIT/go2rtc/internal/app"
|
||||||
|
"github.com/AlexxIT/go2rtc/pkg/creds"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func Init() {
|
||||||
|
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
|
// Message - struct for data exchange in Web API
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Value any `json:"value,omitempty"`
|
Value any `json:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) String() string {
|
func (m *Message) String() (value string) {
|
||||||
if s, ok := m.Value.(string); ok {
|
if s, ok := m.Value.(string); ok {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return ""
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) GetString(key string) string {
|
func (m *Message) Unmarshal(v any) error {
|
||||||
if v, ok := m.Value.(map[string]any); ok {
|
b, err := json.Marshal(m.Value)
|
||||||
if s, ok := v[key].(string); ok {
|
if err != nil {
|
||||||
return s
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ""
|
return json.Unmarshal(b, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WSHandler func(tr *Transport, msg *Message) error
|
type WSHandler func(tr *Transport, msg *Message) error
|
||||||
|
|
||||||
func HandleWS(msgType string, handler WSHandler) {
|
func HandleFunc(msgType string, handler WSHandler) {
|
||||||
wsHandlers[msgType] = handler
|
wsHandlers[msgType] = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +84,7 @@ func initWS(origin string) {
|
|||||||
if o.Host == r.Host {
|
if o.Host == r.Host {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
log.Trace().Msgf("[api.ws] origin=%s, host=%s", o.Host, r.Host)
|
log.Trace().Msgf("[api] ws origin=%s, host=%s", o.Host, r.Host)
|
||||||
// https://github.com/AlexxIT/go2rtc/issues/118
|
// https://github.com/AlexxIT/go2rtc/issues/118
|
||||||
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
if i := strings.IndexByte(o.Host, ':'); i > 0 {
|
||||||
return o.Host[:i] == r.Host
|
return o.Host[:i] == r.Host
|
||||||
@@ -84,13 +108,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr := &Transport{Request: r}
|
tr := &Transport{Request: r}
|
||||||
tr.OnWrite(func(msg any) {
|
tr.OnWrite(func(msg any) error {
|
||||||
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||||
|
|
||||||
if data, ok := msg.([]byte); ok {
|
if data, ok := msg.([]byte); ok {
|
||||||
_ = ws.WriteMessage(websocket.BinaryMessage, data)
|
return ws.WriteMessage(websocket.BinaryMessage, data)
|
||||||
} else {
|
} else {
|
||||||
_ = ws.WriteJSON(msg)
|
return ws.WriteJSON(msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -104,12 +128,13 @@ func apiWS(w http.ResponseWriter, r *http.Request) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Str("type", msg.Type).Msg("[api.ws] msg")
|
log.Trace().Str("type", msg.Type).Msg("[api] ws msg")
|
||||||
|
|
||||||
if handler := wsHandlers[msg.Type]; handler != nil {
|
if handler := wsHandlers[msg.Type]; handler != nil {
|
||||||
go func() {
|
go func() {
|
||||||
if err = handler(tr, msg); err != nil {
|
if err = handler(tr, msg); err != nil {
|
||||||
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + err.Error()})
|
errMsg := creds.SecretString(err.Error())
|
||||||
|
tr.Write(&Message{Type: "error", Value: msg.Type + ": " + errMsg})
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@@ -130,11 +155,11 @@ type Transport struct {
|
|||||||
wrmx sync.Mutex
|
wrmx sync.Mutex
|
||||||
|
|
||||||
onChange func()
|
onChange func()
|
||||||
onWrite func(msg any)
|
onWrite func(msg any) error
|
||||||
onClose []func()
|
onClose []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transport) OnWrite(f func(msg any)) {
|
func (t *Transport) OnWrite(f func(msg any) error) {
|
||||||
t.mx.Lock()
|
t.mx.Lock()
|
||||||
if t.onChange != nil {
|
if t.onChange != nil {
|
||||||
t.onChange()
|
t.onChange()
|
||||||
@@ -145,7 +170,7 @@ func (t *Transport) OnWrite(f func(msg any)) {
|
|||||||
|
|
||||||
func (t *Transport) Write(msg any) {
|
func (t *Transport) Write(msg any) {
|
||||||
t.wrmx.Lock()
|
t.wrmx.Lock()
|
||||||
t.onWrite(msg)
|
_ = t.onWrite(msg)
|
||||||
t.wrmx.Unlock()
|
t.wrmx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,3 +208,20 @@ func (t *Transport) WithContext(f func(ctx map[any]any)) {
|
|||||||
f(t.ctx)
|
f(t.ctx)
|
||||||
t.mx.Unlock()
|
t.mx.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Writer() io.Writer {
|
||||||
|
return &writer{t: t}
|
||||||
|
}
|
||||||
|
|
||||||
|
type writer struct {
|
||||||
|
t *Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writer) Write(p []byte) (n int, err error) {
|
||||||
|
w.t.wrmx.Lock()
|
||||||
|
if err = w.t.onWrite(p); err == nil {
|
||||||
|
n = len(p)
|
||||||
|
}
|
||||||
|
w.t.wrmx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# App
|
||||||
|
|
||||||
|
The application module is responsible for reading configuration files, running other modules and setting up [logs](#log).
|
||||||
|
|
||||||
|
The configuration can be edited through the application's WebUI with code highlighting, syntax and specification checking.
|
||||||
|
|
||||||
|
- By default, go2rtc will search for the `go2rtc.yaml` config file in the current working directory
|
||||||
|
- go2rtc supports multiple config files:
|
||||||
|
- `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml`
|
||||||
|
- go2rtc supports inline config in multiple formats from the command line:
|
||||||
|
- **YAML**: `go2rtc -c '{log: {format: text}}'`
|
||||||
|
- **JSON**: `go2rtc -c '{"log":{"format":"text"}}'`
|
||||||
|
- **key=value**: `go2rtc -c log.format=text`
|
||||||
|
- Each subsequent config will overwrite the previous one (but only for defined params)
|
||||||
|
|
||||||
|
```
|
||||||
|
go2rtc -config "{log: {format: text}}" -config /config/go2rtc.yaml -config "{rtsp: {listen: ''}}" -config /usr/local/go2rtc/go2rtc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
or a simpler version
|
||||||
|
|
||||||
|
```
|
||||||
|
go2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
There is support for loading external variables into the config. First, they will be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0
|
||||||
|
|
||||||
|
rtsp:
|
||||||
|
username: ${RTSP_USER:admin} # "admin" if "RTSP_USER" not set
|
||||||
|
password: ${RTSP_PASS:secret} # "secret" if "RTSP_PASS" not set
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Schema
|
||||||
|
|
||||||
|
Editors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) support autocomplete and syntax validation.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/www/schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
or from a running go2rtc:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# yaml-language-server: $schema=http://localhost:1984/schema.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
- Default values may change in updates
|
||||||
|
- FFmpeg module has many presets, they are not listed here because they may also change in updates
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api:
|
||||||
|
listen: ":1984" # default public port for WebUI and HTTP API
|
||||||
|
|
||||||
|
ffmpeg:
|
||||||
|
bin: "ffmpeg" # default binary path for FFmpeg
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: "info" # default log level
|
||||||
|
output: "stdout"
|
||||||
|
time: "UNIXMS"
|
||||||
|
|
||||||
|
rtsp:
|
||||||
|
listen: ":8554" # default public port for RTSP server
|
||||||
|
default_query: "video&audio"
|
||||||
|
|
||||||
|
srtp:
|
||||||
|
listen: ":8443" # default public port for SRTP server (used for HomeKit)
|
||||||
|
|
||||||
|
webrtc:
|
||||||
|
listen: ":8555" # default public port for WebRTC server (TCP and UDP)
|
||||||
|
ice_servers:
|
||||||
|
- urls: [ "stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Log
|
||||||
|
|
||||||
|
You can set different log levels for different modules.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
log:
|
||||||
|
format: "" # empty (default, autodetect color support), color, json, text
|
||||||
|
level: "info" # disabled, trace, debug, info (default), warn, error
|
||||||
|
output: "stdout" # empty (only to memory), stderr, stdout (default)
|
||||||
|
time: "UNIXMS" # empty (disable timestamp), UNIXMS (default), UNIXMICRO, UNIXNANO
|
||||||
|
|
||||||
|
api: trace # module name: log level
|
||||||
|
```
|
||||||
|
|
||||||
|
Modules: `api`, `streams`, `rtsp`, `webrtc`, `mp4`, `hls`, `mjpeg`, `hass`, `homekit`, `onvif`, `rtmp`, `webtorrent`, `wyoming`, `echo`, `exec`, `expr`, `ffmpeg`, `wyze`, `xiaomi`.
|
||||||
@@ -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,19 +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/cmd/srtp.Init"),
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/srtp.Init"),
|
||||||
|
|
||||||
|
// homekit
|
||||||
|
[]byte("created by github.com/AlexxIT/go2rtc/internal/homekit.Init"),
|
||||||
|
|
||||||
// webrtc/api.go
|
// webrtc/api.go
|
||||||
[]byte("created by github.com/pion/ice/v2.NewTCPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v4.NewTCPMuxDefault"),
|
||||||
[]byte("created by github.com/pion/ice/v2.NewUDPMuxDefault"),
|
[]byte("created by github.com/pion/ice/v4.NewUDPMuxDefault"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
func stackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -51,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")
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user