tests/testthat/test-strip-metadata.R

# Tests for strip_metadata(): the de-identification metadata scrub. The
# command-construction path (run = FALSE) is tested purely; the guard that
# establishes which tags actually clear and that stream-copy preserves A/V is
# gated on the ffmpeg/ffprobe binaries.

test_that("strip_metadata() compiles to a bit-exact stream copy that drops metadata", {
  f <- make_input()
  cmd <- strip_metadata(f, "clean.mp4", run = FALSE)
  # Lossless carry-through of every stream.
  expect_match(cmd, "-codec:v copy -codec:a copy", fixed = TRUE)
  expect_match(cmd, "-map \"0\"", fixed = TRUE)
  # The scrub itself: global tags, chapters, and FFmpeg's own re-added tags.
  expect_match(cmd, "-map_metadata -1", fixed = TRUE)
  expect_match(cmd, "-map_chapters -1", fixed = TRUE)
  expect_match(cmd, "-fflags +bitexact", fixed = TRUE)
  expect_match(cmd, '"clean.mp4"', fixed = TRUE)
  # Binary-free (IP1/D002): this whole test has no skip_if_no_ffmpeg and never
  # shells out, so it passes on a machine with no ffmpeg by construction.
})

test_that("strip_metadata() clears identifying tags, chapters, and FFmpeg's own tags", {
  infile <- make_tagged_video()
  # Sanity: the fixture really carries the container-level tags we remove. (The
  # per-stream tag the fixture injects is not sanity-checked here: whether
  # `-metadata:s:v:0` surfaces in mov stream tags is ffmpeg-version dependent, so
  # we assert only on the *output* below, which is robust across builds.)
  before <- probe_format_tags(infile)
  expect_true(any(grepl("^title=", before)))
  expect_true(any(grepl("^creation_time=", before)))

  outfile <- withr::local_tempfile(fileext = ".mp4")
  strip_metadata(infile, outfile)

  after <- probe_format_tags(outfile)
  # The identifying global tags are gone...
  identifying <- c("title", "comment", "location", "location-eng",
                   "creation_time")
  for (key in identifying) {
    expect_false(any(grepl(paste0("^", key, "="), after)),
                 label = sprintf("format tag %s cleared", key))
  }
  # ...and -fflags +bitexact stops FFmpeg re-stamping its own creation_time /
  # encoder tag (the de-id + reproducibility guard).
  expect_false(any(grepl("^encoder=", after)))
  expect_false(any(grepl("^creation_time=", after)))

  # AC2 also covers *stream*-level tags: the per-stream identifying tag and the
  # per-stream encoder tag are gone; only inert housekeeping (handler_name,
  # language) may remain. Guards against a stream-tag leak passing green.
  after_streams <- probe_stream_tags(outfile)
  for (key in c("name", "title", "creation_time", "encoder", "comment")) {
    expect_false(any(grepl(paste0("^", key, "="), after_streams)),
                 label = sprintf("stream tag %s cleared", key))
  }
  expect_false(any(grepl("CAM-OPERATOR-JANE", after_streams)))

  # No chapters survive the scrub.
  chapters <- ffprobe(sprintf(
    '-v error -show_entries chapter=id -of csv=p=0 "%s"', outfile
  ))
  expect_length(chapters[nzchar(chapters)], 0)
})

test_that("strip_metadata() preserves the A/V streams and rotation (stream copy)", {
  infile <- make_tagged_video()
  outfile <- withr::local_tempfile(fileext = ".mp4")
  strip_metadata(infile, outfile)

  # Same streams, same codecs, in the same order.
  codecs <- function(f) ffprobe(sprintf(
    '-v error -show_entries stream=codec_name -of csv=p=0 "%s"', f
  ))
  expect_equal(codecs(outfile), codecs(infile))
  # Duration is unchanged (lossless copy), within a small remux tolerance.
  expect_equal(probe_duration(outfile), probe_duration(infile), tolerance = 0.05)
  # The rotation display matrix (side data, not a metadata tag) survives.
  expect_equal(probe_rotation(outfile), 90)
})

Try the tidymedia package in your browser

Any scripts or data that you put into this service are public.

tidymedia documentation built on Oct. 11, 2026, 5:08 p.m.