www in docker support
This commit is contained in:
parent
539a848e95
commit
c227fce036
2145 changed files with 399596 additions and 58 deletions
|
|
@ -0,0 +1,23 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import org.jcodec.common.model.AudioBuffer;
|
||||
import org.jcodec.common.model.Packet;
|
||||
|
||||
public class AudioFrameWithPacket {
|
||||
private AudioBuffer audio;
|
||||
|
||||
private Packet packet;
|
||||
|
||||
public AudioFrameWithPacket(AudioBuffer audio, Packet packet) {
|
||||
this.audio = audio;
|
||||
this.packet = packet;
|
||||
}
|
||||
|
||||
public AudioBuffer getAudio() {
|
||||
return this.audio;
|
||||
}
|
||||
|
||||
public Packet getPacket() {
|
||||
return this.packet;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
|
||||
public interface Filter {
|
||||
PixelStore.LoanerPicture filter(Picture paramPicture, PixelStore paramPixelStore);
|
||||
|
||||
ColorSpace getInputColor();
|
||||
|
||||
ColorSpace getOutputColor();
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
public enum Options {
|
||||
PROFILE, INTERLACED, DOWNSCALE;
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.jcodec.common.AudioCodecMeta;
|
||||
import org.jcodec.common.VideoCodecMeta;
|
||||
import org.jcodec.common.model.Packet;
|
||||
|
||||
public interface PacketSink {
|
||||
void outputVideoPacket(Packet paramPacket, VideoCodecMeta paramVideoCodecMeta) throws IOException;
|
||||
|
||||
void outputAudioPacket(Packet paramPacket, AudioCodecMeta paramAudioCodecMeta) throws IOException;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.jcodec.common.model.Packet;
|
||||
|
||||
public interface PacketSource {
|
||||
Packet inputVideoPacket() throws IOException;
|
||||
|
||||
Packet inputAudioPacket() throws IOException;
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
|
||||
public interface PixelStore {
|
||||
LoanerPicture getPicture(int paramInt1, int paramInt2, ColorSpace paramColorSpace);
|
||||
|
||||
void putBack(LoanerPicture paramLoanerPicture);
|
||||
|
||||
void retake(LoanerPicture paramLoanerPicture);
|
||||
|
||||
public static class LoanerPicture {
|
||||
private Picture picture;
|
||||
|
||||
private int refCnt;
|
||||
|
||||
public LoanerPicture(Picture picture, int refCnt) {
|
||||
this.picture = picture;
|
||||
this.refCnt = refCnt;
|
||||
}
|
||||
|
||||
public Picture getPicture() {
|
||||
return this.picture;
|
||||
}
|
||||
|
||||
public int getRefCnt() {
|
||||
return this.refCnt;
|
||||
}
|
||||
|
||||
public void decRefCnt() {
|
||||
this.refCnt--;
|
||||
}
|
||||
|
||||
public boolean unused() {
|
||||
return (this.refCnt <= 0);
|
||||
}
|
||||
|
||||
public void incRefCnt() {
|
||||
this.refCnt++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
|
||||
public class PixelStoreImpl implements PixelStore {
|
||||
private List<Picture> buffers = new ArrayList<>();
|
||||
|
||||
public PixelStore.LoanerPicture getPicture(int width, int height, ColorSpace color) {
|
||||
for (Picture picture : this.buffers) {
|
||||
if (picture.getWidth() == width && picture.getHeight() == height &&
|
||||
picture.getColor() == color) {
|
||||
this.buffers.remove(picture);
|
||||
return new PixelStore.LoanerPicture(picture, 1);
|
||||
}
|
||||
}
|
||||
return new PixelStore.LoanerPicture(Picture.create(width, height, color), 1);
|
||||
}
|
||||
|
||||
public void putBack(PixelStore.LoanerPicture frame) {
|
||||
frame.decRefCnt();
|
||||
if (frame.unused()) {
|
||||
Picture pixels = frame.getPicture();
|
||||
pixels.setCrop(null);
|
||||
this.buffers.add(pixels);
|
||||
}
|
||||
}
|
||||
|
||||
public void retake(PixelStore.LoanerPicture frame) {
|
||||
frame.incRefCnt();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
|
||||
public interface Sink {
|
||||
void init() throws IOException;
|
||||
|
||||
void outputVideoFrame(VideoFrameWithPacket paramVideoFrameWithPacket) throws IOException;
|
||||
|
||||
void outputAudioFrame(AudioFrameWithPacket paramAudioFrameWithPacket) throws IOException;
|
||||
|
||||
void finish() throws IOException;
|
||||
|
||||
ColorSpace getInputColor();
|
||||
|
||||
void setOption(Options paramOptions, Object paramObject);
|
||||
|
||||
boolean isVideo();
|
||||
|
||||
boolean isAudio();
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.jcodec.codecs.h264.H264Encoder;
|
||||
import org.jcodec.codecs.png.PNGEncoder;
|
||||
import org.jcodec.codecs.prores.ProresEncoder;
|
||||
import org.jcodec.codecs.raw.RAWVideoEncoder;
|
||||
import org.jcodec.codecs.vpx.IVFMuxer;
|
||||
import org.jcodec.codecs.vpx.VP8Encoder;
|
||||
import org.jcodec.codecs.wav.WavMuxer;
|
||||
import org.jcodec.codecs.y4m.Y4MMuxer;
|
||||
import org.jcodec.common.AudioCodecMeta;
|
||||
import org.jcodec.common.AudioEncoder;
|
||||
import org.jcodec.common.AudioFormat;
|
||||
import org.jcodec.common.Codec;
|
||||
import org.jcodec.common.Format;
|
||||
import org.jcodec.common.Muxer;
|
||||
import org.jcodec.common.MuxerTrack;
|
||||
import org.jcodec.common.VideoCodecMeta;
|
||||
import org.jcodec.common.VideoEncoder;
|
||||
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.Picture;
|
||||
import org.jcodec.common.model.Size;
|
||||
import org.jcodec.containers.imgseq.ImageSequenceMuxer;
|
||||
import org.jcodec.containers.mkv.muxer.MKVMuxer;
|
||||
import org.jcodec.containers.mp4.muxer.MP4Muxer;
|
||||
import org.jcodec.containers.raw.RawMuxer;
|
||||
|
||||
public class SinkImpl implements Sink, PacketSink {
|
||||
private String destName;
|
||||
|
||||
private SeekableByteChannel destStream;
|
||||
|
||||
private Muxer muxer;
|
||||
|
||||
private MuxerTrack videoOutputTrack;
|
||||
|
||||
private MuxerTrack audioOutputTrack;
|
||||
|
||||
private boolean framesOutput;
|
||||
|
||||
private Codec outputVideoCodec;
|
||||
|
||||
private Codec outputAudioCodec;
|
||||
|
||||
private Format outputFormat;
|
||||
|
||||
private final ThreadLocal<ByteBuffer> bufferStore;
|
||||
|
||||
private AudioEncoder audioEncoder;
|
||||
|
||||
private VideoEncoder videoEncoder;
|
||||
|
||||
private String profile;
|
||||
|
||||
private boolean interlaced;
|
||||
|
||||
public void outputVideoPacket(Packet packet, VideoCodecMeta codecMeta) throws IOException {
|
||||
if (!this.outputFormat.isVideo())
|
||||
return;
|
||||
if (this.videoOutputTrack == null)
|
||||
this.videoOutputTrack = this.muxer.addVideoTrack(this.outputVideoCodec, codecMeta);
|
||||
this.videoOutputTrack.addFrame(packet);
|
||||
this.framesOutput = true;
|
||||
}
|
||||
|
||||
public void outputAudioPacket(Packet audioPkt, AudioCodecMeta audioCodecMeta) throws IOException {
|
||||
if (!this.outputFormat.isAudio())
|
||||
return;
|
||||
if (this.audioOutputTrack == null)
|
||||
this.audioOutputTrack = this.muxer.addAudioTrack(this.outputAudioCodec, audioCodecMeta);
|
||||
this.audioOutputTrack.addFrame(audioPkt);
|
||||
this.framesOutput = true;
|
||||
}
|
||||
|
||||
public void initMuxer() throws IOException {
|
||||
if (this.destStream == null && this.outputFormat != Format.IMG)
|
||||
this.destStream = NIOUtils.writableFileChannel(this.destName);
|
||||
if (Format.MKV == this.outputFormat) {
|
||||
this.muxer = new MKVMuxer(this.destStream);
|
||||
} else if (Format.MOV == this.outputFormat) {
|
||||
this.muxer = MP4Muxer.createMP4MuxerToChannel(this.destStream);
|
||||
} else if (Format.IVF == this.outputFormat) {
|
||||
this.muxer = new IVFMuxer(this.destStream);
|
||||
} else if (Format.IMG == this.outputFormat) {
|
||||
this.muxer = new ImageSequenceMuxer(this.destName);
|
||||
} else if (Format.WAV == this.outputFormat) {
|
||||
this.muxer = new WavMuxer(this.destStream);
|
||||
} else if (Format.Y4M == this.outputFormat) {
|
||||
this.muxer = new Y4MMuxer(this.destStream);
|
||||
} else if (Format.RAW == this.outputFormat) {
|
||||
this.muxer = new RawMuxer(this.destStream);
|
||||
} else {
|
||||
throw new RuntimeException("The output format " + String.valueOf(this.outputFormat) + " is not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
public void finish() throws IOException {
|
||||
if (this.framesOutput) {
|
||||
this.muxer.finish();
|
||||
} else {
|
||||
Logger.warn("No frames output.");
|
||||
}
|
||||
if (this.destStream != null)
|
||||
IOUtils.closeQuietly(this.destStream);
|
||||
}
|
||||
|
||||
public SinkImpl(String destName, Format outputFormat, Codec outputVideoCodec, Codec outputAudioCodec) {
|
||||
if (destName == null && outputFormat == Format.IMG)
|
||||
throw new IllegalArgumentException("A destination file should be specified for the image muxer.");
|
||||
this.destName = destName;
|
||||
this.outputFormat = outputFormat;
|
||||
this.outputVideoCodec = outputVideoCodec;
|
||||
this.outputAudioCodec = outputAudioCodec;
|
||||
this.outputFormat = outputFormat;
|
||||
this.bufferStore = new ThreadLocal<>();
|
||||
}
|
||||
|
||||
public static SinkImpl createWithStream(SeekableByteChannel destStream, Format outputFormat, Codec outputVideoCodec, Codec outputAudioCodec) {
|
||||
SinkImpl result = new SinkImpl(null, outputFormat, outputVideoCodec, outputAudioCodec);
|
||||
result.destStream = destStream;
|
||||
return result;
|
||||
}
|
||||
|
||||
public void init() throws IOException {
|
||||
initMuxer();
|
||||
if (this.outputFormat.isVideo() && this.outputVideoCodec != null)
|
||||
if (Codec.PRORES == this.outputVideoCodec) {
|
||||
this.videoEncoder = ProresEncoder.createProresEncoder(this.profile, this.interlaced);
|
||||
} else if (Codec.H264 == this.outputVideoCodec) {
|
||||
this.videoEncoder = H264Encoder.createH264Encoder();
|
||||
} else if (Codec.VP8 == this.outputVideoCodec) {
|
||||
this.videoEncoder = VP8Encoder.createVP8Encoder(10);
|
||||
} else if (Codec.PNG == this.outputVideoCodec) {
|
||||
this.videoEncoder = new PNGEncoder();
|
||||
} else if (Codec.RAW == this.outputVideoCodec) {
|
||||
this.videoEncoder = new RAWVideoEncoder();
|
||||
} else {
|
||||
throw new RuntimeException("Could not find encoder for the codec: " + String.valueOf(this.outputVideoCodec));
|
||||
}
|
||||
}
|
||||
|
||||
protected VideoEncoder.EncodedFrame encodeVideo(Picture frame, ByteBuffer _out) {
|
||||
if (!this.outputFormat.isVideo())
|
||||
return null;
|
||||
return this.videoEncoder.encodeFrame(frame, _out);
|
||||
}
|
||||
|
||||
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 {
|
||||
public ByteBuffer encode(ByteBuffer audioPkt, ByteBuffer buf) {
|
||||
return audioPkt;
|
||||
}
|
||||
}
|
||||
|
||||
protected ByteBuffer encodeAudio(AudioBuffer audioBuffer) {
|
||||
if (this.audioEncoder == null) {
|
||||
AudioFormat format = audioBuffer.getFormat();
|
||||
this.audioEncoder = createAudioEncoder(this.outputAudioCodec, format);
|
||||
}
|
||||
return this.audioEncoder.encode(audioBuffer.getData(), null);
|
||||
}
|
||||
|
||||
public void setProfile(String profile) {
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
public void setInterlaced(Boolean interlaced) {
|
||||
this.interlaced = interlaced;
|
||||
}
|
||||
|
||||
public void outputVideoFrame(VideoFrameWithPacket videoFrame) throws IOException {
|
||||
if (!this.outputFormat.isVideo() || this.outputVideoCodec == null)
|
||||
return;
|
||||
ByteBuffer buffer = this.bufferStore.get();
|
||||
int bufferSize = this.videoEncoder.estimateBufferSize(videoFrame.getFrame().getPicture());
|
||||
if (buffer == null || bufferSize < buffer.capacity()) {
|
||||
buffer = ByteBuffer.allocate(bufferSize);
|
||||
this.bufferStore.set(buffer);
|
||||
}
|
||||
buffer.clear();
|
||||
Picture frame = videoFrame.getFrame().getPicture();
|
||||
VideoEncoder.EncodedFrame enc = encodeVideo(frame, buffer);
|
||||
Packet outputVideoPacket = Packet.createPacketWithData(videoFrame.getPacket(), NIOUtils.clone(enc.getData()));
|
||||
outputVideoPacket.setFrameType(enc.isKeyFrame() ? Packet.FrameType.KEY : Packet.FrameType.INTER);
|
||||
outputVideoPacket(outputVideoPacket,
|
||||
VideoCodecMeta.createSimpleVideoCodecMeta(new Size(frame.getWidth(), frame.getHeight()), frame.getColor()));
|
||||
}
|
||||
|
||||
public void outputAudioFrame(AudioFrameWithPacket audioFrame) throws IOException {
|
||||
if (!this.outputFormat.isAudio() || this.outputAudioCodec == null)
|
||||
return;
|
||||
outputAudioPacket(Packet.createPacketWithData(audioFrame.getPacket(), encodeAudio(audioFrame.getAudio())),
|
||||
AudioCodecMeta.fromAudioFormat(audioFrame.getAudio().getFormat()));
|
||||
}
|
||||
|
||||
public ColorSpace getInputColor() {
|
||||
if (this.videoEncoder == null)
|
||||
throw new IllegalStateException("Video encoder has not been initialized, init() must be called before using this class.");
|
||||
ColorSpace[] colorSpaces = this.videoEncoder.getSupportedColorSpaces();
|
||||
return (colorSpaces == null) ? null : colorSpaces[0];
|
||||
}
|
||||
|
||||
public void setOption(Options option, Object value) {
|
||||
if (option == Options.PROFILE) {
|
||||
this.profile = (String)value;
|
||||
} else if (option == Options.INTERLACED) {
|
||||
this.interlaced = (Boolean)value;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
return this.outputFormat.isVideo();
|
||||
}
|
||||
|
||||
public boolean isAudio() {
|
||||
return this.outputFormat.isAudio();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.jcodec.common.AudioCodecMeta;
|
||||
import org.jcodec.common.VideoCodecMeta;
|
||||
|
||||
public interface Source {
|
||||
void init(PixelStore paramPixelStore) throws IOException;
|
||||
|
||||
void seekFrames(int paramInt) throws IOException;
|
||||
|
||||
VideoFrameWithPacket getNextVideoFrame() throws IOException;
|
||||
|
||||
AudioFrameWithPacket getNextAudioFrame() throws IOException;
|
||||
|
||||
void finish();
|
||||
|
||||
boolean haveAudio();
|
||||
|
||||
void setOption(Options paramOptions, Object paramObject);
|
||||
|
||||
VideoCodecMeta getVideoCodecMeta();
|
||||
|
||||
AudioCodecMeta getAudioCodecMeta();
|
||||
|
||||
boolean isVideo();
|
||||
|
||||
boolean isAudio();
|
||||
}
|
||||
|
|
@ -0,0 +1,463 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import net.sourceforge.jaad.aac.AACException;
|
||||
import org.jcodec.codecs.aac.AACDecoder;
|
||||
import org.jcodec.codecs.h264.BufferH264ES;
|
||||
import org.jcodec.codecs.h264.H264Decoder;
|
||||
import org.jcodec.codecs.h264.H264Utils;
|
||||
import org.jcodec.codecs.mjpeg.JpegDecoder;
|
||||
import org.jcodec.codecs.mjpeg.JpegToThumb2x2;
|
||||
import org.jcodec.codecs.mjpeg.JpegToThumb4x4;
|
||||
import org.jcodec.codecs.mpeg12.MPEGDecoder;
|
||||
import org.jcodec.codecs.mpeg12.Mpeg2Thumb2x2;
|
||||
import org.jcodec.codecs.mpeg12.Mpeg2Thumb4x4;
|
||||
import org.jcodec.codecs.mpeg4.MPEG4Decoder;
|
||||
import org.jcodec.codecs.png.PNGDecoder;
|
||||
import org.jcodec.codecs.prores.ProresDecoder;
|
||||
import org.jcodec.codecs.prores.ProresToThumb;
|
||||
import org.jcodec.codecs.prores.ProresToThumb2x2;
|
||||
import org.jcodec.codecs.prores.ProresToThumb4x4;
|
||||
import org.jcodec.codecs.raw.RAWVideoDecoder;
|
||||
import org.jcodec.codecs.vpx.VP8Decoder;
|
||||
import org.jcodec.codecs.wav.WavDemuxer;
|
||||
import org.jcodec.common.AudioCodecMeta;
|
||||
import org.jcodec.common.AudioDecoder;
|
||||
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.SeekableDemuxerTrack;
|
||||
import org.jcodec.common.Tuple;
|
||||
import org.jcodec.common.VideoCodecMeta;
|
||||
import org.jcodec.common.VideoDecoder;
|
||||
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.Packet;
|
||||
import org.jcodec.common.model.Picture;
|
||||
import org.jcodec.common.model.Size;
|
||||
import org.jcodec.containers.imgseq.ImageSequenceDemuxer;
|
||||
import org.jcodec.containers.mkv.demuxer.MKVDemuxer;
|
||||
import org.jcodec.containers.mp3.MPEGAudioDemuxer;
|
||||
import org.jcodec.containers.mp4.demuxer.MP4Demuxer;
|
||||
import org.jcodec.containers.mps.MPEGDemuxer;
|
||||
import org.jcodec.containers.mps.MPSDemuxer;
|
||||
import org.jcodec.containers.mps.MTSDemuxer;
|
||||
import org.jcodec.containers.webp.WebpDemuxer;
|
||||
import org.jcodec.containers.y4m.Y4MDemuxer;
|
||||
|
||||
public class SourceImpl implements Source, PacketSource {
|
||||
private String sourceName;
|
||||
|
||||
private SeekableByteChannel sourceStream;
|
||||
|
||||
private Demuxer demuxVideo;
|
||||
|
||||
private Demuxer demuxAudio;
|
||||
|
||||
private Format inputFormat;
|
||||
|
||||
private DemuxerTrack videoInputTrack;
|
||||
|
||||
private DemuxerTrack audioInputTrack;
|
||||
|
||||
private Tuple._3<Integer, Integer, Codec> inputVideoCodec;
|
||||
|
||||
private Tuple._3<Integer, Integer, Codec> inputAudioCodec;
|
||||
|
||||
private List<VideoFrameWithPacket> frameReorderBuffer;
|
||||
|
||||
private List<Packet> videoPacketReorderBuffer;
|
||||
|
||||
private PixelStore pixelStore;
|
||||
|
||||
private VideoCodecMeta videoCodecMeta;
|
||||
|
||||
private AudioCodecMeta audioCodecMeta;
|
||||
|
||||
private AudioDecoder audioDecoder;
|
||||
|
||||
private VideoDecoder videoDecoder;
|
||||
|
||||
private int downscale = 1;
|
||||
|
||||
public static MPEGDecoder createMpegDecoder(int downscale) {
|
||||
if (downscale == 2)
|
||||
return new Mpeg2Thumb4x4();
|
||||
if (downscale == 4)
|
||||
return new Mpeg2Thumb2x2();
|
||||
return new MPEGDecoder();
|
||||
}
|
||||
|
||||
public static ProresDecoder createProresDecoder(int downscale) {
|
||||
if (2 == downscale)
|
||||
return new ProresToThumb4x4();
|
||||
if (4 == downscale)
|
||||
return new ProresToThumb2x2();
|
||||
if (8 == downscale)
|
||||
return new ProresToThumb();
|
||||
return new ProresDecoder();
|
||||
}
|
||||
|
||||
public void initDemuxer() throws FileNotFoundException, IOException {
|
||||
if (this.inputFormat != Format.IMG)
|
||||
this.sourceStream = NIOUtils.readableFileChannel(this.sourceName);
|
||||
if (Format.MOV == this.inputFormat) {
|
||||
this.demuxVideo = this.demuxAudio = MP4Demuxer.createMP4Demuxer(this.sourceStream);
|
||||
} else if (Format.MKV == this.inputFormat) {
|
||||
this.demuxVideo = this.demuxAudio = new MKVDemuxer(this.sourceStream);
|
||||
} else if (Format.IMG == this.inputFormat) {
|
||||
this.demuxVideo = new ImageSequenceDemuxer(this.sourceName, Integer.MAX_VALUE);
|
||||
} else if (Format.WEBP == this.inputFormat) {
|
||||
this.demuxVideo = new WebpDemuxer(this.sourceStream);
|
||||
} else if (Format.MPEG_PS == this.inputFormat) {
|
||||
this.demuxVideo = this.demuxAudio = new MPSDemuxer(this.sourceStream);
|
||||
} else if (Format.Y4M == this.inputFormat) {
|
||||
Y4MDemuxer y4mDemuxer = new Y4MDemuxer(this.sourceStream);
|
||||
this.demuxVideo = this.demuxAudio = y4mDemuxer;
|
||||
this.videoInputTrack = y4mDemuxer;
|
||||
} else if (Format.H264 == this.inputFormat) {
|
||||
this.demuxVideo = new BufferH264ES(NIOUtils.fetchAllFromChannel(this.sourceStream));
|
||||
} else if (Format.WAV == this.inputFormat) {
|
||||
this.demuxAudio = new WavDemuxer(this.sourceStream);
|
||||
} else if (Format.MPEG_AUDIO == this.inputFormat) {
|
||||
this.demuxAudio = new MPEGAudioDemuxer(this.sourceStream);
|
||||
} else if (Format.MPEG_TS == this.inputFormat) {
|
||||
MTSDemuxer mtsDemuxer = new MTSDemuxer(this.sourceStream);
|
||||
MPSDemuxer mpsDemuxer = null;
|
||||
if (this.inputVideoCodec != null) {
|
||||
mpsDemuxer = new MPSDemuxer(mtsDemuxer.getProgram(((Integer)this.inputVideoCodec.v0).intValue()));
|
||||
this.videoInputTrack = openTSTrack(mpsDemuxer, (Integer)this.inputVideoCodec.v1);
|
||||
this.demuxVideo = mpsDemuxer;
|
||||
}
|
||||
if (this.inputAudioCodec != null) {
|
||||
if (this.inputVideoCodec == null || this.inputVideoCodec.v0 != this.inputAudioCodec.v0)
|
||||
mpsDemuxer = new MPSDemuxer(mtsDemuxer.getProgram(((Integer)this.inputAudioCodec.v0).intValue()));
|
||||
this.audioInputTrack = openTSTrack(mpsDemuxer, (Integer)this.inputAudioCodec.v1);
|
||||
this.demuxAudio = mpsDemuxer;
|
||||
}
|
||||
for (Iterator<Integer> iterator = mtsDemuxer.getPrograms().iterator(); iterator.hasNext(); ) {
|
||||
int pid = iterator.next();
|
||||
if ((this.inputVideoCodec == null || pid != (Integer)this.inputVideoCodec.v0) && (this.inputAudioCodec == null || pid != (Integer)this.inputAudioCodec.v0)) {
|
||||
Logger.info("Unused program: " + pid);
|
||||
mtsDemuxer.getProgram(pid).close();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Input format: " + String.valueOf(this.inputFormat) + " is not supported.");
|
||||
}
|
||||
if (this.demuxVideo != null && this.inputVideoCodec != null) {
|
||||
List<? extends DemuxerTrack> videoTracks = this.demuxVideo.getVideoTracks();
|
||||
if (videoTracks.size() > 0)
|
||||
this.videoInputTrack = videoTracks.get(((Integer)this.inputVideoCodec.v1).intValue());
|
||||
}
|
||||
if (this.demuxAudio != null && this.inputAudioCodec != null) {
|
||||
List<? extends DemuxerTrack> audioTracks = this.demuxAudio.getAudioTracks();
|
||||
if (audioTracks.size() > 0)
|
||||
this.audioInputTrack = audioTracks.get(((Integer)this.inputAudioCodec.v1).intValue());
|
||||
}
|
||||
}
|
||||
|
||||
protected int seekToKeyFrame(int frame) throws IOException {
|
||||
if (this.videoInputTrack instanceof SeekableDemuxerTrack) {
|
||||
SeekableDemuxerTrack seekable = (SeekableDemuxerTrack)this.videoInputTrack;
|
||||
seekable.gotoSyncFrame((long)frame);
|
||||
return (int)seekable.getCurFrame();
|
||||
}
|
||||
Logger.warn("Can not seek in " + String.valueOf(this.videoInputTrack) + " container.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
private MPEGDemuxer.MPEGDemuxerTrack openTSTrack(MPSDemuxer demuxerVideo, Integer selectedTrack) {
|
||||
int trackNo = 0;
|
||||
for (MPEGDemuxer.MPEGDemuxerTrack track : demuxerVideo.getTracks()) {
|
||||
if (trackNo == selectedTrack)
|
||||
return track;
|
||||
track.ignore();
|
||||
trackNo++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Packet inputVideoPacket() throws IOException {
|
||||
Packet packet;
|
||||
do {
|
||||
packet = getNextVideoPacket();
|
||||
if (packet == null)
|
||||
continue;
|
||||
this.videoPacketReorderBuffer.add(packet);
|
||||
} while (packet != null && this.videoPacketReorderBuffer.size() <= 7);
|
||||
if (this.videoPacketReorderBuffer.size() == 0)
|
||||
return null;
|
||||
Packet out = (Packet)this.videoPacketReorderBuffer.remove(0);
|
||||
int duration = Integer.MAX_VALUE;
|
||||
for (Packet packet2 : this.videoPacketReorderBuffer) {
|
||||
int cand = (int)(packet2.getPts() - out.getPts());
|
||||
if (cand > 0 && cand < duration)
|
||||
duration = cand;
|
||||
}
|
||||
if (duration != Integer.MAX_VALUE)
|
||||
out.setDuration((long)duration);
|
||||
return out;
|
||||
}
|
||||
|
||||
private Packet getNextVideoPacket() throws IOException {
|
||||
if (this.videoInputTrack == null)
|
||||
return null;
|
||||
Packet nextFrame = this.videoInputTrack.nextFrame();
|
||||
if (this.videoDecoder == null) {
|
||||
this.videoDecoder = createVideoDecoder((Codec)this.inputVideoCodec.v2, this.downscale, nextFrame.getData(), null);
|
||||
if (this.videoDecoder != null)
|
||||
this.videoCodecMeta = this.videoDecoder.getCodecMeta(nextFrame.getData());
|
||||
}
|
||||
return nextFrame;
|
||||
}
|
||||
|
||||
public Packet inputAudioPacket() throws IOException {
|
||||
if (this.audioInputTrack == null)
|
||||
return null;
|
||||
Packet audioPkt = this.audioInputTrack.nextFrame();
|
||||
if (this.audioDecoder == null && audioPkt != null) {
|
||||
this.audioDecoder = createAudioDecoder(audioPkt.getData());
|
||||
if (this.audioDecoder != null)
|
||||
this.audioCodecMeta = this.audioDecoder.getCodecMeta(audioPkt.getData());
|
||||
}
|
||||
return audioPkt;
|
||||
}
|
||||
|
||||
public DemuxerTrackMeta getTrackVideoMeta() {
|
||||
if (this.videoInputTrack == null)
|
||||
return null;
|
||||
return this.videoInputTrack.getMeta();
|
||||
}
|
||||
|
||||
public DemuxerTrackMeta getAudioMeta() {
|
||||
if (this.audioInputTrack == null)
|
||||
return null;
|
||||
return this.audioInputTrack.getMeta();
|
||||
}
|
||||
|
||||
public boolean haveAudio() {
|
||||
return (this.audioInputTrack != null);
|
||||
}
|
||||
|
||||
public void finish() {
|
||||
if (this.sourceStream != null)
|
||||
IOUtils.closeQuietly(this.sourceStream);
|
||||
}
|
||||
|
||||
public SourceImpl(String sourceName, Format inputFormat, Tuple._3<Integer, Integer, Codec> inputVideoCodec, Tuple._3<Integer, Integer, Codec> inputAudioCodec) {
|
||||
this.sourceName = sourceName;
|
||||
this.inputFormat = inputFormat;
|
||||
this.inputVideoCodec = inputVideoCodec;
|
||||
this.inputAudioCodec = inputAudioCodec;
|
||||
this.frameReorderBuffer = new ArrayList<>();
|
||||
this.videoPacketReorderBuffer = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void init(PixelStore pixelStore) throws IOException {
|
||||
this.pixelStore = pixelStore;
|
||||
initDemuxer();
|
||||
}
|
||||
|
||||
private AudioDecoder createAudioDecoder(ByteBuffer codecPrivate) throws AACException {
|
||||
if (Codec.AAC == this.inputAudioCodec.v2)
|
||||
return new AACDecoder(codecPrivate);
|
||||
if (Codec.PCM == this.inputAudioCodec.v2)
|
||||
return new RawAudioDecoder(getAudioMeta().getAudioCodecMeta().getFormat());
|
||||
return null;
|
||||
}
|
||||
|
||||
private VideoDecoder createVideoDecoder(Codec codec, int downscale, ByteBuffer codecPrivate, VideoCodecMeta videoCodecMeta) {
|
||||
if (Codec.H264 == codec)
|
||||
return H264Decoder.createH264DecoderFromCodecPrivate(codecPrivate);
|
||||
if (Codec.PNG == codec)
|
||||
return new PNGDecoder();
|
||||
if (Codec.MPEG2 == codec)
|
||||
return createMpegDecoder(downscale);
|
||||
if (Codec.PRORES == codec)
|
||||
return createProresDecoder(downscale);
|
||||
if (Codec.VP8 == codec)
|
||||
return new VP8Decoder();
|
||||
if (Codec.JPEG == codec)
|
||||
return createJpegDecoder(downscale);
|
||||
if (Codec.MPEG4 == codec)
|
||||
return new MPEG4Decoder();
|
||||
if (Codec.RAW == codec) {
|
||||
Size dim = videoCodecMeta.getSize();
|
||||
return new RAWVideoDecoder(dim.getWidth(), dim.getHeight());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Picture decodeVideo(ByteBuffer data, Picture target1) {
|
||||
return this.videoDecoder.decodeFrame(data, target1.getData());
|
||||
}
|
||||
|
||||
protected ByteBuffer decodeAudio(ByteBuffer audioPkt) throws IOException {
|
||||
if (this.inputAudioCodec.v2 == Codec.PCM)
|
||||
return audioPkt;
|
||||
AudioBuffer decodeFrame = this.audioDecoder.decodeFrame(audioPkt, null);
|
||||
return decodeFrame.getData();
|
||||
}
|
||||
|
||||
private static class RawAudioDecoder implements AudioDecoder {
|
||||
private AudioFormat format;
|
||||
|
||||
public RawAudioDecoder(AudioFormat format) {
|
||||
this.format = format;
|
||||
}
|
||||
|
||||
public AudioBuffer decodeFrame(ByteBuffer frame, ByteBuffer dst) throws IOException {
|
||||
return new AudioBuffer(frame, this.format, frame.remaining() / this.format.getFrameSize());
|
||||
}
|
||||
|
||||
public AudioCodecMeta getCodecMeta(ByteBuffer data) throws IOException {
|
||||
return AudioCodecMeta.fromAudioFormat(this.format);
|
||||
}
|
||||
}
|
||||
|
||||
public void seekFrames(int seekFrames) throws IOException {
|
||||
if (seekFrames == 0)
|
||||
return;
|
||||
int skipFrames = seekFrames - seekToKeyFrame(seekFrames);
|
||||
Packet inVideoPacket;
|
||||
while (skipFrames > 0 && (inVideoPacket = getNextVideoPacket()) != null) {
|
||||
PixelStore.LoanerPicture loanerBuffer = getPixelBuffer(inVideoPacket.getData());
|
||||
Picture decodedFrame = decodeVideo(inVideoPacket.getData(), loanerBuffer.getPicture());
|
||||
if (decodedFrame == null) {
|
||||
this.pixelStore.putBack(loanerBuffer);
|
||||
continue;
|
||||
}
|
||||
this.frameReorderBuffer.add(new VideoFrameWithPacket(inVideoPacket, new PixelStore.LoanerPicture(decodedFrame, 1)));
|
||||
if (this.frameReorderBuffer.size() > 7) {
|
||||
Collections.sort(this.frameReorderBuffer);
|
||||
VideoFrameWithPacket removed = (VideoFrameWithPacket)this.frameReorderBuffer.remove(0);
|
||||
skipFrames--;
|
||||
if (removed.getFrame() != null)
|
||||
this.pixelStore.putBack(removed.getFrame());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void detectFrameType(Packet inVideoPacket) {
|
||||
if (this.inputVideoCodec.v2 != Codec.H264)
|
||||
return;
|
||||
inVideoPacket.setFrameType(
|
||||
H264Utils.isByteBufferIDRSlice(inVideoPacket.getData()) ? Packet.FrameType.KEY : Packet.FrameType.INTER);
|
||||
}
|
||||
|
||||
protected PixelStore.LoanerPicture getPixelBuffer(ByteBuffer firstFrame) {
|
||||
VideoCodecMeta videoMeta = getVideoCodecMeta();
|
||||
Size size = videoMeta.getSize();
|
||||
return this.pixelStore.getPicture(size.getWidth() + 15 & 0xFFFFFFF0, size.getHeight() + 15 & 0xFFFFFFF0,
|
||||
videoMeta.getColor());
|
||||
}
|
||||
|
||||
public VideoCodecMeta getVideoCodecMeta() {
|
||||
if (this.videoCodecMeta != null)
|
||||
return this.videoCodecMeta;
|
||||
DemuxerTrackMeta meta = getTrackVideoMeta();
|
||||
if (meta != null && meta.getVideoCodecMeta() != null)
|
||||
this.videoCodecMeta = meta.getVideoCodecMeta();
|
||||
return this.videoCodecMeta;
|
||||
}
|
||||
|
||||
public VideoFrameWithPacket getNextVideoFrame() throws IOException {
|
||||
Packet inVideoPacket;
|
||||
while ((inVideoPacket = getNextVideoPacket()) != null) {
|
||||
if (inVideoPacket.getFrameType() == Packet.FrameType.UNKNOWN)
|
||||
detectFrameType(inVideoPacket);
|
||||
Picture decodedFrame = null;
|
||||
PixelStore.LoanerPicture pixelBuffer = getPixelBuffer(inVideoPacket.getData());
|
||||
decodedFrame = decodeVideo(inVideoPacket.getData(), pixelBuffer.getPicture());
|
||||
if (decodedFrame == null) {
|
||||
this.pixelStore.putBack(pixelBuffer);
|
||||
continue;
|
||||
}
|
||||
this.frameReorderBuffer.add(new VideoFrameWithPacket(inVideoPacket, new PixelStore.LoanerPicture(decodedFrame, 1)));
|
||||
if (this.frameReorderBuffer.size() > 7)
|
||||
return removeFirstFixDuration(this.frameReorderBuffer);
|
||||
}
|
||||
if (this.frameReorderBuffer.size() > 0)
|
||||
return removeFirstFixDuration(this.frameReorderBuffer);
|
||||
return null;
|
||||
}
|
||||
|
||||
private VideoFrameWithPacket removeFirstFixDuration(List<VideoFrameWithPacket> reorderBuffer) {
|
||||
Collections.sort(reorderBuffer);
|
||||
VideoFrameWithPacket frame = (VideoFrameWithPacket)reorderBuffer.remove(0);
|
||||
if (!reorderBuffer.isEmpty()) {
|
||||
VideoFrameWithPacket nextFrame = reorderBuffer.get(0);
|
||||
frame.getPacket().setDuration(nextFrame.getPacket().getPts() - frame.getPacket().getPts());
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
public AudioFrameWithPacket getNextAudioFrame() throws IOException {
|
||||
AudioBuffer audioBuffer;
|
||||
Packet audioPkt = inputAudioPacket();
|
||||
if (audioPkt == null)
|
||||
return null;
|
||||
if (this.inputAudioCodec.v2 == Codec.PCM) {
|
||||
DemuxerTrackMeta audioMeta = getAudioMeta();
|
||||
audioBuffer = new AudioBuffer(audioPkt.getData(), audioMeta.getAudioCodecMeta().getFormat(), audioMeta.getTotalFrames());
|
||||
} else {
|
||||
audioBuffer = this.audioDecoder.decodeFrame(audioPkt.getData(), null);
|
||||
}
|
||||
return new AudioFrameWithPacket(audioBuffer, audioPkt);
|
||||
}
|
||||
|
||||
public Tuple._3<Integer, Integer, Codec> getIntputVideoCodec() {
|
||||
return this.inputVideoCodec;
|
||||
}
|
||||
|
||||
public Tuple._3<Integer, Integer, Codec> getInputAudioCode() {
|
||||
return this.inputAudioCodec;
|
||||
}
|
||||
|
||||
public void setOption(Options option, Object value) {
|
||||
if (option == Options.DOWNSCALE)
|
||||
this.downscale = (Integer)value;
|
||||
}
|
||||
|
||||
public AudioCodecMeta getAudioCodecMeta() {
|
||||
if (this.audioInputTrack != null && this.audioInputTrack.getMeta() != null &&
|
||||
this.audioInputTrack.getMeta().getAudioCodecMeta() != null)
|
||||
return this.audioInputTrack.getMeta().getAudioCodecMeta();
|
||||
return this.audioCodecMeta;
|
||||
}
|
||||
|
||||
public boolean isVideo() {
|
||||
if (!this.inputFormat.isVideo())
|
||||
return false;
|
||||
List<? extends DemuxerTrack> tracks = this.demuxVideo.getVideoTracks();
|
||||
return (tracks != null && tracks.size() > 0);
|
||||
}
|
||||
|
||||
public boolean isAudio() {
|
||||
if (!this.inputFormat.isAudio())
|
||||
return false;
|
||||
List<? extends DemuxerTrack> tracks = this.demuxAudio.getAudioTracks();
|
||||
return (tracks != null && tracks.size() > 0);
|
||||
}
|
||||
|
||||
public static JpegDecoder createJpegDecoder(int downscale) {
|
||||
if (downscale == 2)
|
||||
return new JpegToThumb4x4();
|
||||
if (downscale == 4)
|
||||
return new JpegToThumb2x2();
|
||||
return new JpegDecoder();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,396 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.jcodec.api.transcode.filters.DumpMvFilter;
|
||||
import org.jcodec.api.transcode.filters.ScaleFilter;
|
||||
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.JCodecUtil;
|
||||
import org.jcodec.common.TrackType;
|
||||
import org.jcodec.common.Tuple;
|
||||
import org.jcodec.common.logging.LogLevel;
|
||||
import org.jcodec.common.logging.Logger;
|
||||
import org.jcodec.common.logging.OutLogSink;
|
||||
import org.jcodec.common.model.Packet;
|
||||
import org.jcodec.common.tools.MainUtils;
|
||||
import org.jcodec.common.tools.MathUtil;
|
||||
import org.jcodec.platform.Platform;
|
||||
|
||||
public class TranscodeMain {
|
||||
private static final MainUtils.Flag FLAG_INPUT = new MainUtils.Flag("input", "i", "Designates an input argument", MainUtils.FlagType.VOID);
|
||||
|
||||
private static final MainUtils.Flag FLAG_MAP_VIDEO = MainUtils.Flag.flag("map:v", "mv", "Map a video from a specified input into this output");
|
||||
|
||||
private static final MainUtils.Flag FLAG_MAP_AUDIO = MainUtils.Flag.flag("map:a", "ma", "Map a audio from a specified input into this output");
|
||||
|
||||
private static final MainUtils.Flag FLAG_SEEK_FRAMES = MainUtils.Flag.flag("seek-frames", null, "Seek frames");
|
||||
|
||||
private static final MainUtils.Flag FLAG_MAX_FRAMES = MainUtils.Flag.flag("max-frames", "limit", "Max frames");
|
||||
|
||||
private static final MainUtils.Flag FLAG_AUDIO_CODEC = MainUtils.Flag.flag("codec:audio", "acodec", "Audio codec [default=auto].");
|
||||
|
||||
private static final MainUtils.Flag FLAG_VIDEO_CODEC = MainUtils.Flag.flag("codec:video", "vcodec", "Video codec [default=auto].");
|
||||
|
||||
private static final MainUtils.Flag FLAG_FORMAT = MainUtils.Flag.flag("format", "f", "Format [default=auto].");
|
||||
|
||||
private static final MainUtils.Flag FLAG_PROFILE = MainUtils.Flag.flag("profile", null, "Profile to use (supported by some encoders).");
|
||||
|
||||
private static final MainUtils.Flag FLAG_INTERLACED = MainUtils.Flag.flag("interlaced", null, "Encode output as interlaced (supported by Prores encoder).");
|
||||
|
||||
private static final MainUtils.Flag FLAG_DUMPMV = MainUtils.Flag.flag("dumpMv", null, "Dump motion vectors (supported by h.264 decoder).");
|
||||
|
||||
private static final MainUtils.Flag FLAG_DUMPMVJS = MainUtils.Flag.flag("dumpMvJs", null, "Dump motion vectors in form of JASON file (supported by h.264 decoder).");
|
||||
|
||||
private static final MainUtils.Flag FLAG_DOWNSCALE = MainUtils.Flag.flag("downscale", null, "Decode frames in downscale (supported by MPEG, Prores and Jpeg decoders).");
|
||||
|
||||
private static final MainUtils.Flag FLAG_VIDEO_FILTER = MainUtils.Flag.flag("videoFilter", "vf", "Contains a comma separated list of video filters with arguments.");
|
||||
|
||||
private static final MainUtils.Flag[] ALL_FLAGS = new MainUtils.Flag[] {
|
||||
FLAG_INPUT, FLAG_FORMAT, FLAG_VIDEO_CODEC, FLAG_AUDIO_CODEC, FLAG_SEEK_FRAMES, FLAG_MAX_FRAMES, FLAG_PROFILE, FLAG_INTERLACED, FLAG_DUMPMV, FLAG_DUMPMVJS,
|
||||
FLAG_DOWNSCALE, FLAG_MAP_VIDEO, FLAG_MAP_AUDIO, FLAG_VIDEO_FILTER };
|
||||
|
||||
private static Map<String, Format> extensionToF = new HashMap<>();
|
||||
|
||||
private static Map<String, Codec> extensionToC = new HashMap<>();
|
||||
|
||||
private static Map<Format, Codec> videoCodecsForF = new HashMap<>();
|
||||
|
||||
private static Map<Format, Codec> audioCodecsForF = new HashMap<>();
|
||||
|
||||
private static Set<Codec> supportedDecoders = new HashSet<>();
|
||||
|
||||
private static Map<String, Class<? extends Filter>> knownFilters = new HashMap<>();
|
||||
|
||||
static {
|
||||
extensionToF.put("mp3", Format.MPEG_AUDIO);
|
||||
extensionToF.put("mp2", Format.MPEG_AUDIO);
|
||||
extensionToF.put("mp1", Format.MPEG_AUDIO);
|
||||
extensionToF.put("mpg", Format.MPEG_PS);
|
||||
extensionToF.put("mpeg", Format.MPEG_PS);
|
||||
extensionToF.put("m2p", Format.MPEG_PS);
|
||||
extensionToF.put("ps", Format.MPEG_PS);
|
||||
extensionToF.put("vob", Format.MPEG_PS);
|
||||
extensionToF.put("evo", Format.MPEG_PS);
|
||||
extensionToF.put("mod", Format.MPEG_PS);
|
||||
extensionToF.put("tod", Format.MPEG_PS);
|
||||
extensionToF.put("ts", Format.MPEG_TS);
|
||||
extensionToF.put("m2t", Format.MPEG_TS);
|
||||
extensionToF.put("mp4", Format.MOV);
|
||||
extensionToF.put("m4a", Format.MOV);
|
||||
extensionToF.put("m4v", Format.MOV);
|
||||
extensionToF.put("mov", Format.MOV);
|
||||
extensionToF.put("3gp", Format.MOV);
|
||||
extensionToF.put("mkv", Format.MKV);
|
||||
extensionToF.put("webm", Format.MKV);
|
||||
extensionToF.put("264", Format.H264);
|
||||
extensionToF.put("jsv", Format.H264);
|
||||
extensionToF.put("h264", Format.H264);
|
||||
extensionToF.put("raw", Format.RAW);
|
||||
extensionToF.put("", Format.RAW);
|
||||
extensionToF.put("flv", Format.FLV);
|
||||
extensionToF.put("avi", Format.AVI);
|
||||
extensionToF.put("jpg", Format.IMG);
|
||||
extensionToF.put("jpeg", Format.IMG);
|
||||
extensionToF.put("png", Format.IMG);
|
||||
extensionToF.put("mjp", Format.MJPEG);
|
||||
extensionToF.put("ivf", Format.IVF);
|
||||
extensionToF.put("y4m", Format.Y4M);
|
||||
extensionToF.put("wav", Format.WAV);
|
||||
extensionToC.put("mpg", Codec.MPEG2);
|
||||
extensionToC.put("mpeg", Codec.MPEG2);
|
||||
extensionToC.put("m2p", Codec.MPEG2);
|
||||
extensionToC.put("ps", Codec.MPEG2);
|
||||
extensionToC.put("vob", Codec.MPEG2);
|
||||
extensionToC.put("evo", Codec.MPEG2);
|
||||
extensionToC.put("mod", Codec.MPEG2);
|
||||
extensionToC.put("tod", Codec.MPEG2);
|
||||
extensionToC.put("ts", Codec.MPEG2);
|
||||
extensionToC.put("m2t", Codec.MPEG2);
|
||||
extensionToC.put("m4a", Codec.AAC);
|
||||
extensionToC.put("mkv", Codec.H264);
|
||||
extensionToC.put("webm", Codec.VP8);
|
||||
extensionToC.put("264", Codec.H264);
|
||||
extensionToC.put("raw", Codec.RAW);
|
||||
extensionToC.put("jpg", Codec.JPEG);
|
||||
extensionToC.put("jpeg", Codec.JPEG);
|
||||
extensionToC.put("png", Codec.PNG);
|
||||
extensionToC.put("mjp", Codec.JPEG);
|
||||
extensionToC.put("y4m", Codec.RAW);
|
||||
videoCodecsForF.put(Format.MPEG_PS, Codec.MPEG2);
|
||||
audioCodecsForF.put(Format.MPEG_PS, Codec.MP2);
|
||||
videoCodecsForF.put(Format.MOV, Codec.H264);
|
||||
audioCodecsForF.put(Format.MOV, Codec.AAC);
|
||||
videoCodecsForF.put(Format.MKV, Codec.VP8);
|
||||
audioCodecsForF.put(Format.MKV, Codec.VORBIS);
|
||||
audioCodecsForF.put(Format.WAV, Codec.PCM);
|
||||
videoCodecsForF.put(Format.H264, Codec.H264);
|
||||
videoCodecsForF.put(Format.RAW, Codec.RAW);
|
||||
videoCodecsForF.put(Format.FLV, Codec.H264);
|
||||
videoCodecsForF.put(Format.AVI, Codec.MPEG4);
|
||||
videoCodecsForF.put(Format.IMG, Codec.PNG);
|
||||
videoCodecsForF.put(Format.MJPEG, Codec.JPEG);
|
||||
videoCodecsForF.put(Format.IVF, Codec.VP8);
|
||||
videoCodecsForF.put(Format.Y4M, Codec.RAW);
|
||||
supportedDecoders.add(Codec.AAC);
|
||||
supportedDecoders.add(Codec.H264);
|
||||
supportedDecoders.add(Codec.JPEG);
|
||||
supportedDecoders.add(Codec.MPEG2);
|
||||
supportedDecoders.add(Codec.PCM);
|
||||
supportedDecoders.add(Codec.PNG);
|
||||
supportedDecoders.add(Codec.MPEG4);
|
||||
supportedDecoders.add(Codec.PRORES);
|
||||
supportedDecoders.add(Codec.RAW);
|
||||
supportedDecoders.add(Codec.VP8);
|
||||
supportedDecoders.add(Codec.MP3);
|
||||
supportedDecoders.add(Codec.MP2);
|
||||
supportedDecoders.add(Codec.MP1);
|
||||
knownFilters.put("scale", ScaleFilter.class);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Logger.addSink(new OutLogSink(System.out, new OutLogSink.SimpleFormat("#message"), LogLevel.INFO));
|
||||
MainUtils.Cmd cmd = MainUtils.parseArguments(args, ALL_FLAGS);
|
||||
Transcoder.TranscoderBuilder builder = Transcoder.newTranscoder();
|
||||
List<Source> sources = new ArrayList<>();
|
||||
List<Tuple._3<Integer, Integer, Codec>> inputCodecsVideo = new ArrayList<>();
|
||||
List<Tuple._3<Integer, Integer, Codec>> inputCodecsAudio = new ArrayList<>();
|
||||
for (int index = 0; index < cmd.argsLength(); index++) {
|
||||
if (cmd.getBooleanFlagI(index, FLAG_INPUT)) {
|
||||
Format inputFormat;
|
||||
Tuple._3<Integer, Integer, Codec> inputCodecVideo = null, inputCodecAudio = null;
|
||||
String input = cmd.getArg(index);
|
||||
String inputFormatRaw = cmd.getStringFlagI(index, FLAG_FORMAT);
|
||||
if (inputFormatRaw == null) {
|
||||
inputFormat = getFormatFromExtension(input);
|
||||
if (inputFormat != Format.IMG) {
|
||||
Format detectFormat = JCodecUtil.detectFormat(new File(input));
|
||||
if (detectFormat != null)
|
||||
inputFormat = detectFormat;
|
||||
}
|
||||
} else {
|
||||
inputFormat = Format.valueOf(inputFormatRaw.toUpperCase());
|
||||
}
|
||||
if (inputFormat == null) {
|
||||
Logger.error("Input format could not be detected");
|
||||
return;
|
||||
}
|
||||
Logger.info(String.format("Input stream %d: %s", index, String.valueOf(inputFormat)));
|
||||
int videoTrackNo = -1;
|
||||
String inputCodecVideoRaw = cmd.getStringFlagI(index, FLAG_VIDEO_CODEC);
|
||||
if (inputCodecVideoRaw == null) {
|
||||
if (inputFormat == Format.IMG) {
|
||||
inputCodecVideo = Tuple.triple(Integer.valueOf(0), Integer.valueOf(0), getCodecFromExtension(input));
|
||||
} else if (inputFormat.isVideo()) {
|
||||
inputCodecVideo = selectSuitableTrack(input, inputFormat, TrackType.VIDEO);
|
||||
}
|
||||
} else {
|
||||
inputCodecVideo = Tuple.triple(Integer.valueOf(0), Integer.valueOf(0), Codec.valueOf(inputCodecVideoRaw.toUpperCase()));
|
||||
}
|
||||
if (inputCodecVideo != null)
|
||||
if (inputFormat == Format.MPEG_TS) {
|
||||
Logger.info(String.format("Video codec: %s[pid=%d,stream=%d]", String.valueOf(inputCodecVideo.v2), inputCodecVideo.v0, inputCodecVideo.v1));
|
||||
} else {
|
||||
Logger.info(String.format("Video codec: %s", String.valueOf(inputCodecVideo.v2)));
|
||||
}
|
||||
String inputCodecAudioRaw = cmd.getStringFlagI(index, FLAG_AUDIO_CODEC);
|
||||
if (inputCodecAudioRaw == null) {
|
||||
if (inputFormat.isAudio())
|
||||
inputCodecAudio = selectSuitableTrack(input, inputFormat, TrackType.AUDIO);
|
||||
} else {
|
||||
inputCodecAudio = Tuple.triple(Integer.valueOf(0), Integer.valueOf(0), Codec.valueOf(inputCodecAudioRaw.toUpperCase()));
|
||||
}
|
||||
if (inputCodecAudio != null)
|
||||
if (inputFormat == Format.MPEG_TS) {
|
||||
Logger.info(String.format("Audio codec: %s[pid=%d,stream=%d]", String.valueOf(inputCodecAudio.v2), inputCodecAudio.v0, inputCodecAudio.v1));
|
||||
} else {
|
||||
Logger.info(String.format("Audio codec: %s", String.valueOf(inputCodecAudio.v2)));
|
||||
}
|
||||
Source source = new SourceImpl(input, inputFormat, inputCodecVideo, inputCodecAudio);
|
||||
Integer downscale = cmd.getIntegerFlagID(index, FLAG_DOWNSCALE, Integer.valueOf(1));
|
||||
if (downscale != null && 1 << MathUtil.log2(downscale.intValue()) != downscale) {
|
||||
Logger.error("Only values [2, 4, 8] are supported for " + String.valueOf(FLAG_DOWNSCALE) + ", the option will have no effect.");
|
||||
} else {
|
||||
source.setOption(Options.DOWNSCALE, downscale);
|
||||
}
|
||||
source.setOption(Options.PROFILE, cmd.getStringFlagI(index, FLAG_PROFILE));
|
||||
source.setOption(Options.INTERLACED, cmd.getBooleanFlagID(index, FLAG_INTERLACED, Boolean.valueOf(false)));
|
||||
sources.add(source);
|
||||
inputCodecsVideo.add(inputCodecVideo);
|
||||
inputCodecsAudio.add(inputCodecAudio);
|
||||
builder.addSource(source);
|
||||
builder.setSeekFrames(sources.size() - 1, cmd.getIntegerFlagID(index, FLAG_SEEK_FRAMES, Integer.valueOf(0)).intValue())
|
||||
.setMaxFrames(sources.size() - 1, cmd.getIntegerFlagID(index, FLAG_MAX_FRAMES, Integer.valueOf(Integer.MAX_VALUE)).intValue());
|
||||
}
|
||||
}
|
||||
if (sources.isEmpty()) {
|
||||
MainUtils.printHelpArgs(ALL_FLAGS, new String[] { "input", "output" });
|
||||
return;
|
||||
}
|
||||
List<Sink> sinks = new ArrayList<>();
|
||||
for (int i = 0; i < cmd.argsLength(); i++) {
|
||||
if (!cmd.getBooleanFlagI(i, FLAG_INPUT)) {
|
||||
Format outputFormat;
|
||||
String output = cmd.getArg(i);
|
||||
String outputFormatRaw = cmd.getStringFlagI(i, FLAG_FORMAT);
|
||||
if (outputFormatRaw == null) {
|
||||
outputFormat = getFormatFromExtension(output);
|
||||
} else {
|
||||
outputFormat = Format.valueOf(outputFormatRaw.toUpperCase());
|
||||
}
|
||||
String outputCodecVideoRaw = cmd.getStringFlagI(i, FLAG_VIDEO_CODEC);
|
||||
Codec outputCodecVideo = null;
|
||||
boolean videoCopy = false;
|
||||
if (outputCodecVideoRaw == null) {
|
||||
outputCodecVideo = getCodecFromExtension(output);
|
||||
if (outputCodecVideo == null)
|
||||
outputCodecVideo = getFirstVideoCodecForFormat(outputFormat);
|
||||
} else if ("copy".equalsIgnoreCase(outputCodecVideoRaw)) {
|
||||
videoCopy = true;
|
||||
} else if ("none".equalsIgnoreCase(outputCodecVideoRaw)) {
|
||||
outputCodecVideo = null;
|
||||
} else {
|
||||
outputCodecVideo = Codec.valueOf(outputCodecVideoRaw.toUpperCase());
|
||||
}
|
||||
String outputCodecAudioRaw = cmd.getStringFlagI(i, FLAG_AUDIO_CODEC);
|
||||
Codec outputCodecAudio = null;
|
||||
boolean audioCopy = false;
|
||||
if (outputCodecAudioRaw == null) {
|
||||
if (outputFormat.isAudio())
|
||||
outputCodecAudio = getFirstAudioCodecForFormat(outputFormat);
|
||||
} else if ("copy".equalsIgnoreCase(outputCodecAudioRaw)) {
|
||||
audioCopy = true;
|
||||
} else if ("none".equalsIgnoreCase(outputCodecVideoRaw)) {
|
||||
outputCodecAudio = null;
|
||||
} else {
|
||||
outputCodecAudio = Codec.valueOf(outputCodecAudioRaw.toUpperCase());
|
||||
}
|
||||
int audioMap = cmd.getIntegerFlagID(i, FLAG_MAP_AUDIO, Integer.valueOf(0));
|
||||
if (audioMap > sources.size())
|
||||
Logger.error("Can not map audio from source " + audioMap + ", " +
|
||||
sources.size() + " sources specified.");
|
||||
int videoMap = cmd.getIntegerFlagID(i, FLAG_MAP_VIDEO, Integer.valueOf(0));
|
||||
if (videoMap > sources.size())
|
||||
Logger.error("Can not map video from source " + videoMap + ", " +
|
||||
sources.size() + " sources specified.");
|
||||
if (videoCopy) {
|
||||
Tuple._3<Integer, Integer, Codec> inputCodecVideo = inputCodecsVideo.get(videoMap);
|
||||
outputCodecVideo = (inputCodecVideo != null) ? (Codec)inputCodecVideo.v2 : null;
|
||||
}
|
||||
if (audioCopy) {
|
||||
Tuple._3<Integer, Integer, Codec> inputCodecAudio = inputCodecsAudio.get(audioMap);
|
||||
outputCodecAudio = (inputCodecAudio != null) ? (Codec)inputCodecAudio.v2 : null;
|
||||
}
|
||||
Sink sink = new SinkImpl(output, outputFormat, outputCodecVideo, outputCodecAudio);
|
||||
sinks.add(sink);
|
||||
builder.addSink(sink);
|
||||
builder.setAudioMapping(audioMap, sinks.size() - 1, audioCopy);
|
||||
builder.setVideoMapping(videoMap, sinks.size() - 1, videoCopy);
|
||||
if (cmd.getBooleanFlagI(i, FLAG_DUMPMV)) {
|
||||
builder.addFilter(sinks.size() - 1, new DumpMvFilter(false));
|
||||
} else if (cmd.getBooleanFlagI(i, FLAG_DUMPMVJS)) {
|
||||
builder.addFilter(sinks.size() - 1, new DumpMvFilter(true));
|
||||
}
|
||||
String vf = cmd.getStringFlagI(i, FLAG_VIDEO_FILTER);
|
||||
if (vf != null)
|
||||
addVideoFilters(vf, builder, sinks.size() - 1);
|
||||
}
|
||||
}
|
||||
if (sources.isEmpty() || sinks.isEmpty()) {
|
||||
MainUtils.printHelpArgs(ALL_FLAGS, new String[] { "input", "output" });
|
||||
return;
|
||||
}
|
||||
Transcoder transcoder = builder.create();
|
||||
transcoder.transcode();
|
||||
}
|
||||
|
||||
private static void addVideoFilters(String vf, Transcoder.TranscoderBuilder builder, int sinkIndex) {
|
||||
if (vf == null)
|
||||
return;
|
||||
for (String filter : vf.split(",")) {
|
||||
String[] parts = filter.split("=");
|
||||
String filterName = parts[0];
|
||||
Class<? extends Filter> filterClass = knownFilters.get(filterName);
|
||||
if (filterClass == null) {
|
||||
Logger.error("Unknown filter: " + filterName);
|
||||
throw new RuntimeException("Unknown filter: " + filterName);
|
||||
}
|
||||
if (parts.length > 1) {
|
||||
String filterArgs = parts[1];
|
||||
String[] split = filterArgs.split(":");
|
||||
Integer[] params = new Integer[split.length];
|
||||
for (int i = 0; i < split.length; i++)
|
||||
params[i] = Integer.parseInt(split[i]);
|
||||
try {
|
||||
Filter f = Platform.newInstance(filterClass, params);
|
||||
builder.addFilter(sinkIndex, f);
|
||||
} catch (Exception e) {
|
||||
String message = "The filter " + filterName + " doesn't take " + split.length + " arguments.";
|
||||
Logger.error(message);
|
||||
throw new RuntimeException(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Codec getFirstAudioCodecForFormat(Format inputFormat) {
|
||||
return audioCodecsForF.get(inputFormat);
|
||||
}
|
||||
|
||||
private static Codec getFirstVideoCodecForFormat(Format inputFormat) {
|
||||
return videoCodecsForF.get(inputFormat);
|
||||
}
|
||||
|
||||
private static Codec detectVideoDecoder(DemuxerTrack track) throws IOException {
|
||||
DemuxerTrackMeta meta = track.getMeta();
|
||||
if (meta != null) {
|
||||
Codec codec = meta.getCodec();
|
||||
if (codec != null)
|
||||
return codec;
|
||||
}
|
||||
Packet packet = track.nextFrame();
|
||||
if (packet == null)
|
||||
return null;
|
||||
return JCodecUtil.detectDecoder(packet.getData());
|
||||
}
|
||||
|
||||
private static Tuple._3<Integer, Integer, Codec> selectSuitableTrack(String input, Format format, TrackType targetType) throws IOException {
|
||||
Tuple._2<Integer, Demuxer> demuxerPid;
|
||||
if (format == Format.MPEG_TS) {
|
||||
demuxerPid = JCodecUtil.createM2TSDemuxer(new File(input), targetType);
|
||||
} else {
|
||||
demuxerPid = Tuple.pair(Integer.valueOf(0), JCodecUtil.createDemuxer(format, new File(input)));
|
||||
}
|
||||
if (demuxerPid == null || demuxerPid.v1 == null)
|
||||
return null;
|
||||
int trackNo = 0;
|
||||
List<? extends DemuxerTrack> tracks = (targetType == TrackType.VIDEO) ? ((Demuxer)demuxerPid.v1).getVideoTracks() : (
|
||||
(Demuxer)demuxerPid.v1).getAudioTracks();
|
||||
for (DemuxerTrack demuxerTrack : tracks) {
|
||||
Codec codec = detectVideoDecoder(demuxerTrack);
|
||||
if (supportedDecoders.contains(codec))
|
||||
return Tuple.triple((Integer)demuxerPid.v0, Integer.valueOf(trackNo), codec);
|
||||
trackNo++;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Format getFormatFromExtension(String output) {
|
||||
String extension = output.replaceFirst(".*\\.([^\\.]+$)", "$1");
|
||||
return extensionToF.get(extension);
|
||||
}
|
||||
|
||||
private static Codec getCodecFromExtension(String output) {
|
||||
String extension = output.replaceFirst(".*\\.([^\\.]+$)", "$1");
|
||||
return extensionToC.get(extension);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import org.jcodec.api.transcode.filters.ColorTransformFilter;
|
||||
import org.jcodec.common.AudioCodecMeta;
|
||||
import org.jcodec.common.IntArrayList;
|
||||
import org.jcodec.common.VideoCodecMeta;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Packet;
|
||||
|
||||
public class Transcoder {
|
||||
static final int REORDER_BUFFER_SIZE = 7;
|
||||
|
||||
private Source[] sources;
|
||||
|
||||
private Sink[] sinks;
|
||||
|
||||
private List<Filter>[] extraFilters;
|
||||
|
||||
private int[] seekFrames;
|
||||
|
||||
private int[] maxFrames;
|
||||
|
||||
private Mapping[] videoMappings;
|
||||
|
||||
private Mapping[] audioMappings;
|
||||
|
||||
private Transcoder(Source[] source, Sink[] sink, Mapping[] videoMappings, Mapping[] audioMappings, List<Filter>[] extraFilters, int[] seekFrames, int[] maxFrames) {
|
||||
this.extraFilters = extraFilters;
|
||||
this.videoMappings = videoMappings;
|
||||
this.audioMappings = audioMappings;
|
||||
this.seekFrames = seekFrames;
|
||||
this.maxFrames = maxFrames;
|
||||
this.sources = source;
|
||||
this.sinks = sink;
|
||||
}
|
||||
|
||||
private static class Mapping {
|
||||
private int source;
|
||||
|
||||
private boolean copy;
|
||||
|
||||
public Mapping(int source, boolean copy) {
|
||||
this.source = source;
|
||||
this.copy = copy;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Stream {
|
||||
private static final double AUDIO_LEADING_TIME = 0.2D;
|
||||
|
||||
private LinkedList<VideoFrameWithPacket> videoQueue;
|
||||
|
||||
private LinkedList<AudioFrameWithPacket> audioQueue;
|
||||
|
||||
private List<Filter> filters;
|
||||
|
||||
private List<Filter> extraFilters;
|
||||
|
||||
private Sink sink;
|
||||
|
||||
private boolean videoCopy;
|
||||
|
||||
private boolean audioCopy;
|
||||
|
||||
private PixelStore pixelStore;
|
||||
|
||||
private VideoCodecMeta videoCodecMeta;
|
||||
|
||||
private AudioCodecMeta audioCodecMeta;
|
||||
|
||||
private static final int REORDER_LENGTH = 5;
|
||||
|
||||
public Stream(Sink sink, boolean videoCopy, boolean audioCopy, List<Filter> extraFilters, PixelStore pixelStore) {
|
||||
this.sink = sink;
|
||||
this.videoCopy = videoCopy;
|
||||
this.audioCopy = audioCopy;
|
||||
this.extraFilters = extraFilters;
|
||||
this.pixelStore = pixelStore;
|
||||
this.videoQueue = new LinkedList<>();
|
||||
this.audioQueue = new LinkedList<>();
|
||||
}
|
||||
|
||||
private List<Filter> initColorTransform(ColorSpace sourceColor, List<Filter> extraFilters, Sink sink) {
|
||||
List<Filter> filters = new ArrayList<>();
|
||||
for (Filter filter : extraFilters) {
|
||||
ColorSpace colorSpace = filter.getInputColor();
|
||||
if (!sourceColor.matches(colorSpace))
|
||||
filters.add(new ColorTransformFilter(colorSpace));
|
||||
filters.add(filter);
|
||||
if (filter.getOutputColor() != ColorSpace.SAME)
|
||||
sourceColor = filter.getOutputColor();
|
||||
}
|
||||
ColorSpace inputColor = sink.getInputColor();
|
||||
if (inputColor != null && inputColor != sourceColor)
|
||||
filters.add(new ColorTransformFilter(inputColor));
|
||||
return filters;
|
||||
}
|
||||
|
||||
public void tryFlushQueues() throws IOException {
|
||||
if (this.videoQueue.size() <= 0)
|
||||
return;
|
||||
if (this.videoCopy && this.videoQueue.size() < 5)
|
||||
return;
|
||||
if (!hasLeadingAudio())
|
||||
return;
|
||||
VideoFrameWithPacket firstVideoFrame = this.videoQueue.get(0);
|
||||
if (this.videoCopy)
|
||||
for (VideoFrameWithPacket videoFrame : this.videoQueue) {
|
||||
if (videoFrame.getPacket().getFrameNo() < firstVideoFrame.getPacket().getFrameNo())
|
||||
firstVideoFrame = videoFrame;
|
||||
}
|
||||
int aqSize = this.audioQueue.size();
|
||||
for (int af = 0; af < aqSize; af++) {
|
||||
AudioFrameWithPacket audioFrame = this.audioQueue.get(0);
|
||||
if (audioFrame.getPacket().getPtsD() >= firstVideoFrame.getPacket().getPtsD() + 0.2D)
|
||||
break;
|
||||
this.audioQueue.remove(0);
|
||||
if (this.audioCopy && this.sink instanceof PacketSink) {
|
||||
((PacketSink)this.sink).outputAudioPacket(audioFrame.getPacket(), this.audioCodecMeta);
|
||||
} else {
|
||||
this.sink.outputAudioFrame(audioFrame);
|
||||
}
|
||||
}
|
||||
this.videoQueue.remove(firstVideoFrame);
|
||||
if (this.videoCopy && this.sink instanceof PacketSink) {
|
||||
((PacketSink)this.sink).outputVideoPacket(firstVideoFrame.getPacket(), this.videoCodecMeta);
|
||||
} else {
|
||||
PixelStore.LoanerPicture frame = filterFrame(firstVideoFrame);
|
||||
this.sink.outputVideoFrame(new VideoFrameWithPacket(firstVideoFrame.getPacket(), frame));
|
||||
this.pixelStore.putBack(frame);
|
||||
}
|
||||
}
|
||||
|
||||
private PixelStore.LoanerPicture filterFrame(VideoFrameWithPacket firstVideoFrame) {
|
||||
PixelStore.LoanerPicture frame = firstVideoFrame.getFrame();
|
||||
for (Filter filter : this.filters) {
|
||||
PixelStore.LoanerPicture old = frame;
|
||||
frame = filter.filter(frame.getPicture(), this.pixelStore);
|
||||
if (frame == null) {
|
||||
frame = old;
|
||||
continue;
|
||||
}
|
||||
this.pixelStore.putBack(old);
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
|
||||
public void finalFlushQueues() throws IOException {
|
||||
VideoFrameWithPacket lastVideoFrame = null;
|
||||
for (VideoFrameWithPacket videoFrame : this.videoQueue) {
|
||||
if (lastVideoFrame == null || videoFrame.getPacket().getPtsD() >= lastVideoFrame.getPacket().getPtsD())
|
||||
lastVideoFrame = videoFrame;
|
||||
}
|
||||
if (lastVideoFrame != null) {
|
||||
for (AudioFrameWithPacket audioFrame : this.audioQueue) {
|
||||
if (audioFrame.getPacket().getPtsD() > lastVideoFrame.getPacket().getPtsD())
|
||||
break;
|
||||
if (this.audioCopy && this.sink instanceof PacketSink) {
|
||||
((PacketSink)this.sink).outputAudioPacket(audioFrame.getPacket(), this.audioCodecMeta);
|
||||
continue;
|
||||
}
|
||||
this.sink.outputAudioFrame(audioFrame);
|
||||
}
|
||||
for (VideoFrameWithPacket videoFrame : this.videoQueue) {
|
||||
if (videoFrame != null) {
|
||||
if (this.videoCopy && this.sink instanceof PacketSink) {
|
||||
((PacketSink)this.sink).outputVideoPacket(videoFrame.getPacket(), this.videoCodecMeta);
|
||||
continue;
|
||||
}
|
||||
PixelStore.LoanerPicture frame = filterFrame(videoFrame);
|
||||
this.sink.outputVideoFrame(new VideoFrameWithPacket(videoFrame.getPacket(), frame));
|
||||
this.pixelStore.putBack(frame);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (AudioFrameWithPacket audioFrame : this.audioQueue) {
|
||||
if (this.audioCopy && this.sink instanceof PacketSink) {
|
||||
((PacketSink)this.sink).outputAudioPacket(audioFrame.getPacket(), this.audioCodecMeta);
|
||||
continue;
|
||||
}
|
||||
this.sink.outputAudioFrame(audioFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addVideoPacket(VideoFrameWithPacket videoFrame, VideoCodecMeta meta) {
|
||||
if (videoFrame.getFrame() != null)
|
||||
this.pixelStore.retake(videoFrame.getFrame());
|
||||
this.videoQueue.add(videoFrame);
|
||||
this.videoCodecMeta = meta;
|
||||
if (this.filters == null)
|
||||
this.filters = initColorTransform(this.videoCodecMeta.getColor(), this.extraFilters, this.sink);
|
||||
}
|
||||
|
||||
public void addAudioPacket(AudioFrameWithPacket videoFrame, AudioCodecMeta meta) {
|
||||
this.audioQueue.add(videoFrame);
|
||||
this.audioCodecMeta = meta;
|
||||
}
|
||||
|
||||
public boolean needsVideoFrame() {
|
||||
if (this.videoQueue.size() <= 0)
|
||||
return true;
|
||||
if (this.videoCopy && this.videoQueue.size() < 5)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean hasLeadingAudio() {
|
||||
VideoFrameWithPacket firstVideoFrame = this.videoQueue.get(0);
|
||||
for (AudioFrameWithPacket audioFrame : this.audioQueue) {
|
||||
if (audioFrame.getPacket().getPtsD() >= firstVideoFrame.getPacket().getPtsD() + 0.2D)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void transcode() throws IOException {
|
||||
PixelStore pixelStore = new PixelStoreImpl();
|
||||
List<Stream>[] videoStreams = new List[this.sources.length];
|
||||
List<Stream>[] audioStreams = new List[this.sources.length];
|
||||
boolean[] decodeVideo = new boolean[this.sources.length];
|
||||
boolean[] decodeAudio = new boolean[this.sources.length];
|
||||
boolean[] finishedVideo = new boolean[this.sources.length];
|
||||
boolean[] finishedAudio = new boolean[this.sources.length];
|
||||
Stream[] allStreams = new Stream[this.sinks.length];
|
||||
int[] videoFramesRead = new int[this.sources.length];
|
||||
for (int k = 0; k < this.sources.length; k++) {
|
||||
videoStreams[k] = new ArrayList<>();
|
||||
audioStreams[k] = new ArrayList<>();
|
||||
}
|
||||
for (int j = 0; j < this.sinks.length; j++)
|
||||
this.sinks[j].init();
|
||||
for (int i = 0; i < this.sources.length; i++) {
|
||||
this.sources[i].init(pixelStore);
|
||||
this.sources[i].seekFrames(this.seekFrames[i]);
|
||||
}
|
||||
for (int s = 0; s < this.sinks.length; s++) {
|
||||
Stream stream = new Stream(this.sinks[s], (this.videoMappings[s]).copy, (this.audioMappings[s]).copy, this.extraFilters[s], pixelStore);
|
||||
allStreams[s] = stream;
|
||||
if (this.sources[(this.videoMappings[s]).source].isVideo()) {
|
||||
videoStreams[(this.videoMappings[s]).source].add(stream);
|
||||
if (!(this.videoMappings[s]).copy)
|
||||
decodeVideo[(this.videoMappings[s]).source] = true;
|
||||
} else {
|
||||
finishedVideo[(this.videoMappings[s]).source] = true;
|
||||
}
|
||||
if (this.sources[(this.audioMappings[s]).source].isAudio()) {
|
||||
audioStreams[(this.audioMappings[s]).source].add(stream);
|
||||
if (!(this.audioMappings[s]).copy)
|
||||
decodeAudio[(this.audioMappings[s]).source] = true;
|
||||
} else {
|
||||
finishedAudio[(this.audioMappings[s]).source] = true;
|
||||
}
|
||||
}
|
||||
try {
|
||||
boolean allFinished;
|
||||
do {
|
||||
for (int i1 = 0; i1 < this.sources.length; i1++) {
|
||||
Source source = this.sources[i1];
|
||||
boolean needsVideoFrame = !finishedVideo[i1];
|
||||
for (Stream stream : videoStreams[i1])
|
||||
needsVideoFrame &= (stream.needsVideoFrame() || stream.hasLeadingAudio() || finishedAudio[i1]) ? true : false;
|
||||
if (needsVideoFrame) {
|
||||
VideoFrameWithPacket nextVideoFrame;
|
||||
if (videoFramesRead[i1] >= this.maxFrames[i1]) {
|
||||
nextVideoFrame = null;
|
||||
finishedVideo[i1] = true;
|
||||
} else if (decodeVideo[i1] || !(source instanceof PacketSource)) {
|
||||
nextVideoFrame = source.getNextVideoFrame();
|
||||
if (nextVideoFrame == null) {
|
||||
finishedVideo[i1] = true;
|
||||
} else {
|
||||
videoFramesRead[i1] = videoFramesRead[i1] + 1;
|
||||
printLegend((int)nextVideoFrame.getPacket().getFrameNo(), 0,
|
||||
nextVideoFrame.getPacket());
|
||||
}
|
||||
} else {
|
||||
Packet packet = ((PacketSource)source).inputVideoPacket();
|
||||
if (packet == null) {
|
||||
finishedVideo[i1] = true;
|
||||
} else {
|
||||
videoFramesRead[i1] = videoFramesRead[i1] + 1;
|
||||
}
|
||||
nextVideoFrame = new VideoFrameWithPacket(packet, null);
|
||||
}
|
||||
if (finishedVideo[i1]) {
|
||||
for (Stream stream : videoStreams[i1]) {
|
||||
for (int ss = 0; ss < audioStreams.length; ss++)
|
||||
audioStreams[ss].remove(stream);
|
||||
}
|
||||
videoStreams[i1].clear();
|
||||
}
|
||||
if (nextVideoFrame != null) {
|
||||
for (Stream stream : videoStreams[i1])
|
||||
stream.addVideoPacket(nextVideoFrame, source.getVideoCodecMeta());
|
||||
if (nextVideoFrame.getFrame() != null)
|
||||
pixelStore.putBack(nextVideoFrame.getFrame());
|
||||
}
|
||||
}
|
||||
if (!audioStreams[i1].isEmpty()) {
|
||||
AudioFrameWithPacket nextAudioFrame;
|
||||
if (decodeAudio[i1] || !(source instanceof PacketSource)) {
|
||||
nextAudioFrame = source.getNextAudioFrame();
|
||||
if (nextAudioFrame == null)
|
||||
finishedAudio[i1] = true;
|
||||
} else {
|
||||
Packet packet = ((PacketSource)source).inputAudioPacket();
|
||||
if (packet == null) {
|
||||
finishedAudio[i1] = true;
|
||||
nextAudioFrame = null;
|
||||
} else {
|
||||
nextAudioFrame = new AudioFrameWithPacket(null, packet);
|
||||
}
|
||||
}
|
||||
if (nextAudioFrame != null)
|
||||
for (Stream stream : audioStreams[i1])
|
||||
stream.addAudioPacket(nextAudioFrame, source.getAudioCodecMeta());
|
||||
} else {
|
||||
finishedAudio[i1] = true;
|
||||
}
|
||||
}
|
||||
for (int i2 = 0; i2 < allStreams.length; i2++)
|
||||
allStreams[i2].tryFlushQueues();
|
||||
allFinished = true;
|
||||
for (int i3 = 0; i3 < this.sources.length; i3++)
|
||||
allFinished &= finishedVideo[i3] & finishedAudio[i3];
|
||||
} while (!allFinished);
|
||||
for (int n = 0; n < allStreams.length; n++)
|
||||
allStreams[n].finalFlushQueues();
|
||||
} finally {
|
||||
for (int n = 0; n < this.sources.length; n++)
|
||||
this.sources[0].finish();
|
||||
for (int m = 0; m < this.sinks.length; m++)
|
||||
this.sinks[m].finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void printLegend(int frameNo, int maxFrames, Packet inVideoPacket) {
|
||||
if (frameNo % 100 == 0)
|
||||
System.out.print(String.format("[%6d]\r", frameNo));
|
||||
}
|
||||
|
||||
public static class TranscoderBuilder {
|
||||
private List<Source> source = new ArrayList<>();
|
||||
|
||||
private List<Sink> sink = new ArrayList<>();
|
||||
|
||||
private List<List<Filter>> filters = new ArrayList<>();
|
||||
|
||||
private IntArrayList seekFrames = new IntArrayList(20);
|
||||
|
||||
private IntArrayList maxFrames = new IntArrayList(20);
|
||||
|
||||
private List<Transcoder.Mapping> videoMappings = new ArrayList<>();
|
||||
|
||||
private List<Transcoder.Mapping> audioMappings = new ArrayList<>();
|
||||
|
||||
public TranscoderBuilder addFilter(int sink, Filter filter) {
|
||||
this.filters.get(sink).add(filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranscoderBuilder setSeekFrames(int source, int seekFrames) {
|
||||
this.seekFrames.set(source, seekFrames);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranscoderBuilder setMaxFrames(int source, int maxFrames) {
|
||||
this.maxFrames.set(source, maxFrames);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranscoderBuilder addSource(Source source) {
|
||||
this.source.add(source);
|
||||
this.seekFrames.add(0);
|
||||
this.maxFrames.add(Integer.MAX_VALUE);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranscoderBuilder addSink(Sink sink) {
|
||||
this.sink.add(sink);
|
||||
this.videoMappings.add(new Transcoder.Mapping(0, false));
|
||||
this.audioMappings.add(new Transcoder.Mapping(0, false));
|
||||
this.filters.add(new ArrayList<>());
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranscoderBuilder setVideoMapping(int src, int sink, boolean copy) {
|
||||
this.videoMappings.set(sink, new Transcoder.Mapping(src, copy));
|
||||
return this;
|
||||
}
|
||||
|
||||
public TranscoderBuilder setAudioMapping(int src, int sink, boolean copy) {
|
||||
this.audioMappings.set(sink, new Transcoder.Mapping(src, copy));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Transcoder create() {
|
||||
return new Transcoder(this.source.<Source>toArray(new Source[0]), this.sink.<Sink>toArray(new Sink[0]),
|
||||
this.videoMappings.<Transcoder.Mapping>toArray(new Transcoder.Mapping[0]), this.audioMappings.<Transcoder.Mapping>toArray(new Transcoder.Mapping[0]),
|
||||
this.filters.<List<Filter>>toArray(new List[0]), this.seekFrames.toArray(), this.maxFrames.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
public static TranscoderBuilder newTranscoder() {
|
||||
return new TranscoderBuilder();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package org.jcodec.api.transcode;
|
||||
|
||||
import org.jcodec.common.model.Packet;
|
||||
|
||||
public class VideoFrameWithPacket implements Comparable<VideoFrameWithPacket> {
|
||||
private Packet packet;
|
||||
|
||||
private PixelStore.LoanerPicture frame;
|
||||
|
||||
public VideoFrameWithPacket(Packet inFrame, PixelStore.LoanerPicture dec2) {
|
||||
this.packet = inFrame;
|
||||
this.frame = dec2;
|
||||
}
|
||||
|
||||
public int compareTo(VideoFrameWithPacket arg) {
|
||||
if (arg == null)
|
||||
return -1;
|
||||
long pts1 = this.packet.getPts();
|
||||
long pts2 = arg.packet.getPts();
|
||||
return (pts1 > pts2) ? 1 : ((pts1 == pts2) ? 0 : -1);
|
||||
}
|
||||
|
||||
public Packet getPacket() {
|
||||
return this.packet;
|
||||
}
|
||||
|
||||
public PixelStore.LoanerPicture getFrame() {
|
||||
return this.frame;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package org.jcodec.api.transcode.filters;
|
||||
|
||||
import org.jcodec.api.transcode.Filter;
|
||||
import org.jcodec.api.transcode.PixelStore;
|
||||
import org.jcodec.common.logging.Logger;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
import org.jcodec.scale.ColorUtil;
|
||||
import org.jcodec.scale.Transform;
|
||||
|
||||
public class ColorTransformFilter implements Filter {
|
||||
private Transform transform;
|
||||
|
||||
private ColorSpace outputColor;
|
||||
|
||||
public ColorTransformFilter(ColorSpace outputColor) {
|
||||
this.outputColor = outputColor;
|
||||
}
|
||||
|
||||
public PixelStore.LoanerPicture filter(Picture picture, PixelStore store) {
|
||||
if (this.transform == null) {
|
||||
this.transform = ColorUtil.getTransform(picture.getColor(), this.outputColor);
|
||||
Logger.debug("Creating transform: " + String.valueOf(this.transform));
|
||||
}
|
||||
PixelStore.LoanerPicture outFrame = store.getPicture(picture.getWidth(), picture.getHeight(), this.outputColor);
|
||||
outFrame.getPicture().setCrop(picture.getCrop());
|
||||
this.transform.transform(picture, outFrame.getPicture());
|
||||
return outFrame;
|
||||
}
|
||||
|
||||
public ColorSpace getInputColor() {
|
||||
return ColorSpace.ANY_PLANAR;
|
||||
}
|
||||
|
||||
public ColorSpace getOutputColor() {
|
||||
return this.outputColor;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package org.jcodec.api.transcode.filters;
|
||||
|
||||
import org.jcodec.api.transcode.Filter;
|
||||
import org.jcodec.api.transcode.PixelStore;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
|
||||
public class CropFilter implements Filter {
|
||||
public PixelStore.LoanerPicture filter(Picture picture, PixelStore store) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ColorSpace getInputColor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ColorSpace getOutputColor() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package org.jcodec.api.transcode.filters;
|
||||
|
||||
import org.jcodec.api.transcode.Filter;
|
||||
import org.jcodec.api.transcode.PixelStore;
|
||||
import org.jcodec.codecs.h264.H264Utils;
|
||||
import org.jcodec.codecs.h264.io.model.Frame;
|
||||
import org.jcodec.codecs.h264.io.model.SliceType;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
|
||||
public class DumpMvFilter implements Filter {
|
||||
private boolean js;
|
||||
|
||||
public DumpMvFilter(boolean js) {
|
||||
this.js = js;
|
||||
}
|
||||
|
||||
public PixelStore.LoanerPicture filter(Picture picture, PixelStore pixelStore) {
|
||||
Frame dec = (Frame)picture;
|
||||
if (!this.js) {
|
||||
dumpMvTxt(dec);
|
||||
} else {
|
||||
dumpMvJs(dec);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void dumpMvTxt(Frame dec) {
|
||||
System.err.println("FRAME ================================================================");
|
||||
if (dec.getFrameType() == SliceType.I)
|
||||
return;
|
||||
H264Utils.MvList2D mvs = dec.getMvs();
|
||||
for (int i = 0; i < 2; i++) {
|
||||
System.err.println(((i == 0) ? "BCK" : "FWD") + " ===========================================================================");
|
||||
for (int blkY = 0; blkY < mvs.getHeight(); blkY++) {
|
||||
StringBuilder line0 = new StringBuilder();
|
||||
StringBuilder line1 = new StringBuilder();
|
||||
StringBuilder line2 = new StringBuilder();
|
||||
StringBuilder line3 = new StringBuilder();
|
||||
line0.append("+");
|
||||
line1.append("|");
|
||||
line2.append("|");
|
||||
line3.append("|");
|
||||
for (int blkX = 0; blkX < mvs.getWidth(); blkX++) {
|
||||
line0.append("------+");
|
||||
line1.append(String.format("%6d|", H264Utils.Mv.mvX(mvs.getMv(blkX, blkY, i))));
|
||||
line2.append(String.format("%6d|", H264Utils.Mv.mvY(mvs.getMv(blkX, blkY, i))));
|
||||
line3.append(String.format(" %2d|", H264Utils.Mv.mvRef(mvs.getMv(blkX, blkY, i))));
|
||||
}
|
||||
System.err.println(line0.toString());
|
||||
System.err.println(line1.toString());
|
||||
System.err.println(line2.toString());
|
||||
System.err.println(line3.toString());
|
||||
}
|
||||
if (dec.getFrameType() != SliceType.B)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void dumpMvJs(Frame dec) {
|
||||
System.err.println("{");
|
||||
if (dec.getFrameType() == SliceType.I)
|
||||
return;
|
||||
H264Utils.MvList2D mvs = dec.getMvs();
|
||||
for (int i = 0; i < 2; i++) {
|
||||
System.err.println(((i == 0) ? "backRef" : "forwardRef") + ": [");
|
||||
for (int blkY = 0; blkY < mvs.getHeight(); blkY++) {
|
||||
for (int blkX = 0; blkX < mvs.getWidth(); blkX++)
|
||||
System.err.println("{x: " + blkX + ", y: " + blkY + ", mx: " + H264Utils.Mv.mvX(mvs.getMv(blkX, blkY, i)) + ", my: " +
|
||||
H264Utils.Mv.mvY(mvs.getMv(blkX, blkY, i)) + ", ridx:" + H264Utils.Mv.mvRef(mvs.getMv(blkX, blkY, i)) + "},");
|
||||
}
|
||||
System.err.println("],");
|
||||
if (dec.getFrameType() != SliceType.B)
|
||||
break;
|
||||
}
|
||||
System.err.println("}");
|
||||
}
|
||||
|
||||
public ColorSpace getInputColor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ColorSpace getOutputColor() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package org.jcodec.api.transcode.filters;
|
||||
|
||||
import org.jcodec.api.transcode.Filter;
|
||||
import org.jcodec.api.transcode.PixelStore;
|
||||
import org.jcodec.common.model.ColorSpace;
|
||||
import org.jcodec.common.model.Picture;
|
||||
import org.jcodec.common.model.Size;
|
||||
import org.jcodec.scale.BaseResampler;
|
||||
import org.jcodec.scale.LanczosResampler;
|
||||
|
||||
public class ScaleFilter implements Filter {
|
||||
private BaseResampler resampler;
|
||||
|
||||
private ColorSpace currentColor;
|
||||
|
||||
private Size currentSize;
|
||||
|
||||
private Size targetSize;
|
||||
|
||||
private int width;
|
||||
|
||||
private int height;
|
||||
|
||||
public ScaleFilter(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Size getTarget() {
|
||||
return new Size(this.width, this.height);
|
||||
}
|
||||
|
||||
public PixelStore.LoanerPicture filter(Picture picture, PixelStore store) {
|
||||
Size pictureSize = picture.getSize();
|
||||
if (this.resampler == null || this.currentColor != picture.getColor() || !pictureSize.equals(this.currentSize)) {
|
||||
this.currentColor = picture.getColor();
|
||||
this.currentSize = picture.getSize();
|
||||
this.targetSize = new Size(this.width & this.currentColor.getWidthMask(), this.height & this.currentColor.getHeightMask());
|
||||
this.resampler = new LanczosResampler(this.currentSize, this.targetSize);
|
||||
}
|
||||
PixelStore.LoanerPicture dest = store.getPicture(this.targetSize.getWidth(), this.targetSize.getHeight(), this.currentColor);
|
||||
this.resampler.resample(picture, dest.getPicture());
|
||||
return dest;
|
||||
}
|
||||
|
||||
public ColorSpace getInputColor() {
|
||||
return ColorSpace.ANY_PLANAR;
|
||||
}
|
||||
|
||||
public ColorSpace getOutputColor() {
|
||||
return ColorSpace.SAME;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue