Skip to content

feat: Add RTSPS (TLS) and SDES-SRTP support for RTSP pull streams#2004

Open
devedse wants to merge 5 commits intoOvenMediaLabs:masterfrom
devedse:feature/rtsps-support
Open

feat: Add RTSPS (TLS) and SDES-SRTP support for RTSP pull streams#2004
devedse wants to merge 5 commits intoOvenMediaLabs:masterfrom
devedse:feature/rtsps-support

Conversation

@devedse
Copy link
Copy Markdown
Contributor

@devedse devedse commented Feb 27, 2026

Summary

Adds support for rtsps:// URLs in the RTSP pull provider, enabling encrypted RTSP streaming with TLS and SDES-SRTP.

What this does

  • RTSPS (TLS): Supports rtsps:// scheme with TLS handshake, encrypted signalling and interleaved data via TlsClientDataIoCallback
  • SDES-SRTP: Parses a=crypto: SDP attributes (RFC 4568) and sets up per-channel SRTP sessions for encrypted RTP/RTCP
  • Crypto suites: AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, AEAD_AES_128_GCM
  • Pipeline: Chains SrtpTransport between RtpRtcp and RtspcStream nodes when crypto is present
  • Track lookup: Preserves RtspData channel ID through SRTP decryption so RtpRtcp can resolve tracks correctly

Why it's needed

Many IP cameras and RTSP sources (e.g. Unifi cameras) only expose encrypted RTSPS endpoints. Without this, OvenMediaEngine cannot pull from these sources.

Files changed (9)

  • orchestrator.cpp — Register rtsps scheme as RtspPull provider
  • rtspc_stream.cpp/h — TLS connection, SRTP setup, encrypted send/recv
  • srtp_transport.cpp/h — Per-channel SRTP sessions for RTSP interleaved mode
  • rtp_rtcp.cpp — Channel-based track lookup through SRTP node
  • media_description.cpp/h — Parse a=crypto: SDP attributes
  • sdp_regex_pattern.h — Regex for crypto attribute parsing

- Add rtsps:// scheme support with TLS handshake via TlsClientDataIoCallback
- Parse a=crypto SDP attributes (RFC 4568) for SDES-SRTP key exchange
- Per-channel SRTP sessions in SrtpTransport for interleaved RTSP
- Preserve RtspData channel ID through SRTP decryption for track lookup
- Support AES_CM_128_HMAC_SHA1_80, AES_CM_128_HMAC_SHA1_32, AEAD_AES_128_GCM
- Register rtsps scheme in orchestrator as RtspPull provider
@getroot
Copy link
Copy Markdown
Member

getroot commented Mar 26, 2026

Thank you for the PR.

Now that the new version (0.20.5) has been released, we can start reviewing this PR. Sorry for the long delay.

In the meantime, there have been many changes, so this PR now has conflicts. Could you please resolve them?

Also, unfortunately, I do not have a camera that supports RTSPS, so I am unable to test it myself. If you could provide an RTSPS camera URL, it would be very helpful for the review. If possible, I would appreciate it if you could send the RTSPS URL to support@ovenmedialabs.com
.

@getroot getroot self-requested a review March 26, 2026 08:31
Copy link
Copy Markdown
Member

@getroot getroot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, I did a rough initial review at the code level. Once the conflicts are resolved and testing becomes possible, I will review it more thoroughly.


stop_watch.Update();

if (RequestDescribe() == false)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is difficult to understand the intention behind merging the RequestDescribe step into ConnectTo. Was there a specific reason for this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So just to be absolutely honest, I'm not really a C++ developer nor experienced in the whole streaming stuff. My knowledge is mainly on usage as a consumer. I am though experienced in other programming languages so I can at least understand most of the changes that were made.

In this case this seems to have been a change done by AI without any specific reason. I've restored RequestDescribe() as a separate method now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit: 002f2c3

return false;
}

constexpr size_t SRTP_KEY_MATERIAL_LENGTH = 30; // master key (16) + master salt (14)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For AEAD_AES_128_GCM, it should be 28 bytes. It seems this needs to be fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly have no opinion nor knowledge on this. Copilot came up with this:

You're right, that's a bug. I had a single constant of 30 bytes for all suites, but AEAD_AES_128_GCM uses a 12-byte salt instead of 14, so it needs 28 bytes. Fixed it so the expected key length is now determined per crypto suite alongside the suite selection.

But please verify this one thoroughly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit: 1e32d48

return true;
}

bool SrtpTransport::AddChannelKeyMaterial(uint8_t rtp_channel_id, uint64_t crypto_suite, std::shared_ptr<ov::Data> key)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It creates only the recv session and does not set up _send_session. Since OnDataReceivedFromPrevNode() immediately returns false when _send_session is null, all RTCP Receiver Reports sent from OME to the camera are dropped. In SDES-SRTP, key negotiation for OME's outgoing stream and initialization of _send_session are required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, no input from my side on this one:

Copilot response:

That's a significant oversight, thanks for the detailed explanation. You're absolutely right, without send sessions all outgoing RTCP gets silently dropped since [OnDataReceivedFromPrevNode()] bails out on the null check. I've fixed this by having [AddChannelKeyMaterial()] create both inbound and outbound sessions per channel using the same key (as SDES-SRTP uses symmetric keying). The outgoing path now looks up the correct per-channel send session using an SSRC to channel mapping learned from incoming RTP.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit: e8c3498

@devedse devedse requested a review from a team as a code owner March 27, 2026 13:27
@devedse devedse requested review from getroot and removed request for a team March 27, 2026 13:27
devedse added 3 commits March 27, 2026 14:49
ConnectTo() now only handles TCP connection and TLS handshake.
The RTSP DESCRIBE request is moved back to its own RequestDescribe()
method, called separately from StartStream(), restoring the original
separation of concerns.
AEAD_AES_128_GCM requires 28 bytes (16-byte key + 12-byte salt),
not 30 bytes like AES_CM_128_HMAC_SHA1_80/32 (16-byte key + 14-byte
salt). Replace the fixed constant with per-suite expected lengths.
AddChannelKeyMaterial() now creates both recv (inbound) and send
(outbound) sessions per channel, using the same key for both
directions as required by SDES-SRTP (RFC 4568).

OnDataReceivedFromPrevNode() now supports per-channel send session
lookup for outgoing RTCP. It learns SSRC-to-channel mappings from
incoming RTP packets and uses the first report block SSRC in outgoing
RTCP to select the correct send session, with a fallback to the first
available session.

The send path now forwards with explicit NodeType (Srtp/Srtcp) so
downstream nodes can distinguish encrypted RTP from RTCP.

RtspcStream::OnDataReceivedFromPrevNode() now handles NodeType::Srtcp
alongside NodeType::Rtcp for SRTP-encrypted outgoing RTCP.
@devedse
Copy link
Copy Markdown
Contributor Author

devedse commented Mar 27, 2026

I merged the changes from main into this branch. Could you please carefully re-review again?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds encrypted RTSP pull support by introducing rtsps:// (RTSP over TLS) signalling and SDES-SRTP handling for interleaved RTP/RTCP, including SDP a=crypto parsing and per-channel SRTP sessions.

Changes:

  • Register rtsps:// URLs to use the existing RTSP pull provider path.
  • Add TLS handshake + encrypted RTSP signalling (send/receive) in RtspcStream.
  • Parse SDP a=crypto: attributes and configure per-interleaved-channel SRTP via SrtpTransport, with track resolution updates in RtpRtcp.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/projects/providers/rtspc/rtspc_stream.h Adds TLS callback interface + SRTP/TLS state fields to RTSP pull stream.
src/projects/providers/rtspc/rtspc_stream.cpp Implements RTSPS connect/handshake, TLS send/recv, SDP crypto detection, and SRTP transport chaining.
src/projects/orchestrator/orchestrator.cpp Maps rtsps scheme to ProviderType::RtspPull.
src/projects/modules/sdp/sdp_regex_pattern.h Adds regex + match function for a=crypto: attributes.
src/projects/modules/sdp/media_description.h Introduces CryptoAttr storage/accessors for SDP crypto lines.
src/projects/modules/sdp/media_description.cpp Implements parsing/storage of a=crypto: plus accessors.
src/projects/modules/rtp_rtcp/rtp_rtcp.cpp Enables RTSP/SRTP channel-based track lookup using RtspData wrappers.
src/projects/modules/dtls_srtp/srtp_transport.h Adds per-channel SRTP keying APIs/state for RTSP interleaved mode.
src/projects/modules/dtls_srtp/srtp_transport.cpp Implements per-channel SRTP sessions and preserves RtspData channel IDs through decrypt/encrypt.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

session->Release();
}
}
_channel_send_sessions.clear();
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SrtpTransport::Stop() clears the per-channel session maps but leaves _ssrc_to_channel populated. This mapping can persist across restarts and grow unnecessarily; clear it during Stop() alongside the other per-stream state.

Suggested change
_channel_send_sessions.clear();
_channel_send_sessions.clear();
_ssrc_to_channel.clear();

Copilot uses AI. Check for mistakes.
Comment on lines 969 to +975
{
type = ProviderType::RtspPull;
}
else if (lower_scheme == "rtsps")
{
type = ProviderType::RtspPull;
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rtsps scheme handling is functionally identical to the rtsp branch and duplicates the assignment. This can be simplified to a single condition (e.g., (lower_scheme == "rtsp" || lower_scheme == "rtsps")) to reduce branching and keep scheme mappings easier to maintain.

Copilot uses AI. Check for mistakes.
Comment thread src/projects/modules/sdp/media_description.cpp
Comment on lines +1401 to +1410
size_t received_length = 0;
auto error = _signalling_socket->Recv(data, length, &received_length);
if (error != nullptr)
{
if (error->GetCode() == EAGAIN || error->GetCode() == EWOULDBLOCK)
{
// Non-blocking socket would block
errno = EAGAIN;
return -1;
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OnTlsReadData() calls _signalling_socket->Recv(...) without MSG_DONTWAIT / non_block=true. In PLAYING state ReceivePacket(true) relies on TLS reads being non-blocking; this callback can block indefinitely and stall the pull loop (ping, demux, etc.). Consider making socket reads non-blocking after the TLS handshake (or implementing a non-blocking handshake loop) and calling Recv(..., /non_block=/true) in that mode so SSL_read can return SSL_ERROR_WANT_READ instead of blocking.

Copilot uses AI. Check for mistakes.
Comment on lines +1074 to +1078
if (non_block)
{
// Retry later for non-blocking mode
return true;
}
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In TLS mode, if Decrypt() returns false (fatal TLS error / protocol error), the non_block path returns true and continues as if no error occurred. This can hide TLS failures and leave the stream stuck. Treat Decrypt()==false as an error regardless of non_block (only empty decrypted data should be used for retry semantics).

Suggested change
if (non_block)
{
// Retry later for non-blocking mode
return true;
}
// Treat TLS decryption failure as a fatal error regardless of non_block

Copilot uses AI. Check for mistakes.
Comment thread src/projects/providers/rtspc/rtspc_stream.cpp
@devedse
Copy link
Copy Markdown
Contributor Author

devedse commented Apr 10, 2026

@getroot I think it would be best if you pick up the remaining changes. I'm not sure which ones make sense.

@getroot
Copy link
Copy Markdown
Member

getroot commented Apr 17, 2026

@devedse Understood. I have many high-priority tasks right now, so I will handle this later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants