summaryrefslogtreecommitdiffstats
path: root/content/posts/converting-mod-moi-to-mp4
diff options
context:
space:
mode:
Diffstat (limited to 'content/posts/converting-mod-moi-to-mp4')
-rw-r--r--content/posts/converting-mod-moi-to-mp4/index.org188
1 files changed, 188 insertions, 0 deletions
diff --git a/content/posts/converting-mod-moi-to-mp4/index.org b/content/posts/converting-mod-moi-to-mp4/index.org
new file mode 100644
index 0000000..3b7b826
--- /dev/null
+++ b/content/posts/converting-mod-moi-to-mp4/index.org
@@ -0,0 +1,188 @@
+#+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:
+
+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 <directory>...'
+ 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!