Skip to content
Open
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: 5 additions & 6 deletions app/src/main/java/com/pedro/streamer/rotation/CameraFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import com.pedro.extrasources.CameraXSource
import com.pedro.library.base.StreamBase
import com.pedro.library.base.recording.RecordController
import com.pedro.library.generic.GenericStream
import com.pedro.library.util.BitrateAdapter
import com.pedro.library.util.QueueAwareBitrateAdapter
import com.pedro.streamer.R
import com.pedro.streamer.utils.PathUtils
import com.pedro.streamer.utils.toast
Expand Down Expand Up @@ -95,10 +95,8 @@ class CameraFragment: Fragment(), ConnectChecker {
private val aBitrate = 128 * 1000
private var recordPath = ""
//Bitrate adapter used to change the bitrate on fly depend of the bandwidth.
private val bitrateAdapter = BitrateAdapter {
genericStream.setVideoBitrateOnFly(it)
}.apply {
setMaxBitrate(vBitrate + aBitrate)
private val bitrateAdapter = QueueAwareBitrateAdapter(maxBitrate = vBitrate + aBitrate) {
genericStream.setVideoBitrateOnFly(it - aBitrate)
}

@SuppressLint("ClickableViewAccessibility")
Expand Down Expand Up @@ -203,6 +201,7 @@ class CameraFragment: Fragment(), ConnectChecker {
}

override fun onConnectionStarted(url: String) {
bitrateAdapter.reset()
}

override fun onConnectionSuccess() {
Expand All @@ -223,7 +222,7 @@ class CameraFragment: Fragment(), ConnectChecker {

override fun onStreamingStats(report: StreamingStatsReport) {
onMainThreadHandler {
bitrateAdapter.adaptBitrate(report.smoothedBitrate, genericStream.getStreamClient().hasCongestion())
bitrateAdapter.onStreamingStats(report)
if (report.throughput != Throughput.UNKNOWN) {
txtBitrate.text = String.format(
Locale.getDefault(),
Expand Down
1 change: 1 addition & 0 deletions common/src/main/java/com/pedro/common/BitrateManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,6 @@ open class BitrateManager(private val bitrateChecker: BitrateChecker) {
fun reset() {
bitrate = 0
bitrateOld = 0
timeStamp = TimeUtils.getCurrentTimeMillis()
}
}
13 changes: 13 additions & 0 deletions common/src/main/java/com/pedro/common/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import android.media.MediaFormat
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Surface
import androidx.annotation.RequiresApi
import com.pedro.common.frame.MediaFrame
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.io.IOException
Expand All @@ -47,6 +49,7 @@ import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext

/**
* Created by pedro on 3/11/23.
Expand Down Expand Up @@ -366,3 +369,13 @@ fun ByteBuffer.clone(data: ByteArray): ByteBuffer {
source.get(data, 0, length)
return ByteBuffer.wrap(data, 0, length).slice()
}

@JvmOverloads
fun getSuspendContext(dispatcher: CoroutineDispatcher = Dispatchers.IO) = object: Continuation<Any?> {
override val context: CoroutineContext
get() = dispatcher

override fun resumeWith(result: Result<Any?>) {
result.exceptionOrNull()?.let { Log.e("getSuspendContext", "Error", it) }
}
}
33 changes: 33 additions & 0 deletions common/src/test/java/com/pedro/common/BitrateManagerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
Expand Down Expand Up @@ -90,4 +91,36 @@ class BitrateManagerTest {
val marginError = 20
assertTrue(expectedResult - marginError <= resultValue.firstValue && resultValue.firstValue <= expectedResult + marginError)
}

@Test
fun `GIVEN an idle instance WHEN reset and measure a second THEN report the real bitrate`() = runTest {
val bitrateManager = BitrateManager(connectChecker)
//the instance is created when the client is built, the stream may start much later
fakeTime += 300_000
bitrateManager.reset()

fakeTime += 1000
bitrateManager.calculateBitrate(3_000_000L)

val resultValue = argumentCaptor<Long>()
verify(connectChecker, times(1)).onNewBitrate(resultValue.capture())
assertEquals(3_000_000L, resultValue.firstValue)
}

@Test
fun `GIVEN a measured bitrate WHEN reset THEN start a new window instead of averaging the pause`() = runTest {
val bitrateManager = BitrateManager(connectChecker)
fakeTime += 1000
bitrateManager.calculateBitrate(1_000_000L)

//a reconnection: the sender is stopped for a while and started again
fakeTime += 120_000
bitrateManager.reset()
fakeTime += 1000
bitrateManager.calculateBitrate(2_000_000L)

val resultValue = argumentCaptor<Long>()
verify(connectChecker, times(2)).onNewBitrate(resultValue.capture())
assertEquals(2_000_000L, resultValue.secondValue)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,13 @@ class Camera2ApiManager(context: Context) {
}
}

private fun clearRegions(): Array<MeteringRectangle>? {
val sensor = cameraCharacteristics?.secureGet(
CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE) ?: return null
return arrayOf(MeteringRectangle(0, 0, sensor.width(), sensor.height(),
MeteringRectangle.METERING_WEIGHT_DONT_CARE))
}

/**
* @param mode value from CameraCharacteristics.CONTROL_AWB_MODE_*
*/
Expand All @@ -413,8 +420,7 @@ class Camera2ApiManager(context: Context) {
if (!modes.contains(mode)) return false
val maxRegionsAwb = characteristics.secureGet(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB) ?: 0
if (maxRegionsAwb > 0) {
val clearRect = MeteringRectangle(0, 0, 0, 0, MeteringRectangle.METERING_WEIGHT_DONT_CARE)
builderInputSurface.set(CaptureRequest.CONTROL_AWB_REGIONS, arrayOf(clearRect))
clearRegions()?.let { builderInputSurface.set(CaptureRequest.CONTROL_AWB_REGIONS, it) }
}
builderInputSurface.set(CaptureRequest.CONTROL_AWB_MODE, mode)
isAutoWhiteBalanceEnabled = applyRequest(builderInputSurface)
Expand Down Expand Up @@ -462,8 +468,7 @@ class Camera2ApiManager(context: Context) {
if (!modes.contains(CaptureRequest.CONTROL_AE_MODE_ON)) return false
val maxRegionsAe = characteristics.secureGet(CameraCharacteristics.CONTROL_MAX_REGIONS_AE) ?: 0
if (maxRegionsAe > 0) {
val clearRect = MeteringRectangle(0, 0, 0, 0, MeteringRectangle.METERING_WEIGHT_DONT_CARE)
builderInputSurface.set(CaptureRequest.CONTROL_AE_REGIONS, arrayOf(clearRect))
clearRegions()?.let { builderInputSurface.set(CaptureRequest.CONTROL_AE_REGIONS, it) }
}
builderInputSurface.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
isAutoExposureEnabled = applyRequest(builderInputSurface)
Expand Down Expand Up @@ -784,8 +789,7 @@ class Camera2ApiManager(context: Context) {
builderInputSurface.setTag("")
val maxRegionsAf = characteristics.secureGet(CameraCharacteristics.CONTROL_MAX_REGIONS_AF) ?: 0
if (maxRegionsAf > 0) {
val clearRect = MeteringRectangle(0, 0, 0, 0, MeteringRectangle.METERING_WEIGHT_DONT_CARE)
builderInputSurface.set(CaptureRequest.CONTROL_AF_REGIONS, arrayOf(clearRect))
clearRegions()?.let { builderInputSurface.set(CaptureRequest.CONTROL_AF_REGIONS, it) }
}
builderInputSurface.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)
builderInputSurface.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,10 @@ public boolean prepareVideo(int width, int height, int fps, int bitrate, int iFr
}
FormatVideoEncoder formatVideoEncoder =
glInterface == null ? FormatVideoEncoder.YUV420Dynamical : FormatVideoEncoder.SURFACE;
return videoEncoder.prepareVideoEncoder(width, height, fps, bitrate, rotation, iFrameInterval,
boolean result = videoEncoder.prepareVideoEncoder(width, height, fps, bitrate, rotation, iFrameInterval,
formatVideoEncoder, profile, level);
forceFpsLimit(true);
return result;
}

/**
Expand Down
31 changes: 30 additions & 1 deletion library/src/main/java/com/pedro/library/base/Camera2Base.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import android.content.Context;
import android.graphics.Point;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.TotalCaptureResult;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaRecorder;
Expand Down Expand Up @@ -67,6 +69,8 @@
import java.util.Arrays;
import java.util.List;

import kotlin.Unit;

/**
* Wrapper to stream with camera2 api and microphone. Support stream with SurfaceView, TextureView,
* OpenGlView(Custom SurfaceView that use OpenGl) and Context(background mode). All views use
Expand Down Expand Up @@ -151,6 +155,29 @@ public void setCustomAudioEffect(CustomAudioEffect customAudioEffect) {
microphoneManager.setCustomAudioEffect(customAudioEffect);
}

public interface RequestListener {
void onRequest(CaptureRequest.Builder builder);
}

public interface CaptureResultListener {
void onCaptureResult(TotalCaptureResult result);
}

public boolean setCustomRequest(RequestListener listener) {
return cameraManager.setCustomRequest((builder) -> {
if (listener != null) listener.onRequest(builder);
return Unit.INSTANCE;
});
}

public void setCustomOnCaptureCompletedCallback(CaptureResultListener listener) {
cameraManager.setCustomOnCaptureCompletedCallback(listener == null ? null :
(session, request, result) -> {
listener.onCaptureResult(result);
return Unit.INSTANCE;
});
}

/**
* @param callback get fps while record or stream
*/
Expand Down Expand Up @@ -364,8 +391,10 @@ public boolean prepareVideo(
iFrameInterval, FormatVideoEncoder.SURFACE, profile, level);
if (!result) return false;
}
return videoEncoder.prepareVideoEncoder(width, height, fps, bitrate, rotation,
boolean result = videoEncoder.prepareVideoEncoder(width, height, fps, bitrate, rotation,
iFrameInterval, FormatVideoEncoder.SURFACE, profile, level);
forceFpsLimit(true);
return result;
}

public boolean prepareVideo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ public boolean prepareVideo(int width, int height, int fps, int bitrate, int rot
glStreamInterface.setIsPortrait(isPortrait);
}
}
forceFpsLimit(true);
return videoInitialized;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ private boolean finishPrepareVideo(int bitRate, int rotation, int profile, int
if (!result) return false;
result = videoDecoder.prepareVideo(videoEncoder.getInputSurface());
videoEnabled = result;
forceFpsLimit(true);
return result;
}

Expand Down
2 changes: 1 addition & 1 deletion library/src/main/java/com/pedro/library/base/StreamBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -540,10 +540,10 @@ abstract class StreamBase(
audioSource.stop()
glInterface.removeMediaCodecSurface()
glInterface.removeMediaCodecRecordSurface()
if (!isOnPreview) glInterface.stop()
videoEncoder.stop()
videoEncoderRecord.stop()
audioEncoder.stop()
if (!isOnPreview) glInterface.stop()
if (!isRecording) recordController.resetFormats()
}

Expand Down
11 changes: 9 additions & 2 deletions library/src/main/java/com/pedro/library/util/BitrateAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
*/
public class BitrateAdapter {

private static final float MAX_BITRATE_TOLERANCE = 0.95f;

public interface Listener {
void onBitrateAdapted(int bitrate);
}
Expand Down Expand Up @@ -74,7 +76,8 @@ public void adaptBitrate(long actualBitrate, boolean hasCongestion) {
private int getBitrateAdapted(int bitrate) {
if (bitrate >= maxBitrate) { //You have high speed and max bitrate. Keep max speed
oldBitrate = maxBitrate;
} else if (bitrate <= oldBitrate * 0.9f) { //You have low speed and bitrate too high. Reduce bitrate by 10%.
} else if (bitrate <= oldBitrate * 0.9f || isStuckAtMaxBitrate(bitrate)) {
//You have low speed and bitrate too high. Reduce bitrate by 10%.
oldBitrate = (int) (bitrate * decreaseRange);
} else { //You have high speed and bitrate too low. Increase bitrate by 10%.
oldBitrate = (int) (bitrate * increaseRange);
Expand All @@ -83,8 +86,12 @@ private int getBitrateAdapted(int bitrate) {
return oldBitrate;
}

private boolean isStuckAtMaxBitrate(int bitrate) {
return oldBitrate >= maxBitrate && bitrate < maxBitrate * MAX_BITRATE_TOLERANCE;
}

private int getBitrateAdapted(int bitrate, boolean hasCongestion) {
if (bitrate >= maxBitrate) { //You have high speed and max bitrate. Keep max speed
if (bitrate >= maxBitrate && !hasCongestion) { //You have high speed and max bitrate. Keep max speed
oldBitrate = maxBitrate;
} else if (hasCongestion) { //You have low speed and bitrate too high. Reduce bitrate by 10%.
oldBitrate = (int) (bitrate * decreaseRange);
Expand Down
Loading
Loading