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.
 
 
 
 
 
 

290 lines
11 KiB

import os, sys
from json import loads
import uuid
from asyncio_dvrip import DVRIPCam
import asyncio
from nvr_core import NVR
from nvr_types import File
import platform
import aiofiles
import hashlib
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)
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_from_string(b64))
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
self.error = ""
@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:
while True:
chunk = await out.read(32 * 1024)
if chunk:
yield chunk
else:
break
yield b""
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
cmd = f"{self.python_win32} {self.converter_script} --help"
if platform.system() == "Linux":
cmd = "wine " + cmd
try:
return not call(cmd.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 = []
exec_cmd = ""
if platform.system() == "Windows":
exec_cmd = f"{self.python_win32}"
elif platform.system() == "Linux":
exec_cmd = "wine"
exec_string.append(self.python_win32)
else:
raise Exception("Unknown platform to transcode")
exec_string.append(self.converter_script)
exec_string.append(source_file)
print("execute", exec_cmd, exec_string)
proc = await asyncio.create_subprocess_exec(exec_cmd, *exec_string)
await proc.communicate()
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 = ["-y", "-i", source_file, "-movflags", "faststart", f"{source_file}.mp4"]
print("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(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_safe(self, status: TranscodeStatus, file:File, nvr: NVR, reCreate:bool = False):
try:
await self.processing(status, file, nvr, reCreate)
except Exception as e:
self.statuses[status.b64].error = str(e)
async def processing(self, status: TranscodeStatus, file:File, nvr: NVR, reCreate:bool = False):
raw_file = os.path.join(self.transcode_directory, status.uuid + ".h264")
if not os.path.exists(raw_file) or os.path.getsize(raw_file) != file.size:
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)
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")
else:
print("File already content on server")
print("logout from nvr, he is not more needed")
nvr.logout()
self.statuses[status.b64].avi = 0
avi_file = raw_file + ".avi"
if not os.path.exists(avi_file) or reCreate:
avi_file = await self.h264toavi(raw_file)
else:
print("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)
else:
print("file mp4 format already exists")
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)
async def transcode_test(self, raw_file):
avi_file = await self.h264toavi(raw_file)
mp4_file = await self.avitomp4(avi_file)
def h264toavi_test(self, raw_file):
loop = asyncio.get_event_loop()
tasks = [loop.create_task(self.transcode_test(raw_file))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
class Config:
def __init__(self, config_name = "config.json", args = None) -> None:
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"))
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")
parser.add_argument("--err-check", action="store_true")
parser.add_argument("--test-h264toavi", type=str)
args = parser.parse_args()
config = Config(args = args)
if args.err_check:
sys.exit(0 if config.transcode_tools.enabled else 1)
if args.test_h264toavi:
config.transcode_tools.h264toavi_test(args.test_h264toavi)
sys.exit(0)
sys.exit(0)