Skip to content

Commit 14d07d5

Browse files
committed
Merge feature/plan-5-ffi-jni: C FFI, JNI bindings, and Java samples
Completes libroqr: a C++20 RoQR (RTMP over QUIC) implementation with a sans-I/O core, picoquic transport, RTMP/AMF/E-RTMP gateway module, test relay, ffmpeg-validated gateways, and C/JNI/Java bindings.
2 parents 30757e5 + acf1ae1 commit 14d07d5

30 files changed

Lines changed: 1691 additions & 1 deletion

CMakeLists.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ option(ROQR_BUILD_QUIC "Build the picoquic transport (needs picoquic)" OFF)
2121
option(ROQR_BUILD_TOOLS "Build tools (roqr-relayd; needs ROQR_BUILD_QUIC)" OFF)
2222
option(ROQR_BUILD_RTMP "Build the RTMP/AMF gateway module" ON)
2323
option(ROQR_BUILD_EXAMPLES "Build gateway library and example apps" ON)
24+
option(ROQR_BUILD_FFI "Build the C FFI shared library" ON)
25+
option(ROQR_BUILD_JNI "Build JNI bindings (needs a JDK)" OFF)
26+
27+
# The SHARED roqr-ffi library links the static libraries (and picoquic), so
28+
# they must be position-independent — but only when FFI is actually built.
29+
# Setting this directory-scoped variable before add_subdirectory propagates
30+
# it to every subproject (including the picoquic add_subdirectory), so the
31+
# core-only build (no FFI) is unaffected.
32+
if(ROQR_BUILD_FFI AND ROQR_BUILD_QUIC AND ROQR_BUILD_RTMP)
33+
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
34+
endif()
2435

2536
add_subdirectory(core)
2637

@@ -57,6 +68,26 @@ if(ROQR_BUILD_TOOLS)
5768
add_subdirectory(tools/relayd)
5869
endif()
5970

71+
if(ROQR_BUILD_FFI)
72+
if(ROQR_BUILD_QUIC AND ROQR_BUILD_RTMP)
73+
add_subdirectory(ffi)
74+
else()
75+
message(STATUS "ROQR_BUILD_FFI needs QUIC and RTMP; skipping FFI in this config")
76+
endif()
77+
endif()
78+
79+
if(ROQR_BUILD_JNI)
80+
if(NOT TARGET roqr-ffi)
81+
message(FATAL_ERROR "ROQR_BUILD_JNI requires the FFI library (ROQR_BUILD_FFI plus QUIC+RTMP)")
82+
endif()
83+
find_package(JNI)
84+
if(JNI_FOUND OR ANDROID)
85+
add_subdirectory(jni)
86+
else()
87+
message(WARNING "JNI not found; skipping roqr-jni")
88+
endif()
89+
endif()
90+
6091
if(ROQR_BUILD_TESTS)
6192
enable_testing()
6293
add_subdirectory(tests)

CMakePresets.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"ROQR_BUILD_TESTS": "ON",
1010
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON",
1111
"ROQR_BUILD_QUIC": "ON",
12-
"ROQR_BUILD_TOOLS": "ON"
12+
"ROQR_BUILD_TOOLS": "ON",
13+
"ROQR_BUILD_JNI": "ON"
1314
}
1415
},
1516
{

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,16 @@ The core protocol library builds without picoquic:
3434
- `tools/relayd/` the RoQR test relay
3535
- `examples/` roqr-ingest, roqr-egress, roqr-duplex
3636

37+
## Language bindings
38+
39+
- `ffi/` a C ABI (`roqr.h` for the client, `roqr_rtmp.h` for the ingest/egress
40+
gateways) in `libroqr-ffi.so`.
41+
- `jni/` JNI bindings (`org.red5.roqr`) in `libroqr-jni.so` + `roqr.jar`,
42+
built when `-DROQR_BUILD_JNI=ON` and a JDK is present.
43+
- `examples/java/` Java publish/play samples (see `examples/java/README.md`).
44+
- Android: see `cmake/android-jni.md` for the NDK cross-compile.
45+
46+
Native callbacks (`MessageListener`) fire on a native thread the binding
47+
attaches to the JVM for the call; do not block in them.
48+
3749
License: Apache-2.0.

cmake/android-jni.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Building the RoQR JNI library for Android (NDK)
2+
3+
The `roqr-jni` shared library and its dependencies (roqr-core, roqr-quic,
4+
roqr-rtmp, roqr-gateway, roqr-ffi, picoquic, picotls, OpenSSL) are all
5+
C/C++ and cross-compile for Android with the NDK toolchain. The Java
6+
classes in `jni/java/org/red5/roqr` are platform-independent and go into
7+
your Android app or an AAR unchanged.
8+
9+
## Prerequisites
10+
11+
- Android NDK r25+ (`ANDROID_NDK_HOME` set).
12+
- OpenSSL built for the target ABI. This repo's sibling `openssl-android`
13+
tree provides prebuilt static libs; point `OPENSSL_ROOT_DIR` at the ABI
14+
you are building.
15+
- picoquic/picotls source (fetched by `scripts/setup_picoquic_deps.sh`);
16+
they build under the NDK toolchain via the same source-tree mechanism
17+
`FindPicoquic.cmake` uses.
18+
19+
## Configure
20+
21+
```
22+
export ANDROID_ABI=arm64-v8a # or armeabi-v7a, x86_64
23+
export ANDROID_PLATFORM=android-24
24+
25+
eval "$(scripts/setup_picoquic_deps.sh)"
26+
27+
cmake -S . -B build/android-$ANDROID_ABI \
28+
-DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
29+
-DANDROID_ABI=$ANDROID_ABI \
30+
-DANDROID_PLATFORM=$ANDROID_PLATFORM \
31+
-DOPENSSL_ROOT_DIR=/path/to/openssl-android/$ANDROID_ABI \
32+
-DROQR_BUILD_QUIC=ON -DROQR_BUILD_RTMP=ON \
33+
-DROQR_BUILD_EXAMPLES=ON -DROQR_BUILD_FFI=ON \
34+
-DROQR_BUILD_JNI=ON -DROQR_BUILD_TESTS=OFF -DROQR_BUILD_TOOLS=OFF
35+
36+
cmake --build build/android-$ANDROID_ABI --target roqr-jni
37+
```
38+
39+
The root `CMakeLists.txt` still calls `find_package(JNI)` whenever
40+
`ROQR_BUILD_JNI=ON`, including on Android, but the `jni` subdirectory is
41+
gated with `if(JNI_FOUND OR ANDROID)` so it is added regardless of whether
42+
`find_package(JNI)` succeeds under the NDK toolchain (`FindJNI` can be
43+
unreliable there). Inside `jni/CMakeLists.txt`, the
44+
`target_include_directories(... ${JNI_INCLUDE_DIRS})` and
45+
`target_link_libraries(... ${JNI_LIBRARIES})` lines are further guarded with
46+
`if(NOT ANDROID)`, since the NDK sysroot already provides `jni.h` on the
47+
default include path and needs no separate JNI libraries to link against.
48+
49+
Note that a HOST JDK is still required even for the NDK configure: the NDK
50+
only supplies `jni.h`, not a `javac`. `jni/CMakeLists.txt` calls
51+
`find_package(Java REQUIRED COMPONENTS Development)` and `add_jar` to build
52+
`roqr.jar` from the platform-independent Java sources, and both need a host
53+
JDK's `javac` on `PATH` (or discoverable via `JAVA_HOME`) regardless of the
54+
Android target ABI.
55+
56+
## Package
57+
58+
Copy `build/android-<abi>/jni/libroqr-jni.so` into
59+
`app/src/main/jniLibs/<abi>/`, add the `org.red5.roqr` Java sources (or the
60+
`roqr.jar`) to your app, and call `System.loadLibrary("roqr-jni")` (already
61+
done in the static initializers).
62+
63+
## ABI note
64+
65+
Build one `libroqr-jni.so` per ABI you ship. The Java API is identical
66+
across ABIs.

examples/java/PlaySample.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import org.red5.roqr.EgressGateway;
2+
3+
/** Serves a RoQR stream to an RTMP player.
4+
* Usage: PlaySample <rtmpPort> <roqrHost> <roqrPort> <stream> */
5+
public final class PlaySample {
6+
public static void main(String[] args) throws Exception {
7+
if (args.length < 4) {
8+
System.err.println(
9+
"usage: PlaySample <rtmpPort> <roqrHost> <roqrPort> <stream>");
10+
System.exit(2);
11+
}
12+
int rtmpPort = Integer.parseInt(args[0]);
13+
String host = args[1];
14+
int roqrPort = Integer.parseInt(args[2]);
15+
String stream = args[3];
16+
17+
try (EgressGateway egress = new EgressGateway()) {
18+
if (!egress.start(rtmpPort, host, roqrPort, stream, true)) {
19+
System.err.println("failed to start egress");
20+
System.exit(1);
21+
}
22+
if (!egress.waitPlaying(5000)) {
23+
System.err.println("warning: RoQR play not confirmed");
24+
}
25+
System.out.printf(
26+
"PlaySample: play with ffplay rtmp://127.0.0.1:%d/live/%s%n",
27+
rtmpPort, stream);
28+
System.out.println("Ctrl-C to stop.");
29+
Thread.currentThread().join();
30+
}
31+
}
32+
}

examples/java/PublishSample.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import org.red5.roqr.IngestGateway;
2+
3+
/** Bridges an RTMP publisher into a RoQR server.
4+
* Usage: PublishSample <rtmpPort> <roqrHost> <roqrPort> */
5+
public final class PublishSample {
6+
public static void main(String[] args) throws Exception {
7+
if (args.length < 3) {
8+
System.err.println(
9+
"usage: PublishSample <rtmpPort> <roqrHost> <roqrPort>");
10+
System.exit(2);
11+
}
12+
int rtmpPort = Integer.parseInt(args[0]);
13+
String host = args[1];
14+
int roqrPort = Integer.parseInt(args[2]);
15+
16+
try (IngestGateway ingest = new IngestGateway()) {
17+
if (!ingest.start(rtmpPort, host, roqrPort, true)) {
18+
System.err.println("failed to start ingest");
19+
System.exit(1);
20+
}
21+
System.out.printf(
22+
"PublishSample: publish RTMP to rtmp://127.0.0.1:%d/live/<name>%n",
23+
rtmpPort);
24+
System.out.println("Ctrl-C to stop.");
25+
Thread.currentThread().join();
26+
}
27+
}
28+
}

examples/java/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# libroqr Java samples
2+
3+
Build the JNI bindings and the samples:
4+
5+
```
6+
eval "$(scripts/setup_picoquic_deps.sh)"
7+
cmake --preset dev # ROQR_BUILD_JNI=ON in the dev preset
8+
cmake --build --preset dev
9+
```
10+
11+
Run (point java at the built native lib and jars):
12+
13+
```
14+
JNI_DIR=build/dev/jni
15+
JAR=$(find build/dev -name roqr.jar)
16+
SAMPLES=$(find build/dev -name roqr-samples.jar)
17+
18+
# Publisher gateway on :1935 -> RoQR server 127.0.0.1:4443
19+
java -Djava.library.path=$JNI_DIR -cp "$JAR:$SAMPLES" \
20+
PublishSample 1935 127.0.0.1 4443
21+
# then: ffmpeg ... -f flv rtmp://127.0.0.1:1935/live/cam
22+
23+
# Player gateway on :1936 <- RoQR server, stream "cam"
24+
java -Djava.library.path=$JNI_DIR -cp "$JAR:$SAMPLES" \
25+
PlaySample 1936 127.0.0.1 4443 cam
26+
# then: ffplay rtmp://127.0.0.1:1936/live/cam
27+
```
28+
29+
A `roqr-relayd --mode media` (or any RoQR server) must be running at the
30+
RoQR host/port.

ffi/CMakeLists.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
add_library(roqr-ffi SHARED
2+
src/roqr_ffi.cpp
3+
src/roqr_rtmp_ffi.cpp
4+
)
5+
6+
target_include_directories(roqr-ffi PUBLIC
7+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
8+
)
9+
10+
target_link_libraries(roqr-ffi PRIVATE roqr-core roqr-quic roqr-gateway)
11+
12+
target_compile_features(roqr-ffi PRIVATE cxx_std_20)
13+
target_compile_options(roqr-ffi PRIVATE
14+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>)
15+
16+
set_target_properties(roqr-ffi PROPERTIES
17+
VERSION ${PROJECT_VERSION}
18+
SOVERSION 0)
19+
20+
install(TARGETS roqr-ffi LIBRARY DESTINATION lib)
21+
install(DIRECTORY include/roqr DESTINATION include)

ffi/include/roqr/roqr.h

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#ifndef ROQR_H
2+
#define ROQR_H
3+
4+
#include <stddef.h>
5+
#include <stdint.h>
6+
7+
#ifdef __cplusplus
8+
extern "C" {
9+
#endif
10+
11+
/* Error codes. 0x00-0x07 mirror the RoQR draft Table 2 application error
12+
* codes; values >= 100 are FFI-local. */
13+
typedef enum roqr_error {
14+
ROQR_OK = 0,
15+
ROQR_ERR_GENERAL = 1,
16+
ROQR_ERR_INTERNAL = 2,
17+
ROQR_ERR_FRAME_ENCODING = 3,
18+
ROQR_ERR_STREAM_CREATION = 4,
19+
ROQR_ERR_FRAME_CANCELLED = 5,
20+
ROQR_ERR_UNKNOWN_FLOW = 6,
21+
ROQR_ERR_EXPECTATION_UNMET = 7,
22+
ROQR_ERR_INVALID_ARG = 100,
23+
ROQR_ERR_CONNECT_FAILED = 101,
24+
ROQR_ERR_TIMEOUT = 102
25+
} roqr_error;
26+
27+
typedef enum roqr_delivery_mode {
28+
ROQR_DELIVERY_STREAM = 0,
29+
ROQR_DELIVERY_DATAGRAM = 1,
30+
ROQR_DELIVERY_AUTO = 2
31+
} roqr_delivery_mode;
32+
33+
/* One RoQR frame crossing the ABI. On the receive callback, `payload`
34+
* points into memory owned by the library and valid only for the duration
35+
* of the callback; copy it if you need it later. */
36+
typedef struct roqr_frame {
37+
uint64_t flow_id;
38+
uint64_t timestamp;
39+
uint8_t message_type;
40+
uint64_t message_stream_id;
41+
uint64_t chunk_stream_id;
42+
const uint8_t* payload;
43+
size_t payload_len;
44+
} roqr_frame;
45+
46+
typedef struct roqr_client roqr_client;
47+
48+
/* Library version, "major.minor.patch". */
49+
const char* roqr_version(void);
50+
51+
roqr_client* roqr_client_create(void);
52+
void roqr_client_destroy(roqr_client* client);
53+
54+
/* Receive callback: fires on the QUIC network thread. Do NOT block and do
55+
* NOT call roqr_client_destroy or roqr_client_wait_* from inside it;
56+
* roqr_client_send/close/bind_flow/retire_flow are safe to call. `frame`
57+
* and its payload are valid only for the duration of the call. */
58+
typedef void (*roqr_message_cb)(const roqr_frame* frame, void* user_data);
59+
60+
/* Close callback: fires on the network thread with the peer's application
61+
* error code (0 for a clean close). Same non-blocking rules apply. */
62+
typedef void (*roqr_closed_cb)(uint64_t app_error_code, void* user_data);
63+
64+
/* Set handlers before roqr_client_connect. */
65+
void roqr_client_set_on_message(roqr_client* client, roqr_message_cb cb,
66+
void* user_data);
67+
void roqr_client_set_on_closed(roqr_client* client, roqr_closed_cb cb,
68+
void* user_data);
69+
70+
roqr_error roqr_client_connect(roqr_client* client, const char* host,
71+
uint16_t port, int insecure_skip_verify);
72+
/* Returns 1 if connected within timeout_ms, 0 on timeout. */
73+
int roqr_client_wait_connected(roqr_client* client, int timeout_ms);
74+
int roqr_client_datagrams_negotiated(roqr_client* client);
75+
76+
/* Thread-safe. Returns ROQR_ERR_INVALID_ARG for a null client/frame or an
77+
* empty payload (RoQR requires payload length > 0). */
78+
roqr_error roqr_client_send(roqr_client* client, const roqr_frame* frame,
79+
roqr_delivery_mode mode);
80+
81+
void roqr_client_bind_flow(roqr_client* client, uint64_t flow_id);
82+
void roqr_client_retire_flow(roqr_client* client, uint64_t flow_id);
83+
84+
void roqr_client_close(roqr_client* client, uint64_t app_error_code);
85+
int roqr_client_wait_closed(roqr_client* client, int timeout_ms);
86+
87+
#ifdef __cplusplus
88+
}
89+
#endif
90+
91+
#endif /* ROQR_H */

ffi/include/roqr/roqr_rtmp.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#ifndef ROQR_RTMP_H
2+
#define ROQR_RTMP_H
3+
4+
#include <stdint.h>
5+
6+
#include "roqr/roqr.h" /* roqr_error */
7+
8+
#ifdef __cplusplus
9+
extern "C" {
10+
#endif
11+
12+
typedef struct roqr_ingest roqr_ingest;
13+
typedef struct roqr_egress roqr_egress;
14+
15+
roqr_ingest* roqr_ingest_create(void);
16+
void roqr_ingest_destroy(roqr_ingest* ingest);
17+
/* Starts the RTMP listener on rtmp_port and connects to the RoQR server on
18+
* publish. ROQR_OK means the RTMP listener is up; it does NOT mean the RoQR
19+
* server leg is live. Wait for roqr_ingest_wait_publishing to confirm the
20+
* end-to-end path. */
21+
roqr_error roqr_ingest_start(roqr_ingest* ingest, uint16_t rtmp_port,
22+
const char* roqr_host, uint16_t roqr_port,
23+
int insecure_skip_verify);
24+
int roqr_ingest_wait_publishing(roqr_ingest* ingest, int timeout_ms);
25+
void roqr_ingest_stop(roqr_ingest* ingest);
26+
27+
roqr_egress* roqr_egress_create(void);
28+
void roqr_egress_destroy(roqr_egress* egress);
29+
/* Starts the RTMP listener on rtmp_port and plays stream_name from the RoQR
30+
* server. Same readiness caveat as ingest: wait for
31+
* roqr_egress_wait_playing. */
32+
roqr_error roqr_egress_start(roqr_egress* egress, uint16_t rtmp_port,
33+
const char* roqr_host, uint16_t roqr_port,
34+
const char* stream_name,
35+
int insecure_skip_verify);
36+
int roqr_egress_wait_playing(roqr_egress* egress, int timeout_ms);
37+
void roqr_egress_stop(roqr_egress* egress);
38+
39+
#ifdef __cplusplus
40+
}
41+
#endif
42+
43+
#endif /* ROQR_RTMP_H */

0 commit comments

Comments
 (0)