org.mp4parser.boxes.microsoft.XtraBox Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of isoparser Show documentation
Show all versions of isoparser Show documentation
A generic parser and writer for all ISO 14496 based files (MP4, Quicktime, DCF, PDCF, ...)
/*
* Copyright 2008 CoreMedia AG, Hamburg
*
* 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.
*/
package org.mp4parser.boxes.microsoft;
import org.mp4parser.support.AbstractBox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Date;
import java.util.Vector;
/**
* 4cc = "{@value #TYPE}"
* Windows Media Xtra Box.
*
* I can't find definitive documentation on this from Microsoft so it's cobbled together from
* various sources. Mostly ExifTool for Perl.
*
* Various references:
* https://msdn.microsoft.com/en-us/library/windows/desktop/dd743066(v=vs.85).aspx
* https://metacpan.org/source/EXIFTOOL/Image-ExifTool-9.76/lib/Image/ExifTool/Microsoft.pm
* http://www.ventismedia.com/mantis/view.php?id=12017
* http://www.hydrogenaudio.org/forums/index.php?showtopic=75123&st=250
* http://www.mediamonkey.com/forum/viewtopic.php?f=1&t=76321
* https://code.google.com/p/mp4v2/issues/detail?id=113
*
* @author marwatk
*/
public class XtraBox extends AbstractBox {
private static Logger LOG = LoggerFactory.getLogger(XtraBox.class);
public static final String TYPE = "Xtra";
public static final int MP4_XTRA_BT_UNICODE = 8;
public static final int MP4_XTRA_BT_INT64 = 19;
public static final int MP4_XTRA_BT_FILETIME = 21;
public static final int MP4_XTRA_BT_GUID = 72;
//http://stackoverflow.com/questions/5398557/java-library-for-dealing-with-win32-filetime
private static final long FILETIME_EPOCH_DIFF = 11644473600000L;
private static final long FILETIME_ONE_MILLISECOND = 10 * 1000;
Vector tags = new Vector();
ByteBuffer data;
private boolean successfulParse = false;
public XtraBox() {
super("Xtra");
}
public XtraBox(String type) {
super(type);
}
private static long filetimeToMillis(final long filetime) {
return (filetime / FILETIME_ONE_MILLISECOND) - FILETIME_EPOCH_DIFF;
}
private static long millisToFiletime(final long millis) {
return (millis + FILETIME_EPOCH_DIFF) * FILETIME_ONE_MILLISECOND;
}
private static void writeAsciiString(ByteBuffer dest, String s) {
try {
dest.put(s.getBytes("US-ASCII"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Shouldn't happen", e);
}
}
private static String readAsciiString(ByteBuffer content, int length) {
byte s[] = new byte[length];
content.get(s);
try {
return new String(s, "US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Shouldn't happen", e);
}
}
private static String readUtf16String(ByteBuffer content, int length) {
char s[] = new char[(length / 2) - 1];
for (int i = 0; i < (length / 2) - 1; i++) {
s[i] = content.getChar();
}
content.getChar(); //Discard terminating null
return new String(s);
}
private static void writeUtf16String(ByteBuffer dest, String s) {
char ar[] = s.toCharArray();
for (int i = 0; i < ar.length; i++) { //Probably not the best way to do this but it preserves the byte order
dest.putChar(ar[i]);
}
dest.putChar((char) 0); //Terminating null
}
@Override
protected long getContentSize() {
if (successfulParse) {
return detailSize();
} else {
return data.limit();
}
}
private int detailSize() {
int size = 0;
for (int i = 0; i < tags.size(); i++) {
size += tags.elementAt(i).getContentSize();
}
return size;
}
public String toString() {
if (!this.isParsed()) {
this.parseDetails();
}
StringBuffer b = new StringBuffer();
b.append("XtraBox[");
for (XtraTag tag : tags) {
for (XtraValue value : tag.values) {
b.append(tag.tagName);
b.append("=");
b.append(value.toString());
b.append(";");
}
}
b.append("]");
return b.toString();
}
@Override
public void _parseDetails(ByteBuffer content) {
int boxSize = content.remaining();
data = content.slice(); //Keep this in case we fail to parse
successfulParse = false;
try {
tags.clear();
while (content.remaining() > 0) {
XtraTag tag = new XtraTag();
tag.parse(content);
tags.addElement(tag);
}
int calcSize = detailSize();
if (boxSize != calcSize) {
throw new RuntimeException("Improperly handled Xtra tag: Calculated sizes don't match ( " + boxSize + "/" + calcSize + ")");
}
successfulParse = true;
} catch (Exception e) {
successfulParse = false;
LOG.error("Malformed Xtra Tag detected: {}", e.toString());
content.position(content.position() + content.remaining());
} finally {
content.order(ByteOrder.BIG_ENDIAN); //Just in case we bailed out mid-parse we don't want to leave the byte order in MS land
}
}
@Override
protected void getContent(ByteBuffer byteBuffer) {
if (successfulParse) {
for (int i = 0; i < tags.size(); i++) {
tags.elementAt(i).getContent(byteBuffer);
}
} else {
data.rewind();
byteBuffer.put(data);
}
}
/**
* Returns a list of the tag names present in this Xtra Box
*
* @return Possibly empty (zero length) array of tag names present
*/
public String[] getAllTagNames() {
String names[] = new String[tags.size()];
for (int i = 0; i < tags.size(); i++) {
XtraTag tag = tags.elementAt(i);
names[i] = tag.tagName;
}
return names;
}
/**
* Returns the first String value found for this tag
*
* @param name Tag name
* @return First String value found
*/
public String getFirstStringValue(String name) {
Object objs[] = getValues(name);
for (Object obj : objs) {
if (obj instanceof String) {
return (String) obj;
}
}
return null;
}
/**
* Returns the first Date value found for this tag
*
* @param name Tag name
* @return First Date value found
*/
public Date getFirstDateValue(String name) {
Object objs[] = getValues(name);
for (Object obj : objs) {
if (obj instanceof Date) {
return (Date) obj;
}
}
return null;
}
/**
* Returns the first Long value found for this tag
*
* @param name Tag name
* @return First long value found
*/
public Long getFirstLongValue(String name) {
Object objs[] = getValues(name);
for (Object obj : objs) {
if (obj instanceof Long) {
return (Long) obj;
}
}
return null;
}
/**
* Returns an array of values for this tag. Empty array when tag is not present
*
* @param name Tag name to retrieve
* @return Possibly empty array of values (possible types are String, Long, Date and byte[] )
*/
public Object[] getValues(String name) {
XtraTag tag = getTagByName(name);
Object values[];
if (tag != null) {
values = new Object[tag.values.size()];
for (int i = 0; i < tag.values.size(); i++) {
values[i] = tag.values.elementAt(i).getValueAsObject();
}
} else {
values = new Object[0];
}
return values;
}
/**
* Removes specified tag (all values for that tag will be removed)
*
* @param name Tag to remove
*/
public void removeTag(String name) {
XtraTag tag = getTagByName(name);
if (tag != null) {
tags.remove(tag);
}
}
/**
* Removes and recreates tag using specified String values
*
* @param name Tag name to replace
* @param values New String values
*/
public void setTagValues(String name, String values[]) {
removeTag(name);
XtraTag tag = new XtraTag(name);
for (int i = 0; i < values.length; i++) {
tag.values.addElement(new XtraValue(values[i]));
}
tags.addElement(tag);
}
/**
* Removes and recreates tag using specified String value
*
* @param name Tag name to replace
* @param value New String value
*/
public void setTagValue(String name, String value) {
setTagValues(name, new String[]{value});
}
/**
* Removes and recreates tag using specified Date value
*
* @param name Tag name to replace
* @param date New Date value
*/
public void setTagValue(String name, Date date) {
removeTag(name);
XtraTag tag = new XtraTag(name);
tag.values.addElement(new XtraValue(date));
tags.addElement(tag);
}
/**
* Removes and recreates tag using specified Long value
*
* @param name Tag name to replace
* @param value New Long value
*/
public void setTagValue(String name, long value) {
removeTag(name);
XtraTag tag = new XtraTag(name);
tag.values.addElement(new XtraValue(value));
tags.addElement(tag);
}
private XtraTag getTagByName(String name) {
for (XtraTag tag : tags) {
if (tag.tagName.equals(name)) {
return tag;
}
}
return null;
}
private static class XtraTag {
private int inputSize; //For debugging only
private String tagName;
private Vector values;
private XtraTag() {
values = new Vector();
}
private XtraTag(String name) {
this();
tagName = name;
}
private void parse(ByteBuffer content) {
inputSize = content.getInt();
int tagLength = content.getInt();
tagName = readAsciiString(content, tagLength);
int count = content.getInt();
for (int i = 0; i < count; i++) {
XtraValue val = new XtraValue();
val.parse(content);
values.addElement(val);
}
if (inputSize != getContentSize()) {
throw new RuntimeException("Improperly handled Xtra tag: Sizes don't match ( " + inputSize + "/" + getContentSize() + ") on " + tagName);
}
}
private void getContent(ByteBuffer b) {
b.putInt(getContentSize());
b.putInt(tagName.length());
writeAsciiString(b, tagName);
b.putInt(values.size());
for (int i = 0; i < values.size(); i++) {
values.elementAt(i).getContent(b);
}
}
private int getContentSize() {
//Size: 4
//TagLength: 4
//Tag: tagLength;
//Count: 4
//Values: count * values.getContentSize();
int size = 12 + tagName.length();
for (int i = 0; i < values.size(); i++) {
size += values.elementAt(i).getContentSize();
}
return size;
}
public String toString() {
StringBuffer b = new StringBuffer();
b.append(tagName);
b.append(" [");
b.append(inputSize);
b.append("/");
b.append(values.size());
b.append("]:\n");
for (int i = 0; i < values.size(); i++) {
b.append(" ");
b.append(values.elementAt(i).toString());
b.append("\n");
}
return b.toString();
}
}
private static class XtraValue {
public int type;
public String stringValue;
public long longValue;
public byte[] nonParsedValue;
public Date fileTimeValue;
private XtraValue() {
}
private XtraValue(String val) {
type = MP4_XTRA_BT_UNICODE;
stringValue = val;
}
private XtraValue(long longVal) {
type = MP4_XTRA_BT_INT64;
longValue = longVal;
}
private XtraValue(Date time) {
type = MP4_XTRA_BT_FILETIME;
fileTimeValue = time;
}
private Object getValueAsObject() {
switch (type) {
case MP4_XTRA_BT_UNICODE:
return stringValue;
case MP4_XTRA_BT_INT64:
return new Long(longValue);
case MP4_XTRA_BT_FILETIME:
return fileTimeValue;
case MP4_XTRA_BT_GUID:
default:
return nonParsedValue;
}
}
private void parse(ByteBuffer content) {
int length = content.getInt() - 6; //length + type are included in length
type = content.getShort();
content.order(ByteOrder.LITTLE_ENDIAN);
switch (type) {
case MP4_XTRA_BT_UNICODE:
stringValue = readUtf16String(content, length);
break;
case MP4_XTRA_BT_INT64:
longValue = content.getLong();
break;
case MP4_XTRA_BT_FILETIME:
fileTimeValue = new Date(filetimeToMillis(content.getLong()));
break;
case MP4_XTRA_BT_GUID:
default:
nonParsedValue = new byte[length];
content.get(nonParsedValue);
break;
}
content.order(ByteOrder.BIG_ENDIAN);
}
private void getContent(ByteBuffer b) {
try {
int length = getContentSize();
b.putInt(length);
b.putShort((short) type);
b.order(ByteOrder.LITTLE_ENDIAN);
switch (type) {
case MP4_XTRA_BT_UNICODE:
writeUtf16String(b, stringValue);
break;
case MP4_XTRA_BT_INT64:
b.putLong(longValue);
break;
case MP4_XTRA_BT_FILETIME:
b.putLong(millisToFiletime(fileTimeValue.getTime()));
break;
case MP4_XTRA_BT_GUID:
default:
b.put(nonParsedValue);
break;
}
} finally {
b.order(ByteOrder.BIG_ENDIAN);
}
}
private int getContentSize() {
//Length: 4 bytes
//Type: 2 bytes
//Content: length bytes
int size = 6;
switch (type) {
case MP4_XTRA_BT_UNICODE:
size += (stringValue.length() * 2) + 2; //Plus 2 for trailing null
break;
case MP4_XTRA_BT_INT64:
case MP4_XTRA_BT_FILETIME:
size += 8;
break;
case MP4_XTRA_BT_GUID:
default:
size += nonParsedValue.length;
break;
}
return size;
}
public String toString() {
switch (type) {
case MP4_XTRA_BT_UNICODE:
return "[string]" + stringValue;
case MP4_XTRA_BT_INT64:
return "[long]" + longValue;
case MP4_XTRA_BT_FILETIME:
return "[filetime]" + fileTimeValue.toString();
case MP4_XTRA_BT_GUID:
default:
return "[GUID](nonParsed)";
}
}
}
}