package com.googlecode.mp4parser.authoring.builder;
import com.coremedia.iso.IsoBufferWrapper;
import com.coremedia.iso.IsoBufferWrapperImpl;
import com.coremedia.iso.IsoFile;
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.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.SampleTableBox;
import com.coremedia.iso.boxes.SampleToChunkBox;
import com.coremedia.iso.boxes.SoundMediaHeaderBox;
import com.coremedia.iso.boxes.StaticChunkOffsetBox;
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.coremedia.iso.boxes.WriteListener;
import com.coremedia.iso.boxes.fragment.MovieExtendsBox;
import com.coremedia.iso.boxes.fragment.MovieFragmentBox;
import com.coremedia.iso.boxes.fragment.MovieFragmentHeaderBox;
import com.coremedia.iso.boxes.fragment.SampleFlags;
import com.coremedia.iso.boxes.fragment.TrackExtendsBox;
import com.coremedia.iso.boxes.fragment.TrackFragmentBox;
import com.coremedia.iso.boxes.fragment.TrackFragmentHeaderBox;
import com.coremedia.iso.boxes.fragment.TrackRunBox;
import com.coremedia.iso.boxes.mdat.MediaDataBoxWithSamples;
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.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.logging.Logger;
/**
* Creates a fragmented MP4 file.
*/
public class FragmentedMp4Builder implements Mp4Builder {
FragmentIntersectionFinder intersectionFinder;
private static final Logger LOG = Logger.getLogger(FragmentedMp4Builder.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));
int maxNumberOfFragments = 0;
for (Track track : movie.getTracks()) {
int currentLength = intersectionFinder.sampleNumbers(track, movie).length;
maxNumberOfFragments = currentLength > maxNumberOfFragments ? currentLength : maxNumberOfFragments;
}
for (int i = 0; i < maxNumberOfFragments; i++) {
for (Track track : movie.getTracks()) {
int[] startSamples = intersectionFinder.sampleNumbers(track, movie);
if (i < startSamples.length) {
int startSample = startSamples[i];
int endSample = startSamples.length >= (i + 1) ? track.getSamples().size() : startSamples[i + 1];
isoFile.addBox(createMoof(startSample, endSample, track, i));
isoFile.addBox(new MediaDataBoxWithSamples(track.getSamples().subList(startSample, endSample)));
} else {
//obivous this track has not that many fragments
}
}
}
return isoFile;
}
private MovieFragmentBox createMoof(int startSample, int endSample, Track track, int sequenceNumber) {
List<IsoBufferWrapper> samples = track.getSamples().subList(startSample, endSample);
long[] sampleSizes = new long[samples.size()];
for (int i = 0; i < sampleSizes.length; i++) {
sampleSizes[i] = samples.get(i).size();
}
final TrackFragmentHeaderBox tfhd = new TrackFragmentHeaderBox();
tfhd.setBaseDataOffset(-1);
SampleFlags sf = new SampleFlags(0x0000c0);
tfhd.setDefaultSampleFlags(sf);
MovieFragmentBox moof = new MovieFragmentBox();
moof.addWriteListener(new WriteListener() {
public void beforeWrite(long offset) {
tfhd.setBaseDataOffset(offset);
}
});
MovieFragmentHeaderBox mfhd = new MovieFragmentHeaderBox();
moof.addBox(mfhd);
TrackFragmentBox traf = new TrackFragmentBox();
moof.addBox(traf);
traf.addBox(tfhd);
TrackRunBox trun = new TrackRunBox();
traf.addBox(trun);
mfhd.setSequenceNumber(sequenceNumber);
tfhd.setTrackId(track.getTrackMetaData().getTrackId());
trun.setFlags(0x100 | 0x200 | 0x001 | 0x800 | 0x004);
trun.setFirstSampleFlags(new SampleFlags(0x00000040));
// sampleDuration | sampleSize | dataOffset | compositionTime | firstSampleFlags
List<TrackRunBox.Entry> entries = new ArrayList<TrackRunBox.Entry>(endSample - startSample);
int mdatSize = 0;
Queue<TimeToSampleBox.Entry> timeQueue = new LinkedList<TimeToSampleBox.Entry>(track.getDecodingTimeEntries());
long durationEntriesLeft = timeQueue.peek().getCount();
Queue<CompositionTimeToSample.Entry> compositionTimeQueue =
track.getCompositionTimeEntries() != null && track.getCompositionTimeEntries().size() > 0 ?
new LinkedList<CompositionTimeToSample.Entry>(track.getCompositionTimeEntries()) : null;
long compositionTimeEntriesLeft = compositionTimeQueue != null ? compositionTimeQueue.peek().getCount() : -1;
if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty() ||
track.getSyncSamples() != null && track.getSyncSamples().length != 0) {
trun.setFlags(trun.getFlags() | 0x400);
}
for (int i = startSample; i < endSample; i++) {
TrackRunBox.Entry entry = new TrackRunBox.Entry();
entry.setSampleSize(sampleSizes[i]);
mdatSize += sampleSizes[i];
if (trun.isSampleFlagsPresentPresent()) {
long flag = 0;
if (track.getSampleDependencies() != null && !track.getSampleDependencies().isEmpty()) {
SampleDependencyTypeBox.Entry e = track.getSampleDependencies().get(i);
flag |= ((e.getSampleDependsOn() & 3) << 24);
flag |= ((e.getSampleIsDependentOn() & 3) << 22);
flag |= ((e.getSampleHasRedundancy() & 3) << 20);
}
if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) {
if (Arrays.binarySearch(track.getSyncSamples(), i) < 0) {
// we have to mark non-sync samples!
flag |= (1 << 16);
}
}
// i don't have sample degradation
entry.setSampleFlags(new SampleFlags(flag));
}
entry.setSampleDuration(timeQueue.peek().getDelta());
if (--durationEntriesLeft == 0 && timeQueue.size() > 1) {
timeQueue.remove();
durationEntriesLeft = timeQueue.peek().getCount();
}
if (compositionTimeQueue != null) {
entry.setSampleCompositionTimeOffset(compositionTimeQueue.peek().getOffset());
if (--compositionTimeEntriesLeft == 0 && compositionTimeQueue.size() > 1) {
compositionTimeQueue.remove();
compositionTimeEntriesLeft = compositionTimeQueue.element().getCount();
}
}
entries.add(entry);
}
//System.err.println(endSample - startSample);
//System.err.println("entries.size() " + entries.size());
trun.setEntries(entries);
trun.setDataOffset((int) (8 + moof.getSize())); // mdat header + moof size
return moof;
}
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);
movieBox.addBox(mvhd);
MovieExtendsBox mvex = new MovieExtendsBox();
for (Track track : movie.getTracks()) {
// Remove all boxes except the SampleDescriptionBox.
TrackExtendsBox trex = new TrackExtendsBox();
trex.setTrackId(track.getTrackMetaData().getTrackId());
trex.setDefaultSampleDescriptionIndex(1);
trex.setDefaultSampleDuration(0);
trex.setDefaultSampleSize(0);
trex.setDefaultSampleFlags(new SampleFlags(0));
// Don't set any good defaults here.
mvex.addBox(trex);
}
movieBox.addBox(mvex);
for (Track track : movie.getTracks()) {
if (track.getType() != Track.Type.UNKNOWN) {
movieBox.addBox(createTrackBox(track, movie));
}
}
// metadata here
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);
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());
stbl.addBox(new TimeToSampleBox());
stbl.addBox(new SampleToChunkBox());
stbl.addBox(new StaticChunkOffsetBox());
minf.addBox(stbl);
mdia.addBox(minf);
return trackBox;
}
public void setIntersectionFinder(FragmentIntersectionFinder intersectionFinder) {
this.intersectionFinder = intersectionFinder;
}
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);
}
}