package com.castlabs.dash.helpers; import com.coremedia.iso.Hex; import com.coremedia.iso.boxes.Box; import com.coremedia.iso.boxes.OriginalFormatBox; import com.coremedia.iso.boxes.SampleDescriptionBox; import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; import com.coremedia.iso.boxes.sampleentry.SampleEntry; import com.googlecode.mp4parser.authoring.Edit; import com.googlecode.mp4parser.authoring.Track; import com.googlecode.mp4parser.boxes.AC3SpecificBox; import com.googlecode.mp4parser.boxes.DTSSpecificBox; import com.googlecode.mp4parser.boxes.EC3SpecificBox; import com.googlecode.mp4parser.boxes.MLPSpecificBox; import com.googlecode.mp4parser.boxes.mp4.ESDescriptorBox; import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.AudioSpecificConfig; import com.googlecode.mp4parser.boxes.mp4.objectdescriptors.DecoderConfigDescriptor; import com.googlecode.mp4parser.util.Path; import com.mp4parser.iso14496.part15.AvcConfigurationBox; import com.mp4parser.iso14496.part15.HevcConfigurationBox; import com.mp4parser.iso14496.part30.XMLSubtitleSampleEntry; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Gets the precise MIME type according to RFC6381. * http://tools.ietf.org/html/rfc6381 */ public final class DashHelper { public static long getAudioSamplingRate(AudioSampleEntry e) { ESDescriptorBox esds = Path.getPath(e, "esds"); if (esds != null) { final DecoderConfigDescriptor decoderConfigDescriptor = esds.getEsDescriptor().getDecoderConfigDescriptor(); final AudioSpecificConfig audioSpecificConfig = decoderConfigDescriptor.getAudioSpecificInfo(); if (audioSpecificConfig.getExtensionAudioObjectType() > 0 && audioSpecificConfig.sbrPresentFlag) { return audioSpecificConfig.getExtensionSamplingFrequency(); } else { return audioSpecificConfig.getSamplingFrequency(); } } else { return e.getSampleRate(); } } public static ChannelConfiguration getChannelConfiguration(AudioSampleEntry e) { DTSSpecificBox ddts = Path.getPath(e, "ddts"); if (ddts != null) { return getDTSChannelConfig(e, ddts); } MLPSpecificBox dmlp = Path.getPath(e, "dmlp"); if (dmlp != null) { return null; // getMLPChannelConfig(e, dmlp); } ESDescriptorBox esds = Path.getPath(e, "esds"); if (esds != null) { return getAACChannelConfig(e, esds); } esds = Path.getPath(e, "..../esds"); // Apple does weird things if (esds != null) { return getAACChannelConfig(e, esds); } AC3SpecificBox dac3 = Path.getPath(e, "dac3"); if (dac3 != null) { return getAC3ChannelConfig(e, dac3); } EC3SpecificBox dec3 = Path.getPath(e, "dec3"); if (dec3 != null) { return getEC3ChannelConfig(e, dec3); } return null; } private static ChannelConfiguration getEC3ChannelConfig(AudioSampleEntry e, EC3SpecificBox dec3) { final List<EC3SpecificBox.Entry> ec3SpecificBoxEntries = dec3.getEntries(); int audioChannelValue = 0; for (EC3SpecificBox.Entry ec3SpecificBoxEntry : ec3SpecificBoxEntries) { audioChannelValue |= getDolbyAudioChannelValue(ec3SpecificBoxEntry.acmod, ec3SpecificBoxEntry.lfeon, ec3SpecificBoxEntry.chan_loc); } ChannelConfiguration cc = new ChannelConfiguration(); cc.value = Hex.encodeHex(new byte[]{(byte) ((audioChannelValue >> 8) & 0xFF), (byte) (audioChannelValue & 0xFF)}); cc.schemeIdUri = "urn:dolby:dash:audio_channel_configuration:2011"; return cc; } private static ChannelConfiguration getAC3ChannelConfig(AudioSampleEntry e, AC3SpecificBox dac3) { ChannelConfiguration cc = new ChannelConfiguration(); int audioChannelValue = getDolbyAudioChannelValue(dac3.getAcmod(), dac3.getLfeon(), 0); cc.value = Hex.encodeHex(new byte[]{(byte) ((audioChannelValue >> 8) & 0xFF), (byte) (audioChannelValue & 0xFF)}); cc.schemeIdUri = "urn:dolby:dash:audio_channel_configuration:2011"; return cc; } private static int getDolbyAudioChannelValue(int acmod, int lfeon, int chan_loc) { int audioChannelValue; switch (acmod) { case 0: audioChannelValue = 0xA000; break; case 1: audioChannelValue = 0x4000; break; case 2: audioChannelValue = 0xA000; break; case 3: audioChannelValue = 0xE000; break; case 4: audioChannelValue = 0xA100; break; case 5: audioChannelValue = 0xE100; break; case 6: audioChannelValue = 0xB800; break; case 7: audioChannelValue = 0xF800; break; default: throw new RuntimeException("Unexpected acmod " + acmod); } if (lfeon == 1) { audioChannelValue += 1; } int[] chanLoc2audioChannelConfiguration = new int[]{ 0b0000010000000000, // 0 - Lc/Rc 0b0000001000000000, // 1 - Lls/Lrs 0b0000000100000000, // 2 - Cs 0b0000000010000000, // 3 - Ts 0b0000000001000000, // 4 - Lsd/Rsd 0b0000000000100000, // 5 - Lw/Rw 0b0000000000010000, // 6 - Lvh/Rvh 0b0000000000001000, // 7 - Cvh 0b0000000000000010, // 8 - LFE2 }; for (int i = 0; i < chanLoc2audioChannelConfiguration.length; i++) { if ((chan_loc & (0b000000001 << i)) >0) { audioChannelValue |= chanLoc2audioChannelConfiguration[i]; } } return audioChannelValue; } public static double getEarliestTrackPresentationTime(List<Edit> edits) { double earliestTrackPresentationTime = 0; boolean acceptEdit = true; boolean acceptDwell = true; for (Edit edit : edits) { if (edit.getMediaTime() == -1 && !acceptDwell) { throw new RuntimeException("Cannot accept edit list for processing (1)"); } if (edit.getMediaTime() >= 0 && !acceptEdit) { throw new RuntimeException("Cannot accept edit list for processing (2)"); } if (edit.getMediaTime() == -1) { earliestTrackPresentationTime += edit.getSegmentDuration(); } else /* if edit.getMediaTime() >= 0 */ { earliestTrackPresentationTime -= (double) edit.getMediaTime() / edit.getTimeScale(); acceptEdit = false; acceptDwell = false; } } return earliestTrackPresentationTime; } private static ChannelConfiguration getAACChannelConfig(AudioSampleEntry e, ESDescriptorBox esds) { final DecoderConfigDescriptor decoderConfigDescriptor = esds.getEsDescriptor().getDecoderConfigDescriptor(); final AudioSpecificConfig audioSpecificConfig = decoderConfigDescriptor.getAudioSpecificInfo(); ChannelConfiguration cc = new ChannelConfiguration(); cc.schemeIdUri = "urn:mpeg:dash:23003:3:audio_channel_configuration:2011"; cc.value = "2"; if (audioSpecificConfig != null && audioSpecificConfig.getChannelConfiguration()>2 ) { // in case of mono let's assume stereo as it will be Parametric Stereo in most cases. cc.value = String.valueOf(audioSpecificConfig.getChannelConfiguration()); } return cc; } /** * Returns the number of frame which correspond to the time given. */ public static int time2Frames(Track track, double timeInSeconds) { int i = 0; while (timeInSeconds > 0) { timeInSeconds -= (double) track.getSampleDurations()[i] / track.getTrackMetaData().getTimescale(); i++; } return i; } private static int getNumChannels(DTSSpecificBox dtsSpecificBox) { final int channelLayout = dtsSpecificBox.getChannelLayout(); int numChannels = 0; int dwChannelMask = 0; if ((channelLayout & 0x0001) == 0x0001) { //0001h Center in front of listener 1 numChannels += 1; dwChannelMask |= 0x00000004; //SPEAKER_FRONT_CENTER } if ((channelLayout & 0x0002) == 0x0002) { //0002h Left/Right in front 2 numChannels += 2; dwChannelMask |= 0x00000001; //SPEAKER_FRONT_LEFT dwChannelMask |= 0x00000002; //SPEAKER_FRONT_RIGHT } if ((channelLayout & 0x0004) == 0x0004) { //0004h Left/Right surround on side in rear 2 numChannels += 2; //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT } if ((channelLayout & 0x0008) == 0x0008) { //0008h Low frequency effects subwoofer 1 numChannels += 1; dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY } if ((channelLayout & 0x0010) == 0x0010) { //0010h Center surround in rear 1 numChannels += 1; dwChannelMask |= 0x00000100; //SPEAKER_BACK_CENTER } if ((channelLayout & 0x0020) == 0x0020) { //0020h Left/Right height in front 2 numChannels += 2; dwChannelMask |= 0x00001000; //SPEAKER_TOP_FRONT_LEFT dwChannelMask |= 0x00004000; //SPEAKER_TOP_FRONT_RIGHT } if ((channelLayout & 0x0040) == 0x0040) { //0040h Left/Right surround in rear 2 numChannels += 2; dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT } if ((channelLayout & 0x0080) == 0x0080) { //0080h Center Height in front 1 numChannels += 1; dwChannelMask |= 0x00002000; //SPEAKER_TOP_FRONT_CENTER } if ((channelLayout & 0x0100) == 0x0100) { //0100h Over the listener’s head 1 numChannels += 1; dwChannelMask |= 0x00000800; //SPEAKER_TOP_CENTER } if ((channelLayout & 0x0200) == 0x0200) { //0200h Between left/right and center in front 2 numChannels += 2; dwChannelMask |= 0x00000040; //SPEAKER_FRONT_LEFT_OF_CENTER dwChannelMask |= 0x00000080; //SPEAKER_FRONT_RIGHT_OF_CENTER } if ((channelLayout & 0x0400) == 0x0400) { //0400h Left/Right on side in front 2 numChannels += 2; dwChannelMask |= 0x00000200; //SPEAKER_SIDE_LEFT dwChannelMask |= 0x00000400; //SPEAKER_SIDE_RIGHT } if ((channelLayout & 0x0800) == 0x0800) { //0800h Left/Right surround on side 2 numChannels += 2; //* if Lss, Rss exist, then this position is equivalent to Lsr, Rsr respectively dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT } if ((channelLayout & 0x1000) == 0x1000) { //1000h Second low frequency effects subwoofer 1 numChannels += 1; dwChannelMask |= 0x00000008; //SPEAKER_LOW_FREQUENCY } if ((channelLayout & 0x2000) == 0x2000) { //2000h Left/Right height on side 2 numChannels += 2; dwChannelMask |= 0x00000010; //SPEAKER_BACK_LEFT dwChannelMask |= 0x00000020; //SPEAKER_BACK_RIGHT } if ((channelLayout & 0x4000) == 0x4000) { //4000h Center height in rear 1 numChannels += 1; dwChannelMask |= 0x00010000; //SPEAKER_TOP_BACK_CENTER } if ((channelLayout & 0x8000) == 0x8000) { //8000h Left/Right height in rear 2 numChannels += 2; dwChannelMask |= 0x00008000; //SPEAKER_TOP_BACK_LEFT dwChannelMask |= 0x00020000; //SPEAKER_TOP_BACK_RIGHT } if ((channelLayout & 0x10000) == 0x10000) { //10000h Center below in front numChannels += 1; } if ((channelLayout & 0x20000) == 0x20000) { //20000h Left/Right below in front numChannels += 2; } return numChannels; } private static ChannelConfiguration getDTSChannelConfig(AudioSampleEntry e, DTSSpecificBox ddts) { ChannelConfiguration cc = new ChannelConfiguration(); cc.value = Integer.toString(getNumChannels(ddts)); cc.schemeIdUri = "urn:dts:dash:audio_channel_configuration:2012"; return cc; } /** * Gets the codec according to RFC 6381 from a <code>SampleEntry</code>. * * @param se <code>SampleEntry</code> to id the codec. * @return codec according to RFC */ public static String getRfc6381Codec(SampleEntry se) { OriginalFormatBox frma = Path.getPath((Box) se, "sinf/frma"); String type; if (frma != null) { type = frma.getDataFormat(); } else { type = se.getType(); } if ("avc1".equals(type) || "avc2".equals(type) || "avc3".equals(type) || "avc4".equals(type)) { AvcConfigurationBox avcConfigurationBox = Path.getPath((Box) se, "avcC"); List<byte[]> spsbytes = avcConfigurationBox.getSequenceParameterSets(); byte[] CodecInit = new byte[3]; CodecInit[0] = spsbytes.get(0)[1]; CodecInit[1] = spsbytes.get(0)[2]; CodecInit[2] = spsbytes.get(0)[3]; return (type + "." + Hex.encodeHex(CodecInit)).toLowerCase(); } else if (type.equals("mp4a")) { ESDescriptorBox esDescriptorBox = Path.getPath((Box) se, "esds"); if (esDescriptorBox == null) { esDescriptorBox = Path.getPath((Box) se, "..../esds"); // Apple does weird things } final DecoderConfigDescriptor decoderConfigDescriptor = esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor(); final AudioSpecificConfig audioSpecificConfig = decoderConfigDescriptor.getAudioSpecificInfo(); if (audioSpecificConfig != null && audioSpecificConfig.sbrPresentFlag && !audioSpecificConfig.psPresentFlag) { return "mp4a.40.5"; } else if (audioSpecificConfig != null && audioSpecificConfig.sbrPresentFlag && audioSpecificConfig.psPresentFlag) { return "mp4a.40.29"; } else { return "mp4a.40.2"; } } else if (type.equals("mp4v")) { ESDescriptorBox esDescriptorBox = Path.getPath((Box) se, "esds"); if (esDescriptorBox == null) { esDescriptorBox = Path.getPath((Box) se, "..../esds"); // Apple does weird things } if (esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor().getObjectTypeIndication() == 0x6C) { return "mp4v." + Integer.toHexString(esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor().getObjectTypeIndication()); } else { throw new RuntimeException("I don't know how to construct codec for mp4v with OTI " + esDescriptorBox.getEsDescriptor().getDecoderConfigDescriptor().getObjectTypeIndication() ); } } else if (type.equals("dtsl") || type.equals("dtsl") || type.equals("dtse")) { return type; } else if (type.equals("ec-3") || type.equals("ac-3") || type.equals("mlpa")) { return type; } else if (type.equals("hev1") || type.equals("hvc1")) { int c; HevcConfigurationBox hvcc = Path.getPath((Box) se, "hvcC"); String codec = ""; codec += type + "."; if (hvcc.getGeneral_profile_space() == 1) { codec += "A"; } else if (hvcc.getGeneral_profile_space() == 2) { codec += "B"; } else if (hvcc.getGeneral_profile_space() == 3) { codec += "C"; } //profile idc encoded as a decimal number codec += hvcc.getGeneral_profile_idc(); //general profile compatibility flags: hexa, bit-reversed { long val = hvcc.getGeneral_profile_compatibility_flags(); long i, res = 0; for (i = 0; i < 31; i++) { res |= val & 1; res <<= 1; val >>= 1; } res |= val & 1; codec += "."; codec += Long.toHexString(res); } if (hvcc.isGeneral_tier_flag()) { codec += ".H"; } else { codec += ".L"; } codec += hvcc.getGeneral_level_idc(); long _general_constraint_indicator_flags = hvcc.getGeneral_constraint_indicator_flags(); if (hvcc.getHevcDecoderConfigurationRecord().isFrame_only_constraint_flag()) { _general_constraint_indicator_flags |= 1l << 47; } if (hvcc.getHevcDecoderConfigurationRecord().isNon_packed_constraint_flag()) { _general_constraint_indicator_flags |= 1l << 46; } if (hvcc.getHevcDecoderConfigurationRecord().isInterlaced_source_flag()) { _general_constraint_indicator_flags |= 1l << 45; } if (hvcc.getHevcDecoderConfigurationRecord().isProgressive_source_flag()) { _general_constraint_indicator_flags |= 1l << 44; } codec += "." + hexByte(_general_constraint_indicator_flags >> 40); if ((_general_constraint_indicator_flags & 0xFFFFFFFFFFL) > 0) { codec += "." + hexByte(_general_constraint_indicator_flags >> 32); if ((_general_constraint_indicator_flags & 0xFFFFFFFFL) > 0) { codec += "." + hexByte(_general_constraint_indicator_flags >> 24); if ((_general_constraint_indicator_flags & 0xFFFFFFL) > 0) { codec += "." + hexByte(_general_constraint_indicator_flags >> 16); if (((_general_constraint_indicator_flags & 0xFFFFL)) > 0) { codec += "." + hexByte(_general_constraint_indicator_flags >> 8); if (((_general_constraint_indicator_flags & 0xFFL)) > 0) { codec += "." + hexByte(_general_constraint_indicator_flags); } } } } } return codec; } else if (type.equals("stpp")) { XMLSubtitleSampleEntry stpp = (XMLSubtitleSampleEntry) se; if (stpp.getSchemaLocation().contains("cff-tt-text-ttaf1-dfxp")) { return "cfft"; } else if (stpp.getSchemaLocation().contains("cff-tt-image-ttaf1-dfxp")) { return "cffi"; } else { return "stpp"; } } else { return null; } } static String hexByte(long l) { return Hex.encodeHex(new byte[]{(byte) (l & 0xFF)}); } public static String getFormat(Track track) { SampleDescriptionBox stsd = track.getSampleDescriptionBox(); OriginalFormatBox frma = Path.getPath(stsd, "enc./sinf/frma"); if (frma != null) { return frma.getDataFormat(); } else { if (stsd.getSampleEntry() != null) { return stsd.getSampleEntry().getType(); } else { return null; } } } public static class ChannelConfiguration { public String schemeIdUri = ""; public String value = ""; } public static Locale getTextTrackLocale(File textTrack) throws IOException { Pattern patternFilenameIncludesLanguage = Pattern.compile(".*[-_](.+)$"); String ext = FilenameUtils.getExtension(textTrack.getName()); String basename = FilenameUtils.getBaseName(textTrack.getName()); if (ext.equals("vtt")) { Matcher m = patternFilenameIncludesLanguage.matcher(basename); if (m.matches()) { return Locale.forLanguageTag(m.group(1)); } else { throw new IOException("Cannot determine language of " + textTrack + " please use the pattern filename-[language-tag].vtt"); } } else if (ext.equals("xml") || ext.equals("dfxp") || ext.equals("ttml")) { DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); try { DocumentBuilder builder = builderFactory.newDocumentBuilder(); String xml = FileUtils.readFileToString(textTrack); Document xmlDocument = builder.parse(new ByteArrayInputStream(xml.getBytes())); String lang = xmlDocument.getDocumentElement().getAttribute("xml:lang"); NodeList nl = xmlDocument.getDocumentElement().getElementsByTagName("div"); for (int i = 0; i < nl.getLength(); i++) { Attr langInDiv = (Attr) nl.item(i).getAttributes().getNamedItem("xml:lang"); if (langInDiv != null) { lang = langInDiv.getValue(); } } if (lang != null) { return Locale.forLanguageTag(lang); } else { Matcher m2 = patternFilenameIncludesLanguage.matcher(basename); if (m2.matches()) { return Locale.forLanguageTag(m2.group(1)); } else { throw new IOException("Cannot determine language of " + textTrack + " please use either the xml:lang attribute or a filename pattern like filename-[language-tag].[xml|dfxp]"); } } } catch (ParserConfigurationException e) { e.printStackTrace(); throw new IOException("Cannot instantiate XML parser to determine textTrack language"); } catch (SAXException e) { e.printStackTrace(); throw new IOException("Cannot parse XML to extract text track's language"); } } else { throw new IOException("Unknown subtitle format in " + textTrack); } } public static String filename2UrlPath(String filename) { URI uri; try { uri = new URI(null, null, null, -1, filename, null, null); } catch (URISyntaxException e) { throw new RuntimeException(e); } return uri.toASCIIString(); } }