Nothing
# M47: the pass-through verbs state their audio selection instead of inheriting
# FFmpeg's implicit stream choice.
#
# standardize_video() and anonymize_video() emitted no -map at all before this
# milestone, so FFmpeg's implicit selection applied: one stream of each type,
# preferring the audio track carrying the container's DEFAULT disposition.
# Measured on a 3-audio-track .mkv with DEFAULT moved to track 1 (ffmpeg 8.1.2,
# macOS): one audio stream out, and it was `spa`, the SECOND track. Which track
# survived was therefore a property of the input's flags and of the FFmpeg
# build, which is the invisible variation D023 exists to remove.
#
# Both verbs now compile `-map 0:v? -map 0:a?` when no track is named and
# `-map 0:v? -map 0:a:<n>` when one is. A uniform `-map 0` was rejected at plan
# time because it fails outright into .mp4 on a subtitle-bearing input (exit 8,
# no default mp4 subtitle encoder).
#
# The `?` on the unselected specifiers is what keeps a stream-less input
# working: a bare `-map 0:a` aborts FFmpeg on a silent video and a bare
# `-map 0:v` aborts on an audio-only file, both exit 234, where master emitted
# no map and exited 0. The named specifier keeps no `?` so that naming a track
# the input lacks stays an error (D023). See the "tolerates" tests at the foot
# of this file.
regions_1 <- function() {
data.frame(x = 0, y = 0, width = 10, height = 10)
}
std_jobs <- function(f, ...) {
tibble::tibble(input = c(f, f), output = c("a.mp4", "b.mp4"), ...)
}
anon_jobs <- function(f, ...) {
tibble::tibble(
input = c(f, f), output = c("a.mp4", "b.mp4"),
regions = list(regions_1(), regions_1()), ...
)
}
# The pre-M47 commands, recorded from master at f3c3054 and committed here as
# literals. They are written as templates taking the maps because the claim
# under test is "these two -map arguments appeared and nothing else moved", and
# a comparison against "what master returns today" stops being checkable the
# moment this branch merges and master becomes this code.
standardize_command <- function(infile, maps = "") {
paste0(
'-y -i "', infile, '"',
' -vf "crop=w=floor(in_w/2)*2:h=floor(in_h/2)*2:x=(in_w-out_w)/2:y=(in_h-out_h)/2"',
" -codec:v libx264 -codec:a copy -pix_fmt yuv420p -movflags +faststart ",
maps, '"out.mp4"'
)
}
anonymize_command <- function(infile, maps = "") {
paste0(
'-y -i "', infile, '"',
' -vf "crop=w=floor(in_w/2)*2:h=floor(in_h/2)*2:x=(in_w-out_w)/2:y=(in_h-out_h)/2',
',drawbox=x=0:y=0:w=10:h=10:c=black:t=fill"',
" -codec:v libx264 -codec:a copy -pix_fmt yuv420p ",
maps, '"out.mp4"'
)
}
# Count -map arguments. A containment assertion cannot see a duplicate, and
# ffm_map() appends since M43, so the count is the discriminator (M43/M45).
map_count <- function(cmd) {
vapply(cmd, function(x) {
sum(gregexpr("-map ", x, fixed = TRUE)[[1]] > 0)
}, integer(1), USE.NAMES = FALSE)
}
# AC1 -------------------------------------------------------------------------
test_that("an unset audio_stream compiles every video and every audio stream", {
f <- make_input()
expect_identical(
standardize_video(f, "out.mp4", run = FALSE),
standardize_command(f, "-map \"0:v?\" -map \"0:a?\" ")
)
expect_identical(
anonymize_video(f, "out.mp4", regions = regions_1(), run = FALSE),
anonymize_command(f, "-map \"0:v?\" -map \"0:a?\" ")
)
})
test_that("an explicit NULL audio_stream compiles what leaving it unset does", {
f <- make_input()
expect_identical(
standardize_video(f, "out.mp4", audio_stream = NULL, run = FALSE),
standardize_video(f, "out.mp4", run = FALSE)
)
expect_identical(
anonymize_video(f, "out.mp4", regions = regions_1(), audio_stream = NULL,
run = FALSE),
anonymize_video(f, "out.mp4", regions = regions_1(), run = FALSE)
)
})
# AC2 -------------------------------------------------------------------------
test_that("a named audio_stream compiles that track and no other", {
f <- make_input()
expect_identical(
standardize_video(f, "out.mp4", audio_stream = 2, run = FALSE),
standardize_command(f, "-map \"0:v?\" -map \"0:a:2\" ")
)
expect_identical(
anonymize_video(f, "out.mp4", regions = regions_1(), audio_stream = 2,
run = FALSE),
anonymize_command(f, "-map \"0:v?\" -map \"0:a:2\" ")
)
})
test_that("both verbs compile exactly two maps either way", {
f <- make_input()
cmds <- c(
standardize_video(f, "out.mp4", run = FALSE),
standardize_video(f, "out.mp4", audio_stream = 2, run = FALSE),
anonymize_video(f, "out.mp4", regions = regions_1(), run = FALSE),
anonymize_video(f, "out.mp4", regions = regions_1(), audio_stream = 2,
run = FALSE)
)
expect_identical(map_count(cmds), rep(2L, 4))
# A named call must not ALSO carry the every-track map -- ffm_map() appends,
# so `-map 0:a? -map 0:a:2` would satisfy a containment check for either one
# on its own (M45's absence-assertion trick).
expect_false(grepl("-map \"0:a?\" ", cmds[[2]], fixed = TRUE))
expect_false(grepl("-map \"0:a?\" ", cmds[[4]], fixed = TRUE))
})
test_that("audio_stream = 0 names the first track rather than emitting 0:a", {
f <- make_input()
expect_identical(
standardize_video(f, "out.mp4", audio_stream = 0, run = FALSE),
standardize_command(f, "-map \"0:v?\" -map \"0:a:0\" ")
)
})
# AC3 -------------------------------------------------------------------------
test_that("a bad audio_stream aborts naming the argument and blaming the verb", {
f <- make_input()
bad <- list("1", 1.5, -1, NA, NA_integer_, c(0, 1))
for (value in bad) {
err <- expect_error(
standardize_video(f, "out.mp4", audio_stream = value, run = FALSE)
)
expect_match(conditionMessage(err), "audio_stream")
expect_identical(rlang::call_name(conditionCall(err)), "standardize_video")
err <- expect_error(
anonymize_video(f, "out.mp4", regions = regions_1(),
audio_stream = value, run = FALSE)
)
expect_match(conditionMessage(err), "audio_stream")
expect_identical(rlang::call_name(conditionCall(err)), "anonymize_video")
}
})
# AC7 -------------------------------------------------------------------------
test_that("run = FALSE runs no binary at the default hardware", {
f <- make_input()
# Count invocations rather than stop()ing in the mock: these call sites sit
# under tryCatch() in places, which swallows a raising mock and leaves the
# test green with the gate it exists to pin deleted (M44).
n <- 0L
local_mocked_bindings(
run_program = function(...) {
n <<- n + 1L
list(status = 0L, stdout = character(), stderr = character())
},
find_ffmpeg = function(...) {
n <<- n + 1L
"ffmpeg"
},
find_ffprobe = function(...) {
n <<- n + 1L
"ffprobe"
}
)
standardize_video(f, "out.mp4", run = FALSE)
standardize_video(f, "out.mp4", audio_stream = 2, run = FALSE)
anonymize_video(f, "out.mp4", regions = regions_1(), run = FALSE)
anonymize_video(f, "out.mp4", regions = regions_1(), audio_stream = 2,
run = FALSE)
# All FOUR entry points, not just the two scalars: AC7 says "across all
# four" and Coverage maps it to the batch tasks too. Reviewing this
# milestone caught the batch pair missing here (F3).
standardize_video_batch(std_jobs(f), audio_stream = 2, run = FALSE)
anonymize_video_batch(anon_jobs(f), audio_stream = 2, run = FALSE)
expect_identical(n, 0L)
# Prove the mock is actually in scope for the batch verbs rather than
# silently inert: one run = TRUE batch call must trip the counter. Without
# this, `n == 0` is equally consistent with "no binary ran" and "the mock
# never bound" (M39's discriminate-the-test rule, M44's counting-mock rule).
standardize_video_batch(std_jobs(f), run = TRUE)
expect_gt(n, 0L)
# For hardware = "nvenc", which is NOT binary-free here, see the D034 test
# below. M47 carried that gap as a comment; M54 made it an assertion.
})
test_that("hardware = 'nvenc' probes FFmpeg while building, though run = FALSE", {
# D034: a probe whose result enters the compiled command runs when the
# pipeline is built -- `run` gates execution, never construction. Counting at
# ffmpeg_encoders() rather than find_ffmpeg(): ffmpeg() shells out through
# system(), not run_program(), so the purity block's mock above cannot see it.
f <- make_input()
withr::local_options(tidymedia.hardware_encoders = NULL) # force the real probe
probes <- 0L
local_mocked_bindings(
ffmpeg_encoders = function(...) {
probes <<- probes + 1L
tibble::tibble(name = "h264_nvenc")
}
)
# Default hardware: resolve_hw_encoder() returns before reaching has_hardware_encoder().
standardize_video(f, "out.mp4", run = FALSE)
anonymize_video(f, "out.mp4", regions = regions_1(), run = FALSE)
expect_identical(probes, 0L)
# nvenc: the probe runs during construction, with run = FALSE throughout.
# M67 made the answer session-scoped, so each measured call discards the memo
# first: without that, calls 2 and 3 would read the first call's answer and
# the test would measure the memo instead of D034's construction-time probe.
standardize_video(f, "out.mp4", hardware = "nvenc", run = FALSE)
expect_gt(probes, 0L)
before <- probes
forget_ffmpeg_capabilities()
anonymize_video(f, "out.mp4", regions = regions_1(), hardware = "nvenc",
run = FALSE)
expect_gt(probes, before)
before <- probes
forget_ffmpeg_capabilities()
standardize_video_batch(std_jobs(f), hardware = "nvenc", run = FALSE)
expect_gt(probes, before)
})
# AC4 / AC5: the batch siblings --------------------------------------------
test_that("the batch argument reaches every row", {
f <- make_input()
out <- standardize_video_batch(std_jobs(f), audio_stream = 2, run = FALSE)
expect_true(all(grepl("-map \"0:v?\" -map \"0:a:2\"", out$command, fixed = TRUE)))
out <- anonymize_video_batch(anon_jobs(f), audio_stream = 2, run = FALSE)
expect_true(all(grepl("-map \"0:v?\" -map \"0:a:2\"", out$command, fixed = TRUE)))
})
test_that("an audio_stream column overrides the argument per row", {
f <- make_input()
# Row 2's NA is the column form of the NULL sentinel: it keeps that row on
# every audio track, overriding the argument rather than deferring to it.
out <- standardize_video_batch(
std_jobs(f, audio_stream = c(1, NA)), audio_stream = 2, run = FALSE
)
expect_match(out$command[[1]], "-map \"0:a:1\"", fixed = TRUE)
expect_match(out$command[[2]], "-map \"0:a?\"", fixed = TRUE)
# A PREFIX probe, so it keeps no closing quote: what it refutes is any
# `-map "0:a:<n>"` on the NA row, and a closed `-map "0:a:"` matches nothing.
expect_false(grepl("-map \"0:a:", out$command[[2]], fixed = TRUE))
})
test_that("a one-row batch compiles what the scalar verb compiles", {
f <- make_input()
out <- standardize_video_batch(
tibble::tibble(input = f, output = "out.mp4"), audio_stream = 2,
run = FALSE
)
expect_identical(
as.character(out$command[[1]]),
as.character(standardize_video(f, "out.mp4", audio_stream = 2,
run = FALSE))
)
out <- anonymize_video_batch(
tibble::tibble(input = f, output = "out.mp4",
regions = list(regions_1())),
audio_stream = 2, run = FALSE
)
expect_identical(
as.character(out$command[[1]]),
as.character(anonymize_video(f, "out.mp4", regions = regions_1(),
audio_stream = 2, run = FALSE))
)
})
test_that("a wrongly typed audio_stream column aborts before any row runs", {
f <- make_input()
for (bad in list(c("1", "2"), c(TRUE, FALSE))) {
err <- expect_error(
standardize_video_batch(std_jobs(f, audio_stream = bad), run = FALSE)
)
expect_match(conditionMessage(err), "audio_stream")
# The hint has to be true on THIS family. Borrowing the extraction verbs'
# wording would tell a caller NA keeps the first track, where here it keeps
# them all (M40's stale-hint lesson, which is about a caller being ADDED).
expect_match(conditionMessage(err), "keep every audio track")
expect_no_match(conditionMessage(err), "first audio track")
expect_no_match(conditionMessage(err), "drop audio")
}
err <- expect_error(
anonymize_video_batch(anon_jobs(f, audio_stream = c("1", "2")),
run = FALSE)
)
expect_match(conditionMessage(err), "keep every audio track")
})
test_that("an all-NA audio_stream column is accepted, being logical", {
f <- make_input()
# R types c(NA, NA) as logical, so an is.numeric-only guard would wrongly
# reject the column spelling of "leave every row unselected" (M34).
out <- standardize_video_batch(
std_jobs(f, audio_stream = c(NA, NA)), run = FALSE
)
expect_true(all(grepl("-map \"0:a?\"", out$command, fixed = TRUE)))
})
test_that("a scalar NA audio_stream aborts rather than compiling every track", {
f <- make_input()
# Load-bearing here in a way it is not on the scalar verb: the column path
# reads NA as the NULL sentinel, so without the front-door guard this would
# quietly compile 0:a? instead of erroring (M37/M41).
expect_error(
standardize_video_batch(std_jobs(f), audio_stream = NA, run = FALSE),
"audio_stream"
)
})
# AC6: the stream-less inputs an explicit map can break --------------------
# Stating a selection means naming stream types the input may not have, and
# FFmpeg treats an unmatched `-map` as fatal rather than empty. These two are
# the regression guards for the `?` suffix: with a bare `0:a`/`0:v` both abort
# at exit 234, where master -- emitting no map at all -- exits 0.
test_that("a video-only input still standardizes", {
skip_if_no_ffmpeg()
infile <- make_silent_video()
outfile <- withr::local_tempfile(fileext = ".mp4")
expect_no_error(standardize_video(infile, outfile))
expect_true(file.exists(outfile))
expect_identical(stream_types(outfile), "video")
})
test_that("an audio-only input still standardizes", {
skip_if_no_ffmpeg()
infile <- make_silent_audio()
outfile <- withr::local_tempfile(fileext = ".mp4")
expect_no_error(standardize_video(infile, outfile))
expect_true(file.exists(outfile))
expect_identical(stream_types(outfile), "audio")
})
test_that("a video-only input still anonymizes", {
skip_if_no_ffmpeg()
infile <- make_silent_video()
outfile <- withr::local_tempfile(fileext = ".mp4")
expect_no_error(anonymize_video(infile, outfile, regions = regions_1()))
expect_identical(stream_types(outfile), "video")
})
# AC6: which track actually comes out -------------------------------------
test_that("an unset audio_stream now carries every track, not FFmpeg's pick", {
skip_if_no_ffmpeg()
skip_if_no_ffprobe()
# DEFAULT on track 1 so "the heuristic" and "track 0" cannot coincide: on a
# fixture leaving DEFAULT at 0 this test passes with audio_stream ignored
# entirely, which is what the criteria audit caught in the drafted version.
infile <- make_multitrack_video(default_track = 1)
outfile <- withr::local_tempfile(fileext = ".mkv")
standardize_video(infile, outfile)
expect_identical(stream_types(outfile), c("video", "audio", "audio", "audio"))
})
test_that("a named audio_stream takes that track and no other", {
skip_if_no_ffmpeg()
skip_if_no_ffprobe()
infile <- make_multitrack_video(default_track = 1)
outfile <- withr::local_tempfile(fileext = ".mkv")
# Track 2 (`fra`) is neither the first track nor the DEFAULT one, so no
# implicit selection can produce it -- only the map can.
standardize_video(infile, outfile, audio_stream = 2)
expect_identical(stream_types(outfile), c("video", "audio"))
expect_identical(audio_languages(outfile), "fra")
})
test_that("naming a track the input lacks stays an FFmpeg error", {
skip_if_no_ffmpeg()
infile <- make_multitrack_video()
outfile <- withr::local_tempfile(fileext = ".mkv")
# The `?` that makes an unselected map tolerant is deliberately absent here:
# every @param audio_stream in the package promises this is an FFmpeg error
# rather than an R one (D023), and a silent audio-less output would be worse.
expect_error(standardize_video(infile, outfile, audio_stream = 9))
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.