package com.googlecode.mp4parser.authoring.builder; import com.coremedia.iso.BoxParser; import com.coremedia.iso.IsoBufferWrapper; import com.coremedia.iso.IsoBufferWrapperImpl; import com.coremedia.iso.IsoFile; import com.coremedia.iso.IsoOutputStream; import com.coremedia.iso.boxes.AbstractBox; import com.coremedia.iso.boxes.Box; import com.coremedia.iso.boxes.CompositionTimeToSample; import com.coremedia.iso.boxes.DataEntryUrlBox; import com.coremedia.iso.boxes.DataInformationBox; import com.coremedia.iso.boxes.DataReferenceBox; import com.coremedia.iso.boxes.EditBox; import com.coremedia.iso.boxes.EditListBox; import com.coremedia.iso.boxes.FileTypeBox; import com.coremedia.iso.boxes.HandlerBox; import com.coremedia.iso.boxes.HintMediaHeaderBox; 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.NullMediaHeaderBox; 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.SoundMediaHeaderBox; 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.coremedia.iso.boxes.VideoMediaHeaderBox; import com.googlecode.mp4parser.authoring.DateHelper; import com.googlecode.mp4parser.authoring.Movie; import com.googlecode.mp4parser.authoring.Track; import java.io.IOException; import java.util.Collections; 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.Logger; /** * Creates a plain MP4 file from a video. Plain as plain can be. */ public class DefaultMp4Builder implements Mp4Builder { Set<StaticChunkOffsetBox> chunkOffsetBoxes = new HashSet<StaticChunkOffsetBox>(); private static Logger LOG = Logger.getLogger(DefaultMp4Builder.class.getName()); public IsoFile build(Movie movie) throws IOException { LOG.info("Creating movie " + movie); IsoFile isoFile = new IsoFile(new IsoBufferWrapperImpl(new byte[]{})); isoFile.parse(); // 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)); Box mdat = new InterleaveChunkMdat(movie); isoFile.addBox(mdat); /* dataOffset is where the first sample starts. Since we created the chunk offset boxes without knowing this offset and temporarely */ long dataOffset = mdat.calculateOffset() + 8; for (StaticChunkOffsetBox chunkOffsetBox : chunkOffsetBoxes) { long[] offsets = chunkOffsetBox.getChunkOffsets(); for (int i = 0; i < offsets.length; i++) { offsets[i] += dataOffset; } } return isoFile; } private MovieBox createMovieBox(Movie movie) { MovieBox movieBox = new MovieBox(); List<Box> movieBoxChildren = new LinkedList<Box>(); 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); movieBoxChildren.add(mvhd); for (Track track : movie.getTracks()) { if (track.getType() != Track.Type.UNKNOWN) { movieBoxChildren.add(createTrackBox(track, movie)); } } // metadata here movieBox.setBoxes(movieBoxChildren); return movieBox; } 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()); 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); switch (track.getType()) { case VIDEO: hdlr.setHandlerType("vide"); break; case SOUND: hdlr.setHandlerType("soun"); break; case HINT: hdlr.setHandlerType("hint"); break; case TEXT: hdlr.setHandlerType("text"); break; default: throw new RuntimeException("Dont know handler type " + track.getType()); } MediaInformationBox minf = new MediaInformationBox(); switch (track.getType()) { case VIDEO: VideoMediaHeaderBox vmhd = new VideoMediaHeaderBox(); minf.addBox(vmhd); break; case SOUND: SoundMediaHeaderBox smhd = new SoundMediaHeaderBox(); minf.addBox(smhd); break; case HINT: HintMediaHeaderBox hmhd = new HintMediaHeaderBox(); minf.addBox(hmhd); break; case TEXT: case NULL: NullMediaHeaderBox nmhd = new NullMediaHeaderBox(); minf.addBox(nmhd); break; } // 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()); if (track.getDecodingTimeEntries() != null && !track.getDecodingTimeEntries().isEmpty()) { TimeToSampleBox stts = new TimeToSampleBox(); stts.setEntries(track.getDecodingTimeEntries()); stbl.addBox(stts); } if (track.getCompositionTimeEntries() != null && !track.getCompositionTimeEntries().isEmpty()) { CompositionTimeToSample ctts = new CompositionTimeToSample(); ctts.setEntries(track.getCompositionTimeEntries()); stbl.addBox(ctts); } if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { SyncSampleBox stss = new SyncSampleBox(); stss.setSampleNumber(track.getSyncSamples()); stbl.addBox(stss); } if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) { SampleDependencyTypeBox sdtp = new SampleDependencyTypeBox(); sdtp.setEntries(track.getSampleDependencies()); stbl.addBox(sdtp); } long chunkSize[] = getChunkSizes(track, movie); 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 < chunkSize.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 != chunkSize[i]) { stsc.getEntries().add(new SampleToChunkBox.Entry(i + 1, chunkSize[i], 1)); lastChunkSize = chunkSize[i]; } } stbl.addBox(stsc); SampleSizeBox stsz = new SampleSizeBox(); long[] sizes = new long[track.getSamples().size()]; for (int i = 0; i < sizes.length; i++) { sizes[i] = track.getSamples().get(i).size(); } stsz.setSampleSizes(sizes); 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[chunkSize.length]; // all tracks have the same number of chunks LOG.fine("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId()); for (int i = 0; i < chunkSize.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 LOG.finer("Calculating chunk offsets for track_" + track.getTrackMetaData().getTrackId() + " chunk " + i); for (Track current : movie.getTracks()) { LOG.finest("Adding offsets of track_" + current.getTrackMetaData().getTrackId()); long[] chunkSizes = getChunkSizes(current, movie); long firstSampleOfChunk = 0; for (int j = 0; j < i; j++) { firstSampleOfChunk += chunkSizes[j]; } if (current == track) { chunkOffset[i] = offset; } for (long j = firstSampleOfChunk; j < firstSampleOfChunk + chunkSizes[i]; j++) { if (j > Integer.MAX_VALUE) { throw new InternalError("I cannot deal with a number of samples > Integer.MAX_VALUE"); } offset += current.getSamples().get((int) j).size(); } } } stco.setChunkOffsets(chunkOffset); stbl.addBox(stco); minf.addBox(stbl); mdia.addBox(minf); return trackBox; } private static class InterleaveChunkMdat extends AbstractBox { List<Track> tracks; Map<Track, long[]> chunks = new HashMap<Track, long[]>(); private InterleaveChunkMdat(Movie movie) { super(IsoFile.fourCCtoBytes("mdat")); tracks = movie.getTracks(); for (Track track : movie.getTracks()) { chunks.put(track, getChunkSizes(track, movie)); } } @Override protected long getContentSize() { long size = 0; for (Track track : tracks) { for (IsoBufferWrapper sample : track.getSamples()) { size += sample.size(); } } return size; } @Override public void parse(IsoBufferWrapper in, long size, BoxParser boxParser, Box lastMovieFragmentBox) throws IOException { throw new InternalError("This box cannot be created by parsing"); } @Override protected void getContent(IsoOutputStream os) throws IOException { long aaa = 0; // all tracks have the same number of chunks for (int i = 0; i < chunks.values().iterator().next().length; i++) { for (Track track : tracks) { long[] chunkSizes = chunks.get(track); long firstSampleOfChunk = 0; for (int j = 0; j < i; j++) { firstSampleOfChunk += chunkSizes[j]; } for (long j = firstSampleOfChunk; j < firstSampleOfChunk + chunkSizes[i]; j++) { if (j > Integer.MAX_VALUE) { throw new InternalError("I cannot deal with a number of samples > Integer.MAX_VALUE"); } IsoBufferWrapper ibw = track.getSamples().get((int) j); while (ibw.remaining() >= 1024) { os.write(ibw.read(1024)); } while (ibw.remaining() > 0) { os.write(ibw.readByte()); } } } } System.err.println(aaa); } } /** * Gets the chunk sizes for the given track. * * @param track * @param movie * @return */ static long[] getChunkSizes(Track track, Movie movie) { Track referenceTrack = null; long[] referenceChunkStarts = null; long referenceSampleCount = 0; for (Track test : movie.getTracks()) { if (test.getSyncSamples() != null && test.getSyncSamples().length > 0) { referenceTrack = test; referenceChunkStarts = test.getSyncSamples(); referenceSampleCount = test.getSamples().size(); } } if (referenceTrack == null) { throw new RuntimeException("need some sync samples"); } long[] chunkSizes = new long[referenceTrack.getSyncSamples().length]; long sc = track.getSamples().size(); // Since the number of sample differs per track enormously 25 fps vs Audio for example // we calculate the stretch. Stretch is the number of samples in current track that // are needed for the time one sample in reference track is presented. double stretch = (double) sc / referenceSampleCount; for (int i = 0; i < chunkSizes.length; i++) { long start = Math.round(stretch * ((referenceChunkStarts[i]) - 1)); long end = 0; if (referenceChunkStarts.length == i + 1) { end = Math.round(stretch * (referenceSampleCount)); } else { end = Math.round(stretch * ((referenceChunkStarts[i + 1] - 1))); } chunkSizes[i] = end - start; // The Stretch makes sure that there are as much audio and video chunks! } assert track.getSamples().size() == sum(chunkSizes) : "The number of samples and the sum of all chunk lengths must be equal"; return chunkSizes; } private static long sum(long[] ls) { long rc = 0; for (long l : ls) { rc += l; } return rc; } protected 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); } }