From ebd7fba69d2b7c0117d36649bb388f339087a219 Mon Sep 17 00:00:00 2001 From: pawank925 Date: Fri, 6 Nov 2026 21:57:10 +0530 Subject: [PATCH 1/2] Add Webm medium preset and fix MP4/Webm encoding presets (#271) --- CHANGELOG.md | 10 ++++++ src/zimscraperlib/video/presets.py | 45 ++++++++++++++++++++---- tests/video/test_video.py | 56 ++++++++++++++++++++++++++---- 3 files changed, 98 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fc56646..2034c694 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `VideoWebmMedium` preset: VP9 at 240k with the height capped at 720, a middle ground between `VideoWebmLow` and `VideoWebmHigh` (#271) + +### Changed + +- `VideoMp4High` now encodes at CRF 28 instead of 20, which regularly produced a file larger than its input, and adds `-movflags +faststart` (#271) +- All Webm presets (`VideoWebmLow`, `VideoWebmMedium`, `VideoWebmHigh`) now use `-row-mt 1` for faster VP9 encoding (#271) +- Bumped `VideoWebmLow` to v4 and `VideoWebmHigh` to v3 following the `-row-mt` addition (#271) + ### Fixed - Fix JS rewriting of `this.` so it reliably detects a leading newline (and prepends the needed `;`) regardless of the amount of whitespace, and no longer wrongly rewrites `this` when preceded by `.`/`$` and several spaces (#381) diff --git a/src/zimscraperlib/video/presets.py b/src/zimscraperlib/video/presets.py index 09b7a007..a5cb61b8 100644 --- a/src/zimscraperlib/video/presets.py +++ b/src/zimscraperlib/video/presets.py @@ -33,7 +33,7 @@ class VideoWebmLow(Config): 128k target video bitrate but stay within quality boundaries. 48k audio bitrate""" - VERSION = 3 + VERSION = 4 ext = "webm" mimetype = f"{preset_type}/webm" @@ -47,6 +47,7 @@ class VideoWebmLow(Config): "-quality": "good", # codec preset "-speed": "4", # Encoding speed (compromise between quality and encoding time) "-vf": "scale='480:trunc(ow/a/2)*2'", # frame size + "-row-mt": "1", "-codec:a": "libvorbis", # audio codec "-b:a": "48k", # target audio bitrate "-ar": "44100", # audio sampling rate @@ -54,6 +55,36 @@ class VideoWebmLow(Config): } +class VideoWebmMedium(Config): + """Medium Quality webm video + + 720:h format, height capped at 720 and width adjusted to keep aspect + ratio, so smaller videos are not upscaled + 240k target video bitrate but stay within quality boundaries. + 48k audio bitrate""" + + VERSION = 1 + + ext = "webm" + mimetype = f"{preset_type}/webm" + + options: ClassVar[dict[str, str | None]] = { + "-codec:v": "libvpx-vp9", + "-b:v": "240k", + "-qmin": "32", + "-qmax": "58", + "-g": "240", + "-quality": "good", + "-speed": "3", + "-vf": "scale='-2:min(720,ih)':force_original_aspect_ratio=increase", + "-row-mt": "1", + "-codec:a": "libvorbis", + "-b:a": "48k", + "-ar": "44100", + "-ac": "2", + } + + class VideoMp4Low(Config): """Low Quality mp4 video @@ -88,7 +119,7 @@ class VideoWebmHigh(Config): 25 constant quality""" - VERSION = 2 + VERSION = 3 ext = "webm" mimetype = f"{preset_type}/webm" @@ -101,6 +132,7 @@ class VideoWebmHigh(Config): "-g": "240", # Number of frames allowed between keyframes "-quality": "good", # codec preset "-speed": "1", # Encoding speed (compromise between quality and encoding time) + "-row-mt": "1", "-codec:a": "libvorbis", # audio codec "-b:a": "48k", # target audio bitrate "-ar": "44100", # audio sampling rate @@ -109,11 +141,11 @@ class VideoWebmHigh(Config): class VideoMp4High(Config): - """Low Quality mp4 video + """High Quality mp4 video - 20 constant quality""" + 28 constant quality""" - VERSION = 1 + VERSION = 2 ext = "mp4" mimetype = f"{preset_type}/mp4" @@ -121,5 +153,6 @@ class VideoMp4High(Config): options: ClassVar[dict[str, str | None]] = { "-codec:v": "h264", # video codec "-codec:a": "aac", # audio codec - "-crf": "20", # constant quality, lower value gives better qual and larger size + "-crf": "28", # constant quality, lower value gives better qual and larger size + "-movflags": "+faststart", } diff --git a/tests/video/test_video.py b/tests/video/test_video.py index f1f23586..2b1ad75d 100644 --- a/tests/video/test_video.py +++ b/tests/video/test_video.py @@ -13,6 +13,7 @@ VideoMp4Low, VideoWebmHigh, VideoWebmLow, + VideoWebmMedium, VoiceMp3Low, ) @@ -178,9 +179,9 @@ def test_preset_has_mime_and_ext(): def test_preset_video_webm_low(): config = VideoWebmLow() - assert config.VERSION == 3 + assert config.VERSION == 4 args = config.to_ffmpeg_args() - assert len(args) == 26 + assert len(args) == 28 options_map = [ ("codec:v", "libvpx-vp9"), ("codec:a", "libvorbis"), @@ -191,6 +192,7 @@ def test_preset_video_webm_low(): ("qmin", "30"), ("qmax", "40"), ("vf", "scale='480:trunc(ow/a/2)*2'"), + ("row-mt", "1"), ("g", "240"), ("speed", "4"), ("ac", "2"), @@ -210,11 +212,49 @@ def test_preset_video_webm_low(): assert idx != -1 and args[idx + 1] == "25" +def test_preset_video_webm_medium(): + config = VideoWebmMedium() + assert config.VERSION == 1 + args = config.to_ffmpeg_args() + assert len(args) == 28 + options_map = [ + ("codec:v", "libvpx-vp9"), + ("codec:a", "libvorbis"), + ("b:v", "240k"), + ("ar", "44100"), + ("b:a", "48k"), + ("quality", "good"), + ("qmin", "32"), + ("qmax", "58"), + ( + "vf", + "scale='-2:min(720,ih)':force_original_aspect_ratio=increase", + ), + ("row-mt", "1"), + ("g", "240"), + ("speed", "3"), + ("ac", "2"), + ] + for option, val in options_map: + idx = args.index(f"-{option}") + assert idx != -1 + assert args[idx + 1] == val + + # test updating values + config = VideoWebmMedium(**{"-speed": "4"}) + config["-b:v"] = "300k" + args = config.to_ffmpeg_args() + idx = args.index("-speed") + assert idx != -1 and args[idx + 1] == "4" + idx = args.index("-b:v") + assert idx != -1 and args[idx + 1] == "300k" + + def test_preset_video_webm_high(): config = VideoWebmHigh() - assert config.VERSION == 2 + assert config.VERSION == 3 args = config.to_ffmpeg_args() - assert len(args) == 24 + assert len(args) == 26 options_map = [ ("codec:v", "libvpx-vp9"), ("codec:a", "libvorbis"), @@ -224,6 +264,7 @@ def test_preset_video_webm_high(): ("quality", "good"), ("qmin", "26"), ("qmax", "54"), + ("row-mt", "1"), ("g", "240"), ("speed", "1"), ("ac", "2"), @@ -279,13 +320,14 @@ def test_preset_video_mp4_low(): def test_preset_video_mp4_high(): config = VideoMp4High() - assert config.VERSION == 1 + assert config.VERSION == 2 args = config.to_ffmpeg_args() - assert len(args) == 8 + assert len(args) == 10 options_map = [ ("codec:v", "h264"), ("codec:a", "aac"), - ("crf", "20"), + ("crf", "28"), + ("movflags", "+faststart"), ] for option, val in options_map: idx = args.index(f"-{option}") From 9c7c2c4bbd68febf5a61c8e7979e087c434feb17 Mon Sep 17 00:00:00 2001 From: pawank925 Date: Fri, 6 Nov 2026 23:41:08 +0530 Subject: [PATCH 2/2] Add slow reencode tests for WebmMedium and Mp4High presets (#271) --- tests/video/test_video.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/video/test_video.py b/tests/video/test_video.py index 2b1ad75d..81c4406f 100644 --- a/tests/video/test_video.py +++ b/tests/video/test_video.py @@ -397,6 +397,18 @@ def test_preset_voice_mp3_low(): VideoWebmLow().to_ffmpeg_args(), {"codecs": ["vp9", "vorbis"], "duration": 2}, ), + ( + "video.mp4", + "video.webm", + VideoWebmMedium().to_ffmpeg_args(), + {"codecs": ["vp9", "vorbis"], "duration": 2}, + ), + ( + "video.mkv", + "video.mp4", + VideoMp4High().to_ffmpeg_args(), + {"codecs": ["h264", "aac"], "duration": 2}, + ), ( "video.webm", "video.mkv",