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"
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:

17
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)
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 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)
self.logger.debug(f"\r{downloaded_bytes}/{file.size}")

1
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

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
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)

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

@ -4,14 +4,14 @@
<ng-container *ngIf="!loading && !status.done">
<p>Прогресс загрузки h264x {{status.h264}} %</p>
<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>
<p>Прогресс загрузки avi {{status.mp4}} %</p>
<p>Прогресс перекодировки mp4 {{status.mp4}} %</p>
<mat-progress-bar [mode]="'determinate'" [value]="status.mp4"></mat-progress-bar>
</ng-container>
<ng-container *ngIf="!loading && status.done">
<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>
</ng-container>
</mat-dialog-content>

Loading…
Cancel
Save