Saturday, April 28, 2012

Extract a DVD to an ISO Image

I needed to copy an old DVD (playable home movies), and came across this gem. This command will extract the DVD to an ISO image, ready for burning. I don't know if it works with copy-protected DVDs, but it was exactly what I needed.

$ readom dev=/dev/cdrom f=target.iso

Friday, April 13, 2012

Shrinking a Decade's Worth of ... Part 2

The video compression (see the previous post) worked really well. It worked so well, in fact, that now my videos are much smaller, in total, than my photos.

Here we go again! Well, not exactly. You see, there is a much easier solution to batch-shrinking image files. It doesn't require any scripting at all.

Warning! mogrify modifies files in place!
Before you do this, make sure you have another copy of your image files. mogrify modifies the originals in place. JPEG is a lossy format, and recompressing the files will result in some loss of image quality. As an example of what can go wrong:

DON'T! $ mogrify -quality 7 *.jpg DON'T!

would essentially destroy your originals.

Here are the steps for Ubuntu. Windows users, you are on your own for the installation.

  • Install ImageMagick.
    $ sudo apt-get install imagemagick
  • cd to the directory with your images.
  • Assuming JPEG format, type:
    $ mogrify -quality 75 *.jpg

Wow, that was easy. I love Ubuntu!

For my images, which came from a Digital Elph, the re-compressed images take up less than half the space of the originals, and to me the images look just as good. Experiment with the quality setting and see what works best for you.

Tuesday, April 10, 2012

Shrinking a Decade's Worth of Video Clips

My family just loves to shoot video clips with our digital cameras. I must admit, I love watching old videos of the kids when they were little. But storing all those videos is becoming a nightmare. The library has grown more than the kids!

I figured I could convert the videos to some other format and save some space. I was thinking there had to be a reason why AVI isn't used for streaming web videos. I poked around on the web, and after some trial and error, came up with the following command pattern using ffmpeg.

user@computer:~/Videos$ ffmpeg -threads 0 -i movie001.AVI -b 1500K \
  -vcodec libx264 -vpre slow movie001.mp4

As it turns out, all of my cameras are really bad at on-the-fly video compression. As an example, I have a 33 second, 61MB video clip. After transcoding to mp4 it's 6.4MB. The new clip looks as good as the original. This is going to solve all my video storage problems.

Okay, so I am motivated, and I have a command that will do what I want. But I need to do this for hundreds of video clips, and my bash programming stinks. I am pretty good with one-liners. Throw in looping and awk or sed and I'm lost.

What's a Java programmer to do?

Java isn't the best scripting language. Perl and Python are great for scripting, but I use them so seldom that I am constantly looking up simple stuff. I like Beanshell, but it lacks polish for command-line programming. So how about Groovy?

Groovy syntax matches Java almost exactly (better than Beanshell) and also extends it with Ruby-esque extensions and code-scrunching syntactical sugar. In other words, I can write a concise script that I can still read in six months - when I need it again.

The Groovy script below takes a list of video files and converts them to mp4. You can modify it to allow different file types or to tweak the ffmpeg settings:

user@computer:~/Videos$ transcode.gy *.avi

Here is the full listing for transcode.gy. It's "enigmatic" Groovy (closures, syntactic sugar, etc.), but if you are comfortable with Java you won't have any trouble following along. Enjoy!

 #!/usr/bin/env groovy 
 /**
  * This software is distributed WITHOUT WARRANTIES OR CONDITIONS OF ANY
  * KIND, either express or implied. Copy it, use it, sell it, even 
  * take credit for it if you want. But don't come back to me if there
  * is a problem.
  */  
 import java.util.regex.Matcher  
 import java.util.regex.Pattern  
 List<File> files = args.collect { new File(it).canonicalFile }  
 files.each {   
     if(it.isDirectory())   
         throw new IOException("Can't process folder: " + it.path)  
     if(!it.isFile())  
         throw new FileNotFoundException(it.path) 
     /* Update this regular-expression to allow different video types. */
     if(!(it.name ==~ /(?i).*\.((avi)|(mod)|(mpeg))/))   
         throw new IOException("File type not supported: " + it.path) 
 }  
 files.each {  
     Matcher matcher = (it.name =~ /.*\.([a-zA-Z]+)/)  
     matcher.find()  
     String ext = matcher.group(1)  
     String newName = "${it.name[0..-ext.size()-2]}.mp4"  
     Process p =   
         ["ffmpeg",   
         "-threads", "0", 
             /* 0 indicates use all cores. */  
         "-i", it.canonicalPath,
         "-b", "1500K",
             /* Default 200K. You can tweak this setting to change video 
              * size and quality. */
         "-vcodec", "libx264",   
         "-vpre", "slow", 
             /* Preset file. Also default, normal, veryslow, max, hq. 
              * Search for files ending in '.ffpreset' to see what's 
              * available to you. */
         "-y", 
             /* Overwrite the .mp4 file if one already exists. */
         new File(it.parentFile, newName)].execute()  
     p.waitForProcessOutput(System.out, System.err)
     if(p.exitValue() != 0) 
         throw new Exception("Failed: ffmpeg ${p.exitValue()} ${it.path}")
 }  

Thanks to John Dyer for posting simplified command-line examples of ffmpeg for several popular video formats, including H.264.


Paul Williams pointed out in the comments that you can do much the same thing with this simple Bash script.

for i in `ls <input_dir>` 
do 
    ffmpeg \
        -threads 0 \
        -i $i \
        -b 1500K \
        -vcodec libx264 \
        -vpre slow \
        <output_dir>/`basename $i .AVI`.mp4
done

I hardly ever get to use for ... in in my common Bash tasks because most commands I use take multiple files as input, whereas ffmpeg does not. I'll have to try to remember this next time I run into a similar situation.

If I left out the error checking and comments in the Groovy script, it would be a lot shorter, but not that short. I wonder how small I could make the program if I wrote it in Java? That could be interesting...


Here's a Java version using Jaks and Apache commons-io for a cleaner implementation. Lots of standard Java boilerplate (I use Eclipse to manage that bit), and there is a Maven component to this that I'm not going to go into. Keeping in mind that I left in some error checking code, the meat of the program is comparable to the size of the Bash script, and somewhat shorter than the original Groovy script - which is mostly due to me being a bit rusty with enigmatic Groovy, I think.

 1:  import static java.util.Arrays.asList;  
 2:  import static org.apache.commons.io.FilenameUtils.getBaseName;  
 3:  import static org.apache.commons.io.FilenameUtils.getExtension;  
 4:  import java.io.File;  
 5:  import java.io.FileNotFoundException;  
 6:  import java.util.List;  
 7:  import com.googlecode.jaks.cli.AbstractJaksCommand;  
 8:  import com.googlecode.jaks.cli.JaksNonOption;  
 9:  import com.googlecode.jaks.system.Subprocess;  
10:  public class Transcode extends AbstractJaksCommand  
11:  {  
12:      @JaksNonOption(required=true)  
13:      public List<File> vids;  
14:      @Override  
15:      public void execute() throws Exception   
16:      {  
17:          for(final File vid : vids)  
18:          {  
19:              if(!asList("avi", "mod", "mpeg").contains(getExtension(vid.getName())))  
20:              {  
21:                  throw new IllegalArgumentException(vid.getPath() + " is not a convertable file type.");  
22:              }  
23:              if(!vid.isFile())  
24:              {  
25:                  throw new FileNotFoundException(vid.getPath());  
26:              }  
27:              new Subprocess("ffmpeg",   
28:                      "-threads", "0",  
29:                      "-i", vid.getCanonicalPath(),  
30:                      "-b", "1500K",  
31:                      "-vcodec", "libx264",  
32:                      "-vpre", "slow",  
33:                      "-y",  
34:                      new File(vid.getParentFile(), getBaseName(vid.getName()) + ".mp4").getPath()  
35:                  ).call();  
36:          }  
37:      }  
38:  }