Skip to content

Instantly share code, notes, and snippets.

@archagon
Last active October 12, 2015 22:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save archagon/4094122 to your computer and use it in GitHub Desktop.
Save archagon/4094122 to your computer and use it in GitHub Desktop.
Quickly converts h264 videos into an iOS-compatible format by only re-encoding the audio. (I wasn't aware of any software that did this when I first wrote the script. Recently, I discovered that MP4tools does the job quite nicely, and additionally Subler works great for retrieving metadata and converting subtitles. As a result, I'm abandoning wo…
#!/usr/bin/python
# This script takes a video and remuxes it as an iOS-compatible video, re-encoding the audio if necessary.
# Note that the video WILL NOT be re-encoded. The script will fail if the video is not h264.
#
# Requires ffmpeg, ffprobe, and afconvert to be installed.
import re
import os
import sys
import subprocess
script_name_color = "\033[1;33m"
script_color_reset = "\033[0m"
def colorise(text):
return script_name_color + text + script_color_reset
def error(msg):
script_name = os.path.basename(__file__)
full_msg = colorise("[%s]") + " Error: %s"
print full_msg % (script_name, msg)
sys.exit(1)
def warning(msg):
script_name = os.path.basename(__file__)
full_msg = colorise("[%s]") + " Warning: %s"
print full_msg % (script_name, msg)
def message(msg):
script_name = os.path.basename(__file__)
full_msg = colorise("[%s]") + " %s"
print full_msg % (script_name, msg)
def file_exists(filename):
try:
with open(filename) as f:
return True
except IOError as e:
return False
filename = None
output_name = None
output_path = None
# TODO: keep existing audio flag; maybe different modes for remux, convert, keep all?
metadata = None
#####################################################################
# Step 0: Check that video file exists and setup initial variables. #
#####################################################################
if (len(sys.argv) < 2 or len(sys.argv) > 3):
error("invalid number of arguments")
filename = sys.argv[1]
if (len(sys.argv) == 3):
output_path = sys.argv[2]
output_name = os.path.split(output_path)[1]
output_path = os.path.split(output_path)[0]
else:
output_path = filename
output_name = os.path.splitext(os.path.split(output_path)[1])[0] + ".ios" + ".mp4"
output_path = os.path.split(output_path)[0]
message("outputting as " + output_name + " to " + (output_path if len(output_path) > 0 else os.getcwd()))
if not file_exists(filename):
error("file does not exist")
###########################
# Step 1: Get video info. #
###########################
info_args = ["ffprobe", "-show_streams", filename]
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("ffprobe returned a non-zero error code")
metadata = []
info_in_stream = False
for line in info_out.splitlines():
if not info_in_stream:
match = re.match(r"\s*\[STREAM\]\s*", line)
if match:
info_in_stream = True
metadata.append({})
else:
match = re.match(r"\s*\[[/]STREAM\]\s*", line)
if match:
info_in_stream = False
else:
match = re.match(r"\s*codec[_]name\s*[=]\s*(.*)\s*", line)
if match:
metadata[-1]["codec_name"] = match.group(1)
continue
match = re.match(r"\s*codec[_]type\s*[=]\s*(.*)\s*", line)
if match:
metadata[-1]["codec_type"] = match.group(1)
continue
match = re.match(r"\s*TAG[:]language\s*[=]\s*(.*)\s*", line)
if match:
metadata[-1]["language"] = match.group(1)
match = re.match(r"\s*channels\s*[=]\s*(.*)\s*", line)
if match:
metadata[-1]["channels"] = int(match.group(1))
# TODO: streams have "index" field; does it matter?
has_non_h264 = False
for i in range(len(metadata)):
stream = metadata[i]
if stream["codec_type"] == "audio":
output = "stream %d has %s with codec %s (%d channels)" + (" and language %s" if stream["language"] else "")
output_parameters = (i, stream["codec_type"], stream["codec_name"], stream["channels"]) + ((stream["language"],) if stream["language"] else ())
message(output % output_parameters)
else:
output = "stream %d has %s with codec %s" + (" and language %s" if stream["language"] else "")
output_parameters = (i, stream["codec_type"], stream["codec_name"]) + ((stream["language"],) if stream["language"] else ())
message(output % output_parameters)
if stream["codec_type"] == "video" and stream["codec_name"] != "h264":
has_non_h264 = True
if has_non_h264:
error("file has non-h264 video stream")
################################################################
# Step 2: If audio is not AAC, demux audio and convert to AAC. #
################################################################
# yes, I know that ffmpeg has aac converting but I wanted to use the native (non-reverse-engineered) afconvert
# note that if the audio is aac with more than 2 channels, there may be audible quality loss from transcoding aac -> aac
stream_directory_name = os.path.splitext(output_name)[0] + "." + "streams"
stream_directory_filename = os.path.join(output_path, stream_directory_name)
if not os.path.exists(stream_directory_filename):
os.makedirs(stream_directory_filename)
# TODO: remove this directory after next step
for i in range(len(metadata)):
stream = metadata[i]
# TODO: check for frequency
# TODO: I'd prefer to use the afconvet demuxer, but I can't seem to get it to work;
# last I tried, it only left the back channels and ignored the front channels
if stream["codec_type"] == "audio" and (stream["codec_name"] != "aac" or stream["channels"] > 2):
message("demuxing audio stream %d..." % (i))
demux_name = os.path.splitext(output_name)[0] + "." + str(i) + ".wav"
demux_filename = os.path.join(stream_directory_filename, demux_name)
demux_audio_args = ["ffmpeg", "-async", "1", "-i", filename, "-ac", "2", "-y", demux_filename]
demux_audio_call = subprocess.Popen(demux_audio_args)
demux_audio_return = demux_audio_call.wait()
if demux_audio_return != 0:
warning("ffmpeg audio demux returned a non-zero error code")
if not file_exists(demux_filename):
error("demuxed audio file does not exist for stream " + str(i))
message("re-encoding audio stream %d..." % (i))
audio_aac_name = "__" + os.path.splitext(filename)[0] + "." + str(i) + ".m4a"
encode_args = ["afconvert", "--verbose", "--data", "aac", "--bitrate", "256000", "--quality", "127", "--strategy", "2", demux_filename, audio_aac_name]
encode_call = subprocess.Popen(encode_args)
encode_return = encode_call.wait()
if encode_return != 0:
warning("afconvert audio encode returned a non-zero error code")
stream["aac_name"] = audio_aac_name
os.remove(demux_filename)
################################################
# Step 3: Run ffmpeg to create the final file. #
################################################
message("creating final file...")
args_input = ["ffmpeg", "-i", filename]
args_map = []
args_last = ["-vcodec", "copy", "-acodec", "copy", "-scodec", "copy", "-y", output_name]
non_aac_streams_found = 0
for i in range(len(metadata)):
stream = metadata[i]
if "aac_name" in stream:
non_aac_streams_found += 1
args_input += ["-i", stream["aac_name"]]
args_map += ["-map", str(non_aac_streams_found)+":0"]
else:
# TODO: make subtitles work
if stream["codec_type"] != "subtitle":
args_map += ["-map", "0:"+str(i)]
args = args_input + args_map + args_last
output_call = subprocess.Popen(args)
output_return = output_call.wait()
if output_return != 0:
warning("ffmpeg remux returned a non-zero error code")
for i in range(len(metadata)):
stream = metadata[i]
if "aac_name" in stream:
os.remove(stream["aac_name"])
message("all done!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment