Skip to content

Instantly share code, notes, and snippets.

@Metal-666
Last active March 19, 2025 16:44
Show Gist options
  • Select an option

  • Save Metal-666/ca319f03ea0de8e8367e01a068f70437 to your computer and use it in GitHub Desktop.

Select an option

Save Metal-666/ca319f03ea0de8e8367e01a068f70437 to your computer and use it in GitHub Desktop.
My Reaper script for inserting playthrough footage into the timeline. Check the comments for usage info.
import os
import sys
from datetime import datetime, date, time, timedelta
# Config
FOOTAGE_DIRECTORY = "D:\\Files\\PlaythroughFootage" # <- this directory will be searched for the video clips
FOOTAGE_TRACK_NAME = "FOOTAGE" # <- the full footage track name will be: <SOURCE_TRACK_NAME> <FOOTAGE_TRACK_NAME>
FOOTAGE_FILE_EXTENSION = ".mkv" # <- used to filter the files found in the footage directory
FOOTAGE_LATENCY_COMPENSATION = 0.25 # <- shifts the inserted footage to the left by this amount (in seconds)
CURRENT_PROJECT = 0 # <- all operations will be done inside the first project tab
def main(): # <- most of the code is inside a function for easy script exiting using 'return'
# Setting up important variables
trackCount = RPR_GetNumTracks()
# Validating things
if not os.path.isdir(FOOTAGE_DIRECTORY):
RPR_ShowMessageBox(f"Path '{FOOTAGE_DIRECTORY}' does not point to a valid directory!", "Footage directory not found!", 0)
return
if trackCount == 0:
RPR_ShowMessageBox(f"The project does not contain any tracks!", "No source tracks!", 0)
return
# Using user input to choose the source track
userInput = RPR_GetUserInputs("Insert footage for track:", 1, "Track #", "", 512)
if not userInput[0]: # <- the first value tells us whether the input dialog was cancelled or not
return
sourceTrackNumber = int(userInput[4]) # <- the fifth value is the actual input
sourceTrackIndex = sourceTrackNumber - 1 # <- for convenience, we ask the user to enter the track number (they start at 1), then we convert it to the 0-based index
# Validating user input
if sourceTrackIndex < 0 or sourceTrackIndex >= trackCount:
RPR_ShowMessageBox(f"Track index {sourceTrackIndex} is out of range [0, {trackCount - 1}]!", "Track not found!", 0)
return
sourceTrack = RPR_GetTrack(CURRENT_PROJECT, sourceTrackIndex)
sourceTrackName = RPR_GetSetMediaTrackInfo_String(sourceTrack, "P_NAME", "", False)[3]
footageTrackIndex = trackCount # <- the footage track will be inserted at the end of the track list
# Creating the footage track
RPR_InsertTrackInProject(CURRENT_PROJECT, footageTrackIndex, 0)
footageTrack = RPR_GetTrack(CURRENT_PROJECT, footageTrackIndex)
RPR_GetSetMediaTrackInfo_String(footageTrack, "P_NAME", f"{sourceTrackName} {FOOTAGE_TRACK_NAME}", True)
RPR_SetMediaTrackInfo_Value(footageTrack, "B_MAINSEND", False) # <- effectively disables the audio on the track
RPR_SetOnlyTrackSelected(footageTrack) # <- selecting the footage track is required to insert media items into it
# Discovering the footage files
footageFiles = [] # <- will contain (filePath, fileCreationDatetime) tuples
for fileName in os.listdir(FOOTAGE_DIRECTORY):
if not fileName.endswith(FOOTAGE_FILE_EXTENSION):
continue
filePath = os.path.join(FOOTAGE_DIRECTORY, fileName)
creationDatetime = datetime.fromtimestamp(os.path.getctime(filePath))
footageFiles.append((filePath, creationDatetime))
footageFiles.sort(key = lambda x: x[1]) # <- files are sorted by date
footageFiles.reverse() # <- puts newest files at the front
errors = [] # <- a list of errors which occured during the footage insertion
# Iterating over the media items in the source track and inserting the corresponding footage clips into the footage track
for i in range(RPR_GetTrackNumMediaItems(sourceTrack)):
errorDetected = False
# First, let's determine the key parameters of each media item.
# These are Length, Position and Start Offset
sourceMediaItem = RPR_GetTrackMediaItem(sourceTrack, i)
sourceTakeIndex = int(RPR_GetMediaItemInfo_Value(sourceMediaItem, "I_CURTAKE"))
sourceTake = RPR_GetMediaItemTake(sourceMediaItem, sourceTakeIndex)
sourceTakeName = RPR_GetSetMediaItemTakeInfo_String(sourceTake, 'P_NAME', '', False)[3]
source = RPR_GetMediaItemTake_Source(sourceTake)
position = RPR_GetMediaItemInfo_Value(sourceMediaItem, "D_POSITION")
length = RPR_GetMediaItemInfo_Value(sourceMediaItem, "D_LENGTH")
startOffset = RPR_GetMediaItemTakeInfo_Value(sourceTake, "D_STARTOFFS")
sourceDate = RPR_GetMediaFileMetadata(source, "BWF:OriginationDate", "", 128)[3]
sourceTime = RPR_GetMediaFileMetadata(source, "BWF:OriginationTime", "", 128)[3]
sourceTrueButImpreciseDatetime = datetime.combine(date.fromisoformat(sourceDate), time.fromisoformat(sourceTime))
sourceFilePath = RPR_GetMediaSourceFileName(source, "", 512)[1]
sourceDatetime = datetime.fromtimestamp(os.path.getctime(sourceFilePath))
timeMismatchDetected = False
# Ideally we would want to use th embedded time metadata of the source file to figure out the offsets. Unfortunately, the BWF time tag only
# goes down to second precision, which is not good enough for our use case. Therefore we rely on the filesystem timestamp instead.
# The problem is, the file creation time reported by the filesystem can be incorrect (it can be changed manually; also lookup "filesystem tunneling")
# so we compare it with the embedded timestamp tag first. If the difference between two timestamps is greater than 1 second,
# we can assume something went wrong with the file creation time, so we'll fallback to the embedded timestamp
if (sourceTrueButImpreciseDatetime - sourceDatetime).total_seconds() > 1:
sourceDatetime = sourceTrueButImpreciseDatetime
timeMismatchDetected = True
errorDetected = True
# Now we search for the footage file, which contains the current media item. Remember, the files are sorted by date backwards,
# so we are effectively going back in time in search of the first video clip that was created before the media item (therefore will contain it).
# It is possible that the video file was cut short before the media item was recorded, so it would be good to add more checks here
# (video creation time + video duration < media creation time = video was cut short and does not contain the media item)
relevantFootageFile = None
for footageFile in footageFiles:
if footageFile[1] < sourceDatetime:
relevantFootageFile = footageFile
break
if relevantFootageFile is None:
errorDetected = True # <- technically unnecessary, but I'd still like to keep this here
errors.append(f"Failed to find video file for the media item #{i} ({sourceTakeName})")
continue
# How far into the video does the recorded media item start?
timeDifference = sourceDatetime - footageFile[1]
timeDifferenceSeconds = timeDifference.total_seconds() # <- this far
# Now that we have all the necessary data, let's insert the footage clip for the current media item
RPR_InsertMedia(relevantFootageFile[0], 0)
footageMediaItem = RPR_GetTrackMediaItem(footageTrack, RPR_GetTrackNumMediaItems(footageTrack) - 1)
footageTake = RPR_GetMediaItemTake(footageMediaItem, 0)
RPR_SetMediaItemPosition(footageMediaItem, position, True)
RPR_SetMediaItemLength(footageMediaItem, length, True)
RPR_SetMediaItemTakeInfo_Value(footageTake, "D_STARTOFFS", startOffset + timeDifferenceSeconds + FOOTAGE_LATENCY_COMPENSATION)
# If there was a time mismatch, we don't want to lock the media item, as the user will have to adjust it manually
if not timeMismatchDetected:
RPR_SetMediaItemInfo_Value(footageMediaItem, "C_LOCK", 1)
else:
errors.append(f"File creation time discrepancy detected in the media item #{i} ({sourceTakeName}). You'll have to manually adjust this item!")
# Finally, let's set the footage media item's color
mediaItemColor = None
if errorDetected:
mediaItemColor = RPR_ColorToNative(243, 139, 168) # <- if there was an error, we'll color it red
else:
mediaItemColor = RPR_ColorToNative(166, 227, 161) # <- if there wasn't one, we'll color it green
RPR_SetMediaItemInfo_Value(footageMediaItem, "I_CUSTOMCOLOR", mediaItemColor|0x1000000)
# Script execution report
if len(errors) > 0:
RPR_ShowMessageBox(f"The following errors occured while inserting the footage:\n- {'\n- '.join(errors)}", "Done!", 0)
else:
RPR_ShowMessageBox("All footage inserted successfully!", "Done!", 0)
RPR_SetEditCurPos2(CURRENT_PROJECT, 0, True, True) # <- moves the playhead to the beginning of the project
main()
@Metal-666
Copy link
Author

Metal-666 commented Mar 11, 2025

Notes:

  • At the moment, the script mostly relies on file metadata (reported by the filesystem) to determine the time offsets. This is not ideal since files can be moved around and copied and stuff, but I don't know of any better way. OBS does not write any time metadata to the video files and you cannot tell it to specify milliseconds in filenames (seconds won't give us enough accuracy). Reaper does write some metadata to the .wav files but the precision ends at seconds, so we can't rely on that either. It is however used as a fallback in case the difference between the embedded timestamp and filesystem timestamp is too large (could indicate that the file metadata was altered, or filesystem tunneling occured)

@Metal-666
Copy link
Author

Metal-666 commented Mar 11, 2025

Screenshots:

  1. Before running the script. Some audio was recorded, then chopped up and moved around.
    Screenshot 2025-03-11 201857

  2. After running the script. The video footage was inserted correctly (you can tell by the matching audio waveforms)
    Screenshot 2025-03-11 202142_edited

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment