package org.jcodec.containers.mp4;
import org.jcodec.common.model.RationalLarge;
import org.jcodec.containers.mp4.boxes.Box;
import org.jcodec.containers.mp4.boxes.Edit;
import org.jcodec.containers.mp4.boxes.MovieBox;
import org.jcodec.containers.mp4.boxes.NodeBox;
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 org.jcodec.containers.mp4.demuxer.TimecodeMP4DemuxerTrack;
import java.io.IOException;
import java.util.List;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Quicktime time conversion utilities
*
* @author The JCodec project
*
*/
public class QTTimeUtil {
/**
* Calculates track duration considering edits
*
* @param track
* @return
*/
public static long getEditedDuration(TrakBox track) {
List<Edit> edits = track.getEdits();
if (edits == null)
return track.getDuration();
long duration = 0;
for (Edit edit : edits) {
duration += edit.getDuration();
}
return duration;
}
/**
* Finds timevalue of a frame number
*
* might be an expensive operation sinse it traverses compressed time to
* sample table
*
* @param frameNumber
* @return
*/
public static long frameToTimevalue(TrakBox trak, int frameNumber) {
TimeToSampleBox stts = NodeBox.findFirstPath(trak, TimeToSampleBox.class, Box.path("mdia.minf.stbl.stts"));
TimeToSampleEntry[] timeToSamples = stts.getEntries();
long pts = 0;
int sttsInd = 0, sttsSubInd = frameNumber;
while (sttsSubInd >= timeToSamples[sttsInd].getSampleCount()) {
sttsSubInd -= timeToSamples[sttsInd].getSampleCount();
pts += timeToSamples[sttsInd].getSampleCount() * timeToSamples[sttsInd].getSampleDuration();
sttsInd++;
}
return pts + timeToSamples[sttsInd].getSampleDuration() * sttsSubInd;
}
/**
* Finds frame by timevalue
*
* @param tv
* @return
*/
public static int timevalueToFrame(TrakBox trak, long tv) {
TimeToSampleEntry[] tts = NodeBox.findFirstPath(trak, TimeToSampleBox.class, Box.path("mdia.minf.stbl.stts")).getEntries();
int frame = 0;
for (int i = 0; tv > 0 && i < tts.length; i++) {
long rem = tv / tts[i].getSampleDuration();
tv -= tts[i].getSampleCount() * tts[i].getSampleDuration();
frame += tv > 0 ? tts[i].getSampleCount() : rem;
}
return frame;
}
/**
* Converts media timevalue to edited timevalue
*
* @param trak
* @param mediaTv
* @param movieTimescale
* @return
*/
public static long mediaToEdited(TrakBox trak, long mediaTv, int movieTimescale) {
if (trak.getEdits() == null)
return mediaTv;
long accum = 0;
for (Edit edit : trak.getEdits()) {
if (mediaTv < edit.getMediaTime())
return accum;
long duration = trak.rescale(edit.getDuration(), movieTimescale);
if (edit.getMediaTime() != -1
&& (mediaTv >= edit.getMediaTime() && mediaTv < edit.getMediaTime() + duration)) {
accum += mediaTv - edit.getMediaTime();
break;
}
accum += duration;
}
return accum;
}
/**
* Converts edited timevalue to media timevalue
*
* @param trak
* @param mediaTv
* @param movieTimescale
* @return
*/
public static long editedToMedia(TrakBox trak, long editedTv, int movieTimescale) {
if (trak.getEdits() == null)
return editedTv;
long accum = 0;
for (Edit edit : trak.getEdits()) {
long duration = trak.rescale(edit.getDuration(), movieTimescale);
if (accum + duration > editedTv) {
return edit.getMediaTime() + editedTv - accum;
}
accum += duration;
}
return accum;
}
/**
* Calculates frame number as it shows in quicktime player
*
* @param movie
* @param mediaFrameNo
* @return
*/
public static int qtPlayerFrameNo(MovieBox movie, int mediaFrameNo) {
TrakBox videoTrack = movie.getVideoTrack();
long editedTv = mediaToEdited(videoTrack, frameToTimevalue(videoTrack, mediaFrameNo), movie.getTimescale());
return tv2QTFrameNo(movie, editedTv);
}
public static int tv2QTFrameNo(MovieBox movie, long tv) {
TrakBox videoTrack = movie.getVideoTrack();
TrakBox timecodeTrack = movie.getTimecodeTrack();
if (timecodeTrack != null && BoxUtil.containsBox2(videoTrack, "tref", "tmcd")) {
return timevalueToTimecodeFrame(timecodeTrack, new RationalLarge(tv, videoTrack.getTimescale()),
movie.getTimescale());
} else {
return timevalueToFrame(videoTrack, tv);
}
}
/**
* Calculates and formats standard time as in Quicktime player
*
* @param movie
* @param mediaFrameNo
* @return
*/
public static String qtPlayerTime(MovieBox movie, int mediaFrameNo) {
TrakBox videoTrack = movie.getVideoTrack();
long editedTv = mediaToEdited(videoTrack, frameToTimevalue(videoTrack, mediaFrameNo), movie.getTimescale());
int sec = (int) (editedTv / videoTrack.getTimescale());
return String.format("%02d", sec / 3600) + "_" + String.format("%02d", (sec % 3600) / 60) + "_"
+ String.format("%02d", sec % 60);
}
/**
* Calculates and formats tape timecode as in Quicktime player
*
* @param timecodeTrack
* @param tv
* @param startCounter
* @return
* @throws IOException
*/
public static String qtPlayerTimecodeFromMovie(MovieBox movie, TimecodeMP4DemuxerTrack timecodeTrack, int mediaFrameNo)
throws IOException {
TrakBox videoTrack = movie.getVideoTrack();
long editedTv = mediaToEdited(videoTrack, frameToTimevalue(videoTrack, mediaFrameNo), movie.getTimescale());
TrakBox tt = timecodeTrack.getBox();
int ttTimescale = tt.getTimescale();
long ttTv = editedToMedia(tt, editedTv * ttTimescale / videoTrack.getTimescale(), movie.getTimescale());
return formatTimecode(
timecodeTrack.getBox(),
timecodeTrack.getStartTimecode()
+ timevalueToTimecodeFrame(timecodeTrack.getBox(), new RationalLarge(ttTv, ttTimescale),
movie.getTimescale()));
}
/**
* Calculates and formats tape timecode as in Quicktime player
*
* @param timecodeTrack
* @param tv
* @param startCounter
* @return
* @throws IOException
*/
public static String qtPlayerTimecode(TimecodeMP4DemuxerTrack timecodeTrack, RationalLarge tv, int movieTimescale)
throws IOException {
TrakBox tt = timecodeTrack.getBox();
int ttTimescale = tt.getTimescale();
long ttTv = editedToMedia(tt, tv.multiplyS(ttTimescale), movieTimescale);
return formatTimecode(
timecodeTrack.getBox(),
timecodeTrack.getStartTimecode()
+ timevalueToTimecodeFrame(timecodeTrack.getBox(), new RationalLarge(ttTv, ttTimescale),
movieTimescale));
}
/**
* Converts timevalue to frame number based on timecode track
*
* @param timecodeTrack
* @param tv
* @return
*/
public static int timevalueToTimecodeFrame(TrakBox timecodeTrack, RationalLarge tv, int movieTimescale) {
TimecodeSampleEntry se = (TimecodeSampleEntry) timecodeTrack.getSampleEntries()[0];
return (int) ((2 * tv.multiplyS(se.getTimescale()) / se.getFrameDuration()) + 1) / 2;
}
/**
* Formats tape timecode based on frame counter
*
* @param timecodeTrack
* @param counter
* @return
*/
public static String formatTimecode(TrakBox timecodeTrack, int counter) {
TimecodeSampleEntry tmcd = NodeBox.findFirstPath(timecodeTrack, TimecodeSampleEntry.class, Box.path("mdia.minf.stbl.stsd.tmcd"));
byte nf = tmcd.getNumFrames();
String tc = String.format("%02d", counter % nf);
counter /= nf;
tc = String.format("%02d", counter % 60) + ":" + tc;
counter /= 60;
tc = String.format("%02d", counter % 60) + ":" + tc;
counter /= 60;
tc = String.format("%02d", counter) + ":" + tc;
return tc;
}
}