portable-ffmpeg 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- portable_ffmpeg/__init__.py +29 -0
- portable_ffmpeg/config.py +106 -0
- portable_ffmpeg/core.py +149 -0
- portable_ffmpeg/downloaders.py +155 -0
- portable_ffmpeg/enums.py +54 -0
- portable_ffmpeg/py.typed +0 -0
- portable_ffmpeg-0.1.0.dist-info/METADATA +168 -0
- portable_ffmpeg-0.1.0.dist-info/RECORD +10 -0
- portable_ffmpeg-0.1.0.dist-info/WHEEL +4 -0
- portable_ffmpeg-0.1.0.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Static FFmpeg binaries for Windows, macOS, and Linux.
|
|
2
|
+
|
|
3
|
+
This package automatically downloads and manages static FFmpeg binaries
|
|
4
|
+
for cross-platform use without requiring system installation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core import (
|
|
8
|
+
add_to_path,
|
|
9
|
+
clear_cache,
|
|
10
|
+
get_ffmpeg,
|
|
11
|
+
print_paths,
|
|
12
|
+
remove_from_path,
|
|
13
|
+
run_ffmpeg,
|
|
14
|
+
run_ffprobe,
|
|
15
|
+
)
|
|
16
|
+
from .enums import Architectures, FFmpegVersions, OperatingSystems
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"Architectures",
|
|
20
|
+
"FFmpegVersions",
|
|
21
|
+
"OperatingSystems",
|
|
22
|
+
"add_to_path",
|
|
23
|
+
"clear_cache",
|
|
24
|
+
"get_ffmpeg",
|
|
25
|
+
"print_paths",
|
|
26
|
+
"remove_from_path",
|
|
27
|
+
"run_ffmpeg",
|
|
28
|
+
"run_ffprobe",
|
|
29
|
+
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Configuration for FFmpeg download URLs by platform and architecture."""
|
|
2
|
+
|
|
3
|
+
from .downloaders import (
|
|
4
|
+
BaseFFmpegDownloader,
|
|
5
|
+
FFmpegDownloadSingleTar,
|
|
6
|
+
FFmpegDownloadSingleZip,
|
|
7
|
+
FFmpegDownloadTwoZips,
|
|
8
|
+
)
|
|
9
|
+
from .enums import Architectures, FFmpegVersions, OperatingSystems
|
|
10
|
+
|
|
11
|
+
# Download URLs for different platforms, architectures, and versions
|
|
12
|
+
DOWNLOAD_URLS: dict[
|
|
13
|
+
OperatingSystems,
|
|
14
|
+
dict[Architectures, dict[FFmpegVersions, BaseFFmpegDownloader]],
|
|
15
|
+
] = {
|
|
16
|
+
OperatingSystems.WINDOWS: {
|
|
17
|
+
Architectures.AMD64: {
|
|
18
|
+
FFmpegVersions.LATEST: FFmpegDownloadSingleZip(
|
|
19
|
+
url="https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip",
|
|
20
|
+
ffmpeg_name="ffmpeg.exe",
|
|
21
|
+
ffprobe_name="ffprobe.exe",
|
|
22
|
+
),
|
|
23
|
+
FFmpegVersions.V7: FFmpegDownloadSingleZip(
|
|
24
|
+
url="https://www.gyan.dev/ffmpeg/builds/packages/ffmpeg-7.1.1-essentials_build.zip",
|
|
25
|
+
ffmpeg_name="ffmpeg.exe",
|
|
26
|
+
ffprobe_name="ffprobe.exe",
|
|
27
|
+
),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
OperatingSystems.OSX: {
|
|
31
|
+
Architectures.AMD64: {
|
|
32
|
+
FFmpegVersions.LATEST: FFmpegDownloadTwoZips(
|
|
33
|
+
ffmpeg_url="https://www.osxexperts.net/ffmpeg71intel.zip",
|
|
34
|
+
ffprobe_url="https://www.osxexperts.net/ffprobe71intel.zip",
|
|
35
|
+
ffmpeg_name="ffmpeg",
|
|
36
|
+
ffprobe_name="ffprobe",
|
|
37
|
+
),
|
|
38
|
+
FFmpegVersions.V7: FFmpegDownloadTwoZips(
|
|
39
|
+
ffmpeg_url="https://www.osxexperts.net/ffmpeg71intel.zip",
|
|
40
|
+
ffprobe_url="https://www.osxexperts.net/ffprobe71intel.zip",
|
|
41
|
+
ffmpeg_name="ffmpeg",
|
|
42
|
+
ffprobe_name="ffprobe",
|
|
43
|
+
),
|
|
44
|
+
},
|
|
45
|
+
Architectures.ARM64: {
|
|
46
|
+
FFmpegVersions.LATEST: FFmpegDownloadTwoZips(
|
|
47
|
+
ffmpeg_url="https://www.osxexperts.net/ffmpeg711arm.zip",
|
|
48
|
+
ffprobe_url="https://www.osxexperts.net/ffprobe711arm.zip",
|
|
49
|
+
ffmpeg_name="ffmpeg",
|
|
50
|
+
ffprobe_name="ffprobe",
|
|
51
|
+
),
|
|
52
|
+
FFmpegVersions.V7: FFmpegDownloadTwoZips(
|
|
53
|
+
ffmpeg_url="https://www.osxexperts.net/ffmpeg711arm.zip",
|
|
54
|
+
ffprobe_url="https://www.osxexperts.net/ffprobe711arm.zip",
|
|
55
|
+
ffmpeg_name="ffmpeg",
|
|
56
|
+
ffprobe_name="ffprobe",
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
OperatingSystems.LINUX: {
|
|
61
|
+
Architectures.AMD64: {
|
|
62
|
+
FFmpegVersions.LATEST: FFmpegDownloadSingleTar(
|
|
63
|
+
url="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
|
|
64
|
+
ffmpeg_name="ffmpeg",
|
|
65
|
+
ffprobe_name="ffprobe",
|
|
66
|
+
),
|
|
67
|
+
FFmpegVersions.V7: FFmpegDownloadSingleTar(
|
|
68
|
+
url="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz",
|
|
69
|
+
ffmpeg_name="ffmpeg",
|
|
70
|
+
ffprobe_name="ffprobe",
|
|
71
|
+
),
|
|
72
|
+
FFmpegVersions.V6: FFmpegDownloadSingleTar(
|
|
73
|
+
url="https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-6.0.1-amd64-static.tar.xz",
|
|
74
|
+
ffmpeg_name="ffmpeg",
|
|
75
|
+
ffprobe_name="ffprobe",
|
|
76
|
+
),
|
|
77
|
+
FFmpegVersions.V5: FFmpegDownloadSingleTar(
|
|
78
|
+
url="https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-amd64-static.tar.xz",
|
|
79
|
+
ffmpeg_name="ffmpeg",
|
|
80
|
+
ffprobe_name="ffprobe",
|
|
81
|
+
),
|
|
82
|
+
},
|
|
83
|
+
Architectures.ARM64: {
|
|
84
|
+
FFmpegVersions.LATEST: FFmpegDownloadSingleTar(
|
|
85
|
+
url="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz",
|
|
86
|
+
ffmpeg_name="ffmpeg",
|
|
87
|
+
ffprobe_name="ffprobe",
|
|
88
|
+
),
|
|
89
|
+
FFmpegVersions.V7: FFmpegDownloadSingleTar(
|
|
90
|
+
url="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz",
|
|
91
|
+
ffmpeg_name="ffmpeg",
|
|
92
|
+
ffprobe_name="ffprobe",
|
|
93
|
+
),
|
|
94
|
+
FFmpegVersions.V6: FFmpegDownloadSingleTar(
|
|
95
|
+
url="https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-6.0.1-arm64-static.tar.xz",
|
|
96
|
+
ffmpeg_name="ffmpeg",
|
|
97
|
+
ffprobe_name="ffprobe",
|
|
98
|
+
),
|
|
99
|
+
FFmpegVersions.V5: FFmpegDownloadSingleTar(
|
|
100
|
+
url="https://johnvansickle.com/ffmpeg/old-releases/ffmpeg-5.1.1-arm64-static.tar.xz",
|
|
101
|
+
ffmpeg_name="ffmpeg",
|
|
102
|
+
ffprobe_name="ffprobe",
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
}
|
portable_ffmpeg/core.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Core functionality for managing FFmpeg binaries."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import threading
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .config import DOWNLOAD_URLS
|
|
11
|
+
from .enums import Architectures, FFmpegVersions, OperatingSystems
|
|
12
|
+
|
|
13
|
+
CACHE_DIR = Path(__file__).parent / "binaries"
|
|
14
|
+
_download_lock = threading.Lock()
|
|
15
|
+
_path_lock = threading.Lock()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_ffmpeg(version: FFmpegVersions = FFmpegVersions.LATEST) -> tuple[Path, Path]:
|
|
19
|
+
"""Download and return paths to static ffmpeg and ffprobe binaries.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
version: FFmpeg major version to download (default: latest).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Tuple containing paths to ffmpeg and ffprobe binaries.
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
system = OperatingSystems.from_current_system()
|
|
29
|
+
arch = Architectures.from_current_architecture()
|
|
30
|
+
|
|
31
|
+
# Create cache directory inside the package
|
|
32
|
+
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Get platform-specific configuration
|
|
35
|
+
if system not in DOWNLOAD_URLS:
|
|
36
|
+
msg = f"Unsupported operating system: {system}"
|
|
37
|
+
raise ValueError(msg)
|
|
38
|
+
|
|
39
|
+
if arch not in DOWNLOAD_URLS[system]:
|
|
40
|
+
msg = f"Unsupported {system} architecture: {arch}"
|
|
41
|
+
raise ValueError(msg)
|
|
42
|
+
|
|
43
|
+
if version not in DOWNLOAD_URLS[system][arch]:
|
|
44
|
+
msg = f"Unsupported FFmpeg version {version} for {system} {arch}"
|
|
45
|
+
raise ValueError(msg)
|
|
46
|
+
|
|
47
|
+
config = DOWNLOAD_URLS[system][arch][version]
|
|
48
|
+
|
|
49
|
+
# Check if binaries already exist in cache
|
|
50
|
+
cache_subdir = f"{system.value}-{arch.value}-{version.value}"
|
|
51
|
+
platform_cache_dir = CACHE_DIR / cache_subdir
|
|
52
|
+
ffmpeg_path = platform_cache_dir / config.ffmpeg_name
|
|
53
|
+
ffprobe_path = platform_cache_dir / config.ffprobe_name
|
|
54
|
+
|
|
55
|
+
with _download_lock:
|
|
56
|
+
if not ffmpeg_path.exists() or not ffprobe_path.exists():
|
|
57
|
+
# Use lock to prevent concurrent downloads
|
|
58
|
+
print(f"Downloading FFmpeg {version.value} binaries for {system} {arch}...")
|
|
59
|
+
# Create platform-specific cache directory
|
|
60
|
+
|
|
61
|
+
# Handle corrupted cache (file exists where directory should be)
|
|
62
|
+
if platform_cache_dir.exists() and not platform_cache_dir.is_dir():
|
|
63
|
+
platform_cache_dir.unlink()
|
|
64
|
+
|
|
65
|
+
platform_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
# Download and extract binaries using dataclass method
|
|
68
|
+
ffmpeg_path, ffprobe_path = config.download_files(platform_cache_dir)
|
|
69
|
+
|
|
70
|
+
return ffmpeg_path, ffprobe_path
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def clear_cache() -> None:
|
|
74
|
+
"""Clear the cached FFmpeg binaries.
|
|
75
|
+
|
|
76
|
+
This removes all downloaded binaries from the cache directory,
|
|
77
|
+
forcing them to be re-downloaded on the next call to get_ffmpeg().
|
|
78
|
+
"""
|
|
79
|
+
if CACHE_DIR.exists():
|
|
80
|
+
shutil.rmtree(CACHE_DIR)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def add_to_path(*, weak: bool = False, version: FFmpegVersions = FFmpegVersions.LATEST) -> None:
|
|
84
|
+
"""Add FFmpeg binaries to system PATH.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
weak: If True, only add to PATH if ffmpeg is not already available.
|
|
88
|
+
If False, always add to PATH (taking precedence).
|
|
89
|
+
version: FFmpeg major version to use (default: latest).
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
with _path_lock:
|
|
93
|
+
if weak and shutil.which("ffmpeg") is not None:
|
|
94
|
+
return # ffmpeg already available, don't override
|
|
95
|
+
|
|
96
|
+
ffmpeg_path, _ = get_ffmpeg(version)
|
|
97
|
+
bin_dir = str(ffmpeg_path.parent)
|
|
98
|
+
|
|
99
|
+
current_path = os.environ.get("PATH", "")
|
|
100
|
+
if bin_dir not in current_path:
|
|
101
|
+
os.environ["PATH"] = f"{bin_dir}{os.pathsep}{current_path}"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def remove_from_path(version: FFmpegVersions = FFmpegVersions.LATEST) -> None:
|
|
105
|
+
"""Remove FFmpeg binaries from system PATH.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
version: FFmpeg major version to use (default: latest).
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
with _path_lock:
|
|
112
|
+
ffmpeg_path, _ = get_ffmpeg(version)
|
|
113
|
+
bin_dir = str(ffmpeg_path.parent)
|
|
114
|
+
|
|
115
|
+
current_path = os.environ.get("PATH", "")
|
|
116
|
+
path_parts = current_path.split(os.pathsep)
|
|
117
|
+
|
|
118
|
+
# Remove our binary directory from PATH
|
|
119
|
+
new_path_parts = [part for part in path_parts if part != bin_dir]
|
|
120
|
+
os.environ["PATH"] = os.pathsep.join(new_path_parts)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_ffmpeg(version: FFmpegVersions = FFmpegVersions.LATEST) -> None:
|
|
124
|
+
"""Entry point to run ffmpeg binary directly.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
version: FFmpeg major version to use (default: latest).
|
|
128
|
+
|
|
129
|
+
"""
|
|
130
|
+
ffmpeg_path, _ = get_ffmpeg(version)
|
|
131
|
+
subprocess.run([str(ffmpeg_path)] + sys.argv[1:], check=False)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_ffprobe(version: FFmpegVersions = FFmpegVersions.LATEST) -> None:
|
|
135
|
+
"""Entry point to run ffprobe binary directly.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
version: FFmpeg major version to use (default: latest).
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
_, ffprobe_path = get_ffmpeg(version)
|
|
142
|
+
subprocess.run([str(ffprobe_path)] + sys.argv[1:], check=False)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def print_paths() -> None:
|
|
146
|
+
"""Print the paths to the FFmpeg and FFprobe binaries."""
|
|
147
|
+
ffmpeg_path, ffprobe_path = get_ffmpeg()
|
|
148
|
+
print(f"FFmpeg: {ffmpeg_path}")
|
|
149
|
+
print(f"FFprobe: {ffprobe_path}")
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Download classes for different FFmpeg binary formats."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import tarfile
|
|
5
|
+
import tempfile
|
|
6
|
+
import urllib.request
|
|
7
|
+
import zipfile
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _download_file(url: str, file_path: Path | str) -> None:
|
|
14
|
+
"""Download a file from a URL."""
|
|
15
|
+
print(f"🔽 Downloading from {url}")
|
|
16
|
+
last_value = 0
|
|
17
|
+
|
|
18
|
+
def reporthook(block_num: int, block_size: int, total_size: int) -> None:
|
|
19
|
+
nonlocal last_value
|
|
20
|
+
if total_size > 0:
|
|
21
|
+
downloaded = block_num * block_size
|
|
22
|
+
percent = downloaded * 100 // total_size
|
|
23
|
+
if percent != last_value:
|
|
24
|
+
last_value = percent
|
|
25
|
+
if percent == 100:
|
|
26
|
+
sys.stdout.write("\r✅ Download complete!\n")
|
|
27
|
+
else:
|
|
28
|
+
sys.stdout.write(f"\r🔽 {percent:3d}%")
|
|
29
|
+
sys.stdout.flush()
|
|
30
|
+
else:
|
|
31
|
+
total = block_num * block_size
|
|
32
|
+
if last_value != total:
|
|
33
|
+
last_value = total
|
|
34
|
+
sys.stdout.write(f"\r🔽 {block_num * block_size} bytes")
|
|
35
|
+
sys.stdout.flush()
|
|
36
|
+
|
|
37
|
+
# Kick off download
|
|
38
|
+
urllib.request.urlretrieve(url, file_path, reporthook)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _extract_zip_files(zip_file: Path, outfolder: Path, target_names: list[str]) -> list[Path]:
|
|
42
|
+
"""Extract specific files from a zip archive."""
|
|
43
|
+
extracted_files = []
|
|
44
|
+
with zipfile.ZipFile(zip_file, "r") as zip_ref:
|
|
45
|
+
for member in zip_ref.namelist():
|
|
46
|
+
filename = Path(member).name
|
|
47
|
+
if filename in target_names:
|
|
48
|
+
# Extract the file to outfolder with just the filename
|
|
49
|
+
member_data = zip_ref.read(member)
|
|
50
|
+
output_path = outfolder / filename
|
|
51
|
+
output_path.write_bytes(member_data)
|
|
52
|
+
# Set executable permissions on Unix systems
|
|
53
|
+
if sys.platform != "win32":
|
|
54
|
+
output_path.chmod(0o755)
|
|
55
|
+
extracted_files.append(output_path)
|
|
56
|
+
return extracted_files
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _extract_tar_files(tar_file: Path, outfolder: Path, target_names: list[str]) -> list[Path]:
|
|
60
|
+
"""Extract specific files from a tar archive."""
|
|
61
|
+
extracted_files = []
|
|
62
|
+
with tarfile.open(tar_file, "r:*") as tar_ref:
|
|
63
|
+
for member in tar_ref.getmembers():
|
|
64
|
+
if member.isfile():
|
|
65
|
+
filename = Path(member.name).name
|
|
66
|
+
if filename in target_names:
|
|
67
|
+
# Extract the file to outfolder with just the filename
|
|
68
|
+
extracted_file = tar_ref.extractfile(member)
|
|
69
|
+
if extracted_file:
|
|
70
|
+
output_path = outfolder / filename
|
|
71
|
+
output_path.write_bytes(extracted_file.read())
|
|
72
|
+
# Set executable permissions on Unix systems
|
|
73
|
+
if sys.platform != "win32":
|
|
74
|
+
output_path.chmod(0o755)
|
|
75
|
+
extracted_files.append(output_path)
|
|
76
|
+
return extracted_files
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass
|
|
80
|
+
class BaseFFmpegDownloader(ABC):
|
|
81
|
+
"""Base class for downloading FFmpeg binaries."""
|
|
82
|
+
|
|
83
|
+
ffmpeg_name: str
|
|
84
|
+
ffprobe_name: str
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def download_files(self, outfolder: Path) -> tuple[Path, Path]:
|
|
88
|
+
"""Download and extract the FFmpeg and FFprobe binaries."""
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class FFmpegDownloadSingleZip(BaseFFmpegDownloader):
|
|
93
|
+
"""Configuration for downloading ffmpeg and ffprobe binaries."""
|
|
94
|
+
|
|
95
|
+
url: str
|
|
96
|
+
|
|
97
|
+
def download_files(self, outfolder: Path) -> tuple[Path, Path]:
|
|
98
|
+
"""Download and extract zip archive containing both binaries."""
|
|
99
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
100
|
+
tmp_file = Path(tmp_dir) / "download.zip"
|
|
101
|
+
_download_file(self.url, tmp_file)
|
|
102
|
+
|
|
103
|
+
_extract_zip_files(
|
|
104
|
+
zip_file=tmp_file,
|
|
105
|
+
outfolder=outfolder,
|
|
106
|
+
target_names=[self.ffmpeg_name, self.ffprobe_name],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return outfolder / self.ffmpeg_name, outfolder / self.ffprobe_name
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass
|
|
113
|
+
class FFmpegDownloadSingleTar(BaseFFmpegDownloader):
|
|
114
|
+
"""Configuration for downloading ffmpeg and ffprobe binaries as a single tar archive."""
|
|
115
|
+
|
|
116
|
+
url: str
|
|
117
|
+
|
|
118
|
+
def download_files(self, outfolder: Path) -> tuple[Path, Path]:
|
|
119
|
+
"""Download and extract tar archive containing both binaries."""
|
|
120
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
121
|
+
tmp_file = Path(tmp_dir) / "download.tar.xz"
|
|
122
|
+
_download_file(self.url, tmp_file)
|
|
123
|
+
|
|
124
|
+
_extract_tar_files(
|
|
125
|
+
tar_file=tmp_file,
|
|
126
|
+
outfolder=outfolder,
|
|
127
|
+
target_names=[self.ffmpeg_name, self.ffprobe_name],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return outfolder / self.ffmpeg_name, outfolder / self.ffprobe_name
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class FFmpegDownloadTwoZips(BaseFFmpegDownloader):
|
|
135
|
+
"""Configuration for downloading ffmpeg and ffprobe binaries as separate zip files."""
|
|
136
|
+
|
|
137
|
+
ffmpeg_url: str
|
|
138
|
+
ffprobe_url: str
|
|
139
|
+
|
|
140
|
+
def download_files(self, outfolder: Path) -> tuple[Path, Path]:
|
|
141
|
+
"""Download and extract two separate zip files for ffmpeg and ffprobe."""
|
|
142
|
+
target_binaries = {self.ffmpeg_url: self.ffmpeg_name, self.ffprobe_url: self.ffprobe_name}
|
|
143
|
+
|
|
144
|
+
for url, target_name in target_binaries.items():
|
|
145
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
146
|
+
tmp_file = Path(tmp_dir) / f"{target_name}.zip"
|
|
147
|
+
_download_file(url, tmp_file)
|
|
148
|
+
|
|
149
|
+
_extract_zip_files(
|
|
150
|
+
zip_file=tmp_file,
|
|
151
|
+
outfolder=outfolder,
|
|
152
|
+
target_names=[target_name],
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return outfolder / self.ffmpeg_name, outfolder / self.ffprobe_name
|
portable_ffmpeg/enums.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Operating system and architecture enums for FFmpeg binary selection."""
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OperatingSystems(Enum):
|
|
8
|
+
"""Supported operating systems for ffmpeg binaries."""
|
|
9
|
+
|
|
10
|
+
WINDOWS = "windows"
|
|
11
|
+
OSX = "osx"
|
|
12
|
+
LINUX = "linux"
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_current_system(cls) -> "OperatingSystems":
|
|
16
|
+
"""Get the current operating system."""
|
|
17
|
+
system = platform.system().lower()
|
|
18
|
+
if system == "windows":
|
|
19
|
+
return cls.WINDOWS
|
|
20
|
+
if system == "darwin":
|
|
21
|
+
return cls.OSX
|
|
22
|
+
if system == "linux":
|
|
23
|
+
return cls.LINUX
|
|
24
|
+
|
|
25
|
+
msg = f"Unsupported operating system: {system}"
|
|
26
|
+
raise ValueError(msg)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Architectures(Enum):
|
|
30
|
+
"""Supported architectures for ffmpeg binaries."""
|
|
31
|
+
|
|
32
|
+
AMD64 = "amd64"
|
|
33
|
+
ARM64 = "arm64"
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_current_architecture(cls) -> "Architectures":
|
|
37
|
+
"""Get the current architecture."""
|
|
38
|
+
machine = platform.machine().lower()
|
|
39
|
+
if machine in ["x86_64", "amd64"]:
|
|
40
|
+
return cls.AMD64
|
|
41
|
+
if machine in ["aarch64", "arm64"]:
|
|
42
|
+
return cls.ARM64
|
|
43
|
+
|
|
44
|
+
msg = f"Unsupported architecture: {machine}"
|
|
45
|
+
raise ValueError(msg)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FFmpegVersions(Enum):
|
|
49
|
+
"""Supported FFmpeg major versions."""
|
|
50
|
+
|
|
51
|
+
LATEST = "latest"
|
|
52
|
+
V7 = "7"
|
|
53
|
+
V6 = "6"
|
|
54
|
+
V5 = "5"
|
portable_ffmpeg/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: portable-ffmpeg
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Downloads static ffmpeg builds for Windows, macOS, and Linux
|
|
5
|
+
Project-URL: Homepage, https://github.com/mrkbac/portable-ffmpeg
|
|
6
|
+
Project-URL: Repository, https://github.com/mrkbac/portable-ffmpeg
|
|
7
|
+
Project-URL: Issues, https://github.com/mrkbac/portable-ffmpeg/issues
|
|
8
|
+
Author-email: Marko Bausch <60338487+mrkbac@users.noreply.github.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: audio,ffmpeg,ffprobe,multimedia,portable,static,video
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
26
|
+
Requires-Dist: pyright; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.4.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# Portable FFmpeg
|
|
33
|
+
|
|
34
|
+
Downloads static ffmpeg builds for Windows, macOS, and Linux.
|
|
35
|
+
|
|
36
|
+
- Supports Windows, macOS, and Linux
|
|
37
|
+
- Supports x86_64 architecture on all platforms, ARM64 on macOS and Linux
|
|
38
|
+
- Automatic platform detection and binary caching
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Python API
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from portable_ffmpeg import get_ffmpeg
|
|
46
|
+
ffmpeg_path, ffprobe_path = get_ffmpeg()
|
|
47
|
+
print(ffmpeg_path) # Path to ffmpeg executable
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Command Line Interface
|
|
51
|
+
|
|
52
|
+
After installation, you can use `static_ffmpeg` and `static_ffprobe` directly from the command line:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Use static ffmpeg
|
|
56
|
+
static_ffmpeg -i input.mp4 output.mp3
|
|
57
|
+
|
|
58
|
+
# Use static ffprobe
|
|
59
|
+
static_ffprobe -v quiet -print_format json -show_format input.mp4
|
|
60
|
+
|
|
61
|
+
# Print paths to the binaries
|
|
62
|
+
print_paths
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The CLI commands automatically download and cache the appropriate static binaries for your platform.
|
|
66
|
+
|
|
67
|
+
### PATH Management
|
|
68
|
+
|
|
69
|
+
You can also programmatically manage your PATH:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from portable_ffmpeg import add_to_path, remove_from_path
|
|
73
|
+
|
|
74
|
+
# Add FFmpeg binaries to PATH
|
|
75
|
+
add_to_path()
|
|
76
|
+
|
|
77
|
+
# Add only if FFmpeg is not already available (weak mode)
|
|
78
|
+
add_to_path(weak=True)
|
|
79
|
+
|
|
80
|
+
# Remove from PATH
|
|
81
|
+
remove_from_path()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Sources of Static Builds
|
|
85
|
+
|
|
86
|
+
This package downloads static FFmpeg binaries from the following trusted sources:
|
|
87
|
+
|
|
88
|
+
### Windows (x86_64 only)
|
|
89
|
+
|
|
90
|
+
- **Source**: [CODEX FFMPEG @ gyan.dev](https://www.gyan.dev/ffmpeg/builds/)
|
|
91
|
+
- **URL**: `https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip`
|
|
92
|
+
- **Description**: Official Windows builds, 64-bit static binaries licensed as GPLv3
|
|
93
|
+
|
|
94
|
+
### macOS
|
|
95
|
+
|
|
96
|
+
#### Intel (x86_64)
|
|
97
|
+
|
|
98
|
+
- **Source**: [OSXExperts.net](http://www.osxexperts.net/)
|
|
99
|
+
- **URLs**:
|
|
100
|
+
- FFmpeg: `https://www.osxexperts.net/ffmpeg71intel.zip`
|
|
101
|
+
- FFprobe: `https://www.osxexperts.net/ffprobe71intel.zip`
|
|
102
|
+
- **Description**: Static FFmpeg 7.1 binaries for macOS Intel processors
|
|
103
|
+
|
|
104
|
+
#### Apple Silicon (arm64)
|
|
105
|
+
|
|
106
|
+
- **Source**: [OSXExperts.net](http://www.osxexperts.net/)
|
|
107
|
+
- **URLs**:
|
|
108
|
+
- FFmpeg: `https://www.osxexperts.net/ffmpeg711arm.zip`
|
|
109
|
+
- FFprobe: `https://www.osxexperts.net/ffprobe711arm.zip`
|
|
110
|
+
- **Description**: Static FFmpeg 7.1.1 binaries optimized for Apple Silicon processors
|
|
111
|
+
|
|
112
|
+
### Linux (x86_64 and arm64)
|
|
113
|
+
|
|
114
|
+
- **Source**: [John Van Sickle - FFmpeg Static Builds](https://johnvansickle.com/ffmpeg/)
|
|
115
|
+
- **URLs**:
|
|
116
|
+
- x86_64: `https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz`
|
|
117
|
+
- arm64: `https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz`
|
|
118
|
+
- **Description**: Latest versions of FFmpeg for Linux kernels 3.2.0+, statically linked
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
### Alternative Sources
|
|
122
|
+
|
|
123
|
+
The following sources provide FFmpeg static builds but are not currently supported by this package. They could be considered for future releases:
|
|
124
|
+
|
|
125
|
+
- **[FFmpeg Martin Riedl](https://ffmpeg.martin-riedl.de/)**
|
|
126
|
+
- **Platforms**: macOS, Linux
|
|
127
|
+
- **Architectures**: x86_64, ARM64
|
|
128
|
+
- **Description**: Regularly updated static builds with comprehensive codec support
|
|
129
|
+
|
|
130
|
+
- **[BtbN FFmpeg Builds](https://github.com/BtbN/FFmpeg-Builds)**
|
|
131
|
+
- **Platforms**: Windows, Linux
|
|
132
|
+
- **Architectures**: x86_64, ARM64, x86 (Windows only)
|
|
133
|
+
- **Description**: GitHub Actions-built binaries with automated releases
|
|
134
|
+
|
|
135
|
+
- **[EverMeet FFmpeg](https://evermeet.cx/ffmpeg/)**
|
|
136
|
+
- **Platforms**: macOS only
|
|
137
|
+
- **Architectures**: x86_64
|
|
138
|
+
- **Description**: Long-standing macOS static builds provider
|
|
139
|
+
|
|
140
|
+
- **[FreeBSD FFmpeg Static](https://github.com/Thefrank/ffmpeg-static-freebsd/)**
|
|
141
|
+
- **Platforms**: FreeBSD only
|
|
142
|
+
- **Architectures**: Various
|
|
143
|
+
- **Description**: Static builds specifically for FreeBSD systems
|
|
144
|
+
|
|
145
|
+
- **[John Van Sickle Extended](https://johnvansickle.com/ffmpeg/)**
|
|
146
|
+
- **Additional Architectures**: i686, armhf, armel (beyond the currently supported x86_64 and ARM64)
|
|
147
|
+
- **Description**: Comprehensive Linux support including older and embedded architectures
|
|
148
|
+
|
|
149
|
+
## Caching
|
|
150
|
+
|
|
151
|
+
Downloaded binaries are cached in the package's `binaries/` directory to avoid re-downloading. Each virtual environment gets its own copy of the binaries.
|
|
152
|
+
|
|
153
|
+
The cache is organized by platform and architecture (e.g., `linux-amd64`, `windows-amd64`, `osx-arm64`).
|
|
154
|
+
|
|
155
|
+
### Cache Management
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from portable_ffmpeg import clear_cache
|
|
159
|
+
|
|
160
|
+
# Clear all cached binaries
|
|
161
|
+
clear_cache()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Or use the command line:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
clear_cache
|
|
168
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
portable_ffmpeg/__init__.py,sha256=krQpv5t46LGAk9_lwbpGjViMbbnJkyaSPaeq-6TwUqs,626
|
|
2
|
+
portable_ffmpeg/config.py,sha256=jvLmGk0NWVzflZd8xgslIqWjU06bw7XUGmwqiYGM5AQ,4509
|
|
3
|
+
portable_ffmpeg/core.py,sha256=H6Q1XhgE1D87UwiuSSBZeg4J2t8EWN6juLl9cXcLyCc,4918
|
|
4
|
+
portable_ffmpeg/downloaders.py,sha256=a6NX9FPODujrtpCvZloTOqPv9Th32FqjaYeCgAH9z44,5706
|
|
5
|
+
portable_ffmpeg/enums.py,sha256=FxA4jboyEpBvtwsLAgRaWScToyMttPlyZCGwQZ2uYEY,1353
|
|
6
|
+
portable_ffmpeg/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
portable_ffmpeg-0.1.0.dist-info/METADATA,sha256=xlW8wlDUt4zWLgB-blONcOWAVv21rSxbtZCnkDUiSyE,5629
|
|
8
|
+
portable_ffmpeg-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
portable_ffmpeg-0.1.0.dist-info/entry_points.txt,sha256=fUJF_ovTj115pznpHnUAkHqY1kUuV2gniiNJqJGxTs4,190
|
|
10
|
+
portable_ffmpeg-0.1.0.dist-info/RECORD,,
|