diff --git a/backend/config_parser.py b/backend/config_parser.py index 42ce40e..bde6179 100644 --- a/backend/config_parser.py +++ b/backend/config_parser.py @@ -84,29 +84,33 @@ class Go2Rtc: LNX = "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_amd64" def __init__(self, port = 1984) -> None: + self.logger = create_logger(Go2Rtc.__name__) self.port = port self.enabled = False try: self.check_exists() + self.logger.info("Go2rtc support enabled") except: - print("go2rtc is disabled") + self.logger.error("Go2rtc is disabled") pass def check_exists(self): go2rtc_directory = os.path.join(app_dir(), "go2rtc") if os.path.exists(go2rtc_directory): - print("Go2Rtc directory exists") + self.logger.debug("Go2Rtc directory exists") if platform.system() == "Windows" and os.path.exists(os.path.join(go2rtc_directory, "go2rtc.exe")): - print("[WIN] Go2Rtc is exists, continue create config") + self.logger.debug("[WIN] Go2Rtc is exists, continue create config") self.exec = os.path.join(go2rtc_directory, "go2rtc.exe") self.enabled = True elif platform.system() == "Linux" and os.path.exists(os.path.join(go2rtc_directory, "go2rtc")): - print("[LNX] Go2Rtc is exists, continue create config") + self.logger.debug("[LNX] Go2Rtc is exists, continue create config") self.exec = os.path.join(go2rtc_directory, "go2rtc") self.enabled = True else: + self.logger.error(f"Unknown platform: {platform.system()}") raise Exception(f"go2rtc not downloaded, windows: {self.WIN} linux: {self.LNX}") else: + self.logger.warning("Go2Rtc not found, he is disabled") raise Exception("Go2Rtc not found, he is disabled") #http://localhost:1984/go2rtc/stream.html?src=0_0_0 @@ -127,7 +131,7 @@ class Go2Rtc: lines += Go2RtcChannel(recorder, 2).generate_lines() cfg_file = os.path.join(app_dir(), "go2rtc", "go2rtc.yaml") - print(f"go2rtc config: {cfg_file}") + self.logger.debug(f"go2rtc config: {cfg_file}") async with aiofiles.open(cfg_file, "w", encoding="utf8") as cfg: await cfg.write(lines) @@ -140,34 +144,35 @@ class TranscodeTools: WIN32PYTHON = "python-win32" def __init__(self, tools_directory, transcode_directory, hide_checks = True) -> None: + self.logger = create_logger(TranscodeTools.__name__) 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.logger.error("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") + self.logger.error("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") + self.logger.error("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") + self.logger.error("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.logger.error("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") + self.logger.error("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") + self.logger.info("Transcode tools " + "enabled" if self.enabled else "disabled") @property def check_exists_needed_files(self): @@ -219,7 +224,7 @@ class TranscodeTools: exec_string.append(self.converter_script) exec_string.append(source_file) - print("execute", exec_cmd, exec_string) + self.logger.debug(f"execute {exec_cmd} {exec_string}") proc = await asyncio.create_subprocess_exec(exec_cmd, *exec_string) await proc.communicate() @@ -232,7 +237,7 @@ class TranscodeTools: 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) + self.logger.debug(f"execute {exec_string}") proc = await asyncio.create_subprocess_exec("ffmpeg", *exec_string) await proc.communicate() @@ -264,17 +269,17 @@ class TranscodeTools: return if not os.path.exists(raw_file) or os.path.getsize(raw_file) != file.size: - print("save raw file to", raw_file) + self.logger.debug(f"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") + self.logger.debug("raw file is downloaded") else: - print("File already content on server") - print("logout from nvr, he is not more needed") + self.logger.debug("File already content on server") + self.logger.debug("logout from nvr, he is not more needed") nvr.logout() self.statuses[status.b64].avi = 0 @@ -282,16 +287,16 @@ class TranscodeTools: if not os.path.exists(avi_file) or reCreate: avi_file = await self.h264toavi(raw_file) else: - print("file avi format already exitst") + self.logger.debug("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) + mp4_file = await self.avitomp4(avi_file, True) else: - print("file mp4 format already exists") + self.logger.debug("file mp4 format already exists") self.statuses[status.b64].mp4 = 100 self.statuses[status.b64].outFile = mp4_file @@ -310,6 +315,7 @@ class TranscodeTools: class Config: def __init__(self, config_name = "config.json", args = None) -> None: + self.logger = create_logger(Config.__name__) raw = load_config(config_name) if args == None or not args.err_check else {} self.listen_address = raw.get("backend", {}).get("address", "0.0.0.0") self.listen_port = int(raw.get("backend", {}).get("port", "8080")) @@ -319,10 +325,10 @@ class Config: self.recorders.append(Recorder(raw_server.get("ip"), raw_server.get("port"), raw_server.get("user"), raw_server.get("password"), raw_server.get("name", ""), index=i)) i += 1 if (self.recorders.__len__() == 0): - print("Recorders not find") + self.logger.warning("Recorders not find") else: for recorder in self.recorders: - print(recorder) + self.logger.info(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: diff --git a/backend/global_funcs.py b/backend/global_funcs.py index 245396f..b44c81b 100644 --- a/backend/global_funcs.py +++ b/backend/global_funcs.py @@ -3,14 +3,18 @@ import os import sys from json import loads import uuid +import logging + 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) @@ -19,4 +23,15 @@ def load_config(config_name): return loads(f.read()) except Exception as e: print("cannot find or parse config.json", e) - sys.exit(1) \ No newline at end of file + sys.exit(1) + + +def create_logger(t, level:int = logging.INFO, format=None): + logger = logging.getLogger(t) + logger.setLevel(level) + ch = logging.StreamHandler() + if format: + formatter = logging.Formatter(format) + ch.setFormatter(formatter) + logger.addHandler(ch) + return logger diff --git a/backend/nvr_core.py b/backend/nvr_core.py index f09b09b..8dfa4f4 100644 --- a/backend/nvr_core.py +++ b/backend/nvr_core.py @@ -6,6 +6,7 @@ from nvr_types import File as NvrFile from nvr_types import list_local_files from nvr_types import PRIMARY_STREAM, SECONDARY_STREAM from nvr_types import H264 +from global_funcs import create_logger START = "2024-08-04 6:22:34" END = "2024-08-04 23:23:09" @@ -19,6 +20,7 @@ def date_today(begin = True): class NVR: def __init__(self, client, loop) -> None: + self.logger = create_logger(NVR.__name__) self.client:DVRIPCam = client self.loop = loop @@ -36,7 +38,7 @@ class NVR: start = date_today() if not end: end = date_today(False) - print("Search files", start, end) + self.logger.info(f"Search files from {start} to {end}") for raw_file in await list_local_files(self.client, startTime=start, endTime=end, filetype=ftype, channel=channel, streamType=stype): if json: yield NvrFile(raw_file, channel, stype).json @@ -45,13 +47,13 @@ class NVR: async def stream_file(self, file: NvrFile) -> bytes: len_data = await file.generate_first_bytes(self.client) - print("len data =",len_data) + self.logger.debug(f"len data = {len_data}") if (len_data is None): yield b"" else: async for chunk in file.get_file_stream(self.client, len_data): if (chunk == None): - print("end of file") + self.logger.debug("end of file") break yield chunk @@ -61,4 +63,4 @@ class NVR: async for byte in file.generate_bytes(self.client): f.write(byte) downloaded_bytes += len(byte) - print("\r", downloaded_bytes, "/", file.size) \ No newline at end of file + self.logger.debug(f"\r{downloaded_bytes}/{file.size}") \ No newline at end of file diff --git a/backend/nvr_types.py b/backend/nvr_types.py index 1f974b8..d9b4517 100644 --- a/backend/nvr_types.py +++ b/backend/nvr_types.py @@ -52,7 +52,6 @@ class File: @staticmethod def from_b64(b64): data = json.loads(base64.b64decode(b64).decode('utf-8')) - print(data) return File(data, data.get("channel"), data.get("stream")) @staticmethod diff --git a/backend/server.py b/backend/server.py index 1463c3a..6e6f7c0 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1,10 +1,12 @@ -from fastapi import FastAPI, Response, BackgroundTasks +from fastapi import FastAPI, Response, BackgroundTasks, Header from fastapi.responses import StreamingResponse, FileResponse import uvicorn import traceback +import aiofiles from config_parser import Config as ConfigParser from config_parser import TranscodeStatus, TranscodeTools, Go2Rtc +from global_funcs import create_logger from nvr_core import NVR from nvr_types import File @@ -16,6 +18,7 @@ class Server: API_BASE_REF = "/api/dvrip" def __init__(self): + self.logger = create_logger(Server.__name__) self.setup_events() self.setup_routers() @@ -28,7 +31,7 @@ class Server: channels = await nvr.channels() nvr.logout() self.config.recorders[i].channels = len(channels) - print(f"{self.config.recorders[i]} channels count: {self.config.recorders[i].channels}") + self.logger.info(f"{self.config.recorders[i]} channels count: {self.config.recorders[i].channels}") await self.go2rtc.start_go2rtc(self.config.recorders) def setup_routers(self): @@ -93,13 +96,12 @@ class Server: await nvr.login() nvr.client.debug() file: File = File.from_b64(b64 + "==") - print("open") async def after(): try: await nvr.client.busy.release() except: - print("Already released") + pass nvr.logout() background_tasks.add_task(after) @@ -160,6 +162,38 @@ class Server: response.status_code = 400 return {"ok":False, "error":e} + @self.app.get(self.API_BASE_REF + "/transcode/stream") + async def getTranscodeStream(response: Response, b64:str, range: str = Header(None), chunk_size:int = 256): + try: + if len(b64) == 0: + response.status_code = 404 + return "" + + if not b64 in self.config.transcode_tools.statuses: + response.status_code = 404 + return "" + + if self.config.transcode_tools.statuses[b64].done: + start, end = range.replace("bytes=", "").split("-") + start = int(start) + end = int(end) if end else start + 1024 * chunk_size + async with aiofiles.open(self.config.transcode_tools.statuses[b64].outFile, "rb") as video: + await video.seek(start) + data = await video.read(end - start) + headers = { + 'Content-Range': f'bytes {str(start)}-{str(end)}/{self.config.transcode_tools.statuses[b64].outSize}', + 'Accept-Ranges': 'bytes' + } + return Response(data, status_code=206, headers=headers, media_type="video/mp4") + else: + response.status_code = 429 + return "" + + except Exception as e: + traceback.print_exc() + response.status_code = 400 + return {"ok":False, "error":e} + @self.app.get(self.API_BASE_REF + "/stream/{recorder_index}/{channel_index}/{stream_index}") async def getGo2RtcStream(recorder_index, channel_index, stream_index): return self.go2rtc.get_stream(recorder_index, channel_index, stream_index) diff --git a/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html b/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html index 5fa5366..737cbb6 100644 --- a/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html +++ b/frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html @@ -4,14 +4,14 @@ Прогресс загрузки h264x {{status.h264}} % - Прогресс загрузки avi {{status.avi}} % + Прогресс перекодировки avi {{status.avi}} % - Прогресс загрузки avi {{status.mp4}} % + Прогресс перекодировки mp4 {{status.mp4}} % - +
Прогресс загрузки h264x {{status.h264}} %
Прогресс загрузки avi {{status.avi}} %
Прогресс перекодировки avi {{status.avi}} %
Прогресс загрузки avi {{status.mp4}} %
Прогресс перекодировки mp4 {{status.mp4}} %