DefaultMp4Builder.java revision dd9eb897ee7c7b507cbdcf80263bb4b5de6966bf
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        if (tkhd.getCreationTime() >= 1l << 32 ||
230                tkhd.getModificationTime() >= 1l << 32 ||
231                tkhd.getDuration() >= 1l << 32) {
232            tkhd.setVersion(1);
233        }
234
235        trackBox.addBox(tkhd);
236
237/*
238        EditBox edit = new EditBox();
239        EditListBox editListBox = new EditListBox();
240        editListBox.setEntries(Collections.singletonList(
241                new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1)));
242        edit.addBox(editListBox);
243        trackBox.addBox(edit);
244*/
245
246        MediaBox mdia = new MediaBox();
247        trackBox.addBox(mdia);
248        MediaHeaderBox mdhd = new MediaHeaderBox();
249        mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
250        mdhd.setDuration(getDuration(track));
251        mdhd.setTimescale(track.getTrackMetaData().getTimescale());
252        mdhd.setLanguage(track.getTrackMetaData().getLanguage());
253        mdia.addBox(mdhd);
254        HandlerBox hdlr = new HandlerBox();
255        mdia.addBox(hdlr);
256
257        hdlr.setHandlerType(track.getHandler());
258
259        MediaInformationBox minf = new MediaInformationBox();
260        minf.addBox(track.getMediaHeaderBox());
261
262        // dinf: all these three boxes tell us is that the actual
263        // data is in the current file and not somewhere external
264        DataInformationBox dinf = new DataInformationBox();
265        DataReferenceBox dref = new DataReferenceBox();
266        dinf.addBox(dref);
267        DataEntryUrlBox url = new DataEntryUrlBox();
268        url.setFlags(1);
269        dref.addBox(url);
270        minf.addBox(dinf);
271        //
272
273        SampleTableBox stbl = new SampleTableBox();
274
275        stbl.addBox(track.getSampleDescriptionBox());
276
277        List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries();
278        if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) {
279            TimeToSampleBox stts = new TimeToSampleBox();
280            stts.setEntries(track.getDecodingTimeEntries());
281            stbl.addBox(stts);
282        }
283
284        List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries();
285        if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) {
286            CompositionTimeToSample ctts = new CompositionTimeToSample();
287            ctts.setEntries(compositionTimeToSampleEntries);
288            stbl.addBox(ctts);
289        }
290
291        long[] syncSamples = track.getSyncSamples();
292        if (syncSamples != null && syncSamples.length > 0) {
293            SyncSampleBox stss = new SyncSampleBox();
294            stss.setSampleNumber(syncSamples);
295            stbl.addBox(stss);
296        }
297
298        if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) {
299            SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox();
300            sdtp.setEntries(track.getSampleDependencies());
301            stbl.addBox(sdtp);
302        }
303        HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>();
304        for (Track current : movie.getTracks()) {
305            track2ChunkSizes.put(current, getChunkSizes(current, movie));
306        }
307        int[] tracksChunkSizes = track2ChunkSizes.get(track);
308
309        SampleToChunkBox stsc = new SampleToChunkBox();
310        stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>());
311        long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size
312        for (int i = 0; i < tracksChunkSizes.length; i++) {
313            // The sample description index references the sample description box
314            // that describes the samples of this chunk. My Tracks cannot have more
315            // than one sample description box. Therefore 1 is always right
316            // the first chunk has the number '1'
317            if (lastChunkSize != tracksChunkSizes[i]) {
318                stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1));
319                lastChunkSize = tracksChunkSizes[i];
320            }
321        }
322        stbl.addBox(stsc);
323
324        SampleSizeBox stsz = new SampleSizeBox();
325        stsz.setSampleSizes(track2SampleSizes.get(track));
326
327        stbl.addBox(stsz);
328        // The ChunkOffsetBox we create here is just a stub
329        // since we haven't created the whole structure we can't tell where the
330        // first chunk starts (mdat box). So I just let the chunk offset
331        // start at zero and I will add the mdat offset later.
332        StaticChunkOffsetBox stco = new StaticChunkOffsetBox();
333        this.chunkOffsetBoxes.add(stco);
334        long offset = 0;
335        long[] chunkOffset = new long[tracksChunkSizes.length];
336        // all tracks have the same number of chunks
337        if (LOG.isLoggable(Level.FINE)) {
338            LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId());
339        }
340
341
342        for (int i = 0; i < tracksChunkSizes.length; i++) {
343            // The filelayout will be:
344            // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n
345            // calculating the offsets
346            if (LOG.isLoggable(Level.FINER)) {
347                LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i);
348            }
349            for (Track current : movie.getTracks()) {
350                if (LOG.isLoggable(Level.FINEST)) {
351                    LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId());
352                }
353                int[] chunkSizes = track2ChunkSizes.get(current);
354                long firstSampleOfChunk = 0;
355                for (int j = 0; j < i; j++) {
356                    firstSampleOfChunk += chunkSizes[j];
357                }
358                if (current == track) {
359                    chunkOffset[i] = offset;
360                }
361                for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
362                    offset += track2SampleSizes.get(current)[j];
363                }
364            }
365        }
366        stco.setChunkOffsets(chunkOffset);
367        stbl.addBox(stco);
368        minf.addBox(stbl);
369        mdia.addBox(minf);
370
371        return trackBox;
372    }
373
374    private class InterleaveChunkMdat implements Box {
375        List<Track> tracks;
376        List<ByteBuffer> samples = new ArrayList<ByteBuffer>();
377        ContainerBox parent;
378
379        long contentSize = 0;
380
381        public ContainerBox getParent() {
382            return parent;
383        }
384
385        public void setParent(ContainerBox parent) {
386            this.parent = parent;
387        }
388
389        public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
390        }
391
392        private InterleaveChunkMdat(Movie movie) {
393
394            tracks = movie.getTracks();
395            Map<Track, int[]> chunks = new HashMap<Track, int[]>();
396            for (Track track : movie.getTracks()) {
397                chunks.put(track, getChunkSizes(track, movie));
398            }
399
400            for (int i = 0; i < chunks.values().iterator().next().length; i++) {
401                for (Track track : tracks) {
402
403                    int[] chunkSizes = chunks.get(track);
404                    long firstSampleOfChunk = 0;
405                    for (int j = 0; j < i; j++) {
406                        firstSampleOfChunk += chunkSizes[j];
407                    }
408
409                    for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
410
411                        ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j);
412                        contentSize += s.limit();
413                        samples.add((ByteBuffer) s.rewind());
414                    }
415
416                }
417
418            }
419
420        }
421
422        public long getDataOffset() {
423            Box b = this;
424            long offset = 16;
425            while (b.getParent() != null) {
426                for (Box box : b.getParent().getBoxes()) {
427                    if (b == box) {
428                        break;
429                    }
430                    offset += box.getSize();
431                }
432                b = b.getParent();
433            }
434            return offset;
435        }
436
437
438        public String getType() {
439            return "mdat";
440        }
441
442        public long getSize() {
443            return 16 + contentSize;
444        }
445
446        private boolean isSmallBox(long contentSize) {
447            return (contentSize + 8) < 4294967296L;
448        }
449
450
451        public void getBox(WritableByteChannel writableByteChannel) throws IOException {
452            ByteBuffer bb = ByteBuffer.allocate(16);
453            long size = getSize();
454            if (isSmallBox(size)) {
455                IsoTypeWriter.writeUInt32(bb, size);
456            } else {
457                IsoTypeWriter.writeUInt32(bb, 1);
458            }
459            bb.put(IsoFile.fourCCtoBytes("mdat"));
460            if (isSmallBox(size)) {
461                bb.put(new byte[8]);
462            } else {
463                IsoTypeWriter.writeUInt64(bb, size);
464            }
465            bb.rewind();
466            writableByteChannel.write(bb);
467            if (writableByteChannel instanceof GatheringByteChannel) {
468                List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples);
469
470
471                for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) {
472                    List<ByteBuffer> sublist = nuSamples.subList(
473                            i * STEPSIZE, // start
474                            (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end
475                    ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]);
476                    do {
477                        ((GatheringByteChannel) writableByteChannel).write(sampleArray);
478                    } while (sampleArray[sampleArray.length - 1].remaining() > 0);
479                }
480                //System.err.println(bytesWritten);
481            } else {
482                for (ByteBuffer sample : samples) {
483                    sample.rewind();
484                    writableByteChannel.write(sample);
485                }
486            }
487        }
488
489    }
490
491    /**
492     * Gets the chunk sizes for the given track.
493     *
494     * @param track
495     * @param movie
496     * @return
497     */
498    int[] getChunkSizes(Track track, Movie movie) {
499
500        long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie);
501        int[] chunkSizes = new int[referenceChunkStarts.length];
502
503
504        for (int i = 0; i < referenceChunkStarts.length; i++) {
505            long start = referenceChunkStarts[i] - 1;
506            long end;
507            if (referenceChunkStarts.length == i + 1) {
508                end = track.getSamples().size();
509            } else {
510                end = referenceChunkStarts[i + 1] - 1;
511            }
512
513            chunkSizes[i] = l2i(end - start);
514            // The Stretch makes sure that there are as much audio and video chunks!
515        }
516        assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal";
517        return chunkSizes;
518
519
520    }
521
522
523    private static long sum(int[] ls) {
524        long rc = 0;
525        for (long l : ls) {
526            rc += l;
527        }
528        return rc;
529    }
530
531    protected static long getDuration(Track track) {
532        long duration = 0;
533        for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
534            duration += entry.getCount() * entry.getDelta();
535        }
536        return duration;
537    }
538
539    public long getTimescale(Movie movie) {
540        long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale();
541        for (Track track : movie.getTracks()) {
542            timescale = gcd(track.getTrackMetaData().getTimescale(), timescale);
543        }
544        return timescale;
545    }
546
547    public static long gcd(long a, long b) {
548        if (b == 0) {
549            return a;
550        }
551        return gcd(b, a % b);
552    }
553
554    public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) {
555        ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size());
556        for (ByteBuffer buffer : samples) {
557            int lastIndex = nuSamples.size() - 1;
558            if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() &&
559                    nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) {
560                ByteBuffer oldBuffer = nuSamples.remove(lastIndex);
561                ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice();
562                // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset.
563                nuSamples.add(nu);
564            } else if (lastIndex >= 0 &&
565                    buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer &&
566                    nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) {
567                // This can go wrong - but will it?
568                ByteBuffer oldBuffer = nuSamples.get(lastIndex);
569                oldBuffer.limit(buffer.limit() + oldBuffer.limit());
570            } else {
571                nuSamples.add(buffer);
572            }
573        }
574        return nuSamples;
575    }
576}
577