MP4Info 0.2 → 0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +2 -1
- data/lib/mp4info.rb +124 -31
- data/test/test.rb +83 -82
- metadata +4 -3
data/README
CHANGED
|
@@ -4,7 +4,8 @@ It is based on the Perl module MP4::Info (http://search.cpan.org/~jhar/MP4-Info/
|
|
|
4
4
|
Note: MP4Info does not currently support Unicode strings.
|
|
5
5
|
|
|
6
6
|
= License
|
|
7
|
-
Copyright (
|
|
7
|
+
Copyright (c) 2004-2007 Jonathan Harris <jhar@cpan.org>
|
|
8
|
+
Copyright (C) 2006-2007 Jason Terk <rain@xidus.net>
|
|
8
9
|
|
|
9
10
|
This program is free software; you can redistribute it and/or modify
|
|
10
11
|
it under the terms of version 2 of the GNU General Public License as
|
data/lib/mp4info.rb
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
# Note: MP4Info does not currently support Unicode strings.
|
|
5
5
|
#
|
|
6
6
|
# = License
|
|
7
|
-
# Copyright (
|
|
7
|
+
# Copyright (c) 2004-2007 Jonathan Harris <jhar@cpan.org>
|
|
8
|
+
# Copyright (C) 2006-2007 Jason Terk <rain@xidus.net>
|
|
8
9
|
#
|
|
9
10
|
# This program is free software; you can redistribute it and/or modify
|
|
10
11
|
# it under the terms of version 2 of the GNU General Public License as
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
#
|
|
22
23
|
# See the README file for usage information.
|
|
23
24
|
|
|
25
|
+
require 'tempfile'
|
|
26
|
+
|
|
24
27
|
class MP4Info
|
|
25
28
|
# Initialize a new MP4Info object from an IO object
|
|
26
29
|
def initialize(io_stream)
|
|
@@ -51,7 +54,8 @@ class MP4Info
|
|
|
51
54
|
# Non standard data atoms
|
|
52
55
|
@other_atoms = {
|
|
53
56
|
"MDAT" => :parse_mdat, "META" => :parse_meta,
|
|
54
|
-
"MVHD" => :parse_mvhd, "STSD" => :parse_stsd
|
|
57
|
+
"MVHD" => :parse_mvhd, "STSD" => :parse_stsd,
|
|
58
|
+
"MOOV" => :parse_moov
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
# Info/Tag aliases
|
|
@@ -62,7 +66,7 @@ class MP4Info
|
|
|
62
66
|
|
|
63
67
|
# Sanity check
|
|
64
68
|
head = read_or_raise(io_stream, 8, "#{io_stream} does not appear to be an IO stream")
|
|
65
|
-
raise "#{io_stream} does not appear to be an
|
|
69
|
+
raise "#{io_stream} does not appear to be an MP4 file" unless head[4..7].downcase == "ftyp"
|
|
66
70
|
|
|
67
71
|
# Back to the beginning
|
|
68
72
|
io_stream.rewind
|
|
@@ -101,38 +105,54 @@ class MP4Info
|
|
|
101
105
|
private
|
|
102
106
|
# Parse a container
|
|
103
107
|
def parse_container(io_stream, level, size)
|
|
104
|
-
level
|
|
105
|
-
|
|
108
|
+
level += 1
|
|
109
|
+
container_end = io_stream.pos + size
|
|
106
110
|
|
|
107
|
-
while io_stream.pos <
|
|
108
|
-
parse_atom io_stream, level
|
|
111
|
+
while io_stream.pos < container_end do
|
|
112
|
+
parse_atom io_stream, level, container_end - io_stream.pos
|
|
109
113
|
end
|
|
110
114
|
|
|
111
|
-
if (io_stream.pos !=
|
|
115
|
+
if (io_stream.pos != container_end)
|
|
112
116
|
raise "Parse error"
|
|
113
117
|
end
|
|
114
118
|
end
|
|
115
119
|
|
|
116
120
|
# Parse an atom
|
|
117
|
-
def parse_atom(io_stream, level)
|
|
121
|
+
def parse_atom(io_stream, level, parent_size)
|
|
118
122
|
head = read_or_raise(io_stream, 8, "Premature end of file")
|
|
119
123
|
|
|
120
124
|
size, id = head.unpack("Na4")
|
|
121
|
-
|
|
125
|
+
|
|
126
|
+
if (size == 0)
|
|
127
|
+
position = io_stream.pos
|
|
128
|
+
io_stream.seek(0, 2)
|
|
129
|
+
size = io_stream.pos - position
|
|
130
|
+
io_stream.seek(position, 0)
|
|
131
|
+
elsif (size == 1)
|
|
122
132
|
# Extended size, whatever that means
|
|
123
133
|
head = read_or_raise(io_stream, 8, "Premature end of file")
|
|
124
134
|
hi, low = head.unpack("NN")
|
|
125
135
|
size = hi * (2**32) + low - 16
|
|
136
|
+
|
|
137
|
+
if (size > parent_size)
|
|
138
|
+
# Atom extends outside of parent container; skip to the end
|
|
139
|
+
io_stream.seek(parent_size - 16, 1)
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
size -= 16
|
|
126
144
|
else
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (size <= 0)
|
|
131
|
-
if (size == 0 and level ==1)
|
|
145
|
+
if (size > parent_size)
|
|
146
|
+
# Atom extends outside of parent container; skip to the end
|
|
147
|
+
io_stream.seek(parent_size - 8, 1)
|
|
132
148
|
return
|
|
133
|
-
else
|
|
134
|
-
raise "Parse error"
|
|
135
149
|
end
|
|
150
|
+
|
|
151
|
+
size -= 8;
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if (size < 0)
|
|
155
|
+
raise "Parse error"
|
|
136
156
|
end
|
|
137
157
|
|
|
138
158
|
re = /[^\w\-]/
|
|
@@ -153,6 +173,25 @@ class MP4Info
|
|
|
153
173
|
end
|
|
154
174
|
end
|
|
155
175
|
|
|
176
|
+
# Parse a MOOV container
|
|
177
|
+
#
|
|
178
|
+
# Pre-conditions: size = size of atom contents
|
|
179
|
+
# io_stream points to start of atom contents
|
|
180
|
+
#
|
|
181
|
+
# Post-condition: io_stream points past end of atom contents
|
|
182
|
+
def parse_moov(io_stream, level, size)
|
|
183
|
+
data = read_or_raise(io_stream, size, "Premature end of file")
|
|
184
|
+
|
|
185
|
+
cache = Tempfile.new "mp4info"
|
|
186
|
+
cache.write data
|
|
187
|
+
cache.open
|
|
188
|
+
cache.rewind
|
|
189
|
+
|
|
190
|
+
parse_container(cache, level, size)
|
|
191
|
+
|
|
192
|
+
cache.close!
|
|
193
|
+
end
|
|
194
|
+
|
|
156
195
|
# Parse an MDAT atom
|
|
157
196
|
#
|
|
158
197
|
# Pre-conditions: size = size of atom contents
|
|
@@ -161,7 +200,7 @@ class MP4Info
|
|
|
161
200
|
# Post-condition: io_stream points past end of atom contents
|
|
162
201
|
def parse_mdat(io_stream, level, size)
|
|
163
202
|
@info_atoms["SIZE"] = 0 unless @info_atoms["SIZE"]
|
|
164
|
-
@info_atoms["SIZE"]
|
|
203
|
+
@info_atoms["SIZE"] += size
|
|
165
204
|
io_stream.seek(size, 1)
|
|
166
205
|
end
|
|
167
206
|
|
|
@@ -225,7 +264,9 @@ class MP4Info
|
|
|
225
264
|
# Is this an audio track?
|
|
226
265
|
if (data_format == "mp4a" || data_format == "drms" ||
|
|
227
266
|
data_format == "samr" || data_format == "sawb" ||
|
|
228
|
-
data_format == "sawp" || data_format == "enca"
|
|
267
|
+
data_format == "sawp" || data_format == "enca" ||
|
|
268
|
+
data_format == "alac" )
|
|
269
|
+
@info_atoms["ENCODING"] = data_format
|
|
229
270
|
@info_atoms["FREQUENCY"] = (data[40..43].unpack("N")[0] * 1.0) / 65536000
|
|
230
271
|
printf " %sFreq=%s\n", ' ' * ( 2 * level ), @info_atoms["FREQUENCY"] if $DEBUG
|
|
231
272
|
end
|
|
@@ -234,6 +275,45 @@ class MP4Info
|
|
|
234
275
|
@info_atoms["ENCRYPTED"] = true;
|
|
235
276
|
end
|
|
236
277
|
end
|
|
278
|
+
|
|
279
|
+
# User-defined box. Used by PSP - See ffmpeg libavformat/movenc.c
|
|
280
|
+
#
|
|
281
|
+
# Pre-conditions: size = size of atom contents
|
|
282
|
+
# io_stream points to start of atom contents
|
|
283
|
+
#
|
|
284
|
+
# Post-condition: io_stream points past end of atom contents
|
|
285
|
+
def parse_uuid(io_stream, level, size)
|
|
286
|
+
data = read_or_raise(io_stream, size, "Premature end of file")
|
|
287
|
+
|
|
288
|
+
return unless size > 26
|
|
289
|
+
|
|
290
|
+
u1, u2, u3, u4 = data.unpack 'a4NNN'
|
|
291
|
+
|
|
292
|
+
if (u1 == "USMT")
|
|
293
|
+
pspsize, pspid = data[16..23].unpack 'Na4'
|
|
294
|
+
|
|
295
|
+
return unless pspsize == size - 16
|
|
296
|
+
|
|
297
|
+
if (pspid == "MTDT")
|
|
298
|
+
nblocks = data[24..25].unpack 'n'
|
|
299
|
+
data = data[26..(data.length - 1)]
|
|
300
|
+
|
|
301
|
+
while nblocks
|
|
302
|
+
bsize, btype, flags, ptype = data.unpack 'nNnn'
|
|
303
|
+
|
|
304
|
+
if (btype == 1 && bsize == 12 &&
|
|
305
|
+
ptype == 1 && @data_atoms["NAM"].nil?)
|
|
306
|
+
@data_atoms["NAM"] = data[10..(10 + bsize - 11)]
|
|
307
|
+
elsif (btype == 4 && bsize > 12 && ptype == 1)
|
|
308
|
+
@data_atoms["TOO"] = data[10..(10 + bsize - 11)]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
data = data[bsize..(data.length - 1)]
|
|
312
|
+
nblocks -= 1
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
237
317
|
|
|
238
318
|
def parse_data(io_stream, level, size, id)
|
|
239
319
|
# Possible genres...
|
|
@@ -322,27 +402,38 @@ class MP4Info
|
|
|
322
402
|
ver = data.unpack("N")[0]
|
|
323
403
|
if (ver == 0)
|
|
324
404
|
return unless size > 7
|
|
325
|
-
size
|
|
405
|
+
size -= 7
|
|
326
406
|
type = 1
|
|
327
407
|
data = data[6..(6 + size - 1)]
|
|
328
408
|
|
|
329
409
|
if (id == "TITL")
|
|
330
|
-
return if
|
|
410
|
+
return if !@data_atoms["NAM"].nil?
|
|
331
411
|
id = "NAM"
|
|
332
412
|
elsif (id == "DSCP")
|
|
333
|
-
return if
|
|
413
|
+
return if !@data_atoms["CMT"].nil?
|
|
334
414
|
id = "CMT"
|
|
335
415
|
elsif (id == "PERF")
|
|
336
|
-
return if
|
|
416
|
+
return if !@data_atoms["ART"].nil?
|
|
337
417
|
id = "ART"
|
|
338
418
|
elsif (id == "AUTH")
|
|
339
|
-
return if
|
|
419
|
+
return if !@data_atoms["WRT"].nil?
|
|
340
420
|
id = "WRT"
|
|
341
421
|
end
|
|
342
422
|
end
|
|
343
423
|
end
|
|
344
424
|
|
|
345
|
-
if (
|
|
425
|
+
if (id == "MEAN" || id == "NAME" || id == "DATA")
|
|
426
|
+
if id == "DATA"
|
|
427
|
+
data = data[8..(data.length - 1)]
|
|
428
|
+
else
|
|
429
|
+
data = data[4..(data.length - 1)]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
@data_atoms[id] = data
|
|
433
|
+
return
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
if (type.nil?)
|
|
346
437
|
return unless size > 16
|
|
347
438
|
size, atom, type = data.unpack("Na4N")
|
|
348
439
|
|
|
@@ -355,21 +446,23 @@ class MP4Info
|
|
|
355
446
|
|
|
356
447
|
printf " %sType=#{type}, Size=#{size}\n", ' ' * ( 2 * level ) if $DEBUG
|
|
357
448
|
|
|
358
|
-
if (
|
|
449
|
+
if (id == "COVR")
|
|
450
|
+
@data_atoms[id] = data
|
|
451
|
+
elsif (type == 0)
|
|
359
452
|
ints = data.unpack("n" * (size / 2))
|
|
360
453
|
if (id == "GNRE")
|
|
361
454
|
@data_atoms[id] = mp4_genres[ints[0]]
|
|
362
|
-
elsif (
|
|
363
|
-
@data_atoms[id] = [ints[1], ints[2]]
|
|
364
|
-
|
|
455
|
+
elsif (id == "DISK" || id == "TRKN")
|
|
456
|
+
@data_atoms[id] = [ints[1], (size >= 6 ? ints[2] : 0)] if size >= 4
|
|
457
|
+
elsif (size >= 4)
|
|
365
458
|
@data_atoms[id] = ints[1]
|
|
366
459
|
end
|
|
367
460
|
elsif (type == 1)
|
|
368
461
|
if (id == "GEN")
|
|
369
|
-
return if
|
|
462
|
+
return if !@data_atoms["GNRE"].nil?
|
|
370
463
|
id = "GNRE"
|
|
371
464
|
elsif (id == "AART")
|
|
372
|
-
return if
|
|
465
|
+
return if !@data_atoms["ART"].nil?
|
|
373
466
|
id = "ART"
|
|
374
467
|
elsif (id == "DAY")
|
|
375
468
|
data = data[0..3]
|
data/test/test.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
|
|
3
|
-
# Copyright (
|
|
3
|
+
# Copyright (c) 2004-2007 Jonathan Harris <jhar@cpan.org>
|
|
4
|
+
# Copyright (C) 2006-2007 Jason Terk <rain@xidus.net>
|
|
4
5
|
#
|
|
5
6
|
# This program is free software; you can redistribute it and/or modify
|
|
6
7
|
# it under the terms of version 2 of the GNU General Public License as
|
|
@@ -73,33 +74,33 @@ class TestMP4Info < Test::Unit::TestCase
|
|
|
73
74
|
|
|
74
75
|
info = {
|
|
75
76
|
:ALB => 'Album',
|
|
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
|
-
|
|
77
|
+
:APID => nil,
|
|
78
|
+
:ART => 'Artist',
|
|
79
|
+
:CMT => "Comment\r\n2nd line",
|
|
80
|
+
:COVR => nil,
|
|
81
|
+
:CPIL => 0,
|
|
82
|
+
:CPRT => nil,
|
|
83
|
+
:DAY => '2004',
|
|
84
|
+
:DISK => [3,4],
|
|
85
|
+
:GNRE => 'Acid Jazz',
|
|
86
|
+
:GRP => 'Grouping',
|
|
87
|
+
:NAM => 'Name',
|
|
88
|
+
:TMPO => 100,
|
|
89
|
+
:TOO => 'iTunes v4.6.0.15, QuickTime 6.5.1',
|
|
90
|
+
:TRKN => [1,2],
|
|
91
|
+
:WRT => 'Composer',
|
|
92
|
+
:VERSION => 4,
|
|
93
|
+
:LAYER => 1,
|
|
94
|
+
:BITRATE => 50,
|
|
95
|
+
:FREQUENCY => 44.1,
|
|
96
|
+
:SIZE => 6962,
|
|
97
|
+
:SECS => 1,
|
|
98
|
+
:MM => 0,
|
|
99
|
+
:SS => 1,
|
|
100
|
+
:MS => 90,
|
|
101
|
+
:TIME => '00:01',
|
|
102
|
+
:COPYRIGHT => nil,
|
|
103
|
+
:ENCRYPTED => nil
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
mp4 = MP4Info.open(file)
|
|
@@ -115,33 +116,33 @@ class TestMP4Info < Test::Unit::TestCase
|
|
|
115
116
|
|
|
116
117
|
info = {
|
|
117
118
|
:ALB => nil,
|
|
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
|
-
|
|
119
|
+
:APID => nil,
|
|
120
|
+
:ART => 'Artist',
|
|
121
|
+
:CMT => nil,
|
|
122
|
+
:COVR => nil,
|
|
123
|
+
:CPIL => nil,
|
|
124
|
+
:CPRT => nil,
|
|
125
|
+
:DAY => nil,
|
|
126
|
+
:DISK => nil,
|
|
127
|
+
:GNRE => nil,
|
|
128
|
+
:GRP => nil,
|
|
129
|
+
:NAM => 'Name',
|
|
130
|
+
:TMPO => nil,
|
|
131
|
+
:TOO => 'Nero AAC Codec 2.9.9.91',
|
|
132
|
+
:TRKN => nil,
|
|
133
|
+
:WRT => nil,
|
|
134
|
+
:VERSION => 4,
|
|
135
|
+
:LAYER => 1,
|
|
136
|
+
:BITRATE => 21,
|
|
137
|
+
:FREQUENCY => 8,
|
|
138
|
+
:SIZE => 3030,
|
|
139
|
+
:SECS => 1,
|
|
140
|
+
:MM => 0,
|
|
141
|
+
:SS => 1,
|
|
142
|
+
:MS => 153,
|
|
143
|
+
:TIME => '00:01',
|
|
144
|
+
:COPYRIGHT => nil,
|
|
145
|
+
:ENCRYPTED => nil
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
mp4 = MP4Info.open(file)
|
|
@@ -161,33 +162,33 @@ class TestMP4Info < Test::Unit::TestCase
|
|
|
161
162
|
|
|
162
163
|
info = {
|
|
163
164
|
:ALB => 'Album',
|
|
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
|
-
|
|
165
|
+
:APID => nil,
|
|
166
|
+
:ART => 'AÆtist',
|
|
167
|
+
:CMT => 'Comment',
|
|
168
|
+
:COVR => nil,
|
|
169
|
+
:CPIL => nil,
|
|
170
|
+
:CPRT => nil,
|
|
171
|
+
:DAY => 2004,
|
|
172
|
+
:DISK => nil,
|
|
173
|
+
:GNRE => 'Acid Jazz',
|
|
174
|
+
:GRP => nil,
|
|
175
|
+
:NAM => 'N™me',
|
|
176
|
+
:TMPO => nil,
|
|
177
|
+
:TOO => 'Helix Producer SDK 10.0 for Windows, Build 10.0.0.240',
|
|
178
|
+
:TRKN => [1,0],
|
|
179
|
+
:WRT => nil,
|
|
180
|
+
:VERSION => 4,
|
|
181
|
+
:LAYER => 1,
|
|
182
|
+
:BITRATE => 93,
|
|
183
|
+
:FREQUENCY => 1, # What part of "the sampling rate of the audio should be ... documented in the samplerate field" don't Real understand?
|
|
184
|
+
:SIZE => 131682,
|
|
185
|
+
:SECS => 11,
|
|
186
|
+
:MM => 0,
|
|
187
|
+
:SS => 11,
|
|
188
|
+
:MS => 53,
|
|
189
|
+
:TIME => '00:11',
|
|
190
|
+
:COPYRIGHT => nil,
|
|
191
|
+
:ENCRYPTED => nil
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
mp4 = MP4Info.open(file)
|
metadata
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
|
-
rubygems_version: 0.
|
|
2
|
+
rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
|
4
4
|
name: MP4Info
|
|
5
5
|
version: !ruby/object:Gem::Version
|
|
6
|
-
version: "0.
|
|
7
|
-
date:
|
|
6
|
+
version: "0.3"
|
|
7
|
+
date: 2007-04-15 00:00:00 -04:00
|
|
8
8
|
summary: MP4 tag reading library
|
|
9
9
|
require_paths:
|
|
10
10
|
- lib
|
|
@@ -25,6 +25,7 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
|
25
25
|
platform: ruby
|
|
26
26
|
signing_key:
|
|
27
27
|
cert_chain:
|
|
28
|
+
post_install_message:
|
|
28
29
|
authors:
|
|
29
30
|
- Jason Terk
|
|
30
31
|
files:
|