diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/IMediaProjectionSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/IMediaProjectionSource.kt new file mode 100644 index 00000000..9ac28beb --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/IMediaProjectionSource.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 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.core.internal.sources + +import androidx.activity.result.ActivityResult + +/** + * Interface to implement class that uses MediaProjection. + * This interface is used to get the activity result from the activity. + */ +interface IMediaProjectionSource { + /** + * Set the activity result to get the media projection. + */ + var activityResult: ActivityResult? +} \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/AudioRecordSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/AudioRecordSource.kt new file mode 100644 index 00000000..a06a94e2 --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/AudioRecordSource.kt @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2021 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.core.internal.sources.audio + +import android.Manifest +import android.media.AudioRecord +import android.media.AudioTimestamp +import android.media.MediaFormat +import android.media.audiofx.AcousticEchoCanceler +import android.media.audiofx.NoiseSuppressor +import android.os.Build +import androidx.annotation.RequiresPermission +import io.github.thibaultbee.streampack.core.data.AudioConfig +import io.github.thibaultbee.streampack.core.internal.data.Frame +import io.github.thibaultbee.streampack.core.internal.sources.IFrameSource +import io.github.thibaultbee.streampack.core.internal.utils.TimeUtils +import io.github.thibaultbee.streampack.core.logger.Logger +import java.nio.ByteBuffer + +/** + * The [AudioRecordSource] class is an implementation of [IAudioSourceInternal] that captures audio + * from [AudioRecord]. + * + * @param enableAcousticEchoCanceler [Boolean.true] to enable AcousticEchoCanceler + * @param enableNoiseSuppressor [Boolean.true] to enable NoiseSuppressor + */ +sealed class AudioRecordSource( + private val enableAcousticEchoCanceler: Boolean = true, + private val enableNoiseSuppressor: Boolean = true +) : IAudioSourceInternal, IFrameSource { + private var audioRecord: AudioRecord? = null + + private var processor: EffectProcessor? = null + + private var mutedByteArray: ByteArray? = null + override var isMuted: Boolean = false + + private val isRunning: Boolean + get() = audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING + + protected abstract fun buildAudioRecord( + config: AudioConfig, + bufferSize: Int + ): AudioRecord + + @RequiresPermission(Manifest.permission.RECORD_AUDIO) + override fun configure(config: AudioConfig) { + /** + * [configure] might be called multiple times. + * If audio source is already running, we need to prevent reconfiguration. + */ + audioRecord?.let { + if (it.state == AudioRecord.RECORDSTATE_RECORDING) { + throw IllegalStateException("Audio source is already running") + } else { + release() + } + } + + val bufferSize = AudioRecord.getMinBufferSize( + config.sampleRate, + config.channelConfig, + config.byteFormat + ) + + if (bufferSize <= 0) { + throw IllegalArgumentException(audioRecordErrorToString(bufferSize)) + } + + /** + * Initialized mutedByteArray with bufferSize. The read buffer length may be different + * from bufferSize. In this case, mutedByteArray will be resized. + */ + mutedByteArray = ByteArray(bufferSize) + + audioRecord = buildAudioRecord(config, bufferSize).also { + processor = EffectProcessor( + enableAcousticEchoCanceler, + enableNoiseSuppressor, + it.audioSessionId + ) + + if (it.state != AudioRecord.STATE_INITIALIZED) { + throw IllegalArgumentException("Failed to initialized audio source with config: $config") + } + } + } + + override fun startStream() { + val audioRecord = requireNotNull(audioRecord) + audioRecord.startRecording() + } + + override fun stopStream() { + if (!isRunning) { + Logger.d(TAG, "Not running") + return + } + + // Stop audio record + audioRecord?.stop() + } + + override fun release() { + mutedByteArray = null + + // Release audio record + audioRecord?.release() + audioRecord = null + + processor?.release() + processor = null + } + + private fun getTimestamp(audioRecord: AudioRecord): Long { + // Get timestamp from AudioRecord + // If we can not get timestamp through getTimestamp, we timestamp audio sample. + val timestampOut = AudioTimestamp() + var timestamp: Long = -1 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (audioRecord.getTimestamp( + timestampOut, + AudioTimestamp.TIMEBASE_MONOTONIC + ) == AudioRecord.SUCCESS + ) { + timestamp = timestampOut.nanoTime / 1000 // to us + } + } + + // Fallback + if (timestamp < 0) { + timestamp = TimeUtils.currentTime() + } + + return timestamp + } + + override fun getFrame(buffer: ByteBuffer): Frame { + val audioRecord = requireNotNull(audioRecord) + val length = audioRecord.read(buffer, buffer.remaining()) + if (length >= 0) { + return if (isMuted) { + if (length != mutedByteArray?.size) { + mutedByteArray = ByteArray(length) + } + buffer.put(mutedByteArray!!, 0, length) + buffer.clear() + Frame( + buffer, + getTimestamp(audioRecord), + format = rawFormat + ) + } else { + Frame( + buffer, + getTimestamp(audioRecord), + format = rawFormat + ) + } + } else { + throw IllegalArgumentException(audioRecordErrorToString(length)) + } + } + + private fun audioRecordErrorToString(audioRecordError: Int) = when (audioRecordError) { + AudioRecord.ERROR_INVALID_OPERATION -> "AudioRecord returns an invalid operation error" + AudioRecord.ERROR_BAD_VALUE -> "AudioRecord returns a bad value error" + AudioRecord.ERROR_DEAD_OBJECT -> "AudioRecord returns a dead object error" + else -> "Unknown audio record error: $audioRecordError" + } + + companion object { + private const val TAG = "AudioSource" + + private val rawFormat = MediaFormat().apply { + setString( + MediaFormat.KEY_MIME, + MediaFormat.MIMETYPE_AUDIO_RAW + ) + } + } + + private class EffectProcessor( + enableAcousticEchoCanceler: Boolean, + enableNoiseSuppressor: Boolean, + audioSessionId: Int + ) { + private val acousticEchoCanceler = + if (enableAcousticEchoCanceler) initAcousticEchoCanceler(audioSessionId) else null + + private val noiseSuppressor = + if (enableNoiseSuppressor) initNoiseSuppressor(audioSessionId) else null + + + fun release() { + acousticEchoCanceler?.release() + noiseSuppressor?.release() + } + + companion object { + private fun initNoiseSuppressor(audioSessionId: Int): NoiseSuppressor? { + if (!NoiseSuppressor.isAvailable()) { + Logger.w(TAG, "Noise suppressor is not available") + return null + } + + val noiseSuppressor = try { + NoiseSuppressor.create(audioSessionId) + } catch (t: Throwable) { + Logger.e(TAG, "Failed to create noise suppressor", t) + return null + } + + if (noiseSuppressor == null) { + Logger.w(TAG, "Failed to create noise suppressor") + return null + } + + val result = noiseSuppressor.setEnabled(true) + if (result != NoiseSuppressor.SUCCESS) { + noiseSuppressor.release() + Logger.w(TAG, "Failed to enable noise suppressor") + return null + } + + return noiseSuppressor + } + + private fun initAcousticEchoCanceler(audioSessionId: Int): AcousticEchoCanceler? { + if (!AcousticEchoCanceler.isAvailable()) { + Logger.w(TAG, "Acoustic echo canceler is not available") + return null + } + + val acousticEchoCanceler = try { + AcousticEchoCanceler.create(audioSessionId) + } catch (t: Throwable) { + Logger.e(TAG, "Failed to create acoustic echo canceler", t) + return null + } + + if (acousticEchoCanceler == null) { + Logger.w(TAG, "Failed to create acoustic echo canceler") + return null + } + + val result = acousticEchoCanceler.setEnabled(true) + if (result != AcousticEchoCanceler.SUCCESS) { + acousticEchoCanceler.release() + Logger.w(TAG, "Failed to enable acoustic echo canceler") + return null + } + + return acousticEchoCanceler + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MediaProjectionAudioSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MediaProjectionAudioSource.kt new file mode 100644 index 00000000..8103b471 --- /dev/null +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MediaProjectionAudioSource.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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.core.internal.sources.audio + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFormat +import android.media.AudioPlaybackCaptureConfiguration +import android.media.AudioRecord +import android.media.projection.MediaProjection +import android.media.projection.MediaProjectionManager +import android.os.Build +import androidx.activity.result.ActivityResult +import androidx.annotation.RequiresApi +import io.github.thibaultbee.streampack.core.data.AudioConfig +import io.github.thibaultbee.streampack.core.internal.sources.IMediaProjectionSource + +/** + * The [MediaProjectionAudioSource] class is an implementation of [AudioRecordSource] that + * captures audio played by other apps. + * + * @param context The application context + * @param enableAcousticEchoCanceler [Boolean.true] to enable AcousticEchoCanceler + * @param enableNoiseSuppressor [Boolean.true] to enable NoiseSuppressor + */ +@RequiresApi(Build.VERSION_CODES.Q) +fun MediaProjectionAudioSource( + context: Context, + enableAcousticEchoCanceler: Boolean = true, + enableNoiseSuppressor: Boolean = true +) = MediaProjectionAudioSource( + context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager, + enableAcousticEchoCanceler, + enableNoiseSuppressor +) + +/** + * The [MediaProjectionAudioSource] class is an implementation of [IAudioSourceInternal] that + * captures audio played by other apps. + * + * @param mediaProjectionManager The media projection manager + * @param enableAcousticEchoCanceler [Boolean.true] to enable AcousticEchoCanceler + * @param enableNoiseSuppressor [Boolean.true] to enable NoiseSuppressor + */ +@RequiresApi(Build.VERSION_CODES.Q) +class MediaProjectionAudioSource( + private val mediaProjectionManager: MediaProjectionManager, + private val enableAcousticEchoCanceler: Boolean = true, + private val enableNoiseSuppressor: Boolean = true +) : AudioRecordSource(enableAcousticEchoCanceler, enableNoiseSuppressor), IMediaProjectionSource { + private var mediaProjection: MediaProjection? = null + + /** + * Set the activity result to get the media projection. + */ + override var activityResult: ActivityResult? = null + + override fun buildAudioRecord(config: AudioConfig, bufferSize: Int): AudioRecord { + val activityResult = requireNotNull(activityResult) { + "MediaProjection requires an activity result to be set" + } + + mediaProjection = mediaProjectionManager.getMediaProjection( + activityResult.resultCode, + activityResult.data!! + ) + + val audioFormat = AudioFormat.Builder() + .setEncoding(config.byteFormat) + .setSampleRate(config.sampleRate) + .setChannelMask(config.channelConfig) + .build() + + return AudioRecord.Builder() + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(bufferSize) + .setAudioPlaybackCaptureConfig( + AudioPlaybackCaptureConfiguration.Builder(requireNotNull(mediaProjection)) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .build() + ) + .build() + } + + override fun stopStream() { + super.stopStream() + + mediaProjection?.stop() + mediaProjection = null + } +} \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MicrophoneSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MicrophoneSource.kt index dbd19f13..ed652d11 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MicrophoneSource.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/audio/MicrophoneSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 Thibault B. + * Copyright (C) 2024 Thibault B. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,24 +15,15 @@ */ package io.github.thibaultbee.streampack.core.internal.sources.audio -import android.Manifest +import android.media.AudioFormat import android.media.AudioRecord -import android.media.AudioTimestamp -import android.media.MediaFormat import android.media.MediaRecorder -import android.media.audiofx.AcousticEchoCanceler -import android.media.audiofx.NoiseSuppressor import android.os.Build -import androidx.annotation.RequiresPermission import io.github.thibaultbee.streampack.core.data.AudioConfig -import io.github.thibaultbee.streampack.core.internal.data.Frame -import io.github.thibaultbee.streampack.core.internal.sources.IFrameSource -import io.github.thibaultbee.streampack.core.internal.utils.TimeUtils -import io.github.thibaultbee.streampack.core.logger.Logger -import java.nio.ByteBuffer /** - * The [MicrophoneSource] class is an implementation of [IAudioSourceInternal] that captures audio from the microphone. + * The [MicrophoneSource] class is an implementation of [AudioRecordSource] that captures audio + * from the microphone. * * @param enableAcousticEchoCanceler [Boolean.true] to enable AcousticEchoCanceler * @param enableNoiseSuppressor [Boolean.true] to enable NoiseSuppressor @@ -40,230 +31,28 @@ import java.nio.ByteBuffer class MicrophoneSource( private val enableAcousticEchoCanceler: Boolean = true, private val enableNoiseSuppressor: Boolean = true -) : IAudioSourceInternal, IFrameSource { - private var audioRecord: AudioRecord? = null - - private var processor: EffectProcessor? = null - - private var mutedByteArray: ByteArray? = null - override var isMuted: Boolean = false - - private val isRunning: Boolean - get() = audioRecord?.recordingState == AudioRecord.RECORDSTATE_RECORDING - - @RequiresPermission(Manifest.permission.RECORD_AUDIO) - override fun configure(config: AudioConfig) { - /** - * [configure] might be called multiple times. - * If audio source is already running, we need to prevent reconfiguration. - */ - audioRecord?.let { - if (it.state == AudioRecord.RECORDSTATE_RECORDING) { - throw IllegalStateException("Audio source is already running") - } else { - release() - } - } - - val bufferSize = AudioRecord.getMinBufferSize( - config.sampleRate, - config.channelConfig, - config.byteFormat - ) - - if (bufferSize <= 0) { - throw IllegalArgumentException(audioRecordErrorToString(bufferSize)) - } - - /** - * Initialized mutedByteArray with bufferSize. The read buffer length may be different - * from bufferSize. In this case, mutedByteArray will be resized. - */ - mutedByteArray = ByteArray(bufferSize) - - audioRecord = AudioRecord( - MediaRecorder.AudioSource.DEFAULT, config.sampleRate, - config.channelConfig, config.byteFormat, bufferSize - ).also { - processor = EffectProcessor( - enableAcousticEchoCanceler, - enableNoiseSuppressor, - it.audioSessionId - ) - - if (it.state != AudioRecord.STATE_INITIALIZED) { - throw IllegalArgumentException("Failed to initialized audio source with config: $config") - } - } - } - - override fun startStream() { - val audioRecord = requireNotNull(audioRecord) - audioRecord.startRecording() - } - - override fun stopStream() { - if (!isRunning) { - Logger.d(TAG, "Not running") - return - } - - // Stop audio record - audioRecord?.stop() - } - - override fun release() { - mutedByteArray = null - - // Release audio record - audioRecord?.release() - audioRecord = null - - processor?.release() - processor = null - } - - private fun getTimestamp(audioRecord: AudioRecord): Long { - // Get timestamp from AudioRecord - // If we can not get timestamp through getTimestamp, we timestamp audio sample. - val timestampOut = AudioTimestamp() - var timestamp: Long = -1 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (audioRecord.getTimestamp( - timestampOut, - AudioTimestamp.TIMEBASE_MONOTONIC - ) == AudioRecord.SUCCESS - ) { - timestamp = timestampOut.nanoTime / 1000 // to us - } - } - - // Fallback - if (timestamp < 0) { - timestamp = TimeUtils.currentTime() - } - - return timestamp - } - - override fun getFrame(buffer: ByteBuffer): Frame { - val audioRecord = requireNotNull(audioRecord) - val length = audioRecord.read(buffer, buffer.remaining()) - if (length >= 0) { - return if (isMuted) { - if (length != mutedByteArray?.size) { - mutedByteArray = ByteArray(length) - } - buffer.put(mutedByteArray!!, 0, length) - buffer.clear() - Frame( - buffer, - getTimestamp(audioRecord), - format = rawFormat - ) - } else { - Frame( - buffer, - getTimestamp(audioRecord), - format = rawFormat - ) - } +) : AudioRecordSource(enableAcousticEchoCanceler, enableNoiseSuppressor) { + override fun buildAudioRecord(config: AudioConfig, bufferSize: Int): AudioRecord { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val audioFormat = AudioFormat.Builder() + .setEncoding(config.byteFormat) + .setSampleRate(config.sampleRate) + .setChannelMask(config.channelConfig) + .build() + + AudioRecord.Builder() + .setAudioFormat(audioFormat) + .setBufferSizeInBytes(bufferSize) + .setAudioSource(MediaRecorder.AudioSource.DEFAULT) + .build() } else { - throw IllegalArgumentException(audioRecordErrorToString(length)) - } - } - - private fun audioRecordErrorToString(audioRecordError: Int) = when (audioRecordError) { - AudioRecord.ERROR_INVALID_OPERATION -> "AudioRecord returns an invalid operation error" - AudioRecord.ERROR_BAD_VALUE -> "AudioRecord returns a bad value error" - AudioRecord.ERROR_DEAD_OBJECT -> "AudioRecord returns a dead object error" - else -> "Unknown audio record error: $audioRecordError" - } - - companion object { - private const val TAG = "AudioSource" - - private val rawFormat = MediaFormat().apply { - setString( - MediaFormat.KEY_MIME, - MediaFormat.MIMETYPE_AUDIO_RAW + AudioRecord( + MediaRecorder.AudioSource.DEFAULT, + config.sampleRate, + config.channelConfig, + config.byteFormat, + bufferSize ) } } - - private class EffectProcessor( - enableAcousticEchoCanceler: Boolean, - enableNoiseSuppressor: Boolean, - audioSessionId: Int - ) { - private val acousticEchoCanceler = - if (enableAcousticEchoCanceler) initAcousticEchoCanceler(audioSessionId) else null - - private val noiseSuppressor = - if (enableNoiseSuppressor) initNoiseSuppressor(audioSessionId) else null - - - fun release() { - acousticEchoCanceler?.release() - noiseSuppressor?.release() - } - - companion object { - private fun initNoiseSuppressor(audioSessionId: Int): NoiseSuppressor? { - if (!NoiseSuppressor.isAvailable()) { - Logger.w(TAG, "Noise suppressor is not available") - return null - } - - val noiseSuppressor = try { - NoiseSuppressor.create(audioSessionId) - } catch (t: Throwable) { - Logger.e(TAG, "Failed to create noise suppressor", t) - return null - } - - if (noiseSuppressor == null) { - Logger.w(TAG, "Failed to create noise suppressor") - return null - } - - val result = noiseSuppressor.setEnabled(true) - if (result != NoiseSuppressor.SUCCESS) { - noiseSuppressor.release() - Logger.w(TAG, "Failed to enable noise suppressor") - return null - } - - return noiseSuppressor - } - - private fun initAcousticEchoCanceler(audioSessionId: Int): AcousticEchoCanceler? { - if (!AcousticEchoCanceler.isAvailable()) { - Logger.w(TAG, "Acoustic echo canceler is not available") - return null - } - - val acousticEchoCanceler = try { - AcousticEchoCanceler.create(audioSessionId) - } catch (t: Throwable) { - Logger.e(TAG, "Failed to create acoustic echo canceler", t) - return null - } - - if (acousticEchoCanceler == null) { - Logger.w(TAG, "Failed to create acoustic echo canceler") - return null - } - - val result = acousticEchoCanceler.setEnabled(true) - if (result != AcousticEchoCanceler.SUCCESS) { - acousticEchoCanceler.release() - Logger.w(TAG, "Failed to enable acoustic echo canceler") - return null - } - - return acousticEchoCanceler - } - } - } } \ No newline at end of file diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/mediaprojection/MediaProjectionVideoSource.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/mediaprojection/MediaProjectionVideoSource.kt index 974ea1a5..5aacf247 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/mediaprojection/MediaProjectionVideoSource.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/internal/sources/video/mediaprojection/MediaProjectionVideoSource.kt @@ -28,6 +28,7 @@ import androidx.activity.result.ActivityResult import io.github.thibaultbee.streampack.core.data.VideoConfig import io.github.thibaultbee.streampack.core.internal.data.Frame import io.github.thibaultbee.streampack.core.internal.orientation.AbstractSourceOrientationProvider +import io.github.thibaultbee.streampack.core.internal.sources.IMediaProjectionSource import io.github.thibaultbee.streampack.core.internal.sources.video.IVideoSourceInternal import io.github.thibaultbee.streampack.core.internal.utils.extensions.isDevicePortrait import io.github.thibaultbee.streampack.core.internal.utils.extensions.landscapize @@ -37,7 +38,7 @@ import java.nio.ByteBuffer class MediaProjectionVideoSource( context: Context -) : IVideoSourceInternal { +) : IVideoSourceInternal, IMediaProjectionSource { override var outputSurface: Surface? = null override val timestampOffset = 0L override val hasOutputSurface = true @@ -45,11 +46,15 @@ class MediaProjectionVideoSource( override val orientationProvider = ScreenSourceOrientationProvider(context) override fun getFrame(buffer: ByteBuffer): Frame { - throw UnsupportedOperationException("Screen source expects to run in Surface mode") + throw UnsupportedOperationException("Screen source run in Surface mode") } private var mediaProjection: MediaProjection? = null - var activityResult: ActivityResult? = null + + /** + * Set the activity result to get the media projection. + */ + override var activityResult: ActivityResult? = null var listener: Listener? = null @@ -103,15 +108,17 @@ class MediaProjectionVideoSource( override suspend fun startStream() { val videoConfig = requireNotNull(videoConfig) { "Video has not been configured!" } - val activityResult = requireNotNull(activityResult) { "Activity result must be set!" } - - val resultCode = activityResult.resultCode - val resultData = activityResult.data!! + val activityResult = requireNotNull(activityResult) { + "MediaProjection requires an activity result to be set" + } isStoppedByUser = false val orientedSize = orientationProvider.getOrientedSize(videoConfig.resolution) - mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData).apply { + mediaProjection = mediaProjectionManager.getMediaProjection( + activityResult.resultCode, + activityResult.data!! + ).apply { registerCallback(mediaProjectionCallback, virtualDisplayHandler) virtualDisplay = createVirtualDisplay( VIRTUAL_DISPLAY_NAME, @@ -126,7 +133,6 @@ class MediaProjectionVideoSource( } } - override suspend fun stopStream() { isStoppedByUser = true diff --git a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/DefaultScreenRecorderStreamer.kt b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/DefaultScreenRecorderStreamer.kt index 8a7589d3..95e35052 100644 --- a/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/DefaultScreenRecorderStreamer.kt +++ b/core/src/main/java/io/github/thibaultbee/streampack/core/streamers/DefaultScreenRecorderStreamer.kt @@ -23,6 +23,7 @@ import androidx.activity.result.ActivityResult import androidx.core.app.ActivityCompat import io.github.thibaultbee.streampack.core.internal.endpoints.DynamicEndpoint import io.github.thibaultbee.streampack.core.internal.endpoints.IEndpointInternal +import io.github.thibaultbee.streampack.core.internal.sources.IMediaProjectionSource import io.github.thibaultbee.streampack.core.internal.sources.audio.IAudioSourceInternal import io.github.thibaultbee.streampack.core.internal.sources.audio.MicrophoneSource import io.github.thibaultbee.streampack.core.internal.sources.video.mediaprojection.MediaProjectionVideoSource @@ -102,5 +103,8 @@ open class DefaultScreenRecorderStreamer( */ set(value) { mediaProjectionVideoSource.activityResult = value + if (audioSourceInternal is IMediaProjectionSource) { + (audioSourceInternal as IMediaProjectionSource).activityResult = value + } } } \ No newline at end of file