├── LICENSE.txt ├── Makefiles └── scene-detection │ └── Makefile ├── README.org ├── audio-silence ├── chapter-add ├── chapter-csv ├── chapter-extract ├── clip-time ├── combine-clips ├── correct-clip ├── crossfade-clips ├── ebu-meter ├── extract-frame ├── fade-clip ├── fade-normalize ├── fade-title ├── ffmpeg-tips.org ├── img2video ├── loudnorm ├── normalize ├── overlay-clip ├── overlay-pip ├── pan-scan ├── scene-cut ├── scene-detect ├── scene-images ├── scene-time ├── scopes ├── sexagesimal-time ├── subtitle-add ├── tile-thumbnails ├── trim-clip ├── vid2gif ├── waveform ├── webp ├── xfade └── zoompan /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) [year], [fullname] 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefiles/scene-detection/Makefile: -------------------------------------------------------------------------------- 1 | # ffmpeg scene detection makefile 2 | 3 | # Move this Makefile into the same directory as the video to process 4 | 5 | # you need to tell the Makefile the name of the video to process 6 | # either by editing the Makefile and changing input.mp4 to the name of the video to process 7 | # and then running: make 8 | 9 | # or by running: make input=filename.mp4 10 | # where filename.mp4 is the name of the file to process 11 | 12 | # make clean: will prompt you to remove text files and video clips, except the input file 13 | 14 | 15 | # change input.mp4 to the file name to process 16 | input := input.mp4 17 | 18 | # all - run the cut recipe which runs scene-detect, scene-time and scene-cut in order 19 | all: cut 20 | 21 | # scene-detect - input is the prerequisite 22 | detect: $(input) 23 | @scene-detect -i $(input) -o detection.txt 24 | 25 | 26 | # scene-time - detect is the prerequisite 27 | time: detect 28 | @scene-time -i detection.txt -o cutlist.txt 29 | 30 | 31 | # scene-cut - time is the prerequisite 32 | cut: time 33 | @scene-cut -i $(input) -c cutlist.txt 34 | 35 | 36 | # make clean - prompt to remove text files and video clips, except the input file 37 | .PHONY: clean 38 | clean: 39 | rm -fI cutlist.txt detection.txt $(wildcard *-[0-9]*.mp4) 40 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+STARTUP: content 2 | #+OPTIONS: num:nil author:nil 3 | * ffmpeg scripts 4 | 5 | A collection of ffmpeg shell scripts for basic editing tasks 6 | 7 | [[https://github.com/NapoleonWils0n/ffmpeg-scripts][ffmpeg scripts English version]] 8 | 9 | [[https://github.com/NapoleonWils0n/ffmpeg-Skripte][ffmpeg-Skripte Deutsche Ausgabe]] 10 | 11 | + [[#audio-silence][audio-silence]] 12 | + [[#combine-clips][combine-clips]] 13 | + [[#chapter-add][chapter-add]] 14 | + [[#chapter-csv][chapter-csv]] 15 | + [[#clip-extract][clip-extract]] 16 | + [[#clip-time][clip-time]] 17 | + [[#correct-clip][correct-clip]] 18 | + [[#crossfade-clips][crossfade-clips]] 19 | + [[#ebu-meter][ebu-meter]] 20 | + [[#extract-frame][extract-frame]] 21 | + [[#fade-clip][fade-clip]] 22 | + [[#fade-normalize][fade-normalize]] 23 | + [[#fade-title][fade-title]] 24 | + [[#img2video][img2video]] 25 | + [[#loudnorm][loudnorm]] 26 | + [[#normalize][normalize]] 27 | + [[#overlay-clip][overlay-clip]] 28 | + [[#overlay-pip][overlay-pip]] 29 | + [[#pan-scan][pan-scan]] 30 | + [[#scene-cut][scene-cut]] 31 | + [[#scene-detect][scene-detect]] 32 | + [[#scene-images][scene-images]] 33 | + [[#scene-time][scene-time]] 34 | + [[#sexagesimal-time][sexagesimal-time]] 35 | + [[#sub2transcript][subs2transcript]] 36 | + [[#subtitle-add][subtitle-add]] 37 | + [[#scopes][scopes]] 38 | + [[#tile-thumbnails][tile-thumbnails]] 39 | + [[#trim-clip][trim-clip]] 40 | + [[#vid2gif][vid2gif]] 41 | + [[#waveform][waveform]] 42 | + [[#webp][webp]] 43 | + [[#xfade][xfade]] 44 | + [[#zoompan][zoompan]] 45 | 46 | ** scripts install 47 | 48 | [[https://youtu.be/UHshlQvdwcQ][ffmpeg scripts install youtube]] 49 | 50 | *** create a bin directory 51 | 52 | create a bin directory in your home to add the scripts to 53 | 54 | #+BEGIN_SRC sh 55 | mkdir -p ~/bin 56 | #+END_SRC 57 | 58 | if you are using bash add the following code to your ~/.bashrc 59 | 60 | #+BEGIN_SRC sh 61 | if [ -d "$HOME/bin" ]; then 62 | PATH="$HOME/bin:$PATH" 63 | fi 64 | #+END_SRC 65 | 66 | if you are using zsh add the following code to your ~/.zshenv file 67 | 68 | #+begin_src sh 69 | typeset -U PATH path 70 | path=("$HOME/bin" "$path[@]") 71 | export PATH 72 | #+end_src 73 | 74 | + source your ~/.bashrc if you are using the bash shell 75 | 76 | #+BEGIN_SRC sh 77 | source ~/.bashrc 78 | #+END_SRC 79 | 80 | + source your ~/.zshenv if you are using the zsh shell 81 | 82 | #+BEGIN_SRC sh 83 | source ~/.zshenv 84 | #+END_SRC 85 | 86 | *** clone the git repository 87 | 88 | create a git directory in you home folder to download the scripts into, 89 | or use any other location in your file system 90 | 91 | #+BEGIN_SRC sh 92 | mkdir -p ~/git 93 | #+END_SRC 94 | 95 | change directory in the git directory 96 | 97 | #+BEGIN_SRC sh 98 | cd ~/git 99 | #+END_SRC 100 | 101 | clone the git repository 102 | 103 | #+BEGIN_SRC sh 104 | git clone https://github.com/NapoleonWils0n/ffmpeg-scripts.git 105 | #+END_SRC 106 | 107 | update the scripts using git pull 108 | 109 | *** copy or symlink scripts into the bin directory 110 | 111 | you can now either copy the scripts into the ~/bin directory in your home, 112 | or create symbolic links from the scripts in the ~/git/ffmpeg-scripts directory to the ~/bin directory 113 | 114 | creating a symbolic link 115 | 116 | #+BEGIN_SRC sh 117 | ln -s path/to/source path/to/destination 118 | #+END_SRC 119 | 120 | example 121 | 122 | #+BEGIN_SRC sh 123 | ln -s ~/git/ffmpeg-scripts/trim-clip ~/bin 124 | #+END_SRC 125 | 126 | *** ffmpeg install 127 | 128 | **** linux ffmpeg install 129 | 130 | install ffmpeg on debian or ubuntu, 131 | for other linux distros see the documentation for your package manager 132 | 133 | #+BEGIN_SRC sh 134 | sudo apt install ffmpeg 135 | #+END_SRC 136 | 137 | **** mac ffmpeg install 138 | 139 | open a terminal and run the following commands to install the xcode command line tools, homebrew and ffmpeg 140 | 141 | + xcode command line tools install 142 | 143 | #+BEGIN_SRC sh 144 | xcode-select --install 145 | #+END_SRC 146 | 147 | + homebrew install 148 | 149 | #+BEGIN_SRC sh 150 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 151 | #+END_SRC 152 | 153 | + ffmpeg install with libfdk_aac 154 | 155 | #+BEGIN_SRC sh 156 | brew tap homebrew-ffmpeg/ffmpeg 157 | brew install homebrew-ffmpeg/ffmpeg/ffmpeg --with-fdk-aac --HEAD 158 | #+END_SRC 159 | 160 | + ffmpeg upgrade 161 | 162 | #+BEGIN_SRC sh 163 | brew update && brew upgrade homebrew-ffmpeg/ffmpeg/ffmpeg --fetch-HEAD 164 | #+END_SRC 165 | 166 | **** freebsd ffmpeg install 167 | 168 | switch to root and install the ffmpeg package 169 | 170 | #+BEGIN_SRC sh 171 | pkg install ffmpeg 172 | #+END_SRC 173 | 174 | you can also install ffmpeg from ports, 175 | or use poudriere to build the ffmpeg package 176 | 177 | note the ebumeter script uses ffplay which isnt installed with the ffmpeg package, 178 | so you need to build ffmpeg with the sdl option enable from ports or with poudriere 179 | 180 | if you want to use the libfdk_aac audio you should also enable that option when building 181 | the ffmpeg port, and build the lame package for mp3 support 182 | 183 | **** windows ffmpeg install 184 | 185 | install the windows subsystem for linux and then install a linux distro like ubuntu, 186 | then follow the linux install instructions 187 | 188 | ** audio-silence 189 | :PROPERTIES: 190 | :CUSTOM_ID: audio-silence 191 | :END: 192 | 193 | audio-silence add silent audio to a video clip 194 | 195 | If the video doesnt have an audio track the script copies the video track, 196 | and adds a silent audio track to match the duration of the video and creates a new video clip 197 | 198 | If the video has a video and audio track the script only copies the video track, 199 | and adds a silent audio track to match the duration of the video and creates a new video clip. 200 | 201 | [[https://youtu.be/OB8RvyenCLY][audio-silence youtube]] 202 | 203 | + script usage 204 | 205 | #+BEGIN_SRC sh 206 | audio-silence -i input.(mp4|mkv|mov|m4v) -c (mono|stereo) -r (44100|48000) -o output.mp4 207 | #+END_SRC 208 | 209 | + script help 210 | 211 | #+begin_src sh 212 | audio-silence -h 213 | #+end_src 214 | 215 | #+BEGIN_EXAMPLE 216 | # audio-silence add silent audio to a video clip 217 | 218 | audio-silence -i input.(mp4|mkv|mov|m4v) -c (mono|stereo) -r (44100|48000) -o output.mp4 219 | -i input.(mp4|mkv|mov|m4v) 220 | -c (mono|stereo) : optional argument # if option not provided defaults to mono 221 | -r (44100|48000) : optional argument # if option not provided defaults to 44100 222 | -o output.mp4 : optional argument # if option not provided defaults to input-name-silence-date-time 223 | #+END_EXAMPLE 224 | 225 | *** audio-silence batch process 226 | 227 | Batch process files in the current working directory 228 | 229 | Note we omit the -o option to use the default outfile name, 230 | which is infile-name-silence-date-time 231 | 232 | audio-silence batch process without specifying the -c and -r options 233 | using the defaults of -c mono and -r 44100 234 | 235 | #+BEGIN_SRC sh 236 | find . -type f -name "*.mp4" -exec sh -c \ 237 | 'audio-silence -i "${0}"' 238 | "{}" \; 239 | #+END_SRC 240 | 241 | audio-silence batch process and override the defaults 242 | with the -c and -r options 243 | 244 | #+BEGIN_SRC sh 245 | find . -type f -name "*.mp4" -exec sh -c \ 246 | 'audio-silence -i "${0}" -c stereo -r 48000' 247 | "{}" \; 248 | #+END_SRC 249 | 250 | ** chapter-add 251 | :PROPERTIES: 252 | :CUSTOM_ID: chapter-add 253 | :END: 254 | 255 | add chapters to a video or audio file with ffmpeg using a metadata file, 256 | use the chapter-csv script to create the metadata file from a csv files 257 | 258 | + script usage 259 | 260 | #+BEGIN_SRC sh 261 | chapter-add -i input -c metadata.txt -o output 262 | #+END_SRC 263 | 264 | + script help 265 | 266 | #+begin_src sh 267 | chapter-add -h 268 | #+end_src 269 | 270 | ** chapter-csv 271 | :PROPERTIES: 272 | :CUSTOM_ID: chapter-csv 273 | :END: 274 | 275 | convert a csv file into a chapter metadata file for ffmpeg 276 | 277 | + script usage 278 | 279 | #+BEGIN_SRC sh 280 | chapter-csv -i input -o output 281 | #+END_SRC 282 | 283 | + script help 284 | 285 | #+begin_src sh 286 | chapter-add -h 287 | #+end_src 288 | 289 | + csv file example 290 | 291 | The last record is the duration of the video and is used as the end time for the previous chapter,and End isnt used as a chapter 292 | 293 | #+begin_example 294 | 00:00:00,Intro 295 | 00:02:30,Scene 1 296 | 00:05:00,Scene 2 297 | 00:07:00,Scene 3 298 | 00:10:00,End 299 | #+end_example 300 | 301 | ** chapter-extract 302 | :PROPERTIES: 303 | :CUSTOM_ID: chapter-extract 304 | :END: 305 | 306 | extract chapters from a video or audo file and save as a csv file 307 | 308 | + script usage 309 | 310 | #+BEGIN_SRC sh 311 | chapter-extract -i input -o output 312 | #+END_SRC 313 | 314 | + script help 315 | 316 | #+begin_src sh 317 | chapter-extract -h 318 | #+end_src 319 | 320 | + convert the csv file to youtube timestamps 321 | 322 | #+begin_src sh 323 | tr ',' ' ' < input.txt > output.txt 324 | #+end_src 325 | 326 | ** clip-time 327 | :PROPERTIES: 328 | :CUSTOM_ID: clip-time 329 | :END: 330 | 331 | convert timestamps into start and duration 332 | 333 | + script usage 334 | 335 | #+BEGIN_SRC sh 336 | clip-time -i input -o output 337 | #+END_SRC 338 | 339 | + script help 340 | 341 | #+begin_src sh 342 | clip-time -h 343 | #+end_src 344 | 345 | #+begin_example 346 | clip-time -i input -o output 347 | 348 | -i input 349 | -o output 350 | #+end_example 351 | 352 | ** combine-clips 353 | :PROPERTIES: 354 | :CUSTOM_ID: combine-clips 355 | :END: 356 | 357 | combine an image or video file with an audio clip 358 | 359 | [[https://youtu.be/BUrmbakPQY8][combine-clips youtube]] 360 | 361 | + script usage 362 | 363 | #+BEGIN_SRC sh 364 | combine-clips -i input.(mp4|mkv|mov|m4v|png|jpg) -a audio.(m4a|aac|wav|mp3) -o output.mp4 365 | #+END_SRC 366 | 367 | + script help 368 | 369 | #+begin_src sh 370 | combine-clips -h 371 | #+end_src 372 | 373 | #+BEGIN_EXAMPLE 374 | # combine an image or video file with an audio clip 375 | 376 | combine-clips -i input.(mp4|mkv|mov|m4v|png|jpg) -a audio.(m4a|aac|wav|mp3) -o output.mp4 377 | -i input.(mp4|mkv|mov|m4v|png|jpg) 378 | -a audio.(m4a|aac|wav|mp3) 379 | -o output.mp4 : optional argument # if option not provided defaults to input-name-combined-date-time 380 | #+END_EXAMPLE 381 | 382 | *** combine-clips batch process 383 | 384 | Batch process files in the current working directory 385 | 386 | Note we omit the -o option to use the default outfile name, 387 | infile-name-combined-date-time 388 | 389 | + batch combine video and audio files into video clips 390 | 391 | The video and audio files you want to combine must have the same name 392 | 393 | for example 394 | 395 | #+BEGIN_EXAMPLE 396 | file1.mp4 397 | file1.wav 398 | file2.mp4 399 | file2.wav 400 | #+END_EXAMPLE 401 | 402 | running the following code will combine 403 | file1.mp4 with file1.wav and 404 | file2.mp4 with file2.wav 405 | 406 | #+BEGIN_SRC sh 407 | find . -type f -name "*.mp4" -exec sh -c \ 408 | 'combine-clip -i "${0}" -a "${0%.*}.wav"' \ 409 | "{}" \; 410 | #+END_SRC 411 | 412 | + batch combine images and audio files into video clips 413 | 414 | The images and audio files you want to combine must have the same name 415 | 416 | for example 417 | 418 | #+BEGIN_EXAMPLE 419 | file1.png 420 | file1.wav 421 | file2.png 422 | file2.wav 423 | #+END_EXAMPLE 424 | 425 | running the following code will combine 426 | file1.png with file1.wav and 427 | file2.png with file2.wav 428 | 429 | #+BEGIN_SRC sh 430 | find -s . -type f -name "*.png" -exec sh -c \ 431 | 'combine-clip -i "${0}" -a "${0%.*}.wav"' \ 432 | "{}" \; 433 | #+END_SRC 434 | 435 | ** correct-clip 436 | :PROPERTIES: 437 | :CUSTOM_ID: correct-clip 438 | :END: 439 | 440 | + curves code based on: 441 | [[https://video.stackexchange.com/questions/16352/converting-gimp-curves-files-to-photoshop-acv-for-ffmpeg/20005#20005][converting gimp curves files for ffmpeg]] 442 | 443 | correct a video clip by using a gimp curve converted into a ffmpeg curves filter command, 444 | to adjust the levels and white balance 445 | 446 | + requires a curve file created with the following script 447 | [[https://github.com/NapoleonWils0n/curve2ffmpeg][curve2ffmpeg]] 448 | 449 | [[https://youtu.be/wQi3Y-6vWYc][correct-clip youtube]] 450 | 451 | + script usage 452 | 453 | #+BEGIN_SRC sh 454 | correct-clip -i input.(mp4|mkv|mov|m4v) -c curve.txt -o output.mp4 455 | #+END_SRC 456 | 457 | + script help 458 | 459 | #+begin_src sh 460 | correct-clip -h 461 | #+end_src 462 | 463 | #+BEGIN_EXAMPLE 464 | # correct a video clip by using a gimp curve 465 | 466 | # requires a curve file created with the following script 467 | # https://github.com/NapoleonWils0n/curve2ffmpeg 468 | 469 | correct-clip -i input.(mp4|mkv|mov|m4v) -c curve.txt -o output.mp4 470 | -i input.(mp4|mkv|mov|m4v) 471 | -c curve.txt 472 | -o output.mp4 : optional argument # if option not provided defaults to input-name-corrected-date-time 473 | #+END_EXAMPLE 474 | 475 | *** correct-clip batch process 476 | 477 | Batch process files in the current working directory 478 | 479 | Note we omit the -o option to use the default outfile name, 480 | infile-name-corrected-date-time 481 | 482 | The video and gimp curve text files you want to combine must have the same name 483 | 484 | for example 485 | 486 | #+BEGIN_EXAMPLE 487 | file1.mp4 488 | file1.txt 489 | file2.mp4 490 | file2.txt 491 | #+END_EXAMPLE 492 | 493 | running the following code will correct 494 | file1.mp4 with file1.txt gimp curve file and 495 | file2.mp4 with file2.txt gimp curve file 496 | 497 | #+BEGIN_SRC sh 498 | find . -type f -name "*.mp4" -exec sh -c \ 499 | 'correct-clip -i "${0}" -c "${0%.*}.txt"' \ 500 | "{}" \; 501 | #+END_SRC 502 | 503 | ** crossfade-clips 504 | :PROPERTIES: 505 | :CUSTOM_ID: xfade-clips 506 | :END: 507 | 508 | cross fade 2 video clips with either a 1 or 2 second cross fade 509 | the videos must have the same codecs, size and frame rate 510 | 511 | [[https://youtu.be/0HnUNVreMVk][crossfade-clips youtube]] 512 | 513 | + script usage 514 | 515 | #+BEGIN_SRC sh 516 | crossfade-clips -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d (1|2) -o output.mp4 517 | #+END_SRC 518 | 519 | + script help 520 | 521 | #+begin_src sh 522 | crossfade-clips -h 523 | #+end_src 524 | 525 | #+BEGIN_EXAMPLE 526 | # ffmpeg cross fade clips 527 | 528 | crossfade-clips -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d (1|2) -o output.mp4 529 | -a clip1.(mp4|mkv|mov|m4v) : first clip 530 | -b clip2.(mp4|mkv|mov|m4v) : second clip 531 | -d (1|2) : cross fade duration :optional argument # if option not provided defaults to 1 second 532 | -o output.mp4 : optional argument # if option not provided defaults to input-name-xfade-date-time 533 | #+END_EXAMPLE 534 | 535 | ** ebu-meter 536 | :PROPERTIES: 537 | :CUSTOM_ID: ebu-meter 538 | :END: 539 | 540 | ffplay ebu meter 541 | 542 | [[https://youtu.be/8qrT9TfKwUI][ebu-meter youtube]] 543 | 544 | + script usage 545 | 546 | #+BEGIN_SRC sh 547 | ebu-meter -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -t (00) 548 | #+END_SRC 549 | 550 | -t = luf target, eg 16 551 | 552 | + script help 553 | 554 | #+begin_src sh 555 | ebu-meter -h 556 | #+end_src 557 | 558 | #+begin_example 559 | ebu-meter -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -t (00) 560 | #+end_example 561 | 562 | ** extract-frame 563 | :PROPERTIES: 564 | :CUSTOM_ID: extract-frame 565 | :END: 566 | 567 | extract a frame from a video and save as a png image 568 | 569 | [[https://trac.ffmpeg.org/wiki/Seeking][ffmpeg wiki seeking]] 570 | 571 | Note that you can use two different time unit formats: sexagesimal (HOURS:MM:SS.MILLISECONDS, as in 01:23:45.678), or in seconds. 572 | If a fraction is used, such as 02:30.05, this is interpreted as "5 100ths of a second", not as frame 5. 573 | For instance, 02:30.5 would be 2 minutes, 30 seconds, and a half a second, which would be the same as using 150.5 in seconds. 574 | 575 | [[https://youtu.be/cOk0i384crE][extract-frame youtube]] 576 | 577 | + script usage 578 | 579 | #+BEGIN_SRC sh 580 | extract-frame -i input.(mp4|mov|mkv|m4v|webm) -s 00:00:00.000 -t (png|jpg) -x width -y height -o output.(png|jpg) 581 | #+END_SRC 582 | 583 | + script help 584 | 585 | #+begin_src sh 586 | extract-frame -h 587 | #+end_src 588 | 589 | #+BEGIN_EXAMPLE 590 | # extract a frame from a video as a png or jpg 591 | https://trac.ffmpeg.org/wiki/Seeking 592 | 593 | extract-frame -i input.(mp4|mov|mkv|m4v|webm) -s 00:00:00.000 -t (png|jpg) -x width -y height -o output.(png|jpg) 594 | -i input.(mp4|mov|mkv|m4v) 595 | -s 00:00:00.000 : optional argument # if option not provided defaults to 00:00:00 596 | -t (png|jpg) : optional argument # if option not provided defaults to png 597 | -x width : optional argument # 598 | -y height : optional argument # 599 | -o output.(png|jpg) : optional argument # if option not provided defaults to input-name-timecode 600 | #+END_EXAMPLE 601 | 602 | *** extract-frame batch process 603 | 604 | Batch process files in the current working directory 605 | 606 | Note we omit the -o option to use the default outfile name, 607 | infile-name-frame-date-time 608 | 609 | + extract frame with default option of 00:00:00 610 | 611 | #+BEGIN_SRC sh 612 | find . -type f -name "*.mp4" -exec sh -c \ 613 | 'extract-frame -i "${0}"' \ 614 | "{}" \; 615 | #+END_SRC 616 | 617 | + extract frame at 30 seconds into the video 618 | 619 | #+BEGIN_SRC sh 620 | find . -type f -name "*.mp4" -exec sh -c \ 621 | 'extract-frame -i "${0}" -s 00:00:30' \ 622 | "{}" \; 623 | #+END_SRC 624 | 625 | ** fade-clip 626 | :PROPERTIES: 627 | :CUSTOM_ID: fade-clip 628 | :END: 629 | 630 | fade video and audio in and out 631 | 632 | [[https://youtu.be/ea3aCK9htsE][fade-clip youtube]] 633 | 634 | + script usage 635 | 636 | #+BEGIN_SRC sh 637 | fade-clip -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4 638 | #+END_SRC 639 | 640 | + script help 641 | 642 | #+begin_src sh 643 | fade-clip -h 644 | #+end_src 645 | 646 | #+BEGIN_EXAMPLE 647 | # fade video and audio in and out 648 | 649 | fade-clip -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4 650 | -i infile.(mp4|mkv|mov|m4v) 651 | -d (0.[0-9]|1) : optional argument # if option not provided defaults to 0.5 652 | -o output.mp4 : optional argument # if option not provided defaults to input-name-fade-date-time 653 | #+END_EXAMPLE 654 | 655 | *** fade-clip batch process 656 | 657 | Batch process files in the current working directory 658 | 659 | Note we omit the -o option to use the default outfile name, 660 | infile-name-fade-date-time 661 | 662 | + fade-clip with default option of 0.5 663 | 664 | #+BEGIN_SRC sh 665 | find . -type f -name "*.mp4" -exec sh -c \ 666 | 'fade-clip -i "${0}"' \ 667 | "{}" \; 668 | #+END_SRC 669 | 670 | + fade-clip and override the default option of 0.5 with -d 1 for a 1 second fade 671 | 672 | #+BEGIN_SRC sh 673 | find . -type f -name "*.mp4" -exec sh -c \ 674 | 'fade-clip -i "${0}" -d 1' \ 675 | "{}" \; 676 | #+END_SRC 677 | 678 | ** fade-normalize 679 | :PROPERTIES: 680 | :CUSTOM_ID: fade-normalize 681 | :END: 682 | 683 | fade video and audio in and out and normalize 684 | 685 | [[https://youtu.be/jufGDRAn8Ec][fade-normalize youtube]] 686 | 687 | + script usage 688 | 689 | #+BEGIN_SRC sh 690 | fade-normalize -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4 691 | #+END_SRC 692 | 693 | + script help 694 | 695 | #+begin_src sh 696 | fade-normalize -h 697 | #+end_src 698 | 699 | #+BEGIN_EXAMPLE 700 | # fade video and normalize audio levels 701 | 702 | fade-normalize -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4 703 | 704 | -d (0.[0-9]|1) : optional argument # if option not provided defaults to 0.5 705 | -o output.mp4 : optional argument # if option not provided defaults to input-name-normalized-date-time 706 | #+END_EXAMPLE 707 | 708 | *** fade-normalize batch process 709 | 710 | Batch process files in the current working directory 711 | 712 | #+BEGIN_SRC sh 713 | find . -type f -name "*.mp4" -exec sh -c \ 714 | 'fade-normalize -i "${0}" -d 0.5' \ 715 | "{}" \; 716 | #+END_SRC 717 | 718 | ** fade-title 719 | :PROPERTIES: 720 | :CUSTOM_ID: fade-title 721 | :END: 722 | 723 | fade video and audio in and out, 724 | normalize the audio and create video a lower third title from the filename 725 | 726 | [[https://youtu.be/RDnhaX_d9B0][fade-title youtube]] 727 | 728 | + script usage 729 | 730 | #+BEGIN_SRC sh 731 | fade-title -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -s 000 -e 000 -o output.mp4 732 | #+END_SRC 733 | 734 | + script help 735 | 736 | #+begin_src sh 737 | fade-title -h 738 | #+end_src 739 | 740 | #+BEGIN_EXAMPLE 741 | # fade video, audio add title from video filename 742 | 743 | fade-title -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -s 000 -e 000 -o output.mp4 744 | 745 | -i input.(mp4|mkv|mov|m4v) 746 | -d (0.[0-9]|1) : from 0.1 to 0.9 or 1 :optional argument # if option not provided defaults to 0.5 747 | -s 000 : from 000 to 999 748 | -e 000 : from 000 to 999 749 | -o output.mp4 : optional argument # if option not provided defaults to input-name-title-date-time 750 | #+END_EXAMPLE 751 | 752 | *** fade-title batch process 753 | 754 | Batch process files in the current working directory 755 | 756 | #+BEGIN_SRC sh 757 | find . -type f -name "*.mp4" -exec sh -c \ 758 | 'fade-title -i "${0}" -d 0.5 -s 10 -e 20' \ 759 | "{}" \; 760 | #+END_SRC 761 | 762 | ** img2video 763 | :PROPERTIES: 764 | :CUSTOM_ID: img2video 765 | :END: 766 | 767 | convert an image into a video file 768 | 769 | [[https://youtu.be/x_dVVvhKbJE][img2video youtube]] 770 | 771 | + script usage 772 | 773 | #+BEGIN_SRC sh 774 | img2video -i input.(png|jpg|jpeg) -d (000) -o output.mp4 775 | #+END_SRC 776 | 777 | + script help 778 | 779 | #+begin_src sh 780 | img2video -h 781 | #+end_src 782 | 783 | #+BEGIN_EXAMPLE 784 | # image to video 785 | 786 | img2video -i input.(png|jpg|jpeg) -d (000) -o output.mp4 787 | -i input.(mp4|mkv|mov|m4v) 788 | -d (000) : duration 789 | -o output.mp4 : optional argument # if option not provided defaults to input-name-video-date-time 790 | #+END_EXAMPLE 791 | 792 | *** img2video batch process 793 | 794 | Batch process files in the current working directory 795 | 796 | Note we omit the -o option to use the default outfile name, 797 | infile-name-video-date-time 798 | 799 | Batch convert png in the current directory into video clips with a 30 second duration 800 | 801 | #+BEGIN_SRC sh 802 | find . -type f -name "*.png" -exec sh -c \ 803 | 'img2video -i "${0}" -d 30' \ 804 | "{}" \; 805 | #+END_SRC 806 | 807 | ** loudnorm 808 | :PROPERTIES: 809 | :CUSTOM_ID: loudnorm 810 | :END: 811 | 812 | ffmpeg loudnorm 813 | 814 | [[https://youtu.be/8fQpbBCVCRc][loudnorm youtube]] 815 | 816 | + script usage 817 | 818 | #+BEGIN_SRC sh 819 | loudnorm -i infile.(mkv|mp4|mov|m4v|m4a|aac|wav|mp3) 820 | #+END_SRC 821 | 822 | + script help 823 | 824 | #+begin_src sh 825 | loudnorm -h 826 | #+end_src 827 | 828 | #+begin_example 829 | # ffmpeg loudnorm 830 | 831 | loudnorm -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 832 | #+end_example 833 | 834 | ** normalize 835 | :PROPERTIES: 836 | :CUSTOM_ID: normalize 837 | :END: 838 | 839 | normalize audio levels 840 | 841 | [[https://youtu.be/q_UjwuJmya4][normalize youtube]] 842 | 843 | + script usage 844 | 845 | #+BEGIN_SRC sh 846 | normalize -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) -o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 847 | #+END_SRC 848 | 849 | + script help 850 | 851 | #+begin_src sh 852 | normalize -h 853 | #+end_src 854 | 855 | #+BEGIN_EXAMPLE 856 | # normalize audio levels 857 | 858 | normalize -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) -o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 859 | -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 860 | -o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) : optional argument 861 | # if option not provided defaults to input-name-normalized-date-time-extension 862 | #+END_EXAMPLE 863 | 864 | *** normalize batch process 865 | 866 | Batch process files in the current working directory 867 | 868 | Note we omit the -o option to use the default outfile name, 869 | infile-name-normalize-date-time 870 | 871 | Batch normalize mp4 videos in the current directory 872 | 873 | #+BEGIN_SRC sh 874 | find . -type f -name "*.mp4" -exec sh -c \ 875 | 'normalize -i "${0}"' \ 876 | "{}" \; 877 | #+END_SRC 878 | 879 | ** overlay-clip 880 | :PROPERTIES: 881 | :CUSTOM_ID: overlay-clip 882 | :END: 883 | 884 | overlay one video clip on top of another video clip 885 | 886 | [[https://youtu.be/tfzKo9jy2sI][overay-clip youtube]] 887 | 888 | + script usage 889 | 890 | #+BEGIN_SRC sh 891 | overlay-clip -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] -o output.mp4 892 | #+END_SRC 893 | 894 | + script help 895 | 896 | #+begin_src sh 897 | overlay-clip -h 898 | #+end_src 899 | 900 | #+BEGIN_EXAMPLE 901 | # overlay one video clip on top of another video clip 902 | 903 | overlay-clip -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] -o output.mp4 904 | -i input.(mp4|mkv|mov|m4v) : bottom video 905 | -v input.(mp4|mkv|mov|m4v) : overlay video 906 | -p [0-999] : time to overlay the video 907 | -o output.mp4 : optional argument # if option not provided defaults to input-name-overlay-date-time 908 | #+END_EXAMPLE 909 | 910 | ** overlay-pip 911 | :PROPERTIES: 912 | :CUSTOM_ID: overlay-pip 913 | :END: 914 | 915 | create a picture in picture 916 | 917 | [[https://youtu.be/bufAVPT3Cvk][overlay-pip youtube]] 918 | 919 | + script usage 920 | 921 | #+BEGIN_SRC sh 922 | overlay-pip -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] 923 | -m [00] -x (tl|tr|bl|br) -w [000] -f (0.1-9|1) -b [00] -c colour -o output.mp4 924 | #+END_SRC 925 | 926 | + script help 927 | 928 | #+begin_src sh 929 | overlay-pip -h 930 | #+end_src 931 | 932 | #+BEGIN_EXAMPLE 933 | # create a picture in picture 934 | 935 | overlay-pip -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] 936 | -m [00] -x (tl|tr|bl|br) -w [000] -f (0.1-9|1) -b [00] -c colour -o output.mp4 937 | 938 | -i input.(mp4|mkv|mov|m4v) : bottom video 939 | -v input.(mp4|mkv|mov|m4v) : overlay video 940 | -p [0-999] : time to overlay the video 941 | -m [00] : margin defaults to 0 942 | -x (tl|tr|bl|br) : pip position - defaults to tr 943 | -w [000] : width - defaults to 1/4 of video size 944 | -f (0.1-9|1) : fade from 0.1 to 1 - defaults to 0.2 945 | -b [00] : border 946 | -c colour : colour 947 | -o output.mp4 : optional argument # if option not provided defaults to input-name-pip-date-time 948 | #+END_EXAMPLE 949 | 950 | ** pan-scan 951 | :PROPERTIES: 952 | :CUSTOM_ID: pan-scan 953 | :END: 954 | 955 | pan image 956 | 957 | + script usage 958 | 959 | #+BEGIN_SRC sh 960 | pan-scan -i input.(png|jpg|jpeg) -d (000) -p (l|r|u|d) -o output.mp4 961 | #+END_SRC 962 | 963 | + script help 964 | 965 | #+begin_src sh 966 | pan-scan -h 967 | #+end_src 968 | 969 | #+BEGIN_EXAMPLE 970 | # pan scan image 971 | 972 | pan-scan -i input.(png|jpg|jpeg) -d (000) -p (l|r|u|d) -o output.mp4 973 | -i = input.(png|jpg|jpeg) 974 | -d = duration : from 1-999 975 | -p = position : left, right, up, down 976 | -o = output.mp4 : optional argument # default is input-name-pan-date-time 977 | #+END_EXAMPLE 978 | 979 | ** scene-cut 980 | :PROPERTIES: 981 | :CUSTOM_ID: scene-cut 982 | :END: 983 | 984 | scene-cut takes a cut file and video and cuts the video into clips 985 | 986 | + script usage 987 | 988 | #+BEGIN_SRC sh 989 | scene-cut -i input -c cutfile 990 | #+END_SRC 991 | 992 | + script help 993 | 994 | #+begin_src sh 995 | scene-cut -h 996 | #+end_src 997 | 998 | #+BEGIN_EXAMPLE 999 | scene-cut -i input -c cutfile 1000 | 1001 | -i input.(mp4|mov|mkv|m4v) 1002 | -c cutfile 1003 | #+END_EXAMPLE 1004 | 1005 | ffmpeg requires a start point and a duration, not an end point 1006 | 1007 | cut file - hours, minutes, seconds 1008 | in this example we create 2 - 30 seconds clips 1009 | 1010 | a 30 second clip that starts at 00:00:00 1011 | and another 30 second clip that starts at 00:01:00 1012 | 1013 | #+begin_example 1014 | 00:00:00,00:00:30 1015 | 00:01:00,00:00:30 1016 | #+end_example 1017 | 1018 | cut file - seconds 1019 | in this example we create 2 - 30 seconds clips 1020 | 1021 | a 30 second clip that starts at 0 1022 | and another 30 second clip that starts at 60 1023 | 1024 | #+begin_example 1025 | 0,30 1026 | 60,30 1027 | #+end_example 1028 | 1029 | ** scene-detect 1030 | :PROPERTIES: 1031 | :CUSTOM_ID: scene-detect 1032 | :END: 1033 | 1034 | scene-detect takes a video file and a threshold for the scene detection from 0.1 to 0.9 1035 | you can also use the -s and -e options to set a range for thew scene detection, 1036 | if you dont specify a range scene detection will be perform on the whole video 1037 | 1038 | [[https://www.youtube.com/watch?v=nOeaFEHuFyM][ffmpeg scene detection - automatically cut videos into separate scenes]] 1039 | 1040 | [[https://youtu.be/SqvDCpWad9M][ffmpeg scene detection - version 2 - specify a range in the video and cut into separate scenes]] 1041 | 1042 | [[https://youtu.be/GZgE6fYd_wg][ffmpeg scene detect - version 3 - sexagesimal format - hours, minutes, seconds]] 1043 | 1044 | + script usage 1045 | 1046 | #+BEGIN_SRC sh 1047 | scene-detect -s 00:00:00 -i input -e 00:00:00 -t (0.1 - 0.9) -f sec -o output 1048 | #+END_SRC 1049 | 1050 | + script help 1051 | 1052 | #+begin_src sh 1053 | scene-detect -h 1054 | #+end_src 1055 | 1056 | #+BEGIN_EXAMPLE 1057 | scene-detect -s 00:00:00 -i input -e 00:00:00 -t (0.1 - 0.9) -f sec -o output 1058 | 1059 | -s 00:00:00 : start time 1060 | -i input.(mp4|mov|mkv|m4v) 1061 | -e 00:00:00 : end time 1062 | -t (0.1 - 0.9) # threshold 1063 | -f sec # output in seconds 1064 | -o output.txt 1065 | #+END_EXAMPLE 1066 | 1067 | ** scene-images 1068 | :PROPERTIES: 1069 | :CUSTOM_ID: scene-images 1070 | :END: 1071 | 1072 | scene-images takes a video file and a cut file, 1073 | created with the scene-detect script either in seconds or sexagesimal format 1074 | and then creates an image for each cut point 1075 | 1076 | + script usage 1077 | 1078 | #+BEGIN_SRC sh 1079 | scene-images -i input -c cutfile -t (png|jpg) -x width -y height 1080 | #+END_SRC 1081 | 1082 | + script help 1083 | 1084 | #+begin_src sh 1085 | scene-images -h 1086 | #+end_src 1087 | 1088 | #+BEGIN_EXAMPLE 1089 | scene-images -i input -c cutfile -t (png|jpg) -x width -y height 1090 | 1091 | -i input.(mp4|mov|mkv|m4v) 1092 | -c cutfile 1093 | -t (png|jpg) : optional argument # if option not provided defaults to png 1094 | -x width : optional argument # 1095 | -y height : optional argument # 1096 | #+END_EXAMPLE 1097 | 1098 | ** scene-time 1099 | :PROPERTIES: 1100 | :CUSTOM_ID: scene-time 1101 | :END: 1102 | 1103 | scene-time takes a cut file, 1104 | created with the scene-detect script either in seconds or sexagesimal format 1105 | 1106 | #+begin_example 1107 | 0:00:00 1108 | 0:00:11.875000 1109 | 0:00:15.750000 1110 | #+end_example 1111 | 1112 | The script creates clips by subtracting the cut point from the start point 1113 | and converts sexagesimal format and then creates a file with the start point 1114 | a comma and then the duration of the clip 1115 | 1116 | the output of the scene-time script is used with the scene-cut script to create the clips 1117 | 1118 | #+begin_example 1119 | 0,11.875 1120 | 11.875,3.875 1121 | #+end_example 1122 | 1123 | + script usage 1124 | 1125 | #+BEGIN_SRC sh 1126 | scene-time -i input -o output 1127 | #+END_SRC 1128 | 1129 | + script help 1130 | 1131 | #+begin_src sh 1132 | scene-time -h 1133 | #+end_src 1134 | 1135 | #+BEGIN_EXAMPLE 1136 | scene-time -i input -o output 1137 | 1138 | -i input 1139 | -o output 1140 | #+END_EXAMPLE 1141 | 1142 | ** sexagesimal-time 1143 | :PROPERTIES: 1144 | :CUSTOM_ID: sexagesimal-time 1145 | :END: 1146 | 1147 | calculate sexagesimal duration by subtracting the end time from start time for trimming files with ffmpeg 1148 | 1149 | + script help 1150 | 1151 | #+begin_src sh 1152 | sexagesimal-time -h 1153 | #+end_src 1154 | 1155 | example 1156 | 1157 | #+begin_src sh 1158 | sexagesimal-time -s 00:05:30 -e 00:18:47 1159 | #+end_src 1160 | 1161 | ouput 1162 | 1163 | #+begin_example 1164 | 00:13:17 1165 | #+end_example 1166 | 1167 | also works with milliseconds 1168 | 1169 | ** subtitle-add 1170 | :PROPERTIES: 1171 | :CUSTOM_ID: subtitle-add 1172 | :END: 1173 | 1174 | add subtitles to a video file 1175 | 1176 | [[https://youtu.be/p6BHhO5VfEg][subtitle-add youtube]] 1177 | 1178 | + script usage 1179 | 1180 | #+BEGIN_SRC sh 1181 | subtitle-add -i input.(mp4|mkv|mov|m4v) -s subtitle.(srt|vtt) -o output.mp4 1182 | #+END_SRC 1183 | 1184 | + script help 1185 | 1186 | #+begin_src sh 1187 | subtitle-add -h 1188 | #+end_src 1189 | 1190 | #+BEGIN_EXAMPLE 1191 | # add subtitles to a video 1192 | 1193 | subtitle-add -i input.(mp4|mkv|mov|m4v) -s subtitle.srt -o output.mp4 1194 | -i input.(mp4|mkv|mov|m4v) 1195 | -s subtitle.(srt|vtt) 1196 | -o output.mp4 : optional argument # if option not provided defaults to input-name-subs-date-time 1197 | #+END_EXAMPLE 1198 | 1199 | *** subtitle-add batch process 1200 | 1201 | Batch process files in the current working directory 1202 | 1203 | Note we omit the -o option to use the default outfile name, 1204 | infile-name-subs-date-time 1205 | 1206 | The video and subtitle files you want to combine must have the same name 1207 | 1208 | for example 1209 | 1210 | #+BEGIN_EXAMPLE 1211 | file1.mp4 1212 | file1.srt 1213 | file2.mp4 1214 | file2.srt 1215 | #+END_EXAMPLE 1216 | 1217 | running the following code will run the subtitle-add script and combine 1218 | file1.mp4 with file1.srt and 1219 | file2.mp4 with file2.srt 1220 | 1221 | #+BEGIN_SRC sh 1222 | find . -type f -name "*.mp4" -exec sh -c \ 1223 | 'subtitle-add -i "${0}" -s "${0%.*}.srt"' \ 1224 | "{}" \; 1225 | #+END_SRC 1226 | 1227 | ** scopes 1228 | :PROPERTIES: 1229 | :CUSTOM_ID: scopes 1230 | :END: 1231 | 1232 | [[https://www.youtube.com/watch?v=K-ifmNiyFRU][ffplay video scopes youtube video]] 1233 | 1234 | + script usage 1235 | 1236 | #+BEGIN_SRC sh 1237 | scopes -i input = histogram 1238 | scopes -o input = rgb overlay 1239 | scopes -p input = rgb parade 1240 | scopes -s input = rgb overlay and parade 1241 | scopes -w input = waveform 1242 | scopes -v input = vector scope 1243 | #+END_SRC 1244 | 1245 | + script help 1246 | 1247 | #+begin_src sh 1248 | scopes -h 1249 | #+end_src 1250 | 1251 | #+BEGIN_EXAMPLE 1252 | # ffplay video scopes 1253 | 1254 | scopes -i input = histogram 1255 | scopes -o input = rgb overlay 1256 | scopes -p input = rgb parade 1257 | scopes -s input = rgb overlay and parade 1258 | scopes -w input = waveform 1259 | scopes -v input = vector scope 1260 | scopes -h = help 1261 | #+END_EXAMPLE 1262 | 1263 | ** tile-thumbnails 1264 | :PROPERTIES: 1265 | :CUSTOM_ID: tile-thumbnails 1266 | :END: 1267 | 1268 | create thumbnails froma a video and tile into an image 1269 | 1270 | [[https://www.youtube.com/watch?v=gFFvKU9nvZE][tile-thumbnails youtube]] 1271 | 1272 | [[https://ffmpeg.org/ffmpeg-utils.html#color-syntax][ffmpeg colour syntax]] 1273 | 1274 | [[https://trac.ffmpeg.org/wiki/Seeking][ffmpeg wiki seeking]] 1275 | 1276 | Note that you can use two different time unit formats: sexagesimal (HOURS:MM:SS.MILLISECONDS, as in 01:23:45.678), or in seconds. 1277 | If a fraction is used, such as 02:30.05, this is interpreted as "5 100ths of a second", not as frame 5. 1278 | For instance, 02:30.5 would be 2 minutes, 30 seconds, and a half a second, which would be the same as using 150.5 in seconds. 1279 | 1280 | + script usage 1281 | 1282 | #+BEGIN_SRC sh 1283 | tile-thumbnails -i input -s 00:00:00.000 -w 000 -t 0x0 -p 00 -m 00 -c color -f fontcolor -b boxcolor -x on -o output.png 1284 | #+END_SRC 1285 | 1286 | + script help 1287 | 1288 | #+begin_src sh 1289 | tile-thumbnails -h 1290 | #+end_src 1291 | 1292 | #+BEGIN_EXAMPLE 1293 | # create an image with thumbnails from a video 1294 | 1295 | tile-thumbnails -i input -s 00:00:00.000 -w 000 -t 0x0 -p 00 -m 00 -c color -f fontcolor -b boxcolor -x on -o output.png 1296 | 1297 | -i input.(mp4|mkv|mov|m4v|webm) 1298 | -s seek into the video file : default 00:00:05 1299 | -w thumbnail width : 160 1300 | -t tile layout format width x height : 4x3 : default 4x3 1301 | -p padding between images : default 7 1302 | -m margin : default 2 1303 | -c color = https://ffmpeg.org/ffmpeg-utils.html#color-syntax : default black 1304 | -f fontcolor : default white 1305 | -b boxcolor : default black 1306 | -x on : default off, display timestamps 1307 | -o output.png : optional argument 1308 | # if option not provided defaults to input-name-tile-date-time.png" 1309 | #+END_EXAMPLE 1310 | 1311 | If the tiled image only creates one thumbnail from the video and the rest of the image is black, 1312 | then the issue may be the frame rate of the video 1313 | 1314 | you can check the videos frame rate with ffmpeg 1315 | 1316 | #+BEGIN_SRC sh 1317 | ffmpeg -i infile.mp4 1318 | #+END_SRC 1319 | 1320 | if the framerate is 29.97 instead of 30 then you can use ffmpeg to change the framerate and fix the issue 1321 | 1322 | #+BEGIN_SRC sh 1323 | ffmpeg -i infile.mp4 -vf fps=fps=30 outfile.mp4 1324 | #+END_SRC 1325 | 1326 | *** tile-thumbnails batch process 1327 | 1328 | batch process videos and create thumbnails from the videos and tile into an image 1329 | 1330 | #+BEGIN_SRC sh 1331 | find . -type f -name "*.mp4" -exec sh -c \ 1332 | 'tile-thumbnails -i "${0}" -s 00:00:10 -w 200 -t 4x4 -p 7 -m 2 -c white' \ 1333 | "{}" \; 1334 | #+END_SRC 1335 | 1336 | ** trim-clip 1337 | :PROPERTIES: 1338 | :CUSTOM_ID: trim-clip 1339 | :END: 1340 | 1341 | trim video clip 1342 | 1343 | [[https://trac.ffmpeg.org/wiki/Seeking][ffmpeg wiki seeking]] 1344 | 1345 | Note that you can use two different time unit formats: sexagesimal (HOURS:MM:SS.MILLISECONDS, as in 01:23:45.678), or in seconds. 1346 | If a fraction is used, such as 02:30.05, this is interpreted as "5 100ths of a second", not as frame 5. 1347 | For instance, 02:30.5 would be 2 minutes, 30 seconds, and a half a second, which would be the same as using 150.5 in seconds. 1348 | 1349 | [[https://youtu.be/LoKloi5N5p0][trim-clip youtube]] 1350 | 1351 | + script usage 1352 | 1353 | #+BEGIN_SRC sh 1354 | trim-clip -s 00:00:00.000 -i input.(mp4|mov|mkv|m4v|aac|m4a|wav|mp3) -t 00:00:00.000 -o output.(mp4|aac|mp3|wav) 1355 | #+END_SRC 1356 | 1357 | + script help 1358 | 1359 | #+begin_src sh 1360 | trim-clip -h 1361 | #+end_src 1362 | 1363 | #+BEGIN_EXAMPLE 1364 | # trim video or audio clips with millisecond accuracy 1365 | https://trac.ffmpeg.org/wiki/Seeking 1366 | 1367 | trim-clip -s 00:00:00.000 -i input.(mp4|mov|mkv|m4v|aac|m4a|wav|mp3) -t 00:00:00.000 -o output.(mp4|aac|mp3|wav) 1368 | -s 00:00:00.000 : start time 1369 | -i input.(mp4|mov|mkv|m4v|aac|m4a|wav|mp3) 1370 | -t 00:00:00.000 : number of seconds after start time 1371 | -o output.(mp4|aac|mp3|wav) : optional argument 1372 | # if option not provided defaults input-name-[start end].(mp4|webm|aac|mp3|wav|ogg) 1373 | #+END_EXAMPLE 1374 | 1375 | *** trim-clip batch process 1376 | 1377 | Batch process files in the current working directory 1378 | 1379 | Note we omit the -o option to use the default outfile name, 1380 | infile-name-trimmed-date-time 1381 | 1382 | Batch trim all the mp4 files in the current directory, 1383 | from 00:00:00 to 00:00:30 1384 | 1385 | #+BEGIN_SRC sh 1386 | find . -type f -name "*.mp4" -exec sh -c \ 1387 | 'trim-clip -s 00:00:00 -i "${0}" -t 00:00:30' \ 1388 | "{}" \; 1389 | #+END_SRC 1390 | 1391 | ** vid2gif 1392 | :PROPERTIES: 1393 | :CUSTOM_ID: vid2gif 1394 | :END: 1395 | 1396 | create a gif animation from a video 1397 | 1398 | [[https://www.youtube.com/watch?v=V59q5DC9y6A][vid2gif youtube]] 1399 | 1400 | + script usage 1401 | 1402 | #+BEGIN_SRC sh 1403 | vid2gif -s 00:00:00.000 -i input.(mp4|mov|mkv|m4v) -t 00:00:00.000 -f [00] -w [0000] -o output.gif 1404 | #+END_SRC 1405 | 1406 | + script help 1407 | 1408 | #+begin_src sh 1409 | vid2gif -h 1410 | #+end_src 1411 | 1412 | #+BEGIN_EXAMPLE 1413 | # convert a video into a gif animation 1414 | 1415 | vid2gif -s 00:00:00.000 -i input.(mp4|mov|mkv|m4v) -t 00:00:00.000 -f [00] -w [0000] -o output.gif 1416 | -s 00:00:00.000 : start time 1417 | -i input.(mp4|mov|mkv|m4v) 1418 | -t 00:00:00.000 : number of seconds after start time 1419 | -f [00] : framerate 1420 | -w [0000] : width 1421 | -o output.gif : optional argument 1422 | # if option not provided defaults input-name-gif-date-time.gif 1423 | #+END_EXAMPLE 1424 | 1425 | ** waveform 1426 | :PROPERTIES: 1427 | :CUSTOM_ID: waveform 1428 | :END: 1429 | 1430 | create a waveform from an audio or video file and save as a png 1431 | 1432 | [[https://youtu.be/OBnYLVahUaA][waveform youtube]] 1433 | 1434 | + script usage 1435 | 1436 | #+BEGIN_SRC sh 1437 | waveform -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -o output.png 1438 | #+END_SRC 1439 | 1440 | + script help 1441 | 1442 | #+begin_src sh 1443 | waveform -h 1444 | #+end_src 1445 | 1446 | #+BEGIN_EXAMPLE 1447 | # create a waveform from an audio or video file and save as a png 1448 | 1449 | waveform -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -o output.png 1450 | -i output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 1451 | -o output.png : optional argument # if option not provided defaults to input-name-waveform-date-time 1452 | #+END_EXAMPLE 1453 | 1454 | *** waveform batch process 1455 | 1456 | Batch process files in the current working directory 1457 | 1458 | Note we omit the -o option to use the default outfile name, 1459 | infile-name-waveform-date-time 1460 | 1461 | Create waveform images from all the mp4 fies in the current directory 1462 | 1463 | #+BEGIN_SRC sh 1464 | find . -type f -name "*.mp4" -exec sh -c \ 1465 | 'waveform -i "${0}"' \ 1466 | "{}" \; 1467 | #+END_SRC 1468 | 1469 | ** webp 1470 | :PROPERTIES: 1471 | :CUSTOM_ID: webp 1472 | :END: 1473 | 1474 | create a animated webp image from a video with ffmpeg 1475 | 1476 | [[https://www.youtube.com/watch?v=5iXjbQ7uDiM][webp animated images youtube]] 1477 | 1478 | + script usage 1479 | 1480 | #+BEGIN_SRC sh 1481 | webp -i input -c 0-6 -q 0-100 -f 15 -w 600 -p none -o output.webp 1482 | #+END_SRC 1483 | 1484 | + script help 1485 | 1486 | #+begin_src sh 1487 | webp -h 1488 | #+end_src 1489 | 1490 | #+BEGIN_EXAMPLE 1491 | # webp animated image 1492 | 1493 | webp -i input -c 0-6 -q 0-100 -f 15 -w 600 -p none -o output.webp 1494 | -i input 1495 | -c compression level: 0 - 6 : default 4 1496 | -q quality: 0 - 100 : default 80 1497 | -f framerate: default 15 1498 | -w width: default 600px 1499 | -p preset: none|default|picture|photo|drawing|icon|text : default none 1500 | -o output.webp : optional agument 1501 | # if option not provided defaults input-name.webp 1502 | #+END_EXAMPLE 1503 | 1504 | *** webp batch process 1505 | 1506 | Batch process files in the current working directory 1507 | 1508 | #+BEGIN_SRC sh 1509 | find . -type f -name "*.mp4" -exec sh -c 'webp -i "${0}"' "{}" \; 1510 | #+END_SRC 1511 | 1512 | ** xfade 1513 | :PROPERTIES: 1514 | :CUSTOM_ID: xfade 1515 | :END: 1516 | 1517 | + [[https://www.youtube.com/watch?v=McQM3ooNx-4][xfade script demo youtube]] 1518 | 1519 | apply a transition between two clips with the xfade filters 1520 | 1521 | [[https://trac.ffmpeg.org/wiki/Xfade][xfade ffmpeg wiki]] 1522 | 1523 | + script usage 1524 | 1525 | #+begin_src sh 1526 | xfade -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d duration -t transition -f offset -o output.mp4 1527 | #+end_src 1528 | 1529 | + script help 1530 | 1531 | #+begin_src sh 1532 | xfade -h 1533 | #+end_src 1534 | 1535 | #+begin_example 1536 | # ffmpeg xfade transitions 1537 | 1538 | xfade -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d duration -t transition -f offset -o output.mp4 1539 | -a clip1.(mp4|mkv|mov|m4v) : first clip 1540 | -b clip2.(mp4|mkv|mov|m4v) : second clip 1541 | -d duration : transition duration 1542 | -t transition : transition 1543 | -f offset : offset 1544 | -o output.mp4 : optional argument # if option not provided defaults to input-name-xfade-date-time 1545 | 1546 | + transitions 1547 | 1548 | circleclose, circlecrop, circleopen, diagbl, diagbr, diagtl, diagtr, dissolve, distance 1549 | fade, fadeblack, fadegrays, fadewhite, hblur, hlslice, horzclose, horzopen, hrslice 1550 | pixelize, radial, rectcrop, slidedown, slideleft, slideright, slideup, smoothdown 1551 | smoothleft, smoothright, smoothup, squeezeh, squeezev, vdslice, vertclose, vertopen, vuslice 1552 | wipebl, wipebr, wipedown, wipeleft, wiperight, wipetl, wipetr, wipeup 1553 | #+end_example 1554 | 1555 | ** zoompan 1556 | :PROPERTIES: 1557 | :CUSTOM_ID: zoompan 1558 | :END: 1559 | 1560 | convert a image to video and apply the ken burns effect to the clip 1561 | 1562 | + script usage 1563 | 1564 | #+BEGIN_SRC sh 1565 | zoompan -i input.(png|jpg|jpeg) -d (000) -z (in|out) -p (tl|c|tc|tr|bl|br) -o output.mp4 1566 | #+END_SRC 1567 | 1568 | + script help 1569 | 1570 | #+begin_src sh 1571 | zoompan -h 1572 | #+end_src 1573 | 1574 | #+BEGIN_EXAMPLE 1575 | # zoompan, ken burns effect 1576 | 1577 | zoompan -i input.(png|jpg|jpeg) -d (000) -z (in|out) -p (tl|c|tc|tr|bl|br) -o output.mp4 1578 | -i = input.(png|jpg|jpeg) 1579 | -d = duration : from 1-999 1580 | -z = zoom : in or out 1581 | -p = position : zoom to location listed below 1582 | -o = outfile.mp4 : optional argument # default is input-name-zoompan-date-time 1583 | 1584 | +------------------------------+ 1585 | +tl tc tr+ 1586 | + + 1587 | + c + 1588 | + + 1589 | +bl br+ 1590 | +------------------------------+ 1591 | #+END_EXAMPLE 1592 | 1593 | *** zoompan batch process 1594 | 1595 | Batch process files in the current working directory 1596 | 1597 | Note we omit the -o option to use the default outfile name, 1598 | infile-name-zoompan-date-time 1599 | 1600 | Batch process all the png files in the current working directory, 1601 | apply the zoompan script with a 5 second duration, zoom in to the center of the image 1602 | 1603 | #+BEGIN_SRC sh 1604 | find . -type f -name "*.png" -exec sh -c \ 1605 | 'zoompan -i "${0}" -d 5 -z in -p c' \ 1606 | "{}" \; 1607 | #+END_SRC 1608 | -------------------------------------------------------------------------------- /audio-silence: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # audio-silence 5 | # add silent audio to a video clip 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # audio-silence add silent audio to a video clip 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -c (mono|stereo) -r (44100|48000) -o output.mp4 24 | -i input.(mp4|mkv|mov|m4v) 25 | -c (mono|stereo) : optional argument # if option not provided defaults to mono 26 | -r (44100|48000) : optional argument # if option not provided defaults to 44100 27 | -o output.mp4 : optional argument # if option not provided defaults to input-name-silence-date-time" 28 | exit 2 29 | } 30 | 31 | 32 | #=============================================================================== 33 | # error messages 34 | #=============================================================================== 35 | 36 | INVALID_OPT_ERR='Invalid option:' 37 | REQ_ARG_ERR='requires an argument' 38 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 39 | 40 | 41 | #=============================================================================== 42 | # check the number of arguments passed to the script 43 | #=============================================================================== 44 | 45 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 46 | 47 | 48 | #=============================================================================== 49 | # getopts check the options passed to the script 50 | #=============================================================================== 51 | 52 | while getopts ':i:c:r:o:h' opt 53 | do 54 | case ${opt} in 55 | i) infile="${OPTARG}";; 56 | c) channel="${OPTARG}" 57 | { [ "${channel}" = 'mono' ] || [ "${channel}" = 'stereo' ]; } || usage;; 58 | r) rate="${OPTARG}" 59 | { [ "${rate}" = '44100' ] || [ "${rate}" = '48000' ]; } || usage;; 60 | o) outfile="${OPTARG}";; 61 | h) usage;; 62 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 63 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 64 | esac 65 | done 66 | shift $((OPTIND-1)) 67 | 68 | 69 | #=============================================================================== 70 | # variables 71 | #=============================================================================== 72 | 73 | # input 74 | infile_nopath="${infile##*/}" 75 | infile_name="${infile_nopath%.*}" 76 | 77 | # defaults for variables if not defined 78 | channel_default='mono' 79 | rate_default='44100' 80 | outfile_default="${infile_name}-silence-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 81 | 82 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 83 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 84 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 85 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 86 | 87 | # check ffmpeg aac codecs 88 | if [ -z "${aac_check}" ]; then 89 | aac='libfdk_aac' # libfdk_aac codec is installed 90 | else 91 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 92 | fi 93 | 94 | 95 | #=============================================================================== 96 | # functions 97 | #=============================================================================== 98 | 99 | # video function 100 | video_silence () { 101 | ffmpeg \ 102 | -hide_banner \ 103 | -stats -v panic \ 104 | -f lavfi \ 105 | -i anullsrc=channel_layout="${channel:=${channel_default}}":sample_rate="${rate:=${rate_default}}" \ 106 | -i "${infile}" \ 107 | -shortest -c:v copy -c:a "${aac}" \ 108 | -movflags +faststart -f mp4 \ 109 | "${outfile:=${outfile_default}}" 110 | } 111 | 112 | # video and audio function 113 | video_audio_silence () { 114 | ffmpeg \ 115 | -hide_banner \ 116 | -stats -v panic \ 117 | -f lavfi \ 118 | -i anullsrc=channel_layout="${channel:=${channel_default}}":sample_rate="${rate:=${rate_default}}" \ 119 | -i "${infile}" \ 120 | -shortest -c:v copy -c:a "${aac}" \ 121 | -map 0:0 -map 1:0 \ 122 | -movflags +faststart -f mp4 \ 123 | "${outfile:=${outfile_default}}" 124 | } 125 | 126 | # check if the video has an audio track 127 | audio_check="$(ffprobe -i "${infile}" -show_streams -select_streams a -loglevel error)" 128 | 129 | # check if audio_check is null which means the video doesnt have an audio track 130 | if [ -z "${audio_check}" ]; then 131 | video_silence "${infile}" # null value 132 | else 133 | video_audio_silence "${infile}" # non null value 134 | fi 135 | -------------------------------------------------------------------------------- /chapter-add: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # chapter-add 5 | # add chapters to a video or audio file with ffmpeg 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | add chapters to a video or audio file with ffmpeg 21 | $(basename "$0") -i input -c metadata.txt -o output" 22 | exit 2 23 | } 24 | 25 | 26 | #=============================================================================== 27 | # error messages 28 | #=============================================================================== 29 | 30 | INVALID_OPT_ERR='Invalid option:' 31 | REQ_ARG_ERR='requires an argument' 32 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 33 | 34 | 35 | #=============================================================================== 36 | # check the number of arguments passed to the script 37 | #=============================================================================== 38 | 39 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 40 | 41 | 42 | #=============================================================================== 43 | # getopts check the options passed to the script 44 | #=============================================================================== 45 | 46 | while getopts ':i:c:o:h' opt 47 | do 48 | case ${opt} in 49 | i) input="${OPTARG}";; 50 | c) metadata="${OPTARG}";; 51 | h) usage;; 52 | o) output="${OPTARG}";; 53 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 54 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 55 | esac 56 | done 57 | shift $((OPTIND-1)) 58 | 59 | 60 | #=============================================================================== 61 | # variables 62 | #=============================================================================== 63 | 64 | # get the input file name 65 | input_nopath="${input##*/}" 66 | input_name="${input_nopath%.*}" 67 | 68 | # input file extension 69 | input_ext="${input##*.}" 70 | 71 | # output file name 72 | output_default="${input_name}-metadata.${input_ext}" 73 | 74 | 75 | #=============================================================================== 76 | # sexagesimal function 77 | #=============================================================================== 78 | 79 | meta () { 80 | ffmpeg \ 81 | -hide_banner \ 82 | -stats -v panic \ 83 | -i "${input}" \ 84 | -i "${metadata}" -map_metadata 1 -c copy \ 85 | "${output:=${output_default}}" 86 | } 87 | 88 | 89 | #=============================================================================== 90 | # run function 91 | #=============================================================================== 92 | 93 | meta 94 | -------------------------------------------------------------------------------- /chapter-csv: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # chapter-csv 5 | # convert a csv file into a chapter metadata file for ffmpeg 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # awk 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | convert a csv file into a chapter metadata file for ffmpeg 21 | $(basename "$0") -i input -o output" 22 | exit 2 23 | } 24 | 25 | 26 | #=============================================================================== 27 | # error messages 28 | #=============================================================================== 29 | 30 | INVALID_OPT_ERR='Invalid option:' 31 | REQ_ARG_ERR='requires an argument' 32 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 33 | 34 | 35 | #=============================================================================== 36 | # check the number of arguments passed to the script 37 | #=============================================================================== 38 | 39 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 40 | 41 | 42 | #=============================================================================== 43 | # getopts check the options passed to the script 44 | #=============================================================================== 45 | 46 | while getopts ':i:o:h' opt 47 | do 48 | case ${opt} in 49 | i) input="${OPTARG}";; 50 | h) usage;; 51 | o) output="${OPTARG}";; 52 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 53 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 54 | esac 55 | done 56 | shift $((OPTIND-1)) 57 | 58 | 59 | #=============================================================================== 60 | # variables 61 | #=============================================================================== 62 | 63 | # get the input file name 64 | input_nopath="${input##*/}" 65 | input_name="${input_nopath%.*}" 66 | 67 | # output file name 68 | output_default="${input_name}-metadata.txt" 69 | 70 | 71 | #=============================================================================== 72 | # sexagesimal function 73 | #=============================================================================== 74 | 75 | sexagesimal () { 76 | total="$(awk 'END { print NR }' < "${input}")" 77 | awk -F "," ' 78 | { 79 | time = $1 80 | chapter = $2 81 | if (time ~ /:/) { 82 | split(time, t, ":") 83 | combined = (t[1] * 3600) + (t[2] * 60) + t[3] 84 | milliseconds = (combined * 1000) 85 | } 86 | printf("%d,%s\n"), milliseconds, chapter 87 | }' < "${input}" \ 88 | | awk -v records="$total" -F "," ' \ 89 | { if (FNR < records) end = $1-1; else end = $1 } 90 | NR == 1 {print ";FFMETADATA1"} NR > 1 {printf("%s\n%s\n%s%s\n%s%s\n%s%s\n"), "[CHAPTER]", "TIMEBASE=1/1000", "START=", prev, "END=", end, "title=", chapter}; {prev = $1; chapter = $2}' > "${output:=${output_default}}" 91 | } 92 | 93 | 94 | #=============================================================================== 95 | # run function 96 | #=============================================================================== 97 | 98 | sexagesimal 99 | -------------------------------------------------------------------------------- /chapter-extract: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # chapter-extract 5 | # extract chapters from a video or audo file and save as a csv file 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffprobe awk 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | extract chapters from a video or audo file and save as a csv file 21 | $(basename "$0") -i input -o output" 22 | exit 2 23 | } 24 | 25 | 26 | #=============================================================================== 27 | # error messages 28 | #=============================================================================== 29 | 30 | INVALID_OPT_ERR='Invalid option:' 31 | REQ_ARG_ERR='requires an argument' 32 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 33 | 34 | 35 | #=============================================================================== 36 | # check the number of arguments passed to the script 37 | #=============================================================================== 38 | 39 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 40 | 41 | 42 | #=============================================================================== 43 | # getopts check the options passed to the script 44 | #=============================================================================== 45 | 46 | while getopts ':i:o:h' opt 47 | do 48 | case ${opt} in 49 | i) input="${OPTARG}";; 50 | h) usage;; 51 | o) output="${OPTARG}";; 52 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 53 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 54 | esac 55 | done 56 | shift $((OPTIND-1)) 57 | 58 | 59 | #=============================================================================== 60 | # variables 61 | #=============================================================================== 62 | 63 | # get the input file name 64 | input_nopath="${input##*/}" 65 | input_name="${input_nopath%.*}" 66 | 67 | # output file name 68 | output_default="${input_name}-chapters.txt" 69 | 70 | 71 | #=============================================================================== 72 | # sexagesimal function 73 | #=============================================================================== 74 | 75 | extract () { 76 | ffprobe -v panic -hide_banner \ 77 | -show_chapters -sexagesimal -of csv \ 78 | -i "${input}" \ 79 | | awk -F "," '{ 80 | time = $5 81 | chapter = $NF 82 | if (time ~ /:/) { 83 | split(time, t, ".") 84 | sexagesimal = t[1] 85 | } 86 | printf("%s,%s\n"), sexagesimal, chapter 87 | }' > "${output:=${output_default}}" 88 | } 89 | 90 | 91 | #=============================================================================== 92 | # run function 93 | #=============================================================================== 94 | 95 | extract 96 | -------------------------------------------------------------------------------- /clip-time: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # clip-time 5 | # create ffmpeg cutlist 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | $(basename "$0") -i input -o output 21 | 22 | -i input 23 | -o output" 24 | exit 2 25 | } 26 | 27 | 28 | #=============================================================================== 29 | # error messages 30 | #=============================================================================== 31 | 32 | INVALID_OPT_ERR='Invalid option:' 33 | REQ_ARG_ERR='requires an argument' 34 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 35 | 36 | #=============================================================================== 37 | # check the number of arguments passed to the script 38 | #=============================================================================== 39 | 40 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 41 | 42 | 43 | #=============================================================================== 44 | # getopts check the options passed to the script 45 | #=============================================================================== 46 | 47 | while getopts ':i:o:h' opt 48 | do 49 | case ${opt} in 50 | i) input="${OPTARG}";; 51 | h) usage;; 52 | o) output="${OPTARG}";; 53 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 54 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 55 | esac 56 | done 57 | shift $((OPTIND-1)) 58 | 59 | 60 | #=============================================================================== 61 | # variables 62 | #=============================================================================== 63 | 64 | # get the input file name 65 | input_nopath="${input##*/}" 66 | input_name="${input_nopath%.*}" 67 | 68 | # output file name 69 | output_default="${input_name}-cutlist-$(date +"%Y-%m-%d-%H-%M-%S").txt" 70 | 71 | 72 | #=============================================================================== 73 | # awk subtract 2nd line from first line - print start and duration 74 | #=============================================================================== 75 | 76 | seconds () { 77 | awk -F. 'NR > 1 && NR%2 == 0 {printf("%s%s%s\n"), prev, ",", $0-prev}; {prev = $0}' < "${input}" > "${output:=${output_default}}" 78 | } 79 | 80 | #=============================================================================== 81 | # convert sexagesimal to seconds - and subtract 2nd line from first line 82 | # convert from seconds back to sexagesimal 83 | #=============================================================================== 84 | 85 | minutes () { 86 | awk -F: 'NF==3 { printf("%s\n"), ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { printf("$s\n"), 0 + $1 }' < "${input}" \ 87 | | awk 'NR > 1 && NR%2 == 0 {printf("%s%s%s\n"), prev, "\n", $0-prev}; {prev = $0}' \ 88 | | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\ 89 | NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' \ 90 | | awk '{ORS = NR%2 ? "," : "\n"} 1' > "${output:=${output_default}}" 91 | } 92 | 93 | 94 | #=============================================================================== 95 | # timecode regex 96 | #=============================================================================== 97 | 98 | # grab the first line of the file 99 | check=$(head -n1 "${input}") 100 | 101 | # minutes - match 00:00:00.000 102 | minutes_regex='^[0-9]{1,2}:[0-9]{2}:[0-9]{2}([.]{1}[0-9]{1,6})?$' 103 | 104 | # seconds - match 00:00:00.000 105 | seconds_regex='^[0-9]{1,8}([.]{1}[0-9]{1,6})?$' 106 | 107 | # grep for the minutes 108 | minutes=$(echo "${check}" | grep -E "${minutes_regex}") 109 | 110 | # grep for the seconds 111 | seconds=$(echo "${check}" | grep -E "${seconds_regex}") 112 | 113 | 114 | #=============================================================================== 115 | # check timecode and run function 116 | #=============================================================================== 117 | 118 | if [ -n "${minutes}" ]; then 119 | minutes 120 | elif [ -n "${seconds}" ]; then 121 | seconds 122 | else 123 | usage 124 | fi 125 | -------------------------------------------------------------------------------- /combine-clips: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # combine-clips 5 | # combine an image or video file with an audio clip 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg file grep 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # combine an image or video file with an audio clip 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v|png|jpg) -a audio.(m4a|aac|wav|mp3) -o output.mp4 24 | -i input.(mp4|mkv|mov|m4v|png|jpg) 25 | -a audio.(m4a|aac|wav|mp3) 26 | -o output.mp4 : optional argument # if option not provided defaults to input-name-combined-date-time" 27 | exit 2 28 | } 29 | 30 | 31 | #=============================================================================== 32 | # error messages 33 | #=============================================================================== 34 | 35 | INVALID_OPT_ERR='Invalid option:' 36 | REQ_ARG_ERR='requires an argument' 37 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 38 | NOT_MEDIA_FILE_ERR='is not a media file' 39 | 40 | 41 | #=============================================================================== 42 | # check the number of arguments passed to the script 43 | #=============================================================================== 44 | 45 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 46 | 47 | 48 | #=============================================================================== 49 | # getopts check the options passed to the script 50 | #=============================================================================== 51 | 52 | while getopts ':i:a:o:h' opt 53 | do 54 | case ${opt} in 55 | i) infile="${OPTARG}";; 56 | a) audio="${OPTARG}";; 57 | o) outfile="${OPTARG}";; 58 | h) usage;; 59 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 60 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 61 | esac 62 | done 63 | shift $((OPTIND-1)) 64 | 65 | 66 | #=============================================================================== 67 | # variables 68 | #=============================================================================== 69 | 70 | # input, input name 71 | infile_nopath="${infile##*/}" 72 | infile_name="${infile_nopath%.*}" 73 | 74 | # audio file extension 75 | audio_ext="${audio##*.}" 76 | 77 | # file command check input file mime type 78 | infile_filetype="$(file --mime-type -b "${infile}")" 79 | audio_filetype="$(file --mime-type -b "${audio}")" 80 | 81 | # audio and video mimetypes 82 | mov_mime='video/quicktime' 83 | mkv_mime='video/x-matroska' 84 | mp4_mime='video/mp4' 85 | m4v_mime='video/x-m4v' 86 | aac_mime='audio/x-hx-aac-adts' 87 | m4a_mime='audio/mp4' 88 | 89 | # the file command wrongly identifies .m4a audio as a video file 90 | # so we check if the file extension is .m4a and set the mime type to audio/mp4 91 | if [ "${audio_ext}" = 'm4a' ]; then 92 | audio_filetype="${m4a_mime}" 93 | fi 94 | 95 | # image mimetypes 96 | png_mime='image/png' 97 | jpg_mime='image/jpeg' 98 | 99 | # defaults for variables 100 | outfile_default="${infile_name}-combined-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 101 | 102 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 103 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 104 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 105 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 106 | 107 | # check ffmpeg aac codecs 108 | if [ -z "${aac_check}" ]; then 109 | aac='libfdk_aac' # libfdk_aac codec is installed 110 | else 111 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 112 | fi 113 | 114 | #=============================================================================== 115 | # functions 116 | #=============================================================================== 117 | 118 | # video - audio is aac, copy audio stream 119 | record_copy () { 120 | ffmpeg \ 121 | -hide_banner \ 122 | -stats -v panic \ 123 | -i "${infile}" \ 124 | -i "${audio}" \ 125 | -shortest -fflags shortest -max_interleave_delta 100M \ 126 | -c:a copy \ 127 | -c:v copy \ 128 | -map 0:0 -map 1:0 \ 129 | -pix_fmt yuv420p \ 130 | -movflags +faststart \ 131 | -f mp4 \ 132 | "${outfile:=${outfile_default}}" 133 | } 134 | 135 | # video - audio isnt aac, encode audio as aac 136 | record_aac () { 137 | ffmpeg \ 138 | -hide_banner \ 139 | -stats -v panic \ 140 | -i "${infile}" \ 141 | -i "${audio}" \ 142 | -shortest -fflags shortest -max_interleave_delta 100M \ 143 | -c:a "${aac}" \ 144 | -c:v copy \ 145 | -map 0:0 -map 1:0 \ 146 | -pix_fmt yuv420p \ 147 | -movflags +faststart \ 148 | -f mp4 \ 149 | "${outfile:=${outfile_default}}" 150 | } 151 | 152 | # image - audio is aac, copy audio stream 153 | record_img_copy () { 154 | dur="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audio}")" 155 | ffmpeg \ 156 | -hide_banner \ 157 | -stats -v panic \ 158 | -framerate 1/"${dur}" \ 159 | -i "${infile}" \ 160 | -i "${audio}" \ 161 | -c:a copy \ 162 | -c:v libx264 -crf 18 -profile:v high \ 163 | -r 30 -pix_fmt yuv420p \ 164 | -movflags +faststart -f mp4 \ 165 | "${outfile:=${outfile_default}}" 166 | } 167 | 168 | # image - audio isnt aac, encode audio as aac 169 | record_img_aac () { 170 | dur="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audio}")" 171 | ffmpeg \ 172 | -hide_banner \ 173 | -stats -v panic \ 174 | -framerate 1/"${dur}" \ 175 | -i "${infile}" \ 176 | -i "${audio}" \ 177 | -c:a "${aac}" \ 178 | -c:v libx264 -crf 18 -profile:v high \ 179 | -r 30 -pix_fmt yuv420p \ 180 | -movflags +faststart -f mp4 \ 181 | "${outfile:=${outfile_default}}" 182 | } 183 | 184 | 185 | #=============================================================================== 186 | # case statement 187 | #=============================================================================== 188 | 189 | # run the ffmpeg function based on the audio mime type 190 | case "${infile_filetype}" in 191 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") 192 | if [ "${audio_filetype}" = 'audio/x-hx-aac-adts' ]; then 193 | # video - audio is aac, copy audio stream 194 | record_copy 195 | else 196 | # video - audio isnt aac, encode audio as aac 197 | record_aac 198 | fi;; 199 | "${png_mime}"|"${jpg_mime}") 200 | if [ "${audio_filetype}" = "${aac_mime}" ]; then 201 | # image - audio is aac, copy audio stream 202 | record_img_copy 203 | else 204 | # image - audio isnt aac, encode audio as aac 205 | record_img_aac 206 | fi;; 207 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 208 | esac 209 | -------------------------------------------------------------------------------- /correct-clip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # correct-clip 5 | # correct a video clip by using a gimp curve 6 | #=============================================================================== 7 | 8 | # code based on: 9 | # https://video.stackexchange.com/questions/16352/converting-gimp-curves-files-to-photoshop-acv-for-ffmpeg/20005#20005 10 | 11 | # converted into a ffmpeg curves filter command 12 | # to adjust the levels and white balance 13 | 14 | # requires a curve file created with the following script 15 | # https://github.com/NapoleonWils0n/curve2ffmpeg 16 | 17 | # dependencies: 18 | # ffmpeg file grep 19 | 20 | #=============================================================================== 21 | # script usage 22 | #=============================================================================== 23 | 24 | usage() 25 | { 26 | # if argument passed to function echo it 27 | [ -z "${1}" ] || echo "! ${1}" 28 | echo "\ 29 | # correct a video clip by using a gimp curve 30 | 31 | # requires a curve file created with the following script 32 | # https://github.com/NapoleonWils0n/curve2ffmpeg 33 | 34 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -c curve.txt -o output.mp4 35 | -i input.(mp4|mkv|mov|m4v) 36 | -c curve.txt 37 | -o output.mp4 : optional argument # if option not provided defaults to input-name-corrected-date-time" 38 | exit 2 39 | } 40 | 41 | 42 | #=============================================================================== 43 | # error messages 44 | #=============================================================================== 45 | 46 | INVALID_OPT_ERR='Invalid option:' 47 | REQ_ARG_ERR='requires an argument' 48 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 49 | NOT_MEDIA_FILE_ERR='is not a media file' 50 | NOT_TEXT_FILE_ERR='is not a text file' 51 | 52 | 53 | #=============================================================================== 54 | # check the number of arguments passed to the script 55 | #=============================================================================== 56 | 57 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 58 | 59 | 60 | #=============================================================================== 61 | # getopts check the options passed to the script 62 | #=============================================================================== 63 | 64 | while getopts ':i:c:o:h' opt 65 | do 66 | case ${opt} in 67 | i) infile="${OPTARG}";; 68 | c) text="${OPTARG}";; 69 | o) outfile="${OPTARG}";; 70 | h) usage;; 71 | \?) echo "${INVALID_OPT_ERR} ${OPTARG}" 1>&2 && usage;; 72 | :) echo "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2 && usage;; 73 | esac 74 | done 75 | shift $((OPTIND-1)) 76 | 77 | 78 | #=============================================================================== 79 | # variables 80 | #=============================================================================== 81 | 82 | # input, input name 83 | infile_nopath="${infile##*/}" 84 | infile_name="${infile_nopath%.*}" 85 | 86 | # defaults for variables 87 | outfile_default="${infile_name}-corrected-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 88 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 89 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 90 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 91 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 92 | 93 | # check ffmpeg aac codecs 94 | if [ -z "${aac_check}" ]; then 95 | aac='libfdk_aac' # libfdk_aac codec is installed 96 | else 97 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 98 | fi 99 | 100 | 101 | #=============================================================================== 102 | # functions 103 | #=============================================================================== 104 | 105 | correct_v () { 106 | ffmpeg \ 107 | -hide_banner \ 108 | -stats -v panic \ 109 | -i "${infile}" \ 110 | -filter_complex \ 111 | "[0:v]${text_contents}[video]" \ 112 | -map "[video]" \ 113 | -c:v libx264 -preset fast \ 114 | -profile:v high \ 115 | -crf 18 -coder 1 \ 116 | -pix_fmt yuv420p \ 117 | -movflags +faststart \ 118 | -f mp4 \ 119 | "${outfile:=${outfile_default}}" 120 | } 121 | 122 | # correct video and audio tracks 123 | correct_va () { 124 | ffmpeg \ 125 | -hide_banner \ 126 | -stats -v panic \ 127 | -i "${infile}" \ 128 | -filter_complex \ 129 | "[0:v]${text_contents}[video]" \ 130 | -map "[video]" -map 0:a \ 131 | -c:a "${aac}" \ 132 | -c:v libx264 -preset fast \ 133 | -profile:v high \ 134 | -crf 18 -coder 1 \ 135 | -pix_fmt yuv420p \ 136 | -movflags +faststart \ 137 | -f mp4 \ 138 | "${outfile:=${outfile_default}}" 139 | } 140 | 141 | # file command check input file mime type 142 | filetype="$(file --mime-type -b "${infile}")" 143 | textfile="$(file --mime-type -b "${text}")" 144 | 145 | # video mimetypes 146 | mov_mime='video/quicktime' 147 | mkv_mime='video/x-matroska' 148 | mp4_mime='video/mp4' 149 | m4v_mime='video/x-m4v' 150 | 151 | # text mimetype 152 | txt_mime='text/plain' 153 | 154 | # check the files mime type is a video 155 | case "${filetype}" in 156 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}");; 157 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 158 | esac 159 | 160 | # check the text file mime type is a text 161 | case "${textfile}" in 162 | "${txt_mime}");; 163 | *) usage "${textfile} ${NOT_TEXT_FILE_ERR}";; 164 | esac 165 | 166 | # read the contents of the curve text file and store in a variable 167 | text_contents="$(while IFS= read -r line; do echo "${line}"; done < "${text}")" 168 | 169 | # check if video has an audio track 170 | audio_check="$(ffprobe -i "${infile}" -show_streams -select_streams a -loglevel error)" 171 | 172 | # check if audio_check is null which means the video doesnt have an audio track 173 | if [ -z "${audio_check}" ]; then 174 | correct_v "${infile}" # null value 175 | else 176 | correct_va "${infile}" # non null value 177 | fi 178 | -------------------------------------------------------------------------------- /crossfade-clips: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # crossfade-clips 5 | # cross fade video clips 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg file grep 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # ffmpeg cross fade clips 22 | 23 | $(basename "$0") -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d (1|2) -o output.mp4 24 | -a clip1.(mp4|mkv|mov|m4v) : first clip 25 | -b clip2.(mp4|mkv|mov|m4v) : second clip 26 | -d (1|2) : cross fade duration :optional argument # if option not provided defaults to 1 second 27 | -o output.mp4 : optional argument # if option not provided defaults to input-name-xfade-date-time" 28 | exit 2 29 | } 30 | 31 | 32 | #=============================================================================== 33 | # error messages 34 | #=============================================================================== 35 | 36 | INVALID_OPT_ERR='Invalid option:' 37 | REQ_ARG_ERR='requires an argument' 38 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 39 | 40 | 41 | #=============================================================================== 42 | # check the number of arguments passed to the script 43 | #=============================================================================== 44 | 45 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 46 | 47 | 48 | #=============================================================================== 49 | # getopts check the options passed to the script 50 | #=============================================================================== 51 | 52 | while getopts ':a:b:d:o:h' opt 53 | do 54 | case ${opt} in 55 | a) clip1="${OPTARG}";; 56 | b) clip2="${OPTARG}";; 57 | d) dur="${OPTARG}";; 58 | o) outfile="${OPTARG}";; 59 | h) usage;; 60 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 61 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 62 | esac 63 | done 64 | shift $((OPTIND-1)) 65 | 66 | #=============================================================================== 67 | # variables 68 | #=============================================================================== 69 | 70 | # input, input name and extension 71 | clip1_nopath="${clip1##*/}" 72 | clip1_name="${clip1_nopath%.*}" 73 | 74 | # clip durations for fades 75 | clip1_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$clip1" | cut -d\. -f1) 76 | clip2_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$clip2" | cut -d\. -f1) 77 | 78 | # clip1 use the bc command to remove 1 second from length of clip for cross fade 79 | clip1_offset=$(echo "${clip1_dur}-1" | bc -l) 80 | clip2_offset=$(echo "${clip2_dur}-1" | bc -l) 81 | 82 | # variables 83 | outfile_default="${clip1_name}-xfade-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 84 | duration_default='1' 85 | 86 | 87 | #=============================================================================== 88 | # functions 89 | #=============================================================================== 90 | 91 | # cross fade video and audio 92 | xfade_va () { 93 | ffmpeg \ 94 | -hide_banner \ 95 | -stats -v panic \ 96 | -i "${clip1}" -i "${clip2}" \ 97 | -an -filter_complex \ 98 | " [0:v]trim=start=0:end='${clip1_offset}',setpts=PTS-STARTPTS[firstclip]; 99 | [1:v]trim=start=${dur:=${duration_default}},setpts=PTS-STARTPTS[secondclip]; 100 | [0:v]trim=start='${clip1_offset}':end='${clip1_dur}',setpts=PTS-STARTPTS[fadeoutsrc]; 101 | [1:v]trim=start=0:end=${dur:=${duration_default}},setpts=PTS-STARTPTS[fadeinsrc]; 102 | [fadeinsrc]format=pix_fmts=yuva420p, 103 | fade=t=in:st=0:d=${dur:=${duration_default}}:alpha=1[fadein]; 104 | [fadeoutsrc]format=pix_fmts=yuva420p, 105 | fade=t=out:st=0:d=${dur:=${duration_default}}:alpha=1[fadeout]; 106 | [fadein]fifo[fadeinfifo]; 107 | [fadeout]fifo[fadeoutfifo]; 108 | [fadeoutfifo][fadeinfifo]overlay[crossfade]; 109 | [firstclip][crossfade][secondclip]concat=n=3[output]; 110 | [0:a] afade=t=in:st=0:d=${dur:=${duration_default}} [audiofadein]; 111 | [1:a] afade=t=out:st='${clip2_offset}':d=${dur:=${duration_default}} [audiofadeout]; 112 | [audiofadein][audiofadeout] acrossfade=d=${dur:=${duration_default}} [audio] 113 | " \ 114 | -map "[output]" -map "[audio]" "${outfile:=${outfile_default}}" 115 | } 116 | 117 | # cross fade video 118 | xfade_v () { 119 | ffmpeg \ 120 | -hide_banner \ 121 | -stats -v panic \ 122 | -i "${clip1}" -i "${clip2}" \ 123 | -an -filter_complex \ 124 | " [0:v]trim=start=0:end='${clip1_offset}',setpts=PTS-STARTPTS[firstclip]; 125 | [1:v]trim=start=${dur:=${duration_default}},setpts=PTS-STARTPTS[secondclip]; 126 | [0:v]trim=start='${clip1_offset}':end='${clip1_dur}',setpts=PTS-STARTPTS[fadeoutsrc]; 127 | [1:v]trim=start=0:end=${dur:=${duration_default}},setpts=PTS-STARTPTS[fadeinsrc]; 128 | [fadeinsrc]format=pix_fmts=yuva420p, 129 | fade=t=in:st=0:d=${dur:=${duration_default}}:alpha=1[fadein]; 130 | [fadeoutsrc]format=pix_fmts=yuva420p, 131 | fade=t=out:st=0:d=${dur:=${duration_default}}:alpha=1[fadeout]; 132 | [fadein]fifo[fadeinfifo]; 133 | [fadeout]fifo[fadeoutfifo]; 134 | [fadeoutfifo][fadeinfifo]overlay[crossfade]; 135 | [firstclip][crossfade][secondclip]concat=n=3[output] 136 | " \ 137 | -map "[output]" "${outfile:=${outfile_default}}" 138 | } 139 | 140 | # check if video has an audio track 141 | clip1_check="$(ffprobe -i "${clip1}" -show_streams -select_streams a -loglevel error)" 142 | clip2_check="$(ffprobe -i "${clip2}" -show_streams -select_streams a -loglevel error)" 143 | 144 | # check if audio_check is null which means the video doesnt have an audio track 145 | if [ -z "${clip1_check}" ] || [ -z "${clip2_check}" ]; then 146 | xfade_v "${clip1}" "${clip2}" # fade video track 147 | else 148 | xfade_va "${clip1}" "${clip2}" # fade video and audio track 149 | fi 150 | -------------------------------------------------------------------------------- /ebu-meter: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # ebu-meter 5 | # ffplay ebu meter 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffplay 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | [ -z "${1}" ] || echo "! ${1}" 18 | echo "\ 19 | # ffplay ebu meter 20 | 21 | $(basename "$0") -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -t (00)" 22 | exit 2 23 | } 24 | 25 | 26 | #=============================================================================== 27 | # error messages 28 | #=============================================================================== 29 | 30 | INVALID_OPT_ERR='Invalid option:' 31 | REQ_ARG_ERR='requires an argument' 32 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 33 | 34 | 35 | #=============================================================================== 36 | # check the number of arguments passed to the script 37 | #=============================================================================== 38 | 39 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 40 | 41 | 42 | #=============================================================================== 43 | # getopts check the options passed to the script 44 | #=============================================================================== 45 | 46 | # getopts check and validate options 47 | while getopts ':i:t:h' opt 48 | do 49 | case ${opt} in 50 | i) infile="${OPTARG}";; 51 | t) target="${OPTARG}";; 52 | h) usage;; 53 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 54 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 55 | esac 56 | done 57 | shift $((OPTIND-1)) 58 | 59 | 60 | #=============================================================================== 61 | # variables 62 | #=============================================================================== 63 | 64 | # default target level 65 | target_default='16' 66 | 67 | 68 | #=============================================================================== 69 | # functions 70 | #=============================================================================== 71 | 72 | # ebu function 73 | ebu () { 74 | ffplay -hide_banner \ 75 | -f lavfi -i \ 76 | "amovie=${infile}, 77 | ebur128=video=1: 78 | meter=18: 79 | dualmono=true: 80 | target=-${target:=${target_default}}: 81 | size=1280x720 [out0][out1]" 82 | } 83 | 84 | # run the ebu function 85 | ebu "${infile}" 86 | -------------------------------------------------------------------------------- /extract-frame: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # extract-frame 5 | # extract a frame from a video as a png or jpg 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # extract a frame from a video as a png or jpg 22 | https://trac.ffmpeg.org/wiki/Seeking 23 | 24 | $(basename "$0") -i input.(mp4|mov|mkv|m4v|webm) -s 00:00:00.000 -t (png|jpg) -x width -y height -o output.(png|jpg) 25 | -i input.(mp4|mov|mkv|m4v) 26 | -s 00:00:00.000 : optional argument # if option not provided defaults to 00:00:00 27 | -t (png|jpg) : optional argument # if option not provided defaults to png 28 | -x width : optional argument # 29 | -y height : optional argument # 30 | -o output.(png|jpg) : optional argument # if option not provided defaults to input-name-timecode" 31 | exit 2 32 | } 33 | 34 | 35 | #=============================================================================== 36 | # error messages 37 | #=============================================================================== 38 | 39 | INVALID_OPT_ERR='Invalid option:' 40 | REQ_ARG_ERR='requires an argument' 41 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 42 | 43 | 44 | #=============================================================================== 45 | # check the number of arguments passed to the script 46 | #=============================================================================== 47 | 48 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 49 | 50 | 51 | #=============================================================================== 52 | # getopts check the options passed to the script 53 | #=============================================================================== 54 | 55 | while getopts ':i:s:t:x:y:o:h' opt 56 | do 57 | case ${opt} in 58 | i) infile="${OPTARG}";; 59 | s) seconds="${OPTARG}";; 60 | t) image="${OPTARG}";; 61 | x) width="${OPTARG}";; 62 | y) height="${OPTARG}";; 63 | o) outfile="${OPTARG}";; 64 | h) usage;; 65 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 66 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 67 | esac 68 | done 69 | shift $((OPTIND-1)) 70 | 71 | 72 | #=============================================================================== 73 | # variables 74 | #=============================================================================== 75 | 76 | # input, input name 77 | infile_nopath="${infile##*/}" 78 | infile_name="${infile_nopath%.*}" 79 | 80 | # seconds default 81 | seconds_default='00:00:00' 82 | 83 | # image default 84 | image_default="png" 85 | 86 | # output file recording destination 87 | outfile_default="${infile_name}-[${seconds:=${seconds_default}}].${image:=${image_default}}" 88 | 89 | 90 | #=============================================================================== 91 | # functions 92 | #=============================================================================== 93 | 94 | # image to video function 95 | extract () { 96 | ffmpeg \ 97 | -hide_banner \ 98 | -stats -v panic \ 99 | -ss "${seconds:=${seconds_default}}" \ 100 | -i "${infile}" \ 101 | -q:v 2 -f image2 \ 102 | -vframes 1 \ 103 | "${outfile:=${outfile_default}}" 104 | } 105 | 106 | # image to video with scale function 107 | extract_scale () { 108 | ffmpeg \ 109 | -hide_banner \ 110 | -stats -v panic \ 111 | -ss "${seconds:=${seconds_default}}" \ 112 | -i "${infile}" \ 113 | -q:v 2 -f image2 \ 114 | -vframes 1 \ 115 | -vf scale="${width:=-1}:${height:=-1}" \ 116 | "${outfile:=${outfile_default}}" 117 | } 118 | 119 | 120 | #=============================================================================== 121 | # run function 122 | #=============================================================================== 123 | 124 | if [ -n "${width}" ] || [ -n "${height}" ]; then 125 | extract_scale "${infile}" 126 | else 127 | extract "${infile}" 128 | fi 129 | -------------------------------------------------------------------------------- /fade-clip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # fade-clip 5 | # fade video and audio in and out 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg bc 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | echo "\ 18 | # fade video and audio in and out 19 | 20 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4 21 | -i infile.(mp4|mkv|mov|m4v) 22 | -d (0.[0-9]|1) : optional argument # if option not provided defaults to 0.5 23 | -o output.mp4 : optional argument # if option not provided defaults to input-name-fade-date-time" 24 | exit 2 25 | } 26 | 27 | 28 | #=============================================================================== 29 | # error messages 30 | #=============================================================================== 31 | 32 | INVALID_OPT_ERR='Invalid option:' 33 | REQ_ARG_ERR='requires an argument' 34 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 35 | 36 | 37 | #=============================================================================== 38 | # check the number of arguments passed to the script 39 | #=============================================================================== 40 | 41 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 42 | 43 | 44 | #=============================================================================== 45 | # getopts check the options passed to the script 46 | #=============================================================================== 47 | 48 | while getopts ':i:d:o:h' opt 49 | do 50 | case ${opt} in 51 | i) infile="${OPTARG}";; 52 | d) dur="${OPTARG}";; 53 | o) outfile="${OPTARG}";; 54 | h) usage;; 55 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 56 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 57 | esac 58 | done 59 | shift $((OPTIND-1)) 60 | 61 | 62 | #=============================================================================== 63 | # variables 64 | #=============================================================================== 65 | 66 | # input, input name and extension 67 | infile_nopath="${infile##*/}" 68 | infile_name="${infile_nopath%.*}" 69 | 70 | # defaults for variables if not defined 71 | outfile_default="${infile_name}-fade-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 72 | duration_default="0.5" 73 | 74 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 75 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 76 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 77 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 78 | 79 | # check ffmpeg aac codecs 80 | if [ -z "${aac_check}" ]; then 81 | aac='libfdk_aac' # libfdk_aac codec is installed 82 | else 83 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 84 | fi 85 | 86 | 87 | #=============================================================================== 88 | # functions 89 | #=============================================================================== 90 | 91 | # ffmpeg fade video track 92 | fade_v () { 93 | echo '+ Getting video duration' && \ 94 | video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1) 95 | vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l) 96 | ffmpeg \ 97 | -hide_banner \ 98 | -stats -v panic \ 99 | -i "${infile}" \ 100 | -filter_complex \ 101 | " [0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[fv] " \ 102 | -map "[fv]" \ 103 | -c:v libx264 -preset fast \ 104 | -profile:v high \ 105 | -crf 18 -coder 1 \ 106 | -pix_fmt yuv420p \ 107 | -movflags +faststart \ 108 | -f mp4 \ 109 | "${outfile:=${outfile_default}}" 110 | } 111 | 112 | # fade video and audio tracks 113 | fade_va () { 114 | echo '+ Getting video duration' && \ 115 | video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1) 116 | vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l) 117 | ffmpeg \ 118 | -hide_banner \ 119 | -stats -v panic \ 120 | -i "${infile}" \ 121 | -filter_complex \ 122 | " [0:a] afade=t=in:st=0:d=${dur:=${duration_default}},afade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[fa]; 123 | [0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[fv] 124 | " \ 125 | -map "[fv]" -map "[fa]" \ 126 | -c:a "${aac}" \ 127 | -c:v libx264 -preset fast \ 128 | -profile:v high \ 129 | -crf 18 -coder 1 \ 130 | -pix_fmt yuv420p \ 131 | -movflags +faststart \ 132 | -f mp4 \ 133 | "${outfile:=${outfile_default}}" 134 | } 135 | 136 | # check if video has an audio track 137 | audio_check="$(ffprobe -i "${infile}" -show_streams -select_streams a -loglevel error)" 138 | 139 | # check if audio_check is null which means the video doesnt have an audio track 140 | if [ -z "${audio_check}" ]; then 141 | fade_v "${infile}" # null value 142 | else 143 | fade_va "${infile}" # non null value 144 | fi 145 | -------------------------------------------------------------------------------- /fade-normalize: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # fade-normalize 5 | # fade video and normalize audio levels 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg awk grep 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # fade video and normalize audio levels 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -o output.mp4 24 | 25 | -d (0.[0-9]|1) : optional argument # if option not provided defaults to 0.5 26 | -o output.mp4 : optional argument # if option not provided defaults to input-name-normalized-date-time" 27 | exit 2 28 | } 29 | 30 | 31 | #=============================================================================== 32 | # error messages 33 | #=============================================================================== 34 | 35 | INVALID_OPT_ERR='Invalid option:' 36 | REQ_ARG_ERR='requires an argument' 37 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 38 | 39 | 40 | #=============================================================================== 41 | # check the number of arguments passed to the script 42 | #=============================================================================== 43 | 44 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 45 | 46 | 47 | #=============================================================================== 48 | # getopts check the options passed to the script 49 | #=============================================================================== 50 | 51 | while getopts ':i:d:o:h' opt 52 | do 53 | case ${opt} in 54 | i) infile="${OPTARG}";; 55 | d) dur="${OPTARG}";; 56 | o) outfile="${OPTARG}";; 57 | h) usage;; 58 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 59 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 60 | esac 61 | done 62 | shift $((OPTIND-1)) 63 | 64 | 65 | #=============================================================================== 66 | # variables 67 | #=============================================================================== 68 | 69 | # input, input name and file extension 70 | infile_nopath="${infile##*/}" 71 | infile_name="${infile_nopath%.*}" 72 | 73 | # defaults for variables if not defined 74 | outfile_default="${infile_name}-normalized-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 75 | duration_default="0.5" 76 | 77 | # print analyzing file 78 | echo '+ Analyzing file with ffmpeg' 79 | 80 | # ffmpeg loudnorm get stats from file 81 | normalize=$(ffmpeg \ 82 | -hide_banner \ 83 | -i "${infile}" \ 84 | -af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \ 85 | -f null - 2>&1 | tail -n 12) 86 | 87 | # read the output of normalize line by line and store in variables 88 | for line in "${normalize}"; do 89 | measured_I=$(echo "${line}" | awk -F' ' '/Input Integrated:/ {print $3}') 90 | measured_TP=$(echo "${line}" | awk -F' ' '/Input True Peak:/ {print $4}') 91 | measured_LRA=$(echo "${line}" | awk -F' ' '/Input LRA:/ {print $3}') 92 | measured_thresh=$(echo "${line}" | awk -F' ' '/Input Threshold:/ {print $3}') 93 | offset=$(echo "${line}" | awk -F' ' '/Target Offset:/ {print $3}') 94 | done 95 | 96 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 97 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 98 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 99 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 100 | 101 | # check ffmpeg aac codecs 102 | if [ -z "${aac_check}" ]; then 103 | aac='libfdk_aac' # libfdk_aac codec is installed 104 | else 105 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 106 | fi 107 | 108 | 109 | #=============================================================================== 110 | # functions 111 | #=============================================================================== 112 | 113 | # video function 114 | video () { 115 | video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1) 116 | vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l) 117 | ffmpeg \ 118 | -hide_banner \ 119 | -stats -v panic \ 120 | -i "${infile}" \ 121 | -filter_complex \ 122 | "[0:a] afade=t=in:st=0:d=${dur:=${duration_default}},afade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}, 123 | compand=attacks=0:points=-70/-90|-24/-12|0/-6|20/-6, 124 | highpass=f=60, 125 | lowpass=f=13700, 126 | afftdn=nt=w, 127 | adeclick, 128 | deesser, 129 | loudnorm=I=-16: 130 | dual_mono=true: 131 | TP=-1.5: 132 | LRA=11: 133 | measured_I=${measured_I}: 134 | measured_LRA=${measured_LRA}: 135 | measured_TP=${measured_TP}: 136 | measured_thresh=${measured_thresh}: 137 | offset=${offset}: 138 | linear=true: 139 | print_format=summary [audio]; 140 | [0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}[video]" \ 141 | -map "[video]" -map "[audio]" \ 142 | -c:a "${aac}" -ar 44100 \ 143 | -c:v libx264 -preset fast \ 144 | -profile:v high \ 145 | -crf 18 -coder 1 \ 146 | -pix_fmt yuv420p \ 147 | -movflags +faststart \ 148 | -f mp4 \ 149 | "${outfile:=${outfile_default}}" 150 | } 151 | 152 | # run the video function 153 | video "${infile}" 154 | -------------------------------------------------------------------------------- /fade-title: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # fade-title 5 | # fade video, audio add title from video filename 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg file awk grep bc cut tail 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # fade video, audio add title from video filename 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -d (0.[0-9]|1) -s 000 -e 000 -o output.mp4 24 | 25 | -i input.(mp4|mkv|mov|m4v) 26 | -d (0.[0-9]|1) : from 0.1 to 0.9 or 1 :optional argument # if option not provided defaults to 0.5 27 | -s 000 : from 000 to 999 28 | -e 000 : from 000 to 999 29 | -o output.mp4 : optional argument # if option not provided defaults to input-name-title-date-time" 30 | exit 2 31 | } 32 | 33 | 34 | #=============================================================================== 35 | # error messages 36 | #=============================================================================== 37 | 38 | INVALID_OPT_ERR='Invalid option:' 39 | REQ_ARG_ERR='requires an argument' 40 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 41 | NOT_MEDIA_FILE_ERR='is not a media file' 42 | TITLE_FADE_ERR='title end must be after title start' 43 | 44 | 45 | #=============================================================================== 46 | # check the number of arguments passed to the script 47 | #=============================================================================== 48 | 49 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 50 | 51 | 52 | #=============================================================================== 53 | # getopts check the options passed to the script 54 | #=============================================================================== 55 | 56 | while getopts ':i:d:s:e:o:h' opt 57 | do 58 | case ${opt} in 59 | i) infile="${OPTARG}";; 60 | d) dur="${OPTARG}";; 61 | s) title_start="${OPTARG}";; 62 | e) title_end="${OPTARG}";; 63 | o) outfile="${OPTARG}";; 64 | h) usage;; 65 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 66 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 67 | esac 68 | done 69 | shift $((OPTIND-1)) 70 | 71 | 72 | #=============================================================================== 73 | # variables 74 | #=============================================================================== 75 | 76 | # input, input name and file extension 77 | infile_nopath="${infile##*/}" 78 | infile_name="${infile_nopath%.*}" 79 | 80 | # file command check input file mime type 81 | filetype="$(file --mime-type -b "${infile}")" 82 | 83 | # video mimetypes 84 | mov_mime='video/quicktime' 85 | mkv_mime='video/x-matroska' 86 | mp4_mime='video/mp4' 87 | m4v_mime='video/x-m4v' 88 | 89 | # check the files mime type 90 | case "${filetype}" in 91 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}");; 92 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 93 | esac 94 | 95 | # defaults for variables if not defined 96 | outfile_default="${infile_name}-title-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 97 | duration_default="0.5" 98 | 99 | # print analyzing file 100 | echo '+ Analyzing file with ffmpeg' 101 | 102 | # ffmpeg loudnorm get stats from file 103 | normalize=$(ffmpeg \ 104 | -hide_banner \ 105 | -i "${infile}" \ 106 | -af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \ 107 | -f null - 2>&1 | tail -n 12) 108 | 109 | # read the output of normalize line by line and store in variables 110 | for line in "${normalize}"; do 111 | measured_I=$(echo "${line}" | awk -F' ' '/Input Integrated:/ {print $3}') 112 | measured_TP=$(echo "${line}" | awk -F' ' '/Input True Peak:/ {print $4}') 113 | measured_LRA=$(echo "${line}" | awk -F' ' '/Input LRA:/ {print $3}') 114 | measured_thresh=$(echo "${line}" | awk -F' ' '/Input Threshold:/ {print $3}') 115 | offset=$(echo "${line}" | awk -F' ' '/Target Offset:/ {print $3}') 116 | done 117 | 118 | # video duration and video offset minus 1 second for fade out 119 | video_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${infile}" | cut -d\. -f1) 120 | vid_offset=$(echo "${video_dur}-${dur:=${duration_default}}" | bc -l) 121 | 122 | # video height 123 | video_size=$(ffprobe -v error -show_entries stream=height -of default=noprint_wrappers=1:nokey=1 "${infile}") 124 | 125 | # video title from filename 126 | title="${infile_name}" 127 | 128 | # video title variables 129 | font="OpenSans-Regular.ttf" 130 | font_color="white" 131 | boxcolor="black@0.4" 132 | 133 | # video title fade 134 | DS="${title_start}" # display start 135 | DE="${title_end}" # display end, number of seconds after start 136 | FID="${dur:=${duration_default}}" # fade in duration 137 | FOD="${dur:=${duration_default}}" # fade out duration 138 | 139 | # check title end is a number larger than title start 140 | if [ "${DE}" -le "${DS}" ]; then 141 | echo "${TITLE_FADE_ERR}" && usage 142 | fi 143 | 144 | # calculate drawbox and drawtext size based on video height 145 | case "${video_size}" in 146 | 1080) # 1080 height 147 | drawbox_height=$(echo "${video_size}/13.4" | bc) 148 | drawtext_size=$(echo "${drawbox_height}/2" | bc) 149 | ;; 150 | 720) # 720 height 151 | drawbox_height=$(echo "${video_size}/9" | bc) 152 | drawtext_size=$(echo "${drawbox_height}/2" | bc) 153 | ;; 154 | *) # all other heights 155 | drawbox_height=$(echo "${video_size}/9" | bc) 156 | drawtext_size=$(echo "${drawbox_height}/2" | bc) 157 | ;; 158 | esac 159 | 160 | # drawbox, drawtext size 161 | boxheight="${drawbox_height}" 162 | font_size="${drawtext_size}" 163 | 164 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 165 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 166 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 167 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 168 | 169 | # check ffmpeg aac codecs 170 | if [ -z "${aac_check}" ]; then 171 | aac='libfdk_aac' # libfdk_aac codec is installed 172 | else 173 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 174 | fi 175 | 176 | 177 | #=============================================================================== 178 | # functions 179 | #=============================================================================== 180 | 181 | # video function 182 | video () { 183 | ffmpeg \ 184 | -hide_banner \ 185 | -stats -v panic \ 186 | -i "${infile}" \ 187 | -filter_complex \ 188 | "[0:a] afade=t=in:st=0:d=${dur:=${duration_default}},afade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}, 189 | compand=attacks=0:points=-70/-90|-24/-12|0/-6|20/-6, 190 | highpass=f=60, 191 | lowpass=f=13700, 192 | afftdn=nt=w, 193 | adeclick, 194 | deesser, 195 | loudnorm=I=-16: 196 | dual_mono=true: 197 | TP=-1.5: 198 | LRA=11: 199 | measured_I=${measured_I}: 200 | measured_LRA=${measured_LRA}: 201 | measured_TP=${measured_TP}: 202 | measured_thresh=${measured_thresh}: 203 | offset=${offset}: 204 | linear=true: 205 | print_format=summary [audio]; 206 | [0:v] fade=t=in:st=0:d=${dur:=${duration_default}},fade=t=out:st='${vid_offset}':d=${dur:=${duration_default}}, \ 207 | format=yuv444p, 208 | drawbox=enable='between(t,${DS},${DE})': 209 | y=(ih-h/PHI)-(${boxheight}): 210 | color=${boxcolor}: 211 | width=iw:height=${boxheight}:t=fill, 212 | drawtext=fontfile=${font}: 213 | text=${title}: 214 | fontcolor=${font_color}:fontsize=${font_size}: 215 | x=20: 216 | y=h-(${boxheight})-(${boxheight}/2)+th/4: 217 | :fontcolor_expr=fdfdfd%{eif\\\\: clip(255*(1*between(t\\, $DS + $FID\\, $DE - $FOD) + ((t - $DS)/$FID)*between(t\\, $DS\\, $DS + $FID) + (-(t - $DE)/$FOD)*between(t\\, $DE - $FOD\\, $DE) )\\, 0\\, 255) \\\\: x\\\\: 2 }, \ 218 | format=yuv420p[video]" \ 219 | -map "[video]" -map "[audio]" \ 220 | -c:a "${aac}" -ar 44100 \ 221 | -c:v libx264 -preset fast \ 222 | -profile:v high \ 223 | -crf 18 -coder 1 \ 224 | -pix_fmt yuv420p \ 225 | -movflags +faststart \ 226 | -f mp4 \ 227 | "${outfile:=${outfile_default}}" 228 | } 229 | 230 | # check the files mime type 231 | case "${filetype}" in 232 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") video "${infile}";; 233 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 234 | esac 235 | -------------------------------------------------------------------------------- /ffmpeg-tips.org: -------------------------------------------------------------------------------- 1 | #+STARTUP: content 2 | #+OPTIONS: num:nil author:nil 3 | 4 | * ffmpeg tips 5 | ** Extracting audio 6 | *** Extracting audio from video 7 | 8 | Extracting audio from a video file 9 | 10 | #+BEGIN_SRC sh 11 | ffmpeg -i infile.mp4 -vn -c:a copy outfile.m4a 12 | #+END_SRC 13 | 14 | *** Batch extracting audio from video 15 | 16 | Batch extract audio from video with find 17 | 18 | #+BEGIN_SRC sh 19 | find . -type f -name "*.mp4" -exec sh -c \ 20 | 'ffmpeg -i "${0}" -vn -c:a copy "${0%.*}.m4a"' \ 21 | "{}" \; 22 | #+END_SRC 23 | 24 | ** Extract video tracks 25 | *** extract video with ffmpeg 26 | 27 | ffmpeg extract video 28 | 29 | #+BEGIN_SRC sh 30 | ffmpeg -i infile.mp4 -an -c:v copy outfile.mp4 31 | #+END_SRC 32 | 33 | *** batch extract video with ffmpeg 34 | 35 | ffmpeg batch extract video 36 | 37 | #+BEGIN_SRC sh 38 | find . -type f -name "*.mp4" -exec sh -c \ 39 | 'ffmpeg -i "${0}" -an -c:v copy "${0%.*}-extracted.mp4"' \ 40 | "{}" \; 41 | #+END_SRC 42 | 43 | ** change framerate with ffmpeg 44 | 45 | change a videos framerate to 30fps 46 | 47 | #+BEGIN_SRC sh 48 | ffmpeg -i infile.mp4 -filter:v fps=fps=30 outfile.mp4 49 | #+END_SRC 50 | 51 | ** change framereate batch process 52 | 53 | change frame batch process to 30fps 54 | 55 | #+BEGIN_SRC sh 56 | find . -type f -name "*.mp4" -exec sh -c \ 57 | 'ffmpeg -i "${0}" -filter:v fps=fps=30 "${0%.*}-framerate.mp4"' \ 58 | "{}" \; 59 | #+END_SRC 60 | 61 | ** convert stereo to mono 62 | 63 | #+BEGIN_SRC sh 64 | ffmpeg -i infile.mp4 -ac 1 outfile.mp4 65 | #+END_SRC 66 | 67 | ** convert stereo to mono batch process 68 | 69 | change frame batch process to 30fps 70 | 71 | #+BEGIN_SRC sh 72 | find . -type f -name "*.mp4" -exec sh -c \ 73 | 'ffmpeg -i "${0}" -ac 1 "${0%.*}-mono.mp4"' \ 74 | "{}" \; 75 | #+END_SRC 76 | 77 | ** change audio rate 78 | 79 | change audio rate to 44100 80 | 81 | #+BEGIN_SRC sh 82 | ffmpeg -i infile.mp4 -ar 44100 outfile.mp4 83 | #+END_SRC 84 | ** change audio rate batch process 85 | 86 | change audio rate to 44100 batch process 87 | 88 | #+BEGIN_SRC sh 89 | find . -type f -name "*.mp4" -exec sh -c \ 90 | 'ffmpeg -i "${0}" -ar 44100 "${0%.*}-audiorate.mp4"' \ 91 | "{}" \; 92 | #+END_SRC 93 | 94 | ** scale and pad video to 1080 95 | 96 | scale and pad a video to 1080 97 | 98 | #+BEGIN_SRC sh 99 | ffmpeg -i fps.mp4 -vf "scale=-1:1080,pad=1920:ih:(ow-iw)/2" outfile.mp4 100 | #+END_SRC 101 | 102 | ** scale and pad video to 1080 batch process 103 | 104 | scale and pad a video to 1080 batch process 105 | 106 | #+BEGIN_SRC sh 107 | find . -type f -name "*.mp4" -exec sh -c \ 108 | 'ffmpeg -i "${0}" -vf "scale=-1:1080,pad=1920:ih:(ow-iw)/2" "${0%.*}-1080.mp4"' \ 109 | "{}" \; 110 | #+END_SRC 111 | ** Trim video 112 | 113 | Trim a video clip with a new start and end point 114 | 115 | #+BEGIN_SRC sh 116 | ffmpeg \ 117 | -ss 00:00:30 \ 118 | -i infile.mp4 \ 119 | -t 00:01:30 \ 120 | -c:a copy \ 121 | -c:v libx264 -profile:v high -pix_fmt yuv420p \ 122 | -movflags +faststart -f mp4 outfile.mp4 123 | #+END_SRC 124 | 125 | ** Extract frame as image 126 | 127 | Find all mp4 or mkv files and extract a frame as a png image 128 | 129 | #+BEGIN_SRC sh 130 | find . -type f -name "*.[mp4kv]*" -exec sh -c \ 131 | 'ffmpeg -ss 00:00:05 -i "$1" -q:v 2 -f image2 -vframes 1 "${1%.*}.png" -hide_banner' sh {} \; 132 | #+END_SRC 133 | 134 | ** Convert mkv to mp4 135 | 136 | Convert a mkv video to a mp4 file 137 | to import into your video editor like Final Cut Pro 138 | 139 | #+BEGIN_SRC sh 140 | ffmpeg -i infile.mkv \ 141 | -c:v libx264 -crf 18 -profile:v high \ 142 | -pix_fmt yuv420p -movflags +faststart -f mp4 \ 143 | outfile.mp4 144 | #+END_SRC 145 | 146 | ** Convert audio 147 | *** Convert wav to m4a 148 | 149 | Find wav files and convert to m4a 150 | 151 | #+BEGIN_SRC sh 152 | find . -type f -name "*.wav" -exec sh -c \ 153 | 'ffmpeg -i "$0" -map 0:0 -c:a aac -b:a 320k "${0%.*}.m4a"' "{}" \; 154 | #+END_SRC 155 | 156 | *** Convert wav to mp3 157 | 158 | Find wav files and convert to mp3 159 | 160 | #+BEGIN_SRC sh 161 | find . -type f -name "*.wav" -exec sh -c \ 162 | 'ffmpeg -i "$0" -map 0:0 -c:a libmp3lame -b:a 320k "${0%.*}.mp3"' "{}" \; 163 | #+END_SRC 164 | 165 | ** ffmpeg concat clips 166 | 167 | create a list of all the mp4s in the current directory 168 | 169 | #+BEGIN_SRC sh 170 | printf "file '%s'\n" *.mp4 > list.txt 171 | #+END_SRC 172 | 173 | use ffplay to play the list of videos in the text file 174 | 175 | #+BEGIN_SRC sh 176 | ffmpeg -f concat -i list.txt -c copy outfile.mp4 177 | #+END_SRC 178 | 179 | use ffmpeg to concat the video file in the text file 180 | 181 | #+BEGIN_SRC sh 182 | ffmpeg -f concat -i list.txt -c copy outfile.mp4 183 | #+END_SRC 184 | 185 | use subshell to generate a list of the mp4s in the current directory 186 | 187 | #+BEGIN_SRC sh 188 | ffmpeg -f concat -i <( for f in *.mp4; do echo "file '$(pwd)/$f'"; done ) outfile.mp4 189 | #+END_SRC 190 | 191 | ** Closed captions 192 | 193 | Extracting, adding and deleting closed captions from videos 194 | 195 | *** youtube_dl download subtitles 196 | 197 | youtube_dl download subtitles from video 198 | 199 | #+BEGIN_SRC sh 200 | youtube-dl --write-sub --sub-lang en --skip-download 'youtube-url' 201 | #+END_SRC 202 | 203 | youtube-dl batch download subtitles from a text file with youtube urls 204 | 205 | #+BEGIN_SRC sh 206 | youtube-dl --write-sub --sub-lang en --skip-download -a links.txt 207 | #+END_SRC 208 | 209 | **** convert closed captions to srt 210 | 211 | convert scc closed captions to srt subtitles, 212 | and remove text formatting and font tags 213 | for youtube 214 | 215 | #+BEGIN_SRC sh 216 | ffmpeg -i infile.scc -c:s text outfile.srt 217 | #+END_SRC 218 | 219 | convert the vtt subtitles from youtube to srt format 220 | 221 | #+BEGIN_SRC sh 222 | ffmpeg -i infile.vtt -c:s text outfile.srt 223 | #+END_SRC 224 | 225 | batch convert vtt subtitles to srt format 226 | 227 | #+BEGIN_SRC sh 228 | find . -type f -name "*.vtt" -exec sh -c 'ffmpeg -i "$0" \ 229 | -c:s text "${0%.*}.srt"' "{}" \; 230 | #+END_SRC 231 | 232 | **** ffmpeg add subtitles to video 233 | 234 | #+BEGIN_SRC sh 235 | ffmpeg -i infile.mp4 \ 236 | -f srt -i infile.srt \ 237 | -c:a copy -c:v copy -c:s \ 238 | mov_text -metadata:s:s:0 \ 239 | language=eng \ 240 | -movflags +faststart \ 241 | outfile.mp4 242 | #+END_SRC 243 | 244 | **** remove close captions 245 | 246 | remove close captions from video without re encode 247 | 248 | #+BEGIN_SRC sh 249 | ffmpeg -i infile.mp4 \ 250 | -c copy \ 251 | -bsf:v "filter_units=remove_type=6" \ 252 | -movflags +faststart \ 253 | outfile.mp4 254 | #+END_SRC 255 | 256 | *** ccextractor 257 | 258 | Closed caption extractor for MPEG and H264 files 259 | 260 | Extract closed captions from video and save as a srt file 261 | 262 | #+BEGIN_SRC sh 263 | ccextractor infile.mp4 264 | #+END_SRC 265 | 266 | **** Linux ccextractor install 267 | 268 | #+BEGIN_SRC sh 269 | sudo apt install ccextractor 270 | #+END_SRC 271 | 272 | **** Freebsd ccextractor install 273 | 274 | Freebsd ccextractor install 275 | 276 | #+BEGIN_SRC sh 277 | pkg install ccextractor 278 | #+END_SRC 279 | 280 | ** convert ipad video to 1080 281 | 282 | convert to 1080 30fps 283 | 284 | #+BEGIN_SRC sh 285 | ffmpeg \ 286 | -i infile.mp4 \ 287 | -vf "scale=-1:1080,pad=1920:ih:(ow-iw)/2,fps=fps=30" \ 288 | outfile.mp4 289 | #+END_SRC 290 | 291 | convert to 1080 30fps mono 292 | 293 | #+BEGIN_SRC sh 294 | ffmpeg \ 295 | -i infile.mp4 \ 296 | -vf "scale=-1:1080,pad=1920:ih:(ow-iw)/2,fps=fps=30" \ 297 | -ac 1 outfile.mp4 298 | #+END_SRC 299 | -------------------------------------------------------------------------------- /img2video: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # img2video 5 | # image to video 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | echo "\ 18 | # image to video 19 | 20 | $(basename "$0") -i input.(png|jpg|jpeg) -d (000) -o output.mp4 21 | -i input.(mp4|mkv|mov|m4v) 22 | -d (000) : duration 23 | -o output.mp4 : optional argument # if option not provided defaults to input-name-video-date-time" 24 | exit 2 25 | } 26 | 27 | 28 | #=============================================================================== 29 | # error messages 30 | #=============================================================================== 31 | 32 | INVALID_OPT_ERR='Invalid option:' 33 | REQ_ARG_ERR='requires an argument' 34 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 35 | 36 | 37 | #=============================================================================== 38 | # check the number of arguments passed to the script 39 | #=============================================================================== 40 | 41 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 42 | 43 | 44 | #=============================================================================== 45 | # getopts check the options passed to the script 46 | #=============================================================================== 47 | 48 | while getopts ':i:d:o:h' opt 49 | do 50 | case ${opt} in 51 | i) infile="${OPTARG}";; 52 | d) dur="${OPTARG}";; 53 | o) outfile="${OPTARG}";; 54 | h) usage;; 55 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 56 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 57 | esac 58 | done 59 | shift $((OPTIND-1)) 60 | 61 | 62 | #=============================================================================== 63 | # variables 64 | #=============================================================================== 65 | 66 | # input, input name and extension 67 | infile_nopath="${infile##*/}" 68 | infile_name="${infile_nopath%.*}" 69 | 70 | # output file recording destination 71 | outfile_default="${infile_name}-video-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 72 | 73 | 74 | #=============================================================================== 75 | # functions 76 | #=============================================================================== 77 | 78 | # image to video function 79 | imgtovid () { 80 | ffmpeg \ 81 | -hide_banner \ 82 | -stats -v panic \ 83 | -framerate 1/"${dur}" \ 84 | -i "${infile}" \ 85 | -c:v libx264 -crf 18 -profile:v high \ 86 | -r 30 -pix_fmt yuv420p \ 87 | -movflags +faststart -f mp4 \ 88 | "${outfile:=${outfile_default}}" 89 | } 90 | 91 | # run the imgtovid function 92 | imgtovid "${infile}" 93 | -------------------------------------------------------------------------------- /loudnorm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # loudnorm 5 | # ffmpeg loudnorm 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # ffmpeg loudnorm 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3)" 24 | exit 2 25 | } 26 | 27 | 28 | #=============================================================================== 29 | # error messages 30 | #=============================================================================== 31 | 32 | INVALID_OPT_ERR='Invalid option:' 33 | REQ_ARG_ERR='requires an argument' 34 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 35 | 36 | 37 | #=============================================================================== 38 | # check the number of arguments passed to the script 39 | #=============================================================================== 40 | 41 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 42 | 43 | 44 | #=============================================================================== 45 | # getopts check the options passed to the script 46 | #=============================================================================== 47 | 48 | while getopts ':i:h' opt 49 | do 50 | case ${opt} in 51 | i) infile="${OPTARG}";; 52 | h) usage;; 53 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 54 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 55 | esac 56 | done 57 | shift $((OPTIND-1)) 58 | 59 | 60 | #=============================================================================== 61 | # functions 62 | #=============================================================================== 63 | 64 | # ffmpeg loudnorm get stats from file 65 | normalize () { 66 | ffmpeg \ 67 | -hide_banner \ 68 | -i "${infile}" \ 69 | -af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \ 70 | -f null - 71 | } 72 | 73 | # run the normalize function 74 | normalize "${infile}" 75 | -------------------------------------------------------------------------------- /normalize: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # nomalize 5 | # normalize audio levels 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg file awk tail 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | echo "\ 18 | # normalize audio levels 19 | 20 | $(basename "$0") -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) -o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 21 | -i input.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 22 | -o output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) : optional argument 23 | # if option not provided defaults to input-name-normalized-date-time-extension" 24 | exit 2 25 | } 26 | 27 | #=============================================================================== 28 | # error messages 29 | #=============================================================================== 30 | 31 | INVALID_OPT_ERR='Invalid option:' 32 | REQ_ARG_ERR='requires an argument' 33 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 34 | NOT_MEDIA_FILE_ERR='is not a media file' 35 | 36 | 37 | #=============================================================================== 38 | # check the number of arguments passed to the script 39 | #=============================================================================== 40 | 41 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 42 | 43 | 44 | #=============================================================================== 45 | # getopts check the options passed to the script 46 | #=============================================================================== 47 | 48 | while getopts ':i:o:h' opt 49 | do 50 | case ${opt} in 51 | i) infile="${OPTARG}";; 52 | o) outfile="${OPTARG}";; 53 | h) usage;; 54 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 55 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 56 | esac 57 | done 58 | shift $((OPTIND-1)) 59 | 60 | 61 | #=============================================================================== 62 | # variables 63 | #=============================================================================== 64 | 65 | # input, input name 66 | infile_nopath="${infile##*/}" 67 | infile_name="${infile_nopath%.*}" 68 | infile_ext="${infile##*.}" 69 | 70 | # file command check input file mime type 71 | filetype="$(file --mime-type -b "${infile}")" 72 | 73 | # audio and video mimetypes 74 | mov_mime='video/quicktime' 75 | mkv_mime='video/x-matroska' 76 | mp4_mime='video/mp4' 77 | m4v_mime='video/x-m4v' 78 | wav_mime='audio/x-wav' 79 | audio_mime='audio/mpeg' 80 | aac_mime='audio/x-hx-aac-adts' 81 | m4a_mime='audio/mp4' 82 | 83 | # the file command wrongly identifies .m4a audio as a video file 84 | # so we check if the file extension is .m4a and set the mime type to audio/mp4 85 | if [ "${infile_ext}" = 'm4a' ]; then 86 | filetype="${m4a_mime}" 87 | fi 88 | 89 | # check the files mime type 90 | case "${filetype}" in 91 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}"| \ 92 | "${wav_mime}"|"${audio_mime}"|"${aac_mime}"|"${m4a_mime}");; 93 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 94 | esac 95 | 96 | # print analyzing file 97 | echo '+ Analyzing file with ffmpeg' 98 | 99 | # ffmpeg loudnorm get stats from file 100 | normalize=$(ffmpeg \ 101 | -hide_banner \ 102 | -i "${infile}" \ 103 | -af "loudnorm=I=-16:dual_mono=true:TP=-1.5:LRA=11:print_format=summary" \ 104 | -f null - 2>&1 | tail -n 12) 105 | 106 | # read the output of normalize line by line and store in variables 107 | for line in "${normalize}"; do 108 | measured_I=$(echo "${line}" | awk -F' ' '/Input Integrated:/ {print $3}') 109 | measured_TP=$(echo "${line}" | awk -F' ' '/Input True Peak:/ {print $4}') 110 | measured_LRA=$(echo "${line}" | awk -F' ' '/Input LRA:/ {print $3}') 111 | measured_thresh=$(echo "${line}" | awk -F' ' '/Input Threshold:/ {print $3}') 112 | offset=$(echo "${line}" | awk -F' ' '/Target Offset:/ {print $3}') 113 | done 114 | 115 | # defaults for variables if not defined 116 | audio_default="${infile_name}-normalized-$(date +"%Y-%m-%d-%H-%M-%S").${infile_ext}" 117 | video_default="${infile_name}-normalized-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 118 | 119 | 120 | #=============================================================================== 121 | # functions 122 | #=============================================================================== 123 | 124 | # audio function 125 | audio () { 126 | ffmpeg \ 127 | -hide_banner \ 128 | -stats -v panic \ 129 | -i "${infile}" \ 130 | -filter_complex \ 131 | "loudnorm=I=-16: 132 | dual_mono=true: 133 | TP=-1.5: 134 | LRA=11: 135 | measured_I=${measured_I}: 136 | measured_LRA=${measured_LRA}: 137 | measured_TP=${measured_TP}: 138 | measured_thresh=${measured_thresh}: 139 | offset=${offset}: 140 | linear=true: 141 | print_format=summary [audio]" \ 142 | -map "[audio]" \ 143 | -ar 44100 \ 144 | "${outfile:=${audio_default}}" 145 | } 146 | 147 | # video function 148 | video () { 149 | ffmpeg \ 150 | -hide_banner \ 151 | -stats -v panic \ 152 | -i "${infile}" \ 153 | -c:v copy \ 154 | -filter_complex \ 155 | "loudnorm=I=-16: 156 | dual_mono=true: 157 | TP=-1.5: 158 | LRA=11: 159 | measured_I=${measured_I}: 160 | measured_LRA=${measured_LRA}: 161 | measured_TP=${measured_TP}: 162 | measured_thresh=${measured_thresh}: 163 | offset=${offset}: 164 | linear=true: 165 | print_format=summary [audio]" \ 166 | -map 0:v -map "[audio]" \ 167 | -ar 44100 \ 168 | -pix_fmt yuv420p \ 169 | -movflags +faststart \ 170 | -f mp4 \ 171 | "${outfile:=${video_default}}" 172 | } 173 | 174 | # check if the mime type is audio or video then run the correct function 175 | case "${filetype}" in 176 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") video "${infile}";; 177 | "${wav_mime}"|"${audio_mime}"|"${aac_mime}"|"${m4a_mime}") audio "${infile}";; 178 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 179 | esac 180 | -------------------------------------------------------------------------------- /overlay-clip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # overlay-clip 5 | # overlay one video clip on top of another video clip 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe cut 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # overlay one video clip on top of another video clip 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] -o output.mp4 24 | -i input.(mp4|mkv|mov|m4v) : bottom video 25 | -v input.(mp4|mkv|mov|m4v) : overlay video 26 | -p [0-999] : time to overlay the video 27 | -o output.mp4 : optional argument # if option not provided defaults to input-name-overlay-date-time" 28 | exit 2 29 | } 30 | 31 | 32 | #=============================================================================== 33 | # error messages 34 | #=============================================================================== 35 | 36 | INVALID_OPT_ERR='Invalid option:' 37 | REQ_ARG_ERR='requires an argument' 38 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 39 | 40 | 41 | #=============================================================================== 42 | # check number of arguments passed to the script 43 | #=============================================================================== 44 | 45 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 46 | 47 | 48 | #=============================================================================== 49 | # getopts check the options passed to the script 50 | #=============================================================================== 51 | 52 | while getopts ':i:v:p:o:h' opt 53 | do 54 | case ${opt} in 55 | i) video="${OPTARG}";; 56 | v) overlay="${OPTARG}";; 57 | p) position="${OPTARG}";; 58 | o) outfile="${OPTARG}";; 59 | h) usage;; 60 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 61 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 62 | esac 63 | done 64 | shift $((OPTIND-1)) 65 | 66 | 67 | #=============================================================================== 68 | # variables 69 | #=============================================================================== 70 | 71 | # video name extension and overlay video extensions 72 | video_nopath="${video##*/}" 73 | video_name="${video_nopath%.*}" 74 | 75 | # defaults for variables if not defined 76 | outfile_default="${video_name}-overlay-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 77 | 78 | 79 | #=============================================================================== 80 | # functions 81 | #=============================================================================== 82 | 83 | # overlay video function 84 | overlay_video () { 85 | ffmpeg \ 86 | -hide_banner \ 87 | -stats -v panic \ 88 | -i "${video}" \ 89 | -i "${overlay}" \ 90 | -filter_complex \ 91 | "[0:0]setpts=PTS-STARTPTS[firstclip]; 92 | [1:0]setpts=PTS+${position}/TB[secondclip]; 93 | [firstclip][secondclip]overlay=enable='between(t\,${position},${dp})'[ov]" \ 94 | -map "[ov]" -map 0:1 \ 95 | -pix_fmt yuv420p \ 96 | -c:a copy -c:v libx264 -crf 18 \ 97 | -movflags +faststart \ 98 | -f mp4 \ 99 | "${outfile:=${outfile_default}}" 100 | } 101 | 102 | # get overlay videos duration with ffprobe 103 | duration="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${overlay}" | cut -d\. -f1)" 104 | 105 | # position + duration 106 | dp="$((position+duration))" 107 | 108 | # run the overlay_video function 109 | overlay_video "${video}" "${overlay}" 110 | -------------------------------------------------------------------------------- /overlay-pip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # overlay-pip 5 | # create a picture in picture video 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe cut bc 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # create a picture in picture 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -v input.(mp4|mkv|mov|m4v) -p [0-999] 24 | -m [00] -x (tl|tr|bl|br) -w [000] -f (0.1-9|1) -b [00] -c colour -o output.mp4 25 | 26 | -i input.(mp4|mkv|mov|m4v) : bottom video 27 | -v input.(mp4|mkv|mov|m4v) : overlay video 28 | -p [0-999] : time to overlay the video 29 | -m [00] : margin defaults to 0 30 | -x (tl|tr|bl|br) : pip position - defaults to tr 31 | -w [000] : width - defaults to 1/4 of video size 32 | -f (0.1-9|1) : fade from 0.1 to 1 - defaults to 0.2 33 | -b [00] : border 34 | -c colour : colour 35 | -o output.mp4 : optional argument # if option not provided defaults to input-name-pip-date-time" 36 | exit 2 37 | } 38 | 39 | 40 | #=============================================================================== 41 | # error messages 42 | #=============================================================================== 43 | 44 | INVALID_OPT_ERR='Invalid option:' 45 | REQ_ARG_ERR='requires an argument' 46 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 47 | 48 | 49 | #=============================================================================== 50 | # check the number of arguments passed to the script 51 | #=============================================================================== 52 | 53 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 54 | 55 | 56 | #=============================================================================== 57 | # getopts check the options passed to the script 58 | #=============================================================================== 59 | 60 | while getopts ':i:v:p:m:x:w:f:c:b:o:h' opt 61 | do 62 | case ${opt} in 63 | i) video="${OPTARG}";; 64 | v) overlay="${OPTARG}";; 65 | p) position="${OPTARG}";; 66 | m) margin="${OPTARG}";; 67 | x) pip="${OPTARG}" 68 | case "${pip}" in 69 | tl|tr|bl|br);; 70 | *) usage "${pip} ${INVALID_OPT_ERR}";; 71 | esac;; 72 | w) width="${OPTARG}";; 73 | f) fade="${OPTARG}";; 74 | c) colour="${OPTARG}";; 75 | b) border="${OPTARG}";; 76 | o) outfile="${OPTARG}";; 77 | h) usage;; 78 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 79 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 80 | esac 81 | done 82 | shift $((OPTIND-1)) 83 | 84 | 85 | #=============================================================================== 86 | # variables 87 | #=============================================================================== 88 | 89 | # video name extension and overlay video extensions 90 | video_nopath="${video##*/}" 91 | video_name="${video_nopath%.*}" 92 | 93 | # defaults for variables if not defined 94 | outfile_default="${video_name}-pip-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 95 | 96 | 97 | #=============================================================================== 98 | # functions 99 | #=============================================================================== 100 | 101 | # overlay video function 102 | overlay_video () { 103 | ffmpeg \ 104 | -hide_banner \ 105 | -stats -v panic \ 106 | -i "${video}" \ 107 | -i "${overlay}" \ 108 | -filter_complex \ 109 | "[0:0]setpts=PTS-STARTPTS[firstclip]; 110 | [1:0]setpts=PTS+${position}/TB[secondclip]; 111 | [firstclip][secondclip]overlay=enable='between(t\,${position},${dp})'[ov]; 112 | [0:0]scale=${width_scale:=${width_default}}${draw_border}[pip]; 113 | [pip]format=pix_fmts=yuva420p,fade=t=in:st=${position}:d=${fade:=${fade_default}}:alpha=1,fade=t=out:st=${fade_end}:d=${fade:=${fade_default}}:alpha=1[pipfade]; 114 | [ov][pipfade]overlay=${pip:=${pip_default}}:enable='between(t\,${position},${dp})'[pv]" \ 115 | -map "[pv]" -map 0:1 \ 116 | -pix_fmt yuv420p \ 117 | -c:a copy -c:v libx264 -crf 18 \ 118 | -movflags +faststart \ 119 | -f mp4 \ 120 | "${outfile:=${outfile_default}}" 121 | } 122 | 123 | # get overlay videos duration with ffprobe 124 | duration="$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${overlay}" | cut -d\. -f1)" 125 | 126 | # position + duration 127 | dp="$((position+duration))" 128 | 129 | # fade default 130 | fade_default='0.2' 131 | fade_end="$(echo "${dp}" - "${fade:=${fade_default}}" | bc)" 132 | 133 | # pip padding 134 | margin_default='20' 135 | 136 | # pip default position 137 | pip_default="${tr}" 138 | 139 | # pip position 140 | tl="${margin:=${margin_default}}:${margin:=${margin_default}}" 141 | tr="main_w-overlay_w-${margin:=${margin_default}}:${margin:=${margin_default}}" 142 | bl="${margin:=${margin_default}}:main_h-overlay_h-${margin:=${margin_default}}" 143 | br="main_w-overlay_w-${margin:=${margin_default}}:main_h-overlay_h-${margin:=${margin_default}}" 144 | 145 | # pip window size 146 | width_default='iw/4:ih/4' 147 | 148 | # check if width is null and unset 149 | if [ -z "${width}" ]; then 150 | : # width not set pass 151 | else 152 | width_scale="${width}:-1" 153 | fi 154 | 155 | # border 156 | # divide border by 2 for the offset 157 | border_default='4' 158 | offset_default='2' 159 | colour_default='#2f2f2f' 160 | offset="$((${border:=${border_default}}/2))" 161 | draw_border=",pad=w=${border:=${border_default}}+iw:h=${border:=${border_default}}+ih:x=${offset:=${offset_default}}:y=${offset:=${offset_default}}:color=${colour:=${colour_default}}" 162 | 163 | # dont show border if border set to 0 164 | if [ "${border}" = 0 ]; then 165 | draw_border='' 166 | fi 167 | 168 | # pip position case 169 | case "${pip}" in 170 | tl)pip="${tl}";; 171 | tr)pip="${tr}";; 172 | bl)pip="${bl}";; 173 | br)pip="${br}";; 174 | *) pip="${tr}";; 175 | esac 176 | 177 | # run the overlay_video function 178 | overlay_video "${video}" "${overlay}" 179 | -------------------------------------------------------------------------------- /pan-scan: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # pan-scan 5 | # pan scan over an image 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # pan scan image 22 | 23 | $(basename "$0") -i input.(png|jpg|jpeg) -d (000) -p (l|r|u|d) -o output.mp4 24 | -i = input.(png|jpg|jpeg) 25 | -d = duration : from 1-999 26 | -p = position : left, right, up, down 27 | -o = output.mp4 : optional argument # default is input-name-pan-date-time" 28 | exit 2 29 | } 30 | 31 | 32 | #=============================================================================== 33 | # error messages 34 | #=============================================================================== 35 | 36 | INVALID_OPT_ERR='Invalid option:' 37 | REQ_ARG_ERR='requires an argument' 38 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 39 | NOT_MEDIA_FILE_ERR='is not a media file' 40 | 41 | 42 | #=============================================================================== 43 | # check the number of arguments passed to the script 44 | #=============================================================================== 45 | 46 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 47 | 48 | 49 | #=============================================================================== 50 | # getopts check the options passed to the script 51 | #=============================================================================== 52 | 53 | while getopts ':i:d:p:o:h' opt 54 | do 55 | case ${opt} in 56 | i) infile="${OPTARG}";; 57 | d) dur="${OPTARG}";; 58 | p) position="${OPTARG}";; 59 | o) outfile="${OPTARG}";; 60 | h) usage;; 61 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 62 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 63 | esac 64 | done 65 | shift $((OPTIND-1)) 66 | 67 | 68 | #=============================================================================== 69 | # variables 70 | #=============================================================================== 71 | 72 | # input, input name and extension 73 | infile_nopath="${infile##*/}" 74 | infile_name="${infile_nopath%.*}" 75 | 76 | # check if tile is null 77 | if [ -z "${infile}" ]; then 78 | : # tile variable not set : = pass 79 | else 80 | # ffprobe get image height 81 | imgsize="$(ffprobe -v error -select_streams v:0 -show_entries stream=height,width -of csv=s=x:p=0 "${infile}")" 82 | img_w=$(echo "${imgsize}" | awk -F'x' '{print $1}') 83 | img_h=$(echo "${imgsize}" | awk -F'x' '{print $2}') 84 | fi 85 | 86 | # pan 87 | left="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.05:h=3*${img_h}/1.05:x=t*(in_w-out_w)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \ 88 | right="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.05:h=3*${img_h}/1.05:x=(in_w-out_w)-t*(in_w-out_w)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \ 89 | up="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.2:h=3*${img_h}/1.2:y=t*(in_h-out_h)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \ 90 | down="scale=w=-2:h=3*${img_h},crop=w=3*${img_w}/1.2:h=3*${img_h}/1.2:y=(in_h-out_h)-t*(in_h-out_h)/${dur},scale=w=${img_w}:h=${img_h},setsar=1" \ 91 | 92 | # output file recording destination 93 | outfile_default="${infile_name}-pan-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 94 | 95 | 96 | #=============================================================================== 97 | # functions 98 | #=============================================================================== 99 | 100 | # zoom function 101 | pan_func () { 102 | ffmpeg \ 103 | -hide_banner \ 104 | -stats -v panic \ 105 | -r 30 \ 106 | -y -loop 1 \ 107 | -i "${infile}" -ss 0 -t "${dur}" \ 108 | -filter_complex \ 109 | "${1}" \ 110 | -y -shortest \ 111 | -c:v libx264 -crf 18 -profile:v high \ 112 | -r 30 -pix_fmt yuv420p \ 113 | -movflags +faststart -f mp4 \ 114 | "${outfile:=${outfile_default}}" 115 | } 116 | 117 | # check zoom and position 118 | case "${position}" in 119 | l) pan_func "${left}";; 120 | r) pan_func "${right}" "${infile}";; 121 | u) pan_func "${up}" "${infile}";; 122 | d) pan_func "${down}" "${infile}";; 123 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 124 | esac 125 | -------------------------------------------------------------------------------- /scene-cut: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # scene-cut 5 | # ffmpeg scene cut 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | $(basename "$0") -i input -c cutfile 21 | 22 | -i input.(mp4|mov|mkv|m4v) 23 | -c cutfile" 24 | exit 2 25 | } 26 | 27 | 28 | #=============================================================================== 29 | # error messages 30 | #=============================================================================== 31 | 32 | INVALID_OPT_ERR='Invalid option:' 33 | REQ_ARG_ERR='requires an argument' 34 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 35 | 36 | #=============================================================================== 37 | # check the number of arguments passed to the script 38 | #=============================================================================== 39 | 40 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 41 | 42 | 43 | #=============================================================================== 44 | # getopts check the options passed to the script 45 | #=============================================================================== 46 | 47 | while getopts ':i:c:h' opt 48 | do 49 | case ${opt} in 50 | i) input="${OPTARG}";; 51 | c) cutfile="${OPTARG}";; 52 | h) usage;; 53 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 54 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 55 | esac 56 | done 57 | shift $((OPTIND-1)) 58 | 59 | 60 | #=============================================================================== 61 | # variables 62 | #=============================================================================== 63 | 64 | # get the input file name 65 | input_nopath="${input##*/}" 66 | input_name="${input_nopath%.*}" 67 | 68 | #=============================================================================== 69 | # ffmpeg create clips - nostdin needed to avoid clash with read command 70 | #=============================================================================== 71 | 72 | trim_video () { 73 | # awk convert start and end time from sexagesimal time to seconds 74 | # add the start and end time to get the endpoint and convert from seconds back to sexagesimal time 75 | endtime=$(printf "%s %s\n" "${start}" "${duration}" \ 76 | | awk ' 77 | { 78 | start = $1 79 | end = $2 80 | if (start ~ /:/) { 81 | split(start, t, ":") 82 | sseconds = (t[1] * 3600) + (t[2] * 60) + t[3] 83 | } 84 | if (end ~ /:/) { 85 | split(end, t, ":") 86 | eseconds = (t[1] * 3600) + (t[2] * 60) + t[3] 87 | } 88 | duration = eseconds + sseconds 89 | printf("%s\n"), duration 90 | }' \ 91 | | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\ 92 | NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' 93 | ) 94 | output="${input_name}-[${start}-${endtime}].mp4" 95 | ffmpeg \ 96 | -nostdin \ 97 | -hide_banner \ 98 | -stats -v panic \ 99 | -ss "${start}" \ 100 | -i "${input}" \ 101 | -t "${duration}" \ 102 | -c:a aac \ 103 | -c:v libx264 -profile:v high \ 104 | -pix_fmt yuv420p -movflags +faststart \ 105 | -f mp4 \ 106 | "${output}" 107 | } 108 | 109 | 110 | #=============================================================================== 111 | # read file and set IFS=, read = input before , duration = input after , 112 | #=============================================================================== 113 | 114 | count=1 115 | while IFS=, read -r start duration; do 116 | trim_video 117 | done < "${cutfile}" 118 | -------------------------------------------------------------------------------- /scene-detect: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # scene-detect 5 | # ffmpeg scene detection 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe awk 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | $(basename "$0") -s 00:00:00 -i input -e 00:00:00 -t (0.1 - 0.9) -f sec -o output 21 | 22 | -s 00:00:00 : start time 23 | -i input.(mp4|mov|mkv|m4v) 24 | -e 00:00:00 : end time 25 | -t (0.1 - 0.9) # threshold 26 | -f sec # output in seconds 27 | -o output.txt" 28 | exit 2 29 | } 30 | 31 | 32 | #=============================================================================== 33 | # error messages 34 | #=============================================================================== 35 | 36 | INVALID_OPT_ERR='Invalid option:' 37 | REQ_ARG_ERR='requires an argument' 38 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 39 | NOT_MEDIA_FILE_ERR='is not a media file' 40 | 41 | 42 | #=============================================================================== 43 | # check the number of arguments passed to the script 44 | #=============================================================================== 45 | 46 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 47 | 48 | 49 | #=============================================================================== 50 | # getopts check the options passed to the script 51 | #=============================================================================== 52 | 53 | while getopts ':s:i:e:t:o:f:h' opt 54 | do 55 | case ${opt} in 56 | s) start="${OPTARG}";; 57 | i) input="${OPTARG}";; 58 | e) end="${OPTARG}";; 59 | t) threshold="${OPTARG}";; 60 | o) output="${OPTARG}";; 61 | f) format="${OPTARG}";; 62 | h) usage;; 63 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 64 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 65 | esac 66 | done 67 | shift $((OPTIND-1)) 68 | 69 | 70 | #=============================================================================== 71 | # variables 72 | #=============================================================================== 73 | 74 | # get the input file name 75 | input_nopath="${input##*/}" 76 | input_name="${input_nopath%.*}" 77 | 78 | # output file name 79 | output_default="${input_name}-detection-$(date +"%Y-%m-%d-%H-%M-%S").txt" 80 | 81 | # threshold default 82 | threshold_default='0.3' 83 | 84 | # start default 85 | start_default='0.0' 86 | 87 | 88 | #=============================================================================== 89 | # ffprobe video duration 90 | #=============================================================================== 91 | 92 | ffduration () { 93 | # video duration to append to cutfile 94 | duration_default=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${input}") 95 | 96 | # if video duration is empty exit 97 | [ -n "${duration_default}" ] || usage "${input} ${NOT_MEDIA_FILE_ERR}" 98 | } 99 | 100 | 101 | #=============================================================================== 102 | # convert time input to seconds 103 | #=============================================================================== 104 | 105 | checktime () { 106 | start=$(echo "${start}" | awk -F: 'NF==3 { print ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { print 0 + $1 }') 107 | end=$(echo "${end}" | awk -F: 'NF==3 { print ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { print 0 + $1 }') 108 | } 109 | 110 | 111 | #=============================================================================== 112 | # ffmpeg scene detection 113 | #=============================================================================== 114 | 115 | # scene detection 116 | ffdetection () { 117 | detection="$(ffmpeg -hide_banner -i "${input}" -filter_complex "select='gt(scene,"${threshold:=${threshold_default}}")',metadata=print:file=-" -f null -)" 118 | } 119 | 120 | # scene detection range 121 | ffdetection_range () { 122 | detection="$(ffmpeg -hide_banner -i "${input}" \ 123 | -filter_complex "[0:0]select='between(t\,"${start}"\,"${end}")'[time];\ 124 | [time]select='gt(scene,"${threshold:=${threshold_default}}")',metadata=print:file=-[out]" -map "[out]" -f null -)" 125 | } 126 | 127 | 128 | #=============================================================================== 129 | # create cutfile - prepend start and append end or duration 130 | #=============================================================================== 131 | 132 | cutfile_seconds () { 133 | echo "${detection}" \ 134 | | awk -F':' 'BEGIN { printf("%s\n", '"${start:=${start_default}}"') }/pts_time/ { printf("%s\n", $4) } END { printf("%s\n", '"${end:=${duration_default}}"') }' > "${output:=${output_default}}" 135 | } 136 | 137 | cutfile_minutes () { 138 | echo "${detection}" \ 139 | | awk -F':' 'BEGIN { printf("%s\n", '"${start:=${start_default}}"') }/pts_time/ { printf("%s\n", $4) } END { printf("%s\n", '"${end:=${duration_default}}"') }' | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\ 140 | NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' \ 141 | > "${output:=${output_default}}" 142 | } 143 | 144 | 145 | #=============================================================================== 146 | # run function 147 | #=============================================================================== 148 | 149 | if [ -n "${start}" ] && [ -n "${end}" ]; then 150 | # scene detect range in video 151 | checktime 152 | ffdetection_range 153 | if [ -n "${format}" ]; then 154 | cutfile_seconds 155 | else 156 | cutfile_minutes 157 | fi 158 | elif [ -n "${start}" ]; then 159 | usage "${start} ${WRONG_ARGS_ERR}" 160 | elif [ -n "${end}" ]; then 161 | usage "${end} ${WRONG_ARGS_ERR}" 162 | else 163 | # scene detect entire video 164 | ffduration 165 | ffdetection 166 | if [ -n "${format}" ]; then 167 | cutfile_seconds 168 | else 169 | cutfile_minutes 170 | fi 171 | fi 172 | -------------------------------------------------------------------------------- /scene-images: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # scene-images 5 | # ffmpeg scene thumbnails 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | $(basename "$0") -i input -c cutfile -t (png|jpg) -x width -y height 21 | 22 | -i input.(mp4|mov|mkv|m4v) 23 | -c cutfile 24 | -t (png|jpg) : optional argument # if option not provided defaults to png 25 | -x width : optional argument # 26 | -y height : optional argument #" 27 | exit 2 28 | } 29 | 30 | 31 | #=============================================================================== 32 | # error messages 33 | #=============================================================================== 34 | 35 | INVALID_OPT_ERR='Invalid option:' 36 | REQ_ARG_ERR='requires an argument' 37 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 38 | 39 | 40 | #=============================================================================== 41 | # check the number of arguments passed to the script 42 | #=============================================================================== 43 | 44 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 45 | 46 | 47 | #=============================================================================== 48 | # getopts check the options passed to the script 49 | #=============================================================================== 50 | 51 | while getopts ':i:c:t:x:y:h' opt 52 | do 53 | case ${opt} in 54 | i) input="${OPTARG}";; 55 | c) cutfile="${OPTARG}";; 56 | t) image="${OPTARG}";; 57 | x) width="${OPTARG}";; 58 | y) height="${OPTARG}";; 59 | h) usage;; 60 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 61 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 62 | esac 63 | done 64 | shift $((OPTIND-1)) 65 | 66 | 67 | #=============================================================================== 68 | # variables 69 | #=============================================================================== 70 | 71 | # get the input file name 72 | input_nopath="${input##*/}" 73 | input_name="${input_nopath%.*}" 74 | 75 | # image default 76 | image_default="png" 77 | 78 | 79 | #=============================================================================== 80 | # ffmpeg extract image 81 | #=============================================================================== 82 | 83 | 84 | # image function 85 | extract () { 86 | output="${input_name}-[${seconds}].${image:=${image_default}}" 87 | ffmpeg \ 88 | -nostdin \ 89 | -hide_banner \ 90 | -stats -v panic \ 91 | -ss "${seconds}" \ 92 | -i "${input}" \ 93 | -q:v 2 -f image2 \ 94 | -vframes 1 \ 95 | "${output}" 96 | } 97 | 98 | 99 | # image and scale function 100 | extract_scale () { 101 | output="${input_name}-[${seconds}].${image:=${image_default}}" 102 | ffmpeg \ 103 | -nostdin \ 104 | -hide_banner \ 105 | -stats -v panic \ 106 | -ss "${seconds}" \ 107 | -i "${input}" \ 108 | -q:v 2 -f image2 \ 109 | -vframes 1 \ 110 | -vf scale="${width:=-1}:${height:=-1}" \ 111 | "${output}" 112 | } 113 | 114 | 115 | #=============================================================================== 116 | # read file and run ffmpeg 117 | #=============================================================================== 118 | 119 | 120 | if [ -n "${width}" ] || [ -n "${height}" ]; then 121 | count=1 122 | while IFS= read -r seconds; do 123 | extract_scale 124 | done < "${cutfile}" 125 | else 126 | count=1 127 | while IFS= read -r seconds; do 128 | extract 129 | done < "${cutfile}" 130 | fi 131 | 132 | -------------------------------------------------------------------------------- /scene-time: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # scene-time 5 | # create ffmpeg cutlist 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg awk head grep 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | $(basename "$0") -i input -o output 21 | 22 | -i input 23 | -o output" 24 | exit 2 25 | } 26 | 27 | 28 | #=============================================================================== 29 | # error messages 30 | #=============================================================================== 31 | 32 | INVALID_OPT_ERR='Invalid option:' 33 | REQ_ARG_ERR='requires an argument' 34 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 35 | 36 | 37 | #=============================================================================== 38 | # check the number of arguments passed to the script 39 | #=============================================================================== 40 | 41 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 42 | 43 | 44 | #=============================================================================== 45 | # getopts check the options passed to the script 46 | #=============================================================================== 47 | 48 | while getopts ':i:o:h' opt 49 | do 50 | case ${opt} in 51 | i) input="${OPTARG}";; 52 | h) usage;; 53 | o) output="${OPTARG}";; 54 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 55 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 56 | esac 57 | done 58 | shift $((OPTIND-1)) 59 | 60 | 61 | #=============================================================================== 62 | # variables 63 | #=============================================================================== 64 | 65 | # get the input file name 66 | input_nopath="${input##*/}" 67 | input_name="${input_nopath%.*}" 68 | 69 | # output file name 70 | output_default="${input_name}-cutlist-$(date +"%Y-%m-%d-%H-%M-%S").txt" 71 | 72 | 73 | #=============================================================================== 74 | # awk subtract 2nd line from first line - print start and duration 75 | #=============================================================================== 76 | 77 | seconds () { 78 | awk -F. 'NR > 1 {printf("%s%s%s\n"), prev, ",", $0-prev}; {prev = $0}' < "${input}" > "${output:=${output_default}}" 79 | } 80 | 81 | #=============================================================================== 82 | # convert sexagesimal to seconds - and subtract 2nd line from first line 83 | # convert from seconds back to sexagesimal 84 | #=============================================================================== 85 | 86 | minutes () { 87 | awk -F: 'NF==3 { printf("%s\n"), ($1 * 3600) + ($2 * 60) + $3 } NF==2 { print ($1 * 60) + $2 } NF==1 { printf("$s\n"), 0 + $1 }' < "${input}" \ 88 | | awk 'NR > 1 {printf("%s%s%s\n"), prev, "\n", $0-prev}; {prev = $0}' \ 89 | | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\ 90 | NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' \ 91 | | awk '{ORS = NR%2 ? "," : "\n"} 1' > "${output:=${output_default}}" 92 | } 93 | 94 | 95 | #=============================================================================== 96 | # timecode regex 97 | #=============================================================================== 98 | 99 | # grab the first line of the file 100 | check=$(head -n1 "${input}") 101 | 102 | # minutes - match 00:00:00.000 103 | minutes_regex='^[0-9]{1,2}:[0-9]{2}:[0-9]{2}([.]{1}[0-9]{1,3})?$' 104 | 105 | # seconds - match 00:00:00.000 106 | seconds_regex='^[0-9]{1,8}([.]{1}[0-9]{1,3})?$' 107 | 108 | # grep for the minutes 109 | minutes=$(echo "${check}" | grep -E "${minutes_regex}") 110 | 111 | # grep for the seconds 112 | seconds=$(echo "${check}" | grep -E "${seconds_regex}") 113 | 114 | 115 | #=============================================================================== 116 | # check timecode and run function 117 | #=============================================================================== 118 | 119 | if [ -n "${minutes}" ]; then 120 | minutes 121 | elif [ -n "${seconds}" ]; then 122 | seconds 123 | else 124 | usage 125 | fi 126 | -------------------------------------------------------------------------------- /scopes: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # scopes 5 | # ffplay video scopes 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffplay 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | [ -z "${1}" ] || echo "! ${1}" 18 | echo "\ 19 | # ffplay video scopes 20 | 21 | $(basename "$0") -i input = histogram 22 | $(basename "$0") -o input = rgb overlay 23 | $(basename "$0") -p input = rgb parade 24 | $(basename "$0") -s input = rgb overlay and parade 25 | $(basename "$0") -w input = waveform 26 | $(basename "$0") -v input = vector scope 27 | $(basename "$0") -h = help 28 | " 29 | exit 2 30 | } 31 | 32 | 33 | #=============================================================================== 34 | # error messages 35 | #=============================================================================== 36 | 37 | INVALID_OPT_ERR='Invalid option:' 38 | REQ_ARG_ERR='requires an argument' 39 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 40 | 41 | 42 | #=============================================================================== 43 | # check the number of arguments passed to the script 44 | #=============================================================================== 45 | 46 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 47 | 48 | 49 | #=============================================================================== 50 | # functions 51 | #=============================================================================== 52 | 53 | # histogram overlay 54 | histogram_overlay () { 55 | ffplay "${infile}" \ 56 | -hide_banner \ 57 | -stats -v panic \ 58 | -vf \ 59 | "split=2[a][b], 60 | [b]histogram, 61 | format=yuva444p[hh], 62 | [a][hh]overlay=x=W-w:y=H-h" 63 | } 64 | 65 | 66 | # rgb overlay 67 | rgb_overlay () { 68 | ffplay "${infile}" \ 69 | -hide_banner \ 70 | -stats -v panic \ 71 | -vf \ 72 | "format=gbrp, 73 | split=2[a][b], 74 | [b]waveform=g=green: 75 | s=ire: 76 | fl=numbers: 77 | filter=lowpass: 78 | components=7: 79 | display=overlay[bb], 80 | [a][bb]vstack" 81 | } 82 | 83 | 84 | # rgb parade 85 | rgb_parade () { 86 | ffplay "${infile}" \ 87 | -hide_banner \ 88 | -stats -v panic \ 89 | -vf \ 90 | "format=gbrp, 91 | split=2[a][b], 92 | [b]waveform=g=green: 93 | s=ire: 94 | fl=numbers: 95 | filter=lowpass: 96 | components=7[bb], 97 | [a][bb]vstack" 98 | } 99 | 100 | 101 | # rgb overlay and parade stacked 102 | rgb_stacked () { 103 | ffplay "${infile}" \ 104 | -hide_banner \ 105 | -stats -v panic \ 106 | -vf \ 107 | "format=gbrp, 108 | split=3[a][b][c], 109 | [b]waveform=g=green: 110 | s=ire: 111 | fl=numbers: 112 | filter=lowpass: 113 | components=7: 114 | display=overlay[bb], 115 | [c]waveform=g=green: 116 | s=ire: 117 | fl=numbers: 118 | filter=lowpass: 119 | components=7[cc], 120 | [bb][cc]vstack[d], 121 | [a][d]vstack" 122 | } 123 | 124 | 125 | # waveform lowpass 126 | waveform_lowpass () { 127 | ffplay "${infile}" \ 128 | -hide_banner \ 129 | -stats -v panic \ 130 | -vf \ 131 | "split=2[a][b], 132 | [b]waveform=f=lowpass: 133 | s=ire: 134 | g=green: 135 | e=instant[bb], 136 | [a][bb]vstack" 137 | } 138 | 139 | 140 | # vectorscope 141 | vectorscope () { 142 | ffplay "${infile}" \ 143 | -hide_banner \ 144 | -stats -v panic \ 145 | -vf \ 146 | "format=yuva444p9, 147 | split=2[m][v], 148 | [v]vectorscope=b=0.7: 149 | m=color4: 150 | e=peak+instant: 151 | f=name: 152 | g=color[v], 153 | [m][v]overlay=x=W-w:y=H-h" 154 | } 155 | 156 | 157 | #=============================================================================== 158 | # getopts check the options passed to the script 159 | #=============================================================================== 160 | 161 | while getopts ':i:o:p:s:w:v:h' opt 162 | do 163 | case ${opt} in 164 | i) infile="${OPTARG}" 165 | histogram_overlay;; 166 | o) infile="${OPTARG}" 167 | rgb_overlay;; 168 | p) infile="${OPTARG}" 169 | rgb_parade;; 170 | s) infile="${OPTARG}" 171 | rgb_stacked;; 172 | w) infile="${OPTARG}" 173 | waveform_lowpass;; 174 | v) infile="${OPTARG}" 175 | vectorscope;; 176 | h) usage;; 177 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 178 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 179 | esac 180 | done 181 | shift $((OPTIND-1)) 182 | -------------------------------------------------------------------------------- /sexagesimal-time: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # sexagesimal-time 5 | # calculate sexagesimal duration by subtacting end time from start time 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # awk 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | calculate sexagesimal duration by subtacting end time from start time 21 | $(basename "$0") -s 00:00:00 -e 00:00:00" 22 | exit 2 23 | } 24 | 25 | 26 | #=============================================================================== 27 | # error messages 28 | #=============================================================================== 29 | 30 | INVALID_OPT_ERR='Invalid option:' 31 | REQ_ARG_ERR='requires an argument' 32 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 33 | 34 | 35 | #=============================================================================== 36 | # check the number of arguments passed to the script 37 | #=============================================================================== 38 | 39 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 40 | 41 | 42 | #=============================================================================== 43 | # getopts check the options passed to the script 44 | #=============================================================================== 45 | 46 | while getopts ':s:e:h' opt 47 | do 48 | case ${opt} in 49 | s) start="${OPTARG}";; 50 | h) usage;; 51 | e) end="${OPTARG}";; 52 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 53 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 54 | esac 55 | done 56 | shift $((OPTIND-1)) 57 | 58 | 59 | #=============================================================================== 60 | # function 61 | #=============================================================================== 62 | 63 | sexagesimal () { 64 | printf "%s %s\n" "${start}" "${end}" \ 65 | | awk ' 66 | { 67 | start = $1 68 | end = $2 69 | if (start ~ /:/) { 70 | split(start, t, ":") 71 | sseconds = (t[1] * 3600) + (t[2] * 60) + t[3] 72 | } 73 | if (end ~ /:/) { 74 | split(end, t, ":") 75 | eseconds = (t[1] * 3600) + (t[2] * 60) + t[3] 76 | } 77 | duration = eseconds - sseconds 78 | printf("%s\n"), duration 79 | }' \ 80 | | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\ 81 | NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' 82 | } 83 | 84 | 85 | #=============================================================================== 86 | # run function 87 | #=============================================================================== 88 | 89 | sexagesimal 90 | -------------------------------------------------------------------------------- /subtitle-add: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # subtitle-add 5 | # add subtitles to a video 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # add subtitles to a video 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v) -s subtitle.(srt|vtt) -o output.mp4 24 | -i input.(mp4|mkv|mov|m4v) 25 | -s subtitle.(srt|vtt) 26 | -o output.mp4 : optional argument # if option not provided defaults to input-name-subs-date-time" 27 | exit 2 28 | } 29 | 30 | 31 | #=============================================================================== 32 | # error messages 33 | #=============================================================================== 34 | 35 | INVALID_OPT_ERR='Invalid option:' 36 | REQ_ARG_ERR='requires an argument' 37 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 38 | 39 | 40 | #=============================================================================== 41 | # check the number of arguments passed to the script 42 | #=============================================================================== 43 | 44 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 45 | 46 | 47 | #=============================================================================== 48 | # getopts check the options passed to the script 49 | #=============================================================================== 50 | 51 | while getopts ':i:s:o:h' opt 52 | do 53 | case ${opt} in 54 | i) video="${OPTARG}";; 55 | s) subs="${OPTARG}";; 56 | o) outfile="${OPTARG}";; 57 | h) usage;; 58 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 59 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 60 | esac 61 | done 62 | shift $((OPTIND-1)) 63 | 64 | 65 | #=============================================================================== 66 | # variables 67 | #=============================================================================== 68 | 69 | # input, input name and extension 70 | video_nopath="${video##*/}" 71 | video_name="${video_nopath%.*}" 72 | 73 | # defaults for variables if not defined 74 | outfile_default="${video_name}-subs-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 75 | 76 | 77 | #=============================================================================== 78 | # functions 79 | #=============================================================================== 80 | 81 | # audio is aac, copy audio stream 82 | addsubs () { 83 | ffmpeg \ 84 | -hide_banner \ 85 | -stats -v panic \ 86 | -i "${video}" \ 87 | -f srt \ 88 | -i "${subs}" \ 89 | -c:a copy \ 90 | -c:v copy \ 91 | -c:s mov_text -metadata:s:s:0 \ 92 | language=eng \ 93 | -movflags +faststart \ 94 | -f mp4 \ 95 | "${outfile:=${outfile_default}}" 96 | } 97 | 98 | # run the addsubs function 99 | addsubs "$video" "$subs" 100 | -------------------------------------------------------------------------------- /tile-thumbnails: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # tile-thumbnails 5 | # create an image with thumbnails from a video 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe awk bc 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | echo "\ 18 | # create an image with thumbnails from a video 19 | 20 | $(basename "$0") -i input -s 00:00:00.000 -w 000 -t 0x0 -p 00 -m 00 -c color -f fontcolor -b boxcolor -x on -o output.png 21 | 22 | -i input.(mp4|mkv|mov|m4v|webm) 23 | -s seek into the video file : default 00:00:05 24 | -w thumbnail width : 160 25 | -t tile layout format width x height : 4x3 : default 4x3 26 | -p padding between images : default 7 27 | -m margin : default 2 28 | -c color = https://ffmpeg.org/ffmpeg-utils.html#color-syntax : default black 29 | -f fontcolor : default white 30 | -b boxcolor : default black 31 | -x on : default off, display timestamps 32 | -o output.png : optional argument 33 | # if option not provided defaults to input-name-tile-date-time.png" 34 | exit 2 35 | } 36 | 37 | 38 | #=============================================================================== 39 | # error messages 40 | #=============================================================================== 41 | 42 | INVALID_OPT_ERR='Invalid option:' 43 | REQ_ARG_ERR='requires an argument' 44 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 45 | 46 | 47 | #=============================================================================== 48 | # check the number of arguments passed to the script 49 | #=============================================================================== 50 | 51 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 52 | 53 | 54 | #=============================================================================== 55 | # getopts check the options passed to the script 56 | #=============================================================================== 57 | 58 | while getopts ':i:s:w:t:p:m:c:b:f:x:o:h' opt 59 | do 60 | case ${opt} in 61 | i) infile="${OPTARG}";; 62 | s) seek="${OPTARG}";; 63 | w) scale="${OPTARG}";; 64 | t) tile="${OPTARG}";; 65 | p) padding="${OPTARG}";; 66 | m) margin="${OPTARG}";; 67 | c) color="${OPTARG}";; 68 | f) fontcolor="${OPTARG}";; 69 | b) boxcolor="${OPTARG}";; 70 | x) timestamp="${OPTARG}";; 71 | o) outfile="${OPTARG}";; 72 | h) usage;; 73 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 74 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 75 | esac 76 | done 77 | shift $((OPTIND-1)) 78 | 79 | 80 | #=============================================================================== 81 | # variables 82 | #=============================================================================== 83 | 84 | # input, input name 85 | infile_nopath="${infile##*/}" 86 | infile_name="${infile_nopath%.*}" 87 | 88 | # ffprobe get fps and duration 89 | videostats=$(ffprobe \ 90 | -v error \ 91 | -select_streams v:0 \ 92 | -show_entries stream=r_frame_rate:format=duration \ 93 | -of default=noprint_wrappers=1 \ 94 | "${infile}") 95 | 96 | # fps 97 | fps=$(echo "${videostats}" | awk -F'[=//]' '/r_frame_rate/{print $2/$3}') 98 | 99 | # duration 100 | duration=$(echo "${videostats}" | awk -F'[=/.]' '/duration/{print $2}') 101 | 102 | # check if tile is null 103 | if [ -z "${tile}" ]; then 104 | : # tile variable not set : = pass 105 | else 106 | # tile variable set 107 | # tile layout 108 | tile_w=$(echo "${tile}" | awk -F'x' '{print $1}') 109 | tile_h=$(echo "${tile}" | awk -F'x' '{print $2}') 110 | # title sum 111 | tile_sum=$(echo "${tile_w} * ${tile_h}" | bc) 112 | fi 113 | 114 | # defaults 115 | seek_default='00:00:05' 116 | scale_default='160' 117 | tile_layout_default='4x3' 118 | tile_default='12' 119 | padding_default='7' 120 | margin_default='2' 121 | color_default='black' 122 | fontcolor_default='white' 123 | boxcolor_default='black' 124 | timestamp_default='off' 125 | pts_default='5' 126 | pts=$(printf "%s %s\n" "${seek}" | awk '{ 127 | start = $1 128 | if (start ~ /:/) { 129 | split(start, t, ":") 130 | seconds = (t[1] * 3600) + (t[2] * 60) + t[3] 131 | } 132 | printf("%s\n"), seconds 133 | }') 134 | 135 | outfile_default="${infile_name}-tile-$(date +"%Y-%m-%d-%H-%M-%S").png" 136 | 137 | # duration * fps / number of tiles 138 | frames=$(echo "${duration} * ${fps} / ${tile_sum:=${tile_default}}" | bc) 139 | 140 | 141 | #=============================================================================== 142 | # functions 143 | #=============================================================================== 144 | 145 | # contact sheet - no timestamps 146 | tilevideo () { 147 | ffmpeg \ 148 | -hide_banner \ 149 | -stats -v panic \ 150 | -ss "${seek:=${seek_default}}" \ 151 | -i "${infile}" \ 152 | -frames 1 -vf "select=not(mod(n\,${frames})),scale=${scale:=${scale_default}}:-1,tile=${tile:=${tile_layout_default}}:padding=${padding:=${padding_default}}:margin=${margin:=${margin_default}}:color=${color:=${color_default}}" \ 153 | "${outfile:=${outfile_default}}" 154 | } 155 | 156 | 157 | # contact sheet - timestamps 158 | timestamp () { 159 | ffmpeg \ 160 | -hide_banner \ 161 | -stats -v panic \ 162 | -ss "${seek:=${seek_default}}" \ 163 | -i "${infile}" \ 164 | -frames 1 -vf "drawtext=text='%{pts\:hms\:${pts:=${pts_default}}}':x='(main_w-text_w)/2':y='(main_h-text_h)':fontcolor=${fontcolor:=${fontcolor_default}}:fontsize='(main_h/8)':boxcolor=${boxcolor:=${boxcolor_default}}:box=1,select=not(mod(n\,${frames})),scale=${scale:=${scale_default}}:-1,tile=${tile:=${tile_layout_default}}:padding=${padding:=${padding_default}}:margin=${margin:=${margin_default}}:color=${color:=${color_default}}" \ 165 | "${outfile:=${outfile_default}}" 166 | } 167 | 168 | 169 | #=============================================================================== 170 | # check option passed to script 171 | #=============================================================================== 172 | 173 | if [ "${timestamp}" == on ]; then 174 | timestamp "${infile}" # -x on 175 | elif [ ! -z "${fontcolor}" ]; then 176 | timestamp "${infile}" # -f 177 | elif [ ! -z "${boxcolor}" ]; then 178 | timestamp "${infile}" # -b 179 | elif [ -z "${timestamp}" ]; then 180 | tilevideo "${infile}" # no timestamp 181 | else 182 | tilevideo "${infile}" # no timestamp 183 | fi 184 | -------------------------------------------------------------------------------- /trim-clip: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # trim-clip 5 | # trim video or audio clips with millisecond accuracy 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg file 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | # trim video or audio clips with millisecond accuracy 21 | https://trac.ffmpeg.org/wiki/Seeking 22 | 23 | $(basename "$0") -s 00:00:00.000 -i input -t 00:00:00.000 -o output) 24 | -s 00:00:00.000 : start time 25 | -i input.(mp4|mov|mkv|m4v|webm|aac|m4a|wav|mp3|ogg) 26 | -t 00:00:00.000 : number of seconds after start time 27 | -o output.(mp4|webm|aac|mp3|wav|ogg) : optional argument 28 | # if option not provided defaults input-name-[start end].(mp4|webm|aac|mp3|wav|ogg)" 29 | exit 2 30 | } 31 | 32 | 33 | #=============================================================================== 34 | # error messages 35 | #=============================================================================== 36 | 37 | INVALID_OPT_ERR='Invalid option:' 38 | REQ_ARG_ERR='requires an argument' 39 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 40 | NOT_MEDIA_FILE_ERR='is not a media file' 41 | 42 | 43 | #=============================================================================== 44 | # check the number of arguments passed to the script 45 | #=============================================================================== 46 | 47 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 48 | 49 | 50 | #=============================================================================== 51 | # getopts check the options passed to the script 52 | #=============================================================================== 53 | 54 | while getopts ':s:i:t:o:h' opt 55 | do 56 | case ${opt} in 57 | s) start="${OPTARG}";; 58 | i) infile="${OPTARG}";; 59 | t) end="${OPTARG}";; 60 | h) usage;; 61 | o) outfile="${OPTARG}";; 62 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 63 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 64 | esac 65 | done 66 | shift $((OPTIND-1)) 67 | 68 | 69 | #=============================================================================== 70 | # variables 71 | #=============================================================================== 72 | 73 | # input name 74 | infile_nopath="${infile##*/}" 75 | infile_name="${infile_nopath%.*}" 76 | 77 | # input file extension 78 | infile_ext="${infile##*.}" 79 | 80 | # file command check input file mime type 81 | filetype="$(file --mime-type -b "${infile}")" 82 | 83 | # video mimetypes 84 | mov_mime='video/quicktime' 85 | mkv_mime='video/x-matroska' 86 | mp4_mime='video/mp4' 87 | webm_mime='video/webm' 88 | m4v_mime='video/x-m4v' 89 | wav_mime='audio/x-wav' 90 | audio_mime='audio/mpeg' 91 | aac_mime='audio/x-hx-aac-adts' 92 | m4a_mime='audio/mp4' 93 | ogg_mime='audio/ogg' 94 | 95 | # the file command wrongly identifies .m4a audio as a video file 96 | # so we check if the file extension is .m4a and set the mime type to audio/mp4 97 | if [ "${infile_ext}" = 'm4a' ]; then 98 | filetype="${m4a_mime}" 99 | fi 100 | 101 | # awk convert start and end time from sexagesimal time to seconds 102 | # add the start and end time to get the endpoint and convert from seconds back to sexagesimal time 103 | endtime=$(printf "%s %s\n" "${start}" "${end}" \ 104 | | awk ' 105 | { 106 | start = $1 107 | end = $2 108 | if (start ~ /:/) { 109 | split(start, t, ":") 110 | sseconds = (t[1] * 3600) + (t[2] * 60) + t[3] 111 | } 112 | if (end ~ /:/) { 113 | split(end, t, ":") 114 | eseconds = (t[1] * 3600) + (t[2] * 60) + t[3] 115 | } 116 | duration = eseconds + sseconds 117 | printf("%s\n"), duration 118 | }' \ 119 | | awk -F. 'NF==1 { printf("%02d:%02d:%02d\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60) }\ 120 | NF==2 { printf("%02d:%02d:%02d.%s\n"), ($1 / 3600), ($1 % 3600 / 60), ($1 % 60), ($2) }' 121 | ) 122 | 123 | # defaults for variables if not defined 124 | videofile_default="${infile_name}-[${start}-${endtime}].mp4" 125 | webm_default="${infile_name}-[${start}-${endtime}].webm" 126 | aac_default="${infile_name}-[${start}-${endtime}].aac" 127 | mp3_default="${infile_name}-[${start}-${endtime}].mp3" 128 | wav_default="${infile_name}-[${start}-${endtime}].wav" 129 | m4a_default="${infile_name}-[${start}-${endtime}].m4a" 130 | ogg_default="${infile_name}-[${start}-${endtime}].ogg" 131 | 132 | 133 | #=============================================================================== 134 | # check if the libfdk_aac codec is installed, if not fall back to the aac codec 135 | #=============================================================================== 136 | 137 | aac_codec="$(ffmpeg -hide_banner -stats -v panic -h encoder=libfdk_aac)" 138 | aac_error="Codec 'libfdk_aac' is not recognized by FFmpeg." 139 | aac_check="$(echo "${aac_codec}" | grep "${aac_error}")" 140 | 141 | # check ffmpeg aac codecs 142 | if [ -z "${aac_check}" ]; then 143 | aac='libfdk_aac' # libfdk_aac codec is installed 144 | else 145 | aac='aac' # libfdk_aac codec isnt installed, fall back to aac codec 146 | fi 147 | 148 | 149 | #=============================================================================== 150 | # audio and video functions 151 | #=============================================================================== 152 | 153 | # trim video clip 154 | trim_video () { 155 | ffmpeg \ 156 | -hide_banner \ 157 | -stats -v panic \ 158 | -ss "${start}" \ 159 | -i "${infile}" \ 160 | -t "${end}" \ 161 | -c:a "${aac}" \ 162 | -c:v libx264 -profile:v high \ 163 | -pix_fmt yuv420p -movflags +faststart \ 164 | -f mp4 \ 165 | "${outfile:=${videofile_default}}" 166 | } 167 | 168 | 169 | # trim webm video clip 170 | trim_webm () { 171 | ffmpeg \ 172 | -hide_banner \ 173 | -stats -v panic \ 174 | -ss "${start}" \ 175 | -i "${infile}" \ 176 | -t "${end}" \ 177 | -c:a libopus \ 178 | -c:v vp9 \ 179 | -f webm \ 180 | "${outfile:=${webm_default}}" 181 | } 182 | 183 | # trim aac audio clip 184 | trim_aac () { 185 | ffmpeg \ 186 | -hide_banner \ 187 | -stats -v panic \ 188 | -ss "${start}" \ 189 | -i "${infile}" \ 190 | -t "${end}" \ 191 | -c:a "${aac}" \ 192 | -f adts \ 193 | "${outfile:=${aac_default}}" 194 | } 195 | 196 | # trim m4a audio clip 197 | trim_m4a () { 198 | ffmpeg \ 199 | -hide_banner \ 200 | -stats -v panic \ 201 | -ss "${start}" \ 202 | -i "${infile}" \ 203 | -t "${end}" \ 204 | -c:a "${aac}" \ 205 | -f mp4 \ 206 | "${outfile:=${m4a_default}}" 207 | } 208 | 209 | # trim mp3 audio clip 210 | trim_mp3 () { 211 | ffmpeg \ 212 | -hide_banner \ 213 | -stats -v panic \ 214 | -ss "${start}" \ 215 | -i "${infile}" \ 216 | -t "${end}" \ 217 | -c:a libmp3lame \ 218 | -f mp3 \ 219 | "${outfile:=${mp3_default}}" 220 | } 221 | 222 | # trim wav audio clip 223 | trim_wav () { 224 | ffmpeg \ 225 | -hide_banner \ 226 | -stats -v panic \ 227 | -ss "${start}" \ 228 | -i "${infile}" \ 229 | -t "${end}" \ 230 | -c:a pcm_s16le \ 231 | -f wav \ 232 | "${outfile:=${wav_default}}" 233 | } 234 | 235 | 236 | # trim ogg audio clip 237 | trim_ogg () { 238 | ffmpeg \ 239 | -hide_banner \ 240 | -stats -v panic \ 241 | -ss "${start}" \ 242 | -i "${infile}" \ 243 | -t "${end}" \ 244 | -c:a libopus \ 245 | -f ogg \ 246 | "${outfile:=${ogg_default}}" 247 | } 248 | 249 | #=============================================================================== 250 | # check the files mime type 251 | #=============================================================================== 252 | 253 | case "${filetype}" in 254 | "${mov_mime}"|"${mkv_mime}"|"${mp4_mime}"|"${m4v_mime}") trim_video "${infile}";; 255 | "${webm_mime}") trim_webm "${infile}";; 256 | "${aac_mime}") trim_aac "${infile}";; 257 | "${m4a_mime}") trim_m4a "${infile}";; 258 | "${audio_mime}") trim_mp3 "${infile}";; 259 | "${wav_mime}") trim_wav "${infile}";; 260 | "${ogg_mime}") trim_ogg "${infile}";; 261 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 262 | esac 263 | -------------------------------------------------------------------------------- /vid2gif: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # vid2gif 5 | # convert a video into a gif animation 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe cut 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # convert a video into a gif animation 22 | 23 | $(basename "$0") -s 00:00:00.000 -i input.(mp4|mov|mkv|m4v) -t 00:00:00.000 -f [00] -w [0000] -o output.gif 24 | -s 00:00:00.000 : start time 25 | -i input.(mp4|mov|mkv|m4v) 26 | -t 00:00:00.000 : number of seconds after start time 27 | -f [00] : framerate 28 | -w [0000] : width 29 | -o output.gif : optional argument 30 | # if option not provided defaults input-name-gif-date-time.gif" 31 | exit 2 32 | } 33 | 34 | 35 | #=============================================================================== 36 | # error messages 37 | #=============================================================================== 38 | 39 | INVALID_OPT_ERR='Invalid option:' 40 | REQ_ARG_ERR='requires an argument' 41 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 42 | 43 | 44 | #=============================================================================== 45 | # check the number of arguments passed to the script 46 | #=============================================================================== 47 | 48 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 49 | 50 | 51 | #=============================================================================== 52 | # getopts check the options passed to the script 53 | #=============================================================================== 54 | 55 | while getopts ':s:i:t:f:w:o:h' opt 56 | do 57 | case ${opt} in 58 | s) start="${OPTARG}";; 59 | i) infile="${OPTARG}";; 60 | t) end="${OPTARG}";; 61 | f) framerate="${OPTARG}";; 62 | w) width="${OPTARG}";; 63 | h) usage;; 64 | o) outfile="${OPTARG}";; 65 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 66 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 67 | esac 68 | done 69 | shift $((OPTIND-1)) 70 | 71 | 72 | #=============================================================================== 73 | # variables 74 | #=============================================================================== 75 | 76 | # input name 77 | infile_nopath="${infile##*/}" 78 | infile_name="${infile_nopath%.*}" 79 | 80 | # defaults for variables if not defined 81 | start_default='00:00:00' 82 | end_default=$(ffprobe -v error -sexagesimal -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "$infile" | cut -d\. -f1) 83 | framerate_default='15' 84 | width_default='320' 85 | outfile_default="${infile_name}-gif-$(date +"%Y-%m-%d-%H-%M-%S").gif" 86 | 87 | 88 | #=============================================================================== 89 | # functions 90 | #=============================================================================== 91 | 92 | # create gif function 93 | create_gif () { 94 | ffmpeg \ 95 | -hide_banner \ 96 | -stats -v panic \ 97 | -ss "${start:=${start_default}}" \ 98 | -i "${infile}" \ 99 | -t "${end:=${end_default}}" \ 100 | -filter_complex "[0:v] fps=${framerate:=${framerate_default}},scale=${width:=${width_default}}:-1:flags=lanczos,split [a][b];[a] palettegen [p];[b][p] paletteuse" \ 101 | "${outfile:=${outfile_default}}" 102 | } 103 | 104 | # run the create_gif function 105 | create_gif "${infile}" 106 | -------------------------------------------------------------------------------- /waveform: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # waveform 5 | # create a waveform from an audio or video file and save as a png 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # create a waveform from an audio or video file and save as a png 22 | 23 | $(basename "$0") -i input.(mp4|mkv|mov|m4v|webm|aac|m4a|wav|mp3) -o output.png 24 | -i output.(mp4|mkv|mov|m4v|aac|m4a|wav|mp3) 25 | -o output.png : optional argument # if option not provided defaults to input-name-waveform-date-time" 26 | exit 2 27 | } 28 | 29 | 30 | #=============================================================================== 31 | # error messages 32 | #=============================================================================== 33 | 34 | INVALID_OPT_ERR='Invalid option:' 35 | REQ_ARG_ERR='requires an argument' 36 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 37 | 38 | 39 | #=============================================================================== 40 | # check the number of arguments passed to the script 41 | #=============================================================================== 42 | 43 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 44 | 45 | 46 | #=============================================================================== 47 | # getopts check the options passed to the script 48 | #=============================================================================== 49 | 50 | while getopts ':i:o:h' opt 51 | do 52 | case ${opt} in 53 | i) infile="${OPTARG}";; 54 | o) outfile="${OPTARG}";; 55 | h) usage;; 56 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 57 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 58 | esac 59 | done 60 | shift $((OPTIND-1)) 61 | 62 | 63 | #=============================================================================== 64 | # variables 65 | #=============================================================================== 66 | 67 | # variables 68 | infile_nopath="${infile##*/}" 69 | infile_name="${infile_nopath%.*}" 70 | 71 | # defaults for variables if not defined 72 | outfile_default="${infile_name}-waveform-$(date +"%Y-%m-%d-%H-%M-%S").png" 73 | 74 | 75 | #=============================================================================== 76 | # functions 77 | #=============================================================================== 78 | 79 | # waveform function 80 | wform () { 81 | ffmpeg \ 82 | -hide_banner \ 83 | -stats -v panic \ 84 | -i "${infile}" \ 85 | -filter_complex "aformat=channel_layouts=mono,showwavespic=s=1000x200" \ 86 | -frames:v 1 \ 87 | -f apng \ 88 | "${outfile:=${outfile_default}}" 89 | } 90 | 91 | # run the wform function 92 | wform "${infile}" 93 | -------------------------------------------------------------------------------- /webp: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # webp 5 | # ffmpeg libwebp_anim 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage () { 16 | # if argument passed to function echo it 17 | [ -z "${1}" ] || echo "! ${1}" 18 | # display help 19 | echo "\ 20 | # webp animated image 21 | 22 | $(basename "$0") -i input -c 0-6 -q 0-100 -f 15 -w 600 -p none -o output.webp 23 | -i input 24 | -c compression level: 0 - 6 : default 4 25 | -q quality: 0 - 100 : default 80 26 | -f framerate: default 15 27 | -w width: default 600px 28 | -p preset: none|default|picture|photo|drawing|icon|text : default none 29 | -o output.webp : optional agument 30 | # if option not provided defaults input-name.webp" 31 | exit 2 32 | } 33 | 34 | 35 | #=============================================================================== 36 | # error messages 37 | #=============================================================================== 38 | 39 | INVALID_OPT_ERR='Invalid option:' 40 | REQ_ARG_ERR='requires an argument' 41 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 42 | 43 | 44 | #=============================================================================== 45 | # check the number of arguments passed to the script 46 | #=============================================================================== 47 | 48 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 49 | 50 | 51 | #=============================================================================== 52 | # getopts check the options passed to the script 53 | #=============================================================================== 54 | 55 | while getopts ':i:c:q:f:w:p:o:h' opt 56 | do 57 | case ${opt} in 58 | i) input="${OPTARG}";; 59 | c) compression="${OPTARG}";; 60 | q) quality="${OPTARG}";; 61 | f) framerate="${OPTARG}";; 62 | w) width="${OPTARG}";; 63 | p) preset="${OPTARG}";; 64 | o) output="${OPTARG}";; 65 | h) usage;; 66 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 67 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 68 | esac 69 | done 70 | shift $((OPTIND-1)) 71 | 72 | 73 | #=============================================================================== 74 | # variables 75 | #=============================================================================== 76 | 77 | # input name 78 | input_nopath="${input##*/}" 79 | input_name="${input_nopath%.*}" 80 | 81 | # defaults for variables if not defined 82 | compression_default='4' 83 | quality_default='80' 84 | preset_default='none' 85 | framerate_default='15' 86 | width_default='600' 87 | output_default="${input_name}.webp" 88 | 89 | 90 | #=============================================================================== 91 | # ffmpeg webp animation function 92 | #=============================================================================== 93 | 94 | animation () { 95 | ffmpeg \ 96 | -hide_banner \ 97 | -stats -v panic \ 98 | -i "${input}" \ 99 | -c:v libwebp_anim \ 100 | -lossless 0 \ 101 | -compression_level "${compression:=${compression_default}}" \ 102 | -quality "${quality:=${quality_default}}" \ 103 | -cr_threshold 0 \ 104 | -cr_size 16 \ 105 | -preset "${preset:=${preset_default}}" \ 106 | -loop 1 \ 107 | -an -vsync 0 \ 108 | -vf "fps=fps=${framerate:=${framerate_default}},scale=${width:=${width_default}}:-1:flags=lanczos" \ 109 | "${output:=${output_default}}" 110 | } 111 | 112 | 113 | #=============================================================================== 114 | # run ffmpeg webp animation function 115 | #=============================================================================== 116 | 117 | animation 118 | -------------------------------------------------------------------------------- /xfade: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # xfade 5 | # ffmpeg xfade transitions 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe bc 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | usage() 16 | { 17 | # if argument passed to function echo it 18 | [ -z "${1}" ] || echo "! ${1}" 19 | # display help 20 | echo "\ 21 | # ffmpeg xfade transitions 22 | 23 | $(basename "$0") -a clip1.(mp4|mkv|mov|m4v) -b clip2.(mp4|mkv|mov|m4v) -d duration -t transition -f offset -o output.mp4 24 | -a clip1.(mp4|mkv|mov|m4v) : first clip 25 | -b clip2.(mp4|mkv|mov|m4v) : second clip 26 | -d duration : transition duration 27 | -t transition : transition 28 | -f offset : offset 29 | -o output.mp4 : optional argument # if option not provided defaults to input-name-xfade-date-time 30 | 31 | + transitions 32 | 33 | circleclose, circlecrop, circleopen, diagbl, diagbr, diagtl, diagtr, dissolve, distance 34 | fade, fadeblack, fadegrays, fadewhite, hblur, hlslice, horzclose, horzopen, hrslice 35 | pixelize, radial, rectcrop, slidedown, slideleft, slideright, slideup, smoothdown 36 | smoothleft, smoothright, smoothup, squeezeh, squeezev, vdslice, vertclose, vertopen, vuslice 37 | wipebl, wipebr, wipedown, wipeleft, wiperight, wipetl, wipetr, wipeup" 38 | exit 2 39 | } 40 | 41 | 42 | #=============================================================================== 43 | # error messages 44 | #=============================================================================== 45 | 46 | INVALID_OPT_ERR='Invalid option:' 47 | REQ_ARG_ERR='requires an argument' 48 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 49 | 50 | 51 | #=============================================================================== 52 | # check the number of arguments passed to the script 53 | #=============================================================================== 54 | 55 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 56 | 57 | 58 | #=============================================================================== 59 | # getopts check the options passed to the script 60 | #=============================================================================== 61 | 62 | while getopts ':a:b:d:t:f:o:h' opt 63 | do 64 | case ${opt} in 65 | a) clip1="${OPTARG}";; 66 | b) clip2="${OPTARG}";; 67 | d) dur="${OPTARG}";; 68 | t) transition="${OPTARG}";; 69 | f) offset="${OPTARG}";; 70 | o) outfile="${OPTARG}";; 71 | h) usage;; 72 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 73 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 74 | esac 75 | done 76 | shift $((OPTIND-1)) 77 | 78 | 79 | #=============================================================================== 80 | # variables 81 | #=============================================================================== 82 | 83 | # input, input name and extension 84 | clip1_nopath="${clip1##*/}" 85 | clip1_name="${clip1_nopath%.*}" 86 | 87 | # defaults 88 | duration_default='0.5' 89 | transition_default='fade' 90 | outfile_default="${clip1_name}-xfade-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 91 | 92 | # offset and duration options not set 93 | if [ -z "${offset}" ];then 94 | # clip 1 duration to calculate default transition offset 95 | clip1_dur=$(ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${clip1}") 96 | 97 | # round down decimal point 98 | clip1_time=$(printf "%.1f\n" "${clip1_dur}") 99 | 100 | # clip1 duration minus duration default and 0.1 seconds which is needed for the transition 101 | offset_default=$(echo "${clip1_time}" - "${dur:=${duration_default}}" - 0.1 | bc -l) 102 | 103 | # audio trim default match offset default 104 | audio_trim_default=$(echo "${clip1_time}" - 0.1 | bc -l) 105 | else 106 | # offset and duration options set 107 | # trim audio to match length of the duration and offset 108 | audio_trim=$(echo "${dur} + ${offset}" | bc -l) 109 | fi 110 | 111 | 112 | #=============================================================================== 113 | # function 114 | #=============================================================================== 115 | 116 | # cross fade video and audio 117 | xfade_va () { 118 | ffmpeg \ 119 | -hide_banner \ 120 | -stats -v panic \ 121 | -i "${clip1}" -i "${clip2}" \ 122 | -filter_complex \ 123 | "[0:v][1:v]xfade=transition='${transition:=${transition_default}}':duration='${dur:=${duration_default}}':offset='${offset:=${offset_default}}'[xfade]; 124 | [0:a]atrim=0:'${audio_trim:=${audio_trim_default}}'[atrim]; 125 | [atrim][1:a]acrossfade=d='${dur:=${duration_default}}'[afade] 126 | " \ 127 | -map "[xfade]" -map "[afade]" \ 128 | -pix_fmt yuv420p \ 129 | -movflags +faststart -f mp4 \ 130 | "${outfile:=${outfile_default}}" 131 | } 132 | 133 | 134 | # run the xfade_va function 135 | xfade_va "${clip1}" "${clip2}" 136 | -------------------------------------------------------------------------------- /zoompan: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #=============================================================================== 4 | # zoompan 5 | # ken burns effect 6 | #=============================================================================== 7 | 8 | # dependencies: 9 | # ffmpeg ffprobe 10 | 11 | #=============================================================================== 12 | # script usage 13 | #=============================================================================== 14 | 15 | # script usage 16 | usage () 17 | { 18 | # if argument passed to function echo it 19 | [ -z "${1}" ] || echo "! ${1}" 20 | # display help 21 | echo "\ 22 | # zoompan, ken burns effect 23 | 24 | $(basename "$0") -i input.(png|jpg|jpeg) -d (000) -z (in|out) -p (tl|c|tc|tr|bl|br) -o output.mp4 25 | -i = input.(png|jpg|jpeg) 26 | -d = duration : from 1-999 27 | -z = zoom : in or out 28 | -p = position : zoom to location listed below 29 | -o = outfile.mp4 : optional argument # default is input-name-zoompan-date-time 30 | 31 | +------------------------------+ 32 | +tl tc tr+ 33 | + + 34 | + c + 35 | + + 36 | +bl br+ 37 | +------------------------------+" 38 | exit 2 39 | } 40 | 41 | 42 | #=============================================================================== 43 | # error messages 44 | #=============================================================================== 45 | 46 | INVALID_OPT_ERR='Invalid option:' 47 | REQ_ARG_ERR='requires an argument' 48 | WRONG_ARGS_ERR='wrong number of arguments passed to script' 49 | NOT_MEDIA_FILE_ERR='is not a media file' 50 | 51 | 52 | #=============================================================================== 53 | # check the number of arguments passed to the script 54 | #=============================================================================== 55 | 56 | [ $# -gt 0 ] || usage "${WRONG_ARGS_ERR}" 57 | 58 | 59 | #=============================================================================== 60 | # getopts check the options passed to the script 61 | #=============================================================================== 62 | 63 | while getopts ':i:d:z:p:o:h' opt 64 | do 65 | case ${opt} in 66 | i) infile="${OPTARG}";; 67 | d) dur="${OPTARG}";; 68 | z) zoom="${OPTARG}" 69 | [ "${zoom}" = 'in' ] || [ "${zoom}" = 'out' ];; 70 | p) position="${OPTARG}";; 71 | o) outfile="${OPTARG}";; 72 | h) usage;; 73 | \?) usage "${INVALID_OPT_ERR} ${OPTARG}" 1>&2;; 74 | :) usage "${INVALID_OPT_ERR} ${OPTARG} ${REQ_ARG_ERR}" 1>&2;; 75 | esac 76 | done 77 | shift $((OPTIND-1)) 78 | 79 | 80 | #=============================================================================== 81 | # variables 82 | #=============================================================================== 83 | 84 | # input, input name and extension 85 | infile_nopath="${infile##*/}" 86 | infile_name="${infile_nopath%.*}" 87 | 88 | # ffprobe get image height 89 | imgheight="$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "${infile}")" 90 | 91 | # frames per seconds 92 | infps='30' 93 | 94 | # zoom in positions 95 | 96 | # zoom in top left 97 | zi_top_left="scale=-2:10*ih,\ 98 | zoompan=z='min(zoom+0.0015,1.5)':\ 99 | fps=${infps}:d=${infps}*${dur},\ 100 | scale=-2:${imgheight}" \ 101 | 102 | # zoom in center 103 | zi_center="scale=-2:10*ih,\ 104 | zoompan=z='min(zoom+0.0015,1.5)':\ 105 | fps=${infps}:d=${infps}*${dur}:\ 106 | x='iw/2-(iw/zoom/2)':\ 107 | y='ih/2-(ih/zoom/2)',\ 108 | scale=-2:${imgheight}" \ 109 | 110 | # zoom in top center 111 | zi_top_center="scale=-2:10*ih,\ 112 | zoompan=z='min(zoom+0.0015,1.5)':\ 113 | fps=${infps}:d=${infps}*${dur}:\ 114 | x='iw/2-(iw/zoom/2)',\ 115 | scale=-2:${imgheight}" \ 116 | 117 | # zoom in top right 118 | zi_top_right="scale=-2:10*ih,\ 119 | zoompan=z='min(zoom+0.0015,1.5)':\ 120 | fps=${infps}:d=${infps}*${dur}:\ 121 | x='iw/zoom-(iw/zoom/2)',\ 122 | scale=-2:${imgheight}" \ 123 | 124 | # zoom out positions 125 | 126 | # zoom out top left 127 | zo_top_left="scale=-2:2*ih,\ 128 | zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\ 129 | fps=${infps}:d=${infps}*${dur},\ 130 | scale=-2:${imgheight}" \ 131 | 132 | zo_center="scale=-2:2*ih,\ 133 | zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\ 134 | fps=${infps}:d=${infps}*${dur}:\ 135 | x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)',\ 136 | scale=-2:${imgheight}" \ 137 | 138 | # zoom out top center 139 | zo_top_center="scale=-2:2*ih,\ 140 | zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\ 141 | fps=${infps}:d=${infps}*${dur}:\ 142 | x='iw/2-(iw/zoom/2)',\ 143 | scale=-2:${imgheight}" \ 144 | 145 | # zoom out top right 146 | zo_top_right="scale=-2:2*ih,\ 147 | zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\ 148 | fps=${infps}:d=${infps}*${dur}:\ 149 | x='iw/zoom-(iw/zoom/2)',\ 150 | scale=-2:${imgheight}" \ 151 | 152 | # zoom out bottom left 153 | zo_bottom_left="scale=-2:2*ih,\ 154 | zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\ 155 | fps=${infps}:d=${infps}*${dur}:\ 156 | y='ih-ih/zoom',\ 157 | scale=-2:${imgheight}" \ 158 | 159 | # zoom out bottom right 160 | zo_bottom_right="scale=-2:2*ih,\ 161 | zoompan=z='if(lte(zoom,1.0),1.5,max(1.001,zoom-0.0015))':\ 162 | fps=${infps}:d=${infps}*${dur}:\ 163 | x='iw-iw/zoom':\ 164 | y='ih-ih/zoom',\ 165 | scale=-2:${imgheight}" \ 166 | 167 | # outfile file recording destination 168 | outfile_default="${infile_name}-zoompan-$(date +"%Y-%m-%d-%H-%M-%S").mp4" 169 | 170 | 171 | #=============================================================================== 172 | # function 173 | #=============================================================================== 174 | 175 | # zoom function 176 | zoom_func () { 177 | ffmpeg \ 178 | -hide_banner \ 179 | -stats -v panic \ 180 | -r 30 \ 181 | -i "${infile}" \ 182 | -filter_complex \ 183 | "${1}" \ 184 | -y -shortest \ 185 | -c:v libx264 -crf 18 -profile:v high \ 186 | -r 30 -pix_fmt yuv420p \ 187 | -movflags +faststart -f mp4 \ 188 | "${outfile:=${outfile_default}}" 189 | } 190 | 191 | # check zoom and position 192 | case "${zoom}" in 193 | in) 194 | case "${position}" in 195 | tl) zoom_func "${zi_top_left}" "${infile}";; 196 | tc) zoom_func "${zi_top_center}" "${infile}";; 197 | c) zoom_func "${zi_center}" "${infile}";; 198 | tr) zoom_func "${zi_top_right}" "${infile}";; 199 | *) help;; 200 | esac 201 | ;; 202 | out) 203 | case "${position}" in 204 | tl) zoom_func "${zo_top_left}" "${infile}";; 205 | tc) zoom_func "${zo_top_center}" "${infile}";; 206 | c) zoom_func "${zo_center}" "${infile}";; 207 | tr) zoom_func "${zo_top_right}" "${infile}";; 208 | bl) zoom_func "${zo_bottom_left}" "${infile}";; 209 | br) zoom_func "${zo_bottom_right}" "${infile}";; 210 | *) help;; 211 | esac 212 | ;; 213 | *) usage "${infile} ${NOT_MEDIA_FILE_ERR}";; 214 | esac 215 | --------------------------------------------------------------------------------