package org.jcodec.containers.mp4.demuxer;
import static org.jcodec.common.TrackType.AUDIO;
import static org.jcodec.common.TrackType.OTHER;
import static org.jcodec.common.TrackType.VIDEO;
import static org.jcodec.containers.mp4.QTTimeUtil.mediaToEdited;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.jcodec.codecs.aac.AACUtils;
import org.jcodec.codecs.aac.ADTSParser;
import org.jcodec.codecs.aac.ADTSParser.Header;
import org.jcodec.codecs.h264.H264Utils;
import org.jcodec.codecs.h264.mp4.AvcCBox;
import org.jcodec.common.AudioCodecMeta;
import org.jcodec.common.Codec;
import org.jcodec.common.DemuxerTrackMeta;
import org.jcodec.common.TrackType;
import org.jcodec.common.VideoCodecMeta;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.ColorSpace;
import org.jcodec.common.model.Packet.FrameType;
import org.jcodec.containers.mp4.MP4Packet;
import org.jcodec.containers.mp4.MP4TrackType;
import org.jcodec.containers.mp4.boxes.AudioSampleEntry;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox;
import org.jcodec.containers.mp4.boxes.CompositionOffsetsBox.Entry;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
import org.jcodec.containers.mp4.boxes.PixelAspectExt;
import org.jcodec.containers.mp4.boxes.SampleSizesBox;
import org.jcodec.containers.mp4.boxes.SyncSamplesBox;
import org.jcodec.containers.mp4.boxes.TrakBox;
import org.jcodec.containers.mp4.boxes.VideoSampleEntry;
import org.jcodec.platform.Platform;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Regular MP4 track containing frames
*
* @author The JCodec project
*
*/
public class FramesMP4DemuxerTrack extends AbstractMP4DemuxerTrack {
private int[] sizes;
private long offInChunk;
private int noInChunk;
private int[] syncSamples;
private int[] partialSync;
private int ssOff;
private int psOff;
private Entry[] compOffsets;
private int cttsInd;
private int cttsSubInd;
private SeekableByteChannel input;
private MovieBox movie;
private ByteBuffer codecPrivate;
private AvcCBox avcC;
public FramesMP4DemuxerTrack(MovieBox mov, TrakBox trak, SeekableByteChannel input) {
super(trak);
this.input = input;
this.movie = mov;
SampleSizesBox stsz = NodeBox.findFirstPath(trak, SampleSizesBox.class, Box.path("mdia.minf.stbl.stsz"));
SyncSamplesBox stss = NodeBox.findFirstPath(trak, SyncSamplesBox.class, Box.path("mdia.minf.stbl.stss"));
SyncSamplesBox stps = NodeBox.findFirstPath(trak, SyncSamplesBox.class, Box.path("mdia.minf.stbl.stps"));
CompositionOffsetsBox ctts = NodeBox.findFirstPath(trak, CompositionOffsetsBox.class, Box.path("mdia.minf.stbl.ctts"));
compOffsets = ctts == null ? null : ctts.getEntries();
if (stss != null) {
syncSamples = stss.getSyncSamples();
}
if (stps != null) {
partialSync = stps.getSyncSamples();
}
sizes = stsz.getSizes();
if (getCodec() == Codec.H264) {
avcC = H264Utils.parseAVCC((VideoSampleEntry) getSampleEntries()[0]);
}
codecPrivate = getCodecPrivate();
}
@Override
public synchronized MP4Packet nextFrame() throws IOException {
if (curFrame >= sizes.length)
return null;
int size = sizes[(int) curFrame];
return getNextFrame(ByteBuffer.allocate(size));
}
@Override
public synchronized MP4Packet getNextFrame(ByteBuffer storage) throws IOException {
if (curFrame >= sizes.length)
return null;
int size = sizes[(int) curFrame];
if (storage != null && storage.remaining() < size) {
throw new IllegalArgumentException("Buffer size is not enough to fit a packet");
}
long pktPos = chunkOffsets[Math.min(chunkOffsets.length - 1, stcoInd)] + offInChunk;
ByteBuffer result = readPacketData(input, storage, pktPos, size);
if (result != null && result.remaining() < size)
return null;
int duration = timeToSamples[sttsInd].getSampleDuration();
boolean sync = syncSamples == null;
if (syncSamples != null && ssOff < syncSamples.length && (curFrame + 1) == syncSamples[ssOff]) {
sync = true;
ssOff++;
}
boolean psync = false;
if (partialSync != null && psOff < partialSync.length && (curFrame + 1) == partialSync[psOff]) {
psync = true;
psOff++;
}
long realPts = pts;
if (compOffsets != null) {
realPts = pts + compOffsets[cttsInd].getOffset();
cttsSubInd++;
if (cttsInd < compOffsets.length - 1 && cttsSubInd == compOffsets[cttsInd].getCount()) {
cttsInd++;
cttsSubInd = 0;
}
}
MP4Packet pkt = new MP4Packet(result == null ? null : convertPacket(result),
mediaToEdited(box, realPts, movie.getTimescale()), timescale, duration, curFrame,
sync ? FrameType.KEY : FrameType.INTER, null, 0, realPts, sampleToChunks[stscInd].getEntry() - 1,
pktPos, size, psync);
offInChunk += size;
curFrame++;
noInChunk++;
if (noInChunk >= sampleToChunks[stscInd].getCount()) {
noInChunk = 0;
offInChunk = 0;
nextChunk();
}
shiftPts(1);
return pkt;
}
@Override
public ByteBuffer convertPacket(ByteBuffer result) {
if (codecPrivate != null) {
if (getCodec() == Codec.H264) {
ByteBuffer annexbCoded = H264Utils.decodeMOVPacket(result, avcC);
if (H264Utils.isByteBufferIDRSlice(annexbCoded)) {
return NIOUtils.combine(codecPrivate, annexbCoded);
} else {
return annexbCoded;
}
} else if (getCodec() == Codec.AAC) {
// !!! crcAbsent, numAACFrames
Header adts = AACUtils.streamInfoToADTS(codecPrivate, true, 1, result.remaining());
ByteBuffer adtsRaw = ByteBuffer.allocate(7);
ADTSParser.write(adts, adtsRaw);
return NIOUtils.combine(adtsRaw, result);
}
}
return result;
}
@Override
public boolean gotoSyncFrame(long frameNo) {
if (syncSamples == null)
return gotoFrame(frameNo);
else {
if (frameNo < 0)
throw new IllegalArgumentException("negative frame number");
if (frameNo >= getFrameCount())
return false;
if (frameNo == curFrame)
return true;
for (int i = 0; i < syncSamples.length; i++) {
if (syncSamples[i] - 1 > frameNo)
return gotoFrame(syncSamples[i - 1] - 1);
}
return gotoFrame(syncSamples[syncSamples.length - 1] - 1);
}
}
@Override
protected void seekPointer(long frameNo) {
if (compOffsets != null) {
cttsSubInd = (int) frameNo;
cttsInd = 0;
while (cttsSubInd >= compOffsets[cttsInd].getCount()) {
cttsSubInd -= compOffsets[cttsInd].getCount();
cttsInd++;
}
}
curFrame = (int) frameNo;
stcoInd = 0;
stscInd = 0;
noInChunk = (int) frameNo;
offInChunk = 0;
while (noInChunk >= sampleToChunks[stscInd].getCount()) {
noInChunk -= sampleToChunks[stscInd].getCount();
nextChunk();
}
for (int i = 0; i < noInChunk; i++) {
offInChunk += sizes[(int) frameNo - noInChunk + i];
}
if (syncSamples != null)
for (ssOff = 0; ssOff < syncSamples.length && syncSamples[ssOff] < curFrame + 1; ssOff++)
;
if (partialSync != null)
for (psOff = 0; psOff < partialSync.length && partialSync[psOff] < curFrame + 1; psOff++)
;
}
@Override
public long getFrameCount() {
return sizes.length;
}
public ByteBuffer getCodecPrivate() {
Codec codec = getCodec();
if (codec == Codec.H264) {
AvcCBox avcC = H264Utils.parseAVCC((VideoSampleEntry) getSampleEntries()[0]);
return H264Utils.avcCToAnnexB(avcC);
} else if(codec == Codec.AAC) {
return AACUtils.getCodecPrivate(getSampleEntries()[0]);
}
// This codec does not have private section
return null;
}
@Override
public DemuxerTrackMeta getMeta() {
int[] seekFrames;
if (syncSamples == null) {
// all frames are I-frames
seekFrames = new int[(int) getFrameCount()];
for (int i = 0; i < seekFrames.length; i++) {
seekFrames[i] = i;
}
} else {
seekFrames = Platform.copyOfInt(syncSamples, syncSamples.length);
for (int i = 0; i < seekFrames.length; i++)
seekFrames[i]--;
}
MP4TrackType type = getType();
TrackType t = type == MP4TrackType.VIDEO ? VIDEO : (type == MP4TrackType.SOUND ? AUDIO : OTHER);
VideoCodecMeta videoCodecMeta = null;
AudioCodecMeta audioCodecMeta = null;
if (type == MP4TrackType.VIDEO) {
videoCodecMeta = new VideoCodecMeta(box.getCodedSize(), ColorSpace.YUV420);
PixelAspectExt pasp = NodeBox.findFirst(getSampleEntries()[0], PixelAspectExt.class, "pasp");
if (pasp != null)
videoCodecMeta.setPixelAspectRatio(pasp.getRational());
} else if (type == MP4TrackType.SOUND) {
AudioSampleEntry ase = (AudioSampleEntry) getSampleEntries()[0];
audioCodecMeta = new AudioCodecMeta(ase.getFormat());
}
DemuxerTrackMeta meta = new DemuxerTrackMeta(t, getCodec(), (double) duration / timescale, seekFrames,
sizes.length, getCodecPrivate(), videoCodecMeta, audioCodecMeta);
return meta;
}
}