diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/utils/ChannelWithCloseableData.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/utils/ChannelWithCloseableData.kt index 130cf4450..35a803271 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/elements/utils/ChannelWithCloseableData.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/elements/utils/ChannelWithCloseableData.kt @@ -34,10 +34,14 @@ import java.io.Closeable */ class ChannelWithCloseableData( capacity: Int = RENDEZVOUS, - onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND + onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, + onUndeliveredElement: ((T) -> Unit) = {} ) : ReceiveChannel> { private val channel = - Channel>(capacity, onBufferOverflow, onUndeliveredElement = { it.close() }) + Channel>(capacity, onBufferOverflow, onUndeliveredElement = { + it.close() + onUndeliveredElement(it.data) + }) /** * Sends data along with a close action to the channel. diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/regulator/IBitrateRegulator.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/regulator/IBitrateRegulator.kt index 367af6973..4cd1a5a11 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/regulator/IBitrateRegulator.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/regulator/IBitrateRegulator.kt @@ -22,13 +22,13 @@ import io.github.thibaultbee.streampack.core.configuration.BitrateRegulatorConfi */ interface IBitrateRegulator { /** - * Calls regularly to get new stats + * Calls regularly to get new metrics * - * @param stats transmission stats + * @param metrics transmission metrics * @param currentVideoBitrate current video bitrate target in bits/s. * @param currentAudioBitrate current audio bitrate target in bits/s. */ - fun update(stats: Any, currentVideoBitrate: Int, currentAudioBitrate: Int) + fun update(metrics: Any, currentVideoBitrate: Int, currentAudioBitrate: Int) /** * Factory interface you must use to create a [BitrateRegulator] object. diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/data/storage/DataStoreRepository.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/data/storage/DataStoreRepository.kt index ddab33b3d..a170631d3 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/data/storage/DataStoreRepository.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/data/storage/DataStoreRepository.kt @@ -164,7 +164,7 @@ class DataStoreRepository( EndpointType.RTMP -> { val url = preferences[stringPreferencesKey(context.getString(R.string.rtmp_server_url_key))] - ?: context.getString(R.string.default_rtmp_url) + ?: context.getString(R.string.rtmp_default_url) UriMediaDescriptor(context, url) } } @@ -172,19 +172,71 @@ class DataStoreRepository( val bitrateRegulatorConfigFlow: Flow = dataStore.data.map { preferences -> + val endpointTypeId = + preferences[stringPreferencesKey(context.getString(R.string.endpoint_type_key))]?.toInt() + ?: EndpointType.SRT.id + val isBitrateRegulatorEnable = - preferences[booleanPreferencesKey(context.getString(R.string.srt_server_enable_bitrate_regulation_key))] + preferences[booleanPreferencesKey( + context.getString( + when (endpointTypeId) { + EndpointType.SRT.id -> { + R.string.srt_server_enable_bitrate_regulation_key + } + + EndpointType.RTMP.id -> { + R.string.rtmp_server_enable_bitrate_regulation_key + } + + else -> { + throw IllegalArgumentException("Unknown endpoint type") + } + } + ) + )] ?: true if (!isBitrateRegulatorEnable) { return@map null } val videoMinBitrate = - preferences[intPreferencesKey(context.getString(R.string.srt_server_video_min_bitrate_key))]?.toInt() + preferences[intPreferencesKey( + context.getString( + when (endpointTypeId) { + EndpointType.SRT.id -> { + R.string.srt_server_video_min_bitrate_key + } + + EndpointType.RTMP.id -> { + R.string.rtmp_server_video_min_bitrate_key + } + + else -> { + throw IllegalArgumentException("Unknown endpoint type") + } + } + ) + )] ?.times(1000) ?: 300000 val videoMaxBitrate = - preferences[intPreferencesKey(context.getString(R.string.srt_server_video_target_bitrate_key))]?.toInt() + preferences[intPreferencesKey( + context.getString( + when (endpointTypeId) { + EndpointType.SRT.id -> { + R.string.srt_server_video_target_bitrate_key + } + + EndpointType.RTMP.id -> { + R.string.rtmp_server_video_target_bitrate_key + } + + else -> { + throw IllegalArgumentException("Unknown endpoint type") + } + } + ) + )] ?.times(1000) ?: 10000000 BitrateRegulatorConfig( diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt index 0b3dbd592..35d331522 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/main/PreviewViewModel.kt @@ -65,6 +65,7 @@ import io.github.thibaultbee.streampack.core.streamers.single.VideoOnlySingleStr import io.github.thibaultbee.streampack.core.streamers.single.withAudio import io.github.thibaultbee.streampack.core.streamers.single.withVideo import io.github.thibaultbee.streampack.core.utils.extensions.isClosedException +import io.github.thibaultbee.streampack.ext.rtmp.regulator.controllers.simpleRtmpBitrateRegulatorControllerFactory import io.github.thibaultbee.streampack.ext.srt.regulator.controllers.simpleSrtBitrateRegulatorControllerFactory import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers @@ -77,6 +78,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex @@ -312,16 +314,32 @@ class PreviewViewModel(private val application: Application) : ObservableViewMod val descriptor = storageRepository.endpointDescriptorFlow.first() streamer.startStream(descriptor) - if (descriptor.type.sinkType == MediaSinkType.SRT) { + if ((descriptor.type.sinkType == MediaSinkType.RTMP) || (descriptor.type.sinkType == MediaSinkType.SRT)) { val bitrateRegulatorConfig = storageRepository.bitrateRegulatorConfigFlow.first() if (bitrateRegulatorConfig != null) { Log.i(TAG, "Add bitrate regulator controller") - streamer.addBitrateRegulatorController( - simpleSrtBitrateRegulatorControllerFactory( - bitrateRegulatorConfig = bitrateRegulatorConfig - ) - ) + val controllerFactory = + when (descriptor.type.sinkType) { + MediaSinkType.RTMP -> { + simpleRtmpBitrateRegulatorControllerFactory( + bitrateRegulatorConfig = bitrateRegulatorConfig + ) + } + + MediaSinkType.SRT -> { + simpleSrtBitrateRegulatorControllerFactory( + bitrateRegulatorConfig = bitrateRegulatorConfig + ) + } + + else -> { + null + } + } + controllerFactory?.let { + streamer.addBitrateRegulatorController(it) + } ?: Log.e(TAG, "Controller factory is null") } } } catch (e: CancellationException) { diff --git a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt index 2a8079eda..04a3b1b1b 100644 --- a/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt +++ b/demos/camera/src/main/java/io/github/thibaultbee/streampack/app/ui/settings/SettingsFragment.kt @@ -131,18 +131,30 @@ class SettingsFragment : PreferenceFragmentCompat() { this.findPreference(getString(R.string.srt_server_port_key))!! } - private val serverEnableBitrateRegulationPreference: SwitchPreference by lazy { + private val srtServerEnableBitrateRegulationPreference: SwitchPreference by lazy { this.findPreference(getString(R.string.srt_server_enable_bitrate_regulation_key))!! } - private val serverTargetVideoBitratePreference: SeekBarPreference by lazy { + private val srtServerTargetVideoBitratePreference: SeekBarPreference by lazy { this.findPreference(getString(R.string.srt_server_video_target_bitrate_key))!! } - private val serverMinVideoBitratePreference: SeekBarPreference by lazy { + private val srtServerMinVideoBitratePreference: SeekBarPreference by lazy { this.findPreference(getString(R.string.srt_server_video_min_bitrate_key))!! } + private val rtmpServerEnableBitrateRegulationPreference: SwitchPreference by lazy { + this.findPreference(getString(R.string.rtmp_server_enable_bitrate_regulation_key))!! + } + + private val rtmpServerTargetVideoBitratePreference: SeekBarPreference by lazy { + this.findPreference(getString(R.string.rtmp_server_video_target_bitrate_key))!! + } + + private val rtmpServerMinVideoBitratePreference: SeekBarPreference by lazy { + this.findPreference(getString(R.string.rtmp_server_video_min_bitrate_key))!! + } + private val fileNamePreference: EditTextPreference by lazy { this.findPreference(getString(R.string.file_name_key))!! } @@ -417,26 +429,50 @@ class SettingsFragment : PreferenceFragmentCompat() { editText.filters = arrayOf(InputFilter.LengthFilter(5)) } - serverTargetVideoBitratePreference.isVisible = - serverEnableBitrateRegulationPreference.isChecked - serverMinVideoBitratePreference.isVisible = - serverEnableBitrateRegulationPreference.isChecked - serverEnableBitrateRegulationPreference.setOnPreferenceChangeListener { _, newValue -> - serverTargetVideoBitratePreference.isVisible = newValue as Boolean - serverMinVideoBitratePreference.isVisible = newValue + srtServerTargetVideoBitratePreference.isVisible = + srtServerEnableBitrateRegulationPreference.isChecked + srtServerMinVideoBitratePreference.isVisible = + srtServerEnableBitrateRegulationPreference.isChecked + srtServerEnableBitrateRegulationPreference.setOnPreferenceChangeListener { _, newValue -> + srtServerTargetVideoBitratePreference.isVisible = newValue as Boolean + srtServerMinVideoBitratePreference.isVisible = newValue + true + } + + srtServerTargetVideoBitratePreference.setOnPreferenceChangeListener { _, newValue -> + if ((newValue as Int) < srtServerMinVideoBitratePreference.value) { + srtServerMinVideoBitratePreference.value = newValue + } + true + } + + srtServerMinVideoBitratePreference.setOnPreferenceChangeListener { _, newValue -> + if ((newValue as Int) > srtServerTargetVideoBitratePreference.value) { + srtServerTargetVideoBitratePreference.value = newValue + } + true + } + + rtmpServerTargetVideoBitratePreference.isVisible = + rtmpServerEnableBitrateRegulationPreference.isChecked + rtmpServerMinVideoBitratePreference.isVisible = + rtmpServerEnableBitrateRegulationPreference.isChecked + rtmpServerEnableBitrateRegulationPreference.setOnPreferenceChangeListener { _, newValue -> + rtmpServerTargetVideoBitratePreference.isVisible = newValue as Boolean + rtmpServerMinVideoBitratePreference.isVisible = newValue true } - serverTargetVideoBitratePreference.setOnPreferenceChangeListener { _, newValue -> - if ((newValue as Int) < serverMinVideoBitratePreference.value) { - serverMinVideoBitratePreference.value = newValue + rtmpServerTargetVideoBitratePreference.setOnPreferenceChangeListener { _, newValue -> + if ((newValue as Int) < rtmpServerMinVideoBitratePreference.value) { + rtmpServerMinVideoBitratePreference.value = newValue } true } - serverMinVideoBitratePreference.setOnPreferenceChangeListener { _, newValue -> - if ((newValue as Int) > serverTargetVideoBitratePreference.value) { - serverTargetVideoBitratePreference.value = newValue + rtmpServerMinVideoBitratePreference.setOnPreferenceChangeListener { _, newValue -> + if ((newValue as Int) > rtmpServerTargetVideoBitratePreference.value) { + rtmpServerTargetVideoBitratePreference.value = newValue } true } @@ -459,7 +495,7 @@ class SettingsFragment : PreferenceFragmentCompat() { // Update file extension if (endpoint.hasFileCapabilities) { // Remove previous extension - FileExtension.entries.forEach { + FileExtension.entries.forEach { _ -> fileNamePreference.text = fileNamePreference.text?.substringBeforeLast(".") } // Add correct extension diff --git a/demos/camera/src/main/res/values/strings.xml b/demos/camera/src/main/res/values/strings.xml index 1a8393963..1fa81cee8 100644 --- a/demos/camera/src/main/res/values/strings.xml +++ b/demos/camera/src/main/res/values/strings.xml @@ -127,7 +127,14 @@ rtmp_server_key RTMP Server rtmp_server_url_key - rtmp://192.168.1.192/s/streamKey + rtmp://192.168.1.192/s/streamKey + rtmp_server_enable_bitrate_regulation_key + Enable bitrate regulation + rtmp_server_video_target_bitrate_key + Video target bitrate (kb/s) + rtmp_server_video_min_bitrate_key + Video minimum bitrate (kb/s) + URL file_endpoint_key diff --git a/demos/camera/src/main/res/xml/root_preferences.xml b/demos/camera/src/main/res/xml/root_preferences.xml index e47de9ad3..58a3a89b7 100644 --- a/demos/camera/src/main/res/xml/root_preferences.xml +++ b/demos/camera/src/main/res/xml/root_preferences.xml @@ -167,11 +167,33 @@ app:title="@string/rtmp_server"> + + + + + ( - 10 /* Arbitrary buffer size. TODO: add a parameter to set it */, BufferOverflow.DROP_OLDEST + 10 /* Arbitrary buffer size. TODO: add a parameter to set it */, BufferOverflow.DROP_OLDEST, + onUndeliveredElement = { flvTag -> + frameDropped.incrementAndFetch() + if (flvTag.data is AudioData) { + audioFrameDropped.incrementAndFetch() + } else if (flvTag.data is VideoData) { + videoFrameDropped.incrementAndFetch() + } + } ) private val flvTagBuilder = FlvTagBuilder(flvTagChannel) @@ -70,12 +90,25 @@ class RtmpEndpoint internal constructor( private val connectionBuilder = RtmpConnectionBuilder(selectorManager) private var rtmpClient: RtmpClient? = null - private var startUpTimestamp = INVALID_TIMESTAMP private val timestampMutex = Mutex() - override val metrics: Any - get() = TODO("Not yet implemented") + private var previousMetrics: RtmpMetrics = RtmpMetrics.ZERO + override val metrics: RtmpMetrics + get() { + val metrics = + rtmpClient?.metrics ?: throw IllegalStateException("Socket is not initialized") + val correctedMetrics = metrics.copy( + messagesSendDropped = metrics.messagesSendDropped + frameDropped.load(), + audioMessagesSendDropped = metrics.audioMessagesSendDropped + audioFrameDropped.load(), + videoMessagesSendDropped = metrics.videoMessagesSendDropped + videoFrameDropped.load() + ) + return synchronized(this) { + val deltaMetrics = correctedMetrics - previousMetrics + previousMetrics = correctedMetrics + deltaMetrics + } + } private val _isOpenFlow = MutableStateFlow(false) override val isOpenFlow = _isOpenFlow.asStateFlow() @@ -95,6 +128,15 @@ class RtmpEndpoint internal constructor( } } + private fun resetMetrics() { + frameDropped.store(0) + audioFrameDropped.store(0) + videoFrameDropped.store(0) + synchronized(this) { + previousMetrics = RtmpMetrics.ZERO + } + } + private suspend fun safeClient(block: suspend (RtmpClient) -> T): T { val rtmpClient = requireNotNull(rtmpClient) { "Not opened" } require(!rtmpClient.isClosed) { "Connection closed" } @@ -109,6 +151,7 @@ class RtmpEndpoint internal constructor( return@withContext } + resetMetrics() rtmpClient = connectionBuilder.connect(descriptor.uri.toString()).apply { _isOpenFlow.emit(true) diff --git a/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/RtmpBitrateRegulator.kt b/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/RtmpBitrateRegulator.kt new file mode 100644 index 000000000..89d4728d8 --- /dev/null +++ b/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/RtmpBitrateRegulator.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2026 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.regulator + +import io.github.komedia.komuxer.rtmp.util.metrics.RtmpMetrics +import io.github.thibaultbee.streampack.core.configuration.BitrateRegulatorConfig +import io.github.thibaultbee.streampack.core.regulator.BitrateRegulator +import io.github.thibaultbee.streampack.core.regulator.IBitrateRegulator + +/** + * Base class of RTMP bitrate regulation implementation. + * + * If you want to implement your custom bitrate regulator, it must inherit from this class. + * The bitrate regulator object is created by streamers with the [IBitrateRegulator.Factory]. + * + * @param bitrateRegulatorConfig bitrate regulation configuration + * @param onVideoTargetBitrateChange call when you have to change video bitrate + * @param onAudioTargetBitrateChange call when you have to change audio bitrate + */ +abstract class RtmpBitrateRegulator( + bitrateRegulatorConfig: BitrateRegulatorConfig, + onVideoTargetBitrateChange: ((Int) -> Unit), + onAudioTargetBitrateChange: ((Int) -> Unit) +) : BitrateRegulator( + bitrateRegulatorConfig, + onVideoTargetBitrateChange, + onAudioTargetBitrateChange +) { + override fun update(metrics: Any, currentVideoBitrate: Int, currentAudioBitrate: Int) = + update(metrics as RtmpMetrics, currentVideoBitrate, currentAudioBitrate) + + /** + * Call regularly to get new RTMP metrics + * + * @param metrics RTMP transmission metrics + * @param currentVideoBitrate current video bitrate target in bits/s. + * @param currentAudioBitrate current audio bitrate target in bits/s. + */ + abstract fun update(metrics: RtmpMetrics, currentVideoBitrate: Int, currentAudioBitrate: Int) + + /** + * Factory interface you must use to create a [RtmpBitrateRegulator] object. + * If you want to create a custom RTMP bitrate regulation implementation, create a factory that + * implements this interface. + */ + interface Factory : IBitrateRegulator.Factory { + /** + * Creates a [RtmpBitrateRegulator] object from given parameters + * + * @param bitrateRegulatorConfig bitrate regulation configuration + * @param onVideoTargetBitrateChange call when you have to change video bitrate + * @param onAudioTargetBitrateChange call when you have to change audio bitrate + * @return a [RtmpBitrateRegulator] object + */ + override fun newBitrateRegulator( + bitrateRegulatorConfig: BitrateRegulatorConfig, + onVideoTargetBitrateChange: ((Int) -> Unit), + onAudioTargetBitrateChange: ((Int) -> Unit) + ): RtmpBitrateRegulator + } +} \ No newline at end of file diff --git a/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/SimpleRtmpBitrateRegulator.kt b/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/SimpleRtmpBitrateRegulator.kt new file mode 100644 index 000000000..de1b248d5 --- /dev/null +++ b/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/SimpleRtmpBitrateRegulator.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2026 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.regulator + +import io.github.komedia.komuxer.rtmp.util.metrics.RtmpMetrics +import io.github.thibaultbee.streampack.core.configuration.BitrateRegulatorConfig +import io.github.thibaultbee.streampack.core.regulator.BitrateRegulator +import kotlin.math.max +import kotlin.math.min + +/** + * A [BitrateRegulator] that reduce video bitrate when packets are lost. + * + * @param bitrateRegulatorConfig bitrate regulation configuration + * @param onVideoTargetBitrateChange call when you have to change video bitrate + * @param onAudioTargetBitrateChange call when you have to change audio bitrate + */ +class SimpleRtmpBitrateRegulator( + bitrateRegulatorConfig: BitrateRegulatorConfig, + onVideoTargetBitrateChange: ((Int) -> Unit), + onAudioTargetBitrateChange: ((Int) -> Unit) +) : RtmpBitrateRegulator( + bitrateRegulatorConfig, + onVideoTargetBitrateChange, + onAudioTargetBitrateChange +) { + companion object { + const val MINIMUM_DECREASE_THRESHOLD = 100000 // b/s + const val MAXIMUM_INCREASE_THRESHOLD = 200000 // b/s + } + + /** + * Call regularly to get new RTMP metrics + * + * @param metrics RTMP transmission metrics + * @param currentVideoBitrate current video bitrate target in bits/s. + * @param currentAudioBitrate current audio bitrate target in bits/s. + */ + override fun update(metrics: RtmpMetrics, currentVideoBitrate: Int, currentAudioBitrate: Int) { + if (metrics.messagesSendDropped > 0) { + // Detected packet loss - quickly react + val newVideoBitrate = currentVideoBitrate - max( + currentVideoBitrate * 20 / 100, // too late - drop bitrate by 20 % + MINIMUM_DECREASE_THRESHOLD // getting down by 100000 b/s minimum + ) + onVideoTargetBitrateChange( + max( + newVideoBitrate, + bitrateRegulatorConfig.videoBitrateRange.lower + ) + ) + } else if (currentVideoBitrate < bitrateRegulatorConfig.videoBitrateRange.upper) { + // Try to increase to the max target + val newVideoBitrate = currentVideoBitrate + min( + (bitrateRegulatorConfig.videoBitrateRange.upper - currentVideoBitrate) * 50 / 100, // getting slower when reaching target bitrate + MAXIMUM_INCREASE_THRESHOLD // not increasing to fast + ) + onVideoTargetBitrateChange( + min( + newVideoBitrate, + bitrateRegulatorConfig.videoBitrateRange.upper + ) + ) + } + } + + /** + * Factory interface you must use to create a [SimpleRtmpBitrateRegulator] object. + * If you want to create a custom RTMP bitrate regulation implementation, create a factory that + * implements this interface. + */ + class Factory : RtmpBitrateRegulator.Factory { + /** + * Creates a [SimpleRtmpBitrateRegulator] object from given parameters + * + * @param bitrateRegulatorConfig bitrate regulation configuration + * @param onVideoTargetBitrateChange call when you have to change video bitrate + * @param onAudioTargetBitrateChange call when you have to change audio bitrate + * @return a [SimpleRtmpBitrateRegulator] object + */ + override fun newBitrateRegulator( + bitrateRegulatorConfig: BitrateRegulatorConfig, + onVideoTargetBitrateChange: ((Int) -> Unit), + onAudioTargetBitrateChange: ((Int) -> Unit) + ): SimpleRtmpBitrateRegulator { + return SimpleRtmpBitrateRegulator( + bitrateRegulatorConfig, + onVideoTargetBitrateChange, + onAudioTargetBitrateChange + ) + } + } +} \ No newline at end of file diff --git a/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/controllers/RtmpBitrateRegulatorControllerFactories.kt b/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/controllers/RtmpBitrateRegulatorControllerFactories.kt new file mode 100644 index 000000000..eb102122a --- /dev/null +++ b/extensions/rtmp/src/main/java/io/github/thibaultbee/streampack/ext/rtmp/regulator/controllers/RtmpBitrateRegulatorControllerFactories.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2026 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.regulator.controllers + +import io.github.thibaultbee.streampack.core.configuration.BitrateRegulatorConfig +import io.github.thibaultbee.streampack.core.regulator.controllers.SimpleBitrateRegulatorController +import io.github.thibaultbee.streampack.core.regulator.controllers.SimpleBitrateRegulatorController.Companion.DEFAULT_POLLING_TIME_IN_MS +import io.github.thibaultbee.streampack.ext.rtmp.regulator.RtmpBitrateRegulator +import io.github.thibaultbee.streampack.ext.rtmp.regulator.SimpleRtmpBitrateRegulator + +/** + * A [SimpleBitrateRegulatorController.Factory] for [RtmpBitrateRegulator]. + * + * @param bitrateRegulatorFactory the [RtmpBitrateRegulator.Factory] implementation. Use it to make your own bitrate regulator. + * @param bitrateRegulatorConfig bitrate regulator configuration + * @param pollingTimeInMs delay between each call to [RtmpBitrateRegulator.update] + * + * @see SimpleBitrateRegulatorController.Factory + * @see SimpleRtmpBitrateRegulator.Factory + */ +fun simpleRtmpBitrateRegulatorControllerFactory( + bitrateRegulatorFactory: RtmpBitrateRegulator.Factory = SimpleRtmpBitrateRegulator.Factory(), + bitrateRegulatorConfig: BitrateRegulatorConfig = BitrateRegulatorConfig(), + pollingTimeInMs: Long = DEFAULT_POLLING_TIME_IN_MS +) = SimpleBitrateRegulatorController.Factory( + bitrateRegulatorFactory, bitrateRegulatorConfig, pollingTimeInMs +) diff --git a/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/DummySrtBitrateRegulator.kt b/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/DummySrtBitrateRegulator.kt index 65dfd6916..65101a21b 100644 --- a/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/DummySrtBitrateRegulator.kt +++ b/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/DummySrtBitrateRegulator.kt @@ -42,12 +42,12 @@ class DummySrtBitrateRegulator( const val SEND_PACKET_THRESHOLD = 50 } - override fun update(stats: Stats, currentVideoBitrate: Int, currentAudioBitrate: Int) { - val estimatedBandwidth = (stats.mbpsBandwidth * 1000000).toInt() + override fun update(metrics: Stats, currentVideoBitrate: Int, currentAudioBitrate: Int) { + val estimatedBandwidth = (metrics.mbpsBandwidth * 1000000).toInt() if (currentVideoBitrate > bitrateRegulatorConfig.videoBitrateRange.lower) { val newVideoBitrate = when { - stats.pktSndLoss > 0 -> { + metrics.pktSndLoss > 0 -> { // Detected packet loss - quickly react currentVideoBitrate - max( currentVideoBitrate * 20 / 100, // too late - drop bitrate by 20 % @@ -55,7 +55,7 @@ class DummySrtBitrateRegulator( ) } - stats.pktSndBuf > SEND_PACKET_THRESHOLD -> { + metrics.pktSndBuf > SEND_PACKET_THRESHOLD -> { // Try to avoid congestion currentVideoBitrate - max( currentVideoBitrate * 10 / 100, // drop bitrate by 10 % @@ -80,10 +80,8 @@ class DummySrtBitrateRegulator( ) // Don't go under videoBitrateRange.lower return } - } - - // Can bitrate go upper? - if (currentVideoBitrate < bitrateRegulatorConfig.videoBitrateRange.upper) { + // Can bitrate go upper? + } else if (currentVideoBitrate < bitrateRegulatorConfig.videoBitrateRange.upper) { val newVideoBitrate = when { (currentVideoBitrate + currentAudioBitrate) < estimatedBandwidth -> { currentVideoBitrate + min( diff --git a/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/SrtBitrateRegulator.kt b/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/SrtBitrateRegulator.kt index dbfb53a84..6e5b4443c 100644 --- a/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/SrtBitrateRegulator.kt +++ b/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/SrtBitrateRegulator.kt @@ -39,17 +39,17 @@ abstract class SrtBitrateRegulator( onVideoTargetBitrateChange, onAudioTargetBitrateChange ) { - override fun update(stats: Any, currentVideoBitrate: Int, currentAudioBitrate: Int) = - update(stats as Stats, currentVideoBitrate, currentAudioBitrate) + override fun update(metrics: Any, currentVideoBitrate: Int, currentAudioBitrate: Int) = + update(metrics as Stats, currentVideoBitrate, currentAudioBitrate) /** - * Call regularly to get new SRT stats + * Call regularly to get new SRT metrics * - * @param stats SRT transmission stats + * @param metrics SRT transmission metrics * @param currentVideoBitrate current video bitrate target in bits/s. * @param currentAudioBitrate current audio bitrate target in bits/s. */ - abstract fun update(stats: Stats, currentVideoBitrate: Int, currentAudioBitrate: Int) + abstract fun update(metrics: Stats, currentVideoBitrate: Int, currentAudioBitrate: Int) /** * Factory interface you must use to create a [SrtBitrateRegulator] object. diff --git a/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/controllers/DefaultSrtBitrateRegulatorController.kt b/extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/controllers/SrtBitrateRegulatorControllerFactories.kt similarity index 100% rename from extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/controllers/DefaultSrtBitrateRegulatorController.kt rename to extensions/srt/src/main/java/io/github/thibaultbee/streampack/ext/srt/regulator/controllers/SrtBitrateRegulatorControllerFactories.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2cb2b2401..feda42b74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,7 @@ kotlinxIo = "0.8.0" material = "1.13.0" mockk = "1.14.5" robolectric = "4.16" -komuxer = "0.3.4" +komuxer = "0.4.0" srtdroid = "1.9.5" junitKtx = "1.3.0" compose = "1.10.1" diff --git a/settings.gradle.kts b/settings.gradle.kts index de694da52..16ddf629a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() + mavenLocal() mavenCentral() } }