Add VP9/Opus support, use VP8 by default

- support vp9 and opus
- switch to 2 pass encoding
- use ffmpeg -movflags +faststart instead of qtfaststart
This commit is contained in:
j 2016-06-23 14:10:32 +00:00
parent aaacc48259
commit 4785f314cb
3 changed files with 94 additions and 53 deletions

View file

@ -142,6 +142,7 @@ def load_config(init=False):
formats = config.get('video', {}).get('formats') formats = config.get('video', {}).get('formats')
if set(old_formats) != set(formats): if set(old_formats) != set(formats):
sformats = supported_formats() sformats = supported_formats()
settings.FFMPEG_SUPPORTS_VP9 = 'vp9' in sformats
if sformats: if sformats:
for f in formats: for f in formats:
if f not in sformats or not sformats[f]: if f not in sformats or not sformats[f]:

View file

@ -57,7 +57,10 @@ def supported_formats():
return { return {
'ogg': 'libtheora' in stdout and 'libvorbis' in stdout, 'ogg': 'libtheora' in stdout and 'libvorbis' in stdout,
'webm': 'libvpx' in stdout and 'libvorbis' in stdout, 'webm': 'libvpx' in stdout and 'libvorbis' in stdout,
'vp8': 'libvpx' in stdout and 'libvorbis' in stdout,
'vp9': 'libvpx-vp9' in stdout and 'libopus' in stdout,
'mp4': 'libx264' in stdout and 'DEA.L. aac' in stdout, 'mp4': 'libx264' in stdout and 'DEA.L. aac' in stdout,
'h264': 'libx264' in stdout and 'DEA.L. aac' in stdout,
} }
@ -78,6 +81,8 @@ def stream(video, target, profile, info, audio_track=0, flags={}):
''' '''
profile, format = profile.split('.') profile, format = profile.split('.')
bpp = 0.17 bpp = 0.17
video_codec = 'libvpx'
audio_codec = 'libvorbis'
if 'error' in info: if 'error' in info:
return False, "Unsupported Format" return False, "Unsupported Format"
@ -140,11 +145,25 @@ def stream(video, target, profile, info, audio_track=0, flags={}):
else: else:
height = 96 height = 96
if settings.FFMPEG_SUPPORTS_VP9:
audio_codec = 'libopus'
video_codec = 'libvpx-vp9'
audiorate = 22050 audiorate = 22050
audioquality = -1 audioquality = -1
audiobitrate = '22k' audiobitrate = '22k'
audiochannels = 1 audiochannels = 1
if format == 'webm' and audio_codec == 'libopus':
audiorate = 48000
if not audiobitrate:
audiobitrate = '%sk' % {
-1: 32, 0: 48, 1: 64, 2: 96, 3: 112, 4: 128,
5: 144, 6: 160, 7: 192, 8: 256, 9: 320, 10: 512,
}[audioquality]
if format == 'webm' and video_codec == 'libvpx-vp9':
bpp = 0.15
if info['video'] and 'display_aspect_ratio' in info['video'][0]: if info['video'] and 'display_aspect_ratio' in info['video'][0]:
# dont make video bigger # dont make video bigger
height = min(height, info['video'][0]['height']) height = min(height, info['video'][0]['height'])
@ -197,11 +216,17 @@ def stream(video, target, profile, info, audio_track=0, flags={}):
] ]
if format == 'webm': if format == 'webm':
video_settings += [ video_settings += [
'-c:v', video_codec,
'-deadline', 'good', '-deadline', 'good',
'-cpu-used', '0', '-cpu-used', '1' if video_codec == 'libvpx-vp9' else '0',
'-lag-in-frames', '16', '-lag-in-frames', '25',
'-auto-alt-ref', '1', '-auto-alt-ref', '1',
] ]
if video_codec == 'libvpx-vp9':
video_settings += [
'-tile-columns', '6',
'-frame-parallel', '1',
]
if format == 'mp4': if format == 'mp4':
video_settings += [ video_settings += [
'-c:v', 'libx264', '-c:v', 'libx264',
@ -232,7 +257,9 @@ def stream(video, target, profile, info, audio_track=0, flags={}):
else: else:
video_settings += ['-map', '0:%s,0:%s' % (info['audio'][audio_track]['id'], n)] video_settings += ['-map', '0:%s,0:%s' % (info['audio'][audio_track]['id'], n)]
mono_mix = False mono_mix = False
audio_settings = ['-ar', str(audiorate), '-aq', str(audioquality)] audio_settings = ['-ar', str(audiorate)]
if audio_codec != 'libopus':
audio_settings += ['-aq', str(audioquality)]
if mono_mix: if mono_mix:
ac = 2 ac = 2
else: else:
@ -246,55 +273,63 @@ def stream(video, target, profile, info, audio_track=0, flags={}):
audio_settings += ['-ab', audiobitrate] audio_settings += ['-ab', audiobitrate]
if format == 'mp4': if format == 'mp4':
audio_settings += ['-c:a', 'aac', '-strict', '-2'] audio_settings += ['-c:a', 'aac', '-strict', '-2']
elif audio_codec == 'libopus':
audio_settings += ['-c:a', 'libopus', '-frame_duration', '60']
else: else:
audio_settings += ['-c:a', 'libvorbis'] audio_settings += ['-c:a', audio_codec]
else: else:
audio_settings = ['-an'] audio_settings = ['-an']
cmd = [settings.FFMPEG, cmds = []
'-nostats', '-loglevel', 'error',
'-y', '-i', video, '-threads', '4', '-map_metadata', '-1', '-sn'] \ base = [settings.FFMPEG,
+ audio_settings \ '-nostats', '-loglevel', 'error',
+ video_settings '-y', '-i', video, '-threads', '4', '-map_metadata', '-1', '-sn']
if format == 'webm': if format == 'webm':
enc_target = target + '.tmp.webm' enc_target = target + '.tmp.webm'
elif format == 'mp4':
enc_target = target + '.tmp.mp4'
else: else:
enc_target = target enc_target = target
if format == 'webm': if format == 'webm':
cmd += ['-f', 'webm', enc_target] post = ['-f', 'webm', enc_target]
elif format == 'mp4': elif format == 'mp4':
# mp4 needs postprocessing(qt-faststart), write to temp file post = ['-movflags', '+faststart', '-f', 'mp4', enc_target]
cmd += ["%s.mp4" % enc_target]
else: else:
cmd += [enc_target] post = [target]
if video_settings != ['-vn']:
pass1_post = post[:]
pass1_post[-1] = '/dev/null'
if format == 'webm':
pass1_post = ['-speed', '4'] + pass1_post
post = ['-speed', '1'] + post
cmds.append(base + ['-an', '-pass', '1', '-passlogfile', '%s.log' % target]
+ video_settings + pass1_post)
cmds.append(base + ['-pass', '2', '-passlogfile', '%s.log' % target]
+ audio_settings + video_settings + post)
else:
cmds.append(base + audio_settings + video_settings + post)
# print(cmd) # print(cmds)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, n = 0
stdout=subprocess.PIPE, for cmd in cmds:
stderr=subprocess.STDOUT, n += 1
close_fds=True)
stdout, stderr = p.communicate()
if p.returncode != 0:
t = "%s.mp4" % enc_target if format == 'mp4' else enc_target
if os.path.exists(t):
os.unlink(t)
if os.path.exists(target):
os.unlink(target)
stdout = stdout.replace('\r\n', '\n').replace('\r', '\n')
return False, stdout
if format == 'mp4':
cmd = ['qt-faststart', "%s.mp4" % enc_target, enc_target]
# print(cmd)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=open('/dev/null', 'w'), stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
close_fds=True) close_fds=True)
p.communicate() stdout, stderr = p.communicate()
os.unlink("%s.mp4" % enc_target)
elif format == 'webm' and audio_only: if p.returncode != 0:
if os.path.exists(enc_target):
os.unlink(enc_target)
if os.path.exists(target):
os.unlink(target)
stdout = stdout.replace('\r\n', '\n').replace('\r', '\n')
return False, stdout
if format == 'webm' and audio_only:
cmd = ['mkvmerge', '-w', '-o', target, '--cues', '-1:all', enc_target] cmd = ['mkvmerge', '-w', '-o', target, '--cues', '-1:all', enc_target]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
@ -305,6 +340,8 @@ def stream(video, target, profile, info, audio_track=0, flags={}):
enc_target = target enc_target = target
if p.returncode == 0 and enc_target != target: if p.returncode == 0 and enc_target != target:
shutil.move(enc_target, target) shutil.move(enc_target, target)
for f in glob('%s.log*' % target):
os.unlink(f)
return True, None return True, None
@ -358,7 +395,8 @@ def ffmpeg_frame_cmd(video, frame, position, height=128):
def ffmpeg_version(): def ffmpeg_version():
p = subprocess.Popen([settings.FFMPEG], p = subprocess.Popen([settings.FFMPEG],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) stdout=subprocess.PIPE,
stderr=subprocess.PIPE, close_fds=True)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
version = stderr.split(' ')[2].split('-')[0] version = stderr.split(' ')[2].split('-')[0]
try: try:
@ -414,16 +452,17 @@ def timeline(video, prefix, modes=None, size=None):
size = [64, 16] size = [64, 16]
if isinstance(video, basestring): if isinstance(video, basestring):
video = [video] video = [video]
cmd = [ cmd = ['../bin/oxtimelines',
os.path.join(settings.PROJECT_ROOT, '../bin/oxtimelines'), '-s', ','.join(map(str, reversed(sorted(size)))),
'-s', ','.join(map(str, reversed(sorted(size)))), '-m', ','.join(modes),
'-m', ','.join(modes), '-o', prefix,
'-o', prefix, '-c', os.path.join(prefix, 'cuts.json'),
'-c', os.path.join(prefix, 'cuts.json'), ] + video
] + video
# print(cmd)
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
close_fds=True) close_fds=True)
# print(cmd)
# p = subprocess.Popen(cmd)
p.wait() p.wait()
@ -437,8 +476,8 @@ def average_color(prefix, start=0, end=0, mode='antialias'):
start = int(start * 25) start = int(start * 25)
end = int(end * 25) end = int(end * 25)
mode = 'timeline' + mode mode = 'timeline' + mode
timelines = ox.sorted_strings(filter(lambda t: t!= '%s%s%sp.jpg'%(prefix, mode, height), timelines = ox.sorted_strings(filter(lambda t: t != '%s%s%sp.jpg' % (prefix, mode, height),
glob("%s%s%sp*.jpg"%(prefix, mode, height)))) glob("%s%s%sp*.jpg" % (prefix, mode, height))))
for image in timelines: for image in timelines:
start_offset = 0 start_offset = 0
if start and frames + 1500 <= start: if start and frames + 1500 <= start:
@ -526,7 +565,7 @@ def timeline_strip(item, cuts, info, prefix):
timeline_image = Image.new('RGB', (timeline_width, timeline_height)) timeline_image = Image.new('RGB', (timeline_width, timeline_height))
if frame in cuts: if frame in cuts:
c = cuts.index(frame) c = cuts.index(frame)
if c +1 < len(cuts): if c + 1 < len(cuts):
duration = cuts[c + 1] - cuts[c] duration = cuts[c + 1] - cuts[c]
frames = math.ceil(duration / (video_width * timeline_height / video_height)) frames = math.ceil(duration / (video_width * timeline_height / video_height))
widths = divide(duration, frames) widths = divide(duration, frames)
@ -545,7 +584,7 @@ def timeline_strip(item, cuts, info, prefix):
box = (0, top, video_width, top + height) box = (0, top, video_width, top + height)
if _debug: if _debug:
print(frame, 'cut', c, 'frame', s, frame, 'width', widths[s], box) print(frame, 'cut', c, 'frame', s, frame, 'width', widths[s], box)
#FIXME: why does this have to be frame+1? # FIXME: why does this have to be frame+1?
frame_image = Image.open(item.frame((frame+1)/fps)) frame_image = Image.open(item.frame((frame+1)/fps))
frame_image = frame_image.crop(box).resize((widths[s], timeline_height), Image.ANTIALIAS) frame_image = frame_image.crop(box).resize((widths[s], timeline_height), Image.ANTIALIAS)
for x_ in range(widths[s]): for x_ in range(widths[s]):
@ -569,17 +608,17 @@ def chop(video, start, end):
settings.FFMPEG, settings.FFMPEG,
'-y', '-y',
'-i', video, '-i', video,
'-ss', '%.3f'%start, '-ss', '%.3f' % start,
'-t', '%.3f'%t, '-t', '%.3f' % t,
'-c:v', 'copy', '-c:v', 'copy',
'-c:a', 'copy', '-c:a', 'copy',
'-f', ext[1:], '-f', ext[1:],
choped_video choped_video
] ]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, p = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stdout=open('/dev/null', 'w'), stdout=open('/dev/null', 'w'),
stderr=open('/dev/null', 'w'), stderr=open('/dev/null', 'w'),
close_fds=True) close_fds=True)
p.wait() p.wait()
f = open(choped_video, 'r') f = open(choped_video, 'r')
os.unlink(choped_video) os.unlink(choped_video)

View file

@ -165,6 +165,7 @@ LOGGING = {
AUTH_PROFILE_MODULE = 'user.UserProfile' AUTH_PROFILE_MODULE = 'user.UserProfile'
AUTH_CHECK_USERNAME = True AUTH_CHECK_USERNAME = True
FFMPEG = 'ffmpeg' FFMPEG = 'ffmpeg'
FFMPEG_SUPPORTS_VP9 = True
#========================================================================= #=========================================================================
#Pan.do/ra related settings settings #Pan.do/ra related settings settings