package com.castlabs.dash.dashfragmenter.representation; import com.castlabs.dash.helpers.DashHelper; import com.castlabs.dash.helpers.SapHelper; import com.castlabs.dash.helpers.Timing; import com.coremedia.iso.BoxParser; import com.coremedia.iso.IsoFile; import com.coremedia.iso.IsoTypeWriter; import com.coremedia.iso.boxes.*; import com.coremedia.iso.boxes.fragment.*; import com.coremedia.iso.boxes.sampleentry.AudioSampleEntry; import com.googlecode.mp4parser.DataSource; import com.googlecode.mp4parser.authoring.Edit; import com.googlecode.mp4parser.authoring.Sample; import com.googlecode.mp4parser.authoring.Track; import com.googlecode.mp4parser.authoring.tracks.CencEncryptedTrack; import com.googlecode.mp4parser.boxes.dece.SampleEncryptionBox; import com.googlecode.mp4parser.boxes.mp4.samplegrouping.CencSampleEncryptionInformationGroupEntry; import com.googlecode.mp4parser.boxes.mp4.samplegrouping.GroupEntry; import com.googlecode.mp4parser.boxes.mp4.samplegrouping.SampleGroupDescriptionBox; import com.googlecode.mp4parser.boxes.mp4.samplegrouping.SampleToGroupBox; import com.googlecode.mp4parser.boxes.threegpp26244.SegmentIndexBox; import com.googlecode.mp4parser.util.Path; import com.googlecode.mp4parser.util.UUIDConverter; import com.mp4parser.iso14496.part12.SampleAuxiliaryInformationOffsetsBox; import com.mp4parser.iso14496.part12.SampleAuxiliaryInformationSizesBox; import com.mp4parser.iso23001.part7.CencSampleAuxiliaryDataFormat; import com.mp4parser.iso23001.part7.ProtectionSystemSpecificHeaderBox; import com.mp4parser.iso23001.part7.TrackEncryptionBox; import mpegCenc2013.DefaultKIDAttribute; import mpegDashSchemaMpd2011.*; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import java.io.IOException; import java.math.BigInteger; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; import java.util.*; import static com.castlabs.dash.helpers.BoxHelper.boxToBytes; import static com.castlabs.dash.helpers.ManifestHelper.convertFramerate; import static com.castlabs.dash.helpers.Timing.getDuration; import static com.castlabs.dash.helpers.Timing.getPtss; import static com.castlabs.dash.helpers.Timing.getTimeMappingEditTime; import static com.googlecode.mp4parser.util.CastUtils.l2i; public abstract class AbstractRepresentationBuilder extends AbstractList<Container> implements Mp4RepresentationBuilder { protected Track theTrack; protected final List<ProtectionSystemSpecificHeaderBox> psshs; protected Date date = new Date(); protected String source; protected long[] segmentStartSamples; protected long[] fragmentStartSamples; public AbstractRepresentationBuilder(Track track, List<ProtectionSystemSpecificHeaderBox> psshs, String source, long[] segmentStartSamples, long[] fragmentStartSamples) { this.theTrack = track; this.psshs = psshs; this.source = source; this.fragmentStartSamples = fragmentStartSamples; this.segmentStartSamples = segmentStartSamples; } public String getSource() { return source; } public Track getTrack() { return theTrack; } public Container getInitSegment() { List<Box> initSegment = new ArrayList<Box>(); List<String> minorBrands = new ArrayList<String>(); minorBrands.add("isom"); minorBrands.add("iso6"); minorBrands.add("avc1"); initSegment.add(new FileTypeBox("isom", 0, minorBrands)); initSegment.add(createMoov()); return new ListContainer(initSegment); } protected Box createMoov() { MovieBox movieBox = new MovieBox(); movieBox.addBox(createMvhd()); movieBox.addBox(createTrak(theTrack)); movieBox.addBox(createMvex()); if (psshs != null) { for (ProtectionSystemSpecificHeaderBox pssh : psshs) { movieBox.addBox(pssh); } } // metadata here return movieBox; } protected Box createTrex(Track track) { TrackExtendsBox trex = new TrackExtendsBox(); trex.setTrackId(track.getTrackMetaData().getTrackId()); trex.setDefaultSampleDescriptionIndex(1); trex.setDefaultSampleDuration(0); trex.setDefaultSampleSize(0); SampleFlags sf = new SampleFlags(); if ("soun".equals(track.getHandler()) || "subt".equals(track.getHandler()) || "text".equals(track.getHandler())) { // as far as I know there is no audio encoding // where the sample are not self contained. // same seems to be true for subtitle tracks sf.setSampleDependsOn(2); sf.setSampleIsDependedOn(2); } trex.setDefaultSampleFlags(sf); return trex; } protected Box createMvex() { MovieExtendsBox mvex = new MovieExtendsBox(); final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox(); mved.setVersion(1); mved.setFragmentDuration(theTrack.getDuration()); mvex.addBox(mved); mvex.addBox(createTrex(theTrack)); return mvex; } protected Box createTkhd(Track track) { TrackHeaderBox tkhd = new TrackHeaderBox(); tkhd.setVersion(1); tkhd.setFlags(7); // enabled, in movie, in previe, in poster tkhd.setAlternateGroup(track.getTrackMetaData().getGroup()); tkhd.setCreationTime(track.getTrackMetaData().getCreationTime()); // We need to take edit list box into account in trackheader duration // but as long as I don't support edit list boxes it is sufficient to // just translate media duration to movie timescale if (track.getEdits().isEmpty()) { tkhd.setDuration(track.getDuration()); } else { long dur = 0; for (Edit edit : track.getEdits()) { dur += edit.getMediaTime() != -1 ? edit.getSegmentDuration() * track.getTrackMetaData().getTimescale() : 0; } tkhd.setDuration(dur); } tkhd.setHeight(track.getTrackMetaData().getHeight()); tkhd.setWidth(track.getTrackMetaData().getWidth()); tkhd.setLayer(track.getTrackMetaData().getLayer()); tkhd.setModificationTime(getDate()); tkhd.setTrackId(track.getTrackMetaData().getTrackId()); tkhd.setVolume(track.getTrackMetaData().getVolume()); return tkhd; } protected Box createMvhd() { MovieHeaderBox mvhd = new MovieHeaderBox(); mvhd.setVersion(1); mvhd.setCreationTime(getDate()); mvhd.setModificationTime(getDate()); mvhd.setDuration(0);//no duration in moov for fragmented movies long movieTimeScale = theTrack.getTrackMetaData().getTimescale(); mvhd.setTimescale(movieTimeScale); mvhd.setNextTrackId(theTrack.getTrackMetaData().getTrackId() + 1); return mvhd; } protected Box createStbl(Track track) { SampleTableBox stbl = new SampleTableBox(); createStsd(track, stbl); stbl.addBox(new TimeToSampleBox()); stbl.addBox(new SampleToChunkBox()); stbl.addBox(new SampleSizeBox()); stbl.addBox(new StaticChunkOffsetBox()); return stbl; } protected void createStsd(Track track, SampleTableBox stbl) { stbl.addBox(track.getSampleDescriptionBox()); } protected Box createMinf(Track track) { MediaInformationBox minf = new MediaInformationBox(); if (track.getHandler().equals("vide")) { minf.addBox(new VideoMediaHeaderBox()); } else if (track.getHandler().equals("soun")) { minf.addBox(new SoundMediaHeaderBox()); } else if (track.getHandler().equals("text")) { minf.addBox(new NullMediaHeaderBox()); } else if (track.getHandler().equals("subt")) { minf.addBox(new SubtitleMediaHeaderBox()); } else if (track.getHandler().equals("hint")) { minf.addBox(new HintMediaHeaderBox()); } else if (track.getHandler().equals("sbtl")) { minf.addBox(new NullMediaHeaderBox()); } minf.addBox(createDinf()); minf.addBox(createStbl(track)); return minf; } protected Box createMdiaHdlr(Track track) { HandlerBox hdlr = new HandlerBox(); hdlr.setHandlerType(track.getHandler()); return hdlr; } protected Box createMdhd(Track track) { MediaHeaderBox mdhd = new MediaHeaderBox(); mdhd.setCreationTime(track.getTrackMetaData().getCreationTime()); mdhd.setModificationTime(getDate()); mdhd.setDuration(0);//no duration in moov for fragmented movies mdhd.setTimescale(track.getTrackMetaData().getTimescale()); mdhd.setLanguage(track.getTrackMetaData().getLanguage()); return mdhd; } public Date getDate() { return date; } protected DataInformationBox createDinf() { DataInformationBox dinf = new DataInformationBox(); DataReferenceBox dref = new DataReferenceBox(); dinf.addBox(dref); DataEntryUrlBox url = new DataEntryUrlBox(); url.setFlags(1); dref.addBox(url); return dinf; } protected Box createMdia(Track track) { MediaBox mdia = new MediaBox(); mdia.addBox(createMdhd(track)); mdia.addBox(createMdiaHdlr(track)); mdia.addBox(createMinf(track)); return mdia; } protected Box createTrak(Track track) { TrackBox trackBox = new TrackBox(); trackBox.addBox(createTkhd(track)); Box edts = createEdts(track); if (edts != null) { trackBox.addBox(edts); } trackBox.addBox(createMdia(track)); return trackBox; } protected Box createEdts(Track track) { if (track.getEdits() != null && track.getEdits().size() > 0) { EditListBox elst = new EditListBox(); elst.setVersion(1); List<EditListBox.Entry> entries = new ArrayList<EditListBox.Entry>(); for (Edit edit : track.getEdits()) { entries.add(new EditListBox.Entry(elst, Math.round(edit.getSegmentDuration() * track.getTrackMetaData().getTimescale()), edit.getMediaTime() * track.getTrackMetaData().getTimescale() / edit.getTimeScale(), edit.getMediaRate())); } elst.setEntries(entries); EditBox edts = new EditBox(); edts.addBox(elst); return edts; } else { return null; } } protected void addContentProtection(RepresentationType representation) { List<String> keyIds = new ArrayList<String>(); if (theTrack instanceof CencEncryptedTrack) { keyIds.add(((CencEncryptedTrack) theTrack).getDefaultKeyId().toString()); for (GroupEntry ge : theTrack.getSampleGroups().keySet()) { if (ge instanceof CencSampleEncryptionInformationGroupEntry) { if (((CencSampleEncryptionInformationGroupEntry) ge).getKid() != null) { keyIds.add(((CencSampleEncryptionInformationGroupEntry) ge).getKid().toString()); } } } } if (!keyIds.isEmpty()) { DescriptorType contentProtection = representation.addNewContentProtection(); final DefaultKIDAttribute defaultKIDAttribute = DefaultKIDAttribute.Factory.newInstance(); defaultKIDAttribute.setDefaultKID(keyIds); contentProtection.set(defaultKIDAttribute); contentProtection.setSchemeIdUri("urn:mpeg:dash:mp4protection:2011"); contentProtection.setValue("cenc"); } if (psshs != null) { for (ProtectionSystemSpecificHeaderBox pssh : psshs) { DescriptorType dt = representation.addNewContentProtection(); byte[] psshContent = pssh.getContent(); dt.setSchemeIdUri("urn:uuid:" + UUIDConverter.convert(pssh.getSystemId()).toString()); if (Arrays.equals(ProtectionSystemSpecificHeaderBox.PLAYREADY_SYSTEM_ID, pssh.getSystemId())) { dt.setValue("MSPR 2.0"); Node playReadyCPN = dt.getDomNode(); Document d = playReadyCPN.getOwnerDocument(); Element pro = d.createElementNS("urn:microsoft:playready", "pro"); Element prPssh = d.createElementNS("urn:mpeg:cenc:2013", "pssh"); pro.appendChild(d.createTextNode(Base64.getEncoder().encodeToString(psshContent))); prPssh.appendChild(d.createTextNode(Base64.getEncoder().encodeToString(boxToBytes(pssh)))); playReadyCPN.appendChild(pro); playReadyCPN.appendChild(prPssh); } if (Arrays.equals(ProtectionSystemSpecificHeaderBox.WIDEVINE, pssh.getSystemId())) { // Widevvine Node widevineCPN = dt.getDomNode(); Document d = widevineCPN.getOwnerDocument(); Element wvPssh = d.createElementNS("urn:mpeg:cenc:2013", "pssh"); wvPssh.appendChild(d.createTextNode(Base64.getEncoder().encodeToString(boxToBytes(pssh)))); widevineCPN.appendChild(wvPssh); } } } } public int size() { return segmentStartSamples.length; } public Container get(int index) { List<Box> moofMdat = new ArrayList<Box>(); long startSample = segmentStartSamples[index]; long endSample; if (index + 1 < segmentStartSamples.length) { endSample = segmentStartSamples[index + 1]; } else { endSample = theTrack.getSamples().size() - 1; } long fragmentStartSample; long fragmentEndSample; do { fragmentStartSample = startSample; int fIndex = Arrays.binarySearch(fragmentStartSamples, startSample); if (fIndex + 1 < fragmentStartSamples.length) { fragmentEndSample = fragmentStartSamples[index + 1]; } else { fragmentEndSample = theTrack.getSamples().size() + 1; } moofMdat.add(createMoof(fragmentStartSample, fragmentEndSample, theTrack, fIndex + 1)); // it's one bases moofMdat.add(createMdat(fragmentStartSample, fragmentEndSample)); } while (fragmentEndSample < endSample); return new ListContainer(moofMdat); } /** * Creates a 'moof' box for a given sequence of samples. * * @param startSample low endpoint (inclusive) of the sample sequence * @param endSample high endpoint (exclusive) of the sample sequence * @param track source of the samples * @param sequenceNumber the fragment index of the requested list of samples * @return the list of TrackRun boxes. */ protected Box createMoof(long startSample, long endSample, Track track, int sequenceNumber) { MovieFragmentBox moof = new MovieFragmentBox(); createMfhd(sequenceNumber, moof); createTraf(startSample, endSample, track, moof); TrackRunBox firstTrun = moof.getTrackRunBoxes().get(0); firstTrun.setDataOffset(1); // dummy to make size correct firstTrun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size return moof; } protected void createMfhd(int sequenceNumber, MovieFragmentBox parent) { MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox(); mfhd.setSequenceNumber(sequenceNumber); parent.addBox(mfhd); } public Container getIndexSegment() { SegmentIndexBox sidx = new SegmentIndexBox(); sidx.setVersion(0); sidx.setFlags(0); sidx.setReserved(0); Container initSegment = getInitSegment(); TrackHeaderBox tkhd = Path.getPath(initSegment, "/moov[0]/trak[0]/tkhd[0]"); MediaHeaderBox mdhd = Path.getPath(initSegment, "/moov[0]/trak[0]/mdia[0]/mdhd[0]"); sidx.setReferenceId(tkhd.getTrackId()); sidx.setTimeScale(mdhd.getTimescale()); // we only have one long[] ptss = getPtss(Path.<TrackRunBox>getPath(get(0), "/moof[0]/traf[0]/trun[0]")); Arrays.sort(ptss); // index 0 has now the earliest presentation time stamp! long timeMappingEdit = getTimeMappingEditTime(initSegment); sidx.setEarliestPresentationTime(ptss[0] - timeMappingEdit<0?0:ptss[0] - timeMappingEdit); List<SegmentIndexBox.Entry> entries = sidx.getEntries(); TrackExtendsBox trex = Path.getPath(initSegment, "/moov[0]/mvex[0]/trex[0]"); // ugly code ... for (Container c : this) { int size = 0; for (Box box : c.getBoxes()) { size += l2i(box.getSize()); } MovieFragmentBox moof = Path.getPath(c, "moof[0]"); SegmentIndexBox.Entry entry = new SegmentIndexBox.Entry(); entries.add(entry); entry.setReferencedSize(size); TrackRunBox trun = Path.getPath(moof, "traf[0]/trun[0]"); ptss = getPtss(trun); entry.setSapType(SapHelper.getFirstFrameSapType(ptss, SapHelper.getSampleFlags(0, trun, trex))); entry.setSubsegmentDuration(Timing.getDuration(Path.<TrackRunBox>getPath(moof, "traf[0]/trun[0]"))); entry.setStartsWithSap((byte) 1); // we know it - no need to lookup } sidx.setFirstOffset(0); return new ListContainer(Collections.<Box>singletonList(sidx)); } protected void createTfdt(long startSample, Track track, TrackFragmentBox parent) { TrackFragmentBaseMediaDecodeTimeBox tfdt = new TrackFragmentBaseMediaDecodeTimeBox(); tfdt.setVersion(1); long startTime = 0; long[] times = track.getSampleDurations(); for (int i = 1; i < startSample; i++) { startTime += times[i - 1]; } tfdt.setBaseMediaDecodeTime(startTime); parent.addBox(tfdt); } /** * Gets the sizes of a sequence of samples. * * @param startSample low endpoint (inclusive) of the sample sequence * @param endSample high endpoint (exclusive) of the sample sequence * @return the sample sizes in the given interval */ protected long[] getSampleSizes(long startSample, long endSample) { List<Sample> samples = getSamples(startSample, endSample); long[] sampleSizes = new long[samples.size()]; for (int i = 0; i < sampleSizes.length; i++) { sampleSizes[i] = samples.get(i).getSize(); } return sampleSizes; } /** * Creates one or more theTrack run boxes for a given sequence. * * @param startSample low endpoint (inclusive) of the sample sequence * @param endSample high endpoint (exclusive) of the sample sequence * @param track source of the samples * @param parent the created box must be added to this box */ protected void createTrun(long startSample, long endSample, Track track, TrackFragmentBox parent) { TrackRunBox trun = new TrackRunBox(); trun.setVersion(1); long[] sampleSizes = getSampleSizes(startSample, endSample); trun.setSampleDurationPresent(true); trun.setSampleSizePresent(true); List<TrackRunBox.Entry> entries = new ArrayList<TrackRunBox.Entry>(l2i(endSample - startSample)); List<CompositionTimeToSample.Entry> compositionTimeEntries = track.getCompositionTimeEntries(); int compositionTimeQueueIndex = 0; CompositionTimeToSample.Entry[] compositionTimeQueue = compositionTimeEntries != null && compositionTimeEntries.size() > 0 ? compositionTimeEntries.toArray(new CompositionTimeToSample.Entry[compositionTimeEntries.size()]) : null; long compositionTimeEntriesLeft = compositionTimeQueue != null ? compositionTimeQueue[compositionTimeQueueIndex].getCount() : -1; trun.setSampleCompositionTimeOffsetPresent(compositionTimeEntriesLeft > 0); // fast forward composition stuff for (long i = 1; i < startSample; i++) { if (compositionTimeQueue != null) { //trun.setSampleCompositionTimeOffsetPresent(true); if (--compositionTimeEntriesLeft == 0 && (compositionTimeQueue.length - compositionTimeQueueIndex) > 1) { compositionTimeQueueIndex++; compositionTimeEntriesLeft = compositionTimeQueue[compositionTimeQueueIndex].getCount(); } } } boolean sampleFlagsRequired = (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty() || track.getSyncSamples() != null && track.getSyncSamples().length != 0); trun.setSampleFlagsPresent(sampleFlagsRequired); for (int i = 0; i < sampleSizes.length; i++) { TrackRunBox.Entry entry = new TrackRunBox.Entry(); entry.setSampleSize(sampleSizes[i]); if (sampleFlagsRequired) { //if (false) { SampleFlags sflags = new SampleFlags(); if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { SampleDependencyTypeBox.Entry e = track.getSampleDependencies().get(i); sflags.setSampleDependsOn(e.getSampleDependsOn()); sflags.setSampleIsDependedOn(e.getSampleIsDependentOn()); sflags.setSampleHasRedundancy(e.getSampleHasRedundancy()); } if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { // we have to mark non-sync samples! if (Arrays.binarySearch(track.getSyncSamples(), startSample + i) >= 0) { sflags.setSampleIsDifferenceSample(false); sflags.setSampleDependsOn(2); } else { sflags.setSampleIsDifferenceSample(true); sflags.setSampleDependsOn(1); } } // i don't have sample degradation entry.setSampleFlags(sflags); } entry.setSampleDuration(track.getSampleDurations()[l2i(startSample + i - 1)]); if (compositionTimeQueue != null) { entry.setSampleCompositionTimeOffset(compositionTimeQueue[compositionTimeQueueIndex].getOffset()); if (--compositionTimeEntriesLeft == 0 && (compositionTimeQueue.length - compositionTimeQueueIndex) > 1) { compositionTimeQueueIndex++; compositionTimeEntriesLeft = compositionTimeQueue[compositionTimeQueueIndex].getCount(); } } entries.add(entry); } trun.setEntries(entries); parent.addBox(trun); } protected void createTraf(long startSample, long endSample, Track track, MovieFragmentBox parent) { TrackFragmentBox traf = new TrackFragmentBox(); parent.addBox(traf); createTfhd(traf); TrackFragmentHeaderBox tfhd = (TrackFragmentHeaderBox) traf.getBoxes().get(traf.getBoxes().size()-1); createTfdt(startSample, track, traf); createTrun(startSample, endSample, track, traf); TrackRunBox trun = (TrackRunBox) traf.getBoxes().get(traf.getBoxes().size()-1); SampleFlags first = null; SampleFlags second = null; boolean allFllowingSame = true; for (TrackRunBox.Entry entry : trun.getEntries()) { if (first == null) { first = entry.getSampleFlags(); } else if (second == null) { second = entry.getSampleFlags(); } else { allFllowingSame &= second.equals(entry.getSampleFlags()); } } if (allFllowingSame && second != null) { trun.setSampleFlagsPresent(false); trun.setFirstSampleFlags(first); tfhd.setDefaultSampleFlags(second); } createSubs(startSample, endSample, track, traf); if (track instanceof CencEncryptedTrack) { createSaiz(startSample, endSample, (CencEncryptedTrack) track, traf); createSenc(startSample, endSample, (CencEncryptedTrack) track, traf); createSaio(traf); } Map<String, List<GroupEntry>> groupEntryFamilies = new HashMap<String, List<GroupEntry>>(); for (Map.Entry<GroupEntry, long[]> sg : track.getSampleGroups().entrySet()) { String type = sg.getKey().getType(); List<GroupEntry> groupEntries = groupEntryFamilies.get(type); if (groupEntries == null) { groupEntries = new ArrayList<GroupEntry>(); groupEntryFamilies.put(type, groupEntries); } groupEntries.add(sg.getKey()); } for (Map.Entry<String, List<GroupEntry>> sg : groupEntryFamilies.entrySet()) { SampleGroupDescriptionBox sgpd = new SampleGroupDescriptionBox(); String type = sg.getKey(); sgpd.setGroupEntries(sg.getValue()); SampleToGroupBox sbgp = new SampleToGroupBox(); sbgp.setGroupingType(type); SampleToGroupBox.Entry last = null; for (int i = l2i(startSample - 1); i < l2i(endSample - 1); i++) { int index = 0; for (int j = 0; j < sg.getValue().size(); j++) { GroupEntry groupEntry = sg.getValue().get(j); long[] sampleNums = track.getSampleGroups().get(groupEntry); if (Arrays.binarySearch(sampleNums, i) >= 0) { index = j + 1; } } if (last == null || last.getGroupDescriptionIndex() != index) { last = new SampleToGroupBox.Entry(1, index); sbgp.getEntries().add(last); } else { last.setSampleCount(last.getSampleCount() + 1); } } traf.addBox(sgpd); traf.addBox(sbgp); } } protected void createSubs(long startSample, long endSample, Track track, TrackFragmentBox traf) { SubSampleInformationBox subs = track.getSubsampleInformationBox(); if (subs != null) { SubSampleInformationBox fragmentSubs = new SubSampleInformationBox(); fragmentSubs.setEntries(subs.getEntries().subList(l2i(startSample - 1), l2i(endSample - 1))); traf.addBox(fragmentSubs); } } protected void createTfhd(TrackFragmentBox parent) { TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox(); SampleFlags sf = new SampleFlags(); tfhd.setDefaultSampleFlags(sf); tfhd.setBaseDataOffset(-1); tfhd.setTrackId(theTrack.getTrackMetaData().getTrackId()); tfhd.setDefaultBaseIsMoof(true); parent.addBox(tfhd); } protected void createSenc(long startSample, long endSample, CencEncryptedTrack track, TrackFragmentBox parent) { SampleEncryptionBox senc = new SampleEncryptionBox(); senc.setSubSampleEncryption(track.hasSubSampleEncryption()); senc.setEntries(track.getSampleEncryptionEntries().subList(l2i(startSample - 1), l2i(endSample - 1))); parent.addBox(senc); } protected void createSaio(TrackFragmentBox parent) { SampleAuxiliaryInformationOffsetsBox saio = new SampleAuxiliaryInformationOffsetsBox(); parent.addBox(saio); assert parent.getBoxes(TrackRunBox.class).size() == 1 : "Don't know how to deal with multiple Track Run Boxes when encrypting"; saio.setAuxInfoType("cenc"); saio.setFlags(1); long offset = 0; offset += 8; // traf header till 1st child box for (Box box : parent.getBoxes()) { if (box instanceof SampleEncryptionBox) { offset += ((SampleEncryptionBox) box).getOffsetToFirstIV(); break; } else { offset += box.getSize(); } } MovieFragmentBox moof = (MovieFragmentBox) parent.getParent(); offset += 16; // traf header till 1st child box for (Box box : moof.getBoxes()) { if (box == parent) { break; } else { offset += box.getSize(); } } saio.setOffsets(new long[]{offset}); } protected void createSaiz(long startSample, long endSample, CencEncryptedTrack track, TrackFragmentBox parent) { SampleDescriptionBox sampleDescriptionBox = track.getSampleDescriptionBox(); TrackEncryptionBox tenc = Path.getPath(sampleDescriptionBox, "enc.[0]/sinf[0]/schi[0]/tenc[0]"); SampleAuxiliaryInformationSizesBox saiz = new SampleAuxiliaryInformationSizesBox(); saiz.setAuxInfoType("cenc"); saiz.setFlags(1); if (track.hasSubSampleEncryption()) { short[] sizes = new short[l2i(endSample - startSample)]; List<CencSampleAuxiliaryDataFormat> auxs = track.getSampleEncryptionEntries().subList(l2i(startSample - 1), l2i(endSample - 1)); for (int i = 0; i < sizes.length; i++) { sizes[i] = (short) auxs.get(i).getSize(); } saiz.setSampleInfoSizes(sizes); } else { saiz.setDefaultSampleInfoSize(tenc.getDefaultIvSize()); saiz.setSampleCount(l2i(endSample - startSample)); } parent.addBox(saiz); } protected Box createMdat(final long startSample, final long endSample) { class Mdat implements Box { Container parent; long size_ = -1; public Container getParent() { return parent; } public void setParent(Container parent) { this.parent = parent; } public long getOffset() { throw new RuntimeException("Doesn't have any meaning for programmatically created boxes"); } public long getSize() { if (size_ != -1) return size_; long size = 8; // I don't expect 2gig fragments for (Sample sample : getSamples(startSample, endSample)) { size += sample.getSize(); } size_ = size; return size; } public String getType() { return "mdat"; } public void getBox(WritableByteChannel writableByteChannel) throws IOException { ByteBuffer header = ByteBuffer.allocate(8); IsoTypeWriter.writeUInt32(header, l2i(getSize())); header.put(IsoFile.fourCCtoBytes(getType())); header.rewind(); writableByteChannel.write(header); List<Sample> samples = getSamples(startSample, endSample); for (Sample sample : samples) { sample.writeTo(writableByteChannel); } } public void parse(DataSource fileChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException { } } return new Mdat(); } /** * Gets all samples starting with <code>startSample</code> (one based -> one is the first) and * ending with <code>endSample</code> (exclusive). * * @param startSample low endpoint (inclusive) of the sample sequence * @param endSample high endpoint (exclusive) of the sample sequence * @return a <code>List<ByteBuffer></code> of raw samples */ protected List<Sample> getSamples(long startSample, long endSample) { // since startSample and endSample are one-based substract 1 before addressing list elements return theTrack.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1); } public String getCodec() { return DashHelper.getRfc6381Codec(theTrack.getSampleDescriptionBox().getSampleEntry()); } public static long getBandwidth(Track track) { long size = 0; List<Sample> samples = track.getSamples(); int increment = samples.size() / Math.min(samples.size(), 10000); int sampleSize = 1; // start with one so that we never get into a divided by zero situation for (int i = 0; i < (samples.size()-increment); i+=increment) { size += samples.get(i).getSize(); sampleSize++; } size = (size / sampleSize) * track.getSamples().size(); double duration = (double) track.getDuration() / track.getTrackMetaData().getTimescale(); return (long) ((size * 8 / duration / 100)) * 100; } public long getBandwidth() { return getBandwidth(theTrack); } public RepresentationType getLiveRepresentation() { RepresentationType representation = getBaseRepresentation(); SegmentTemplateType segmentTemplate = representation.addNewSegmentTemplate(); SegmentTimelineType segmentTimeline = segmentTemplate.addNewSegmentTimeline(); TrackRunBox firstTrun = Path.getPath(this.get(0), "moof/traf/trun"); long[] ptss = getPtss(firstTrun); Arrays.sort(ptss); // index 0 has now the earliest presentation time stamp! long timeMappingEdit = getTimeMappingEditTime(getInitSegment()); long startTime = ptss[0] - timeMappingEdit; SegmentTimelineType.S lastSegmentTimelineS = null; for (Container container : this) { long duration = 0; List<TrackRunBox> truns = Path.getPaths(container, "moof/traf/trun"); for (TrackRunBox trun : truns) { duration += getDuration(trun); } if (lastSegmentTimelineS != null && lastSegmentTimelineS.getD().equals(BigInteger.valueOf(duration))) { if (lastSegmentTimelineS.isSetR()) { lastSegmentTimelineS.setR(lastSegmentTimelineS.getR().add(BigInteger.ONE)); } else { lastSegmentTimelineS.setR(BigInteger.ONE); } } else { SegmentTimelineType.S s = segmentTimeline.addNewS(); s.setD((BigInteger.valueOf(duration))); s.setT(BigInteger.valueOf(startTime)); lastSegmentTimelineS = s; } startTime += duration; } return representation; } public RepresentationType getBaseRepresentation() { RepresentationType representation = RepresentationType.Factory.newInstance(); representation.setProfiles("urn:mpeg:dash:profile:isoff-on-demand:2011"); if (theTrack.getHandler().equals("vide")) { long videoHeight = (long) theTrack.getTrackMetaData().getHeight(); long videoWidth = (long) theTrack.getTrackMetaData().getWidth(); double framesPerSecond = (double) (theTrack.getSamples().size() * theTrack.getTrackMetaData().getTimescale()) / theTrack.getDuration(); representation.setMimeType("video/mp4"); representation.setCodecs(getCodec()); representation.setWidth(videoWidth); representation.setHeight(videoHeight); representation.setFrameRate(convertFramerate(framesPerSecond)); representation.setSar("1:1"); // too hard to find it out. Ignoring even though it should be set according to DASH-AVC-264-v2.00-hd-mca.pdf } else if (theTrack.getHandler().equals("soun")) { AudioSampleEntry ase = (AudioSampleEntry) theTrack.getSampleDescriptionBox().getSampleEntry(); representation.setMimeType("audio/mp4"); representation.setCodecs(DashHelper.getRfc6381Codec(ase)); representation.setAudioSamplingRate("" + DashHelper.getAudioSamplingRate(ase)); DescriptorType audio_channel_conf = representation.addNewAudioChannelConfiguration(); DashHelper.ChannelConfiguration cc = DashHelper.getChannelConfiguration(ase); audio_channel_conf.setSchemeIdUri(cc.schemeIdUri); audio_channel_conf.setValue(cc.value); } else if (theTrack.getHandler().equals("subt") || theTrack.getHandler().equals("text")) { representation.setMimeType("application/mp4"); representation.setCodecs(getCodec()); representation.setStartWithSAP(1); } representation.setBandwidth(getBandwidth ()); addContentProtection(representation); return representation; } public RepresentationType getOnDemandRepresentation() { RepresentationType representation = getBaseRepresentation(); SegmentBaseType segBaseType = representation.addNewSegmentBase(); segBaseType.setTimescale(theTrack.getTrackMetaData().getTimescale()); segBaseType.setIndexRangeExact(true); long initSize = 0; for (Box b : getInitSegment().getBoxes()) { initSize += b.getSize(); } URLType initialization = segBaseType.addNewInitialization(); long indexSize = 0; for (Box b : getIndexSegment().getBoxes()) { indexSize += b.getSize(); } segBaseType.setIndexRange(String.format("%s-%s", initSize, initSize + indexSize - 1)); initialization.setRange(String.format("0-%s", initSize - 1)); return representation; } }