diff --git a/CHANGES.txt b/CHANGES.txt index 80e898aa721..981d4be904c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,11 @@ Release 4.1.0 - unreleased + * A plain MP4 is typed by the tracks it holds rather than by its brand: + MP4TrackDetector reads the handler types in the movie box and returns + video/mp4, audio/mp4 or application/mp4. The brand of most MP4 files, + isom, says nothing about their content, so the mime magic could only + fall back to video/quicktime (TIKA-3646, TIKA-2935). + * Raster previews for the vector thumbnails of Office documents: the new poi-metafile-renderer draws EMF and WMF images through POI (a PNG of a configurable width; Word's bitmap-in-WMF thumbnails from the bitmap diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/detect/mp4/MP4TrackDetector.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/detect/mp4/MP4TrackDetector.java new file mode 100644 index 00000000000..7d5612b8ab1 --- /dev/null +++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/detect/mp4/MP4TrackDetector.java @@ -0,0 +1,310 @@ +/* + * 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.detect.mp4; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.io.IOUtils; + +import org.apache.tika.annotation.TikaComponent; +import org.apache.tika.detect.Detector; +import org.apache.tika.io.TikaInputStream; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.mp4.Mp4Boxes; + +/** + * Types a plain MP4 file by what it contains, as RFC 4337 asks: video/mp4 + * when it has a video track, audio/mp4 when it has only audio, and + * application/mp4 when it has neither (TIKA-2935, TIKA-3646). + *
+ * The mime magic cannot do this. It only knows the brand in the + * {@code ftyp} box, and the brand of the great majority of MP4 files, + * {@code isom}, says nothing about their content: Tika's own + * {@code testMP4Video.mp4} and {@code testMP4AudioOnly.mp4} both carry it. + * Brands that name a format of their own (M4A, 3GP, HEIC, AVIF, CR3, ...) + * keep their magic and are left alone here. + *
+ * The detector walks the top level boxes by their size fields, which skips
+ * the media data rather than reading it, and reads the handler type of each
+ * track in the movie box. A file whose movie box is unreachable (missing,
+ * beyond the limits below, or not backed by a file) is left to the magic.
+ */
+@TikaComponent
+public class MP4TrackDetector implements Detector {
+
+ private static final long serialVersionUID = 1L;
+
+ static final MediaType MP4_VIDEO = MediaType.video("mp4");
+ static final MediaType MP4_AUDIO = MediaType.audio("mp4");
+ static final MediaType MP4_APPLICATION = MediaType.application("mp4");
+
+ /**
+ * The brands of a plain MP4. A brand naming a specific format is not
+ * here: those files are typed by their own magic.
+ */
+ private static final Set
+ * Shared by the parts of Tika that read these files without handing them to
+ * a full parser: {@link Mp4SampleEntries} and the MP4 detector. Every method
+ * takes the region the boxes live in and returns -1 rather than reading
+ * outside it, so a crafted size ends a walk instead of a parse.
+ */
+public final class Mp4Boxes {
+
+ /**
+ * The header of a box with a 32 bit size: the size and the FourCC.
+ */
+ public static final int HEADER = 8;
+
+ /**
+ * The header of a box with a 64 bit largesize.
+ */
+ public static final int LARGE_HEADER = 16;
+
+ private Mp4Boxes() {
+ }
+
+ /**
+ * The offset one past the box at {@code pos}, or -1 when its size is
+ * invalid or reaches past {@code end}.
+ */
+ public static int boxEnd(byte[] b, int pos, int end) {
+ if (pos < 0 || pos > end - HEADER || end > b.length) {
+ return -1;
+ }
+ long size = EndianUtils.getUIntBE(b, pos);
+ if (size == 1) {
+ if (pos > end - LARGE_HEADER) {
+ return -1;
+ }
+ //a largesize beyond 63 bits goes negative and fails the check below
+ size = (EndianUtils.getUIntBE(b, pos + HEADER) << 32)
+ + EndianUtils.getUIntBE(b, pos + HEADER + 4);
+ if (size < LARGE_HEADER) {
+ return -1;
+ }
+ } else if (size == 0) {
+ //the box extends to the end of what encloses it
+ size = end - pos;
+ } else if (size < HEADER) {
+ return -1;
+ }
+ if (size > end - pos) {
+ return -1;
+ }
+ return pos + (int) size;
+ }
+
+ /**
+ * The offset where the payload of the box at {@code pos} starts, which
+ * follows the largesize where there is one, or -1 for an invalid box.
+ */
+ public static int payloadStart(byte[] b, int pos, int end) {
+ if (boxEnd(b, pos, end) < 0) {
+ return -1;
+ }
+ return EndianUtils.getUIntBE(b, pos) == 1 ? pos + LARGE_HEADER : pos + HEADER;
+ }
+
+ /**
+ * The offset of the first box of the given type among the boxes in
+ * {@code [pos, end)}, or -1 if there is none.
+ *
+ * @param maxBoxes how many boxes to look at before giving up
+ */
+ public static int findBox(byte[] b, int pos, int end, String type, int maxBoxes) {
+ for (int box = 0; box < maxBoxes && pos >= 0 && pos <= end - HEADER; box++) {
+ int boxEnd = boxEnd(b, pos, end);
+ if (boxEnd < 0 || boxEnd <= pos) {
+ return -1;
+ }
+ if (type.equals(fourCC(b, pos + 4))) {
+ return pos;
+ }
+ pos = boxEnd;
+ }
+ return -1;
+ }
+
+ /**
+ * Reads a FourCC as it is, for comparing against known box types.
+ */
+ public static String fourCC(byte[] b, int pos) {
+ return new String(b, pos, 4, StandardCharsets.ISO_8859_1);
+ }
+
+ /**
+ * Reads a FourCC for exposing it as a metadata value: null unless all four
+ * bytes are printable ASCII, with trailing spaces trimmed (QuickTime pads
+ * short codes such as 'raw ' and 'rle ' with spaces). Codes that are blank
+ * after trimming are null as well.
+ */
+ public static String printableFourCC(byte[] b, int pos) {
+ int len = 4;
+ while (len > 0 && b[pos + len - 1] == ' ') {
+ len--;
+ }
+ if (len == 0) {
+ return null;
+ }
+ for (int i = 0; i < len; i++) {
+ int c = b[pos + i] & 0xFF;
+ if (c < 0x20 || c > 0x7E) {
+ return null;
+ }
+ }
+ return new String(b, pos, len, StandardCharsets.US_ASCII);
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/Mp4SampleEntries.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/Mp4SampleEntries.java
index 91bc9ff2fa8..b277270b00b 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/Mp4SampleEntries.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/Mp4SampleEntries.java
@@ -16,7 +16,6 @@
*/
package org.apache.tika.parser.mp4;
-import java.nio.charset.StandardCharsets;
import org.apache.tika.io.EndianUtils;
@@ -36,6 +35,12 @@ final class Mp4SampleEntries {
*/
static final int SAMPLE_ENTRY_FIELDS = 8;
+ /**
+ * How many child boxes of an entry are looked at when resolving a
+ * protected entry's original format.
+ */
+ private static final int MAX_CHILD_BOXES = 64;
+
interface Visitor {
/**
* @param fourCC the entry's FourCC, or null if it is not printable
@@ -74,7 +79,7 @@ static void walk(byte[] b, Visitor visitor) {
return;
}
int end = pos + (int) size;
- visitor.entry(printableFourCC(b, pos + 4), b, pos + header, end);
+ visitor.entry(Mp4Boxes.printableFourCC(b, pos + 4), b, pos + header, end);
pos = end;
}
}
@@ -99,76 +104,18 @@ static boolean isProtected(String fourCC) {
* @param end offset one past the entry's last byte
*/
static String originalFormat(byte[] b, int pos, int end) {
- int sinf = findBox(b, pos, end, "sinf");
+ int sinf = Mp4Boxes.findBox(b, pos, end, "sinf", MAX_CHILD_BOXES);
if (sinf < 0) {
return null;
}
- int sinfEnd = boxEnd(b, sinf, end);
- int frma = findBox(b, sinf + 8, sinfEnd, "frma");
- if (frma < 0 || boxEnd(b, frma, sinfEnd) < frma + 12) {
+ int sinfEnd = Mp4Boxes.boxEnd(b, sinf, end);
+ int frma = sinfEnd < 0 ? -1
+ : Mp4Boxes.findBox(b, sinf + 8, sinfEnd, "frma", MAX_CHILD_BOXES);
+ if (frma < 0 || Mp4Boxes.boxEnd(b, frma, sinfEnd) < frma + 12) {
//the box must hold its 4 byte payload, not borrow it from the next box
return null;
}
- return printableFourCC(b, frma + 8);
- }
-
- /**
- * Returns the offset of the first box of the given type among the boxes
- * in [pos, end), or -1.
- */
- private static int findBox(byte[] b, int pos, int end, String type) {
- while (pos >= 0 && pos + 8 <= end) {
- int boxEnd = boxEnd(b, pos, end);
- if (boxEnd < 0) {
- return -1;
- }
- if (type.equals(fourCC(b, pos + 4))) {
- return pos;
- }
- pos = boxEnd;
- }
- return -1;
+ return Mp4Boxes.printableFourCC(b, frma + 8);
}
- /**
- * Returns the offset one past the box starting at pos, or -1 if its size
- * is invalid or runs past end.
- */
- private static int boxEnd(byte[] b, int pos, int end) {
- long size = EndianUtils.getUIntBE(b, pos);
- if (size < 8 || size > end - pos) {
- return -1;
- }
- return pos + (int) size;
- }
-
- /**
- * Reads a FourCC as it is, for comparing against known box types.
- */
- static String fourCC(byte[] b, int pos) {
- return new String(b, pos, 4, StandardCharsets.ISO_8859_1);
- }
-
- /**
- * Reads a FourCC for exposing it as a metadata value: null unless all four
- * bytes are printable ASCII, with trailing spaces trimmed (QuickTime pads
- * short codes such as 'raw ' and 'rle ' with spaces). Codes that are blank
- * after trimming are null as well.
- */
- static String printableFourCC(byte[] b, int pos) {
- int len = 4;
- while (len > 0 && b[pos + len - 1] == ' ') {
- len--;
- }
- if (len == 0) {
- return null;
- }
- for (int i = 0; i < len; i++) {
- int c = b[pos + i] & 0xFF;
- if (c < 0x20 || c > 0x7E) {
- return null;
- }
- }
- return new String(b, pos, len, StandardCharsets.US_ASCII);
- }
}
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
index b8bdc87458f..acab3cc99e2 100644
--- 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
@@ -121,7 +121,7 @@ private static int findEsdsAverageBitRate(byte[] b, int pos, int end, int depth)
if (size < 8 || size > end - pos) {
return 0;
}
- String type = Mp4SampleEntries.fourCC(b, pos + 4);
+ String type = Mp4Boxes.fourCC(b, pos + 4);
if ("esds".equals(type)) {
return readEsdsAverageBitRate(b, pos + 8, pos + (int) size);
}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4VideoHandler.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4VideoHandler.java
index 247f41c912c..566fe135acc 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4VideoHandler.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/main/java/org/apache/tika/parser/mp4/TikaMp4VideoHandler.java
@@ -93,7 +93,7 @@ private static int findBtrtAverageBitRate(byte[] b, int pos, int end) {
if (size < 8 || size > end - pos) {
return 0;
}
- if ("btrt".equals(Mp4SampleEntries.fourCC(b, pos + 4)) && pos + 20 <= end) {
+ if ("btrt".equals(Mp4Boxes.fourCC(b, pos + 4)) && pos + 20 <= end) {
long averageBitRate = EndianUtils.getUIntBE(b, pos + 16);
return averageBitRate > 0 && averageBitRate <= Integer.MAX_VALUE
? (int) averageBitRate : 0;
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/detect/mp4/MP4TrackDetectorTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/detect/mp4/MP4TrackDetectorTest.java
new file mode 100644
index 00000000000..379b6bc8239
--- /dev/null
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/detect/mp4/MP4TrackDetectorTest.java
@@ -0,0 +1,225 @@
+/*
+ * 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.detect.mp4;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Random;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import org.apache.tika.TikaTest;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.ParseContext;
+
+/**
+ * An MP4 is typed by the tracks it holds, not by its brand: Tika's own
+ * fixtures for video and for audio both carry the isom brand (TIKA-3646).
+ */
+public class MP4TrackDetectorTest extends TikaTest {
+
+ private final MP4TrackDetector detector = new MP4TrackDetector();
+
+ @ParameterizedTest
+ @CsvSource({"testMP4Video.mp4, video/mp4", "testMP4AudioOnly.mp4, audio/mp4"})
+ public void testTypeFollowsTheTracks(String file, String expected) throws Exception {
+ try (InputStream is = getResourceAsStream("/test-documents/" + file);
+ TikaInputStream tis = TikaInputStream.get(is)) {
+ assertEquals(expected, detect(tis));
+ }
+ }
+
+ /**
+ * The movie box may sit behind the media data; walking the box sizes
+ * skips over that rather than reading it.
+ */
+ @Test
+ public void testMovieBoxAfterTheMediaData(@TempDir Path tmp) throws Exception {
+ Path file = tmp.resolve("moov-last.mp4");
+ Files.write(file, mp4(box("free", new byte[8]), mdat(1024 * 1024), moov("soun")));
+ try (TikaInputStream tis = TikaInputStream.get(file)) {
+ assertEquals("audio/mp4", detect(tis));
+ }
+ }
+
+ /**
+ * A movie box further in than the prefix is reached by spooling the
+ * stream, which is what the parse would do next anyway.
+ */
+ @Test
+ public void testMovieBoxBeyondTheStreamPrefix() throws Exception {
+ byte[] mp4 = mp4(mdat(1024 * 1024), moov("soun"));
+ try (TikaInputStream tis = TikaInputStream.get(mp4)) {
+ assertEquals("audio/mp4", detect(tis));
+ }
+ }
+
+ /**
+ * A video track anywhere in the movie makes it a video.
+ */
+ @Test
+ public void testAudioAndVideoTracks() throws Exception {
+ byte[] mp4 = mp4(box("moov", concat(track("soun"), track("vide"))));
+ try (TikaInputStream tis = TikaInputStream.get(mp4)) {
+ assertEquals("video/mp4", detect(tis));
+ }
+ }
+
+ /**
+ * A movie whose tracks are neither audio nor video is neither.
+ */
+ @Test
+ public void testTrackless() throws Exception {
+ byte[] mp4 = mp4(moov("hint"));
+ try (TikaInputStream tis = TikaInputStream.get(mp4)) {
+ assertEquals("application/mp4", detect(tis));
+ }
+ }
+
+ /**
+ * The handler of the metadata is not a track handler: it lives in
+ * moov/udta/meta, not in moov/trak/mdia.
+ */
+ @Test
+ public void testMetadataHandlerIsNotATrack() throws Exception {
+ byte[] meta = box("udta", box("meta", hdlr("vide")));
+ byte[] mp4 = mp4(box("moov", concat(track("soun"), meta)));
+ try (TikaInputStream tis = TikaInputStream.get(mp4)) {
+ assertEquals("audio/mp4", detect(tis));
+ }
+ }
+
+ /**
+ * Brands that name a format of their own keep their own magic, and a
+ * file without a reachable movie box is left to it as well.
+ */
+ @ParameterizedTest
+ @CsvSource({"M4A , true", "heic, true", "isom, false"})
+ public void testUnclaimedFiles(String brand, boolean hasMoov) throws Exception {
+ byte[] mp4 = hasMoov ? mp4(brand, moov("soun")) : mp4(brand, mdat(64));
+ try (TikaInputStream tis = TikaInputStream.get(mp4)) {
+ assertEquals("application/octet-stream", detect(tis));
+ }
+ }
+
+ /**
+ * Every truncation of a well formed file, and random bytes behind a
+ * valid header, must end the walk rather than the detection.
+ */
+ @Test
+ public void testMalformedFilesAreHarmless() throws Exception {
+ byte[] mp4 = mp4(box("free", new byte[8]), mdat(64), moov("soun"));
+ for (int length = 0; length <= mp4.length; length++) {
+ try (TikaInputStream tis = TikaInputStream.get(Arrays.copyOf(mp4, length))) {
+ detect(tis);
+ }
+ }
+ Random random = new Random(42);
+ byte[] header = mp4();
+ for (int i = 0; i < 200; i++) {
+ byte[] noise = new byte[256];
+ random.nextBytes(noise);
+ System.arraycopy(header, 0, noise, 0, Math.min(header.length, noise.length));
+ try (TikaInputStream tis = TikaInputStream.get(noise)) {
+ detect(tis);
+ }
+ }
+ }
+
+ /**
+ * A box declaring more than the file holds ends the walk.
+ */
+ @Test
+ public void testOversizedBox() throws Exception {
+ byte[] mp4 = mp4(box("mdat", new byte[8]));
+ int mdat = mp4.length - 16;
+ mp4[mdat] = 0x7F;
+ mp4[mdat + 1] = (byte) 0xFF;
+ mp4[mdat + 2] = (byte) 0xFF;
+ mp4[mdat + 3] = (byte) 0xFF;
+ try (TikaInputStream tis = TikaInputStream.get(mp4)) {
+ assertEquals("application/octet-stream", detect(tis));
+ }
+ }
+
+ private String detect(TikaInputStream tis) throws IOException {
+ return detector.detect(tis, new Metadata(), new ParseContext()).toString();
+ }
+
+ private static byte[] mp4(byte[]... boxes) throws IOException {
+ return mp4("isom", boxes);
+ }
+
+ private static byte[] mp4(String brand, byte[]... boxes) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ out.write(box("ftyp", (brand + " " + brand).getBytes(StandardCharsets.US_ASCII)));
+ for (byte[] b : boxes) {
+ out.write(b);
+ }
+ return out.toByteArray();
+ }
+
+ private static byte[] mdat(int size) throws IOException {
+ return box("mdat", new byte[size]);
+ }
+
+ private static byte[] moov(String handler) throws IOException {
+ return box("moov", track(handler));
+ }
+
+ /**
+ * A track box holding the handler where a real one has it: trak, mdia,
+ * hdlr.
+ */
+ private static byte[] track(String handler) throws IOException {
+ return box("trak", box("mdia", hdlr(handler)));
+ }
+
+ private static byte[] hdlr(String handler) throws IOException {
+ return box("hdlr",
+ concat(new byte[8], handler.getBytes(StandardCharsets.US_ASCII), new byte[12]));
+ }
+
+ private static byte[] box(String type, byte[] payload) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ int size = 8 + payload.length;
+ out.write(new byte[]{(byte) (size >>> 24), (byte) (size >>> 16), (byte) (size >>> 8),
+ (byte) size});
+ out.write(type.getBytes(StandardCharsets.US_ASCII));
+ out.write(payload);
+ return out.toByteArray();
+ }
+
+ private static byte[] concat(byte[]... parts) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ for (byte[] p : parts) {
+ out.write(p);
+ }
+ return out.toByteArray();
+ }
+}
diff --git a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/Mp4SampleEntriesTest.java b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/Mp4SampleEntriesTest.java
index 9b3a2499924..0d486f7971e 100644
--- a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/Mp4SampleEntriesTest.java
+++ b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-audiovideo-module/src/test/java/org/apache/tika/parser/mp4/Mp4SampleEntriesTest.java
@@ -68,14 +68,14 @@ public void testTruncatedAndUndersizedEntriesStopTheWalk() {
@Test
public void testPrintableFourCC() {
- assertEquals("mp4a", Mp4SampleEntries.printableFourCC(ascii("mp4a"), 0));
+ assertEquals("mp4a", Mp4Boxes.printableFourCC(ascii("mp4a"), 0));
//QuickTime pads short codes with spaces
- assertEquals("raw", Mp4SampleEntries.printableFourCC(ascii("raw "), 0));
- assertEquals("rle", Mp4SampleEntries.printableFourCC(ascii("rle "), 0));
- assertNull(Mp4SampleEntries.printableFourCC(ascii(" "), 0));
- assertNull(Mp4SampleEntries.printableFourCC(new byte[]{0, 1, 2, 3}, 0));
- assertNull(Mp4SampleEntries.printableFourCC(new byte[]{'a', 'v', 'c', 0x7F}, 0));
- assertNull(Mp4SampleEntries.printableFourCC(new byte[]{(byte) 0xE4, 'v', 'c', '1'}, 0));
+ assertEquals("raw", Mp4Boxes.printableFourCC(ascii("raw "), 0));
+ assertEquals("rle", Mp4Boxes.printableFourCC(ascii("rle "), 0));
+ assertNull(Mp4Boxes.printableFourCC(ascii(" "), 0));
+ assertNull(Mp4Boxes.printableFourCC(new byte[]{0, 1, 2, 3}, 0));
+ assertNull(Mp4Boxes.printableFourCC(new byte[]{'a', 'v', 'c', 0x7F}, 0));
+ assertNull(Mp4Boxes.printableFourCC(new byte[]{(byte) 0xE4, 'v', 'c', '1'}, 0));
//an unprintable FourCC reaches the visitor as null but does not stop the walk
byte[] stsd = stsd(entry(24, "\u0001vc1", 16), entry(24, "mp4a", 16));
assertEquals(List.of("null:16:32", "mp4a:40:56"), walk(stsd));