--- title: "A research preprocessing workflow" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{A research preprocessing workflow} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) # The capability chunk in section 1 starts FFmpeg, so it is evaluated only when # FFmpeg is installed. Every other evaluated chunk uses `run = FALSE` and starts # nothing. has_ffmpeg <- nzchar(Sys.which("ffmpeg")) ``` ```{r setup} library(tidymedia) ``` This page follows one research project from raw recordings to shared files. The study records conversations between two people, with one camera on each person. The recordings need the same steps before coding and sound analysis. tidymedia suits this job. Each step runs the same way on every file, and each step gives you a command that you can save and run again. Most examples below use `run = FALSE`. The function then returns the FFmpeg command without running it, so you can read what it would do. Leave out `run = FALSE` to process the files. Some examples work on a whole folder, so they are shown but not run. The others use the short sample clip that comes with the package: ```{r} session <- system.file("extdata", "sample.mp4", package = "tidymedia") ``` Say the study folder has one file for each camera in each session: ```{r, eval = FALSE} jobs <- ffm_jobs("study/raw", type = "video") jobs #> # A tibble: 4 × 1 #> input #> #> 1 /data/study/raw/session01_camA.mp4 #> 2 /data/study/raw/session01_camB.mp4 #> 3 /data/study/raw/session02_camA.mp4 #> 4 /data/study/raw/session02_camB.mp4 ``` `ffm_jobs()` returns a jobs table for the batch functions. It has one row for each file, with the file's full path in an `input` column. The full path is why the output above shows `/data/study/raw` and not `study/raw`. If the folder has no video files, `ffm_jobs()` stops with an error. The next section adds an `output` column to the table and passes it to a `*_batch()` function. ## 1. Standardize the recordings Cameras often differ in picture size, [frame rate](tidymedia.html#glossary) and [codec](tidymedia.html#glossary). Those differences make later steps harder. `standardize_video()` [re-encodes](tidymedia.html#glossary) a file to one common format: ```{r} standardize_video( session, "session01_camA_std.mp4", width = 1280, height = 720, fps = 30, run = FALSE ) ``` To do the same for the whole folder, use the batch version with the jobs table. Each task function has a `*_batch()` version like this one. See `vignette("batch")` for how batches work. ```{r, eval = FALSE} jobs$output <- sub("/raw/", "/std/", jobs$input) standardize_video_batch(jobs, width = 1280, height = 720, fps = 30) ``` ### Using video hardware Re-encoding a large study on the main processor can be slow. Functions that re-encode video, such as `standardize_video()` and `format_for_web()`, take a `hardware` argument. It moves the work to a [hardware encoder](tidymedia.html#glossary). There are two choices. Use `"nvenc"` for an NVIDIA graphics card, with an FFmpeg build that supports nvenc. Use `"videotoolbox"` for Apple hardware on macOS. These functions show what your FFmpeg can do. `ffmpeg_codecs()` and `ffmpeg_encoders()` list its codecs and encoders. `hardware_encoder()` gives the encoder name for a codec and a hardware choice. `has_hardware_encoder()` says whether your FFmpeg has that encoder: ```{r, eval = has_ffmpeg} head(ffmpeg_codecs()) encoders <- ffmpeg_encoders() head(encoders[encoders$type == "Video", c("name", "details")]) hardware_encoder("h264", "nvenc") hardware_encoder("h264", "videotoolbox") has_hardware_encoder("h264", "videotoolbox") ``` `has_hardware_encoder()` checks how FFmpeg was built. A `TRUE` result does not prove that the hardware works. When it is `TRUE`, name that hardware in the batch: ```{r, eval = FALSE} standardize_video_batch( jobs, width = 1280, height = 720, fps = 30, hardware = "videotoolbox" ) ``` If the hardware you name is not available, the call stops with an error. So a shared script never changes the codec without telling you. Add `fallback = TRUE` to use the main processor instead. The two choices support different codecs. NVIDIA nvenc supports H.264, HEVC and AV1. Apple videotoolbox supports H.264 and HEVC. A codec that the hardware does not support is an error that names both. A hardware encoder trades some quality for speed. At the same bit rate, a hardware encoder gives a picture that is a little worse than the software encoder `libx264` or `libx265`. So it suits previews and bulk conversion, and the software encoder suits a copy that you will keep or analyze frame by frame. These functions also take a `quality` argument. It is the encoder's own rate-control value, passed through unchanged. `libx264` and `libx265` read it as `-crf` (0 to 51). The nvenc encoders read it as `-cq` (0 to 51), and the videotoolbox encoders as `-q:v` (1 to 100). Each scale is its own, so pick the number for the encoder you named: ```{r, eval = FALSE} standardize_video("raw.mp4", "std.mp4", hardware = "videotoolbox", quality = 60) ``` When `fallback = TRUE` falls back to software, the value is dropped and the message says so. Leave `quality` as `NULL` to keep the encoder's own default. tidymedia does not do hardware decoding or run filters on a graphics card. For those, use the direct command `ffmpeg()`. ## 2. Prepare the audio Sound analysis and transcription tools work best with clean audio at an even loudness. `normalize_audio()` sets a file's loudness to a target. Here the target is -23 [LUFS](tidymedia.html#glossary), the level that the EBU R 128 broadcast standard uses. The output of `normalize_audio()` has one audio [stream](tidymedia.html#glossary) and no video. So give it an audio file name. An `.mp4` name would give you a video file with sound and no picture: ```{r} normalize_audio(session, "session01_camA_norm.wav", target_loudness = -23, run = FALSE) ``` To set the loudness and keep the picture, first write the audio file as above. Then use the direct command `ffmpeg()` to put it back with the video. A transcription tool needs the audio in its own file. `convert_audio()` converts the audio to the format that the output extension names. Here that is a `.wav` file for a speech recognition tool: ```{r} convert_audio(session, "session01_camA.wav", run = FALSE) ``` If you need the audio as it is, `extract_audio()` copies it without converting it. See `vignette("tidymedia")`. ## 3. Frames for visual coding Coding facial expressions or gestures frame by frame needs still images. `extract_frame()` saves one frame at a given time. This is useful for a picture in a coding manual: ```{r} extract_frame(session, "session01_camA_t30.png", timestamp = 30, run = FALSE) ``` `sample_frames()` saves frames at a fixed rate, as numbered image files. Here it saves one frame per second, for coding by hand or by a computer vision tool: ```{r, eval = FALSE} sample_frames("session01_camA_std.mp4", outdir = "frames/session01_camA", fps = 1) ``` ## 4. De-identify before sharing Before you share recordings with coders, you often must remove information that identifies people. Two task functions cover the common cases. `strip_metadata()` removes the file's metadata, such as the camera model, GPS location and recording time. It does not change the audio or the video: ```{r} strip_metadata(session, "session01_camA_clean.mp4", run = FALSE) ``` `anonymize_video()` fills one or more rectangles with a solid color. It does not find or follow faces. You give the position of an area that does not move, such as a name caption on screen. You give the areas as a data frame, with one row for each box: ```{r} regions <- tibble::tibble( x = 16, y = 640, width = 360, height = 64 # lower-left name caption ) anonymize_video(session, "session01_camA_deid.mp4", regions = regions, run = FALSE) ``` ## 5. Assemble and share If a session was recorded in parts, `concatenate_videos()` joins the parts into one file. The parts must have the same codec, [container](tidymedia.html#glossary), picture size and frame rate. Here they do, because step 1 made them the same: ```{r, eval = FALSE} concatenate_videos( c("session01_camA_part1.mp4", "session01_camA_part2.mp4"), "session01_camA_full.mp4" ) ``` Last, `format_for_web()` makes an H.264 file that most browsers can play. It moves the file's index to the start, so coders can watch it in a browser before it has fully downloaded: ```{r} format_for_web(session, "session01_camA_share.mp4", run = FALSE) ``` ## Reproducibility Each task function returns the FFmpeg command it used. A batch run also keeps these commands, in a `command` column. Save the commands, and you have a full record of how each file was made. You can then run the same steps on the next group of recordings. The command records what you asked for. A manifest records what happened: the FFmpeg version, the time, and checksums of the files. Run the batch with `manifest = TRUE` and read the manifest with `ffm_manifest()`. To check that each output has the duration, size and codecs you asked for, use `verify_media()`. `vignette("verification")` covers both, and also shows how to stop a file that hangs. ## Where to next - `vignette("batch")` explains the batch functions in more detail. - `vignette("metadata")` shows how to read each file's metadata into a tibble. - `vignette("verification")` shows how to check outputs, record how files were made and limit run time. - `vignette("tidymedia")` explains the task functions and the pipeline functions.