diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java b/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java index 695f1a74a..285a70cbf 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java @@ -23,6 +23,17 @@ */ public abstract class NativeObject { + // Loaded before any class that wraps a native object is used, also when + // it is used on its own, without a factory that loaded the library. + static { + try { + NativeLoader.loadLibrary("webrtc-java"); + } + catch (Exception e) { + throw new RuntimeException("Load library 'webrtc-java' failed", e); + } + } + /** * Field used to store a pointer to the native instance allocated on the * heap. Don't modify this value directly, or else you risk causing 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 1253c1053..0369463aa 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 @@ -18,6 +18,7 @@ import java.nio.ByteBuffer; +import dev.onvoid.webrtc.internal.NativeLoader; import dev.onvoid.webrtc.media.FourCC; /** @@ -29,6 +30,15 @@ */ public final class VideoBufferConverter { + static { + try { + NativeLoader.loadLibrary("webrtc-java"); + } + catch (Exception e) { + throw new RuntimeException("Load library 'webrtc-java' failed", e); + } + } + /** * Convert an arbitrary {@link VideoFrameBuffer} to the specified pixel format * (given by {@code fourCC}) and write the result into a destination byte array. diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/desktop/PowerManagement.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/desktop/PowerManagement.java index c416b4583..4ae1c1886 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/desktop/PowerManagement.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/desktop/PowerManagement.java @@ -16,6 +16,8 @@ package dev.onvoid.webrtc.media.video.desktop; +import dev.onvoid.webrtc.internal.NativeLoader; + /** * Power management assertions to wake the display and prevent it from going to sleep on user idle. * @@ -23,6 +25,15 @@ */ public class PowerManagement { + static { + try { + NativeLoader.loadLibrary("webrtc-java"); + } + catch (Exception e) { + throw new RuntimeException("Load library 'webrtc-java' failed", e); + } + } + /** * Declares that the user is active on the system and prevents the sleep idle time-out. */ diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneLoadingTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneLoadingTests.java new file mode 100644 index 000000000..103a62ddf --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneLoadingTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2026 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.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Uses each class that applications may use on their own in a JVM of its + * own. In the JVM the other tests share, some earlier test has always loaded + * the native library, which hides a class that needs it and does not load it. + */ +@Execution(ExecutionMode.SAME_THREAD) +class StandaloneLoadingTests { + + private static final long TIMEOUT_SECONDS = 60; + + + @TestFactory + Stream loadsLibraryOnItsOwn() { + return Arrays.stream(StandaloneUse.CLASSES) + .map(name -> DynamicTest.dynamicTest(name, () -> runAlone(name))); + } + + private static void runAlone(String name) throws Exception { + List command = new ArrayList<>(); + command.add(Paths.get(System.getProperty("java.home"), "bin", "java").toString()); + command.add("-cp"); + command.add(classPath()); + command.add(StandaloneUse.class.getName()); + command.add(name); + + Process process = new ProcessBuilder(command) + .redirectErrorStream(true) + .start(); + + // Read while it runs, so that a full pipe cannot stall it. + ByteArrayOutputStream output = new ByteArrayOutputStream(); + Thread reader = new Thread(() -> copy(process.getInputStream(), output)); + reader.start(); + + assertTrue(process.waitFor(TIMEOUT_SECONDS, TimeUnit.SECONDS), () -> { + process.destroyForcibly(); + return name + " did not finish within " + TIMEOUT_SECONDS + " s"; + }); + reader.join(); + + String log = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertEquals(0, process.exitValue(), () -> name + " failed in a fresh JVM:\n" + log); + } + + /** + * The classes and libraries this JVM runs with, whether they are on the + * class path or, as for a module, on the module path. + */ + private static String classPath() { + Set entries = new LinkedHashSet<>(); + + for (String property : new String[] { "jdk.module.path", "java.class.path" }) { + String value = System.getProperty(property); + + if (value != null && !value.isEmpty()) { + entries.addAll(Arrays.asList(value.split(File.pathSeparator))); + } + } + + return String.join(File.pathSeparator, entries); + } + + private static void copy(InputStream input, ByteArrayOutputStream output) { + byte[] buffer = new byte[8192]; + int read; + + try { + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + } + } + catch (IOException e) { + // The process ended; what was read is kept. + } + } +} diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneUse.java b/webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneUse.java new file mode 100644 index 000000000..a042a27c0 --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneUse.java @@ -0,0 +1,161 @@ +/* + * Copyright 2026 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.internal; + +import java.nio.ByteBuffer; + +import dev.onvoid.webrtc.media.FourCC; +import dev.onvoid.webrtc.media.audio.AudioConverter; +import dev.onvoid.webrtc.media.audio.AudioResampler; +import dev.onvoid.webrtc.media.audio.CustomAudioSource; +import dev.onvoid.webrtc.media.video.CustomVideoSource; +import dev.onvoid.webrtc.media.video.I420Buffer; +import dev.onvoid.webrtc.media.video.NativeI420Buffer; +import dev.onvoid.webrtc.media.video.VideoBufferConverter; +import dev.onvoid.webrtc.media.video.VideoCapture; +import dev.onvoid.webrtc.media.video.VideoFrameBuffer; + +/** + * Uses one class that applications may use on their own, as the first and + * only thing a fresh JVM does, so that nothing else has loaded the native + * library before. Exits with 0 once the class worked. + */ +public final class StandaloneUse { + + static final String[] CLASSES = { + "AudioConverter", + "AudioResampler", + "CustomAudioSource", + "CustomVideoSource", + "NativeI420Buffer", + "VideoBufferConverter", + "VideoCapture" + }; + + + public static void main(String[] args) throws Exception { + switch (args[0]) { + case "AudioConverter": + new AudioConverter(48000, 1, 16000, 1).dispose(); + break; + case "AudioResampler": + new AudioResampler(48000, 16000, 1).dispose(); + break; + case "CustomAudioSource": + new CustomAudioSource().dispose(); + break; + case "CustomVideoSource": + new CustomVideoSource().dispose(); + break; + case "NativeI420Buffer": + NativeI420Buffer.allocate(2, 2).release(); + break; + case "VideoBufferConverter": + // Over a buffer of Java's own, so that no other native class is used first. + VideoBufferConverter.convertFromI420(new JavaI420Buffer(2, 2), new byte[16], FourCC.RGBA); + break; + case "VideoCapture": + new VideoCapture().dispose(); + break; + default: + throw new IllegalArgumentException("Unknown class " + args[0]); + } + } + + + + private static final class JavaI420Buffer implements I420Buffer { + + private final int width; + + private final int height; + + private final ByteBuffer dataY; + + private final ByteBuffer dataU; + + private final ByteBuffer dataV; + + + JavaI420Buffer(int width, int height) { + this.width = width; + this.height = height; + this.dataY = ByteBuffer.allocateDirect(width * height); + this.dataU = ByteBuffer.allocateDirect(getStrideU() * ((height + 1) / 2)); + this.dataV = ByteBuffer.allocateDirect(getStrideV() * ((height + 1) / 2)); + } + + @Override + public ByteBuffer getDataY() { + return dataY; + } + + @Override + public ByteBuffer getDataU() { + return dataU; + } + + @Override + public ByteBuffer getDataV() { + return dataV; + } + + @Override + public int getStrideY() { + return width; + } + + @Override + public int getStrideU() { + return (width + 1) / 2; + } + + @Override + public int getStrideV() { + return (width + 1) / 2; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public I420Buffer toI420() { + return this; + } + + @Override + public VideoFrameBuffer cropAndScale(int cropX, int cropY, int cropWidth, int cropHeight, + int scaleWidth, int scaleHeight) { + throw new UnsupportedOperationException(); + } + + @Override + public void retain() { + } + + @Override + public void release() { + } + } +}