Getested und ein Berechnungsfehler beseitigt in Time to sec
This commit is contained in:
parent
c68aff4e87
commit
3bcf8b1bc3
6 changed files with 116 additions and 105 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
118
app/main.py
118
app/main.py
|
|
@ -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")
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 = ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue