Custom Content Generation for PreShow
I would like to preface this post by thanking Matt for his guidance and advice in producing the PreShow I was searching for. Essentially what I wanted was an experience which was personal to me but also different every time so I wouldn't get bored of watching it. So I split it up into each component I was looking for. This may be different for you but for the purposes of this post we will follow what I did. My components were as follows:
- A welcome message recorded by one of my friends randomly selected as the narrator for this preshow
- A random advertisement
- A message from the narrator introducing the trailers
- Trailers
- A message from the narrator introducing the main event
In order to achieve this in PreShow it would be very straightforward to put a bunch of videos in each of the directories and have it randomly select one but this wouldn't really follow a theme or thread throughout the preshow that I was looking for. So in order to achieve this I found myself a selection of nice looking abstract backgrounds and some nice sounding background music creating 10 second and 30 second versions of each. This way it would be possible to choose one background, one piece of music and one narrator to create our preshow. However there were a few challenges to overcome. PreShow requires the content to be in the directory before the start of the show, so any script which created the content during a sequence would not work as the content wouldn't exist as far as the step to play it is concerned. This also isn't helped by the fact that PreShow doesn't await the completion of any action so a sleep command would be the only way to make preshow wait for us. There is also a final nail in the coffin in that any python dependencies cannot be resolved via the ScriptCommand as Kodi is running its own very unique version of python in which dependencies are imported via addons and many libraries in python do not have Kodi addon equivalents. So where do we go from here?
Introducing Flask: Flask is a library in python that allows you to create a web application very easily. For our usage we can see it as a way for Kodi to tell our code to run by sending a web request. By running a HTTPCommand action we can reach our web endpoint and our code runs in our own version of python with our own dependencies. But this doesn't resolve the content having to be there before the preshow starts. Well, it can. If instead of running our action at the start of our show we run it after our preshow (and before our feature) we can generate the next preshow while watching the current feature. This is nifty because our generation will have completed by the time we have finished our movie, ready for our next showing - whenever that may be. There is one thing to note here, when generating our content our file names must be static ie. the same each time we generate a new one, this is to avoid the possibility of PreShow not updating its directories and attempting to play a file that no longer exists, if the file names are the same PreShow does not know the difference. So, without further ado, here is my flask code to generate a random preshow:
import os, shutil, random, logging, time
from moviepy.editor import *
from flask import Flask
app = Flask(__name__)
preshow_dir = "WHEREVER_YOUR_PRESHOW_CONTENT_DIRECTORY_IS"
preshow_bumper_dir = preshow_dir + "/Video Bumpers"
@app.route("/", methods=["POST"])
def update_preshow():
print("Updating Preshow", flush=True)
"""
Delete any files in the following folders:
Theater Intro
Intermission
Trailers Intro
Feature Intro
"""
shutil.rmtree(preshow_bumper_dir + "/Theater Intro")
shutil.rmtree(preshow_bumper_dir + "/Intermission")
shutil.rmtree(preshow_bumper_dir + "/Trailers Intro")
shutil.rmtree(preshow_bumper_dir + "/Feature Intro")
os.mkdir(preshow_bumper_dir + "/Theater Intro")
os.mkdir(preshow_bumper_dir + "/Intermission")
os.mkdir(preshow_bumper_dir + "/Trailers Intro")
os.mkdir(preshow_bumper_dir + "/Feature Intro")
print("Starting content generation", flush=True)
"""
Choose a random video background and music from Video Backgrounds folder - get both the 10 second and 30 second version
Choose a random background music for following items:
Theater Intro: 30 seconds
Trailers Intro: 10 seconds
Feature Intro: 10 seconds
"""
video_background = random.choice(os.listdir(preshow_dir + "/Custom Resources/Video Backgrounds/30 seconds"))
theater_intro_background = preshow_dir + "/Custom Resources/Video Backgrounds/30 seconds/" + video_background
trailers_intro_background = preshow_dir + "/Custom Resources/Video Backgrounds/10 seconds/" + video_background
feature_intro_background = preshow_dir + "/Custom Resources/Video Backgrounds/10 seconds/" + video_background
theater_intro_music = preshow_dir + "/Custom Resources/Background Music/30 seconds/" + random.choice(os.listdir(preshow_dir + "/Custom Resources/Background Music/30 seconds"))
trailers_intro_music = preshow_dir + "/Custom Resources/Background Music/10 seconds/" + random.choice(os.listdir(preshow_dir + "/Custom Resources/Background Music/10 seconds"))
feature_intro_music = preshow_dir + "/Custom Resources/Background Music/10 seconds/" + random.choice(os.listdir(preshow_dir + "/Custom Resources/Background Music/10 seconds"))
"""
Overlay random friend audio and selected background music (quiet) onto selected background for each of the following and save in corresponding folder:
Theater Intro
Trailers Intro
Feature Intro
"""
friend = random.choice(os.listdir(preshow_dir + "/Custom Resources/Friends"))
theater_intro_voice = preshow_dir + "/Custom Resources/Friends/" + friend + "/Theater Intro.wav"
trailers_intro_voice = preshow_dir + "/Custom Resources/Friends/" + friend + "/Trailers Intro.wav"
feature_intro_voice = preshow_dir + "/Custom Resources/Friends/" + friend + "/Feature Intro.wav"
theater_intro_background_clip = VideoFileClip(theater_intro_background)
theater_intro_music_clip = AudioFileClip(theater_intro_music).fx(afx.audio_normalize).fx(afx.volumex, 0.1)
theater_intro_voice_clip = AudioFileClip(theater_intro_voice).fx(afx.audio_normalize)
theater_intro_mix = CompositeAudioClip([theater_intro_music_clip, theater_intro_voice_clip])
theater_intro_final_clip = theater_intro_background_clip.set_audio(theater_intro_mix)
theater_intro_final_clip.write_videofile(preshow_bumper_dir + "/Theater Intro/Welcome.mp4")
trailers_intro_background_clip = VideoFileClip(trailers_intro_background)
trailers_intro_music_clip = AudioFileClip(trailers_intro_music).fx(afx.audio_normalize).fx(afx.volumex, 0.1)
trailers_intro_voice_clip = AudioFileClip(trailers_intro_voice).fx(afx.audio_normalize)
trailers_intro_mix = CompositeAudioClip([trailers_intro_music_clip, trailers_intro_voice_clip])
trailers_intro_final_clip = trailers_intro_background_clip.set_audio(trailers_intro_mix)
trailers_intro_final_clip.write_videofile(preshow_bumper_dir + "/Trailers Intro/Trailers Intro.mp4")
feature_intro_background_clip = VideoFileClip(feature_intro_background)
feature_intro_music_clip = AudioFileClip(feature_intro_music).fx(afx.audio_normalize).fx(afx.volumex, 0.1)
feature_intro_voice_clip = AudioFileClip(feature_intro_voice).fx(afx.audio_normalize)
feature_intro_mix = CompositeAudioClip([feature_intro_music_clip, feature_intro_voice_clip])
feature_intro_final_clip = feature_intro_background_clip.set_audio(feature_intro_mix)
feature_intro_final_clip.write_videofile(preshow_bumper_dir + "/Feature Intro/Feature Intro.mp4")
"""
Choose random friend and copy their advert into the Intermission Folder
"""
friend = random.choice(os.listdir(preshow_dir + "/Custom Resources/Friends"))
shutil.copyfile(preshow_dir + "/Custom Resources/Friends/" + friend + "/Advert.mp4", preshow_bumper_dir + "/Intermission/Advert.mp4")
print("Completed PreShow Update", flush=True)
return {}
And the action to run it is very simple (Assuming you are running your flask endpoint on localhost port 5000 (the default)
http://127.0.0.1:5000
POST: {}
All you have to do to get this working is ping the endpoint yourself once to generate the first batch and then add this one action to your sequence just before your feature. I hope this post gives you an idea of what it is possible to achieve with this awesome addon!