Code komplett überarbeitet, Config yaml eingesetzt und ffmpeg Klasse angelegt
This commit is contained in:
parent
f44c6d0af1
commit
e37ca3155c
5 changed files with 131 additions and 90 deletions
10
app/config.yaml
Normal file
10
app/config.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
convert:
|
||||||
|
max_process: 1
|
||||||
|
language:
|
||||||
|
- deu
|
||||||
|
- eng
|
||||||
|
|
||||||
|
subtitle:
|
||||||
|
blacklist:
|
||||||
|
- hdmv_pgs_subtitle
|
||||||
|
- dvd_subtitle
|
||||||
6
app/config_class.py
Normal file
6
app/config_class.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
def __init__(self):
|
||||||
|
with open("app/config.yaml") as file:
|
||||||
|
self.config = yaml.safe_load(file)
|
||||||
54
app/ffmpeg_class.py
Normal file
54
app/ffmpeg_class.py
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
class Ffmpeg:
|
||||||
|
def __init__(config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def video_info():
|
||||||
|
command_info = [
|
||||||
|
"ffprobe", "-v",
|
||||||
|
"error",
|
||||||
|
"-select_streams",
|
||||||
|
f"{select}",
|
||||||
|
"-show_entries",
|
||||||
|
"stream=index,channels,codec_name,codec_type,pix_fmt,level,"
|
||||||
|
"film_grain,r_frame_rate,bit_rate,sample_rate,width,height,size,tags:stream_tags=language,duration",
|
||||||
|
"-show_entries",
|
||||||
|
"format=size,bit_rate,nb_streams",
|
||||||
|
"-of", "json",
|
||||||
|
source_file
|
||||||
|
]
|
||||||
|
|
||||||
|
return command_info
|
||||||
|
|
||||||
|
def video_convert():
|
||||||
|
command_convert = [
|
||||||
|
"ffmpeg", "-y", "-i", obj.source_file,
|
||||||
|
"-map", "0:0",
|
||||||
|
"-c:v", "libsvtav1",
|
||||||
|
"-preset", "5",
|
||||||
|
"-crf", "30",
|
||||||
|
"-g", "240",
|
||||||
|
"-pix_fmt", "yuv420p10le",
|
||||||
|
"-svtav1-params", "tune=0:film-grain=8",
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(obj.streams_audio):
|
||||||
|
for audio_stream in obj.streams_audio:
|
||||||
|
if audio_stream.get("tags", {}).get("language", None) in config["convert"]["language"]:
|
||||||
|
command_convert.extend([
|
||||||
|
"-map", f"0:{audio_stream['index']}",
|
||||||
|
f"-c:a", "libopus",
|
||||||
|
f"-b:a", "320k",
|
||||||
|
f"-ac", str(audio_stream['channels'])
|
||||||
|
])
|
||||||
|
|
||||||
|
# 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"]:
|
||||||
|
command_convert.extend([
|
||||||
|
"-map", f"0:{subtitle_stream['index']}",
|
||||||
|
])
|
||||||
|
command_convert.append(obj.output_file)
|
||||||
|
|
||||||
|
return command_convert
|
||||||
128
app/main.py
128
app/main.py
|
|
@ -16,21 +16,23 @@ from fastapi.templating import Jinja2Templates
|
||||||
from starlette.websockets import WebSocketState
|
from starlette.websockets import WebSocketState
|
||||||
|
|
||||||
import app.video_class as vc
|
import app.video_class as vc
|
||||||
|
from app.ffmpeg_class import FfmpegCommands as fc
|
||||||
# Settings
|
from app.config_class import Config as c
|
||||||
language = ["ger", "eng"]
|
|
||||||
subtitle_codec_blacklist = ["hdmv_pgs_subtitle", "dvd_subtitle"]
|
|
||||||
max_tasks = 1
|
|
||||||
|
|
||||||
# Globale Variablen
|
# Globale Variablen
|
||||||
queue = asyncio.Queue()
|
config = c.Config()
|
||||||
read_output_task = None
|
ffmpeg: ffmpeg = fc.Ffmpeg(config)
|
||||||
|
queue: queue = queue.Queue()
|
||||||
|
semaphore = threading.Semaphore(config["convert"]["max_process"])
|
||||||
|
|
||||||
|
thread_output = None
|
||||||
video_files = {}
|
video_files = {}
|
||||||
|
date = date.today()
|
||||||
|
|
||||||
|
# Prozess Objects
|
||||||
active_process = set()
|
active_process = set()
|
||||||
active_tasks = set()
|
active_tasks = set()
|
||||||
connected_clients = set()
|
clients = set()
|
||||||
semaphore = threading.Semaphore(max_tasks)
|
|
||||||
date = date.today()
|
|
||||||
|
|
||||||
if not os.path.exists("./logs"):
|
if not os.path.exists("./logs"):
|
||||||
os.mkdir("./logs")
|
os.mkdir("./logs")
|
||||||
|
|
@ -80,19 +82,7 @@ def get_video_information(media_path):
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_ffprobe(select, source_file):
|
def get_ffprobe(select, source_file):
|
||||||
command = [
|
command = ffmpeg.video_info()
|
||||||
"ffprobe", "-v",
|
|
||||||
"error",
|
|
||||||
"-select_streams",
|
|
||||||
f"{select}",
|
|
||||||
"-show_entries",
|
|
||||||
"stream=index,channels,codec_name,codec_type,pix_fmt,level,"
|
|
||||||
"film_grain,r_frame_rate,bit_rate,sample_rate,width,height,size,tags:stream_tags=language,duration",
|
|
||||||
"-show_entries",
|
|
||||||
"format=size,bit_rate,nb_streams",
|
|
||||||
"-of", "json",
|
|
||||||
source_file
|
|
||||||
]
|
|
||||||
|
|
||||||
result = subprocess.run(command, stdout=subprocess.PIPE, text=True)
|
result = subprocess.run(command, stdout=subprocess.PIPE, text=True)
|
||||||
json_data = json.loads(result.stdout)
|
json_data = json.loads(result.stdout)
|
||||||
|
|
@ -118,41 +108,14 @@ def video_convert(obj):
|
||||||
|
|
||||||
semaphore.acquire()
|
semaphore.acquire()
|
||||||
|
|
||||||
|
result: str = None
|
||||||
obj.convert_start = time.time()
|
obj.convert_start = time.time()
|
||||||
# Erstelle und setze einen Event-Loop für diesen Thread
|
|
||||||
loop = asyncio.new_event_loop()
|
loop = asyncio.new_event_loop()
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
command = [
|
command = ffmpeg.video_convert()
|
||||||
"ffmpeg", "-y", "-i", obj.source_file,
|
|
||||||
"-map", "0:0",
|
|
||||||
"-c:v", "libsvtav1",
|
|
||||||
"-preset", "5",
|
|
||||||
"-crf", "30",
|
|
||||||
"-g", "240",
|
|
||||||
"-pix_fmt", "yuv420p10le",
|
|
||||||
"-svtav1-params", "tune=0:film-grain=8",
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(obj.streams_audio):
|
|
||||||
for audio_stream in obj.streams_audio:
|
|
||||||
if audio_stream.get("tags", {}).get("language", None) in language:
|
|
||||||
command.extend([
|
|
||||||
"-map", f"0:{audio_stream['index']}",
|
|
||||||
f"-c:a", "libopus",
|
|
||||||
f"-b:a", "320k",
|
|
||||||
f"-ac", str(audio_stream['channels'])
|
|
||||||
])
|
|
||||||
|
|
||||||
# Subtitle-Streams einbinden
|
|
||||||
if len(obj.streams_subtitle):
|
|
||||||
for subtitle_stream in obj.streams_subtitle:
|
|
||||||
if subtitle_stream.get("codec_name") not in subtitle_codec_blacklist:
|
|
||||||
if subtitle_stream.get("tags", {}).get("language", None) in language:
|
|
||||||
command.extend([
|
|
||||||
"-map", f"0:{subtitle_stream['index']}",
|
|
||||||
])
|
|
||||||
command.append(obj.output_file)
|
|
||||||
logging.info(f"{command}")
|
logging.info(f"{command}")
|
||||||
loop.run_until_complete(queue.put(video_list()))
|
loop.run_until_complete(queue.put(video_list()))
|
||||||
|
|
||||||
|
|
@ -173,17 +136,19 @@ def video_convert(obj):
|
||||||
|
|
||||||
if obj.process.poll() == 0:
|
if obj.process.poll() == 0:
|
||||||
obj.finished = 1
|
obj.finished = 1
|
||||||
logging.info(f"Process Finished({obj.process.returncode}): {obj.file_name}")
|
result = "Finished"
|
||||||
loop.run_until_complete(queue.put(obj.to_dict()))
|
|
||||||
|
|
||||||
elif obj.process.poll() != 0:
|
elif obj.process.poll() != 0:
|
||||||
obj.finished = 2
|
obj.finished = 2
|
||||||
logging.info(f"Process Failure({obj.process.returncode}): {obj.file_name}")
|
result = "Failure"
|
||||||
loop.run_until_complete(queue.put(obj.to_dict()))
|
|
||||||
|
|
||||||
|
logging.info(f"Process {result}({obj.process.returncode}): {obj.file_name}")
|
||||||
|
|
||||||
|
loop.run_until_complete(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()
|
semaphore.release()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -193,30 +158,24 @@ def video_convert(obj):
|
||||||
|
|
||||||
#UviCorn WebServer Teil
|
#UviCorn WebServer Teil
|
||||||
#----------------------------------------------------------------------------------------------------------------------
|
#----------------------------------------------------------------------------------------------------------------------
|
||||||
def read_output(qu):
|
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
|
||||||
|
|
||||||
# Erstelle und setze einen Event-Loop für diesen Thread
|
|
||||||
loop = asyncio.new_event_loop()
|
|
||||||
asyncio.set_event_loop(loop)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if not len(active_process):
|
if not len(active_process):
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for obj in list(active_process):
|
for obj in list(active_process):
|
||||||
line_error = obj.process.stderr.read(1024)
|
line_error = obj.process.stderr.read(1024).decode()
|
||||||
|
|
||||||
line_error_decoded = line_error.decode()
|
logging.info(f"Data ffmpeg: {line_error}")
|
||||||
|
obj.extract_convert_data(line_error)
|
||||||
logging.info(f"Data ffmpeg: {line_error_decoded}")
|
|
||||||
obj.extract_convert_data(line_error_decoded)
|
|
||||||
|
|
||||||
# Verwende den Event-Loop, um die Daten in die asyncio Queue zu legen
|
# Verwende den Event-Loop, um die Daten in die asyncio Queue zu legen
|
||||||
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()))
|
queue.put(obj.to_dict())
|
||||||
|
|
||||||
def video_list():
|
def video_list():
|
||||||
vlist = []
|
vlist = []
|
||||||
|
|
@ -244,40 +203,45 @@ async def display_paths(request: Request):
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
async def websocket_v(websocket: WebSocket):
|
async def websocket_v(websocket: WebSocket):
|
||||||
"""WebSocket-Verbindung für die Kommunikation mit Clients."""
|
"""WebSocket-Verbindung für die Kommunikation mit Clients."""
|
||||||
global read_output_task, connected_clients
|
global thread_output, clients
|
||||||
|
|
||||||
connected_clients.add(websocket)
|
clients.add(websocket)
|
||||||
await websocket.accept()
|
await websocket.accept()
|
||||||
|
|
||||||
if read_output_task is None:
|
if thread_output is None:
|
||||||
# Startet den Thread zum Verarbeiten der Prozessausgaben
|
# Startet den Thread zum Verarbeiten der Prozessausgaben
|
||||||
read_output_task = threading.Thread(target=read_output, args=(queue,))
|
thread_output = threading.Thread(target=read_output, args=(queue,))
|
||||||
read_output_task.start()
|
thread_output.start()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
var_first_sending = 0
|
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
|
|
||||||
await websocket.send_text(message)
|
|
||||||
if not var_first_sending:
|
if not var_first_sending:
|
||||||
await queue.put(video_list())
|
queue.put(video_list())
|
||||||
var_first_sending = 1
|
var_first_sending = 1
|
||||||
|
|
||||||
|
if websocket not in clients:
|
||||||
|
break
|
||||||
|
|
||||||
|
message = queue.get() # Warten auf neue Nachricht aus der Queue
|
||||||
|
|
||||||
|
for client in clients:
|
||||||
|
await client.send_text(message)
|
||||||
|
|
||||||
|
|
||||||
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)
|
clients.discard(websocket)
|
||||||
if websocket.client_state == WebSocketState.CONNECTED:
|
if websocket.client_state == WebSocketState.CONNECTED:
|
||||||
await websocket.close()
|
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(clients), "active_processes": len(active_process), "active_tasks": len(active_tasks)}
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, reload=False)
|
uvicorn.run("app.main:app", host="127.0.0.1", port=8000, reload=False)
|
||||||
|
|
@ -40,7 +40,7 @@ class Video:
|
||||||
self.size: int = 0
|
self.size: int = 0
|
||||||
self.time: int = 0
|
self.time: int = 0
|
||||||
self.bitrate: int = 0
|
self.bitrate: int = 0
|
||||||
self.speed: int = 0
|
self.speed: float = 0
|
||||||
self.loading: int = 0
|
self.loading: int = 0
|
||||||
|
|
||||||
# Process
|
# Process
|
||||||
|
|
@ -124,27 +124,36 @@ class Video:
|
||||||
return obj_vars
|
return obj_vars
|
||||||
|
|
||||||
def extract_convert_data(self, line_decoded):
|
def extract_convert_data(self, line_decoded):
|
||||||
|
# Frames
|
||||||
frame = re.findall(r"frame=\s*(\d+)", line_decoded)
|
frame = re.findall(r"frame=\s*(\d+)", line_decoded)
|
||||||
self.frame = int(frame[0]) if frame else 0
|
if frame and int(frame[0]) > self.frame:
|
||||||
|
self.frame = int(frame[0])
|
||||||
|
|
||||||
|
# FPS
|
||||||
fps = re.findall(r"fps=\s*(\d+)", line_decoded)
|
fps = re.findall(r"fps=\s*(\d+)", line_decoded)
|
||||||
self.fps = int(fps[0]) if fps else 0
|
self.fps = int(fps[0]) if fps else 0
|
||||||
|
|
||||||
|
# Quantizer
|
||||||
q = re.findall(r"q=\s*(\d+.\d+)", line_decoded)
|
q = re.findall(r"q=\s*(\d+.\d+)", line_decoded)
|
||||||
self.q = q[0] if q else 0
|
self.q = int([0]) if q else 0
|
||||||
|
|
||||||
|
# File Size
|
||||||
size = re.findall(r"size=\s*(\d+)", line_decoded)
|
size = re.findall(r"size=\s*(\d+)", line_decoded)
|
||||||
self.size = self.convert_kb_mb(size[0]) if size else 0
|
if size and self.convert_kb_mb(size[0]) > self.size:
|
||||||
|
self.size = self.convert_kb_mb(size[0])
|
||||||
|
|
||||||
|
# Time
|
||||||
time = re.findall(r"time=\s*(\d+:\d+:\d+)", line_decoded)
|
time = re.findall(r"time=\s*(\d+:\d+:\d+)", line_decoded)
|
||||||
time_v = time[0] if time else "00:00:00"
|
time_v = time[0] if time else "00:00:00"
|
||||||
self.time = self.time_in_sec(time_v)
|
self.time = self.time_in_sec(time_v)
|
||||||
|
|
||||||
|
# Bitrate
|
||||||
bitrate = re.findall(r"bitrate=\s*(\d+)", line_decoded)
|
bitrate = re.findall(r"bitrate=\s*(\d+)", line_decoded)
|
||||||
self.bitrate = self.convert_kb_mb(bitrate[0]) if bitrate else 0
|
self.bitrate = self.convert_kb_mb(bitrate[0]) if bitrate else 0
|
||||||
|
|
||||||
|
# Speed
|
||||||
speed = re.findall(r"speed=\s*(\d+\.\d+)", line_decoded)
|
speed = re.findall(r"speed=\s*(\d+\.\d+)", line_decoded)
|
||||||
self.speed = speed[0] if speed else 0
|
self.speed = float(speed[0]) if speed else 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def time_in_sec(time_str):
|
def time_in_sec(time_str):
|
||||||
|
|
@ -153,9 +162,7 @@ class Video:
|
||||||
if len(hs_ms_s[0]) >= 3:
|
if len(hs_ms_s[0]) >= 3:
|
||||||
if hs_ms_s[0][0].isdigit() and hs_ms_s[0][1].isdigit() and hs_ms_s[0][2].isdigit():
|
if hs_ms_s[0][0].isdigit() and hs_ms_s[0][1].isdigit() and hs_ms_s[0][2].isdigit():
|
||||||
try:
|
try:
|
||||||
time = int(hs_ms_s[0][0]) * 60 + int(hs_ms_s[0][1]) * 60 + int(hs_ms_s[0][2])
|
return int(hs_ms_s[0][0]) * 60 + int(hs_ms_s[0][1]) * 60 + int(hs_ms_s[0][2])
|
||||||
print(time)
|
|
||||||
return time
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logging.error(f"Wert: {time_str} Fehler: {e}")
|
logging.error(f"Wert: {time_str} Fehler: {e}")
|
||||||
return 0
|
return 0
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue