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