Home | History | Annotate | Download | only in builder
      1 /*
      2  * Copyright 2012 Sebastian Annies, Hamburg
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the License);
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an AS IS BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.googlecode.mp4parser.authoring.builder;
     17 
     18 import com.coremedia.iso.BoxParser;
     19 import com.coremedia.iso.IsoFile;
     20 import com.coremedia.iso.IsoTypeWriter;
     21 import com.coremedia.iso.boxes.*;
     22 import com.coremedia.iso.boxes.fragment.*;
     23 import com.googlecode.mp4parser.authoring.DateHelper;
     24 import com.googlecode.mp4parser.authoring.Movie;
     25 import com.googlecode.mp4parser.authoring.Track;
     26 
     27 import java.io.IOException;
     28 import java.nio.ByteBuffer;
     29 import java.nio.channels.GatheringByteChannel;
     30 import java.nio.channels.ReadableByteChannel;
     31 import java.nio.channels.WritableByteChannel;
     32 import java.util.*;
     33 import java.util.logging.Logger;
     34 
     35 import static com.googlecode.mp4parser.util.CastUtils.l2i;
     36 
     37 /**
     38  * Creates a fragmented MP4 file.
     39  */
     40 public class FragmentedMp4Builder implements Mp4Builder {
     41     private static final Logger LOG = Logger.getLogger(FragmentedMp4Builder.class.getName());
     42 
     43     protected FragmentIntersectionFinder intersectionFinder;
     44 
     45     public FragmentedMp4Builder() {
     46         this.intersectionFinder = new SyncSampleIntersectFinderImpl();
     47     }
     48 
     49     public List<String> getAllowedHandlers() {
     50         return Arrays.asList("soun", "vide");
     51     }
     52 
     53     public Box createFtyp(Movie movie) {
     54         List<String> minorBrands = new LinkedList<String>();
     55         minorBrands.add("isom");
     56         minorBrands.add("iso2");
     57         minorBrands.add("avc1");
     58         return new FileTypeBox("isom", 0, minorBrands);
     59     }
     60 
     61     /**
     62      * Some formats require sorting of the fragments. E.g. Ultraviolet CFF files are required
     63      * to contain the fragments size sort:
     64      * <ul>
     65      * <li>video[1].getBytes().length < audio[1].getBytes().length < subs[1].getBytes().length</li>
     66      * <li> audio[2].getBytes().length < video[2].getBytes().length < subs[2].getBytes().length</li>
     67      * </ul>
     68      *
     69      * make this fragment:
     70      *
     71      * <ol>
     72      *     <li>video[1]</li>
     73      *     <li>audio[1]</li>
     74      *     <li>subs[1]</li>
     75      *     <li>audio[2]</li>
     76      *     <li>video[2]</li>
     77      *     <li>subs[2]</li>
     78      * </ol>
     79      *
     80      * @param tracks the list of tracks to returned sorted
     81      * @param cycle current fragment (sorting may vary between the fragments)
     82      * @param intersectionMap a map from tracks to their fragments' first samples.
     83      * @return the list of tracks in order of appearance in the fragment
     84      */
     85     protected List<Track> sortTracksInSequence(List<Track> tracks, final int cycle, final Map<Track, long[]> intersectionMap) {
     86         tracks = new LinkedList<Track>(tracks);
     87         Collections.sort(tracks, new Comparator<Track>() {
     88             public int compare(Track o1, Track o2) {
     89                 long[] startSamples1 = intersectionMap.get(o1);
     90                 long startSample1 = startSamples1[cycle];
     91                 // one based sample numbers - the first sample is 1
     92                 long endSample1 = cycle + 1 < startSamples1.length ? startSamples1[cycle + 1] : o1.getSamples().size() + 1;
     93                 long[] startSamples2 = intersectionMap.get(o2);
     94                 long startSample2 = startSamples2[cycle];
     95                 // one based sample numbers - the first sample is 1
     96                 long endSample2 = cycle + 1 < startSamples2.length ? startSamples2[cycle + 1] : o2.getSamples().size() + 1;
     97                 List<ByteBuffer> samples1 = o1.getSamples().subList(l2i(startSample1) - 1, l2i(endSample1) - 1);
     98                 List<ByteBuffer> samples2 = o2.getSamples().subList(l2i(startSample2) - 1, l2i(endSample2) - 1);
     99                 int size1 = 0;
    100                 for (ByteBuffer byteBuffer : samples1) {
    101                     size1 += byteBuffer.limit();
    102                 }
    103                 int size2 = 0;
    104                 for (ByteBuffer byteBuffer : samples2) {
    105                     size2 += byteBuffer.limit();
    106                 }
    107                 return size1 - size2;
    108             }
    109         });
    110         return tracks;
    111     }
    112 
    113     protected List<Box> createMoofMdat(final Movie movie) {
    114         List<Box> boxes = new LinkedList<Box>();
    115         HashMap<Track, long[]> intersectionMap = new HashMap<Track, long[]>();
    116         int maxNumberOfFragments = 0;
    117         for (Track track : movie.getTracks()) {
    118             long[] intersects = intersectionFinder.sampleNumbers(track, movie);
    119             intersectionMap.put(track, intersects);
    120             maxNumberOfFragments = Math.max(maxNumberOfFragments, intersects.length);
    121         }
    122 
    123 
    124         int sequence = 1;
    125         // this loop has two indices:
    126 
    127         for (int cycle = 0; cycle < maxNumberOfFragments; cycle++) {
    128 
    129             final List<Track> sortedTracks = sortTracksInSequence(movie.getTracks(), cycle, intersectionMap);
    130 
    131             for (Track track : sortedTracks) {
    132                 if (getAllowedHandlers().isEmpty() || getAllowedHandlers().contains(track.getHandler())) {
    133                     long[] startSamples = intersectionMap.get(track);
    134                     //some tracks may have less fragments -> skip them
    135                     if (cycle < startSamples.length) {
    136 
    137                         long startSample = startSamples[cycle];
    138                         // one based sample numbers - the first sample is 1
    139                         long endSample = cycle + 1 < startSamples.length ? startSamples[cycle + 1] : track.getSamples().size() + 1;
    140 
    141                         // if startSample == endSample the cycle is empty!
    142                         if (startSample != endSample) {
    143                             boxes.add(createMoof(startSample, endSample, track, sequence));
    144                             boxes.add(createMdat(startSample, endSample, track, sequence++));
    145                         }
    146                     }
    147                 }
    148             }
    149         }
    150         return boxes;
    151     }
    152 
    153     /**
    154      * {@inheritDoc}
    155      */
    156     public IsoFile build(Movie movie) {
    157         LOG.fine("Creating movie " + movie);
    158         IsoFile isoFile = new IsoFile();
    159 
    160 
    161         isoFile.addBox(createFtyp(movie));
    162         isoFile.addBox(createMoov(movie));
    163 
    164         for (Box box : createMoofMdat(movie)) {
    165             isoFile.addBox(box);
    166         }
    167         isoFile.addBox(createMfra(movie, isoFile));
    168 
    169         return isoFile;
    170     }
    171 
    172     protected Box createMdat(final long startSample, final long endSample, final Track track, final int i) {
    173 
    174         class Mdat implements Box {
    175             ContainerBox parent;
    176 
    177             public ContainerBox getParent() {
    178                 return parent;
    179             }
    180 
    181             public void setParent(ContainerBox parent) {
    182                 this.parent = parent;
    183             }
    184 
    185             public long getSize() {
    186                 long size = 8; // I don't expect 2gig fragments
    187                 for (ByteBuffer sample : getSamples(startSample, endSample, track, i)) {
    188                     size += sample.limit();
    189                 }
    190                 return size;
    191             }
    192 
    193             public String getType() {
    194                 return "mdat";
    195             }
    196 
    197             public void getBox(WritableByteChannel writableByteChannel) throws IOException {
    198                 List<ByteBuffer> bbs = getSamples(startSample, endSample, track, i);
    199                 final List<ByteBuffer> samples = ByteBufferHelper.mergeAdjacentBuffers(bbs);
    200                 ByteBuffer header = ByteBuffer.allocate(8);
    201                 IsoTypeWriter.writeUInt32(header, l2i(getSize()));
    202                 header.put(IsoFile.fourCCtoBytes(getType()));
    203                 header.rewind();
    204                 writableByteChannel.write(header);
    205                 if (writableByteChannel instanceof GatheringByteChannel) {
    206 
    207                     int STEPSIZE = 1024;
    208                     // This is required to prevent android from crashing
    209                     // it seems that {@link GatheringByteChannel#write(java.nio.ByteBuffer[])}
    210                     // just handles up to 1024 buffers
    211                     for (int i = 0; i < Math.ceil((double) samples.size() / STEPSIZE); i++) {
    212                         List<ByteBuffer> sublist = samples.subList(
    213                                 i * STEPSIZE, // start
    214                                 (i + 1) * STEPSIZE < samples.size() ? (i + 1) * STEPSIZE : samples.size()); // end
    215                         ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]);
    216                         do {
    217                             ((GatheringByteChannel) writableByteChannel).write(sampleArray);
    218                         } while (sampleArray[sampleArray.length - 1].remaining() > 0);
    219                     }
    220                     //System.err.println(bytesWritten);
    221                 } else {
    222                     for (ByteBuffer sample : samples) {
    223                         sample.rewind();
    224                         writableByteChannel.write(sample);
    225                     }
    226                 }
    227 
    228             }
    229 
    230             public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
    231 
    232             }
    233         }
    234 
    235         return new Mdat();
    236     }
    237 
    238     protected Box createTfhd(long startSample, long endSample, Track track, int sequenceNumber) {
    239         TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox();
    240         SampleFlags sf = new SampleFlags();
    241 
    242         tfhd.setDefaultSampleFlags(sf);
    243         tfhd.setBaseDataOffset(-1);
    244         tfhd.setTrackId(track.getTrackMetaData().getTrackId());
    245         return tfhd;
    246     }
    247 
    248     protected Box createMfhd(long startSample, long endSample, Track track, int sequenceNumber) {
    249         MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox();
    250         mfhd.setSequenceNumber(sequenceNumber);
    251         return mfhd;
    252     }
    253 
    254     protected Box createTraf(long startSample, long endSample, Track track, int sequenceNumber) {
    255         TrackFragmentBox traf = new TrackFragmentBox();
    256         traf.addBox(createTfhd(startSample, endSample, track, sequenceNumber));
    257         for (Box trun : createTruns(startSample, endSample, track, sequenceNumber)) {
    258             traf.addBox(trun);
    259         }
    260 
    261         return traf;
    262     }
    263 
    264 
    265     /**
    266      * Gets the all samples starting with <code>startSample</code> (one based -> one is the first) and
    267      * ending with <code>endSample</code> (exclusive).
    268      *
    269      * @param startSample    low endpoint (inclusive) of the sample sequence
    270      * @param endSample      high endpoint (exclusive) of the sample sequence
    271      * @param track          source of the samples
    272      * @param sequenceNumber the fragment index of the requested list of samples
    273      * @return a <code>List&lt;ByteBuffer></code> of raw samples
    274      */
    275     protected List<ByteBuffer> getSamples(long startSample, long endSample, Track track, int sequenceNumber) {
    276         // since startSample and endSample are one-based substract 1 before addressing list elements
    277         return track.getSamples().subList(l2i(startSample) - 1, l2i(endSample) - 1);
    278     }
    279 
    280     /**
    281      * Gets the sizes of a sequence of samples-
    282      *
    283      * @param startSample    low endpoint (inclusive) of the sample sequence
    284      * @param endSample      high endpoint (exclusive) of the sample sequence
    285      * @param track          source of the samples
    286      * @param sequenceNumber the fragment index of the requested list of samples
    287      * @return
    288      */
    289     protected long[] getSampleSizes(long startSample, long endSample, Track track, int sequenceNumber) {
    290         List<ByteBuffer> samples = getSamples(startSample, endSample, track, sequenceNumber);
    291 
    292         long[] sampleSizes = new long[samples.size()];
    293         for (int i = 0; i < sampleSizes.length; i++) {
    294             sampleSizes[i] = samples.get(i).limit();
    295         }
    296         return sampleSizes;
    297     }
    298 
    299     /**
    300      * Creates one or more track run boxes for a given sequence.
    301      *
    302      * @param startSample    low endpoint (inclusive) of the sample sequence
    303      * @param endSample      high endpoint (exclusive) of the sample sequence
    304      * @param track          source of the samples
    305      * @param sequenceNumber the fragment index of the requested list of samples
    306      * @return the list of TrackRun boxes.
    307      */
    308     protected List<? extends Box> createTruns(long startSample, long endSample, Track track, int sequenceNumber) {
    309         TrackRunBox trun = new TrackRunBox();
    310         long[] sampleSizes = getSampleSizes(startSample, endSample, track, sequenceNumber);
    311 
    312         trun.setSampleDurationPresent(true);
    313         trun.setSampleSizePresent(true);
    314         List<TrackRunBox.Entry> entries = new ArrayList<TrackRunBox.Entry>(l2i(endSample - startSample));
    315 
    316 
    317         Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries());
    318         long left = startSample - 1;
    319         long curEntryLeft = timeQueue.peek().getCount();
    320         while (left > curEntryLeft) {
    321             left -= curEntryLeft;
    322             timeQueue.remove();
    323             curEntryLeft = timeQueue.peek().getCount();
    324         }
    325         curEntryLeft -= left;
    326 
    327 
    328         Queue<CompositionTimeToSample.Entry> compositionTimeQueue =
    329                 track.getCompositionTimeEntries() != null && track.getCompositionTimeEntries().size() > 0 ?
    330                         new LinkedList<CompositionTimeToSample.Entry>(track.getCompositionTimeEntries()) : null;
    331         long compositionTimeEntriesLeft = compositionTimeQueue != null ? compositionTimeQueue.peek().getCount() : -1;
    332 
    333 
    334         trun.setSampleCompositionTimeOffsetPresent(compositionTimeEntriesLeft > 0);
    335 
    336         // fast forward composition stuff
    337         for (long i = 1; i < startSample; i++) {
    338             if (compositionTimeQueue != null) {
    339                 //trun.setSampleCompositionTimeOffsetPresent(true);
    340                 if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) {
    341                     compositionTimeQueue.remove();
    342                     compositionTimeEntriesLeft = compositionTimeQueue.element().getCount();
    343                 }
    344             }
    345         }
    346 
    347         boolean sampleFlagsRequired = (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty() ||
    348                 track.getSyncSamples() != null && track.getSyncSamples().length != 0);
    349 
    350         trun.setSampleFlagsPresent(sampleFlagsRequired);
    351 
    352         for (int i = 0; i < sampleSizes.length; i++) {
    353             TrackRunBox.Entry entry = new TrackRunBox.Entry();
    354             entry.setSampleSize(sampleSizes[i]);
    355             if (sampleFlagsRequired) {
    356                 //if (false) {
    357                 SampleFlags sflags = new SampleFlags();
    358 
    359                 if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) {
    360                     SampleDependencyTypeBox.Entry e = track.getSampleDependencies().get(i);
    361                     sflags.setSampleDependsOn(e.getSampleDependsOn());
    362                     sflags.setSampleIsDependedOn(e.getSampleIsDependentOn());
    363                     sflags.setSampleHasRedundancy(e.getSampleHasRedundancy());
    364                 }
    365                 if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
    366                     // we have to mark non-sync samples!
    367                     if (Arrays.binarySearch(track.getSyncSamples(), startSample + i) >= 0) {
    368                         sflags.setSampleIsDifferenceSample(false);
    369                         sflags.setSampleDependsOn(2);
    370                     } else {
    371                         sflags.setSampleIsDifferenceSample(true);
    372                         sflags.setSampleDependsOn(1);
    373                     }
    374                 }
    375                 // i don't have sample degradation
    376                 entry.setSampleFlags(sflags);
    377 
    378             }
    379 
    380             entry.setSampleDuration(timeQueue.peek().getDelta());
    381             if (--curEntryLeft == 0 && timeQueue.size() > 1) {
    382                 timeQueue.remove();
    383                 curEntryLeft = timeQueue.peek().getCount();
    384             }
    385 
    386             if (compositionTimeQueue != null) {
    387                 entry.setSampleCompositionTimeOffset(compositionTimeQueue.peek().getOffset());
    388                 if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) {
    389                     compositionTimeQueue.remove();
    390                     compositionTimeEntriesLeft = compositionTimeQueue.element().getCount();
    391                 }
    392             }
    393             entries.add(entry);
    394         }
    395 
    396         trun.setEntries(entries);
    397 
    398         return Collections.singletonList(trun);
    399     }
    400 
    401     /**
    402      * Creates a 'moof' box for a given sequence of samples.
    403      *
    404      * @param startSample    low endpoint (inclusive) of the sample sequence
    405      * @param endSample      high endpoint (exclusive) of the sample sequence
    406      * @param track          source of the samples
    407      * @param sequenceNumber the fragment index of the requested list of samples
    408      * @return the list of TrackRun boxes.
    409      */
    410     protected Box createMoof(long startSample, long endSample, Track track, int sequenceNumber) {
    411         MovieFragmentBox moof = new MovieFragmentBox();
    412         moof.addBox(createMfhd(startSample, endSample, track, sequenceNumber));
    413         moof.addBox(createTraf(startSample, endSample, track, sequenceNumber));
    414 
    415         TrackRunBox firstTrun = moof.getTrackRunBoxes().get(0);
    416         firstTrun.setDataOffset(1); // dummy to make size correct
    417         firstTrun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size
    418 
    419         return moof;
    420     }
    421 
    422     /**
    423      * Creates a single 'mvhd' movie header box for a given movie.
    424      *
    425      * @param movie the concerned movie
    426      * @return an 'mvhd' box
    427      */
    428     protected Box createMvhd(Movie movie) {
    429         MovieHeaderBox mvhd = new MovieHeaderBox();
    430         mvhd.setVersion(1);
    431         mvhd.setCreationTime(DateHelper.convert(new Date()));
    432         mvhd.setModificationTime(DateHelper.convert(new Date()));
    433         long movieTimeScale = movie.getTimescale();
    434         long duration = 0;
    435 
    436         for (Track track : movie.getTracks()) {
    437             long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale();
    438             if (tracksDuration > duration) {
    439                 duration = tracksDuration;
    440             }
    441 
    442 
    443         }
    444 
    445         mvhd.setDuration(duration);
    446         mvhd.setTimescale(movieTimeScale);
    447         // find the next available trackId
    448         long nextTrackId = 0;
    449         for (Track track : movie.getTracks()) {
    450             nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId;
    451         }
    452         mvhd.setNextTrackId(++nextTrackId);
    453         return mvhd;
    454     }
    455 
    456     /**
    457      * Creates a fully populated 'moov' box with all child boxes. Child boxes are:
    458      * <ul>
    459      * <li>{@link #createMvhd(com.googlecode.mp4parser.authoring.Movie) mvhd}</li>
    460      * <li>{@link #createMvex(com.googlecode.mp4parser.authoring.Movie)  mvex}</li>
    461      * <li>a {@link #createTrak(com.googlecode.mp4parser.authoring.Track, com.googlecode.mp4parser.authoring.Movie)  trak} for every track</li>
    462      * </ul>
    463      *
    464      * @param movie the concerned movie
    465      * @return fully populated 'moov'
    466      */
    467     protected Box createMoov(Movie movie) {
    468         MovieBox movieBox = new MovieBox();
    469 
    470         movieBox.addBox(createMvhd(movie));
    471         movieBox.addBox(createMvex(movie));
    472 
    473         for (Track track : movie.getTracks()) {
    474             movieBox.addBox(createTrak(track, movie));
    475         }
    476         // metadata here
    477         return movieBox;
    478 
    479     }
    480 
    481     /**
    482      * Creates a 'tfra' - track fragment random access box for the given track with the isoFile.
    483      * The tfra contains a map of random access points with time as key and offset within the isofile
    484      * as value.
    485      *
    486      * @param track   the concerned track
    487      * @param isoFile the track is contained in
    488      * @return a track fragment random access box.
    489      */
    490     protected Box createTfra(Track track, IsoFile isoFile) {
    491         TrackFragmentRandomAccessBox tfra = new TrackFragmentRandomAccessBox();
    492         tfra.setVersion(1); // use long offsets and times
    493         List<TrackFragmentRandomAccessBox.Entry> offset2timeEntries = new LinkedList<TrackFragmentRandomAccessBox.Entry>();
    494         List<Box> boxes = isoFile.getBoxes();
    495         long offset = 0;
    496         long duration = 0;
    497         for (Box box : boxes) {
    498             if (box instanceof MovieFragmentBox) {
    499                 List<TrackFragmentBox> trafs = ((MovieFragmentBox) box).getBoxes(TrackFragmentBox.class);
    500                 for (int i = 0; i < trafs.size(); i++) {
    501                     TrackFragmentBox traf = trafs.get(i);
    502                     if (traf.getTrackFragmentHeaderBox().getTrackId() == track.getTrackMetaData().getTrackId()) {
    503                         // here we are at the offset required for the current entry.
    504                         List<TrackRunBox> truns = traf.getBoxes(TrackRunBox.class);
    505                         for (int j = 0; j < truns.size(); j++) {
    506                             List<TrackFragmentRandomAccessBox.Entry> offset2timeEntriesThisTrun = new LinkedList<TrackFragmentRandomAccessBox.Entry>();
    507                             TrackRunBox trun = truns.get(j);
    508                             for (int k = 0; k < trun.getEntries().size(); k++) {
    509                                 TrackRunBox.Entry trunEntry = trun.getEntries().get(k);
    510                                 SampleFlags sf = null;
    511                                 if (k == 0 && trun.isFirstSampleFlagsPresent()) {
    512                                     sf = trun.getFirstSampleFlags();
    513                                 } else if (trun.isSampleFlagsPresent()) {
    514                                     sf = trunEntry.getSampleFlags();
    515                                 } else {
    516                                     List<MovieExtendsBox> mvexs = isoFile.getMovieBox().getBoxes(MovieExtendsBox.class);
    517                                     for (MovieExtendsBox mvex : mvexs) {
    518                                         List<TrackExtendsBox> trexs = mvex.getBoxes(TrackExtendsBox.class);
    519                                         for (TrackExtendsBox trex : trexs) {
    520                                             if (trex.getTrackId() == track.getTrackMetaData().getTrackId()) {
    521                                                 sf = trex.getDefaultSampleFlags();
    522                                             }
    523                                         }
    524                                     }
    525 
    526                                 }
    527                                 if (sf == null) {
    528                                     throw new RuntimeException("Could not find any SampleFlags to indicate random access or not");
    529                                 }
    530                                 if (sf.getSampleDependsOn() == 2) {
    531                                     offset2timeEntriesThisTrun.add(new TrackFragmentRandomAccessBox.Entry(
    532                                             duration,
    533                                             offset,
    534                                             i + 1, j + 1, k + 1));
    535                                 }
    536                                 duration += trunEntry.getSampleDuration();
    537                             }
    538                             if (offset2timeEntriesThisTrun.size() == trun.getEntries().size() && trun.getEntries().size() > 0) {
    539                                 // Oooops every sample seems to be random access sample
    540                                 // is this an audio track? I don't care.
    541                                 // I just use the first for trun sample for tfra random access
    542                                 offset2timeEntries.add(offset2timeEntriesThisTrun.get(0));
    543                             } else {
    544                                 offset2timeEntries.addAll(offset2timeEntriesThisTrun);
    545                             }
    546                         }
    547                     }
    548                 }
    549             }
    550 
    551 
    552             offset += box.getSize();
    553         }
    554         tfra.setEntries(offset2timeEntries);
    555         tfra.setTrackId(track.getTrackMetaData().getTrackId());
    556         return tfra;
    557     }
    558 
    559     /**
    560      * Creates a 'mfra' - movie fragment random access box for the given movie in the given
    561      * isofile. Uses {@link #createTfra(com.googlecode.mp4parser.authoring.Track, com.coremedia.iso.IsoFile)}
    562      * to generate the child boxes.
    563      *
    564      * @param movie   concerned movie
    565      * @param isoFile concerned isofile
    566      * @return a complete 'mfra' box
    567      */
    568     protected Box createMfra(Movie movie, IsoFile isoFile) {
    569         MovieFragmentRandomAccessBox mfra = new MovieFragmentRandomAccessBox();
    570         for (Track track : movie.getTracks()) {
    571             mfra.addBox(createTfra(track, isoFile));
    572         }
    573 
    574         MovieFragmentRandomAccessOffsetBox mfro = new MovieFragmentRandomAccessOffsetBox();
    575         mfra.addBox(mfro);
    576         mfro.setMfraSize(mfra.getSize());
    577         return mfra;
    578     }
    579 
    580     protected Box createTrex(Movie movie, Track track) {
    581         TrackExtendsBox trex = new TrackExtendsBox();
    582         trex.setTrackId(track.getTrackMetaData().getTrackId());
    583         trex.setDefaultSampleDescriptionIndex(1);
    584         trex.setDefaultSampleDuration(0);
    585         trex.setDefaultSampleSize(0);
    586         SampleFlags sf = new SampleFlags();
    587         if ("soun".equals(track.getHandler())) {
    588             // as far as I know there is no audio encoding
    589             // where the sample are not self contained.
    590             sf.setSampleDependsOn(2);
    591             sf.setSampleIsDependedOn(2);
    592         }
    593         trex.setDefaultSampleFlags(sf);
    594         return trex;
    595     }
    596 
    597     /**
    598      * Creates a 'mvex' - movie extends box and populates it with 'trex' boxes
    599      * by calling {@link #createTrex(com.googlecode.mp4parser.authoring.Movie, com.googlecode.mp4parser.authoring.Track)}
    600      * for each track to generate them
    601      *
    602      * @param movie the source movie
    603      * @return a complete 'mvex'
    604      */
    605     protected Box createMvex(Movie movie) {
    606         MovieExtendsBox mvex = new MovieExtendsBox();
    607         final MovieExtendsHeaderBox mved = new MovieExtendsHeaderBox();
    608         for (Track track : movie.getTracks()) {
    609             final long trackDuration = getTrackDuration(movie, track);
    610             if (mved.getFragmentDuration() < trackDuration) {
    611                 mved.setFragmentDuration(trackDuration);
    612             }
    613         }
    614         mvex.addBox(mved);
    615 
    616         for (Track track : movie.getTracks()) {
    617             mvex.addBox(createTrex(movie, track));
    618         }
    619         return mvex;
    620     }
    621 
    622     protected Box createTkhd(Movie movie, Track track) {
    623         TrackHeaderBox tkhd = new TrackHeaderBox();
    624         tkhd.setVersion(1);
    625         int flags = 0;
    626         if (track.isEnabled()) {
    627             flags += 1;
    628         }
    629 
    630         if (track.isInMovie()) {
    631             flags += 2;
    632         }
    633 
    634         if (track.isInPreview()) {
    635             flags += 4;
    636         }
    637 
    638         if (track.isInPoster()) {
    639             flags += 8;
    640         }
    641         tkhd.setFlags(flags);
    642 
    643         tkhd.setAlternateGroup(track.getTrackMetaData().getGroup());
    644         tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
    645         // We need to take edit list box into account in trackheader duration
    646         // but as long as I don't support edit list boxes it is sufficient to
    647         // just translate media duration to movie timescale
    648         tkhd.setDuration(getTrackDuration(movie, track));
    649         tkhd.setHeight(track.getTrackMetaData().getHeight());
    650         tkhd.setWidth(track.getTrackMetaData().getWidth());
    651         tkhd.setLayer(track.getTrackMetaData().getLayer());
    652         tkhd.setModificationTime(DateHelper.convert(new Date()));
    653         tkhd.setTrackId(track.getTrackMetaData().getTrackId());
    654         tkhd.setVolume(track.getTrackMetaData().getVolume());
    655         return tkhd;
    656     }
    657 
    658     private long getTrackDuration(Movie movie, Track track) {
    659         return getDuration(track) * movie.getTimescale() / track.getTrackMetaData().getTimescale();
    660     }
    661 
    662     protected Box createMdhd(Movie movie, Track track) {
    663         MediaHeaderBox mdhd = new MediaHeaderBox();
    664         mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
    665         mdhd.setDuration(getDuration(track));
    666         mdhd.setTimescale(track.getTrackMetaData().getTimescale());
    667         mdhd.setLanguage(track.getTrackMetaData().getLanguage());
    668         return mdhd;
    669     }
    670 
    671     protected Box createStbl(Movie movie, Track track) {
    672         SampleTableBox stbl = new SampleTableBox();
    673 
    674         stbl.addBox(track.getSampleDescriptionBox());
    675         stbl.addBox(new TimeToSampleBox());
    676         //stbl.addBox(new SampleToChunkBox());
    677         stbl.addBox(new StaticChunkOffsetBox());
    678         return stbl;
    679     }
    680 
    681     protected Box createMinf(Track track, Movie movie) {
    682         MediaInformationBox minf = new MediaInformationBox();
    683         minf.addBox(track.getMediaHeaderBox());
    684         minf.addBox(createDinf(movie, track));
    685         minf.addBox(createStbl(movie, track));
    686         return minf;
    687     }
    688 
    689     protected Box createMdiaHdlr(Track track, Movie movie) {
    690         HandlerBox hdlr = new HandlerBox();
    691         hdlr.setHandlerType(track.getHandler());
    692         return hdlr;
    693     }
    694 
    695     protected Box createMdia(Track track, Movie movie) {
    696         MediaBox mdia = new MediaBox();
    697         mdia.addBox(createMdhd(movie, track));
    698 
    699 
    700         mdia.addBox(createMdiaHdlr(track, movie));
    701 
    702 
    703         mdia.addBox(createMinf(track, movie));
    704         return mdia;
    705     }
    706 
    707     protected Box createTrak(Track track, Movie movie) {
    708         LOG.fine("Creating Track " + track);
    709         TrackBox trackBox = new TrackBox();
    710         trackBox.addBox(createTkhd(movie, track));
    711         trackBox.addBox(createMdia(track, movie));
    712         return trackBox;
    713     }
    714 
    715     protected DataInformationBox createDinf(Movie movie, Track track) {
    716         DataInformationBox dinf = new DataInformationBox();
    717         DataReferenceBox dref = new DataReferenceBox();
    718         dinf.addBox(dref);
    719         DataEntryUrlBox url = new DataEntryUrlBox();
    720         url.setFlags(1);
    721         dref.addBox(url);
    722         return dinf;
    723     }
    724 
    725     public FragmentIntersectionFinder getFragmentIntersectionFinder() {
    726         return intersectionFinder;
    727     }
    728 
    729     public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) {
    730         this.intersectionFinder = intersectionFinder;
    731     }
    732 
    733     protected long getDuration(Track track) {
    734         long duration = 0;
    735         for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
    736             duration += entry.getCount() * entry.getDelta();
    737         }
    738         return duration;
    739     }
    740 
    741 
    742 }
    743