/* * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package com.castlabs.dash.helpers; import com.castlabs.dash.dashfragmenter.sequences.DashFileSetSequence; import com.coremedia.iso.boxes.Box; import com.coremedia.iso.boxes.Container; import com.coremedia.iso.boxes.MediaHeaderBox; import com.coremedia.iso.boxes.fragment.MovieFragmentBox; import com.coremedia.iso.boxes.fragment.TrackExtendsBox; import com.coremedia.iso.boxes.fragment.TrackFragmentHeaderBox; import com.coremedia.iso.boxes.fragment.TrackRunBox; import com.googlecode.mp4parser.authoring.Track; import com.googlecode.mp4parser.authoring.tracks.CencEncryptedTrack; import com.googlecode.mp4parser.util.Path; import mpegDashSchemaMpd2011.*; import org.apache.xmlbeans.GDuration; import java.io.IOException; import java.math.BigDecimal; import java.util.*; public abstract class AbstractManifestWriter { private final Map<? extends Track, Container> trackContainer; private final Map<? extends Track, Long> trackBitrates; DashFileSetSequence dashFileSetSequence; public AbstractManifestWriter(Map<? extends Track, Container> trackContainer, Map<? extends Track, Long> trackBitrates, DashFileSetSequence dashFileSetSequence) { this.trackContainer = trackContainer; this.trackBitrates = trackBitrates; this.dashFileSetSequence = dashFileSetSequence; } public GDuration getMinBufferTime() { int requiredTimeInS = 0; for (Map.Entry<? extends Track, Container> trackContainerEntry : trackContainer.entrySet()) { long bitrate = trackBitrates.get(trackContainerEntry.getKey()); long timescale = ((MediaHeaderBox) Path.getPath(trackContainerEntry.getValue(), "/moov[0]/trak[0]/mdia[0]/mdhd[0]")).getTimescale(); long requiredBuffer = 0; Iterator<Box> iterator = trackContainerEntry.getValue().getBoxes().iterator(); while (iterator.hasNext()) { Box moofCand = iterator.next(); if (!moofCand.getType().equals("moof")) { continue; } MovieFragmentBox moof = (MovieFragmentBox) moofCand; iterator.next(); // skip mdat Buffer currentBuffer = new Buffer(bitrate, timescale); currentBuffer.simPlayback(moof.getSize(), 0); for (TrackRunBox trun : moof.getTrackRunBoxes()) { for (TrackRunBox.Entry entry : trun.getEntries()) { long sampleDuration; if (trun.isSampleDurationPresent()) { sampleDuration = trun.getEntries().get(0).getSampleDuration(); } else { TrackFragmentHeaderBox tfhd = Path.getPath(moof, "traf[0]/tfhd[0]"); if (tfhd.hasDefaultSampleDuration()) { sampleDuration = tfhd.getDefaultSampleDuration(); } else { TrackExtendsBox trex = Path.getPath(trackContainerEntry.getValue(), "/moov[0]/mvex[0]/trex[0]"); sampleDuration = trex.getDefaultSampleDuration(); } } long size; if (trun.isSampleSizePresent()) { size = entry.getSampleSize(); } else { TrackFragmentHeaderBox tfhd = Path.getPath(moof, "traf[0]/tfhd[0]"); if (tfhd.hasDefaultSampleSize()) { size = tfhd.getDefaultSampleSize(); } else { TrackExtendsBox trex = Path.getPath(trackContainerEntry.getValue(), "/moov[0]/mvex[0]/trex[0]"); size = trex.getDefaultSampleSize(); } } currentBuffer.simPlayback(size, sampleDuration); } } requiredBuffer = Math.max(requiredBuffer, -(currentBuffer.minBufferFullness + Math.min(0, currentBuffer.currentBufferFullness))); } requiredTimeInS = (int) Math.max(requiredTimeInS, Math.ceil((double) requiredBuffer / (bitrate / 8))); } return new GDuration(1, 0, 0, 0, 0, 0, requiredTimeInS, BigDecimal.ZERO); } public abstract String getProfile(); public MPDDocument getManifest() throws IOException { MPDDocument mdd = MPDDocument.Factory.newInstance(); MPDtype mpd = mdd.addNewMPD(); PeriodType periodType = mpd.addNewPeriod(); periodType.setId("0"); periodType.setStart(new GDuration(1, 0, 0, 0, 0, 0, 0, BigDecimal.ZERO)); ProgramInformationType programInformationType = mpd.addNewProgramInformation(); programInformationType.setMoreInformationURL("www.castLabs.com"); createPeriod(periodType); mpd.setProfiles(getProfile()); mpd.setType(PresentationType.STATIC); // no mpd update strategy implemented yet, could be dynamic mpd.setMinBufferTime(getMinBufferTime()); mpd.setMediaPresentationDuration(periodType.getDuration()); return mdd; } UUID getKeyId(Track track) { if (track instanceof CencEncryptedTrack) { return ((CencEncryptedTrack) track).getDefaultKeyId(); } else { return null; } } protected AdaptationSetType createAdaptationSet(PeriodType periodType, List<Track> tracks, String role, long id) { UUID keyId = null; String language = null; for (Track track : tracks) { if (keyId != null && !keyId.equals(getKeyId(track))) { throw new RuntimeException("The ManifestWriter cannot deal with more than ONE key per adaptation set."); } keyId = getKeyId(track); if (language != null && !language.endsWith(track.getTrackMetaData().getLanguage())) { throw new RuntimeException("The ManifestWriter cannot deal with more than ONE language per adaptation set."); } language = track.getTrackMetaData().getLanguage(); } AdaptationSetType adaptationSet = periodType.addNewAdaptationSet(); if (id != -1) { adaptationSet.setId(id); } if (role != null) { DescriptorType roleDescriptorType = adaptationSet.addNewRole(); String scheme = role.split("\\|")[0]; String value = role.split("\\|")[1]; roleDescriptorType.setSchemeIdUri(scheme); roleDescriptorType.setValue(value); } if (!tracks.get(0).getHandler().equals("subt")) { adaptationSet.setSegmentAlignment(true); adaptationSet.setSubsegmentAlignment(true); adaptationSet.setSubsegmentStartsWithSAP(1); adaptationSet.setStartWithSAP(1); adaptationSet.setBitstreamSwitching(true); } if (tracks.get(0).getHandler().equals("soun")) { adaptationSet.setMimeType("audio/mp4"); if (!"und".equals(language)) { adaptationSet.setLang(Locale.forLanguageTag(language).getLanguage()); } } else if (tracks.get(0).getHandler().equals("vide")) { adaptationSet.setMimeType("video/mp4"); } else if (tracks.get(0).getHandler().equals("subt")) { adaptationSet.setMimeType("video/mp4"); if (!"und".equals(language)) { adaptationSet.setLang(Locale.forLanguageTag(language).getLanguage()); } } else { throw new RuntimeException("Don't know what to do with handler type = " + tracks.get(0).getHandler()); } if (keyId != null) { addContentProtection(adaptationSet, keyId); } return adaptationSet; } protected void addContentProtection(AdaptationSetType adaptationSet, UUID keyId) { dashFileSetSequence.addContentProtection(adaptationSet, keyId); } protected void createInitialization(URLType urlType, Track track) { long offset = 0; long start = 0; for (Box box : trackContainer.get(track).getBoxes()) { if ("ftyp".equals(box.getType())) { start = offset; } if ("moov".equals(box.getType())) { urlType.setRange(String.format("%s-%s", start, offset + box.getSize() - 1)); break; } offset += box.getSize(); } } abstract protected void createPeriod(PeriodType periodType) throws IOException; class Buffer { final long bandwidth; final long timescale; long currentBufferFullness = 0; long minBufferFullness = 0; public Buffer(long bandwidth, long timescale) { this.bandwidth = bandwidth; this.timescale = timescale; } void simPlayback(long size, long videoTime) { currentBufferFullness -= size; currentBufferFullness += ((double) videoTime / timescale) * bandwidth / 8; if (currentBufferFullness < minBufferFullness) { minBufferFullness = currentBufferFullness; } } } }