from genericpath import exists import os, sys from json import loads from turtle import width import uuid from asyncio_dvrip import DVRIPCam import asyncio from nvr_core import NVR from nvr_types import File import platform import aiofiles import hashlib def uuid_from_string(string:str): hex_string = hashlib.md5(string.encode("utf8")).hexdigest() return uuid.UUID(hex=hex_string) def app_dir(): return os.path.dirname(os.path.abspath(__file__)) def load_config(config_name): try: path = os.path.join(app_dir(), config_name) print("Looking config file", path) with open(path, "r", encoding="utf8") as f: return loads(f.read()) except Exception as e: print("cannot find or parse config.json", e) sys.exit(1) class Recorder: loop = asyncio.get_event_loop() def __init__(self, address, port, username, password, name = ""): self.address = address self.port = int(port) self.username = username self.password = password self.name = name @property def nvr(self): client = DVRIPCam(self.address, port = self.port, user = self.username, password = self.password) return NVR(client, self.loop) def __str__(self) -> str: if not self.name: return f"{self.address}:{self.port}" else: return self.name class TranscodeStatus: def __init__(self, b64) -> None: self.b64 = b64 self.uuid = str(uuid_from_string(b64)) self.h264 = 0 self.downloaded_h264_bytes = 0 self.total_h264_bytes = 0 self.avi = 0 self.mp4 = 0 self.outFile = None self.done = False self.outSize = 0 @property def outName(self): if self.outFile: return os.path.split(self.outFile)[-1] return "" async def generate_bytes(self): async with aiofiles.open(self.outFile, "rb") as out: while True: chunk = await out.read(32 * 1024) if chunk: yield chunk else: break yield b"" class TranscodeTools: statuses:dict[str, TranscodeStatus] = {} WIN32PYTHON = "python-win32" def __init__(self, tools_directory, transcode_directory, hide_checks = True) -> None: self.hide_checks = hide_checks self.tools_directory = tools_directory self.transcode_directory = transcode_directory if not os.path.exists(tools_directory): print("download git repo https://git.pblr-nyk.pro/gsd/MiskaRisa264 and place in backend folder to enable transcode tools") self.enabled = False else: python_win32_exists = self.python_win32_exists if not python_win32_exists: print("download https://www.python.org/ftp/python/3.12.3/python-3.12.3-embed-win32.zip and unzip in backend/MiskaRisa/python-win32 all contains files") check_exists_needed_files = self.check_exists_needed_files if not check_exists_needed_files: print("MiskaRisa264 is not fully downloaded, watch in directory to find lost files") check_ffmpeg = self.check_ffmpeg() if not check_ffmpeg: print("ffmpeg in not installed on system or on windows in not in PATH env") check_converter = self.check_converter() if not check_converter: print("failed run h264_converter.py with python-win32") self.enabled = check_exists_needed_files and python_win32_exists and check_ffmpeg and check_converter if not self.enabled: print("Cannot enabled transcode tools, have a errors on init, run config_parser with --no-hide-check parameters to more info") print("Transcode tools", "enabled" if self.enabled else "disabled") @property def check_exists_needed_files(self): for file in ["H264Play.dll", "h264_converter.py", "StreamReader.dll"]: if not os.path.exists(os.path.join(self.tools_directory, file)): return False return True @property def python_win32_exists(self): return os.path.exists(os.path.join(self.tools_directory, self.WIN32PYTHON)) @property def python_win32(self): return os.path.join(self.tools_directory, self.WIN32PYTHON, "python.exe") @property def converter_script(self): return os.path.join(self.tools_directory, "h264_converter.py") def check_ffmpeg(self): from subprocess import call, DEVNULL try: return not call("ffmpeg -version".split(), stdin=DEVNULL if self.hide_checks else None, stdout=DEVNULL if self.hide_checks else None, stderr=DEVNULL if self.hide_checks else None) except: return False def check_converter(self): from subprocess import call, DEVNULL try: return not call(f"{self.python_win32} {self.converter_script} --help".split(), stdin=DEVNULL if self.hide_checks else None, stdout=DEVNULL if self.hide_checks else None, stderr=DEVNULL if self.hide_checks else None) except: return False async def h264toavi(self, source_file, delete_source_file = False): exec_string = [] exec_cmd = "" if platform.system() == "Windows": exec_cmd = f"{self.python_win32}" elif platform.system() == "Linux": exec_cmd = "wine" exec_string.append(self.python_win32) raise Exception("Unknown platform to transcode") exec_string.append(self.converter_script) exec_string.append(source_file) print("execute", exec_cmd, exec_string) proc = await asyncio.create_subprocess_exec(exec_cmd, *exec_string) await proc.communicate() if delete_source_file: os.remove(source_file) if os.path.exists(source_file + ".avi"): return source_file + ".avi" else: raise Exception("AVI not be created") async def avitomp4(self, source_file, delete_source_file = False): exec_string = ["-y", "-i", source_file, "-movflags", "faststart", f"{source_file}.mp4"] print("execute", exec_string) proc = await asyncio.create_subprocess_exec("ffmpeg", *exec_string) await proc.communicate() if delete_source_file: os.remove(source_file) if os.path.exists(source_file + ".mp4"): return source_file + ".mp4" else: raise Exception("MP4 not be created") def deleteFile(self, source_file): os.remove(source_file) async def processing(self, status: TranscodeStatus, file:File, nvr: NVR, reCreate:bool = False): raw_file = os.path.join(self.transcode_directory, status.uuid + ".h264") if not os.path.exists(raw_file) or os.path.getsize(raw_file) != file.size: print("save raw file to", raw_file) async with aiofiles.open(raw_file, "wb") as raw: self.statuses[status.b64].total_h264_bytes = file.size async for chunk in nvr.stream_file(file): self.statuses[status.b64].downloaded_h264_bytes += len(chunk) self.statuses[status.b64].h264 = round(100 * self.statuses[status.b64].downloaded_h264_bytes / self.statuses[status.b64].total_h264_bytes) await raw.write(chunk) print("raw file is downloaded") else: print("File already content on server") print("logout from nvr, he is not more needed") nvr.logout() self.statuses[status.b64].avi = 0 avi_file = raw_file + ".avi" if not os.path.exists(avi_file) or reCreate: avi_file = await self.h264toavi(raw_file) else: print("file avi format already exitst") self.statuses[status.b64].avi = 100 self.statuses[status.b64].mp4 = 0 mp4_file = avi_file + ".mp4" if not os.path.exists(mp4_file) or reCreate: mp4_file = await self.avitomp4(avi_file) else: print("file mp4 format already exists") self.statuses[status.b64].mp4 = 100 self.statuses[status.b64].outFile = mp4_file self.statuses[status.b64].done = True self.statuses[status.b64].outSize = os.path.getsize(mp4_file) async def transcode_test(self, raw_file): avi_file = await self.h264toavi(raw_file) mp4_file = await self.avitomp4(avi_file) def h264toavi_test(self, raw_file): loop = asyncio.get_event_loop() tasks = [loop.create_task(self.transcode_test(raw_file))] loop.run_until_complete(asyncio.wait(tasks)) loop.close() class Config: def __init__(self, config_name = "config.json", args = None) -> None: raw = load_config(config_name) self.listen_address = raw.get("backend", {}).get("address", "0.0.0.0") self.listen_port = int(raw.get("backend", {}).get("port", "8080")) self.recorders = [] for raw_server in raw.get("recorders", []): self.recorders.append(Recorder(raw_server.get("ip"), raw_server.get("port"), raw_server.get("user"), raw_server.get("password"), raw_server.get("name", ""))) if (self.recorders.__len__() == 0): print("Recorders not find") else: for recorder in self.recorders: print(recorder) self.transcode_tools:TranscodeTools = self.check_transcode_tools(not args.no_hide_check if args != None else True) def getRecorder(self, index = 0) -> Recorder: return self.recorders[index] def getRecorders(self): return [str(r) for r in self.recorders] def check_transcode_directory(self): t_dir = os.path.join(app_dir(), "transcode") if not os.path.exists(t_dir): os.mkdir(t_dir) return t_dir def check_transcode_tools(self, hide_check): tools_dir = os.path.join(app_dir(), "MiskaRisa264") return TranscodeTools(tools_dir, self.check_transcode_directory(), hide_check) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser() parser.add_argument("--no-hide-check", action="store_true") parser.add_argument("--test-h264toavi", type=str) args = parser.parse_args() config = Config(args = args) if args.test_h264toavi: config.transcode_tools.h264toavi_test(args.test_h264toavi) sys.exit(0) sys.exit(0)