#+TITLE: Converting Old Camcorder MOD and MOI files to MP4 Video #+DATE: 2024-01-05T21:57:24-05:00 #+DRAFT: true #+DESCRIPTION: #+TAGS[]: ffmpeg, video #+KEYWORDS[]: ffmpeg, video, camcorder, jvc, mod, moi #+SLUG: #+SUMMARY: #+ATTR_HTML: :title JVC Camdorder Video to MP4 #+ATTR_HTML: :alt JVC Camdorder Video to MP4 [[file:cover.png]] 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. #+begin_src $ ls Card\ 1/ DCIM EXTMOV PRIVATE SD_VIDEO #+end_src =DCIM= contained normal JPEG images, and videos were stored in =SD_VIDEO=. The videos were then grouped into sub-directories. #+begin_src $ ls Card\ 1/SD_VIDEO/ MGR_INFO PRG001 PRG002 #+end_src Finally the videos themselves were stored in a directory that looked roughly like this. #+begin_src $ ls Card\ 1/SD_VIDEO/PRG002/ MOV002.MOD MOV003.MOD MOV007.MOD MOV008.MOD MOV009.MOD MOV00A.MOD MOV002.MOI MOV003.MOI MOV007.MOI MOV008.MOI MOV009.MOI MOV00A.MOI #+end_src 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. #+begin_src $ 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 #+end_src Using the =file= command, we can get some information about what kind of video is might contain. #+begin_src $ file MOV002.MOD MOV002.MOD: MPEG sequence, v2, program multiplex #+end_src We can check the file using =ffprobe= to get more details. #+begin_src $ 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 #+end_src 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 [[https://en.wikipedia.org/wiki/MOI_(file_format)][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. #+begin_src 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') } end if ARGV.empty? || ['-h', '--help'].include?(ARGV[0]) puts 'usage: convert.rb ...' puts ' Convert one or more directories full of MOI and MOD files into MP4 files with correct metadata' return end 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" next end 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}\"" system(ffmpeg) system(touch) end end #+end_src 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!