package org.jcodec.movtool.streaming;
import static org.jcodec.containers.mp4.MP4TrackType.SOUND;
import static org.jcodec.containers.mp4.MP4TrackType.TIMECODE;
import static org.jcodec.containers.mp4.MP4TrackType.VIDEO;
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.api.UnhandledStateException;
import org.jcodec.codecs.h264.H264Utils;
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.MP4TrackType;
import org.jcodec.containers.mp4.boxes.AudioSampleEntry;
import org.jcodec.containers.mp4.boxes.Box.LeafBox;
import org.jcodec.containers.mp4.boxes.ChannelBox;
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.CompositionOffsetsBox.Entry;
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.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.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.muxer.FramesMP4MuxerTrack;
import org.jcodec.containers.mp4.muxer.MP4Muxer;
import org.jcodec.movtool.streaming.VirtualMP4Movie.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 = MovieBox.createMovieBox();
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];
CodecMeta codecMeta = track.getCodecMeta();
boolean pcm = (codecMeta instanceof AudioCodecMeta) && ((AudioCodecMeta) codecMeta).isPCM();
int trackTimescale = track.getPreferredTimescale();
if (trackTimescale <= 0) {
if (pcm)
trackTimescale = getPCMTs((AudioCodecMeta) codecMeta, 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 = TrakBox.createTrakBox();
Size dd = new Size(0, 0), sd = new Size(0, 0);
if (codecMeta instanceof VideoCodecMeta) {
VideoCodecMeta meta = (VideoCodecMeta) codecMeta;
Rational pasp = meta.getPasp();
if (pasp == null)
sd = dd = meta.getSize();
else {
sd = meta.getSize();
dd = new Size(pasp.multiplyS(sd.getWidth()), sd.getHeight());
}
}
TrackHeaderBox tkhd = TrackHeaderBox.createTrackHeaderBox(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 = MediaBox.createMediaBox();
trak.add(media);
media.add(MediaHeaderBox.createMediaHeaderBox(trackTimescale, totalDur, 0, new Date().getTime(), new Date().getTime(), 0));
MP4TrackType tt = (codecMeta instanceof AudioCodecMeta) ? MP4TrackType.SOUND : MP4TrackType.VIDEO;
if (tt == MP4TrackType.VIDEO) {
NodeBox tapt = new NodeBox(new Header("tapt"));
tapt.add(ClearApertureBox.createClearApertureBox(dd.getWidth(), dd.getHeight()));
tapt.add(ProductionApertureBox.createProductionApertureBox(dd.getWidth(), dd.getHeight()));
tapt.add(EncodedPixelBox.createEncodedPixelBox(sd.getWidth(), sd.getHeight()));
trak.add(tapt);
}
HandlerBox hdlr = HandlerBox.createHandlerBox("mhlr", tt.getHandler(), "appl", 0, 0);
media.add(hdlr);
MediaInfoBox minf = MediaInfoBox.createMediaInfoBox();
media.add(minf);
mediaHeader(minf, tt);
minf.add(HandlerBox.createHandlerBox("dhlr", "url ", "appl", 0, 0));
addDref(minf);
NodeBox stbl = new NodeBox(new Header("stbl"));
minf.add(stbl);
stbl.add(SampleDescriptionBox.createSampleDescriptionBox(new SampleEntry[] { toSampleEntry(codecMeta) }));
if (pcm) {
populateStblPCM(stbl, chunks, trackId, codecMeta);
} else {
populateStblGeneric(stbl, chunks, trackId, codecMeta, trackTimescale);
}
addEdits(trak, track, defaultTimescale, trackTimescale);
movie.add(trak);
}
brand.getFileTypeBox().write(buf);
movie.write(buf);
Header.createHeader("mdat", dataSize).write(buf);
buf.flip();
return buf;
}
private static SampleEntry toSampleEntry(CodecMeta se) {
Rational pasp = null;
SampleEntry vse;
if ("avc1".equals(se.getFourcc())) {
vse = H264Utils.createMOVSampleEntryFromBytes(se.getCodecPrivate().duplicate());
pasp = ((VideoCodecMeta) se).getPasp();
} else if (se instanceof VideoCodecMeta) {
VideoCodecMeta ss = (VideoCodecMeta) se;
pasp = ss.getPasp();
vse = MP4Muxer.videoSampleEntry(se.getFourcc(), ss.getSize(), "JCodec");
} else {
AudioCodecMeta ss = (AudioCodecMeta) se;
if (ss.isPCM()) {
vse = MP4Muxer.audioSampleEntry(se.getFourcc(), 1, ss.getSampleSize(), ss.getChannelCount(),
ss.getSampleRate(), ss.getEndian());
} else {
vse = FramesMP4MuxerTrack.compressedAudioSampleEntry(se.getFourcc(), 1, ss.getSampleSize(),
ss.getChannelCount(), ss.getSampleRate(), ss.getSamplesPerPacket(), ss.getBytesPerPacket(),
ss.getBytesPerFrame());
}
ChannelBox chan = ChannelBox.createChannelBox();
AudioSampleEntry.setLabels(ss.getChannelLabels(), chan);
vse.add(chan);
}
if (pasp != null)
vse.add(PixelAspectExt.createPixelAspectExt(pasp));
return vse;
}
private static int chooseTimescale(PacketChunk[] chunks, int trackId) {
for (int ch = 0; ch < chunks.length; ch++) {
if (chunks[ch].getTrackNo() == 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.getTrackNo();
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, CodecMeta 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.getTrackNo() == 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(SyncSamplesBox.createSyncSamplesBox(stss.toArray()));
stbl.add(ChunkOffsets64Box.createChunkOffsets64Box(stco.toArray()));
stbl.add(SampleToChunkBox.createSampleToChunkBox(new SampleToChunkEntry[] { new SampleToChunkEntry(1, 1, 1) }));
stbl.add(SampleSizesBox.createSampleSizesBox2(stsz.toArray()));
stbl.add(TimeToSampleBox.createTimeToSampleBox(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(CompositionOffsetsBox.createCompositionOffsetsBox(compositionOffsets.toArray(new Entry[0])));
}
}
private static void populateStblPCM(NodeBox stbl, PacketChunk[] chunks, int trackId, CodecMeta se)
throws IOException {
AudioCodecMeta ase = (AudioCodecMeta) se;
int frameSize = ase.getFrameSize();
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.getTrackNo() == 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(ChunkOffsets64Box.createChunkOffsets64Box(stco.toArray()));
stbl.add(SampleToChunkBox.createSampleToChunkBox(stsc.toArray(new SampleToChunkEntry[0])));
stbl.add(SampleSizesBox.createSampleSizesBox(ase.getFrameSize(), totalFrames));
stbl.add(TimeToSampleBox.createTimeToSampleBox(new TimeToSampleEntry[] { new TimeToSampleEntry(totalFrames, 1) }));
}
private static int getPCMTs(AudioCodecMeta se, PacketChunk[] chunks, int trackId) throws IOException {
for (int chunkNo = 0; chunkNo < chunks.length; chunkNo++) {
if (chunks[chunkNo].getTrackNo() == trackId) {
return (int) Math.round(chunks[chunkNo].getDataLen()
/ (se.getFrameSize() * chunks[chunkNo].getPacket().getDuration()));
}
}
throw new RuntimeException("Crap");
}
private static void mediaHeader(MediaInfoBox minf, MP4TrackType type) {
if (VIDEO == type) {
VideoMediaHeaderBox vmhd = VideoMediaHeaderBox.createVideoMediaHeaderBox(0, 0, 0, 0);
vmhd.setFlags(1);
minf.add(vmhd);
} else if(SOUND == type) {
SoundMediaHeaderBox smhd = SoundMediaHeaderBox.createSoundMediaHeaderBox();
smhd.setFlags(1);
minf.add(smhd);
} else if(TIMECODE == type) {
NodeBox gmhd = new NodeBox(new Header("gmhd"));
gmhd.add(GenericMediaInfoBox.createGenericMediaInfoBox());
NodeBox tmcd = new NodeBox(new Header("tmcd"));
gmhd.add(tmcd);
tmcd.add(TimecodeMediaInfoBox
.createTimecodeMediaInfoBox((short) 0, (short) 0, (short) 12, new short[] { 0, 0, 0 }, new short[] {
0xff, 0xff, 0xff }, "Lucida Grande"));
minf.add(gmhd);
} else {
throw new UnhandledStateException("Handler " + type.getHandler() + " not supported");
}
}
private static void addDref(NodeBox minf) {
DataInfoBox dinf = DataInfoBox.createDataInfoBox();
minf.add(dinf);
DataRefBox dref = DataRefBox.createDataRefBox();
dinf.add(dref);
dref.add(LeafBox.createLeafBox(Header.createHeader("alis", 0), ByteBuffer.wrap(new byte[] { 0, 0, 0, 1 })));
}
private static MovieHeaderBox movieHeader(NodeBox movie, int nTracks, long duration, int timescale) {
return MovieHeaderBox.createMovieHeaderBox(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);
}
}