mirror of
https://github.com/Arrowar/StreamingCommunity.git
synced 2025-06-06 11:35:29 +00:00
Fix ffmpeg installation on linux (#244)
* Fix ffmpeg installation * Remove check file exists after ffmpeg conversion
This commit is contained in:
parent
8e20ad8ff6
commit
dd2bec1bb9
@ -1,7 +1,6 @@
|
|||||||
# 31.01.24
|
# 31.01.24
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import List, Dict
|
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):
|
def join_video(video_path: str, out_path: str, codec: M3U8_Codec = None):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Joins single ts video file to mp4
|
Joins single ts video file to mp4
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- video_path (str): The path to the video file.
|
- video_path (str): The path to the video file.
|
||||||
- out_path (str): The path to save the output file.
|
- out_path (str): The path to save the output file.
|
||||||
- vcodec (str): The video codec to use. Defaults to 'copy'.
|
- codec (M3U8_Codec): 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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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]
|
ffmpeg_cmd = [FFMPEG_PATH]
|
||||||
|
|
||||||
# Enabled the use of gpu
|
# 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")
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join video")
|
||||||
print()
|
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):
|
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.
|
Each dictionary should contain the 'path' key with the path to the audio file.
|
||||||
- out_path (str): The path to save the output 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'))
|
video_audio_same_duration = check_duration_v_a(video_path, audio_tracks[0].get('path'))
|
||||||
|
|
||||||
# Start command with locate ffmpeg
|
# 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")
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join audio")
|
||||||
print()
|
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):
|
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.
|
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.
|
- 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]
|
ffmpeg_cmd = [FFMPEG_PATH, "-i", video_path]
|
||||||
|
|
||||||
# Add subtitle input files first
|
# 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"]
|
ffmpeg_cmd += [out_path, "-y"]
|
||||||
logging.info(f"FFmpeg command: {ffmpeg_cmd}")
|
logging.info(f"FFmpeg command: {ffmpeg_cmd}")
|
||||||
|
|
||||||
|
|
||||||
# Run join
|
# Run join
|
||||||
if DEBUG_MODE:
|
if DEBUG_MODE:
|
||||||
subprocess.run(ffmpeg_cmd, check=True)
|
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]] ...")
|
console.log(f"[purple]FFmpeg [white][[cyan]Join subtitle[white]] ...")
|
||||||
with suppress_output():
|
with suppress_output():
|
||||||
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle")
|
capture_ffmpeg_real_time(ffmpeg_cmd, "[cyan]Join subtitle")
|
||||||
print()
|
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)
|
|
@ -1,18 +1,19 @@
|
|||||||
# 24.01.2024
|
# 24.01.2024
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import glob
|
||||||
|
import gzip
|
||||||
|
import shutil
|
||||||
|
import tarfile
|
||||||
|
import zipfile
|
||||||
|
import logging
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import zipfile
|
|
||||||
import tarfile
|
|
||||||
import logging
|
|
||||||
import requests
|
|
||||||
import shutil
|
|
||||||
import glob
|
|
||||||
from typing import Optional, Tuple
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
# External library
|
# External library
|
||||||
|
import requests
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn
|
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn
|
||||||
|
|
||||||
@ -20,44 +21,30 @@ from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRe
|
|||||||
# Variable
|
# Variable
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
# https://github.com/eugeneware/ffmpeg-static/releases
|
|
||||||
# https://github.com/GyanD/codexffmpeg/releases/
|
|
||||||
FFMPEG_CONFIGURATION = {
|
FFMPEG_CONFIGURATION = {
|
||||||
'windows': {
|
'windows': {
|
||||||
'base_dir': lambda home: os.path.join(os.path.splitdrive(home)[0] + os.path.sep, 'binary'),
|
'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',
|
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-win32-{arch}.gz',
|
||||||
'file_extension': '.zip',
|
'file_extension': '.gz',
|
||||||
'executables': ['ffmpeg.exe', 'ffprobe.exe', 'ffplay.exe']
|
'executables': ['ffmpeg-win32-{arch}', 'ffprobe-win32-{arch}']
|
||||||
},
|
},
|
||||||
'darwin': {
|
'darwin': {
|
||||||
'base_dir': lambda home: os.path.join(home, 'Applications', 'binary'),
|
'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',
|
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-darwin-{arch}.gz',
|
||||||
'file_extension': '.zip',
|
'file_extension': '.gz',
|
||||||
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
'executables': ['ffmpeg-darwin-{arch}', 'ffprobe-darwin-{arch}']
|
||||||
},
|
},
|
||||||
'linux': {
|
'linux': {
|
||||||
'base_dir': lambda home: os.path.join(home, '.local', 'bin', 'binary'),
|
'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',
|
'download_url': 'https://github.com/eugeneware/ffmpeg-static/releases/latest/download/ffmpeg-linux-{arch}.gz',
|
||||||
'file_extension': '.tar.xz',
|
'file_extension': '.gz',
|
||||||
'executables': ['ffmpeg', 'ffprobe', 'ffplay']
|
'executables': ['ffmpeg-linux-{arch}', 'ffprobe-linux-{arch}']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class FFMPEGDownloader:
|
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):
|
def __init__(self):
|
||||||
self.os_name = self._detect_system()
|
self.os_name = self._detect_system()
|
||||||
self.arch = self._detect_arch()
|
self.arch = self._detect_arch()
|
||||||
@ -70,9 +57,6 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Normalized operating system name ('windows', 'darwin', or 'linux')
|
str: Normalized operating system name ('windows', 'darwin', or 'linux')
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the operating system is not supported
|
|
||||||
"""
|
"""
|
||||||
system = platform.system().lower()
|
system = platform.system().lower()
|
||||||
if system in FFMPEG_CONFIGURATION:
|
if system in FFMPEG_CONFIGURATION:
|
||||||
@ -85,18 +69,17 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Normalized architecture name (e.g., 'x86_64', 'arm64')
|
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()
|
machine = platform.machine().lower()
|
||||||
arch_map = {
|
arch_map = {
|
||||||
'amd64': 'x86_64',
|
'amd64': 'x64',
|
||||||
'x86_64': 'x86_64',
|
'x86_64': 'x64',
|
||||||
'x64': 'x86_64',
|
'x64': 'x64',
|
||||||
'arm64': 'arm64',
|
'arm64': 'arm64',
|
||||||
'aarch64': 'arm64'
|
'aarch64': 'arm64',
|
||||||
|
'armv7l': 'arm',
|
||||||
|
'i386': 'ia32',
|
||||||
|
'i686': 'ia32'
|
||||||
}
|
}
|
||||||
return arch_map.get(machine, machine)
|
return arch_map.get(machine, machine)
|
||||||
|
|
||||||
@ -106,11 +89,6 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Path to the base directory
|
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)
|
base_dir = FFMPEG_CONFIGURATION[self.os_name]['base_dir'](self.home_dir)
|
||||||
os.makedirs(base_dir, exist_ok=True)
|
os.makedirs(base_dir, exist_ok=True)
|
||||||
@ -157,9 +135,6 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Optional[str]: The latest version string, or None if retrieval fails.
|
Optional[str]: The latest version string, or None if retrieval fails.
|
||||||
|
|
||||||
Raises:
|
|
||||||
requests.exceptions.RequestException: If there are network-related errors.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Use GitHub API to fetch the latest release
|
# Use GitHub API to fetch the latest release
|
||||||
@ -184,10 +159,6 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: True if download was successful, False otherwise.
|
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:
|
try:
|
||||||
response = requests.get(url, stream=True)
|
response = requests.get(url, stream=True)
|
||||||
@ -233,6 +204,13 @@ class FFMPEGDownloader:
|
|||||||
elif archive_path.endswith('.tar.xz'):
|
elif archive_path.endswith('.tar.xz'):
|
||||||
with tarfile.open(archive_path) as tar_ref:
|
with tarfile.open(archive_path) as tar_ref:
|
||||||
tar_ref.extractall(extraction_path)
|
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]
|
config = FFMPEG_CONFIGURATION[self.os_name]
|
||||||
executables = config['executables']
|
executables = config['executables']
|
||||||
@ -240,7 +218,6 @@ class FFMPEGDownloader:
|
|||||||
|
|
||||||
for executable in executables:
|
for executable in executables:
|
||||||
exe_paths = glob.glob(os.path.join(extraction_path, '**', executable), recursive=True)
|
exe_paths = glob.glob(os.path.join(extraction_path, '**', executable), recursive=True)
|
||||||
|
|
||||||
if exe_paths:
|
if exe_paths:
|
||||||
dest_path = os.path.join(self.base_dir, executable)
|
dest_path = os.path.join(self.base_dir, executable)
|
||||||
shutil.copy2(exe_paths[0], dest_path)
|
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.
|
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.
|
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]
|
config = FFMPEG_CONFIGURATION[self.os_name]
|
||||||
download_url = config['download_url'].format(
|
executables = [exe.format(arch=self.arch) for exe in config['executables']]
|
||||||
version=version,
|
|
||||||
arch=self.arch
|
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]]:
|
def check_ffmpeg() -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user