package org.jcodec.containers.mp4.demuxer; import org.jcodec.common.IntArrayList; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.model.TapeTimecode; import org.jcodec.containers.mp4.MP4Packet; import org.jcodec.containers.mp4.QTTimeUtil; import org.jcodec.containers.mp4.boxes.Box; import org.jcodec.containers.mp4.boxes.ChunkOffsets64Box; import org.jcodec.containers.mp4.boxes.ChunkOffsetsBox; import org.jcodec.containers.mp4.boxes.MovieBox; import org.jcodec.containers.mp4.boxes.NodeBox; import org.jcodec.containers.mp4.boxes.SampleToChunkBox; import org.jcodec.containers.mp4.boxes.SampleToChunkBox.SampleToChunkEntry; import org.jcodec.containers.mp4.boxes.TimeToSampleBox; import org.jcodec.containers.mp4.boxes.TimeToSampleBox.TimeToSampleEntry; import org.jcodec.containers.mp4.boxes.TimecodeSampleEntry; import org.jcodec.containers.mp4.boxes.TrakBox; import java.io.IOException; import java.nio.ByteBuffer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Timecode track, provides timecode information for video track * * @author The JCodec project * */ public class TimecodeMP4DemuxerTrack { private TrakBox box; private TimeToSampleEntry[] timeToSamples; private int[] sampleCache; private TimecodeSampleEntry tse; private SeekableByteChannel input; private MovieBox movie; private long[] chunkOffsets; private SampleToChunkEntry[] sampleToChunks; public TimecodeMP4DemuxerTrack(MovieBox movie, TrakBox trak, SeekableByteChannel input) throws IOException { this.box = trak; this.input = input; this.movie = movie; NodeBox stbl = trak.getMdia().getMinf().getStbl(); TimeToSampleBox stts = NodeBox.findFirst(stbl, TimeToSampleBox.class, "stts"); SampleToChunkBox stsc = NodeBox.findFirst(stbl, SampleToChunkBox.class, "stsc"); ChunkOffsetsBox stco = NodeBox.findFirst(stbl, ChunkOffsetsBox.class, "stco"); ChunkOffsets64Box co64 = NodeBox.findFirst(stbl, ChunkOffsets64Box.class, "co64"); timeToSamples = stts.getEntries(); chunkOffsets = stco != null ? stco.getChunkOffsets() : co64.getChunkOffsets(); sampleToChunks = stsc.getSampleToChunk(); if (chunkOffsets.length == 1) { cacheSamples(sampleToChunks, chunkOffsets); } tse = (TimecodeSampleEntry) box.getSampleEntries()[0]; } public MP4Packet getTimecode(MP4Packet pkt) throws IOException { long tv = QTTimeUtil.editedToMedia(box, box.rescale(pkt.getPts(), pkt.getTimescale()), movie.getTimescale()); int sample; int ttsInd = 0, ttsSubInd = 0; for (sample = 0; sample < sampleCache.length - 1; sample++) { int dur = timeToSamples[ttsInd].getSampleDuration(); if (tv < dur) break; tv -= dur; ttsSubInd++; if (ttsInd < timeToSamples.length - 1 && ttsSubInd >= timeToSamples[ttsInd].getSampleCount()) ttsInd++; } int frameNo = (int) ((((2 * tv * tse.getTimescale()) / box.getTimescale()) / tse.getFrameDuration()) + 1) / 2; return MP4Packet.createMP4PacketWithTimecode(pkt, _getTimecode(getTimecodeSample(sample), frameNo, tse)); } private int getTimecodeSample(int sample) throws IOException { if (sampleCache != null) return sampleCache[sample]; else { synchronized (input) { int stscInd, stscSubInd; for (stscInd = 0, stscSubInd = sample; stscInd < sampleToChunks.length && stscSubInd >= sampleToChunks[stscInd].getCount(); stscSubInd -= sampleToChunks[stscInd] .getCount(), stscInd++) ; long offset = chunkOffsets[stscInd] + (Math.min(stscSubInd, sampleToChunks[stscInd].getCount() - 1) << 2); if (input.position() != offset) input.setPosition(offset); ByteBuffer buf = NIOUtils.fetchFromChannel(input, 4); return buf.getInt(); } } } private TapeTimecode _getTimecode(int startCounter, int frameNo, TimecodeSampleEntry entry) { int frame = dropFrameAdjust(frameNo + startCounter, entry); int sec = frame / entry.getNumFrames(); return new TapeTimecode((short) (sec / 3600), (byte) ((sec / 60) % 60), (byte) (sec % 60), (byte) (frame % entry.getNumFrames()), entry.isDropFrame()); } private int dropFrameAdjust(int frame, TimecodeSampleEntry entry) { if (entry.isDropFrame()) { long D = frame / 17982; long M = frame % 17982; frame += 18 * D + 2 * ((M - 2) / 1798); } return frame; } private void cacheSamples(SampleToChunkEntry[] sampleToChunks, long[] chunkOffsets) throws IOException { synchronized (input) { int stscInd = 0; IntArrayList ss = IntArrayList.createIntArrayList(); for (int chunkNo = 0; chunkNo < chunkOffsets.length; chunkNo++) { int nSamples = sampleToChunks[stscInd].getCount(); if (stscInd < sampleToChunks.length - 1 && chunkNo + 1 >= sampleToChunks[stscInd + 1].getFirst()) stscInd++; long offset = chunkOffsets[chunkNo]; input.setPosition(offset); ByteBuffer buf = NIOUtils.fetchFromChannel(input, nSamples * 4); for (int i = 0; i < nSamples; i++) { ss.add(buf.getInt()); } } sampleCache = ss.toArray(); } } /** * * @return * @throws IOException * @deprecated Use getTimecode to automatically populate tape timecode for * each frame */ public int getStartTimecode() throws IOException { return getTimecodeSample(0); } public TrakBox getBox() { return box; } public int parseTimecode(String tc) { String[] split = tc.split(":"); TimecodeSampleEntry tmcd = NodeBox.findFirstPath(box, TimecodeSampleEntry.class, Box.path("mdia.minf.stbl.stsd.tmcd")); byte nf = tmcd.getNumFrames(); return Integer.parseInt(split[3]) + Integer.parseInt(split[2]) * nf + Integer.parseInt(split[1]) * 60 * nf + Integer.parseInt(split[0]) * 3600 * nf; } public int timeCodeToFrameNo(String timeCode) throws Exception { if (isValidTimeCode(timeCode)) { int movieFrame = parseTimecode(timeCode.trim()) - sampleCache[0]; int frameRate = tse.getNumFrames(); long framesInTimescale = movieFrame * tse.getTimescale(); long mediaToEdited = QTTimeUtil.mediaToEdited(box, framesInTimescale / frameRate, movie.getTimescale()) * frameRate; return (int) (mediaToEdited / box.getTimescale()); } return -1; } private static boolean isValidTimeCode(String input) { Pattern p = Pattern.compile("[0-9][0-9]:[0-5][0-9]:[0-5][0-9]:[0-2][0-9]"); Matcher m = p.matcher(input); if (input != null && !input.trim().equals("") && m.matches()) { return true; } return false; } }