Skip to content

Instantly share code, notes, and snippets.

@BigRoy
Created May 20, 2022 10:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save BigRoy/cefe7c3617be947f6aac570cb0a9f951 to your computer and use it in GitHub Desktop.
Save BigRoy/cefe7c3617be947f6aac570cb0a9f951 to your computer and use it in GitHub Desktop.
Extracted from maya-capture-ui-cb's ffmpeg overlays to share the overall implementation details.
import logging
import subprocess
from datetime import datetime
import os
import re
import tempfile
log = logging.getLogger(__name__)
# Locate ffmpeg if full path provided, otherwise use ffmpeg executable name
FFMPEG = os.environ.get("FFMPEG_PATH", None)
if FFMPEG is None:
# Use the one from OpenPype
try:
from openpype.lib import get_ffmpeg_tool_path
FFMPEG = get_ffmpeg_tool_path("ffmpeg")
except ImportError as exc:
FFMPEG = "ffmpeg"
# Locate the fixtures for custom font and a logo overlay
FIXTURES = os.path.join(os.path.dirname(__file__),
"fixtures")
FONT_PATH = os.path.join(FIXTURES, 'DejaVuSansMono.ttf').replace("\\", "/")
FONT_PATH = FONT_PATH.replace(":", "\\:") # escape colon
LOGO_PATH = os.path.join(FIXTURES, "logo.png").replace("\\", "/")
# Text draw string
TEXT_DRAW = (
'drawtext=fontsize=16:'
'fontcolor=white:'
"fontfile='{0}':".format(FONT_PATH) +
'box=1:boxcolor=black@0.60:boxborderw=2:'
"text='{text}':"
'x={x}:'
'y={y}'
)
def draw_text(text, x, y, fps=25.0):
"""Returns a complex drawtext filter string
When `text` is a list of values it will set the values per frame using
the `sendcmd` functionality of ffmpeg. This will write the command to
a temporary file to allow for large commands to be created (500+ frames)
Args:
text (str, list): The value or values
x (str): The x position
y (str): The y position
fps (float): The frames per second to be interpreted for the list
of values. This is required separately to accurately "burn in"
the multiple values at the correct times.
Returns:
str: The complex filter string.
"""
# TODO: Optimize this for large videos 10000+ frames (400 seconds)
fps = float(fps)
eps = 0.001
if eps > 1.0 / fps:
# This would only happen for 1000+ FPS.
raise RuntimeError("The FPS is too high for the precision to "
"write multiple values over time.")
cmd = ""
is_dynamic = isinstance(text, list)
if is_dynamic:
with tempfile.NamedTemporaryFile(delete=False) as f:
for i, value in enumerate(text):
seconds = i / fps
# Escape special character
value = str(value).replace(":", "\\:")
line = ("{start} drawtext reinit text='{value}';"
"\n".format(start=seconds-eps,
value=value))
f.write(line)
f.flush()
path = f.name
path = path.replace("\\", "/")
path = path.replace(":", "\\:")
cmd += "sendcmd=f='{0}', ".format(path)
value = text[0] if is_dynamic else text
draw = TEXT_DRAW.format(text=value,
x=x,
y=y)
cmd += draw
return cmd
def overlay_video(source,
output,
start,
end,
scene,
username,
focal_length,
fps=25.0,
logo=True,
include_alpha=False,
audio_track=None,
create_no_window=False,
verbose=False):
"""Overlay information on video using ffmpeg"""
# Ensure integers
start = int(start)
end = int(end)
# Get timestamp
date = datetime.now()
timestamp = date.strftime("%Y-%m-%d %H:%M:%S")
timestamp = timestamp.replace(":", "\\:") # escape
# region text draws
text_draws = []
# drawtext focal at top center
FOCAL_FORMAT = "{0:.1f} mm"
if isinstance(focal_length, list):
focal_length = [FOCAL_FORMAT.format(value) for value in focal_length]
else:
focal_length = FOCAL_FORMAT.format(focal_length)
draw = draw_text(text=focal_length,
x="(w-text_w)/2",
y="10")
text_draws.append(draw)
# drawtext name top center-right
draw = TEXT_DRAW.format(text=username,
x="(w-text_w)/1.5",
y="10")
text_draws.append(draw)
# drawtext timestamp at top right
draw = TEXT_DRAW.format(text=timestamp,
x="(w-text_w)-10",
y="10")
text_draws.append(draw)
# drawtext frame ranges bottom right
frame = "{current} [ {start} : {end} ]".format(
current="%{eif:n+" + str(start) + ":d}",
start=start,
end=end
)
frame = frame.replace(":", "\\:") # escape
draw = TEXT_DRAW.format(text=frame,
x="(w-text_w)-10",
y="(h-text_h)-10")
text_draws.append(draw)
# drawtext scene name bottom left
draw = TEXT_DRAW.format(text=scene,
x="10",
y="(h-text_h)-10")
text_draws.append(draw)
render(source, output, text_draws,
fps=fps,
start=start,
logo=logo,
include_alpha=include_alpha,
audio_track=audio_track,
create_no_window=create_no_window,
verbose=verbose)
def render(source,
output,
text_draws=tuple(),
fps=25.0,
start=0,
logo=True,
include_alpha=False,
audio_track=None,
create_no_window=False,
verbose=False):
"""Render source to output with FFMPEG"""
# load source video
cmd = r'{exe} -y '.format(exe=FFMPEG)
# Test whether it's likely an image sequence so we can force the
# correct framerate and start frame.
is_sequence = bool(re.search("%[0-9]*d", source))
if is_sequence:
# todo: Allow non integer framerates (e.g. NTSC at 30000/1001)
# See: https://video.stackexchange.com/a/13074
cmd += ' -framerate {framerate} '.format(framerate=int(fps))
if start != 0:
# Allow FFMPEG to read the image sequence from the start frame
cmd += ' -start_number {start} '.format(start=int(start))
cmd += ' -i "{source}" '.format(source=source)
# Force forward slashes
cmd = cmd.replace("\\", "/")
# load logo input
cmd += ' -i "{source}" '.format(source=LOGO_PATH).replace("\\", "/")
if audio_track:
cmd += ' -i "{source}" '.format(source=audio_track)
# Start a complex filter (to also fix H264 divisible by 2 requirement)
cmd += (
' -filter_complex "'
'[0:v]scale=trunc(iw/2)*2:trunc(ih/2)*2' # fix divisible by 2
)
# Overlay logo
if logo:
cmd += "[scaled];[scaled][1:v]overlay=10:10" # overlay logo
# Merge text draws into the command
if text_draws:
cmd += "[ol];[ol]" # keep videofilter open
draw_cmd = ", ".join(text_draws) # merge text draws
cmd += draw_cmd
# end videofilter, map outputs and set compression
cmd += '[out]" -map "[out]" ' # map filter output to video stream
if audio_track:
cmd += ' -map "2:a" ' # map audio
else:
# If no explicit audio track provided try and map the audio
# of the first input if it contains any
cmd += ' -map "a:0?" ' # map first input's audio (if available)
# Decide on codec based on Alpha plus whether it's image sequence output
is_sequence_output = bool(re.search("%[0-9]*d", output))
if not is_sequence_output:
if not include_alpha:
# defaults to libx264
# profile to fix adobe skipping frames
cmd += ' -profile:v baseline '
# increase quality (default: 23)
cmd += ' -crf 19 '
# ensure correct pixel format for baseline profile
cmd += '-pix_fmt yuv420p '
else:
# .mov with Prores 444 with Alpha
cmd += " -codec prores_ks " \
" -pix_fmt yuva444p10le " \
" -alpha_bits 16 " \
" -profile:v 4444 " \
" -f mov "
else:
print("Sequence output codecs not implemented, "
"result could be unexpected..")
if audio_track:
# Ensure we encode the audio correctly
cmd += " -c:a copy "
# define output path
cmd += ' "{output}"'.format(output=output)
if verbose:
log.info(cmd)
kwargs = {}
if create_no_window:
CREATE_NO_WINDOW = 0x08000000
kwargs["creationflags"] = CREATE_NO_WINDOW
try:
subprocess.check_output(cmd, stderr=subprocess.STDOUT, **kwargs)
except subprocess.CalledProcessError as exc:
log.error(exc)
log.error("STDOUT:\n{0}".format(exc.output))
raise RuntimeError("Failed FFMPEG render with overlay.")
@BigRoy
Copy link
Author

BigRoy commented May 20, 2022

For animated focal lengths in the overlay you'll need to pass a list of values to the command (per frame!) for focal length.
This is how we query those values of the focal length for the capture duration:

def _get_focal_length(cam, start, end):

    plug = "{0}.focalLength".format(cam)
    if not cmds.listConnections(plug, destination=False, source=True):
        # static
        return cmds.getAttr(plug)
    else:
        # dynamic
        return [cmds.getAttr(plug, time=t) for t in range(int(start),
                                                          int(end))]

And then can pass that result into the function:

source = "D:/video.mov"
output = "D:/video_overlay.mov"
start = 1
end = 100
scene = "source_filename.ma"
username = "royn"

# Get the animated focal length from camera in current Maya scene
focal_length = _get_focal_length(cam, start, end)

overlay_video(source, output, start, end, scene, username, focal_length,
              verbose=True)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment