You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

238 lines
8.8 KiB

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
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.uuid4())
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:
yield await out.read(32 * 1024)
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 = ""
if platform.system() == "Windows":
exec_string += ""
elif platform.system() == "Linux":
exec_string += "wine "
else:
raise Exception("Unknown platform to transcode")
exec_string += str(self.python_win32) + " "
exec_string += str(self.converter_script) + " "
exec_string += str(source_file)
print("execute", exec_string)
proc = await asyncio.create_subprocess_exec(exec_string)
await proc.wait()
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 = f"ffmpeg -y -i {source_file} {source_file}.mp4"
print("execute", exec_string)
proc = await asyncio.create_subprocess_exec(exec_string)
await proc.wait()
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):
raw_file = os.path.join(self.transcode_directory, status.uuid + ".h264")
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)
await raw.write(chunk)
print("raw file is downloaded")
print("logout from nvr, he is not more needed")
nvr.logout()
self.statuses[status.b64].avi = 0
avi_file = await self.h264toavi(raw_file)
self.statuses[status.b64].avi = 100
self.statuses[status.b64].mp4 = 0
mp4_file = await self.avitomp4(avi_file)
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)
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")
args = parser.parse_args()
if args.no_hide_check:
Config(args = args)
sys.exit(0)