This commit is contained in:
Eduard Wisch 2025-03-03 19:27:42 +01:00
parent 7ea9407d41
commit f44c6d0af1
9 changed files with 156 additions and 55 deletions

Binary file not shown.

View file

@ -8,17 +8,19 @@ import threading
import time import time
from datetime import date from datetime import date
import uvicorn
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from starlette.websockets import WebSocketState
import app.video_class as vc import app.video_class as vc
# Settings # Settings
language = ["ger", "eng"] language = ["ger", "eng"]
subtitle_codec_blacklist = ["hdmv_pgs_subtitle", "dvd_subtitle"] subtitle_codec_blacklist = ["hdmv_pgs_subtitle", "dvd_subtitle"]
max_tasks = 2 max_tasks = 1
# Globale Variablen # Globale Variablen
queue = asyncio.Queue() queue = asyncio.Queue()
@ -27,7 +29,7 @@ video_files = {}
active_process = set() active_process = set()
active_tasks = set() active_tasks = set()
connected_clients = set() connected_clients = set()
semaphore = threading.Semaphore(1) semaphore = threading.Semaphore(max_tasks)
date = date.today() date = date.today()
if not os.path.exists("./logs"): if not os.path.exists("./logs"):
@ -105,7 +107,7 @@ def get_ffprobe(select, source_file):
# Convert Process ------------------------------------------------------------------------------------------------------ # Convert Process ------------------------------------------------------------------------------------------------------
def queue_video(): def queue_video():
for key, obj in video_files.items(): for key, obj in video_files.items():
with semaphore: if obj.finished == 0:
obj.task = threading.Thread(target=video_convert, args=(obj,)) obj.task = threading.Thread(target=video_convert, args=(obj,))
obj.task.start() obj.task.start()
active_tasks.add(obj) active_tasks.add(obj)
@ -114,6 +116,8 @@ def queue_video():
def video_convert(obj): def video_convert(obj):
global active_process global active_process
semaphore.acquire()
obj.convert_start = time.time() obj.convert_start = time.time()
# Erstelle und setze einen Event-Loop für diesen Thread # Erstelle und setze einen Event-Loop für diesen Thread
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
@ -123,7 +127,7 @@ def video_convert(obj):
"ffmpeg", "-y", "-i", obj.source_file, "ffmpeg", "-y", "-i", obj.source_file,
"-map", "0:0", "-map", "0:0",
"-c:v", "libsvtav1", "-c:v", "libsvtav1",
"-preset", "8", "-preset", "5",
"-crf", "30", "-crf", "30",
"-g", "240", "-g", "240",
"-pix_fmt", "yuv420p10le", "-pix_fmt", "yuv420p10le",
@ -150,6 +154,7 @@ def video_convert(obj):
]) ])
command.append(obj.output_file) command.append(obj.output_file)
logging.info(f"{command}") logging.info(f"{command}")
loop.run_until_complete(queue.put(video_list()))
# Prozess # Prozess
try: try:
@ -179,9 +184,11 @@ def video_convert(obj):
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"Convert Process Failure: {e}") logging.error(f"Convert Process Failure: {e}")
#UviCorn WebServer Teil #UviCorn WebServer Teil
@ -194,11 +201,9 @@ def read_output(qu):
loop = asyncio.new_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
print(active_process)
while True: while True:
if not len(active_process): if not len(active_process):
time.sleep(30) time.sleep(5)
continue continue
for obj in list(active_process): for obj in list(active_process):
@ -213,6 +218,13 @@ def read_output(qu):
logging.info(f"Data Packet created: {obj.to_dict()}") logging.info(f"Data Packet created: {obj.to_dict()}")
loop.run_until_complete(qu.put(obj.to_dict())) loop.run_until_complete(qu.put(obj.to_dict()))
def video_list():
vlist = []
for video in video_files.values():
vlist.append(video.get_vars())
return json.dumps({"video_list": vlist})
@app.post("/") @app.post("/")
async def receive_video_file(data: dict): async def receive_video_file(data: dict):
@ -242,20 +254,30 @@ async def websocket_v(websocket: WebSocket):
read_output_task = threading.Thread(target=read_output, args=(queue,)) read_output_task = threading.Thread(target=read_output, args=(queue,))
read_output_task.start() read_output_task.start()
# await queue.put(obj_list())
try: try:
var_first_sending = 0
while True: while True:
if websocket not in connected_clients:
break
message = await queue.get() # Warten auf neue Nachricht aus der Queue message = await queue.get() # Warten auf neue Nachricht aus der Queue
await websocket.send_text(message) await websocket.send_text(message)
if not var_first_sending:
await queue.put(video_list())
var_first_sending = 1
except WebSocketDisconnect: except WebSocketDisconnect:
logging.info("WebSocket disconnected") logging.info("WebSocket disconnected")
except Exception as e: except Exception as e:
logging.error(f"WebSocket error: {e}") logging.error(f"WebSocket error: {e}")
finally: finally:
connected_clients.discard(websocket) connected_clients.discard(websocket)
await websocket.close() if websocket.client_state == WebSocketState.CONNECTED:
await websocket.close()
@app.get("/clients") @app.get("/clients")
async def get_clients_count(): async def get_clients_count():
return {"active_clients": len(connected_clients), "active_processes": len(active_process), "active_tasks": len(active_tasks)} return {"active_clients": len(connected_clients), "active_processes": len(active_process), "active_tasks": len(active_tasks)}
if __name__ == "__main__":
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, reload=False)

View file

@ -8,7 +8,9 @@
</head> </head>
<body> <body>
<h1>Video-Konvertierung Fortschritt</h1> <h1>Video-Konvertierung Fortschritt</h1>
<div id="videos"></div> <div id="videos"></div><br />
<div id="video_list" class="video"><div class="video-header"><h1>Warteliste</h1></div></div><br />
<div id="video_finished" class="video"><div class="video-header"><h1>Fertig</h1></div></div>
<script src="/webs/webs.js"></script> <script src="/webs/webs.js"></script>
</body> </body>
</html> </html>

View file

@ -9,12 +9,16 @@ from collections import deque
class Video: class Video:
def __init__(self, path, video_streams, video_format, audio_streams, subtitle_streams): def __init__(self, path, video_streams, video_format, audio_streams, subtitle_streams):
self.id = id(self) self.id = id(self)
#Source
self.source_file = path self.source_file = path
self.file_name = os.path.basename(self.source_file) self.file_name = os.path.basename(self.source_file)
self.duration = self.time_in_sec(video_streams[0]["tags"].get("DURATION" or "duration", "00:00:00")) self.duration = self.time_in_sec(video_streams[0]["tags"].get("DURATION" or "duration", "00:00:00"))
self.frame_rate = self.frame_rate(video_streams) self.frame_rate = self.frame_rate(video_streams)
self.frames_max = self.frame_rate * self.duration self.frames_max = self.frame_rate * self.duration
self.source_file_size = video_format.get("size", 0)
# Target
self.output_file = f"{path.rsplit(".", 1)[0]}.webm" self.output_file = f"{path.rsplit(".", 1)[0]}.webm"
self.convert_start = 0 self.convert_start = 0
self.convert_end = 0 self.convert_end = 0
@ -30,15 +34,14 @@ class Video:
self.format = [video_format] self.format = [video_format]
# Datenpaket # Datenpaket
self.frame = 0 self.frame: int = 0
self.fps = 0 self.fps: int = 0
self.q = 0 self.q: int = 0
self.size = 0 self.size: int = 0
self.time = 0 self.time: int = 0
self.bitrate = 0 self.bitrate: int = 0
self.speed = 0 self.speed: int = 0
self.loading = 0 self.loading: int = 0
self.count_empty_data = 0
# Process # Process
self.task = None self.task = None
@ -109,7 +112,16 @@ class Video:
return json.dumps({"data_flow":data_packet}) return json.dumps({"data_flow":data_packet})
def get_vars(self): def get_vars(self):
return json.dumps({"obj_list":vars(self)}) obj_vars = {
"id": self.id,
"file_name": self.file_name,
"duration": self.format_time(self.duration),
"source_file_size": self.convert_kb_mb(self.source_file_size),
"finished": self.finished
}
return obj_vars
def extract_convert_data(self, line_decoded): def extract_convert_data(self, line_decoded):
frame = re.findall(r"frame=\s*(\d+)", line_decoded) frame = re.findall(r"frame=\s*(\d+)", line_decoded)
@ -178,23 +190,22 @@ class Video:
@staticmethod @staticmethod
def format_time(seconds): def format_time(seconds):
# Berechne die Anzahl der Tage, Stunden, Minuten und Sekunden days = round(seconds // (24 * 3600))
days = round(seconds // (24 * 3600)) # 1 Tag = 24 Stunden * 3600 Sekunden seconds %= (24 * 3600)
seconds %= (24 * 3600) # Restliche Sekunden nach Tagen
if days: if days:
d = f"{days} Tage" d = f"{days} Tage"
else: else:
d = "" d = ""
hours = round(seconds // 3600) # 1 Stunde = 3600 Sekunden hours = round(seconds // 3600)
seconds %= 3600 # Restliche Sekunden nach Stunden seconds %= 3600
if hours: if hours:
h = f"{hours} Std" h = f"{hours} Std"
else: else:
h = "" h = ""
minutes = math.ceil(seconds // 60) # 1 Minute = 60 Sekunden minutes = math.ceil(seconds // 60)
seconds %= 60 # Restliche Sekunden nach Minuten seconds %= 60
if minutes: if minutes:
m = f"{minutes} Min" m = f"{minutes} Min"
else: else:

BIN
app/webs/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
app/webs/mkv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -56,16 +56,10 @@ h1 {
} }
.table_video_info{ .table_video_info{
border: None; border: 1px solid black;
width: 100%; width: 100%;
} }
th, td {
width: 16%; /* Jede Zelle nimmt 33% der Breite der Tabelle ein */
padding: 16px;
text-align: left; /* Optional: Text ausrichten */
}
.icons { .icons {
width: 25px; width: 25px;
float: right; float: right;
@ -73,7 +67,8 @@ th, td {
.label { .label {
text-align: left; text-align: left;
vertical-align: middle; vertical-align: top;
display: inline-block;
} }
.loader { .loader {

View file

@ -1,6 +1,8 @@
let ws = new WebSocket("ws://127.0.0.1:8000/ws"); let ws = new WebSocket("ws://127.0.0.1:8000/ws");
let videoQueue = {}; // Hier speichern wir alle laufenden Videos let videoQueue = {}; // Hier speichern wir alle laufenden Videos
console.log("Start Script");
ws.onmessage = function(event) { ws.onmessage = function(event) {
try { try {
let packet = JSON.parse(event.data); let packet = JSON.parse(event.data);
@ -22,8 +24,22 @@ ws.onmessage = function(event) {
} else { } else {
console.error("Fehlende Felder in der Nachricht", message); console.error("Fehlende Felder in der Nachricht", message);
} }
} else { }
console.error("Kein data_flow in der empfangenen Nachricht", packet);
if (packet.video_list !== undefined) {
let video_list = packet.video_list;
createVideoList(video_list);
createVideoListFinished(video_list);
video_list.forEach(list => {
if(list.finished === 1) {
let container = document.getElementById(list.id); // Holt das Element mit der id
if (container) {
container.remove(); // Entfernt das Element aus dem DOM
}
}
});
} }
} catch (e) { } catch (e) {
console.error("Fehler beim Parsen der WebSocket-Nachricht:", e, event.data); console.error("Fehler beim Parsen der WebSocket-Nachricht:", e, event.data);
@ -34,12 +50,66 @@ ws.onerror = function(event) {
console.error("WebSocket Fehler:", event); console.error("WebSocket Fehler:", event);
}; };
function sekundenInStundenMinuten(sekunden) { function createVideoList(video) {
const stunden = Math.floor(sekunden / 3600); // 1 Stunde = 3600 Sekunden let container = document.createElement("div");
const minuten = Math.floor((sekunden % 3600) / 60); // Restsekunden in Minuten umrechnen container.className = "video_list";
const verbleibendeSekunden = sekunden % 60; // Übrige Sekunden container.id = "remaining"
return `${stunden} Stunden, ${minuten} Minuten, ${verbleibendeSekunden} Sekunden`; let html = ``;
video.forEach(list => {
if(list.finished === 0) {
html += `<tr>
<td width="5%" align="right"><img src="/webs/mkv.png" width="20px"></td>
<td width="65%" align="left"><span>${list.file_name}</span></td>
<td width="15%" align="right"><span>${list.duration}</span></td>
<td width="15%" align="right"><span>${list.source_file_size}</span></td>
</tr>`;
}
});
container.innerHTML = `
<div class="video-info">
<div>
<table class="table_video_wait" id="remaining_list" border="0" width="100%">
${html}
</table>
</div>
</div>`;
document.getElementById("video_list").appendChild(container);
return container;
}
function createVideoListFinished(video) {
let container = document.createElement("div");
container.className = "video_list";
container.id = "finished"
let html = ``;
video.forEach(list => {
if(list.finished === 1) {
html += `<tr>
<td width="5%" align="right"><img src="/webs/mkv.png" width="20px"></td>
<td width="65%" align="left"><span>${list.file_name}</span></td>
<td width="15%" align="right"><span>${list.duration}</span></td>
<td width="15%" align="right"><span>${list.source_file_size}</span></td>
</tr>`;
}
});
container.innerHTML = `
<div class="video-info">
<div>
<table class="table_video_wait" id="remaining_list" border="0" width="100%">
${html}
</table>
</div>
</div>`;
document.getElementById("video_finished").appendChild(container);
return container;
} }
function createVideoElement(id, source, target, path) { function createVideoElement(id, source, target, path) {
@ -54,21 +124,21 @@ function createVideoElement(id, source, target, path) {
</div> </div>
<div class="video-info"> <div class="video-info">
<div><span class="path">${path}</span></div> <div><span class="path">${path}</span></div>
<table class="table_video_info"> <table border="0" width="100%" style="border-spacing: 20px;">
<tr> <tr>
<th><img src="/webs/animation-32.png" class="icons"></th><th class="label"><div title="Anzahl Frames"><span class="frame">0</span> Anz</div></th> <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>
<th><img src="/webs/fps-32.png" class="icons"></th><th class="label"><div title="Frames / Sekunde"><span class="fps">0</span> fps</div></th> <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>
<th><img src="/webs/q-24.png" class="icons"></th><th class="label"><div title="Quantizer"><span class="q">0 </span> Q</div></th> <td><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>
<th><img src="/webs/ssd-30.png" class="icons"></th><th class="label"><div title="Dateigröße"><span class="size">0 </span> MB</div></th> <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>
<th><img src="/webs/bitrate-30.png" class="icons"></th><th class="label"><div title="Bitrate"><span class="bitrate">0 </span> Mb/s</div></th> <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>
<th><img src="/webs/speed-32.png" class="icons"></th><th class="label"><div title="Speed"><span class="speed">0</span> x</div></th> <td><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>
<th><img src="/webs/timer-50.png" class="icons"></th><th class="label" colspan="2"><div title="Verbleibend"><span class="time_remaining">0 </span></div></th> <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>
<th></th><th></th> <td></td><td></td>
<th></th><th><div class="loader"><span class="finished"></span></div></th> <td></td><td align="right"><div class="loader"><span class="finished"></span></div></td>
</tr> </tr>
</table> </table>
</div> </div>
@ -94,14 +164,15 @@ function updateVideoElement(id, data) {
let progress = data.loading; // Annahme: `loading` kommt als Zahl von 0 bis 100 let progress = data.loading; // Annahme: `loading` kommt als Zahl von 0 bis 100
progressBar.style.width = progress + "%" progressBar.style.width = progress + "%"
if (data.finished == 0) { if (data.finished == 3) {
progressBar.style.background = "linear-gradient(90deg, #4caf50, #00c853)"; progressBar.style.background = "linear-gradient(90deg, #4caf50, #00c853)";
container.querySelector(".finished").textContent = "Läuft"; container.querySelector(".finished").textContent = "Läuft";
} else if(data.finished == 1) { } else if(data.finished == 1) {
progressBar.style.background = "#4caf50"; progressBar.style.background = "#4caf50";
container.querySelector(".finished").textContent = "Fertig"; container.querySelector(".finished").textContent = "Fertig";
} else { } else if(data.finished == 2) {
progressBar.style.background = "#ff0000"; progressBar.style.background = "#ff0000";
container.querySelector(".finished").textContent = "Fehler"; container.querySelector(".finished").textContent = "Fehler";
} }
} }
console.log("End Script");