package org.jcodec.api;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.jcodec.api.specific.AVCMP4Adaptor;
import org.jcodec.api.specific.ContainerAdaptor;
import org.jcodec.common.DemuxerTrackMeta;
import org.jcodec.common.Format;
import org.jcodec.common.JCodecUtil;
import org.jcodec.common.SeekableDemuxerTrack;
import org.jcodec.common.io.FileChannelWrapper;
import org.jcodec.common.io.NIOUtils;
import org.jcodec.common.io.SeekableByteChannel;
import org.jcodec.common.model.Packet;
import org.jcodec.common.model.Picture8Bit;
import org.jcodec.containers.mp4.demuxer.MP4Demuxer;
/**
* This class is part of JCodec ( www.jcodec.org ) This software is distributed
* under FreeBSD License
*
* Extracts frames from a movie into uncompressed images suitable for
* processing.
*
* Supports going to random points inside of a movie ( seeking ) by frame number
* of by second.
*
* NOTE: Supports only AVC ( H.264 ) in MP4 ( ISO BMF, QuickTime ) at this
* point.
*
* @author The JCodec project
*
*/
public class FrameGrab8Bit {
private SeekableDemuxerTrack videoTrack;
private ContainerAdaptor decoder;
private ThreadLocal<byte[][]> buffers;
public static FrameGrab8Bit createFrameGrab8Bit(SeekableByteChannel _in) throws IOException, JCodecException {
ByteBuffer header = ByteBuffer.allocate(65536);
_in.read(header);
header.flip();
Format detectFormat = JCodecUtil.detectFormatBuffer(header);
SeekableDemuxerTrack videoTrack_;
switch (detectFormat) {
case MOV:
MP4Demuxer d1 = new MP4Demuxer(_in);
videoTrack_ = (SeekableDemuxerTrack)d1.getVideoTrack();
break;
case MPEG_PS:
throw new UnsupportedFormatException("MPEG PS is temporarily unsupported.");
case MPEG_TS:
throw new UnsupportedFormatException("MPEG TS is temporarily unsupported.");
default:
throw new UnsupportedFormatException("Container format is not supported by JCodec");
}
FrameGrab8Bit fg = new FrameGrab8Bit(videoTrack_, detectDecoder(videoTrack_));
fg.decodeLeadingFrames();
return fg;
}
public FrameGrab8Bit(SeekableDemuxerTrack videoTrack, ContainerAdaptor decoder) {
this.videoTrack = videoTrack;
this.decoder = decoder;
this.buffers = new ThreadLocal<byte[][]>();
}
private SeekableDemuxerTrack sdt() throws JCodecException {
if (!(videoTrack instanceof SeekableDemuxerTrack))
throw new JCodecException("Not a seekable track");
return (SeekableDemuxerTrack) videoTrack;
}
/**
* Position frame grabber to a specific second in a movie. As a result the
* next decoded frame will be precisely at the requested second.
*
* WARNING: potentially very slow. Use only when you absolutely need precise
* seek. Tries to seek to exactly the requested second and for this it might
* have to decode a sequence of frames from the closes key frame. Depending
* on GOP structure this may be as many as 500 frames.
*
* @param second
* @return
* @throws IOException
* @throws JCodecException
*/
public FrameGrab8Bit seekToSecondPrecise(double second) throws IOException, JCodecException {
sdt().seek(second);
decodeLeadingFrames();
return this;
}
/**
* Position frame grabber to a specific frame in a movie. As a result the
* next decoded frame will be precisely the requested frame number.
*
* WARNING: potentially very slow. Use only when you absolutely need precise
* seek. Tries to seek to exactly the requested frame and for this it might
* have to decode a sequence of frames from the closes key frame. Depending
* on GOP structure this may be as many as 500 frames.
*
* @param frameNumber
* @return
* @throws IOException
* @throws JCodecException
*/
public FrameGrab8Bit seekToFramePrecise(int frameNumber) throws IOException, JCodecException {
sdt().gotoFrame(frameNumber);
decodeLeadingFrames();
return this;
}
/**
* Position frame grabber to a specific second in a movie.
*
* Performs a sloppy seek, meaning that it may actually not seek to exact
* second requested, instead it will seek to the closest key frame
*
* NOTE: fast, as it just seeks to the closest previous key frame and
* doesn't try to decode frames in the middle
*
* @param second
* @return
* @throws IOException
* @throws JCodecException
*/
public FrameGrab8Bit seekToSecondSloppy(double second) throws IOException, JCodecException {
sdt().seek(second);
goToPrevKeyframe();
return this;
}
/**
* Position frame grabber to a specific frame in a movie
*
* Performs a sloppy seek, meaning that it may actually not seek to exact
* frame requested, instead it will seek to the closest key frame
*
* NOTE: fast, as it just seeks to the closest previous key frame and
* doesn't try to decode frames in the middle
*
* @param frameNumber
* @return
* @throws IOException
* @throws JCodecException
*/
public FrameGrab8Bit seekToFrameSloppy(int frameNumber) throws IOException, JCodecException {
sdt().gotoFrame(frameNumber);
goToPrevKeyframe();
return this;
}
private void goToPrevKeyframe() throws IOException, JCodecException {
sdt().gotoFrame(detectKeyFrame((int) sdt().getCurFrame()));
}
private void decodeLeadingFrames() throws IOException, JCodecException {
SeekableDemuxerTrack sdt = sdt();
int curFrame = (int) sdt.getCurFrame();
int keyFrame = detectKeyFrame(curFrame);
sdt.gotoFrame(keyFrame);
Packet frame = sdt.nextFrame();
if (decoder == null)
decoder = detectDecoder(sdt);
while (frame.getFrameNo() < curFrame) {
decoder.decodeFrame8Bit(frame, getBuffer());
frame = sdt.nextFrame();
}
sdt.gotoFrame(curFrame);
}
private byte[][] getBuffer() {
byte[][] buf = buffers.get();
if (buf == null) {
buf = decoder.allocatePicture8Bit();
buffers.set(buf);
}
return buf;
}
private int detectKeyFrame(int start) throws IOException {
int[] seekFrames = videoTrack.getMeta().getSeekFrames();
if (seekFrames == null)
return start;
int prev = seekFrames[0];
for (int i = 1; i < seekFrames.length; i++) {
if (seekFrames[i] > start)
break;
prev = seekFrames[i];
}
return prev;
}
private static ContainerAdaptor detectDecoder(SeekableDemuxerTrack videoTrack) throws JCodecException {
DemuxerTrackMeta meta = videoTrack.getMeta();
switch (meta.getCodec()) {
case H264:
return new AVCMP4Adaptor(meta);
default:
throw new UnsupportedFormatException("Codec is not supported");
}
}
/**
* Get frame at current position in JCodec native image
*
* @return A decoded picture with metadata.
* @throws IOException
*/
public PictureWithMetadata8Bit getNativeFrameWithMetadata() throws IOException {
Packet frame = videoTrack.nextFrame();
if (frame == null)
return null;
Picture8Bit picture = decoder.decodeFrame8Bit(frame, getBuffer());
return new PictureWithMetadata8Bit(picture, frame.getPtsD(), frame.getDurationD());
}
/**
* Get frame at current position in JCodec native image
*
* @return
* @throws IOException
*/
public Picture8Bit getNativeFrame() throws IOException {
Packet frame = videoTrack.nextFrame();
if (frame == null)
return null;
return decoder.decodeFrame8Bit(frame, getBuffer());
}
/**
* Get frame at a specified second as JCodec image
*
* @param file
* @param second
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getFrameAtSec(File file, double second) throws IOException, JCodecException {
FileChannelWrapper ch = null;
try {
ch = NIOUtils.readableChannel(file);
return createFrameGrab8Bit(ch).seekToSecondPrecise(second).getNativeFrame();
} finally {
NIOUtils.closeQuietly(ch);
}
}
/**
* Get frame at a specified second as JCodec image
*
* @param file
* @param second
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getFrameFromChannelAtSec(SeekableByteChannel file, double second)
throws JCodecException, IOException {
return createFrameGrab8Bit(file).seekToSecondPrecise(second).getNativeFrame();
}
/**
* Get frame at a specified frame number as JCodec image
*
* @param file
* @param second
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getFrameFromFile(File file, int frameNumber) throws IOException, JCodecException {
FileChannelWrapper ch = null;
try {
ch = NIOUtils.readableChannel(file);
return createFrameGrab8Bit(ch).seekToFramePrecise(frameNumber).getNativeFrame();
} finally {
NIOUtils.closeQuietly(ch);
}
}
/**
* Get frame at a specified frame number as JCodec image
*
* @param file
* @param second
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getFrameFromChannel(SeekableByteChannel file, int frameNumber)
throws JCodecException, IOException {
return createFrameGrab8Bit(file).seekToFramePrecise(frameNumber).getNativeFrame();
}
/**
* Get a specified frame by number from an already open demuxer track
*
* @param vt
* @param decoder
* @param frameNumber
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getNativeFrameAtFrame(SeekableDemuxerTrack vt, ContainerAdaptor decoder, int frameNumber)
throws IOException, JCodecException {
return new FrameGrab8Bit(vt, decoder).seekToFramePrecise(frameNumber).getNativeFrame();
}
/**
* Get a specified frame by second from an already open demuxer track
*
* @param vt
* @param decoder
* @param frameNumber
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getNativeFrameAtSec(SeekableDemuxerTrack vt, ContainerAdaptor decoder, double second)
throws IOException, JCodecException {
return new FrameGrab8Bit(vt, decoder).seekToSecondPrecise(second).getNativeFrame();
}
/**
* Get a specified frame by number from an already open demuxer track (
* sloppy mode, i.e. nearest keyframe )
*
* @param vt
* @param decoder
* @param frameNumber
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getNativeFrameSloppy(SeekableDemuxerTrack vt, ContainerAdaptor decoder, int frameNumber)
throws IOException, JCodecException {
return new FrameGrab8Bit(vt, decoder).seekToFrameSloppy(frameNumber).getNativeFrame();
}
/**
* Get a specified frame by second from an already open demuxer track (
* sloppy mode, i.e. nearest keyframe )
*
* @param vt
* @param decoder
* @param frameNumber
* @return
* @throws IOException
* @throws JCodecException
*/
public static Picture8Bit getNativeFrameAtSecSloppy(SeekableDemuxerTrack vt, ContainerAdaptor decoder, double second)
throws IOException, JCodecException {
return new FrameGrab8Bit(vt, decoder).seekToSecondSloppy(second).getNativeFrame();
}
/**
* Gets info about the media
*
* @return
*/
public MediaInfo getMediaInfo() {
return decoder.getMediaInfo();
}
/**
* @return the videoTrack
*/
public SeekableDemuxerTrack getVideoTrack() {
return videoTrack;
}
/**
* @return the decoder
*/
public ContainerAdaptor getDecoder() {
return decoder;
}
}