JVC Camdorder Video to MP4

I recently found a bin of SD cards from a JVC camcorder I used years ago for school projects. I wanted to import the videos into DigiKam to catalog them with the other photos and videos I've taken with other cameras. When trying to complete this I quickly ran into an issue. The camcorder recorded videos into a combination of MOD and MOI files, which most software doesn't recognize.

The SD cards contained directories structured like this.

$ ls Card\ 1/

DCIM contained normal JPEG images, and videos were stored in SD_VIDEO.

The videos were then grouped into sub-directories.

$ ls Card\ 1/SD_VIDEO/

Finally the videos themselves were stored in a directory that looked roughly like this.

$ ls Card\ 1/SD_VIDEO/PRG002/

But what are these files and what do they contain?

We can see immediately from looking at the file sizes that the MOD files probably contain the video, and that the MOI files are most likely some sort of metadata.

$ cd Card\ 1/SD_VIDEO/PRG002
$ ls -lh
total 592M
-rw-------. 1 dante dante 684K Sep  6  2009 MOV002.MOD
-rw-------. 1 dante dante  275 Sep  6  2009 MOV002.MOI
-rw-------. 1 dante dante 1.2M Sep  6  2009 MOV003.MOD
-rw-------. 1 dante dante  278 Sep  6  2009 MOV003.MOI
-rw-------. 1 dante dante  88M Sep  6  2009 MOV007.MOD
-rw-------. 1 dante dante 1.1K Sep  6  2009 MOV007.MOI
-rw-------. 1 dante dante 7.5M Sep  6  2009 MOV008.MOD
-rw-------. 1 dante dante  345 Sep  6  2009 MOV008.MOI
-rw-------. 1 dante dante 3.6M Sep  6  2009 MOV009.MOD
-rw-------. 1 dante dante  302 Sep  6  2009 MOV009.MOI
-rw-------. 1 dante dante 217M Sep  6  2009 MOV00A.MOD
-rw-------. 1 dante dante 2.4K Sep  6  2009 MOV00A.MOI

Using the file command, we can get some information about what kind of video is might contain.

$ file MOV002.MOD
MOV002.MOD: MPEG sequence, v2, program multiplex

We can check the file using ffprobe to get more details.

$ ffprobe MOV002.MOD
Input #0, mpeg, from 'MOV002.MOD':
  Duration: 00:00:01.02, start: 0.215278, bitrate: 5472 kb/s
  Stream #0:0[0x1e0]: Video: mpeg2video (Main), yuv420p(tv, smpte170m, top first), 720x480 [SAR 32:27 DAR 16:9], 29.97 fps, 29.97 tbr, 90k tbn
    Side data:
      cpb: bitrate max/min/avg: 7700000/0/0 buffer size: 1835008 vbv_delay: N/A
  Stream #0:1[0x80]: Audio: ac3, 48000 Hz, stereo, fltp, 256 kb/s

Nice! It looks like we're dealing with a combination of mpeg2 video and ac3 audio.

We should be able to run this through ffmpeg and get a usable file out of it. I've decided to convert the videos to MP4s for the greatest compatibility with media manager software. Since MP4s are allowed to contain MPEG2 video, we will be able to use the -vcodec copy option to leave the video untouched.

Unfortunately ac3 audio isn't really supported by the MP4 container. It could probably be done but it won't be well supported in most video players. For this reason we'll be transcoding the audio to AAC using -acodec aac.

We only have one problem remaining. The file metadata, like the day the video was recorded, is stored in the MOI file. We want to preserve that so all the videos can be ordered correctly in DigiKam.

Fortunately there's a stub article on Wikipedia that describes some of the contents of the MOI files, including the date and time information. Now all we have to do is parse it and add that information into the MP4 when we convert it.

We can extract the metadata from the MOI file and then re-insert it as a creation_time metadata field in the MP4. We can accomplish this using the -metadata creation_time flag on ffmpeg.

It will also be a good idea to modify the file's timestamp in case an application can't read the metadata correctly. Unfortunately I wasn't able to find a reliable way to modify a file's creation timestamp, but we can modify its last modified and last accessed timestamps using touch.

Now we just need to write a small script to automate this process and we should be good to go!

This is what I came up with using Ruby.

#!/usr/bin/env ruby
# frozen_string_literal: true

# https://en.wikipedia.org/wiki/MOI_(file_format)

def parse_moi(filename)
  data = File.read(filename)
  version = data[0..1]
  size = data[2..5].unpack1('L>')
  year = data[6..7].unpack1('S>')
  month = data[8].unpack1('C')
  day = data[9].unpack1('C')
  hour = data[0xa].unpack1('C')
  minutes = data[0xb].unpack1('C')
  milliseconds = data[0xc..0xd].unpack1('S>')
  seconds = milliseconds / 1000

    version: version,
    size: size,
    year: year.to_s,
    month: month.to_s.rjust(2, '0'),
    day: day.to_s.rjust(2, '0'),
    hour: hour.to_s.rjust(2, '0'),
    minutes: minutes.to_s.rjust(2, '0'),
    seconds: seconds.to_s.rjust(2, '0')

if ARGV.empty? || ['-h', '--help'].include?(ARGV[0])
  puts 'usage: convert.rb <directory>...'
  puts '  Convert one or more directories full of MOI and MOD files into MP4 files with correct metadata'

ARGV.each do |dir|
  moi_files = Dir[File.join(dir, '*.MOI')]

  moi_files.each do |moi|
    data = parse_moi(moi)
    puts "#{moi}: #{data}"
    mod = moi.sub(/MOI$/, 'MOD')
    raise "Video file doesn't exist: #{mod}" unless File.exist?(mod)

    mp4 = moi.sub(/MOI$/, 'mp4')
    if File.exist?(mp4)
      puts "#{mp4} already exists, skipping"

    ffmpeg = "ffmpeg -i \"#{mod}\" -vcodec copy -acodec aac -metadata \"creation_time=#{data[:year]}-#{data[:month]}-#{data[:day]} #{data[:hour]}:#{data[:minutes]}:#{data[:seconds]}Z\" \"#{mp4}\""
    touch = "TZ=UTC touch -t #{data[:year]}#{data[:month]}#{data[:day]}#{data[:hour]}#{data[:minutes]}.#{data[:seconds]} \"#{mp4}\""


All we have to do now is run the script with the directories containing the old camcorder videos and we'll be left with newly converted and widely compatible MP4s, ready to be watched!