diff --git a/backend/config_parser.py b/backend/config_parser.py index 8619722..87abb9f 100644 --- a/backend/config_parser.py +++ b/backend/config_parser.py @@ -10,6 +10,9 @@ import aiofiles from global_funcs import * +class NeedNVR(Exception): + pass + class Recorder: loop = asyncio.get_event_loop() def __init__(self, address, port, username, password, name = "", index = 0): @@ -140,7 +143,6 @@ class Go2Rtc: await cfg.write(lines) await asyncio.create_subprocess_exec(self.exec, *["-c", cfg_file]) - class TranscodeTools: statuses:dict[str, TranscodeStatus] = {} @@ -251,17 +253,48 @@ class TranscodeTools: else: raise Exception("MP4 not be created") + async def anytoimage(self, source_file, out_file, delete_source_file = False): + exec_string = ["-y", "-i", source_file, "-ss", "1", "-vframes", "1", out_file] + self.logger.debug(f"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(out_file): + return out_file + else: + raise Exception(f"{out_file} not be created") + def deleteFile(self, source_file): os.remove(source_file) + async def processing_preview(self, file:File, nvr: NVR, ext = "webp"): + raw_file = file.previewPath(self.transcode_directory) + preview_file = f"{raw_file}.{ext}" + if os.path.exists(preview_file) and os.path.getsize(preview_file) != 0: + return preview_file + + if nvr is None: + raise NeedNVR + + if not os.path.exists(raw_file) or os.path.getsize(raw_file) != 0: + async with aiofiles.open(raw_file, "wb") as raw: + async for chunk in nvr.stream_file(file, 1024 * 512): + await raw.write(chunk) + + nvr.logout() + + return await self.anytoimage(raw_file, preview_file, True) + async def processing_safe(self, status: TranscodeStatus, file:File, nvr: NVR, reCreate:bool = False): try: - await self.processing(status, file, nvr, reCreate) + await self.processing_mp4(status, file, nvr, reCreate) except Exception as e: traceback.print_exc() self.statuses[status.b64].error = str(e) - async def processing(self, status: TranscodeStatus, file:File, nvr: NVR, reCreate:bool = False): + async def processing_mp4(self, status: TranscodeStatus, file:File, nvr: NVR, reCreate:bool = False): raw_file = file.localPath(self.transcode_directory) self.logger.info(f"save path: {raw_file}") diff --git a/backend/nvr_core.py b/backend/nvr_core.py index e331384..6b0d1b3 100644 --- a/backend/nvr_core.py +++ b/backend/nvr_core.py @@ -49,13 +49,13 @@ class NVR: else: yield NvrFile(raw_file, channel, stype) - async def stream_file(self, file: NvrFile) -> bytes: - len_data = await file.generate_first_bytes(self.client) + async def stream_file(self, file: NvrFile, max_file_size = None) -> bytes: + len_data = await file.get_file_stream_start(self.client) self.logger.debug(f"[{self.index}] len data = {len_data}, streaming file content") if (len_data is None): yield b"" else: - async for chunk in file.get_file_stream(self.client, len_data): + async for chunk in file.get_file_stream(self.client, len_data, max_file_size): if (chunk == None): self.logger.debug(f"[{self.index}] end of file") break diff --git a/backend/nvr_types.py b/backend/nvr_types.py index 094feb7..ec7297e 100644 --- a/backend/nvr_types.py +++ b/backend/nvr_types.py @@ -79,9 +79,9 @@ class File: return self.genPath(root, subPath, self.type) def previewPath(self, root, subPath = "preview"): - return self.genPath(root, subPath, "jpg") + return self.genPath(root, subPath, self.type) - async def generate_first_bytes(self, client:DVRIPCam, version = 0): + async def get_file_stream_start(self, client:DVRIPCam, version = 0): client.logger.debug("init request") #init request await client.send( @@ -167,8 +167,12 @@ class File: ) = struct.unpack("BB2xII2xHI", data) return len_data - async def get_file_stream(self, client: DVRIPCam, first_chunk_size) -> bytes: - yield bytes(await client.receive_with_timeout(first_chunk_size)) + async def get_file_stream(self, client: DVRIPCam, first_chunk_size, max_file_size) -> bytes: + file_size = 0 + first_bytes = await client.receive_with_timeout(first_chunk_size) + file_size += len(first_bytes) + yield bytes(first_bytes) + while True: header = await client.receive_with_timeout(20) len_data = struct.unpack("I", header[16:])[0] @@ -176,7 +180,16 @@ class File: if len_data == 0: break - yield bytes(await client.receive_with_timeout(len_data)) + receive_bytes = await client.receive_with_timeout(len_data) + file_size += len(receive_bytes) + yield bytes(receive_bytes) + + if max_file_size is not None and file_size >= max_file_size: + break + + self.get_file_stream_end(client) + + async def get_file_stream_end(self, client: DVRIPCam): try: if client and client.busy: await client.busy.release() diff --git a/backend/server.py b/backend/server.py index b6afe2e..c6e2668 100644 --- a/backend/server.py +++ b/backend/server.py @@ -5,10 +5,11 @@ import traceback import aiofiles from config_parser import Config as ConfigParser -from config_parser import TranscodeStatus, TranscodeTools, Go2Rtc +from config_parser import TranscodeStatus, TranscodeTools, Go2Rtc, NeedNVR from global_funcs import create_logger from nvr_core import NVR from nvr_types import File +import os class Server: app: FastAPI = FastAPI() @@ -191,6 +192,31 @@ class Server: traceback.print_exc() response.status_code = 400 return {"ok":False, "error":e} + @self.app.get(self.API_BASE_REF + "/preview/{recorder_index}") + async def getTranscodePreview(response: Response, recorder_index:int, b64:str): + try: + if len(b64) == 0: + response.status_code = 404 + return "" + + file: File = File.from_b64(b64 + "==") + + try: + preview = await self.config.transcode_tools.processing_preview(file, None, "webp") + except NeedNVR: + nvr:NVR = self.config.getRecorder(recorder_index).nvr + await nvr.login() + preview = await self.config.transcode_tools.processing_preview(file, nvr, "webp") + + headers = {} + headers.update({"Content-Length":str(os.path.getsize(preview))}) + headers.update({"Content-Disposition": f'attachment; filename="preview.webp"'}) + return FileResponse(preview, media_type="application/octet-stream", headers=headers) + + 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): 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 737cbb6..cbe743b 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 @@ -18,4 +18,5 @@ + Превью