diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/FFmpegProcess.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/FFmpegProcess.java
index e6fe0a6be..77193644e 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/FFmpegProcess.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/FFmpegProcess.java
@@ -246,6 +246,7 @@ public void doStart() throws SensorException, SensorHubException {
// Reinit executable. Not always necessary, but doesn't hurt.
executable.init();
process.start(this::onError);
+ //process.start(this::onError);
} catch (ProcessException e) {
logger.error("Could not initialize process.", e);
return;
@@ -459,7 +460,7 @@ public void publishData() {
}
- // Listens for event from dara
+ // Listens for event from data
protected class DataQueuePusher implements IEventListener {
DataQueue dataQueue;
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/CodecEnum.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/CodecEnum.java
index 701c49996..d0cdd3db3 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/CodecEnum.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/CodecEnum.java
@@ -11,15 +11,14 @@
public enum CodecEnum {
//AUTO("auto"),
H264(AV_CODEC_ID_H264),
- H265(AV_CODEC_ID_H265),
- HEVC(AV_CODEC_ID_HEVC), // HEVC and H265 are the same. Having both in this enum helps with auto codec detection.
+ HEVC(AV_CODEC_ID_HEVC),
MJPEG(AV_CODEC_ID_MJPEG),
VP8(AV_CODEC_ID_VP8),
VP9(AV_CODEC_ID_VP9),
MPEG2(AV_CODEC_ID_MPEG2TS),
MPEG4(AV_CODEC_ID_MPEG4),
- RGB(AV_PIX_FMT_RGB24),
- YUV(AV_PIX_FMT_YUV420P);
+ RGB24(AV_PIX_FMT_RGB24),
+ YUV420P(AV_PIX_FMT_YUV420P);
int ffmpegId;
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFMpegTranscoder.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFMpegTranscoder.java
index 9ea520d92..1a8d7af87 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFMpegTranscoder.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFMpegTranscoder.java
@@ -18,18 +18,13 @@
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avutil.AVFrame;
import org.bytedeco.javacpp.BytePointer;
-import org.bytedeco.javacpp.DoublePointer;
-import org.bytedeco.javacpp.Pointer;
-import org.sensorhub.api.common.SensorHubException;
import org.sensorhub.api.processing.OSHProcessInfo;
-import org.sensorhub.impl.process.video.transcoder.coders.Coder;
-import org.sensorhub.impl.process.video.transcoder.coders.Decoder;
-import org.sensorhub.impl.process.video.transcoder.coders.Encoder;
-import org.sensorhub.impl.process.video.transcoder.coders.SwScaler;
-import org.sensorhub.impl.process.video.transcoder.formatters.AVByteFormatter;
-import org.sensorhub.impl.process.video.transcoder.formatters.PacketFormatter;
-import org.sensorhub.impl.process.video.transcoder.formatters.RgbFormatter;
-import org.sensorhub.impl.process.video.transcoder.formatters.YuvFormatter;
+import org.sensorhub.impl.process.video.transcoder.coders.*;
+import org.sensorhub.impl.process.video.transcoder.formatters.*;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecInfo;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecOptions;
+import org.sensorhub.impl.process.video.transcoder.helpers.FullCodecEnum;
+import org.sensorhub.impl.process.video.transcoder.helpers.FullPixelEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vast.data.DataBlockCompressed;
@@ -39,13 +34,10 @@
import org.vast.swe.helper.RasterHelper;
import javax.annotation.Nullable;
-import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
-import static org.bytedeco.ffmpeg.global.avcodec.*;
import static org.bytedeco.ffmpeg.global.avutil.*;
-import static org.bytedeco.ffmpeg.global.swscale.*;
/**
*
@@ -62,8 +54,7 @@ public class FFMpegTranscoder extends ExecutableProcessImpl
public static final OSHProcessInfo INFO = new OSHProcessInfo("video:FFMpegTranscoder", "FFMPEG Video Transcoder", null, FFMpegTranscoder.class);
- AtomicBoolean doRun = new AtomicBoolean(true);
- AtomicBoolean isRunning = new AtomicBoolean(false);
+ AtomicBoolean isInit = new AtomicBoolean(false);
Time inputTimeStamp;
Count inputWidth, inputHeight;
DataArray imgIn;
@@ -73,18 +64,14 @@ public class FFMpegTranscoder extends ExecutableProcessImpl
Text inCodecParam;
Text outCodecParam;
- List> videoProcs;
- AVByteFormatter inputFormatter, outputFormatter;
- ArrayDeque inputPackets;
- ArrayDeque outputPackets;
- Thread outputThread;
+ List videoProcs;
+ AVByteFormatter inputFormatter, outputFormatter;
- HashMap decOptions = new HashMap<>();
- HashMap encOptions = new HashMap<>();
+ CodecOptions decOptions, encOptions;
final boolean publish = false;
- CodecEnum inCodec;
- CodecEnum outCodec;
+ CodecInfo inCodec;
+ CodecInfo outCodec;
RasterHelper swe = new RasterHelper();
int width, height, outWidth, outHeight;
@@ -122,13 +109,13 @@ public FFMpegTranscoder()
paramData.add("inCodec", inCodecParam = swe.createText()
.definition(SWEHelper.getPropertyUri("Codec"))
.label("Input Codec Name")
- .addAllowedValues(CodecEnum.class)
+ //.addAllowedValues(CodecEnum.class)
.build());
paramData.add("outCodec", outCodecParam = swe.createText()
.definition(SWEHelper.getPropertyUri("Codec"))
.label("Output Codec Name")
- .addAllowedValues(CodecEnum.class)
+ //.addAllowedValues(CodecEnum.class)
.build());
// outputs
@@ -156,81 +143,86 @@ public FFMpegTranscoder()
}
+ private void initFormatters() throws ProcessException {
+ inputFormatter = getFormatter(inCodec, width, height);
+ outputFormatter = getFormatter(outCodec, outWidth, outHeight);
+ }
+
/**
* Initializes all encoder/decoder/swscaler objects. These objects are added to the {@link FFMpegTranscoder#videoProcs}
* queue in the order they should process the incoming data. At most, the flow will be Decoder -> SWScale -> Encoder.
*/
- private void initCoders() {
- try {
- stopProcessThreads();
- } catch (Exception e){
- logger.error("Transcoder could not stop process threads during re-init.", e);
- }
+ private void initCodecs() {
if (videoProcs != null) {
videoProcs.clear();
}
videoProcs = new ArrayList<>();
+ Decoder decoder = null;
+ Encoder encoder = null;
+ SwScaler swScaler = null;
+
if (!isUncompressed(inCodec)) {
- videoProcs.add(new Decoder(inCodec.ffmpegId, decOptions));
- }
- if (width != outWidth || height != outHeight || (isUncompressed(inCodec) && isUncompressed(outCodec))) {
- int inFmt = isUncompressed(inCodec) ? inCodec.ffmpegId : AV_PIX_FMT_YUV420P;
- int outFmt = isUncompressed(outCodec) ? outCodec.ffmpegId : AV_PIX_FMT_YUV420P;
- videoProcs.add(new SwScaler(inFmt, outFmt, width, height, outWidth, outHeight));
+ decoder = new Decoder(inCodec, outCodec, decOptions);
+ inCodec.pixelFmt = decoder.init();
+ if (inCodec.pixelFmt == null)
+ inCodec.pixelFmt = FullPixelEnum.YUV420P;
+ //videoProcs.add(decoder);
}
+
if (!isUncompressed(outCodec)) {
- videoProcs.add(new Encoder(outCodec.ffmpegId, encOptions));
+ encoder = new Encoder(inCodec, outCodec, encOptions);
+ outCodec.pixelFmt = encoder.init();
+ //videoProcs.add(encoder);
}
- inputPackets = new ArrayDeque<>();
- if (videoProcs.get(0) != null) {
- videoProcs.get(0).setInQueue(inputPackets);
- }
- for (int i = 1; i < videoProcs.size(); i++) {
- videoProcs.get(i).setInQueue(videoProcs.get(i - 1).getOutQueue());
- }
- try {
- outputPackets = (ArrayDeque) videoProcs.get(videoProcs.size() - 1).getOutQueue();
- } catch (Exception e) {
- logger.warn("No processes running on video input. Check codec/resolution settings.", e);
- outputPackets = inputPackets;
- }
- }
+ // Always want swScaler. Decoder can output frames in a format different from what was set.
+ swScaler = new SwScaler(inCodec, outCodec, width, height, outWidth, outHeight);
+ swScaler.init();
+ //videoProcs.add(swScaler);
- /**
- * Invoked during the first call to {@link FFMpegTranscoder#execute()} (when {@link FFMpegTranscoder#isRunning} is false).
- * Start all {@link Coder} thread objects stored in {@link FFMpegTranscoder#videoProcs}.
- */
- private void startProcessThreads() {
- doRun.set(true);
- if (videoProcs == null || videoProcs.isEmpty() || videoProcs.get(0).getState() != Thread.State.NEW) {
- initCoders();
- }
+ if (decoder != null) { videoProcs.add(decoder); }
+ if (swScaler != null) { videoProcs.add(swScaler); }
+ if (encoder != null) { videoProcs.add(encoder); }
+
+ logger.info("Input pixel format: {}, Output pixel format: {}", inCodec.pixelFmt, outCodec.pixelFmt);
+
+ if (inCodec.pixelFmt == null || outCodec.pixelFmt == null) { logger.warn("Pixel format is null"); }
- for (Thread process : videoProcs) {
- process.start();
+ }
+
+ private void initPipeline() {
+ // Frame pipe between decoder, swscaler, encoder
+ for (int i = 0; i < videoProcs.size() - 1; i++) {
+ var nextProc = videoProcs.get(i + 1);
+ videoProcs.get(i).registerCallback(packet -> {
+ nextProc.submitInputPacket(packet);
+ });
}
- outputThread.start();
- isRunning.set(true);
+ // Output
+ var finalProc = videoProcs.get(videoProcs.size() - 1);
+ finalProc.registerCallback(packet -> {
+ try {
+ publishFrameData(outputFormatter.convertOutput(packet));
+ } finally {
+ finalProc.deallocateOutputPacket(packet);
+ }
+ });
}
/**
* Invoked on process stop and init.
- * Stop all {@link Coder} thread objects stored in {@link FFMpegTranscoder#videoProcs}.
+ * Stop all {@link Codec} thread objects stored in {@link FFMpegTranscoder#videoProcs}.
*/
- private void stopProcessThreads() throws InterruptedException {
- doRun.set(false); //TODO These atomic booleans may be entirely unnecessary, remove
+ private void stopProcessing() throws InterruptedException {
+ isInit.set(false);
if (videoProcs != null) {
- for (Thread thread : videoProcs) {
- thread.interrupt();
- thread.join();
+ for (Codec codec : videoProcs) {
+ codec.close();
}
+ videoProcs.clear();
}
- outputThread.interrupt();
- outputThread.join();
- isRunning.set(false);
}
@@ -266,12 +258,10 @@ public void notifyParamChange()
@Override
public void stop() {
- if (isRunning.get()) {
- try {
- stopProcessThreads();
- } catch (InterruptedException e) {
- logger.warn("Interrupted while stopping process threads");
- }
+ try {
+ stopProcessing();
+ } catch (InterruptedException e) {
+ logger.warn("Interrupted while stopping process threads");
}
super.stop();
}
@@ -321,32 +311,30 @@ private void setImgEncoding() {
@Override
public void init() throws ProcessException
{
- doRun.set(true);
- isRunning.set(false);
+ if (!isInit.compareAndSet(false, true)) {
+ return;
+ }
// init decoder according to configured codec
// TODO: Automatically detect input codec from compression in data struct?
try
{
- inCodec = CodecEnum.valueOf(inCodecParam.getData().getStringValue().toUpperCase());
- outCodec = CodecEnum.valueOf(outCodecParam.getData().getStringValue().toUpperCase());
+ //inCodec = CodecEnum.valueOf(inCodecParam.getData().getStringValue().toUpperCase());
+ //outCodec = CodecEnum.valueOf(outCodecParam.getData().getStringValue().toUpperCase());
+ inCodec = CodecInfo.newCodecInfoFromName(inCodecParam.getData().getStringValue());
+ outCodec = CodecInfo.newCodecInfoFromName(outCodecParam.getData().getStringValue());
setImgEncoding();
-
initCodecOptions();
-
- // processThreads are always running, passing available data from decoder to encoder and encoder to output
- initCoders();
- outputThread = new Thread(this::outputProcess);
-
- inputFormatter = getFormatter(inCodec, width, height);
- outputFormatter = getFormatter(outCodec, outWidth, outHeight);
+ initCodecs();
+ initFormatters();
+ initPipeline();
imgOut.setData(new DataBlockCompressed());
}
catch (IllegalArgumentException e)
{
- reportError("Unsupported codec" + inCodecParam.getData().getStringValue() + ". Must be one of " + Arrays.toString(CodecEnum.values()));
+ reportError("Unsupported codec " + inCodecParam.getData().getStringValue() + ". Must be one of " + Arrays.toString(CodecEnum.values()), e);
}
super.init();
@@ -356,65 +344,59 @@ public void init() throws ProcessException
* Creates maps of options for the encoder/decoder, including framerate, pixel format, bitrate, and image size.
*/
private void initCodecOptions() {
- decOptions = new HashMap<>();
- encOptions = new HashMap<>();
+ var decOptionBuilder = new CodecOptions.Builder();
+ var encOptionBuilder = new CodecOptions.Builder();
//DataComponent temp;
int fps = safeGetCountVal(inputFps);
- if (fps > 0) {
- decOptions.put("fps", fps);
- encOptions.put("fps", fps);
- }
-
- encOptions.put("pix_fmt", AV_PIX_FMT_YUV420P);
- decOptions.put("pix_fmt", AV_PIX_FMT_YUV420P);
-
int bitrate = safeGetCountVal(inputBitrate);
- if (bitrate > 0) {
- decOptions.put("bit_rate", bitrate);
- encOptions.put("bit_rate", bitrate); // Just assuming input br is the same as out, could this change?
- }
-
width = safeGetCountVal(inputWidth);
if (width <= 0) {
- width = imgIn.getComponent("row").getComponentCount();
+ try {
+ width = imgIn.getComponent("row").getComponentCount();
+ } catch (Exception e) {
+ width = 1920;
+ logger.warn("Input width not specified, using default: 1920", e);
+ }
}
- decOptions.put("width", width);
height = safeGetCountVal(inputHeight);
if (height <= 0) {
- height = imgIn.getComponentCount();
+ try {
+ height = imgIn.getComponentCount();
+ } catch (Exception e) {
+ height = 1080;
+ logger.warn("Input height not specified, using default: 1080", e);
+ }
}
- decOptions.put("height", height);
outHeight = safeGetCountVal(outputHeight);
if (outHeight <= 0) {
try {
outHeight = imgOut.getComponentCount();
- } catch (Exception ignored) {
- outHeight = 0;
+ } catch (Exception e) {
+ outHeight = height;
+ logger.warn("Output height not specified, using input height", e);
}
}
- if (outHeight > 0) {
- encOptions.put("height", outHeight);
- } else {
- encOptions.put("height", height);
- }
outWidth = safeGetCountVal(outputWidth);
if (outWidth <= 0) {
try {
outWidth = imgIn.getComponent("row").getComponentCount();
- } catch (Exception ignored) {
- outWidth = 0;
+ } catch (Exception e) {
+ outWidth = width;
+ logger.warn("Output width not specified, using input width", e);
}
}
- if (outWidth > 0) {
- encOptions.put("width", outWidth);
- } else {
- encOptions.put("width", width);
- }
+
+ decOptions = decOptionBuilder.setFps(fps).setBitRate(bitrate)
+ .setWidth(width).setHeight(height).presetUltraFast().tuneZeroLatency()
+ .setComplianceUnofficial().build();
+ encOptions = encOptionBuilder.setFps(fps).setBitRate(bitrate)
+ .setWidth(outWidth).setHeight(outHeight).presetUltraFast().tuneZeroLatency()
+ .setComplianceUnofficial().build();
}
/**
@@ -426,25 +408,31 @@ private void initCodecOptions() {
* @return Formatter object.
* @throws ProcessException Thrown when width and height are not provided for an uncompressed format.
*/
- private AVByteFormatter getFormatter(CodecEnum codec, @Nullable Integer width, @Nullable Integer height) throws ProcessException {
+ private AVByteFormatter getFormatter(CodecInfo codec, @Nullable Integer width, @Nullable Integer height) throws ProcessException {
try {
+ if (isUncompressed(codec))
+ return new FrameFormatter(width, height, codec.pixelFmt.ffmpegId);
+ else
+ return new PacketFormatter();
+ /*
return switch (codec) {
case RGB -> new RgbFormatter(width, height);
case YUV -> new YuvFormatter(width, height);
default -> new PacketFormatter();
};
+ */
} catch (NullPointerException e) {
- reportError("Raw formatter for " + codec + " requires non-null width and height.", e);
+ reportError("Formatter for " + codec + " requires non-null width and height.", e);
}
return null;
}
/**
* @param codec Video codec or uncompressed format.
- * @return Is the codec {@link CodecEnum#RGB} or {@link CodecEnum#YUV}?
+ * @return Is the codec {@link FullCodecEnum#RAWVIDEO}?
*/
- private boolean isUncompressed(CodecEnum codec) {
- return codec == CodecEnum.RGB || codec == CodecEnum.YUV;
+ private boolean isUncompressed(CodecInfo codec) {
+ return codec.codec == FullCodecEnum.RAWVIDEO;
}
/**
@@ -480,12 +468,15 @@ private void publishFrameData(byte[] frameData) {
((DataBlockCompressed) imgOut.getData()).setUnderlyingObject(frameData.clone());
// also copy frame timestamp
- double ts;
+ double ts = System.currentTimeMillis() / 1000d;
+ /*
if (inputTimeStamp != null && inputTimeStamp.getData() != null) {
ts = inputTimeStamp.getData().getDoubleValue();
} else {
ts = System.currentTimeMillis();
}
+
+ */
outputTimeStamp.getData().setDoubleValue(ts);
try {
//logger.debug("Publishing");
@@ -503,42 +494,29 @@ public void execute() throws ProcessException
logger.warn("Input image is null");
return;
}
+
// Start the threads if not already started
- if (!isRunning.get()) {
- startProcessThreads();
+ if (!isInit.get()) {
+ init();
}
- inputPackets.add(
- inputFormatter.convertInput(
- ((DataBlockCompressed)imgIn.getData()).getUnderlyingObject().clone()
- ));
+ if (!isVideoProcChainReady()) {
+ logger.warn("Video processor not ready");
+ return;
+ }
+ videoProcs.get(0).submitInputPacket(
+ inputFormatter.convertInput(((DataBlockCompressed)imgIn.getData()).getUnderlyingObject().clone())
+ );
}
- /**
- * Runs inside a separate thread. Receives any {@link AVPacket}s or {@link AVFrame}s from the last {@link Coder}
- * in the {@link FFMpegTranscoder#videoProcs} queue, converts the struct to bytes, and publishes the data.
- * @see FFMpegTranscoder#publishFrameData(byte[])
- */
- private void outputProcess() {
- while (doRun.get() && !Thread.currentThread().isInterrupted()) {
- while (outputPackets == null || outputPackets.isEmpty()) {
- if (Thread.currentThread().isInterrupted()) {
- return;
- }
- Thread.onSpinWait();
- }
- if (outputPackets != null && !outputPackets.isEmpty()) {
- for (var packet : outputPackets) {
- if (Thread.currentThread().isInterrupted()) {
- return;
- }
- publishFrameData(outputFormatter.convertOutput(outputPackets.poll()));
- //frameData = null;
- //packet = null;
- }
+ private boolean isVideoProcChainReady() {
+ for (Codec proc : videoProcs) {
+ if (!proc.isReady()) {
+ return false;
}
}
+ return true;
}
@Override
@@ -553,24 +531,11 @@ public void dispose()
{
super.dispose();
- doRun.set(false);
+ isInit.set(false);
if (videoProcs != null) {
- for (Thread t : videoProcs) {
- t.interrupt();
- try {
- t.join();
- } catch (InterruptedException e) {
- logger.error("Error waiting for process thread {} to finish", t.getName());
- }
- }
- }
- if (outputThread != null) {
- outputThread.interrupt();
- try {
- outputThread.join();
- } catch (InterruptedException e) {
- logger.error("Error waiting for process thread {} to finish", outputThread.getName());
+ for (Codec proc : videoProcs) {
+ proc.close();
}
}
}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderConfig.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderConfig.java
index b14c3bc5c..365093129 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderConfig.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderConfig.java
@@ -6,17 +6,23 @@
public class FFmpegTranscoderConfig extends FFmpegProcessConfig {
@DisplayInfo.Required
- @DisplayInfo(label="Input Codec")
+ @DisplayInfo(label="Input Format")
public CodecEnum inCodec = CodecEnum.H264;
- @DisplayInfo(label="Automatically Detect Input Codec", desc="If enabled, process will attempt to determine the input codec."
- + " If a codec could not be determined from the input data, it will fall back to the selected Input Codec.")
+ @DisplayInfo(label="Input Format Override")
+ public String inCodecOverride = "";
+
+ @DisplayInfo(label="Automatically Detect Input Format", desc="If enabled, process will attempt to determine the input format."
+ + " If a codec could not be determined from the input data, it will fall back to the selected Input Format.")
public boolean detectInput = false;
@DisplayInfo.Required
- @DisplayInfo(label="Output Codec")
+ @DisplayInfo(label="Output Format")
public CodecEnum outCodec = CodecEnum.H264;
+ @DisplayInfo(label="Output Format Override")
+ public String outCodecOverride = "";
+
@DisplayInfo(label="Output Width")
public Integer outputWidth = null;
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderProcess.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderProcess.java
index a6fe55895..c6d0073a1 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderProcess.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/FFmpegTranscoderProcess.java
@@ -19,10 +19,18 @@ public FFmpegTranscoderProcess() {
@Override
public void initExcProcess(IProcessExec executable) {
DataBlock inCodec = new DataBlockString(1);
- inCodec.setStringValue(((FFmpegTranscoderConfig)config).inCodec.toString());
+ if (!((FFmpegTranscoderConfig)config).inCodecOverride.isBlank()) {
+ inCodec.setStringValue(((FFmpegTranscoderConfig)config).inCodecOverride);
+ } else {
+ inCodec.setStringValue(((FFmpegTranscoderConfig) config).inCodec.toString());
+ }
DataBlock outCodec = new DataBlockString(1);
- outCodec.setStringValue(((FFmpegTranscoderConfig)config).outCodec.toString());
+ if (!((FFmpegTranscoderConfig)config).outCodecOverride.isBlank()) {
+ outCodec.setStringValue(((FFmpegTranscoderConfig)config).outCodecOverride);
+ } else {
+ outCodec.setStringValue(((FFmpegTranscoderConfig) config).outCodec.toString());
+ }
DataBlockInt outWidthBlock = new DataBlockInt(1);
if (((FFmpegTranscoderConfig) config).outputWidth != null) {
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/README.md b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/README.md
index 7ccf4a2db..fd89f33ab 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/README.md
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/README.md
@@ -2,7 +2,8 @@
## Overview
-This module provides a process module that can decode and/or encode video.
+FFmpeg video decode/encode/transcode module.
+Input and output may be compressed or uncompressed video.
## Configuration
@@ -24,17 +25,185 @@ When added to an OpenSensorHub node, the process has the following configuration
- **Video Source:**
A module with video output. Once the transcoder starts, video from this source module
will be decoded/encoded and outputted from the transcoder.
- - **Input Codec:** (Optional)
- The codec used for decoding the incoming video data. If incoming video is uncompressed,
+ - **Input Format:** (Optional)
+ The format used for decoding the incoming video data. If incoming video is uncompressed,
select either RGB or YUV.
- - **Output Codec:**
- The codec used for encoding the outgoing video data. If outgoing video should be uncompressed,
+ - **Input Format Override:** (Optional)
+ Manually specify any input codec or pixel format, allowing for any format not available in the short Input Codec list.
+ Only required if the format is not in the Input Format list. Otherwise, leave blank.
+ A full list of codecs and pixel formats is available at the end of this file.
+ - **Output Format:**
+ The format used for encoding the outgoing video data. If outgoing video should be uncompressed,
select either RGB or YUV.
+ - **Output Format Override:** (Optional)
+ Manually specify any output codec or pixel format, allowing for any format not available in the short Output Codec list.
+ Only required if the format is not in the Output Format list. Otherwise, leave blank.
+ A full list of codecs and pixel formats is available at the end of this file.
- **Output Width:** (Optional)
The width of the output video frame. Leave this empty to avoid scaling the video frame size.
- **Output Height:** (Optional)
The height of the output video frame. Leave this empty to avoid scaling the video frame size.
- **Auto Start:**
If checked, automatically start this sensor when the OpenSensorHub node is launched.
- - **Automatically Detect Input Codec:**
- If checked, automatically determine the input video codec based on the input's encoding information.
\ No newline at end of file
+ - **Automatically Detect Input Format:**
+ If checked, automatically determine the input video format based on the input's encoding information.
+
+## All Supported Formats
+This list contains all the supported codecs and pixel formats. To use a format, copy it into one of the format
+override config fields.
+
+| Codecs (Compressed Video) | Pixel Formats (Uncompressed Video) |
+|---------------------------|------------------------------------|
+| H264 | YUV420P |
+| HEVC | YUV422P |
+| MJPEG | YUV444P |
+| VP8 | YUV410P |
+| VP9 | YUV411P |
+| MPEG2 | YUV440P |
+| MPEG4 | YUVJ420P |
+| AV1 | YUVJ422P |
+| THEORA | YUVJ444P |
+| MPEG1VIDEO | YUVJ440P |
+| WMV1 | YUV420P9BE |
+| WMV2 | YUV420P9LE |
+| WMV3 | YUV420P10BE |
+| VC1 | YUV420P10LE |
+| FLV1 | YUV420P12BE |
+| FLASHSV | YUV420P12LE |
+| FLASHSV2 | YUV420P14BE |
+| RV10 | YUV420P14LE |
+| RV20 | YUV420P16BE |
+| RV30 | YUV420P16LE |
+| RV40 | YUV422P9BE |
+| CINEPAK | YUV422P9LE |
+| INDEO2 | YUV422P10BE |
+| INDEO3 | YUV422P10LE |
+| INDEO4 | YUV422P12BE |
+| INDEO5 | YUV422P12LE |
+| MSMPEG4V1 | YUV422P14BE |
+| MSMPEG4V2 | YUV422P14LE |
+| MSMPEG4V3 | YUV422P16BE |
+| H261 | YUV422P16LE |
+| H263 | YUV444P9BE |
+| H263I | YUV444P9LE |
+| H263P | YUV444P10BE |
+| SNOW | YUV444P10LE |
+| SVQ1 | YUV444P12BE |
+| SVQ3 | YUV444P12LE |
+| DVVIDEO | YUV444P14BE |
+| HUFFYUV | YUV444P14LE |
+| FFVHUFF | YUV444P16BE |
+| FFV1 | YUV444P16LE |
+| ASV1 | YUYV422 |
+| ASV2 | UYVY422 |
+| VCR1 | YVYU422 |
+| CLJR | UYYVYY411 |
+| MDEC | NV12 |
+| ROQ | NV21 |
+| INTERPLAY_VIDEO | NV16 |
+| XAN_WC3 | NV20LE |
+| XAN_WC4 | NV20BE |
+| RPZA | NV24 |
+| SMC | NV42 |
+| GIF | RGB24 |
+| PNG | BGR24 |
+| PPM | ARGB |
+| PBM | RGBA |
+| PGM | ABGR |
+| PAM | BGRA |
+| BMP | RGB0 |
+| TIFF | BGR0 |
+| SGI | RGB8 |
+| ALIAS_PIX | BGR8 |
+| DPX | RGB4 |
+| EXR | BGR4 |
+| WEBP | RGB4_BYTE |
+| DIRAC | BGR4_BYTE |
+| DNXHD | RGB48BE |
+| PRORES | RGB48LE |
+| JPEG2000 | RGBA64BE |
+| JPEGLS | RGBA64LE |
+| HAP | BGR48BE |
+| | BGR48LE |
+| | BGRA64BE |
+| | BGRA64LE |
+| | RGB565BE |
+| | RGB565LE |
+| | RGB555BE |
+| | RGB555LE |
+| | RGB444BE |
+| | RGB444LE |
+| | BGR565BE |
+| | BGR565LE |
+| | BGR555BE |
+| | BGR555LE |
+| | BGR444BE |
+| | BGR444LE |
+| | GRAY8 |
+| | GRAY8A |
+| | GRAY9BE |
+| | GRAY9LE |
+| | GRAY10BE |
+| | GRAY10LE |
+| | GRAY12BE |
+| | GRAY12LE |
+| | GRAY14BE |
+| | GRAY14LE |
+| | GRAY16BE |
+| | GRAY16LE |
+| | MONOWHITE |
+| | MONOBLACK |
+| | YA8 |
+| | YA16BE |
+| | YA16LE |
+| | YUVA420P |
+| | YUVA422P |
+| | YUVA444P |
+| | YUVA420P9BE |
+| | YUVA420P9LE |
+| | YUVA422P9BE |
+| | YUVA422P9LE |
+| | YUVA444P9BE |
+| | YUVA444P9LE |
+| | YUVA420P10BE |
+| | YUVA420P10LE |
+| | YUVA422P10BE |
+| | YUVA422P10LE |
+| | YUVA444P10BE |
+| | YUVA444P10LE |
+| | YUVA420P16BE |
+| | YUVA420P16LE |
+| | YUVA422P16BE |
+| | YUVA422P16LE |
+| | YUVA444P16BE |
+| | YUVA444P16LE |
+| | BAYER_BGGR8 |
+| | BAYER_RGGB8 |
+| | BAYER_GBRG8 |
+| | BAYER_GRBG8 |
+| | BAYER_BGGR16LE |
+| | BAYER_BGGR16BE |
+| | BAYER_RGGB16LE |
+| | BAYER_RGGB16BE |
+| | BAYER_GBRG16LE |
+| | BAYER_GBRG16BE |
+| | BAYER_GRBG16LE |
+| | BAYER_GRBG16BE |
+| | GRAYF32BE |
+| | GRAYF32LE |
+| | PAL8 |
+| | XYZ12LE |
+| | XYZ12BE |
+| | XTOP |
+| | P010LE |
+| | P010BE |
+| | P016LE |
+| | P016BE |
+| | P210BE |
+| | P210LE |
+| | P410BE |
+| | P410LE |
+| | P216BE |
+| | P216LE |
+| | P416BE |
+| | P416LE |
\ No newline at end of file
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Codec.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Codec.java
new file mode 100644
index 000000000..f1e01f8a7
--- /dev/null
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Codec.java
@@ -0,0 +1,387 @@
+package org.sensorhub.impl.process.video.transcoder.coders;
+
+import static org.bytedeco.ffmpeg.global.avutil.*;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.bytedeco.ffmpeg.global.avcodec.*;
+
+import org.bytedeco.ffmpeg.avcodec.AVCodec;
+import org.bytedeco.ffmpeg.avcodec.AVCodecContext;
+import org.bytedeco.ffmpeg.avcodec.AVPacket;
+import org.bytedeco.ffmpeg.avutil.AVFrame;
+import org.bytedeco.javacpp.BytePointer;
+import org.bytedeco.javacpp.IntPointer;
+import org.bytedeco.javacpp.Pointer;
+import org.bytedeco.javacpp.PointerPointer;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecInfo;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecOptions;
+import org.sensorhub.impl.process.video.transcoder.helpers.FullPixelEnum;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Consumes AVPackets/AVFrames and produces AVPackets/AVFrames. Asynchronously submit packets to the codec with
+ * {@link #submitInputPacket(Pointer input)}. Listen for output by submitting a {@link CodecCallback} (typically a lambda)
+ * to {@link #registerCallback(CodecCallback callback)}. If transcoding, use callbacks to connect the output of one
+ * {@link #Codec} instance to the {@link #submitInputPacket(Pointer input)} method of the next.
+ *
+ *
AVPacket/AVFrame ownership is transferred to the consumer/listener. The consumer shall be responsible for
+ * deallocating AVPackets/AVFrames by passing them to the {@link #deallocateOutputPacket(Pointer packet)} method of the
+ * codec which most recently produced them. ONLY deallocate AVPackets/AVFrames that are not in use by a
+ * {@link #Codec}. {@link #submitInputPacket(Pointer input)} will automatically deallocate its input.
+ *
+ *
Codec/Callback Sample:
+ *
+ * {@code
+ * // Packet pipe between decoder, swscaler, encoder
+ * for (int i = 0; i < codecList.size() - 1; i++) {
+ * var nextCodec = codecList.get(i + 1);
+ * codecList.get(i).registerCallback(packet -> {
+ * nextCodec.submitInputPacket(packet);
+ * });
+ * }
+ *
+ * // Output
+ * var finalProc = codecList.get(codecList.size() - 1);
+ * finalProc.registerCallback(packet -> {
+ * try {
+ * publishFrameData(outputFormatter.convertOutput(packet));
+ * } finally {
+ * finalProc.deallocateOutputPacket(packet);
+ * }
+ * });
+ * }
+ *
+ * @param Input class (either AVFrame or AVPacket)
+ * @param Output class (either AVFrame or AVPacket)
+ */
+public abstract class Codec implements AutoCloseable {
+
+
+ public interface CodecCallback {
+ // The recipient does not need to deallocate the output; this is done automatically
+ public abstract void onPacket(O packet);
+ }
+
+ protected static final Logger logger = LoggerFactory.getLogger(Codec.class);
+
+ private static int codecCount = 0;
+ private final int codecNum = codecCount++;
+ private final ExecutorService executor = Executors.newSingleThreadExecutor(
+ r -> new Thread(r, "ffmpeg-codec-thread-" + codecNum));
+ private final ExecutorService outputExecutor = Executors.newSingleThreadExecutor(r -> new Thread(r, "ffmpeg-codec-output-thread-" + codecNum));
+ private final Map, ExecutorService> callbackMap = new HashMap<>();
+
+ CodecInfo inputFormat;
+ CodecInfo outputFormat;
+ protected AVCodecContext codec_ctx;
+ protected AVCodec codec;
+ protected final Queue inQueue = new ConcurrentLinkedQueue<>();
+ protected final Queue outQueue = new ConcurrentLinkedQueue<>();
+ protected final int maxQueue = 10;
+ private final AtomicBoolean isProcessing = new AtomicBoolean(false);
+ private final AtomicBoolean isAlive = new AtomicBoolean(false); // Set false to indicate packets should no longer be accepted
+ final Object contextLock = new Object();
+ Class inputClass;
+ Class outputClass;
+ CodecOptions options;
+ AtomicBoolean isNotifying = new AtomicBoolean(false);
+ byte[] headers;
+
+ public Codec(CodecInfo inFormatInfo, CodecInfo outFormatInfo, Class inputClass, Class outputClass, CodecOptions options) {
+ super();
+
+ if ((inputClass != AVPacket.class && inputClass != AVFrame.class)
+ || (outputClass != AVPacket.class && outputClass != AVFrame.class)) {
+ throw new IllegalArgumentException("Input and output classes must be either AVPacket or AVFrame");
+ }
+
+ this.inputFormat = inFormatInfo.clone();
+ this.inputClass = inputClass;
+ this.outputFormat = outFormatInfo.clone();
+ this.outputClass = outputClass;
+ this.options = options;
+ }
+
+ public FullPixelEnum init() {
+ initContext();
+ initOptions();
+ var pixFmt = openContext();
+ isAlive.set(true);
+ return pixFmt;
+ }
+
+ private void submitTask(Runnable task) {
+ if (!executor.isShutdown()) {
+ executor.submit(() -> {
+ try {
+ task.run();
+ } catch (Throwable t) {
+ // Prevent silent thread replacement — log and propagate
+ // only after codec state is consistent
+ logger.error("Fatal error in codec runner thread", t);
+ //throw t; // will kill this task but not spawn a new thread silently
+ }
+ });
+ }
+ }
+
+ protected void addOutPacket(O outPacket) {
+ while (outQueue.size() >= maxQueue) {
+ var packet = outQueue.poll();
+ deallocateOutputPacket(packet);
+ }
+ outQueue.add(outPacket);
+ }
+
+ protected void addInPacket(I inPacket) {
+ while (inQueue.size() >= maxQueue) {
+ var packet = inQueue.poll();
+ deallocateInputPacket(packet);
+ }
+ inQueue.add(inPacket);
+ }
+
+ public boolean isReady() {
+ return isAlive.get();
+ }
+
+ public Class getOutputClass() {
+ return outputClass;
+ }
+
+ public Class getInputClass() {
+ return inputClass;
+ }
+
+ protected abstract void initContext();
+
+ protected FullPixelEnum openContext() {
+ int ret;
+ FullPixelEnum pixelFmt;
+
+ codec_ctx.flags(codec_ctx.flags() | AV_CODEC_FLAG_GLOBAL_HEADER);
+
+ if ((ret = avcodec_open2(codec_ctx, codec, (PointerPointer>) null)) < 0) {
+ logFFmpeg(ret);
+ throw new IllegalStateException("Error opening codec " + codec.name().getString());
+ }
+
+ headers = getExtradata(codec_ctx);
+
+ try {
+ var desc = av_pix_fmt_desc_get(codec_ctx.pix_fmt());
+ pixelFmt = FullPixelEnum.valueOf(desc.name().getString().toUpperCase());
+ inputFormat.pixelFmt = pixelFmt;
+ outputFormat.pixelFmt = pixelFmt;
+ desc.deallocate();
+ } catch (Exception e) {
+ pixelFmt = null;
+ logger.warn("Could not determine codec info for " + codec.name().getString(), e);
+ }
+ return pixelFmt;
+ }
+
+ protected static byte[] getExtradata(AVCodecContext codecCtx) {
+ if (codecCtx.extradata() == null || codecCtx.extradata_size() == 0)
+ return null;
+
+ byte[] data = new byte[codecCtx.extradata_size()];
+ codecCtx.extradata().get(data);
+ return data;
+ }
+
+ protected static void logFFmpeg(int retCode) {
+ BytePointer buf = new BytePointer(AV_ERROR_MAX_STRING_SIZE);
+ av_strerror(retCode, buf, buf.capacity());
+ logger.warn("FFmpeg returned error code {}: {}", retCode, buf.getString());
+ }
+
+ protected static void setCodecPixFmt(AVCodecContext codec_ctx, FullPixelEnum desiredFmt) {
+ String codecString = codec_ctx.codec().name().getString();
+ PointerPointer pixelFmts = new PointerPointer<>(1);
+
+ avcodec_get_supported_config(codec_ctx, null, AV_CODEC_CONFIG_PIX_FORMAT, 0, pixelFmts, (IntPointer) null);
+ IntPointer fmts = pixelFmts.get(IntPointer.class, 0);
+ // If null, all formats are supported
+ if (fmts == null || fmts.isNull()) {
+ if (desiredFmt == FullPixelEnum.NONE) {
+ codec_ctx.pix_fmt(AV_PIX_FMT_YUV420P);
+ } else {
+ codec_ctx.pix_fmt(desiredFmt.ffmpegId);
+ }
+ }
+ else {
+ boolean found = false;
+ for (int i = 0; fmts.get(i) != AV_PIX_FMT_NONE; i++) {
+ if (fmts.get(i) == desiredFmt.ffmpegId) {
+ found = true;
+ codec_ctx.pix_fmt(fmts.get(i));
+ break;
+ }
+ }
+ if (!found) {
+ logger.warn("Preferred pixel format for codec {} could not be found", codecString);
+ IntPointer loss = new IntPointer(1);
+ int fmt = avcodec_find_best_pix_fmt_of_list(fmts, desiredFmt.ffmpegId, 0, loss);
+ codec_ctx.pix_fmt(fmt);
+ }
+ }
+ pixelFmts.deallocate();
+ }
+
+ /**
+ * Set certain options in the codec context.
+ * Context must be allocated first using {@link #initContext()}.
+ */
+ protected void initOptions() {
+
+ if (options.bitRate() > 0) {
+ codec_ctx.bit_rate(options.bitRate() * 1000);
+ } else {
+ codec_ctx.bit_rate(0);
+ }
+
+ codec_ctx.width(options.width());
+ codec_ctx.height(options.height());
+
+ codec_ctx.time_base(av_make_q(1, 90000));
+
+ if (options.fps() > 0) {
+ codec_ctx.framerate(av_make_q(options.fps(), 1));
+ } else {
+ codec_ctx.framerate(av_make_q(25, 1));
+ }
+
+ if (inputFormat.codec.ffmpegId == AV_CODEC_ID_H264) {
+ // OpenH264 only supports Baseline (66) and Main (77)
+ codec_ctx.profile(AV_PROFILE_H264_MAIN);
+
+ // Enable frame skip so bitrate control works correctly,
+ // or it falls back to quality mode and ignores the bitrate setting
+ av_opt_set(codec_ctx.priv_data(), "skip_frames", "1", 0);
+
+ // OpenH264 uses slice_mode instead of preset
+ av_opt_set(codec_ctx.priv_data(), "slice_mode", "auto", 0);
+ }
+
+ //av_opt_set(codec_ctx.priv_data(), "preset", options.preset(), 0);
+ //av_opt_set(codec_ctx.priv_data(), "tune", options.tune(), 0);
+ codec_ctx.strict_std_compliance(options.compliance()); // Needed so that yuvj420p works (used for mjpeg, must be set to unofficial)
+ }
+
+ public abstract void deallocateInputPacket(I packet);
+ public abstract void deallocateOutputPacket(O packet);
+ protected abstract O cloneOutput(O packet);
+ protected abstract void processInputPacket(I inputPacket);
+
+
+ public void submitInputPacket(I inputPacket) {
+ synchronized (contextLock) {
+ if (!isAlive.get()) {
+ return;
+ }
+ if (inputPacket != null) {
+ addInPacket(inputPacket);
+ }
+ if (isProcessing.compareAndSet(false, true)) {
+ submitTask(() -> {
+ // Process the input
+ while (!inQueue.isEmpty()) {
+ I inPacket = inQueue.poll();
+ processInputPacket(inPacket);
+ deallocateInputPacket(inPacket);
+ }
+
+ if (!outQueue.isEmpty() && isNotifying.compareAndSet(false, true)) {
+ outputExecutor.submit(() -> {
+ while (!outQueue.isEmpty()) {
+ O outputPacket = outQueue.poll();
+ notifyCallbacks(outputPacket);
+ }
+ isNotifying.set(false);
+ });
+ }
+
+ isProcessing.set(false);
+ });
+ }
+ }
+ }
+
+ private void notifyCallbacks(O outputPacket) {
+ for (var entry: callbackMap.entrySet()) {
+ var clonedOutputPacket = cloneOutput(outputPacket);
+ entry.getValue().submit(() -> {
+ entry.getKey().onPacket(clonedOutputPacket);
+ });
+ }
+ deallocateOutputPacket(outputPacket);
+ }
+
+ public void registerCallback(CodecCallback callback) {
+ if (!callbackMap.containsKey(callback)) {
+ callbackMap.put(callback, Executors.newSingleThreadExecutor(r -> new Thread(r, "ffmpeg-codec-" + codecNum + "-callback-thread")));
+ } else {
+ logger.warn("This callback was already registered for codec " + codecNum);
+ }
+ }
+
+ public void unregisterCallback(CodecCallback callback) {
+ callbackMap.remove(callback);
+ }
+
+ public void unregisterAllCallbacks() {
+ callbackMap.clear();
+ }
+
+ @Override
+ public void close() {
+ synchronized (contextLock) {
+ if (isAlive.compareAndSet(true, false)) {
+
+ // Submit cleanup *before* shutdown so it is the last task to run
+ executor.submit(this::cleanup);
+ executor.shutdownNow();
+ try {
+ executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+ } catch (InterruptedException ignored) {
+ logger.warn("Interrupted while waiting for ffmpeg thread to finish");
+ Thread.currentThread().interrupt();
+ }
+
+ outputExecutor.shutdownNow();
+ try {
+ outputExecutor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
+ } catch (InterruptedException ignored) {
+ logger.warn("Interrupted while waiting for ffmpeg thread to finish");
+ Thread.currentThread().interrupt();
+ }
+
+ unregisterAllCallbacks();
+ }
+ }
+ }
+
+ private void cleanup() {
+ if (codec_ctx != null) {
+ avcodec_free_context(codec_ctx);
+ }
+ codec_ctx = null;
+ codec = null;
+
+ unregisterAllCallbacks();
+
+ for (var packet : outQueue) {
+ deallocateOutputPacket(packet);
+ }
+ }
+}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Coder.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Coder.java
deleted file mode 100644
index 5cc663d6d..000000000
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Coder.java
+++ /dev/null
@@ -1,191 +0,0 @@
-package org.sensorhub.impl.process.video.transcoder.coders;
-
-import static org.bytedeco.ffmpeg.global.avutil.*;
-
-import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.Queue;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import static org.bytedeco.ffmpeg.global.avcodec.*;
-
-import org.bytedeco.ffmpeg.avcodec.AVCodecContext;
-import org.bytedeco.ffmpeg.avcodec.AVPacket;
-import org.bytedeco.ffmpeg.avutil.AVFrame;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public abstract class Coder extends Thread {
-
- protected static final Logger logger = LoggerFactory.getLogger(Coder.class);
-
- int codecId;
- private static final int MAX_QUEUE_SIZE = 500;
- protected AVCodecContext codec_ctx;
- protected Queue inPackets; // ONLY allow the main loop to poll
- protected final Queue outPackets; // ONLY allow the main loop to add
- protected I inPacket;
- protected O outPacket;
- //volatile boolean isProcessing = false; // Set to true at the start of the loop, false at the end
- volatile boolean isGettingPackets = false;
- final Object waitingObj = new Object();
- Class inputClass;
- Class outputClass;
- HashMap options;
-
- public AtomicBoolean doRun = new AtomicBoolean(true);
-
- public Coder(int codecId, Class inputClass, Class outputClass, HashMap options) {
- super();
-
- assert inputClass == AVPacket.class || inputClass == AVFrame.class;
- assert outputClass == AVPacket.class || outputClass == AVFrame.class;
- assert options != null;
-
- this.codecId = codecId;
- this.inPackets = new ArrayDeque<>();
- this.outPackets = new ArrayDeque<>();
- this.inputClass = inputClass;
- this.outputClass = outputClass;
- this.options = options;
- }
-
- /**
- * Gets reference to output queue. Typically used as the input queue for another {@link Coder}.
- * @return Output queue containing either {@link AVPacket}s or {@link AVFrame}s.
- */
- public Queue getOutQueue() {
- return outPackets;
- }
-
- /**
- * Sets input queue.
- * @param inPackets Typically the output queue of another {@link Coder}, containing either
- * {@link AVPacket}s or {@link AVFrame}s.
- */
- public void setInQueue(Queue > inPackets) {
- this.inPackets = (Queue) inPackets;
- }
-
- // Safety net queue purging.
- // Sometimes, queues can grow very large (like when a thread hangs up or is paused in the debugger).
- // Can recover, so we don't want memory issues from too many items in the queues.
- private void queuePurge() {
-
- synchronized (outPackets) {
- if (outPackets.size() > MAX_QUEUE_SIZE) { logger.warn("Output queue is larger than max ({} > {}). Purging queue.", outPackets.size(), MAX_QUEUE_SIZE); }
- while (outPackets.size() > MAX_QUEUE_SIZE) {
- deallocateOutputPacket(outPackets.poll());
- }
- }
- }
-
- protected abstract void deallocateInputPacket(I packet);
-
- protected abstract void deallocateOutputPacket(O packet);
-
- protected abstract void deallocateOutQueue();
-
- // To be implemented by subclasses, encoder/decoder
- protected abstract void initContext();
-
- // Take data from input queue and send to encoder/decoder
- protected abstract void sendInPacket();
-
- // Take data from encoder/decoder and send to output queue
- protected abstract void receiveOutPacket();
-
- // Allocate packets/frames
- protected abstract void allocatePackets();
-
- // Deallocate packets/frames
- protected abstract void deallocatePackets();
-
- /**
- * Set certain options in the codec context.
- * @param codec_ctx Codec context. Context must be allocated first.
- */
- protected void initOptions(AVCodecContext codec_ctx) {
-
- codec_ctx.time_base(av_make_q(1, options.getOrDefault("fps", 30)));
-
- if (options.containsKey("bit_rate")) {
- codec_ctx.bit_rate(options.get("bit_rate") * 1000);
- } else {
- //codec_ctx.bit_rate(150*1000);
- }
-
- codec_ctx.width(options.getOrDefault("width", 1920));
-
- codec_ctx.height(options.getOrDefault("height", 1080));
-
- if (options.containsKey("pix_fmt")) {
- codec_ctx.pix_fmt(options.get("pix_fmt"));
- } else {
- if (codecId == AV_CODEC_ID_MJPEG) {
- codec_ctx.pix_fmt(AV_PIX_FMT_YUVJ420P);
- } else {
- codec_ctx.pix_fmt(AV_PIX_FMT_YUV420P);
- }
- }
-
- av_opt_set(codec_ctx.priv_data(), "preset", "ultrafast", 0);
- av_opt_set(codec_ctx.priv_data(), "tune", "zerolatency", 0);
- codec_ctx.strict_std_compliance(FF_COMPLIANCE_UNOFFICIAL); // Needed so that yuvj420p works (used for mjpeg)
- }
-
- @Override
- public void run() {
- initContext();
- inPacket = (I)(new Object());
- outPacket = (O)(new Object());
-
- allocatePackets();
-
- // init FFMPEG logging
- av_log_set_level(logger.isDebugEnabled() ? AV_LOG_INFO : AV_LOG_FATAL);
- //av_log_set_level(AV_LOG_DEBUG);
-
- // Actual start of run
- // Codec should be initialized by now
- doRun.set(true);
-
- while (doRun.get() && !Thread.currentThread().isInterrupted()) {
- while (inPackets == null || inPackets.isEmpty()) {
- if (!doRun.get() || Thread.currentThread().isInterrupted()) { break; }
- Thread.onSpinWait();
- }
- while (inPackets != null && !inPackets.isEmpty()) {
- if (!doRun.get() || Thread.currentThread().isInterrupted()) { break; }
-
- queuePurge();
-
- //logger.debug("Queue size: {}", inPackets.size());
- // Get data from in queue
- // Send data to encoder/decoder
- //logger.debug("{}: Sending packet", this.getClass().getName());
- sendInPacket();
-
- // Receive data from encoder/decoder
- // Add data to out queue
- //logger.debug("{}: Receiving packet", this.getClass().getName());
- receiveOutPacket();
-
- //isProcessing = false;
- // Wake a thread waiting for packets.
-
- }
- }
-
- // End of coding
- synchronized (waitingObj) {
- waitingObj.notifyAll();
- }
-
- deallocatePackets();
- if (codec_ctx != null) {
- avcodec_free_context(codec_ctx);
- }
- codec_ctx = null;
- }
-}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Decoder.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Decoder.java
index fc5709954..f8340644c 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Decoder.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Decoder.java
@@ -1,102 +1,74 @@
package org.sensorhub.impl.process.video.transcoder.coders;
-import org.bytedeco.ffmpeg.avcodec.AVCodec;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avutil.AVFrame;
-import org.bytedeco.javacpp.PointerPointer;
-
-import java.util.HashMap;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecInfo;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecOptions;
import static org.bytedeco.ffmpeg.global.avcodec.*;
import static org.bytedeco.ffmpeg.global.avutil.*;
-public class Decoder extends Coder {
- public Decoder(int codecId, HashMap options) {
- super(codecId, AVPacket.class, AVFrame.class, options);
+public class Decoder extends Codec {
+
+ public Decoder(CodecInfo inFormatInfo, CodecInfo outFormatInfo, CodecOptions options) {
+ super(inFormatInfo, outFormatInfo, AVPacket.class, AVFrame.class, options);
}
@Override
protected void initContext() {
- AVCodec codec = avcodec_find_decoder(codecId);
- codec_ctx = avcodec_alloc_context3(codec);
-
- initOptions(codec_ctx);
-
- if (avcodec_open2(codec_ctx, codec, (PointerPointer>)null) < 0) {
- throw new IllegalStateException("Error initializing " + codec.name().getString() + " decoder");
+ synchronized (contextLock) {
+ codec = avcodec_find_decoder(inputFormat.codec.ffmpegId);
+ codec_ctx = avcodec_alloc_context3(codec);
+ codec_ctx.codec_id(inputFormat.codec.ffmpegId);
+ //setCodecPixFmt(codec_ctx, outputFormat.pixelFmt);
+ //codec_ctx.pix_fmt(outputFormat.pixelFmt().ffmpegId);
}
}
- // Get compressed packet, send to decoder
- @Override
- protected void sendInPacket() {
- inPacket = inPackets.poll();
- //logger.debug("decode send:");
- //logger.debug(" data[0]: {}", inPacket.data());
- //logger.debug("Sent frame to encoder");
- avcodec_send_packet(codec_ctx, av_packet_clone(inPacket));
- //av_packet_free(inPacket);
- }
-
- // Receive uncompressed frame from decoder
@Override
- protected void receiveOutPacket() {
- synchronized (outPackets) {
- while (avcodec_receive_frame(codec_ctx, outPacket) >= 0) {
- //av_packet_free(inPacket);
- outPackets.add(av_frame_clone(outPacket));
- //av_frame_free(outPacket);
- //logger.debug("Decode Packet added");
- }
+ public void deallocateInputPacket(AVPacket packet) {
+ if (packet != null) {
+ av_packet_free(packet);
}
}
@Override
- protected void deallocateInputPacket(AVPacket packet) {
- av_packet_free(packet);
- packet = null;
+ public void deallocateOutputPacket(AVFrame packet) {
+ if (packet != null) {
+ av_frame_free(packet);
+ }
}
@Override
- protected void deallocateOutputPacket(AVFrame packet) {
- av_frame_free(packet);
- packet = null;
+ protected AVFrame cloneOutput(AVFrame packet) {
+ if (packet != null) {
+ return av_frame_clone(packet);
+ } else {
+ return null;
+ }
}
@Override
- protected void deallocateOutQueue() {
- outPackets.clear();
- }
+ protected synchronized void processInputPacket(AVPacket inputPacket) {
+ if (inputPacket != null && !inputPacket.isNull()) {
+ int ret;
+ if ((ret = avcodec_send_packet(codec_ctx, inputPacket)) < 0) {
+ //logger.warn("Error sending packet to decoder");
+ //logFFmpeg(ret);
+ //avcodec_flush_buffers(codec_ctx);
+ return;
+ }
- @Override
- protected void allocatePackets() {
- inPacket = new AVPacket();
- inPacket = av_packet_alloc();
- av_init_packet(inPacket);
- outPacket = new AVFrame();
- outPacket = av_frame_alloc();
- }
+ AVFrame outputPacket = av_frame_alloc();
- @Override
- protected void deallocatePackets() {
- if (inPacket != null) {
- av_packet_free(inPacket);
- }
- if (outPacket != null) {
- av_frame_free(outPacket);
- }
- if (outPackets != null) {
- for (AVFrame frame : outPackets) {
- av_frame_free(frame);
+ while (avcodec_receive_frame(codec_ctx, outputPacket) >= 0) {
+ if (!outputPacket.isNull()) {
+ addOutPacket(outputPacket);
+ }
+ outputPacket = av_frame_alloc();
}
- outPackets.clear();
- }
- if (inPackets != null) {
- for (AVPacket packet : inPackets) {
- av_packet_free(packet);
- }
- inPackets.clear();
+ av_frame_free(outputPacket);
}
}
}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Encoder.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Encoder.java
index da47fcdfc..df1666f9c 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Encoder.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/Encoder.java
@@ -1,111 +1,139 @@
package org.sensorhub.impl.process.video.transcoder.coders;
-import org.bytedeco.ffmpeg.avcodec.AVCodec;
import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avutil.AVFrame;
import org.bytedeco.javacpp.BytePointer;
-import org.bytedeco.javacpp.PointerPointer;
-
-import java.util.HashMap;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecInfo;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecOptions;
+import org.sensorhub.impl.process.video.transcoder.helpers.FullCodecEnum;
+import org.sensorhub.impl.process.video.transcoder.helpers.FullPixelEnum;
import static org.bytedeco.ffmpeg.global.avcodec.*;
import static org.bytedeco.ffmpeg.global.avutil.*;
-public class Encoder extends Coder {
- long pts = 0;
+public class Encoder extends Codec {
+
+ volatile long pts = 0;
+ long timebaseNum, timebaseDen, framerateNum, framerateDen;
+ final int GOP_SIZE = 10;
+ volatile long frameSinceGop = 0;
+ final Object timestampLock = new Object();
- public Encoder(int codecId, HashMap options) {
- super(codecId, AVFrame.class, AVPacket.class, options);
+ public Encoder(CodecInfo inFormatInfo, CodecInfo outFormatInfo, CodecOptions options) {
+ super(inFormatInfo, outFormatInfo, AVFrame.class, AVPacket.class, options);
}
+ // Override so we can get the timebase
@Override
- protected void deallocateOutQueue() {
- outPackets.clear();
+ public FullPixelEnum init() {
+ var pixfmt = super.init();
+ timebaseNum = codec_ctx.time_base().num();
+ timebaseDen = codec_ctx.time_base().den();
+ framerateNum = codec_ctx.framerate().num();
+ framerateDen = codec_ctx.framerate().den();
+ //GOP_SIZE = codec_ctx.gop_size();
+ return pixfmt;
}
@Override
protected void initContext() {
- pts = 0;
- AVCodec codec = avcodec_find_encoder(codecId);
- codec_ctx = avcodec_alloc_context3(codec);
-
- initOptions(codec_ctx);
- int ret = avcodec_open2(codec_ctx, codec, (PointerPointer>)null);
- if (ret < 0) {
- BytePointer errorBuffer = new BytePointer(AV_ERROR_MAX_STRING_SIZE);
-
- av_strerror(ret, errorBuffer, AV_ERROR_MAX_STRING_SIZE);
- logger.debug("Receive Error: {}", errorBuffer.getString());
- throw new IllegalStateException("Error initializing " + codec.name().getString() + " encoder");
- }
- }
+ synchronized (contextLock) {
+ pts = 0;
- @Override
- protected void sendInPacket() {
- inPacket = inPackets.poll();
- //pts++;
- //inPacket.pts(pts);
+ // For H264, prefer x264 over OpenH264 — better option compatibility
+ if (outputFormat.codec == FullCodecEnum.H264) {
+ codec = avcodec_find_encoder_by_name("libx264");
+ }
- avcodec_send_frame(codec_ctx, av_frame_clone(inPacket));
- //av_frame_free(inPacket);
+ // Fall back to the default encoder for this codec ID
+ if (codec == null || codec.isNull()) {
+ codec = avcodec_find_encoder(outputFormat.codec.ffmpegId);
+ }
- }
+ if (codec == null || codec.isNull()) {
+ throw new IllegalStateException("Could not find encoder for: " + outputFormat.codec);
+ }
- @Override
- protected void receiveOutPacket() {
- /*
- synchronized (outPackets) {
+ codec_ctx = avcodec_alloc_context3(codec);
- }
+ if (codec_ctx == null || codec_ctx.isNull()) {
+ throw new IllegalStateException("Could not allocate encoder context for: " + codec.name().getString());
+ }
+
+ codec_ctx.gop_size(GOP_SIZE);
+ codec_ctx.max_b_frames(0);
- */
- int ret = 0;
- while (( ret = avcodec_receive_packet(codec_ctx, outPacket) ) >= 0) {
- outPackets.add(av_packet_clone(outPacket));
+ setCodecPixFmt(codec_ctx, inputFormat.pixelFmt);
+
+ logger.debug("Using encoder: {}", codec.name().getString());
}
- //BytePointer errorBuffer = new BytePointer(AV_ERROR_MAX_STRING_SIZE);
- //av_strerror(ret, errorBuffer, AV_ERROR_MAX_STRING_SIZE);
- //logger.debug("Receive Error: {}", errorBuffer.getString());
}
@Override
- protected void deallocateInputPacket(AVFrame packet) {
- av_frame_free(packet);
- packet = null;
+ public void deallocateInputPacket(AVFrame packet) {
+ if (packet != null) {
+ av_frame_free(packet);
+ }
}
@Override
- protected void deallocateOutputPacket(AVPacket packet) {
- av_packet_free(packet);
- packet = null;
+ public void deallocateOutputPacket(AVPacket packet) {
+ if (packet != null) {
+ av_packet_free(packet);
+ }
}
@Override
- protected void allocatePackets() {
- inPacket = av_frame_alloc();
- outPacket = av_packet_alloc();
- av_init_packet(outPacket);
+ protected AVPacket cloneOutput(AVPacket packet) {
+ if (packet != null) {
+ return av_packet_clone(packet);
+ } else {
+ return null;
+ }
}
@Override
- protected void deallocatePackets() {
- if (inPacket != null) {
- av_frame_free(inPacket);
- }
- if (outPacket != null) {
- av_packet_free(outPacket);
- }
- if (outPackets != null) {
- for (AVPacket packet : outPackets) {
- av_packet_free(packet);
+ protected synchronized void processInputPacket(AVFrame inputPacket) {
+ if (inputPacket != null && !inputPacket.isNull()) {
+ int ret;
+ if (inputPacket.format() != codec_ctx.pix_fmt()) {
+ throw new IllegalArgumentException("AVFrame pixel format: " + FullPixelEnum.fromId(inputPacket.format())
+ + " incompatible with codec pixel format: " + FullPixelEnum.fromId(codec_ctx.pix_fmt()));
}
- outPackets.clear();
- }
- if (inPackets != null) {
- for (AVFrame frame : inPackets) {
- av_frame_free(frame);
+
+ if (frameSinceGop++ >= GOP_SIZE) {
+ frameSinceGop = 0;
+ inputPacket.pict_type(AV_PICTURE_TYPE_I);
+ inputPacket.flags(inputPacket.flags() | AVFrame.AV_FRAME_FLAG_KEY);
+ }
+
+ if ((ret = avcodec_send_frame(codec_ctx, inputPacket)) < 0) {
+ //logger.warn("Error sending packet to encoder");
+ //logFFmpeg(ret);
+ //avcodec_flush_buffers(codec_ctx);
+ return;
}
- inPackets.clear();
+
+ AVPacket outputPacket = av_packet_alloc();
+
+ while (avcodec_receive_packet(codec_ctx, outputPacket) >= 0) {
+ if (!outputPacket.isNull()) {
+ // Add headers if necessary/available
+ if (((outputPacket.flags() & AV_PKT_FLAG_KEY) != 0) && headers != null) {
+ int originalSize = outputPacket.size();
+ byte[] original = new byte[originalSize];
+ outputPacket.data().capacity(originalSize).position(0).get(original);
+ av_grow_packet(outputPacket, headers.length);
+ BytePointer dst = outputPacket.data().capacity((long) headers.length + originalSize);
+ dst.position(0).put(headers);
+ dst.position(headers.length).put(original);
+ }
+ addOutPacket(outputPacket);
+ }
+ outputPacket = av_packet_alloc();
+ }
+
+ av_packet_free(outputPacket);
}
}
}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/SwScaler.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/SwScaler.java
index 3bf44c047..a87beac4c 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/SwScaler.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/coders/SwScaler.java
@@ -1,113 +1,90 @@
package org.sensorhub.impl.process.video.transcoder.coders;
-import org.bytedeco.ffmpeg.avcodec.AVCodec;
-import org.bytedeco.ffmpeg.avcodec.AVPacket;
import org.bytedeco.ffmpeg.avutil.AVFrame;
import org.bytedeco.ffmpeg.swscale.SwsContext;
-import org.bytedeco.javacpp.BytePointer;
import org.bytedeco.javacpp.DoublePointer;
-import org.bytedeco.javacpp.PointerPointer;
+import org.sensorhub.impl.process.video.transcoder.helpers.CodecInfo;
+import org.sensorhub.impl.process.video.transcoder.helpers.FullPixelEnum;
-import java.util.HashMap;
-
-import static org.bytedeco.ffmpeg.global.avcodec.*;
import static org.bytedeco.ffmpeg.global.avutil.*;
import static org.bytedeco.ffmpeg.global.swscale.*;
-public class SwScaler extends Coder {
+public class SwScaler extends Codec {
long pts = 0;
SwsContext swsContext;
- final int inPixFmt, outPixFmt, inWidth, inHeight, outWidth, outHeight;
+ final int inWidth, inHeight, outWidth, outHeight;
- public SwScaler(int inPixFmt, int outPixFmt, int inWidth, int inHeight, int outWidth, int outHeight) {
- super(0, AVFrame.class, AVFrame.class, new HashMap());
- this.inPixFmt = inPixFmt;
- this.outPixFmt = outPixFmt;
+ public SwScaler(CodecInfo inputFormat, CodecInfo outputFormat, int inWidth, int inHeight, int outWidth, int outHeight) {
+ super(inputFormat, outputFormat, AVFrame.class, AVFrame.class, null);
this.inWidth = inWidth;
this.inHeight = inHeight;
this.outWidth = outWidth;
this.outHeight = outHeight;
}
- @Override
- protected void deallocateOutQueue() {
- outPackets.clear();
- }
-
@Override
protected void initContext() {
- pts = 0;
- swsContext = sws_getContext(inWidth, inHeight, inPixFmt,
- outWidth, outHeight, outPixFmt,
+ swsContext = sws_getContext(inWidth, inHeight, inputFormat.pixelFmt.ffmpegId,
+ outWidth, outHeight, outputFormat.pixelFmt.ffmpegId,
SWS_BICUBIC, null, null, (DoublePointer) null);
-
}
@Override
- protected void sendInPacket() {
- inPacket = inPackets.poll();
- //pts++;
- //inPacket.pts(pts);
- sws_scale_frame(swsContext, outPacket, inPacket);
- //avcodec_send_frame(codec_ctx, av_frame_clone(inPacket));
- //av_frame_free(inPacket);
+ protected void initOptions() {} // no options for swscaler
- }
+ @Override
+ protected FullPixelEnum openContext() {
+ return outputFormat.pixelFmt;
+ } // no codec to open
@Override
- protected void receiveOutPacket() {
- // We already have outPacket at this point (one method needed for scaling rather than two)
- // Just use this to add the output to the out queue and make the buffers writable
- outPackets.add(av_frame_clone(outPacket));
- av_frame_make_writable(outPacket);
+ public void deallocateInputPacket(AVFrame packet) {
+ if (packet != null) {
+ av_frame_free(packet);
+ }
}
@Override
- protected void deallocateInputPacket(AVFrame packet) {
- av_frame_free(packet);
- packet = null;
+ public void deallocateOutputPacket(AVFrame packet) {
+ if (packet != null) {
+ av_frame_free(packet);
+ }
}
@Override
- protected void deallocateOutputPacket(AVFrame packet) {
- av_frame_free(packet);
- packet = null;
+ protected AVFrame cloneOutput(AVFrame packet) {
+ if (packet != null) {
+ return av_frame_clone(packet);
+ } else {
+ return null;
+ }
}
@Override
- protected void allocatePackets() {
- inPacket = av_frame_alloc();
+ protected synchronized void processInputPacket(AVFrame inputPacket) {
+ if (inputPacket == null) { return; }
+
+ // FFmpeg can be weird when it comes to decoder pixfmt. May need to change
+ // on the fly (usually first frame).
+ if (inputPacket.format() != inputFormat.pixelFmt.ffmpegId) {
+ inputFormat.pixelFmt = FullPixelEnum.fromId(inputPacket.format());
+ initContext();
+ }
- outPacket = av_frame_alloc();
- outPacket.format(outPixFmt);
- outPacket.width(outWidth);
- outPacket.height(outHeight);
- av_image_alloc(outPacket.data(), outPacket.linesize(),
- outWidth, outHeight, outPixFmt, 1);
+ AVFrame outputPacket = av_frame_alloc();
+ outputPacket.format(outputFormat.pixelFmt.ffmpegId);
+ outputPacket.width(outWidth);
+ outputPacket.height(outHeight);
+ av_frame_get_buffer(outputPacket, 0);
+ sws_scale_frame(swsContext, outputPacket, inputPacket);
+ addOutPacket(outputPacket);
}
@Override
- protected void deallocatePackets() {
- if (swsContext != null) {
+ public void close() {
+ synchronized (contextLock) {
+ super.close();
sws_freeContext(swsContext);
}
- if (inPacket != null) {
- av_frame_free(inPacket);
- }
- if (outPacket != null) {
- av_frame_free(outPacket);
- }
- if (outPackets != null) {
- for (AVFrame frame : outPackets) {
- av_frame_free(frame);
- }
- outPackets.clear();
- }
- if (inPackets != null) {
- for (AVFrame frame : inPackets) {
- av_frame_free(frame);
- }
- inPackets.clear();
- }
}
}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/AVByteFormatter.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/AVByteFormatter.java
index 60227176d..c9b71f651 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/AVByteFormatter.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/AVByteFormatter.java
@@ -5,7 +5,7 @@
import org.bytedeco.javacpp.Pointer;
import org.sensorhub.impl.process.video.transcoder.CodecEnum;
-public abstract class AVByteFormatter {
+public interface AVByteFormatter {
public abstract T convertInput(byte[] inputData);
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/FrameFormatter.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/FrameFormatter.java
new file mode 100644
index 000000000..dd2e30b31
--- /dev/null
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/FrameFormatter.java
@@ -0,0 +1,231 @@
+package org.sensorhub.impl.process.video.transcoder.formatters;
+
+import org.bytedeco.ffmpeg.avutil.*;
+import org.bytedeco.ffmpeg.global.avutil;
+import org.bytedeco.javacpp.BytePointer;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.bytedeco.ffmpeg.global.avutil.*;
+
+// This class is intended to replace both RgbFormatter and YuvFormatter as a generic solution
+public class FrameFormatter implements AVByteFormatter {
+
+ private final int width;
+ private final int height;
+ private final int pixFmt;
+ private final AVPixFmtDescriptor desc;
+ private final int planeCount;
+ private final int[] planeWidths;
+ private final int[] planeHeights;
+ private final int[] planeSizes;
+ private final int totalSize;
+ boolean isPlanar;
+
+ public FrameFormatter(int width, int height, int pixFmt) {
+ this.width = width;
+ this.height = height;
+ this.pixFmt = pixFmt;
+ this.desc = avutil.av_pix_fmt_desc_get(pixFmt);
+ isPlanar = (desc.flags() & AV_PIX_FMT_FLAG_PLANAR) != 0;
+
+ if (isPlanar) {
+ this.planeCount = countPlanes(desc);
+
+ planeWidths = new int[planeCount];
+ planeHeights = new int[planeCount];
+ planeSizes = new int[planeCount];
+
+ calculatePlaneSizes(desc, width, height, planeWidths, planeHeights, planeSizes);
+ } else {
+ planeCount = 1;
+ planeWidths = new int[1];
+ planeHeights = new int[1];
+ planeSizes = new int[1];
+ planeWidths[0] = width;
+ planeHeights[0] = height;
+ planeSizes[0] = width * height * ((av_get_bits_per_pixel(desc) + 7) / 8);
+ }
+ totalSize = calcByteSize(planeSizes);
+ }
+
+ public int getPixFmt() {
+ return pixFmt;
+ }
+
+ public int getTotalSize() {
+ return totalSize;
+ }
+
+ private static int calcByteSize(int[] planeSizes) {
+ int sum = 0;
+ for (int s : planeSizes) sum += s;
+ return sum;
+ }
+
+ @Override
+ public AVFrame convertInput(byte[] inputData) {
+ AVFrame newFrame = generateFrame();
+
+ if (isPlanar)
+ setFrameDataPlanar(newFrame, inputData);
+ else
+ setFrameDataPacked(newFrame, inputData);
+
+ return newFrame;
+ }
+
+ private void setFrameDataPlanar(AVFrame newFrame, byte[] inputData) {
+ int offset = 0;
+
+ for (int p = 0; p < planeCount; p++) {
+ int w = planeWidths[p];
+ int h = planeHeights[p];
+ int stride = newFrame.linesize(p);
+
+ BytePointer dst = newFrame.data(p)
+ .capacity((long) stride * h)
+ .position(0);
+
+ int bitsPerComponent = desc.comp(p < 1 ? 0 : 1).depth();
+ int rowBytes = w * ((bitsPerComponent + 7) / 8);
+
+ for (int y = 0; y < h; y++) {
+ dst.position((long) y * stride);
+ dst.put(inputData, offset + y * rowBytes, rowBytes);
+ }
+
+ offset += planeSizes[p];
+ }
+ }
+
+ private void setFrameDataPacked(AVFrame newFrame, byte[] inputData) {
+ int linesize = newFrame.linesize(0);
+ BytePointer dst = newFrame.data(0)
+ .capacity((long) linesize * height)
+ .position(0);
+
+ int bytesPerPixel = (av_get_bits_per_pixel(desc) + 7) / 8;
+
+ int rowBytes = width * bytesPerPixel;
+
+ int offset = 0;
+
+ for (int y = 0; y < height; y++) {
+ dst.position((long) y * linesize)
+ .put(inputData, offset, rowBytes);
+
+ offset += rowBytes;
+ }
+ }
+
+ private AVFrame generateFrame() {
+ AVFrame newFrame = av_frame_alloc();
+ newFrame.format(pixFmt);
+ newFrame.width(width);
+ newFrame.height(height);
+ int ret = av_frame_get_buffer(newFrame, 0);
+ if (ret < 0) {
+ av_frame_free(newFrame);
+ throw new IllegalStateException("Could not allocate AVFrame buffer, ffmpeg error: " + ret);
+ }
+ return newFrame;
+ }
+
+ @Override
+ public byte[] convertOutput(AVFrame outputFrame) {
+ if (outputFrame.format() != pixFmt) {
+ throw new IllegalArgumentException(
+ "Unexpected frame pixel format: " + outputFrame.format() + ", expected " + pixFmt);
+ }
+
+ byte[] out = new byte[totalSize];
+
+ if (isPlanar) {
+ getFrameDataPlanar(outputFrame, out);
+ } else {
+ getFrameDataPacked(outputFrame, out);
+ }
+ return out;
+ }
+
+ private void getFrameDataPlanar(AVFrame outputFrame, byte[] out) {
+ int offset = 0;
+ for (int plane = 0; plane < planeCount; plane++) {
+ int w = planeWidth(plane);
+ int h = planeHeight(plane);
+ int srcStride = outputFrame.linesize(plane);
+
+ BytePointer src = outputFrame.data(plane)
+ .capacity((long) srcStride * h)
+ .position(0);
+
+ int bitsPerComponent = desc.comp(plane < 1 ? 0 : 1).depth();
+ int rowBytes = w * ((bitsPerComponent + 7) / 8);
+
+ for (int y = 0; y < h; y++) {
+ src.position((long) y * srcStride);
+ src.get(out, offset, rowBytes);
+ offset += rowBytes;
+ }
+ }
+ }
+
+ private void getFrameDataPacked(AVFrame outputFrame, byte[] out) {
+ int bytesPerPixel = (av_get_bits_per_pixel(desc) + 7) / 8;
+ int rowBytes = width * bytesPerPixel;
+ int linesize = outputFrame.linesize(0);
+
+ BytePointer src = outputFrame.data(0)
+ .capacity((long) linesize * height);
+
+ int offset = 0;
+ for (int y = 0; y < height; y++) {
+ src.position((long) y * linesize)
+ .get(out, offset, rowBytes);
+ offset += rowBytes;
+ }
+ }
+
+ private int planeWidth(int plane) {
+ if (plane == 0)
+ return width;
+ else
+ return width >> desc.log2_chroma_w();
+ }
+
+ private int planeHeight(int plane) {
+ if (plane == 0)
+ return height;
+ else
+ return height >> desc.log2_chroma_h();
+ }
+
+ // Helper functions for PLANAR formats. Untested with packed formats.
+ private static int countPlanes(AVPixFmtDescriptor desc) {
+ Set planes = new HashSet<>();
+ for (int i = 0; i < desc.nb_components(); i++) {
+ planes.add(desc.comp(i).plane());
+ }
+ return planes.size();
+ }
+
+ private static void calculatePlaneSizes(AVPixFmtDescriptor desc, int width, int height, int[] planeWidths, int[] planeHeights, int[] planeSizes) {
+ for (int c = 0; c < desc.nb_components(); c++) {
+ AVComponentDescriptor comp = desc.comp(c);
+ int p = comp.plane();
+
+ int shiftW = (p == 0) ? 0 : desc.log2_chroma_w();
+ int shiftH = (p == 0) ? 0 : desc.log2_chroma_h();
+
+ planeWidths[p] = width >> shiftW;
+ planeHeights[p] = height >> shiftH;
+ }
+
+ for (int p = 0; p < planeSizes.length; p++) {
+ int bytesPerSample = (desc.comp(p == 0 ? 0 : 1).depth() + 7) / 8;
+ planeSizes[p] = planeWidths[p] * planeHeights[p] * bytesPerSample;
+ }
+ }
+}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/PacketFormatter.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/PacketFormatter.java
index b3b09149b..46699ff2c 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/PacketFormatter.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/PacketFormatter.java
@@ -10,7 +10,7 @@
import static org.bytedeco.ffmpeg.global.avutil.*;
import static org.bytedeco.ffmpeg.global.swscale.*;
-public class PacketFormatter extends AVByteFormatter {
+public class PacketFormatter implements AVByteFormatter {
/**
* Converts an array of bytes from compressed video into an {@link AVPacket}.
@@ -22,7 +22,8 @@ public AVPacket convertInput(byte[] inputData) {
AVPacket newPacket = av_packet_alloc();
av_new_packet(newPacket, inputData.length);
newPacket.data().position(0);
- newPacket.data().put(inputData.clone(), 0, inputData.length);
+ newPacket.data().limit(inputData.length);
+ newPacket.data().put(inputData);
return newPacket;
}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/RgbFormatter.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/RgbFormatter.java
index 2e0eb7fd8..5d5766c83 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/RgbFormatter.java
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/formatters/RgbFormatter.java
@@ -7,7 +7,7 @@
import static org.bytedeco.ffmpeg.global.avutil.*;
import static org.bytedeco.ffmpeg.global.swscale.*;
-public class RgbFormatter extends AVByteFormatter {
+public class RgbFormatter implements AVByteFormatter {
protected final int width, height, size;
protected final int pixFormat;
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/CodecInfo.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/CodecInfo.java
new file mode 100644
index 000000000..f4cc69bf1
--- /dev/null
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/CodecInfo.java
@@ -0,0 +1,37 @@
+package org.sensorhub.impl.process.video.transcoder.helpers;
+
+public class CodecInfo implements Cloneable {
+ public FullCodecEnum codec;
+ public FullPixelEnum pixelFmt;
+
+ public static CodecInfo newCodecInfoFromName(String name) {
+ FullCodecEnum codec;
+ FullPixelEnum pixel;
+ try {
+ codec = Enum.valueOf(FullCodecEnum.class, name);
+ pixel = FullPixelEnum.YUVJ420P;
+ } catch (Exception e) {
+ codec = FullCodecEnum.RAWVIDEO;
+ pixel = Enum.valueOf(FullPixelEnum.class, name);
+ }
+
+ return new CodecInfo(codec, pixel);
+ }
+
+ public CodecInfo(FullCodecEnum codec, FullPixelEnum pixelFmt) {
+ this.codec = codec;
+ this.pixelFmt = pixelFmt;
+ }
+
+ @Override
+ public CodecInfo clone() {
+ try {
+ CodecInfo clone = (CodecInfo) super.clone();
+ clone.codec = codec;
+ clone.pixelFmt = pixelFmt;
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/CodecOptions.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/CodecOptions.java
new file mode 100644
index 000000000..64e0771b3
--- /dev/null
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/CodecOptions.java
@@ -0,0 +1,79 @@
+package org.sensorhub.impl.process.video.transcoder.helpers;
+import static org.bytedeco.ffmpeg.global.avcodec.*;
+
+public record CodecOptions(int fps, int bitRate, int width, int height, int compliance, String preset, String tune) {
+ public CodecOptions(int fps, int bitRate, int width, int height, int compliance, String preset, String tune) {
+ this.fps = Math.max(fps, 1);
+ this.bitRate = bitRate;
+ this.width = Math.max(width, 1);
+ this.height = Math.max(height, 1);
+ this.compliance = compliance;
+ this.preset = preset;
+ this.tune = tune;
+ }
+
+ public static class Builder {
+ private int fps = 30;
+ private int bitRate = -1;
+ private int width = 1920;
+ private int height = 1080;
+ private int compliance = FF_COMPLIANCE_UNOFFICIAL;
+ private String preset = null;
+ private String tune = null;
+
+ public CodecOptions build() {
+ return new CodecOptions(fps, bitRate, width, height, compliance, preset, tune);
+ }
+
+ public Builder setFps(int fps) {
+ this.fps = fps;
+ return this;
+ }
+
+ public Builder setBitRate(int bitRate) {
+ this.bitRate = bitRate;
+ return this;
+ }
+
+ public Builder setWidth(int width) {
+ this.width = width;
+ return this;
+ }
+
+ public Builder setHeight(int height) {
+ this.height = height;
+ return this;
+ }
+
+ public Builder setCompliance(int compliance) {
+ this.compliance = compliance;
+ return this;
+ }
+
+ // Convenience, unofficial provides widest range of pixel formats
+ public Builder setComplianceUnofficial() {
+ this.compliance = FF_COMPLIANCE_UNOFFICIAL;
+ return this;
+ }
+
+ public Builder setPreset(String preset) {
+ this.preset = preset;
+ return this;
+ }
+
+ public Builder presetUltraFast() {
+ this.preset = "ultrafast";
+ return this;
+ }
+
+ public Builder setTune(String tune) {
+ this.tune = tune;
+ return this;
+ }
+
+ public Builder tuneZeroLatency() {
+ this.tune = "zerolatency";
+ return this;
+ }
+ }
+}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/FullCodecEnum.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/FullCodecEnum.java
new file mode 100644
index 000000000..1db8625f6
--- /dev/null
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/FullCodecEnum.java
@@ -0,0 +1,100 @@
+package org.sensorhub.impl.process.video.transcoder.helpers;
+
+import org.bytedeco.ffmpeg.global.avcodec;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum FullCodecEnum {
+ H264(avcodec.AV_CODEC_ID_H264),
+ HEVC(avcodec.AV_CODEC_ID_HEVC),
+ MJPEG(avcodec.AV_CODEC_ID_MJPEG),
+ VP8(avcodec.AV_CODEC_ID_VP8),
+ VP9(avcodec.AV_CODEC_ID_VP9),
+ MPEG2(avcodec.AV_CODEC_ID_MPEG2VIDEO),
+ MPEG4(avcodec.AV_CODEC_ID_MPEG4),
+ AV1(avcodec.AV_CODEC_ID_AV1),
+ THEORA(avcodec.AV_CODEC_ID_THEORA),
+ MPEG1VIDEO(avcodec.AV_CODEC_ID_MPEG1VIDEO),
+ WMV1(avcodec.AV_CODEC_ID_WMV1),
+ WMV2(avcodec.AV_CODEC_ID_WMV2),
+ WMV3(avcodec.AV_CODEC_ID_WMV3),
+ VC1(avcodec.AV_CODEC_ID_VC1),
+ FLV1(avcodec.AV_CODEC_ID_FLV1),
+ FLASHSV(avcodec.AV_CODEC_ID_FLASHSV),
+ FLASHSV2(avcodec.AV_CODEC_ID_FLASHSV2),
+ RV10(avcodec.AV_CODEC_ID_RV10),
+ RV20(avcodec.AV_CODEC_ID_RV20),
+ RV30(avcodec.AV_CODEC_ID_RV30),
+ RV40(avcodec.AV_CODEC_ID_RV40),
+ CINEPAK(avcodec.AV_CODEC_ID_CINEPAK),
+ INDEO2(avcodec.AV_CODEC_ID_INDEO2),
+ INDEO3(avcodec.AV_CODEC_ID_INDEO3),
+ INDEO4(avcodec.AV_CODEC_ID_INDEO4),
+ INDEO5(avcodec.AV_CODEC_ID_INDEO5),
+ MSMPEG4V1(avcodec.AV_CODEC_ID_MSMPEG4V1),
+ MSMPEG4V2(avcodec.AV_CODEC_ID_MSMPEG4V2),
+ MSMPEG4V3(avcodec.AV_CODEC_ID_MSMPEG4V3),
+ H261(avcodec.AV_CODEC_ID_H261),
+ H263(avcodec.AV_CODEC_ID_H263),
+ H263I(avcodec.AV_CODEC_ID_H263I),
+ H263P(avcodec.AV_CODEC_ID_H263P),
+ SNOW(avcodec.AV_CODEC_ID_SNOW),
+ SVQ1(avcodec.AV_CODEC_ID_SVQ1),
+ SVQ3(avcodec.AV_CODEC_ID_SVQ3),
+ DVVIDEO(avcodec.AV_CODEC_ID_DVVIDEO),
+ HUFFYUV(avcodec.AV_CODEC_ID_HUFFYUV),
+ FFVHUFF(avcodec.AV_CODEC_ID_FFVHUFF),
+ FFV1(avcodec.AV_CODEC_ID_FFV1),
+ ASV1(avcodec.AV_CODEC_ID_ASV1),
+ ASV2(avcodec.AV_CODEC_ID_ASV2),
+ VCR1(avcodec.AV_CODEC_ID_VCR1),
+ CLJR(avcodec.AV_CODEC_ID_CLJR),
+ MDEC(avcodec.AV_CODEC_ID_MDEC),
+ ROQ(avcodec.AV_CODEC_ID_ROQ),
+ INTERPLAY_VIDEO(avcodec.AV_CODEC_ID_INTERPLAY_VIDEO),
+ XAN_WC3(avcodec.AV_CODEC_ID_XAN_WC3),
+ XAN_WC4(avcodec.AV_CODEC_ID_XAN_WC4),
+ RPZA(avcodec.AV_CODEC_ID_RPZA),
+ SMC(avcodec.AV_CODEC_ID_SMC),
+ GIF(avcodec.AV_CODEC_ID_GIF),
+ RAWVIDEO(avcodec.AV_CODEC_ID_RAWVIDEO),
+ PNG(avcodec.AV_CODEC_ID_PNG),
+ PPM(avcodec.AV_CODEC_ID_PPM),
+ PBM(avcodec.AV_CODEC_ID_PBM),
+ PGM(avcodec.AV_CODEC_ID_PGM),
+ PAM(avcodec.AV_CODEC_ID_PAM),
+ BMP(avcodec.AV_CODEC_ID_BMP),
+ TIFF(avcodec.AV_CODEC_ID_TIFF),
+ SGI(avcodec.AV_CODEC_ID_SGI),
+ ALIAS_PIX(avcodec.AV_CODEC_ID_ALIAS_PIX),
+ DPX(avcodec.AV_CODEC_ID_DPX),
+ EXR(avcodec.AV_CODEC_ID_EXR),
+ WEBP(avcodec.AV_CODEC_ID_WEBP),
+ DIRAC(avcodec.AV_CODEC_ID_DIRAC),
+ DNXHD(avcodec.AV_CODEC_ID_DNXHD),
+ PRORES(avcodec.AV_CODEC_ID_PRORES),
+ JPEG2000(avcodec.AV_CODEC_ID_JPEG2000),
+ JPEGLS(avcodec.AV_CODEC_ID_JPEGLS),
+ HAP(avcodec.AV_CODEC_ID_HAP);
+
+ public int ffmpegId;
+
+ private static final Map BY_ID;
+
+ static {
+ BY_ID = new HashMap<>();
+ for (FullCodecEnum fmt : values()) {
+ BY_ID.put(fmt.ffmpegId, fmt);
+ }
+ }
+
+ public static FullCodecEnum fromId(int ffmpegId) {
+ return BY_ID.getOrDefault(ffmpegId, RAWVIDEO);
+ }
+
+ FullCodecEnum(int ffmpegId)
+ {
+ this.ffmpegId = ffmpegId;
+ }
+}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/FullPixelEnum.java b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/FullPixelEnum.java
new file mode 100644
index 000000000..be7002944
--- /dev/null
+++ b/processing/sensorhub-process-ffmpeg/src/main/java/org/sensorhub/impl/process/video/transcoder/helpers/FullPixelEnum.java
@@ -0,0 +1,205 @@
+package org.sensorhub.impl.process.video.transcoder.helpers;
+
+import org.bytedeco.ffmpeg.global.avutil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public enum FullPixelEnum {
+
+ NONE(avutil.AV_PIX_FMT_NONE),
+
+ // --- Planar YUV ---
+ YUV420P(avutil.AV_PIX_FMT_YUV420P),
+ YUV422P(avutil.AV_PIX_FMT_YUV422P),
+ YUV444P(avutil.AV_PIX_FMT_YUV444P),
+ YUV410P(avutil.AV_PIX_FMT_YUV410P),
+ YUV411P(avutil.AV_PIX_FMT_YUV411P),
+ YUV440P(avutil.AV_PIX_FMT_YUV440P),
+ YUVJ420P(avutil.AV_PIX_FMT_YUVJ420P),
+ YUVJ422P(avutil.AV_PIX_FMT_YUVJ422P),
+ YUVJ444P(avutil.AV_PIX_FMT_YUVJ444P),
+ YUVJ440P(avutil.AV_PIX_FMT_YUVJ440P),
+ YUV420P9BE(avutil.AV_PIX_FMT_YUV420P9BE),
+ YUV420P9LE(avutil.AV_PIX_FMT_YUV420P9LE),
+ YUV420P10BE(avutil.AV_PIX_FMT_YUV420P10BE),
+ YUV420P10LE(avutil.AV_PIX_FMT_YUV420P10LE),
+ YUV420P12BE(avutil.AV_PIX_FMT_YUV420P12BE),
+ YUV420P12LE(avutil.AV_PIX_FMT_YUV420P12LE),
+ YUV420P14BE(avutil.AV_PIX_FMT_YUV420P14BE),
+ YUV420P14LE(avutil.AV_PIX_FMT_YUV420P14LE),
+ YUV420P16BE(avutil.AV_PIX_FMT_YUV420P16BE),
+ YUV420P16LE(avutil.AV_PIX_FMT_YUV420P16LE),
+ YUV422P9BE(avutil.AV_PIX_FMT_YUV422P9BE),
+ YUV422P9LE(avutil.AV_PIX_FMT_YUV422P9LE),
+ YUV422P10BE(avutil.AV_PIX_FMT_YUV422P10BE),
+ YUV422P10LE(avutil.AV_PIX_FMT_YUV422P10LE),
+ YUV422P12BE(avutil.AV_PIX_FMT_YUV422P12BE),
+ YUV422P12LE(avutil.AV_PIX_FMT_YUV422P12LE),
+ YUV422P14BE(avutil.AV_PIX_FMT_YUV422P14BE),
+ YUV422P14LE(avutil.AV_PIX_FMT_YUV422P14LE),
+ YUV422P16BE(avutil.AV_PIX_FMT_YUV422P16BE),
+ YUV422P16LE(avutil.AV_PIX_FMT_YUV422P16LE),
+ YUV444P9BE(avutil.AV_PIX_FMT_YUV444P9BE),
+ YUV444P9LE(avutil.AV_PIX_FMT_YUV444P9LE),
+ YUV444P10BE(avutil.AV_PIX_FMT_YUV444P10BE),
+ YUV444P10LE(avutil.AV_PIX_FMT_YUV444P10LE),
+ YUV444P12BE(avutil.AV_PIX_FMT_YUV444P12BE),
+ YUV444P12LE(avutil.AV_PIX_FMT_YUV444P12LE),
+ YUV444P14BE(avutil.AV_PIX_FMT_YUV444P14BE),
+ YUV444P14LE(avutil.AV_PIX_FMT_YUV444P14LE),
+ YUV444P16BE(avutil.AV_PIX_FMT_YUV444P16BE),
+ YUV444P16LE(avutil.AV_PIX_FMT_YUV444P16LE),
+
+ // --- Packed YUV ---
+ YUYV422(avutil.AV_PIX_FMT_YUYV422),
+ UYVY422(avutil.AV_PIX_FMT_UYVY422),
+ YVYU422(avutil.AV_PIX_FMT_YVYU422),
+ UYYVYY411(avutil.AV_PIX_FMT_UYYVYY411),
+ NV12(avutil.AV_PIX_FMT_NV12),
+ NV21(avutil.AV_PIX_FMT_NV21),
+ NV16(avutil.AV_PIX_FMT_NV16),
+ NV20LE(avutil.AV_PIX_FMT_NV20LE),
+ NV20BE(avutil.AV_PIX_FMT_NV20BE),
+ NV24(avutil.AV_PIX_FMT_NV24),
+ NV42(avutil.AV_PIX_FMT_NV42),
+
+ // --- RGB ---
+ RGB24(avutil.AV_PIX_FMT_RGB24),
+ BGR24(avutil.AV_PIX_FMT_BGR24),
+ ARGB(avutil.AV_PIX_FMT_ARGB),
+ RGBA(avutil.AV_PIX_FMT_RGBA),
+ ABGR(avutil.AV_PIX_FMT_ABGR),
+ BGRA(avutil.AV_PIX_FMT_BGRA),
+ RGB0(avutil.AV_PIX_FMT_RGB0),
+ BGR0(avutil.AV_PIX_FMT_BGR0),
+ RGB8(avutil.AV_PIX_FMT_RGB8),
+ BGR8(avutil.AV_PIX_FMT_BGR8),
+ RGB4(avutil.AV_PIX_FMT_RGB4),
+ BGR4(avutil.AV_PIX_FMT_BGR4),
+ RGB4_BYTE(avutil.AV_PIX_FMT_RGB4_BYTE),
+ BGR4_BYTE(avutil.AV_PIX_FMT_BGR4_BYTE),
+ RGB48BE(avutil.AV_PIX_FMT_RGB48BE),
+ RGB48LE(avutil.AV_PIX_FMT_RGB48LE),
+ RGBA64BE(avutil.AV_PIX_FMT_RGBA64BE),
+ RGBA64LE(avutil.AV_PIX_FMT_RGBA64LE),
+ BGR48BE(avutil.AV_PIX_FMT_BGR48BE),
+ BGR48LE(avutil.AV_PIX_FMT_BGR48LE),
+ BGRA64BE(avutil.AV_PIX_FMT_BGRA64BE),
+ BGRA64LE(avutil.AV_PIX_FMT_BGRA64LE),
+ RGB565BE(avutil.AV_PIX_FMT_RGB565BE),
+ RGB565LE(avutil.AV_PIX_FMT_RGB565LE),
+ RGB555BE(avutil.AV_PIX_FMT_RGB555BE),
+ RGB555LE(avutil.AV_PIX_FMT_RGB555LE),
+ RGB444BE(avutil.AV_PIX_FMT_RGB444BE),
+ RGB444LE(avutil.AV_PIX_FMT_RGB444LE),
+ BGR565BE(avutil.AV_PIX_FMT_BGR565BE),
+ BGR565LE(avutil.AV_PIX_FMT_BGR565LE),
+ BGR555BE(avutil.AV_PIX_FMT_BGR555BE),
+ BGR555LE(avutil.AV_PIX_FMT_BGR555LE),
+ BGR444BE(avutil.AV_PIX_FMT_BGR444BE),
+ BGR444LE(avutil.AV_PIX_FMT_BGR444LE),
+
+ // --- Grayscale ---
+ GRAY8(avutil.AV_PIX_FMT_GRAY8),
+ GRAY8A(avutil.AV_PIX_FMT_GRAY8A),
+ GRAY9BE(avutil.AV_PIX_FMT_GRAY9BE),
+ GRAY9LE(avutil.AV_PIX_FMT_GRAY9LE),
+ GRAY10BE(avutil.AV_PIX_FMT_GRAY10BE),
+ GRAY10LE(avutil.AV_PIX_FMT_GRAY10LE),
+ GRAY12BE(avutil.AV_PIX_FMT_GRAY12BE),
+ GRAY12LE(avutil.AV_PIX_FMT_GRAY12LE),
+ GRAY14BE(avutil.AV_PIX_FMT_GRAY14BE),
+ GRAY14LE(avutil.AV_PIX_FMT_GRAY14LE),
+ GRAY16BE(avutil.AV_PIX_FMT_GRAY16BE),
+ GRAY16LE(avutil.AV_PIX_FMT_GRAY16LE),
+ MONOWHITE(avutil.AV_PIX_FMT_MONOWHITE),
+ MONOBLACK(avutil.AV_PIX_FMT_MONOBLACK),
+
+ // --- YA ---
+ YA8(avutil.AV_PIX_FMT_YA8),
+ YA16BE(avutil.AV_PIX_FMT_YA16BE),
+ YA16LE(avutil.AV_PIX_FMT_YA16LE),
+
+ // --- YUVA ---
+ YUVA420P(avutil.AV_PIX_FMT_YUVA420P),
+ YUVA422P(avutil.AV_PIX_FMT_YUVA422P),
+ YUVA444P(avutil.AV_PIX_FMT_YUVA444P),
+ YUVA420P9BE(avutil.AV_PIX_FMT_YUVA420P9BE),
+ YUVA420P9LE(avutil.AV_PIX_FMT_YUVA420P9LE),
+ YUVA422P9BE(avutil.AV_PIX_FMT_YUVA422P9BE),
+ YUVA422P9LE(avutil.AV_PIX_FMT_YUVA422P9LE),
+ YUVA444P9BE(avutil.AV_PIX_FMT_YUVA444P9BE),
+ YUVA444P9LE(avutil.AV_PIX_FMT_YUVA444P9LE),
+ YUVA420P10BE(avutil.AV_PIX_FMT_YUVA420P10BE),
+ YUVA420P10LE(avutil.AV_PIX_FMT_YUVA420P10LE),
+ YUVA422P10BE(avutil.AV_PIX_FMT_YUVA422P10BE),
+ YUVA422P10LE(avutil.AV_PIX_FMT_YUVA422P10LE),
+ YUVA444P10BE(avutil.AV_PIX_FMT_YUVA444P10BE),
+ YUVA444P10LE(avutil.AV_PIX_FMT_YUVA444P10LE),
+ YUVA420P16BE(avutil.AV_PIX_FMT_YUVA420P16BE),
+ YUVA420P16LE(avutil.AV_PIX_FMT_YUVA420P16LE),
+ YUVA422P16BE(avutil.AV_PIX_FMT_YUVA422P16BE),
+ YUVA422P16LE(avutil.AV_PIX_FMT_YUVA422P16LE),
+ YUVA444P16BE(avutil.AV_PIX_FMT_YUVA444P16BE),
+ YUVA444P16LE(avutil.AV_PIX_FMT_YUVA444P16LE),
+
+ // --- Bayer ---
+ BAYER_BGGR8(avutil.AV_PIX_FMT_BAYER_BGGR8),
+ BAYER_RGGB8(avutil.AV_PIX_FMT_BAYER_RGGB8),
+ BAYER_GBRG8(avutil.AV_PIX_FMT_BAYER_GBRG8),
+ BAYER_GRBG8(avutil.AV_PIX_FMT_BAYER_GRBG8),
+ BAYER_BGGR16LE(avutil.AV_PIX_FMT_BAYER_BGGR16LE),
+ BAYER_BGGR16BE(avutil.AV_PIX_FMT_BAYER_BGGR16BE),
+ BAYER_RGGB16LE(avutil.AV_PIX_FMT_BAYER_RGGB16LE),
+ BAYER_RGGB16BE(avutil.AV_PIX_FMT_BAYER_RGGB16BE),
+ BAYER_GBRG16LE(avutil.AV_PIX_FMT_BAYER_GBRG16LE),
+ BAYER_GBRG16BE(avutil.AV_PIX_FMT_BAYER_GBRG16BE),
+ BAYER_GRBG16LE(avutil.AV_PIX_FMT_BAYER_GRBG16LE),
+ BAYER_GRBG16BE(avutil.AV_PIX_FMT_BAYER_GRBG16BE),
+
+ // --- Floating Point ---
+ GRAYF32BE(avutil.AV_PIX_FMT_GRAYF32BE),
+ GRAYF32LE(avutil.AV_PIX_FMT_GRAYF32LE),
+
+ // --- Palette ---
+ PAL8(avutil.AV_PIX_FMT_PAL8),
+
+ // --- XYZ ---
+ XYZ12LE(avutil.AV_PIX_FMT_XYZ12LE),
+ XYZ12BE(avutil.AV_PIX_FMT_XYZ12BE),
+
+ // --- Miscellaneous ---
+ XTOP(avutil.AV_PIX_FMT_X2RGB10LE),
+ P010LE(avutil.AV_PIX_FMT_P010LE),
+ P010BE(avutil.AV_PIX_FMT_P010BE),
+ P016LE(avutil.AV_PIX_FMT_P016LE),
+ P016BE(avutil.AV_PIX_FMT_P016BE),
+ P210BE(avutil.AV_PIX_FMT_P210BE),
+ P210LE(avutil.AV_PIX_FMT_P210LE),
+ P410BE(avutil.AV_PIX_FMT_P410BE),
+ P410LE(avutil.AV_PIX_FMT_P410LE),
+ P216BE(avutil.AV_PIX_FMT_P216BE),
+ P216LE(avutil.AV_PIX_FMT_P216LE),
+ P416BE(avutil.AV_PIX_FMT_P416BE),
+ P416LE(avutil.AV_PIX_FMT_P416LE);
+
+ public int ffmpegId;
+ private static final Map BY_ID;
+
+ static {
+ BY_ID = new HashMap<>();
+ for (FullPixelEnum fmt : values()) {
+ BY_ID.put(fmt.ffmpegId, fmt);
+ }
+ }
+
+ public static FullPixelEnum fromId(int ffmpegId) {
+ return BY_ID.getOrDefault(ffmpegId, NONE);
+ }
+
+ FullPixelEnum(int ffmpegId)
+ {
+ this.ffmpegId = ffmpegId;
+ }
+}
diff --git a/processing/sensorhub-process-ffmpeg/src/main/resources/org/sensorhub/impl/process/video/transcoder/README.md b/processing/sensorhub-process-ffmpeg/src/main/resources/org/sensorhub/impl/process/video/transcoder/README.md
index 3ba1e0c72..fd89f33ab 100644
--- a/processing/sensorhub-process-ffmpeg/src/main/resources/org/sensorhub/impl/process/video/transcoder/README.md
+++ b/processing/sensorhub-process-ffmpeg/src/main/resources/org/sensorhub/impl/process/video/transcoder/README.md
@@ -2,11 +2,12 @@
## Overview
-This module provides a process module that will can decode and encode video.
+FFmpeg video decode/encode/transcode module.
+Input and output may be compressed or uncompressed video.
## Configuration
-When added to an OpenSensorHub node, the process has the following configuration properties:
+When added to an OpenSensorHub node, the process has the following configuration options:
- **General:**
- **Module ID:** *Not editable.*
@@ -24,17 +25,185 @@ When added to an OpenSensorHub node, the process has the following configuration
- **Video Source:**
A module with video output. Once the transcoder starts, video from this source module
will be decoded/encoded and outputted from the transcoder.
- - **Input Codec:** (Optional)
- The codec used for decoding the incoming video data. If incoming video is uncompressed,
+ - **Input Format:** (Optional)
+ The format used for decoding the incoming video data. If incoming video is uncompressed,
select either RGB or YUV.
- - **Output Codec:**
- The codec used for encoding the outgoing video data. If outgoing video should be uncompressed,
+ - **Input Format Override:** (Optional)
+ Manually specify any input codec or pixel format, allowing for any format not available in the short Input Codec list.
+ Only required if the format is not in the Input Format list. Otherwise, leave blank.
+ A full list of codecs and pixel formats is available at the end of this file.
+ - **Output Format:**
+ The format used for encoding the outgoing video data. If outgoing video should be uncompressed,
select either RGB or YUV.
+ - **Output Format Override:** (Optional)
+ Manually specify any output codec or pixel format, allowing for any format not available in the short Output Codec list.
+ Only required if the format is not in the Output Format list. Otherwise, leave blank.
+ A full list of codecs and pixel formats is available at the end of this file.
- **Output Width:** (Optional)
The width of the output video frame. Leave this empty to avoid scaling the video frame size.
- **Output Height:** (Optional)
The height of the output video frame. Leave this empty to avoid scaling the video frame size.
- **Auto Start:**
If checked, automatically start this sensor when the OpenSensorHub node is launched.
- - **Automatically Detect Input Codec:**
- If checked, automatically determine the input video codec based on the input's encoding information.
\ No newline at end of file
+ - **Automatically Detect Input Format:**
+ If checked, automatically determine the input video format based on the input's encoding information.
+
+## All Supported Formats
+This list contains all the supported codecs and pixel formats. To use a format, copy it into one of the format
+override config fields.
+
+| Codecs (Compressed Video) | Pixel Formats (Uncompressed Video) |
+|---------------------------|------------------------------------|
+| H264 | YUV420P |
+| HEVC | YUV422P |
+| MJPEG | YUV444P |
+| VP8 | YUV410P |
+| VP9 | YUV411P |
+| MPEG2 | YUV440P |
+| MPEG4 | YUVJ420P |
+| AV1 | YUVJ422P |
+| THEORA | YUVJ444P |
+| MPEG1VIDEO | YUVJ440P |
+| WMV1 | YUV420P9BE |
+| WMV2 | YUV420P9LE |
+| WMV3 | YUV420P10BE |
+| VC1 | YUV420P10LE |
+| FLV1 | YUV420P12BE |
+| FLASHSV | YUV420P12LE |
+| FLASHSV2 | YUV420P14BE |
+| RV10 | YUV420P14LE |
+| RV20 | YUV420P16BE |
+| RV30 | YUV420P16LE |
+| RV40 | YUV422P9BE |
+| CINEPAK | YUV422P9LE |
+| INDEO2 | YUV422P10BE |
+| INDEO3 | YUV422P10LE |
+| INDEO4 | YUV422P12BE |
+| INDEO5 | YUV422P12LE |
+| MSMPEG4V1 | YUV422P14BE |
+| MSMPEG4V2 | YUV422P14LE |
+| MSMPEG4V3 | YUV422P16BE |
+| H261 | YUV422P16LE |
+| H263 | YUV444P9BE |
+| H263I | YUV444P9LE |
+| H263P | YUV444P10BE |
+| SNOW | YUV444P10LE |
+| SVQ1 | YUV444P12BE |
+| SVQ3 | YUV444P12LE |
+| DVVIDEO | YUV444P14BE |
+| HUFFYUV | YUV444P14LE |
+| FFVHUFF | YUV444P16BE |
+| FFV1 | YUV444P16LE |
+| ASV1 | YUYV422 |
+| ASV2 | UYVY422 |
+| VCR1 | YVYU422 |
+| CLJR | UYYVYY411 |
+| MDEC | NV12 |
+| ROQ | NV21 |
+| INTERPLAY_VIDEO | NV16 |
+| XAN_WC3 | NV20LE |
+| XAN_WC4 | NV20BE |
+| RPZA | NV24 |
+| SMC | NV42 |
+| GIF | RGB24 |
+| PNG | BGR24 |
+| PPM | ARGB |
+| PBM | RGBA |
+| PGM | ABGR |
+| PAM | BGRA |
+| BMP | RGB0 |
+| TIFF | BGR0 |
+| SGI | RGB8 |
+| ALIAS_PIX | BGR8 |
+| DPX | RGB4 |
+| EXR | BGR4 |
+| WEBP | RGB4_BYTE |
+| DIRAC | BGR4_BYTE |
+| DNXHD | RGB48BE |
+| PRORES | RGB48LE |
+| JPEG2000 | RGBA64BE |
+| JPEGLS | RGBA64LE |
+| HAP | BGR48BE |
+| | BGR48LE |
+| | BGRA64BE |
+| | BGRA64LE |
+| | RGB565BE |
+| | RGB565LE |
+| | RGB555BE |
+| | RGB555LE |
+| | RGB444BE |
+| | RGB444LE |
+| | BGR565BE |
+| | BGR565LE |
+| | BGR555BE |
+| | BGR555LE |
+| | BGR444BE |
+| | BGR444LE |
+| | GRAY8 |
+| | GRAY8A |
+| | GRAY9BE |
+| | GRAY9LE |
+| | GRAY10BE |
+| | GRAY10LE |
+| | GRAY12BE |
+| | GRAY12LE |
+| | GRAY14BE |
+| | GRAY14LE |
+| | GRAY16BE |
+| | GRAY16LE |
+| | MONOWHITE |
+| | MONOBLACK |
+| | YA8 |
+| | YA16BE |
+| | YA16LE |
+| | YUVA420P |
+| | YUVA422P |
+| | YUVA444P |
+| | YUVA420P9BE |
+| | YUVA420P9LE |
+| | YUVA422P9BE |
+| | YUVA422P9LE |
+| | YUVA444P9BE |
+| | YUVA444P9LE |
+| | YUVA420P10BE |
+| | YUVA420P10LE |
+| | YUVA422P10BE |
+| | YUVA422P10LE |
+| | YUVA444P10BE |
+| | YUVA444P10LE |
+| | YUVA420P16BE |
+| | YUVA420P16LE |
+| | YUVA422P16BE |
+| | YUVA422P16LE |
+| | YUVA444P16BE |
+| | YUVA444P16LE |
+| | BAYER_BGGR8 |
+| | BAYER_RGGB8 |
+| | BAYER_GBRG8 |
+| | BAYER_GRBG8 |
+| | BAYER_BGGR16LE |
+| | BAYER_BGGR16BE |
+| | BAYER_RGGB16LE |
+| | BAYER_RGGB16BE |
+| | BAYER_GBRG16LE |
+| | BAYER_GBRG16BE |
+| | BAYER_GRBG16LE |
+| | BAYER_GRBG16BE |
+| | GRAYF32BE |
+| | GRAYF32LE |
+| | PAL8 |
+| | XYZ12LE |
+| | XYZ12BE |
+| | XTOP |
+| | P010LE |
+| | P010BE |
+| | P016LE |
+| | P016BE |
+| | P210BE |
+| | P210LE |
+| | P410BE |
+| | P410LE |
+| | P216BE |
+| | P216LE |
+| | P416BE |
+| | P416LE |
\ No newline at end of file
diff --git a/services/sensorhub-service-video/src/main/java/org/sensorhub/impl/service/sos/video/MP4Serializer.java b/services/sensorhub-service-video/src/main/java/org/sensorhub/impl/service/sos/video/MP4Serializer.java
index 1825381dd..11359cc00 100644
--- a/services/sensorhub-service-video/src/main/java/org/sensorhub/impl/service/sos/video/MP4Serializer.java
+++ b/services/sensorhub-service-video/src/main/java/org/sensorhub/impl/service/sos/video/MP4Serializer.java
@@ -72,7 +72,7 @@ public void sendNextFrame(DataBlock nextFrame, OutputStream os) throws IOExcepti
ByteBuffer nals = ByteBuffer.wrap(frameData);
// debug
- //os.write(frameData);
+ os.write(frameData);
//os.flush();
// look for next nal unit