From dd2bec1bb992618c4422667ec609226d8f0d5578 Mon Sep 17 00:00:00 2001 From: Dark1291 Date: Tue, 4 Feb 2025 15:26:59 +0100 Subject: [PATCH] Fix ffmpeg installation on linux (#244) * Fix ffmpeg installation * Remove check file exists after ffmpeg conversion --- StreamingCommunity/Lib/FFmpeg/command.py | 42 +----- StreamingCommunity/Util/ffmpeg_installer.py | 134 ++++++++------------ 2 files changed, 55 insertions(+), 121 deletions(-) diff --git a/StreamingCommunity/Lib/FFmpeg/command.py b/StreamingCommunity/Lib/FFmpeg/command.py index cc9e8bc..4129c88 100644 --- a/StreamingCommunity/Lib/FFmpeg/command.py +++ b/StreamingCommunity/Lib/FFmpeg/command.py @@ -1,7 +1,6 @@ # 31.01.24 import sys -import time import logging import subprocess from typing import List, Dict @@ -36,24 +35,14 @@ FFMPEG_PATH = os_summary.ffmpeg_path def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None): - """ Joins single ts video file to mp4 Parameters: - video_path (str): The path to the video file. - out_path (str): The path to save the output file. - - vcodec (str): The video codec to use. Defaults to 'copy'. - - acodec (str): The audio codec to use. Defaults to 'aac'. - - bitrate (str): The bitrate for the audio stream. Defaults to '192k'. - - force_ts (bool): Force video path to be mpegts as input. + - codec (M3U8_Codec): The video codec to use. Defaults to 'copy'. """ - - if not os_manager.check_file(video_path): - logging.error("Missing input video for ffmpeg conversion.") - sys.exit(0) - - # Start command with locate ffmpeg ffmpeg_cmd = [FFMPEG_PATH] # Enabled the use of gpu @@ -121,11 +110,6 @@ def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None): capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video") print() - time.sleep(0.5) - if not os_manager.check_file(out_path): - logging.error("Missing output video for ffmpeg conversion video.") - sys.exit(0) - def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: str, codec: M3U8_Codec = None): """ @@ -137,11 +121,6 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s Each dictionary should contain the 'path' key with the path to the audio file. - out_path (str): The path to save the output file. """ - - if not os_manager.check_file(video_path): - logging.error("Missing input video for ffmpeg conversion.") - sys.exit(0) - video_audio_same_duration = check_duration_v_a(video_path, audio_tracks[0].get('path')) # Start command with locate ffmpeg @@ -225,11 +204,6 @@ def join_audios(video_path: str, audio_tracks: List[Dict[str, str]], out_path: s capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio") print() - time.sleep(0.5) - if not os_manager.check_file(out_path): - logging.error("Missing output video for ffmpeg conversion audio.") - sys.exit(0) - def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_path: str): """ @@ -241,12 +215,6 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat Each dictionary should contain the 'path' key with the path to the subtitle file and the 'name' key with the name of the subtitle. - out_path (str): The path to save the output file. """ - - if not os_manager.check_file(video_path): - logging.error("Missing input video for ffmpeg conversion.") - sys.exit(0) - - # Start command with locate ffmpeg ffmpeg_cmd = [FFMPEG_PATH, "-i", video_path] # Add subtitle input files first @@ -274,7 +242,6 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat ffmpeg_cmd += [out_path, "-y"] logging.info(f"FFmpeg command: {ffmpeg_cmd}") - # Run join if DEBUG_MODE: subprocess.run(ffmpeg_cmd, check=True) @@ -288,9 +255,4 @@ def join_subtitle(video_path: str, subtitles_list: List[Dict[str, str]], out_pat console.log(f"[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...") with suppress_output(): capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle") - print() - - time.sleep(0.5) - if not os_manager.check_file(out_path): - logging.error("Missing output video for ffmpeg conversion subtitle.") - sys.exit(0) + print() \ No newline at end of file diff --git a/StreamingCommunity/Util/ffmpeg_installer.py b/StreamingCommunity/Util/ffmpeg_installer.py index 7126711..87b1729 100644 --- a/StreamingCommunity/Util/ffmpeg_installer.py +++ b/StreamingCommunity/Util/ffmpeg_installer.py @@ -1,18 +1,19 @@ # 24.01.2024 import os +import glob +import gzip +import shutil +import tarfile +import zipfile +import logging import platform import subprocess -import zipfile -import tarfile -import logging -import requests -import shutil -import glob from typing import Optional, Tuple # External library +import requests from rich.console import Console from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn @@ -20,44 +21,30 @@ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRe # Variable console = Console() -# https://github.com/eugeneware/ffmpeg-static/releases -# https://github.com/GyanD/codexffmpeg/releases/ + FFMPEG_CONFIGURATION = { 'windows': { 'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary'), - 'download_url': 'https://github.com/GyanD/codexffmpeg/releases/download/{version}/ffmpeg-{version}-full_build.zip', - 'file_extension': '.zip', - 'executables': ['ffmpeg.exe', 'ffprobe.exe', 'ffplay.exe'] + 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-win32-{arch}.gz', + 'file_extension': '.gz', + 'executables': ['ffmpeg-win32-{arch}', 'ffprobe-win32-{arch}'] }, 'darwin': { 'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'), - 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/download/b{version}/ffmpeg-macOS-{arch}.zip', - 'file_extension': '.zip', - 'executables': ['ffmpeg', 'ffprobe', 'ffplay'] + 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-darwin-{arch}.gz', + 'file_extension': '.gz', + 'executables': ['ffmpeg-darwin-{arch}', 'ffprobe-darwin-{arch}'] }, 'linux': { 'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'), - 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/download/b{version}/ffmpeg-linux-{arch}.tar.xz', - 'file_extension': '.tar.xz', - 'executables': ['ffmpeg', 'ffprobe', 'ffplay'] + 'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-linux-{arch}.gz', + 'file_extension': '.gz', + 'executables': ['ffmpeg-linux-{arch}', 'ffprobe-linux-{arch}'] } } class FFMPEGDownloader: - """ - A class for downloading and managing FFmpeg executables. - - This class handles the detection of the operating system, downloading of FFmpeg binaries, - and management of the FFmpeg executables (ffmpeg, ffprobe, and ffplay). - - Attributes: - os_name (str): The detected operating system name - arch (str): The system architecture (e.g., x86_64, arm64) - home_dir (str): User's home directory path - base_dir (str): Base directory for storing FFmpeg binaries - """ - def __init__(self): self.os_name = self._detect_system() self.arch = self._detect_arch() @@ -70,9 +57,6 @@ class FFMPEGDownloader: Returns: str: Normalized operating system name ('windows', 'darwin', or 'linux') - - Raises: - ValueError: If the operating system is not supported """ system = platform.system().lower() if system in FFMPEG_CONFIGURATION: @@ -85,18 +69,17 @@ class FFMPEGDownloader: Returns: str: Normalized architecture name (e.g., 'x86_64', 'arm64') - - The method normalizes various architecture names to consistent values: - - amd64/x86_64/x64 -> x86_64 - - arm64/aarch64 -> arm64 """ machine = platform.machine().lower() arch_map = { - 'amd64': 'x86_64', - 'x86_64': 'x86_64', - 'x64': 'x86_64', - 'arm64': 'arm64', - 'aarch64': 'arm64' + 'amd64': 'x64', + 'x86_64': 'x64', + 'x64': 'x64', + 'arm64': 'arm64', + 'aarch64': 'arm64', + 'armv7l': 'arm', + 'i386': 'ia32', + 'i686': 'ia32' } return arch_map.get(machine, machine) @@ -106,11 +89,6 @@ class FFMPEGDownloader: Returns: str: Path to the base directory - - The directory location varies by operating system: - - Windows: C:\\binary - - macOS: ~/Applications/binary - - Linux: ~/.local/bin/binary """ base_dir = FFMPEG_CONFIGURATION[self.os_name]['base_dir'](self.home_dir) os.makedirs(base_dir, exist_ok=True) @@ -157,9 +135,6 @@ class FFMPEGDownloader: Returns: Optional[str]: The latest version string, or None if retrieval fails. - - Raises: - requests.exceptions.RequestException: If there are network-related errors. """ try: # Use GitHub API to fetch the latest release @@ -184,10 +159,6 @@ class FFMPEGDownloader: Returns: bool: True if download was successful, False otherwise. - - Raises: - requests.exceptions.RequestException: If there are network-related errors - IOError: If there are issues writing to the destination file """ try: response = requests.get(url, stream=True) @@ -233,6 +204,13 @@ class FFMPEGDownloader: elif archive_path.endswith('.tar.xz'): with tarfile.open(archive_path) as tar_ref: tar_ref.extractall(extraction_path) + elif archive_path.endswith('.gz'): + file_name = os.path.basename(archive_path)[:-3] # Remove extension .gz + output_path = os.path.join(extraction_path, file_name) + with gzip.open(archive_path, 'rb') as f_in, open(output_path, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + else: + raise ValueError("Unsupported archive format") config = FFMPEG_CONFIGURATION[self.os_name] executables = config['executables'] @@ -240,7 +218,6 @@ class FFMPEGDownloader: for executable in executables: exe_paths = glob.glob(os.path.join(extraction_path, '**', executable), recursive=True) - if exe_paths: dest_path = os.path.join(self.base_dir, executable) shutil.copy2(exe_paths[0], dest_path) @@ -269,34 +246,29 @@ class FFMPEGDownloader: Tuple[Optional[str], Optional[str], Optional[str]]: Paths to ffmpeg, ffprobe, and ffplay executables. Returns None for each executable that couldn't be downloaded or set up. """ - existing_ffmpeg, existing_ffprobe, existing_ffplay = self._check_existing_binaries() - if all([existing_ffmpeg, existing_ffprobe, existing_ffplay]): - return existing_ffmpeg, existing_ffprobe, existing_ffplay - - repo = 'GyanD/codexffmpeg' if self._detect_system() == 'windows' else 'eugeneware/ffmpeg-static' - console.print(f"[red]Use {repo} repo for downloading ffmpeg.") - version = self._get_latest_version(repo) - - if not version: - logging.error("Cannot proceed: version not found") - return None, None, None - config = FFMPEG_CONFIGURATION[self.os_name] - download_url = config['download_url'].format( - version=version, - arch=self.arch + executables = [exe.format(arch=self.arch) for exe in config['executables']] + + for executable in executables: + download_url = f"https://github.com/eugeneware/ffmpeg-static/releases/latest/download/{executable}.gz" + download_path = os.path.join(self.base_dir, f"{executable}.gz") + final_path = os.path.join(self.base_dir, executable) + + console.print(f"[bold blue]Downloading {executable} from GitHub[/]") + if not self._download_file(download_url, download_path): + return None, None, None + + with gzip.open(download_path, 'rb') as f_in, open(final_path, 'wb') as f_out: + shutil.copyfileobj(f_in, f_out) + + os.chmod(final_path, 0o755) + os.remove(download_path) + + return ( + os.path.join(self.base_dir, executables[0]), + os.path.join(self.base_dir, executables[1]), + None ) - - download_path = os.path.join( - self.base_dir, - f'ffmpeg-{version}{config["file_extension"]}' - ) - - console.print(f"[bold blue]Downloading FFmpeg from:[/] {download_url}") - if not self._download_file(download_url, download_path): - return None, None, None - - return self._extract_and_copy_binaries(download_path) def check_ffmpeg() -> Tuple[Optional[str], Optional[str], Optional[str]]: """