Browse Source

logger / video stream on front

master
gsd 8 months ago
parent
commit
a9394f7b2f
  1. 52
      backend/config_parser.py
  2. 17
      backend/global_funcs.py
  3. 10
      backend/nvr_core.py
  4. 1
      backend/nvr_types.py
  5. 42
      backend/server.py
  6. 6
      frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html

52
backend/config_parser.py

@ -84,29 +84,33 @@ class Go2Rtc:
LNX = "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_amd64" LNX = "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.4/go2rtc_linux_amd64"
def __init__(self, port = 1984) -> None: def __init__(self, port = 1984) -> None:
self.logger = create_logger(Go2Rtc.__name__)
self.port = port self.port = port
self.enabled = False self.enabled = False
try: try:
self.check_exists() self.check_exists()
self.logger.info("Go2rtc support enabled")
except: except:
print("go2rtc is disabled") self.logger.error("Go2rtc is disabled")
pass pass
def check_exists(self): def check_exists(self):
go2rtc_directory = os.path.join(app_dir(), "go2rtc") go2rtc_directory = os.path.join(app_dir(), "go2rtc")
if os.path.exists(go2rtc_directory): 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")): 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.exec = os.path.join(go2rtc_directory, "go2rtc.exe")
self.enabled = True self.enabled = True
elif platform.system() == "Linux" and os.path.exists(os.path.join(go2rtc_directory, "go2rtc")): 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.exec = os.path.join(go2rtc_directory, "go2rtc")
self.enabled = True self.enabled = True
else: else:
self.logger.error(f"Unknown platform: {platform.system()}")
raise Exception(f"go2rtc not downloaded, windows: {self.WIN} linux: {self.LNX}") raise Exception(f"go2rtc not downloaded, windows: {self.WIN} linux: {self.LNX}")
else: else:
self.logger.warning("Go2Rtc not found, he is disabled")
raise Exception("Go2Rtc not found, he is disabled") raise Exception("Go2Rtc not found, he is disabled")
#http://localhost:1984/go2rtc/stream.html?src=0_0_0 #http://localhost:1984/go2rtc/stream.html?src=0_0_0
@ -127,7 +131,7 @@ class Go2Rtc:
lines += Go2RtcChannel(recorder, 2).generate_lines() lines += Go2RtcChannel(recorder, 2).generate_lines()
cfg_file = os.path.join(app_dir(), "go2rtc", "go2rtc.yaml") 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: async with aiofiles.open(cfg_file, "w", encoding="utf8") as cfg:
await cfg.write(lines) await cfg.write(lines)
@ -140,34 +144,35 @@ class TranscodeTools:
WIN32PYTHON = "python-win32" WIN32PYTHON = "python-win32"
def __init__(self, tools_directory, transcode_directory, hide_checks = True) -> None: def __init__(self, tools_directory, transcode_directory, hide_checks = True) -> None:
self.logger = create_logger(TranscodeTools.__name__)
self.hide_checks = hide_checks self.hide_checks = hide_checks
self.tools_directory = tools_directory self.tools_directory = tools_directory
self.transcode_directory = transcode_directory self.transcode_directory = transcode_directory
if not os.path.exists(tools_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 self.enabled = False
else: else:
python_win32_exists = self.python_win32_exists python_win32_exists = self.python_win32_exists
if not 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 check_exists_needed_files = self.check_exists_needed_files
if not 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() check_ffmpeg = self.check_ffmpeg()
if not 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() check_converter = self.check_converter()
if not 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 self.enabled = check_exists_needed_files and python_win32_exists and check_ffmpeg and check_converter
if not self.enabled: 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 @property
def check_exists_needed_files(self): def check_exists_needed_files(self):
@ -219,7 +224,7 @@ class TranscodeTools:
exec_string.append(self.converter_script) exec_string.append(self.converter_script)
exec_string.append(source_file) 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) proc = await asyncio.create_subprocess_exec(exec_cmd, *exec_string)
await proc.communicate() await proc.communicate()
@ -232,7 +237,7 @@ class TranscodeTools:
async def avitomp4(self, source_file, delete_source_file = False): async def avitomp4(self, source_file, delete_source_file = False):
exec_string = ["-y", "-i", source_file, "-movflags", "faststart", f"{source_file}.mp4"] 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) proc = await asyncio.create_subprocess_exec("ffmpeg", *exec_string)
await proc.communicate() await proc.communicate()
@ -264,17 +269,17 @@ class TranscodeTools:
return return
if not os.path.exists(raw_file) or os.path.getsize(raw_file) != file.size: 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: async with aiofiles.open(raw_file, "wb") as raw:
self.statuses[status.b64].total_h264_bytes = file.size self.statuses[status.b64].total_h264_bytes = file.size
async for chunk in nvr.stream_file(file): async for chunk in nvr.stream_file(file):
self.statuses[status.b64].downloaded_h264_bytes += len(chunk) 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) 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) await raw.write(chunk)
print("raw file is downloaded") self.logger.debug("raw file is downloaded")
else: else:
print("File already content on server") self.logger.debug("File already content on server")
print("logout from nvr, he is not more needed") self.logger.debug("logout from nvr, he is not more needed")
nvr.logout() nvr.logout()
self.statuses[status.b64].avi = 0 self.statuses[status.b64].avi = 0
@ -282,16 +287,16 @@ class TranscodeTools:
if not os.path.exists(avi_file) or reCreate: if not os.path.exists(avi_file) or reCreate:
avi_file = await self.h264toavi(raw_file) avi_file = await self.h264toavi(raw_file)
else: 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].avi = 100
self.statuses[status.b64].mp4 = 0 self.statuses[status.b64].mp4 = 0
mp4_file = avi_file + ".mp4" mp4_file = avi_file + ".mp4"
if not os.path.exists(mp4_file) or reCreate: 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: 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].mp4 = 100
self.statuses[status.b64].outFile = mp4_file self.statuses[status.b64].outFile = mp4_file
@ -310,6 +315,7 @@ class TranscodeTools:
class Config: class Config:
def __init__(self, config_name = "config.json", args = None) -> None: 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 {} 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_address = raw.get("backend", {}).get("address", "0.0.0.0")
self.listen_port = int(raw.get("backend", {}).get("port", "8080")) 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)) 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 i += 1
if (self.recorders.__len__() == 0): if (self.recorders.__len__() == 0):
print("Recorders not find") self.logger.warning("Recorders not find")
else: else:
for recorder in self.recorders: 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) self.transcode_tools:TranscodeTools = self.check_transcode_tools(not args.no_hide_check if args != None else True)
def getRecorder(self, index = 0) -> Recorder: def getRecorder(self, index = 0) -> Recorder:

17
backend/global_funcs.py

@ -3,14 +3,18 @@ import os
import sys import sys
from json import loads from json import loads
import uuid import uuid
import logging
def uuid_from_string(string:str): def uuid_from_string(string:str):
hex_string = hashlib.md5(string.encode("utf8")).hexdigest() hex_string = hashlib.md5(string.encode("utf8")).hexdigest()
return uuid.UUID(hex=hex_string) return uuid.UUID(hex=hex_string)
def app_dir(): def app_dir():
return os.path.dirname(os.path.abspath(__file__)) return os.path.dirname(os.path.abspath(__file__))
def load_config(config_name): def load_config(config_name):
try: try:
path = os.path.join(app_dir(), config_name) path = os.path.join(app_dir(), config_name)
@ -19,4 +23,15 @@ def load_config(config_name):
return loads(f.read()) return loads(f.read())
except Exception as e: except Exception as e:
print("cannot find or parse config.json", e) print("cannot find or parse config.json", e)
sys.exit(1) 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

10
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 list_local_files
from nvr_types import PRIMARY_STREAM, SECONDARY_STREAM from nvr_types import PRIMARY_STREAM, SECONDARY_STREAM
from nvr_types import H264 from nvr_types import H264
from global_funcs import create_logger
START = "2024-08-04 6:22:34" START = "2024-08-04 6:22:34"
END = "2024-08-04 23:23:09" END = "2024-08-04 23:23:09"
@ -19,6 +20,7 @@ def date_today(begin = True):
class NVR: class NVR:
def __init__(self, client, loop) -> None: def __init__(self, client, loop) -> None:
self.logger = create_logger(NVR.__name__)
self.client:DVRIPCam = client self.client:DVRIPCam = client
self.loop = loop self.loop = loop
@ -36,7 +38,7 @@ class NVR:
start = date_today() start = date_today()
if not end: if not end:
end = date_today(False) 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): for raw_file in await list_local_files(self.client, startTime=start, endTime=end, filetype=ftype, channel=channel, streamType=stype):
if json: if json:
yield NvrFile(raw_file, channel, stype).json yield NvrFile(raw_file, channel, stype).json
@ -45,13 +47,13 @@ class NVR:
async def stream_file(self, file: NvrFile) -> bytes: async def stream_file(self, file: NvrFile) -> bytes:
len_data = await file.generate_first_bytes(self.client) 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): if (len_data is None):
yield b"" yield b""
else: 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):
if (chunk == None): if (chunk == None):
print("end of file") self.logger.debug("end of file")
break break
yield chunk yield chunk
@ -61,4 +63,4 @@ class NVR:
async for byte in file.generate_bytes(self.client): async for byte in file.generate_bytes(self.client):
f.write(byte) f.write(byte)
downloaded_bytes += len(byte) downloaded_bytes += len(byte)
print("\r", downloaded_bytes, "/", file.size) self.logger.debug(f"\r{downloaded_bytes}/{file.size}")

1
backend/nvr_types.py

@ -52,7 +52,6 @@ class File:
@staticmethod @staticmethod
def from_b64(b64): def from_b64(b64):
data = json.loads(base64.b64decode(b64).decode('utf-8')) data = json.loads(base64.b64decode(b64).decode('utf-8'))
print(data)
return File(data, data.get("channel"), data.get("stream")) return File(data, data.get("channel"), data.get("stream"))
@staticmethod @staticmethod

42
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 from fastapi.responses import StreamingResponse, FileResponse
import uvicorn import uvicorn
import traceback import traceback
import aiofiles
from config_parser import Config as ConfigParser from config_parser import Config as ConfigParser
from config_parser import TranscodeStatus, TranscodeTools, Go2Rtc from config_parser import TranscodeStatus, TranscodeTools, Go2Rtc
from global_funcs import create_logger
from nvr_core import NVR from nvr_core import NVR
from nvr_types import File from nvr_types import File
@ -16,6 +18,7 @@ class Server:
API_BASE_REF = "/api/dvrip" API_BASE_REF = "/api/dvrip"
def __init__(self): def __init__(self):
self.logger = create_logger(Server.__name__)
self.setup_events() self.setup_events()
self.setup_routers() self.setup_routers()
@ -28,7 +31,7 @@ class Server:
channels = await nvr.channels() channels = await nvr.channels()
nvr.logout() nvr.logout()
self.config.recorders[i].channels = len(channels) 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) await self.go2rtc.start_go2rtc(self.config.recorders)
def setup_routers(self): def setup_routers(self):
@ -93,13 +96,12 @@ class Server:
await nvr.login() await nvr.login()
nvr.client.debug() nvr.client.debug()
file: File = File.from_b64(b64 + "==") file: File = File.from_b64(b64 + "==")
print("open")
async def after(): async def after():
try: try:
await nvr.client.busy.release() await nvr.client.busy.release()
except: except:
print("Already released") pass
nvr.logout() nvr.logout()
background_tasks.add_task(after) background_tasks.add_task(after)
@ -160,6 +162,38 @@ class Server:
response.status_code = 400 response.status_code = 400
return {"ok":False, "error":e} 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}") @self.app.get(self.API_BASE_REF + "/stream/{recorder_index}/{channel_index}/{stream_index}")
async def getGo2RtcStream(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) return self.go2rtc.get_stream(recorder_index, channel_index, stream_index)

6
frontend/ang_dvrip/src/app/modals/transcode-modal/transcode-modal.component.html

@ -4,14 +4,14 @@
<ng-container *ngIf="!loading && !status.done"> <ng-container *ngIf="!loading && !status.done">
<p>Прогресс загрузки h264x {{status.h264}} %</p> <p>Прогресс загрузки h264x {{status.h264}} %</p>
<mat-progress-bar [mode]="'determinate'" [value]="status.h264"></mat-progress-bar> <mat-progress-bar [mode]="'determinate'" [value]="status.h264"></mat-progress-bar>
<p>Прогресс загрузки avi {{status.avi}} %</p> <p>Прогресс перекодировки avi {{status.avi}} %</p>
<mat-progress-bar [mode]="'determinate'" [value]="status.avi"></mat-progress-bar> <mat-progress-bar [mode]="'determinate'" [value]="status.avi"></mat-progress-bar>
<p>Прогресс загрузки avi {{status.mp4}} %</p> <p>Прогресс перекодировки mp4 {{status.mp4}} %</p>
<mat-progress-bar [mode]="'determinate'" [value]="status.mp4"></mat-progress-bar> <mat-progress-bar [mode]="'determinate'" [value]="status.mp4"></mat-progress-bar>
</ng-container> </ng-container>
<ng-container *ngIf="!loading && status.done"> <ng-container *ngIf="!loading && status.done">
<video controls> <video controls>
<source [src]="'api/dvrip/transcode/download?b64=' + status.b64" type="video/mp4"/> <source [src]="'api/dvrip/transcode/stream?b64=' + status.b64" type="video/mp4"/>
</video> </video>
</ng-container> </ng-container>
</mat-dialog-content> </mat-dialog-content>

Loading…
Cancel
Save