3

I am trying to use FFmpeg to convert a video to images in Bash. I would like to use the videos' filenames to name corresponding images (followed by a integer number).

I was able to do this if I was exporting the files in the same directory:

for file in `find . -name "*.mp4"`; do ffmpeg -i $file -q 1 $file'_'%d.jpeg; done

However, if I were to export images into a desired directory, I got errors. Sees like the value of $file is not just the file name, but a directory.

what I was trying:

for file in `find . -name "*.mp4"`; do ffmpeg -i $file -q 1 ~/testfolder/$file'_'%d.jpeg; done

My question is: how could I properly extract just the filename to use here?

Mateusz Piotrowski
  • 8,029
  • 10
  • 53
  • 79
Shawn
  • 33
  • 5

3 Answers3

4

Do it like this, as it will work with files containing any special characters (including newlines, which are rarely used but valid in filenames).

destdir=~/testfolder/
while read -u 5 -r -d '' file
do
  name=$(basename "$file")
  ffmpeg -i "$file" -q 1 "$destdir/${file}_%d.jpeg" </dev/null
done 5< <(find . -name "*.mp4" -print0)

The basename command is an external program. You can achieve the same result with the following lines using only shell expansions :

name=${file%/}
name=${name##*/}
Fred
  • 6,590
  • 9
  • 20
  • 1
    Note that you may need to redirect the standard input for `ffmpeg` from `/dev/null` so that it doesn't read from the output of the `find` command. – chepner Feb 26 '17 at 13:44
  • I added a file descriptor for the loop, and redirected `ffmpeg` input from `/dev/null`. Does `ffmpep` actually read from standard input, or is that more of a general precaution? – Fred Feb 26 '17 at 15:05
2

You don't need neither basename nor a loop for this; You can use -type f to avoid find reporting directories in it's result combined with -exec option to execute directly your command.

Something like this should work for all your mp4 files:

find . -type f -name '*.mp4' -exec bash -c 'ffmpeg -i $0 -q 1 $0'_'%d.jpeg' {} \; 

See a small demo test:

$ find . -type f -name 'a*.txt' -exec bash -c 'echo ffmpeg -i $0 ' {} \; 
ffmpeg -i ./a.txt
ffmpeg -i ./a spaced file.txt
ffmpeg -i ./aa.txt
ffmpeg -i ./cheatsheets/awk-cheat-sheet-gv.txt

As you can see second file has spaces in the file name, but is handled correctly by find.
On the contrary such a spaced file will break with a for loop.

If you insist to make this job with a loop , then this must be a while loop as advised by Fred.

As a modification to Fred solution you can avoid the use of basename using the -printf capabilities of find:

while read -r -d '' file;do 
  ffmpeg -i "$file" -q 1 "${file}_%d.jpeg"
done < <(find . -name "*.mp4" -printf %f\\0)

-printf %f according to man page of find prints the file name stripped, and appending \\0 (null char) after each file name we ensure correct filenames handling even if names contain spaces or other special chars.

Small test:

$ while read -r -d '' file;do echo "ffmpeg -i $file -q 1 ${file}_%d.jpeg";done < <(find . -name "a*.txt" -printf %f\\0)
ffmpeg -i a.txt -q 1 a.txt_%d.jpeg
ffmpeg -i a spaced file.txt -q 1 a spaced file.txt_%d.jpeg
ffmpeg -i aa.txt -q 1 aa.txt_%d.jpeg
ffmpeg -i awk-cheat-sheet-gv.txt -q 1 awk-cheat-sheet-gv.txt_%d.jpeg
George Vasiliou
  • 6,130
  • 2
  • 20
  • 27
  • Hmm - this solution doesn't put the output files into the destination - it writes them straight back into the input folder - I don't think that is what the OP asked for here. I don't think it actually answers the question. – Danny Staple Feb 26 '17 at 19:51
  • I don't think that it does not work. The -exec part of find is not built by me but by OP (`ffmpeg -i $0 -q 1 $0'_'%d.jpeg`). I just included it inside find , nothing more. – George Vasiliou Feb 26 '17 at 20:08
-1

The bash command basename can be used for this.

In this case the following should list only the files:

for file in $(find . -name "*.mp4"); do echo $(basename $file); done

You can use $(basename $file) in your original code to get the filename only.

for file in $(find . -name "*.mp4"); do 
  ffmpeg -i "${file}" -q 1 "~/testfolder/$(basename $file)'_'%d.jpeg"
done
Danny Staple
  • 7,101
  • 4
  • 43
  • 56
  • Thanks a lot, @Danny! It works! To clarify, I needed to substitute the ~/testfolder/$file'_'%d.jpeg with ~/testfolder/$(basename $file)'_'%d.jpeg – Shawn Feb 25 '17 at 23:23
  • Ah yes - I missed that it was used twice. I've corrected that answer now. If it works - can you marked it as the accepted answer please? – Danny Staple Feb 25 '17 at 23:26
  • Just did! Thanks again! – Shawn Feb 25 '17 at 23:28
  • 2
    Please note that using a command substitution in a `for` loop will break on any file that contains characters that cause word splitting to operate (e.g. spaces). Consider using a `while` loop with the `find` command fed to the loop with a process substitution (`< <(find...)`). – Fred Feb 25 '17 at 23:32
  • To add to @Fred's valid point: [Do not use `for` to parse command output](http://mywiki.wooledge.org/DontReadLinesWithFor). `basename` is an _external utility_ that you can _call from_ Bash, but it has nothing to do with Bash per se. Not double-quoting the variable references and command substitutions makes their values subject to word-splitting and globbing. – mklement0 Feb 25 '17 at 23:49
  • 1
    This makes sense. I was attempting to answer the question in the context of the OP's code. Thank you for the further reading @mklement0, I'll digest that now. – Danny Staple Feb 25 '17 at 23:51
  • @DannyStaple: Understood re focusing on the immediate fix, but I think it's important to at least _call out_ bad practices in the code copied from the OP, or, preferably, show a robust alternative. As an aside: The first `ffmpeg` operand must be just `"$file"`, because input files that aren't located directly in `.` can't be passed by their filename only. – mklement0 Feb 26 '17 at 02:57
  • Ah good spot - that needs a fix. For my own education, why process substitution and not piping? – Danny Staple Feb 26 '17 at 11:46
  • 2
    @DannyStaple: You need to @-mention me to be notified of responses. Piping involves subshells, and if you define variables in a subshell, the current shell doesn't see them. Using a process substitution allows you to run `read` in the current shell. Compare `unset var; echo 'hi' | read var; declare -p var` to `unset var; read var < <(echo 'hi'); declare -p var` – mklement0 Feb 26 '17 at 13:35