Why does calling ffmpeg mess up reading variables? - bash

A few years ago when I was younger, more carefree, and, uh, less cognisant of good practice in writing shell scripts; I wrote a quick-and-dirty script to assist with a task I was facing:
#!/bin/bash
# autohighlighter which generates highlights from a video clip and file
process_highlight_file() {
n=0
while read -r line; do
begin=$(echo "$line" | awk '{ print $1 }' )
end=$(echo "$line" | awk '{ print $2 }')
hilightname=$(echo "$line" | awk '{ print $3 }')
printf "Begin highlight called %s at %s, to %s\n" "$hilightname" "$begin" "$end"
echo "$begin $end"
sleep 2
echo "ffmpeg -y -ss $begin -i $videofile -to $end -c copy -avoid_negative_ts 1 $hilightname.mkv"
ffmpeg -loglevel quiet -hide_banner -y -ss "$begin" -i "$videofile" -to "$end" -c copy -avoid_negative_ts 1 "$hilightname.mkv"
if [ "$n" -eq 0 ]; then
echo -n "melt $hilightname.mkv " > constructed.melt
else
echo -n "$hilightname.mkv -mix 120 -mixer luma " >> constructed.melt
fi
(( n++ ))
done < $highlightfile
echo -n "-consumer avformat:$videofile-highlights.mkv crf=18" >> constructed.melt
}
highlightfile=$2
videofile=$1
process_highlight_file
exit 0
I call it with the video file name and a highlight file with the following tab-separated contents:
3:55 4:15 tutorialcomplete
10:50 11:15 firstkill
13:30 14:00 pickpocket
If I comment out the actual call to ffmpeg, I get sensible output:
Begin highlight called tutorialcomplete at 3:55, to 4:15
3:55 4:15
ffmpeg -y -ss 3:55 -i 2019-08-27 20-31-27.mkv -to 4:15 -c copy -avoid_negative_ts 1 tutorialcomplete.mkv
Begin highlight called firstkill at 10:50, to 11:15
10:50 11:15
ffmpeg -y -ss 10:50 -i /tmp/footage/gamefootage/pending/sor/2019-08-27 20-31-27.mkv -to 11:15 -c copy -avoid_negative_ts 1 firstkill.mkv
Begin highlight called pickpocket at 13:30, to 14:00
13:30 14:00
ffmpeg -y -ss 13:30 -i /tmp/footage/gamefootage/pending/sor/2019-08-27 20-31-27.mkv -to 14:00 -c copy -avoid_negative_ts 1 pickpocket.mkv
All good, if rather overkill on the debug output.
If I uncomment the call to ffmpeg, I get:
Begin highlight called tutorialcomplete at 3:55, to 4:15
3:55 4:15
ffmpeg -y -ss 3:55 -i /tmp/footage/gamefootage/pending/sor/2019-08-27 20-31-27.mkv -to 4:15 -c copy -avoid_negative_ts 1 tutorialcomplete.mkv
Begin highlight called at firstkill, to
firstkill
ffmpeg -y -ss firstkill -i /tmp/footage/gamefootage/pending/sor/2019-08-27 20-31-27.mkv -to -c copy -avoid_negative_ts 1 .mkv
Begin highlight called pickpocket at 13:30, to 14:00
13:30 14:00
ffmpeg -y -ss 13:30 -i /tmp/footage/gamefootage/pending/sor/2019-08-27 20-31-27.mkv -to 14:00 -c copy -avoid_negative_ts 1 pickpocket.mkv
Naturally, ffmpeg complains that "firstkill" is not a valid time to seek to. If I add further lines to the file I am passing in, it seems to only affect the second pass through the loop. Additional output is also produced:
Enter command: <target>|all <time>|-1 <command>[ <argument>]
The going theory is that a line isn't properly terminated. However, I can't seem to track that down in the script or the input.
I'm aware there are a number of poor practices, foibles and other unexpected behaviour here, for which past-me definitely takes the blame and hangs their head in shame! That said, why does calling ffmpeg in the loop here cause values from a file to be parsed incorrectly, or: why does commenting out the line with the call to ffmpeg give the variables the correct value. Also, why does it only affect the second pass of the loop? Where does Enter command come from?
Additionally, shellcheck.net doesn't complain about anything in the code.

You need to prevent FFMpeg from consuming the stdin characters stream.
See: man ffmpeg.1
-stdin
Enable interaction on standard input. On by default unless standard input is used as an input. To explicitly disable interaction you need to specify -nostdin.
Disabling interaction on standard input is useful, for example, if ffmpeg is in the background process group. Roughly the same result can be achieved with ffmpeg ... < /dev/null but it requires a shell.
Here in your loop, ffmpeg is consuming the input from your while loop commands block.
while read -r line; do
...
# here ffmpeg defaults to consuming the same input
# $highlightfile that is fed to the while loop commands block.
ffmpeg -loglevel quiet -hide_banner -y -ss "$begin" -i "$videofile" -to "$end" -c copy -avoid_negative_ts 1 "$hilightname.mkv"
done < $highlightfile
A fixed version of your code:
#!/usr/bin/env bash
# autohighlighter which generates highlights from a video clip and file
process_highlight_file() {
videofile="$1"
highlightfile="$2"
n=0
melt_caller=(melt)
while read -r begin end hilightname; do
printf 'Begin highlight called %s at %s, to %s\n' "$hilightname" "$begin" "$end"
ffmpeg_caller=(ffmpeg -nostdin -loglevel quiet -hide_banner -y -ss "$begin" -i "$videofile" -to "$end" -c copy -avoid_negative_ts 1 "$hilightname.mkv")
env printf '%q ' "${ffmpeg_caller[#]}" && echo
"${ffmpeg_caller[#]}"
((n++)) && melt_caller+=(-mix 120 -mixer luma)
melt_caller+=("$hilightname.mkv")
done <"$highlightfile"
melt_caller+=(-consumer "avformat:$videofile-highlights.mkv" 'crf=18')
env printf '%q ' "${melt_caller[#]}" && echo
"${melt_caller[#]}"
}
process_highlight_file "$#"

Related

ffmpeg command exports flac with wrong 'length' metadata, works fine for mp3

I have some audio recorded in Audacity 3.2.3 that I have exported as an mp3 and a flac. Then I have this file split_by_silence.sh
Which has hardcoded input path values that take an input file, split it by detecting silence, and then finally run an ffmpeg command to split the files. If you save the below code into a file split.sh, you can call it with the command $ ./split_by_silence.sh "value1" "value2"
# ./split_by_silence.sh "full_lowq.flac" %03d_output.flac
#IN=$1
#OUT=$2
OUT="%03d_output.flac"
IN="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/dog on fire.flac"
OUTPUT_LOCATION="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/"
true ${SD_PARAMS:="-18dB"};
true ${MIN_FRAGMENT_DURATION:="20"};
export MIN_FRAGMENT_DURATION
if [ -z "$OUT" ]; then
echo "Usage: split_by_silence.sh full.mp3 output_template_%03d.mp3"
echo "Depends on FFmpeg, Bash, Awk, Perl 5. Not tested on Mac or Windows."
echo ""
echo "Environment variables (with their current values):"
echo " SD_PARAMS=$SD_PARAMS Parameters for FFmpeg's silencedetect filter: noise tolerance and minimal silence duration"
echo " MIN_FRAGMENT_DURATION=$MIN_FRAGMENT_DURATION Minimal fragment duration"
exit 1
fi
#
# get comma separated list of split points (use ffmpeg to determine points where audio is at SD_PARAMS [-18db] )
#
echo "_______________________"
echo "Determining split points..." >& 2
SPLITS=$(
ffmpeg -v warning -i "$IN" -af silencedetect="$SD_PARAMS",ametadata=mode=print:file=-:key=lavfi.silence_start -vn -sn -f s16le -y /dev/null \
| grep lavfi.silence_start= \
| cut -f 2-2 -d= \
| perl -ne '
our $prev;
INIT { $prev = 0.0; }
chomp;
if (($_ - $prev) >= $ENV{MIN_FRAGMENT_DURATION}) {
print "$_,";
$prev = $_;
}
' \
| sed 's!,$!!'
)
echo "SPLITS= $SPLITS"
#
# Add 5 seconds to each of the comma separated numbers
#
# Convert the comma-separated string into an array
arr=($(echo $SPLITS | tr ',' '\n'))
# Initialize a new array to store the results
new_arr=()
# Iterate through each element and add 5 seconds of padding
for i in "${arr[#]}"; do
result=$(echo "$i + 5" | bc -l)
new_arr+=("$result")
done
# Convert the array back into a comma-separated string
NEW_SPLITS=$(IFS=,; echo "${new_arr[*]}")
# Print the result
echo "NEW_SPLITS= $NEW_SPLITS"
SPLITS=$NEW_SPLITS
#
# Print how many tracks should be exported
#
res="${SPLITS//[^,]}"
CHARCOUNT="${#res}"
num=$((CHARCOUNT + 2))
echo "Exporting $num tracks"
echo "_______________________"
#
# Split audio into individual tracks
#
current_directory=$(pwd)
cd "$OUTPUT_LOCATION"
echo "Running ffmpeg command: "
ffmpeg -i "$IN" -c copy -map 0 -f segment -segment_times "$SPLITS" "$OUT"
#ffmpeg -i "full_lowq.flac" -c copy -map 0 -f segment -segment_times "302.825,552.017" "%03d_output.flac"
echo "Done."
cd $current_directory
echo "running flac command"
# check flac file intrgrity
If I call this code for my flac file:
OUT="%03d_output.flac"
IN="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/dog on fire.flac"
The outputted files have an incorrect metadata for the length. They all report as having the same length, but if i import any of them into audacity, the file has a correct length.
but if i run this for my mp3 file, we can see the correct length metadata:
OUT="%03d_output.mp3"
IN="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/dogs on fire.mp3"
So there is something with my ffmpeg command that causes it to export flac files with wrong 'length' metadata
ffmpeg -i "$IN" -c copy -map 0 -f segment -segment_times "$SPLITS" "$OUT"
I've tried with the flac example to change -c copy to -c:a flac, but that just gives every output flac file a length of 00:00:00
is it a problem with my ffmpeg command? Or my files? https://file.io/tIFsa1l70076
it works for mp3 files just fine, why does it have this issue with flac?

"Quoting within quoting" question for professed bash affectionate

Out of sheer curiosity I would like to know how this quoting dilemma can be fixed.
I already solved the issue by circumnavigating it (I added [vcodec!*=av01] to the -f argument and simply removed the --exec part entirely). Otherwise it only worked, when there were no spaces or minus signs in the --exec argument.
The culprit line is the last and the issue is at the end with the --exec argument. You can ignore the rest.
Thanks for your help on the road to enlightenment! ;-)
#!/bin/bash
trap "exit" INT
avtomp4conv () {
# tests if the given file (in argument) is an AV1 media and if so, converts it to mp4
echo "${1}"
if ($( ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 "${1}" | grep -i av > /dev/null )); then
echo "$1 bad codec"
ffmpeg -hide_banner -loglevel error -stats -i "${1}" -movflags faststart -preset ultrafast "${1%.mp4}_fixed.mp4" && mv "${1}" bogus/ && mv -n "${1%.mp4}_fixed.mp4" "${1}"
fi
}
# ... lotsa other stuff ...
export -f avtomp4conv
cat links.txt | parallel -u -I % --retries 3 --max-args 1 --jobs 4 python3 `which youtube-dl` -c -f "'bestvideo[height<=720][ext=mp4]+bestaudio[ext=m4a]/mp4'" --external-downloader aria2c --external-downloader-args "'-x 4 -s 4'" --exec \'bash -c \"export -f avtomp4conv\;avtomp4conv \{\}\"\' %
Use another function to save you from the double indirection in a single command (parallel executes youtube-dl that executes avtomp4conv). GNU parallel uses your current shell to execute its commands, so no need for bash -c here.
avtomp4conv () {
...
}
ytdl() {
youtube-dl ... --exec "bash -c 'avtomp4conv \"$0\"' {}"
}
export -f avtomp4conv ytdl
< links.txt parallel ... ytdl
Without the function ytdl you could try the following. But why bother with these nested quotes?
< links.txt parallel ... -I insteadOf{} \
"youtube-dl ... --exec \"bash -c 'avtomp4conv \\\"\$0\\\"' {}\""

FFmpeg batch convert multiple files

This command works great:
ffmpeg \
-i /mnt/share/Movies2/"nameofmovie.mkv" \
-map 0:0 \
-map 0:1 \
-map 0:2 \
-c:v libx264 \
-preset veryfast \
-tune film \
-crf 18 \
-c:a:0 copy \
-c:a:1 copy \
/mnt/share/Converted/"nameofmovie".mkv
But i want to either be able to read the input file from a text file or to run this command one after another for each file i want to convert. Is there a script i can set up to do this? Not all the files are in the same folder or the same format so something where i could just change the file name and format would be great. I used to have a bash script that could do this for an entire folder but that's not what i am trying to do here. I am using Ubuntu server 18.04
Also i'm pretty new to this i've found this for a whole folder:
for i in *.avi;
do name=`echo $i | cut -d'.' -f1`;
echo $name;
ffmpeg -i "$i" "${name}.mov";
done
But i dont know how to adapt this for individual files
Create a text file the_list.txt as follows:
File1.mp4:mp4
File2.avi:avi
Here the first field is the filename, the second the source format (can be derived from file extenstion, but keeping it simple).
Create a script do_conv.sh:
cat the_list.txt | while read L
do
FNAME=$( echo $L | cut -d':' -f1 )
SRCFMT=$( echo $L | cut -d':' -f2 )
echo 'Next File: ${FNAME}"
if [ "${SRCFMT}" = "mp4" ]
then
ffmpeg -i ${FNAME} ....
elif [ "${SRCFMT}" = "mp4" ]
then
ffmpeg -i ${FNAME} ....
else
ffmpeg -i ${FNAME} ....
fi
done
Not sure exactly what conversion you want to be but hopefully this template can get you started.

Mixing two wav files in FFmpeg

I have 2 wav files wav1 and wav2.I want to play wav2 every 45 seconds
over the wav1 using FFmpeg.
Ok, first create a 45-second file to loop of wav2
ffmpeg -i wav2 -af apad -t 45 wav2-padded.wav
Now, the mix
ffmpeg -i wav1 -f lavfi -i amovie=wav2-padded:loop=9999 -filter_complex [0][1]amix[out] -map [out] -shortest mixed.wav
I think this should accomplish what you want
#!/bin/bash
interval_millis=45000
regex="Duration: ([0-9:.]+)"
[[ $(ffmpeg -i $1 2>&1) =~ $regex ]]
duration_millis=$(echo "${BASH_REMATCH[1]}" | \
awk -F: '{ print 1000 * (($1 * 3600) + ($2 * 60) + $3) }')
duration_counter=$interval_millis
i=1
while [[ $duration_counter -lt $duration_millis ]]; do
inputs+=" -i $2"
delay_params+="[$i]adelay=$duration_counter[del$i];"
amix_params+="[del$i]"
((i++))
((duration_counter += $interval_millis))
done
ffmpeg -i $1 $inputs -filter_complex \
"$delay_params[0]${amix_params}amix=inputs=$i:duration=first" out.wav
I had some problems with adelay on a 2015 build of ffmpeg, so make sure you have an up-to-date version to run this. You call the script as
./script.sh long-file short-file

How to concatenate two MP4 files using FFmpeg?

I'm trying to concatenate two mp4 files using ffmpeg. I need this to be an automatic process hence why I chose ffmpeg. I'm converting the two files into .ts files and then concatenating them and then trying to encode that concatenated .ts file. The files are h264 and aac encoded and I'm hoping to keep the quality the same or as close to original as possible.
ffmpeg -i part1.mp4 -vcodec copy -vbsf h264_mp4toannexb -acodec copy part1.ts
ffmpeg -i part2.mp4 -vcodec copy -vbsf h264_mp4toannexb -acodec copy part2.ts
cat part1.ts part2.ts > parts.ts
ffmpeg -y -i parts.ts -acodec copy -ar 44100 -ab 96k -coder ac -vbsf h264_mp4toannexb parts.mp4
Unfortunately I'm getting the following error message coming back from ffmpeg during encoding:
[h264 # 0x1012600]sps_id out of range
[h264 # 0x1012600]non-existing SPS 0 referenced in buffering period
[h264 # 0x1012600]sps_id out of range
[h264 # 0x1012600]non-existing SPS 0 referenced in buffering period
[NULL # 0x101d600]error, non monotone timestamps 13779431 >= 13779431kbits/s
av_interleaved_write_frame(): Error while opening file
This happens about half way through encoding which makes me think that you can't concat two .ts files together and have it work.
FFmpeg has three concatenation methods:
1. concat video filter
Use this method if your inputs do not have the same parameters (width, height, etc), or are not the same formats/codecs, or if you want to perform any filtering.
Note that this method performs a re-encode of all inputs. If you want to avoid the re-encode, you could re-encode just the inputs that don't match so they share the same codec and other parameters, then use the concat demuxer to avoid re-encoding everything.
ffmpeg -i opening.mkv -i episode.mkv -i ending.mkv \
-filter_complex "[0:v] [0:a] [1:v] [1:a] [2:v] [2:a] \
concat=n=3:v=1:a=1 [v] [a]" \
-map "[v]" -map "[a]" output.mkv
2. concat demuxer
Use this method when you want to avoid a re-encode and your format does not support file-level concatenation (most files used by general users do not support file-level concatenation).
$ cat mylist.txt
file '/path/to/file1'
file '/path/to/file2'
file '/path/to/file3'
$ ffmpeg -f concat -safe 0 -i mylist.txt -c copy output.mp4
For Windows:
(echo file 'first file.mp4' & echo file 'second file.mp4' )>list.txt
ffmpeg -safe 0 -f concat -i list.txt -c copy output.mp4
3. concat protocol
Use this method with formats that support file-level concatenation
(MPEG-1, MPEG-2 PS, DV). Do not use with MP4.
ffmpeg -i "concat:input1|input2" -codec copy output.mkv
This method does not work for many formats, including MP4, due to the nature of these formats and the simplistic concatenation performed by this method.
If in doubt about which method to use, try the concat demuxer.
Also see
FFmpeg FAQ: How can I join video files?
FFmpeg Wiki: How to concatenate (join, merge) media files
FOR MP4 FILES
For .mp4 files (which I obtained from DailyMotion.com: a 50 minute tv episode, downloadable only in three parts, as three .mp4 video files) the following was an effective solution for Windows 7, and does NOT involve re-encoding the files.
I renamed the files (as file1.mp4, file2.mp4, file3.mp4) such that the parts were in the correct order for viewing the complete tv episode.
Then I created a simple batch file (concat.bat), with the following contents:
:: Create File List
echo file file1.mp4 > mylist.txt
echo file file2.mp4 >> mylist.txt
echo file file3.mp4 >> mylist.txt
:: Concatenate Files
ffmpeg -f concat -i mylist.txt -c copy output.mp4
The batch file, and ffmpeg.exe, must both be put in the same folder as the .mp4 files to be joined. Then run the batch file. It will typically take less than ten seconds to run.
.
Addendum (2018/10/21) -
If what you were looking for is a method for specifying all the mp4 files in the current folder without a lot of retyping, try this in your Windows batch file instead (MUST include the option -safe 0):
:: Create File List
for %%i in (*.mp4) do echo file '%%i'>> mylist.txt
:: Concatenate Files
ffmpeg -f concat -safe 0 -i mylist.txt -c copy output.mp4
This works on Windows 7, in a batch file. Don't try using it on the command line, because it only works in a batch file!
Following concatenates three .mp3 files into one .m4a.
ffmpeg -i input1.mp3 -i input2.mp3 -i input3.mp3 -filter_complex "concat=n=3:v=0:a=1" -vn -y input.m4a
Meanings of Options
-filter_complex "concat=n=3:v=0:a=1
concat: concatenate filter joining streams
n: count of input segments (= synchronized audio-video streams or audio-only or video-only stream)
v: output video stream count
a: output audio stream count
-vn: disable video (-an would disable audio)
-y: overwrite output files without prompts
Refer man ffmpeg or ffmpeg -h full to print all options (including all format and codec specific options).
for MP4 files:
If they are not exactly same (100% same codec, same resolution, same type) MP4 files, then you have to trans-code them into intermediate streams at first:
ffmpeg -i myfile1.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts temp1.ts
ffmpeg -i myfile2.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts temp2.ts
// now join
ffmpeg -i "concat:temp1.ts|temp2.ts" -c copy -bsf:a aac_adtstoasc output.mp4
NOTE!:
Output will be like first file ( and not a second one)
Here's a fast (takes less than 1 minute) and lossless way to do this without needing intermediate files:
ls Movie_Part_1.mp4 Movie_Part_2.mp4 | \
perl -ne 'print "file $_"' | \
ffmpeg -f concat -i - -c copy Movie_Joined.mp4
The "ls" contains the files to join
The "perl" creates the concatenation file on-the-fly into a pipe
The "-i -" part tells ffmpeg to read from the pipe
(note - my files had no spaces or weird stuff in them - you'll need appropriate shell-escaping if you want to do this idea with "hard" files).
I found the pipe operator did not work for me when using option 3 to concat several MP4s on a Mac in the accepted answer.
The following one-liner works in bash (Mac, Linux) and does not require an intermediate file:
ffmpeg -f concat -safe 0 -i <(for f in ./*.mp4; do echo "file '$PWD/$f'"; done) -c copy output.mp4
Here, the <() syntax actually creates a temporary file "in the background" so to say
I ended up using mpg as the intermediate format and it worked (NOTE this is a dangerous example, -qscale 0 will re-encode the video...)
ffmpeg -i 1.mp4 -qscale 0 1.mpg
ffmpeg -i 2.mp4 -qscale 0 2.mpg
cat 1.mpg 2.mpg | ffmpeg -f mpeg -i - -qscale 0 -vcodec mpeg4 output.mp4
Here 2 pure bash solutions using only ffmpeg and not using
an intermediary file
perl, python nor javascript
One-liner solution using ls
ls video1.mp4 video2.mp4 | while read line; do echo file \'$line\'; done | ffmpeg -protocol_whitelist file,pipe -f concat -i - -c copy output.mp4
Function which takes 0 or 2+ arguments
#######################################
# Merge mp4 files into one output mp4 file
# usage:
# mergemp4 #merges all mp4 in current directory
# mergemp4 video1.mp4 video2.mp4
# mergemp4 video1.mp4 video2.mp4 [ video3.mp4 ...] output.mp4
#######################################
function mergemp4() {
if [ $# = 1 ]; then return; fi
outputfile="output.mp4"
#if no arguments we take all mp4 in current directory as array
if [ $# = 0 ]; then inputfiles=($(ls -1v *.mp4)); fi
if [ $# = 2 ]; then inputfiles=($1 $2); fi
if [ $# -ge 3 ]; then
outputfile=${#: -1} # Get the last argument
inputfiles=(${#:1:$# - 1}) # Get all arguments besides last one as array
fi
# -y: automatically overwrite output file if exists
# -loglevel quiet: disable ffmpeg logs
ffmpeg -y \
-loglevel quiet \
-f concat \
-safe 0 \
-i <(for f in $inputfiles; do echo "file '$PWD/$f'"; done) \
-c copy $outputfile
if test -f "$outputfile"; then echo "$outputfile created"; fi
}
Note: had tried some solutions in this thread and none satisfied me
Detailed documentation on various ways of concatenation in ffmpeg can be found here.
You can use 'Concat filter' for quick concatenation.
It performs a re-encode. This option is best when inputs have different video/audio formats.
For Concatenating 2 files:
ffmpeg -i input1.mp4 -i input2.webm \
-filter_complex "[0:v:0] [0:a:0] [1:v:0] [1:a:0] concat=n=2:v=1:a=1 [v] [a]" \
-map "[v]" -map "[a]" output.mp4
For Concatenating 3 files:
ffmpeg -i input1.mp4 -i input2.webm -i input3.mp4 \
-filter_complex "[0:v:0] [0:a:0] [1:v:0] [1:a:0] [2:v:0] [2:a:0] concat=n=3:v=1:a=1 [v] [a]" \
-map "[v]" -map "[a]" output.mp4
This works for same as well as multiple input file types.
based on rogerdpack's and Ed999's responses, I've created my .sh version
#!/bin/bash
[ -e list.txt ] && rm list.txt
for f in *.mp4
do
echo "file $f" >> list.txt
done
ffmpeg -f concat -i list.txt -c copy joined-out.mp4 && rm list.txt
it joins all the *.mp4 files in current folder into joined-out.mp4
tested on mac.
resulting filesize is exact sum of my 60 tested files. Should not be any loss. Just what I needed
From the documentation here: https://trac.ffmpeg.org/wiki/Concatenate
If you have MP4 files, these could be losslessly concatenated by first transcoding them to MPEG-2 transport streams. With H.264 video and AAC audio, the following can be used:
ffmpeg -i input1.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate1.ts
ffmpeg -i input2.mp4 -c copy -bsf:v h264_mp4toannexb -f mpegts intermediate2.ts
ffmpeg -i "concat:intermediate1.ts|intermediate2.ts" -c copy -bsf:a aac_adtstoasc output.mp4
This approach works on all platforms.
I needed the ability to encapsulate this in a cross platform script, so I used fluent-ffmpeg and came up with the following solution:
const unlink = path =>
new Promise((resolve, reject) =>
fs.unlink(path, err => (err ? reject(err) : resolve()))
)
const createIntermediate = file =>
new Promise((resolve, reject) => {
const out = `${Math.random()
.toString(13)
.slice(2)}.ts`
ffmpeg(file)
.outputOptions('-c', 'copy', '-bsf:v', 'h264_mp4toannexb', '-f', 'mpegts')
.output(out)
.on('end', () => resolve(out))
.on('error', reject)
.run()
})
const concat = async (files, output) => {
const names = await Promise.all(files.map(createIntermediate))
const namesString = names.join('|')
await new Promise((resolve, reject) =>
ffmpeg(`concat:${namesString}`)
.outputOptions('-c', 'copy', '-bsf:a', 'aac_adtstoasc')
.output(output)
.on('end', resolve)
.on('error', reject)
.run()
)
names.map(unlink)
}
concat(['file1.mp4', 'file2.mp4', 'file3.mp4'], 'output.mp4').then(() =>
console.log('done!')
)
this worked for me (on windows)
ffmpeg -i "concat:input1|input2" -codec copy output
an example...
ffmpeg -i "concat:01.mp4|02.mp4" -codec copy output.mp4
Python
Using some python code to do it with as many mp4 there are in a folder (install python from python.org, copy and paste and save this code into a file called mp4.py and run it from the cmd opened in the folder with python mp4.py and all the mp4 in the folder will be concatenated)
import glob
import os
stringa = ""
for f in glob.glob("*.mp4"):
stringa += f + "|"
os.system("ffmpeg -i \"concat:" + stringa + "\" -codec copy output.mp4")
Version 2 with Python
Taken from my post on my blog, this is how I do it in python:
import os
import glob
def concatenate():
stringa = "ffmpeg -i \"concat:"
elenco_video = glob.glob("*.mp4")
elenco_file_temp = []
for f in elenco_video:
file = "temp" + str(elenco_video.index(f) + 1) + ".ts"
os.system("ffmpeg -i " + f + " -c copy -bsf:v h264_mp4toannexb -f mpegts " + file)
elenco_file_temp.append(file)
print(elenco_file_temp)
for f in elenco_file_temp:
stringa += f
if elenco_file_temp.index(f) != len(elenco_file_temp)-1:
stringa += "|"
else:
stringa += "\" -c copy -bsf:a aac_adtstoasc output.mp4"
print(stringa)
os.system(stringa)
concatenate()
Late answer, but this is the only option that actually worked for me:
(echo file '1.mp4' & echo file '2.mp4' & echo file '3.mp4' & echo file '4.mp4') | ffmpeg -protocol_whitelist file,pipe -f concat -safe 0 -i pipe: -vcodec copy -acodec copy "1234.mp4"
For .mp4 files, I found it works better and faster to use the opensource command line tool: mp4box. Then You can use it this way:
mp4box.exe -add video1.mp4 -cat video2.mp4 destvideo.mp4
Download it here for most platforms: https://gpac.wp.imt.fr/mp4box/
Here is a script I made to concatenate several GoPro mp4's into a 720p mp4. Hope it's of help.
#!/bin/sh
cmd="( "
for i; do
cmd="${cmd}ffmpeg -i $i -ab 256000 -vb 10000000 -mbd rd -trellis 2 -cmp 2 -subcmp 2 -g 100 -f mpeg -; "
done
cmd="${cmd} ) | ffmpeg -i - -vb 10000000 -ab 256000 -s 1280x720 -y out-`date +%F-%H%M.%S`.mp4"
echo "${cmd}"
eval ${cmd}
For Windows Powershell
create a file list
PS > ls file* | % { $n = $_.name; "file '\$n'" } | out-file mylist.txt
check the file created
PS > cat mylist.txt
file '/path/to/file1.mkv'
file '/path/to/file2.mkv'
file '/path/to/file3.mkv'
run ffmpeg (don't forget attach ./ for the list file)
PS > ffmpeg -safe 0 -f concat -i ./mylist.txt -c copy file.mkv
ffmpeg version git-2020-05-15-b18fd2b Copyright (c) 2000-2020 the FFmpeg developers
...
The main answer didn't work for me because it gave me the errors I describe below in the "Troubleshooting" section, so here is my own answer.
How to concatenate or combine many mp4 video files using ffmpeg
Quick Summary
Create an inputs.txt file containing a list of all file paths to combine (see example below). Then, combine all above videos into one like this:
# Option 1 (preferred): into a single mp4 video with AAC audio encoding
# and original video encoding
time ffmpeg -f concat -safe 0 -i inputs.txt -c:v copy -c:a aac output.mp4
# Option 2: into a single .mkv video with original audio and video encoding
time ffmpeg -f concat -safe 0 -i inputs.txt -c copy output.mkv
Details and full example of combining Wyze security camera footage
Tested on Ubuntu 20.04, combining a bunch of my Wyze security camera videos into one. (I plugged in the camera's micro SD card directly into my computer).
Create a file named inputs.txt, containing a list of all files you'd like to concatenate. Ex:
file 'path/to/file1.mp4'
file 'path/to/file2.mp4'
file 'path/to/file3.mp4'
In my case, I combined 69 minutes of video from 13 Nov. 2022 at 18:31hrs (6:31pm) to 13 Nov. 2022 at 19:39hrs (7:39pm). In Wyze security camera paths, that means from record/20221113/18/31.mp4' to record/20221113/19/39.mp4, inclusive. So, here is my full inputs.txt file:
file '/media/gabriel/3339-3730/record/20221113/18/31.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/32.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/33.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/34.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/35.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/36.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/37.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/38.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/39.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/40.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/41.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/42.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/43.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/44.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/45.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/46.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/47.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/48.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/49.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/50.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/51.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/52.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/53.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/54.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/55.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/56.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/57.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/58.mp4'
file '/media/gabriel/3339-3730/record/20221113/18/59.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/00.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/01.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/02.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/03.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/04.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/05.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/06.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/07.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/08.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/09.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/10.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/11.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/12.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/13.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/14.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/15.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/16.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/17.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/18.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/19.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/20.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/21.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/22.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/23.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/24.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/25.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/26.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/27.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/28.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/29.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/30.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/31.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/32.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/33.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/34.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/35.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/36.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/37.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/38.mp4'
file '/media/gabriel/3339-3730/record/20221113/19/39.mp4'
Combine all above videos into one:
# Option 1 (preferred): into a single mp4 video with AAC audio encoding
# and original video encoding
time ffmpeg -f concat -safe 0 -i inputs.txt -c:v copy -c:a aac output.mp4
# Option 2: into a single .mkv video with original audio and video encoding
time ffmpeg -f concat -safe 0 -i inputs.txt -c copy output.mkv
In my case, the original 69 files took up 411.1 MB. Here are the results of the two commands above:
output.mp4 took 11 seconds to produce and is 380.3 MB in size. The AAC-encoded audio is apparently a little smaller.
output.mkv took 8 seconds to produce and is 410.7MB in size.
Troubleshooting/possible errors
If you run ffmpeg -f concat -i inputs.txt -c copy output.mp4 and get this error:
[concat # 0x563ca0173700] Unsafe file name '/media/gabriel/3339-3730/record/20221113/18/31.mp4'
inputs.txt: Operation not permitted
...it is because you forgot to use -safe 0.
See:
https://nono.ma/says/concat-unsafe-file-name-operation-not-permitted
ffmpeg concat: "Unsafe file name"
If you run ffmpeg -f concat -safe 0 -i inputs.txt -c copy output.mp4 and get this error:
[mp4 # 0x55ced96f2100] Could not find tag for codec pcm_alaw in stream #1, codec not currently supported in container
Could not write header for output file #0 (incorrect codec parameters ?): Invalid argument
...it is because "ffmpeg does not support PCM (pcm_alaw, pcm_s16le, etc) in the MP4 container." See here: codec not currently supported in container and here. So, run time ffmpeg -f concat -safe 0 -i inputs.txt -c:v copy -c:a aac output.mp4 instead, to re-encode the audio into AAC format. Or, run time ffmpeg -f concat -safe 0 -i inputs.txt -c copy output.mkv to write into a .mkv container instead of into a .mp4 container.
Here is a script (works for an arbitrary number of specified files (not just all in the working directory), without additional files, also works for .mov; tested on macOS):
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Usage: `basename $0` input_1.mp4 input_2.mp4 ... output.mp4"
exit 0
fi
ARGS=("$#") # determine all arguments
output=${ARGS[${#ARGS[#]}-1]} # get the last argument (output file)
unset ARGS[${#ARGS[#]}-1] # drop it from the array
(for f in "${ARGS[#]}"; do echo "file '$f'"; done) | ffmpeg -protocol_whitelist file,pipe -f concat -safe 0 -i pipe: -vcodec copy -acodec copy $output
For those who need to concatenate a number of MP4 videos encoded with H.264, I propose a Python script mp4concat.py that automates the Concat protocol/using intermediate files paragraph from the ffmpeg documentation.
Download the script either from the link above or with
wget https://gist.githubusercontent.com/mwouts/115956d3fc7ece55cbce483a7a5148bd/raw/4bc5e03952f1b981dac3f97f4daf82c907774b17/mp4concat.py
and then use it as
python3 mp4concat.py input1.mp4 ... inputN.mp4 --output output.mp4
Merging all mp4 files from current directory
I personnaly like not creating external file that I have to delete afterwards, so my solution was following which includes files numbering listing (like file_1_name, file_2_name, file_10_name, file_20_name, file_100_name, ...)
#!/bin/bash
filesList=""
for file in $(ls -1v *.mp4);do #lists even numbered file
filesList="${filesList}${file}|"
done
filesList=${filesList%?} # removes trailing pipe
ffmpeg -i "concat:$filesList" -c copy $(date +%Y%m%d_%H%M%S)_merged.mp4
After various tries below script worked for me on windows 10 powershell.
$files=Get-ChildItem -path e:\ -Filter *.mp4
$files| ForEach-Object {"file '$($_.FullName)'"}| Out-File -FilePath e:\temp.txt -Encoding ASCII
if (-not (test-path "e:\ffmpeg\bin\ffmpeg.exe")) {throw "e:\ffmpeg\bin\ffmpeg.exe needed"}
E:\ffmpeg\bin\ffmpeg.exe -safe 0 -f concat -i "e:\temp.txt" -c copy -bsf:v hevc_mp4toannexb -an e:\joined.mp4
# Conversion Cleanup
Remove-Item e:\temp.txt
Here first two lines create a text file temp.txt which has following content
file 'e:\first.mp4'
file 'e:\second.mp4'
3rd, 4th lines checks if ffmpeg is available at path and create the "joined.mp4"
The key differences from other answers are as below
usage of -bsf:v hevc_mp4toannexb -an
for my mp4 file above worked, you may need to use other alternatives like below depending on your video encoding.
h264_mp4toannexb
All such possible Bitstream filters can be found at https://ffmpeg.org/ffmpeg-bitstream-filters.html
Here's my method for joining a directory full of MP4 files using command substitution and the concat video filter (this will re-encode) - figured someone else will get some use out of this one-liner, especially if you have many files (I just joined 17 files in one fell swoop):
ffmpeg $(for f in *.mp4 ; do echo -n "-i $f "; done) -filter_complex \
"$(i=0 ; for f in *.mp4 ; do echo -n "[$i:v] [$i:a] " ; i=$((i+1)) ; done \
&& echo "concat=n=$i:v=1:a=1 [v] [a]")" -map "[v]" -map "[a]" output.mp4
N.B. this command joins your files in the order in which they're named (i.e. the same order as they're presented if you run ls *.mp4) - in my case, they each had a track number, so it worked great.
If you want to concatenate files without having to create an annoying separate text file just to include the file names you can do this:
echo -e "file 'in1.mp4'\nfile 'in2.mp4'" | ffmpeg -f concat -safe 0 -i /dev/stdin -c copy out.mp4
In fact every time a command requires a file to be passed in you can instead echo out a string and pipe it out to your command and just use /dev/stdin as the filename.
ffmpeg \
-i input_1.mp4 \
-i input_2.mp4 \
-filter_complex '[0:v]pad=iw*2:ih[int];[int][1:v]overlay=W/2:0[vid]' \
-map [vid] \
-c:v libx264 \
-crf 23 \
-preset veryfast \
output.mp4
The concat protocol described here;
https://trac.ffmpeg.org/wiki/Concatenate#protocol
When implemented using named pipes to avoid intermediate files
Is very fast (read: instant), has no frames dropped, and works well.
Remember to delete the named pipe files and remember to check if the video is H264 and AAC which you can do with just ffmpeg -i filename.mp4 (check for h264 and aac mentions)
The accepted answer in the form of reusable PowerShell script
Param(
[string]$WildcardFilePath,
[string]$OutFilePath
)
try
{
$tempFile = [System.IO.Path]::GetTempFileName()
Get-ChildItem -path $wildcardFilePath | foreach { "file '$_'" } | Out-File -FilePath $tempFile -Encoding ascii
ffmpeg.exe -safe 0 -f concat -i $tempFile -c copy $outFilePath
}
finally
{
Remove-Item $tempFile
}
If you prefer method #2 from rogerdpack's answer but you don't want to use pipe (e.g. you just want to use execv in C) or don't want to create extra files (list.txt), then just combine concat demuxer with data and file protocols, i.e. FFmpeg allows you to inline input files almost as in HTML:
<img src="data:image/png;base64,..." alt="" />
ffmpeg -i 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAQ4AQAAAADAqPzuAAABEklEQVR4Ae3BAQ0AAADCIPunfg8HDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4FT45QABPFL5RwAAAABJRU5ErkJggg==' /tmp/blackbg.mp4
Below is my program (put it in /usr/bin/ffconcat) that automates inlining of "a file containing a list of filepaths". Also, unlike all other answers, you can use any FFmpeg options.
If you use something other than bash language (C, Node.js), then just look at the usage () and the last line.
#!/bin/bash
# ffconcat v0.3
# #author Arzet Ro, 2021 <arzeth0#gmail.com>
# #license CC-0 (Public Domain)
function usage ()
{
echo "\
ffconcat's usage:
ffconcat (anyFfmpegInputOptions) -i /tmp/a.mp4 -i ... -i ... /tmp/out.mp4 (anyFfmpegOutputOptions)
ffconcat -vn /tmp/a.mp4 /tmp/b.opus /tmp/out.mp4 -y
ffconcat -http -i https://a/h264#720p#25fps+opus.mp4 -i ftp://127.0.0.1/h264#720p#30fps+opus.mp4 -i /tmp/c.opus /tmp/out.mkv
ffconcat -http -i https://a/vp9#1080p#30fps+opus.mp4 -i ftp://127.0.0.1/vp9#720p#30fps+opus.mp4 -i /tmp/c.opus /tmp/out.mp4
WARNING: ffconcat uses `concat` demuxer; when
using both this demuxer AND -y, FFmpeg doesn't check if
an input file and output file
are the same file, so your 100 GB input file
could immediately become 10 KB.
ffconcat checks that only when neither -i
nor new FFmpeg release's boolean args (see valuelessFfmpegArgs in the code)
are specified.
ffmpeg has no -http.
ffconcat has -http because ffmpeg automatically
sets allowed protocols depending on -f and -i.
But when -f concat, ffmpeg doesn't know what to do with -i.
ffmpeg and mpv support VP9+Opus in .mp4
Only one video codec is possible in an output file.
You can't have both AAC and Opus in one .mp4 (not sure about other containers).
If you combine VP9 videos, then make sure they have the same FPS.
If you combine H264 videos of different resolutions,
then you get A/V desync
and also either
1) the start of video streams after the first video stream are cut
2) or video player freezes for 5 seconds when switching between video streams.
Also it seems if DAR (display aspect ratio) differs (at least in H.264)
then incorrect (5x longer) duration is estimated
and mpv displays the second video with 1 FPS.
You can see the info about an input file
with
mediainfo file.mp4
or
ffprobe -hide_banner -protocol_whitelist file,rtp,udp -show_streams file.mp4"
}
# Configuration [begin]
forceRequireIArgumentForInputFiles=0
# Configuration [end]
in_array ()
{
local e match="$1"
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}
if [[ "$#" == 0 ]]
then
usage
exit
fi
requireIArgumentForInputFiles=0
if in_array "--help" "$#"
then
usage
exit
elif in_array "-help" "$#"
then
usage
exit
elif in_array "-i" "$#"
then
requireIArgumentForInputFiles=1
elif [[ "$forceRequireIArgumentForInputFiles" == "1" ]]
then
>&2 echo "forceRequireIArgumentForInputFiles=1, so you need -i"
usage
exit 1
fi
NL=$'\n'
inputOptions=()
outputOptions=()
inputFilesRawArray=()
outputFile=""
declare -a valuelessFfmpegArgs=("-http" "-hide_banner" "-dn" "-n" "-y" "-vn" "-an" "-autorotate" "-noautorotate" "-autoscale" "-noautoscale" "-stats" "-nostats" "-stdin" "-nostdin" "-ilme" "-vstats" "-psnr" "-qphist" "-hwaccels" "-sn" "-fix_sub_duration" "-ignore_unknown" "-copy_unknown" "-benchmark" "-benchmark_all" "-dump" "-hex" "-re" "-copyts" "-start_at_zero" "-shortest" "-accurate_seek" "-noaccurate_seek" "-seek_timestamp" "write_id3v2" "write_apetag" "write_mpeg2" "ignore_loop" "skip_rate_check" "no_resync_search" "export_xmp")
#^ used when requireIArgumentForInputFiles=0
# TODO: fill all the args
# grep -C 3 AV_OPT_TYPE_BOOL libavformat/ libavcodec/
# grep -C 3 OPT_BOOL fftools/
# Unfortunately, unlike MPV, FFmpeg neither
# requires nor supports `=`, i.e. `--y --i=file.mp4'
# instead of `-y -i file.mp4`.
# Which means it's unclear whether an argument
# is a value of an argument or an input/output file.
areFfmpegArgsAllowed=1
isHttpMode=0
if in_array "-http" "$#"
then
isHttpMode=1
fi
# if an argument is not a boolean argument, then what key needs a value
secondArgumentIsWantedByThisFirstArgument=""
# if requireIArgumentForInputFiles=1
# then secondArgumentIsWantedByThisFirstArgument must be either "" or "-i"
isCurrentArgumentI=0
localRawFilesArray=()
outputFile=""
for arg in "$#"
do
if [[
"$secondArgumentIsWantedByThisFirstArgument" == ""
&&
"$arg" == "-http"
]]
then
continue
fi
if [[ "$arg" == "--" ]]
then
areFfmpegArgsAllowed=0
continue
fi
if [[
(
"$areFfmpegArgsAllowed" == "1"
||
"$secondArgumentIsWantedByThisFirstArgument" != ""
)
&& !(
"$requireIArgumentForInputFiles" == "1"
&&
"$secondArgumentIsWantedByThisFirstArgument" == "-i"
)
&&
(
"$secondArgumentIsWantedByThisFirstArgument" != ""
||
(
"$requireIArgumentForInputFiles" == "0"
&&
"$arg" = -*
)
||
(
"$requireIArgumentForInputFiles" == "1"
)
)
]]
then
if [[ !(
"$requireIArgumentForInputFiles" == "1"
&&
"$arg" == "-i"
) ]]
then
if (( ${#inputFilesRawArray[#]} == 0 ))
then
inputOptions+=("$arg")
else
outputOptions+=("$arg")
fi
fi
elif [[
"$requireIArgumentForInputFiles" == "0"
||
"$secondArgumentIsWantedByThisFirstArgument" == "-i"
]]
then
if echo -n "$arg" | egrep '^(https?|ftp)://'
then
inputFilesRawArray+=("$arg")
localRawFilesArray+=("$arg")
else
tmp=`echo -n "$arg" | sed 's#^file:##'`
localRawFilesArray+=("$tmp")
if [[ "$secondArgumentIsWantedByThisFirstArgument" == "-i" ]]
then
if ! ls -1d -- "$tmp" >/dev/null 2>/dev/null
then
>&2 echo "Input file '$tmp' not found"
exit 1
fi
fi
tmp=`echo -n "$tmp" | sed -E 's#(\s|\\\\)#\\\\\1#g' | sed "s#'#\\\\\'#g"`
# ^ FIXME: does it work for all filenames?
inputFilesRawArray+=("file:$tmp")
fi
elif [[
"$requireIArgumentForInputFiles" == "1"
&&
"$secondArgumentIsWantedByThisFirstArgument" != "-i"
]]
then
if echo -n "$arg" | egrep '^(https?|ftp)://'
then
outputFile="$arg"
else
outputFile=`echo -n "$arg" | sed 's#^file:##'`
outputFile="file:$outputFile"
fi
else
usage
exit 1
fi
if [[
"$secondArgumentIsWantedByThisFirstArgument" != ""
||
"$areFfmpegArgsAllowed" == "0"
]]
then
secondArgumentIsWantedByThisFirstArgument=""
else
if [[ "$requireIArgumentForInputFiles" == "1" && "$arg" == "-i" ]]
then
secondArgumentIsWantedByThisFirstArgument="$arg"
elif [[ "$requireIArgumentForInputFiles" == "0" && "$arg" = -* ]]
then
if ! in_array "$arg" ${valuelessFfmpegArgs[#]}
then
secondArgumentIsWantedByThisFirstArgument="$arg"
fi
fi
fi
done
if [[
"$requireIArgumentForInputFiles" == "0"
&&
"$outputFile" == ""
]]
then
outputFile="${localRawFilesArray[((${#localRawFilesArray[#]}-1))]}"
fi
actualOutputFile="$outputFile"
if [[ "$requireIArgumentForInputFiles" == "0" || "file:" =~ ^"$outputFile"* ]]
then
actualOutputFile=`echo -n "$actualOutputFile" | sed 's#^file:##'`
actualOutputFile=`readlink -nf -- "$actualOutputFile"`
fi
if [[ "$requireIArgumentForInputFiles" == "0" ]]
then
unset 'inputFilesRawArray[((${#inputFilesRawArray[#]}-1))]'
unset 'localRawFilesArray[((${#localRawFilesArray[#]}-1))]'
outputOptions+=("$outputFile")
fi
#>&2 echo Input: ${inputFilesRawArray[#]}
#if [[ "$requireIArgumentForInputFiles" == "0" ]]
#then
# >&2 echo Output: $outputFile
#fi
if (( ${#inputFilesRawArray[#]} < 2 ))
then
>&2 echo "Error: Minimum 2 input files required, ${#inputFilesRawArray[#]} given."
>&2 echo Input: ${inputFilesRawArray[#]}
if [[ "$requireIArgumentForInputFiles" == "0" ]]
then
>&2 echo Output: $outputFile
fi
usage
#outputFile=/dev/null
exit 1
fi
if [[
"$requireIArgumentForInputFiles" == "0"
&&
"$outputFile" == ""
]]
then
>&2 echo "Error: No output file specified."
usage
exit 1
fi
ffmpegInputList=""
firstFileDone=0
inputFilesRawArrayLength=${#inputFilesRawArray[#]}
for (( i = 0; i < inputFilesRawArrayLength; i++ ))
do
lf="${localRawFilesArray[$i]}"
f="${inputFilesRawArray[$i]}"
if [[ "${inputFilesRawArray[$i]}" =~ ^file: ]]
then
actualF=`readlink -nf -- "$lf"`
if [[ "$actualF" == "$actualOutputFile" ]]
then
>&2 echo "Error: The same file '$actualF' is used both as an input file and an output file"
exit 1
fi
fi
if [[ "$firstFileDone" == "1" ]]
then
ffmpegInputList+="$NL"
fi
ffmpegInputList+="file $f"
firstFileDone=1
done
protocol_whitelist_appendage=""
if [[ "$isHttpMode" == "1" ]]
then
protocol_whitelist_appendage=",ftp,http,https"
fi
# Also print the final line:
set -x
ffmpeg \
-safe 0 \
-f concat \
-protocol_whitelist data,file"$protocol_whitelist_appendage" \
"${inputOptions[#]}" \
-i "data:text/plain;charset=UTF-8,${ffmpegInputList}" \
-c copy \
"${outputOptions[#]}"
# $ffmpegInputList is
# file file:./test.mp4\nfile file:/home/aaa.mp4\nfile http://a/b.aac
# All whitespace and ' in ffmpegInputList are escaped with `\`.
Percent-encoding (JavaScript's encodeURI/encodeURIComponent) (%20, etc.) is not needed in -i, unlike in HTML.
I had clips with different encoders libx264 and H.264
I converted all clips to libx264 and used demuxer approach from top-voted answer.
ffmpeg -i input.flv -vcodec libx264 output.mp4
ffmpeg -i input1.ts -i input2.ts -i input3.ts -filter_complex "concat=n=3:v=1:a=0" -vn -y output.ts
enjoy!!

Resources