This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 6ffb5dda61 TIKA-4779: Add audio:bitrate, audio:is-variable-bitrate and
audio:has-drm (#2953)
6ffb5dda61 is described below
commit 6ffb5dda616a231b309754313451c24faa432969
Author: Dominik Schmidt <[email protected]>
AuthorDate: Tue Jul 21 22:15:49 2026 +0200
TIKA-4779: Add audio:bitrate, audio:is-variable-bitrate and audio:has-drm
(#2953)
---
.../main/java/org/apache/tika/metadata/Audio.java | 22 +++
.../java/org/apache/tika/parser/mp3/Mp3Parser.java | 51 ++++++
.../org/apache/tika/parser/mp3/MpegStream.java | 34 +++-
.../apache/tika/parser/mp4/TikaMp4BoxHandler.java | 6 +
.../tika/parser/mp4/TikaMp4SoundHandler.java | 193 +++++++++++++++++++++
.../org/apache/tika/parser/ogg/VorbisParser.java | 18 ++
.../org/apache/tika/parser/mp3/Mp3ParserTest.java | 36 ++++
.../org/apache/tika/parser/mp4/MP4ParserTest.java | 22 +++
.../apache/tika/parser/ogg/VorbisParserTest.java | 104 +++++++++++
.../resources/test-documents/testMP3cbrInfoTag.mp3 | Bin 0 -> 6681 bytes
.../test/resources/test-documents/testMP3vbr.mp3 | Bin 0 -> 3129 bytes
.../test/resources/test-documents/testMP4_drm.m4a | Bin 0 -> 464 bytes
.../resources/test-documents/testMP4_esdsFlags.m4a | Bin 0 -> 512 bytes
.../test/resources/test-documents/testVORBIS.ogg | Bin 0 -> 4241 bytes
14 files changed, 484 insertions(+), 2 deletions(-)
diff --git a/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
b/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
index f20463bfd3..9b84b0eb3f 100644
--- a/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
+++ b/tika-core/src/main/java/org/apache/tika/metadata/Audio.java
@@ -49,4 +49,26 @@ public interface Audio {
* The disc value exactly as tagged, see {@link #RAW_TRACK_NUMBER}.
*/
Property RAW_DISC_NUMBER = Property.internalText("audio:raw-disc-number");
+
+ /**
+ * Average or nominal bitrate in bits per second (averaged over the MP3
+ * frames, the Vorbis nominal bitrate, or the MP4 'esds' average bitrate).
+ * A per-stream value: in a file with several audio tracks it reflects
+ * the last sound track's sample description.
+ */
+ Property BITRATE = Property.internalInteger("audio:bitrate");
+
+ /**
+ * True if the stream is variable bitrate: the MP3 frames declare differing
+ * bitrates, or the Vorbis identification header does not declare one fixed
+ * rate for upper, nominal and lower.
+ */
+ Property IS_VARIABLE_BITRATE =
Property.internalBoolean("audio:is-variable-bitrate");
+
+ /**
+ * True if the container declares DRM protection through a protected
+ * sample entry format such as 'drms' or 'enca'. A file-level flag: any
+ * protected audio track sets it. Only set when protection is detected.
+ */
+ Property HAS_DRM = Property.internalBoolean("audio:has-drm");
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
index 2f51477eb9..82fb40dd4f 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/Mp3Parser.java
@@ -96,12 +96,30 @@ public class Mp3Parser implements Parser {
// Now iterate over all audio frames in the file
AudioFrame frame = mpegStream.nextFrame();
float duration = 0;
+ long bitRateSum = 0;
+ long frameCount = 0;
+ int baselineBitRate = -1;
+ boolean variableBitRate = false;
+ boolean firstFrame = true;
boolean skipped = true;
while (frame != null && skipped) {
duration += frame.getDuration();
if (firstAudio == null) {
firstAudio = frame;
}
+ //the Xing/Info/VBRI header is a valid MPEG frame but carries no
+ //audio and may use a different bitrate, so keep it out of the
stats
+ if (!(firstFrame && isMetadataFrame(mpegStream))) {
+ int bitRate = frame.getBitRate();
+ bitRateSum += bitRate;
+ frameCount++;
+ if (baselineBitRate < 0) {
+ baselineBitRate = bitRate;
+ } else if (bitRate != baselineBitRate) {
+ variableBitRate = true;
+ }
+ }
+ firstFrame = false;
skipped = mpegStream.skipFrame();
if (skipped) {
frame = mpegStream.nextFrame();
@@ -136,6 +154,12 @@ public class Mp3Parser implements Parser {
ret.lyrics = lyrics;
ret.tags = tags.toArray(new ID3Tags[0]);
ret.duration = duration;
+ if (frameCount > 0) {
+ //MP3 frame duration does not depend on the bitrate, so the plain
+ //mean over the frames is the true average bitrate
+ ret.averageBitRate = (int) (bitRateSum / frameCount);
+ ret.variableBitRate = variableBitRate;
+ }
return ret;
}
@@ -159,6 +183,10 @@ public class Mp3Parser implements Parser {
metadata.set(XMPDM.DURATION, audioAndTags.durationSeconds());
}
+ if (audioAndTags.averageBitRate > 0) {
+ metadata.set(Audio.BITRATE, audioAndTags.averageBitRate);
+ metadata.set(Audio.IS_VARIABLE_BITRATE,
audioAndTags.variableBitRate);
+ }
if (audioAndTags.audio != null) {
metadata.set("channels",
String.valueOf(audioAndTags.audio.getChannels()));
metadata.set("version", audioAndTags.audio.getVersion());
@@ -278,11 +306,34 @@ public class Mp3Parser implements Parser {
public int getMaxRecordSize() {
return ID3v2Frame.getMaxRecordSize();
}
+ /**
+ * Does the current frame's payload start with a Xing, Info or VBRI
+ * header? Those tag frames describe the stream rather than carrying
+ * audio. The markers sit at small fixed offsets that depend on version
+ * and channel mode, all within the first 40 payload bytes.
+ */
+ private static boolean isMetadataFrame(MpegStream mpegStream) throws
IOException {
+ byte[] payload = mpegStream.peekFramePayload(40);
+ for (int i = 0; i + 4 <= payload.length; i++) {
+ if ((payload[i] == 'X' && payload[i + 1] == 'i'
+ && payload[i + 2] == 'n' && payload[i + 3] == 'g')
+ || (payload[i] == 'I' && payload[i + 1] == 'n'
+ && payload[i + 2] == 'f' && payload[i + 3] == 'o')
+ || (payload[i] == 'V' && payload[i + 1] == 'B'
+ && payload[i + 2] == 'R' && payload[i + 3] == 'I')) {
+ return true;
+ }
+ }
+ return false;
+ }
+
protected static class ID3TagsAndAudio {
private ID3Tags[] tags;
private AudioFrame audio;
private LyricsHandler lyrics;
private float duration; // Milliseconds
+ private int averageBitRate; // bits per second, 0 if no frame was seen
+ private boolean variableBitRate;
private float durationSeconds() {
return duration / 1000;
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
index a87757253a..2a8c1aa839 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp3/MpegStream.java
@@ -19,6 +19,7 @@ package org.apache.tika.parser.mp3;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
+import java.util.Arrays;
import org.apache.commons.io.IOUtils;
@@ -113,6 +114,12 @@ class MpegStream extends PushbackInputStream {
*/
private static final int HEADER_SIZE = 4;
+ /**
+ * Pushback capacity: enough for the header handling plus
+ * {@link #peekFramePayload(int)} peeks into the frame payload.
+ */
+ private static final int PEEK_BUFFER_SIZE = 48;
+
/**
* The current MPEG header.
*/
@@ -130,7 +137,7 @@ class MpegStream extends PushbackInputStream {
* @param in the underlying audio stream
*/
public MpegStream(InputStream in) {
- super(in, 2 * HEADER_SIZE);
+ super(in, PEEK_BUFFER_SIZE);
}
/**
@@ -285,7 +292,9 @@ class MpegStream extends PushbackInputStream {
public boolean skipFrame() throws IOException {
if (currentHeader != null) {
long toSkip = currentHeader.getLength() - HEADER_SIZE;
- long skipped = IOUtils.skip(in, toSkip);
+ //skip through this stream, not the wrapped one, so bytes pushed
+ //back by peekFramePayload are honored
+ long skipped = IOUtils.skip(this, toSkip);
currentHeader = null;
if (skipped < toSkip) {
return false;
@@ -295,6 +304,27 @@ class MpegStream extends PushbackInputStream {
return false;
}
+ /**
+ * Reads up to {@code count} bytes of the current frame's payload and
+ * pushes them back, leaving the stream position and the frame accounting
+ * undisturbed. Used to recognize metadata-only frames (Xing/Info/VBRI).
+ */
+ byte[] peekFramePayload(int count) throws IOException {
+ byte[] buffer = new byte[count];
+ int read = 0;
+ while (read < count) {
+ int r = read(buffer, read, count - read);
+ if (r < 0) {
+ break;
+ }
+ read += r;
+ }
+ if (read > 0) {
+ unread(buffer, 0, read);
+ }
+ return read == count ? buffer : Arrays.copyOf(buffer, Math.max(read,
0));
+ }
+
/**
* Advances the underlying stream until the first byte of frame sync is
* found.
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
index d59070bfca..9d15d3afeb 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4BoxHandler.java
@@ -117,6 +117,12 @@ public class TikaMp4BoxHandler extends Mp4BoxHandler {
Long movieTimescale =
directory.getLongObject(Mp4Directory.TAG_TIME_SCALE);
return new TikaMp4MetaHandler(metadata, context, tikaMetadata,
emptyEditDuration, movieTimescale == null ? 0 :
movieTimescale);
+ } else if (box.equals("hdlr") && payload != null && payload.length >=
12
+ && payload[8] == 's' && payload[9] == 'o'
+ && payload[10] == 'u' && payload[11] == 'n') {
+ //sound track: our handler additionally reads DRM markers and the
+ //esds average bitrate from the sample description
+ return new TikaMp4SoundHandler(metadata, context, tikaMetadata);
}
return super.processBox(box, payload, size, context);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java
new file mode 100644
index 0000000000..770c4cd722
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4SoundHandler.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.mp4;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import com.drew.imaging.mp4.Mp4Handler;
+import com.drew.metadata.Metadata;
+import com.drew.metadata.mp4.Mp4Context;
+import com.drew.metadata.mp4.media.Mp4SoundHandler;
+
+import org.apache.tika.io.EndianUtils;
+import org.apache.tika.metadata.Audio;
+
+/**
+ * Extends the sound track handling with what the base handler does not read
+ * from the sample description: DRM protection markers (protected sample entry
+ * formats such as 'drms' or 'enca') and the average bitrate from the 'esds'
+ * elementary stream descriptor. See TIKA-4779.
+ */
+class TikaMp4SoundHandler extends Mp4SoundHandler {
+
+ private final org.apache.tika.metadata.Metadata tikaMetadata;
+
+ TikaMp4SoundHandler(Metadata metadata, Mp4Context context,
+ org.apache.tika.metadata.Metadata tikaMetadata) {
+ super(metadata, context);
+ this.tikaMetadata = tikaMetadata;
+ }
+
+ @Override
+ public Mp4Handler<?> processBox(String type, byte[] payload, long boxSize,
+ Mp4Context context) throws IOException {
+ if ("stsd".equals(type) && payload != null) {
+ extractFromSampleDescriptions(payload);
+ }
+ return super.processBox(type, payload, boxSize, context);
+ }
+
+ /**
+ * Walks the sample description entries: 4 bytes version and flags, a
+ * 4 byte entry count, then one sample entry per count, each starting with
+ * its own size and format fourcc.
+ */
+ private void extractFromSampleDescriptions(byte[] b) {
+ if (b.length < 8) {
+ return;
+ }
+ long entryCount = EndianUtils.getUIntBE(b, 4);
+ int pos = 8;
+ for (long i = 0; i < entryCount && pos + 8 <= b.length; i++) {
+ long size = EndianUtils.getUIntBE(b, pos);
+ if (size < 16 || size > b.length - pos) {
+ break;
+ }
+ int end = pos + (int) size;
+ String format = fourCc(b, pos + 4);
+ //protected streams replace the codec fourcc with a protected
+ //entry format: 'drms' (FairPlay) or 'enca' (ISO common encryption)
+ if ("drms".equals(format) || "enca".equals(format)) {
+ tikaMetadata.set(Audio.HAS_DRM, true);
+ }
+ if (pos + 18 <= end) {
+ //sample entry: 8 byte header, 6 reserved, 2 data ref index,
+ //then version-dependent fixed sound fields before child boxes
+ int version = EndianUtils.getUShortBE(b, pos + 16);
+ int bitRate = findEsdsAverageBitRate(b, pos +
soundEntrySize(version), end);
+ if (bitRate > 0) {
+ tikaMetadata.set(Audio.BITRATE, bitRate);
+ }
+ }
+ pos = end;
+ }
+ }
+
+ /**
+ * Size of the fixed part of a sound sample entry, after which the child
+ * boxes start: 36 bytes for version 0, 52 for version 1 (four extra
+ * 32-bit QuickTime fields), 72 for version 2.
+ */
+ private static int soundEntrySize(int version) {
+ if (version == 1) {
+ return 52;
+ }
+ if (version == 2) {
+ return 72;
+ }
+ return 36;
+ }
+
+ /**
+ * Scans the child boxes of a sample entry for an 'esds' box and returns
+ * its average bitrate, or 0 if there is none. QuickTime version 1/2
+ * entries may nest the 'esds' inside a 'wave' extension box.
+ */
+ private static int findEsdsAverageBitRate(byte[] b, int pos, int end) {
+ while (pos >= 0 && pos + 8 <= end) {
+ long size = EndianUtils.getUIntBE(b, pos);
+ if (size < 8 || size > end - pos) {
+ return 0;
+ }
+ String type = fourCc(b, pos + 4);
+ if ("esds".equals(type)) {
+ return readEsdsAverageBitRate(b, pos + 8, pos + (int) size);
+ }
+ if ("wave".equals(type)) {
+ int nested = findEsdsAverageBitRate(b, pos + 8, pos + (int)
size);
+ if (nested > 0) {
+ return nested;
+ }
+ }
+ pos += (int) size;
+ }
+ return 0;
+ }
+
+ /**
+ * Extracts the average bitrate from an 'esds' box body, or returns 0 if
+ * the descriptors cannot be walked. The chain is an ES_Descriptor (tag
+ * 0x03) with three optional fields signalled by its flags byte, followed
+ * by a DecoderConfigDescriptor (tag 0x04) whose fixed fields end with the
+ * maximum and average bitrates.
+ */
+ private static int readEsdsAverageBitRate(byte[] b, int pos, int end) {
+ //4 bytes version and flags, then the ES descriptor
+ pos += 4;
+ if (pos >= end || b[pos] != 0x03) {
+ return 0;
+ }
+ pos = skipDescriptorLength(b, pos + 1);
+ if (pos + 3 > end) {
+ return 0;
+ }
+ //ES_ID (2 bytes), then a flags/priority byte announcing the
+ //optional stream dependence, URL and OCR fields
+ int flags = b[pos + 2] & 0xFF;
+ pos += 3;
+ if ((flags & 0x80) != 0) {
+ pos += 2;
+ }
+ if ((flags & 0x40) != 0) {
+ if (pos >= end) {
+ return 0;
+ }
+ pos += 1 + (b[pos] & 0xFF);
+ }
+ if ((flags & 0x20) != 0) {
+ pos += 2;
+ }
+ if (pos >= end || b[pos] != 0x04) {
+ return 0;
+ }
+ pos = skipDescriptorLength(b, pos + 1);
+ //object type (1), stream type (1), buffer size (3), max bitrate (4)
+ pos += 9;
+ if (pos + 4 > end) {
+ return 0;
+ }
+ long averageBitRate = EndianUtils.getUIntBE(b, pos);
+ return averageBitRate > 0 && averageBitRate <= Integer.MAX_VALUE
+ ? (int) averageBitRate : 0;
+ }
+
+ /**
+ * Skips a descriptor's variable length encoding (bytes with the high bit
+ * set continue the length) and returns the position of the payload.
+ */
+ private static int skipDescriptorLength(byte[] b, int pos) {
+ while (pos < b.length && (b[pos] & 0x80) != 0) {
+ pos++;
+ }
+ return pos + 1;
+ }
+
+ private static String fourCc(byte[] b, int pos) {
+ return new String(b, pos, 4, StandardCharsets.ISO_8859_1);
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
index a4263db45a..dc6b78f995 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/ogg/VorbisParser.java
@@ -32,6 +32,7 @@ import org.xml.sax.SAXException;
import org.apache.tika.annotation.TikaComponent;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Audio;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.XMPDM;
import org.apache.tika.mime.MediaType;
@@ -88,6 +89,23 @@ public class VorbisParser extends OggAudioParser {
metadata.set(XMPDM.AUDIO_SAMPLE_RATE, (int) info.getRate());
metadata.add("version", "Vorbis " + info.getVersion());
+ int upper = info.getBitrateUpper();
+ int nominal = info.getBitrateNominal();
+ int lower = info.getBitrateLower();
+ if (nominal > 0 || upper > 0 || lower > 0) {
+ if (nominal > 0) {
+ metadata.set(Audio.BITRATE, nominal);
+ } else if (upper > 0 && upper == lower) {
+ //a zero-width bitrate bracket declares an exact rate as well
+ metadata.set(Audio.BITRATE, upper);
+ }
+ //fixed rate when all three declared rates agree, or when a
+ //zero-width bracket is declared without a nominal rate
+ boolean fixedRate = (nominal > 0 && upper == nominal && lower ==
nominal)
+ || (nominal <= 0 && upper > 0 && upper == lower);
+ metadata.set(Audio.IS_VARIABLE_BITRATE, !fixedRate);
+ }
+
extractChannelInfo(metadata, info);
}
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
index e59f5716ca..af1c03f55d 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp3/Mp3ParserTest.java
@@ -104,6 +104,8 @@ public class Mp3ParserTest extends TikaTest {
assertEquals("MPEG 3 Layer III Version 1", metadata.get("version"));
assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE));
assertEquals("1", metadata.get("channels"));
+ assertEquals("128000", metadata.get(Audio.BITRATE));
+ assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
// Check XMPDM-typed audio properties
assertEquals("Test Album", metadata.get(XMPDM.ALBUM));
@@ -166,6 +168,36 @@ public class Mp3ParserTest extends TikaTest {
checkDuration(metadata, 2);
}
+ /**
+ * The frames of testMP3vbr.mp3 alternate between 128 and 192 kbps, so
+ * the reported bitrate is the average and the stream is variable rate.
+ */
+ @Test
+ public void testMp3VariableBitRate() throws Exception {
+ Metadata metadata = new Metadata();
+ getText("testMP3vbr.mp3", metadata);
+
+ assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
+ assertEquals("160000", metadata.get(Audio.BITRATE));
+ assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+
+ /**
+ * The first frame of testMP3cbrInfoTag.mp3 is an 'Info' tag frame
+ * written at 128 kbps while the audio frames are 320 kbps. The tag frame
+ * must not show up in the bitrate statistics: the file is constant rate
+ * at 320 kbps, not variable rate.
+ */
+ @Test
+ public void testMp3ConstantBitRateWithInfoTagFrame() throws Exception {
+ Metadata metadata = new Metadata();
+ getText("testMP3cbrInfoTag.mp3", metadata);
+
+ assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
+ assertEquals("320000", metadata.get(Audio.BITRATE));
+ assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+
/**
* Test that with only ID3v2 tags, of version 2.4, we get the full
* set of information out.
@@ -271,6 +303,10 @@ public class Mp3ParserTest extends TikaTest {
Metadata metadata = new Metadata();
String content = getText("testMP3lyrics.mp3", metadata);
+ //a real 192 kbps constant bitrate file
+ assertEquals("192000", metadata.get(Audio.BITRATE));
+ assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+
assertEquals("audio/mpeg", metadata.get(Metadata.CONTENT_TYPE));
assertEquals("Test Title", metadata.get(TikaCoreProperties.TITLE));
assertEquals("Test Artist", metadata.get(TikaCoreProperties.CREATOR));
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
index 4ceb23d49d..addf349d73 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/MP4ParserTest.java
@@ -108,6 +108,9 @@ public class MP4ParserTest extends TikaTest {
assertEquals("Test Genre", metadata.get(XMPDM.GENRE));
assertEquals("Test Comments",
metadata.get(XMPDM.LOG_COMMENT.getName()));
assertEquals("1", metadata.get(XMPDM.TRACK_NUMBER));
+ //average bitrate from the esds elementary stream descriptor
+ assertEquals("256000", metadata.get(Audio.BITRATE));
+ assertNull(metadata.get(Audio.HAS_DRM));
//the totals from the trkn/disk atoms were previously read and
discarded
assertEquals("42", metadata.get(Audio.TRACK_COUNT));
assertEquals("Test Album Artist", metadata.get(XMPDM.ALBUM_ARTIST));
@@ -292,6 +295,25 @@ public class MP4ParserTest extends TikaTest {
return vals;
} */
+ @Test
+ public void testDrmProtectedM4a() throws Exception {
+ //the sample description declares a protected 'drms' sample entry
+ Metadata metadata = new Metadata();
+ getText("testMP4_drm.m4a", metadata);
+ assertEquals("true", metadata.get(Audio.HAS_DRM));
+ }
+
+ @Test
+ public void testEsdsWithDescriptorFlags() throws Exception {
+ //the ES descriptor declares the optional stream dependence, URL and
+ //OCR fields, which shift the DecoderConfigDescriptor; the URL string
+ //deliberately reads "sinf" so a raw fourcc scan would misreport DRM
+ Metadata metadata = new Metadata();
+ getText("testMP4_esdsFlags.m4a", metadata);
+ assertEquals("96000", metadata.get(Audio.BITRATE));
+ assertNull(metadata.get(Audio.HAS_DRM));
+ }
+
@Test
public void testQuickTimeMetadataKeys() throws Exception {
//QuickTime item-list metadata (moov/meta/keys+ilst, the
com.apple.quicktime.*
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
new file mode 100644
index 0000000000..142d202da2
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/ogg/VorbisParserTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.ogg;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.gagravarr.vorbis.VorbisInfo;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Audio;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.XMPDM;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * Tests the bitrate mapping from the Vorbis identification header, end to end
+ * against a real file and at unit level with directly constructed info
+ * objects for the header combinations the real fixture does not cover.
+ */
+public class VorbisParserTest extends TikaTest {
+
+ @Test
+ public void testRealFile() throws Exception {
+ //a real quality-mode encoding: nominal 80000, no upper/lower bounds
+ Metadata metadata = new Metadata();
+ try (TikaInputStream tis = TikaInputStream.get(
+ getResourceAsStream("/test-documents/testVORBIS.ogg"))) {
+ new VorbisParser().parse(tis, new DefaultHandler(), metadata,
+ new ParseContext());
+ }
+ assertEquals("80000", metadata.get(Audio.BITRATE));
+ assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ assertEquals("44100", metadata.get(XMPDM.AUDIO_SAMPLE_RATE));
+ }
+
+ private static Metadata extractInfo(int upper, int nominal, int lower)
throws Exception {
+ VorbisInfo info = new VorbisInfo();
+ info.setRate(44100);
+ info.setBitrateUpper(upper);
+ info.setBitrateNominal(nominal);
+ info.setBitrateLower(lower);
+
+ Metadata metadata = new Metadata();
+ new VorbisParser().extractInfo(metadata, info);
+ return metadata;
+ }
+
+ @Test
+ public void testNominalOnlyIsVariable() throws Exception {
+ //the common quality-mode encoding declares only a nominal rate
+ Metadata metadata = extractInfo(0, 192000, 0);
+ assertEquals("192000", metadata.get(Audio.BITRATE));
+ assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+
+ @Test
+ public void testAllRatesEqualIsFixed() throws Exception {
+ Metadata metadata = extractInfo(192000, 192000, 192000);
+ assertEquals("192000", metadata.get(Audio.BITRATE));
+ assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+
+ @Test
+ public void testZeroWidthBracketIsFixed() throws Exception {
+ //managed-mode encoding without a nominal rate: an upper and lower
+ //bound that agree still pin the stream to one exact rate
+ Metadata metadata = extractInfo(128000, 0, 128000);
+ assertEquals("128000", metadata.get(Audio.BITRATE));
+ assertEquals("false", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+
+ @Test
+ public void testDivergentBracketIsVariable() throws Exception {
+ //a real bracket without a nominal rate: variable, no single bitrate
+ Metadata metadata = extractInfo(256000, 0, 96000);
+ assertNull(metadata.get(Audio.BITRATE));
+ assertEquals("true", metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+
+ @Test
+ public void testNoDeclaredRates() throws Exception {
+ Metadata metadata = extractInfo(0, 0, 0);
+ assertNull(metadata.get(Audio.BITRATE));
+ assertNull(metadata.get(Audio.IS_VARIABLE_BITRATE));
+ }
+}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3cbrInfoTag.mp3
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3cbrInfoTag.mp3
new file mode 100644
index 0000000000..25cfcb0e49
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3cbrInfoTag.mp3
differ
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3vbr.mp3
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3vbr.mp3
new file mode 100644
index 0000000000..6d0267ca55
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP3vbr.mp3
differ
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_drm.m4a
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_drm.m4a
new file mode 100644
index 0000000000..4a37c1282b
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_drm.m4a
differ
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_esdsFlags.m4a
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_esdsFlags.m4a
new file mode 100644
index 0000000000..5fda062fb7
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testMP4_esdsFlags.m4a
differ
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS.ogg
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS.ogg
new file mode 100644
index 0000000000..1a02d2205a
Binary files /dev/null and
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/resources/test-documents/testVORBIS.ogg
differ