diff --git a/app/ffmpeg_class.py b/app/ffmpeg_class.py index 89b5b1b..8fed620 100644 --- a/app/ffmpeg_class.py +++ b/app/ffmpeg_class.py @@ -1,12 +1,9 @@ -from app.main import video_convert - - -class Ffmpeg: - def __init__(config): +class FFCmd: + def __init__(self, config): self.config = config @staticmethod - def video_info(): + def video_info(select, source_file): command_info = [ "ffprobe", "-v", "error", @@ -23,8 +20,7 @@ class Ffmpeg: return command_info - @staticmethod - def video_convert(): + def video_convert(self, obj): command_convert = [ "ffmpeg", "-y", "-i", obj.source_file, "-map", "0:0", @@ -33,12 +29,12 @@ class Ffmpeg: "-crf", "30", "-g", "240", "-pix_fmt", "yuv420p10le", - "-svtav1-params", "tune=0:film-grain=8", + "-svtav1-params", "tune=0:film-grain=8:threads=16", ] if len(obj.streams_audio): for audio_stream in obj.streams_audio: - if audio_stream.get("tags", {}).get("language", None) in config["convert"]["language"]: + if audio_stream.get("tags", {}).get("language", None) in self.config["convert"]["language"]: command_convert.extend([ "-map", f"0:{audio_stream['index']}", f"-c:a", "libopus", @@ -49,8 +45,8 @@ class Ffmpeg: # Subtitle-Streams einbinden if len(obj.streams_subtitle): for subtitle_stream in obj.streams_subtitle: - if subtitle_stream.get("codec_name") not in config["subtitle"]["blacklist"]: - if subtitle_stream.get("tags", {}).get("language", None) in config["convert"]["language"]: + if subtitle_stream.get("codec_name") not in self.config["subtitle"]["blacklist"]: + if subtitle_stream.get("tags", {}).get("language", None) in self.config["convert"]["language"]: command_convert.extend([ "-map", f"0:{subtitle_stream['index']}", ]) @@ -59,7 +55,7 @@ class Ffmpeg: return command_convert @staticmethod - def command_as_string(self, cmd): + def command_as_string(cmd): command_as_string: str = "" for para in cmd: if "/" not in para: diff --git a/app/main.py b/app/main.py index ab0d65d..65a2902 100755 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,10 @@ +import asyncio import os.path import re import subprocess import json import logging -import threading import time -import queue from datetime import date import uvicorn @@ -15,16 +14,16 @@ from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from starlette.websockets import WebSocketState -import app.video_class as vc -from app.ffmpeg_class import Ffmpeg as Fc -from app.config_class import Config as Cfg +import video_class as vc +from ffmpeg_class import FFCmd as Fc +from config_class import Config as Cfg # Globale Variablen config = Cfg().config ffmpeg = Fc(config) -queue: queue = queue.Queue() -semaphore = threading.Semaphore(config["convert"]["max_process"]) +queue = asyncio.Queue() +semaphore = asyncio.Semaphore(config["convert"]["max_process"]) thread_output = None video_files = {} @@ -96,87 +95,81 @@ def get_ffprobe(select, source_file): return json_data.get("streams", []) # Convert Process ------------------------------------------------------------------------------------------------------ -def queue_video(): - for key, obj in video_files.items(): - if obj.finished == 0: - obj.task = threading.Thread(target=video_convert, args=(obj,)) - obj.task.start() - active_tasks.add(obj) - logging.info(f"Warteschlange started Auftrag - {obj.task}") +async def queue_video(): + async with semaphore: + for key, obj in video_files.items(): + if obj.finished == 0: + obj.task = await asyncio.create_task(video_convert(obj)) + active_tasks.add(obj) + logging.info(f"Warteschlange started Auftrag - {obj.task}") -def video_convert(obj): - global active_process +async def video_convert(obj): + """Startet die Videokonvertierung asynchron.""" + global active_process, active_tasks - semaphore.acquire() - - result: str = "" obj.convert_start = time.time() + command = ffmpeg.video_convert(obj) - command = ffmpeg.video_convert(config, obj) + logging.info(f"Starte Konvertierung: {command}") - logging.info(f"{command}") - - # Prozess try: - obj.process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE + # Starte den Subprozess asynchron + obj.process = await asyncio.create_subprocess_exec( + *command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE ) active_process.add(obj) obj.finished = 3 - queue.put(video_list()) + await queue.put(await video_list()) - while obj.process.poll() is None: - logging.info(f"{obj.file_name} ... Running") - time.sleep(10) + while obj.process.returncode is None: + logging.info(f"{obj.file_name} ... läuft noch") + await asyncio.sleep(10) - if obj.process.poll() == 0: + # Prozess beendet, Status auswerten + if obj.process.returncode == 0: obj.finished = 1 result = "Finished" - elif obj.process.poll() != 0: + else: obj.finished = 2 result = "Failure" - logging.info(f"Process {result}({obj.process.returncode}): {obj.file_name}") - - queue.put(obj.to_dict()) + logging.info(f"Prozess {result}({obj.process.returncode}): {obj.file_name}") + + await queue.put(obj.to_dict()) active_process.discard(obj) active_tasks.discard(obj) obj.convert_end = time.time() - semaphore.release() - except Exception as e: obj.finished = 2 - semaphore.release() - logging.error(f"Convert Process Failure: {e}") + logging.error(f"Fehler in video_convert(): {e}") #UviCorn WebServer Teil #---------------------------------------------------------------------------------------------------------------------- -def read_output(): +async def read_output(): """Thread-Funktion, die Prozessausgaben in die Queue legt.""" global active_process while True: if not len(active_process): - time.sleep(30) - queue.put(video_list()) + await asyncio.sleep(30) + await queue.put(video_list()) continue for obj in list(active_process): - line_error = obj.process.stderr.read(1024).decode() + line = await obj.process.stderr.read(1024) + line_error = line.decode() - logging.info(f"Data ffmpeg: {line_error}") obj.extract_convert_data(line_error) - # Verwende den Event-Loop, um die Daten in die asyncio Queue zu legen - logging.info(f"Data Packet created: {obj.to_dict()}") - queue.put(obj.to_dict()) + await queue.put(obj.to_dict()) + await asyncio.sleep(3) -def video_list(): +async def video_list(): vlist = [] for video in video_files.values(): vlist.append(video.get_vars()) @@ -187,18 +180,20 @@ def video_list(): @app.post("/") async def receive_video_file(data: dict): if get_video_information(data): - queue_video() + await queue_video() + await queue.put(await video_list()) else: logging.error(f"Videos konnten nicht verarbeitet werden! Warteschleife wurde nicht gestarted") @app.get("/command", response_class=HTMLResponse) async def display_paths(request: Request): + list_command: list = [] for video in video_files.values(): if video.finished == 3: - ffmpeg. + list_command.append(ffmpeg.command_as_string(ffmpeg.video_convert(video))) - return templates.TemplateResponse("command.html", {"request": request, "videos": video_files, "command": list_command}) + return templates.TemplateResponse("command.html", {"request": request, "videos": video_files, "commands": list_command}) @app.get("/webs-ui", response_class=HTMLResponse) async def display_paths(request: Request): @@ -214,23 +209,20 @@ async def websocket_v(websocket: WebSocket): if thread_output is None: # Startet den Thread zum Verarbeiten der Prozessausgaben - thread_output = threading.Thread(target=read_output) - thread_output.start() + thread_output = asyncio.create_task(read_output()) try: var_first_sending = 0 while True: if not var_first_sending: - queue.put(video_list()) - var_first_sending = 1 + await queue.put(await video_list()) + var_first_sending = True - if websocket not in clients: - break - - message = queue.get() # Warten auf neue Nachricht aus der Queue + message = await queue.get() - for client in clients: + # Alle Clients benachrichtigen + for client in list(clients): await client.send_text(message) @@ -240,7 +232,7 @@ async def websocket_v(websocket: WebSocket): logging.error(f"WebSocket error: {e}") finally: clients.discard(websocket) - if websocket.client_state == WebSocketState.CONNECTED: + if websocket.client_state == WebSocketState.DISCONNECTED: await websocket.close() @app.get("/clients") @@ -248,4 +240,4 @@ async def get_clients_count(): return {"active_clients": len(clients), "active_processes": len(active_process), "active_tasks": len(active_tasks)} if __name__ == "__main__": - uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=False) \ No newline at end of file + uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=False, log_level="debug") \ No newline at end of file diff --git a/app/templates/command.html b/app/templates/command.html index 7d655a6..64524c4 100644 --- a/app/templates/command.html +++ b/app/templates/command.html @@ -5,10 +5,10 @@ Video Liste -

Vorhandene Videos

+

FFmpeg Kommandos der Laufenden Video Konvertierungen

diff --git a/app/video_class.py b/app/video_class.py index a17fa92..43cd3d5 100644 --- a/app/video_class.py +++ b/app/video_class.py @@ -35,7 +35,7 @@ class Video: # Datenpaket self.frame: int = 0 - self.fps: int = 0 + self.fps: float = 0 self.q: int = 0 self.size: int = 0 self.time: int = 0 @@ -130,8 +130,8 @@ class Video: self.frame = int(frame[0]) # FPS - fps = re.findall(r"fps=\s*(\d+)", line_decoded) - self.fps = int(fps[0]) if fps else 0 + fps = re.findall(r"fps=\s*(\d+.\d*)", line_decoded) + self.fps = float(fps[0]) if fps else 0 # Quantizer q = re.findall(r"q=\s*(\d+).\d+", line_decoded) @@ -200,21 +200,21 @@ class Video: days = round(seconds // (24 * 3600)) seconds %= (24 * 3600) if days: - d = f"{days} Tage" + d = f"{days} T" else: d = "" hours = round(seconds // 3600) seconds %= 3600 if hours: - h = f"{hours} Std" + h = f"{hours} S" else: h = "" minutes = math.ceil(seconds // 60) seconds %= 60 if minutes: - m = f"{minutes} Min" + m = f"{minutes} M" else: m = "" diff --git a/app/webs/webs.css b/app/webs/webs.css index d5b564c..deb8c90 100644 --- a/app/webs/webs.css +++ b/app/webs/webs.css @@ -55,20 +55,33 @@ h1 { font-weight: bold; } -.table_video_info{ - border: 1px solid black; - width: 100%; -} - .icons { width: 25px; float: right; } +.video_info { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + width: 100% +} + +.table_video_info { + border-spacing: 15px; + border: 1; +} + .label { - text-align: left; + text-align: right; vertical-align: top; - display: inline-block; + width: 100px; +} + +.img { + align-items: right; + width: 100px; } .loader { @@ -87,4 +100,4 @@ h1 { .path { font-size: 12px; -} \ No newline at end of file +} diff --git a/app/webs/webs.js b/app/webs/webs.js index e81db52..bbd536c 100644 --- a/app/webs/webs.js +++ b/app/webs/webs.js @@ -122,26 +122,36 @@ function createVideoElement(id, source, target, path) {
-
-
${path}
- +
${path}

+
+
- - - + + + + + + - - - + + + + + + - - - + + + + + +
0 Anz
0 fps
0 Q
0 Anz
0 fps
0 Q
0 MB
0 Mb/s
0 x
0 MB
0 Mb/s
0 x
0
0
+ `; document.getElementById("videos").appendChild(container);