package org.jcodec.api.transcode; import static org.jcodec.common.io.NIOUtils.readableFileChannel; import static org.jcodec.common.io.NIOUtils.writableFileChannel; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.jcodec.codecs.aac.AACDecoder; import org.jcodec.codecs.h264.BufferH264ES; import org.jcodec.codecs.h264.H264Decoder; import org.jcodec.codecs.h264.H264Encoder; import org.jcodec.codecs.h264.H264Utils; import org.jcodec.codecs.mjpeg.JpegDecoder; import org.jcodec.codecs.mpeg12.MPEGDecoder; import org.jcodec.codecs.png.PNGDecoder; import org.jcodec.codecs.png.PNGEncoder; import org.jcodec.codecs.prores.ProresDecoder; import org.jcodec.codecs.prores.ProresEncoder; import org.jcodec.codecs.raw.RAWVideoDecoder; import org.jcodec.codecs.vpx.IVFMuxer; import org.jcodec.codecs.vpx.VP8Decoder; import org.jcodec.codecs.vpx.VP8Encoder; import org.jcodec.codecs.wav.WavDemuxer; import org.jcodec.codecs.wav.WavMuxer; import org.jcodec.common.AudioCodecMeta; import org.jcodec.common.AudioDecoder; import org.jcodec.common.AudioEncoder; import org.jcodec.common.AudioFormat; import org.jcodec.common.Codec; import org.jcodec.common.Demuxer; import org.jcodec.common.DemuxerTrack; import org.jcodec.common.DemuxerTrackMeta; import org.jcodec.common.Format; import org.jcodec.common.Muxer; import org.jcodec.common.MuxerTrack; import org.jcodec.common.SeekableDemuxerTrack; import org.jcodec.common.Tuple._3; import org.jcodec.common.VideoCodecMeta; import org.jcodec.common.VideoDecoder; import org.jcodec.common.VideoEncoder; import org.jcodec.common.VideoEncoder.EncodedFrame; import org.jcodec.common.io.IOUtils; import org.jcodec.common.io.NIOUtils; import org.jcodec.common.io.SeekableByteChannel; import org.jcodec.common.logging.Logger; import org.jcodec.common.model.AudioBuffer; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Packet; import org.jcodec.common.model.Packet.FrameType; import org.jcodec.common.model.Picture8Bit; import org.jcodec.common.model.Size; import org.jcodec.containers.imgseq.ImageSequenceDemuxer; import org.jcodec.containers.imgseq.ImageSequenceMuxer; import org.jcodec.containers.mkv.demuxer.MKVDemuxer; import org.jcodec.containers.mkv.muxer.MKVMuxer; import org.jcodec.containers.mp4.demuxer.MP4Demuxer; import org.jcodec.containers.mp4.muxer.MP4Muxer; import org.jcodec.containers.mps.MPEGDemuxer.MPEGDemuxerTrack; import org.jcodec.containers.mps.MPSDemuxer; import org.jcodec.containers.mps.MTSDemuxer; import org.jcodec.containers.webp.WebpDemuxer; import org.jcodec.containers.y4m.Y4MDemuxer; import org.jcodec.api.transcode.filters.ColorTransformFilter; import net.sourceforge.jaad.aac.AACException; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Transcoder core. * * @author The JCodec project * */ public class Transcoder { private static final int REORDER_BUFFER_SIZE = 7; private ThreadLocal<Picture8Bit> pixelBufferStore = new ThreadLocal<Picture8Bit>(); private ThreadLocal<ByteBuffer> bufferStore = new ThreadLocal<ByteBuffer>(); private Format inputFormat; private Format outputFormat; private _3<Integer, Integer, Codec> inputVideoCodec; private Codec outputVideoCodec; private _3<Integer, Integer, Codec> inputAudioCodec; private Codec outputAudioCodec; private int seekFrames; private int maxFrames; private String sourceName; private String destName; private boolean videoCodecCopy; private boolean audioCodecCopy; private List<Filter> extraFilters; private SeekableByteChannel sourceStream; private SeekableByteChannel destStream; private Demuxer demuxVideo; private Demuxer demuxAudio; private Muxer muxer; private MuxerTrack videoOutputTrack; private DemuxerTrack videoInputTrack; private DemuxerTrack audioInputTrack; private MuxerTrack audioOutputTrack; private AudioDecoder audioDecoder; private AudioEncoder audioEncoder; private VideoDecoder videoDecoder; private VideoEncoder videoEncoder; private String profile; private Boolean interlaced; private Integer downscale; private VideoCodecMeta videoCool; private List<Filter> filters = new ArrayList<Filter>(); private boolean framesOutput; public Transcoder(String sourceName, String destName, Format inputFormat, Format outputFormat, _3<Integer, Integer, Codec> inputVideoCodec, Codec outputVideoCodec, _3<Integer, Integer, Codec> inputAudioCodec, Codec outputAudioCodec, boolean videoCodecCopy, boolean audioCodecCopy, List<Filter> extraFilters) { this.sourceName = sourceName; this.destName = destName; this.inputFormat = inputFormat; this.outputFormat = outputFormat; this.inputVideoCodec = inputVideoCodec; this.outputVideoCodec = outputVideoCodec; this.inputAudioCodec = inputAudioCodec; this.outputAudioCodec = outputAudioCodec; this.videoCodecCopy = videoCodecCopy; this.audioCodecCopy = audioCodecCopy; this.extraFilters = extraFilters; // Inferring video-only or audio-only output if (!outputFormat.isVideo()) { this.inputVideoCodec = null; this.outputVideoCodec = null; } if (!outputFormat.isAudio()) { this.inputAudioCodec = null; this.outputAudioCodec = null; } } public Format getInputFormat() { return inputFormat; } public Format getOutputFormat() { return outputFormat; } public _3<Integer, Integer, Codec> getIntputVideoCodec() { return inputVideoCodec; } public Codec getOutputVideoCodec() { return outputVideoCodec; } public _3<Integer, Integer, Codec> getInputAudioCode() { return inputAudioCodec; } public Codec getOutputAudioCodec() { return outputAudioCodec; } public int getSeekFrames() { return seekFrames; } public void setSeekFrames(int seekFrames) { this.seekFrames = seekFrames; } public int getMaxFrames() { return maxFrames; } public void setMaxFrames(int maxFrames) { this.maxFrames = maxFrames; } public void setProfile(String profile) { this.profile = profile; } public void setInterlaced(Boolean interlaced) { this.interlaced = interlaced; } public void setDownscale(int downscale) { this.downscale = downscale; } /** * Filters the decoded image before it gets to encoder. * * @author stan */ public static interface Filter { Picture8Bit filter(Picture8Bit picture, PixelStore store); } public static interface PixelStore { Picture8Bit getPicture(int width, int height, ColorSpace color); void putBack(Picture8Bit frame); } public static class PixelStoreImpl implements PixelStore { private List<Picture8Bit> buffers = new ArrayList<Picture8Bit>(); @Override public Picture8Bit getPicture(int width, int height, ColorSpace color) { for (Picture8Bit picture8Bit : buffers) { if (picture8Bit.getWidth() == width && picture8Bit.getHeight() == height && picture8Bit.getColor() == color) { buffers.remove(picture8Bit); return picture8Bit; } } return Picture8Bit.create(width, height, color); } @Override public void putBack(Picture8Bit frame) { frame.setCrop(null); buffers.add(frame); } } protected void initDecode(String sourceName) throws IOException { if (inputFormat != Format.IMG) sourceStream = readableFileChannel(sourceName); switch (inputFormat) { case MOV: demuxVideo = demuxAudio = new MP4Demuxer(sourceStream); break; case MKV: demuxVideo = demuxAudio = new MKVDemuxer(sourceStream); break; case IMG: demuxVideo = new ImageSequenceDemuxer(sourceName, maxFrames); break; case WEBP: demuxVideo = new WebpDemuxer(sourceStream); break; case MPEG_PS: demuxVideo = demuxAudio = new MPSDemuxer(sourceStream); break; case Y4M: Y4MDemuxer y4mDemuxer = new Y4MDemuxer(sourceStream); demuxVideo = demuxAudio = y4mDemuxer; videoInputTrack = y4mDemuxer; break; case H264: demuxVideo = new BufferH264ES(NIOUtils.fetchFromChannel(sourceStream)); break; case WAV: demuxAudio = new WavDemuxer(sourceStream); break; case MPEG_TS: MTSDemuxer mtsDemuxer = new MTSDemuxer(sourceStream); MPSDemuxer mpsDemuxer = null; if (inputVideoCodec != null) { mpsDemuxer = new MPSDemuxer(mtsDemuxer.getProgram(inputVideoCodec.v0)); videoInputTrack = openTSTrack(mpsDemuxer, inputVideoCodec.v1); demuxVideo = mpsDemuxer; } if (inputAudioCodec != null) { if (inputVideoCodec == null || inputVideoCodec.v0 != inputAudioCodec.v0) { mpsDemuxer = new MPSDemuxer(mtsDemuxer.getProgram(inputAudioCodec.v0)); } audioInputTrack = openTSTrack(mpsDemuxer, inputAudioCodec.v1); demuxAudio = mpsDemuxer; } for (int pid : mtsDemuxer.getPrograms()) { if ((inputVideoCodec == null || pid != inputVideoCodec.v0) && (inputAudioCodec == null || pid != inputAudioCodec.v0)) { Logger.info("Unused program: " + pid); mtsDemuxer.getProgram(pid).close(); } } default: throw new RuntimeException("Input format: " + inputFormat + " is not supported."); } if (demuxVideo != null && inputVideoCodec != null) { List<? extends DemuxerTrack> videoTracks = demuxVideo.getVideoTracks(); if (videoTracks.size() > 0) { videoInputTrack = videoTracks.get(inputVideoCodec.v1); DemuxerTrackMeta meta = videoInputTrack.getMeta(); if (meta != null) videoDecoder = createVideoDecoder(inputVideoCodec.v2, downscale, meta.getCodecPrivate(), meta.getVideoCodecMeta()); } } if (demuxAudio != null && inputAudioCodec != null) { List<? extends DemuxerTrack> audioTracks = demuxAudio.getAudioTracks(); if (audioTracks.size() > 0) { audioInputTrack = audioTracks.get(inputAudioCodec.v1); DemuxerTrackMeta meta = audioInputTrack.getMeta(); if (meta != null) audioDecoder = createAudioDecoder(meta.getCodecPrivate()); } } } private AudioDecoder createAudioDecoder(ByteBuffer codecPrivate) throws AACException { switch (inputAudioCodec.v2) { case AAC: return new AACDecoder(codecPrivate); case PCM: return new RawAudioDecoder(audioInputTrack.getMeta().getAudioCodecMeta().getFormat()); } return null; } private VideoDecoder createVideoDecoder(Codec codec, int downscale, ByteBuffer codecPrivate, VideoCodecMeta videoCodecMeta) { switch (codec) { case H264: return H264Decoder.createH264DecoderFromCodecPrivate(codecPrivate); case PNG: return new PNGDecoder(); case MPEG2: return MPEGDecoder.createMpegDecoder(downscale); case PRORES: return ProresDecoder.createProresDecoder(downscale); case VP8: return new VP8Decoder(); case JPEG: return JpegDecoder.createJpegDecoder(downscale); case RAW: Size dim = videoCodecMeta.getSize(); return new RAWVideoDecoder(dim.getWidth(), dim.getHeight()); } return null; } private MPEGDemuxerTrack openTSTrack(MPSDemuxer demuxerVideo, Integer selectedTrack) { int trackNo = 0; for (MPEGDemuxerTrack track : demuxerVideo.getTracks()) { if (trackNo == selectedTrack) { return track; } else track.ignore(); ++trackNo; } return null; } protected void initEncode(String destName) throws IOException { if (outputFormat != Format.IMG) destStream = writableFileChannel(destName); switch (outputFormat) { case MKV: muxer = new MKVMuxer(destStream); break; case MOV: muxer = MP4Muxer.createMP4MuxerToChannel(destStream); break; case IVF: muxer = new IVFMuxer(destStream); break; case IMG: muxer = new ImageSequenceMuxer(destName); break; case WAV: muxer = new WavMuxer(destStream); break; } if (outputVideoCodec != null) { switch (outputVideoCodec) { case PRORES: videoEncoder = new ProresEncoder(profile, interlaced); break; case H264: videoEncoder = H264Encoder.createH264Encoder(); break; case VP8: videoEncoder = VP8Encoder.createVP8Encoder(10); break; case PNG: videoEncoder = new PNGEncoder(); break; default: throw new RuntimeException("Could not find encoder for the codec: " + outputVideoCodec); } filters.add(0, new ColorTransformFilter(videoEncoder.getSupportedColorSpaces()[0])); filters.addAll(extraFilters); } } protected void finishEncode() throws IOException { if (framesOutput) { muxer.finish(); } else { Logger.warn("No frames output."); } if (destStream != null) { IOUtils.closeQuietly(destStream); } } protected Picture8Bit createPixelBuffer(ByteBuffer firstFrame) { if (videoCool == null) { DemuxerTrackMeta meta = videoInputTrack.getMeta(); if (meta != null && meta.getVideoCodecMeta() != null) { videoCool = meta.getVideoCodecMeta(); } else { videoCool = videoDecoder.getCodecMeta(firstFrame); } } Size size = videoCool.getSize(); return Picture8Bit.create((size.getWidth() + 15) & ~0xf, (size.getHeight() + 15) & ~0xf, videoCool.getColor()); } protected Packet inputVideoPacket() throws IOException { if (videoInputTrack == null) return null; Packet nextFrame = videoInputTrack.nextFrame(); if (nextFrame != null) Logger.debug(String.format("Input frame: pts=%d, duration=%d", nextFrame.getPts(), nextFrame.getDuration())); if (videoDecoder == null) { videoDecoder = createVideoDecoder(inputVideoCodec.v2, downscale, nextFrame.getData(), null); } if (videoCodecCopy && videoOutputTrack == null) { VideoCodecMeta meta = videoDecoder.getCodecMeta(nextFrame.getData()); ; videoOutputTrack = muxer.addVideoTrack(inputVideoCodec.v2, meta); } return nextFrame; } protected void outputVideoPacket(Packet packet) throws IOException { videoOutputTrack.addFrame(packet); framesOutput = true; } protected Picture8Bit decodeVideo(ByteBuffer data, Picture8Bit target1) { return videoDecoder.decodeFrame8Bit(data, target1.getData()); } protected EncodedFrame encodeVideo(Picture8Bit frame, ByteBuffer _out) { if (videoOutputTrack == null) { videoOutputTrack = muxer.addVideoTrack(outputVideoCodec, new VideoCodecMeta(new Size(frame.getWidth(), frame.getHeight()), frame.getColor())); } return videoEncoder.encodeFrame8Bit(frame, _out); } protected boolean haveAudio() { return audioInputTrack != null; } protected Packet inputAudioPacket() throws IOException { if (audioInputTrack == null) return null; Packet packet = audioInputTrack.nextFrame(); if (audioDecoder == null) { audioDecoder = createAudioDecoder(packet.getData()); } if (audioOutputTrack == null) { AudioCodecMeta meta = audioDecoder.getCodecMeta(packet.getData()); audioOutputTrack = muxer.addAudioTrack(outputAudioCodec, meta); } return packet; } protected void outputAudioPacket(Packet audioPkt) throws IOException { audioOutputTrack.addFrame(audioPkt); framesOutput = true; } protected ByteBuffer decodeAudio(ByteBuffer audioPkt) throws IOException { if (inputAudioCodec.v2 == Codec.PCM) { AudioFormat format = audioInputTrack.getMeta().getAudioCodecMeta().getFormat(); if (audioEncoder == null) { audioEncoder = createAudioEncoder(inputAudioCodec.v2, format); } return audioPkt; } else { AudioBuffer decodeFrame = audioDecoder.decodeFrame(audioPkt, null); if (audioOutputTrack == null) { this.audioOutputTrack = muxer.addAudioTrack(outputAudioCodec, new AudioCodecMeta(decodeFrame.getFormat())); } if (audioEncoder == null) { audioEncoder = createAudioEncoder(outputAudioCodec, decodeFrame.getFormat()); } return decodeFrame.getData(); } } private AudioEncoder createAudioEncoder(Codec codec, AudioFormat format) { if (codec != Codec.PCM) { throw new RuntimeException("Only PCM audio encoding (RAW audio) is supported."); } return new RawAudioEncoder(); } private static class RawAudioEncoder implements AudioEncoder { @Override public ByteBuffer encode(ByteBuffer audioPkt, ByteBuffer buf) { return audioPkt; } } private static class RawAudioDecoder implements AudioDecoder { private AudioFormat format; public RawAudioDecoder(AudioFormat format) { this.format = format; } @Override public AudioBuffer decodeFrame(ByteBuffer frame, ByteBuffer dst) throws IOException { return new AudioBuffer(frame, format, frame.remaining() / format.getFrameSize()); } @Override public AudioCodecMeta getCodecMeta(ByteBuffer data) throws IOException { return new AudioCodecMeta(format); } } protected ByteBuffer encodeAudio(ByteBuffer wrap) { return audioEncoder.encode(wrap, null); } protected boolean seek(int frame) throws IOException { Packet inFrame; if (videoInputTrack instanceof SeekableDemuxerTrack) { SeekableDemuxerTrack seekable = (SeekableDemuxerTrack) videoInputTrack; seekable.gotoFrame(frame); while ((inFrame = inputVideoPacket()) != null && !inFrame.isKeyFrame()) ; seekable.gotoFrame(inFrame.getFrameNo()); } else { Logger.error("Can not seek in " + videoInputTrack + " container."); return false; } return true; } protected int getBufferSize(Picture8Bit frame) { return videoEncoder.estimateBufferSize(frame); } protected boolean audioCodecCopy() { return audioCodecCopy; } protected boolean videoCodecCopy() { return videoCodecCopy; } private class FrameWithPacket implements Comparable<FrameWithPacket> { private Packet packet; private Picture8Bit frame; public FrameWithPacket(Packet inFrame, Picture8Bit dec2) { this.packet = inFrame; this.frame = dec2; } @Override public int compareTo(FrameWithPacket arg) { if (arg == null) return -1; else { long pts1 = packet.getPts(); long pts2 = arg.packet.getPts(); return pts1 > pts2 ? 1 : (pts1 == pts2 ? 0 : -1); } } } public void transcode() throws IOException { List<FrameWithPacket> reorderBuffer = new ArrayList<FrameWithPacket>(); try { initDecode(sourceName); initEncode(destName); int skipFrames = 0; if (seekFrames > 0) { if (!seek(seekFrames)) { Logger.warn("Unable to seek, will have to skip."); skipFrames = seekFrames; } } if (maxFrames < Integer.MAX_VALUE) maxFrames += seekFrames; Packet inVideoPacket; boolean framesDecoded = false; PixelStore pixelsStore = new PixelStoreImpl(); for (int frameNo = 0; (inVideoPacket = inputVideoPacket()) != null && frameNo <= maxFrames; frameNo++) { if (skipFrames > 0) { skipFrames--; continue; } if (!videoCodecCopy() && !framesDecoded) { if (inVideoPacket.getFrameType() == FrameType.UNKOWN) { detectFrameType(inVideoPacket); } if (!inVideoPacket.isKeyFrame()) continue; } framesDecoded = true; if (haveAudio()) { double endPts = inVideoPacket.getPtsD() + 0.2; outputAudioPacketsTo(endPts); } Picture8Bit decodedFrame = null; if (!videoCodecCopy()) { Picture8Bit pixelBuffer = pixelBufferStore.get(); if (pixelBuffer == null) { pixelBuffer = createPixelBuffer(inVideoPacket.getData()); pixelBufferStore.set(pixelBuffer); } decodedFrame = decodeVideo(inVideoPacket.getData(), pixelBuffer); if (decodedFrame == null) continue; for (Filter filter : filters) { decodedFrame = filter.filter(decodedFrame, pixelsStore); } } printLegend(frameNo, maxFrames, inVideoPacket); if (reorderBuffer.size() > REORDER_BUFFER_SIZE) { outFrames(reorderBuffer, pixelsStore, 1); } reorderBuffer.add(new FrameWithPacket(inVideoPacket, decodedFrame)); } if (reorderBuffer.size() > 0) { outFrames(reorderBuffer, pixelsStore, reorderBuffer.size()); } // Remaining audio packets outputAudioPacketsTo(Double.MAX_VALUE); } catch (Exception e) { e.printStackTrace(System.err); // Logger.error("Error in transcode: " + e.getMessage()); } finally { finishDecode(); finishEncode(); } } private void detectFrameType(Packet inVideoPacket) { if (inputVideoCodec.v2 != Codec.H264) { throw new RuntimeException("Input frame type detection is only supported for h.264"); } inVideoPacket.setFrameType(H264Utils.isByteBufferIDRSlice(inVideoPacket.getData()) ? FrameType.KEY : FrameType.INTER); } private void finishDecode() { if (sourceStream != null) IOUtils.closeQuietly(sourceStream); } private void outputAudioPacketsTo(double endPts) throws IOException { Packet audioPkt; do { audioPkt = inputAudioPacket(); if (audioPkt == null) break; if (!audioCodecCopy()) { ByteBuffer decodedAudio = decodeAudio(audioPkt.getData()); outputAudioPacket(Packet.createPacketWithData(audioPkt, encodeAudio(decodedAudio))); } else { outputAudioPacket(audioPkt); } } while (audioPkt.getPtsD() < endPts); } private void printLegend(int frameNo, int maxFrames, Packet inVideoPacket) { if (frameNo % 100 == 0) System.out.print(String.format("[%6d]\r", frameNo)); } private void outFrames(List<FrameWithPacket> frames, PixelStore pixelStore, int nFrames) throws IOException { long duration = findDuration(frames); System.out.println("\n" + duration); if (!videoCodecCopy) Collections.sort(frames); for (int i = 0; i < nFrames; i++) { FrameWithPacket frame = frames.remove(0); Packet outputVideoPacket; if (frame.frame != null) { ByteBuffer buffer = bufferStore.get(); int bufferSize = getBufferSize(frame.frame); if (buffer == null || bufferSize < buffer.capacity()) { buffer = ByteBuffer.allocate(bufferSize); bufferStore.set(buffer); } buffer.clear(); EncodedFrame enc = encodeVideo(frame.frame, buffer); pixelStore.putBack(frame.frame); outputVideoPacket = Packet.createPacketWithData(frame.packet, NIOUtils.clone(enc.getData())); outputVideoPacket.setFrameType(enc.isKeyFrame() ? FrameType.KEY : FrameType.INTER); } else { outputVideoPacket = Packet.createPacketWithData(frame.packet, NIOUtils.clone(frame.packet.getData())); } if (duration != -1) outputVideoPacket.setDuration(duration); outputVideoPacket(outputVideoPacket); } } private long findDuration(List<FrameWithPacket> frames) { long min = Long.MAX_VALUE; for (FrameWithPacket frame1 : frames) { long pts1 = frame1.packet.getPts(); for (FrameWithPacket frame2 : frames) { long pts2 = frame2.packet.getPts(); long duration = pts2 - pts1; if (duration > 0 && duration < min) min = duration; } } return min; } }