diff --git a/webrtc-jni/src/main/cpp/include/JNI_VideoBufferConverter.h b/webrtc-jni/src/main/cpp/include/JNI_VideoBufferConverter.h index 81f24d56..8373597a 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_VideoBufferConverter.h +++ b/webrtc-jni/src/main/cpp/include/JNI_VideoBufferConverter.h @@ -23,6 +23,22 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_I420toDirectBuffer (JNIEnv *, jclass, jobject, jint, jobject, jint, jobject, jint, jobject, jint, jint, jint); + /* + * Class: dev_onvoid_webrtc_media_video_VideoBufferConverter + * Method: byteArrayToI420 + * Signature: ([BIILjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;II)V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_byteArrayToI420 + (JNIEnv *, jclass, jbyteArray, jint, jint, jobject, jint, jobject, jint, jobject, jint, jint); + + /* + * Class: dev_onvoid_webrtc_media_video_VideoBufferConverter + * Method: directBufferToI420 + * Signature: (Ljava/nio/ByteBuffer;IILjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;II)V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_directBufferToI420 + (JNIEnv *, jclass, jobject, jint, jint, jobject, jint, jobject, jint, jobject, jint, jint); + #ifdef __cplusplus } #endif diff --git a/webrtc-jni/src/main/cpp/src/JNI_VideoBufferConverter.cpp b/webrtc-jni/src/main/cpp/src/JNI_VideoBufferConverter.cpp index 4f62654e..11a684ed 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_VideoBufferConverter.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_VideoBufferConverter.cpp @@ -17,7 +17,9 @@ #include "JNI_VideoBufferConverter.h" #include "JavaRuntimeException.h" -#include "libyuv/convert_from.h" +#include "api/video/i420_buffer.h" + +#include "libyuv/convert.h" #include "libyuv/video_common.h" size_t CalcBufferSize(int width, int height, int fourCC) { @@ -78,10 +80,17 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_I jbyte * arrayPtr = env->GetByteArrayElements(dst, nullptr); uint8_t * dstPtr = reinterpret_cast(arrayPtr); - libyuv::ConvertFromI420(srcY, srcStrideY, srcU, srcStrideU, srcV, srcStrideV, - dstPtr, 0, width, height, static_cast(fourCC)); + const int conversionResult = libyuv::ConvertFromI420(srcY, srcStrideY, srcU, srcStrideU, + srcV, srcStrideV, dstPtr, 0, width, height, static_cast(fourCC)); + + if (conversionResult < 0) { + env->Throw(jni::JavaRuntimeException(env, "Failed to convert buffer to I420: %d", + conversionResult)); + } + else { + env->SetByteArrayRegion(dst, 0, arrayLength, arrayPtr); + } - env->SetByteArrayRegion(dst, 0, arrayLength, arrayPtr); env->ReleaseByteArrayElements(dst, arrayPtr, JNI_ABORT); } @@ -105,8 +114,87 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_I return; } - libyuv::ConvertFromI420(srcY, srcStrideY, srcU, srcStrideU, srcV, srcStrideV, - address, 0, width, height, static_cast(fourCC)); + const int conversionResult = libyuv::ConvertFromI420(srcY, srcStrideY, srcU, srcStrideU, + srcV, srcStrideV, address, 0, width, height, static_cast(fourCC)); + + if (conversionResult < 0) { + env->Throw(jni::JavaRuntimeException(env, "Failed to convert buffer to I420: %d", + conversionResult)); + } + } + else { + env->Throw(jni::JavaRuntimeException(env, "Non-direct buffer provided")); + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_byteArrayToI420 +(JNIEnv * env, jclass cls, jbyteArray src, jint width, jint height, jobject jDstY, jint dstStrideY, + jobject jDstU, jint dstStrideU, jobject jDstV, jint dstStrideV, jint fourCC) +{ + jsize arrayLength = env->GetArrayLength(src); + size_t requiredSize = CalcBufferSize(width, height, fourCC); + + if (arrayLength < requiredSize) { + env->Throw(jni::JavaRuntimeException(env, "Insufficient buffer size [has %d, need %zd]", + arrayLength, requiredSize)); + return; + } + + uint8_t * dstY = static_cast(env->GetDirectBufferAddress(jDstY)); + uint8_t * dstU = static_cast(env->GetDirectBufferAddress(jDstU)); + uint8_t * dstV = static_cast(env->GetDirectBufferAddress(jDstV)); + + jbyte * arrayPtr = env->GetByteArrayElements(src, nullptr); + const uint8_t * srcPtr = reinterpret_cast(arrayPtr); + + const int conversionResult = libyuv::ConvertToI420( + srcPtr, arrayLength, + dstY, dstStrideY, + dstU, dstStrideU, + dstV, dstStrideV, + 0, 0, width, height, width, height, + libyuv::kRotate0, static_cast(fourCC)); + + if (conversionResult < 0) { + env->Throw(jni::JavaRuntimeException(env, "Failed to convert buffer to I420: %d", + conversionResult)); + } + + env->ReleaseByteArrayElements(src, arrayPtr, JNI_ABORT); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_video_VideoBufferConverter_directBufferToI420 +(JNIEnv * env, jclass, jobject src, jint width, jint height, jobject jDstY, jint dstStrideY, + jobject jDstU, jint dstStrideU, jobject jDstV, jint dstStrideV, jint fourCC) +{ + uint8_t * dstY = static_cast(env->GetDirectBufferAddress(jDstY)); + uint8_t * dstU = static_cast(env->GetDirectBufferAddress(jDstU)); + uint8_t * dstV = static_cast(env->GetDirectBufferAddress(jDstV)); + + const uint8_t * address = static_cast(env->GetDirectBufferAddress(src)); + + if (address != NULL) { + size_t bufferLength = env->GetDirectBufferCapacity(src); + size_t requiredSize = CalcBufferSize(width, height, fourCC); + + if (bufferLength < requiredSize) { + env->Throw(jni::JavaRuntimeException(env, "Insufficient buffer size [has %zd, need %zd]", + bufferLength, requiredSize)); + return; + } + + const int conversionResult = libyuv::ConvertToI420( + address, bufferLength, + dstY, dstStrideY, + dstU, dstStrideU, + dstV, dstStrideV, + 0, 0, width, height, width, height, + libyuv::kRotate0, static_cast(fourCC)); + + if (conversionResult < 0) { + env->Throw(jni::JavaRuntimeException(env, "Failed to convert buffer to I420: %d", + conversionResult)); + } } else { env->Throw(jni::JavaRuntimeException(env, "Non-direct buffer provided")); diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoBufferConverter.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoBufferConverter.java index 6fcd1236..688efc06 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoBufferConverter.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoBufferConverter.java @@ -48,6 +48,9 @@ public static void convertFromI420(VideoFrameBuffer src, ByteBuffer dst, FourCC if (dst == null) { throw new NullPointerException("Destination buffer must not be null"); } + if (dst.isReadOnly()) { + throw new IllegalArgumentException("Destination buffer must not be read-only"); + } I420Buffer i420 = src.toI420(); @@ -81,6 +84,61 @@ public static void convertFromI420(VideoFrameBuffer src, ByteBuffer dst, FourCC } } + public static void convertToI420(byte[] src, I420Buffer dst, FourCC fourCC) throws Exception { + if (src == null) { + throw new NullPointerException("Source buffer must not be null"); + } + if (dst == null) { + throw new NullPointerException("Destination buffer must not be null"); + } + + byteArrayToI420( + src, + dst.getWidth(), dst.getHeight(), + dst.getDataY(), dst.getStrideY(), + dst.getDataU(), dst.getStrideU(), + dst.getDataV(), dst.getStrideV(), + fourCC.value()); + } + + public static void convertToI420(ByteBuffer src, I420Buffer dst, FourCC fourCC) throws Exception { + if (src == null) { + throw new NullPointerException("Source buffer must not be null"); + } + if (dst == null) { + throw new NullPointerException("Destination buffer must not be null"); + } + + if (src.isDirect()) { + directBufferToI420( + src, + dst.getWidth(), dst.getHeight(), + dst.getDataY(), dst.getStrideY(), + dst.getDataU(), dst.getStrideU(), + dst.getDataV(), dst.getStrideV(), + fourCC.value()); + } + else { + byte[] arrayBuffer; + + if (src.hasArray()) { + arrayBuffer = src.array(); + } + else { + arrayBuffer = new byte[src.remaining()]; + src.get(arrayBuffer); + } + + byteArrayToI420( + arrayBuffer, + dst.getWidth(), dst.getHeight(), + dst.getDataY(), dst.getStrideY(), + dst.getDataU(), dst.getStrideU(), + dst.getDataV(), dst.getStrideV(), + fourCC.value()); + } + } + private native static void I420toByteArray( ByteBuffer srcY, int srcStrideY, ByteBuffer srcU, int srcStrideU, @@ -97,4 +155,20 @@ private native static void I420toDirectBuffer( int width, int height, int fourCC) throws Exception; + private native static void byteArrayToI420( + byte[] src, + int width, int height, + ByteBuffer dstY, int dstStrideY, + ByteBuffer dstU, int dstStrideU, + ByteBuffer dstV, int dstStrideV, + int fourCC) throws Exception; + + private native static void directBufferToI420( + ByteBuffer src, + int width, int height, + ByteBuffer dstY, int dstStrideY, + ByteBuffer dstU, int dstStrideU, + ByteBuffer dstV, int dstStrideV, + int fourCC) throws Exception; + } diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/media/video/VideoBufferConverterTest.java b/webrtc/src/test/java/dev/onvoid/webrtc/media/video/VideoBufferConverterTest.java new file mode 100644 index 00000000..943ff540 --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/media/video/VideoBufferConverterTest.java @@ -0,0 +1,347 @@ +/* + * Copyright 2025 Alex Andres + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.onvoid.webrtc.media.video; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.ByteBuffer; + +import dev.onvoid.webrtc.TestBase; +import dev.onvoid.webrtc.media.FourCC; + +import org.junit.jupiter.api.Test; + +class VideoBufferConverterTest extends TestBase { + + private static final int WIDTH = 32; + private static final int HEIGHT = 8; + + @Test + void convertFromI420ToByteArray() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + byte[] rgba = new byte[4 * WIDTH * HEIGHT]; + VideoBufferConverter.convertFromI420(i420, rgba, FourCC.RGBA); + verifyI420Buffer(i420, false); + verifyRGBAArray(rgba, true); + } + + @Test + void convertFromI420ToDirectBuffer() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + ByteBuffer rgba = ByteBuffer.allocateDirect(4 * WIDTH * HEIGHT); + VideoBufferConverter.convertFromI420(i420, rgba, FourCC.RGBA); + verifyI420Buffer(i420, false); + verifyRGBABuffer(rgba, true); + } + + @Test + void convertFromI420ToNonDirectBuffer() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + ByteBuffer rgba = ByteBuffer.allocate(4 * WIDTH * HEIGHT); + VideoBufferConverter.convertFromI420(i420, rgba, FourCC.RGBA); + verifyI420Buffer(i420, false); + verifyRGBABuffer(rgba, true); + } + + @Test + void convertFromI420ToReadOnlyDirectBuffer() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + ByteBuffer rgba = ByteBuffer.allocateDirect(4 * WIDTH * HEIGHT); + assertThrows(IllegalArgumentException.class, () -> { + VideoBufferConverter.convertFromI420(i420, rgba.asReadOnlyBuffer(), FourCC.RGBA); + }); + } + + @Test + void convertFromI420ToReadOnlyNonDirectBuffer() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + ByteBuffer rgba = ByteBuffer.allocate(4 * WIDTH * HEIGHT); + assertThrows(IllegalArgumentException.class, () -> { + VideoBufferConverter.convertFromI420(i420, rgba.asReadOnlyBuffer(), FourCC.RGBA); + }); + } + + @Test + void convertFromI420ToSmallByteArray() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + byte[] rgba = new byte[4 * WIDTH * HEIGHT - 1]; + assertThrows(RuntimeException.class, () -> { + VideoBufferConverter.convertFromI420(i420, rgba, FourCC.RGBA); + }); + } + + @Test + void convertFromI420ToSmallDirectBuffer() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + ByteBuffer rgba = ByteBuffer.allocateDirect(4 * WIDTH * HEIGHT - 1); + assertThrows(RuntimeException.class, () -> { + VideoBufferConverter.convertFromI420(i420, rgba, FourCC.RGBA); + }); + } + + @Test + void convertFromI420ToSmallNonDirectBuffer() throws Exception { + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + initializeI420Buffer(i420); + ByteBuffer rgba = ByteBuffer.allocate(4 * WIDTH * HEIGHT - 1); + assertThrows(RuntimeException.class, () -> { + VideoBufferConverter.convertFromI420(i420, rgba, FourCC.RGBA); + }); + } + + @Test + void convertFromByteArrayToI420() throws Exception { + byte[] rgba = new byte[4 * WIDTH * HEIGHT]; + initializeRGBAArray(rgba); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + VideoBufferConverter.convertToI420(rgba, i420, FourCC.RGBA); + verifyRGBAArray(rgba, false); + verifyI420Buffer(i420, true); + } + + @Test + void convertFromDirectBufferToI420() throws Exception { + ByteBuffer rgba = ByteBuffer.allocateDirect(4 * WIDTH * HEIGHT); + initializeRGBABuffer(rgba); + rgba.rewind(); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + VideoBufferConverter.convertToI420(rgba, i420, FourCC.RGBA); + verifyRGBABuffer(rgba, false); + verifyI420Buffer(i420, true); + } + + @Test + void convertFromNonDirectBufferToI420() throws Exception { + ByteBuffer rgba = ByteBuffer.allocate(4 * WIDTH * HEIGHT); + initializeRGBABuffer(rgba); + rgba.rewind(); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + VideoBufferConverter.convertToI420(rgba, i420, FourCC.RGBA); + verifyRGBABuffer(rgba, false); + verifyI420Buffer(i420, true); + } + + @Test + void convertFromReadOnlyDirectBufferToI420() throws Exception { + ByteBuffer rgba = ByteBuffer.allocateDirect(4 * WIDTH * HEIGHT); + initializeRGBABuffer(rgba); + rgba.rewind(); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + VideoBufferConverter.convertToI420(rgba.asReadOnlyBuffer(), i420, FourCC.RGBA); + verifyRGBABuffer(rgba, false); + verifyI420Buffer(i420, true); + } + + @Test + void convertFromReadOnlyNonDirectBufferToI420() throws Exception { + ByteBuffer rgba = ByteBuffer.allocate(4 * WIDTH * HEIGHT); + initializeRGBABuffer(rgba); + rgba.rewind(); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + VideoBufferConverter.convertToI420(rgba.asReadOnlyBuffer(), i420, FourCC.RGBA); + verifyRGBABuffer(rgba, false); + verifyI420Buffer(i420, true); + } + + @Test + void convertFromSmallByteArrayToI420() throws Exception { + byte[] rgba = new byte[4 * WIDTH * HEIGHT - 1]; + initializeRGBAArray(rgba); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + assertThrows(RuntimeException.class, () -> { + VideoBufferConverter.convertToI420(rgba, i420, FourCC.RGBA); + }); + } + + @Test + void convertFromSmallDirectBufferToI420() throws Exception { + ByteBuffer rgba = ByteBuffer.allocateDirect(4 * WIDTH * HEIGHT - 1); + initializeRGBABuffer(rgba); + rgba.rewind(); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + assertThrows(RuntimeException.class, () -> { + VideoBufferConverter.convertToI420(rgba, i420, FourCC.RGBA); + }); + } + + @Test + void convertFromSmallNonDirectBufferToI420() throws Exception { + ByteBuffer rgba = ByteBuffer.allocate(4 * WIDTH * HEIGHT - 1); + initializeRGBABuffer(rgba); + rgba.rewind(); + NativeI420Buffer i420 = NativeI420Buffer.allocate(WIDTH, HEIGHT); + assertThrows(RuntimeException.class, () -> { + VideoBufferConverter.convertToI420(rgba, i420, FourCC.RGBA); + }); + } + + private void initializeI420Buffer(I420Buffer i420) { + ByteBuffer dataY = i420.getDataY(); + int strideY = i420.getStrideY(); + ByteBuffer dataU = i420.getDataU(); + int strideU = i420.getStrideU(); + ByteBuffer dataV = i420.getDataV(); + int strideV = i420.getStrideV(); + + byte value = 0; + for (int y = 0; y < HEIGHT; y++) { + for (int x = 0; x < WIDTH; x++) { + dataY.put(y * strideY + x, value++); + } + } + for (int y = 0; y < HEIGHT / 2; y++) { + for (int x = 0; x < WIDTH / 2; x++) { + dataU.put(y * strideU + x, value++); + dataV.put(y * strideV + x, value++); + } + } + } + + private void initializeRGBAArray(byte[] rgba) { + byte value = 0; + for (int i = 0; i < rgba.length; i++) { + rgba[i] = value++; + } + } + + private void initializeRGBABuffer(ByteBuffer rgba) { + byte value = 0; + for (int i = 0; i < rgba.limit(); i++) { + rgba.put(value++); + } + } + + private void verifyI420Buffer(I420Buffer i420, boolean expectingChanges) { + assertEquals(WIDTH, i420.getWidth()); + assertEquals(HEIGHT, i420.getHeight()); + + ByteBuffer dataY = i420.getDataY(); + int strideY = i420.getStrideY(); + ByteBuffer dataU = i420.getDataU(); + int strideU = i420.getStrideU(); + ByteBuffer dataV = i420.getDataV(); + int strideV = i420.getStrideV(); + + if (expectingChanges) { + boolean nonZeroY = false; + boolean nonZeroU = false; + boolean nonZeroV = false; + + for (int y = 0; y < HEIGHT; y++) { + for (int x = 0; x < WIDTH; x++) { + nonZeroY |= (dataY.get(y * strideY + x) != 0); + } + } + for (int y = 0; y < HEIGHT / 2; y++) { + for (int x = 0; x < WIDTH / 2; x++) { + nonZeroU |= (dataU.get(y * strideU + x) != 0); + nonZeroV |= (dataV.get(y * strideV + x) != 0); + } + } + + assertTrue(nonZeroY); + assertTrue(nonZeroU); + assertTrue(nonZeroV); + } + else { + byte value = 0; + for (int y = 0; y < HEIGHT; y++) { + for (int x = 0; x < WIDTH; x++) { + assertEquals(value++, dataY.get(y * strideY + x)); + } + } + for (int y = 0; y < HEIGHT / 2; y++) { + for (int x = 0; x < WIDTH / 2; x++) { + assertEquals(value++, dataU.get(y * strideU + x)); + assertEquals(value++, dataV.get(y * strideV + x)); + } + } + } + } + + private void verifyRGBAArray(byte[] rgba, boolean expectingChanges) { + assertEquals(4 * WIDTH * HEIGHT, rgba.length); + + if (expectingChanges) { + boolean nonZeroR = false; + boolean nonZeroG = false; + boolean nonZeroB = false; + + for (int i = 0; i < 4 * WIDTH * HEIGHT; i += 4) { + byte a = rgba[i + 0]; + byte b = rgba[i + 1]; + byte g = rgba[i + 2]; + byte r = rgba[i + 3]; + assertEquals((byte) -1, a); + nonZeroB |= (b != 0); + nonZeroG |= (g != 0); + nonZeroR |= (r != 0); + } + + assertTrue(nonZeroR); + assertTrue(nonZeroG); + assertTrue(nonZeroB); + } + else { + byte value = 0; + for (int i = 0; i < 4 * WIDTH * HEIGHT; i++) { + assertEquals(value++, rgba[i]); + } + } + } + + private void verifyRGBABuffer(ByteBuffer rgba, boolean expectingChanges) { + assertEquals(4 * WIDTH * HEIGHT, rgba.limit()); + + if (expectingChanges) { + boolean nonZeroR = false; + boolean nonZeroG = false; + boolean nonZeroB = false; + + for (int i = 0; i < 4 * WIDTH * HEIGHT; i += 4) { + byte a = rgba.get(); + byte b = rgba.get(); + byte g = rgba.get(); + byte r = rgba.get(); + assertEquals((byte) -1, a); + nonZeroB |= (b != 0); + nonZeroG |= (g != 0); + nonZeroR |= (r != 0); + } + + assertTrue(nonZeroR); + assertTrue(nonZeroG); + assertTrue(nonZeroB); + } + else { + byte value = 0; + for (int i = 0; i < 4 * WIDTH * HEIGHT; i++) { + assertEquals(value++, rgba.get()); + } + } + } + +} \ No newline at end of file