Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.nio.ByteBuffer;

import dev.onvoid.webrtc.internal.NativeLoader;
import dev.onvoid.webrtc.media.FourCC;

/**
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,24 @@

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.
*
* @author Alex Andres
*/
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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DynamicTest> loadsLibraryOnItsOwn() {
return Arrays.stream(StandaloneUse.CLASSES)
.map(name -> DynamicTest.dynamicTest(name, () -> runAlone(name)));
}

private static void runAlone(String name) throws Exception {
List<String> 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<String> 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.
}
}
}
161 changes: 161 additions & 0 deletions webrtc/src/test/java/dev/onvoid/webrtc/internal/StandaloneUse.java
Original file line number Diff line number Diff line change
@@ -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() {
}
}
}
Loading