From fb170bd78de2d11a89122c187dac7445c0b60e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Opfermann?= Date: Sun, 25 Feb 2024 17:27:02 +0100 Subject: [PATCH 1/6] Add more UDTA information. Mostly related to the basic information that can be fount in the Windows Details panel, title, subtitle, etc. --- .../com/drew/metadata/mp4/Mp4BoxHandler.java | 699 ++++++++++-------- .../com/drew/metadata/mp4/Mp4Directory.java | 142 ++-- 2 files changed, 463 insertions(+), 378 deletions(-) diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 17e56e642..0cb6cfbe6 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -20,6 +20,22 @@ */ package com.drew.metadata.mp4; +import static com.drew.metadata.mp4.Mp4Directory.TAG_CATEGORY; +import static com.drew.metadata.mp4.Mp4Directory.TAG_COMMENT; +import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_MOOD; +import static com.drew.metadata.mp4.Mp4Directory.TAG_SUBTITLE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_TITLE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_USER_RATING; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import com.drew.imaging.mp4.Mp4Handler; import com.drew.lang.DateUtil; import com.drew.lang.Rational; @@ -28,319 +44,380 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; -import com.drew.metadata.mp4.media.*; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE; -import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE; +import com.drew.metadata.mp4.media.Mp4HintHandler; +import com.drew.metadata.mp4.media.Mp4MetaHandler; +import com.drew.metadata.mp4.media.Mp4SoundHandler; +import com.drew.metadata.mp4.media.Mp4TextHandler; +import com.drew.metadata.mp4.media.Mp4UuidBoxHandler; +import com.drew.metadata.mp4.media.Mp4VideoHandler; /** * @author Payton Garland */ -public class Mp4BoxHandler extends Mp4Handler -{ - public Mp4BoxHandler(Metadata metadata) - { - super(metadata); - } - - @NotNull - @Override - protected Mp4Directory getDirectory() - { - return new Mp4Directory(); - } - - @Override - public boolean shouldAcceptBox(@NotNull String type) - { - return type.equals(Mp4BoxTypes.BOX_FILE_TYPE) - || type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER) - || type.equals(Mp4BoxTypes.BOX_HANDLER) - || type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER) - || type.equals(Mp4BoxTypes.BOX_TRACK_HEADER) - || type.equals(Mp4BoxTypes.BOX_USER_DATA) - || type.equals(Mp4BoxTypes.BOX_USER_DEFINED); - } - - @Override - public boolean shouldAcceptContainer(@NotNull String type) - { - return type.equals(Mp4ContainerTypes.BOX_TRACK) - || type.equals(Mp4ContainerTypes.BOX_METADATA) - || type.equals(Mp4ContainerTypes.BOX_MOVIE) - || type.equals(Mp4ContainerTypes.BOX_MEDIA); - } - - @Override - public Mp4Handler processBox(@NotNull String type, @Nullable byte[] payload, long boxSize, Mp4Context context) throws IOException - { - if (payload != null) { - SequentialReader reader = new SequentialByteArrayReader(payload); - if (type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER)) { - processMovieHeader(reader); - } else if (type.equals(Mp4BoxTypes.BOX_FILE_TYPE)) { - processFileType(reader, boxSize); - } else if (type.equals(Mp4BoxTypes.BOX_HANDLER)) { - - // ISO/IED 14496-12:2015 pg.7 - - reader.skip(4); // one byte version, three bytes flags - - // ISO/IED 14496-12:2015 pg.30 - - reader.skip(4); // Pre-defined - String handlerType = reader.getString(4); - reader.skip(12); // Reserved - String name = reader.getNullTerminatedString((int)boxSize - 32, Charset.defaultCharset(), false); - - final String HANDLER_SOUND_MEDIA = "soun"; - final String HANDLER_VIDEO_MEDIA = "vide"; - final String HANDLER_HINT_MEDIA = "hint"; - final String HANDLER_TEXT_MEDIA = "text"; - final String HANDLER_META_MEDIA = "meta"; - - if (handlerType.equals(HANDLER_SOUND_MEDIA)) { - return new Mp4SoundHandler(metadata, context); - } else if (handlerType.equals(HANDLER_VIDEO_MEDIA)) { - return new Mp4VideoHandler(metadata, context); - } else if (handlerType.equals(HANDLER_HINT_MEDIA)) { - return new Mp4HintHandler(metadata, context); - } else if (handlerType.equals(HANDLER_TEXT_MEDIA)) { - return new Mp4TextHandler(metadata, context); - } else if (handlerType.equals(HANDLER_META_MEDIA)) { - return new Mp4MetaHandler(metadata, context); - } - return this; - } else if (type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER)) { - processMediaHeader(reader, context); - } else if (type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)) { - processTrackHeader(reader); - } else if (type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) { - Mp4UuidBoxHandler userBoxHandler = new Mp4UuidBoxHandler(metadata); - userBoxHandler.processBox(type, payload, boxSize, context); - } else if (type.equals(Mp4BoxTypes.BOX_USER_DATA)) { - processUserData(reader, payload.length); - } - } else { - if (type.equals(Mp4ContainerTypes.BOX_COMPRESSED_MOVIE)) { - directory.addError("Compressed MP4 movies not supported"); - } - } - return this; - } - - private static final Pattern COORDINATE_PATTERN = Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)"); - - private void processUserData(@NotNull SequentialReader reader, int length) throws IOException - { - final int LOCATION_CODE = 0xA978797A; // "©xyz" - - String coordinateString = null; - - while (reader.getPosition() < length) { - long size = reader.getUInt32(); - if (size <= 4) - break; - int kind = reader.getInt32(); - if (kind == LOCATION_CODE) { - int xyzLength = reader.getUInt16(); - reader.skip(2); - coordinateString = reader.getString(xyzLength, "UTF-8"); - } else if (size >= 8) { - reader.skip(size - 8); - } else { - return; - } - } - - if (coordinateString != null) { - final Matcher matcher = COORDINATE_PATTERN.matcher(coordinateString); - if (matcher.find()) { - final double latitude = Double.parseDouble(matcher.group(1)); - final double longitude = Double.parseDouble(matcher.group(2)); - - directory.setDouble(TAG_LATITUDE, latitude); - directory.setDouble(TAG_LONGITUDE, longitude); - } - } - } - - private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException - { - // ISO/IED 14496-12:2015 pg.8 - - String majorBrand = reader.getString(4); - long minorVersion = reader.getUInt32(); - - // TODO avoid array list - ArrayList compatibleBrands = new ArrayList(); - for (int i = 16; i < boxSize; i += 4) { - compatibleBrands.add(reader.getString(4)); - } - - directory.setString(Mp4Directory.TAG_MAJOR_BRAND, majorBrand); - directory.setLong(Mp4Directory.TAG_MINOR_VERSION, minorVersion); - directory.setStringArray(Mp4Directory.TAG_COMPATIBLE_BRANDS, compatibleBrands.toArray(new String[compatibleBrands.size()])); - } - - private void processMovieHeader(@NotNull SequentialReader reader) throws IOException - { - // ISO/IED 14496-12:2015 pg.23 - - short version = reader.getUInt8(); - - reader.skip(3); // flags - - long creationTime; - long modificationTime; - long timescale; - long duration; - - if (version == 1) { - creationTime = reader.getInt64(); - modificationTime = reader.getInt64(); - timescale = reader.getUInt32(); - duration = reader.getInt64(); - } else { - creationTime = reader.getUInt32(); - modificationTime = reader.getUInt32(); - timescale = reader.getUInt32(); - duration = reader.getUInt32(); - } - - int rate = reader.getInt32(); - int volume = reader.getInt16(); - reader.skip(2); // Reserved - reader.skip(8); // Reserved - int[] matrix = new int[]{ - reader.getInt32(), - reader.getInt32(), - reader.getInt32(), - reader.getInt32(), - reader.getInt32(), - reader.getInt32(), - reader.getInt32(), - reader.getInt32(), - reader.getInt32() - }; - reader.skip(24); // Pre-defined - long nextTrackID = reader.getUInt32(); - - // Get creation/modification times - directory.setDate(Mp4Directory.TAG_CREATION_TIME, DateUtil.get1Jan1904EpochDate(creationTime)); - directory.setDate(Mp4Directory.TAG_MODIFICATION_TIME, DateUtil.get1Jan1904EpochDate(modificationTime)); - - // Get duration and time scale - directory.setLong(Mp4Directory.TAG_DURATION, duration); - directory.setLong(Mp4Directory.TAG_TIME_SCALE, timescale); - directory.setRational(Mp4Directory.TAG_DURATION_SECONDS, new Rational(duration, timescale)); - - directory.setIntArray(Mp4Directory.TAG_TRANSFORMATION_MATRIX, matrix); - - // Calculate preferred rate fixed point - double preferredRateInteger = (rate & 0xFFFF0000) >> 16; - double preferredRateFraction = (rate & 0x0000FFFF) / Math.pow(2, 4); - directory.setDouble(Mp4Directory.TAG_PREFERRED_RATE, preferredRateInteger + preferredRateFraction); - - // Calculate preferred volume fixed point - double preferredVolumeInteger = (volume & 0xFF00) >> 8; - double preferredVolumeFraction = (volume & 0x00FF) / Math.pow(2, 2); - directory.setDouble(Mp4Directory.TAG_PREFERRED_VOLUME, preferredVolumeInteger + preferredVolumeFraction); - - directory.setLong(Mp4Directory.TAG_NEXT_TRACK_ID, nextTrackID); - } - - private void processMediaHeader(@NotNull SequentialReader reader, Mp4Context context) throws IOException - { - // ISO/IED 14496-12:2015 pg.7 - - int version = reader.getUInt8(); - - reader.skip(3); // flags - - // ISO/IED 14496-12:2015 pg.29 - - if (version == 1) { - context.creationTime = reader.getInt64(); - context.modificationTime = reader.getInt64(); - context.timeScale = (long)reader.getInt32(); - context.duration = reader.getInt64(); - } else { - context.creationTime = reader.getUInt32(); - context.modificationTime = reader.getUInt32(); - context.timeScale = reader.getUInt32(); - context.duration = reader.getUInt32(); - } - - int languageBits = reader.getInt16(); - - context.language = new String(new char[] - { - (char)(((languageBits & 0x7C00) >> 10) + 0x60), - (char)(((languageBits & 0x03E0) >> 5) + 0x60), - (char)((languageBits & 0x001F) + 0x60) - }); - } - - private void processTrackHeader(@NotNull SequentialReader reader) throws IOException - { - // ISO/IED 14496-12:2015 pg.7 - - int version = reader.getUInt8(); - - reader.skip(3); // flags - - // ISO/IED 14496-12:2005 pg.17-18 - - long creationTime; - long modificationTime; - long trackID; - long duration; - - if (version == 1) { - creationTime = reader.getInt64(); - modificationTime = reader.getInt64(); - trackID = reader.getInt32(); - reader.skip(4); // reserved - duration = reader.getInt64(); - } else { - creationTime = reader.getUInt32(); - modificationTime = reader.getUInt32(); - trackID = reader.getUInt32(); - reader.skip(4); - duration = reader.getUInt32(); - } - - reader.skip(8); // reserved - - int layer = reader.getInt16(); - int alternateGroup = reader.getInt16(); - int volume = reader.getInt16(); - - reader.skip(2); // reserved - - int[] matrix = new int[9]; - for (int i = 0; i < 9; i++) { - matrix[i] = reader.getInt32(); - } - - long width = reader.getInt32(); - long height = reader.getInt32(); - - // TODO seems wrong to only set this once - if (width != 0 && height != 0 && directory.getDoubleObject(Mp4Directory.TAG_ROTATION) == null) { - int x = matrix[1] + matrix[4]; - int y = matrix[0] + matrix[3]; - double theta = Math.atan2(y, x); - double degree = Math.toDegrees(theta); - degree -= 45; - directory.setDouble(Mp4Directory.TAG_ROTATION, degree); - } - } +public class Mp4BoxHandler extends Mp4Handler { + public Mp4BoxHandler(Metadata metadata) { + super(metadata); + } + + @NotNull + @Override + protected Mp4Directory getDirectory() { + return new Mp4Directory(); + } + + @Override + public boolean shouldAcceptBox(@NotNull String type) { + return type.equals(Mp4BoxTypes.BOX_FILE_TYPE) || type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER) + || type.equals(Mp4BoxTypes.BOX_HANDLER) || type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER) + || type.equals(Mp4BoxTypes.BOX_TRACK_HEADER) || type.equals(Mp4BoxTypes.BOX_USER_DATA) + || type.equals(Mp4BoxTypes.BOX_USER_DEFINED); + } + + @Override + public boolean shouldAcceptContainer(@NotNull String type) { + return type.equals(Mp4ContainerTypes.BOX_TRACK) || type.equals(Mp4ContainerTypes.BOX_METADATA) + || type.equals(Mp4ContainerTypes.BOX_MOVIE) || type.equals(Mp4ContainerTypes.BOX_MEDIA); + } + + @Override + public Mp4Handler processBox(@NotNull String type, @Nullable byte[] payload, long boxSize, Mp4Context context) + throws IOException { + if (payload != null) { + SequentialReader reader = new SequentialByteArrayReader(payload); + if (type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER)) { + processMovieHeader(reader); + } else if (type.equals(Mp4BoxTypes.BOX_FILE_TYPE)) { + processFileType(reader, boxSize); + } else if (type.equals(Mp4BoxTypes.BOX_HANDLER)) { + + // ISO/IED 14496-12:2015 pg.7 + + reader.skip(4); // one byte version, three bytes flags + + // ISO/IED 14496-12:2015 pg.30 + + reader.skip(4); // Pre-defined + String handlerType = reader.getString(4); + reader.skip(12); // Reserved + String name = reader.getNullTerminatedString((int) boxSize - 32, Charset.defaultCharset(), false); + + final String HANDLER_SOUND_MEDIA = "soun"; + final String HANDLER_VIDEO_MEDIA = "vide"; + final String HANDLER_HINT_MEDIA = "hint"; + final String HANDLER_TEXT_MEDIA = "text"; + final String HANDLER_META_MEDIA = "meta"; + + if (handlerType.equals(HANDLER_SOUND_MEDIA)) { + return new Mp4SoundHandler(metadata, context); + } else if (handlerType.equals(HANDLER_VIDEO_MEDIA)) { + return new Mp4VideoHandler(metadata, context); + } else if (handlerType.equals(HANDLER_HINT_MEDIA)) { + return new Mp4HintHandler(metadata, context); + } else if (handlerType.equals(HANDLER_TEXT_MEDIA)) { + return new Mp4TextHandler(metadata, context); + } else if (handlerType.equals(HANDLER_META_MEDIA)) { + return new Mp4MetaHandler(metadata, context); + } + return this; + } else if (type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER)) { + processMediaHeader(reader, context); + } else if (type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)) { + processTrackHeader(reader); + } else if (type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) { + Mp4UuidBoxHandler userBoxHandler = new Mp4UuidBoxHandler(metadata); + userBoxHandler.processBox(type, payload, boxSize, context); + } else if (type.equals(Mp4BoxTypes.BOX_USER_DATA)) { + processUserData(reader, payload.length); + } + } else { + if (type.equals(Mp4ContainerTypes.BOX_COMPRESSED_MOVIE)) { + directory.addError("Compressed MP4 movies not supported"); + } + } + return this; + } + + private static final Pattern COORDINATE_PATTERN = Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)"); + + private void processUserData(@NotNull SequentialReader reader, int length) throws IOException { + final int LOCATION_CODE = 0xA978797A; // "©xyz" + final int META_TYPE = 0x6D657461; // "meta" + final int XTRA_TYPE = 0x58747261; // "Xtra" + + String coordinateString = null; + + while (reader.getPosition() < length) { + long size = reader.getUInt32(); + if (size <= 4) + break; + int kind = reader.getInt32(); + if (kind == LOCATION_CODE) { + int xyzLength = reader.getUInt16(); + reader.skip(2); + coordinateString = reader.getString(xyzLength, "UTF-8"); + } else if (kind == META_TYPE && size > 16) { + reader.skip(4); + processUserDataMeta(reader, length, size - 12); + } else if (kind == XTRA_TYPE && size > 16) { + processUserDataMetaXtra(reader, length, size - 8); + } else if (size >= 8) { + reader.skip(size - 8); + } else { + return; + } + } + + if (coordinateString != null) { + final Matcher matcher = COORDINATE_PATTERN.matcher(coordinateString); + if (matcher.find()) { + final double latitude = Double.parseDouble(matcher.group(1)); + final double longitude = Double.parseDouble(matcher.group(2)); + + directory.setDouble(TAG_LATITUDE, latitude); + directory.setDouble(TAG_LONGITUDE, longitude); + } + } + } + + private void processUserDataMeta(@NotNull SequentialReader reader, int length, long blockSize) throws IOException { + final int HDLR_TYPE = 0x68646C72; // "hdlr" + final int ILST_TYPE = 0x696C7374; // "ilst" + + long initialPosition = reader.getPosition(); + + while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { + long size = reader.getUInt32(); + if (size <= 4) + break; + int kind = reader.getInt32(); + if (kind == HDLR_TYPE) { + // nothing + reader.skip(size - 8); + } else if (kind == ILST_TYPE && size > 16) { + processUserDataMetaIList(reader, length, size - 8); + } + } + } + + private void processUserDataMetaIList(@NotNull SequentialReader reader, int length, long blockSize) + throws IOException { + final int CNAM_TYPE = 0xA96E616D; // "©nam" + final int CCMT_TYPE = 0xA9636D74; // "©cmt" + long initialPosition = reader.getPosition(); + + while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { + long size = reader.getUInt32(); + if (size <= 4) + break; + int kind = reader.getInt32(); + if (kind == CNAM_TYPE) { + long cnamSize = reader.getUInt32(); + if (cnamSize > 16) { + reader.skip(12); + directory.setString(TAG_TITLE, reader.getString((int) cnamSize - 16, "UTF-8")); + } + } else if (kind == CCMT_TYPE) { + long ccmtSize = reader.getUInt32(); + if (ccmtSize > 16) { + reader.skip(12); + directory.setString(TAG_COMMENT, reader.getString((int) ccmtSize - 16, "UTF-8")); + } + } else { + // nothing + } + } + } + + private void processUserDataMetaXtra(@NotNull SequentialReader reader, int length, long blockSize) + throws IOException { + long initialPosition = reader.getPosition(); + while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { + long key_size = reader.getUInt32(); + long key_name_size = reader.getUInt32(); + String key_name = reader.getString((int) key_name_size, "UTF-8"); + long entry_count = reader.getUInt32(); + if (key_name.equals("WM/SubTitle")) { + String value = getProcessUserDataMetaXtraValue(reader, entry_count); + directory.setString(TAG_SUBTITLE, value); + } else if (key_name.equals("WM/SharedUserRating")) { + long value_size = reader.getUInt32(); + int value_type = reader.getUInt16(); + directory.setLong(TAG_USER_RATING, reader.getInt64()); + } else if (key_name.equals("WM/Category")) { + String value = getProcessUserDataMetaXtraValue(reader, entry_count); + directory.setString(TAG_CATEGORY, value); + } else if (key_name.equals("WM/Mood")) { + String value = getProcessUserDataMetaXtraValue(reader, entry_count); + directory.setString(TAG_MOOD, value); + } + } + } + + private String getProcessUserDataMetaXtraValue(@NotNull SequentialReader reader, long entry_count) + throws IOException { + List result = new ArrayList<>(); + for (long i = 0; i < entry_count; ++i) { + long value_size = reader.getUInt32(); + int val_type = reader.getUInt16(); + result.add(reader.getString((int) value_size - 6, "UTF-8")); + } + return String.join(" | ", result); + } + + private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException { + // ISO/IED 14496-12:2015 pg.8 + + String majorBrand = reader.getString(4); + long minorVersion = reader.getUInt32(); + + // TODO avoid array list + ArrayList compatibleBrands = new ArrayList(); + for (int i = 16; i < boxSize; i += 4) { + compatibleBrands.add(reader.getString(4)); + } + + directory.setString(Mp4Directory.TAG_MAJOR_BRAND, majorBrand); + directory.setLong(Mp4Directory.TAG_MINOR_VERSION, minorVersion); + directory.setStringArray(Mp4Directory.TAG_COMPATIBLE_BRANDS, + compatibleBrands.toArray(new String[compatibleBrands.size()])); + } + + private void processMovieHeader(@NotNull SequentialReader reader) throws IOException { + // ISO/IED 14496-12:2015 pg.23 + + short version = reader.getUInt8(); + + reader.skip(3); // flags + + long creationTime; + long modificationTime; + long timescale; + long duration; + + if (version == 1) { + creationTime = reader.getInt64(); + modificationTime = reader.getInt64(); + timescale = reader.getUInt32(); + duration = reader.getInt64(); + } else { + creationTime = reader.getUInt32(); + modificationTime = reader.getUInt32(); + timescale = reader.getUInt32(); + duration = reader.getUInt32(); + } + + int rate = reader.getInt32(); + int volume = reader.getInt16(); + reader.skip(2); // Reserved + reader.skip(8); // Reserved + int[] matrix = new int[] { reader.getInt32(), reader.getInt32(), reader.getInt32(), reader.getInt32(), + reader.getInt32(), reader.getInt32(), reader.getInt32(), reader.getInt32(), reader.getInt32() }; + reader.skip(24); // Pre-defined + long nextTrackID = reader.getUInt32(); + + // Get creation/modification times + directory.setDate(Mp4Directory.TAG_CREATION_TIME, DateUtil.get1Jan1904EpochDate(creationTime)); + directory.setDate(Mp4Directory.TAG_MODIFICATION_TIME, DateUtil.get1Jan1904EpochDate(modificationTime)); + + // Get duration and time scale + directory.setLong(Mp4Directory.TAG_DURATION, duration); + directory.setLong(Mp4Directory.TAG_TIME_SCALE, timescale); + directory.setRational(Mp4Directory.TAG_DURATION_SECONDS, new Rational(duration, timescale)); + + directory.setIntArray(Mp4Directory.TAG_TRANSFORMATION_MATRIX, matrix); + + // Calculate preferred rate fixed point + double preferredRateInteger = (rate & 0xFFFF0000) >> 16; + double preferredRateFraction = (rate & 0x0000FFFF) / Math.pow(2, 4); + directory.setDouble(Mp4Directory.TAG_PREFERRED_RATE, preferredRateInteger + preferredRateFraction); + + // Calculate preferred volume fixed point + double preferredVolumeInteger = (volume & 0xFF00) >> 8; + double preferredVolumeFraction = (volume & 0x00FF) / Math.pow(2, 2); + directory.setDouble(Mp4Directory.TAG_PREFERRED_VOLUME, preferredVolumeInteger + preferredVolumeFraction); + + directory.setLong(Mp4Directory.TAG_NEXT_TRACK_ID, nextTrackID); + } + + private void processMediaHeader(@NotNull SequentialReader reader, Mp4Context context) throws IOException { + // ISO/IED 14496-12:2015 pg.7 + + int version = reader.getUInt8(); + + reader.skip(3); // flags + + // ISO/IED 14496-12:2015 pg.29 + + if (version == 1) { + context.creationTime = reader.getInt64(); + context.modificationTime = reader.getInt64(); + context.timeScale = (long) reader.getInt32(); + context.duration = reader.getInt64(); + } else { + context.creationTime = reader.getUInt32(); + context.modificationTime = reader.getUInt32(); + context.timeScale = reader.getUInt32(); + context.duration = reader.getUInt32(); + } + + int languageBits = reader.getInt16(); + + context.language = new String(new char[] { (char) (((languageBits & 0x7C00) >> 10) + 0x60), + (char) (((languageBits & 0x03E0) >> 5) + 0x60), (char) ((languageBits & 0x001F) + 0x60) }); + } + + private void processTrackHeader(@NotNull SequentialReader reader) throws IOException { + // ISO/IED 14496-12:2015 pg.7 + + int version = reader.getUInt8(); + + reader.skip(3); // flags + + // ISO/IED 14496-12:2005 pg.17-18 + + long creationTime; + long modificationTime; + long trackID; + long duration; + + if (version == 1) { + creationTime = reader.getInt64(); + modificationTime = reader.getInt64(); + trackID = reader.getInt32(); + reader.skip(4); // reserved + duration = reader.getInt64(); + } else { + creationTime = reader.getUInt32(); + modificationTime = reader.getUInt32(); + trackID = reader.getUInt32(); + reader.skip(4); + duration = reader.getUInt32(); + } + + reader.skip(8); // reserved + + int layer = reader.getInt16(); + int alternateGroup = reader.getInt16(); + int volume = reader.getInt16(); + + reader.skip(2); // reserved + + int[] matrix = new int[9]; + for (int i = 0; i < 9; i++) { + matrix[i] = reader.getInt32(); + } + + long width = reader.getInt32(); + long height = reader.getInt32(); + + // TODO seems wrong to only set this once + if (width != 0 && height != 0 && directory.getDoubleObject(Mp4Directory.TAG_ROTATION) == null) { + int x = matrix[1] + matrix[4]; + int y = matrix[0] + matrix[3]; + double theta = Math.atan2(y, x); + double degree = Math.toDegrees(theta); + degree -= 45; + directory.setDouble(Mp4Directory.TAG_ROTATION, degree); + } + } } diff --git a/Source/com/drew/metadata/mp4/Mp4Directory.java b/Source/com/drew/metadata/mp4/Mp4Directory.java index 1355f370f..5f3d2f5f4 100644 --- a/Source/com/drew/metadata/mp4/Mp4Directory.java +++ b/Source/com/drew/metadata/mp4/Mp4Directory.java @@ -20,84 +20,92 @@ */ package com.drew.metadata.mp4; +import java.util.HashMap; + import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; -import java.util.HashMap; - public class Mp4Directory extends Directory { - public static final int TAG_CREATION_TIME = 0x0100; - public static final int TAG_MODIFICATION_TIME = 0x0101; - public static final int TAG_TIME_SCALE = 0x0102; - public static final int TAG_DURATION = 0x0103; - public static final int TAG_DURATION_SECONDS = 0x0104; - public static final int TAG_PREFERRED_RATE = 0x0105; - public static final int TAG_PREFERRED_VOLUME = 0x0106; - public static final int TAG_PREVIEW_TIME = 0x0108; - public static final int TAG_PREVIEW_DURATION = 0x0109; - public static final int TAG_POSTER_TIME = 0x010A; - public static final int TAG_SELECTION_TIME = 0x010B; - public static final int TAG_SELECTION_DURATION = 0x010C; - public static final int TAG_CURRENT_TIME = 0x010D; - public static final int TAG_NEXT_TRACK_ID = 0x010E; - public static final int TAG_TRANSFORMATION_MATRIX = 0x010F; - public static final int TAG_ROTATION = 0x0200; - public static final int TAG_LATITUDE = 0x2001; - public static final int TAG_LONGITUDE = 0x2002; - public static final int TAG_MEDIA_TIME_SCALE = 0x0306; - - public static final int TAG_MAJOR_BRAND = 1; - public static final int TAG_MINOR_VERSION = 2; - public static final int TAG_COMPATIBLE_BRANDS = 3; + public static final int TAG_CREATION_TIME = 0x0100; + public static final int TAG_MODIFICATION_TIME = 0x0101; + public static final int TAG_TIME_SCALE = 0x0102; + public static final int TAG_DURATION = 0x0103; + public static final int TAG_DURATION_SECONDS = 0x0104; + public static final int TAG_PREFERRED_RATE = 0x0105; + public static final int TAG_PREFERRED_VOLUME = 0x0106; + public static final int TAG_PREVIEW_TIME = 0x0108; + public static final int TAG_PREVIEW_DURATION = 0x0109; + public static final int TAG_POSTER_TIME = 0x010A; + public static final int TAG_SELECTION_TIME = 0x010B; + public static final int TAG_SELECTION_DURATION = 0x010C; + public static final int TAG_CURRENT_TIME = 0x010D; + public static final int TAG_NEXT_TRACK_ID = 0x010E; + public static final int TAG_TRANSFORMATION_MATRIX = 0x010F; + public static final int TAG_ROTATION = 0x0200; + public static final int TAG_LATITUDE = 0x2001; + public static final int TAG_LONGITUDE = 0x2002; + public static final int TAG_MEDIA_TIME_SCALE = 0x0306; + public static final int TAG_TITLE = 0x3000; + public static final int TAG_COMMENT = 0x3001; + public static final int TAG_SUBTITLE = 0x3002; + public static final int TAG_USER_RATING = 0x3003; + public static final int TAG_CATEGORY = 0x3004; + public static final int TAG_MOOD = 0x3005; - @NotNull - private static final HashMap _tagNameMap = new HashMap(); + public static final int TAG_MAJOR_BRAND = 1; + public static final int TAG_MINOR_VERSION = 2; + public static final int TAG_COMPATIBLE_BRANDS = 3; + @NotNull + private static final HashMap _tagNameMap = new HashMap(); - static { - _tagNameMap.put(TAG_MAJOR_BRAND, "Major Brand"); - _tagNameMap.put(TAG_MINOR_VERSION, "Minor Version"); - _tagNameMap.put(TAG_COMPATIBLE_BRANDS, "Compatible Brands"); + static { + _tagNameMap.put(TAG_MAJOR_BRAND, "Major Brand"); + _tagNameMap.put(TAG_MINOR_VERSION, "Minor Version"); + _tagNameMap.put(TAG_COMPATIBLE_BRANDS, "Compatible Brands"); - _tagNameMap.put(TAG_CREATION_TIME, "Creation Time"); - _tagNameMap.put(TAG_MODIFICATION_TIME, "Modification Time"); - _tagNameMap.put(TAG_TIME_SCALE, "Media Time Scale"); - _tagNameMap.put(TAG_DURATION, "Duration"); - _tagNameMap.put(TAG_DURATION_SECONDS, "Duration in Seconds"); - _tagNameMap.put(TAG_PREFERRED_RATE, "Preferred Rate"); - _tagNameMap.put(TAG_PREFERRED_VOLUME, "Preferred Volume"); - _tagNameMap.put(TAG_PREVIEW_TIME, "Preview Time"); - _tagNameMap.put(TAG_PREVIEW_DURATION, "Preview Duration"); - _tagNameMap.put(TAG_POSTER_TIME, "Poster Time"); - _tagNameMap.put(TAG_SELECTION_TIME, "Selection Time"); - _tagNameMap.put(TAG_SELECTION_DURATION, "Selection Duration"); - _tagNameMap.put(TAG_CURRENT_TIME, "Current Time"); - _tagNameMap.put(TAG_NEXT_TRACK_ID, "Next Track ID"); - _tagNameMap.put(TAG_TRANSFORMATION_MATRIX, "Transformation Matrix"); - _tagNameMap.put(TAG_ROTATION, "Rotation"); - _tagNameMap.put(TAG_LATITUDE, "Latitude"); - _tagNameMap.put(TAG_LONGITUDE, "Longitude"); + _tagNameMap.put(TAG_CREATION_TIME, "Creation Time"); + _tagNameMap.put(TAG_MODIFICATION_TIME, "Modification Time"); + _tagNameMap.put(TAG_TIME_SCALE, "Media Time Scale"); + _tagNameMap.put(TAG_DURATION, "Duration"); + _tagNameMap.put(TAG_DURATION_SECONDS, "Duration in Seconds"); + _tagNameMap.put(TAG_PREFERRED_RATE, "Preferred Rate"); + _tagNameMap.put(TAG_PREFERRED_VOLUME, "Preferred Volume"); + _tagNameMap.put(TAG_PREVIEW_TIME, "Preview Time"); + _tagNameMap.put(TAG_PREVIEW_DURATION, "Preview Duration"); + _tagNameMap.put(TAG_POSTER_TIME, "Poster Time"); + _tagNameMap.put(TAG_SELECTION_TIME, "Selection Time"); + _tagNameMap.put(TAG_SELECTION_DURATION, "Selection Duration"); + _tagNameMap.put(TAG_CURRENT_TIME, "Current Time"); + _tagNameMap.put(TAG_NEXT_TRACK_ID, "Next Track ID"); + _tagNameMap.put(TAG_TRANSFORMATION_MATRIX, "Transformation Matrix"); + _tagNameMap.put(TAG_ROTATION, "Rotation"); + _tagNameMap.put(TAG_LATITUDE, "Latitude"); + _tagNameMap.put(TAG_LONGITUDE, "Longitude"); + _tagNameMap.put(TAG_TITLE, "Title"); + _tagNameMap.put(TAG_COMMENT, "Comment"); + _tagNameMap.put(TAG_SUBTITLE, "Subtitle"); + _tagNameMap.put(TAG_USER_RATING, "User rating"); + _tagNameMap.put(TAG_CATEGORY, "Tags"); + _tagNameMap.put(TAG_MOOD, "Mood"); - _tagNameMap.put(TAG_MEDIA_TIME_SCALE, "Media Time Scale"); - } + _tagNameMap.put(TAG_MEDIA_TIME_SCALE, "Media Time Scale"); + } - public Mp4Directory() - { - this.setDescriptor(new Mp4Descriptor(this)); - } + public Mp4Directory() { + this.setDescriptor(new Mp4Descriptor(this)); + } - @Override - @NotNull - public String getName() - { - return "MP4"; - } + @Override + @NotNull + public String getName() { + return "MP4"; + } - @Override - @NotNull - protected HashMap getTagNameMap() - { - return _tagNameMap; - } + @Override + @NotNull + protected HashMap getTagNameMap() { + return _tagNameMap; + } } From a0723d99a7d35aa48cff80715548f88cb499e48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Opfermann?= Date: Sun, 25 Feb 2024 17:31:09 +0100 Subject: [PATCH 2/6] Revert "Add more UDTA information. Mostly related to the basic information that can be fount in the Windows Details panel, title, subtitle, etc." This reverts commit fb170bd78de2d11a89122c187dac7445c0b60e81. --- .../com/drew/metadata/mp4/Mp4BoxHandler.java | 699 ++++++++---------- .../com/drew/metadata/mp4/Mp4Directory.java | 142 ++-- 2 files changed, 378 insertions(+), 463 deletions(-) diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 0cb6cfbe6..17e56e642 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -20,22 +20,6 @@ */ package com.drew.metadata.mp4; -import static com.drew.metadata.mp4.Mp4Directory.TAG_CATEGORY; -import static com.drew.metadata.mp4.Mp4Directory.TAG_COMMENT; -import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE; -import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE; -import static com.drew.metadata.mp4.Mp4Directory.TAG_MOOD; -import static com.drew.metadata.mp4.Mp4Directory.TAG_SUBTITLE; -import static com.drew.metadata.mp4.Mp4Directory.TAG_TITLE; -import static com.drew.metadata.mp4.Mp4Directory.TAG_USER_RATING; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import com.drew.imaging.mp4.Mp4Handler; import com.drew.lang.DateUtil; import com.drew.lang.Rational; @@ -44,380 +28,319 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; -import com.drew.metadata.mp4.media.Mp4HintHandler; -import com.drew.metadata.mp4.media.Mp4MetaHandler; -import com.drew.metadata.mp4.media.Mp4SoundHandler; -import com.drew.metadata.mp4.media.Mp4TextHandler; -import com.drew.metadata.mp4.media.Mp4UuidBoxHandler; -import com.drew.metadata.mp4.media.Mp4VideoHandler; +import com.drew.metadata.mp4.media.*; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE; /** * @author Payton Garland */ -public class Mp4BoxHandler extends Mp4Handler { - public Mp4BoxHandler(Metadata metadata) { - super(metadata); - } - - @NotNull - @Override - protected Mp4Directory getDirectory() { - return new Mp4Directory(); - } - - @Override - public boolean shouldAcceptBox(@NotNull String type) { - return type.equals(Mp4BoxTypes.BOX_FILE_TYPE) || type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER) - || type.equals(Mp4BoxTypes.BOX_HANDLER) || type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER) - || type.equals(Mp4BoxTypes.BOX_TRACK_HEADER) || type.equals(Mp4BoxTypes.BOX_USER_DATA) - || type.equals(Mp4BoxTypes.BOX_USER_DEFINED); - } - - @Override - public boolean shouldAcceptContainer(@NotNull String type) { - return type.equals(Mp4ContainerTypes.BOX_TRACK) || type.equals(Mp4ContainerTypes.BOX_METADATA) - || type.equals(Mp4ContainerTypes.BOX_MOVIE) || type.equals(Mp4ContainerTypes.BOX_MEDIA); - } - - @Override - public Mp4Handler processBox(@NotNull String type, @Nullable byte[] payload, long boxSize, Mp4Context context) - throws IOException { - if (payload != null) { - SequentialReader reader = new SequentialByteArrayReader(payload); - if (type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER)) { - processMovieHeader(reader); - } else if (type.equals(Mp4BoxTypes.BOX_FILE_TYPE)) { - processFileType(reader, boxSize); - } else if (type.equals(Mp4BoxTypes.BOX_HANDLER)) { - - // ISO/IED 14496-12:2015 pg.7 - - reader.skip(4); // one byte version, three bytes flags - - // ISO/IED 14496-12:2015 pg.30 - - reader.skip(4); // Pre-defined - String handlerType = reader.getString(4); - reader.skip(12); // Reserved - String name = reader.getNullTerminatedString((int) boxSize - 32, Charset.defaultCharset(), false); - - final String HANDLER_SOUND_MEDIA = "soun"; - final String HANDLER_VIDEO_MEDIA = "vide"; - final String HANDLER_HINT_MEDIA = "hint"; - final String HANDLER_TEXT_MEDIA = "text"; - final String HANDLER_META_MEDIA = "meta"; - - if (handlerType.equals(HANDLER_SOUND_MEDIA)) { - return new Mp4SoundHandler(metadata, context); - } else if (handlerType.equals(HANDLER_VIDEO_MEDIA)) { - return new Mp4VideoHandler(metadata, context); - } else if (handlerType.equals(HANDLER_HINT_MEDIA)) { - return new Mp4HintHandler(metadata, context); - } else if (handlerType.equals(HANDLER_TEXT_MEDIA)) { - return new Mp4TextHandler(metadata, context); - } else if (handlerType.equals(HANDLER_META_MEDIA)) { - return new Mp4MetaHandler(metadata, context); - } - return this; - } else if (type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER)) { - processMediaHeader(reader, context); - } else if (type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)) { - processTrackHeader(reader); - } else if (type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) { - Mp4UuidBoxHandler userBoxHandler = new Mp4UuidBoxHandler(metadata); - userBoxHandler.processBox(type, payload, boxSize, context); - } else if (type.equals(Mp4BoxTypes.BOX_USER_DATA)) { - processUserData(reader, payload.length); - } - } else { - if (type.equals(Mp4ContainerTypes.BOX_COMPRESSED_MOVIE)) { - directory.addError("Compressed MP4 movies not supported"); - } - } - return this; - } - - private static final Pattern COORDINATE_PATTERN = Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)"); - - private void processUserData(@NotNull SequentialReader reader, int length) throws IOException { - final int LOCATION_CODE = 0xA978797A; // "©xyz" - final int META_TYPE = 0x6D657461; // "meta" - final int XTRA_TYPE = 0x58747261; // "Xtra" - - String coordinateString = null; - - while (reader.getPosition() < length) { - long size = reader.getUInt32(); - if (size <= 4) - break; - int kind = reader.getInt32(); - if (kind == LOCATION_CODE) { - int xyzLength = reader.getUInt16(); - reader.skip(2); - coordinateString = reader.getString(xyzLength, "UTF-8"); - } else if (kind == META_TYPE && size > 16) { - reader.skip(4); - processUserDataMeta(reader, length, size - 12); - } else if (kind == XTRA_TYPE && size > 16) { - processUserDataMetaXtra(reader, length, size - 8); - } else if (size >= 8) { - reader.skip(size - 8); - } else { - return; - } - } - - if (coordinateString != null) { - final Matcher matcher = COORDINATE_PATTERN.matcher(coordinateString); - if (matcher.find()) { - final double latitude = Double.parseDouble(matcher.group(1)); - final double longitude = Double.parseDouble(matcher.group(2)); - - directory.setDouble(TAG_LATITUDE, latitude); - directory.setDouble(TAG_LONGITUDE, longitude); - } - } - } - - private void processUserDataMeta(@NotNull SequentialReader reader, int length, long blockSize) throws IOException { - final int HDLR_TYPE = 0x68646C72; // "hdlr" - final int ILST_TYPE = 0x696C7374; // "ilst" - - long initialPosition = reader.getPosition(); - - while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { - long size = reader.getUInt32(); - if (size <= 4) - break; - int kind = reader.getInt32(); - if (kind == HDLR_TYPE) { - // nothing - reader.skip(size - 8); - } else if (kind == ILST_TYPE && size > 16) { - processUserDataMetaIList(reader, length, size - 8); - } - } - } - - private void processUserDataMetaIList(@NotNull SequentialReader reader, int length, long blockSize) - throws IOException { - final int CNAM_TYPE = 0xA96E616D; // "©nam" - final int CCMT_TYPE = 0xA9636D74; // "©cmt" - long initialPosition = reader.getPosition(); - - while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { - long size = reader.getUInt32(); - if (size <= 4) - break; - int kind = reader.getInt32(); - if (kind == CNAM_TYPE) { - long cnamSize = reader.getUInt32(); - if (cnamSize > 16) { - reader.skip(12); - directory.setString(TAG_TITLE, reader.getString((int) cnamSize - 16, "UTF-8")); - } - } else if (kind == CCMT_TYPE) { - long ccmtSize = reader.getUInt32(); - if (ccmtSize > 16) { - reader.skip(12); - directory.setString(TAG_COMMENT, reader.getString((int) ccmtSize - 16, "UTF-8")); - } - } else { - // nothing - } - } - } - - private void processUserDataMetaXtra(@NotNull SequentialReader reader, int length, long blockSize) - throws IOException { - long initialPosition = reader.getPosition(); - while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { - long key_size = reader.getUInt32(); - long key_name_size = reader.getUInt32(); - String key_name = reader.getString((int) key_name_size, "UTF-8"); - long entry_count = reader.getUInt32(); - if (key_name.equals("WM/SubTitle")) { - String value = getProcessUserDataMetaXtraValue(reader, entry_count); - directory.setString(TAG_SUBTITLE, value); - } else if (key_name.equals("WM/SharedUserRating")) { - long value_size = reader.getUInt32(); - int value_type = reader.getUInt16(); - directory.setLong(TAG_USER_RATING, reader.getInt64()); - } else if (key_name.equals("WM/Category")) { - String value = getProcessUserDataMetaXtraValue(reader, entry_count); - directory.setString(TAG_CATEGORY, value); - } else if (key_name.equals("WM/Mood")) { - String value = getProcessUserDataMetaXtraValue(reader, entry_count); - directory.setString(TAG_MOOD, value); - } - } - } - - private String getProcessUserDataMetaXtraValue(@NotNull SequentialReader reader, long entry_count) - throws IOException { - List result = new ArrayList<>(); - for (long i = 0; i < entry_count; ++i) { - long value_size = reader.getUInt32(); - int val_type = reader.getUInt16(); - result.add(reader.getString((int) value_size - 6, "UTF-8")); - } - return String.join(" | ", result); - } - - private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException { - // ISO/IED 14496-12:2015 pg.8 - - String majorBrand = reader.getString(4); - long minorVersion = reader.getUInt32(); - - // TODO avoid array list - ArrayList compatibleBrands = new ArrayList(); - for (int i = 16; i < boxSize; i += 4) { - compatibleBrands.add(reader.getString(4)); - } - - directory.setString(Mp4Directory.TAG_MAJOR_BRAND, majorBrand); - directory.setLong(Mp4Directory.TAG_MINOR_VERSION, minorVersion); - directory.setStringArray(Mp4Directory.TAG_COMPATIBLE_BRANDS, - compatibleBrands.toArray(new String[compatibleBrands.size()])); - } - - private void processMovieHeader(@NotNull SequentialReader reader) throws IOException { - // ISO/IED 14496-12:2015 pg.23 - - short version = reader.getUInt8(); - - reader.skip(3); // flags - - long creationTime; - long modificationTime; - long timescale; - long duration; - - if (version == 1) { - creationTime = reader.getInt64(); - modificationTime = reader.getInt64(); - timescale = reader.getUInt32(); - duration = reader.getInt64(); - } else { - creationTime = reader.getUInt32(); - modificationTime = reader.getUInt32(); - timescale = reader.getUInt32(); - duration = reader.getUInt32(); - } - - int rate = reader.getInt32(); - int volume = reader.getInt16(); - reader.skip(2); // Reserved - reader.skip(8); // Reserved - int[] matrix = new int[] { reader.getInt32(), reader.getInt32(), reader.getInt32(), reader.getInt32(), - reader.getInt32(), reader.getInt32(), reader.getInt32(), reader.getInt32(), reader.getInt32() }; - reader.skip(24); // Pre-defined - long nextTrackID = reader.getUInt32(); - - // Get creation/modification times - directory.setDate(Mp4Directory.TAG_CREATION_TIME, DateUtil.get1Jan1904EpochDate(creationTime)); - directory.setDate(Mp4Directory.TAG_MODIFICATION_TIME, DateUtil.get1Jan1904EpochDate(modificationTime)); - - // Get duration and time scale - directory.setLong(Mp4Directory.TAG_DURATION, duration); - directory.setLong(Mp4Directory.TAG_TIME_SCALE, timescale); - directory.setRational(Mp4Directory.TAG_DURATION_SECONDS, new Rational(duration, timescale)); - - directory.setIntArray(Mp4Directory.TAG_TRANSFORMATION_MATRIX, matrix); - - // Calculate preferred rate fixed point - double preferredRateInteger = (rate & 0xFFFF0000) >> 16; - double preferredRateFraction = (rate & 0x0000FFFF) / Math.pow(2, 4); - directory.setDouble(Mp4Directory.TAG_PREFERRED_RATE, preferredRateInteger + preferredRateFraction); - - // Calculate preferred volume fixed point - double preferredVolumeInteger = (volume & 0xFF00) >> 8; - double preferredVolumeFraction = (volume & 0x00FF) / Math.pow(2, 2); - directory.setDouble(Mp4Directory.TAG_PREFERRED_VOLUME, preferredVolumeInteger + preferredVolumeFraction); - - directory.setLong(Mp4Directory.TAG_NEXT_TRACK_ID, nextTrackID); - } - - private void processMediaHeader(@NotNull SequentialReader reader, Mp4Context context) throws IOException { - // ISO/IED 14496-12:2015 pg.7 - - int version = reader.getUInt8(); - - reader.skip(3); // flags - - // ISO/IED 14496-12:2015 pg.29 - - if (version == 1) { - context.creationTime = reader.getInt64(); - context.modificationTime = reader.getInt64(); - context.timeScale = (long) reader.getInt32(); - context.duration = reader.getInt64(); - } else { - context.creationTime = reader.getUInt32(); - context.modificationTime = reader.getUInt32(); - context.timeScale = reader.getUInt32(); - context.duration = reader.getUInt32(); - } - - int languageBits = reader.getInt16(); - - context.language = new String(new char[] { (char) (((languageBits & 0x7C00) >> 10) + 0x60), - (char) (((languageBits & 0x03E0) >> 5) + 0x60), (char) ((languageBits & 0x001F) + 0x60) }); - } - - private void processTrackHeader(@NotNull SequentialReader reader) throws IOException { - // ISO/IED 14496-12:2015 pg.7 - - int version = reader.getUInt8(); - - reader.skip(3); // flags - - // ISO/IED 14496-12:2005 pg.17-18 - - long creationTime; - long modificationTime; - long trackID; - long duration; - - if (version == 1) { - creationTime = reader.getInt64(); - modificationTime = reader.getInt64(); - trackID = reader.getInt32(); - reader.skip(4); // reserved - duration = reader.getInt64(); - } else { - creationTime = reader.getUInt32(); - modificationTime = reader.getUInt32(); - trackID = reader.getUInt32(); - reader.skip(4); - duration = reader.getUInt32(); - } - - reader.skip(8); // reserved - - int layer = reader.getInt16(); - int alternateGroup = reader.getInt16(); - int volume = reader.getInt16(); - - reader.skip(2); // reserved - - int[] matrix = new int[9]; - for (int i = 0; i < 9; i++) { - matrix[i] = reader.getInt32(); - } - - long width = reader.getInt32(); - long height = reader.getInt32(); - - // TODO seems wrong to only set this once - if (width != 0 && height != 0 && directory.getDoubleObject(Mp4Directory.TAG_ROTATION) == null) { - int x = matrix[1] + matrix[4]; - int y = matrix[0] + matrix[3]; - double theta = Math.atan2(y, x); - double degree = Math.toDegrees(theta); - degree -= 45; - directory.setDouble(Mp4Directory.TAG_ROTATION, degree); - } - } +public class Mp4BoxHandler extends Mp4Handler +{ + public Mp4BoxHandler(Metadata metadata) + { + super(metadata); + } + + @NotNull + @Override + protected Mp4Directory getDirectory() + { + return new Mp4Directory(); + } + + @Override + public boolean shouldAcceptBox(@NotNull String type) + { + return type.equals(Mp4BoxTypes.BOX_FILE_TYPE) + || type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER) + || type.equals(Mp4BoxTypes.BOX_HANDLER) + || type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER) + || type.equals(Mp4BoxTypes.BOX_TRACK_HEADER) + || type.equals(Mp4BoxTypes.BOX_USER_DATA) + || type.equals(Mp4BoxTypes.BOX_USER_DEFINED); + } + + @Override + public boolean shouldAcceptContainer(@NotNull String type) + { + return type.equals(Mp4ContainerTypes.BOX_TRACK) + || type.equals(Mp4ContainerTypes.BOX_METADATA) + || type.equals(Mp4ContainerTypes.BOX_MOVIE) + || type.equals(Mp4ContainerTypes.BOX_MEDIA); + } + + @Override + public Mp4Handler processBox(@NotNull String type, @Nullable byte[] payload, long boxSize, Mp4Context context) throws IOException + { + if (payload != null) { + SequentialReader reader = new SequentialByteArrayReader(payload); + if (type.equals(Mp4BoxTypes.BOX_MOVIE_HEADER)) { + processMovieHeader(reader); + } else if (type.equals(Mp4BoxTypes.BOX_FILE_TYPE)) { + processFileType(reader, boxSize); + } else if (type.equals(Mp4BoxTypes.BOX_HANDLER)) { + + // ISO/IED 14496-12:2015 pg.7 + + reader.skip(4); // one byte version, three bytes flags + + // ISO/IED 14496-12:2015 pg.30 + + reader.skip(4); // Pre-defined + String handlerType = reader.getString(4); + reader.skip(12); // Reserved + String name = reader.getNullTerminatedString((int)boxSize - 32, Charset.defaultCharset(), false); + + final String HANDLER_SOUND_MEDIA = "soun"; + final String HANDLER_VIDEO_MEDIA = "vide"; + final String HANDLER_HINT_MEDIA = "hint"; + final String HANDLER_TEXT_MEDIA = "text"; + final String HANDLER_META_MEDIA = "meta"; + + if (handlerType.equals(HANDLER_SOUND_MEDIA)) { + return new Mp4SoundHandler(metadata, context); + } else if (handlerType.equals(HANDLER_VIDEO_MEDIA)) { + return new Mp4VideoHandler(metadata, context); + } else if (handlerType.equals(HANDLER_HINT_MEDIA)) { + return new Mp4HintHandler(metadata, context); + } else if (handlerType.equals(HANDLER_TEXT_MEDIA)) { + return new Mp4TextHandler(metadata, context); + } else if (handlerType.equals(HANDLER_META_MEDIA)) { + return new Mp4MetaHandler(metadata, context); + } + return this; + } else if (type.equals(Mp4BoxTypes.BOX_MEDIA_HEADER)) { + processMediaHeader(reader, context); + } else if (type.equals(Mp4BoxTypes.BOX_TRACK_HEADER)) { + processTrackHeader(reader); + } else if (type.equals(Mp4BoxTypes.BOX_USER_DEFINED)) { + Mp4UuidBoxHandler userBoxHandler = new Mp4UuidBoxHandler(metadata); + userBoxHandler.processBox(type, payload, boxSize, context); + } else if (type.equals(Mp4BoxTypes.BOX_USER_DATA)) { + processUserData(reader, payload.length); + } + } else { + if (type.equals(Mp4ContainerTypes.BOX_COMPRESSED_MOVIE)) { + directory.addError("Compressed MP4 movies not supported"); + } + } + return this; + } + + private static final Pattern COORDINATE_PATTERN = Pattern.compile("([+-]\\d+\\.\\d+)([+-]\\d+\\.\\d+)"); + + private void processUserData(@NotNull SequentialReader reader, int length) throws IOException + { + final int LOCATION_CODE = 0xA978797A; // "©xyz" + + String coordinateString = null; + + while (reader.getPosition() < length) { + long size = reader.getUInt32(); + if (size <= 4) + break; + int kind = reader.getInt32(); + if (kind == LOCATION_CODE) { + int xyzLength = reader.getUInt16(); + reader.skip(2); + coordinateString = reader.getString(xyzLength, "UTF-8"); + } else if (size >= 8) { + reader.skip(size - 8); + } else { + return; + } + } + + if (coordinateString != null) { + final Matcher matcher = COORDINATE_PATTERN.matcher(coordinateString); + if (matcher.find()) { + final double latitude = Double.parseDouble(matcher.group(1)); + final double longitude = Double.parseDouble(matcher.group(2)); + + directory.setDouble(TAG_LATITUDE, latitude); + directory.setDouble(TAG_LONGITUDE, longitude); + } + } + } + + private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException + { + // ISO/IED 14496-12:2015 pg.8 + + String majorBrand = reader.getString(4); + long minorVersion = reader.getUInt32(); + + // TODO avoid array list + ArrayList compatibleBrands = new ArrayList(); + for (int i = 16; i < boxSize; i += 4) { + compatibleBrands.add(reader.getString(4)); + } + + directory.setString(Mp4Directory.TAG_MAJOR_BRAND, majorBrand); + directory.setLong(Mp4Directory.TAG_MINOR_VERSION, minorVersion); + directory.setStringArray(Mp4Directory.TAG_COMPATIBLE_BRANDS, compatibleBrands.toArray(new String[compatibleBrands.size()])); + } + + private void processMovieHeader(@NotNull SequentialReader reader) throws IOException + { + // ISO/IED 14496-12:2015 pg.23 + + short version = reader.getUInt8(); + + reader.skip(3); // flags + + long creationTime; + long modificationTime; + long timescale; + long duration; + + if (version == 1) { + creationTime = reader.getInt64(); + modificationTime = reader.getInt64(); + timescale = reader.getUInt32(); + duration = reader.getInt64(); + } else { + creationTime = reader.getUInt32(); + modificationTime = reader.getUInt32(); + timescale = reader.getUInt32(); + duration = reader.getUInt32(); + } + + int rate = reader.getInt32(); + int volume = reader.getInt16(); + reader.skip(2); // Reserved + reader.skip(8); // Reserved + int[] matrix = new int[]{ + reader.getInt32(), + reader.getInt32(), + reader.getInt32(), + reader.getInt32(), + reader.getInt32(), + reader.getInt32(), + reader.getInt32(), + reader.getInt32(), + reader.getInt32() + }; + reader.skip(24); // Pre-defined + long nextTrackID = reader.getUInt32(); + + // Get creation/modification times + directory.setDate(Mp4Directory.TAG_CREATION_TIME, DateUtil.get1Jan1904EpochDate(creationTime)); + directory.setDate(Mp4Directory.TAG_MODIFICATION_TIME, DateUtil.get1Jan1904EpochDate(modificationTime)); + + // Get duration and time scale + directory.setLong(Mp4Directory.TAG_DURATION, duration); + directory.setLong(Mp4Directory.TAG_TIME_SCALE, timescale); + directory.setRational(Mp4Directory.TAG_DURATION_SECONDS, new Rational(duration, timescale)); + + directory.setIntArray(Mp4Directory.TAG_TRANSFORMATION_MATRIX, matrix); + + // Calculate preferred rate fixed point + double preferredRateInteger = (rate & 0xFFFF0000) >> 16; + double preferredRateFraction = (rate & 0x0000FFFF) / Math.pow(2, 4); + directory.setDouble(Mp4Directory.TAG_PREFERRED_RATE, preferredRateInteger + preferredRateFraction); + + // Calculate preferred volume fixed point + double preferredVolumeInteger = (volume & 0xFF00) >> 8; + double preferredVolumeFraction = (volume & 0x00FF) / Math.pow(2, 2); + directory.setDouble(Mp4Directory.TAG_PREFERRED_VOLUME, preferredVolumeInteger + preferredVolumeFraction); + + directory.setLong(Mp4Directory.TAG_NEXT_TRACK_ID, nextTrackID); + } + + private void processMediaHeader(@NotNull SequentialReader reader, Mp4Context context) throws IOException + { + // ISO/IED 14496-12:2015 pg.7 + + int version = reader.getUInt8(); + + reader.skip(3); // flags + + // ISO/IED 14496-12:2015 pg.29 + + if (version == 1) { + context.creationTime = reader.getInt64(); + context.modificationTime = reader.getInt64(); + context.timeScale = (long)reader.getInt32(); + context.duration = reader.getInt64(); + } else { + context.creationTime = reader.getUInt32(); + context.modificationTime = reader.getUInt32(); + context.timeScale = reader.getUInt32(); + context.duration = reader.getUInt32(); + } + + int languageBits = reader.getInt16(); + + context.language = new String(new char[] + { + (char)(((languageBits & 0x7C00) >> 10) + 0x60), + (char)(((languageBits & 0x03E0) >> 5) + 0x60), + (char)((languageBits & 0x001F) + 0x60) + }); + } + + private void processTrackHeader(@NotNull SequentialReader reader) throws IOException + { + // ISO/IED 14496-12:2015 pg.7 + + int version = reader.getUInt8(); + + reader.skip(3); // flags + + // ISO/IED 14496-12:2005 pg.17-18 + + long creationTime; + long modificationTime; + long trackID; + long duration; + + if (version == 1) { + creationTime = reader.getInt64(); + modificationTime = reader.getInt64(); + trackID = reader.getInt32(); + reader.skip(4); // reserved + duration = reader.getInt64(); + } else { + creationTime = reader.getUInt32(); + modificationTime = reader.getUInt32(); + trackID = reader.getUInt32(); + reader.skip(4); + duration = reader.getUInt32(); + } + + reader.skip(8); // reserved + + int layer = reader.getInt16(); + int alternateGroup = reader.getInt16(); + int volume = reader.getInt16(); + + reader.skip(2); // reserved + + int[] matrix = new int[9]; + for (int i = 0; i < 9; i++) { + matrix[i] = reader.getInt32(); + } + + long width = reader.getInt32(); + long height = reader.getInt32(); + + // TODO seems wrong to only set this once + if (width != 0 && height != 0 && directory.getDoubleObject(Mp4Directory.TAG_ROTATION) == null) { + int x = matrix[1] + matrix[4]; + int y = matrix[0] + matrix[3]; + double theta = Math.atan2(y, x); + double degree = Math.toDegrees(theta); + degree -= 45; + directory.setDouble(Mp4Directory.TAG_ROTATION, degree); + } + } } diff --git a/Source/com/drew/metadata/mp4/Mp4Directory.java b/Source/com/drew/metadata/mp4/Mp4Directory.java index 5f3d2f5f4..1355f370f 100644 --- a/Source/com/drew/metadata/mp4/Mp4Directory.java +++ b/Source/com/drew/metadata/mp4/Mp4Directory.java @@ -20,92 +20,84 @@ */ package com.drew.metadata.mp4; -import java.util.HashMap; - import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; +import java.util.HashMap; + public class Mp4Directory extends Directory { - public static final int TAG_CREATION_TIME = 0x0100; - public static final int TAG_MODIFICATION_TIME = 0x0101; - public static final int TAG_TIME_SCALE = 0x0102; - public static final int TAG_DURATION = 0x0103; - public static final int TAG_DURATION_SECONDS = 0x0104; - public static final int TAG_PREFERRED_RATE = 0x0105; - public static final int TAG_PREFERRED_VOLUME = 0x0106; - public static final int TAG_PREVIEW_TIME = 0x0108; - public static final int TAG_PREVIEW_DURATION = 0x0109; - public static final int TAG_POSTER_TIME = 0x010A; - public static final int TAG_SELECTION_TIME = 0x010B; - public static final int TAG_SELECTION_DURATION = 0x010C; - public static final int TAG_CURRENT_TIME = 0x010D; - public static final int TAG_NEXT_TRACK_ID = 0x010E; - public static final int TAG_TRANSFORMATION_MATRIX = 0x010F; - public static final int TAG_ROTATION = 0x0200; - public static final int TAG_LATITUDE = 0x2001; - public static final int TAG_LONGITUDE = 0x2002; - public static final int TAG_MEDIA_TIME_SCALE = 0x0306; - public static final int TAG_TITLE = 0x3000; - public static final int TAG_COMMENT = 0x3001; - public static final int TAG_SUBTITLE = 0x3002; - public static final int TAG_USER_RATING = 0x3003; - public static final int TAG_CATEGORY = 0x3004; - public static final int TAG_MOOD = 0x3005; + public static final int TAG_CREATION_TIME = 0x0100; + public static final int TAG_MODIFICATION_TIME = 0x0101; + public static final int TAG_TIME_SCALE = 0x0102; + public static final int TAG_DURATION = 0x0103; + public static final int TAG_DURATION_SECONDS = 0x0104; + public static final int TAG_PREFERRED_RATE = 0x0105; + public static final int TAG_PREFERRED_VOLUME = 0x0106; + public static final int TAG_PREVIEW_TIME = 0x0108; + public static final int TAG_PREVIEW_DURATION = 0x0109; + public static final int TAG_POSTER_TIME = 0x010A; + public static final int TAG_SELECTION_TIME = 0x010B; + public static final int TAG_SELECTION_DURATION = 0x010C; + public static final int TAG_CURRENT_TIME = 0x010D; + public static final int TAG_NEXT_TRACK_ID = 0x010E; + public static final int TAG_TRANSFORMATION_MATRIX = 0x010F; + public static final int TAG_ROTATION = 0x0200; + public static final int TAG_LATITUDE = 0x2001; + public static final int TAG_LONGITUDE = 0x2002; + public static final int TAG_MEDIA_TIME_SCALE = 0x0306; + + public static final int TAG_MAJOR_BRAND = 1; + public static final int TAG_MINOR_VERSION = 2; + public static final int TAG_COMPATIBLE_BRANDS = 3; - public static final int TAG_MAJOR_BRAND = 1; - public static final int TAG_MINOR_VERSION = 2; - public static final int TAG_COMPATIBLE_BRANDS = 3; + @NotNull + private static final HashMap _tagNameMap = new HashMap(); - @NotNull - private static final HashMap _tagNameMap = new HashMap(); - static { - _tagNameMap.put(TAG_MAJOR_BRAND, "Major Brand"); - _tagNameMap.put(TAG_MINOR_VERSION, "Minor Version"); - _tagNameMap.put(TAG_COMPATIBLE_BRANDS, "Compatible Brands"); + static { + _tagNameMap.put(TAG_MAJOR_BRAND, "Major Brand"); + _tagNameMap.put(TAG_MINOR_VERSION, "Minor Version"); + _tagNameMap.put(TAG_COMPATIBLE_BRANDS, "Compatible Brands"); - _tagNameMap.put(TAG_CREATION_TIME, "Creation Time"); - _tagNameMap.put(TAG_MODIFICATION_TIME, "Modification Time"); - _tagNameMap.put(TAG_TIME_SCALE, "Media Time Scale"); - _tagNameMap.put(TAG_DURATION, "Duration"); - _tagNameMap.put(TAG_DURATION_SECONDS, "Duration in Seconds"); - _tagNameMap.put(TAG_PREFERRED_RATE, "Preferred Rate"); - _tagNameMap.put(TAG_PREFERRED_VOLUME, "Preferred Volume"); - _tagNameMap.put(TAG_PREVIEW_TIME, "Preview Time"); - _tagNameMap.put(TAG_PREVIEW_DURATION, "Preview Duration"); - _tagNameMap.put(TAG_POSTER_TIME, "Poster Time"); - _tagNameMap.put(TAG_SELECTION_TIME, "Selection Time"); - _tagNameMap.put(TAG_SELECTION_DURATION, "Selection Duration"); - _tagNameMap.put(TAG_CURRENT_TIME, "Current Time"); - _tagNameMap.put(TAG_NEXT_TRACK_ID, "Next Track ID"); - _tagNameMap.put(TAG_TRANSFORMATION_MATRIX, "Transformation Matrix"); - _tagNameMap.put(TAG_ROTATION, "Rotation"); - _tagNameMap.put(TAG_LATITUDE, "Latitude"); - _tagNameMap.put(TAG_LONGITUDE, "Longitude"); - _tagNameMap.put(TAG_TITLE, "Title"); - _tagNameMap.put(TAG_COMMENT, "Comment"); - _tagNameMap.put(TAG_SUBTITLE, "Subtitle"); - _tagNameMap.put(TAG_USER_RATING, "User rating"); - _tagNameMap.put(TAG_CATEGORY, "Tags"); - _tagNameMap.put(TAG_MOOD, "Mood"); + _tagNameMap.put(TAG_CREATION_TIME, "Creation Time"); + _tagNameMap.put(TAG_MODIFICATION_TIME, "Modification Time"); + _tagNameMap.put(TAG_TIME_SCALE, "Media Time Scale"); + _tagNameMap.put(TAG_DURATION, "Duration"); + _tagNameMap.put(TAG_DURATION_SECONDS, "Duration in Seconds"); + _tagNameMap.put(TAG_PREFERRED_RATE, "Preferred Rate"); + _tagNameMap.put(TAG_PREFERRED_VOLUME, "Preferred Volume"); + _tagNameMap.put(TAG_PREVIEW_TIME, "Preview Time"); + _tagNameMap.put(TAG_PREVIEW_DURATION, "Preview Duration"); + _tagNameMap.put(TAG_POSTER_TIME, "Poster Time"); + _tagNameMap.put(TAG_SELECTION_TIME, "Selection Time"); + _tagNameMap.put(TAG_SELECTION_DURATION, "Selection Duration"); + _tagNameMap.put(TAG_CURRENT_TIME, "Current Time"); + _tagNameMap.put(TAG_NEXT_TRACK_ID, "Next Track ID"); + _tagNameMap.put(TAG_TRANSFORMATION_MATRIX, "Transformation Matrix"); + _tagNameMap.put(TAG_ROTATION, "Rotation"); + _tagNameMap.put(TAG_LATITUDE, "Latitude"); + _tagNameMap.put(TAG_LONGITUDE, "Longitude"); - _tagNameMap.put(TAG_MEDIA_TIME_SCALE, "Media Time Scale"); - } + _tagNameMap.put(TAG_MEDIA_TIME_SCALE, "Media Time Scale"); + } - public Mp4Directory() { - this.setDescriptor(new Mp4Descriptor(this)); - } + public Mp4Directory() + { + this.setDescriptor(new Mp4Descriptor(this)); + } - @Override - @NotNull - public String getName() { - return "MP4"; - } + @Override + @NotNull + public String getName() + { + return "MP4"; + } - @Override - @NotNull - protected HashMap getTagNameMap() { - return _tagNameMap; - } + @Override + @NotNull + protected HashMap getTagNameMap() + { + return _tagNameMap; + } } From 2a7b7e24bd6d577f7726724751b8dbdc24aed516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Opfermann?= Date: Sun, 25 Feb 2024 17:34:57 +0100 Subject: [PATCH 3/6] Add more UDTA information. Mostly related to the basic information that can be fount in the Windows Details panel, title, subtitle, etc. --- .../com/drew/metadata/mp4/Mp4BoxHandler.java | 134 ++++++++++++++++-- .../com/drew/metadata/mp4/Mp4Directory.java | 16 ++- 2 files changed, 133 insertions(+), 17 deletions(-) diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 17e56e642..042f22f32 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -20,6 +20,22 @@ */ package com.drew.metadata.mp4; +import static com.drew.metadata.mp4.Mp4Directory.TAG_CATEGORY; +import static com.drew.metadata.mp4.Mp4Directory.TAG_COMMENT; +import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_MOOD; +import static com.drew.metadata.mp4.Mp4Directory.TAG_SUBTITLE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_TITLE; +import static com.drew.metadata.mp4.Mp4Directory.TAG_USER_RATING; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + import com.drew.imaging.mp4.Mp4Handler; import com.drew.lang.DateUtil; import com.drew.lang.Rational; @@ -28,16 +44,12 @@ import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; -import com.drew.metadata.mp4.media.*; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static com.drew.metadata.mp4.Mp4Directory.TAG_LATITUDE; -import static com.drew.metadata.mp4.Mp4Directory.TAG_LONGITUDE; +import com.drew.metadata.mp4.media.Mp4HintHandler; +import com.drew.metadata.mp4.media.Mp4MetaHandler; +import com.drew.metadata.mp4.media.Mp4SoundHandler; +import com.drew.metadata.mp4.media.Mp4TextHandler; +import com.drew.metadata.mp4.media.Mp4UuidBoxHandler; +import com.drew.metadata.mp4.media.Mp4VideoHandler; /** * @author Payton Garland @@ -140,6 +152,8 @@ public Mp4Handler processBox(@NotNull String type, @Nullable byte[] payload, private void processUserData(@NotNull SequentialReader reader, int length) throws IOException { final int LOCATION_CODE = 0xA978797A; // "©xyz" + final int META_TYPE = 0x6D657461; // "meta" + final int XTRA_TYPE = 0x58747261; // "Xtra" String coordinateString = null; @@ -152,11 +166,16 @@ private void processUserData(@NotNull SequentialReader reader, int length) throw int xyzLength = reader.getUInt16(); reader.skip(2); coordinateString = reader.getString(xyzLength, "UTF-8"); - } else if (size >= 8) { - reader.skip(size - 8); - } else { - return; - } + } else if (kind == META_TYPE && size > 16) { + reader.skip(4); + processUserDataMeta(reader, length, size - 12); + } else if (kind == XTRA_TYPE && size > 16) { + processUserDataMetaXtra(reader, length, size - 8); + } else if (size >= 8) { + reader.skip(size - 8); + } else { + return; + } } if (coordinateString != null) { @@ -170,6 +189,91 @@ private void processUserData(@NotNull SequentialReader reader, int length) throw } } } + + private void processUserDataMeta(@NotNull SequentialReader reader, int length, long blockSize) throws IOException { + final int HDLR_TYPE = 0x68646C72; // "hdlr" + final int ILST_TYPE = 0x696C7374; // "ilst" + + long initialPosition = reader.getPosition(); + + while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { + long size = reader.getUInt32(); + if (size <= 4) + break; + int kind = reader.getInt32(); + if (kind == HDLR_TYPE) { + // nothing + reader.skip(size - 8); + } else if (kind == ILST_TYPE && size > 16) { + processUserDataMetaIList(reader, length, size - 8); + } + } + } + + private void processUserDataMetaIList(@NotNull SequentialReader reader, int length, long blockSize) + throws IOException { + final int CNAM_TYPE = 0xA96E616D; // "©nam" + final int CCMT_TYPE = 0xA9636D74; // "©cmt" + long initialPosition = reader.getPosition(); + + while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { + long size = reader.getUInt32(); + if (size <= 4) + break; + int kind = reader.getInt32(); + if (kind == CNAM_TYPE) { + long cnamSize = reader.getUInt32(); + if (cnamSize > 16) { + reader.skip(12); + directory.setString(TAG_TITLE, reader.getString((int) cnamSize - 16, "UTF-8")); + } + } else if (kind == CCMT_TYPE) { + long ccmtSize = reader.getUInt32(); + if (ccmtSize > 16) { + reader.skip(12); + directory.setString(TAG_COMMENT, reader.getString((int) ccmtSize - 16, "UTF-8")); + } + } else { + // nothing + } + } + } + + private void processUserDataMetaXtra(@NotNull SequentialReader reader, int length, long blockSize) + throws IOException { + long initialPosition = reader.getPosition(); + while (reader.getPosition() < length && (reader.getPosition() - initialPosition) < blockSize) { + long key_size = reader.getUInt32(); + long key_name_size = reader.getUInt32(); + String key_name = reader.getString((int) key_name_size, "UTF-8"); + long entry_count = reader.getUInt32(); + if (key_name.equals("WM/SubTitle")) { + String value = getProcessUserDataMetaXtraValue(reader, entry_count); + directory.setString(TAG_SUBTITLE, value); + } else if (key_name.equals("WM/SharedUserRating")) { + long value_size = reader.getUInt32(); + int value_type = reader.getUInt16(); + directory.setLong(TAG_USER_RATING, reader.getInt64()); + } else if (key_name.equals("WM/Category")) { + String value = getProcessUserDataMetaXtraValue(reader, entry_count); + directory.setString(TAG_CATEGORY, value); + } else if (key_name.equals("WM/Mood")) { + String value = getProcessUserDataMetaXtraValue(reader, entry_count); + directory.setString(TAG_MOOD, value); + } + } + } + + private String getProcessUserDataMetaXtraValue(@NotNull SequentialReader reader, long entry_count) + throws IOException { + List result = new ArrayList<>(); + for (long i = 0; i < entry_count; ++i) { + long value_size = reader.getUInt32(); + int val_type = reader.getUInt16(); + result.add(reader.getString((int) value_size - 6, "UTF-8")); + } + return String.join(" | ", result); + } private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException { diff --git a/Source/com/drew/metadata/mp4/Mp4Directory.java b/Source/com/drew/metadata/mp4/Mp4Directory.java index 1355f370f..8369fc805 100644 --- a/Source/com/drew/metadata/mp4/Mp4Directory.java +++ b/Source/com/drew/metadata/mp4/Mp4Directory.java @@ -20,11 +20,11 @@ */ package com.drew.metadata.mp4; +import java.util.HashMap; + import com.drew.lang.annotations.NotNull; import com.drew.metadata.Directory; -import java.util.HashMap; - public class Mp4Directory extends Directory { public static final int TAG_CREATION_TIME = 0x0100; @@ -46,6 +46,12 @@ public class Mp4Directory extends Directory { public static final int TAG_LATITUDE = 0x2001; public static final int TAG_LONGITUDE = 0x2002; public static final int TAG_MEDIA_TIME_SCALE = 0x0306; + public static final int TAG_TITLE = 0x3000; + public static final int TAG_COMMENT = 0x3001; + public static final int TAG_SUBTITLE = 0x3002; + public static final int TAG_USER_RATING = 0x3003; + public static final int TAG_CATEGORY = 0x3004; + public static final int TAG_MOOD = 0x3005; public static final int TAG_MAJOR_BRAND = 1; public static final int TAG_MINOR_VERSION = 2; @@ -78,6 +84,12 @@ public class Mp4Directory extends Directory { _tagNameMap.put(TAG_ROTATION, "Rotation"); _tagNameMap.put(TAG_LATITUDE, "Latitude"); _tagNameMap.put(TAG_LONGITUDE, "Longitude"); + _tagNameMap.put(TAG_TITLE, "Title"); + _tagNameMap.put(TAG_COMMENT, "Comment"); + _tagNameMap.put(TAG_SUBTITLE, "Subtitle"); + _tagNameMap.put(TAG_USER_RATING, "User rating"); + _tagNameMap.put(TAG_CATEGORY, "Tags"); + _tagNameMap.put(TAG_MOOD, "Mood"); _tagNameMap.put(TAG_MEDIA_TIME_SCALE, "Media Time Scale"); } From 76a0d18b75c471c89dc6df12d0c8d69fb4b4b320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Opfermann?= Date: Sun, 25 Feb 2024 17:47:39 +0100 Subject: [PATCH 4/6] Correction for Java 7 using local StringUtil.join instead of the Java standard one which is probably Java 8 --- Source/com/drew/metadata/mp4/Mp4BoxHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 042f22f32..312973fbe 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -41,6 +41,7 @@ import com.drew.lang.Rational; import com.drew.lang.SequentialByteArrayReader; import com.drew.lang.SequentialReader; +import com.drew.lang.StringUtil; import com.drew.lang.annotations.NotNull; import com.drew.lang.annotations.Nullable; import com.drew.metadata.Metadata; @@ -272,7 +273,7 @@ private String getProcessUserDataMetaXtraValue(@NotNull SequentialReader reader, int val_type = reader.getUInt16(); result.add(reader.getString((int) value_size - 6, "UTF-8")); } - return String.join(" | ", result); + return StringUtil.join(result, " | "); } private void processFileType(@NotNull SequentialReader reader, long boxSize) throws IOException From 935ffcd3d0daa41fc4c4fc662f74eceacb1cb3e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Opfermann?= Date: Sun, 25 Feb 2024 18:44:37 +0100 Subject: [PATCH 5/6] Changed the charset of the windows encoding to match the MP4 files --- Source/com/drew/metadata/mp4/Mp4BoxHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 312973fbe..8b558d2a1 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -271,7 +271,7 @@ private String getProcessUserDataMetaXtraValue(@NotNull SequentialReader reader, for (long i = 0; i < entry_count; ++i) { long value_size = reader.getUInt32(); int val_type = reader.getUInt16(); - result.add(reader.getString((int) value_size - 6, "UTF-8")); + result.add(reader.getString((int) value_size - 6,"UTF-16LE")); } return StringUtil.join(result, " | "); } From 3f96b81ae061652facba266a679e628836ffd868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Opfermann?= Date: Sun, 25 Feb 2024 19:03:53 +0100 Subject: [PATCH 6/6] There is always one null byte at the end of the string which needs to be removed. It's also a security in case of a multiple null byte string, it replaces all of these nulls by an empty character. --- Source/com/drew/metadata/mp4/Mp4BoxHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java index 8b558d2a1..29861f6a3 100644 --- a/Source/com/drew/metadata/mp4/Mp4BoxHandler.java +++ b/Source/com/drew/metadata/mp4/Mp4BoxHandler.java @@ -271,7 +271,7 @@ private String getProcessUserDataMetaXtraValue(@NotNull SequentialReader reader, for (long i = 0; i < entry_count; ++i) { long value_size = reader.getUInt32(); int val_type = reader.getUInt16(); - result.add(reader.getString((int) value_size - 6,"UTF-16LE")); + result.add(reader.getString((int) value_size - 6,"UTF-16LE").replace("\0", "")); } return StringUtil.join(result, " | "); }