Getested und ein Berechnungsfehler beseitigt in Time to sec

This commit is contained in:
Eduard Wisch 2025-03-12 06:39:25 +01:00
parent c68aff4e87
commit 3bcf8b1bc3
6 changed files with 116 additions and 105 deletions

View file

@ -1,12 +1,9 @@
from app.main import video_convert class FFCmd:
def __init__(self, config):
class Ffmpeg:
def __init__(config):
self.config = config self.config = config
@staticmethod @staticmethod
def video_info(): def video_info(select, source_file):
command_info = [ command_info = [
"ffprobe", "-v", "ffprobe", "-v",
"error", "error",
@ -23,8 +20,7 @@ class Ffmpeg:
return command_info return command_info
@staticmethod def video_convert(self, obj):
def video_convert():
command_convert = [ command_convert = [
"ffmpeg", "-y", "-i", obj.source_file, "ffmpeg", "-y", "-i", obj.source_file,
"-map", "0:0", "-map", "0:0",
@ -33,12 +29,12 @@ class Ffmpeg:
"-crf", "30", "-crf", "30",
"-g", "240", "-g", "240",
"-pix_fmt", "yuv420p10le", "-pix_fmt", "yuv420p10le",
"-svtav1-params", "tune=0:film-grain=8", "-svtav1-params", "tune=0:film-grain=8:threads=16",
] ]
if len(obj.streams_audio): if len(obj.streams_audio):
for audio_stream in 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([ command_convert.extend([
"-map", f"0:{audio_stream['index']}", "-map", f"0:{audio_stream['index']}",
f"-c:a", "libopus", f"-c:a", "libopus",
@ -49,8 +45,8 @@ class Ffmpeg:
# Subtitle-Streams einbinden # Subtitle-Streams einbinden
if len(obj.streams_subtitle): if len(obj.streams_subtitle):
for subtitle_stream in 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("codec_name") not in self.config["subtitle"]["blacklist"]:
if subtitle_stream.get("tags", {}).get("language", None) in config["convert"]["language"]: if subtitle_stream.get("tags", {}).get("language", None) in self.config["convert"]["language"]:
command_convert.extend([ command_convert.extend([
"-map", f"0:{subtitle_stream['index']}", "-map", f"0:{subtitle_stream['index']}",
]) ])
@ -59,7 +55,7 @@ class Ffmpeg:
return command_convert return command_convert
@staticmethod @staticmethod
def command_as_string(self, cmd): def command_as_string(cmd):
command_as_string: str = "" command_as_string: str = ""
for para in cmd: for para in cmd:
if "/" not in para: if "/" not in para:

View file

@ -1,11 +1,10 @@
import asyncio
import os.path import os.path
import re import re
import subprocess import subprocess
import json import json
import logging import logging
import threading
import time import time
import queue
from datetime import date from datetime import date
import uvicorn import uvicorn
@ -15,16 +14,16 @@ from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.websockets import WebSocketState from starlette.websockets import WebSocketState
import app.video_class as vc import video_class as vc
from app.ffmpeg_class import Ffmpeg as Fc from ffmpeg_class import FFCmd as Fc
from app.config_class import Config as Cfg from config_class import Config as Cfg
# Globale Variablen # Globale Variablen
config = Cfg().config config = Cfg().config
ffmpeg = Fc(config) ffmpeg = Fc(config)
queue: queue = queue.Queue() queue = asyncio.Queue()
semaphore = threading.Semaphore(config["convert"]["max_process"]) semaphore = asyncio.Semaphore(config["convert"]["max_process"])
thread_output = None thread_output = None
video_files = {} video_files = {}
@ -96,87 +95,81 @@ def get_ffprobe(select, source_file):
return json_data.get("streams", []) return json_data.get("streams", [])
# Convert Process ------------------------------------------------------------------------------------------------------ # Convert Process ------------------------------------------------------------------------------------------------------
def queue_video(): async def queue_video():
for key, obj in video_files.items(): async with semaphore:
if obj.finished == 0: for key, obj in video_files.items():
obj.task = threading.Thread(target=video_convert, args=(obj,)) if obj.finished == 0:
obj.task.start() obj.task = await asyncio.create_task(video_convert(obj))
active_tasks.add(obj) active_tasks.add(obj)
logging.info(f"Warteschlange started Auftrag - {obj.task}") logging.info(f"Warteschlange started Auftrag - {obj.task}")
def video_convert(obj): async def video_convert(obj):
global active_process """Startet die Videokonvertierung asynchron."""
global active_process, active_tasks
semaphore.acquire()
result: str = ""
obj.convert_start = time.time() 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: try:
obj.process = subprocess.Popen( # Starte den Subprozess asynchron
command, obj.process = await asyncio.create_subprocess_exec(
stdout=subprocess.PIPE, *command,
stderr=subprocess.PIPE stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
) )
active_process.add(obj) active_process.add(obj)
obj.finished = 3 obj.finished = 3
queue.put(video_list()) await queue.put(await video_list())
while obj.process.poll() is None: while obj.process.returncode is None:
logging.info(f"{obj.file_name} ... Running") logging.info(f"{obj.file_name} ... läuft noch")
time.sleep(10) await asyncio.sleep(10)
if obj.process.poll() == 0: # Prozess beendet, Status auswerten
if obj.process.returncode == 0:
obj.finished = 1 obj.finished = 1
result = "Finished" result = "Finished"
elif obj.process.poll() != 0: else:
obj.finished = 2 obj.finished = 2
result = "Failure" result = "Failure"
logging.info(f"Process {result}({obj.process.returncode}): {obj.file_name}") logging.info(f"Prozess {result}({obj.process.returncode}): {obj.file_name}")
queue.put(obj.to_dict()) await queue.put(obj.to_dict())
active_process.discard(obj) active_process.discard(obj)
active_tasks.discard(obj) active_tasks.discard(obj)
obj.convert_end = time.time() obj.convert_end = time.time()
semaphore.release()
except Exception as e: except Exception as e:
obj.finished = 2 obj.finished = 2
semaphore.release() logging.error(f"Fehler in video_convert(): {e}")
logging.error(f"Convert Process Failure: {e}")
#UviCorn WebServer Teil #UviCorn WebServer Teil
#---------------------------------------------------------------------------------------------------------------------- #----------------------------------------------------------------------------------------------------------------------
def read_output(): async def read_output():
"""Thread-Funktion, die Prozessausgaben in die Queue legt.""" """Thread-Funktion, die Prozessausgaben in die Queue legt."""
global active_process global active_process
while True: while True:
if not len(active_process): if not len(active_process):
time.sleep(30) await asyncio.sleep(30)
queue.put(video_list()) await queue.put(video_list())
continue continue
for obj in list(active_process): 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) obj.extract_convert_data(line_error)
# Verwende den Event-Loop, um die Daten in die asyncio Queue zu legen await queue.put(obj.to_dict())
logging.info(f"Data Packet created: {obj.to_dict()}") await asyncio.sleep(3)
queue.put(obj.to_dict())
def video_list(): async def video_list():
vlist = [] vlist = []
for video in video_files.values(): for video in video_files.values():
vlist.append(video.get_vars()) vlist.append(video.get_vars())
@ -187,18 +180,20 @@ def video_list():
@app.post("/") @app.post("/")
async def receive_video_file(data: dict): async def receive_video_file(data: dict):
if get_video_information(data): if get_video_information(data):
queue_video() await queue_video()
await queue.put(await video_list())
else: else:
logging.error(f"Videos konnten nicht verarbeitet werden! Warteschleife wurde nicht gestarted") logging.error(f"Videos konnten nicht verarbeitet werden! Warteschleife wurde nicht gestarted")
@app.get("/command", response_class=HTMLResponse) @app.get("/command", response_class=HTMLResponse)
async def display_paths(request: Request): async def display_paths(request: Request):
list_command: list = []
for video in video_files.values(): for video in video_files.values():
if video.finished == 3: 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) @app.get("/webs-ui", response_class=HTMLResponse)
async def display_paths(request: Request): async def display_paths(request: Request):
@ -214,23 +209,20 @@ async def websocket_v(websocket: WebSocket):
if thread_output is None: if thread_output is None:
# Startet den Thread zum Verarbeiten der Prozessausgaben # Startet den Thread zum Verarbeiten der Prozessausgaben
thread_output = threading.Thread(target=read_output) thread_output = asyncio.create_task(read_output())
thread_output.start()
try: try:
var_first_sending = 0 var_first_sending = 0
while True: while True:
if not var_first_sending: if not var_first_sending:
queue.put(video_list()) await queue.put(await video_list())
var_first_sending = 1 var_first_sending = True
if websocket not in clients: message = await queue.get()
break
message = queue.get() # Warten auf neue Nachricht aus der Queue
for client in clients: # Alle Clients benachrichtigen
for client in list(clients):
await client.send_text(message) await client.send_text(message)
@ -240,7 +232,7 @@ async def websocket_v(websocket: WebSocket):
logging.error(f"WebSocket error: {e}") logging.error(f"WebSocket error: {e}")
finally: finally:
clients.discard(websocket) clients.discard(websocket)
if websocket.client_state == WebSocketState.CONNECTED: if websocket.client_state == WebSocketState.DISCONNECTED:
await websocket.close() await websocket.close()
@app.get("/clients") @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)} return {"active_clients": len(clients), "active_processes": len(active_process), "active_tasks": len(active_tasks)}
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=False) uvicorn.run("main:app", host="127.0.0.1", port=8000, reload=False, log_level="debug")

View file

@ -5,10 +5,10 @@
<title>Video Liste</title> <title>Video Liste</title>
</head> </head>
<body> <body>
<h1>Vorhandene Videos</h1> <h1>FFmpeg Kommandos der Laufenden Video Konvertierungen</h1>
<ul> <ul>
{% for key, video in videos.items() %} {% for cmd in commands %}
<li>{{ video.source_file }} <b>{{video.finished}}</b></li> <li>{{ cmd }}</li><br /><br />
{% endfor %} {% endfor %}
</ul> </ul>
</body> </body>

View file

@ -35,7 +35,7 @@ class Video:
# Datenpaket # Datenpaket
self.frame: int = 0 self.frame: int = 0
self.fps: int = 0 self.fps: float = 0
self.q: int = 0 self.q: int = 0
self.size: int = 0 self.size: int = 0
self.time: int = 0 self.time: int = 0
@ -130,8 +130,8 @@ class Video:
self.frame = int(frame[0]) self.frame = int(frame[0])
# FPS # FPS
fps = re.findall(r"fps=\s*(\d+)", line_decoded) fps = re.findall(r"fps=\s*(\d+.\d*)", line_decoded)
self.fps = int(fps[0]) if fps else 0 self.fps = float(fps[0]) if fps else 0
# Quantizer # Quantizer
q = re.findall(r"q=\s*(\d+).\d+", line_decoded) q = re.findall(r"q=\s*(\d+).\d+", line_decoded)
@ -200,21 +200,21 @@ class Video:
days = round(seconds // (24 * 3600)) days = round(seconds // (24 * 3600))
seconds %= (24 * 3600) seconds %= (24 * 3600)
if days: if days:
d = f"{days} Tage" d = f"{days} T"
else: else:
d = "" d = ""
hours = round(seconds // 3600) hours = round(seconds // 3600)
seconds %= 3600 seconds %= 3600
if hours: if hours:
h = f"{hours} Std" h = f"{hours} S"
else: else:
h = "" h = ""
minutes = math.ceil(seconds // 60) minutes = math.ceil(seconds // 60)
seconds %= 60 seconds %= 60
if minutes: if minutes:
m = f"{minutes} Min" m = f"{minutes} M"
else: else:
m = "" m = ""

View file

@ -55,20 +55,33 @@ h1 {
font-weight: bold; font-weight: bold;
} }
.table_video_info{
border: 1px solid black;
width: 100%;
}
.icons { .icons {
width: 25px; width: 25px;
float: right; 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 { .label {
text-align: left; text-align: right;
vertical-align: top; vertical-align: top;
display: inline-block; width: 100px;
}
.img {
align-items: right;
width: 100px;
} }
.loader { .loader {
@ -87,4 +100,4 @@ h1 {
.path { .path {
font-size: 12px; font-size: 12px;
} }

View file

@ -122,26 +122,36 @@ function createVideoElement(id, source, target, path) {
<div class="progress-container"> <div class="progress-container">
<div class="progress-bar"></div> <div class="progress-bar"></div>
</div> </div>
<div class="video-info"> <div><span class="path">${path}</span></div><br />
<div><span class="path">${path}</span></div> <div class="video_info">
<table border="0" width="100%" style="border-spacing: 20px;"> <table class="table_video_info" border="0">
<tr> <tr>
<td><img src="/webs/animation-32.png" class="icons"></td><td class="label"><div title="Anzahl Frames"><span class="frame">0</span> Anz</div></td> <td class="img"><img src="/webs/animation-32.png" class="icons"></td>
<td><img src="/webs/fps-32.png" class="icons"></td><td class="label"><div title="Frames / Sekunde"><span class="fps">0</span> fps</div></td> <td class="label"><div title="Anzahl Frames"><span class="frame">0</span> Anz</div></td>
<td><img src="/webs/q-24.png" class="icons"></td><td class="label"><div title="Quantizer"><span class="q">0 </span> Q</div></td> <td class="img"><img src="/webs/fps-32.png" class="icons"></td>
<td class="label"><div title="Frames / Sekunde"><span class="fps">0</span> fps</div></td>
<td class="img"><img src="/webs/q-24.png" class="icons"></td>
<td class="label"><div title="Quantizer"><span class="q">0 </span> Q</div></td>
</tr> </tr>
<tr> <tr>
<td><img src="/webs/ssd-30.png" class="icons"></td><td class="label"><div title="Dateigröße"><span class="size">0 </span> MB</div></td> <td class="img"><img src="/webs/ssd-30.png" class="icons"></td>
<td><img src="/webs/bitrate-30.png" class="icons"></td><td class="label"><div title="Bitrate"><span class="bitrate">0 </span> Mb/s</div></td> <td class="label"><div title="Dateigröße"><span class="size">0 </span> MB</div></td>
<td><img src="/webs/speed-32.png" class="icons"></td><td class="label"><div title="Speed"><span class="speed">0</span> x</div></td> <td class="img"><img src="/webs/bitrate-30.png" class="icons"></td>
<td class="label"><div title="Bitrate"><span class="bitrate">0 </span> Mb/s</div></td>
<td class="img"><img src="/webs/speed-32.png" class="icons"></td>
<td class="label"><div title="Speed"><span class="speed">0</span> x</div></td>
</tr> </tr>
<tr> <tr>
<td valign="top"><img src="/webs/timer-50.png" class="icons"></td><td class="label"><div title="Verbleibend"><span class="time_remaining">0 </span></div></td> <td class="img"><img src="/webs/timer-50.png" class="icons"></td>
<td></td><td></td> <td class="label"><div title="Verbleibend"><span class="time_remaining">0 </span></div></td>
<td></td><td align="right"><div class="loader"><span class="finished"></span></div></td> <td class="img"></td>
<td class="label"></td>
<td class="img"></td>
<td align="right"><div class="loader"><span class="finished"></span></div></td>
</tr> </tr>
</table> </table>
</div> </div>
`; `;
document.getElementById("videos").appendChild(container); document.getElementById("videos").appendChild(container);