DefaultMp4Builder.java revision 36e04c3847e93ccf4c3e0cde617eecea72c2605d
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.Box;
22import com.coremedia.iso.boxes.CompositionTimeToSample;
23import com.coremedia.iso.boxes.ContainerBox;
24import com.coremedia.iso.boxes.DataEntryUrlBox;
25import com.coremedia.iso.boxes.DataInformationBox;
26import com.coremedia.iso.boxes.DataReferenceBox;
27import com.coremedia.iso.boxes.FileTypeBox;
28import com.coremedia.iso.boxes.HandlerBox;
29import com.coremedia.iso.boxes.MediaBox;
30import com.coremedia.iso.boxes.MediaHeaderBox;
31import com.coremedia.iso.boxes.MediaInformationBox;
32import com.coremedia.iso.boxes.MovieBox;
33import com.coremedia.iso.boxes.MovieHeaderBox;
34import com.coremedia.iso.boxes.SampleDependencyTypeBox;
35import com.coremedia.iso.boxes.SampleSizeBox;
36import com.coremedia.iso.boxes.SampleTableBox;
37import com.coremedia.iso.boxes.SampleToChunkBox;
38import com.coremedia.iso.boxes.StaticChunkOffsetBox;
39import com.coremedia.iso.boxes.SyncSampleBox;
40import com.coremedia.iso.boxes.TimeToSampleBox;
41import com.coremedia.iso.boxes.TrackBox;
42import com.coremedia.iso.boxes.TrackHeaderBox;
43import com.googlecode.mp4parser.authoring.DateHelper;
44import com.googlecode.mp4parser.authoring.Movie;
45import com.googlecode.mp4parser.authoring.Track;
46
47import java.io.IOException;
48import java.nio.ByteBuffer;
49import java.nio.MappedByteBuffer;
50import java.nio.channels.GatheringByteChannel;
51import java.nio.channels.ReadableByteChannel;
52import java.nio.channels.WritableByteChannel;
53import java.util.ArrayList;
54import java.util.Date;
55import java.util.HashMap;
56import java.util.HashSet;
57import java.util.LinkedList;
58import java.util.List;
59import java.util.Map;
60import java.util.Set;
61import java.util.logging.Level;
62import java.util.logging.Logger;
63
64import static com.googlecode.mp4parser.util.CastUtils.l2i;
65
66/**
67 * Creates a plain MP4 file from a video. Plain as plain can be.
68 */
69public class DefaultMp4Builder implements Mp4Builder {
70
71    public int STEPSIZE = 64;
72    Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>();
73    private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName());
74
75    HashMap<Track, List<ByteBuffer>> track2Sample = new HashMap<Track, List<ByteBuffer>>();
76    HashMap<Track, long[]> track2SampleSizes = new HashMap<Track, long[]>();
77    private FragmentIntersectionFinder intersectionFinder = new TwoSecondIntersectionFinder();
78
79    public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) {
80        this.intersectionFinder = intersectionFinder;
81    }
82
83    /**
84     * {@inheritDoc}
85     */
86    public IsoFile build(Movie movie) {
87        LOG.fine("Creating movie " + movie);
88        for (Track track : movie.getTracks()) {
89            // getting the samples may be a time consuming activity
90            List<ByteBuffer> samples = track.getSamples();
91            putSamples(track, samples);
92            long[] sizes = new long[samples.size()];
93            for (int i = 0; i < sizes.length; i++) {
94                sizes[i] = samples.get(i).limit();
95            }
96            putSampleSizes(track, sizes);
97        }
98
99        IsoFile isoFile = new IsoFile();
100        // ouch that is ugly but I don't know how to do it else
101        List<String> minorBrands = new LinkedList<String>();
102        minorBrands.add("isom");
103        minorBrands.add("iso2");
104        minorBrands.add("avc1");
105
106        isoFile.addBox(new FileTypeBox("isom", 0, minorBrands));
107        isoFile.addBox(createMovieBox(movie));
108        InterleaveChunkMdat mdat = new InterleaveChunkMdat(movie);
109        isoFile.addBox(mdat);
110
111        /*
112        dataOffset is where the first sample starts. In this special mdat the samples always start
113        at offset 16 so that we can use the same offset for large boxes and small boxes
114         */
115        long dataOffset = mdat.getDataOffset();
116        for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
117            long[] offsets = chunkOffsetBox.getChunkOffsets();
118            for (int i = 0; i < offsets.length; i++) {
119                offsets[i] += dataOffset;
120            }
121        }
122
123
124        return isoFile;
125    }
126
127    public FragmentIntersectionFinder getFragmentIntersectionFinder() {
128        throw new UnsupportedOperationException("No fragment intersection finder in default MP4 builder!");
129    }
130
131    protected long[] putSampleSizes(Track track, long[] sizes) {
132        return track2SampleSizes.put(track, sizes);
133    }
134
135    protected List<ByteBuffer> putSamples(Track track, List<ByteBuffer> samples) {
136        return track2Sample.put(track, samples);
137    }
138
139    private MovieBox createMovieBox(Movie movie) {
140        MovieBox movieBox = new MovieBox();
141        MovieHeaderBox mvhd = new MovieHeaderBox();
142
143        mvhd.setCreationTime(DateHelper.convert(new Date()));
144        mvhd.setModificationTime(DateHelper.convert(new Date()));
145
146        long movieTimeScale = getTimescale(movie);
147        long duration = 0;
148
149        for (Track track : movie.getTracks()) {
150            long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale();
151            if (tracksDuration > duration) {
152                duration = tracksDuration;
153            }
154
155
156        }
157
158        mvhd.setDuration(duration);
159        mvhd.setTimescale(movieTimeScale);
160        // find the next available trackId
161        long nextTrackId = 0;
162        for (Track track : movie.getTracks()) {
163            nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId;
164        }
165        mvhd.setNextTrackId(++nextTrackId);
166        if (mvhd.getCreationTime() >= 1l << 32 ||
167                mvhd.getModificationTime() >= 1l << 32 ||
168                mvhd.getDuration() >= 1l << 32) {
169            mvhd.setVersion(1);
170        }
171
172        movieBox.addBox(mvhd);
173        for (Track track : movie.getTracks()) {
174            movieBox.addBox(createTrackBox(track, movie));
175        }
176        // metadata here
177        Box udta = createUdta(movie);
178        if (udta != null) {
179            movieBox.addBox(udta);
180        }
181        return movieBox;
182
183    }
184
185    /**
186     * Override to create a user data box that may contain metadata.
187     *
188     * @return a 'udta' box or <code>null</code> if none provided
189     */
190    protected Box createUdta(Movie movie) {
191        return null;
192    }
193
194    private TrackBox createTrackBox(Track track, Movie movie) {
195
196        LOG.info("Creating Mp4TrackImpl " + track);
197        TrackBox trackBox = new TrackBox();
198        TrackHeaderBox tkhd = new TrackHeaderBox();
199        int flags = 0;
200        if (track.isEnabled()) {
201            flags += 1;
202        }
203
204        if (track.isInMovie()) {
205            flags += 2;
206        }
207
208        if (track.isInPreview()) {
209            flags += 4;
210        }
211
212        if (track.isInPoster()) {
213            flags += 8;
214        }
215        tkhd.setFlags(flags);
216
217        tkhd.setAlternateGroup(track.getTrackMetaData().getGroup());
218        tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
219        // We need to take edit list box into account in trackheader duration
220        // but as long as I don't support edit list boxes it is sufficient to
221        // just translate media duration to movie timescale
222        tkhd.setDuration(getDuration(track) * getTimescale(movie) / track.getTrackMetaData().getTimescale());
223        tkhd.setHeight(track.getTrackMetaData().getHeight());
224        tkhd.setWidth(track.getTrackMetaData().getWidth());
225        tkhd.setLayer(track.getTrackMetaData().getLayer());
226        tkhd.setModificationTime(DateHelper.convert(new Date()));
227        tkhd.setTrackId(track.getTrackMetaData().getTrackId());
228        tkhd.setVolume(track.getTrackMetaData().getVolume());
229        tkhd.setMatrix(track.getTrackMetaData().getMatrix());
230        if (tkhd.getCreationTime() >= 1l << 32 ||
231                tkhd.getModificationTime() >= 1l << 32 ||
232                tkhd.getDuration() >= 1l << 32) {
233            tkhd.setVersion(1);
234        }
235
236        trackBox.addBox(tkhd);
237
238/*
239        EditBox edit = new EditBox();
240        EditListBox editListBox = new EditListBox();
241        editListBox.setEntries(Collections.singletonList(
242                new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1)));
243        edit.addBox(editListBox);
244        trackBox.addBox(edit);
245*/
246
247        MediaBox mdia = new MediaBox();
248        trackBox.addBox(mdia);
249        MediaHeaderBox mdhd = new MediaHeaderBox();
250        mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
251        mdhd.setDuration(getDuration(track));
252        mdhd.setTimescale(track.getTrackMetaData().getTimescale());
253        mdhd.setLanguage(track.getTrackMetaData().getLanguage());
254        mdia.addBox(mdhd);
255        HandlerBox hdlr = new HandlerBox();
256        mdia.addBox(hdlr);
257
258        hdlr.setHandlerType(track.getHandler());
259
260        MediaInformationBox minf = new MediaInformationBox();
261        minf.addBox(track.getMediaHeaderBox());
262
263        // dinf: all these three boxes tell us is that the actual
264        // data is in the current file and not somewhere external
265        DataInformationBox dinf = new DataInformationBox();
266        DataReferenceBox dref = new DataReferenceBox();
267        dinf.addBox(dref);
268        DataEntryUrlBox url = new DataEntryUrlBox();
269        url.setFlags(1);
270        dref.addBox(url);
271        minf.addBox(dinf);
272        //
273
274        SampleTableBox stbl = new SampleTableBox();
275
276        stbl.addBox(track.getSampleDescriptionBox());
277
278        List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries();
279        if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) {
280            TimeToSampleBox stts = new TimeToSampleBox();
281            stts.setEntries(track.getDecodingTimeEntries());
282            stbl.addBox(stts);
283        }
284
285        List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries();
286        if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) {
287            CompositionTimeToSample ctts = new CompositionTimeToSample();
288            ctts.setEntries(compositionTimeToSampleEntries);
289            stbl.addBox(ctts);
290        }
291
292        long[] syncSamples = track.getSyncSamples();
293        if (syncSamples != null && syncSamples.length > 0) {
294            SyncSampleBox stss = new SyncSampleBox();
295            stss.setSampleNumber(syncSamples);
296            stbl.addBox(stss);
297        }
298
299        if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) {
300            SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox();
301            sdtp.setEntries(track.getSampleDependencies());
302            stbl.addBox(sdtp);
303        }
304        HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>();
305        for (Track current : movie.getTracks()) {
306            track2ChunkSizes.put(current, getChunkSizes(current, movie));
307        }
308        int[] tracksChunkSizes = track2ChunkSizes.get(track);
309
310        SampleToChunkBox stsc = new SampleToChunkBox();
311        stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>());
312        long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size
313        for (int i = 0; i < tracksChunkSizes.length; i++) {
314            // The sample description index references the sample description box
315            // that describes the samples of this chunk. My Tracks cannot have more
316            // than one sample description box. Therefore 1 is always right
317            // the first chunk has the number '1'
318            if (lastChunkSize != tracksChunkSizes[i]) {
319                stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1));
320                lastChunkSize = tracksChunkSizes[i];
321            }
322        }
323        stbl.addBox(stsc);
324
325        SampleSizeBox stsz = new SampleSizeBox();
326        stsz.setSampleSizes(track2SampleSizes.get(track));
327
328        stbl.addBox(stsz);
329        // The ChunkOffsetBox we create here is just a stub
330        // since we haven't created the whole structure we can't tell where the
331        // first chunk starts (mdat box). So I just let the chunk offset
332        // start at zero and I will add the mdat offset later.
333        StaticChunkOffsetBox stco = new StaticChunkOffsetBox();
334        this.chunkOffsetBoxes.add(stco);
335        long offset = 0;
336        long[] chunkOffset = new long[tracksChunkSizes.length];
337        // all tracks have the same number of chunks
338        if (LOG.isLoggable(Level.FINE)) {
339            LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId());
340        }
341
342
343        for (int i = 0; i < tracksChunkSizes.length; i++) {
344            // The filelayout will be:
345            // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n
346            // calculating the offsets
347            if (LOG.isLoggable(Level.FINER)) {
348                LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i);
349            }
350            for (Track current : movie.getTracks()) {
351                if (LOG.isLoggable(Level.FINEST)) {
352                    LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId());
353                }
354                int[] chunkSizes = track2ChunkSizes.get(current);
355                long firstSampleOfChunk = 0;
356                for (int j = 0; j < i; j++) {
357                    firstSampleOfChunk += chunkSizes[j];
358                }
359                if (current == track) {
360                    chunkOffset[i] = offset;
361                }
362                for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
363                    offset += track2SampleSizes.get(current)[j];
364                }
365            }
366        }
367        stco.setChunkOffsets(chunkOffset);
368        stbl.addBox(stco);
369        minf.addBox(stbl);
370        mdia.addBox(minf);
371
372        return trackBox;
373    }
374
375    private class InterleaveChunkMdat implements Box {
376        List<Track> tracks;
377        List<ByteBuffer> samples = new ArrayList<ByteBuffer>();
378        ContainerBox parent;
379
380        long contentSize = 0;
381
382        public ContainerBox getParent() {
383            return parent;
384        }
385
386        public void setParent(ContainerBox parent) {
387            this.parent = parent;
388        }
389
390        public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
391        }
392
393        private InterleaveChunkMdat(Movie movie) {
394
395            tracks = movie.getTracks();
396            Map<Track, int[]> chunks = new HashMap<Track, int[]>();
397            for (Track track : movie.getTracks()) {
398                chunks.put(track, getChunkSizes(track, movie));
399            }
400
401            for (int i = 0; i < chunks.values().iterator().next().length; i++) {
402                for (Track track : tracks) {
403
404                    int[] chunkSizes = chunks.get(track);
405                    long firstSampleOfChunk = 0;
406                    for (int j = 0; j < i; j++) {
407                        firstSampleOfChunk += chunkSizes[j];
408                    }
409
410                    for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
411
412                        ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j);
413                        contentSize += s.limit();
414                        samples.add((ByteBuffer) s.rewind());
415                    }
416
417                }
418
419            }
420
421        }
422
423        public long getDataOffset() {
424            Box b = this;
425            long offset = 16;
426            while (b.getParent() != null) {
427                for (Box box : b.getParent().getBoxes()) {
428                    if (b == box) {
429                        break;
430                    }
431                    offset += box.getSize();
432                }
433                b = b.getParent();
434            }
435            return offset;
436        }
437
438
439        public String getType() {
440            return "mdat";
441        }
442
443        public long getSize() {
444            return 16 + contentSize;
445        }
446
447        private boolean isSmallBox(long contentSize) {
448            return (contentSize + 8) < 4294967296L;
449        }
450
451
452        public void getBox(WritableByteChannel writableByteChannel) throws IOException {
453            ByteBuffer bb = ByteBuffer.allocate(16);
454            long size = getSize();
455            if (isSmallBox(size)) {
456                IsoTypeWriter.writeUInt32(bb, size);
457            } else {
458                IsoTypeWriter.writeUInt32(bb, 1);
459            }
460            bb.put(IsoFile.fourCCtoBytes("mdat"));
461            if (isSmallBox(size)) {
462                bb.put(new byte[8]);
463            } else {
464                IsoTypeWriter.writeUInt64(bb, size);
465            }
466            bb.rewind();
467            writableByteChannel.write(bb);
468            if (writableByteChannel instanceof GatheringByteChannel) {
469                List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples);
470
471
472                for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) {
473                    List<ByteBuffer> sublist = nuSamples.subList(
474                            i * STEPSIZE, // start
475                            (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end
476                    ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]);
477                    do {
478                        ((GatheringByteChannel) writableByteChannel).write(sampleArray);
479                    } while (sampleArray[sampleArray.length - 1].remaining() > 0);
480                }
481                //System.err.println(bytesWritten);
482            } else {
483                for (ByteBuffer sample : samples) {
484                    sample.rewind();
485                    writableByteChannel.write(sample);
486                }
487            }
488        }
489
490    }
491
492    /**
493     * Gets the chunk sizes for the given track.
494     *
495     * @param track
496     * @param movie
497     * @return
498     */
499    int[] getChunkSizes(Track track, Movie movie) {
500
501        long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie);
502        int[] chunkSizes = new int[referenceChunkStarts.length];
503
504
505        for (int i = 0; i < referenceChunkStarts.length; i++) {
506            long start = referenceChunkStarts[i] - 1;
507            long end;
508            if (referenceChunkStarts.length == i + 1) {
509                end = track.getSamples().size();
510            } else {
511                end = referenceChunkStarts[i + 1] - 1;
512            }
513
514            chunkSizes[i] = l2i(end - start);
515            // The Stretch makes sure that there are as much audio and video chunks!
516        }
517        assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal";
518        return chunkSizes;
519
520
521    }
522
523
524    private static long sum(int[] ls) {
525        long rc = 0;
526        for (long l : ls) {
527            rc += l;
528        }
529        return rc;
530    }
531
532    protected static long getDuration(Track track) {
533        long duration = 0;
534        for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
535            duration += entry.getCount() * entry.getDelta();
536        }
537        return duration;
538    }
539
540    public long getTimescale(Movie movie) {
541        long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale();
542        for (Track track : movie.getTracks()) {
543            timescale = gcd(track.getTrackMetaData().getTimescale(), timescale);
544        }
545        return timescale;
546    }
547
548    public static long gcd(long a, long b) {
549        if (b == 0) {
550            return a;
551        }
552        return gcd(b, a % b);
553    }
554
555    public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) {
556        ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size());
557        for (ByteBuffer buffer : samples) {
558            int lastIndex = nuSamples.size() - 1;
559            if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() &&
560                    nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) {
561                ByteBuffer oldBuffer = nuSamples.remove(lastIndex);
562                ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice();
563                // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset.
564                nuSamples.add(nu);
565            } else if (lastIndex >= 0 &&
566                    buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer &&
567                    nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) {
568                // This can go wrong - but will it?
569                ByteBuffer oldBuffer = nuSamples.get(lastIndex);
570                oldBuffer.limit(buffer.limit() + oldBuffer.limit());
571            } else {
572                nuSamples.add(buffer);
573            }
574        }
575        return nuSamples;
576    }
577}
578