diff options
Diffstat (limited to 'content/posts/converting-mod-moi-to-mp4')
-rw-r--r-- | content/posts/converting-mod-moi-to-mp4/index.org | 188 |
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! |