summaryrefslogtreecommitdiff
path: root/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base
diff options
context:
space:
mode:
Diffstat (limited to 'isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base')
-rw-r--r--isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base576
1 files changed, 576 insertions, 0 deletions
diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base
new file mode 100644
index 0000000..9bd1ca6
--- /dev/null
+++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/.svn/text-base/DefaultMp4Builder.java.svn-base
@@ -0,0 +1,576 @@
+/*
+ * Copyright 2012 Sebastian Annies, Hamburg
+ *
+ * Licensed under the Apache License, Version 2.0 (the License);
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.googlecode.mp4parser.authoring.builder;
+
+import com.coremedia.iso.BoxParser;
+import com.coremedia.iso.IsoFile;
+import com.coremedia.iso.IsoTypeWriter;
+import com.coremedia.iso.boxes.Box;
+import com.coremedia.iso.boxes.CompositionTimeToSample;
+import com.coremedia.iso.boxes.ContainerBox;
+import com.coremedia.iso.boxes.DataEntryUrlBox;
+import com.coremedia.iso.boxes.DataInformationBox;
+import com.coremedia.iso.boxes.DataReferenceBox;
+import com.coremedia.iso.boxes.FileTypeBox;
+import com.coremedia.iso.boxes.HandlerBox;
+import com.coremedia.iso.boxes.MediaBox;
+import com.coremedia.iso.boxes.MediaHeaderBox;
+import com.coremedia.iso.boxes.MediaInformationBox;
+import com.coremedia.iso.boxes.MovieBox;
+import com.coremedia.iso.boxes.MovieHeaderBox;
+import com.coremedia.iso.boxes.SampleDependencyTypeBox;
+import com.coremedia.iso.boxes.SampleSizeBox;
+import com.coremedia.iso.boxes.SampleTableBox;
+import com.coremedia.iso.boxes.SampleToChunkBox;
+import com.coremedia.iso.boxes.StaticChunkOffsetBox;
+import com.coremedia.iso.boxes.SyncSampleBox;
+import com.coremedia.iso.boxes.TimeToSampleBox;
+import com.coremedia.iso.boxes.TrackBox;
+import com.coremedia.iso.boxes.TrackHeaderBox;
+import com.googlecode.mp4parser.authoring.DateHelper;
+import com.googlecode.mp4parser.authoring.Movie;
+import com.googlecode.mp4parser.authoring.Track;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.GatheringByteChannel;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.googlecode.mp4parser.util.CastUtils.l2i;
+
+/**
+ * Creates a plain MP4 file from a video. Plain as plain can be.
+ */
+public class DefaultMp4Builder implements Mp4Builder {
+
+ public int STEPSIZE = 64;
+ Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>();
+ private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName());
+
+ HashMap<Track, List<ByteBuffer>> track2Sample = new HashMap<Track, List<ByteBuffer>>();
+ HashMap<Track, long[]> track2SampleSizes = new HashMap<Track, long[]>();
+ private FragmentIntersectionFinder intersectionFinder = new TwoSecondIntersectionFinder();
+
+ public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) {
+ this.intersectionFinder = intersectionFinder;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public IsoFile build(Movie movie) {
+ LOG.fine("Creating movie " + movie);
+ for (Track track : movie.getTracks()) {
+ // getting the samples may be a time consuming activity
+ List<ByteBuffer> samples = track.getSamples();
+ putSamples(track, samples);
+ long[] sizes = new long[samples.size()];
+ for (int i = 0; i < sizes.length; i++) {
+ sizes[i] = samples.get(i).limit();
+ }
+ putSampleSizes(track, sizes);
+ }
+
+ IsoFile isoFile = new IsoFile();
+ // ouch that is ugly but I don't know how to do it else
+ List<String> minorBrands = new LinkedList<String>();
+ minorBrands.add("isom");
+ minorBrands.add("iso2");
+ minorBrands.add("avc1");
+
+ isoFile.addBox(new FileTypeBox("isom", 0, minorBrands));
+ isoFile.addBox(createMovieBox(movie));
+ InterleaveChunkMdat mdat = new InterleaveChunkMdat(movie);
+ isoFile.addBox(mdat);
+
+ /*
+ dataOffset is where the first sample starts. In this special mdat the samples always start
+ at offset 16 so that we can use the same offset for large boxes and small boxes
+ */
+ long dataOffset = mdat.getDataOffset();
+ for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) {
+ long[] offsets = chunkOffsetBox.getChunkOffsets();
+ for (int i = 0; i < offsets.length; i++) {
+ offsets[i] += dataOffset;
+ }
+ }
+
+
+ return isoFile;
+ }
+
+ public FragmentIntersectionFinder getFragmentIntersectionFinder() {
+ throw new UnsupportedOperationException("No fragment intersection finder in default MP4 builder!");
+ }
+
+ protected long[] putSampleSizes(Track track, long[] sizes) {
+ return track2SampleSizes.put(track, sizes);
+ }
+
+ protected List<ByteBuffer> putSamples(Track track, List<ByteBuffer> samples) {
+ return track2Sample.put(track, samples);
+ }
+
+ private MovieBox createMovieBox(Movie movie) {
+ MovieBox movieBox = new MovieBox();
+ MovieHeaderBox mvhd = new MovieHeaderBox();
+
+ mvhd.setCreationTime(DateHelper.convert(new Date()));
+ mvhd.setModificationTime(DateHelper.convert(new Date()));
+
+ long movieTimeScale = getTimescale(movie);
+ long duration = 0;
+
+ for (Track track : movie.getTracks()) {
+ long tracksDuration = getDuration(track) * movieTimeScale / track.getTrackMetaData().getTimescale();
+ if (tracksDuration > duration) {
+ duration = tracksDuration;
+ }
+
+
+ }
+
+ mvhd.setDuration(duration);
+ mvhd.setTimescale(movieTimeScale);
+ // find the next available trackId
+ long nextTrackId = 0;
+ for (Track track : movie.getTracks()) {
+ nextTrackId = nextTrackId < track.getTrackMetaData().getTrackId() ? track.getTrackMetaData().getTrackId() : nextTrackId;
+ }
+ mvhd.setNextTrackId(++nextTrackId);
+ if (mvhd.getCreationTime() >= 1l << 32 ||
+ mvhd.getModificationTime() >= 1l << 32 ||
+ mvhd.getDuration() >= 1l << 32) {
+ mvhd.setVersion(1);
+ }
+
+ movieBox.addBox(mvhd);
+ for (Track track : movie.getTracks()) {
+ movieBox.addBox(createTrackBox(track, movie));
+ }
+ // metadata here
+ Box udta = createUdta(movie);
+ if (udta != null) {
+ movieBox.addBox(udta);
+ }
+ return movieBox;
+
+ }
+
+ /**
+ * Override to create a user data box that may contain metadata.
+ *
+ * @return a 'udta' box or <code>null</code> if none provided
+ */
+ protected Box createUdta(Movie movie) {
+ return null;
+ }
+
+ private TrackBox createTrackBox(Track track, Movie movie) {
+
+ LOG.info("Creating Mp4TrackImpl " + track);
+ TrackBox trackBox = new TrackBox();
+ TrackHeaderBox tkhd = new TrackHeaderBox();
+ int flags = 0;
+ if (track.isEnabled()) {
+ flags += 1;
+ }
+
+ if (track.isInMovie()) {
+ flags += 2;
+ }
+
+ if (track.isInPreview()) {
+ flags += 4;
+ }
+
+ if (track.isInPoster()) {
+ flags += 8;
+ }
+ tkhd.setFlags(flags);
+
+ tkhd.setAlternateGroup(track.getTrackMetaData().getGroup());
+ tkhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
+ // We need to take edit list box into account in trackheader duration
+ // but as long as I don't support edit list boxes it is sufficient to
+ // just translate media duration to movie timescale
+ tkhd.setDuration(getDuration(track) * getTimescale(movie) / track.getTrackMetaData().getTimescale());
+ tkhd.setHeight(track.getTrackMetaData().getHeight());
+ tkhd.setWidth(track.getTrackMetaData().getWidth());
+ tkhd.setLayer(track.getTrackMetaData().getLayer());
+ tkhd.setModificationTime(DateHelper.convert(new Date()));
+ tkhd.setTrackId(track.getTrackMetaData().getTrackId());
+ tkhd.setVolume(track.getTrackMetaData().getVolume());
+ if (tkhd.getCreationTime() >= 1l << 32 ||
+ tkhd.getModificationTime() >= 1l << 32 ||
+ tkhd.getDuration() >= 1l << 32) {
+ tkhd.setVersion(1);
+ }
+
+ trackBox.addBox(tkhd);
+
+/*
+ EditBox edit = new EditBox();
+ EditListBox editListBox = new EditListBox();
+ editListBox.setEntries(Collections.singletonList(
+ new EditListBox.Entry(editListBox, (long) (track.getTrackMetaData().getStartTime() * getTimescale(movie)), -1, 1)));
+ edit.addBox(editListBox);
+ trackBox.addBox(edit);
+*/
+
+ MediaBox mdia = new MediaBox();
+ trackBox.addBox(mdia);
+ MediaHeaderBox mdhd = new MediaHeaderBox();
+ mdhd.setCreationTime(DateHelper.convert(track.getTrackMetaData().getCreationTime()));
+ mdhd.setDuration(getDuration(track));
+ mdhd.setTimescale(track.getTrackMetaData().getTimescale());
+ mdhd.setLanguage(track.getTrackMetaData().getLanguage());
+ mdia.addBox(mdhd);
+ HandlerBox hdlr = new HandlerBox();
+ mdia.addBox(hdlr);
+
+ hdlr.setHandlerType(track.getHandler());
+
+ MediaInformationBox minf = new MediaInformationBox();
+ minf.addBox(track.getMediaHeaderBox());
+
+ // dinf: all these three boxes tell us is that the actual
+ // data is in the current file and not somewhere external
+ DataInformationBox dinf = new DataInformationBox();
+ DataReferenceBox dref = new DataReferenceBox();
+ dinf.addBox(dref);
+ DataEntryUrlBox url = new DataEntryUrlBox();
+ url.setFlags(1);
+ dref.addBox(url);
+ minf.addBox(dinf);
+ //
+
+ SampleTableBox stbl = new SampleTableBox();
+
+ stbl.addBox(track.getSampleDescriptionBox());
+
+ List<TimeToSampleBox.Entry> decodingTimeToSampleEntries = track.getDecodingTimeEntries();
+ if (decodingTimeToSampleEntries != null && !track.getDecodingTimeEntries().isEmpty()) {
+ TimeToSampleBox stts = new TimeToSampleBox();
+ stts.setEntries(track.getDecodingTimeEntries());
+ stbl.addBox(stts);
+ }
+
+ List<CompositionTimeToSample.Entry> compositionTimeToSampleEntries = track.getCompositionTimeEntries();
+ if (compositionTimeToSampleEntries != null && !compositionTimeToSampleEntries.isEmpty()) {
+ CompositionTimeToSample ctts = new CompositionTimeToSample();
+ ctts.setEntries(compositionTimeToSampleEntries);
+ stbl.addBox(ctts);
+ }
+
+ long[] syncSamples = track.getSyncSamples();
+ if (syncSamples != null && syncSamples.length > 0) {
+ SyncSampleBox stss = new SyncSampleBox();
+ stss.setSampleNumber(syncSamples);
+ stbl.addBox(stss);
+ }
+
+ if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) {
+ SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox();
+ sdtp.setEntries(track.getSampleDependencies());
+ stbl.addBox(sdtp);
+ }
+ HashMap<Track, int[]> track2ChunkSizes = new HashMap<Track, int[]>();
+ for (Track current : movie.getTracks()) {
+ track2ChunkSizes.put(current, getChunkSizes(current, movie));
+ }
+ int[] tracksChunkSizes = track2ChunkSizes.get(track);
+
+ SampleToChunkBox stsc = new SampleToChunkBox();
+ stsc.setEntries(new LinkedList<SampleToChunkBox.Entry>());
+ long lastChunkSize = Integer.MIN_VALUE; // to be sure the first chunks hasn't got the same size
+ for (int i = 0; i < tracksChunkSizes.length; i++) {
+ // The sample description index references the sample description box
+ // that describes the samples of this chunk. My Tracks cannot have more
+ // than one sample description box. Therefore 1 is always right
+ // the first chunk has the number '1'
+ if (lastChunkSize != tracksChunkSizes[i]) {
+ stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, tracksChunkSizes[i], 1));
+ lastChunkSize = tracksChunkSizes[i];
+ }
+ }
+ stbl.addBox(stsc);
+
+ SampleSizeBox stsz = new SampleSizeBox();
+ stsz.setSampleSizes(track2SampleSizes.get(track));
+
+ stbl.addBox(stsz);
+ // The ChunkOffsetBox we create here is just a stub
+ // since we haven't created the whole structure we can't tell where the
+ // first chunk starts (mdat box). So I just let the chunk offset
+ // start at zero and I will add the mdat offset later.
+ StaticChunkOffsetBox stco = new StaticChunkOffsetBox();
+ this.chunkOffsetBoxes.add(stco);
+ long offset = 0;
+ long[] chunkOffset = new long[tracksChunkSizes.length];
+ // all tracks have the same number of chunks
+ if (LOG.isLoggable(Level.FINE)) {
+ LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId());
+ }
+
+
+ for (int i = 0; i < tracksChunkSizes.length; i++) {
+ // The filelayout will be:
+ // chunk_1_track_1,... ,chunk_1_track_n, chunk_2_track_1,... ,chunk_2_track_n, ... , chunk_m_track_1,... ,chunk_m_track_n
+ // calculating the offsets
+ if (LOG.isLoggable(Level.FINER)) {
+ LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i);
+ }
+ for (Track current : movie.getTracks()) {
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId());
+ }
+ int[] chunkSizes = track2ChunkSizes.get(current);
+ long firstSampleOfChunk = 0;
+ for (int j = 0; j < i; j++) {
+ firstSampleOfChunk += chunkSizes[j];
+ }
+ if (current == track) {
+ chunkOffset[i] = offset;
+ }
+ for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
+ offset += track2SampleSizes.get(current)[j];
+ }
+ }
+ }
+ stco.setChunkOffsets(chunkOffset);
+ stbl.addBox(stco);
+ minf.addBox(stbl);
+ mdia.addBox(minf);
+
+ return trackBox;
+ }
+
+ private class InterleaveChunkMdat implements Box {
+ List<Track> tracks;
+ List<ByteBuffer> samples = new ArrayList<ByteBuffer>();
+ ContainerBox parent;
+
+ long contentSize = 0;
+
+ public ContainerBox getParent() {
+ return parent;
+ }
+
+ public void setParent(ContainerBox parent) {
+ this.parent = parent;
+ }
+
+ public void parse(ReadableByteChannel readableByteChannel, ByteBuffer header, long contentSize, BoxParser boxParser) throws IOException {
+ }
+
+ private InterleaveChunkMdat(Movie movie) {
+
+ tracks = movie.getTracks();
+ Map<Track, int[]> chunks = new HashMap<Track, int[]>();
+ for (Track track : movie.getTracks()) {
+ chunks.put(track, getChunkSizes(track, movie));
+ }
+
+ for (int i = 0; i < chunks.values().iterator().next().length; i++) {
+ for (Track track : tracks) {
+
+ int[] chunkSizes = chunks.get(track);
+ long firstSampleOfChunk = 0;
+ for (int j = 0; j < i; j++) {
+ firstSampleOfChunk += chunkSizes[j];
+ }
+
+ for (int j = l2i(firstSampleOfChunk); j < firstSampleOfChunk + chunkSizes[i]; j++) {
+
+ ByteBuffer s = DefaultMp4Builder.this.track2Sample.get(track).get(j);
+ contentSize += s.limit();
+ samples.add((ByteBuffer) s.rewind());
+ }
+
+ }
+
+ }
+
+ }
+
+ public long getDataOffset() {
+ Box b = this;
+ long offset = 16;
+ while (b.getParent() != null) {
+ for (Box box : b.getParent().getBoxes()) {
+ if (b == box) {
+ break;
+ }
+ offset += box.getSize();
+ }
+ b = b.getParent();
+ }
+ return offset;
+ }
+
+
+ public String getType() {
+ return "mdat";
+ }
+
+ public long getSize() {
+ return 16 + contentSize;
+ }
+
+ private boolean isSmallBox(long contentSize) {
+ return (contentSize + 8) < 4294967296L;
+ }
+
+
+ public void getBox(WritableByteChannel writableByteChannel) throws IOException {
+ ByteBuffer bb = ByteBuffer.allocate(16);
+ long size = getSize();
+ if (isSmallBox(size)) {
+ IsoTypeWriter.writeUInt32(bb, size);
+ } else {
+ IsoTypeWriter.writeUInt32(bb, 1);
+ }
+ bb.put(IsoFile.fourCCtoBytes("mdat"));
+ if (isSmallBox(size)) {
+ bb.put(new byte[8]);
+ } else {
+ IsoTypeWriter.writeUInt64(bb, size);
+ }
+ bb.rewind();
+ writableByteChannel.write(bb);
+ if (writableByteChannel instanceof GatheringByteChannel) {
+ List<ByteBuffer> nuSamples = unifyAdjacentBuffers(samples);
+
+
+ for (int i = 0; i < Math.ceil((double) nuSamples.size() / STEPSIZE); i++) {
+ List<ByteBuffer> sublist = nuSamples.subList(
+ i * STEPSIZE, // start
+ (i + 1) * STEPSIZE < nuSamples.size() ? (i + 1) * STEPSIZE : nuSamples.size()); // end
+ ByteBuffer sampleArray[] = sublist.toArray(new ByteBuffer[sublist.size()]);
+ do {
+ ((GatheringByteChannel) writableByteChannel).write(sampleArray);
+ } while (sampleArray[sampleArray.length - 1].remaining() > 0);
+ }
+ //System.err.println(bytesWritten);
+ } else {
+ for (ByteBuffer sample : samples) {
+ sample.rewind();
+ writableByteChannel.write(sample);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Gets the chunk sizes for the given track.
+ *
+ * @param track
+ * @param movie
+ * @return
+ */
+ int[] getChunkSizes(Track track, Movie movie) {
+
+ long[] referenceChunkStarts = intersectionFinder.sampleNumbers(track, movie);
+ int[] chunkSizes = new int[referenceChunkStarts.length];
+
+
+ for (int i = 0; i < referenceChunkStarts.length; i++) {
+ long start = referenceChunkStarts[i] - 1;
+ long end;
+ if (referenceChunkStarts.length == i + 1) {
+ end = track.getSamples().size();
+ } else {
+ end = referenceChunkStarts[i + 1] - 1;
+ }
+
+ chunkSizes[i] = l2i(end - start);
+ // The Stretch makes sure that there are as much audio and video chunks!
+ }
+ assert DefaultMp4Builder.this.track2Sample.get(track).size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal";
+ return chunkSizes;
+
+
+ }
+
+
+ private static long sum(int[] ls) {
+ long rc = 0;
+ for (long l : ls) {
+ rc += l;
+ }
+ return rc;
+ }
+
+ protected static long getDuration(Track track) {
+ long duration = 0;
+ for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) {
+ duration += entry.getCount() * entry.getDelta();
+ }
+ return duration;
+ }
+
+ public long getTimescale(Movie movie) {
+ long timescale = movie.getTracks().iterator().next().getTrackMetaData().getTimescale();
+ for (Track track : movie.getTracks()) {
+ timescale = gcd(track.getTrackMetaData().getTimescale(), timescale);
+ }
+ return timescale;
+ }
+
+ public static long gcd(long a, long b) {
+ if (b == 0) {
+ return a;
+ }
+ return gcd(b, a % b);
+ }
+
+ public List<ByteBuffer> unifyAdjacentBuffers(List<ByteBuffer> samples) {
+ ArrayList<ByteBuffer> nuSamples = new ArrayList<ByteBuffer>(samples.size());
+ for (ByteBuffer buffer : samples) {
+ int lastIndex = nuSamples.size() - 1;
+ if (lastIndex >= 0 && buffer.hasArray() && nuSamples.get(lastIndex).hasArray() && buffer.array() == nuSamples.get(lastIndex).array() &&
+ nuSamples.get(lastIndex).arrayOffset() + nuSamples.get(lastIndex).limit() == buffer.arrayOffset()) {
+ ByteBuffer oldBuffer = nuSamples.remove(lastIndex);
+ ByteBuffer nu = ByteBuffer.wrap(buffer.array(), oldBuffer.arrayOffset(), oldBuffer.limit() + buffer.limit()).slice();
+ // We need to slice here since wrap([], offset, length) just sets position and not the arrayOffset.
+ nuSamples.add(nu);
+ } else if (lastIndex >= 0 &&
+ buffer instanceof MappedByteBuffer && nuSamples.get(lastIndex) instanceof MappedByteBuffer &&
+ nuSamples.get(lastIndex).limit() == nuSamples.get(lastIndex).capacity() - buffer.capacity()) {
+ // This can go wrong - but will it?
+ ByteBuffer oldBuffer = nuSamples.get(lastIndex);
+ oldBuffer.limit(buffer.limit() + oldBuffer.limit());
+ } else {
+ nuSamples.add(buffer);
+ }
+ }
+ return nuSamples;
+ }
+}