Skip to content

Commit 63a9034

Browse files
committed
Merge feature/plan-4-gateways: RTMP-RoQR gateways, media relay, ffmpeg e2e, and CI
2 parents e4b86e8 + 3a29c13 commit 63a9034

33 files changed

Lines changed: 1950 additions & 11 deletions

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
jobs:
9+
core:
10+
name: core-only (no picoquic)
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Install build tools
15+
run: sudo apt-get update && sudo apt-get install -y cmake g++ ninja-build
16+
- name: Configure (core only)
17+
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
18+
-DROQR_BUILD_QUIC=OFF -DROQR_BUILD_RTMP=ON
19+
-DROQR_BUILD_EXAMPLES=OFF -DROQR_BUILD_TESTS=ON
20+
- name: Build
21+
run: cmake --build build --parallel
22+
- name: Test
23+
run: ctest --test-dir build --output-on-failure
24+
25+
full:
26+
name: full (${{ matrix.cc }})
27+
runs-on: ubuntu-latest
28+
strategy:
29+
fail-fast: false
30+
matrix:
31+
include:
32+
- { cc: gcc, cxx: g++ }
33+
- { cc: clang, cxx: clang++ }
34+
steps:
35+
- uses: actions/checkout@v4
36+
- name: Install dependencies
37+
run: sudo apt-get update && sudo apt-get install -y
38+
cmake g++ clang ninja-build libssl-dev ffmpeg
39+
- name: Build picoquic deps
40+
run: eval "$(scripts/setup_picoquic_deps.sh)"; echo "$ROQR_PICOQUIC_SOURCE_DIR"
41+
- name: Configure
42+
env:
43+
CC: ${{ matrix.cc }}
44+
CXX: ${{ matrix.cxx }}
45+
run: |
46+
eval "$(scripts/setup_picoquic_deps.sh)"
47+
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \
48+
-DROQR_BUILD_QUIC=ON -DROQR_BUILD_RTMP=ON \
49+
-DROQR_BUILD_TOOLS=ON -DROQR_BUILD_EXAMPLES=ON \
50+
-DROQR_BUILD_TESTS=ON
51+
- name: Build
52+
run: cmake --build build --parallel
53+
- name: Test
54+
run: ctest --test-dir build --output-on-failure

CMakeLists.txt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,50 @@ set(CMAKE_CXX_STANDARD 20)
99
set(CMAKE_CXX_STANDARD_REQUIRED ON)
1010
set(CMAKE_CXX_EXTENSIONS OFF)
1111

12+
# Collocate all built executables (roqr-relayd, roqr-ingest, roqr-egress,
13+
# roqr-duplex, test binaries) in a single directory so tooling (e.g. the
14+
# ffmpeg e2e test) can find the gateway trio via one bin dir.
15+
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
16+
1217
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
1318

1419
option(ROQR_BUILD_TESTS "Build unit tests" ON)
1520
option(ROQR_BUILD_QUIC "Build the picoquic transport (needs picoquic)" OFF)
1621
option(ROQR_BUILD_TOOLS "Build tools (roqr-relayd; needs ROQR_BUILD_QUIC)" OFF)
1722
option(ROQR_BUILD_RTMP "Build the RTMP/AMF gateway module" ON)
23+
option(ROQR_BUILD_EXAMPLES "Build gateway library and example apps" ON)
1824

1925
add_subdirectory(core)
2026

2127
if(ROQR_BUILD_RTMP)
2228
add_subdirectory(rtmp)
2329
endif()
2430

31+
# The gateway library is needed by both the example binaries and the
32+
# relay's Media mode, so build it whenever its dependencies are present.
33+
if(ROQR_BUILD_QUIC AND ROQR_BUILD_RTMP)
34+
add_subdirectory(gateway)
35+
endif()
36+
37+
if(ROQR_BUILD_EXAMPLES)
38+
if(NOT ROQR_BUILD_QUIC OR NOT ROQR_BUILD_RTMP)
39+
message(FATAL_ERROR "ROQR_BUILD_EXAMPLES requires ROQR_BUILD_QUIC and ROQR_BUILD_RTMP")
40+
endif()
41+
if(ROQR_BUILD_TESTS AND NOT ROQR_BUILD_TOOLS)
42+
message(FATAL_ERROR "ROQR_BUILD_EXAMPLES with ROQR_BUILD_TESTS requires ROQR_BUILD_TOOLS")
43+
endif()
44+
add_subdirectory(examples)
45+
endif()
46+
2547
if(ROQR_BUILD_QUIC)
2648
find_package(Picoquic REQUIRED)
2749
find_package(OpenSSL REQUIRED)
2850
add_subdirectory(quic)
2951
endif()
3052

3153
if(ROQR_BUILD_TOOLS)
32-
if(NOT ROQR_BUILD_QUIC)
33-
message(FATAL_ERROR "ROQR_BUILD_TOOLS requires ROQR_BUILD_QUIC=ON")
54+
if(NOT ROQR_BUILD_QUIC OR NOT ROQR_BUILD_RTMP)
55+
message(FATAL_ERROR "ROQR_BUILD_TOOLS requires ROQR_BUILD_QUIC and ROQR_BUILD_RTMP")
3456
endif()
3557
add_subdirectory(tools/relayd)
3658
endif()

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# libroqr
2+
3+
A C++20 implementation of RoQR (RTMP over QUIC,
4+
draft-gregoire-rtmp-over-quic) with a sans-I/O protocol core, a picoquic
5+
transport, an RTMP/AMF gateway module, a test relay, and example gateways.
6+
7+
## Reference media path
8+
9+
```
10+
ffmpeg (RTMP publish) -> roqr-ingest -> roqr-relayd -> roqr-egress -> ffmpeg (RTMP play)
11+
```
12+
13+
RoQR carries RTMP message metadata and payloads over QUIC streams and
14+
DATAGRAM frames.
15+
16+
## Build
17+
18+
```
19+
eval "$(scripts/setup_picoquic_deps.sh)" # clone + build pinned picoquic/picotls
20+
cmake --preset dev
21+
cmake --build --preset dev
22+
ctest --preset dev
23+
```
24+
25+
The core protocol library builds without picoquic:
26+
`cmake -S . -B build -DROQR_BUILD_QUIC=OFF -DROQR_BUILD_EXAMPLES=OFF && cmake --build build`.
27+
28+
## Layout
29+
30+
- `core/` sans-I/O RoQR frame codec, flow table (no dependencies)
31+
- `quic/` picoquic client transport
32+
- `rtmp/` RTMP handshake, chunking, AMF0, E-RTMP media classifier
33+
- `gateway/` RTMP<->RoQR bridge, ingest/egress gateways
34+
- `tools/relayd/` the RoQR test relay
35+
- `examples/` roqr-ingest, roqr-egress, roqr-duplex
36+
37+
License: Apache-2.0.

examples/CMakeLists.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
add_executable(roqr-ingest ingest_main.cpp)
2+
target_link_libraries(roqr-ingest PRIVATE roqr-gateway)
3+
4+
add_executable(roqr-egress egress_main.cpp)
5+
target_link_libraries(roqr-egress PRIVATE roqr-gateway)
6+
7+
add_executable(roqr-duplex duplex_main.cpp)
8+
target_link_libraries(roqr-duplex PRIVATE roqr-gateway)
9+
10+
foreach(t roqr-ingest roqr-egress roqr-duplex)
11+
target_compile_options(${t} PRIVATE
12+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>)
13+
endforeach()

examples/duplex_main.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#include <csignal>
2+
#include <cstdio>
3+
#include <cstdlib>
4+
#include <cstring>
5+
6+
#include "roqr/gateway/egress.hpp"
7+
#include "roqr/gateway/ingest.hpp"
8+
9+
namespace {
10+
volatile std::sig_atomic_t g_stop = 0;
11+
void on_signal(int) { g_stop = 1; }
12+
} // namespace
13+
14+
int main(int argc, char** argv) {
15+
roqr::gateway::IngestOptions in;
16+
roqr::gateway::EgressOptions eg;
17+
in.rtmp_port = 1935;
18+
eg.rtmp_port = 1936;
19+
for (int i = 1; i < argc; ++i) {
20+
if (!std::strcmp(argv[i], "--roqr-host") && i + 1 < argc) {
21+
in.roqr_host = eg.roqr_host = argv[++i];
22+
} else if (!std::strcmp(argv[i], "--roqr-port") && i + 1 < argc) {
23+
in.roqr_port = eg.roqr_port =
24+
static_cast<uint16_t>(std::atoi(argv[++i]));
25+
} else if (!std::strcmp(argv[i], "--stream") && i + 1 < argc) {
26+
eg.stream_name = argv[++i];
27+
} else {
28+
std::fprintf(stderr,
29+
"usage: roqr-duplex [--roqr-host H] "
30+
"[--roqr-port P] [--stream NAME]\n");
31+
return 2;
32+
}
33+
}
34+
roqr::gateway::IngestGateway ingest;
35+
roqr::gateway::EgressGateway egress;
36+
if (!ingest.start(in) || !egress.start(eg)) {
37+
std::fprintf(stderr, "duplex: failed to start\n");
38+
return 1;
39+
}
40+
std::printf("roqr-duplex: ingest RTMP :%u, egress RTMP :%u, RoQR %s:%u\n",
41+
in.rtmp_port, eg.rtmp_port, in.roqr_host.c_str(),
42+
in.roqr_port);
43+
std::signal(SIGINT, on_signal);
44+
std::signal(SIGTERM, on_signal);
45+
while (!g_stop) {
46+
struct timespec ts {0, 200'000'000};
47+
nanosleep(&ts, nullptr);
48+
}
49+
ingest.stop();
50+
egress.stop();
51+
return 0;
52+
}

examples/egress_main.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#include <csignal>
2+
#include <cstdio>
3+
#include <cstdlib>
4+
#include <cstring>
5+
6+
#include "roqr/gateway/egress.hpp"
7+
8+
namespace {
9+
volatile std::sig_atomic_t g_stop = 0;
10+
void on_signal(int) { g_stop = 1; }
11+
} // namespace
12+
13+
int main(int argc, char** argv) {
14+
roqr::gateway::EgressOptions o;
15+
for (int i = 1; i < argc; ++i) {
16+
if (!std::strcmp(argv[i], "--rtmp-port") && i + 1 < argc) {
17+
o.rtmp_port = static_cast<uint16_t>(std::atoi(argv[++i]));
18+
} else if (!std::strcmp(argv[i], "--roqr-host") && i + 1 < argc) {
19+
o.roqr_host = argv[++i];
20+
} else if (!std::strcmp(argv[i], "--roqr-port") && i + 1 < argc) {
21+
o.roqr_port = static_cast<uint16_t>(std::atoi(argv[++i]));
22+
} else if (!std::strcmp(argv[i], "--stream") && i + 1 < argc) {
23+
o.stream_name = argv[++i];
24+
} else {
25+
std::fprintf(stderr,
26+
"usage: roqr-egress [--rtmp-port P] "
27+
"[--roqr-host H] [--roqr-port P] [--stream NAME]\n");
28+
return 2;
29+
}
30+
}
31+
roqr::gateway::EgressGateway g;
32+
if (!g.start(o)) {
33+
std::fprintf(stderr, "egress: failed to start\n");
34+
return 1;
35+
}
36+
std::printf("roqr-egress: RTMP :%u <- RoQR %s:%u (stream: %s)\n", o.rtmp_port,
37+
o.roqr_host.c_str(), o.roqr_port, o.stream_name.c_str());
38+
std::signal(SIGINT, on_signal);
39+
std::signal(SIGTERM, on_signal);
40+
while (!g_stop) {
41+
struct timespec ts {0, 200'000'000};
42+
nanosleep(&ts, nullptr);
43+
}
44+
g.stop();
45+
return 0;
46+
}

examples/ingest_main.cpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#include <csignal>
2+
#include <cstdio>
3+
#include <cstdlib>
4+
#include <cstring>
5+
6+
#include "roqr/gateway/ingest.hpp"
7+
8+
namespace {
9+
volatile std::sig_atomic_t g_stop = 0;
10+
void on_signal(int) { g_stop = 1; }
11+
} // namespace
12+
13+
int main(int argc, char** argv) {
14+
roqr::gateway::IngestOptions o;
15+
for (int i = 1; i < argc; ++i) {
16+
if (!std::strcmp(argv[i], "--rtmp-port") && i + 1 < argc) {
17+
o.rtmp_port = static_cast<uint16_t>(std::atoi(argv[++i]));
18+
} else if (!std::strcmp(argv[i], "--roqr-host") && i + 1 < argc) {
19+
o.roqr_host = argv[++i];
20+
} else if (!std::strcmp(argv[i], "--roqr-port") && i + 1 < argc) {
21+
o.roqr_port = static_cast<uint16_t>(std::atoi(argv[++i]));
22+
} else {
23+
std::fprintf(stderr,
24+
"usage: roqr-ingest [--rtmp-port P] "
25+
"[--roqr-host H] [--roqr-port P]\n");
26+
return 2;
27+
}
28+
}
29+
roqr::gateway::IngestGateway g;
30+
if (!g.start(o)) {
31+
std::fprintf(stderr, "ingest: failed to start\n");
32+
return 1;
33+
}
34+
std::printf("roqr-ingest: RTMP :%u -> RoQR %s:%u\n", o.rtmp_port,
35+
o.roqr_host.c_str(), o.roqr_port);
36+
std::signal(SIGINT, on_signal);
37+
std::signal(SIGTERM, on_signal);
38+
while (!g_stop) {
39+
struct timespec ts {0, 200'000'000};
40+
nanosleep(&ts, nullptr);
41+
}
42+
g.stop();
43+
return 0;
44+
}

gateway/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
add_library(roqr-gateway STATIC
2+
src/bridge.cpp
3+
src/rtmp_commands.cpp
4+
src/ingest.cpp
5+
src/egress.cpp
6+
)
7+
8+
target_include_directories(roqr-gateway PUBLIC
9+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
10+
)
11+
12+
target_link_libraries(roqr-gateway PUBLIC roqr-core roqr-quic roqr-rtmp)
13+
14+
target_compile_features(roqr-gateway PUBLIC cxx_std_20)
15+
target_compile_options(roqr-gateway PRIVATE
16+
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>)
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
5+
#include "roqr/frame.hpp"
6+
#include "roqr/rtmp/message.hpp"
7+
8+
namespace roqr::gateway {
9+
10+
// Widen an RTMP message into a RoQR frame on the given flow. All RTMP
11+
// metadata fits in the wider RoQR fields, so this never fails. The RoQR
12+
// timestamp carries the fully resolved RTMP message timestamp (draft
13+
// s7.3); the caller must not send a frame with an empty payload (RoQR
14+
// requires Payload Length > 0).
15+
roqr::Frame to_frame(const roqr::rtmp::RtmpMessage& msg, uint64_t flow_id);
16+
17+
// Narrow a RoQR frame back into an RTMP message. Returns false and leaves
18+
// out untouched if timestamp, message_stream_id, or chunk_stream_id exceeds
19+
// 0xFFFFFFFF (the Width bridge rule).
20+
bool to_rtmp(const roqr::Frame& frame, roqr::rtmp::RtmpMessage& out);
21+
22+
} // namespace roqr::gateway
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#pragma once
2+
3+
#include <chrono>
4+
#include <cstdint>
5+
#include <memory>
6+
#include <string>
7+
8+
namespace roqr::gateway {
9+
10+
struct EgressOptions {
11+
uint16_t rtmp_port = 1936;
12+
std::string roqr_host = "127.0.0.1";
13+
uint16_t roqr_port = 4443;
14+
std::string stream_name = "cam";
15+
bool insecure_skip_verify = true;
16+
};
17+
18+
// Accepts one RTMP player (ffplay) on rtmp_port, connects to the RoQR
19+
// server, plays stream_name, and serves received media to the player.
20+
// Applies draft s8 gap recovery: after a suspected datagram gap, drops
21+
// non-keyframe video until the next keyframe/sequence header.
22+
class EgressGateway {
23+
public:
24+
EgressGateway();
25+
~EgressGateway();
26+
bool start(const EgressOptions& options);
27+
void stop();
28+
bool wait_playing(std::chrono::milliseconds timeout);
29+
30+
private:
31+
struct Impl;
32+
std::unique_ptr<Impl> impl_;
33+
};
34+
35+
} // namespace roqr::gateway

0 commit comments

Comments
 (0)