Skip to content

Instantly share code, notes, and snippets.

@archagon
Last active March 10, 2023 13:44
Show Gist options
  • Save archagon/5234347 to your computer and use it in GitHub Desktop.
Save archagon/5234347 to your computer and use it in GitHub Desktop.
A quick and dirty script to convert GDC Vault videos to a pleasant mobile viewing format.
# This script continues the work of gdc-downloader.py and actually combines the video and audio
# into a single video file. The underlay.png file specifies the dimensions of the video.
# Personally, I use an all-black 1024x768 rectangle for optimal iPad viewing.
# As with gdc-downloader.py, code quality is crappy and quickly assembled to work for my
# nefarious purposes.
# Usage is as follows:
#
# gdc-encoder.py [video name] [video path] [output name] [GDC name]
#
# Example call: python gdc-encoder.py CoolGDCVideo2011 ~/Videos/GDC/CoolVideo/ CoolGDCVideoIpad "GDC 2012E"
# You need to have ffmpeg installed in order for this script to work. I recommend macports. If you
# port install ffmpeg with the +nonfree option, you'll get access to the faac encoder instead of the
# default experimental one.
# If you want chapter or Media Kind support for optimal iPhone/iPad viewing pleasure,
# be sure to install SublerCLI and point the constant below to the binary. (Note: this currently
# doesn't work for me, so I do this step manually with the Subler GUI.)
# It seems that chapter marks only work if the Media Kind (which you can set in Subler) is
# set to one of the Movie types. If the Media Kind is set to TV Show, then you can't use
# the chapter marks in Apple's iOS video player.
#############
# Constants #
#############
subler_path = "/This/Is/A/Test/Path/SublerCLI"
underlay_path = "/Users/archagon/Dropbox/Programming"
underlay_name = "underlay.png"
language_codes = {"en":"eng", "jp":"jpn"}
########
# Code #
########
import sys
import os
import subprocess
import re
import datetime
import time
import dateutil.parser as parser
from xml.dom import minidom
def error(message):
print "[gdc-downloader] Error: " + message
sys.exit(1)
def message(msg):
print "[gdc-downloader] Message: " + msg
def assert_file_exists(filename, path):
abs_path = os.path.join(os.path.abspath(path), filename)
if not os.path.exists(abs_path):
error(abs_path + " not found")
def check_dependencies():
assert_file_exists(underlay_name, underlay_path)
# TODO: check ffmpeg, subler
def seconds_from_HHMMSS(string):
split_string = string.split(':')
return int(split_string[0]) * 3600 + int(split_string[1]) * 60 + int(split_string[2])
def seconds_to_HHMMSS(seconds_total):
seconds = seconds_total % 60
minutes = (seconds_total / 60) % 60
hours = (seconds_total / (60 * 60))
output_string = "%02d" % hours
output_string += ":"
output_string += "%02d" % minutes
output_string += ":"
output_string += "%02d" % seconds
return output_string
def parse_metadata(filename_xml, season_name):
metadata = {}
xml_file = open(filename_xml, "r")
xml = xml_file.read()
xml_file.close()
parsed_xml = minidom.parseString(xml)
if not parsed_xml:
return metadata
metadata_xml = parsed_xml.getElementsByTagName("metadata")
if len(metadata_xml) != 1:
return metadata
metadata_xml = metadata_xml[0]
# these tags will go directly into the metadata
if (len(metadata_xml.getElementsByTagName("title")) == 1):
title = metadata_xml.getElementsByTagName("title")[0].firstChild.nodeValue
metadata["title"] = title
if (len(metadata_xml.getElementsByTagName("speaker")) == 1):
artist = metadata_xml.getElementsByTagName("speaker")[0].firstChild.nodeValue
metadata["artist"] = artist
if (len(metadata_xml.getElementsByTagName("date")) == 1):
date = metadata_xml.getElementsByTagName("date")[0].firstChild.nodeValue
date_parsed = (parser.parse(date))
metadata["date"] = date_parsed.isoformat()
if (len(metadata_xml.getElementsByTagName("id")) == 1):
id = metadata_xml.getElementsByTagName("id")[0].firstChild.nodeValue
metadata["episode_id"] = id
# these tags are for calculating the start offset in the script; convert from HH:MM:SS to seconds
if (len(metadata_xml.getElementsByTagName("startTime")) == 1):
start_time = metadata_xml.getElementsByTagName("startTime")[0].firstChild.nodeValue
metadata["!start_time"] = seconds_from_HHMMSS(start_time)
if (len(metadata_xml.getElementsByTagName("endTime")) == 1):
end_time = metadata_xml.getElementsByTagName("endTime")[0].firstChild.nodeValue
metadata["!end_time"] = seconds_from_HHMMSS(end_time)
# a few global tags
# metadata["album_artist"] = season_name
metadata["album"] = season_name
metadata["show"] = season_name
metadata["network"] = "Game Developers Conference"
# chapters for later subler step
metadata["!chapters"] = ""
chapters_xml = parsed_xml.getElementsByTagName("chapters")
if len(chapters_xml) != 1:
return metadata
chapters_xml = chapters_xml[0]
chapters_string = ""
chapters = chapters_xml.getElementsByTagName("chapter")
index = 0
for chapter in chapters:
time = None
title = None
if (len(chapter.getElementsByTagName("time")) == 1):
time_sec = seconds_from_HHMMSS(chapter.getElementsByTagName("time")[0].firstChild.nodeValue)
# offset chapters by startTime (since we'll be cropping the video)
if metadata["!start_time"]:
time_sec = time_sec - metadata["!start_time"]
# make sure there's no negative time
if time_sec < 0:
time_sec = 0
time = seconds_to_HHMMSS(time_sec)
# we need an entry for time 0 for iOS compliance
if index == 0 and time_sec != 0:
chapters_string += "00:00:00.000 Start\n"
if (len(chapter.getElementsByTagName("title")) == 1):
title = chapter.getElementsByTagName("title")[0].firstChild.nodeValue
if time and title:
chapters_string += time + ".000 "
chapters_string += title
chapters_string += '\n'
index += 1
metadata["!chapters"] = chapters_string
return metadata
def does_ffmpeg_have_enc(enc):
info_args = ["ffmpeg", "-encoders"]
info_call = subprocess.Popen(info_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
info_return = info_call.wait()
info_out, info_err = info_call.communicate()
if info_return != 0:
warning("ffmpeg returned a non-zero error code")
for line in info_out.splitlines():
if line.find(" " + enc + " ") != -1:
return True
return False
# audio is a list with pairs of audiocode and filepath
def ffmpeg_combine(speaker, slides, output, audio, metadata):
args = ["ffmpeg"]
# speaker input (default audio)
args.append("-i")
args.append(speaker)
# slides input
args.append("-i")
args.append(slides)
# underlay input
args.append("-loop")
args.append("1")
args.append("-i")
args.append(os.path.join(os.path.abspath(underlay_path), underlay_name))
# optional audio inputs
if audio:
for [audio_code, audio_file] in audio:
args.append("-i")
args.append(audio_file)
# overlay the two videos over the underlay via complex filtergraph
args.append("-filter_complex")
# filtergraph = "\""
filtergraph = "[2:0] [0:0] overlay=20:main_h/2-overlay_h/2 [overlay];"
filtergraph += "[overlay] [1:0] overlay=main_w-overlay_w-20:main_h/2-overlay_h/2 [output]"
# filtergraph += "\""
args.append(filtergraph)
# map video + metadata
args.append("-map")
args.append("[output]:v")
args.append("-metadata:s:0")
args.append("language=eng")
# map original audio + metadata
args.append("-map")
args.append("0:a")
args.append("-metadata:s:1")
args.append("language=eng")
# args.append("-metadata:s:1")
# args.append("title=Default Audio")
# map optional audio + metadata
if audio:
current_input_stream = 3
current_output_stream = 2
for [audio_code, audio_file] in audio:
args.append("-map")
args.append(str(current_input_stream) + ":a")
args.append("-metadata:s:" + str(current_output_stream))
args.append("language=" + audio_code)
current_input_stream += 1
current_output_stream += 1
# map the global metadata
if metadata:
for key in metadata:
if not key.startswith("!"):
args.append("-metadata")
args.append(key + "=" + metadata[key])
# use experimental aac if libfaac is unavailable
if not does_ffmpeg_have_enc("libfaac"):
args.append("-strict")
args.append("experimental")
# up the frequency so it plays on iDevices
args.append("-ar")
args.append("44100")
# crop the video in accordance with the start and end times
if "!start_time" in metadata:
seconds = metadata["!start_time"]
args.append("-ss")
args.append(str(seconds))
if "!start_time" in metadata and "!end_time" in metadata:
start_seconds = metadata["!start_time"]
end_seconds = metadata["!end_time"]
args.append("-t")
args.append(str(end_seconds - start_seconds))
# args.append("-shortest")
args.append(output)
# ffmpeg is a hairy beast
message("ffmpeg call: " + str(args))
try:
retval = subprocess.call(args, stdin=None)
except Exception, e:
error("ffmpeg error")
def create_chapters_file(chapters_file, chapters_data):
chapters = open(chapters_file, "w")
chapters.write(chapters_data)
chapters.close()
# def subler_process(filename_input, filename_output, chapters_file):
# subler_abs_path = os.path.abspath(subler_path)
# subler_args = [subler_abs_path]
# subler_args.append("-source")
# subler_args.append(filename_input)
# subler_args.append("-dest")
# subler_args.append(filename_output)
# subler_args.append("-optimize")
# subler_args.append("-metadata")
# subler_args.append("{Media Kind:TV Show}")
# print subler_args
# try:
# retval = subprocess.call(subler_args, stdin=None)
# except Exception, e:
# error("subler error: " + str(e))
# # TODO: remove old vid
def encode_gdc_video(dir, name, out, gdc_name):
dest_path = os.path.abspath(dir)
filename_xml = os.path.join(os.path.abspath(dir), name + ".xml")
filename_slides = os.path.join(os.path.abspath(dir), name + "-slide.flv")
filename_speaker = os.path.join(os.path.abspath(dir), name + "-speaker.flv")
filename_output = os.path.join(os.path.abspath(dir), out + ".m4v")
filename_subler_output = os.path.join(os.path.abspath(dir), out + "-subl.m4v")
chapters_file = os.path.join(os.path.abspath(dir), out + "-chapters.txt")
assert_file_exists(name + ".xml", dir)
audios = [["en", os.path.join(os.path.abspath(dir), name + "-audio-en.flv")], ["jp", os.path.join(os.path.abspath(dir), name + "-audio-jp.flv")]]
for pair in audios:
code = pair[0]
if code in language_codes:
pair[0] = language_codes[code]
# Step 0: Check dependencies.
check_dependencies()
# Step 1: Parse the metadata.
metadata = parse_metadata(filename_xml, gdc_name)
# Step 2: Combine all the tracks into one m4v file in ffmpeg.
combined_file = ffmpeg_combine(filename_speaker, filename_slides, filename_output, audios, metadata)
# Step 3: Subler niceification.
if subler_path:
# Step 1: Create chapters file.
create_chapters_file(chapters_file, metadata["!chapters"])
# Step 2: Subler it.
# TODO: SublerCLI currently throws an error when I try to use it
# subler_process(filename_output, filename_subler_output, chapters_file)
message("All done!")
if __name__ == "__main__":
if len(sys.argv) == 5:
try:
encode_gdc_video(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])
except KeyboardInterrupt:
error("program interrupted")
else:
error("invalid number of arguments")
# TODO: proper inputs
# TODO: get rid of test paths
# TODO: correct video dimensions
# TODO: chapters don't show up?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment