package org.jcodec.containers.mp4.muxer;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.jcodec.common.Codec;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Packet;
import org.jcodec.common.model.Rational;
import org.jcodec.common.model.TapeTimecode;
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.Box;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.MovieHeaderBox;
import org.jcodec.containers.mp4.boxes.TimecodeSampleEntry;
import org.jcodec.movtool.Util;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Timecode MP4 muxer track
*
* @author The JCodec project
*
*/
public class TimecodeMP4MuxerTrack extends FramesMP4MuxerTrack {
private TapeTimecode prevTimecode;
private TapeTimecode firstTimecode;
private int fpsEstimate;
private long sampleDuration;
private long samplePts;
private int tcFrames;
private List<Edit> lower;
private List<Packet> gop;
public TimecodeMP4MuxerTrack(SeekableByteChannel out, int trackId) {
super(out, trackId, MP4TrackType.TIMECODE, Codec.TIMECODE);
this.lower = new ArrayList<Edit>();
this.gop = new ArrayList<Packet>();
}
public void addTimecode(Packet packet) throws IOException {
if(_timescale == NO_TIMESCALE_SET)
_timescale = packet.getTimescale();
if(_timescale != NO_TIMESCALE_SET && _timescale != packet.getTimescale())
throw new RuntimeException("MP4 timecode track doesn't support timescale switching.");
if (packet.isKeyFrame())
processGop();
gop.add(Packet.createPacketWithData(packet, (ByteBuffer) null));
}
private void processGop() throws IOException {
if (gop.size() > 0) {
for (Packet pkt : sortByDisplay(gop)) {
addTimecodeInt(pkt);
}
gop.clear();
}
}
private List<Packet> sortByDisplay(List<Packet> gop) {
ArrayList<Packet> result = new ArrayList<Packet>(gop);
Collections.sort(result, new Comparator<Packet>() {
public int compare(Packet o1, Packet o2) {
if (o1 == null && o2 == null)
return 0;
else if (o1 == null)
return -1;
else if (o2 == null)
return 1;
else
return o1.getDisplayOrder() > o2.getDisplayOrder() ? 1 : (o1.getDisplayOrder() == o2
.getDisplayOrder() ? 0 : -1);
}
});
return result;
}
protected Box finish(MovieHeaderBox mvhd) throws IOException {
processGop();
outTimecodeSample();
if (sampleEntries.size() == 0)
return null;
if (edits != null) {
edits = Util.editsOnEdits(new Rational(1, 1), lower, edits);
} else
edits = lower;
return super.finish(mvhd);
}
private void addTimecodeInt(Packet packet) throws IOException {
TapeTimecode tapeTimecode = packet.getTapeTimecode();
boolean gap = isGap(prevTimecode, tapeTimecode);
prevTimecode = tapeTimecode;
if (gap) {
outTimecodeSample();
firstTimecode = tapeTimecode;
fpsEstimate = tapeTimecode.isDropFrame() ? 30 : -1;
samplePts += sampleDuration;
sampleDuration = 0;
tcFrames = 0;
}
sampleDuration += packet.getDuration();
tcFrames++;
}
private boolean isGap(TapeTimecode prevTimecode, TapeTimecode tapeTimecode) {
boolean gap = false;
if (prevTimecode == null && tapeTimecode != null) {
gap = true;
} else if (prevTimecode != null) {
if (tapeTimecode == null)
gap = true;
else {
if (prevTimecode.isDropFrame() != tapeTimecode.isDropFrame()) {
gap = true;
} else {
gap = isTimeGap(prevTimecode, tapeTimecode);
}
}
}
return gap;
}
private boolean isTimeGap(TapeTimecode prevTimecode, TapeTimecode tapeTimecode) {
boolean gap = false;
int sec = toSec(tapeTimecode);
int secDiff = sec - toSec(prevTimecode);
if (secDiff == 0) {
int frameDiff = tapeTimecode.getFrame() - prevTimecode.getFrame();
if (fpsEstimate != -1)
frameDiff = (frameDiff + fpsEstimate) % fpsEstimate;
gap = frameDiff != 1;
} else if (secDiff == 1) {
if (fpsEstimate == -1) {
if (tapeTimecode.getFrame() == 0)
fpsEstimate = prevTimecode.getFrame() + 1;
else
gap = true;
} else {
int firstFrame = tapeTimecode.isDropFrame() && (sec % 60) == 0 && (sec % 600) != 0 ? 2 : 0;
if (tapeTimecode.getFrame() != firstFrame || prevTimecode.getFrame() != fpsEstimate - 1)
gap = true;
}
} else {
gap = true;
}
return gap;
}
private void outTimecodeSample() throws IOException {
if (sampleDuration > 0) {
if (firstTimecode != null) {
if (fpsEstimate == -1)
fpsEstimate = prevTimecode.getFrame() + 1;
TimecodeSampleEntry tmcd = TimecodeSampleEntry.createTimecodeSampleEntry((firstTimecode.isDropFrame() ? 1 : 0), _timescale, (int) (sampleDuration / tcFrames), fpsEstimate);
sampleEntries.add(tmcd);
ByteBuffer sample = ByteBuffer.allocate(4);
sample.putInt(toCounter(firstTimecode, fpsEstimate));
sample.flip();
addFrame(MP4Packet.createMP4Packet(sample, samplePts, _timescale, sampleDuration, 0, FrameType.KEY, null, 0, samplePts, sampleEntries.size() - 1));
lower.add(new Edit(sampleDuration, samplePts, 1.0f));
} else {
lower.add(new Edit(sampleDuration, -1, 1.0f));
}
}
}
private int toCounter(TapeTimecode tc, int fps) {
int frames = toSec(tc) * fps + tc.getFrame();
if (tc.isDropFrame()) {
long D = frames / 18000;
long M = frames % 18000;
frames -= 18 * D + 2 * ((M - 2) / 1800);
}
return frames;
}
private int toSec(TapeTimecode tc) {
return tc.getHour() * 3600 + tc.getMinute() * 60 + tc.getSecond();
}
}