Skip to content

Commit

Permalink
feat(*): introducing SrtConnection and RtmpConnection to describe…
Browse files Browse the repository at this point in the history
… live connections
  • Loading branch information
ThibaultBee committed Dec 6, 2023
1 parent 3b9dde0 commit 2c03ec3
Show file tree
Hide file tree
Showing 15 changed files with 532 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ interface ILiveStreamer {
val isConnected: Boolean

/**
* Connect to an remove server.
* Connect to a remote server.
* To avoid creating an unresponsive UI, do not call on main thread.
*
* @param url server url
Expand All @@ -53,4 +53,4 @@ interface ILiveStreamer {
* @throws Exception if connection has failed or configuration has failed or [startStream] has failed too.
*/
suspend fun startStream(url: String)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,22 @@ import android.content.Context
import android.os.Build
import androidx.annotation.RequiresPermission
import io.github.thibaultbee.streampack.app.configuration.Configuration
import io.github.thibaultbee.streampack.ext.srt.data.SrtConnection
import io.github.thibaultbee.streampack.ext.srt.streamers.interfaces.ISrtLiveStreamer
import io.github.thibaultbee.streampack.listeners.OnConnectionListener
import io.github.thibaultbee.streampack.listeners.OnErrorListener
import io.github.thibaultbee.streampack.streamers.StreamerLifeCycleObserver
import io.github.thibaultbee.streampack.streamers.interfaces.IStreamer
import io.github.thibaultbee.streampack.streamers.interfaces.settings.IBaseCameraStreamerSettings
import io.github.thibaultbee.streampack.utils.*
import io.github.thibaultbee.streampack.utils.CameraSettings
import io.github.thibaultbee.streampack.utils.ChunkedFileOutputStream
import io.github.thibaultbee.streampack.utils.backCameraList
import io.github.thibaultbee.streampack.utils.frontCameraList
import io.github.thibaultbee.streampack.utils.getCameraStreamer
import io.github.thibaultbee.streampack.utils.getFileStreamer
import io.github.thibaultbee.streampack.utils.getLiveStreamer
import io.github.thibaultbee.streampack.utils.getStreamer
import io.github.thibaultbee.streampack.utils.isBackCamera
import io.github.thibaultbee.streampack.views.PreviewView
import kotlinx.coroutines.runBlocking
import java.io.File
Expand Down Expand Up @@ -87,13 +96,14 @@ class StreamerManager(
suspend fun startStream() {
if (streamer?.getLiveStreamer() != null) {
getSrtLiveStreamer()?.let {
it.streamId =
configuration.endpoint.srt.streamID
it.passPhrase =
val connection = SrtConnection(
configuration.endpoint.srt.ip,
configuration.endpoint.srt.port,
configuration.endpoint.srt.streamID,
configuration.endpoint.srt.passPhrase
)
it.connect(
configuration.endpoint.srt.ip,
configuration.endpoint.srt.port
connection
)
} ?: streamer?.getLiveStreamer()?.connect(
configuration.endpoint.rtmp.url
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.github.thibaultbee.streampack.data.AudioConfig
import io.github.thibaultbee.streampack.data.BitrateRegulatorConfig
import io.github.thibaultbee.streampack.data.VideoConfig
import io.github.thibaultbee.streampack.ext.rtmp.services.ScreenRecorderRtmpLiveService
import io.github.thibaultbee.streampack.ext.srt.data.SrtConnection
import io.github.thibaultbee.streampack.ext.srt.services.ScreenRecorderSrtLiveService
import io.github.thibaultbee.streampack.ext.srt.streamers.interfaces.ISrtLiveStreamer
import io.github.thibaultbee.streampack.internal.encoders.MediaCodecHelper
Expand Down Expand Up @@ -214,14 +215,13 @@ class MainActivity : AppCompatActivity() {

runBlocking {
streamer.getStreamer<ISrtLiveStreamer>()?.let {
it.streamId =
configuration.endpoint.srt.streamID
it.passPhrase =
configuration.endpoint.srt.passPhrase
it.connect(
val connection = SrtConnection(
configuration.endpoint.srt.ip,
configuration.endpoint.srt.port
configuration.endpoint.srt.port,
configuration.endpoint.srt.streamID,
configuration.endpoint.srt.passPhrase
)
it.connect(connection)
} ?: streamer.getStreamer<ILiveStreamer>()?.connect(
configuration.endpoint.rtmp.url
)
Expand Down
3 changes: 3 additions & 0 deletions extensions/rtmp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ dependencies {

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation "androidx.core:core-ktx:${androidxCoreVersion}"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2023 Thibault B.
*
* 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 io.github.thibaultbee.streampack.ext.rtmp.data

import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test

class RtmpConnectionTest {
@Test
fun fromUrl() {
val url = "rtmp://broadcast.host.com:1234/app/streamKey"
val connection = RtmpConnection.fromUrl(url)
assertEquals("rtmp", connection.scheme)
assertEquals("broadcast.host.com", connection.host)
assertEquals(1234, connection.port)
assertEquals("app", connection.app)
assertEquals("streamKey", connection.streamKey)
}

@Test
fun fromRtmpsUrl() {
val url = "rtmps://broadcast.host.com:1234/app/streamKey"
val connection = RtmpConnection.fromUrl(url)
assertEquals("rtmps", connection.scheme)
assertEquals("broadcast.host.com", connection.host)
assertEquals(1234, connection.port)
assertEquals("app", connection.app)
assertEquals("streamKey", connection.streamKey)
}

@Test
fun fromUrlWithDefaultPort() {
val url = "rtmp://broadcast.host.com/app/streamKey"
val connection = RtmpConnection.fromUrl(url)
assertEquals("rtmp", connection.scheme)
assertEquals("broadcast.host.com", connection.host)
assertEquals(1935, connection.port)
assertEquals("app", connection.app)
assertEquals("streamKey", connection.streamKey)
}

@Test
fun fromRtmpsUrlWithDefaultPort() {
val url = "rtmps://broadcast.host.com/app/streamKey"
val connection = RtmpConnection.fromUrl(url)
assertEquals("rtmps", connection.scheme)
assertEquals("broadcast.host.com", connection.host)
assertEquals(443, connection.port)
assertEquals("app", connection.app)
assertEquals("streamKey", connection.streamKey)
}

@Test
fun fromUrlWithBadScheme() {
val url = "rtp://broadcast.host.com:1234/app/streamKey"
try {
RtmpConnection.fromUrl(url)
Assert.fail("Should throw an exception")
} catch (_: Exception) {
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2023 Thibault B.
*
* 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 io.github.thibaultbee.streampack.ext.rtmp.data

import android.net.Uri
import java.security.InvalidParameterException

/**
* RTMP connection parameters
*
* @param scheme the RTMP scheme (rtmp, rtmps, rtmpt, rtmpe, rtmfp, rtmpte, rtmpts)
* @param host the server ip
* @param port the server port
* @param app the application name
* @param streamKey the stream key
*/
data class RtmpConnection(
val scheme: String, val host: String, val port: Int, val app: String, val streamKey: String
) {
val uri = Uri.Builder()
.scheme(scheme)
.encodedAuthority("$host:$port")
.appendEncodedPath(app)
.appendEncodedPath(streamKey)
.build()

val url = uri.toString()

init {
require(scheme == RTMP_SCHEME || scheme == RTMPS_SCHEME || scheme == RTMPT_SCHEME || scheme == RTMPE_SCHEME || scheme == RTMFP_SCHEME || scheme == RTMPTE_SCHEME || scheme == RTMPTS_SCHEME) { "Invalid scheme $scheme" }
require(host.isNotBlank()) { "Invalid host $host" }
require(port > 0) { "Invalid port $port" }
require(port < 65536) { "Invalid port $port" }
require(app.isNotBlank()) { "Invalid app $app" }
require(streamKey.isNotBlank()) { "Invalid streamKey $streamKey" }
}

companion object {
private const val RTMP_SCHEME = "rtmp"
private const val RTMPS_SCHEME = "rtmps"
private const val RTMPT_SCHEME = "rtmpt"
private const val RTMPE_SCHEME = "rtmpe"
private const val RTMFP_SCHEME = "rtmfp"
private const val RTMPTE_SCHEME = "rtmpte"
private const val RTMPTS_SCHEME = "rtmpts"

private const val DEFAULT_PORT = 1935
private const val SSL_DEFAULT_PORT = 443
private const val HTTP_DEFAULT_PORT = 80

/**
* Creates a RTMP connection from an URL
*
* @param url the server url (syntax: rtmp://host:port/app/streamKey)
* @return RTMP connection
*/
fun fromUrl(url: String): RtmpConnection {
val uri = Uri.parse(url)

val scheme =
uri.scheme ?: throw InvalidParameterException("Invalid scheme ${uri.scheme}")
val host = uri.host ?: throw InvalidParameterException("Invalid host ${uri.host}")
val port = if (uri.port > 0) {
uri.port
} else {
if ((scheme == RTMPS_SCHEME) || (scheme == RTMPTS_SCHEME)) {
SSL_DEFAULT_PORT
} else if ((scheme == RTMPT_SCHEME) || (scheme == RTMPTE_SCHEME)) {
HTTP_DEFAULT_PORT
} else {
DEFAULT_PORT
}
}
if (uri.pathSegments.size < 2) {
throw InvalidParameterException("Invalid path ${uri.path} expected /app/streamKey")
}
val app = uri.pathSegments.minus(uri.lastPathSegment).joinToString("/")
val streamKey = uri.lastPathSegment
?: throw InvalidParameterException("Invalid streamKey ${uri.lastPathSegment}")
return RtmpConnection(scheme, host, port, app, streamKey)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.github.thibaultbee.streampack.ext.rtmp.internal.endpoints

import io.github.thibaultbee.streampack.ext.rtmp.data.RtmpConnection
import io.github.thibaultbee.streampack.internal.data.Packet
import io.github.thibaultbee.streampack.internal.endpoints.ILiveEndpoint
import io.github.thibaultbee.streampack.listeners.OnConnectionListener
Expand All @@ -23,12 +24,10 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import video.api.rtmpdroid.Rtmp
import java.security.InvalidParameterException

class RtmpProducer(
private val coroutineDispatcher: CoroutineDispatcher = Dispatchers.IO
) :
ILiveEndpoint {
) : ILiveEndpoint {
override var onConnectionListener: OnConnectionListener? = null

private var socket = Rtmp()
Expand All @@ -51,16 +50,8 @@ class RtmpProducer(
}

override suspend fun connect(url: String) {
if (!url.startsWith(RTMP_PREFIX) &&
!url.startsWith(RTMPS_PREFIX) &&
!url.startsWith(RTMPT_PREFIX) &&
!url.startsWith(RTMPE_PREFIX) &&
!url.startsWith(RTMFP_PREFIX) &&
!url.startsWith(RTMPTE_PREFIX) &&
!url.startsWith(RTMPTS_PREFIX)
) {
throw InvalidParameterException("URL must start with $RTMP_PREFIX, $RTMPS_PREFIX, $RTMPT_PREFIX, $RTMPE_PREFIX, $RTMFP_PREFIX, $RTMPTE_PREFIX or $RTMPTS_PREFIX")
}
RtmpConnection.fromUrl(url) // URL validation

withContext(coroutineDispatcher) {
try {
isOnError = false
Expand Down Expand Up @@ -134,26 +125,5 @@ class RtmpProducer(

companion object {
private const val TAG = "RtmpProducer"

private const val RTMP_SCHEME = "rtmp"
private const val RTMP_PREFIX = "$RTMP_SCHEME://"

private const val RTMPS_SCHEME = "rtmps"
private const val RTMPS_PREFIX = "$RTMPS_SCHEME://"

private const val RTMPT_SCHEME = "rtmpt"
private const val RTMPT_PREFIX = "$RTMPT_SCHEME://"

private const val RTMPE_SCHEME = "rtmpe"
private const val RTMPE_PREFIX = "$RTMPE_SCHEME://"

private const val RTMFP_SCHEME = "rtmfp"
private const val RTMFP_PREFIX = "$RTMFP_SCHEME://"

private const val RTMPTE_SCHEME = "rtmpte"
private const val RTMPTE_PREFIX = "$RTMPTE_SCHEME://"

private const val RTMPTS_SCHEME = "rtmpts"
private const val RTMPTS_PREFIX = "$RTMPTS_SCHEME://"
}
}
}
3 changes: 3 additions & 0 deletions extensions/srt/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ dependencies {

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation "androidx.core:core-ktx:${androidxCoreVersion}"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
}
Loading

0 comments on commit 2c03ec3

Please sign in to comment.