diff --git a/Pipfile b/Pipfile index 69294954..d1701af2 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ aiohttp = "*" yt-dlp = "*" caribou = "*" coloredlogs = "*" +ffprobe-python = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 0ecc87f3..118187d9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "08575f369e4bb22a440dd5635b4032bab5326794ccac7944ea4a39d0290d32c6" + "sha256": "258ec26d54c61c901fb67cb207bfb76d71d60ae0866f2ff3ab9f5e2e97940c84" }, "pipfile-spec": 6, "requires": { @@ -18,85 +18,85 @@ "default": { "aiohttp": { "hashes": [ - "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994", - "sha256:05a183f1978802588711aed0dea31e697d760ce9055292db9dc1604daa9a8ded", - "sha256:09f23292d29135025e19e8ff4f0a68df078fe4ee013bca0105b2e803989de92d", - "sha256:11ca808f9a6b63485059f5f6e164ef7ec826483c1212a44f268b3653c91237d8", - "sha256:1736d87dad8ef46a8ec9cddd349fa9f7bd3a064c47dd6469c0d6763d3d49a4fc", - "sha256:1df43596b826022b14998f0460926ce261544fedefe0d2f653e1b20f49e96454", - "sha256:23170247ef89ffa842a02bbfdc425028574d9e010611659abeb24d890bc53bb8", - "sha256:2779f5e7c70f7b421915fd47db332c81de365678180a9f3ab404088f87ba5ff9", - "sha256:28185e36a78d247c55e9fbea2332d16aefa14c5276a582ce7a896231c6b1c208", - "sha256:2cbc14a13fb6b42d344e4f27746a4b03a2cb0c1c3c5b932b0d6ad8881aa390e3", - "sha256:2d71abc15ff7047412ef26bf812dfc8d0d1020d664617f4913df2df469f26b76", - "sha256:2d820162c8c2bdbe97d328cd4f417c955ca370027dce593345e437b2e9ffdc4d", - "sha256:317719d7f824eba55857fe0729363af58e27c066c731bc62cd97bc9c3d9c7ea4", - "sha256:35a68cd63ca6aaef5707888f17a70c36efe62b099a4e853d33dc2e9872125be8", - "sha256:3607375053df58ed6f23903aa10cf3112b1240e8c799d243bbad0f7be0666986", - "sha256:366bc870d7ac61726f32a489fbe3d1d8876e87506870be66b01aeb84389e967e", - "sha256:3abf0551874fecf95f93b58f25ef4fc9a250669a2257753f38f8f592db85ddea", - "sha256:3d7f6235c7475658acfc1769d968e07ab585c79f6ca438ddfecaa9a08006aee2", - "sha256:3dd8119752dd30dd7bca7d4bc2a92a59be6a003e4e5c2cf7e248b89751b8f4b7", - "sha256:42fe4fd9f0dfcc7be4248c162d8056f1d51a04c60e53366b0098d1267c4c9da8", - "sha256:45820ddbb276113ead8d4907a7802adb77548087ff5465d5c554f9aa3928ae7d", - "sha256:4790e44f46a4aa07b64504089def5744d3b6780468c4ec3a1a36eb7f2cae9814", - "sha256:4afa8f71dba3a5a2e1e1282a51cba7341ae76585345c43d8f0e624882b622218", - "sha256:4b777c9286b6c6a94f50ddb3a6e730deec327e9e2256cb08b5530db0f7d40fd8", - "sha256:4ee1b4152bc3190cc40ddd6a14715e3004944263ea208229ab4c297712aa3075", - "sha256:51a4cd44788ea0b5e6bb8fa704597af3a30be75503a7ed1098bc5b8ffdf6c982", - "sha256:536b01513d67d10baf6f71c72decdf492fb7433c5f2f133e9a9087379d4b6f31", - "sha256:571760ad7736b34d05597a1fd38cbc7d47f7b65deb722cb8e86fd827404d1f6b", - "sha256:5a2eb5311a37fe105aa35f62f75a078537e1a9e4e1d78c86ec9893a3c97d7a30", - "sha256:5ab16c254e2312efeb799bc3c06897f65a133b38b69682bf75d1f1ee1a9c43a9", - "sha256:65b0a70a25456d329a5e1426702dde67be0fb7a4ead718005ba2ca582d023a94", - "sha256:673343fbc0c1ac44d0d2640addc56e97a052504beacd7ade0dc5e76d3a4c16e8", - "sha256:6777a390e41e78e7c45dab43a4a0196c55c3b8c30eebe017b152939372a83253", - "sha256:6896b8416be9ada4d22cd359d7cb98955576ce863eadad5596b7cdfbf3e17c6c", - "sha256:694df243f394629bcae2d8ed94c589a181e8ba8604159e6e45e7b22e58291113", - "sha256:70e851f596c00f40a2f00a46126c95c2e04e146015af05a9da3e4867cfc55911", - "sha256:7276fe0017664414fdc3618fca411630405f1aaf0cc3be69def650eb50441787", - "sha256:76a86a9989ebf82ee61e06e2bab408aec4ea367dc6da35145c3352b60a112d11", - "sha256:7a94bde005a8f926d0fa38b88092a03dea4b4875a61fbcd9ac6f4351df1b57cd", - "sha256:7ae5f99a32c53731c93ac3075abd3e1e5cfbe72fc3eaac4c27c9dd64ba3b19fe", - "sha256:7e8a3b79b6d186a9c99761fd4a5e8dd575a48d96021f220ac5b5fa856e5dd029", - "sha256:816f4db40555026e4cdda604a1088577c1fb957d02f3f1292e0221353403f192", - "sha256:8303531e2c17b1a494ffaeba48f2da655fe932c4e9a2626c8718403c83e5dd2b", - "sha256:8488519aa05e636c5997719fe543c8daf19f538f4fa044f3ce94bee608817cff", - "sha256:87c8b0a6487e8109427ccf638580865b54e2e3db4a6e0e11c02639231b41fc0f", - "sha256:8c9e5f4d7208cda1a2bb600e29069eecf857e6980d0ccc922ccf9d1372c16f4b", - "sha256:94697c7293199c2a2551e3e3e18438b4cba293e79c6bc2319f5fd652fccb7456", - "sha256:9623cfd9e85b76b83ef88519d98326d4731f8d71869867e47a0b979ffec61c73", - "sha256:98d21092bf2637c5fa724a428a69e8f5955f2182bff61f8036827cf6ce1157bf", - "sha256:99ae01fb13a618b9942376df77a1f50c20a281390dad3c56a6ec2942e266220d", - "sha256:9c196b30f1b1aa3363a69dd69079ae9bec96c2965c4707eaa6914ba099fb7d4f", - "sha256:a00ce44c21612d185c5275c5cba4bab8d7c1590f248638b667ed8a782fa8cd6f", - "sha256:a1b66dbb8a7d5f50e9e2ea3804b01e766308331d0cac76eb30c563ac89c95985", - "sha256:a1d7edf74a36de0e5ca50787e83a77cf352f5504eb0ffa3f07000a911ba353fb", - "sha256:a1e3b3c107ccb0e537f309f719994a55621acd2c8fdf6d5ce5152aed788fb940", - "sha256:a486ddf57ab98b6d19ad36458b9f09e6022de0381674fe00228ca7b741aacb2f", - "sha256:ac9669990e2016d644ba8ae4758688534aabde8dbbc81f9af129c3f5f01ca9cd", - "sha256:b1a2ea8252cacc7fd51df5a56d7a2bb1986ed39be9397b51a08015727dfb69bd", - "sha256:c5b7bf8fe4d39886adc34311a233a2e01bc10eb4e842220235ed1de57541a896", - "sha256:c67a51ea415192c2e53e4e048c78bab82d21955b4281d297f517707dc836bf3d", - "sha256:ca4fddf84ac7d8a7d0866664936f93318ff01ee33e32381a115b19fb5a4d1202", - "sha256:d5b9345ab92ebe6003ae11d8092ce822a0242146e6fa270889b9ba965457ca40", - "sha256:d97c3e286d0ac9af6223bc132dc4bad6540b37c8d6c0a15fe1e70fb34f9ec411", - "sha256:db04d1de548f7a62d1dd7e7cdf7c22893ee168e22701895067a28a8ed51b3735", - "sha256:dcf71c55ec853826cd70eadb2b6ac62ec577416442ca1e0a97ad875a1b3a0305", - "sha256:de3cc86f4ea8b4c34a6e43a7306c40c1275e52bfa9748d869c6b7d54aa6dad80", - "sha256:deac0a32aec29608eb25d730f4bc5a261a65b6c48ded1ed861d2a1852577c932", - "sha256:e18d92c3e9e22553a73e33784fcb0ed484c9874e9a3e96c16a8d6a1e74a0217b", - "sha256:eb6dfd52063186ac97b4caa25764cdbcdb4b10d97f5c5f66b0fa95052e744eb7", - "sha256:f09960b5bb1017d16c0f9e9f7fc42160a5a49fa1e87a175fd4a2b1a1833ea0af", - "sha256:f1e4f254e9c35d8965d377e065c4a8a55d396fe87c8e7e8429bcfdeeb229bfb3", - "sha256:f32c86dc967ab8c719fd229ce71917caad13cc1e8356ee997bf02c5b368799bf", - "sha256:f50b4663c3e0262c3a361faf440761fbef60ccdde5fe8545689a4b3a3c149fb4", - "sha256:f8e05f5163528962ce1d1806fce763ab893b1c5b7ace0a3538cd81a90622f844", - "sha256:f929f4c9b9a00f3e6cc0587abb95ab9c05681f8b14e0fe1daecfa83ea90f8318", - "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887" + "sha256:02ab6006ec3c3463b528374c4cdce86434e7b89ad355e7bf29e2f16b46c7dd6f", + "sha256:04fa38875e53eb7e354ece1607b1d2fdee2d175ea4e4d745f6ec9f751fe20c7c", + "sha256:0b0a6a36ed7e164c6df1e18ee47afbd1990ce47cb428739d6c99aaabfaf1b3af", + "sha256:0d406b01a9f5a7e232d1b0d161b40c05275ffbcbd772dc18c1d5a570961a1ca4", + "sha256:0e49b08eafa4f5707ecfb321ab9592717a319e37938e301d462f79b4e860c32a", + "sha256:0e7ba7ff228c0d9a2cd66194e90f2bca6e0abca810b786901a569c0de082f489", + "sha256:11cb254e397a82efb1805d12561e80124928e04e9c4483587ce7390b3866d213", + "sha256:11ff168d752cb41e8492817e10fb4f85828f6a0142b9726a30c27c35a1835f01", + "sha256:176df045597e674fa950bf5ae536be85699e04cea68fa3a616cf75e413737eb5", + "sha256:219a16763dc0294842188ac8a12262b5671817042b35d45e44fd0a697d8c8361", + "sha256:22698f01ff5653fe66d16ffb7658f582a0ac084d7da1323e39fd9eab326a1f26", + "sha256:237533179d9747080bcaad4d02083ce295c0d2eab3e9e8ce103411a4312991a0", + "sha256:289ba9ae8e88d0ba16062ecf02dd730b34186ea3b1e7489046fc338bdc3361c4", + "sha256:2c59e0076ea31c08553e868cec02d22191c086f00b44610f8ab7363a11a5d9d8", + "sha256:2c9376e2b09895c8ca8b95362283365eb5c03bdc8428ade80a864160605715f1", + "sha256:3135713c5562731ee18f58d3ad1bf41e1d8883eb68b363f2ffde5b2ea4b84cc7", + "sha256:3b9c7426923bb7bd66d409da46c41e3fb40f5caf679da624439b9eba92043fa6", + "sha256:3c0266cd6f005e99f3f51e583012de2778e65af6b73860038b968a0a8888487a", + "sha256:41473de252e1797c2d2293804e389a6d6986ef37cbb4a25208de537ae32141dd", + "sha256:4831df72b053b1eed31eb00a2e1aff6896fb4485301d4ccb208cac264b648db4", + "sha256:49f0c1b3c2842556e5de35f122fc0f0b721334ceb6e78c3719693364d4af8499", + "sha256:4b4c452d0190c5a820d3f5c0f3cd8a28ace48c54053e24da9d6041bf81113183", + "sha256:4ee8caa925aebc1e64e98432d78ea8de67b2272252b0a931d2ac3bd876ad5544", + "sha256:500f1c59906cd142d452074f3811614be04819a38ae2b3239a48b82649c08821", + "sha256:5216b6082c624b55cfe79af5d538e499cd5f5b976820eac31951fb4325974501", + "sha256:54311eb54f3a0c45efb9ed0d0a8f43d1bc6060d773f6973efd90037a51cd0a3f", + "sha256:54631fb69a6e44b2ba522f7c22a6fb2667a02fd97d636048478db2fd8c4e98fe", + "sha256:565760d6812b8d78d416c3c7cfdf5362fbe0d0d25b82fed75d0d29e18d7fc30f", + "sha256:598db66eaf2e04aa0c8900a63b0101fdc5e6b8a7ddd805c56d86efb54eb66672", + "sha256:5c4fa235d534b3547184831c624c0b7c1e262cd1de847d95085ec94c16fddcd5", + "sha256:69985d50a2b6f709412d944ffb2e97d0be154ea90600b7a921f95a87d6f108a2", + "sha256:69da0f3ed3496808e8cbc5123a866c41c12c15baaaead96d256477edf168eb57", + "sha256:6c93b7c2e52061f0925c3382d5cb8980e40f91c989563d3d32ca280069fd6a87", + "sha256:70907533db712f7aa791effb38efa96f044ce3d4e850e2d7691abd759f4f0ae0", + "sha256:81b77f868814346662c96ab36b875d7814ebf82340d3284a31681085c051320f", + "sha256:82eefaf1a996060602f3cc1112d93ba8b201dbf5d8fd9611227de2003dddb3b7", + "sha256:85c3e3c9cb1d480e0b9a64c658cd66b3cfb8e721636ab8b0e746e2d79a7a9eed", + "sha256:8a22a34bc594d9d24621091d1b91511001a7eea91d6652ea495ce06e27381f70", + "sha256:8cef8710fb849d97c533f259103f09bac167a008d7131d7b2b0e3a33269185c0", + "sha256:8d44e7bf06b0c0a70a20f9100af9fcfd7f6d9d3913e37754c12d424179b4e48f", + "sha256:8d7f98fde213f74561be1d6d3fa353656197f75d4edfbb3d94c9eb9b0fc47f5d", + "sha256:8d8e4450e7fe24d86e86b23cc209e0023177b6d59502e33807b732d2deb6975f", + "sha256:8fc49a87ac269d4529da45871e2ffb6874e87779c3d0e2ccd813c0899221239d", + "sha256:90ec72d231169b4b8d6085be13023ece8fa9b1bb495e4398d847e25218e0f431", + "sha256:91c742ca59045dce7ba76cab6e223e41d2c70d79e82c284a96411f8645e2afff", + "sha256:9b05d33ff8e6b269e30a7957bd3244ffbce2a7a35a81b81c382629b80af1a8bf", + "sha256:9b05d5cbe9dafcdc733262c3a99ccf63d2f7ce02543620d2bd8db4d4f7a22f83", + "sha256:9c5857612c9813796960c00767645cb5da815af16dafb32d70c72a8390bbf690", + "sha256:a34086c5cc285be878622e0a6ab897a986a6e8bf5b67ecb377015f06ed316587", + "sha256:ab221850108a4a063c5b8a70f00dd7a1975e5a1713f87f4ab26a46e5feac5a0e", + "sha256:b796b44111f0cab6bbf66214186e44734b5baab949cb5fb56154142a92989aeb", + "sha256:b8c3a67eb87394386847d188996920f33b01b32155f0a94f36ca0e0c635bf3e3", + "sha256:bcb6532b9814ea7c5a6a3299747c49de30e84472fa72821b07f5a9818bce0f66", + "sha256:bcc0ea8d5b74a41b621ad4a13d96c36079c81628ccc0b30cfb1603e3dfa3a014", + "sha256:bea94403a21eb94c93386d559bce297381609153e418a3ffc7d6bf772f59cc35", + "sha256:bff7e2811814fa2271be95ab6e84c9436d027a0e59665de60edf44e529a42c1f", + "sha256:c72444d17777865734aa1a4d167794c34b63e5883abb90356a0364a28904e6c0", + "sha256:c7b5d5d64e2a14e35a9240b33b89389e0035e6de8dbb7ffa50d10d8b65c57449", + "sha256:c7e939f1ae428a86e4abbb9a7c4732bf4706048818dfd979e5e2839ce0159f23", + "sha256:c88a15f272a0ad3d7773cf3a37cc7b7d077cbfc8e331675cf1346e849d97a4e5", + "sha256:c9110c06eaaac7e1f5562caf481f18ccf8f6fdf4c3323feab28a93d34cc646bd", + "sha256:ca7ca5abfbfe8d39e653870fbe8d7710be7a857f8a8386fc9de1aae2e02ce7e4", + "sha256:cae4c0c2ca800c793cae07ef3d40794625471040a87e1ba392039639ad61ab5b", + "sha256:cdefe289681507187e375a5064c7599f52c40343a8701761c802c1853a504558", + "sha256:cf2a0ac0615842b849f40c4d7f304986a242f1e68286dbf3bd7a835e4f83acfd", + "sha256:cfeadf42840c1e870dc2042a232a8748e75a36b52d78968cda6736de55582766", + "sha256:d737e69d193dac7296365a6dcb73bbbf53bb760ab25a3727716bbd42022e8d7a", + "sha256:d7481f581251bb5558ba9f635db70908819caa221fc79ee52a7f58392778c636", + "sha256:df9cf74b9bc03d586fc53ba470828d7b77ce51b0582d1d0b5b2fb673c0baa32d", + "sha256:e1f80197f8b0b846a8d5cf7b7ec6084493950d0882cc5537fb7b96a69e3c8590", + "sha256:ecca113f19d5e74048c001934045a2b9368d77b0b17691d905af18bd1c21275e", + "sha256:ee2527134f95e106cc1653e9ac78846f3a2ec1004cf20ef4e02038035a74544d", + "sha256:f27fdaadce22f2ef950fc10dcdf8048407c3b42b73779e48a4e76b3c35bca26c", + "sha256:f694dc8a6a3112059258a725a4ebe9acac5fe62f11c77ac4dcf896edfa78ca28", + "sha256:f800164276eec54e0af5c99feb9494c295118fc10a11b997bbb1348ba1a52065", + "sha256:ffcd828e37dc219a72c9012ec44ad2e7e3066bec6ff3aaa19e7d435dbf4032ca" ], "index": "pypi", - "version": "==3.9.0" + "version": "==3.9.1" }, "aiosignal": { "hashes": [ @@ -345,6 +345,14 @@ "index": "pypi", "version": "==15.0.1" }, + "ffprobe-python": { + "hashes": [ + "sha256:b02576228f6da0b0bb973e15e5dadb132013a4ded202dabb89ac77779e9634d6", + "sha256:b1d3e69e65e0814e28575f63a47b836a0b7cc8f1ffedd62d27152d9e4488214a" + ], + "index": "pypi", + "version": "==1.0.3" + }, "frozenlist": { "hashes": [ "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6", @@ -430,11 +438,11 @@ }, "idna": { "hashes": [ - "sha256:27009fe2735bf8723353582d48575b23c533cc2c2de7b5a68908d91b5eb18d08", - "sha256:79b8f0ac92d2351be5f6122356c9a592c96d81c9a79e4b488bf2a6a15f88057a" + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" ], "markers": "python_version >= '3.5'", - "version": "==3.5" + "version": "==3.6" }, "multidict": { "hashes": [ diff --git a/app/main.py b/app/main.py index 9fe28d8c..e1a96792 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import asyncio import os from src.Config import Config from src.Notifier import Notifier @@ -7,6 +8,8 @@ from src.DownloadQueue import DownloadQueue from src.Utils import ObjectSerializer from aiohttp import web from aiohttp.web import Request, Response +from src.M3u8 import M3u8 +from src.Segments import Segments import socketio import logging import caribou @@ -178,6 +181,56 @@ class Main: return web.Response(text=self.serializer.encode(history)) + @self.routes.get(self.config.url_prefix + 'm3u8/{file:.*}') + async def m3u8(request: Request) -> Response: + file: str = request.match_info.get('file') + + if not file: + raise web.HTTPBadRequest(reason='file is required') + + return web.Response( + text=M3u8(self.config).make_stream(file), + headers={ + 'Content-Type': 'application/x-mpegURL', + 'Cache-Control': 'no-cache', + 'Access-Control-Max-Age': "300", + } + ) + + @self.routes.get(self.config.url_prefix + 'segments/{segment:\d+}/{file:.*}') + async def segments(request: Request) -> Response: + file: str = request.match_info.get('file') + segment: int = request.match_info.get('segment') + sd: int = request.query.get('sd') + vc: int = int(request.query.get('vc', 0)) + ac: int = int(request.query.get('ac', 0)) + + if not file: + raise web.HTTPBadRequest(reason='file is required') + + if not segment: + raise web.HTTPBadRequest(reason='segment is required') + + segmenter = Segments( + config=self.config, + segment_index=int(segment), + segment_duration=float('{:.6f}'.format(float(sd if sd else M3u8.segment_duration))), + vconvert=True if vc == 1 else False, + aconvert=True if ac == 1 else False + ) + + return web.Response( + body=await segmenter.stream(file), + headers={ + 'Content-Type': 'video/mpegts', + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache', + 'X-Accel-Buffering': 'no', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Max-Age': "300", + } + ) + @self.routes.get(self.config.url_prefix) async def index(_) -> Response: return web.FileResponse(os.path.join( diff --git a/app/src/M3u8.py b/app/src/M3u8.py new file mode 100644 index 00000000..e2e13fdb --- /dev/null +++ b/app/src/M3u8.py @@ -0,0 +1,76 @@ +import json +import logging +import math +import os +from src.Utils import calcDownloadPath +from ffprobe import FFProbe +from src.Config import Config + + +class M3u8: + ok_vcodecs: tuple = ('h264', 'x264', 'avc',) + ok_acodecs: tuple = ('aac', 'mp3', 'opus',) + + segment_duration: float = 10.000000 + config: Config = None + download_path: str = None + + def __init__(self, config: Config, segment_duration: float = 6.000000): + self.config = config + self.download_path = self.config.download_path + self.segment_duration = float(segment_duration) + + def make_stream(self, file: str): + realFile: str = calcDownloadPath( + basePath=self.download_path, + folder=file, + createPath=False + ) + + if not os.path.exists(realFile): + raise Exception(f"File {realFile} does not exist.") + + metadata = FFProbe(realFile) + + # video duration. + duration: float = float(metadata.streams[0].duration) + + m3u8 = "#EXTM3U\n" + m3u8 += "#EXT-X-VERSION:3\n" + m3u8 += f"#EXT-X-TARGETDURATION:{int(self.segment_duration)}\n" + m3u8 += "#EXT-X-MEDIA-SEQUENCE:0\n" + m3u8 += "#EXT-X-PLAYLIST-TYPE:VOD\n" + + segmentSize: float = '{:.6f}'.format(self.segment_duration) + splits: int = math.ceil(duration / self.segment_duration) + + segmentParams = { + } + + for stream in metadata.streams: + if stream.codec_type == 'video': + if stream.codec_name not in self.ok_vcodecs: + segmentParams['vc'] = 1 + if stream.codec_type == 'audio': + if stream.codec_name not in self.ok_acodecs: + segmentParams['ac'] = 1 + + for i in range(splits): + if (i + 1) == splits: + segmentParams.update({ + 'sd': '{:.6f}'.format(duration - (i * self.segment_duration)) + }) + m3u8 += f"#EXTINF:{segmentParams['sd']}, nodesc\n" + else: + m3u8 += f"#EXTINF:{segmentSize}, nodesc\n" + + m3u8 += f"{self.config.url_prefix}segments/{i}/{file}" + if len(segmentParams) > 0: + m3u8 += '?'+'&'.join([f"{key}={value}" for key, + value in segmentParams.items()]) + m3u8 += "\n" + + m3u8 += "#EXT-X-ENDLIST\n" + + # m3u8 = f"{json.dumps(metadata,indent=2,ensure_ascii=False,default=lambda o: o.__dict__)}\n{m3u8}" + return m3u8 diff --git a/app/src/Segments.py b/app/src/Segments.py new file mode 100644 index 00000000..b9f42321 --- /dev/null +++ b/app/src/Segments.py @@ -0,0 +1,115 @@ +import hashlib +import json +import logging +import math +import os +import subprocess +import tempfile +from src.Utils import calcDownloadPath +from src.Config import Config + + +class Segments: + config: Config = None + download_path: str = None + segment_duration: int + segment_index: int + vconvert: bool + aconvert: bool + + def __init__(self, config: Config, segment_index: int, segment_duration: float, vconvert: bool, aconvert: bool): + self.config = config + self.download_path = self.config.download_path + self.segment_duration = float(segment_duration) + self.segment_index = int(segment_index) + self.vconvert = bool(vconvert) + self.aconvert = bool(aconvert) + + async def stream(self, file: str) -> bytes: + realFile: str = calcDownloadPath( + basePath=self.download_path, + folder=file, + createPath=False + ) + + if not os.path.exists(realFile): + raise Exception(f"File {realFile} does not exist.") + + tmpDir: str = tempfile.gettempdir() + tmpFile = os.path.join( + tmpDir, f'ytptube_stream.{hashlib.md5(realFile.encode("utf-8")).hexdigest()}') + + if not os.path.exists(tmpFile): + os.symlink(realFile, tmpFile) + + if self.segment_index == 0: + startTime: float = '{:.6f}'.format(0) + else: + startTime: float = '{:.6f}'.format( + (self.segment_duration * self.segment_index + )) + + ffmpegCmd = [] + ffmpegCmd.append('ffmpeg') + ffmpegCmd.append('-xerror') + ffmpegCmd.append('-hide_banner') + ffmpegCmd.append('-loglevel') + ffmpegCmd.append('error') + + ffmpegCmd.append('-ss') + ffmpegCmd.append(str(startTime if startTime else '0.00000')) + ffmpegCmd.append('-t') + ffmpegCmd.append(str('{:.6f}'.format(self.segment_duration))) + + ffmpegCmd.append('-copyts') + + ffmpegCmd.append('-i') + ffmpegCmd.append(f'file:{tmpFile}') + + ffmpegCmd.append('-pix_fmt') + ffmpegCmd.append('yuv420p') + ffmpegCmd.append('-g') + ffmpegCmd.append('52') + + ffmpegCmd.append('-map') + ffmpegCmd.append('0:v:0') + ffmpegCmd.append('-strict') + ffmpegCmd.append('-2') + + ffmpegCmd.append('-codec:v') + ffmpegCmd.append('libx264' if self.vconvert else 'copy') + if self.vconvert: + ffmpegCmd.append('-crf') + ffmpegCmd.append('23') + ffmpegCmd.append('-preset:v') + ffmpegCmd.append('fast') + ffmpegCmd.append('-level') + ffmpegCmd.append('4.1') + ffmpegCmd.append('-profile:v') + ffmpegCmd.append('baseline') + + # audio section. + ffmpegCmd.append('-map') + ffmpegCmd.append('0:a:0') + ffmpegCmd.append('-codec:a') + ffmpegCmd.append('aac' if self.aconvert else 'copy') + if self.aconvert: + ffmpegCmd.append('-b:a') + ffmpegCmd.append('192k') + ffmpegCmd.append('-ar') + ffmpegCmd.append('22050') + ffmpegCmd.append('-ac') + ffmpegCmd.append('2') + + ffmpegCmd.append('-sn') + + ffmpegCmd.append('-muxdelay') + ffmpegCmd.append('0') + ffmpegCmd.append('-f') + ffmpegCmd.append('mpegts') + ffmpegCmd.append('pipe:1') + + logging.info(f'{self.aconvert}, {self.vconvert} '+' '.join(ffmpegCmd)) + proc = subprocess.run(ffmpegCmd, stdout=subprocess.PIPE) + + return proc.stdout diff --git a/app/src/Utils.py b/app/src/Utils.py index e8055506..eac4637a 100644 --- a/app/src/Utils.py +++ b/app/src/Utils.py @@ -139,7 +139,7 @@ def getAttributes(vclass: str | type) -> dict: return attrs -def calcDownloadPath(basePath: str, folder: str = None) -> str: +def calcDownloadPath(basePath: str, folder: str = None, createPath: bool = True) -> str: """Calculates download path And prevents directory traversal attacks. Returns: @@ -155,7 +155,7 @@ def calcDownloadPath(basePath: str, folder: str = None) -> str: raise Exception( f'Folder "{folder}" must resolve inside the base download directory "{realBasePath}"') - if not os.path.isdir(download_path): + if not os.path.isdir(download_path) and createPath: os.makedirs(download_path, exist_ok=True) return download_path diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b3c3be4e..2b99e29d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@vueuse/core": "^10.6.1", "bulma": "^0.9.4", "core-js": "^3.8.3", + "hls.js": "^1.4.12", "moment": "^2.29.4", "socket.io-client": "^4.7.2", "vue": "^3.2.13", @@ -6745,6 +6746,11 @@ "node": "*" } }, + "node_modules/hls.js": { + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.12.tgz", + "integrity": "sha512-1RBpx2VihibzE3WE9kGoVCtrhhDWTzydzElk/kyRbEOLnb1WIE+3ZabM/L8BqKFTCL3pUy4QzhXgD1Q6Igr1JA==" + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", diff --git a/frontend/package.json b/frontend/package.json index 76094795..b221d76e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@vueuse/core": "^10.6.1", "bulma": "^0.9.4", "core-js": "^3.8.3", + "hls.js": "^1.4.12", "moment": "^2.29.4", "socket.io-client": "^4.7.2", "vue": "^3.2.13", diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 15ded227..134fcb00 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -5,10 +5,11 @@ -