@@ -15369,48 +15369,69 @@ def start_remote(self) -> None:
1536915369 self.web_thread.start()
1537015370 self.web_running = True
1537115371
15372- def download_ffmpeg(self, x) -> None:
15372+ def download_ffmpeg(self, _x) -> None:
15373+ """Download FFmpeg portable binary to User Data dir, supports x64 Windows and Linux"""
1537315374 def go() -> None:
15374- url = "https://github.com/GyanD/codexffmpeg/releases/download/7.1.1/ffmpeg-7.1.1-essentials_build.zip"
15375- sha = "04861d3339c5ebe38b56c19a15cf2c0cc97f5de4fa8910e4d47e5e6404e4a2d4"
15375+ if self.windows:
15376+ url = "https://github.com/GyanD/codexffmpeg/releases/download/8.0.1/ffmpeg-8.0.1-essentials_build.zip"
15377+ sha = "e2aaeaa0fdbc397d4794828086424d4aaa2102cef1fb6874f6ffd29c0b88b673"
15378+ elif not self.macos:
15379+ url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-03-02-13-06/ffmpeg-n8.0.1-66-g27b8d1a017-linux64-gpl-8.0.tar.xz"
15380+ sha = "e6f31af6c1ef49ceec8361185d70283ea8ad6c8ceed652d5dee66ec174a11eea"
1537615381 self.show_message(_("Starting download..."))
15377- try:
15378- f = io.BytesIO()
15379- with requests.get(url, stream=True, timeout=1800) as r: # ffmpeg is 92MB, give it half an hour in case someone is willing to suffer it on a slow connection
15380- dl = 0
15381- total_bytes = int(r.headers.get("Content-Length", 0))
15382- total_mb = round(total_bytes / 1000 / 1000) if total_bytes else 92
15383-
15384- for data in r.iter_content(chunk_size=4096):
15385- dl += len(data)
15386- f.write(data)
15387- mb = round(dl / 1000 / 1000)
15388- if mb % 5 == 0:
15389- self.show_message(_("Downloading... {MB}/{total_mb}").format(MB=mb, total_mb=total_mb))
15390- except Exception as e:
15391- logging.exception("Download failed")
15392- self.show_message(_("Download failed"), str(e), mode="error")
15393- return
15382+ with io.BytesIO() as f:
15383+ try:
15384+ with requests.get(url, stream=True, timeout=1800) as r: # Windows ffmpeg is 101MB, Linux 130MB, give it half an hour in case someone is willing to suffer it on a slow connection
15385+ dl = 0
15386+ total_bytes = int(r.headers.get("Content-Length", 0))
15387+ if self.windows:
15388+ fallback_mb = 101
15389+ elif not self.macos:
15390+ fallback_mb = 130
15391+ total_mb = round(total_bytes / 1000 / 1000) if total_bytes else fallback_mb
15392+
15393+ for data in r.iter_content(chunk_size=4096):
15394+ dl += len(data)
15395+ f.write(data)
15396+ mb = round(dl / 1000 / 1000)
15397+ if mb % 5 == 0:
15398+ self.show_message(_("Downloading... {MB}/{total_mb}").format(MB=mb, total_mb=total_mb))
15399+ except Exception as e:
15400+ logging.exception("Download failed")
15401+ self.show_message(_("Download failed"), str(e), mode="error")
15402+ return
1539415403
15395- f.seek(0)
15396- checksum = hashlib.sha256(f.read()).hexdigest()
15397- if checksum != sha:
15398- self.show_message(_("Download completed but checksum failed"), mode="error")
15399- logging.error(f"Checksum was {checksum} but expected {sha}")
15400- return
15401- self.show_message(_("Download completed.. extracting"))
15402- f.seek(0)
15403- z = zipfile.ZipFile(f, mode="r")
15404- exe = z.open("ffmpeg-7.1.1-essentials_build/bin/ffmpeg.exe")
15405- with (self.user_directory / "ffmpeg.exe").open("wb") as file:
15406- file.write(exe.read())
15404+ f.seek(0)
15405+ checksum = hashlib.sha256(f.read()).hexdigest()
15406+ if checksum != sha:
15407+ self.show_message(_("Download completed but checksum failed"), mode="error")
15408+ logging.error(f"Checksum was {checksum} but expected {sha}")
15409+ return
15410+ self.show_message(_("Download completed.. extracting"))
15411+ f.seek(0)
15412+ if self.windows:
15413+ z = zipfile.ZipFile(f, mode="r")
15414+ with z.open("ffmpeg-8.0.1-essentials_build/bin/ffmpeg.exe") as exe, (self.user_directory / "ffmpeg.exe").open("wb") as file:
15415+ file.write(exe.read())
15416+
15417+ with z.open("ffmpeg-8.0.1-essentials_build/bin/ffprobe.exe") as exe, (self.user_directory / "ffprobe.exe").open("wb") as file:
15418+ file.write(exe.read())
15419+ elif not self.macos:
15420+ import tarfile
15421+
15422+ output_dir = self.user_directory
15423+ wanted = {"ffmpeg", "ffprobe"}
1540715424
15408- exe = z .open("ffmpeg-7.1.1-essentials_build/bin/ffprobe.exe")
15409- with (self.user_directory / "ffprobe.exe").open("wb") as file :
15410- file.write(exe.read())
15425+ with tarfile .open(fileobj=f, mode="r:xz") as tar:
15426+ for member in tar.getmembers() :
15427+ filename = Path(member.name).name
1541115428
15412- exe.close()
15413- self.show_message(_("FFMPEG fetch complete"), mode="done")
15429+ if filename in wanted and member.isfile():
15430+ member.name = filename # strip directory structure
15431+ tar.extract(member, path=output_dir)
15432+
15433+
15434+ self.show_message(_("FFmpeg fetch complete"), mode="done")
1541415435
1541515436 shooter(go)
1541615437
@@ -19068,13 +19089,13 @@ def get_tray_icon(self, name: str) -> str:
1906819089 def test_ffmpeg(self) -> bool:
1906919090 if self.get_ffmpeg():
1907019091 return True
19071- if self.windows :
19072- self.show_message(_("This feature requires FFMPEG . Shall I can download that for you? (92MB )"), mode="confirm")
19092+ if not self.macos :
19093+ self.show_message(_("This feature requires FFmpeg . Shall I can download that for you? (100MB~ )"), mode="confirm")
1907319094 self.gui.message_box_confirm_callback = self.download_ffmpeg
1907419095 self.gui.message_box_no_callback = None
1907519096 self.gui.message_box_confirm_reference = (None,)
1907619097 else:
19077- self.show_message(_("FFMPEG could not be found"))
19098+ self.show_message(_("FFmpeg could not be found"))
1907819099 return False
1907919100
1908019101 def get_ffmpeg(self) -> Path | None:
@@ -19087,6 +19108,11 @@ def get_ffmpeg(self) -> Path | None:
1908719108 if path.is_file():
1908819109 return path
1908919110
19111+ # Portable Linux
19112+ path = self.user_directory / "ffmpeg"
19113+ if path.is_file():
19114+ return path
19115+
1909019116 path = shutil.which("ffmpeg")
1909119117 if path:
1909219118 return Path(path)
@@ -19102,6 +19128,11 @@ def get_ffprobe(self) -> Path | None:
1910219128 if path.is_file():
1910319129 return path
1910419130
19131+ # Portable Linux
19132+ path = self.user_directory / "ffprobe"
19133+ if path.is_file():
19134+ return path
19135+
1910519136 path = shutil.which("ffprobe")
1910619137 if path:
1910719138 return Path(path)
@@ -40049,7 +40080,7 @@ def load_prefs(bag: Bag) -> None:
4004940080 "Cache files from local sources too. (Useful for mounted network drives)")
4005040081 prefs.always_ffmpeg = cf.sync_add(
4005140082 "bool", "always-ffmpeg", prefs.always_ffmpeg,
40052- "Prefer decoding using FFMPEG . Fixes stuttering on Raspberry Pi OS.")
40083+ "Prefer decoding using FFmpeg . Fixes stuttering on Raspberry Pi OS.")
4005340084 prefs.volume_power = cf.sync_add(
4005440085 "int", "volume-curve", prefs.volume_power,
4005540086 "1=Linear volume control. Values above one give greater control bias over lower volume range. Default: 2")
0 commit comments