Last active
March 19, 2025 16:44
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screenshots:
Before running the script. Some audio was recorded, then chopped up and moved around.

After running the script. The video footage was inserted correctly (you can tell by the matching audio waveforms)
