The batch script works because cmd replaces %F%, %D% and %OFF% with literal text before the command runs. PowerShell performs no such blind substitution: it parses each line first, and three of its parsing rules break this command in sequence. Quote the value on the right of the assignment, delimit each variable name with curly braces where a colon follows, and wrap the filter graph in double quotes — then the PowerShell version behaves like the batch one.
Why the batch version works but PowerShell does not
In a batch file, set F=fade stores the characters fade, and the command processor swaps %F% for that text before ffmpeg ever runs. Nothing is parsed — it is pure character substitution.
PowerShell is parser-first. It decides what is a command, an expression or a string before executing anything. Variables interpolate only inside double-quoted strings, bare words resolve as commands, and a colon after a variable name carries a special meaning. A line-for-line translation from batch therefore fails in three places at once, and each failure masks the next.
Three PowerShell parsing rules that break this command
1. $F=fade tries to run a command called fade
The right-hand side of a PowerShell assignment is not raw text. A bare word is resolved as a command, so PowerShell reports that fade is not recognized as a cmdlet — the first error in the console output — and $F is never created. Quote string values:
$F = "fade"
$D = 2 works unquoted because it parses as an integer literal, though quoting it is harmless and keeps the style consistent.
2. $F:duration is not $F plus :duration
This is the subtle failure. A colon directly after a variable name makes PowerShell read the whole token as a namespace-qualified variable — the same syntax as $env:PATH. So $F:duration asks for a variable called duration in a scope named F. No such scope exists, the reference quietly resolves to $null, and $null interpolates as an empty string.
Splice that into the filter string and transition=$F:duration=$D:offset=$OFF collapses to transition===3: both qualified references vanish, leaving only the literal equals signs and the one variable that had no colon after it. Curly braces mark where the variable name ends, so the colon stays literal text:
xfade=transition=${F}:duration=${D}:offset=$OFF
$OFF needed no braces only because nothing follows it. Note that fixing the assignment alone would not help — $F:duration never reads $F in the first place.
3. The filter graph should be a single quoted argument
Filter graphs routinely contain spaces, commas, semicolons and other characters PowerShell treats as separators. An unquoted graph survives only while it happens to contain none of them. Wrapping the argument in double quotes guarantees the entire graph reaches ffmpeg as one argument — and lets the variables inside interpolate.
The corrected script
With all three fixes applied:
$F = "fade"
$D = 2
$OFF = 3
ffmpeg -i 5.mkv -i 10.mkv -filter_complex "[0][1]xfade=transition=${F}:duration=${D}:offset=$OFF" -c:v libx264 -pix_fmt yuv420p -an -r 25 -an out.mkv
Only variables immediately followed by a colon strictly need braces, but using them consistently prevents the problem from returning the next time you edit the graph.
Checking what ffmpeg actually receives
Before running a long native command, assemble its volatile argument in a variable and print it. That shows exactly what the shell produced, without ffmpeg's noise:
$fc = "[0][1]xfade=transition=${F}:duration=${D}:offset=$OFF"
Write-Host $fc
ffmpeg -i 5.mkv -i 10.mkv -filter_complex $fc -c:v libx264 -pix_fmt yuv420p -an -r 25 -an out.mkv
This also explains the misleading ffmpeg errors. const_values array too small for transition followed by Not yet implemented in FFmpeg, patches welcome is what ffmpeg's option parser emits when the value supplied for a constant-style option such as transition matches no known name — here because the value was empty. It does not mean xfade is broken or that a newer build is needed; the value simply never arrived.
Other ways to build the argument, and habits that prevent a repeat
If you would rather not sprinkle braces through the string, the format operator keeps the template and the values separate:
$fc = "[0][1]xfade=transition={0}:duration={1}:offset={2}" -f $F, $D, $OFF
A subexpression works too and is equally explicit:
$fc = "[0][1]xfade=transition=$($F):duration=$($D):offset=$($OFF)"
Whichever style you choose:
- Quote every string value on assignment, however short.
- Delimit with
${ }or$( )whenever a variable is followed by a colon, underscore or letter. - Quote native-command arguments that contain filter or path syntax.
- Print assembled arguments before invoking the program.
Frequently Asked Questions
Why does the identical script work in cmd but fail in PowerShell?
cmd performs literal text substitution of %VAR% before execution, while PowerShell parses the line and applies language rules: command resolution for bare words, namespace parsing for colons, and interpolation only inside double quotes. The same characters mean different things in each shell.
What exactly does ${F} do?
It marks where the variable name ends. Without braces, PowerShell absorbs following characters — a colon creates a qualified-variable token, and other name characters can be swallowed into the name. Braces force the boundary.
Why did ffmpeg report "Not yet implemented in FFmpeg, patches welcome"?
That message comes from ffmpeg's option parser when a value cannot be matched for an option that expects a fixed set of constants. Here the transition option arrived empty, so nothing matched — the feature itself is fine.
Do I need double quotes or single quotes?
Double quotes allow variable interpolation; single quotes treat everything literally, so "${F}" expands but '${F}' does not. Use double quotes when values must be inserted, or single quotes with the format operator for a fixed template.
Does the colon problem affect anything besides ffmpeg?
Yes. Any argument where a variable is immediately followed by a colon — URLs, timestamps like $h:$m, registry paths — hits the same parser behavior. Delimit with ${ } or $( ) whenever a variable is followed by more text.
How can I see the exact argument PowerShell passes to a native program?
Assemble it in a variable and print it with Write-Host before invoking the program. That immediately reveals empty or malformed values without wading through the program's own error output.