Skip to content

Instantly share code, notes, and snippets.

@baku89
Created January 7, 2021 05:34
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 baku89/9293282456d35ebaed684cad21aba06c to your computer and use it in GitHub Desktop.
Save baku89/9293282456d35ebaed684cad21aba06c to your computer and use it in GitHub Desktop.
import os, sys, subprocess
import shutil
import hou
import toolutils
import itertools
import tempfile
import datetime
#----------------------------------------
# Common
def getCachePath():
node = hou.pwd()
out_path = node.parm('output').eval()
out_dir = os.path.dirname(out_path)
cache_name = os.path.splitext(os.path.basename(out_path))[0] + '_cache'
cache_path = os.path.join(out_dir, cache_name, '%s.$F4.jpg' % cache_name)
return cache_path
def getFrameRange(mode):
node = hou.pwd()
start, end, step = node.parmTuple('frange').evalAsInts()
if mode == 'current':
start = min(int(hou.frame()), end)
return (start, end, step)
#----------------------------------------
# Flipbook
def getViewname():
viewer = toolutils.sceneViewer()
viewname = {
'desktop' : viewer.pane().desktop().name(),
'pane' : viewer.name(),
'type' :'world',
'viewport': viewer.curViewport().name()
}
return '{desktop}.{pane}.{type}.{viewport}'.format(**viewname)
def viewwrite(options='', outpath='ip'):
current_view = getViewname()
cmd = "viewwrite %s %s '%s'" % (options, current_view, outpath)
hou.hscript(cmd)
#----------------------------------------
# Export video using FFMPEG
flatten = lambda x: [z for y in x for z in (flatten(y) if hasattr(y, '__iter__') and not isinstance(y, str) else (y,))]
def addDateSuffix(path):
directory, filename = os.path.split(path)
name, ext = os.path.splitext(filename)
suffix = datetime.datetime.now().strftime('_%y-%m-%d_%H-%M-%S')
return os.path.join(directory, name + suffix + ext)
def runCommand(command, cwd):
fh = open("NUL","w")
process = subprocess.Popen(command, stdout=subprocess.PIPE, cwd=cwd)
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
pass
print output.strip()
rc = process.poll()
try:
process.terminate()
except:
pass
return rc
def openFile(filename):
if sys.platform == "win32":
os.startfile(filename)
else:
opener ="open" if sys.platform == "darwin" else "xdg-open"
subprocess.call([opener, filename])
def execute(range_mode):
node = hou.pwd()
start, end, step = getFrameRange(range_mode)
fps = node.parm('fps').evalAsInt()
resx, resy = node.parmTuple('res').evalAsInts()
cache_path = getCachePath()
cache_dir = os.path.dirname(cache_path)
out_file = os.path.abspath(node.parm('output').eval())
# Export Cache
try:
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
os.makedirs(cache_dir)
except OSError:
raise(Exception('Cannot create cache directory.'))
options = '-f %d %d -i %d -c -r %d %d' % (start, end, step, resx, resy)
viewwrite(options, cache_path)
# Manage when output file already exists
if os.path.exists(out_file):
overwrite_mode = node.parm('overwrite').evalAsString()
if overwrite_mode == 'date':
out_file = addDateSuffix(out_file)
elif overwrite_mode == 'skip':
return
# Gather cached images
files = [(i, f, hou.expandStringAtFrame(cache_path, f)) for i, f in enumerate(range(start, end + 1, step))]
# Filter only existing files
files = [x for x in files if os.path.exists(x[2])]
# Make all paths relative to the list file
files = [(i, f, os.path.relpath(path, cache_dir)) for i, f, path in files]
# Determine the location of the list file
txt_path = os.path.abspath(os.path.join(cache_dir, hou.expandString("filelist.export_mp4.txt")))
# Export file list
txt = ""
for i, frame, path in files:
duration = 1 / float(fps)
if i + 1 < len(files):
duration *= files[i+1][1] - frame
txt += "file '%s'\nduration %f\n" % (path, duration)
with open(txt_path, 'w') as txt_file:
txt_file.write(txt)
# Create intermediate directories
out_dir = os.path.dirname(out_file)
if not os.path.exists(out_dir):
os.makedirs(out_dir)
# Execute
ffmpeg_path = node.parm('ffmpeg').eval()
inputs = ['-i', txt_path]
# Setup audio
audio_file = node.parm('audio').eval()
if audio_file:
audio_file = os.path.abspath(audio_file)
audio_start = hou.frameToTime(node.parm('audio_offset_frame').eval())
video_start = hou.frameToTime(start)
offset = audio_start - video_start
inputs.extend(['-ss', str(-offset), '-i', audio_file])
# Setup filters
filter_complex = ['fps=%d' % fps]
# Resize
# filter_complex.append("scale=%d:%d" % node.parmTuple('res').eval())
if node.parm('usebgcolor').eval():
color = node.parmTuple('bgcolor').eval()
color = '#%02x%02x%02x' % tuple([int(v * 255) for v in color])
filter_complex.append("split=2[bg][fg];[bg]drawbox=c=%s@1:replace=1:t=fill[bg];[bg][fg]overlay=format=auto" % color)
args = flatten([
ffmpeg_path,
'-hide_banner',
'-loglevel', 'panic',
'-y',
'-threads', '64',
'-apply_trc', 'bt709',
'-f', 'concat',
'-safe', '0',
inputs,
'-strict', '-2',
'-c:v', 'libx264',
'-preset', 'ultrafast',
'-qp', '0',
'-crf', '20',
'-pix_fmt', 'yuv420p',
'-filter_complex', '[0]' + ','.join(filter_complex),
['-c:a', 'aac'] if audio_file else [],
'-shortest',
out_file])
# Execute the conversion
rc = runCommand(args, cache_dir)
if rc != 0:
raise(Exception('Exporting unsuccessfully finished.'))
if node.parm('play_output').eval():
openFile(out_file)
# Clear cache images
try:
shutil.rmtree(cache_dir)
except OSError:
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment