diff options
Diffstat (limited to 'isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java')
-rw-r--r-- | isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java | 576 |
1 files changed, 576 insertions, 0 deletions
diff --git a/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java new file mode 100644 index 0000000..9bd1ca6 --- /dev/null +++ b/isoparser/src/main/java/com/googlecode/mp4parser/authoring/builder/DefaultMp4Builder.java @@ -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; + } +} |