Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<prop>` 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)
Expand Down
45 changes: 39 additions & 6 deletions src/zimscraperlib/video/presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -47,13 +47,44 @@ 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
"-ac": "2", # force stereo
}


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

Expand Down Expand Up @@ -88,7 +119,7 @@ class VideoWebmHigh(Config):

25 constant quality"""

VERSION = 2
VERSION = 3

ext = "webm"
mimetype = f"{preset_type}/webm"
Expand All @@ -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
Expand All @@ -109,17 +141,18 @@ 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"

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",
}
68 changes: 61 additions & 7 deletions tests/video/test_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
VideoMp4Low,
VideoWebmHigh,
VideoWebmLow,
VideoWebmMedium,
VoiceMp3Low,
)

Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -355,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",
Expand Down