com.drew.metadata.mp4.Mp4BoxHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of metadata-extractor Show documentation
Show all versions of metadata-extractor Show documentation
Java library for extracting EXIF, IPTC, XMP, ICC and other metadata from image and video files.
/*
* Copyright 2002-2019 Drew Noakes and contributors
*
* Licensed 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.
*
* More information about this project is available at:
*
* https://drewnoakes.com/code/exif/
* https://github.com/drewnoakes/metadata-extractor
*/
package com.drew.metadata.mp4;
import com.drew.imaging.mp4.Mp4Handler;
import com.drew.lang.DateUtil;
import com.drew.lang.Rational;
import com.drew.lang.SequentialByteArrayReader;
import com.drew.lang.SequentialReader;
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;
/**
* @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());
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);
}
}
}