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 @@ protected static ID3TagsAndAudio getAllTagHandlers(InputStream tis, ContentHandl // 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 @@ protected static ID3TagsAndAudio getAllTagHandlers(InputStream tis, ContentHandl 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 void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata 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 void setMaxRecordSize(int maxRecordSize) { 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 28b7a71fe6..aebe2af710 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 @@ import java.io.IOException; import java.io.InputStream; import java.io.PushbackInputStream; +import java.util.Arrays; import org.apache.commons.io.IOUtils; @@ -105,6 +106,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. */ @@ -122,7 +129,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); } /** @@ -264,7 +271,9 @@ public AudioFrame nextFrame() throws IOException { 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; @@ -274,6 +283,27 @@ public boolean skipFrame() throws IOException { 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 9f65e48e32..69befa61ca 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 @@ -88,6 +88,12 @@ public Mp4Handler processBox(@NotNull String box, @Nullable byte[] payload, } else if (box.equals("ilst")) { processQuickTimeItemList(payload); return this; + } 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.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 @@ protected void extractInfo(Metadata metadata, VorbisInfo info) throws TikaExcept 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 0bf7053c72..241f53352b 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 void testMp3ParsingID3v2() throws Exception { 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 void testMp3ParsingID3v1v2() throws Exception { 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 void testMp3ParsingLyrics() throws Exception { 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 afa8d97619..fd77f5c37f 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 @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Arrays; @@ -102,6 +103,9 @@ public void testMP4ParsingAudio() throws Exception { 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)); @@ -286,6 +290,25 @@ private Set getVals(Metadata m, String k) { 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