package com.castlabs.dash.dashfragmenter.sequences; import com.castlabs.dash.dashfragmenter.BetterTrackGroupFragmenter; import com.castlabs.dash.dashfragmenter.FileAndTrackSelector; import com.castlabs.dash.dashfragmenter.formats.csf.DashBuilder; import com.castlabs.dash.dashfragmenter.formats.csf.SegmentBaseSingleSidxManifestWriterImpl; import com.castlabs.dash.dashfragmenter.formats.multiplefilessegmenttemplate.ExplodedSegmentListManifestWriterImpl; import com.castlabs.dash.dashfragmenter.formats.multiplefilessegmenttemplate.SingleSidxExplode; import com.castlabs.dash.dashfragmenter.representation.ManifestOptimizer; import com.castlabs.dash.dashfragmenter.representation.Mp4RepresentationBuilder; import com.castlabs.dash.dashfragmenter.representation.SyncSampleAssistedRepresentationBuilder; import com.castlabs.dash.dashfragmenter.tracks.NegativeCtsInsteadOfEdit; import com.castlabs.dash.helpers.BoxHelper; import com.castlabs.dash.helpers.DashHelper; import com.castlabs.dash.helpers.RepresentationBuilderToFile; import com.coremedia.iso.IsoFile; import com.coremedia.iso.boxes.*; import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; import com.googlecode.mp4parser.FileDataSourceViaHeapImpl; import com.googlecode.mp4parser.authoring.*; import com.googlecode.mp4parser.authoring.builder.BetterFragmenter; import com.googlecode.mp4parser.authoring.builder.Fragmenter; import com.googlecode.mp4parser.authoring.builder.StaticFragmentIntersectionFinderImpl; import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; import com.googlecode.mp4parser.authoring.tracks.*; import com.googlecode.mp4parser.authoring.tracks.h264.H264TrackImpl; import com.googlecode.mp4parser.authoring.tracks.ttml.TtmlHelpers; import com.googlecode.mp4parser.authoring.tracks.ttml.TtmlTrackImpl; import com.googlecode.mp4parser.authoring.tracks.webvtt.WebVttTrack; import com.googlecode.mp4parser.boxes.mp4.samplegrouping.CencSampleEncryptionInformationGroupEntry; import com.googlecode.mp4parser.util.Mp4Arrays; import com.googlecode.mp4parser.util.Path; import com.googlecode.mp4parser.util.UUIDConverter; import com.mp4parser.iso23001.part7.ProtectionSystemSpecificHeaderBox; import mpegCenc2013.DefaultKIDAttribute; import mpegDashSchemaMpd2011.*; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.SAXException; import javax.crypto.SecretKey; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPathExpressionException; import java.io.*; import java.net.URISyntaxException; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; import java.security.SecureRandom; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import static com.castlabs.dash.helpers.DashHelper.*; import static com.castlabs.dash.helpers.FileHelpers.isMp4; import static com.castlabs.dash.helpers.LanguageHelper.getFilesLanguage; import static com.castlabs.dash.helpers.ManifestHelper.getApproxTrackSize; import static com.castlabs.dash.helpers.ManifestHelper.getXmlOptions; /** * */ public class DashFileSetSequence { private static Logger LOG = Logger.getLogger(DashFileSetSequence.class.getName()); static Set<String> supportedTypes = new HashSet<>(Arrays.asList("ac-3", "ec-3", "dtsl", "dtsh", "dtse", "avc1", "avc3", "mp4a", "h264", "hev1", "hvc1")); protected UUID audioKeyid; protected SecretKey audioKey; protected UUID videoKeyid; protected SecretKey videoKey; protected Map<UUID, List<ProtectionSystemSpecificHeaderBox>> psshBoxes = new HashMap<UUID, List<ProtectionSystemSpecificHeaderBox>>(); protected List<FileAndTrackSelector> inputFilesOrig; protected List<FileAndTrackSelector> inputFiles; protected File outputDirectory = new File(System.getProperty("user.dir")); protected int sparse = 0; protected int clearlead = 0; protected String encryptionAlgo = "cenc"; protected boolean explode = false; protected String mediaPattern = "$RepresentationID$/media-$Time$.mp4"; protected String initPattern = "$RepresentationID$/init.mp4"; protected String mainLang = "eng"; protected boolean avc1ToAvc3 = false; // This is purely for debugging purposes and WILL weaken security when set to true protected boolean dummyIvs = false; protected boolean encryptButClear = false; protected int minAudioSegmentDuration = 15; protected int minVideoSegmentDuration = 4; protected List<File> subtitles; protected List<File> closedCaptions; protected List<File> trickModeFiles; protected Map<String, String> languageMap = new HashMap<>(); DocumentBuilder documentBuilder; public DashFileSetSequence() { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); //dbf.setNamespaceAware(true); try { documentBuilder = dbf.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new RuntimeException(e); } } public void setPsshBoxes(Map<UUID, List<ProtectionSystemSpecificHeaderBox>> psshBoxes) { this.psshBoxes = psshBoxes; } public void setSubtitles(List<File> subtitles) { this.subtitles = subtitles; } public void setClosedCaptions(List<File> closedCaptions) { this.closedCaptions = closedCaptions; } public void setLanguageMap(Map<String, String> languageMap) { this.languageMap = languageMap; } /** * Sets the minimum audio segment duration. * * @param minAudioSegmentDuration shortest allowable audio segment duration */ public void setMinAudioSegmentDuration(int minAudioSegmentDuration) { this.minAudioSegmentDuration = minAudioSegmentDuration; } /** * Sets the minimum video segment duration. * * @param minVideoSegmentDuration shortest allowable video segment duration */ public void setMinVideoSegmentDuration(int minVideoSegmentDuration) { this.minVideoSegmentDuration = minVideoSegmentDuration; } /** * Turns off the random number generator for IVs and therefore they start at 0x0000000000000000. * * @param dummyIvs <code>true</code> turns off the RNG */ public void setDummyIvs(boolean dummyIvs) { this.dummyIvs = dummyIvs; } /** * Sets the mediaPattern for 'exploded' mode and defines under which name to store the segments. * <p> * This option has no effect when <code>explode==false</code> * * @param mediaPattern under which name to store the segments * @see #setExplode(boolean) */ public void setMediaPattern(String mediaPattern) { this.mediaPattern = mediaPattern; } /** * Sets the initPattern for 'exploded' mode and defines under which name the init segment is stored. * <p> * This option has no effect when <code>explode==false</code> * * @param initPattern under which name the init segment is stored * @see #setExplode(boolean) */ public void setInitPattern(String initPattern) { this.initPattern = initPattern; } /** * The dash.encrypter will convert AVC1 tracks to AVC3 tracks when flag is set. * * @param avc1ToAvc3 triggers avc1 to avc3 conversion */ public void setAvc1ToAvc3(boolean avc1ToAvc3) { this.avc1ToAvc3 = avc1ToAvc3; } public void setEncryptionAlgo(String encryptionAlgo) { this.encryptionAlgo = encryptionAlgo; } public void setAudioKey(SecretKey key) { this.audioKey = key; } public void setVideoKey(SecretKey key) { this.videoKey = key; } public void setAudioKeyid(UUID keyid) { this.audioKeyid = keyid; } public void setVideoKeyid(UUID keyid) { this.videoKeyid = keyid; } public void setInputFiles(List<FileAndTrackSelector> inputFiles) { this.inputFiles = inputFiles; } public void setOutputDirectory(File outputDirectory) { this.outputDirectory = outputDirectory; } public void setExplode(boolean explode) { this.explode = explode; } public void setSparse(int sparse) { this.sparse = sparse; } public void setClearlead(int clearlead) { this.clearlead = clearlead; } public int run() { try { if (outputDirectory.getAbsoluteFile().exists() == outputDirectory.getAbsoluteFile().mkdirs()) { LOG.severe("Output directory does not exist and cannot be created."); return 9745; } long start = System.currentTimeMillis(); long totalSize = 0; for (FileAndTrackSelector inputFile : inputFiles) { totalSize += inputFile.file.length(); } for (File inputFile : safe(closedCaptions)) { totalSize += inputFile.length(); } for (File inputFile : safe(subtitles)) { totalSize += inputFile.length(); } for (File inputFile : safe(trickModeFiles)) { totalSize += inputFile.length(); } Map<TrackProxy, String> track2File = createTracks(); checkUnhandledFile(); alignEditsToZero(track2File); fixAppleOddity(track2File); useNegativeCtsToPreventEdits(track2File); Map<TrackProxy, UUID> track2KeyId = assignKeyIds(track2File); Map<UUID, SecretKey> keyId2Key = createKeyMap(track2KeyId); encryptTracks(track2File, track2KeyId, keyId2Key); // sort by language and codec Map<String, List<TrackProxy>> adaptationSets = findAdaptationSets(track2File.keySet()); Map<String, String> adaptationSet2Role = setAdaptionSetRoles(adaptationSets); // Track sizes are expensive to calculate -> save them for later Map<TrackProxy, Long> trackSizes = calculateTrackSizes(adaptationSets); // sort within the track families by size to get stable output sortTrackFamilies(adaptationSets, trackSizes); // calculate the fragment start samples once & save them for later Map<TrackProxy, long[]> track2SegmentStartSamples = findFragmentStartSamples(adaptationSets); // calculate bitrates Map<TrackProxy, Long> trackBitrate = calculateBitrate(adaptationSets, trackSizes); // generate filenames for later reference Map<TrackProxy, String> trackFilename = generateFilenames(track2File); // export the dashed single track MP4s Map<TrackProxy, Container> track2CsfStructure = createSingleTrackDashedMp4s(track2SegmentStartSamples, trackFilename); Map<TrackProxy, List<File>> trackToFileRepresentation = writeFiles(trackFilename, track2CsfStructure, trackBitrate); generateManifest( adaptationSets, trackBitrate, trackFilename, track2CsfStructure, trackToFileRepresentation, adaptationSet2Role); LOG.info(String.format("Finished fragmenting of %dMB in %.1fs", totalSize / 1024 / 1024, (double) (System.currentTimeMillis() - start) / 1000)); for (TrackProxy trackProxy : trackToFileRepresentation.keySet()) { trackProxy.close(); } return 0; } catch (ExitCodeException e) { LOG.severe(e.getMessage()); return e.getExitCode(); } catch (IOException e) { LOG.log(Level.SEVERE, e.getMessage(), e); return 9015; } } protected void generateManifest(Map<String, List<TrackProxy>> adaptationSets, Map<TrackProxy, Long> trackBitrate, Map<TrackProxy, String> trackFilename, Map<TrackProxy, Container> track2CsfStructure, Map<TrackProxy, List<File>> trackToFileRepresentation, Map<String, String> adaptationSet2Role) throws IOException, ExitCodeException { MPDDocument manifest = createManifest( adaptationSets, trackBitrate, trackFilename, track2CsfStructure, trackToFileRepresentation, adaptationSet2Role); writeManifest(manifest); } protected Map<String, String> setAdaptionSetRoles(Map<String, List<TrackProxy>> adaptationSets) { Map<String, String> roles = new HashMap<String, String>(); for (Map.Entry<String, List<TrackProxy>> stringListEntry : adaptationSets.entrySet()) { if (stringListEntry.getValue().get(0).getHandler().contains("vide")) { roles.put(stringListEntry.getKey(), "urn:mpeg:dash:role:2011|main"); } else if (stringListEntry.getValue().get(0).getHandler().contains("soun")) { String lang = stringListEntry.getValue().get(0).getTrackMetaData().getLanguage(); if (lang.equals(mainLang) || lang.equals("und")) { roles.put(stringListEntry.getKey(), "urn:mpeg:dash:role:2011|main"); } else { roles.put(stringListEntry.getKey(), "urn:mpeg:dash:role:2011|dub"); } } } return roles; } private void useNegativeCtsToPreventEdits(Map<TrackProxy, String> track2File) { for (Map.Entry<TrackProxy, String> entry : track2File.entrySet()) { TrackProxy track = entry.getKey(); if (NegativeCtsInsteadOfEdit.benefitsFromChange(track.getTarget())) { track.setTarget(new NegativeCtsInsteadOfEdit(track.getTarget())); } } } public Map<UUID, SecretKey> createKeyMap(Map<TrackProxy, UUID> track2KeyId) { Map<UUID, SecretKey> keyIds = new HashMap<UUID, SecretKey>(); keyIds.put(audioKeyid, audioKey); keyIds.put(videoKeyid, videoKey); return keyIds; } public Map<TrackProxy, UUID> assignKeyIds(Map<TrackProxy, String> track2File) { Map<TrackProxy, UUID> keyIds = new HashMap<TrackProxy, UUID>(); for (TrackProxy track : track2File.keySet()) { if (track.getHandler().equals("soun") && audioKeyid != null) { keyIds.put(track, audioKeyid); } else if (track.getHandler().equals("vide") && videoKeyid != null) { keyIds.put(track, videoKeyid); } } return keyIds; } public Map<TrackProxy, List<File>> writeFiles( Map<TrackProxy, String> trackFilename, Map<TrackProxy, Container> dashedFiles, Map<TrackProxy, Long> trackBitrate) throws IOException { if (!explode) { return writeFilesSingleSidx(trackFilename, dashedFiles); } else { return writeFilesExploded(trackFilename, dashedFiles, trackBitrate, outputDirectory, initPattern, mediaPattern); } } public MPDDocument createManifest(Map<String, List<TrackProxy>> trackFamilies, Map<TrackProxy, Long> trackBitrate, Map<TrackProxy, String> representationIds, Map<TrackProxy, Container> dashedFiles, Map<TrackProxy, List<File>> trackToFile, Map<String, String> adaptationSet2Role) throws IOException, ExitCodeException { MPDDocument mpdDocument; if (!explode) { mpdDocument = getManifestSingleSidx(trackFamilies, trackBitrate, representationIds, dashedFiles, adaptationSet2Role); } else { mpdDocument = getManifestSegmentList(trackFamilies, trackBitrate, representationIds, dashedFiles, trackToFile, adaptationSet2Role); } DescriptorType subtitleRole = DescriptorType.Factory.newInstance(); subtitleRole.setSchemeIdUri("urn:mpeg:dash:role"); subtitleRole.setValue("subtitle"); addTextTracks(mpdDocument, subtitles, new DescriptorType[]{subtitleRole}, new DescriptorType[0]); DescriptorType captionRole = DescriptorType.Factory.newInstance(); captionRole.setSchemeIdUri("urn:mpeg:dash:role"); captionRole.setValue("caption"); addTextTracks(mpdDocument, closedCaptions, new DescriptorType[]{captionRole}, new DescriptorType[0]); addTrickModeTracks(mpdDocument); ManifestOptimizer.optimize(mpdDocument); return mpdDocument; } private void addTrickModeTracks(MPDDocument mpdDocument) throws IOException { List<Mp4RepresentationBuilder> trickModeRepresentations = new ArrayList<Mp4RepresentationBuilder>(); for (File trickModeFile : safe(trickModeFiles)) { if (isMp4(trickModeFile)) { Movie movie = MovieCreator.build(new FileDataSourceViaHeapImpl(trickModeFile)); for (Track track : movie.getTracks()) { if ("vide".equals(track.getHandler())) { if (videoKeyid != null) { track = new CencEncryptingTrackImpl(track, videoKeyid, videoKey, false); } trickModeRepresentations.add(new SyncSampleAssistedRepresentationBuilder(track, trickModeFile.getName(), time2Frames(track, 3), psshBoxes.get(videoKeyid))); } else { LOG.warning("Excluding " + trickModeFile + " track " + track.getTrackMetaData().getTrackId() + " as it's not a video track"); } } } else if (trickModeFile.getName().endsWith(".h264") || trickModeFile.getName().endsWith(".264")) { Track track = new H264TrackImpl(new FileDataSourceViaHeapImpl(trickModeFile)); if (videoKeyid != null) { track = new CencEncryptingTrackImpl(track, videoKeyid, videoKey, false); } trickModeRepresentations.add(new SyncSampleAssistedRepresentationBuilder(track, trickModeFile.getName(), time2Frames(track, 3), psshBoxes.get(videoKeyid))); } else { throw new IOException("Trick Mode file " + trickModeFile + " is not a valid trick mode file. Expecting *.[h]264 or *.mp4"); } } if (!trickModeRepresentations.isEmpty()) { LOG.info("Creating Trick Mode AdaptationSet"); AdaptationSetType adaptationSet = mpdDocument.getMPD().getPeriodArray(0).addNewAdaptationSet(); DescriptorType essentialProperty = adaptationSet.addNewEssentialProperty(); essentialProperty.setSchemeIdUri("http://dashif.org/guide-lines/trickmode"); essentialProperty.setValue("1"); // an AdaptationSet built with ExplodedSegmentListManifestWriter or SegmentBaseSingleSidxManifestWriterImpl always has id "1" if it's a video. ArrayList<RepresentationType> representations = new ArrayList<RepresentationType>(); for (Mp4RepresentationBuilder mp4RepresentationBuilder : trickModeRepresentations) { LOG.fine("Creating Trick Mode Representation for " + mp4RepresentationBuilder.getSource()); double seconds = (double) mp4RepresentationBuilder.getTrack().getDuration() / mp4RepresentationBuilder.getTrack().getTrackMetaData().getTimescale(); // todo find actual main video FPS - will happen when long maxPlayoutRate = Math.round((double) 25 / ((double) mp4RepresentationBuilder.getTrack().getSamples().size() / seconds)); RepresentationType representation = writeDataAndCreateRepresentation(mp4RepresentationBuilder, Locale.forLanguageTag(mp4RepresentationBuilder.getTrack().getTrackMetaData().getLanguage())); representation.setMaxPlayoutRate(maxPlayoutRate); representations.add(representation); } adaptationSet.setRepresentationArray(representations.toArray(new RepresentationType[representations.size()])); LOG.info("Trick Mode AdaptationSet: Done."); } } RepresentationType writeDataAndCreateRepresentation(Mp4RepresentationBuilder mp4RepresentationBuilder, Locale locale) throws IOException { RepresentationType representation; String id = filename2UrlPath((mp4RepresentationBuilder.getSource())); if (explode) { representation = mp4RepresentationBuilder.getLiveRepresentation(); representation.getSegmentTemplate().setInitialization2(initPattern.replace("%lang%", locale.toLanguageTag())); representation.getSegmentTemplate().setMedia(mediaPattern.replace("%lang%", locale.toLanguageTag())); representation.setId(id); RepresentationBuilderToFile.writeLive(mp4RepresentationBuilder, representation, outputDirectory); } else { representation = mp4RepresentationBuilder.getOnDemandRepresentation(); representation.addNewBaseURL().setStringValue(id + ".mp4"); representation.setId(id); RepresentationBuilderToFile.writeOnDemand(mp4RepresentationBuilder, representation, outputDirectory); } return representation; } public <T> Map<Track, T> t(Map<TrackProxy, T> mapIn) { Map<Track, T> mapOut = new HashMap<Track, T>(); for (Map.Entry<TrackProxy, T> trackProxyTEntry : mapIn.entrySet()) { mapOut.put(trackProxyTEntry.getKey().getTarget(), trackProxyTEntry.getValue()); } return mapOut; } public List<Track> t(List<TrackProxy> listIn) { List<Track> listOut = new ArrayList<Track>(); for (TrackProxy trackProxy : listIn) { listOut.add(trackProxy.getTarget()); } return listOut; } public <T> Map<T, List<Track>> tt(Map<T, List<TrackProxy>> mapIn) { Map<T, List<Track>> mapOut; if (mapIn instanceof LinkedHashMap) { mapOut = new LinkedHashMap<T, List<Track>>(); } else { mapOut = new HashMap<T, List<Track>>(); } for (Map.Entry<T, List<TrackProxy>> tListEntry : mapIn.entrySet()) { mapOut.put(tListEntry.getKey(), t(tListEntry.getValue())); } return mapOut; } protected MPDDocument getManifestSegmentList( Map<String, List<TrackProxy>> trackFamilies, Map<TrackProxy, Long> trackBitrate, Map<TrackProxy, String> representationIds, Map<TrackProxy, Container> dashedFiles, Map<TrackProxy, List<File>> trackToFile, Map<String, String> adaptationSet2Role) throws IOException { return new ExplodedSegmentListManifestWriterImpl(this, tt(trackFamilies), t(dashedFiles), t(trackBitrate), t(representationIds), t(trackToFile), initPattern, mediaPattern, true, adaptationSet2Role).getManifest(); } protected MPDDocument getManifestSingleSidx( Map<String, List<TrackProxy>> trackFamilies, Map<TrackProxy, Long> trackBitrate, Map<TrackProxy, String> representationIds, Map<TrackProxy, Container> dashedFiles, Map<String, String> adaptationSet2Role) throws IOException { return new SegmentBaseSingleSidxManifestWriterImpl(this, tt(trackFamilies), t(dashedFiles), t(trackBitrate), t(representationIds), true, adaptationSet2Role).getManifest(); } public <E> Collection<E> safe(Collection<E> c) { if (c == null) { return Collections.emptySet(); } else { return c; } } public void addTextTracks(MPDDocument mpdDocument, List<File> textTracks, DescriptorType[] roles, DescriptorType[] essentialProperties) throws IOException { for (File textTrack : safe(textTracks)) { addRawTextTrack(mpdDocument, textTrack, roles, essentialProperties); addMuxedTextTrack(mpdDocument, textTrack, roles, essentialProperties); } } private void addMuxedTextTrack(MPDDocument mpdDocument, File textTrackFile, DescriptorType[] roles, DescriptorType[] essentialProperties) throws IOException { LOG.info("Creating Muxed Text Track AdaptationSet for " + textTrackFile.getName()); PeriodType period = mpdDocument.getMPD().getPeriodArray()[0]; Track textTrack; if (textTrackFile.getName().endsWith(".xml") || textTrackFile.getName().endsWith(".dfxp") || textTrackFile.getName().endsWith(".ttml")) { try { textTrack = new TtmlTrackImpl(textTrackFile.getName() + "-mp4", Collections.singletonList(documentBuilder.parse(textTrackFile))); } catch (SAXException | ParserConfigurationException | URISyntaxException e) { throw new IOException(e); } catch (XPathExpressionException e) { throw new IOException(e); } } else if (textTrackFile.getName().endsWith(".vtt")) { textTrack = new WebVttTrack(new FileInputStream(textTrackFile), textTrackFile.getName(), getTextTrackLocale(textTrackFile)); } else { throw new RuntimeException("Not sure what kind of textTrack " + textTrackFile.getName() + " is."); } Locale locale = getTextTrackLocale(textTrackFile); Mp4RepresentationBuilder mp4RepresentationBuilder = new SyncSampleAssistedRepresentationBuilder(textTrack, textTrackFile.getName(), 10, Collections.<ProtectionSystemSpecificHeaderBox>emptyList()); RepresentationType representation = writeDataAndCreateRepresentation(mp4RepresentationBuilder, locale); AdaptationSetType adaptationSet = period.addNewAdaptationSet(); adaptationSet.setLang(locale.getLanguage() + ("".equals(locale.getScript()) ? "" : "-" + locale.getScript())); adaptationSet.setMimeType("application/mp4"); adaptationSet.setRepresentationArray(new RepresentationType[]{representation}); adaptationSet.setRoleArray(roles); adaptationSet.setEssentialPropertyArray(essentialProperties); LOG.info("Muxed Text Track AdaptationSet: Done."); } public void addRawTextTrack(MPDDocument mpdDocument, File textTrack, DescriptorType[] roles, DescriptorType[] essentialProperties) throws IOException { LOG.info("Creating Raw Text Track AdaptationSet for " + textTrack.getName()); PeriodType period = mpdDocument.getMPD().getPeriodArray()[0]; AdaptationSetType adaptationSet = period.addNewAdaptationSet(); File tracksOutputDir = new File(outputDirectory, FilenameUtils.getBaseName(textTrack.getName())); if (tracksOutputDir.getAbsoluteFile().exists() == tracksOutputDir.getAbsoluteFile().mkdirs()) { LOG.severe("Track's output directory does not exist and cannot be created (" + tracksOutputDir.getAbsolutePath() + ")"); } if (textTrack.getName().endsWith(".xml") || textTrack.getName().endsWith(".dfxp") || textTrack.getName().endsWith(".ttml")) { if (textTrack.getName().endsWith(".dfxp")) { adaptationSet.setMimeType("application/ttaf+xml"); } else { adaptationSet.setMimeType("application/ttml+xml"); } try { TtmlHelpers.deepCopyDocument(documentBuilder.parse(textTrack), new File(tracksOutputDir, textTrack.getName())); } catch (SAXException e) { throw new IOException(e); } } else if (textTrack.getName().endsWith(".vtt")) { adaptationSet.setMimeType("text/vtt"); FileUtils.copyFileToDirectory(textTrack, tracksOutputDir); } else { throw new RuntimeException("Not sure what kind of textTrack " + textTrack.getName() + " is."); } Locale locale = getTextTrackLocale(textTrack); adaptationSet.setLang(locale.getLanguage() + ("".equals(locale.getScript()) ? "" : "-" + locale.getScript())); adaptationSet.setRoleArray(roles); adaptationSet.setEssentialPropertyArray(essentialProperties); RepresentationType representation = adaptationSet.addNewRepresentation(); representation.setId(filename2UrlPath(FilenameUtils.getBaseName(textTrack.getName())+ "-" +FilenameUtils.getExtension(textTrack.getName()))); representation.setBandwidth(0); // pointless - just invent a small number BaseURLType baseURL = representation.addNewBaseURL(); baseURL.setStringValue(FilenameUtils.getBaseName(textTrack.getName()) + "/" + textTrack.getName()); LOG.info("Raw Text Track AdaptationSet: Done."); } public void writeManifest(MPDDocument mpdDocument) throws IOException, ExitCodeException { File manifest1 = new File(outputDirectory, "Manifest.mpd"); LOG.info("Writing " + manifest1); mpdDocument.save(manifest1, getXmlOptions()); //LOG.info("Done."); } private void checkUnhandledFile() throws ExitCodeException { for (FileAndTrackSelector inputFile : inputFiles) { LOG.severe("Cannot identify type of " + inputFile.file); } if (inputFiles.size() > 0) { throw new ExitCodeException("Only extensions mp4, ismv, mov, m4v, aac, ac3, ec3, dtshd are known.", 1); } } public Map<TrackProxy, List<File>> writeFilesExploded( Map<TrackProxy, String> trackFilename, Map<TrackProxy, Container> dashedFiles, Map<TrackProxy, Long> trackBitrate, File outputDirectory, String initPattern, String mediaPattern) throws IOException { Map<TrackProxy, List<File>> trackToSegments = new HashMap<TrackProxy, List<File>>(); for (TrackProxy t : trackFilename.keySet()) { SingleSidxExplode singleSidxExplode = new SingleSidxExplode(LOG); singleSidxExplode.setGenerateStypSdix(false); List<File> segments = singleSidxExplode.doIt( dashedFiles.get(t), trackFilename.get(t), trackBitrate.get(t), outputDirectory, initPattern, mediaPattern); //LOG.info("Done."); trackToSegments.put(t, segments); } return trackToSegments; } public Map<TrackProxy, List<File>> writeFilesSingleSidx(Map<TrackProxy, String> trackFilename, Map<TrackProxy, Container> dashedFiles) throws IOException { Map<TrackProxy, List<File>> track2Files = new HashMap<TrackProxy, List<File>>(); for (Map.Entry<TrackProxy, Container> trackContainerEntry : dashedFiles.entrySet()) { File f = new File(outputDirectory, trackFilename.get(trackContainerEntry.getKey())); if (f.exists()) { for (FileAndTrackSelector file : inputFilesOrig) { if (file.file.getAbsolutePath().equals(f.getAbsolutePath())) { throw new IOException("Please choose another output directory as current setting causes input files to be overwritten"); } } } } for (Map.Entry<TrackProxy, Container> trackContainerEntry : dashedFiles.entrySet()) { TrackProxy t = trackContainerEntry.getKey(); File f = new File(outputDirectory, trackFilename.get(t)); LOG.info("Writing " + f.getAbsolutePath()); WritableByteChannel wbc = new FileOutputStream(f).getChannel(); try { List<Box> boxes = trackContainerEntry.getValue().getBoxes(); for (int i = 0; i < boxes.size(); i++) { LOG.finest("Writing... " + boxes.get(i).getType() + " [" + i + " of " + boxes.size() + "]"); boxes.get(i).getBox(wbc); } } finally { wbc.close(); } //LOG.info("Done."); track2Files.put(t, Collections.singletonList(f)); } return track2Files; } public DashBuilder getFileBuilder(Fragmenter fragmenter, Movie m) { DashBuilder dashBuilder = new DashBuilder(); dashBuilder.setFragmenter(fragmenter); return dashBuilder; } public Map<TrackProxy, Container> createSingleTrackDashedMp4s( Map<TrackProxy, long[]> fragmentStartSamples, Map<TrackProxy, String> filenames) throws IOException { HashMap<TrackProxy, Container> containers = new HashMap<TrackProxy, Container>(); Map<Track, long[]> sampleNumbers = new HashMap<Track, long[]>(); for (Map.Entry<TrackProxy, long[]> entry : fragmentStartSamples.entrySet()) { sampleNumbers.put(entry.getKey().getTarget(), entry.getValue()); } for (final Map.Entry<TrackProxy, long[]> trackEntry : fragmentStartSamples.entrySet()) { String filename = filenames.get(trackEntry.getKey()); Movie movie = new Movie(); movie.addTrack(trackEntry.getKey().getTarget()); LOG.info("Creating model for " + filename + "... "); DashBuilder mp4Builder = getFileBuilder( new StaticFragmentIntersectionFinderImpl(sampleNumbers), movie); Container isoFile = mp4Builder.build(movie); containers.put(trackEntry.getKey(), isoFile); } return containers; } public void sortTrackFamilies(Map<String, List<TrackProxy>> trackFamilies, final Map<TrackProxy, Long> sizes) { for (List<TrackProxy> tracks : trackFamilies.values()) { Collections.sort(tracks, new Comparator<TrackProxy>() { public int compare(TrackProxy o1, TrackProxy o2) { return (int) (sizes.get(o1) - sizes.get(o2)); } }); } } /** * Calculates approximate track size suitable for sorting & calculating bitrate but not suitable * for precise calculations. * * @param trackFamilies all tracks grouped by their type. * @return map from track to track's size */ public Map<TrackProxy, Long> calculateTrackSizes(Map<String, List<TrackProxy>> trackFamilies) { HashMap<TrackProxy, Long> sizes = new HashMap<TrackProxy, Long>(); for (List<TrackProxy> tracks : trackFamilies.values()) { for (TrackProxy track : tracks) { sizes.put(track, getApproxTrackSize(track.getTarget())); } } return sizes; } /** * Calculates bitrate from sizes. * * @param trackFamilies all tracks grouped by their type. * @param trackSize size per track * @return bitrate per track */ public Map<TrackProxy, Long> calculateBitrate(Map<String, List<TrackProxy>> trackFamilies, Map<TrackProxy, Long> trackSize) { HashMap<TrackProxy, Long> bitrates = new HashMap<TrackProxy, Long>(); for (List<TrackProxy> tracks : trackFamilies.values()) { for (TrackProxy track : tracks) { double duration = (double) track.getDuration() / track.getTrackMetaData().getTimescale(); long size = trackSize.get(track); long bitrate = (long) ((size * 8 / duration / 100)) * 100; bitrates.put(track, bitrate); } } return bitrates; } /** * Generates filenames from type, language and bitrate. * * @return a descriptive filename <code>type[-lang]-bitrate.mp4</code> */ public Map<TrackProxy, String> generateFilenames(Map<TrackProxy, String> trackOriginalFilename) { HashMap<TrackProxy, String> filenames = new HashMap<TrackProxy, String>(); for (TrackProxy track : trackOriginalFilename.keySet()) { String originalFilename = trackOriginalFilename.get(track); originalFilename = originalFilename.replaceAll(".mov$", ""); originalFilename = originalFilename.replaceAll(".aac$", ""); originalFilename = originalFilename.replaceAll(".ec3$", ""); originalFilename = originalFilename.replaceAll(".ac3$", ""); originalFilename = originalFilename.replaceAll(".dtshd$", ""); originalFilename = originalFilename.replaceAll(".mp4$", ""); for (TrackProxy track1 : filenames.keySet()) { if (track1 != track && trackOriginalFilename.get(track1).equals(trackOriginalFilename.get(track))) { // ouch multiple tracks point to same file originalFilename += "_" + track.getTrackMetaData().getTrackId(); } } if (!explode) { filenames.put(track, String.format("%s.mp4", originalFilename)); } else { filenames.put(track, originalFilename); } } return filenames; } public Map<TrackProxy, long[]> findFragmentStartSamples(Map<String, List<TrackProxy>> trackFamilies) { Map<TrackProxy, long[]> fragmentStartSamples = new HashMap<>(); Map<String, List<TrackProxy>> trackFamiliesForSegments = new HashMap<>(); for (Map.Entry<String, List<TrackProxy>> stringListEntry : trackFamilies.entrySet()) { String shortFamily = stringListEntry.getKey().substring(0, 4); List<TrackProxy> tps = trackFamiliesForSegments.get(shortFamily); if (tps == null) { tps = new ArrayList<>(); trackFamiliesForSegments.put(shortFamily, tps); } tps.addAll(stringListEntry.getValue()); } for (String key : trackFamiliesForSegments.keySet()) { List<TrackProxy> trackProxies = trackFamiliesForSegments.get(key); if (trackProxies.get(0).getHandler().startsWith("vide")) { List<Track> tracks = new ArrayList<>(); for (TrackProxy trackProxy : trackProxies) { tracks.add(trackProxy.getTarget()); } Fragmenter fragmenter = new BetterTrackGroupFragmenter(minVideoSegmentDuration, tracks); for (TrackProxy track : trackProxies) { fragmentStartSamples.put(track, fragmenter.sampleNumbers(track.getTarget())); } } else if (trackProxies.get(0).getHandler().startsWith("soun")) { long[] commonSamples = null; for (TrackProxy track : trackProxies) { Fragmenter soundIntersectionFinder = new BetterFragmenter(minAudioSegmentDuration); long[] nuSamples = soundIntersectionFinder.sampleNumbers(track.getTarget()); if (commonSamples == null) { commonSamples = nuSamples; } else { commonSamples = getCommonIndices(commonSamples, nuSamples); } } for (TrackProxy track : trackProxies) { fragmentStartSamples.put(track, commonSamples); } } else { throw new RuntimeException("An engineer needs to tell me if " + trackProxies.get(0).getHandler() + " is audio or video!"); } } return fragmentStartSamples; } public static long[] getCommonIndices(long[] samples1, long[] samples2) { if (Arrays.equals(samples1, samples2)) { return samples1; } else { int i1 = 0, i2 = 0; long[] result = new long[0]; while (i1 < samples1.length && i2 < samples2.length) { if (samples1[i1] == samples2[i2]) { result = Mp4Arrays.copyOfAndAppend(result, samples1[i1]); i1++; i2++; } else if (samples1[i1] < samples2[i2]) { i1++; } else { i2++; } } return result; } } /** * Creates a Map with Track as key and originating filename as value. * * @return Track too originating file map * @throws IOException */ public Map<TrackProxy, String> createTracks() throws IOException, ExitCodeException { List<FileAndTrackSelector> unhandled = new ArrayList<>(); inputFilesOrig = new ArrayList<>(inputFiles); Map<TrackProxy, String> track2File = new LinkedHashMap<>(); for (FileAndTrackSelector fileAndTrack : inputFiles) { if (fileAndTrack.file.isFile()) { if (isMp4(fileAndTrack.file)) { IsoFile isoFile = new IsoFile(new FileDataSourceViaHeapImpl(fileAndTrack.file)); for (TrackBox trackBox : isoFile.getMovieBox().getBoxes(TrackBox.class)) { if (!fileAndTrack.isSelected(trackBox)) { LOG.info("Excluding " + fileAndTrack.file + " track " + trackBox.getTrackHeaderBox().getTrackId() + " as it is not selected by the track selectors."); continue; } SchemeTypeBox schm = Path.getPath(trackBox, "mdia[0]/minf[0]/stbl[0]/stsd[0]/enc.[0]/sinf[0]/schm[0]"); if (schm != null && (schm.getSchemeType().equals("cenc") || schm.getSchemeType().equals("cbc1"))) { LOG.warning("Excluding " + fileAndTrack.file + " track " + trackBox.getTrackHeaderBox().getTrackId() + " as it is encrypted. Encrypted source tracks are not yet supported"); continue; } Track track = new Mp4TrackImpl(fileAndTrack.file + "[" + trackBox.getTrackHeaderBox().getTrackId() + "]", trackBox); String codec = DashHelper.getFormat(track); if (!supportedTypes.contains(codec)) { LOG.warning("Excluding " + fileAndTrack.file + " track " + track.getTrackMetaData().getTrackId() + " as its codec " + codec + " is not yet supported"); continue; } track2File.put(new TrackProxy(track), fileAndTrack.file.getName()); } } else if (fileAndTrack.file.getName().endsWith(".aac")) { Track track = new AACTrackImpl(new FileDataSourceViaHeapImpl(fileAndTrack.file)); track.getTrackMetaData().setLanguage(getFilesLanguage(fileAndTrack.file).getISO3Language()); track2File.put(new TrackProxy(track), fileAndTrack.file.getName()); assertOptionEmpty(fileAndTrack); LOG.fine("Created AAC Track from " + fileAndTrack.file.getName()); } else if (fileAndTrack.file.getName().endsWith(".h264")) { Track track = new H264TrackImpl(new FileDataSourceViaHeapImpl(fileAndTrack.file)); track2File.put(new TrackProxy(track), fileAndTrack.file.getName()); assertOptionEmpty(fileAndTrack); LOG.fine("Created H264 Track from " + fileAndTrack.file.getName()); } else if (fileAndTrack.file.getName().endsWith(".ac3")) { Track track = new AC3TrackImpl(new FileDataSourceViaHeapImpl(fileAndTrack.file)); track.getTrackMetaData().setLanguage(getFilesLanguage(fileAndTrack.file).getISO3Language()); track2File.put(new TrackProxy(track), fileAndTrack.file.getName()); assertOptionEmpty(fileAndTrack); LOG.fine("Created AC3 Track from " + fileAndTrack.file.getName()); } else if (fileAndTrack.file.getName().endsWith(".ec3")) { Track track = new EC3TrackImpl(new FileDataSourceViaHeapImpl(fileAndTrack.file)); track.getTrackMetaData().setLanguage(getFilesLanguage(fileAndTrack.file).getISO3Language()); track2File.put(new TrackProxy(track), fileAndTrack.file.getName()); assertOptionEmpty(fileAndTrack); LOG.fine("Created EC3 Track from " + fileAndTrack.file.getName()); } else if (fileAndTrack.file.getName().endsWith(".dtshd")) { Track track = new DTSTrackImpl(new FileDataSourceViaHeapImpl(fileAndTrack.file)); track.getTrackMetaData().setLanguage(getFilesLanguage(fileAndTrack.file).getISO3Language()); track2File.put(new TrackProxy(track), fileAndTrack.file.getName()); assertOptionEmpty(fileAndTrack); LOG.fine("Created DTS HD Track from " + fileAndTrack.file.getName()); } else { unhandled.add(fileAndTrack); } } } for (TrackProxy trackProxy : track2File.keySet()) { String newLang = languageMap.get(trackProxy.getTarget().getTrackMetaData().getLanguage()); if (newLang != null) { LOG.info("Replacing input language " + trackProxy.getTarget().getTrackMetaData().getLanguage() + " in " + trackProxy + " with " + newLang); trackProxy.getTarget().getTrackMetaData().setLanguage(newLang); } } inputFiles.retainAll(unhandled); if (avc1ToAvc3) { for (Map.Entry<TrackProxy, String> trackStringEntry : track2File.entrySet()) { if ("avc1".equals(trackStringEntry.getKey().getSampleDescriptionBox().getSampleEntry().getType())) { trackStringEntry.getKey().setTarget(new Avc1ToAvc3TrackImpl(trackStringEntry.getKey().getTarget())); } } } if (track2File.isEmpty()) { throw new ExitCodeException("No tracks found for creating DASH stream.", 9283); } return track2File; } private void assertOptionEmpty(FileAndTrackSelector fileAndTrack) throws ExitCodeException { if (fileAndTrack.language != null || fileAndTrack.type != null || fileAndTrack.trackId >= 0) { throw new ExitCodeException(fileAndTrack + " references a bitstream file but contains track selectors (" + fileAndTrack + ")", 237126); } } long[] longSet2Array(Set<Long> longSet) { long[] r = new long[longSet.size()]; List<Long> longList = new ArrayList<Long>(longSet); for (int i = 0; i < r.length; i++) { r[i] = longList.get(i); } Arrays.sort(r); return r; } public void encryptTracks(Map<TrackProxy, String> track2File, Map<TrackProxy, UUID> track2KeyId, Map<UUID, SecretKey> keyId2Key) { for (Map.Entry<TrackProxy, String> trackStringEntry : track2File.entrySet()) { if (track2KeyId.containsKey(trackStringEntry.getKey())) { TrackProxy t = trackStringEntry.getKey(); UUID keyid = track2KeyId.get(t); SecretKey key = keyId2Key.get(keyid); int clearTillSample = 0; int numSamples = t.getSamples().size(); if (clearlead > 0) { clearTillSample = (int) (clearlead * numSamples / (t.getDuration() / t.getTrackMetaData().getTimescale())); } if (sparse == 0) { CencEncryptingTrackImpl cencTrack; if (clearTillSample > 0) { CencSampleEncryptionInformationGroupEntry e = new CencSampleEncryptionInformationGroupEntry(); e.setEncrypted(false); long[] excludes = new long[clearTillSample]; for (int i = 0; i < excludes.length; i++) { excludes[i] = i; } cencTrack = new CencEncryptingTrackImpl( t.getTarget(), keyid, Collections.singletonMap(keyid, key), Collections.singletonMap(e, excludes), encryptionAlgo, dummyIvs, encryptButClear); } else { cencTrack = new CencEncryptingTrackImpl( t.getTarget(), keyid, Collections.singletonMap(keyid, key), null, encryptionAlgo, dummyIvs, encryptButClear); } t.setTarget(cencTrack); } else if (sparse == 1) { CencSampleEncryptionInformationGroupEntry e = new CencSampleEncryptionInformationGroupEntry(); e.setEncrypted(false); Set<Long> plainSamples = new HashSet<Long>(); if (t.getSyncSamples() != null && t.getSyncSamples().length > 0) { for (long i = 1; i <= t.getSamples().size(); i++) { if (Arrays.binarySearch(t.getSyncSamples(), i) < 0 || i < clearTillSample) { plainSamples.add(i); } } } else { for (int i = 0; i < clearTillSample; i++) { plainSamples.add((long) i); } for (int i = clearTillSample; i < t.getSamples().size(); i++) { if (i % 3 == 0) { plainSamples.add((long) i); } } } CencEncryptingTrackImpl cencTrack = new CencEncryptingTrackImpl( t.getTarget(), keyid, Collections.singletonMap(keyid, key), Collections.singletonMap(e, longSet2Array(plainSamples)), "cenc", dummyIvs, encryptButClear); t.setTarget(cencTrack); } else if (sparse == 2) { CencSampleEncryptionInformationGroupEntry e = new CencSampleEncryptionInformationGroupEntry(); e.setEncrypted(true); e.setKid(keyid); e.setIvSize(8); Set<Long> encryptedSamples = new HashSet<Long>(); if (t.getSyncSamples() != null && t.getSyncSamples().length > 0) { for (int i = 0; i < t.getSyncSamples().length; i++) { if (t.getSyncSamples()[i] >= clearTillSample) { encryptedSamples.add(t.getSyncSamples()[i]); } } } else { SecureRandom r = new SecureRandom(); int encSamples = numSamples / 10; while (--encSamples >= 0) { int s = r.nextInt(numSamples - clearTillSample) + clearTillSample; encryptedSamples.add((long) s); } } t.setTarget(new CencEncryptingTrackImpl( t.getTarget(), null, Collections.singletonMap(keyid, key), Collections.singletonMap(e, longSet2Array(encryptedSamples)), "cenc", dummyIvs, encryptButClear)); } } } } public void fixAppleOddity(Map<TrackProxy, String> track2File) { for (Map.Entry<TrackProxy, String> entry : track2File.entrySet()) { TrackProxy track = entry.getKey(); if (Path.getPath(track.getSampleDescriptionBox(), "...a/wave/esds") != null) { // mp4a or enca final SampleDescriptionBox stsd = track.getSampleDescriptionBox(); AudioSampleEntry ase = (AudioSampleEntry) stsd.getSampleEntry(); List<Box> aseBoxes = new ArrayList<Box>(); aseBoxes.add(Path.getPath(stsd, "...a/wave/esds")); for (Box box : ase.getBoxes()) { if (!box.getType().equals("wave")) { aseBoxes.add(box); } } ase.setBoxes(Collections.<Box>emptyList()); for (Box aseBox : aseBoxes) { ase.addBox(aseBox); } track.setTarget(new StsdCorrectingTrack(track.getTarget(), stsd)); } } } /** * In DASH Some tracks might have an earliest presentation timestamp < 0 * * @param track2File map from track object to originating file */ public void alignEditsToZero(Map<TrackProxy, String> track2File) { double earliestMoviePresentationTime = 0; Map<TrackProxy, Double> startTimes = new HashMap<TrackProxy, Double>(); Map<TrackProxy, Double> ctsOffset = new HashMap<TrackProxy, Double>(); for (TrackProxy track : track2File.keySet()) { List<Edit> edits = track.getEdits(); double earliestTrackPresentationTime = getEarliestTrackPresentationTime(edits); if (track.getCompositionTimeEntries() != null && track.getCompositionTimeEntries().size() > 0) { long currentTime = 0; int[] ptss = Arrays.copyOfRange(CompositionTimeToSample.blowupCompositionTimes(track.getCompositionTimeEntries()), 0, 50); for (int j = 0; j < ptss.length; j++) { ptss[j] += currentTime; currentTime += track.getSampleDurations()[j]; } Arrays.sort(ptss); earliestTrackPresentationTime += (double) ptss[0] / track.getTrackMetaData().getTimescale(); ctsOffset.put(track, (double) ptss[0] / track.getTrackMetaData().getTimescale()); } else { ctsOffset.put(track, 0.0); } startTimes.put(track, earliestTrackPresentationTime); earliestMoviePresentationTime = Math.min(earliestMoviePresentationTime, earliestTrackPresentationTime); } for (TrackProxy track : track2File.keySet()) { double adjustedStartTime = startTimes.get(track) - earliestMoviePresentationTime - ctsOffset.get(track); if (earliestMoviePresentationTime != 0) { LOG.info("Adjusted earliest presentation of " + track.getName() + " from " + startTimes.get(track) + " to " + (startTimes.get(track) - earliestMoviePresentationTime)); } final List<Edit> edits = BoxHelper.getEdits(track.getTarget(), adjustedStartTime); track.setTarget(new WrappingTrack(track.getTarget()) { @Override public List<Edit> getEdits() { return edits; } }); } } public Map<String, List<TrackProxy>> findAdaptationSets(Set<TrackProxy> allTracks) throws IOException, ExitCodeException { HashMap<String, List<TrackProxy>> trackFamilies = new LinkedHashMap<String, List<TrackProxy>>(); for (TrackProxy track : allTracks) { String family; if (track.getTarget().getHandler().equals("soun")) { int channels = ((AudioSampleEntry) track.getSampleDescriptionBox().getSampleEntry()).getChannelCount(); family = DashHelper.getRfc6381Codec(track.getSampleDescriptionBox().getSampleEntry()) + "-" + track.getTrackMetaData().getLanguage() + "-" + channels + "ch"; } else { family = DashHelper.getFormat(track.getTarget()); } List<TrackProxy> tracks = trackFamilies.get(family); if (tracks == null) { tracks = new ArrayList<TrackProxy>(); trackFamilies.put(family, tracks); } tracks.add(track); } for (String fam : trackFamilies.keySet()) { List<TrackProxy> tracks = trackFamilies.get(fam); long timeScale = -1; for (TrackProxy track : tracks) { if (timeScale > 0) { if (timeScale != track.getTrackMetaData().getTimescale()) { throw new ExitCodeException("The tracks " + tracks.get(0) + " and " + track + " have been assigned the same adaptation set but their timescale differs: " + timeScale + " vs. " + track.getTrackMetaData().getTimescale(), 38743); } } else { timeScale = track.getTrackMetaData().getTimescale(); } } } return trackFamilies; } public void setTrickModeFiles(List<File> trickModeFiles) { this.trickModeFiles = trickModeFiles; } private class StsdCorrectingTrack extends WrappingTrack { SampleDescriptionBox stsd; public StsdCorrectingTrack(Track track, SampleDescriptionBox stsd) { super(track); this.stsd = stsd; } public SampleDescriptionBox getSampleDescriptionBox() { return stsd; } @Override public String toString() { return getName(); } } public void addContentProtection(AdaptationSetType adaptationSet, UUID keyId) { DescriptorType contentProtection = adaptationSet.addNewContentProtection(); final DefaultKIDAttribute defaultKIDAttribute = DefaultKIDAttribute.Factory.newInstance(); defaultKIDAttribute.setDefaultKID(Collections.singletonList(keyId.toString())); contentProtection.set(defaultKIDAttribute); contentProtection.setSchemeIdUri("urn:mpeg:dash:mp4protection:2011"); contentProtection.setValue("cenc"); List<ProtectionSystemSpecificHeaderBox> psshs = psshBoxes.get(keyId); for (ProtectionSystemSpecificHeaderBox pssh : safe(psshs)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { pssh.getBox(Channels.newChannel(baos)); } catch (IOException e) { throw new RuntimeException(e); // unexpected } byte[] completePssh = baos.toByteArray(); Node cpn = createContentProctionNode(adaptationSet, "urn:uuid:" + UUIDConverter.convert(pssh.getSystemId()).toString(), null); Document d = cpn.getOwnerDocument(); Element psshElement = d.createElementNS("urn:mpeg:cenc:2013", "pssh"); psshElement.appendChild(d.createTextNode(Base64.getEncoder().encodeToString(completePssh))); cpn.appendChild(psshElement); if (Arrays.equals(ProtectionSystemSpecificHeaderBox.PLAYREADY_SYSTEM_ID, pssh.getSystemId())) { cpn.setNodeValue("MSPR 2.0"); Element pro = d.createElementNS("urn:microsoft:playready", "pro"); pro.appendChild(d.createTextNode(Base64.getEncoder().encodeToString(pssh.getContent()))); cpn.appendChild(pro); } } } protected Node createContentProctionNode(AdaptationSetType adaptationSet, String schemeIdUri, String value) { DescriptorType cpNode = adaptationSet.addNewContentProtection(); if (schemeIdUri != null) { cpNode.setSchemeIdUri(schemeIdUri); } if (value != null) { cpNode.setValue(value); } return cpNode.getDomNode(); } /** * Exception to force exit from tool in functions - kind of goto. */ protected static class ExitCodeException extends Exception { int exitCode; public ExitCodeException(String message, int exitCode) { super(message); this.exitCode = exitCode; } public int getExitCode() { return exitCode; } } }