package org.jcodec.movtool.streaming;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.jcodec.common.IntArrayList;
import org.jcodec.common.LongArrayList;
import org.jcodec.common.model.Rational;
import org.jcodec.common.model.Size;
import org.jcodec.containers.mp4.Brand;
import org.jcodec.containers.mp4.TrackType;
import org.jcodec.containers.mp4.boxes.AudioSampleEntry;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box;
import org.jcodec.containers.mp4.boxes.ClearApertureBox;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox;
import org.jcodec.containers.mp4.boxes.DataInfoBox;
import org.jcodec.containers.mp4.boxes.DataRefBox;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.EncodedPixelBox;
import org.jcodec.containers.mp4.boxes.GenericMediaInfoBox;
import org.jcodec.containers.mp4.boxes.HandlerBox;
import org.jcodec.containers.mp4.boxes.Header;
import org.jcodec.containers.mp4.boxes.LeafBox;
import org.jcodec.containers.mp4.boxes.MediaBox;
import org.jcodec.containers.mp4.boxes.MediaHeaderBox;
import org.jcodec.containers.mp4.boxes.MediaInfoBox;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.MovieHeaderBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.PixelAspectExt;
import org.jcodec.containers.mp4.boxes.ProductionApertureBox;
import org.jcodec.containers.mp4.boxes.SampleDescriptionBox;
import org.jcodec.containers.mp4.boxes.SampleEntry;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox.Entry;
import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry;
import org.jcodec.containers.mp4.boxes.SoundMediaHeaderBox;
import org.jcodec.containers.mp4.boxes.SyncSamplesBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox;
import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry;
import org.jcodec.containers.mp4.boxes.TimecodeMediaInfoBox;
import org.jcodec.containers.mp4.boxes.TrackHeaderBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.containers.mp4.boxes.VideoMediaHeaderBox;
import org.jcodec.containers.mp4.boxes.VideoSampleEntry;
import org.jcodec.containers.mp4.muxer.FramesMP4MuxerTrack;
import org.jcodec.movtool.streaming.VirtualMovie.PacketChunk;
import org.jcodec.movtool.streaming.VirtualTrack.VirtualEdit;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Contains methods to mux virtual movie tracks into real real MP4 movie header
*
* @author The JCodec project
*
*/
public class MovieHelper {
private static final int MEBABYTE = 1024 * 1024;
private static int timescales[] = { 10000, 12000, 15000, 24000, 25000, 30000, 50000, 41000, 48000, 96000 };
public static ByteBuffer produceHeader(PacketChunk[] chunks, VirtualTrack[] tracks, long dataSize, Brand brand)
throws IOException {
int defaultTimescale = 1000;
ByteBuffer buf = ByteBuffer.allocate(6 * MEBABYTE);
MovieBox movie = new MovieBox();
double[] trackDurations = calcTrackDurations(chunks, tracks);
long movieDur = calcMovieDuration(tracks, defaultTimescale, trackDurations);
movie.add(movieHeader(movie, tracks.length, movieDur, defaultTimescale));
for (int trackId = 0; trackId < tracks.length; trackId++) {
// TODO: optimal timescale selection
VirtualTrack track = tracks[trackId];
SampleEntry se = track.getSampleEntry();
boolean pcm = (se instanceof AudioSampleEntry) && ((AudioSampleEntry) se).isPCM();
int trackTimescale = track.getPreferredTimescale();
if (trackTimescale <= 0) {
if (pcm)
trackTimescale = getPCMTs((AudioSampleEntry) se, chunks, trackId);
else
trackTimescale = chooseTimescale(chunks, trackId);
} else if (trackTimescale < 100) {
trackTimescale *= 1000;
} else if (trackTimescale < 1000) {
trackTimescale *= 100;
} else if (trackTimescale < 10000) {
trackTimescale *= 10;
}
long totalDur = (long) (trackTimescale * trackDurations[trackId]);
TrakBox trak = new TrakBox();
Size dd = new Size(0, 0), sd = new Size(0, 0);
if (se instanceof VideoSampleEntry) {
VideoSampleEntry vse = (VideoSampleEntry) se;
PixelAspectExt pasp = Box.findFirst(vse, PixelAspectExt.class, "pasp");
if (pasp == null)
sd = dd = new Size(vse.getWidth(), vse.getHeight());
else {
Rational r = pasp.getRational();
dd = new Size(r.multiplyS(vse.getWidth()), vse.getHeight());
sd = new Size(vse.getWidth(), vse.getHeight());
}
}
TrackHeaderBox tkhd = new TrackHeaderBox(trackId + 1, movieDur, dd.getWidth(), dd.getHeight(),
new Date().getTime(), new Date().getTime(), 1.0f, (short) 0, 0, new int[] { 0x10000, 0, 0, 0,
0x10000, 0, 0, 0, 0x40000000 });
tkhd.setFlags(0xf);
trak.add(tkhd);
MediaBox media = new MediaBox();
trak.add(media);
media.add(new MediaHeaderBox(trackTimescale, totalDur, 0, new Date().getTime(), new Date().getTime(), 0));
TrackType tt = (se instanceof AudioSampleEntry) ? TrackType.SOUND : TrackType.VIDEO;
if (tt == TrackType.VIDEO) {
NodeBox tapt = new NodeBox(new Header("tapt"));
tapt.add(new ClearApertureBox(dd.getWidth(), dd.getHeight()));
tapt.add(new ProductionApertureBox(dd.getWidth(), dd.getHeight()));
tapt.add(new EncodedPixelBox(sd.getWidth(), sd.getHeight()));
trak.add(tapt);
}
HandlerBox hdlr = new HandlerBox("mhlr", tt.getHandler(), "appl", 0, 0);
media.add(hdlr);
MediaInfoBox minf = new MediaInfoBox();
media.add(minf);
mediaHeader(minf, tt);
minf.add(new HandlerBox("dhlr", "url ", "appl", 0, 0));
addDref(minf);
NodeBox stbl = new NodeBox(new Header("stbl"));
minf.add(stbl);
stbl.add(new SampleDescriptionBox(new SampleEntry[] { se }));
if (pcm) {
populateStblPCM(stbl, chunks, trackId, se);
} else {
populateStblGeneric(stbl, chunks, trackId, se, trackTimescale);
}
addEdits(trak, track, defaultTimescale, trackTimescale);
movie.add(trak);
}
brand.getFileTypeBox().write(buf);
movie.write(buf);
new Header("mdat", dataSize).write(buf);
buf.flip();
return buf;
}
private static int chooseTimescale(PacketChunk[] chunks, int trackId) {
for (int ch = 0; ch < chunks.length; ch++) {
if (chunks[ch].getTrack() == trackId) {
double dur = chunks[ch].getPacket().getDuration(), min = Double.MAX_VALUE;
int minTs = -1;
for (int ts = 0; ts < timescales.length; ts++) {
double dd = timescales[ts] * dur;
double diff = dd - (int) dd;
if (diff < min) {
minTs = ts;
min = diff;
}
}
return timescales[minTs];
}
}
return 0;
}
private static void addEdits(TrakBox trak, VirtualTrack track, int defaultTimescale, int trackTimescale) {
VirtualEdit[] edits = track.getEdits();
if (edits == null)
return;
List<Edit> result = new ArrayList<Edit>();
for (VirtualEdit virtualEdit : edits) {
result.add(new Edit((int) (virtualEdit.getDuration() * defaultTimescale),
(int) (virtualEdit.getIn() * trackTimescale), 1f));
}
trak.setEdits(result);
}
private static long calcMovieDuration(VirtualTrack[] tracks, int defaultTimescale, double[] dur) {
long movieDur = 0;
for (int trackId = 0; trackId < tracks.length; trackId++) {
movieDur = Math.max(movieDur, (long) (defaultTimescale * dur[trackId]));
}
return movieDur;
}
private static double[] calcTrackDurations(PacketChunk[] chunks, VirtualTrack[] tracks) {
double dur[] = new double[tracks.length];
Arrays.fill(dur, -1);
for (int chunkId = chunks.length - 1, n = 0; chunkId >= 0 && n < dur.length; chunkId--) {
PacketChunk chunk = chunks[chunkId];
int track = chunk.getTrack();
if (dur[track] == -1) {
dur[track] = chunk.getPacket().getPts() + chunk.getPacket().getDuration();
++n;
}
}
return dur;
}
private static void populateStblGeneric(NodeBox stbl, PacketChunk[] chunks, int trackId, SampleEntry se,
int timescale) throws IOException {
LongArrayList stco = new LongArrayList(250 << 10);
IntArrayList stsz = new IntArrayList(250 << 10);
List<TimeToSampleEntry> stts = new ArrayList<TimeToSampleEntry>();
IntArrayList stss = new IntArrayList(4 << 10);
int prevDur = 0;
int prevCount = -1;
boolean allKey = true;
List<Entry> compositionOffsets = new ArrayList<Entry>();
long ptsEstimate = 0;
int lastCompositionSamples = 0, lastCompositionOffset = 0;
for (int chunkNo = 0; chunkNo < chunks.length; chunkNo++) {
PacketChunk chunk = chunks[chunkNo];
if (chunk.getTrack() == trackId) {
stco.add(chunk.getPos());
stsz.add(Math.max(0, chunk.getDataLen()));
int dur = (int) Math.round(chunk.getPacket().getDuration() * timescale);
if (dur != prevDur) {
if (prevCount != -1)
stts.add(new TimeToSampleEntry(prevCount, prevDur));
prevDur = dur;
prevCount = 0;
}
++prevCount;
boolean key = chunk.getPacket().isKeyframe();
allKey &= key;
if (key)
stss.add(chunk.getPacket().getFrameNo() + 1);
long pts = Math.round(chunk.getPacket().getPts() * timescale);
int compositionOffset = (int)(pts - ptsEstimate);
if (compositionOffset != lastCompositionOffset) {
if (lastCompositionSamples > 0)
compositionOffsets.add(new Entry(lastCompositionSamples, lastCompositionOffset));
lastCompositionOffset = compositionOffset;
lastCompositionSamples = 0;
}
lastCompositionSamples++;
ptsEstimate += dur;
}
}
if (compositionOffsets.size() > 0) {
compositionOffsets.add(new Entry(lastCompositionSamples, lastCompositionOffset));
}
if (prevCount > 0)
stts.add(new TimeToSampleEntry(prevCount, prevDur));
if (!allKey)
stbl.add(new SyncSamplesBox(stss.toArray()));
stbl.add(new ChunkOffsets64Box(stco.toArray()));
stbl.add(new SampleToChunkBox(new SampleToChunkEntry[] { new SampleToChunkEntry(1, 1, 1) }));
stbl.add(new SampleSizesBox(stsz.toArray()));
stbl.add(new TimeToSampleBox(stts.toArray(new TimeToSampleEntry[0])));
compositionOffsets(compositionOffsets, stbl);
}
private static void compositionOffsets(List<Entry> compositionOffsets, NodeBox stbl) {
if (compositionOffsets.size() > 0) {
int min = FramesMP4MuxerTrack.minOffset(compositionOffsets);
for (Entry entry : compositionOffsets) {
entry.offset -= min;
}
stbl.add(new CompositionOffsetsBox(compositionOffsets.toArray(new Entry[0])));
}
}
private static void populateStblPCM(NodeBox stbl, PacketChunk[] chunks, int trackId, SampleEntry se)
throws IOException {
AudioSampleEntry ase = (AudioSampleEntry) se;
int frameSize = ase.calcFrameSize();
LongArrayList stco = new LongArrayList(250 << 10);
List<SampleToChunkEntry> stsc = new ArrayList<SampleToChunkEntry>();
int stscCount = -1, stscFirstChunk = -1, totalFrames = 0;
for (int chunkNo = 0, stscCurChunk = 1; chunkNo < chunks.length; chunkNo++) {
PacketChunk chunk = chunks[chunkNo];
if (chunk.getTrack() == trackId) {
stco.add(chunk.getPos());
int framesPerChunk = chunk.getDataLen() / frameSize;
if (framesPerChunk != stscCount) {
if (stscCount != -1)
stsc.add(new SampleToChunkEntry(stscFirstChunk, stscCount, 1));
stscFirstChunk = stscCurChunk;
stscCount = framesPerChunk;
}
stscCurChunk++;
totalFrames += framesPerChunk;
}
}
if (stscCount != -1)
stsc.add(new SampleToChunkEntry(stscFirstChunk, stscCount, 1));
stbl.add(new ChunkOffsets64Box(stco.toArray()));
stbl.add(new SampleToChunkBox(stsc.toArray(new SampleToChunkEntry[0])));
stbl.add(new SampleSizesBox(ase.calcFrameSize(), totalFrames));
stbl.add(new TimeToSampleBox(new TimeToSampleEntry[] { new TimeToSampleEntry(totalFrames, 1) }));
}
private static int getPCMTs(AudioSampleEntry se, PacketChunk[] chunks, int trackId) throws IOException {
for (int chunkNo = 0; chunkNo < chunks.length; chunkNo++) {
if (chunks[chunkNo].getTrack() == trackId) {
return (int) Math.round(chunks[chunkNo].getDataLen()
/ (se.calcFrameSize() * chunks[chunkNo].getPacket().getDuration()));
}
}
throw new RuntimeException("Crap");
}
private static void mediaHeader(MediaInfoBox minf, TrackType type) {
switch (type) {
case VIDEO:
VideoMediaHeaderBox vmhd = new VideoMediaHeaderBox(0, 0, 0, 0);
vmhd.setFlags(1);
minf.add(vmhd);
break;
case SOUND:
SoundMediaHeaderBox smhd = new SoundMediaHeaderBox();
smhd.setFlags(1);
minf.add(smhd);
break;
case TIMECODE:
NodeBox gmhd = new NodeBox(new Header("gmhd"));
gmhd.add(new GenericMediaInfoBox());
NodeBox tmcd = new NodeBox(new Header("tmcd"));
gmhd.add(tmcd);
tmcd.add(new TimecodeMediaInfoBox((short) 0, (short) 0, (short) 12, new short[] { 0, 0, 0 }, new short[] {
0xff, 0xff, 0xff }, "Lucida Grande"));
minf.add(gmhd);
break;
default:
throw new IllegalStateException("Handler " + type.getHandler() + " not supported");
}
}
private static void addDref(NodeBox minf) {
DataInfoBox dinf = new DataInfoBox();
minf.add(dinf);
DataRefBox dref = new DataRefBox();
dinf.add(dref);
dref.add(new LeafBox(new Header("alis", 0), ByteBuffer.wrap(new byte[] { 0, 0, 0, 1 })));
}
private static MovieHeaderBox movieHeader(NodeBox movie, int nTracks, long duration, int timescale) {
return new MovieHeaderBox(timescale, duration, 1.0f, 1.0f, new Date().getTime(), new Date().getTime(),
new int[] { 0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000 }, nTracks + 1);
}
}