pythonpython-3.xffmpegtwisted

How can twisted spawnProcess send ffmpeg filter_complex parameters with embedded quotes?


I am attempting to send these (simplified) parameters to ffmpeg via reactor.spawnProcess.

['-filter_complex', '"[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]"'

but presumedly code is doing some form of prefixing which ffmpeg can't understand because my filter graph always errors although I can send my exact same text to ffmpeg on command line and it's fine.

I've attempted to repr and escape ('"') and I can't figure out how to get these strings through to ffmpeg.

Edit: Don't know why I didn't try before but I wrote a little script to receive the input parameters and they're fine. Still at a loss to understand why " ".join(params) will run from terminal but it fails in spawnProcess.

Edit: ffmpeg output

Splitting the commandline.
Reading option '-loglevel' ... matched as option 'loglevel' (set logging level) with argument 'debug'.
Reading option '-v' ... matched as option 'v' (set logging level) with argument 'verbose'.
Reading option '-threads' ... matched as AVOption 'threads' with argument 'auto'.
Reading option '-flags' ... matched as AVOption 'flags' with argument '-global_header'.
Reading option '-i' ... matched as input url with argument 'config/colorbars.jpg'.
Reading option '-c:v' ... matched as option 'c' (codec name) with argument 'copy'.
Reading option '-tag:v' ... matched as option 'tag' (force codec tag/fourcc) with argument 'hvc1'.
Reading option '-filter_complex' ... matched as option 'filter_complex' (create a complex filtergraph) with argument '"[0:v]setpts=(RTCTIME-RTCSTART)/(TB*1000000),scale=-1:720[outpad];[outpad]pad=w=1280:h=720:x=-1:y=-1:color=black[out]"'.
Reading option '-map' ... matched as option 'map' (set input stream mapping) with argument '"[out]"'.
Reading option '-c:v' ... matched as option 'c' (codec name) with argument 'hevc_videotoolbox'.
Reading option '-b:v' ... matched as option 'b' (video bitrate (please use -b:v)) with argument '3000K'.
Reading option '-preset' ... matched as AVOption 'preset' with argument 'medium'.
Reading option '-profile:v' ... matched as option 'profile' (set profile) with argument 'main'.
Reading option '-pix_fmt' ... matched as option 'pix_fmt' (set pixel format) with argument 'yuv420p'.
Reading option '-t' ... matched as option 't' (record or transcode "duration" seconds of audio/video) with argument '5'.
Reading option 'temp/apptest_1.mp4' ... matched as output url.
Reading option '-y' ... matched as option 'y' (overwrite output files) with argument '1'.
Finished splitting the commandline.
Parsing a group of options: global .
Applying option loglevel (set logging level) with argument debug.
Applying option v (set logging level) with argument verbose.
Input #0, image2, from 'config/colorbars.jpg':
  Duration: 00:00:00.04, start: 0.000000, bitrate: 4712 kb/s
  Stream #0:0: Video: mjpeg (Baseline), 1 reference frame, yuvj420p(pc, bt470bg/unknown/unknown, center), 1280x720 [SAR 1:1 DAR 16:9], 25 fps, 25 tbr, 25 tbn
[AVFilterGraph @ 0x600001fbc600] No such filter: '"'
Error initializing complex filters.
Invalid argument

Solution: The problem always solves when requesting assistance. Every example at ffmpeg wiki shows double quotes around the filter graph. When sending with spawnProcess this is unnecessary.


Solution

  • The issue is that when you pass a list of strings to spawnProcess, they show up to the subprocess exactly as you have passed them, which means that ffmpeg is seeing a command line argument that is literally "[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]" including the quotes.

    It may help to understand what is happening here by getting Twisted out of the way and simply looking at the output of this very simple Python program when run in various ways:

    # argv.py
    import sys
    print(repr(sys.argv))
    

    Let's pretend this program is ffmpeg.

    At a shell, we can do

    $ python3 argv.py -filter_complex \
        "[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]"
    

    and we will get

    ['argv.py', '-filter_complex', '[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]']
    

    The shell has taken the quoted argument and passed it along as-is.

    Now, the shell can do some other stuff for us in a double-quoted string, such as variable interpolation:

    $ python3 argv.py here is my home "$HOME"
    ['argv.py', 'here', 'is', 'my', 'home', '/Users/glyph']
    

    But Python doesn't know anything about variable expansion; that's done by the shell before we see it at all.

    Another short script that uses os.execv can show you what spawnProcess is doing with arguments in the same way. For example:

    from sys import executable
    from os import execv
    execv(
        executable,
        [executable, "argv.py", "here", "is", "my", "$HOME"],
    )
    

    That simply prints out

    ['argv.py', 'here', 'is', 'my', '$HOME']
    

    Python doesn't know about shell quotes any more than it knows about shell variables when executing a program directly with exec, with no shell involved. So, closer to your original example, at a shell, we might have

    $ python3 argv.py "one argument with spaces, quoted"
    

    with quotes around it, and get

    ['argv.py', 'one argument with spaces, quoted']
    

    as output. But, if we were to put those quotes into a Python string,

    from sys import executable
    from os import execv
    execv(
        executable,
        [executable, "argv.py", '"one argument with spaces, quoted too much"'],
    )
    

    you'll see the quotes make their way into the argument

    ['argv.py', '"one argument with spaces, quoted too much"']
    

    just as they would if you double-quoted it in the shell:

    $ python3 argv.py '"one argument with spaces, extra-quoted from the shell"'
    ['argv.py', '"one argument with spaces, extra-quoted from the shell"']
    

    That is what is happening here. If you pass a string like '"' to the exec family of functions, it gets passed through exactly as '"' to the program you're calling. The ffmpeg documentation uses quotes in its documentation because it is encoding the data of its arguments for a shell, whereas you need to encode the data of its arguments for Python.

    Another way you can think about it is that while shells have the special case of strings-with-no-quotes, strings-with-quotes work almost the same.

    In Python, you have typed '"[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]"'. And if we type exactly that - with the double quotes inside the single quotes - into a shell, we get the same result:

    $ python3 argv.py '"[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]"'
    ['argv.py', '"[0:v]setpts=PTS-STARTPTS,scale=-1:720 [a0];[a0]pad=w=1280:h=720:x=-1:y=-1:color=black[out]"']
    

    In conclusion: this is just how quoting strings works in shells and Python, and doesn't really have anything to do with Twisted.