Last active
May 28, 2025 15:33
-
-
Save flanter21/6fd9d7e16b2d412c40b15504e16a6d88 to your computer and use it in GitHub Desktop.
Convert mediasite slides into video
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 argparse | |
import json | |
import os | |
from PIL import Image | |
def calculate_duration(self, start_frame: int, frames_later: int, timebase: int) -> int: | |
return round((self[start_frame + frames_later] - self[start_frame])/1000 * timebase) | |
def create_slideshow(images: dict[str,int], video_duration: int, image_folder: str = "slides", end_time: int = -1, | |
start_time: int = 0, fps: int = 60, max_interval: int = 0, frameskip: int = 0) -> str: | |
""" | |
Creates a concat.txt for a slideshow video from a list of images. | |
Parameters: | |
images (dict): For each image: name, timestamp at which it is displayed (in milliseconds) | |
image_folder (str): Name of folder containing images | |
fps (int): Frames per second of the output video | |
Mediasite stores time in milliseconds, so multiple of 10 are good approximates, however most videos appear to have originally been 60fps | |
overwrite (bool): Overwrite file with an existing name? | |
max_interval (float): Will combine the durations of frames under the first frame | |
""" | |
output = '' | |
time = 0 # Stores time in milliseconds | |
image_timestamps = list(images.values()) + [video_duration] | |
if end_time <= 0: | |
end_time = video_duration/1000 | |
current_length = 0 # For max_interval | |
image_durations = {} | |
for i, current_image in enumerate(images): | |
time += calculate_duration(image_timestamps, i, 1, fps) | |
# Handle first frame if different start time given | |
if time/fps >= start_time and len(image_durations) == 0: | |
image_durations[current_image] = time - start_time * fps | |
continue | |
# For final slide | |
if time/fps >= end_time and not max_interval: | |
image_durations[current_image] = round(end_time - image_timestamps[i]/1000) * fps | |
break | |
elif time/fps >= end_time and max_interval: | |
image_durations[-1] = round(end_time - images[len(image_durations - 1)]/1000) * fps | |
# Both together is not supported | |
if max_interval and frameskip: | |
raise KeyError | |
# Max interval mode | |
elif max_interval and time/fps >= start_time and time/fps < end_time: | |
current_image_duration = calculate_duration(image_timestamps, i, 1, fps) | |
current_length += current_image_duration | |
if current_length >= max_interval * fps: | |
image_durations[-1] = current_length | |
current_length = 0 | |
continue | |
# Frameskip mode | |
elif frameskip: | |
if i % frameskip != 0: | |
continue | |
# Normal mode | |
elif not (frameskip or max_interval) and len(image_durations) > 0: | |
image_durations[current_image] = calculate_duration(image_timestamps, i, 1, fps) | |
# Loop through images and create concat txt for ffmpeg | |
for current_image in image_durations: | |
output = f"{output}file '{os.path.join(image_folder, current_image)}'\nduration {image_durations[current_image]}\n" | |
# For final slide | |
output = f"{output}file '{os.path.join(image_folder, current_image)}'\n" | |
# Output expected length of video | |
time = end_time - start_time | |
h = time // 3600 | |
m = time % 3600 // 60 | |
s = time % 60 // 1 | |
f = (time % 1) * fps | |
print ("Total duration: " + str(h) + " hours " + str(m) + " minutes " + str(s) + " seconds " + str(f) + " frames") | |
return output | |
def check_images(image_files: list[str], image_folder: str = 'slides', old_image_folder: str = 'original_images', overwrite: bool = False): | |
''' | |
Check all images are same resolution and if not, resize the images. | |
Parameters: | |
image_files (list): List of image files to process | |
image_folder (str): Where the images are located | |
overwrite (bool): Overwrite file with an existing name? | |
''' | |
# Classify images by size | |
images = {} | |
for image in image_files: | |
current_size = Image.open(f'{image_folder}/{image}').size # Contains dimensions of currently opened image | |
if current_size not in images.keys(): | |
images[current_size] = [] | |
images[current_size].append(image) | |
# Find most common resolution | |
most_common_size = max(images.keys()) | |
print("The most common resolution is", most_common_size) | |
# Remove most common resolution | |
images.pop(most_common_size) | |
if len(images): | |
print("These images are not the correct size:") | |
for k, v in images.items(): | |
print (k, v) | |
for images_of_current_resolution in images.values(): | |
for current_image in images_of_current_resolution: | |
current_image_path = os.path.join(image_folder, current_image) | |
moved_image_path = os.path.join(image_folder, old_image_folder, os.path.basename(current_image_path)) | |
if decide_overwrite(moved_image_path, overwrite): | |
image_resized = Image.open(current_image_path).resize(most_common_size) | |
os.makedirs(os.path.join(image_folder, old_image_folder), exist_ok=True) | |
os.rename(current_image_path, moved_image_path) | |
print("Moved", current_image, "to", moved_image_path) | |
image_resized.save(current_image_path) | |
print("Resized", image) | |
def get_image_timestamps(file_name: str = "GetPlayerOptions.json") -> tuple[dict[str, int], int]: | |
''' | |
Get time at which each image is displayed from a GetPlayerOptions.json | |
Parameters: | |
file (str): Give path to GetPlayerOptions.json | |
Returns: | |
images (dict): For each image - name, timestamp at which it is no longer displayed | |
duration (int): Duration in ms (unless mediasite changes their format) | |
''' | |
with open(file_name) as file: | |
presentation_data = json.load(file)['d']['Presentation'] # returns JSON object as a dictionary | |
duration = presentation_data['Duration'] | |
images = {} | |
# Iterating through the json list | |
for stream in presentation_data['Streams']: | |
if stream['HasSlideContent']: | |
for i, current in enumerate (stream['Slides']): | |
current_image = 'slide_{:04d}.jpg'.format(i+1) | |
images[current_image] = current['Time'] | |
# Handle first part, if there is no slide yet | |
if stream['Slides'][0]['Time'] != 0: | |
images['slide_0001.jpg'] = 0 | |
return images, duration | |
def decide_overwrite(file_name: str, overwrite: False): | |
while os.path.isfile(file_name): | |
overwrite = input(f"{file_name} already exists. Do you want to overwrite over it? Enter 'overwrite' or 'skip' ") | |
if overwrite == 'overwrite': | |
return True | |
elif overwrite == 'skip': | |
return False | |
return True | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser() | |
parser.add_argument('-o', '--output', default="concat.txt") | |
parser.add_argument('-ss', '--start', type=int, default=0, help='Desired start time for video') | |
parser.add_argument('-t', '--time', type=int, default=-1, help='Desired end time for video') | |
parser.add_argument('-f', '--overwrite', action='store_true', default=False, help='Specify if you want it to overwrite existing files without asking.') | |
args = parser.parse_args() | |
image_timestamps, video_duration = get_image_timestamps() | |
image_files = list(image_timestamps.keys()) | |
check_images(image_files) | |
if decide_overwrite(args.output, args.overwrite): | |
with open (args.output, "w+", encoding="utf-8") as file: | |
file.write(create_slideshow(image_timestamps, video_duration, "slides", args.time, args.start)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note you may want to use
--cookies-from-browser chrome
instead since session cookies can be retrieved from chrome, allowing more videos to be downloaded.