/* * Copyright 2012 Sebastian Annies, Hamburg * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mp4parser.muxer.builder; import org.mp4parser.*; import org.mp4parser.boxes.iso14496.part12.*; import org.mp4parser.boxes.iso23001.part7.CencSampleAuxiliaryDataFormat; import org.mp4parser.boxes.iso23001.part7.SampleEncryptionBox; import org.mp4parser.boxes.iso23001.part7.TrackEncryptionBox; import org.mp4parser.boxes.samplegrouping.GroupEntry; import org.mp4parser.boxes.samplegrouping.SampleGroupDescriptionBox; import org.mp4parser.boxes.samplegrouping.SampleToGroupBox; import org.mp4parser.muxer.Edit; import org.mp4parser.muxer.Movie; import org.mp4parser.muxer.Sample; import org.mp4parser.muxer.Track; import org.mp4parser.muxer.tracks.CencEncryptedTrack; import org.mp4parser.tools.IsoTypeWriter; import org.mp4parser.tools.Path; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.WritableByteChannel; import java.util.*; import static org.mp4parser.tools.CastUtils.l2i; /** * Creates a fragmented MP4 file. */ public class FragmentedMp4Builder implements Mp4Builder { private static org.slf4j.Logger LOG = LoggerFactory.getLogger(FragmentedMp4Builder.class.getName()); protected Fragmenter fragmenter; public FragmentedMp4Builder() { } private static long getTrackDuration(Movie movie, Track track) { return (track.getDuration() * movie.getTimescale()) / track.getTrackMetaData().getTimescale(); } public Date getDate() { return new Date(); } public ParsableBox createFtyp(Movie movie) { List<String> minorBrands = new LinkedList<String>(); minorBrands.add("mp42"); minorBrands.add("iso6"); minorBrands.add("avc1"); minorBrands.add("isom"); return new FileTypeBox("iso6", 1, minorBrands); } protected List<Box> createMoofMdat(final Movie movie) { List<Box> moofsMdats = new ArrayList<Box>(); HashMap<Track, long[]> intersectionMap = new HashMap<Track, long[]>(); HashMap<Track, Double> track2currentTime = new HashMap<Track, Double>(); for (Track track : movie.getTracks()) { long[] intersects = fragmenter.sampleNumbers(track); intersectionMap.put(track, intersects); track2currentTime.put(track, 0.0); } int sequence = 1; while (!intersectionMap.isEmpty()) { Track earliestTrack = null; double earliestTime = Double.MAX_VALUE; for (Map.Entry<Track, Double> trackEntry : track2currentTime.entrySet()) { if (trackEntry.getValue() < earliestTime) { earliestTime = trackEntry.getValue(); earliestTrack = trackEntry.getKey(); } } assert earliestTrack != null; long[] startSamples = intersectionMap.get(earliestTrack); long startSample = startSamples[0]; long endSample = startSamples.length > 1 ? startSamples[1] : earliestTrack.getSamples().size() + 1; long[] times = earliestTrack.getSampleDurations(); long timscale = earliestTrack.getTrackMetaData().getTimescale(); for (long i = startSample; i < endSample; i++) { earliestTime += (double) times[l2i(i - 1)] / timscale; } createFragment(moofsMdats, earliestTrack, startSample, endSample, sequence); if (startSamples.length == 1) { intersectionMap.remove(earliestTrack); track2currentTime.remove(earliestTrack); // all sample written. } else { long[] nuStartSamples = new long[startSamples.length - 1]; System.arraycopy(startSamples, 1, nuStartSamples, 0, nuStartSamples.length); intersectionMap.put(earliestTrack, nuStartSamples); track2currentTime.put(earliestTrack, earliestTime); } sequence++; } /* sequence = 1; // this loop has two indices: for (int cycle = 0; cycle < maxNumberOfFragments; cycle++) { final List<Track> sortedTracks = sortTracksInSequence(movie.getTracks(), cycle, intersectionMap); for (Track track : sortedTracks) { long[] startSamples = intersectionMap.get(track); long startSample = startSamples[cycle]; // one based sample numbers - the first sample is 1 long endSample = cycle + 1 < startSamples.length ? startSamples[cycle + 1] : track.getSamples().size() + 1; createFragment(moofsMdats, track, startSample, endSample, sequence); sequence++; } }*/ return moofsMdats; } protected int createFragment(List<Box> moofsMdats, Track track, long startSample, long endSample, int sequence) { // if startSample == endSample the cycle is empty! if (startSample != endSample) { moofsMdats.add(createMoof(startSample, endSample, track, sequence)); moofsMdats.add(createMdat(startSample, endSample, track, sequence)); } return sequence; } /** * {@inheritDoc} */ public Container build(Movie movie) { LOG.debug("Creating movie " + movie); if (fragmenter == null) { fragmenter = new DefaultFragmenterImpl(2); } BasicContainer isoFile = new BasicContainer(); isoFile.addBox(createFtyp(movie)); //isoFile.addBox(createPdin(movie)); isoFile.addBox(createMoov(movie)); for (Box box : createMoofMdat(movie)) { isoFile.addBox(box); } isoFile.addBox(createMfra(movie, isoFile)); return isoFile; } protected Box createMdat(final long startSample, final long endSample, final Track track, final int i) { class Mdat implements Box { long size_ = -1; public long getSize() { if (size_ != -1) return size_; long size = 8; // I don't expect 2gig fragments for (Sample sample : getSamples(startSample, endSample, track)) { 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, track); for (Sample sample : samples) { sample.writeTo(writableByteChannel); } } } return new Mdat(); } protected void createTfhd(long startSample, long endSample, Track track, int sequenceNumber, TrackFragmentBox parent) { TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox(); SampleFlags sf = new SampleFlags(); tfhd.setDefaultSampleFlags(sf); tfhd.setBaseDataOffset(-1); tfhd.setTrackId(track.getTrackMetaData().getTrackId()); tfhd.setDefaultBaseIsMoof(true); parent.addBox(tfhd); } protected void createMfhd(long startSample, long endSample, Track track, int sequenceNumber, MovieFragmentBox parent) { MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox(); mfhd.setSequenceNumber(sequenceNumber); parent.addBox(mfhd); } protected void createTraf(long startSample, long endSample, Track track, int sequenceNumber, MovieFragmentBox parent) { TrackFragmentBox traf = new TrackFragmentBox(); parent.addBox(traf); createTfhd(startSample, endSample, track, sequenceNumber, traf); createTfdt(startSample, track, traf); createTrun(startSample, endSample, track, sequenceNumber, traf); if (track instanceof CencEncryptedTrack) { createSaiz(startSample, endSample, (CencEncryptedTrack) track, sequenceNumber, traf); createSenc(startSample, endSample, (CencEncryptedTrack) track, sequenceNumber, traf); createSaio(startSample, endSample, (CencEncryptedTrack) track, sequenceNumber, traf, parent); } 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()); sgpd.setGroupingType(type); 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 + 0x10001; } } 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 createSenc(long startSample, long endSample, CencEncryptedTrack track, int sequenceNumber, 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(long startSample, long endSample, CencEncryptedTrack track, int sequenceNumber, TrackFragmentBox parent, MovieFragmentBox moof) { SchemeTypeBox schm = Path.getPath(track.getSampleDescriptionBox(), "enc.[0]/sinf[0]/schm[0]"); 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(); } } 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, int sequenceNumber, 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 { assert tenc != null; saiz.setDefaultSampleInfoSize(tenc.getDefaultIvSize()); saiz.setSampleCount(l2i(endSample - startSample)); } parent.addBox(saiz); } /** * 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 * @param track source of the samples * @return a <code>List<Sample></code> of raw samples */ protected List<Sample> getSamples(long startSample, long endSample, Track track) { // since startSample and endSample are one-based substract 1 before addressing list elements return track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1); } /** * 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 * @param track source of the samples * @param sequenceNumber the fragment index of the requested list of samples * @return the sample sizes in the given interval */ protected long[] getSampleSizes(long startSample, long endSample, Track track, int sequenceNumber) { List<Sample> samples = getSamples(startSample, endSample, track); long[] sampleSizes = new long[samples.size()]; for (int i = 0; i < sampleSizes.length; i++) { sampleSizes[i] = samples.get(i).getSize(); } return sampleSizes; } 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); } /** * Creates one or more track 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 sequenceNumber the fragment index of the requested list of samples * @param parent the created box must be added to this box */ protected void createTrun(long startSample, long endSample, Track track, int sequenceNumber, TrackFragmentBox parent) { TrackRunBox trun = new TrackRunBox(); trun.setVersion(1); long[] sampleSizes = getSampleSizes(startSample, endSample, track, sequenceNumber); 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.getSampleIsDependedOn()); 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); } /** * 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 ParsableBox createMoof(long startSample, long endSample, Track track, int sequenceNumber) { MovieFragmentBox moof = new MovieFragmentBox(); createMfhd(startSample, endSample, track, sequenceNumber, moof); createTraf(startSample, endSample, track, sequenceNumber, 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; } /** * Creates a single 'mvhd' movie header box for a given movie. * * @param movie the concerned movie * @return an 'mvhd' box */ protected ParsableBox createMvhd(Movie movie) { 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 = movie.getTimescale(); mvhd.setTimescale(movieTimeScale); // find the next available trackId long nextTrackId = 0; for (Track track : movie.getTracks()) { nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId; } mvhd.setNextTrackId(++nextTrackId); return mvhd; } /** * Creates a fully populated 'moov' box with all child boxes. Child boxes are: * <ul> * <li>{@link #createMvhd(Movie) mvhd}</li> * <li>{@link #createMvex(Movie) mvex}</li> * <li>a {@link #createTrak(Track, Movie) trak} for every track</li> * </ul> * * @param movie the concerned movie * @return fully populated 'moov' */ protected ParsableBox createMoov(Movie movie) { MovieBox movieBox = new MovieBox(); movieBox.addBox(createMvhd(movie)); for (Track track : movie.getTracks()) { movieBox.addBox(createTrak(track, movie)); } movieBox.addBox(createMvex(movie)); // metadata here return movieBox; } /** * Creates a 'tfra' - track fragment random access box for the given track with the isoFile. * The tfra contains a map of random access points with time as key and offset within the isofile * as value. * * @param track the concerned track * @param isoFile the track is contained in * @return a track fragment random access box. */ protected Box createTfra(Track track, Container isoFile) { TrackFragmentRandomAccessBox tfra = new TrackFragmentRandomAccessBox(); tfra.setVersion(1); // use long offsets and times List<TrackFragmentRandomAccessBox.Entry> offset2timeEntries = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); TrackExtendsBox trex = null; List<TrackExtendsBox> trexs = Path.getPaths(isoFile, "moov/mvex/trex"); for (TrackExtendsBox innerTrex : trexs) { if (innerTrex.getTrackId() == track.getTrackMetaData().getTrackId()) { trex = innerTrex; } } long offset = 0; long duration = 0; for (Box box : isoFile.getBoxes()) { if (box instanceof MovieFragmentBox) { List<TrackFragmentBox> trafs = ((MovieFragmentBox) box).getBoxes(TrackFragmentBox.class); for (int i = 0; i < trafs.size(); i++) { TrackFragmentBox traf = trafs.get(i); if (traf.getTrackFragmentHeaderBox().getTrackId() == track.getTrackMetaData().getTrackId()) { // here we are at the offset required for the current entry. List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class); for (int j = 0; j < truns.size(); j++) { List<TrackFragmentRandomAccessBox.Entry> offset2timeEntriesThisTrun = new LinkedList<TrackFragmentRandomAccessBox.Entry>(); TrackRunBox trun = truns.get(j); for (int k = 0; k < trun.getEntries().size(); k++) { TrackRunBox.Entry trunEntry = trun.getEntries().get(k); SampleFlags sf; if (k == 0 && trun.isFirstSampleFlagsPresent()) { sf = trun.getFirstSampleFlags(); } else if (trun.isSampleFlagsPresent()) { sf = trunEntry.getSampleFlags(); } else { sf = trex.getDefaultSampleFlags(); } if (sf == null && track.getHandler().equals("vide")) { throw new RuntimeException("Cannot find SampleFlags for video track but it's required to build tfra"); } if (sf == null || sf.getSampleDependsOn() == 2) { offset2timeEntriesThisTrun.add(new TrackFragmentRandomAccessBox.Entry( duration, offset, i + 1, j + 1, k + 1)); } duration += trunEntry.getSampleDuration(); } if (offset2timeEntriesThisTrun.size() == trun.getEntries().size() && trun.getEntries().size() > 0) { // Oooops every sample seems to be random access sample // is this an audio track? I don't care. // I just use the first for trun sample for tfra random access offset2timeEntries.add(offset2timeEntriesThisTrun.get(0)); } else { offset2timeEntries.addAll(offset2timeEntriesThisTrun); } } } } } offset += box.getSize(); } tfra.setEntries(offset2timeEntries); tfra.setTrackId(track.getTrackMetaData().getTrackId()); return tfra; } /** * Creates a 'mfra' - movie fragment random access box for the given movie in the given * isofile. Uses {@link #createTfra(Track, Container)} * to generate the child boxes. * * @param movie concerned movie * @param isoFile concerned isofile * @return a complete 'mfra' box */ protected ParsableBox createMfra(Movie movie, Container isoFile) { MovieFragmentRandomAccessBox mfra = new MovieFragmentRandomAccessBox(); for (Track track : movie.getTracks()) { mfra.addBox(createTfra(track, isoFile)); } MovieFragmentRandomAccessOffsetBox mfro = new MovieFragmentRandomAccessOffsetBox(); mfra.addBox(mfro); mfro.setMfraSize(mfra.getSize()); return mfra; } protected ParsableBox createTrex(Movie movie, 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())) { // 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; } /** * Creates a 'mvex' - movie extends box and populates it with 'trex' boxes * by calling {@link #createTrex(Movie, Track)} * for each track to generate them * * @param movie the source movie * @return a complete 'mvex' */ protected ParsableBox createMvex(Movie movie) { MovieExtendsBox mvex = new MovieExtendsBox(); final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox(); mved.setVersion(1); for (Track track : movie.getTracks()) { final long trackDuration = getTrackDuration(movie, track); if (mved.getFragmentDuration() < trackDuration) { mved.setFragmentDuration(trackDuration); } } mvex.addBox(mved); for (Track track : movie.getTracks()) { mvex.addBox(createTrex(movie, track)); } return mvex; } protected ParsableBox createTkhd(Movie movie, 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 tkhd.setDuration(0);//no duration in moov for fragmented movies 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 ParsableBox createMdhd(Movie movie, 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; } protected ParsableBox createStbl(Movie movie, 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 ParsableBox createMinf(Track track, Movie movie) { 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(movie, track)); minf.addBox(createStbl(movie, track)); return minf; } protected ParsableBox createMdiaHdlr(Track track, Movie movie) { HandlerBox hdlr = new HandlerBox(); hdlr.setHandlerType(track.getHandler()); return hdlr; } protected ParsableBox createMdia(Track track, Movie movie) { MediaBox mdia = new MediaBox(); mdia.addBox(createMdhd(movie, track)); mdia.addBox(createMdiaHdlr(track, movie)); mdia.addBox(createMinf(track, movie)); return mdia; } protected ParsableBox createTrak(Track track, Movie movie) { LOG.debug("Creating Track " + track); TrackBox trackBox = new TrackBox(); trackBox.addBox(createTkhd(movie, track)); ParsableBox edts = createEdts(track, movie); if (edts != null) { trackBox.addBox(edts); } trackBox.addBox(createMdia(track, movie)); return trackBox; } protected ParsableBox createEdts(Track track, Movie movie) { 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() * movie.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 DataInformationBox createDinf(Movie movie, Track track) { DataInformationBox dinf = new DataInformationBox(); DataReferenceBox dref = new DataReferenceBox(); dinf.addBox(dref); DataEntryUrlBox url = new DataEntryUrlBox(); url.setFlags(1); dref.addBox(url); return dinf; } public Fragmenter getFragmenter() { return fragmenter; } public void setFragmenter(Fragmenter fragmenter) { this.fragmenter = fragmenter; } }