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 */
16package com.googlecode.mp4parser.authoring.builder;
17
18import com.coremedia.iso.BoxParser;
19import com.coremedia.iso.IsoFile;
20import com.coremedia.iso.IsoTypeWriter;
21import com.coremedia.iso.boxes.*;
22import com.coremedia.iso.boxes.fragment.*;
23import com.googlecode.mp4parser.authoring.DateHelper;
24import com.googlecode.mp4parser.authoring.Movie;
25import com.googlecode.mp4parser.authoring.Track;
26
27import java.io.IOException;
28import java.nio.ByteBuffer;
29import java.nio.channels.GatheringByteChannel;
30import java.nio.channels.ReadableByteChannel;
31import java.nio.channels.WritableByteChannel;
32import java.util.*;
33import java.util.logging.Logger;
34
35import static com.googlecode.mp4parser.util.CastUtils.l2i;
36
37/**
38 * Creates a fragmented MP4 file.
39 */
40public 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