Skip to content

Commit d035bc1

Browse files
authored
TIKA-4802: Expose the MP4 video bitrate (video:bitrate) (#2984)
* TIKA-4802: Add video:bitrate from the MP4 BitRateBox * TIKA-4802: Regenerate metadata registry for video:bitrate
1 parent cb707ac commit d035bc1

6 files changed

Lines changed: 129 additions & 1 deletion

File tree

tika-core/src/main/java/org/apache/tika/metadata/Video.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,11 @@ public interface Video {
3030
* measured rate.
3131
*/
3232
Property FRAME_RATE = Property.internalReal("video:frame-rate");
33+
34+
/**
35+
* Average bitrate in bits per second, from the video track's BitRateBox
36+
* ('btrt'). A per-stream value: in a file with several video tracks it
37+
* reflects the last one.
38+
*/
39+
Property BITRATE = Property.internalInteger("video:bitrate");
3340
}

tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-key-fields.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@
493493
{"class":"org.apache.tika.metadata.TikaPagedText","field":"PAGE_NUMBER","key":"tk:page:number"},
494494
{"class":"org.apache.tika.metadata.TikaPagedText","field":"PAGE_NUMBERS","key":"tk:page:numbers"},
495495
{"class":"org.apache.tika.metadata.TikaPagedText","field":"PAGE_ROTATION","key":"tk:page:rotation"},
496+
{"class":"org.apache.tika.metadata.Video","field":"BITRATE","key":"video:bitrate"},
496497
{"class":"org.apache.tika.metadata.Video","field":"FRAME_RATE","key":"video:frame-rate"},
497498
{"class":"org.apache.tika.metadata.WARC","field":"WARC_PAYLOAD_CONTENT_TYPE","key":"warc:payload-content-type"},
498499
{"class":"org.apache.tika.metadata.WARC","field":"WARC_RECORD_CONTENT_TYPE","key":"warc:record-content-type"},

tika-metadata-schema/src/main/resources/org/apache/tika/metadata/metadata-keys.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,7 @@
507507
{"key":"tk:version-count","namespace":"tk","valueType":"INTEGER","cardinality":"SIMPLE"},
508508
{"key":"tk:version-number","namespace":"tk","valueType":"INTEGER","cardinality":"SIMPLE"},
509509
{"key":"tk:warn:truncated-metadata","namespace":"tk","valueType":"BOOLEAN","cardinality":"SIMPLE"},
510+
{"key":"video:bitrate","namespace":"video","valueType":"INTEGER","cardinality":"SIMPLE"},
510511
{"key":"video:frame-rate","namespace":"video","valueType":"REAL","cardinality":"SIMPLE"},
511512
{"key":"w:Comments","namespace":"w","valueType":"TEXT","cardinality":"BAG"},
512513
{"key":"warc:WARC-Record-ID","namespace":"warc","valueType":"TEXT","cardinality":"SIMPLE"},

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ public Mp4Handler<?> processBox(@NotNull String box, @Nullable byte[] payload,
126126
//sound track: our handler additionally reads DRM markers and the
127127
//esds average bitrate from the sample description
128128
return new TikaMp4SoundHandler(metadata, context, tikaMetadata);
129+
} else if (box.equals("hdlr") && payload != null && payload.length >= 12
130+
&& payload[8] == 'v' && payload[9] == 'i'
131+
&& payload[10] == 'd' && payload[11] == 'e') {
132+
//video track: our handler additionally reads the btrt average
133+
//bitrate from the sample description
134+
return new TikaMp4VideoHandler(metadata, context, tikaMetadata);
129135
}
130136

131137
return super.processBox(box, payload, size, context);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.tika.parser.mp4;
18+
19+
import java.io.IOException;
20+
import java.nio.charset.StandardCharsets;
21+
22+
import com.drew.imaging.mp4.Mp4Handler;
23+
import com.drew.metadata.Metadata;
24+
import com.drew.metadata.mp4.Mp4Context;
25+
import com.drew.metadata.mp4.media.Mp4VideoHandler;
26+
27+
import org.apache.tika.io.EndianUtils;
28+
import org.apache.tika.metadata.Video;
29+
30+
/**
31+
* Extends the video track handling with what the base handler does not read
32+
* from the sample description: the average bitrate from the 'btrt' BitRateBox.
33+
* See TIKA-4802.
34+
*/
35+
class TikaMp4VideoHandler extends Mp4VideoHandler {
36+
37+
/**
38+
* Fixed size of a VisualSampleEntry (ISO/IEC 14496-12) before its child
39+
* boxes: the 8 byte box header, 8 bytes of SampleEntry (6 reserved, 2 data
40+
* reference index) and 70 bytes of visual fields ending with the 32 byte
41+
* compressor name, the depth and a pre-defined field.
42+
*/
43+
private static final int VISUAL_ENTRY_SIZE = 86;
44+
45+
private final org.apache.tika.metadata.Metadata tikaMetadata;
46+
47+
TikaMp4VideoHandler(Metadata metadata, Mp4Context context,
48+
org.apache.tika.metadata.Metadata tikaMetadata) {
49+
super(metadata, context);
50+
this.tikaMetadata = tikaMetadata;
51+
}
52+
53+
@Override
54+
public Mp4Handler<?> processBox(String type, byte[] payload, long boxSize,
55+
Mp4Context context) throws IOException {
56+
if ("stsd".equals(type) && payload != null) {
57+
extractFromSampleDescriptions(payload);
58+
}
59+
return super.processBox(type, payload, boxSize, context);
60+
}
61+
62+
/**
63+
* Walks the sample description entries: 4 bytes version and flags, a
64+
* 4 byte entry count, then one sample entry per count, each starting with
65+
* its own size and format fourcc.
66+
*/
67+
private void extractFromSampleDescriptions(byte[] b) {
68+
if (b.length < 8) {
69+
return;
70+
}
71+
long entryCount = EndianUtils.getUIntBE(b, 4);
72+
int pos = 8;
73+
for (long i = 0; i < entryCount && pos + 8 <= b.length; i++) {
74+
long size = EndianUtils.getUIntBE(b, pos);
75+
if (size < 16 || size > b.length - pos) {
76+
break;
77+
}
78+
int end = pos + (int) size;
79+
int bitRate = findBtrtAverageBitRate(b, pos + VISUAL_ENTRY_SIZE, end);
80+
if (bitRate > 0) {
81+
tikaMetadata.set(Video.BITRATE, bitRate);
82+
}
83+
pos = end;
84+
}
85+
}
86+
87+
/**
88+
* Scans the child boxes of a sample entry for a 'btrt' BitRateBox and
89+
* returns its average bitrate, or 0 if there is none. The box body is the
90+
* decoding buffer size, the maximum bitrate and the average bitrate.
91+
*/
92+
private static int findBtrtAverageBitRate(byte[] b, int pos, int end) {
93+
while (pos >= 0 && pos + 8 <= end) {
94+
long size = EndianUtils.getUIntBE(b, pos);
95+
if (size < 8 || size > end - pos) {
96+
return 0;
97+
}
98+
if ("btrt".equals(fourCc(b, pos + 4)) && pos + 20 <= end) {
99+
long averageBitRate = EndianUtils.getUIntBE(b, pos + 16);
100+
return averageBitRate > 0 && averageBitRate <= Integer.MAX_VALUE
101+
? (int) averageBitRate : 0;
102+
}
103+
pos += (int) size;
104+
}
105+
return 0;
106+
}
107+
108+
private static String fourCc(byte[] b, int pos) {
109+
return new String(b, pos, 4, StandardCharsets.ISO_8859_1);
110+
}
111+
}

tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,12 @@ public void testMP4MultipleCovers() throws Exception {
185185
// TODO Test an old QuickTime Video File
186186
@Test
187187
public void testVideoFrameRate() throws Exception {
188-
// a 10 fps H.264 clip generated with ffmpeg (color source, 16x16, 1s)
188+
// a 10 fps H.264 clip generated with ffmpeg (color source, 16x16, 1s);
189+
// libx264 also writes the average bitrate into the btrt BitRateBox
189190
XMLResult r = getXML("testMP4Video.mp4");
190191
assertEquals("video/mp4", r.metadata.get(Metadata.CONTENT_TYPE));
191192
assertEquals("10.0", r.metadata.get(Video.FRAME_RATE));
193+
assertEquals("6536", r.metadata.get(Video.BITRATE));
192194
}
193195

194196
@Test

0 commit comments

Comments
 (0)