├── .gitignore ├── LICENSE ├── README.md ├── convert-video.sh ├── detect-crop.sh ├── query-handbrake-log.sh └── transcode-video.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2024 Lisa Melton 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WARNING! 2 | 3 | **These old Bash scripts are now deprecated and no longer maintained!** 4 | 5 | **Instead, use my new and improved Ruby-based tools available at:** 6 | 7 | **** 8 | 9 | **** 10 | 11 | **** 12 | 13 |




14 | 15 | # Video Transcoding Scripts 16 | 17 | Utilities to transcode, inspect and convert videos. 18 | 19 | ## About 20 | 21 | Hi, I'm [Lisa Melton](http://lisamelton.net/). I wrote these scripts to transcode my collection of Blu-ray Discs and DVDs into a smaller, more portable format while remaining high enough quality to be mistaken for the originals. 22 | 23 | While I've used rougher versions of these scripts for many years, I didn't publish any of them until I was featured in, "[How to rip and transcode video for the best quality possible](http://www.imore.com/vector-22-don-melton-transcoding-video)," a podcast with [Rene Ritchie](https://twitter.com/reneritchie). Those initial scripts were only available as separate Gists on GitHub. Now they're all collected in this repository: 24 | 25 | 26 | 27 | All of these scripts are written in [Bash](http://www.gnu.org/software/bash/) and leverage excellent Open Source and cross-platform software like [HandBrake](https://handbrake.fr/), [MKVToolNix](https://www.bunkus.org/videotools/mkvtoolnix/), [MPlayer](http://mplayerhq.hu/), [FFmpeg](http://ffmpeg.org/), and [MP4v2](https://code.google.com/p/mp4v2/). These scripts are essentially intelligent wrappers around these other tools, designed to be executed from the command line shell. 28 | 29 | Even if you don't use any of these scripts, you may find their source code or this "README" document helpful. 30 | 31 | ### Transcoding with `transcode-video.sh` 32 | 33 | The primary script is `transcode-video.sh` and I wrote it because the preset system built into HandBrake wasn't quite powerful enough to automatically change bitrate and other encoding options based on different inputs. Plus, HandBrake's default presets themselves didn't produce what I wanted in terms of predictable output size with sufficient quality. 34 | 35 | HandBrake's "AppleTV 3" preset is closest to what I wanted but transcoding "[Planet Terror (2007)](http://www.blu-ray.com/movies/Planet-Terror-Blu-ray/1248/)" with it results in a huge video bitrate of 19.9 Mbps, very near the original of 22.9 Mbps. And transcoding "[The Girl with the Dragon Tattoo (2011)](http://www.blu-ray.com/movies/The-Girl-with-the-Dragon-Tattoo-Blu-ray/35744/)," while much smaller in output size, lacks detail compared to the original. 36 | 37 | Videos from the [iTunes Store](https://en.wikipedia.org/wiki/ITunes_Store) were my template for a smaller and more portable transcoding format. Their files are very good quality, only about 20% the size of the same video on a Blu-ray Disc, and play on a wide variety of devices. 38 | 39 | To follow that template, the `transcode-video.sh` script configures the [x264 video encoder](http://www.videolan.org/developers/x264.html) within HandBrake to use a [constrained variable bitrate (CVBR)](https://en.wikipedia.org/wiki/Variable_bitrate) mode, and to automatically target bitrates appropriate for different input resolutions. 40 | 41 | Input resolution | Target video bitrate 42 | --- | --- 43 | 1080p or Blu-ray video | 5 Mbps 44 | 720p | 4 Mbps 45 | 480i, 576p or DVD video | 2 Mbps 46 | 47 | These targets are technically maximum bitrates. But since this script modifies CVBR mode with a minimum quality threshold, x264 is allowed to exceed these bitrate limits to maintain that quality level. However, the final output video bitrate is still usually below or near the target. And almost always below the target when additional compression is applied via x264's preset system. 48 | 49 | Which makes videos transcoded with this script very near the same size, quality and configuration as those from the iTunes Store, including their audio tracks. 50 | 51 | If possible, audio is first passed through in its original form. This hardly ever works for Blu-ray Discs but it often will for DVDs and other random videos since this script can take almost any movie file as input. 52 | 53 | When audio transcoding is required, it's done in [AAC format](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) and, if the original is [multi-channel surround sound](https://en.wikipedia.org/wiki/Surround_sound), in [Dolby Digital AC-3 format](https://en.wikipedia.org/wiki/Dolby_Digital). Meaning the output can contain two tracks from the same source in different formats. 54 | 55 | Input channels | Pass through | AAC track | AC-3 track 56 | --- | --- | --- | --- 57 | Mono | AAC only | 80 Kbps | none 58 | Stereo | AAC only | 160 Kbps | none 59 | Surround | AC-3 only, up to 448 Kbps | 160 Kbps | 384 Kbps with 5.1 channels 60 | 61 | The surround pass through bitrate is `448 Kbps` because this is common on DVDs and re-encoding that at `384 Kbps` could degrade quality. 62 | 63 | Additionally, the `transcode-video.sh` script automatically burns any forced subtitle track it detects into the output video track. "Burning" means that the subtitle becomes part of the video itself and isn't retained as a separate track. A "forced" subtitle track is detected by a special flag on that track in the input. 64 | 65 | However, automatic forced subtitle detection only works when the input is a single file and not a disc image directory. 66 | 67 | Another automatic behavior of `transcode-video.sh` is forcing a lower frame rate or applying a `deinterlace` filter to reduce jerky motion and visible artifacts in [interlaced video](https://en.wikipedia.org/wiki/Interlaced_video). 68 | 69 | Most automatic behaviors in this script can be overridden or augmented with additional options. But, other than specifying cropping bounds, using `transcode-video.sh` can be as simple as this: 70 | 71 | transcode-video.sh "/path/to/Movie.mkv" 72 | 73 | Which creates, after what is hopefully a reasonable amount of time, two files in the current working directory: 74 | 75 | Movie.mp4 76 | Movie.mp4.log 77 | 78 | The first file is obviously the output video in [MP4 format](https://en.wikipedia.org/wiki/MPEG-4_Part_14). The second, while not as entertaining, is still useful since it's a log file from which performance metrics, video bitrate, relative quality, etc. can be extracted later. 79 | 80 | ### Crop detection with `detect-crop.sh` 81 | 82 | One transcoding option I usually add on the command line is cropping bounds. And I wrote the `detect-crop.sh` script because this can't be done safely with an automatic behavior in `transcode-video.sh`. 83 | 84 | Removing the black, non-content borders of a video during transcoding is not about making the edges of the output look pretty. Those edges are usually not visible anyway when viewed full screen. 85 | 86 | Cropping is about faster transcoding and higher quality. Fewer pixels to read and write almost always leads to a speed improvement. Fewer pixels also means the x264 encoder within HandBrake doesn't waste bitrate on non-content. 87 | 88 | HandBrake applies automatic crop detection by default. While it's usually correct, it does guess wrong often enough not to be trusted without review. For example, HandBrake's default behavior removes the top and bottom 140 pixels from "[The Dark Knight (2008)](http://www.blu-ray.com/movies/The-Dark-Knight-Blu-ray/743/)" and "[The Hunger Games: Catching Fire (2013)](http://www.blu-ray.com/movies/The-Hunger-Games-Catching-Fire-Blu-ray/67923/)," losing significant portions of their full-frame content. 89 | 90 | And sometimes HandBrake only crops a few pixels from one or more edges, which is too small of a difference in size to improve performance or quality. 91 | 92 | This is why `transcode-video.sh` doesn't allow HandBrake to apply cropping by default. 93 | 94 | Instead, the `detect-crop.sh` script leverages both HandBrake and MPlayer, with additional measurements and constraints, to find the optimal video cropping bounds. It then indicates whether those two programs agree. To aid in review, this script prints commands to the terminal console allowing the recommended (or disputed) crop to be displayed, as well as sample command lines for `transcode-video.sh` itself. And it's this easy to use: 95 | 96 | detect-crop.sh "/path/to/Movie.mkv" 97 | 98 | Which prints out something like this: 99 | 100 | Detecting: /path/to/Movie.mkv 101 | Scanning with `HandBrakeCLI`... 102 | Scanning with `mplayer`... 103 | Scanning with `mplayer`... 104 | Scanning with `mplayer`... 105 | Scanning with `mplayer`... 106 | Scanning with `mplayer`... 107 | Results are identical. 108 | 109 | mplayer -really-quiet -nosound -vf rectangle=1920:816:0:132 '/path/to/Movie.mkv' 110 | mplayer -really-quiet -nosound -vf crop=1920:816:0:132 '/path/to/Movie.mkv' 111 | 112 | transcode-video.sh --crop 132:132:0:0 '/path/to/Movie.mkv' 113 | 114 | Just copy and paste the sample commands to preview or transcode. 115 | 116 | When input is a disc image directory instead of a single file, the `detect-crop.sh` script does not use MPlayer, nor does it print out commands to preview the crop. 117 | 118 | ### Conversion with `convert-video.sh` 119 | 120 | All videos from the iTunes Store are in MP4 format, which is what `transcode-video.sh` creates by default. But this script can also generate [Matroska format](https://en.wikipedia.org/wiki/Matroska) with the `--mkv` option like this: 121 | 122 | transcode-video.sh --mkv --crop 132:132:0:0 "/path/to/Movie.mkv" 123 | 124 | Which creates these two files in the current working directory: 125 | 126 | Movie.mkv 127 | Movie.mkv.log 128 | 129 | And I prefer generating Matroska format `.mkv` files because I can preview them with MPlayer or [VLC](http://www.videolan.org/vlc/) while they're being transcoded. That's just not possible with MP4 format. 130 | 131 | But sometimes I like to play my videos on my iPhone or iPad and those devices work best with MP4 format. So I wrote the `convert-video.sh` script to repackage my videos into the other format without re-transcoding them. And it can work both ways, Matroska to MP4 or vice versa. 132 | 133 | convert-video.sh "Movie.mkv" 134 | 135 | Which creates this MP4 file in the current working directory: 136 | 137 | Movie.mp4 138 | 139 | Or... 140 | 141 | convert-video.sh "Movie.mp4" 142 | 143 | Which creates this Matroska file in the current working directory: 144 | 145 | Movie.mkv 146 | 147 | This script requires a properly organized file, in either format, with compatible video and audio tracks. Most output video files from `transcode-video.sh` meet this criteria, but videos from other sources can sometimes fail. Subtitle tracks are not converted. 148 | 149 | ## Requirements 150 | 151 | All of these scripts work on OS X because that's the platform where I develop, test and use them. But none of them actually require OS X so, technically, they should also work on Windows and Linux. Your mileage may vary. 152 | 153 | Since these scripts are essentially intelligent wrappers around other software, they do require certain command line tools to function. Most of these dependencies are available via [Homebrew](http://brew.sh/), a package manager for OS X. However, HandBrake is available via [Homebrew Cask](http://caskroom.io/), an extension to Homebrew. 154 | 155 | HandBrake can also be downloaded and installed manually. 156 | 157 | Tool | Transcoding | Crop detection | Conversion | Package | Cask 158 | --- | --- | --- | --- | --- | --- 159 | `HandBrakeCLI` | required | required | | | `handbrakecli` 160 | `mkvpropedit` | required | | | `mkvtoolnix` |   161 | `mplayer` | | required | | `mplayer` |   162 | `mkvmerge` | required | | required | `mkvtoolnix` |   163 | `ffmpeg` | | | required | `ffmpeg` |   164 | `mp4track` | required | | required | `mp4v2` |   165 | 166 | As of version 5.0, `transcode-video.sh` requires HandBrake version 0.10.0 or later. 167 | 168 | Installing a package with Homebrew is as simple as: 169 | 170 | brew install mkvtoolnix 171 | 172 | However, installing `ffmpeg` is a bit more complicated due to recent changes in its Homebrew formula: 173 | 174 | brew install --with-faac --with-fdk-aac ffmpeg 175 | 176 | To install both Homebrew Cask and `HandBrakeCLI`, the command line version of HandBrake: 177 | 178 | brew install caskroom/cask/brew-cask 179 | brew cask install handbrakecli 180 | 181 | Nightly builds of HandBrake are also available. While often containing more up-to-date libraries, these versions of HandBrake are not always stable. Use them with caution. 182 | 183 | To install both Homebrew Cask and a nightly build of `HandBrakeCLI`: 184 | 185 | brew install caskroom/cask/brew-cask 186 | brew cask install caskroom/versions/handbrakecli-nightly 187 | 188 | ### Downloading and installing HandBrake manually 189 | 190 | You can find the official release of `HandBrakeCLI` here: 191 | 192 | 193 | 194 | Or a nightly build of `HandBrakeCLI` here: 195 | 196 | 197 | 198 | Whichever version you choose, make sure you download a disk image file containing the command line tool `HandBrakeCLI`, and not just the `HandBrake` application. Disk images containing `HandBrakeCLI` have "CLI" in the filename. 199 | 200 | Open the disk image and then copy `HandBrakeCLI` to a directory listed in your `PATH` environment variable such as `/usr/local/bin`. 201 | 202 | ## Installation 203 | 204 | As of now, all of my scripts must be installed manually. 205 | 206 | You can retrieve them via the command line by cloning the entire repository like this: 207 | 208 | git clone https://github.com/lisamelton/video-transcoding-scripts.git 209 | 210 | Or download them individually from the GitHub website here: 211 | 212 | 213 | 214 | Make sure each script is executable by setting its permissions like this: 215 | 216 | chmod +x transcode-video.sh 217 | 218 | And then copy the scripts to a directory listed in your `PATH` environment variable such as `/usr/local/bin`. 219 | 220 | ## Usage 221 | 222 | All of these scripts can take a single video file as their only argument: 223 | 224 | transcode-video.sh "/path/to/Movie.mkv" 225 | 226 | Use `--help` to understand how to override their default behavior with other options: 227 | 228 | transcode-video.sh --help 229 | 230 | This built-in help is available even if a script's software dependencies are not yet installed. 231 | 232 | While `transcode-video.sh` and `detect-crop.sh` work best with a single video file, both also accept a disc image directory as input: 233 | 234 | transcode-video.sh "/path/to/Movie disc image directory/" 235 | 236 | Disc image directories are unencrypted backups of Blu-ray Discs or DVDs. Typically these formats include more than one video title. These additional titles can be bonus features, alternate versions of a movie, multiple TV show episodes, etc. 237 | 238 | By default, the first title in a disc image directory is selected for transcoding or crop detection. Sometimes this is what you want. But most of the time it'll be the wrong choice. 239 | 240 | If you know the title number you want, e.g. title number `25`, you can specify it with the `--title` option: 241 | 242 | transcode-video.sh --title 25 "/path/to/Movie disc image directory/" 243 | 244 | But usually you need to scan the disk image first to find the correct title number. Scanning is done by using special title number `0`: 245 | 246 | transcode-video.sh --title 0 "/path/to/Movie disc image directory/" 247 | 248 | All the titles in the disc image directory are then listed, along with other useful information like duration and format, so you can identify exactly which title you want. 249 | 250 | ## Guide 251 | 252 | ### Alternatives to transcoding your media 253 | 254 | Before using `transcode-video.sh` or any manual transcoding system, consider these four alternatives: 255 | 256 | 1. Buy or rent videos from online services like Apple, Amazon, Netflix, Hulu, YouTube, etc. Check "[Can I Stream.It?](http://www.canistream.it/)" to see if what you want to watch is available. 257 | * Upside: Often cheaper than buying physical media like Blu-ray Discs and DVDs. 258 | * Upside: Much easier to store and catalog than physical media. 259 | * Upside: Usually playable on mobile devices. 260 | * Downside: Most online services use [digital rights management (DRM)](https://en.wikipedia.org/wiki/Digital_rights_management), locking you into certain devices or ecosystems. 261 | * Downside: We're still years away from a [Spotify](https://www.spotify.com/)-like service for video, so it's likely you'll need to subscribe to multiple services to see anything close to the selection you want. 262 | * Downside: Even with shopping around, you probably won't find everything you want online. 263 | * Downside: Video and audio quality of online formats are nowhere close to that of Blu-ray Discs. 264 | * Downside: Online formats usually don't have the breadth of bonus content available on physical media. 265 | 2. Watch your existing collection of Blu-ray Discs and DVDs on a hardware disc player. 266 | * Upside: No doubt you bought a player when you bought the discs. 267 | * Upside: A hardware disc player is the best vehicle for viewing bonus content and interactive features. 268 | * Downside: Players are noisy, cumbersome, and cluttered with warnings, trailers and other crap to keep you away from promptly watching your videos. 269 | * Downside: Shuffling discs back and forth from their cases to the player is tedious and risks damage to the original media. 270 | 3. [Rip](https://en.wikipedia.org/wiki/Ripping) your collection of discs and watch them in their original format on your [digital media player](https://en.wikipedia.org/wiki/Digital_media_player). 271 | * Upside: Retains original high quality video and audio. 272 | * Upside: Ripping is usually necessary for transcoding anyway. 273 | * Downside: Requires a large capacity hard drive or [network-attached storage (NAS)](https://en.wikipedia.org/wiki/Network-attached_storage) device to hold the ripped files. 274 | * Downside: Most digital media players, like the [Apple TV](https://www.apple.com/appletv/) or [Roku](https://www.roku.com/), don't support playback of ripped media formats. You'll probably need a custom [Mac mini](http://www.apple.com/mac-mini/) or other computer configured as a [home theater PC (HTPC)](https://en.wikipedia.org/wiki/Home_theater_PC). 275 | 4. Dynamically transcode your ripped video files with [Plex Media Server](https://plex.tv/). 276 | * Upside: Easy to install, configure and use. 277 | * Upside: Automatically adjusts transcoding quality and size for different devices. 278 | * Downside: Requires a powerful enough computer somewhere on your network to act as the server and do the actual real-time transcoding. 279 | * Downside: Dynamic transcoding can produce noticeable quality defects in some videos. 280 | 281 | ### Preparing your media for transcoding 282 | 283 | I have four rules when preparing my own media for transcoding: 284 | 285 | 1. Use [MakeMKV](http://www.makemkv.com/) to rip Blu-ray Discs and DVDs. 286 | 2. Rip each selected video as a single Matroska format `.mkv` file. 287 | 3. Look for forced subtitles and isolate them in their own track. 288 | 4. Convert lossless audio tracks to [FLAC format](https://en.wikipedia.org/wiki/FLAC). 289 | 290 | Why MakeMKV? 291 | 292 | * It runs on most desktop computer platforms like OS X, Windows and Linux. There's even a free version available to try before you buy. 293 | * It was designed to decrypt and extract a video track, usually the main feature, of a disc and convert it into a single Matroska format `.mkv` file. And it does this really, really well. 294 | * It can also make an unencrypted backup of your entire Blu-ray or DVD to a disc image directory. 295 | * It's not pretty and it's not particularly easy use. But once you figure out how it works, you can rip your video exactly the way you want. 296 | 297 | Why a single `.mkv` file? 298 | 299 | * Many automatic behaviors and other features in both `transcode-video.sh` and `detect-crop.sh` are not available when input is a disc image directory. This is because that format limits the ability of `HandBrakeCLI` to detect or manipulate certain information about the video. 300 | * Both forced subtitle extraction and lossless audio conversion, detailed below, are not possible when input is a disc image directory. 301 | 302 | Why bother with forced subtitles? 303 | 304 | * Remember "[The Hunt for Red October (1990)](http://www.blu-ray.com/movies/The-Hunt-For-Red-October-Blu-ray/920/)" when Sean Connery and Sam Neill are speaking actual Russian at the beginning of the movie instead of just using cheesy accents like they did the rest of the time? If you speak English, the Blu-ray Disc version provides English subtitles just for those few scenes. They're "forced" on screen for you. Which is actually very convenient. 305 | * Forced subtitles are often embedded within a full subtitle track. And a special flag is set on the portion of that track which is supposed to be forced. MakeMKV can recognize that flag when it converts the video into a single `.mkv` file. It can even extract just the forced portion of that subtitle into a another separate subtitle track. And it can set a different "forced" flag in the output `.mkv` file on that separate track so other software can tell what it's for. 306 | * Not all discs with forced subtitles have those subtitles embedded within other tracks. Sometimes they really are separate. But enough discs are designed with the embedded technique that you should avoid using a disc image directory as input for transcoding. 307 | 308 | Why convert lossless audio? 309 | 310 | * [DTS-HD Master Audio](https://en.wikipedia.org/wiki/DTS-HD_Master_Audio) is the most popular high definition, lossless audio format. It's used on more than 80% of all Blu-ray Discs. 311 | * HandBrake, FFmpeg, MPlayer and other Open Source software can't decode the lossless portion of a DTS-HD audio track. They're only able to extract the non-HD, lossy core which is in [DTS format](https://en.wikipedia.org/wiki/DTS_(sound_system)). 312 | * But MakeMKV can [decode DTS-HD with some help from additional software](http://www.makemkv.com/dtshd/) and convert it into FLAC format which can then be decoded by HandBrake and most other software. Once again, MakeMKV can only do this when it converts the video into a single `.mkv` file. 313 | 314 | ### The evolution of rate control in `transcode-video.sh` 315 | 316 | The x264 video encoder within HandBrake provides either a specific video bitrate or a constant rate factor (CRF) to control how bits are allocated. 317 | 318 | Using a specific video bitrate while encoding in one pass through the input is known as average bitrate (ABR) mode. It's called an "average" because a single pass can only approximate the target bitrate, although it usually comes very close. 319 | 320 | ABR mode creates output of predictable size with unpredictable quality. 321 | 322 | Versions of `transcode-video.sh` prior to 3.0 used a modified ABR mode that improved quality by allowing the bitrate to vary much, much more. This was done by setting x264's rate tolerance, how much it allows bitrate to vary, to the highest possible (or infinite) value. This was known as the "`ratetol=inf`" hack. 323 | 324 | About 90% of the time, this modified ABR mode hack worked very well. But there were some cases where it performed poorly, producing noticeable quality defects. 325 | 326 | Using a constant rate factor is known as variable bitrate (VBR) mode. A rate factor is basically an arbitrary number targeting a constant level of quality. The x264 default CRF value is `23`. Lower values increase quality and bitrate. Higher values lower quality and bitrate. But because the target is a quality level, bitrate always varies significantly depending on input. 327 | 328 | VBR mode creates output of predictable quality with unpredictable size. 329 | 330 | Version 3.0 of `transcode-video.sh` switched to using a constrained variable bitrate (CVBR) mode. This mode set a specific upper limit on bitrate so that it behaved a bit like ABR mode. And that limit wasn't exceeded even when a higher level of quality was selected via the CRF value of `18`. 331 | 332 | This constraint was imposed by using x264's video buffer verifier (VBV) system. Normally this system is used for bandwidth constrained situations such as streaming video. 333 | 334 | Using the VBV system typically means providing two parameters to x264. The first is the maximum rate and the second is the buffer size. The maximum rate is the constraint. Typically the buffer size is set to the same value. But when the maximum rate is a low bitrate, as used by `transcode-video.sh`, then a buffer size of the same value will cause a noticeable quality defect with some input. 335 | 336 | This particular defect usually manifests itself in flat, grainy areas of the input so that it appears as if these areas are going in and out of focus. It's very annoying. 337 | 338 | Version 3.0 of `transcode-video.sh` eliminated this defect by setting the VBV buffer size to _half_ that of the maximum rate. I discovered this almost by accident and still have no idea why the trick works. 339 | 340 | About 95% of the time, this CVBR mode worked very well. It was a significant improvement over the older modified ABR mode hack. But there were a few cases where CVBR mode performed poorly, producing other noticeable quality defects. 341 | 342 | These other defects were caused by the hard upper limit on bitrate. To correctly transcode some input, sometimes you need a bigger bitrate. 343 | 344 | Additionally, even a CRF value of `18` wasn't high enough quality. Bitrates were too low in some cases, which didn't cause defects so much as a loss of detail when compared to the original. 345 | 346 | ### How _modified_ constrained variable bitrate (CVBR) mode works 347 | 348 | Current versions of `transcode-video.sh` modify CVBR mode to impose a minimum quality threshold so that x264 is allowed to exceed bitrate limits to maintain that quality level. This new constraint is simply a maximum CRF value of `25`. Additionally, the target CRF value has been lowered to `16`. 349 | 350 | Together, maximum bitrate and maximum CRF essentially vie for control of the final output video bitrate, with the target CRF value doing its best to make helpful suggestions. What should be chaos turns out beautifully. Even though x264 spews the occasional "VBV underflow" error, it can be safely ignored. 351 | 352 | This means the VBV maximum bitrate becomes a target bitrate when combined with a maximum CRF value. While the final output video bitrate is still usually below or near the target, it can sometimes get larger. On very rare occasions it can even be more than twice the target. 353 | 354 | Don't panic. When additional compression is applied via x264's preset system, the output bitrate is almost always below or near the target. 355 | 356 | If you don't want to apply additional compression to guarantee bitrates are below the target, and you're willing to sacrifice some quality in exchange for size, then you could raise both the CRF and maximum CRF values: 357 | 358 | transcode-video.sh --crf 18 --crf-max 26 "/path/to/Movie.mkv" 359 | 360 | This is essentially what the experimental `--hq` option introduced in version 3.5 of `transcode-video.sh` did. That option is now deprecated, superseded by the new defaults. 361 | 362 | Changing any of these rate control options has to be done very carefully. And I don't recommend it. Use the defaults. 363 | 364 | ### Supersize your transcoding with `--big` 365 | 366 | If reducing output size is less important to you than the possibility of increasing quality, then you can simply raise the default bitrate limits for both video and Dolby Digital AC-3 audio. 367 | 368 | For 1080p or Blu-ray Disc input, you could do this: 369 | 370 | transcode-video.sh --max 8000 --ac3 640 "/path/to/Movie.mkv" 371 | 372 | Which sets the target video bitrate to `8000 Kbps` or `8 Mbps`, and the AC-3 audio bitrate, including the pass through bitrate, to `640 Kbps`. 373 | 374 | But you would likely need to adjust that video bitrate a bit for DVD or other input. Instead, use the `--big` option: 375 | 376 | transcode-video.sh --big "/path/to/Movie.mkv" 377 | 378 | With `--big`, the `transcode-video.sh` script does all the work for you based on the video resolution of your input. 379 | 380 | Input resolution | Target video bitrate with `--big` 381 | --- | --- 382 | 1080p or Blu-ray video | 8 Mbps 383 | 720p | 6 Mbps 384 | 480i, 576p or DVD video | 3 Mbps 385 | 386 | For audio input, the change via `--big` is the same as using `--ac3 640`. Obviously this means there's no impact on the output bitrate of mono and stereo AAC audio tracks. 387 | 388 | Input channels | Pass through
with `--big` | AAC track
with `--big` | AC-3 track
with `--big` 389 | --- | --- | --- | --- 390 | Mono | AAC only | 80 Kbps | none 391 | Stereo | AAC only | 160 Kbps | none 392 | Surround | AC-3 only, up to 640 Kbps | 160 Kbps | 640 Kbps with 5.1 channels 393 | 394 | Keep in mind there's no guarantee that using `--big` will make perceptible quality improvements for every video. But it will improve some of them. Your mileage may vary. 395 | 396 | Also, using `--big` will reduce performance. Why? You're doing more calculations and writing more bits out to disk. And that takes more time. 397 | 398 | ### Understanding and using the x264 preset system 399 | 400 | The `--preset` option in `transcode-video.sh` controls the x264 video encoder, not the other preset system built into HandBrake. It takes a preset name as its single argument: 401 | 402 | transcode-video.sh --preset veryfast "/path/to/Movie.mkv" 403 | 404 | Some x264 presets are also available as shortcut options, i.e. you can use just `--veryfast` instead of having to type `--preset veryfast`: 405 | 406 | transcode-video.sh --veryfast "/path/to/Movie.mkv" 407 | 408 | The x264 presets are supposed to trade encoding speed for compression efficiency, and their names attempt to reflect this. However, that's not quite how they always work. 409 | 410 | Preset name | Shortcut option | Note 411 | --- | --- | --- 412 | `ultrafast` | none | not recommended 413 | `superfast` | none | not recommended 414 | `veryfast` | `--veryfast` | trades precision for speed 415 | `faster` | `--faster` | trades precision for speed 416 | `fast` | `--fast` | trades precision for speed 417 | `medium` | none | default 418 | `slow` | `--slow` | trades speed for compression 419 | `slower` | `--slower` | trades speed for compression 420 | `veryslow` | `--veryslow` | trades speed for compression 421 | `placebo` | none | not recommended 422 | 423 | Prior to version 4.0 of `transcode-video.sh`, `fast` was the default preset. Now `medium` is the default, just like x264 and HandBrake. 424 | 425 | Presets faster than `medium` trade precision for more speed. This can mean a loss of fidelity as preset speed increases. Previous versions of this document used the term "quality" instead of "precision." 426 | 427 | The rate control system implemented in `transcode-video.sh` reduces the risk of blockiness or loss of detail when applying faster presets. And less precision or fidelity is not always a bad thing with noisy source material due to coarse film grain or sloppy mastering. 428 | 429 | However, avoid using `superfast` and `ultrafast` because they do lower quality as well as compression efficiency. 430 | 431 | Presets slower than `medium` trade encoding speed for more compression efficiency. Usually, this means more compression as preset speed decreases. Your mileage may vary. 432 | 433 | When the output video bitrate is not below or near the target using `medium`, applying a slower preset can significantly reduce that bitrate. 434 | 435 | Any quality improvement from using the `slow` preset may not be perceptible for most input. Presets slower than `slow` may actually cause small artifacts for some input due to higher compression. 436 | 437 | The `slower`, `veryslow` and `placebo` presets are modified in `transcode-video.sh` to maintain compatibility with devices from Apple and other manufacturers. When using these presets for output larger than `1280x720` pixels, the [H.264 level](https://en.wikipedia.org/wiki/H.264/MPEG-4_AVC#Levels) is constrained to `4.0`, usually limiting the number of [reference frames](https://en.wikipedia.org/wiki/Reference_frame_(video)). 438 | 439 | Avoid using `placebo` because it's simply not worth the time and may not even produce smaller output than `veryslow`. There's a reason this particular preset doesn't follow the nomenclature. 440 | 441 | ### Evaluating the quality of a transcoding 442 | 443 | Most people watch television anywhere from a distance between 1.5 to 2.5 times the diagonal of their screen. With big-screen desktop computers and mobile devices, that viewing distance has shrunk to where it's essentially the same distance as the screen diagonal. 444 | 445 | The goal of a smaller, more portable format means that a video transcoding will be, by necessity, a lower-bitrate copy. Which also means quality will be lost during the process of compression. The trick is to make that quality loss invisible so that the transcoded copy remains good enough to be mistaken for the original. 446 | 447 | Here are some guidelines to consider when making that evaluation: 448 | 449 | * View the transcoding at the same size and from the same distance as you would the original. Otherwise you're not making a fair comparison. 450 | * Don't pause playback to compare the transcoding with the original on the same frame. The transcoding usually won't look as good because video is designed to be in motion and compression takes advantage of this to fool you. 451 | * View with audio on. Not only do you need to evaluate audio quality, but without audio you're missing the immersive experience designed, once again, to fool you. 452 | * When you do see or hear something that looks or sounds wrong, compare your observation against the original. Even Blu-ray Discs have flaws. These can be caused by bad encoding, bad mastering or just plain bad source material. 453 | 454 | ### Saving time and space by constraining your video and audio 455 | 456 | About half of all high definition video on broadcast television is in 720p format. Most people who watch television don't have a surround sound system with which to listen to it. 457 | 458 | If 720p video and stereo audio look and sound acceptable, you might want to consider these formats to save transcoding time and storage space. This is especially appropriate for mobile devices such as phones with their smaller screens and limited audio output. 459 | 460 | Use the `--720p` option to constrain 1080p or Blu-ray Disc input within a `1280x720` pixel boundary: 461 | 462 | transcode-video.sh --720p "/path/to/Movie.mkv" 463 | 464 | This doesn't affect video input that is already that size or smaller, such as DVDs. 465 | 466 | Add the `--no-surround` option to disable multi-channel surround sound and limit output to, at most, two-channel stereo: 467 | 468 | transcode-video.sh --720p --no-surround "/path/to/Movie.mkv" 469 | 470 | ### Adding audio tracks 471 | 472 | Many Blu-ray Discs and DVDs have additional audio tracks. Some of these tracks might be for other languages and some for commentaries. 473 | 474 | To include audio track `3` when transcoding, use the `--add-audio` option: 475 | 476 | transcode-video.sh --add-audio 3 "/path/to/Movie.mkv" 477 | 478 | To also include audio track `5` and name it "Director Commentary": 479 | 480 | transcode-video.sh --add-audio 3 --add-audio 5,"Director Commentary" "/path/to/Movie.mkv" 481 | 482 | Starting with version 5.0 of `transcode-video.sh`, track names can include a comma (","). 483 | 484 | By default, all added audio tracks are transcoded in AAC format. If the original audio track is multi-channel surround sound, use the `--allow-ac3` option to transcode in Dolby Digital AC–3 format: 485 | 486 | transcode-video.sh --allow-ac3 --add-audio 3 --add-audio 5,"Director Commentary" "/path/to/Movie.mkv" 487 | 488 | The `--allow-ac3` option applies to all added audio tracks. 489 | 490 | ### Including DTS audio 491 | 492 | The DTS audio format has both lossless and lossy variants, and is usually available on Blu-ray Discs. Use the `--allow-dts` option to include these tracks in their original format without transcoding: 493 | 494 | transcode-video.sh --allow-dts "/path/to/Movie.mkv" 495 | 496 | The `--allow-dts` option applies to both the main audio track and all added audio tracks. 497 | 498 | Keep in mind that lossless DTS-HD Master Audio tracks are encoded at bitrates often larger than the default target video bitrate. So including them is of dubious value if your goal with transcoding is compression. 499 | 500 | Also, while `HandBrakeCLI` can include DTS audio tracks within the MP4 format, the output is not compatible with iTunes, Apple TV or many other devices. 501 | 502 | ### Batch control for `transcode-video.sh` 503 | 504 | Although `transcode-video.sh` doesn't handle multiple inputs, it's easy to add this capability by creating a `batch.sh` script. 505 | 506 | Such a script can simply be a list of commands: 507 | 508 | #!/bin/bash 509 | 510 | transcode-video.sh --crop 132:132:0:0 "/path/to/Movie.mkv" 511 | transcode-video.sh --crop "/path/to/Another Movie.mkv" 512 | transcode-video.sh --crop 0:0:240:240 "/path/to/Yet Another Movie.mkv" 513 | 514 | But a better solution is to write the script once and supply the list of movies and their crop values separately: 515 | 516 | #!/bin/bash 517 | 518 | readonly work="$(cd "$(dirname "$0")" && pwd)" 519 | readonly queue="$work/queue.txt" 520 | readonly crops="$work/crops" 521 | 522 | input="$(sed -n 1p "$queue")" 523 | 524 | while [ "$input" ]; do 525 | title_name="$(basename "$input" | sed 's/\.[^.]*$//')" 526 | crop_file="$crops/${title_name}.txt" 527 | 528 | if [ -f "$crop_file" ]; then 529 | crop_option="--crop $(cat "$crop_file")" 530 | else 531 | crop_option='' 532 | fi 533 | 534 | sed -i '' 1d "$queue" || exit 1 535 | 536 | transcode-video.sh $crop_option "$input" 537 | 538 | input="$(sed -n 1p "$queue")" 539 | done 540 | 541 | This requires a `work` directory on disk with three items, one of which is a directory itself: 542 | 543 | batch.sh 544 | crops/ 545 | Movie.txt 546 | Yet Another Movie.txt 547 | queue.txt 548 | 549 | The contents of `crops/Movie.txt` is simply the crop value for `/path/to/Movie.mkv`: 550 | 551 | 132:132:0:0 552 | 553 | And the contents of `queue.txt` is just the list of movies, full paths without quotes, delimited by carriage returns: 554 | 555 | /path/to/Movie.mkv 556 | /path/to/Another Movie.mkv 557 | /path/to/Yet Another Movie.mkv 558 | 559 | Notice that there's no crop file for `/path/to/Another Movie.mkv`. This is because it doesn't require cropping. 560 | 561 | For other options that won't change from input to input, e.g. `--mkv`, simply augment the line in the script calling `transcode-video.sh`: 562 | 563 | transcode-video.sh --mkv $crop_option "$input" 564 | 565 | The transcoding process is started by executing the script: 566 | 567 | ./batch.sh 568 | 569 | The path is first deleted from the `queue.txt` file and then passed as an argument to the `transcode-video.sh` script. To pause after `transcode-video.sh` returns, simply insert a blank line at the top of the `queue.txt` file. 570 | 571 | ## Feedback 572 | 573 | The best way to send feedback is mentioning me, [@lisamelton@mastodon.social](https://mastodon.social/@lisamelton), on Mastodon. I always try to respond quickly but sometimes it may take as long as 24 hours. 574 | 575 | ## Acknowledgements 576 | 577 | A big "thank you" to the developers of HandBrake and the other tools used by these scripts. So much wow. 578 | 579 | Thanks to [Rene Ritchie](https://twitter.com/reneritchie) for letting me continue to babble on about transcoding in his podcasts. 580 | 581 | Thanks to [Joyce Melton](https://twitter.com/erinhalfelven), my sister, for help editing this massive "README" document. 582 | 583 | Many thanks to [Jordan Breeding](https://twitter.com/jorbsd) and numerous others online for their positive feedback, bug reports and useful suggestions. 584 | 585 | ## License 586 | 587 | Video Transcoding Scripts is copyright [Lisa Melton](http://lisamelton.net/) and available under a [MIT license](https://github.com/lisamelton/video-transcoding-scripts/blob/master/LICENSE). 588 | -------------------------------------------------------------------------------- /convert-video.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # convert-video.sh 4 | # 5 | # Copyright (c) 2013-2024 Lisa Melton 6 | # 7 | 8 | about() { 9 | cat <&2 34 | echo "Try \`$program --help\` for more information." >&2 35 | exit 1 36 | } 37 | 38 | die() { 39 | echo "$program: $1" >&2 40 | exit ${2:-1} 41 | } 42 | 43 | readonly program="$(basename "$0")" 44 | 45 | case $1 in 46 | --help) 47 | usage 48 | ;; 49 | --version) 50 | about 51 | ;; 52 | esac 53 | 54 | readonly input="$1" 55 | 56 | if [ ! "$input" ]; then 57 | syntax_error 'too few arguments' 58 | fi 59 | 60 | if [ ! -f "$input" ]; then 61 | die "input file not found: $input" 62 | fi 63 | 64 | for tool in mkvmerge ffmpeg mp4track; do 65 | 66 | if ! $(which $tool >/dev/null); then 67 | die "executable not in \$PATH: $tool" 68 | fi 69 | done 70 | 71 | readonly identification="$(mkvmerge --identify-verbose "$input")" 72 | readonly input_container="$(echo "$identification" | sed -n 's/^File .*: container: \(.*\) \[.*\]$/\1/p')" 73 | 74 | if [ ! "$input_container" ]; then 75 | die "unknown input container format: $input" 76 | fi 77 | 78 | case $input_container in 79 | 'Matroska') 80 | container_format='mp4' 81 | ;; 82 | 'QuickTime/MP4') 83 | container_format='mkv' 84 | ;; 85 | *) 86 | die "unsupported input container format: $input" 87 | ;; 88 | esac 89 | 90 | readonly output="$(basename "${input%.*}").$container_format" 91 | 92 | if [ -e "$output" ]; then 93 | die "output file already exists: $output" 94 | fi 95 | 96 | video_track='' 97 | ac3_audio_track='' 98 | aac_audio_track='' 99 | extra_aac_audio_track_list='' 100 | other_audio_track='' 101 | index='0' 102 | 103 | while read format; do 104 | 105 | if [ ! "$video_track" ] && [ "$format" == 'video (MPEG-4p10/AVC/h.264)' ]; then 106 | video_track="$index" 107 | fi 108 | 109 | if [[ "$format" =~ ^'audio ' ]]; then 110 | 111 | case $format in 112 | 'audio (AC3/EAC3)') 113 | 114 | if [ ! "$ac3_audio_track" ] && [ ! "$other_audio_track" ]; then 115 | ac3_audio_track="$index" 116 | fi 117 | ;; 118 | 'audio (AAC)') 119 | 120 | if [ ! "$other_audio_track" ]; then 121 | 122 | if [ ! "$aac_audio_track" ]; then 123 | aac_audio_track="$index" 124 | else 125 | extra_aac_audio_track_list="$extra_aac_audio_track_list,$index" 126 | fi 127 | fi 128 | ;; 129 | *) 130 | if [ "$input_container" == 'Matroska' ] && [ ! "$other_audio_track" ] && [ ! "$ac3_audio_track" ] && [ ! "$aac_audio_track" ]; then 131 | other_audio_track="$index" 132 | fi 133 | ;; 134 | esac 135 | fi 136 | 137 | index="$((index + 1))" 138 | 139 | done < <(echo "$identification" | sed -n '/^Track ID /s/^Track ID [0-9]\{1,\}: \(.*\) \[.*\]$/\1/p') 140 | 141 | if [ ! "$video_track" ]; then 142 | die "missing H.264 format video track: $input" 143 | fi 144 | 145 | if [ "$input_container" == 'Matroska' ]; then 146 | map_options="-map 0:$video_track" 147 | codec_options='-c copy' 148 | 149 | if [ "$aac_audio_track" ]; then 150 | map_options="$map_options -map 0:$aac_audio_track" 151 | fi 152 | 153 | if $(ffmpeg -version | grep enable-libfdk-aac >/dev/null); then 154 | aac_encoder='libfdk_aac' 155 | else 156 | aac_encoder='libfaac' 157 | fi 158 | 159 | if [ "$ac3_audio_track" ]; then 160 | map_options="$map_options -map 0:$ac3_audio_track" 161 | 162 | if [ ! "$aac_audio_track" ]; then 163 | map_options="$map_options -map 0:$ac3_audio_track" 164 | codec_options="-c:v copy -ac 2 -c:a:0 $aac_encoder -b:a:0 160k -c:a:1 copy" 165 | fi 166 | fi 167 | 168 | if [ "$extra_aac_audio_track_list" ]; then 169 | map_options="$map_options$(echo "$extra_aac_audio_track_list" | sed 's/,/ -map 0:/g')" 170 | fi 171 | 172 | if [ "$other_audio_track" ]; then 173 | map_options="$map_options -map 0:$other_audio_track" 174 | 175 | readonly channels="$(echo "$identification" | 176 | sed -n '/^Track ID '$other_audio_track': /s/^.* audio_channels:\([0-9]\{1,\}\).*$/\1/p')" 177 | 178 | if [ "$channels" ] && (($channels > 2)); then 179 | map_options="$map_options -map 0:$other_audio_track" 180 | codec_options="-c:v copy -ac 2 -c:a:0 $aac_encoder -b:a:0 160k -ac 6 -c:a:1 ac3 -b:a:1 384k" 181 | else 182 | codec_options="-c:v copy -ac 2 -c:a $aac_encoder -b:a 160k" 183 | fi 184 | fi 185 | 186 | echo "Converting to MP4 format: $input" >&2 187 | 188 | time { 189 | ffmpeg \ 190 | -i "$input" \ 191 | $map_options \ 192 | $codec_options \ 193 | "$output" \ 194 | || exit 1 195 | 196 | if [ "$aac_audio_track" ] || [ "$ac3_audio_track" ] || [ "$other_audio_track" ]; then 197 | flag='true' 198 | index='1' 199 | 200 | while read enabled_flag; do 201 | 202 | if [ "$enabled_flag" != "$flag" ]; then 203 | mp4track --track-index $index --enabled $flag "$output" || exit 1 204 | fi 205 | 206 | flag='false' 207 | index="$((index + 1))" 208 | 209 | done < <(mp4track --list "$output" | sed -n '/enabled/p' | sed 1d | sed 's/^[^=]*= //') 210 | fi 211 | } 212 | else 213 | track_order="0:$video_track" 214 | audio_tracks='' 215 | 216 | if [ "$ac3_audio_track" ]; then 217 | track_order="$track_order,0:$ac3_audio_track" 218 | audio_tracks="$ac3_audio_track" 219 | fi 220 | 221 | if [ "$aac_audio_track" ]; then 222 | track_order="$track_order,0:$aac_audio_track" 223 | 224 | if [ "$ac3_audio_track" ]; then 225 | audio_tracks="$audio_tracks,$aac_audio_track" 226 | else 227 | audio_tracks="$aac_audio_track" 228 | fi 229 | 230 | if [ "$extra_aac_audio_track_list" ]; then 231 | track_order="$track_order$(echo "$extra_aac_audio_track_list" | sed 's/,/,0:/g')" 232 | audio_tracks="$audio_tracks$extra_aac_audio_track_list" 233 | fi 234 | fi 235 | 236 | track_name_options=() 237 | 238 | if [ "$audio_tracks" ]; then 239 | audio_tracks_option="--audio-tracks $audio_tracks" 240 | 241 | readonly track_names="$(mp4track --list "$input" | 242 | sed -n '/userDataName/p' | 243 | sed 1d | 244 | sed 's/^[^=]*= //;s/^$//')" 245 | 246 | for index in $(echo "$audio_tracks" | sed 's/,/ /g'); do 247 | name="$(echo "$track_names" | sed -n ${index}p)" 248 | 249 | if [ "$name" ]; then 250 | track_name_options=("${track_name_options[@]}" --track-name "$index:$name") 251 | fi 252 | done 253 | else 254 | audio_tracks_option='--no-audio' 255 | fi 256 | 257 | echo "Converting to Matroska format: $input" >&2 258 | 259 | time mkvmerge \ 260 | --output "$output" \ 261 | --track-order $track_order \ 262 | --disable-track-statistics-tags \ 263 | $audio_tracks_option \ 264 | "${track_name_options[@]}" \ 265 | "$input" \ 266 | || exit 1 267 | fi 268 | -------------------------------------------------------------------------------- /detect-crop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # detect-crop.sh 4 | # 5 | # Copyright (c) 2013-2024 Lisa Melton 6 | # 7 | 8 | about() { 9 | cat <&2 40 | echo "Try \`$program --help\` for more information." >&2 41 | exit 1 42 | } 43 | 44 | die() { 45 | echo "$program: $1" >&2 46 | exit ${2:-1} 47 | } 48 | 49 | deprecated() { 50 | echo "$program: deprecated option: $1" >&2 51 | } 52 | 53 | calculate_crop() { 54 | 55 | if [ "$constrain" ]; then 56 | local delta_x="$(($width - $crop_width))" 57 | local delta_y="$(($height - $crop_height))" 58 | 59 | if (($delta_x && $delta_y)); then 60 | 61 | if (($delta_x > $delta_y)); then 62 | crop_height="$height" 63 | crop_y='0' 64 | else 65 | crop_width="$width" 66 | crop_x='0' 67 | fi 68 | fi 69 | 70 | local min_crop="$(($width / 64))" 71 | min_crop="$(($min_crop + ($min_crop % 2)))" 72 | delta_x="$(($width - $crop_width))" 73 | 74 | if (($delta_x && ($delta_x < $min_crop))); then 75 | crop_width="$width" 76 | crop_x='0' 77 | fi 78 | 79 | delta_y="$(($height - $crop_height))" 80 | 81 | if (($delta_y && ($delta_y < $min_crop))); then 82 | crop_height="$height" 83 | crop_y='0' 84 | fi 85 | fi 86 | 87 | if (($crop_width == 0)); then 88 | crop_width="$width" 89 | fi 90 | 91 | if (($crop_height == 0)); then 92 | crop_height="$height" 93 | fi 94 | 95 | if (($crop_x == $width)); then 96 | crop_x='0' 97 | fi 98 | 99 | if (($crop_y == $height)); then 100 | crop_x='0' 101 | fi 102 | 103 | mplayer_crop="$crop_width:$crop_height:$crop_x:$crop_y" 104 | handbrake_crop="$crop_y:$((height - (crop_y + crop_height))):$crop_x:$((width - (crop_x + crop_width)))" 105 | } 106 | 107 | escape_string() { 108 | echo "$1" | sed "s/'/'\\\''/g;s/^\(.*\)$/'\1'/" 109 | } 110 | 111 | print_commands() { 112 | echo 113 | 114 | if [ -f "$input" ]; then 115 | echo "mplayer -really-quiet -nosound -vf rectangle=$1 $(escape_string "$input")" 116 | echo "mplayer -really-quiet -nosound -vf crop=$1 $(escape_string "$input")" 117 | echo 118 | fi 119 | 120 | echo "transcode-video.sh $([ "$title" == '1' ] || echo "--title $title ")--crop $2 $(escape_string "$input")" 121 | echo 122 | } 123 | 124 | readonly program="$(basename "$0")" 125 | 126 | case $1 in 127 | --help) 128 | usage 129 | ;; 130 | --version) 131 | about 132 | ;; 133 | esac 134 | 135 | debug='' 136 | title='1' 137 | constrain='yes' 138 | max_step=5 139 | values_only='' 140 | 141 | while [ "$1" ]; do 142 | case $1 in 143 | --debug) 144 | debug='yes' 145 | ;; 146 | --title) 147 | title="$(printf '%.0f' "$2")" 148 | shift 149 | 150 | if (($title < 0)); then 151 | die "invalid title number: $title" 152 | fi 153 | ;; 154 | --max-step|--step) 155 | max_step="$(printf '%.0f' "$2")" 156 | shift 157 | 158 | if (($max_step < 1)); then 159 | syntax_error 'maximum step value too small' 160 | 161 | elif (($max_step > 10)); then 162 | syntax_error 'maximum step value too large' 163 | fi 164 | ;; 165 | --no-constrain) 166 | constrain='' 167 | ;; 168 | --constrain) 169 | deprecated "$1" 170 | constrain='yes' 171 | ;; 172 | --values-only) 173 | values_only='yes' 174 | ;; 175 | --with-handbrake) 176 | deprecated "$1" 177 | ;; 178 | -*) 179 | syntax_error "unrecognized option: $1" 180 | ;; 181 | *) 182 | break 183 | ;; 184 | esac 185 | shift 186 | done 187 | 188 | readonly input="$1" 189 | 190 | if [ ! "$input" ]; then 191 | syntax_error 'too few arguments' 192 | fi 193 | 194 | if [ ! -e "$input" ]; then 195 | die "input not found: $input" 196 | fi 197 | 198 | if ! $(which HandBrakeCLI >/dev/null); then 199 | die 'executable not in $PATH: HandBrakeCLI' 200 | fi 201 | 202 | if ! $(which mplayer >/dev/null); then 203 | die 'executable not in $PATH: mplayer' 204 | fi 205 | 206 | if [ "$title" == '0' ]; then 207 | echo "Scanning: $input" >&2 208 | samples='2' 209 | else 210 | samples='1' 211 | fi 212 | 213 | media_info="$(HandBrakeCLI --title $title --scan --previews $samples:0 --input "$input" 2>&1)" 214 | 215 | if [ "$debug" ]; then 216 | echo "$media_info" >&2 217 | fi 218 | 219 | if [ "$title" == '0' ]; then 220 | readonly formatted_titles_info="$(echo "$media_info" | 221 | sed -n '/^+ title /,$p' | 222 | sed '/^ + autocrop: /d;/^ + support /d;/^HandBrake/,$d;s/\(^ *\)+ \(.*$\)/\1\2/')" 223 | 224 | if [ ! "$formatted_titles_info" ]; then 225 | die "no media title available in: $input" 226 | fi 227 | 228 | echo "$formatted_titles_info" 229 | exit 230 | fi 231 | 232 | if [ ! "$(echo "$media_info" | sed -n '/^+ title /,$p')" ]; then 233 | echo "$program: \`title $title\` not found in: $input" >&2 234 | echo "Try \`$program --title 0 [FILE|DIRECTORY]\` to scan for titles." >&2 235 | echo "Try \`$program --help\` for more information." >&2 236 | exit 1 237 | fi 238 | 239 | readonly size_array=($(echo "$media_info" | sed -n 's/^ + size: \([0-9]\{1,\}\)x\([0-9]\{1,\}\).*$/\1 \2/p')) 240 | 241 | if ((${#size_array[*]} != 2)); then 242 | die "no video size information in: $input" 243 | fi 244 | 245 | readonly width="${size_array[0]}" 246 | readonly height="${size_array[1]}" 247 | 248 | readonly duration_array=($(echo "$media_info" | 249 | sed -n 's/^ + duration: \([0-9][0-9]\):\([0-9][0-9]\):\([0-9][0-9]\)$/ \1 \2 \3 /p' | 250 | sed 's/ 0/ /g')) 251 | 252 | if ((${#duration_array[*]} != 3)); then 253 | die "no duration information in: $input" 254 | fi 255 | 256 | readonly duration="$(((duration_array[0] * 60 * 60) + (duration_array[1] * 60) + duration_array[2]))" 257 | 258 | if (($duration < 2)); then 259 | die "duration too short in: $input" 260 | fi 261 | 262 | max_step="$((max_step * 60))" 263 | 264 | step="$((duration / 12))" 265 | 266 | if (($step < 1)); then 267 | step='1' 268 | 269 | elif (($step > $max_step)); then 270 | step="$max_step" 271 | fi 272 | 273 | samples="$(((duration / step) + 1))" 274 | 275 | echo "Detecting: $input" >&2 276 | 277 | if [ -f "$input" ]; then 278 | echo 'Scanning with `HandBrakeCLI`...' >&2 279 | fi 280 | 281 | media_info="$(HandBrakeCLI --title $title --scan --previews $samples:0 --input "$input" 2>&1)" 282 | 283 | readonly autocrop_array=($(echo "$media_info" | 284 | sed -n 's|^ + autocrop: \([0-9/]*\)$|\1|p' | 285 | sed 's|/| |g')) 286 | 287 | if ((${#autocrop_array[*]} != 4)); then 288 | die "no autocrop information in: $input" 289 | fi 290 | 291 | if [ "$debug" ]; then 292 | echo "${autocrop_array[*]}" | sed 's/ /:/g' >&2 293 | fi 294 | 295 | crop_x="${autocrop_array[2]}" 296 | crop_y="${autocrop_array[0]}" 297 | crop_width="$((width - crop_x - ${autocrop_array[3]}))" 298 | crop_height="$((height - crop_y - ${autocrop_array[1]}))" 299 | 300 | calculate_crop 301 | 302 | first_mplayer_crop="$mplayer_crop" 303 | first_handbrake_crop="$handbrake_crop" 304 | 305 | if [ -f "$input" ]; then 306 | crop_width='0' 307 | crop_height='0' 308 | crop_x="$width" 309 | crop_y="$height" 310 | 311 | last_line='' 312 | last_timestamp="$(date +%s)" 313 | 314 | echo 'Scanning with `mplayer`...' >&2 315 | 316 | for start in $(seq $step $step $((duration - step))); do 317 | 318 | while read line; do 319 | 320 | if [ ! "$line" ]; then 321 | continue 322 | fi 323 | 324 | if [ "$line" != "$last_line" ]; then 325 | 326 | if [ "$debug" ]; then 327 | echo "$line" >&2 328 | else 329 | timestamp="$(date +%s)" 330 | 331 | if ((($timestamp - $last_timestamp) >= 5)); then 332 | last_timestamp="$timestamp" 333 | echo 'Scanning with `mplayer`...' >&2 334 | fi 335 | fi 336 | 337 | line_array=($(echo "$line" | sed 's/:/ /g')) 338 | 339 | if (($crop_width < ${line_array[0]})); then 340 | crop_width="${line_array[0]}" 341 | fi 342 | 343 | if (($crop_height < ${line_array[1]})); then 344 | crop_height="${line_array[1]}" 345 | fi 346 | 347 | if (($crop_x > ${line_array[2]})); then 348 | crop_x="${line_array[2]}" 349 | fi 350 | 351 | if (($crop_y > ${line_array[3]})); then 352 | crop_y="${line_array[3]}" 353 | fi 354 | fi 355 | 356 | last_line="$line" 357 | done < <( 358 | mplayer -quiet -benchmark -vo null -ao null -vf cropdetect=24:2 "$input" -ss $start -frames 10 2>/dev/null | 359 | sed -n 's/^.*crop=\([0-9]\{1,\}:[0-9]\{1,\}:[0-9]\{1,\}:[0-9]\{1,\}\).*$/\1/p' 360 | ) 361 | done 362 | 363 | calculate_crop 364 | 365 | if [ "$mplayer_crop" != "$first_mplayer_crop" ] || [ "$handbrake_crop" != "$first_handbrake_crop" ]; then 366 | echo 'Results differ.' >&2 367 | 368 | if [ "$values_only" ]; then 369 | echo "From \`HandBrakeCLI\`: $first_handbrake_crop" >&2 370 | echo "From \`mplayer\`: $handbrake_crop" >&2 371 | exit 1 372 | fi 373 | 374 | echo 375 | echo '# From `HandBrakeCLI`:' 376 | print_commands "$first_mplayer_crop" "$first_handbrake_crop" 377 | echo '# From `mplayer`:' 378 | else 379 | echo 'Results are identical.' >&2 380 | fi 381 | fi 382 | 383 | if [ "$values_only" ]; then 384 | echo "$handbrake_crop" 385 | exit 386 | fi 387 | 388 | print_commands "$mplayer_crop" "$handbrake_crop" 389 | -------------------------------------------------------------------------------- /query-handbrake-log.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # query-handbrake-log.sh 4 | # 5 | # Copyright (c) 2014-2024 Lisa Melton 6 | # 7 | 8 | about() { 9 | cat <&2 46 | echo "Try \`$program --help\` for more information." >&2 47 | exit 1 48 | } 49 | 50 | die() { 51 | echo "$program: $1" >&2 52 | exit ${2:-1} 53 | } 54 | 55 | readonly program="$(basename "$0")" 56 | 57 | case $1 in 58 | --help) 59 | usage 60 | ;; 61 | --version) 62 | about 63 | ;; 64 | esac 65 | 66 | if (($# < 1)); then 67 | syntax_error 'too few arguments' 68 | fi 69 | 70 | readonly info="$1" 71 | 72 | case $info in 73 | time|bitrate|ratefactor) 74 | sort_options='--numeric-sort' 75 | ;; 76 | speed) 77 | sort_options='--numeric-sort --reverse' 78 | ;; 79 | *) 80 | syntax_error "unrecognized information type: $1" 81 | ;; 82 | esac 83 | shift 84 | 85 | while [ "$1" ]; do 86 | case $1 in 87 | --reverse) 88 | case $info in 89 | time|bitrate|ratefactor) 90 | sort_options='--numeric-sort --reverse' 91 | ;; 92 | speed) 93 | sort_options='--numeric-sort' 94 | ;; 95 | esac 96 | ;; 97 | --unsorted) 98 | sort_options='' 99 | ;; 100 | -*) 101 | syntax_error "unrecognized option: $1" 102 | ;; 103 | *) 104 | break 105 | ;; 106 | esac 107 | shift 108 | done 109 | 110 | if (($# < 1)); then 111 | syntax_error 'too few arguments' 112 | fi 113 | 114 | logs=() 115 | directory_count='0' 116 | last_directory='' 117 | 118 | while [ "$1" ]; do 119 | 120 | if [ ! -e "$1" ]; then 121 | die "input not found: $1" 122 | fi 123 | 124 | if [ -d "$1" ]; then 125 | directory_logs=("$1"/*.log) 126 | directory_count="$((directory_count + 1))" 127 | last_directory="$(cd "$1" 2>/dev/null && pwd)" 128 | 129 | if ((${#directory_logs[*]} == 1)) && [ "$(basename "${directory_logs[0]}")" == '*.log' ]; then 130 | die "\`.log\` files not found: $1" 131 | else 132 | logs=("${logs[@]}" "${directory_logs[@]}") 133 | fi 134 | else 135 | file_directory="$(cd "$(dirname "$1")" 2>/dev/null && pwd)" 136 | 137 | if [ "$last_directory" != "$file_directory" ]; then 138 | directory_count="$((directory_count + 1))" 139 | last_directory="$file_directory" 140 | fi 141 | 142 | logs=("${logs[@]}" "$1") 143 | fi 144 | 145 | shift 146 | done 147 | 148 | for item in "${logs[@]}"; do 149 | video_name="$(basename "$item" | sed 's/\.log$//')" 150 | 151 | if (($directory_count > 1)); then 152 | video_name="$video_name ($(dirname "$item"))" 153 | fi 154 | 155 | case $info in 156 | time|speed) 157 | fps="$(grep 'average encoding speed' "$item" | sed 's/^.* \([0-9.]*\) fps$/\1/')" 158 | 159 | if [ ! "$fps" ]; then 160 | 161 | if [ "$info" == 'time' ]; then 162 | echo "00:00:00 $video_name" 163 | else 164 | echo "00.000000 fps $video_name" 165 | fi 166 | 167 | continue 168 | fi 169 | 170 | if [ "$(echo "$fps" | wc -l | sed 's/^[^0-9]*//')" == '2' ]; then 171 | 172 | # Calculate single `fps` result from two-pass `.log` file. 173 | # 174 | pass_1_fps="$(echo "$fps" | sed -n 1p)" 175 | pass_2_fps="$(echo "$fps" | sed -n 2p)" 176 | 177 | fps="$(ruby -e 'printf "%.6f", (1 / ((1 / '$pass_1_fps') + (1 / '$pass_2_fps')))')" 178 | fi 179 | 180 | if [ "$info" == 'time' ]; then 181 | duration="$(grep '+ duration: ' "$item" | sed 's/^.* \([0-9:]*\)$/\1/')" 182 | duration=($(echo " $duration " | sed 's/:/ /g;s/ 0/ /g')) 183 | duration="$(((duration[0] * 60 * 60) + (duration[1] * 60) + duration[2]))" 184 | 185 | rate="$(grep '+ frame rate: ' "$item")" 186 | 187 | if [ ! "$rate" ]; then 188 | echo "00:00:00 $video_name" 189 | continue 190 | fi 191 | 192 | rate="$(echo "$rate" | sed '$!d;s/^.*+ frame rate: //;s/^.* -> constant //;s/ fps -> .*$//;s/ fps$//')" 193 | 194 | duration="$(ruby -e 'printf "%.0f", (('$duration' * '$rate') / '$fps')')" 195 | 196 | ruby -e 'printf "%02d:%02d:%02d '"$video_name"'\n", '$((duration / (60 * 60)))', '$(((duration / 60) % 60))', '$((duration % 60)) 197 | else 198 | echo "$fps fps $video_name" 199 | fi 200 | ;; 201 | bitrate) 202 | kbps="$(grep 'mux: track 0' "$item")" 203 | 204 | if [ ! "$kbps" ]; then 205 | echo "0000.00 kbps $video_name" 206 | continue 207 | fi 208 | 209 | echo "$(echo "$kbps" | sed 's/^.* \([0-9.]* kbps\).*$/\1/') $video_name" 210 | ;; 211 | ratefactor) 212 | qp="$(grep 'x26[45] \[info\]: frame P:' "$item")" 213 | 214 | if [ ! "$qp" ]; then 215 | echo "00.00 $video_name" 216 | continue 217 | fi 218 | 219 | echo "$(echo "$qp" | sed '$!d;s/^.* QP://;s/ *size:.*$//') $video_name" 220 | ;; 221 | esac 222 | 223 | done | 224 | 225 | if [ "$sort_options" ]; then 226 | sort $sort_options 227 | else 228 | cat 229 | fi 230 | -------------------------------------------------------------------------------- /transcode-video.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # transcode-video.sh 4 | # 5 | # Copyright (c) 2013-2024 Lisa Melton 6 | # 7 | 8 | about() { 9 | cat <&2 174 | echo "Try \`$program --help\` for more information." >&2 175 | exit 1 176 | } 177 | 178 | die() { 179 | echo "$program: $1" >&2 180 | exit ${2:-1} 181 | } 182 | 183 | deprecated() { 184 | echo "$program: deprecated option: $1" >&2 185 | } 186 | 187 | deprecated_and_replaced() { 188 | deprecated $1 189 | echo "$program: use this option instead: $2" >&2 190 | } 191 | 192 | escape_string() { 193 | echo "$1" | sed "s/'/'\\\''/g;s/^\(.*\)$/'\1'/" 194 | } 195 | 196 | readonly program="$(basename "$0")" 197 | 198 | # OPTIONS 199 | # 200 | case $1 in 201 | --help|-h|--fullhelp) 202 | usage 203 | ;; 204 | --version) 205 | about 206 | ;; 207 | esac 208 | 209 | media_title='1' 210 | section_options='' 211 | output='' 212 | output_prefix='' 213 | container_format='mp4' 214 | default_max_bitrate_2160p='10000' 215 | default_max_bitrate_1080p='5000' 216 | default_max_bitrate_720p='4000' 217 | default_max_bitrate_480p='2000' 218 | preset='medium' 219 | crop_values='0:0:0:0' 220 | constrain_width='4096' 221 | constrain_height='2304' 222 | custom_width='' 223 | custom_height='' 224 | frame_rate_options='' 225 | main_audio_track='1' 226 | main_audio_track_name='' 227 | single_main_audio='' 228 | extra_audio_tracks=() 229 | add_all_audio='' 230 | allow_ac3='' 231 | allow_dts='' 232 | allow_surround='yes' 233 | ac3_bitrate='384' 234 | pass_ac3_bitrate='448' 235 | copy_ac3='' 236 | copy_all_ac3='' 237 | copy_audio_names='' 238 | burned_subtitle_track='' 239 | auto_burn='yes' 240 | extra_subtitle_tracks=() 241 | add_all_subtitles='' 242 | find_forced='' 243 | burned_srt_file='' 244 | extra_srt_files=() 245 | tune_options='' 246 | max_bitrate='' 247 | vbv_bufsize='' 248 | extra_encopts_options='' 249 | rate_factor='16' 250 | max_rate_factor='25' 251 | filter_options='' 252 | auto_deinterlace='yes' 253 | passthru_options='' 254 | chapter_names_file='' 255 | write_log='yes' 256 | debug='' 257 | 258 | while [ "$1" ]; do 259 | case $1 in 260 | --title) 261 | media_title="$(printf '%.0f' "$2" 2>/dev/null)" 262 | shift 263 | 264 | if (($media_title < 0)); then 265 | die "invalid media title number: $media_title" 266 | fi 267 | ;; 268 | --chapters|--start-at|--stop-at) 269 | section_options="$section_options $1 $2" 270 | shift 271 | ;; 272 | --output|-o) 273 | output="$2" 274 | shift 275 | 276 | if [ -d "$output" ]; then 277 | output_prefix="$(echo "$output" | sed 's|/$||')/" 278 | output='' 279 | else 280 | case $output in 281 | *.mp4|*.mkv|*.m4v) 282 | container_format="${output: -3}" 283 | ;; 284 | *) 285 | die "unsupported filename extension: $output" 286 | ;; 287 | esac 288 | fi 289 | ;; 290 | --mkv|--m4v) 291 | container_format="${1:2}" 292 | 293 | if [ "$output" ]; then 294 | output="${output%.*}.$container_format" 295 | fi 296 | ;; 297 | --preset|--veryfast|--faster|--fast|--slow|--slower|--veryslow) 298 | 299 | if [ "$1" == '--preset' ]; then 300 | preset="$2" 301 | shift 302 | else 303 | preset="${1:2}" 304 | fi 305 | ;; 306 | --big|--better) 307 | [ "$1" == '--better' ] && deprecated_and_replaced "$1" '--big' 308 | default_max_bitrate_2160p='16000' 309 | default_max_bitrate_1080p='8000' 310 | default_max_bitrate_720p='6000' 311 | default_max_bitrate_480p='3000' 312 | ac3_bitrate='640' 313 | ;; 314 | --crop) 315 | crop_values="$2" 316 | shift 317 | ;; 318 | --480p) 319 | constrain_width='854' 320 | constrain_height='480' 321 | ;; 322 | --720p|--resize) 323 | [ "$1" == '--resize' ] && deprecated_and_replaced "$1" '--720p' 324 | constrain_width='1280' 325 | constrain_height='720' 326 | ;; 327 | --1080p) 328 | constrain_width='1920' 329 | constrain_height='1080' 330 | ;; 331 | --2160p) 332 | constrain_width='3840' 333 | constrain_height='2160' 334 | ;; 335 | --width) 336 | custom_width="$(printf '%.0f' "$2" 2>/dev/null)" 337 | shift 338 | 339 | if (($custom_width < 1)); then 340 | die "invalid custom width: $custom_width" 341 | fi 342 | ;; 343 | --height) 344 | custom_height="$(printf '%.0f' "$2" 2>/dev/null)" 345 | shift 346 | 347 | if (($custom_height < 1)); then 348 | die "invalid custom height: $custom_height" 349 | fi 350 | ;; 351 | --rate) 352 | frame_rate_argument="$2" 353 | shift 354 | 355 | frame_rate_options="--rate $(printf '%.3f' "$(echo "$frame_rate_argument" | sed 's/,.*$//')" 2>/dev/null | sed 's/0*$//;s/\.$//')" 356 | 357 | if [[ "$frame_rate_argument" =~ ',limited'$ ]]; then 358 | frame_rate_options="$frame_rate_options --pfr" 359 | 360 | elif [[ "$frame_rate_argument" =~ ',' ]]; then 361 | die "invalid frame rate argument: $frame_rate_argument" 362 | fi 363 | ;; 364 | --audio) 365 | audio_track_argument="$2" 366 | shift 367 | 368 | main_audio_track="$(printf '%.0f' "$(echo "$audio_track_argument" | sed 's/,.*$//')" 2>/dev/null)" 369 | 370 | if (($main_audio_track < 1)); then 371 | die "invalid main audio track: $main_audio_track" 372 | fi 373 | 374 | if [[ "$audio_track_argument" =~ ',' ]]; then 375 | main_audio_track_name="$(echo "$audio_track_argument" | sed 's/^[^,]*,//')" 376 | else 377 | main_audio_track_name='' 378 | fi 379 | ;; 380 | --single) 381 | single_main_audio='yes' 382 | ;; 383 | --add-audio) 384 | extra_audio_tracks=("${extra_audio_tracks[@]}" "$2") 385 | shift 386 | 387 | add_all_audio='' 388 | ;; 389 | --add-all-audio) 390 | add_all_audio="$2" 391 | shift 392 | 393 | case $add_all_audio in 394 | single|double) 395 | ;; 396 | *) 397 | syntax_error "unsupported audio width: $add_all_audio" 398 | ;; 399 | esac 400 | 401 | extra_audio_tracks=() 402 | ;; 403 | --allow-ac3) 404 | allow_ac3='yes' 405 | allow_surround='yes' 406 | ;; 407 | --allow-dts) 408 | allow_ac3='yes' 409 | allow_dts='yes' 410 | allow_surround='yes' 411 | ;; 412 | --no-surround|--no-ac3) 413 | [ "$1" == '--no-ac3' ] && deprecated_and_replaced "$1" '--no-surround' 414 | allow_ac3='' 415 | allow_dts='' 416 | allow_surround='' 417 | copy_ac3='' 418 | copy_all_ac3='' 419 | ;; 420 | --ac3) 421 | ac3_bitrate="$2" 422 | shift 423 | 424 | case $ac3_bitrate in 425 | 384|448|640) 426 | ;; 427 | *) 428 | syntax_error "unsupported AC-3 audio bitrate: $ac3_bitrate" 429 | ;; 430 | esac 431 | ;; 432 | --pass-ac3) 433 | pass_ac3_bitrate="$2" 434 | shift 435 | 436 | case $pass_ac3_bitrate in 437 | 384|448|640) 438 | ;; 439 | *) 440 | syntax_error "unsupported AC-3 audio passthru bitrate: $pass_ac3_bitrate" 441 | ;; 442 | esac 443 | ;; 444 | --copy-ac3) 445 | copy_ac3='yes' 446 | ;; 447 | --copy-all-ac3) 448 | copy_ac3='yes' 449 | copy_all_ac3='yes' 450 | ;; 451 | --copy-audio-names) 452 | copy_audio_names='yes' 453 | ;; 454 | --burn) 455 | burned_subtitle_track="$(printf '%.0f' "$2" 2>/dev/null)" 456 | shift 457 | 458 | if (($burned_subtitle_track < 1)); then 459 | die "invalid burn subtitle track: $burned_subtitle_track" 460 | fi 461 | 462 | burned_srt_file='' 463 | find_forced='' 464 | ;; 465 | --no-auto-burn) 466 | auto_burn='' 467 | ;; 468 | --add-subtitle) 469 | extra_subtitle_tracks=("${extra_subtitle_tracks[@]}" "$2") 470 | shift 471 | 472 | add_all_subtitles='' 473 | ;; 474 | --add-all-subtitles) 475 | add_all_subtitles='yes' 476 | extra_subtitle_tracks=() 477 | ;; 478 | --find-forced) 479 | find_forced="$2" 480 | shift 481 | 482 | case $find_forced in 483 | burn|add) 484 | ;; 485 | *) 486 | syntax_error "invalid find forced argument: $find_forced" 487 | ;; 488 | esac 489 | 490 | burned_subtitle_track='' 491 | auto_burn='' 492 | burned_srt_file='' 493 | ;; 494 | --burn-srt) 495 | burned_srt_file="$2" 496 | shift 497 | 498 | burned_subtitle_track='' 499 | auto_burn='' 500 | find_forced='' 501 | ;; 502 | --add-srt|--srt) 503 | [ "$1" == '--srt' ] && deprecated_and_replaced "$1" '--add-srt' 504 | extra_srt_files=("${extra_srt_files[@]}" "$2") 505 | shift 506 | ;; 507 | --tune) 508 | tune_options="$tune_options --encoder-tune $2" 509 | shift 510 | ;; 511 | --max|--vbv-maxrate|--abr) 512 | [ "$1" == '--abr' ] && deprecated_and_replaced "$1" '--max' 513 | max_bitrate="$(printf '%.0f' "$2" 2>/dev/null)" 514 | shift 515 | 516 | if (($max_bitrate < 1)); then 517 | die "invalid maximum video bitrate: $max_bitrate" 518 | fi 519 | ;; 520 | --buffer|--vbv-bufsize) 521 | vbv_bufsize="$(printf '%.0f' "$2" 2>/dev/null)" 522 | shift 523 | 524 | if (($vbv_bufsize < 1)); then 525 | die "invalid video buffer size: $vbv_bufsize" 526 | fi 527 | ;; 528 | --add-encopts) 529 | extra_encopts_options="$2" 530 | shift 531 | ;; 532 | --crf) 533 | rate_factor="$(printf '%.2f' "$2" 2>/dev/null | sed 's/0*$//;s/\.$//')" 534 | shift 535 | 536 | if (($rate_factor < 0)); then 537 | die "invalid constant rate factor: $rate_factor" 538 | fi 539 | ;; 540 | --crf-max) 541 | max_rate_factor="$2" 542 | shift 543 | 544 | case $max_rate_factor in 545 | none) 546 | max_rate_factor='' 547 | ;; 548 | *) 549 | max_rate_factor="$(printf '%.2f' "$max_rate_factor" 2>/dev/null | sed 's/0*$//;s/\.$//')" 550 | 551 | if (($max_rate_factor < 0)); then 552 | die "invalid maximum constant rate factor: $max_rate_factor" 553 | fi 554 | ;; 555 | esac 556 | ;; 557 | --filter) 558 | filter="$2" 559 | shift 560 | 561 | filter_name="$(echo "$filter" | sed 's/=.*$//')" 562 | 563 | case $filter_name in 564 | deinterlace|decomb|detelecine) 565 | auto_deinterlace='' 566 | ;; 567 | denoise|nlmeans|nlmeans-tune|deblock|rotate|grayscale) 568 | ;; 569 | *) 570 | syntax_error "unsupported video filter: $filter_name" 571 | ;; 572 | esac 573 | 574 | filter_options="$filter_options --$filter" 575 | ;; 576 | --angle|--start-at|--stop-at|--normalize-mix|--drc|--gain) 577 | passthru_options="$passthru_options $1 $2" 578 | shift 579 | ;; 580 | --no-opencl|--optimize|--use-opencl|--use-hwd) 581 | passthru_options="$passthru_options $1" 582 | ;; 583 | --chapter-names) 584 | chapter_names_file="$2" 585 | shift 586 | ;; 587 | --no-log) 588 | write_log='' 589 | ;; 590 | --debug) 591 | debug='yes' 592 | ;; 593 | --hq) 594 | deprecated "$1" 595 | ;; 596 | --with-original-audio) 597 | deprecated_and_replaced "$1" '--allow-dts' 598 | allow_dts='yes' 599 | audio_track="$(printf '%.0f' "$1" 2>/dev/null)" 600 | 601 | if (($audio_track > 0)); then 602 | main_audio_track="$audio_track" 603 | shift 604 | fi 605 | ;; 606 | --detelecine) 607 | deprecated_and_replaced "$1" '--filter detelecine' 608 | filter_options="$filter_options --detelecine" 609 | ;; 610 | --no-auto-detelecine) 611 | deprecated "$1" 612 | ;; 613 | --srt-burn) 614 | deprecated "$1" 615 | srt_number="$(printf '%.0f' "$2" 2>/dev/null)" 616 | 617 | if (($srt_number > 0)); then 618 | shift 619 | fi 620 | ;; 621 | -*) 622 | syntax_error "unrecognized option: $1" 623 | ;; 624 | *) 625 | break 626 | ;; 627 | esac 628 | shift 629 | done 630 | 631 | # INPUT 632 | # 633 | readonly input="$1" 634 | 635 | if [ ! "$input" ]; then 636 | syntax_error 'too few arguments' 637 | fi 638 | 639 | if [ ! -e "$input" ]; then 640 | die "input not found: $input" 641 | fi 642 | 643 | if ! $(which HandBrakeCLI >/dev/null); then 644 | die 'executable not in $PATH: HandBrakeCLI' 645 | fi 646 | 647 | readonly version="$(HandBrakeCLI --preset-list 2>&1)" 648 | 649 | if [ ! "$version" ]; then 650 | die "can't determine HandBrakeCLI version" 651 | fi 652 | 653 | readonly release_version="$(echo "$version" | 654 | sed -n 's/^HandBrake \([0-9]\{1,\}\)\.\([0-9]\{1,\}\)\.\([0-9]\{1,\}\) .*$/\1 \2 \3/p' | 655 | sed 's/ 0\([0-9]\)/ \1/g')" 656 | 657 | if [ "$release_version" ]; then 658 | readonly version_array=($release_version) 659 | 660 | if ((((${version_array[0]} * 10) + ${version_array[1]}) < 10)); then 661 | die 'HandBrake version 0.10.0 or later is required' 662 | fi 663 | else 664 | readonly svn_version="$(echo "$version" | 665 | sed -n 's/^HandBrake svn\([0-9]\{1,\}\) .*$/\1/p')" 666 | 667 | if [ "$svn_version" ]; then 668 | 669 | if (($svn_version < 6536)); then 670 | die 'HandBrake version 0.10.0 or later is required' 671 | fi 672 | else 673 | die "can't determine HandBrakeCLI version" 674 | fi 675 | fi 676 | 677 | if [ "$media_title" == '0' ]; then 678 | echo "Scanning: $input" >&2 679 | fi 680 | 681 | # Leverage `HandBrakeCLI` scan mode to extract all file- or directory-based 682 | # media information. Significantly speed up scan with `--previews 2:0` option 683 | # and argument. 684 | # 685 | readonly media_info="$(HandBrakeCLI --title $media_title --scan --previews 2:0 --input "$input" 2>&1)" 686 | 687 | if [ "$media_title" == '0' ]; then 688 | echo "$media_info" 689 | exit 690 | fi 691 | 692 | if [ "$debug" ]; then 693 | echo "$media_info" >&2 694 | fi 695 | 696 | if ! $(echo "$media_info" | grep -q '^+ title '$media_title':$'); then 697 | echo "$program: media title number $media_title not found in: $input" >&2 698 | echo "Try \`$program --title 0 [FILE|DIRECTORY]\` to scan for media titles." >&2 699 | echo "Try \`$program --help\` for more information." >&2 700 | exit 1 701 | fi 702 | 703 | if [ ! "$output" ]; then 704 | output="$output_prefix$(basename "$input" | sed 's/\.[0-9A-Za-z]\{1,\}$//').$container_format" 705 | fi 706 | 707 | if [ -e "$output" ]; then 708 | die "output file already exists: $output" 709 | fi 710 | 711 | if [ "$crop_values" == 'detect' ]; then 712 | 713 | if ! $(which detect-crop.sh >/dev/null); then 714 | die 'script not in $PATH: detect-crop.sh' 715 | fi 716 | 717 | crop_values="$(detect-crop.sh --title $media_title --values-only "$input")" 718 | 719 | if [ ! "$crop_values" ]; then 720 | die "crop ambiguous or unavailable for: $input" 721 | fi 722 | fi 723 | 724 | # VIDEO 725 | # 726 | readonly size_array=($(echo "$media_info" | sed -n 's/^ + size: \([0-9]\{1,\}\)x\([0-9]\{1,\}\).*$/\1 \2/p')) 727 | 728 | if ((${#size_array[*]} != 2)); then 729 | die "video size not found: $input" 730 | fi 731 | 732 | width="${size_array[0]}" 733 | height="${size_array[1]}" 734 | 735 | if [ "$custom_width" ]; then 736 | 737 | if [ "$custom_width" == "$width" ]; then 738 | custom_width='' 739 | else 740 | width="$custom_width" 741 | fi 742 | fi 743 | 744 | if [ "$custom_height" ]; then 745 | 746 | if [ "$custom_height" == "$height" ]; then 747 | custom_height='' 748 | else 749 | height="$custom_height" 750 | fi 751 | fi 752 | 753 | if [ "$crop_values" != '0:0:0:0' ] && [ "$crop_values" != 'auto' ]; then 754 | readonly crop_array=($(echo "$crop_values" | 755 | sed -n 's/^\([0-9]\{1,\}\):\([0-9]\{1,\}\):\([0-9]\{1,\}\):\([0-9]\{1,\}\)$/ \1 \2 \3 \4 /p' | 756 | sed 's/ 0\([0-9]\)/ \1/g')) 757 | 758 | width="$((width - ${crop_array[2]} - ${crop_array[3]}))" 759 | height="$((height - ${crop_array[0]} - ${crop_array[1]}))" 760 | 761 | if (($width < 1)) || (($height < 1)); then 762 | die "invalid crop: $crop_values" 763 | fi 764 | fi 765 | 766 | if ((($width > $constrain_width)) || (($height > $constrain_height))); then 767 | size_options="--maxWidth $constrain_width --maxHeight $constrain_height" 768 | 769 | if [ "$custom_width" ] || [ "$custom_height" ]; then 770 | size_options="$size_options --custom-anamorphic" 771 | else 772 | size_options="$size_options --loose-anamorphic" 773 | fi 774 | 775 | adjusted_height="$(ruby -e 'printf "%.0f", '$height' * ('$constrain_width'.0 / '$width')')" 776 | adjusted_height=$((adjusted_height - (adjusted_height % 2))) 777 | 778 | if (($adjusted_height > $constrain_height)); then 779 | width="$(ruby -e 'printf "%.0f", '$width' * ('$constrain_height'.0 / '$height')')" 780 | width=$((width + (width % 2))) 781 | height="$constrain_height" 782 | else 783 | width="$constrain_width" 784 | height="$adjusted_height" 785 | fi 786 | 787 | elif [ "$custom_width" ] || [ "$custom_height" ]; then 788 | size_options='--custom-anamorphic' 789 | else 790 | size_options='--strict-anamorphic' 791 | fi 792 | 793 | if [ "$custom_height" ]; then 794 | size_options="--height $height $size_options" 795 | fi 796 | 797 | if [ "$custom_width" ]; then 798 | size_options="--width $width $size_options" 799 | fi 800 | 801 | # Limit `x264` video buffer verifier (VBV) size to values appropriate for 802 | # H.264 level with High profile: 803 | # 804 | # 300000 for level 5.1 (e.g. 2160p input) 805 | # 25000 for level 4.0 (e.g. Blu-ray input) 806 | # 17500 for level 3.1 (e.g. 720p input) 807 | # 12500 for level 3.0 (e.g. DVD input) 808 | # 809 | level_options='' 810 | 811 | if (($width > 1920)) || (($height > 1080)); then 812 | vbv_maxrate="$default_max_bitrate_2160p" 813 | max_bufsize='300000' 814 | 815 | elif (($width > 1280)) || (($height > 720)); then 816 | vbv_maxrate="$default_max_bitrate_1080p" 817 | max_bufsize='25000' 818 | 819 | case $preset in 820 | slow|slower|veryslow|placebo) 821 | level_options="--encoder-level 4.0" 822 | ;; 823 | esac 824 | 825 | # Test for total number of pixels instead of bounds so using `--480p` option, 826 | # with width up to 854 pixels, works like DVD input. 827 | # 828 | elif ((($width * $height) > (720 * 576))); then 829 | vbv_maxrate="$default_max_bitrate_720p" 830 | max_bufsize='17500' 831 | else 832 | vbv_maxrate="$default_max_bitrate_480p" 833 | max_bufsize='12500' 834 | fi 835 | 836 | if [ "$max_bitrate" ]; then 837 | vbv_maxrate="$max_bitrate" 838 | 839 | if (($vbv_maxrate > $max_bufsize)); then 840 | vbv_maxrate="$max_bufsize" 841 | fi 842 | 843 | elif [ -f "$input" ]; then 844 | readonly duration_array=($(echo "$media_info" | 845 | sed -n 's/^ + duration: \([0-9][0-9]\):\([0-9][0-9]\):\([0-9][0-9]\)$/ \1 \2 \3 /p' | 846 | sed 's/ 0/ /g')) 847 | 848 | if ((${#duration_array[*]} == 3)); then 849 | 850 | if $(stat --version 2>/dev/null | grep -q 'GNU'); then 851 | format_option='-c %s' 852 | else 853 | format_option='-f %z' 854 | fi 855 | 856 | # Calculate total bitrate from file size in bits divided by video 857 | # duration in seconds. 858 | # 859 | bitrate="$((($(stat -L $format_option "$input") * 8) / ((duration_array[0] * 60 * 60) + (duration_array[1] * 60) + duration_array[2])))" 860 | 861 | if [ "$bitrate" ]; then 862 | # Convert to kbps and round to nearest thousand. 863 | # 864 | bitrate="$((((bitrate / 1000) / 1000) * 1000))" 865 | 866 | if (($bitrate < $vbv_maxrate)); then 867 | readonly min_bitrate="$((vbv_maxrate / 2))" 868 | 869 | if (($bitrate < $min_bitrate)); then 870 | vbv_maxrate="$min_bitrate" 871 | else 872 | vbv_maxrate="$bitrate" 873 | fi 874 | fi 875 | fi 876 | fi 877 | fi 878 | 879 | if [ "$vbv_bufsize" ]; then 880 | 881 | if (($vbv_bufsize > $max_bufsize)); then 882 | vbv_bufsize="$max_bufsize" 883 | fi 884 | else 885 | # The `x264` video buffer verifier (VBV) size must always be less than the 886 | # maximum rate to maintain quality in constant rate factor (CRF) mode. 887 | # 888 | vbv_bufsize="$((vbv_maxrate / 2))" 889 | fi 890 | 891 | if [ ! "$frame_rate_options" ]; then 892 | frame_rate_options='--rate 30 --pfr' 893 | readonly frame_rate="$(echo "$media_info" | sed -n 's/^.* scan: 2 previews, .* \([0-9]\{1,\}\.[.0-9]\{1,\}\) fps, .*$/\1/p')" 894 | 895 | if [ ! "$frame_rate" ]; then 896 | die "no video frame rate information in: $input" 897 | fi 898 | 899 | readonly video_stream_info="$(echo "$media_info" | sed -n '/^ Stream #[^:]\{1,\}: Video: /p' | sed -n 1p)" 900 | force_frame_rate='' 901 | 902 | if [ "$video_stream_info" ]; then 903 | $(echo "$video_stream_info" | grep -q 'mpeg2video') && force_frame_rate='yes' 904 | else 905 | $(echo "$media_info" | grep -q '^ + vts ') && force_frame_rate='yes' 906 | fi 907 | 908 | if [ "$frame_rate" == '29.970' ]; then 909 | 910 | if [ "$force_frame_rate" ]; then 911 | frame_rate_options='--rate 23.976' 912 | 913 | elif [ "$auto_deinterlace" ]; then 914 | filter_options="$filter_options --deinterlace" 915 | fi 916 | 917 | elif [ "$force_frame_rate" ]; then 918 | 919 | case $frame_rate in 920 | '23.976'|'24.000'|'25.000') 921 | frame_rate_options="--rate $(echo "$frame_rate" | sed 's/\.000$//')" 922 | ;; 923 | esac 924 | fi 925 | fi 926 | 927 | # AUDIO 928 | # 929 | if (($pass_ac3_bitrate < $ac3_bitrate)); then 930 | pass_ac3_bitrate="$ac3_bitrate" 931 | fi 932 | 933 | if [ "$ac3_bitrate" == '640' ]; then 934 | ac3_bitrate='' 935 | fi 936 | 937 | track_index='1' 938 | 939 | audio_track_list='' 940 | audio_encoder_list='' 941 | audio_bitrate_list='' 942 | audio_track_name_list='' 943 | audio_track_name_edits=() 944 | 945 | if [ "$copy_audio_names" ]; then 946 | readonly all_audio_streams_info="$(echo "$media_info" | 947 | sed -n '/^ Stream #[^:]\{1,\}: Audio: /,$p' | 948 | sed '/^[^ ]\{1,\}.*$/,$d' | 949 | sed '/^ Stream #[^:]\{1,\}: Subtitle: /,$d' | 950 | sed '/^ Metadata:$/d' | 951 | sed '$!N;s/\n/ /')" 952 | fi 953 | 954 | readonly all_audio_tracks_info="$(echo "$media_info" | 955 | sed -n '/^ + audio tracks:$/,/^ + subtitle tracks:$/p' | 956 | sed -n '/^ + /p')" 957 | 958 | audio_track_info="$(echo "$all_audio_tracks_info" | sed -n ${main_audio_track}p)" 959 | 960 | if [ "$audio_track_info" ]; then 961 | audio_track_list="$main_audio_track" 962 | 963 | if $(HandBrakeCLI --help 2>/dev/null | grep -q 'ca_aac'); then 964 | aac_encoder='ca_aac' 965 | else 966 | aac_encoder='av_aac' 967 | fi 968 | 969 | if [ "$copy_audio_names" ] && [ ! "$main_audio_track_name" ]; then 970 | track_name="$(echo "$all_audio_streams_info" | 971 | sed -n ${main_audio_track}p | 972 | sed -n 's/^.* \{1,\}title \{1,\}: \(.*\)$/\1/p')" 973 | 974 | if [ "$track_name" ]; then 975 | main_audio_track_name="$track_name" 976 | fi 977 | fi 978 | 979 | sanitized_name="$(echo "$main_audio_track_name" | sed 's/,/_/g')" 980 | 981 | surround_audio_encoder='' 982 | surround_audio_bitrate='' 983 | stereo_audio_encoder="$aac_encoder" 984 | 985 | if [ "$copy_ac3" ] && [[ "$audio_track_info" =~ '(AC3)' ]]; then 986 | surround_audio_encoder='copy' 987 | 988 | elif (($(echo "$audio_track_info" | sed 's/^.*(\([0-9]\{1,\}\)\.\([0-9]\{1,\}\) ch).*$/\1\2/;s/^$/0/') > 20)); then 989 | 990 | if [ "$allow_surround" ]; then 991 | 992 | if ( [[ "$audio_track_info" =~ '(AC3)' ]] && ((($(echo "$audio_track_info" | sed -n 's/^.* \([0-9]\{1,\}\)bps$/\1/p' | sed 's/^$/640/') / 1000) <= $pass_ac3_bitrate)) ) || ( [ "$allow_dts" ] && [[ "$audio_track_info" =~ '(DTS' ]] ); then 993 | surround_audio_encoder='copy' 994 | else 995 | surround_audio_encoder='ac3' 996 | surround_audio_bitrate="$ac3_bitrate" 997 | fi 998 | fi 999 | 1000 | elif [[ "$audio_track_info" =~ '(AAC)' ]]; then 1001 | stereo_audio_encoder='copy' 1002 | fi 1003 | 1004 | if [ "$surround_audio_encoder" ] && [ ! "$single_main_audio" ]; then 1005 | audio_track_list="$main_audio_track,$main_audio_track" 1006 | audio_track_name_list="$sanitized_name,$sanitized_name" 1007 | 1008 | if [ "$container_format" == 'mkv' ]; then 1009 | audio_encoder_list="$surround_audio_encoder,$stereo_audio_encoder" 1010 | audio_bitrate_list="$surround_audio_bitrate," 1011 | else 1012 | audio_encoder_list="$stereo_audio_encoder,$surround_audio_encoder" 1013 | audio_bitrate_list=",$surround_audio_bitrate" 1014 | fi 1015 | 1016 | if [ "$sanitized_name" != "$main_audio_track_name" ]; then 1017 | audio_track_name_edits=(1,"$main_audio_track_name" 2,"$main_audio_track_name") 1018 | fi 1019 | 1020 | track_index='3' 1021 | else 1022 | audio_track_list="$main_audio_track" 1023 | audio_track_name_list="$sanitized_name" 1024 | 1025 | if [ "$surround_audio_encoder" ]; then 1026 | audio_encoder_list="$surround_audio_encoder" 1027 | audio_bitrate_list="$surround_audio_bitrate" 1028 | else 1029 | audio_encoder_list="$stereo_audio_encoder" 1030 | fi 1031 | 1032 | if [ "$sanitized_name" != "$main_audio_track_name" ]; then 1033 | audio_track_name_edits=(1,"$main_audio_track_name") 1034 | fi 1035 | 1036 | track_index='2' 1037 | fi 1038 | 1039 | if [ "$add_all_audio" ]; then 1040 | 1041 | if [ "$add_all_audio" == 'double' ]; then 1042 | width_prefix="$add_all_audio," 1043 | else 1044 | width_prefix='' 1045 | fi 1046 | 1047 | while read line; do 1048 | extra_audio_tracks=("${extra_audio_tracks[@]}" "$width_prefix$(echo "$line" | sed -n 's/^+ \([0-9]\{1,\}\), .*$/\1/p')") 1049 | 1050 | done < <(echo "$all_audio_tracks_info" | sed ${main_audio_track}d) 1051 | fi 1052 | 1053 | for item in "${extra_audio_tracks[@]}"; do 1054 | 1055 | if [ "$(echo "$item" | sed 's/,.*$//')" == 'double' ]; then 1056 | double_audio="$allow_surround" 1057 | item="$(echo "$item" | sed 's/^[^,]*,//')" 1058 | else 1059 | double_audio='' 1060 | fi 1061 | 1062 | track_number="$(printf '%.0f' "$(echo "$item" | sed 's/,.*$//')" 2>/dev/null)" 1063 | 1064 | if (($track_number < 1)); then 1065 | die "invalid additional audio track: $item" 1066 | fi 1067 | 1068 | audio_track_info="$(echo "$all_audio_tracks_info" | sed -n ${track_number}p)" 1069 | 1070 | if [ ! "$audio_track_info" ]; then 1071 | die "missing additional audio track: $input" 1072 | fi 1073 | 1074 | if [[ "$item" =~ ',' ]]; then 1075 | track_name="$(echo "$item" | sed 's/^[^,]*,//')" 1076 | 1077 | elif [ "$copy_audio_names" ]; then 1078 | track_name="$(echo "$all_audio_streams_info" | 1079 | sed -n ${track_number}p | 1080 | sed -n 's/^.* \{1,\}title \{1,\}: \(.*\)$/\1/p')" 1081 | else 1082 | track_name='' 1083 | fi 1084 | 1085 | sanitized_name="$(echo "$track_name" | sed 's/,/_/g')" 1086 | 1087 | surround_audio_encoder='' 1088 | surround_audio_bitrate='' 1089 | stereo_audio_encoder="$aac_encoder" 1090 | 1091 | if [ "$copy_all_ac3" ] && [[ "$audio_track_info" =~ '(AC3)' ]]; then 1092 | stereo_audio_encoder='copy' 1093 | 1094 | elif (($(echo "$audio_track_info" | sed 's/^.*(\([0-9]\{1,\}\)\.\([0-9]\{1,\}\) ch).*$/\1\2/;s/^$/0/') > 20)); then 1095 | 1096 | if [ "$allow_ac3" ] || [ "$double_audio" ]; then 1097 | 1098 | if ( [[ "$audio_track_info" =~ '(AC3)' ]] && ((($(echo "$audio_track_info" | sed -n 's/^.* \([0-9]\{1,\}\)bps$/\1/p' | sed 's/^$/640/') / 1000) <= $pass_ac3_bitrate)) ) || ( [ "$allow_dts" ] && [[ "$audio_track_info" =~ '(DTS' ]] ); then 1099 | surround_audio_encoder='copy' 1100 | else 1101 | surround_audio_encoder='ac3' 1102 | surround_audio_bitrate="$ac3_bitrate" 1103 | fi 1104 | fi 1105 | 1106 | elif [[ "$audio_track_info" =~ '(AAC)' ]]; then 1107 | stereo_audio_encoder='copy' 1108 | fi 1109 | 1110 | if [ "$surround_audio_encoder" ] && [ "$double_audio" ]; then 1111 | audio_track_list="$audio_track_list,$track_number,$track_number" 1112 | audio_track_name_list="$audio_track_name_list,$sanitized_name,$sanitized_name" 1113 | 1114 | if [ "$container_format" == 'mkv' ]; then 1115 | audio_encoder_list="$audio_encoder_list,$surround_audio_encoder,$stereo_audio_encoder" 1116 | audio_bitrate_list="$audio_bitrate_list,$surround_audio_bitrate," 1117 | else 1118 | audio_encoder_list="$audio_encoder_list,$stereo_audio_encoder,$surround_audio_encoder" 1119 | audio_bitrate_list="$audio_bitrate_list,,$surround_audio_bitrate" 1120 | fi 1121 | 1122 | if [ "$sanitized_name" != "$track_name" ]; then 1123 | audio_track_name_edits=("${audio_track_name_edits[@]}" "$track_index,$track_name" "$((track_index + 1)),$track_name") 1124 | fi 1125 | 1126 | track_index="$((track_index + 2))" 1127 | else 1128 | audio_track_list="$audio_track_list,$track_number" 1129 | audio_track_name_list="$audio_track_name_list,$sanitized_name" 1130 | 1131 | if [ "$surround_audio_encoder" ]; then 1132 | audio_encoder_list="$audio_encoder_list,$surround_audio_encoder" 1133 | audio_bitrate_list="$audio_bitrate_list,$surround_audio_bitrate" 1134 | else 1135 | audio_encoder_list="$audio_encoder_list,$stereo_audio_encoder" 1136 | audio_bitrate_list="$audio_bitrate_list," 1137 | fi 1138 | 1139 | if [ "$sanitized_name" != "$track_name" ]; then 1140 | audio_track_name_edits=("${audio_track_name_edits[@]}" "$track_index,$track_name") 1141 | fi 1142 | 1143 | track_index="$((track_index + 1))" 1144 | fi 1145 | done 1146 | 1147 | elif (($main_audio_track > 1)) || ((${#extra_audio_tracks[*]} > 0)); then 1148 | die "missing audio track: $input" 1149 | fi 1150 | 1151 | if [ "$audio_track_list" ]; then 1152 | audio_options="--audio $audio_track_list --aencoder $audio_encoder_list" 1153 | 1154 | if [ "$(echo "$audio_bitrate_list" | sed 's/,//g')" ]; then 1155 | audio_options="$audio_options --ab $audio_bitrate_list" 1156 | fi 1157 | 1158 | if [ "$(echo "$audio_track_name_list" | sed 's/,//g')" ]; then 1159 | audio_options="$audio_options --aname" 1160 | else 1161 | audio_track_name_list='' 1162 | fi 1163 | else 1164 | audio_options='' 1165 | fi 1166 | 1167 | # SUBTITLES 1168 | # 1169 | subtitle_track_list='' 1170 | 1171 | readonly all_subtitle_tracks_info="$(echo "$media_info" | 1172 | sed -n '/^ + subtitle tracks:$/,$p' | 1173 | sed -n '/^ + /p')" 1174 | 1175 | if [ "$burned_subtitle_track" ]; then 1176 | subtitle_track_info="$(echo "$all_subtitle_tracks_info" | sed -n ${burned_subtitle_track}p)" 1177 | 1178 | if [ ! "$subtitle_track_info" ]; then 1179 | die "missing subtitle track: $input" 1180 | fi 1181 | 1182 | subtitle_track_list="$burned_subtitle_track" 1183 | 1184 | elif [ "$auto_burn" ]; then 1185 | readonly all_subtitle_streams_info="$(echo "$media_info" | sed -n '/^ Stream #[^:]\{1,\}: Subtitle: /p')" 1186 | subtitle_track='1' 1187 | 1188 | while : ; do 1189 | subtitle_track_info="$(echo "$all_subtitle_tracks_info" | sed -n ${subtitle_track}p)" 1190 | 1191 | if [ ! "$subtitle_track_info" ]; then 1192 | break 1193 | fi 1194 | 1195 | if [[ "$(echo "$all_subtitle_streams_info" | sed -n ${subtitle_track}p)" =~ '(forced)' ]]; then 1196 | burned_subtitle_track="$subtitle_track" 1197 | subtitle_track_list="$burned_subtitle_track" 1198 | break 1199 | fi 1200 | 1201 | subtitle_track="$((subtitle_track + 1))" 1202 | done 1203 | 1204 | elif [ "$find_forced" ]; then 1205 | subtitle_track_list='scan' 1206 | 1207 | fi 1208 | 1209 | if [ "$add_all_subtitles" ]; then 1210 | 1211 | if [ "$burned_subtitle_track" ]; then 1212 | tracks_info="$(echo "$all_subtitle_tracks_info" | sed ${burned_subtitle_track}d)" 1213 | else 1214 | tracks_info="$all_subtitle_tracks_info" 1215 | fi 1216 | 1217 | while read line; do 1218 | extra_subtitle_tracks=("${extra_subtitle_tracks[@]}" "$(echo "$line" | sed -n 's/^+ \([0-9]\{1,\}\), .*$/\1/p')") 1219 | 1220 | done < <(echo "$tracks_info") 1221 | fi 1222 | 1223 | forced_subtitle_track_number='' 1224 | track_id='1' 1225 | 1226 | for item in "${extra_subtitle_tracks[@]}"; do 1227 | track_number="$(printf '%.0f' "$(echo "$item" | sed 's/^forced,//')" 2>/dev/null)" 1228 | 1229 | if (($track_number < 1)); then 1230 | die "invalid additional subtitle track: $item" 1231 | fi 1232 | 1233 | subtitle_track_info="$(echo "$all_subtitle_tracks_info" | sed -n ${track_number}p)" 1234 | 1235 | if [ ! "$subtitle_track_info" ]; then 1236 | die "missing additional subtitle track: $input" 1237 | fi 1238 | 1239 | if [ "$container_format" != 'mkv' ] && [[ "$subtitle_track_info" =~ '(PGS)' ]]; then 1240 | die "incompatible additional subtitle track for MP4 format: $track_number" 1241 | fi 1242 | 1243 | if [ ! "$subtitle_track_list" ]; then 1244 | subtitle_track_list="$track_number" 1245 | else 1246 | subtitle_track_list="$subtitle_track_list,$track_number" 1247 | fi 1248 | 1249 | if [[ "$item" =~ ^'forced,' ]] && [ ! "$find_forced" ]; then 1250 | 1251 | if [ "$track_number" == "$burned_subtitle_track" ]; then 1252 | die "forced subtitle track is already burned: $track_number" 1253 | fi 1254 | 1255 | if [ "$container_format" == 'mkv' ]; then 1256 | forced_subtitle_track_number="$track_id" 1257 | else 1258 | forced_subtitle_track_number="$track_index" 1259 | fi 1260 | fi 1261 | 1262 | track_id="$((track_id + 1))" 1263 | track_index="$((track_index + 1))" 1264 | done 1265 | 1266 | if [ "$subtitle_track_list" ]; then 1267 | subtitle_options="--subtitle $subtitle_track_list" 1268 | 1269 | if [ "$burned_subtitle_track" ]; then 1270 | subtitle_options="$subtitle_options --subtitle-burned" 1271 | 1272 | elif [ "$find_forced" ]; then 1273 | subtitle_options="$subtitle_options --subtitle-forced" 1274 | 1275 | if [ "$find_forced" == 'burn' ]; then 1276 | subtitle_options="$subtitle_options --subtitle-burned" 1277 | else 1278 | subtitle_options="$subtitle_options --subtitle-default" 1279 | fi 1280 | fi 1281 | else 1282 | subtitle_options='' 1283 | fi 1284 | 1285 | # OTHER SUBTITLES 1286 | # 1287 | tmp='' 1288 | 1289 | if [ "$burned_srt_file" ] || ((${#extra_srt_files[*]} > 0)); then 1290 | trap '[ "$tmp" ] && rm -rf "$tmp"' 0 1291 | trap '[ "$tmp" ] && rm -rf "$tmp"; exit 1' SIGHUP SIGINT SIGQUIT SIGTERM 1292 | 1293 | tmp="/tmp/${program}.$$" 1294 | mkdir -m 700 "$tmp" || exit 1 1295 | fi 1296 | 1297 | srt_file_list='' 1298 | srt_codeset_list='' 1299 | srt_offset_list='' 1300 | srt_lang_list='' 1301 | 1302 | if [ "$burned_srt_file" ]; then 1303 | srt_file="$burned_srt_file" 1304 | srt_offset='' 1305 | srt_codeset='' 1306 | 1307 | while [[ "$srt_file" =~ ',' ]]; do 1308 | srt_prefix="$(echo "$srt_file" | sed 's/,.*$//')" 1309 | 1310 | if [ ! "$srt_offset" ] && [[ "$srt_prefix" =~ ^[+-]?[0-9][0-9]*$ ]]; then 1311 | srt_offset="$(echo "$srt_prefix" | sed 's/^+//')" 1312 | srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')" 1313 | 1314 | elif [ ! "$srt_codeset" ] && [[ "$srt_prefix" =~ ^[0-9A-Za-z] ]] && [[ ! "$srt_prefix" =~ [\ /\\] ]] && [ ! -f "$srt_file" ]; then 1315 | srt_codeset="$srt_prefix" 1316 | srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')" 1317 | else 1318 | break 1319 | fi 1320 | done 1321 | 1322 | # Force filename expansion with `eval` but first escape the string 1323 | # to hide ", $, &, ', (, ), ;, <, >, \, ` and |. 1324 | srt_file="$(eval echo "$(echo "$srt_file" | sed 's/\(["$&'\''();<>\\`|]\)/\\\1/g')")" 1325 | 1326 | if [ ! "$srt_file" ]; then 1327 | syntax_error "missing burned subtitle filename" 1328 | fi 1329 | 1330 | if [ ! -f "$srt_file" ]; then 1331 | die "burned subtitle not found: $srt_file" 1332 | fi 1333 | 1334 | tmp_srt_file_link="$tmp/burned-subtitle.srt" 1335 | ln -s "$(cd "$(dirname "$srt_file")" 2>/dev/null && echo "$(pwd)/$(basename "$srt_file")")" "$tmp_srt_file_link" 1336 | 1337 | srt_file_list="$tmp_srt_file_link" 1338 | srt_codeset_list="$srt_codeset" 1339 | srt_offset_list="$srt_offset" 1340 | fi 1341 | 1342 | for item in "${extra_srt_files[@]}"; do 1343 | srt_file="$item" 1344 | srt_lang='' 1345 | srt_offset='' 1346 | srt_codeset='' 1347 | 1348 | while [[ "$srt_file" =~ ',' ]]; do 1349 | srt_prefix="$(echo "$srt_file" | sed 's/,.*$//')" 1350 | 1351 | if [ "$srt_prefix" == 'forced' ]; then 1352 | 1353 | if [ ! "$find_forced" ]; then 1354 | 1355 | if [ "$container_format" == 'mkv' ]; then 1356 | forced_subtitle_track_number="$track_id" 1357 | else 1358 | forced_subtitle_track_number="$track_index" 1359 | fi 1360 | fi 1361 | 1362 | srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')" 1363 | 1364 | elif [ ! "$srt_lang" ] && [[ "$srt_prefix" =~ ^[a-z][a-z][a-z]$ ]]; then 1365 | srt_lang="$srt_prefix" 1366 | srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')" 1367 | 1368 | elif [ ! "$srt_offset" ] && [[ "$srt_prefix" =~ ^[+-]?[0-9][0-9]*$ ]]; then 1369 | srt_offset="$(echo "$srt_prefix" | sed 's/^+//')" 1370 | srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')" 1371 | 1372 | elif [ ! "$srt_codeset" ] && [[ "$srt_prefix" =~ ^[0-9A-Za-z] ]] && [[ ! "$srt_prefix" =~ [\ /\\] ]] && [ ! -f "$srt_file" ]; then 1373 | srt_codeset="$srt_prefix" 1374 | srt_file="$(echo "$srt_file" | sed 's/^[^,]*,//')" 1375 | else 1376 | break 1377 | fi 1378 | done 1379 | 1380 | # Force filename expansion with `eval` but first escape the string 1381 | # to hide ", $, &, ', (, ), ;, <, >, \, ` and |. 1382 | srt_file="$(eval echo "$(echo "$srt_file" | sed 's/\(["$&'\''();<>\\`|]\)/\\\1/g')")" 1383 | 1384 | if [ ! "$srt_file" ]; then 1385 | syntax_error "missing subtitle filename" 1386 | fi 1387 | 1388 | if [ ! -f "$srt_file" ]; then 1389 | die "subtitle not found: $srt_file" 1390 | fi 1391 | 1392 | tmp_srt_file_link="$tmp/subtitle-$track_id.srt" 1393 | ln -s "$(cd "$(dirname "$srt_file")" 2>/dev/null && echo "$(pwd)/$(basename "$srt_file")")" "$tmp_srt_file_link" 1394 | 1395 | if [ ! "$srt_file_list" ]; then 1396 | srt_file_list="$tmp_srt_file_link" 1397 | srt_codeset_list="$srt_codeset" 1398 | srt_offset_list="$srt_offset" 1399 | srt_lang_list="$srt_lang" 1400 | else 1401 | srt_file_list="$srt_file_list,$tmp_srt_file_link" 1402 | srt_codeset_list="$srt_codeset_list,$srt_codeset" 1403 | srt_offset_list="$srt_offset_list,$srt_offset" 1404 | srt_lang_list="$srt_lang_list,$srt_lang" 1405 | fi 1406 | 1407 | track_id="$((track_id + 1))" 1408 | track_index="$((track_index + 1))" 1409 | done 1410 | 1411 | if [ "$srt_file_list" ]; then 1412 | srt_options="--srt-file $srt_file_list" 1413 | 1414 | if [ "$(echo "$srt_codeset_list" | sed 's/,//g')" ]; then 1415 | srt_options="$srt_options --srt-codeset $srt_codeset_list" 1416 | fi 1417 | 1418 | if [ "$(echo "$srt_offset_list" | sed 's/,//g')" ]; then 1419 | srt_options="$srt_options --srt-offset $srt_offset_list" 1420 | fi 1421 | 1422 | if [ "$(echo "$srt_lang_list" | sed 's/,//g')" ]; then 1423 | srt_options="$srt_options --srt-lang $srt_lang_list" 1424 | fi 1425 | 1426 | if [ "$burned_srt_file" ]; then 1427 | srt_options="$srt_options --srt-burn" 1428 | fi 1429 | else 1430 | srt_options='' 1431 | fi 1432 | 1433 | # OTHER OPTIONS 1434 | # 1435 | if [ "$media_title" == '1' ]; then 1436 | title_options='' 1437 | else 1438 | title_options="--title $media_title" 1439 | fi 1440 | 1441 | section_options="$(echo "$section_options" | sed 's/^ *//')" 1442 | 1443 | if [ "$chapter_names_file" ]; then 1444 | markers_options='--markers=' 1445 | else 1446 | markers_options='--markers' 1447 | fi 1448 | 1449 | if [ "$preset" == 'medium' ]; then 1450 | preset_options='' 1451 | else 1452 | preset_options="--encoder-preset $preset" 1453 | fi 1454 | 1455 | tune_options="$(echo "$tune_options" | sed 's/^ *//')" 1456 | 1457 | encopts_options="vbv-maxrate=$vbv_maxrate:vbv-bufsize=$vbv_bufsize" 1458 | 1459 | # Limit reference frames for playback compatibility with popular devices. 1460 | # 1461 | case $preset in 1462 | slower|veryslow|placebo) 1463 | encopts_options="ref=5:$encopts_options" 1464 | ;; 1465 | esac 1466 | 1467 | if [ "$max_rate_factor" ]; then 1468 | encopts_options="$encopts_options:crf-max=$max_rate_factor" 1469 | fi 1470 | 1471 | if [ "$extra_encopts_options" ]; then 1472 | encopts_options="$(echo "$encopts_options:$extra_encopts_options" | sed 's/:*$//;s/:\{1,\}/:/g')" 1473 | fi 1474 | 1475 | if [ "$crop_values" == 'auto' ]; then 1476 | crop_options='' 1477 | else 1478 | crop_options="--crop $crop_values" 1479 | fi 1480 | 1481 | filter_options="$(echo "$filter_options" | sed 's/^ *//;s/ *$//;s/ \{1,\}/ /g')" 1482 | 1483 | passthru_options="$(echo "$passthru_options" | sed 's/^ *//')" 1484 | 1485 | # DEBUG OUTPUT 1486 | # 1487 | if [ "$debug" ]; then 1488 | echo >&2 1489 | echo "title_options = $title_options" >&2 1490 | echo "section_options = $section_options" >&2 1491 | echo "markers_options = $markers_options" >&2 1492 | echo "chapter_names_file = $chapter_names_file" >&2 1493 | echo "preset_options = $preset_options" >&2 1494 | echo "tune_options = $tune_options" >&2 1495 | echo "encopts_options = $encopts_options" >&2 1496 | echo "level_options = $level_options" >&2 1497 | echo "rate_factor = $rate_factor" >&2 1498 | echo "frame_rate_options = $frame_rate_options" >&2 1499 | echo "audio_options = $audio_options" >&2 1500 | echo "audio_track_name_list = $audio_track_name_list" >&2 1501 | echo "crop_options = $crop_options" >&2 1502 | echo "size_options = $size_options" >&2 1503 | echo "filter_options = $filter_options" >&2 1504 | echo "subtitle_options = $subtitle_options" >&2 1505 | echo "srt_options = $srt_options" >&2 1506 | echo "passthru_options = $passthru_options" >&2 1507 | echo "input = $input" >&2 1508 | echo "output = $output" >&2 1509 | echo >&2 1510 | 1511 | command="$(echo "HandBrakeCLI $title_options $section_options $markers_options" | sed 's/ *$//;s/ \{1,\}/ /g')" 1512 | 1513 | if [ "$chapter_names_file" ]; then 1514 | command="$command$(escape_string "$chapter_names_file")" 1515 | fi 1516 | 1517 | command="$command $(echo "--encoder x264 $preset_options $tune_options --encopts $encopts_options $level_options --quality $rate_factor $frame_rate_options $audio_options" | sed 's/ *$//;s/ \{1,\}/ /g')" 1518 | 1519 | if [ "$audio_track_name_list" ]; then 1520 | command="$command $(escape_string "$audio_track_name_list")" 1521 | fi 1522 | 1523 | command="$command $(echo "$crop_options $size_options $filter_options $subtitle_options $srt_options $passthru_options" | sed 's/^ *//;s/ *$//;s/ \{1,\}/ /g') --input $(escape_string "$input") --output $(escape_string "$output")" 1524 | 1525 | if [ "$write_log" ]; then 1526 | command="$command 2>&1 | tee -a $(escape_string "${output}.log")" 1527 | fi 1528 | 1529 | echo "$command" 1530 | 1531 | for item in "${audio_track_name_edits[@]}"; do 1532 | track_index="$(echo "$item" | sed 's/,.*$//')" 1533 | track_name="$(echo "$item" | sed 's/^[^,]*,//')" 1534 | 1535 | if [ "$container_format" == 'mkv' ]; then 1536 | echo "[ -f $(escape_string "$output") ] && mkvpropedit --quiet --edit track:a$track_index --set name=$(escape_string "$track_name") $(escape_string "$output")" 1537 | else 1538 | echo "[ -f $(escape_string "$output") ] && mp4track --track-index $track_index --hdlrname $(escape_string "$track_name") $(escape_string "$output")" 1539 | echo "[ -f $(escape_string "$output") ] && mp4track --track-index $track_index --udtaname $(escape_string "$track_name") $(escape_string "$output")" 1540 | fi 1541 | done 1542 | 1543 | if [ "$forced_subtitle_track_number" ]; then 1544 | 1545 | if [ "$container_format" == 'mkv' ]; then 1546 | echo "[ -f $(escape_string "$output") ] && mkvpropedit --quiet --edit track:s$forced_subtitle_track_number --set flag-default=1 --set flag-forced=1 $(escape_string "$output")" 1547 | else 1548 | echo "[ -f $(escape_string "$output") ] && mp4track --track-index $forced_subtitle_track_number --enabled true $(escape_string "$output")" 1549 | fi 1550 | 1551 | elif [ "$find_forced" == 'add' ] && [ "$container_format" == 'mkv' ]; then 1552 | echo "[ -f $(escape_string "$output") ] && [ \"\$(mkvmerge --identify-verbose $(escape_string "$output") 2>/dev/null | sed -n '/^Track ID [0-9]\\{1,\\}: subtitles .* default_track:1 .*\$/p')\" ] && mkvpropedit --quiet --edit track:s1 --set flag-forced=1 $(escape_string "$output")" 1553 | fi 1554 | 1555 | exit 1556 | fi 1557 | 1558 | # OUTPUT 1559 | # 1560 | if [ "$container_format" == 'mkv' ]; then 1561 | 1562 | if [ "$find_forced" == 'add' ]; then 1563 | 1564 | if ! $(which mkvmerge >/dev/null); then 1565 | die 'executable not in $PATH: mkvmerge' 1566 | fi 1567 | fi 1568 | 1569 | tool='mkvpropedit' 1570 | else 1571 | tool='mp4track' 1572 | fi 1573 | 1574 | if ( ((${#audio_track_name_edits[*]} > 0)) || [ "$forced_subtitle_track_number" ] ) && ! $(which $tool >/dev/null); then 1575 | die "executable not in \$PATH: $tool" 1576 | fi 1577 | 1578 | if [ "$write_log" ]; then 1579 | log_file="${output}.log" 1580 | else 1581 | log_file='/dev/null' 1582 | fi 1583 | 1584 | echo "Transcoding: $input" >&2 1585 | 1586 | time { 1587 | HandBrakeCLI \ 1588 | $title_options \ 1589 | $section_options \ 1590 | $markers_options"$chapter_names_file" \ 1591 | --encoder x264 \ 1592 | $preset_options \ 1593 | $tune_options \ 1594 | --encopts $encopts_options \ 1595 | $level_options \ 1596 | --quality $rate_factor \ 1597 | $frame_rate_options \ 1598 | $audio_options "$audio_track_name_list" \ 1599 | $crop_options \ 1600 | $size_options \ 1601 | $filter_options \ 1602 | $subtitle_options \ 1603 | $srt_options \ 1604 | $passthru_options \ 1605 | --input "$input" \ 1606 | --output "$output" \ 1607 | 2>&1 | tee -a "$log_file" 1608 | 1609 | if [ -f "$output" ]; then 1610 | 1611 | for item in "${audio_track_name_edits[@]}"; do 1612 | track_index="$(echo "$item" | sed 's/,.*$//')" 1613 | track_name="$(echo "$item" | sed 's/^[^,]*,//')" 1614 | 1615 | if [ "$container_format" == 'mkv' ]; then 1616 | mkvpropedit --quiet --edit track:a$track_index --set name="$track_name" "$output" || exit 1 1617 | else 1618 | mp4track --track-index $track_index --hdlrname "$track_name" "$output" && 1619 | mp4track --track-index $track_index --udtaname "$track_name" "$output" || exit 1 1620 | fi 1621 | done 1622 | 1623 | if [ "$forced_subtitle_track_number" ]; then 1624 | 1625 | if [ "$container_format" == 'mkv' ]; then 1626 | mkvpropedit --quiet --edit track:s$forced_subtitle_track_number --set flag-default=1 --set flag-forced=1 "$output" || exit 1 1627 | else 1628 | mp4track --track-index $forced_subtitle_track_number --enabled true "$output" || exit 1 1629 | fi 1630 | 1631 | elif [ "$find_forced" == 'add' ] && [ "$container_format" == 'mkv' ] && [ "$(mkvmerge --identify-verbose "$output" 2>/dev/null | sed -n '/^Track ID [0-9]\{1,\}: subtitles .* default_track:1 .*$/p')" ]; then 1632 | mkvpropedit --quiet --edit track:s1 --set flag-forced=1 "$output" || exit 1 1633 | 1634 | fi 1635 | fi 1636 | } 1637 | --------------------------------------------------------------------------------