1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
|
#+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 <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!
|