The master repo is setup to build a video file. I (Lindsay) have been running the following lines of code to build the big pieces individually. One version of the final product can be found here. Before running this, you need delete the old contents of 6_visualize/tmp
.
#####################
## Download data
# Uses GD still so need to do this first one separately so it can log in
scipiper::scmake("1_fetch/out/dv_data.rds.ind", "1_fetch.yml")
# Then all the rest
scipiper::scmake("1_fetch/out/dv_data_fixed_gh.rds.ind", "1_fetch.yml")
scipiper::scmake('1_fetch/out/sites_stage.rds.ind', remake_file = '1_fetch.yml')
scipiper::scmake("2_process/out/dv_stats.rds.ind", "2_process.yml")
scipiper::scmake("2_process/out/dv_stats_fixed_gh.rds.ind", "2_process.yml")
scipiper::scmake("2_process/out/dv_stat_styles.rds.ind", "2_process.yml")
#####################
## Create a callouts file (it can be empty if there are none)
file.create("callouts_cfg.yml")
#####################
## Build ALL frames and then make video
# Prior to building the frames:
# 1. Make sure to delete the contents of `callouts_cfg.yml` before building or you will have old events on the new frames.
# 2. Make sure the title in `viz_config.yml` is correct.
# To make absolutely sure that your video will use the right font, you may need to run:
sysfonts::font_add_google('Abel','abel')
scipiper::scmake('6_intro_gif_tasks.yml', remake_file = '6_visualize.yml', force = TRUE)
scipiper::scmake('6_visualize/log/6_intro_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_final_gif_tasks.yml', remake_file = '6_visualize.yml', force = TRUE)
scipiper::scmake('6_visualize/log/6_final_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_timestep_gif_tasks.yml', remake_file = '6_visualize.yml', force = TRUE)
scipiper::scmake('6_visualize/log/6_timestep_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_pause_gif_tasks.yml', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_visualize/log/6_pause_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_visualize/out/year_in_review.mp4', remake_file = '6_visualize.yml', force = TRUE)
Sometimes it is useful to build just a single frame, or subset of frames. Below is some code that helps with that.
# Don't forget that you need the font!
sysfonts::font_add_google('Abel','abel')
# Build a specific subset of days
days <- c(707:717)
scipiper::scmake(sprintf('6_visualize/tmp/frame_20210%s_00.png', days), '6_timestep_gif_tasks.yml')
# Build a single frame:
scipiper::scmake('6_visualize/tmp/frame_20200210_00.png', '6_timestep_gif_tasks.yml')
# Understand events timing through a line chart
dates_of_events <- lapply(yaml::read_yaml("callouts_cfg.yml"), function(x) {
tibble(label = paste(x$text$label, collapse = " "),
start = as.Date(x$event_dates$start), end = as.Date(x$event_dates$end),
txt_s = as.Date(x$text_dates$start), txt_e = as.Date(x$text_dates$end)) %>%
mutate(
txt_in = txt_s - ifelse(is.null(x$fade_in), 9, x$fade_in),
txt_out = txt_e + ifelse(is.null(x$fade_out), 9, x$fade_out)
)
}) %>% bind_rows()
library(ggplot2)
ggplot(dates_of_events, aes(y = 1, yend = 1)) +
geom_segment(aes(x = start, xend = end), size = 3) +
geom_segment(aes(y = 0.5, yend = 0.5, x = txt_s, xend = txt_e), size = 2, color = "blue") +
geom_segment(aes(y = 0.5, yend = 0.5, x = txt_in, xend = txt_out), size = 1, color = "red", linetype = "dotted") +
ylim(0, 2) +
geom_text(aes(x = start, y = 1.5, label = label), hjust = 0) +
facet_grid(label ~ .) +
theme(axis.text=element_blank(), axis.ticks=element_blank(),
strip.background = element_blank(), strip.text = element_blank(),
axis.title = element_blank(), panel.grid = element_blank(),
panel.spacing = unit(0, "lines"))
# Build a frame for the middle of each event
dates_to_build <- lapply(lapply(yaml::read_yaml("callouts_cfg.yml"), '[[', "text_dates"), function(x) {
endDate <- as.Date(x$end)
startDate <- as.Date(x$start)
halfwayDate <- startDate + (endDate - startDate)/2
return(halfwayDate)
}) %>% unlist() %>% as.Date(origin = "1970-01-01") %>% format("%Y%m%d")
# Don't forget the font:
sysfonts::font_add_google('Abel','abel')
scipiper::scmake(sprintf('6_visualize/tmp/frame_%s_00.png', dates_to_build), '6_timestep_gif_tasks.yml', force=TRUE)
It is not currently built into the scipiper
code yet. BUT here is what you need to do:
- Once you have a basic video (see code chunk above), run the script
auto_generate_excel_file_for_review.R
. Upload this to OneDrive and share with the people who will help make the callouts. - Once the callouts are complete in this file. Download it and run the script
auto_callouts_cfg_from_excel_file.R
. Please note the filenames of your Excel files. - Now go manually edit
callouts_cfg.yml
.
Eventually, it would be nice to get this download/read/upload process into the pipeline, but for now it is not.
# Create a table from the final callouts to share as needed
lapply(yaml.load_file("callouts_cfg.yml"), function(x) {
tibble(EventDescription = paste(x$text$label, collapse = " "),
EventStart = x$event_dates$start,
EventEnd = x$event_dates$end)
}) %>%
bind_rows() %>%
filter(nchar(EventDescription) > 0) %>%
arrange(EventStart, EventEnd) %>%
View()
You can make it with or without the basemap. The important part is that it is the right dimensions.
source("1_fetch/src/map_utils.R")
source("2_process/src/project_shift_states.R")
source("6_visualize/src/prep_basemap_fun.R")
source("6_visualize/src/prep_view_fun.R")
source("6_visualize/src/create_animation_frame.R")
viz_config <- yaml::yaml.load_file("viz_config.yml")
viz_config[['basemap']][["border"]] <- viz_config[['basemap']][["col"]] # No outlines on states
states_projected <- project_states('1_fetch/out/pre_state_boundaries_census.zip.ind', viz_config[['projection']])
states_shifted <- shift_states(states_projected, viz_config[['shift']])
create_animation_frame(
png_file="6_visualize/in/overlay_template.png",
config=viz_config[c('width','height')],
prep_view_fun(as_view_polygon(viz_config[c('bbox', 'projection', 'width', 'height')]), viz_config['background_col']),
prep_basemap_fun(states_shifted, viz_config[['basemap']]))
version_info <- "river_conditions_jan_mar_2022"
frame_to_use <- "6_visualize/tmp/frame_20220325_00.png"
run_magick_cmd <- function(command_str) {
if(Sys.info()[['sysname']] == "Windows") {
magick_command <- sprintf('magick %s', command_str)
} else {
magick_command <- command_str
}
system(magick_command)
}
run_magick_cmd("convert -size 11400x3721 canvas:white carousel_background.png")
run_magick_cmd(sprintf("convert -composite -gravity center carousel_background.png %s %s_carousel.png", frame_to_use, version_info))
version_info <- "river_conditions_jan_mar_2022"
frame_to_use <- "6_visualize/tmp/frame_20220325_00.png"
thumbnail_dim <- 500
viz_config <- yaml::yaml.load_file("viz_config.yml")
width <- viz_config[["width"]]
height <- viz_config[["height"]]
x_pos <- viz_config[["footnote_cfg"]][["x_pos"]]
y_pos <- viz_config[["footnote_cfg"]][["y_pos"]]
run_magick_cmd <- function(command_str) {
if(Sys.info()[['sysname']] == "Windows") {
magick_command <- sprintf('magick %s', command_str)
} else {
magick_command <- command_str
}
system(magick_command)
}
# Crop frame to map only view & resize so that width is 500
run_magick_cmd(sprintf("convert %s -gravity West -chop %sx0 -gravity South -chop 0x%s -resize %sx%s drupal_thumbnail_intermediate.png", frame_to_use, width*x_pos*0.80, height*y_pos*2, thumbnail_dim, thumbnail_dim))
# Create a square white image
run_magick_cmd(sprintf("convert -size %sx%s canvas:white drupal_thumbnail.png", thumbnail_dim, thumbnail_dim))
# Paste the map centered in the square white image
run_magick_cmd(sprintf("convert -composite -gravity center drupal_thumbnail.png drupal_thumbnail_intermediate.png %s_thumbnail.png", version_info))
version_info <- "river_conditions_jan_mar_2022"
frame_to_use <- "6_visualize/tmp/frame_20220325_00.png"
visid_file <- "6_visualize/in/visid_overlay.png"
# Get viz frame dimensions and then divide by 2 bc we
# double them in combine_animation_frame
timestep_frame_config <- remake::fetch("timestep_frame_config")
viz_config_dim <- lapply(timestep_frame_config, function(x) x/2)
run_magick_cmd <- function(command_str) {
if(Sys.info()[['sysname']] == "Windows") {
magick_command <- sprintf('magick %s', command_str)
} else {
magick_command <- command_str
}
system(magick_command)
}
# Add cover over logo!
run_magick_cmd(sprintf(
'convert %s -fill white -draw "rectangle 0,%s %s,%s" %s',
frame_to_use,
timestep_frame_config$height,
timestep_frame_config$width/6,
timestep_frame_config$height - timestep_frame_config$height/8,
"6_visualize/tmp/frame_logo_covered.png"))
# Resize the existing frame to fit the black bottom bar
# without changing aspect ratio
run_magick_cmd(sprintf(
"convert %s -resize %sx%s %s",
"6_visualize/tmp/frame_logo_covered.png",
viz_config_dim$width - viz_config_dim$width*0.08691406,
viz_config_dim$height - viz_config_dim$height*0.08691406,
"6_visualize/tmp/frame_resized.png"))
# Put resized frame into an appropriately sized image but with
# space at the bottom for the bar
# 1. Create image that is the right size but just blank
run_magick_cmd(sprintf("convert -size %sx%s canvas:white 6_visualize/tmp/drupal_still.png", viz_config_dim$width, viz_config_dim$height))
# Now add the new frame
run_magick_cmd(sprintf(
"convert -composite -gravity north %s %s %s",
"6_visualize/tmp/drupal_still.png",
"6_visualize/tmp/frame_resized.png",
"6_visualize/tmp/frame_resized_ready.png"))
run_magick_cmd(sprintf(
"convert -composite -gravity southwest %s %s %s_visid_drupal.png",
"6_visualize/tmp/frame_resized_ready.png",
visid_file,
version_info))
version_info <- "river_conditions_jan_mar_2022"
# This works very well for viz_config height and width of 2048 & 4096.
# Unsure about what changes may be needed for other dimensions.
# Get viz frame dimensions and then divide by 2 bc we
# double them in combine_animation_frame
timestep_frame_config <- remake::fetch("timestep_frame_config")
viz_config_dim <- lapply(timestep_frame_config, function(x) x/2)
# Identify files
video_file <- sprintf("6_visualize/out/%s_twitter.mp4", version_info)
video_logo_cover_file <- "6_visualize/tmp/video_logocovered_for_visid.mp4"
video_scaled_for_visid_file <- "6_visualize/tmp/video_scaled_for_visid.mp4"
visid_file <- "6_visualize/in/visid_overlay.png"
video_w_visid_file <- sprintf("6_visualize/out/%s_visid.mp4", version_info)
# Cover up the existing USGS logo
system(sprintf(
'ffmpeg -y -i %s -vf "drawbox=x=0:y=ih-h:w=%s/6:h=%s/8:t=max:color=white" %s',
video_file,
viz_config_dim$width,
viz_config_dim$height,
video_logo_cover_file
))
# Scale and pad the existing video to fit the black bottom bar
# without changing aspect ratio
system(sprintf(
'ffmpeg -y -i %s -vf "scale=%s:%s,pad=%s:%s:(ow-iw)/2:color=white" %s',
video_logo_cover_file,
viz_config_dim$width-viz_config_dim$width*0.08691406,
viz_config_dim$height-viz_config_dim$height*0.08691406,
viz_config_dim$width,
viz_config_dim$height,
video_scaled_for_visid_file
))
# Overlay the visid black bar onto video
system(sprintf(
'ffmpeg -y -i %s -i %s -filter_complex "overlay" -c:v libx264 %s',
video_scaled_for_visid_file,
visid_file,
video_w_visid_file))
version_info <- "river_conditions_jan_mar_2022"
video_file <- sprintf("6_visualize/out/%s_visid.mp4", version_info)
video_resized_for_facebook <- "6_visualize/tmp/video_facebook_aspect_ratio.mp4"
video_downscaled_for_facebook <- sprintf("6_visualize/out/%s_facebook.mp4", version_info)
# Get viz frame dimensions and then divide by 2 bc we
# double them in combine_animation_frame
timestep_frame_config <- remake::fetch("timestep_frame_config")
viz_config_dim <- lapply(timestep_frame_config, function(x) x/2)
# need to have 16:9, not 2:1
new_height <- viz_config_dim$width * 9/16
# Scale and pad the existing video to fit 16:9 aspect ratio
# 0.8691406 is the scale factor from above for the width
# of the logo black bar. Using it here means that we are centering
# the image and taking that black bar into account. It's a bit
# of a mystery to me still but it worked!
system(sprintf(
'ffmpeg -y -i %s -vf "pad=%s:%s:0:(oh-ih)*0.8691406:color=black" %s',
video_file,
viz_config_dim$width,
new_height,
video_resized_for_facebook
))
# Scale down size so it doesn't upload as a 360 video
scale_factor <- 1280 / viz_config_dim$width # 1280 = optimal facebook width
system(sprintf(
'ffmpeg -y -i %s -vf "scale=%s:%s" %s',
video_resized_for_facebook,
viz_config_dim$width * scale_factor,
new_height * scale_factor,
video_downscaled_for_facebook
))
# First, need to overwrite the frames and rebuild the regular video.
# Go to `viz_config.yml` and comment out the regular specs & then uncomment the Instagram ones
# --> Don't forget to update the "subtitle" field <--
# CTRL+F for "instagram" to find it
# REBUILD FRAMES & VIDEO (you will probably need to delete pause frames before they rebuild)
sysfonts::font_add_google('Abel','abel')
scipiper::scmake('6_intro_gif_tasks.yml', remake_file = '6_visualize.yml', force = TRUE)
scipiper::scmake('6_visualize/log/6_intro_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_timestep_gif_tasks.yml', remake_file = '6_visualize.yml', force = TRUE)
scipiper::scmake('6_visualize/log/6_timestep_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_pause_gif_tasks.yml', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_visualize/log/6_pause_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_final_gif_tasks.yml', remake_file = '6_visualize.yml', force = TRUE)
scipiper::scmake('6_visualize/log/6_final_gif_tasks.ind', remake_file = '6_visualize.yml', force=TRUE)
scipiper::scmake('6_visualize/out/year_in_review.mp4', remake_file = '6_visualize.yml', force = TRUE)
video_file <- "6_visualize/out/year_in_review.mp4"
video_title_covered <- "6_visualize/tmp/no_title.mp4"
video_map_only <- "6_visualize/tmp/map_only.mp4"
video_map_square <- "6_visualize/tmp/map_square.mp4"
video_datewheel <- "6_visualize/tmp/datewheel.mp4"
video_legend <- "6_visualize/tmp/legend.mp4"
video_title <- "6_visualize/tmp/title.mp4"
video_footnote <- "6_visualize/tmp/footnote.mp4"
video_logo <- "6_visualize/tmp/logo.mp4"
video_stitched <- "6_visualize/tmp/stitched.mp4"
video_intro <- "6_visualize/tmp/intro.mp4"
video_outro <- "6_visualize/tmp/outro.mp4"
video_stitched_full_length <- "6_visualize/tmp/stitched_full.mp4"
version_info <- "river_conditions_jan_mar_2022"
video_insta <- sprintf("6_visualize/out/%s_insta.mp4", version_info)
reg_animation_start <- 4 # seconds into animation that map is first shown
reg_animation_end <- 48 # seconds into animation that map is last shown
insta_dim <- 600 # square shape
viz_config <- yaml::yaml.load_file("viz_config.yml")
width <- viz_config[["width"]]/2
height <- viz_config[["height"]]/2
## Create a video that contains only the datewheel
# Find wheel location
wheel_radius <- viz_config[["datewheel_cfg"]][["wheel_per"]]*width/2
wheel_center_x <- viz_config[["datewheel_cfg"]][["x_pos"]]*width
wheel_center_y <- viz_config[["datewheel_cfg"]][["y_pos"]]*height
# Now crop out just wheel
buffer<-1.05
system(sprintf(
'ffmpeg -y -i %s -vf "crop=%s:%s:%s:%s" %s',
video_file,
wheel_radius*2*buffer, # diameter of wheel
wheel_radius*2*buffer,
wheel_center_x - wheel_radius*buffer,
wheel_center_y + wheel_radius*1.65,
video_datewheel
))
## Create a video that contains only the legend
# Find legend location
legend_guess_width <- 0.10*width #10% width of video
legend_guess_height <- 0.35*height #30% height of video
legend_x <- viz_config[["legend_cfg"]][["x_pos"]]*width
legend_y <- viz_config[["legend_cfg"]][["y_pos"]]*height
# Now crop out just legend and scale to be bigger
system(sprintf(
'ffmpeg -y -i %s -vf "crop=%s:%s:%s:%s" %s',
video_file,
legend_guess_width,
legend_guess_height,
legend_x - legend_guess_width/1.5,
height - legend_y*1.02,
video_legend
))
## Create a video that contains only the title
# Find title location
title_guess_width <- wheel_radius*2*1.46 # diameter of wheel + some
title_guess_height <- 0.17*height #20% height of video
title_x <- viz_config[["title_cfg"]][["x_pos"]]*width
title_y <- viz_config[["title_cfg"]][["y_pos"]]*height
# Now crop out just title
system(sprintf(
'ffmpeg -y -i %s -vf "crop=%s:%s:%s:%s" %s', # scale=%s:-1
video_file,
title_guess_width,
title_guess_height,
0,
0,
# width/3, # Scale to fit 1/3 of the final video
video_title
))
# Create a video that contains only the footnote
# Find logo location
footnote_guess_width <- 0.60*width #10% width of video
footnote_guess_height <- 0.05*height #3% height of video
footnote_x <- viz_config[["footnote_cfg"]][["x_pos"]]*width
footnote_y <- viz_config[["footnote_cfg"]][["y_pos"]]*height
# Now crop out just logo
system(sprintf(
'ffmpeg -y -i %s -vf "crop=%s:%s:%s:(ih-%s)" %s', # scale=%s:-1
video_file,
footnote_guess_width,
footnote_guess_height,
footnote_x,
footnote_y,
video_footnote
))
## Crop video to create a map version
# Title bleeds into map a bit, so need to cover title part with drawbox
system(sprintf(
'ffmpeg -y -i %s -vf "drawbox=x=0:y=0:w=%s:h=%s:t=max:color=white" %s',
video_file,
title_guess_width,
title_guess_height,
video_title_covered
))
# Find edge of map
x_pos <- viz_config[["footnote_cfg"]][["x_pos"]]
map_info_cutoff <- width*x_pos*0.90
map_guess_width <- width*0.992 - map_info_cutoff
map_guess_height <- height - footnote_guess_height
# Now crop to map
system(sprintf(
'ffmpeg -y -i %s -vf "crop=%s:%s:%s:%s" %s',
video_title_covered,
map_guess_width,
map_guess_height,
map_info_cutoff,
0,
video_map_only
))
## Make map video a square with white space on top by
# increasing height to be the same as width
system(sprintf(
'ffmpeg -y -i %s -vf "pad=iw:iw:(ow-iw)/2:0:color=white" %s',
video_map_only,
video_map_square
))
# Create a video that contains only the logo
# Find logo location
logo_guess_width <- wheel_radius*2*1.2 # diameter of wheel
logo_guess_height <- 0.10*height #10% height of video
logo_x <- 0
logo_y <- 0
# Now crop out just logo
system(sprintf(
'ffmpeg -y -i %s -vf "crop=%s:%s:%s:%s" %s', # scale=%s:-1
video_file,
logo_guess_width,
logo_guess_height,
0,
height,
video_logo
))
center_under_map <- sprintf("%s + (H-%s)/2", map_guess_height, map_guess_height)
center_under_map_with_another <- sprintf("%s + (H-%s)/3", map_guess_height, map_guess_height)
# Overlay these videos on top of existing video
# And cut out intro & outro (reg animation starts at 4 seconds, ends at 45)
system(sprintf(
'ffmpeg -y -i %s -i %s -i %s -i %s -i %s -i %s -filter_complex "overlay=%s:%s,overlay=%s:%s,overlay=%s:%s,overlay=%s:%s,overlay=%s:%s,scale=%s:-1" -ss 00:00:%s -t 00:00:%s %s',
video_map_square,
video_datewheel,
video_legend,
video_title,
video_logo,
video_footnote,
# Add date wheel
"(W-(W/3))",# Center in right half
sprintf("%s - (h/2)", center_under_map), # Center in white space below map
# Add legend
"(W/2)-(w/4)", # Center (but ever so slightly to the right since the title is wider than the wheel)
sprintf("%s - (h/2)", center_under_map), # Center in white space below map
# Add title
"(W*0.05)",# Just in from the left
sprintf("%s - (h/2)", center_under_map_with_another), # Center in white space below map & above logo
# Add logo
"(W*0.05)",# Left
sprintf("%s + (H-%s)*2/3 - (h/2)", map_guess_height, map_guess_height), # Center in white space below map & below title
# Add footnote
sprintf("(W/2)-(w/2)", footnote_guess_width), # Center
sprintf("(H-%s)", footnote_guess_height*1.1), # Just up from bottom
insta_dim,
sprintf("%02d", reg_animation_start), # start animation
sprintf("%02d", reg_animation_end-reg_animation_start), # end animation
video_stitched
))
# Need intro text centered
# So cutting video and then adding in at the beginning
system(sprintf(
'ffmpeg -y -i %s -ss 00:00:00 -t 00:00:%s -vf "crop=iw:(ih-%s):0:0,pad=iw:iw:(ow-iw)/2:(oh-ih)/2:color=white,scale=%s:-1" %s', # scale=%s:-1
video_file,
sprintf("%02d", reg_animation_start-1),
logo_guess_height,
insta_dim,
video_intro
))
# Now do the same thing to the outro
system(sprintf(
'ffmpeg -y -i %s -ss 00:00:%s -vf "crop=iw:(ih-%s):0:0,pad=iw:iw:(ow-iw)/2:(oh-ih)/2:color=white,scale=%s:-1" %s', # scale=%s:-1
video_file,
sprintf("%02d", reg_animation_end+1),
logo_guess_height,
insta_dim,
video_outro
))
# Bring them all together
files_to_cat_fn <- "6_visualize/tmp/videos_to_concat.txt"
writeLines(sprintf("file '%s'", c(basename(video_intro), basename(video_stitched), basename(video_outro))), files_to_cat_fn)
system(sprintf(
'ffmpeg -y -safe 0 -f concat -i %s -c copy %s',
files_to_cat_fn,
video_stitched_full_length
))
# Now edit to be 29 fps as we learned 1/21/2021 - Insta won't let you post too low of an fps
system(sprintf(
'ffmpeg -y -i %s -r 29 %s',
video_stitched_full_length,
video_insta
))
Do this by adding one single still image before the video
version_info <- "river_conditions_jan_mar_2022"
# Make video with still image before
viz_config <- scmake("viz_config")
frame_to_use_t <- 38 # TODO: pick the frame you want
video_reddit <- sprintf("6_visualize/out/%s_reddit.mp4", version_info)
video_in <- sprintf("6_visualize/out/%s_twitter.mp4", version_info)
video_still_frame <- "6_visualize/tmp/video_still_frame.mp4"
# First, cut out just this frame from video
system(sprintf(
'ffmpeg -y -i %s -ss 00:00:%s -t 00:00:00.5 %s',
video_in,
sprintf("%02d", frame_to_use_t),
video_still_frame
))
# Then, add to video
# Bring them all together
file.copy(video_in, sprintf("6_visualize/tmp/%s", basename(video_in)))
files_to_cat_fn <- "6_visualize/tmp/videos_to_concat.txt"
writeLines(sprintf("file '%s'", c(basename(video_still_frame), basename(video_in))), files_to_cat_fn)
system(sprintf(
'ffmpeg -y -safe 0 -f concat -i %s -c copy %s',
files_to_cat_fn,
video_reddit
))