MP4Info 0.2 → 0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|