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
47 changes: 45 additions & 2 deletions src/encode.moon
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,46 @@ get_metadata_flags = ->
title = mp.get_property("filename/no-ext")
return {"--oset-metadata=title=%#{string.len(title)}%#{title}"}

-- Wrap a filter parameter in mpv's raw string syntax so property expansion
-- can't interpret anything inside it.
quote_filter_param = (value) ->
"%#{string.len(value)}%#{value}"

-- mpv exposes libavfilter bridge filters as "lavfi-<name>" and reports their
-- arguments as positional placeholders (@0, @1, ...) or as regular option
-- names. The placeholder form is only understood by mpv's own option parser,
-- so turn it into a libavfilter-style argument list. Values are left unquoted
-- because the GIF format embeds these filters in a raw graph, where mpv's
-- property expansion (and with it the %N% raw string syntax) does not run.
serialize_lavfi_filter = (filter) ->
name = filter["name"]
params = filter["params"] or {}

if name == "lavfi"
graph = params["graph"]
return graph and "#{name}=[#{graph}]" or name

positional = {}
named = {}
for key, value in pairs params
index = key\match("^@(%d+)$")
if index
positional[tonumber(index) + 1] = value
else
named[key] = value

args = {}
if #positional > 0
for value in *positional
append(args, {value})
else
keys = [key for key in pairs named]
table.sort(keys)
for key in *keys
append(args, {"#{key}=#{named[key]}"})

return #args > 0 and "#{name}=#{table.concat(args, ":")}" or name

apply_current_filters = (filters) ->
vf = mp.get_property_native("vf")
msg.verbose("apply_current_filters: got #{#vf} currently applied.")
Expand All @@ -194,8 +234,11 @@ apply_current_filters = (filters) ->
continue
str = filter["name"]
params = filter["params"] or {}
for k, v in pairs params
str = str .. ":#{k}=%#{string.len(v)}%#{v}"
if str == "lavfi" or str\match("^lavfi%-")
str = serialize_lavfi_filter(filter)
else
for k, v in pairs params
str = str .. ":#{k}=#{quote_filter_param(v)}"
append(filters, {str})

get_video_filters = (format, region) ->
Expand Down
55 changes: 55 additions & 0 deletions tests/testcases/test_video_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,58 @@ def test_disabled_current_filters_ignore_color_adjustments(self):
self.assertGreater(len(neutral), 0)
self.assertEqual(disabled, neutral)
self.assertNotEqual(enabled, neutral)

def test_gif_applies_lavfi_crop_current_filter(self):
# mpv reports libavfilter bridge filters such as "lavfi-crop" with
# positional parameters named @0, @1, ...; the GIF format rewrites the
# command line into a raw libavfilter graph, where those names are
# invalid. Regression test for GIF encoding failing with them.
self.openTestVideoFile(self.createVideo())
self.sendCommandToMpv({"command": ["vf", "add", "lavfi-crop=320:100:0:40"]})
self.encodeClip(0, 1, options={
"output_format": "gif",
"apply_current_filters": True,
"output_template": "crop",
"scale_height": 90,
"fps": 10,
})
streams = self.probeVideo(self.tempdir / "crop.gif")["streams"]
self.assertEqual((streams[0]["width"], streams[0]["height"]), (288, 90))

def test_gif_applies_lavfi_graph_current_filter(self):
self.openTestVideoFile(self.createVideo())
self.sendCommandToMpv({"command": ["vf", "add", "lavfi=[crop=320:100:0:40]"]})
self.encodeClip(0, 1, options={
"output_format": "gif",
"apply_current_filters": True,
"output_template": "graph",
"scale_height": 90,
"fps": 10,
})
streams = self.probeVideo(self.tempdir / "graph.gif")["streams"]
self.assertEqual((streams[0]["width"], streams[0]["height"]), (288, 90))

def test_gif_applies_lavfi_eq_current_filter(self):
self.openTestVideoFile(self.createVideo())
self.sendCommandToMpv({"command": ["vf", "add", "lavfi-eq=contrast=1.5:saturation=1.5"]})
self.encodeClip(0, 1, options={
"output_format": "gif",
"apply_current_filters": True,
"output_template": "eq",
"scale_height": 90,
"fps": 10,
})
self.assertGreater((self.tempdir / "eq.gif").stat().st_size, 0)

def test_avc_applies_lavfi_crop_current_filter(self):
self.openTestVideoFile(self.createVideo())
self.sendCommandToMpv({"command": ["vf", "add", "lavfi-crop=320:100:0:40"]})
self.encodeClip(0, 1, options={
"output_format": "avc",
"apply_current_filters": True,
"twopass": False,
"target_filesize": 100,
"output_template": "crop-avc",
})
streams = self.probeVideo(self.tempdir / "crop-avc.mp4")["streams"]
self.assertEqual((streams[0]["width"], streams[0]["height"]), (320, 100))
Loading