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: 6 additions & 5 deletions rtsp/src/main/java/com/pedro/rtsp/rtsp/commands/SdpBody.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ object SdpBody {
} else "a=control:streamid=$trackAudio\r\n"
return "m=audio $port $type ${payload}\r\n" +
"a=rtpmap:$payload OPUS/${RtpConstants.clockOpusFrequency}/2\r\n" +
"a=fmtp:$payload sprop-stereo=${isStereo.toInt()}; sprop-maxcapturerate=$sampleRate\r\n" +
"a=fmtp:$payload sprop-stereo=${isStereo.toInt()};sprop-maxcapturerate=$sampleRate\r\n" +
identifier
}

Expand Down Expand Up @@ -72,7 +72,7 @@ object SdpBody {
} else "a=control:streamid=$trackAudio\r\n"
return "m=audio $port $type ${payload}\r\n" +
"a=rtpmap:$payload MPEG4-GENERIC/$sampleRate/$channels\r\n" +
"a=fmtp:$payload profile-level-id=1; mode=AAC-hbr; config=$configHex; sizelength=13; indexlength=3; indexdeltalength=3\r\n" +
"a=fmtp:$payload profile-level-id=1;mode=AAC-hbr;config=$configHex;sizelength=13;indexlength=3;indexdeltalength=3\r\n" +
identifier
}

Expand All @@ -88,7 +88,7 @@ object SdpBody {
} else "a=control:streamid=$trackVideo\r\n"
return "m=video $port $type $payload\r\n" +
"a=rtpmap:$payload AV1/${RtpConstants.clockVideoFrequency}\r\n" +
"a=fmtp:$payload profile=${sequenceHeader.seqProfile}; level-idx=${sequenceHeader.seqLevelIdx}\r\n" +
"a=fmtp:$payload profile=${sequenceHeader.seqProfile};level-idx=${sequenceHeader.seqLevelIdx}\r\n" +
identifier
}

Expand All @@ -103,13 +103,14 @@ object SdpBody {
val profileLevelId = "%02x%02x%02x".format(
spsH264Parser.profileIdc, spsH264Parser.profileCompatibility, spsH264Parser.levelIdc
)
val asymmetryAllowed = if (secured) "level-asymmetry-allowed=1;" else ""
val identifier = if (secured) {
"a=sendonly\r\n" +
"a=mid:$trackVideo\r\n"
} else "a=control:streamid=$trackVideo\r\n"
return "m=video $port $type $payload\r\n" +
"a=rtpmap:$payload H264/${RtpConstants.clockVideoFrequency}\r\n" +
"a=fmtp:$payload packetization-mode=1; sprop-parameter-sets=$spsString,$ppsString; profile-level-id=$profileLevelId\r\n" +
"a=fmtp:$payload ${asymmetryAllowed}packetization-mode=1;sprop-parameter-sets=$spsString,$ppsString;profile-level-id=$profileLevelId\r\n" +
identifier
}

Expand All @@ -126,7 +127,7 @@ object SdpBody {
} else "a=control:streamid=$trackVideo\r\n"
return "m=video $port $type ${payload}\r\n" +
"a=rtpmap:$payload H265/${RtpConstants.clockVideoFrequency}\r\n" +
"a=fmtp:$payload sprop-sps=$spsString; sprop-pps=$ppsString; sprop-vps=$vpsString\r\n" +
"a=fmtp:$payload sprop-sps=$spsString;sprop-pps=$ppsString;sprop-vps=$vpsString\r\n" +
identifier
}

Expand Down
8 changes: 4 additions & 4 deletions rtsp/src/test/java/com/pedro/rtsp/rtsp/SdpBodyTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ class SdpBodyTest {

val stereo48k = SdpBody.createOpusBody(track, 48000, true)
assertTrue(stereo48k.contains(expectedType))
assertTrue(stereo48k.contains("a=fmtp:$payload sprop-stereo=1; sprop-maxcapturerate=48000\r\n"))
assertTrue(stereo48k.contains("a=fmtp:$payload sprop-stereo=1;sprop-maxcapturerate=48000\r\n"))

val mono16k = SdpBody.createOpusBody(track, 16000, false)
assertTrue(mono16k.contains(expectedType))
assertTrue(mono16k.contains("a=fmtp:$payload sprop-stereo=0; sprop-maxcapturerate=16000\r\n"))
assertTrue(mono16k.contains("a=fmtp:$payload sprop-stereo=0;sprop-maxcapturerate=16000\r\n"))
}

@Test
Expand Down Expand Up @@ -183,7 +183,7 @@ class SdpBodyTest {
val track = 1

val expectedType = "H265/${RtpConstants.clockVideoFrequency}"
val expectedConfig = "sprop-sps=QgEBAQ==; sprop-pps=RAHAZg==; sprop-vps=QAEMAQ=="
val expectedConfig = "sprop-sps=QgEBAQ==;sprop-pps=RAHAZg==;sprop-vps=QAEMAQ=="
val expectedPayload = "a=rtpmap:${RtpConstants.payloadType + track}"
val expectedTrack = "a=control:streamid=${track}"

Expand All @@ -209,7 +209,7 @@ class SdpBodyTest {
assertTrue(result.contains(expectedPayload))
assertTrue(result.contains(expectedTrack))
//seq_profile and seq_level_idx of the obu payload. Parsing the raw obu instead would report 8
assertTrue(result.contains("a=fmtp:${RtpConstants.payloadType + track} profile=0; level-idx=4\r\n"))
assertTrue(result.contains("a=fmtp:${RtpConstants.payloadType + track} profile=0;level-idx=4\r\n"))
}

@Test
Expand Down
53 changes: 37 additions & 16 deletions whip/src/main/java/com/pedro/whip/WhipClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import com.pedro.common.socket.base.UdpStreamSocket
import com.pedro.common.toMediaFrameInfo
import com.pedro.common.toUInt32
import com.pedro.common.validMessage
import com.pedro.rtsp.utils.CryptoProperties
import com.pedro.rtsp.utils.RtpConstants
import com.pedro.whip.dtls.DtlsClientConnection
import com.pedro.whip.dtls.DtlsConnection
import com.pedro.whip.dtls.DtlsServerConnection
import com.pedro.whip.dtls.DtlsTransport
import com.pedro.whip.dtls.SrtpKeys
import com.pedro.whip.utils.Network
import com.pedro.whip.webrtc.CandidateType
import com.pedro.whip.webrtc.CommandsManager
Expand Down Expand Up @@ -269,9 +271,13 @@ class WhipClient(private val connectChecker: ConnectChecker) {

val host = remoteCandidates[0].getRealHost()
val port = remoteCandidates[0].getRealPort()
val socket = StreamSocket.createUdpSocket(socketType, host, port, socketTimeout, receiveSize = RtpConstants.MTU)
socket.connect()
//bind socket instead of connect to avoid ignore packets received from other port
val bindHost = if (host.contains(":")) "::" else "0.0.0.0"
val socket = StreamSocket.createUdpSocket(socketType, bindHost, 0, socketTimeout)
socket.bind()
socket.setRemoteAddress(host, port)

val dtlsTransport = DtlsTransport(socket)
val requestId = commandsManager.generateTransactionId()
val userName = StunAttributeValueParser.createUserName(localFrag, remoteFrag)
val attributes = listOf(
Expand All @@ -283,8 +289,13 @@ class WhipClient(private val connectChecker: ConnectChecker) {

var candidateResponses = 0
var requestSuccessReceived = false
while (candidateResponses < 1 || !requestSuccessReceived) {
val command = commandsManager.readStun(socket)
while (!requestSuccessReceived) {
val bytes = socket.read()
if (bytes.isNotEmpty() && (bytes[0].toInt() and 0xFF) in 20..63) {
dtlsTransport.enqueue(bytes)
continue
}
val command = commandsManager.readStun(bytes)
if (command.header.id.contentEquals(requestId) && command.header.type == HeaderType.SUCCESS) {
requestSuccessReceived = true
} else if (command.header.type == HeaderType.REQUEST) {
Expand All @@ -308,7 +319,12 @@ class WhipClient(private val connectChecker: ConnectChecker) {

var nominateSuccessReceived = false
while (!nominateSuccessReceived) {
val command = commandsManager.readStun(socket)
val bytes = socket.read()
if (bytes.isNotEmpty() && (bytes[0].toInt() and 0xFF) in 20..63) {
dtlsTransport.enqueue(bytes)
continue
}
val command = commandsManager.readStun(bytes)
if (command.header.id.contentEquals(nominateId) && command.header.type == HeaderType.SUCCESS) {
nominateSuccessReceived = true
} else if (command.header.type == HeaderType.REQUEST) {
Expand All @@ -324,9 +340,14 @@ class WhipClient(private val connectChecker: ConnectChecker) {
val certificate = commandsManager.certificate ?: return@launch
val fingerprint = commandsManager.remoteSdpInfo?.fingerprint ?: return@launch

val dtlsResult = CompletableDeferred<Result<List<CryptoProperties>>>()
val dtlsTransport = DtlsTransport(socket)
dtlsConnection = DtlsConnection(certificate, fingerprint)
val dtlsResult = CompletableDeferred<Result<SrtpKeys>>()
val isDtlsServer = commandsManager.remoteSdpInfo?.isActiveMode ?: false
Log.i(TAG, "Using dtls ${if (isDtlsServer) "server" else "client"} role")
dtlsConnection = if (isDtlsServer) {
DtlsServerConnection(certificate, fingerprint)
} else {
DtlsClientConnection(certificate, fingerprint)
}

val dispatchJob = launch {
while (isActive) {
Expand All @@ -339,18 +360,18 @@ class WhipClient(private val connectChecker: ConnectChecker) {
}

dtlsConnection?.start(dtlsTransport, object : DtlsConnection.Callback {
override fun onHandshakeComplete(properties: List<CryptoProperties>) {
dtlsResult.complete(Result.success(properties))
override fun onHandshakeComplete(keys: SrtpKeys) {
dtlsResult.complete(Result.success(keys))
}
override fun onHandshakeFailed(reason: String?) {
dtlsResult.complete(Result.failure(Exception(reason ?: "DTLS handshake failed")))
}
})

val result = withTimeoutOrNull(5_000.milliseconds) { dtlsResult.await() }
val result = withTimeoutOrNull(10_000.milliseconds) { dtlsResult.await() }
?: Result.failure(Exception("timeout"))
val cryptoProperties = result.getOrNull()
if (cryptoProperties == null) {
val srtpKeys = result.getOrNull()
if (srtpKeys == null) {
dispatchJob.cancel()
dtlsConnection?.close()
dtlsConnection = null
Expand All @@ -362,7 +383,7 @@ class WhipClient(private val connectChecker: ConnectChecker) {
Log.i(TAG, "dtls connected!!")
onMainThread { connectChecker.onConnectionSuccess() }
whipSender.setSocketsInfo(socket)
whipSender.setCrypto(cryptoProperties[1])
whipSender.setCrypto(srtpKeys.rtp, srtpKeys.rtcp)
whipSender.start()
}.exceptionOrNull()
if (error != null) {
Expand Down Expand Up @@ -412,9 +433,9 @@ class WhipClient(private val connectChecker: ConnectChecker) {
}

private suspend fun disconnect(clear: Boolean) {
if (isStreaming) whipSender.stop()
dtlsConnection?.close()
dtlsConnection = null
if (isStreaming) whipSender.stop()
val error = runCatching {
withTimeoutOrNull(100.milliseconds) {
commandsManager.writeDelete()
Expand Down
8 changes: 4 additions & 4 deletions whip/src/main/java/com/pedro/whip/WhipSender.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ class WhipSender(
baseSenderReport = BaseSenderReport.getInstance(commandsManager.rtpTracks, socket)
}

fun setCrypto(properties: CryptoProperties) {
baseSenderReport?.setCrypto(properties)
videoPacket.setCryptoProperties(properties)
audioPacket.setCryptoProperties(properties)
fun setCrypto(rtpProperties: CryptoProperties, rtcpProperties: CryptoProperties) {
baseSenderReport?.setCrypto(rtcpProperties)
videoPacket.setCryptoProperties(rtpProperties)
audioPacket.setCryptoProperties(rtpProperties)
}

override fun setVideoInfo(sps: ByteBuffer, pps: ByteBuffer?, vps: ByteBuffer?) {
Expand Down
112 changes: 112 additions & 0 deletions whip/src/main/java/com/pedro/whip/dtls/DtlsClientConnection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
*
* * Copyright (C) 2024 pedroSG94.
* *
* * 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 com.pedro.whip.dtls

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.bouncycastle.tls.AlertDescription
import org.bouncycastle.tls.CertificateRequest
import org.bouncycastle.tls.DTLSClientProtocol
import org.bouncycastle.tls.DTLSTransport
import org.bouncycastle.tls.DefaultTlsClient
import org.bouncycastle.tls.ProtocolVersion
import org.bouncycastle.tls.SRTPProtectionProfile
import org.bouncycastle.tls.SignatureAlgorithm
import org.bouncycastle.tls.TlsAuthentication
import org.bouncycastle.tls.TlsCredentials
import org.bouncycastle.tls.TlsExtensionsUtils
import org.bouncycastle.tls.TlsFatalAlert
import org.bouncycastle.tls.TlsSRTPUtils
import org.bouncycastle.tls.TlsServerCertificate
import org.bouncycastle.tls.TlsUtils
import org.bouncycastle.tls.UseSRTPData
import java.util.Hashtable

/**
* DTLS client role, used when the remote answer is a=setup:passive so we must start the handshake.
*/
class DtlsClientConnection(
private val certificate: DtlsCertificate,
private val remoteFingerprint: String
) : DefaultTlsClient(certificate.crypto), DtlsConnection {

private val srtpProfile = SRTPProtectionProfile.SRTP_AES128_CM_HMAC_SHA1_80
private var dtlsTransport: DTLSTransport? = null
private var exportedSrtpKeys: ByteArray? = null

override fun start(transport: DtlsTransport, callback: DtlsConnection.Callback) {
CoroutineScope(Dispatchers.IO).launch {
try {
dtlsTransport = DTLSClientProtocol().connect(this@DtlsClientConnection, transport)
val keys = exportedSrtpKeys
?: throw IllegalStateException("SRTP keying material not captured during handshake")
callback.onHandshakeComplete(DtlsUtils.deriveSrtpKeys(keys, isClient = true))
} catch (e: Exception) {
callback.onHandshakeFailed(e.message)
}
}
}

override fun close() {
runCatching { dtlsTransport?.close() }
}

override fun getSupportedVersions(): Array<ProtocolVersion> =
arrayOf(ProtocolVersion.DTLSv12)

override fun getClientExtensions(): Hashtable<*, *> {
val extensions = TlsExtensionsUtils.ensureExtensionsInitialised(super.getClientExtensions())
TlsSRTPUtils.addUseSRTPExtension(extensions, UseSRTPData(intArrayOf(srtpProfile), TlsUtils.EMPTY_BYTES))
return extensions
}

override fun processServerExtensions(serverExtensions: Hashtable<*, *>?) {
super.processServerExtensions(serverExtensions)
//the server must accept use_srtp with our profile, if not we can't derive the SRTP keys
val useSrtp = TlsSRTPUtils.getUseSRTPExtension(serverExtensions)
?: throw TlsFatalAlert(AlertDescription.handshake_failure, "server doesn't support use_srtp")
if (useSrtp.protectionProfiles.size != 1 || useSrtp.protectionProfiles[0] != srtpProfile) {
throw TlsFatalAlert(AlertDescription.illegal_parameter, "invalid SRTP protection profile")
}
}

override fun getAuthentication(): TlsAuthentication {
val context = context
return object : TlsAuthentication {
override fun notifyServerCertificate(serverCertificate: TlsServerCertificate) {
DtlsUtils.verifyFingerprint(serverCertificate.certificate, remoteFingerprint)
}

//WebRTC requires mutual authentication, the server validates our fingerprint
override fun getClientCredentials(certificateRequest: CertificateRequest): TlsCredentials {
val sigAlgs = certificateRequest.supportedSignatureAlgorithms
?: TlsUtils.getDefaultSupportedSignatureAlgorithms(context)
return DtlsUtils.buildSignerCredentials(context, certificate, sigAlgs, SignatureAlgorithm.rsa)
}
}
}

override fun notifyHandshakeComplete() {
super.notifyHandshakeComplete()
try {
exportedSrtpKeys = DtlsUtils.exportSrtpKeys(context)
} catch (_: Exception) { }
}
}
Loading
Loading