Committed by
GitHub
android higher level track apis (#22)
* connect options and publish defaults * default capture options * more higher level track apis * selecting camera by deviceId * fix tests
正在显示
20 个修改的文件
包含
516 行增加
和
94 行删除
| 1 | package io.livekit.android | 1 | package io.livekit.android |
| 2 | 2 | ||
| 3 | +import io.livekit.android.room.participant.AudioTrackPublishDefaults | ||
| 4 | +import io.livekit.android.room.participant.VideoTrackPublishDefaults | ||
| 5 | +import io.livekit.android.room.track.LocalAudioTrackOptions | ||
| 6 | +import io.livekit.android.room.track.LocalVideoTrackOptions | ||
| 7 | +import org.webrtc.PeerConnection | ||
| 3 | 8 | ||
| 4 | -class ConnectOptions( | ||
| 5 | - var autoSubscribe: Boolean = true | 9 | + |
| 10 | +data class ConnectOptions( | ||
| 11 | + val autoSubscribe: Boolean = true, | ||
| 12 | + val iceServers: List<PeerConnection.IceServer>? = null, | ||
| 13 | + val rtcConfig: PeerConnection.RTCConfiguration? = null, | ||
| 14 | + /** | ||
| 15 | + * capture and publish audio track on connect, defaults to false | ||
| 16 | + */ | ||
| 17 | + val audio: Boolean = false, | ||
| 18 | + /** | ||
| 19 | + * capture and publish video track on connect, defaults to false | ||
| 20 | + */ | ||
| 21 | + val video: Boolean = false, | ||
| 22 | + | ||
| 23 | + val audioTrackCaptureDefaults: LocalAudioTrackOptions? = null, | ||
| 24 | + val videoTrackCaptureDefaults: LocalVideoTrackOptions? = null, | ||
| 25 | + val audioTrackPublishDefaults: AudioTrackPublishDefaults? = null, | ||
| 26 | + val videoTrackPublishDefaults: VideoTrackPublishDefaults? = null, | ||
| 6 | ) { | 27 | ) { |
| 7 | internal var reconnect: Boolean = false | 28 | internal var reconnect: Boolean = false |
| 8 | } | 29 | } |
| @@ -48,6 +48,28 @@ class LiveKit { | @@ -48,6 +48,28 @@ class LiveKit { | ||
| 48 | room.listener = listener | 48 | room.listener = listener |
| 49 | room.connect(url, token, options) | 49 | room.connect(url, token, options) |
| 50 | 50 | ||
| 51 | + options?.audioTrackCaptureDefaults?.let { | ||
| 52 | + room.localParticipant.audioTrackCaptureDefaults = it | ||
| 53 | + } | ||
| 54 | + options?.videoTrackCaptureDefaults?.let { | ||
| 55 | + room.localParticipant.videoTrackCaptureDefaults = it | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + options?.audioTrackPublishDefaults?.let { | ||
| 59 | + room.localParticipant.audioTrackPublishDefaults = it | ||
| 60 | + } | ||
| 61 | + options?.videoTrackPublishDefaults?.let { | ||
| 62 | + room.localParticipant.videoTrackPublishDefaults = it | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + if (options?.audio == true) { | ||
| 66 | + val audioTrack = room.localParticipant.createAudioTrack() | ||
| 67 | + room.localParticipant.publishAudioTrack(audioTrack) | ||
| 68 | + } | ||
| 69 | + if (options?.video == true) { | ||
| 70 | + val videoTrack = room.localParticipant.createVideoTrack() | ||
| 71 | + room.localParticipant.publishVideoTrack(videoTrack) | ||
| 72 | + } | ||
| 51 | return room | 73 | return room |
| 52 | } | 74 | } |
| 53 | 75 |
| 1 | +package io.livekit.android.room | ||
| 2 | + | ||
| 3 | +import io.livekit.android.room.participant.AudioTrackPublishDefaults | ||
| 4 | +import io.livekit.android.room.participant.VideoTrackPublishDefaults | ||
| 5 | +import io.livekit.android.room.track.LocalAudioTrackOptions | ||
| 6 | +import io.livekit.android.room.track.LocalVideoTrackOptions | ||
| 7 | +import javax.inject.Inject | ||
| 8 | +import javax.inject.Singleton | ||
| 9 | + | ||
| 10 | +@Singleton | ||
| 11 | +class DefaultsManager | ||
| 12 | +@Inject | ||
| 13 | +constructor() { | ||
| 14 | + var audioTrackCaptureDefaults: LocalAudioTrackOptions = LocalAudioTrackOptions() | ||
| 15 | + var audioTrackPublishDefaults: AudioTrackPublishDefaults = AudioTrackPublishDefaults() | ||
| 16 | + var videoTrackCaptureDefaults: LocalVideoTrackOptions = LocalVideoTrackOptions() | ||
| 17 | + var videoTrackPublishDefaults: VideoTrackPublishDefaults = VideoTrackPublishDefaults() | ||
| 18 | +} |
| 1 | +package io.livekit.android.room | ||
| 2 | + | ||
| 3 | +import android.content.Context | ||
| 4 | +import android.hardware.camera2.CameraManager | ||
| 5 | +import android.os.Handler | ||
| 6 | +import android.os.Looper | ||
| 7 | +import org.webrtc.Camera1Enumerator | ||
| 8 | +import org.webrtc.Camera2Enumerator | ||
| 9 | + | ||
| 10 | +object DeviceManager { | ||
| 11 | + | ||
| 12 | + enum class Kind { | ||
| 13 | + // Only camera input currently, audio input/output only has one option atm. | ||
| 14 | + CAMERA; | ||
| 15 | + } | ||
| 16 | + | ||
| 17 | + private val defaultDevices = mutableMapOf<Kind, String>() | ||
| 18 | + private val listeners = | ||
| 19 | + mutableMapOf<Kind, MutableList<OnDeviceAvailabilityChangeListener>>() | ||
| 20 | + | ||
| 21 | + private var hasSetupListeners = false | ||
| 22 | + | ||
| 23 | + @Synchronized | ||
| 24 | + internal fun setupListenersIfNeeded(context: Context) { | ||
| 25 | + if (hasSetupListeners) { | ||
| 26 | + return | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + hasSetupListeners = true | ||
| 30 | + val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager | ||
| 31 | + cameraManager.registerAvailabilityCallback(object : CameraManager.AvailabilityCallback() { | ||
| 32 | + override fun onCameraAvailable(cameraId: String) { | ||
| 33 | + notifyListeners(Kind.CAMERA) | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + override fun onCameraUnavailable(cameraId: String) { | ||
| 37 | + notifyListeners(Kind.CAMERA) | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + override fun onCameraAccessPrioritiesChanged() { | ||
| 41 | + notifyListeners(Kind.CAMERA) | ||
| 42 | + } | ||
| 43 | + }, Handler(Looper.getMainLooper())) | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + fun getDefaultDevice(kind: Kind): String? { | ||
| 47 | + return defaultDevices[kind] | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + fun setDefaultDevice(kind: Kind, deviceId: String?) { | ||
| 51 | + if (deviceId != null) { | ||
| 52 | + defaultDevices[kind] = deviceId | ||
| 53 | + } else { | ||
| 54 | + defaultDevices.remove(kind) | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * @return the list of device ids for [kind] | ||
| 60 | + */ | ||
| 61 | + fun getDevices(context: Context, kind: Kind): List<String> { | ||
| 62 | + return when (kind) { | ||
| 63 | + Kind.CAMERA -> { | ||
| 64 | + val cameraEnumerator = if (Camera2Enumerator.isSupported(context)) { | ||
| 65 | + Camera2Enumerator(context) | ||
| 66 | + } else { | ||
| 67 | + Camera1Enumerator() | ||
| 68 | + } | ||
| 69 | + cameraEnumerator.deviceNames.toList() | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + fun registerOnDeviceAvailabilityChange( | ||
| 75 | + kind: Kind, | ||
| 76 | + listener: OnDeviceAvailabilityChangeListener | ||
| 77 | + ) { | ||
| 78 | + if (listeners[kind] == null) { | ||
| 79 | + listeners[kind] = mutableListOf() | ||
| 80 | + } | ||
| 81 | + listeners[kind]!!.add(listener) | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + fun unregisterOnDeviceAvailabilityChange( | ||
| 85 | + kind: Kind, | ||
| 86 | + listener: OnDeviceAvailabilityChangeListener | ||
| 87 | + ) { | ||
| 88 | + listeners[kind]?.remove(listener) | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + private fun notifyListeners(kind: Kind) { | ||
| 92 | + listeners[kind]?.forEach { | ||
| 93 | + it.onDeviceAvailabilityChanged(kind) | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + interface OnDeviceAvailabilityChangeListener { | ||
| 98 | + fun onDeviceAvailabilityChanged(kind: Kind) | ||
| 99 | + } | ||
| 100 | +} |
| @@ -3,10 +3,7 @@ package io.livekit.android.room | @@ -3,10 +3,7 @@ package io.livekit.android.room | ||
| 3 | import android.os.SystemClock | 3 | import android.os.SystemClock |
| 4 | import io.livekit.android.ConnectOptions | 4 | import io.livekit.android.ConnectOptions |
| 5 | import io.livekit.android.dagger.InjectionNames | 5 | import io.livekit.android.dagger.InjectionNames |
| 6 | -import io.livekit.android.room.track.DataPublishReliability | ||
| 7 | -import io.livekit.android.room.track.Track | ||
| 8 | import io.livekit.android.room.track.TrackException | 6 | import io.livekit.android.room.track.TrackException |
| 9 | -import io.livekit.android.room.track.TrackPublication | ||
| 10 | import io.livekit.android.room.util.* | 7 | import io.livekit.android.room.util.* |
| 11 | import io.livekit.android.util.CloseableCoroutineScope | 8 | import io.livekit.android.util.CloseableCoroutineScope |
| 12 | import io.livekit.android.util.Either | 9 | import io.livekit.android.util.Either |
| @@ -21,7 +18,6 @@ import livekit.LivekitRtc | @@ -21,7 +18,6 @@ import livekit.LivekitRtc | ||
| 21 | import org.webrtc.* | 18 | import org.webrtc.* |
| 22 | import java.net.ConnectException | 19 | import java.net.ConnectException |
| 23 | import java.nio.ByteBuffer | 20 | import java.nio.ByteBuffer |
| 24 | -import java.util.concurrent.TimeUnit | ||
| 25 | import javax.inject.Inject | 21 | import javax.inject.Inject |
| 26 | import javax.inject.Named | 22 | import javax.inject.Named |
| 27 | import javax.inject.Singleton | 23 | import javax.inject.Singleton |
| @@ -101,7 +97,7 @@ internal constructor( | @@ -101,7 +97,7 @@ internal constructor( | ||
| 101 | isSubscriberPrimary = joinResponse.subscriberPrimary | 97 | isSubscriberPrimary = joinResponse.subscriberPrimary |
| 102 | 98 | ||
| 103 | if (!this::publisher.isInitialized) { | 99 | if (!this::publisher.isInitialized) { |
| 104 | - configure(joinResponse) | 100 | + configure(joinResponse, options) |
| 105 | } | 101 | } |
| 106 | // create offer | 102 | // create offer |
| 107 | if (!this.isSubscriberPrimary) { | 103 | if (!this.isSubscriberPrimary) { |
| @@ -111,18 +107,21 @@ internal constructor( | @@ -111,18 +107,21 @@ internal constructor( | ||
| 111 | return joinResponse | 107 | return joinResponse |
| 112 | } | 108 | } |
| 113 | 109 | ||
| 114 | - private fun configure(joinResponse: LivekitRtc.JoinResponse) { | 110 | + private fun configure(joinResponse: LivekitRtc.JoinResponse, connectOptions: ConnectOptions?) { |
| 115 | if (this::publisher.isInitialized || this::subscriber.isInitialized) { | 111 | if (this::publisher.isInitialized || this::subscriber.isInitialized) { |
| 116 | // already configured | 112 | // already configured |
| 117 | return | 113 | return |
| 118 | } | 114 | } |
| 119 | 115 | ||
| 120 | // update ICE servers before creating PeerConnection | 116 | // update ICE servers before creating PeerConnection |
| 121 | - val iceServers = mutableListOf<PeerConnection.IceServer>() | 117 | + val iceServers = if (connectOptions?.iceServers != null) { |
| 118 | + connectOptions.iceServers | ||
| 119 | + } else { | ||
| 120 | + val servers = mutableListOf<PeerConnection.IceServer>() | ||
| 122 | for (serverInfo in joinResponse.iceServersList) { | 121 | for (serverInfo in joinResponse.iceServersList) { |
| 123 | val username = serverInfo.username ?: "" | 122 | val username = serverInfo.username ?: "" |
| 124 | val credential = serverInfo.credential ?: "" | 123 | val credential = serverInfo.credential ?: "" |
| 125 | - iceServers.add( | 124 | + servers.add( |
| 126 | PeerConnection.IceServer | 125 | PeerConnection.IceServer |
| 127 | .builder(serverInfo.urlsList) | 126 | .builder(serverInfo.urlsList) |
| 128 | .setUsername(username) | 127 | .setUsername(username) |
| @@ -131,25 +130,29 @@ internal constructor( | @@ -131,25 +130,29 @@ internal constructor( | ||
| 131 | ) | 130 | ) |
| 132 | } | 131 | } |
| 133 | 132 | ||
| 134 | - if (iceServers.isEmpty()) { | ||
| 135 | - iceServers.addAll(SignalClient.DEFAULT_ICE_SERVERS) | ||
| 136 | - } | ||
| 137 | - joinResponse.iceServersList.forEach { | ||
| 138 | - LKLog.v { "username = \"${it.username}\"" } | ||
| 139 | - LKLog.v { "credential = \"${it.credential}\"" } | ||
| 140 | - LKLog.v { "urls: " } | ||
| 141 | - it.urlsList.forEach { | ||
| 142 | - LKLog.v { " $it" } | 133 | + if (servers.isEmpty()) { |
| 134 | + servers.addAll(SignalClient.DEFAULT_ICE_SERVERS) | ||
| 143 | } | 135 | } |
| 136 | + servers | ||
| 144 | } | 137 | } |
| 145 | 138 | ||
| 146 | // Setup peer connections | 139 | // Setup peer connections |
| 147 | - val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply { | 140 | + val rtcConfig = connectOptions?.rtcConfig?.apply { |
| 141 | + val mergedServers = this.iceServers.toMutableList() | ||
| 142 | + iceServers.forEach { server -> | ||
| 143 | + if (!mergedServers.contains(server)) { | ||
| 144 | + mergedServers.add(server) | ||
| 145 | + } | ||
| 146 | + } | ||
| 147 | + } | ||
| 148 | + ?: PeerConnection.RTCConfiguration(iceServers).apply { | ||
| 148 | sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN | 149 | sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN |
| 149 | - continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY | 150 | + continualGatheringPolicy = |
| 151 | + PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY | ||
| 150 | enableDtlsSrtp = true | 152 | enableDtlsSrtp = true |
| 151 | } | 153 | } |
| 152 | 154 | ||
| 155 | + | ||
| 153 | publisher = pctFactory.create( | 156 | publisher = pctFactory.create( |
| 154 | rtcConfig, | 157 | rtcConfig, |
| 155 | publisherObserver, | 158 | publisherObserver, |
| @@ -24,7 +24,8 @@ constructor( | @@ -24,7 +24,8 @@ constructor( | ||
| 24 | @Assisted private val context: Context, | 24 | @Assisted private val context: Context, |
| 25 | private val engine: RTCEngine, | 25 | private val engine: RTCEngine, |
| 26 | private val eglBase: EglBase, | 26 | private val eglBase: EglBase, |
| 27 | - private val localParticipantFactory: LocalParticipant.Factory | 27 | + private val localParticipantFactory: LocalParticipant.Factory, |
| 28 | + private val defaultsManager: DefaultsManager, | ||
| 28 | ) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { | 29 | ) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() { |
| 29 | init { | 30 | init { |
| 30 | engine.listener = this | 31 | engine.listener = this |
| @@ -51,6 +52,11 @@ constructor( | @@ -51,6 +52,11 @@ constructor( | ||
| 51 | var metadata: String? = null | 52 | var metadata: String? = null |
| 52 | private set | 53 | private set |
| 53 | 54 | ||
| 55 | + var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults | ||
| 56 | + var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults | ||
| 57 | + var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults | ||
| 58 | + var videoTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::videoTrackPublishDefaults | ||
| 59 | + | ||
| 54 | lateinit var localParticipant: LocalParticipant | 60 | lateinit var localParticipant: LocalParticipant |
| 55 | private set | 61 | private set |
| 56 | private val mutableRemoteParticipants = mutableMapOf<String, RemoteParticipant>() | 62 | private val mutableRemoteParticipants = mutableMapOf<String, RemoteParticipant>() |
| @@ -583,6 +589,16 @@ interface RoomListener { | @@ -583,6 +589,16 @@ interface RoomListener { | ||
| 583 | * @param quality the new connection quality | 589 | * @param quality the new connection quality |
| 584 | */ | 590 | */ |
| 585 | fun onConnectionQualityChanged(participant: Participant, quality: ConnectionQuality) {} | 591 | fun onConnectionQualityChanged(participant: Participant, quality: ConnectionQuality) {} |
| 592 | + | ||
| 593 | + companion object { | ||
| 594 | + fun getDefaultDevice(kind: DeviceManager.Kind): String? { | ||
| 595 | + return DeviceManager.getDefaultDevice(kind) | ||
| 596 | + } | ||
| 597 | + | ||
| 598 | + fun setDefaultDevice(kind: DeviceManager.Kind, deviceId: String?) { | ||
| 599 | + DeviceManager.setDefaultDevice(kind, deviceId) | ||
| 600 | + } | ||
| 601 | + } | ||
| 586 | } | 602 | } |
| 587 | 603 | ||
| 588 | sealed class RoomException(message: String? = null, cause: Throwable? = null) : | 604 | sealed class RoomException(message: String? = null, cause: Throwable? = null) : |
| @@ -7,12 +7,16 @@ import com.google.protobuf.ByteString | @@ -7,12 +7,16 @@ import com.google.protobuf.ByteString | ||
| 7 | import dagger.assisted.Assisted | 7 | import dagger.assisted.Assisted |
| 8 | import dagger.assisted.AssistedFactory | 8 | import dagger.assisted.AssistedFactory |
| 9 | import dagger.assisted.AssistedInject | 9 | import dagger.assisted.AssistedInject |
| 10 | +import io.livekit.android.room.DefaultsManager | ||
| 10 | import io.livekit.android.room.RTCEngine | 11 | import io.livekit.android.room.RTCEngine |
| 11 | import io.livekit.android.room.track.* | 12 | import io.livekit.android.room.track.* |
| 12 | import io.livekit.android.util.LKLog | 13 | import io.livekit.android.util.LKLog |
| 13 | import livekit.LivekitModels | 14 | import livekit.LivekitModels |
| 14 | import livekit.LivekitRtc | 15 | import livekit.LivekitRtc |
| 15 | -import org.webrtc.* | 16 | +import org.webrtc.EglBase |
| 17 | +import org.webrtc.PeerConnectionFactory | ||
| 18 | +import org.webrtc.RtpParameters | ||
| 19 | +import org.webrtc.RtpTransceiver | ||
| 16 | import kotlin.math.abs | 20 | import kotlin.math.abs |
| 17 | import kotlin.math.roundToInt | 21 | import kotlin.math.roundToInt |
| 18 | 22 | ||
| @@ -26,8 +30,14 @@ internal constructor( | @@ -26,8 +30,14 @@ internal constructor( | ||
| 26 | private val context: Context, | 30 | private val context: Context, |
| 27 | private val eglBase: EglBase, | 31 | private val eglBase: EglBase, |
| 28 | private val screencastVideoTrackFactory: LocalScreencastVideoTrack.Factory, | 32 | private val screencastVideoTrackFactory: LocalScreencastVideoTrack.Factory, |
| 29 | -) : | ||
| 30 | - Participant(info.sid, info.identity) { | 33 | + private val videoTrackFactory: LocalVideoTrack.Factory, |
| 34 | + private val defaultsManager: DefaultsManager | ||
| 35 | +) : Participant(info.sid, info.identity) { | ||
| 36 | + | ||
| 37 | + var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults | ||
| 38 | + var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults | ||
| 39 | + var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults | ||
| 40 | + var videoTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::videoTrackPublishDefaults | ||
| 31 | 41 | ||
| 32 | init { | 42 | init { |
| 33 | updateFromInfo(info) | 43 | updateFromInfo(info) |
| @@ -45,18 +55,9 @@ internal constructor( | @@ -45,18 +55,9 @@ internal constructor( | ||
| 45 | */ | 55 | */ |
| 46 | fun createAudioTrack( | 56 | fun createAudioTrack( |
| 47 | name: String = "", | 57 | name: String = "", |
| 48 | - options: LocalAudioTrackOptions = LocalAudioTrackOptions(), | 58 | + options: LocalAudioTrackOptions = audioTrackCaptureDefaults, |
| 49 | ): LocalAudioTrack { | 59 | ): LocalAudioTrack { |
| 50 | - val audioConstraints = MediaConstraints() | ||
| 51 | - val items = listOf( | ||
| 52 | - MediaConstraints.KeyValuePair("googEchoCancellation", options.echoCancellation.toString()), | ||
| 53 | - MediaConstraints.KeyValuePair("googAutoGainControl", options.autoGainControl.toString()), | ||
| 54 | - MediaConstraints.KeyValuePair("googHighpassFilter", options.highPassFilter.toString()), | ||
| 55 | - MediaConstraints.KeyValuePair("googNoiseSuppression", options.noiseSuppression.toString()), | ||
| 56 | - MediaConstraints.KeyValuePair("googTypingNoiseDetection", options.typingNoiseDetection.toString()), | ||
| 57 | - ) | ||
| 58 | - audioConstraints.optional.addAll(items) | ||
| 59 | - return LocalAudioTrack.createTrack(context, peerConnectionFactory, audioConstraints, name) | 60 | + return LocalAudioTrack.createTrack(context, peerConnectionFactory, options, name) |
| 60 | } | 61 | } |
| 61 | 62 | ||
| 62 | /** | 63 | /** |
| @@ -66,14 +67,15 @@ internal constructor( | @@ -66,14 +67,15 @@ internal constructor( | ||
| 66 | */ | 67 | */ |
| 67 | fun createVideoTrack( | 68 | fun createVideoTrack( |
| 68 | name: String = "", | 69 | name: String = "", |
| 69 | - options: LocalVideoTrackOptions = LocalVideoTrackOptions(), | 70 | + options: LocalVideoTrackOptions = videoTrackCaptureDefaults.copy(), |
| 70 | ): LocalVideoTrack { | 71 | ): LocalVideoTrack { |
| 71 | return LocalVideoTrack.createTrack( | 72 | return LocalVideoTrack.createTrack( |
| 72 | peerConnectionFactory, | 73 | peerConnectionFactory, |
| 73 | context, | 74 | context, |
| 74 | name, | 75 | name, |
| 75 | options, | 76 | options, |
| 76 | - eglBase | 77 | + eglBase, |
| 78 | + videoTrackFactory, | ||
| 77 | ) | 79 | ) |
| 78 | } | 80 | } |
| 79 | 81 | ||
| @@ -98,9 +100,62 @@ internal constructor( | @@ -98,9 +100,62 @@ internal constructor( | ||
| 98 | ) | 100 | ) |
| 99 | } | 101 | } |
| 100 | 102 | ||
| 103 | + override fun getTrackPublication(source: Track.Source): LocalTrackPublication? { | ||
| 104 | + return super.getTrackPublication(source) as? LocalTrackPublication | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + override fun getTrackPublicationByName(name: String): LocalTrackPublication? { | ||
| 108 | + return super.getTrackPublicationByName(name) as? LocalTrackPublication | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + private suspend fun setTrackEnabled( | ||
| 112 | + source: Track.Source, | ||
| 113 | + enabled: Boolean, | ||
| 114 | + mediaProjectionPermissionResultData: Intent? = null | ||
| 115 | + | ||
| 116 | + ) { | ||
| 117 | + val pub = getTrackPublication(source) | ||
| 118 | + if (enabled) { | ||
| 119 | + if (pub != null) { | ||
| 120 | + pub.muted = false | ||
| 121 | + } else { | ||
| 122 | + when (source) { | ||
| 123 | + Track.Source.CAMERA -> { | ||
| 124 | + val track = createVideoTrack() | ||
| 125 | + publishVideoTrack(track) | ||
| 126 | + } | ||
| 127 | + Track.Source.MICROPHONE -> { | ||
| 128 | + val track = createAudioTrack() | ||
| 129 | + publishAudioTrack(track) | ||
| 130 | + } | ||
| 131 | + Track.Source.SCREEN_SHARE -> { | ||
| 132 | + if (mediaProjectionPermissionResultData == null) { | ||
| 133 | + throw IllegalArgumentException("Media Projection permission result data is required to create a screen share track.") | ||
| 134 | + } | ||
| 135 | + val track = | ||
| 136 | + createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData) | ||
| 137 | + publishVideoTrack(track) | ||
| 138 | + } | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + } else { | ||
| 142 | + pub?.track?.let { track -> | ||
| 143 | + // screenshare cannot be muted, unpublish instead | ||
| 144 | + if (pub.source == Track.Source.SCREEN_SHARE) { | ||
| 145 | + unpublishTrack(track) | ||
| 146 | + } else { | ||
| 147 | + pub.muted = true | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | + } | ||
| 151 | + } | ||
| 152 | + | ||
| 101 | suspend fun publishAudioTrack( | 153 | suspend fun publishAudioTrack( |
| 102 | track: LocalAudioTrack, | 154 | track: LocalAudioTrack, |
| 103 | - options: AudioTrackPublishOptions = AudioTrackPublishOptions(), | 155 | + options: AudioTrackPublishOptions = AudioTrackPublishOptions( |
| 156 | + null, | ||
| 157 | + audioTrackPublishDefaults | ||
| 158 | + ), | ||
| 104 | publishListener: PublishListener? = null | 159 | publishListener: PublishListener? = null |
| 105 | ) { | 160 | ) { |
| 106 | if (localTrackPublications.any { it.track == track }) { | 161 | if (localTrackPublications.any { it.track == track }) { |
| @@ -140,7 +195,7 @@ internal constructor( | @@ -140,7 +195,7 @@ internal constructor( | ||
| 140 | 195 | ||
| 141 | suspend fun publishVideoTrack( | 196 | suspend fun publishVideoTrack( |
| 142 | track: LocalVideoTrack, | 197 | track: LocalVideoTrack, |
| 143 | - options: VideoTrackPublishOptions = VideoTrackPublishOptions(), | 198 | + options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), |
| 144 | publishListener: PublishListener? = null | 199 | publishListener: PublishListener? = null |
| 145 | ) { | 200 | ) { |
| 146 | if (localTrackPublications.any { it.track == track }) { | 201 | if (localTrackPublications.any { it.track == track }) { |
| @@ -339,7 +394,7 @@ internal constructor( | @@ -339,7 +394,7 @@ internal constructor( | ||
| 339 | for (ti in info.tracksList) { | 394 | for (ti in info.tracksList) { |
| 340 | val publication = this.tracks[ti.sid] as? LocalTrackPublication ?: continue | 395 | val publication = this.tracks[ti.sid] as? LocalTrackPublication ?: continue |
| 341 | if (ti.muted != publication.muted) { | 396 | if (ti.muted != publication.muted) { |
| 342 | - publication.setMuted(ti.muted) | 397 | + publication.muted = ti.muted |
| 343 | } | 398 | } |
| 344 | } | 399 | } |
| 345 | } | 400 | } |
| @@ -382,15 +437,53 @@ interface TrackPublishOptions { | @@ -382,15 +437,53 @@ interface TrackPublishOptions { | ||
| 382 | val name: String? | 437 | val name: String? |
| 383 | } | 438 | } |
| 384 | 439 | ||
| 440 | +abstract class BaseVideoTrackPublishOptions { | ||
| 441 | + abstract val videoEncoding: VideoEncoding? | ||
| 442 | + abstract val simulcast: Boolean | ||
| 443 | + //val videoCodec: VideoCodec? = null, | ||
| 444 | +} | ||
| 445 | + | ||
| 446 | +data class VideoTrackPublishDefaults( | ||
| 447 | + override val videoEncoding: VideoEncoding? = null, | ||
| 448 | + override val simulcast: Boolean = false | ||
| 449 | +) : BaseVideoTrackPublishOptions() | ||
| 450 | + | ||
| 385 | data class VideoTrackPublishOptions( | 451 | data class VideoTrackPublishOptions( |
| 386 | override val name: String? = null, | 452 | override val name: String? = null, |
| 387 | - val videoEncoding: VideoEncoding? = null, | ||
| 388 | - //val videoCodec: VideoCodec? = null, | ||
| 389 | - val simulcast: Boolean = false | ||
| 390 | -) : TrackPublishOptions | 453 | + override val videoEncoding: VideoEncoding? = null, |
| 454 | + override val simulcast: Boolean = false | ||
| 455 | +) : BaseVideoTrackPublishOptions(), TrackPublishOptions { | ||
| 456 | + constructor( | ||
| 457 | + name: String? = null, | ||
| 458 | + base: BaseVideoTrackPublishOptions | ||
| 459 | + ) : this( | ||
| 460 | + name, | ||
| 461 | + base.videoEncoding, | ||
| 462 | + base.simulcast | ||
| 463 | + ) | ||
| 464 | +} | ||
| 465 | + | ||
| 466 | +abstract class BaseAudioTrackPublishOptions { | ||
| 467 | + abstract val audioBitrate: Int? | ||
| 468 | + abstract val dtx: Boolean | ||
| 469 | +} | ||
| 470 | + | ||
| 471 | +data class AudioTrackPublishDefaults( | ||
| 472 | + override val audioBitrate: Int? = null, | ||
| 473 | + override val dtx: Boolean = true | ||
| 474 | +) : BaseAudioTrackPublishOptions() | ||
| 391 | 475 | ||
| 392 | data class AudioTrackPublishOptions( | 476 | data class AudioTrackPublishOptions( |
| 393 | override val name: String? = null, | 477 | override val name: String? = null, |
| 394 | - val audioBitrate: Int? = null, | ||
| 395 | - val dtx: Boolean = true | ||
| 396 | -) : TrackPublishOptions | ||
| 478 | + override val audioBitrate: Int? = null, | ||
| 479 | + override val dtx: Boolean = true | ||
| 480 | +) : BaseAudioTrackPublishOptions(), TrackPublishOptions { | ||
| 481 | + constructor( | ||
| 482 | + name: String? = null, | ||
| 483 | + base: BaseAudioTrackPublishOptions | ||
| 484 | + ) : this( | ||
| 485 | + name, | ||
| 486 | + base.audioBitrate, | ||
| 487 | + base.dtx | ||
| 488 | + ) | ||
| 489 | +} |
| @@ -63,9 +63,51 @@ open class Participant(var sid: String, identity: String? = null) { | @@ -63,9 +63,51 @@ open class Participant(var sid: String, identity: String? = null) { | ||
| 63 | when (publication.kind) { | 63 | when (publication.kind) { |
| 64 | Track.Kind.AUDIO -> audioTracks[publication.sid] = publication | 64 | Track.Kind.AUDIO -> audioTracks[publication.sid] = publication |
| 65 | Track.Kind.VIDEO -> videoTracks[publication.sid] = publication | 65 | Track.Kind.VIDEO -> videoTracks[publication.sid] = publication |
| 66 | - else -> {} | 66 | + else -> { |
| 67 | } | 67 | } |
| 68 | } | 68 | } |
| 69 | + } | ||
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * Retrieves the first track that matches the source, or null | ||
| 73 | + */ | ||
| 74 | + open fun getTrackPublication(source: Track.Source): TrackPublication? { | ||
| 75 | + if (source == Track.Source.UNKNOWN) { | ||
| 76 | + return null | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + for ((_, pub) in tracks) { | ||
| 80 | + if (pub.source == source) { | ||
| 81 | + return pub | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + // Alternative heuristics for finding track if source is unknown | ||
| 85 | + if (pub.source == Track.Source.UNKNOWN) { | ||
| 86 | + if (source == Track.Source.MICROPHONE && pub.kind == Track.Kind.AUDIO) { | ||
| 87 | + return pub | ||
| 88 | + } | ||
| 89 | + if (source == Track.Source.CAMERA && pub.kind == Track.Kind.VIDEO && pub.name != "screen") { | ||
| 90 | + return pub | ||
| 91 | + } | ||
| 92 | + if (source == Track.Source.SCREEN_SHARE && pub.kind == Track.Kind.VIDEO && pub.name == "screen") { | ||
| 93 | + return pub | ||
| 94 | + } | ||
| 95 | + } | ||
| 96 | + } | ||
| 97 | + return null | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * Retrieves the first track that matches [name], or null | ||
| 102 | + */ | ||
| 103 | + open fun getTrackPublicationByName(name: String): TrackPublication? { | ||
| 104 | + for ((_, pub) in tracks) { | ||
| 105 | + if (pub.name == name) { | ||
| 106 | + return pub | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + return null | ||
| 110 | + } | ||
| 69 | 111 | ||
| 70 | /** | 112 | /** |
| 71 | * @suppress | 113 | * @suppress |
| @@ -33,7 +33,7 @@ class LocalAudioTrack( | @@ -33,7 +33,7 @@ class LocalAudioTrack( | ||
| 33 | internal fun createTrack( | 33 | internal fun createTrack( |
| 34 | context: Context, | 34 | context: Context, |
| 35 | factory: PeerConnectionFactory, | 35 | factory: PeerConnectionFactory, |
| 36 | - audioConstraints: MediaConstraints = MediaConstraints(), | 36 | + options: LocalAudioTrackOptions = LocalAudioTrackOptions(), |
| 37 | name: String = "" | 37 | name: String = "" |
| 38 | ): LocalAudioTrack { | 38 | ): LocalAudioTrack { |
| 39 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != | 39 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) != |
| @@ -42,6 +42,16 @@ class LocalAudioTrack( | @@ -42,6 +42,16 @@ class LocalAudioTrack( | ||
| 42 | throw SecurityException("Record audio permissions are required to create an audio track.") | 42 | throw SecurityException("Record audio permissions are required to create an audio track.") |
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | + val audioConstraints = MediaConstraints() | ||
| 46 | + val items = listOf( | ||
| 47 | + MediaConstraints.KeyValuePair("googEchoCancellation", options.echoCancellation.toString()), | ||
| 48 | + MediaConstraints.KeyValuePair("googAutoGainControl", options.autoGainControl.toString()), | ||
| 49 | + MediaConstraints.KeyValuePair("googHighpassFilter", options.highPassFilter.toString()), | ||
| 50 | + MediaConstraints.KeyValuePair("googNoiseSuppression", options.noiseSuppression.toString()), | ||
| 51 | + MediaConstraints.KeyValuePair("googTypingNoiseDetection", options.typingNoiseDetection.toString()), | ||
| 52 | + ) | ||
| 53 | + audioConstraints.optional.addAll(items) | ||
| 54 | + | ||
| 45 | val audioSource = factory.createAudioSource(audioConstraints) | 55 | val audioSource = factory.createAudioSource(audioConstraints) |
| 46 | val rtcAudioTrack = | 56 | val rtcAudioTrack = |
| 47 | factory.createAudioTrack(UUID.randomUUID().toString(), audioSource) | 57 | factory.createAudioTrack(UUID.randomUUID().toString(), audioSource) |
| 1 | package io.livekit.android.room.track | 1 | package io.livekit.android.room.track |
| 2 | 2 | ||
| 3 | -class LocalAudioTrackOptions( | ||
| 4 | - var noiseSuppression: Boolean = true, | ||
| 5 | - var echoCancellation: Boolean = true, | ||
| 6 | - var autoGainControl: Boolean = true, | ||
| 7 | - var highPassFilter: Boolean = true, | ||
| 8 | - var typingNoiseDetection: Boolean = true, | 3 | +data class LocalAudioTrackOptions( |
| 4 | + val noiseSuppression: Boolean = true, | ||
| 5 | + val echoCancellation: Boolean = true, | ||
| 6 | + val autoGainControl: Boolean = true, | ||
| 7 | + val highPassFilter: Boolean = true, | ||
| 8 | + val typingNoiseDetection: Boolean = true, | ||
| 9 | ) | 9 | ) |
| @@ -7,6 +7,7 @@ import android.media.projection.MediaProjection | @@ -7,6 +7,7 @@ import android.media.projection.MediaProjection | ||
| 7 | import dagger.assisted.Assisted | 7 | import dagger.assisted.Assisted |
| 8 | import dagger.assisted.AssistedFactory | 8 | import dagger.assisted.AssistedFactory |
| 9 | import dagger.assisted.AssistedInject | 9 | import dagger.assisted.AssistedInject |
| 10 | +import io.livekit.android.room.DefaultsManager | ||
| 10 | import io.livekit.android.room.track.screencapture.ScreenCaptureConnection | 11 | import io.livekit.android.room.track.screencapture.ScreenCaptureConnection |
| 11 | import org.webrtc.* | 12 | import org.webrtc.* |
| 12 | import java.util.* | 13 | import java.util.* |
| @@ -23,6 +24,8 @@ constructor( | @@ -23,6 +24,8 @@ constructor( | ||
| 23 | peerConnectionFactory: PeerConnectionFactory, | 24 | peerConnectionFactory: PeerConnectionFactory, |
| 24 | context: Context, | 25 | context: Context, |
| 25 | eglBase: EglBase, | 26 | eglBase: EglBase, |
| 27 | + defaultsManager: DefaultsManager, | ||
| 28 | + videoTrackFactory: LocalVideoTrack.Factory, | ||
| 26 | ) : LocalVideoTrack( | 29 | ) : LocalVideoTrack( |
| 27 | capturer, | 30 | capturer, |
| 28 | source, | 31 | source, |
| @@ -31,7 +34,9 @@ constructor( | @@ -31,7 +34,9 @@ constructor( | ||
| 31 | rtcTrack, | 34 | rtcTrack, |
| 32 | peerConnectionFactory, | 35 | peerConnectionFactory, |
| 33 | context, | 36 | context, |
| 34 | - eglBase | 37 | + eglBase, |
| 38 | + defaultsManager, | ||
| 39 | + videoTrackFactory | ||
| 35 | ) { | 40 | ) { |
| 36 | 41 | ||
| 37 | private val serviceConnection = ScreenCaptureConnection(context) | 42 | private val serviceConnection = ScreenCaptureConnection(context) |
| @@ -13,7 +13,9 @@ class LocalTrackPublication( | @@ -13,7 +13,9 @@ class LocalTrackPublication( | ||
| 13 | * Mute or unmute the current track. Muting the track would stop audio or video from being | 13 | * Mute or unmute the current track. Muting the track would stop audio or video from being |
| 14 | * transmitted to the server, and notify other participants in the room. | 14 | * transmitted to the server, and notify other participants in the room. |
| 15 | */ | 15 | */ |
| 16 | - fun setMuted(muted: Boolean) { | 16 | + override var muted: Boolean |
| 17 | + get() = super.muted | ||
| 18 | + set(muted) { | ||
| 17 | if (muted == this.muted) { | 19 | if (muted == this.muted) { |
| 18 | return | 20 | return |
| 19 | } | 21 | } |
| @@ -21,7 +23,7 @@ class LocalTrackPublication( | @@ -21,7 +23,7 @@ class LocalTrackPublication( | ||
| 21 | val mediaTrack = track ?: return | 23 | val mediaTrack = track ?: return |
| 22 | 24 | ||
| 23 | mediaTrack.rtcTrack.setEnabled(!muted) | 25 | mediaTrack.rtcTrack.setEnabled(!muted) |
| 24 | - this.muted = muted | 26 | + super.muted = muted |
| 25 | 27 | ||
| 26 | // send updates to server | 28 | // send updates to server |
| 27 | val participant = this.participant.get() as? LocalParticipant ?: return | 29 | val participant = this.participant.get() as? LocalParticipant ?: return |
| @@ -5,6 +5,10 @@ import android.content.Context | @@ -5,6 +5,10 @@ import android.content.Context | ||
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | import android.hardware.camera2.CameraManager | 6 | import android.hardware.camera2.CameraManager |
| 7 | import androidx.core.content.ContextCompat | 7 | import androidx.core.content.ContextCompat |
| 8 | +import dagger.assisted.Assisted | ||
| 9 | +import dagger.assisted.AssistedFactory | ||
| 10 | +import dagger.assisted.AssistedInject | ||
| 11 | +import io.livekit.android.room.DefaultsManager | ||
| 8 | import io.livekit.android.room.track.video.Camera1CapturerWithSize | 12 | import io.livekit.android.room.track.video.Camera1CapturerWithSize |
| 9 | import io.livekit.android.room.track.video.Camera2CapturerWithSize | 13 | import io.livekit.android.room.track.video.Camera2CapturerWithSize |
| 10 | import io.livekit.android.room.track.video.VideoCapturerWithSize | 14 | import io.livekit.android.room.track.video.VideoCapturerWithSize |
| @@ -18,15 +22,19 @@ import java.util.* | @@ -18,15 +22,19 @@ import java.util.* | ||
| 18 | * | 22 | * |
| 19 | * [startCapture] should be called before use. | 23 | * [startCapture] should be called before use. |
| 20 | */ | 24 | */ |
| 21 | -open class LocalVideoTrack( | ||
| 22 | - private var capturer: VideoCapturer, | ||
| 23 | - private var source: VideoSource, | ||
| 24 | - name: String, | ||
| 25 | - var options: LocalVideoTrackOptions, | ||
| 26 | - rtcTrack: org.webrtc.VideoTrack, | 25 | +open class LocalVideoTrack |
| 26 | +@AssistedInject | ||
| 27 | +constructor( | ||
| 28 | + @Assisted private var capturer: VideoCapturer, | ||
| 29 | + @Assisted private var source: VideoSource, | ||
| 30 | + @Assisted name: String, | ||
| 31 | + @Assisted var options: LocalVideoTrackOptions, | ||
| 32 | + @Assisted rtcTrack: org.webrtc.VideoTrack, | ||
| 27 | private val peerConnectionFactory: PeerConnectionFactory, | 33 | private val peerConnectionFactory: PeerConnectionFactory, |
| 28 | private val context: Context, | 34 | private val context: Context, |
| 29 | private val eglBase: EglBase, | 35 | private val eglBase: EglBase, |
| 36 | + private val defaultsManager: DefaultsManager, | ||
| 37 | + private val trackFactory: Factory, | ||
| 30 | ) : VideoTrack(name, rtcTrack) { | 38 | ) : VideoTrack(name, rtcTrack) { |
| 31 | 39 | ||
| 32 | override var rtcTrack: org.webrtc.VideoTrack = rtcTrack | 40 | override var rtcTrack: org.webrtc.VideoTrack = rtcTrack |
| @@ -61,13 +69,18 @@ open class LocalVideoTrack( | @@ -61,13 +69,18 @@ open class LocalVideoTrack( | ||
| 61 | super.stop() | 69 | super.stop() |
| 62 | } | 70 | } |
| 63 | 71 | ||
| 64 | - fun restartTrack(options: LocalVideoTrackOptions = LocalVideoTrackOptions()) { | 72 | + fun setDeviceId(deviceId: String) { |
| 73 | + restartTrack(options.copy(deviceId = deviceId)) | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) { | ||
| 65 | val newTrack = createTrack( | 77 | val newTrack = createTrack( |
| 66 | peerConnectionFactory, | 78 | peerConnectionFactory, |
| 67 | context, | 79 | context, |
| 68 | name, | 80 | name, |
| 69 | options, | 81 | options, |
| 70 | - eglBase | 82 | + eglBase, |
| 83 | + trackFactory | ||
| 71 | ) | 84 | ) |
| 72 | 85 | ||
| 73 | val oldCapturer = capturer | 86 | val oldCapturer = capturer |
| @@ -95,6 +108,17 @@ open class LocalVideoTrack( | @@ -95,6 +108,17 @@ open class LocalVideoTrack( | ||
| 95 | sender?.setTrack(newTrack.rtcTrack, true) | 108 | sender?.setTrack(newTrack.rtcTrack, true) |
| 96 | } | 109 | } |
| 97 | 110 | ||
| 111 | + @AssistedFactory | ||
| 112 | + interface Factory { | ||
| 113 | + fun create( | ||
| 114 | + capturer: VideoCapturer, | ||
| 115 | + source: VideoSource, | ||
| 116 | + name: String, | ||
| 117 | + options: LocalVideoTrackOptions, | ||
| 118 | + rtcTrack: org.webrtc.VideoTrack, | ||
| 119 | + ): LocalVideoTrack | ||
| 120 | + } | ||
| 121 | + | ||
| 98 | companion object { | 122 | companion object { |
| 99 | 123 | ||
| 100 | internal fun createTrack( | 124 | internal fun createTrack( |
| @@ -103,6 +127,7 @@ open class LocalVideoTrack( | @@ -103,6 +127,7 @@ open class LocalVideoTrack( | ||
| 103 | name: String, | 127 | name: String, |
| 104 | options: LocalVideoTrackOptions, | 128 | options: LocalVideoTrackOptions, |
| 105 | rootEglBase: EglBase, | 129 | rootEglBase: EglBase, |
| 130 | + trackFactory: Factory | ||
| 106 | ): LocalVideoTrack { | 131 | ): LocalVideoTrack { |
| 107 | 132 | ||
| 108 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != | 133 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != |
| @@ -112,7 +137,7 @@ open class LocalVideoTrack( | @@ -112,7 +137,7 @@ open class LocalVideoTrack( | ||
| 112 | } | 137 | } |
| 113 | 138 | ||
| 114 | val source = peerConnectionFactory.createVideoSource(options.isScreencast) | 139 | val source = peerConnectionFactory.createVideoSource(options.isScreencast) |
| 115 | - val capturer = createVideoCapturer(context, options.position) ?: TODO() | 140 | + val (capturer, newOptions) = createVideoCapturer(context, options) ?: TODO() |
| 116 | capturer.initialize( | 141 | capturer.initialize( |
| 117 | SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext), | 142 | SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext), |
| 118 | context, | 143 | context, |
| @@ -120,41 +145,44 @@ open class LocalVideoTrack( | @@ -120,41 +145,44 @@ open class LocalVideoTrack( | ||
| 120 | ) | 145 | ) |
| 121 | val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) | 146 | val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source) |
| 122 | 147 | ||
| 123 | - return LocalVideoTrack( | 148 | + return trackFactory.create( |
| 124 | capturer = capturer, | 149 | capturer = capturer, |
| 125 | source = source, | 150 | source = source, |
| 126 | - options = options, | 151 | + options = newOptions, |
| 127 | name = name, | 152 | name = name, |
| 128 | - rtcTrack = track, | ||
| 129 | - peerConnectionFactory = peerConnectionFactory, | ||
| 130 | - context = context, | ||
| 131 | - eglBase = rootEglBase, | 153 | + rtcTrack = track |
| 132 | ) | 154 | ) |
| 133 | } | 155 | } |
| 134 | 156 | ||
| 135 | - private fun createVideoCapturer(context: Context, position: CameraPosition): VideoCapturer? { | ||
| 136 | - val videoCapturer: VideoCapturer? = if (Camera2Enumerator.isSupported(context)) { | ||
| 137 | - createCameraCapturer(context, Camera2Enumerator(context), position) | 157 | + private fun createVideoCapturer( |
| 158 | + context: Context, | ||
| 159 | + options: LocalVideoTrackOptions | ||
| 160 | + ): Pair<VideoCapturer, LocalVideoTrackOptions>? { | ||
| 161 | + val pair = if (Camera2Enumerator.isSupported(context)) { | ||
| 162 | + createCameraCapturer(context, Camera2Enumerator(context), options) | ||
| 138 | } else { | 163 | } else { |
| 139 | - createCameraCapturer(context, Camera1Enumerator(true), position) | 164 | + createCameraCapturer(context, Camera1Enumerator(true), options) |
| 140 | } | 165 | } |
| 141 | - if (videoCapturer == null) { | 166 | + |
| 167 | + if (pair == null) { | ||
| 142 | LKLog.d { "Failed to open camera" } | 168 | LKLog.d { "Failed to open camera" } |
| 143 | return null | 169 | return null |
| 144 | } | 170 | } |
| 145 | - return videoCapturer | 171 | + return pair |
| 146 | } | 172 | } |
| 147 | 173 | ||
| 148 | private fun createCameraCapturer( | 174 | private fun createCameraCapturer( |
| 149 | context: Context, | 175 | context: Context, |
| 150 | enumerator: CameraEnumerator, | 176 | enumerator: CameraEnumerator, |
| 151 | - position: CameraPosition | ||
| 152 | - ): VideoCapturer? { | 177 | + options: LocalVideoTrackOptions |
| 178 | + ): Pair<VideoCapturerWithSize, LocalVideoTrackOptions>? { | ||
| 153 | val deviceNames = enumerator.deviceNames | 179 | val deviceNames = enumerator.deviceNames |
| 154 | var targetDeviceName: String? = null | 180 | var targetDeviceName: String? = null |
| 155 | var targetVideoCapturer: VideoCapturer? = null | 181 | var targetVideoCapturer: VideoCapturer? = null |
| 156 | for (deviceName in deviceNames) { | 182 | for (deviceName in deviceNames) { |
| 157 | - if (enumerator.isFrontFacing(deviceName) && position == CameraPosition.FRONT) { | 183 | + if ((options.deviceId != null && deviceName == options.deviceId) |
| 184 | + || (enumerator.isFrontFacing(deviceName) && options.position == CameraPosition.FRONT) | ||
| 185 | + ) { | ||
| 158 | LKLog.v { "Creating front facing camera capturer." } | 186 | LKLog.v { "Creating front facing camera capturer." } |
| 159 | val videoCapturer = enumerator.createCapturer(deviceName, null) | 187 | val videoCapturer = enumerator.createCapturer(deviceName, null) |
| 160 | if (videoCapturer != null) { | 188 | if (videoCapturer != null) { |
| @@ -162,7 +190,9 @@ open class LocalVideoTrack( | @@ -162,7 +190,9 @@ open class LocalVideoTrack( | ||
| 162 | targetVideoCapturer = videoCapturer | 190 | targetVideoCapturer = videoCapturer |
| 163 | break | 191 | break |
| 164 | } | 192 | } |
| 165 | - } else if (enumerator.isBackFacing(deviceName) && position == CameraPosition.BACK) { | 193 | + } else if ((options.deviceId != null && deviceName == options.deviceId) |
| 194 | + || (enumerator.isBackFacing(deviceName) && options.position == CameraPosition.BACK) | ||
| 195 | + ) { | ||
| 166 | LKLog.v { "Creating back facing camera capturer." } | 196 | LKLog.v { "Creating back facing camera capturer." } |
| 167 | val videoCapturer = enumerator.createCapturer(deviceName, null) | 197 | val videoCapturer = enumerator.createCapturer(deviceName, null) |
| 168 | if (videoCapturer != null) { | 198 | if (videoCapturer != null) { |
| @@ -173,19 +203,40 @@ open class LocalVideoTrack( | @@ -173,19 +203,40 @@ open class LocalVideoTrack( | ||
| 173 | } | 203 | } |
| 174 | } | 204 | } |
| 175 | 205 | ||
| 206 | + // back fill any missing information | ||
| 207 | + val newOptions = options.copy( | ||
| 208 | + deviceId = targetDeviceName, | ||
| 209 | + position = enumerator.getCameraPosition(targetDeviceName!!) | ||
| 210 | + ) | ||
| 176 | if (targetVideoCapturer is Camera1Capturer) { | 211 | if (targetVideoCapturer is Camera1Capturer) { |
| 177 | - return Camera1CapturerWithSize(targetVideoCapturer, targetDeviceName) | 212 | + return Pair( |
| 213 | + Camera1CapturerWithSize(targetVideoCapturer, targetDeviceName), | ||
| 214 | + newOptions | ||
| 215 | + ) | ||
| 178 | } | 216 | } |
| 179 | 217 | ||
| 180 | if (targetVideoCapturer is Camera2Capturer) { | 218 | if (targetVideoCapturer is Camera2Capturer) { |
| 181 | - return Camera2CapturerWithSize( | 219 | + return Pair( |
| 220 | + Camera2CapturerWithSize( | ||
| 182 | targetVideoCapturer, | 221 | targetVideoCapturer, |
| 183 | context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, | 222 | context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, |
| 184 | targetDeviceName | 223 | targetDeviceName |
| 224 | + ), | ||
| 225 | + newOptions | ||
| 185 | ) | 226 | ) |
| 186 | } | 227 | } |
| 187 | 228 | ||
| 188 | return null | 229 | return null |
| 189 | } | 230 | } |
| 231 | + | ||
| 232 | + fun CameraEnumerator.getCameraPosition(deviceName: String): CameraPosition? { | ||
| 233 | + if (isBackFacing(deviceName)) { | ||
| 234 | + return CameraPosition.BACK | ||
| 235 | + } else if (isFrontFacing(deviceName)) { | ||
| 236 | + return CameraPosition.FRONT | ||
| 237 | + } | ||
| 238 | + return null | ||
| 190 | } | 239 | } |
| 240 | + } | ||
| 241 | + | ||
| 191 | } | 242 | } |
| @@ -2,10 +2,15 @@ package io.livekit.android.room.track | @@ -2,10 +2,15 @@ package io.livekit.android.room.track | ||
| 2 | 2 | ||
| 3 | import org.webrtc.RtpParameters | 3 | import org.webrtc.RtpParameters |
| 4 | 4 | ||
| 5 | -class LocalVideoTrackOptions( | ||
| 6 | - var isScreencast: Boolean = false, | ||
| 7 | - var position: CameraPosition = CameraPosition.FRONT, | ||
| 8 | - var captureParams: VideoCaptureParameter = VideoPreset169.QHD.capture | 5 | +data class LocalVideoTrackOptions( |
| 6 | + val isScreencast: Boolean = false, | ||
| 7 | + /** | ||
| 8 | + * Preferred deviceId to capture from. If not set or found, | ||
| 9 | + * will prefer a camera according to [position] | ||
| 10 | + */ | ||
| 11 | + val deviceId: String? = null, | ||
| 12 | + val position: CameraPosition? = CameraPosition.FRONT, | ||
| 13 | + val captureParams: VideoCaptureParameter = VideoPreset169.QHD.capture | ||
| 9 | ) | 14 | ) |
| 10 | 15 | ||
| 11 | data class VideoCaptureParameter( | 16 | data class VideoCaptureParameter( |
| @@ -44,6 +44,34 @@ open class Track( | @@ -44,6 +44,34 @@ open class Track( | ||
| 44 | } | 44 | } |
| 45 | } | 45 | } |
| 46 | 46 | ||
| 47 | + enum class Source { | ||
| 48 | + CAMERA, | ||
| 49 | + MICROPHONE, | ||
| 50 | + SCREEN_SHARE, | ||
| 51 | + UNKNOWN; | ||
| 52 | + | ||
| 53 | + | ||
| 54 | + fun toProto(): LivekitModels.TrackSource { | ||
| 55 | + return when (this) { | ||
| 56 | + CAMERA -> LivekitModels.TrackSource.CAMERA | ||
| 57 | + MICROPHONE -> LivekitModels.TrackSource.MICROPHONE | ||
| 58 | + SCREEN_SHARE -> LivekitModels.TrackSource.SCREEN_SHARE | ||
| 59 | + UNKNOWN -> LivekitModels.TrackSource.UNKNOWN | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + companion object { | ||
| 64 | + fun fromProto(source: LivekitModels.TrackSource): Source { | ||
| 65 | + return when (source) { | ||
| 66 | + LivekitModels.TrackSource.CAMERA -> CAMERA | ||
| 67 | + LivekitModels.TrackSource.MICROPHONE -> MICROPHONE | ||
| 68 | + LivekitModels.TrackSource.SCREEN_SHARE -> SCREEN_SHARE | ||
| 69 | + else -> UNKNOWN | ||
| 70 | + } | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + | ||
| 47 | data class Dimensions(var width: Int, var height: Int) | 75 | data class Dimensions(var width: Int, var height: Int) |
| 48 | 76 | ||
| 49 | open fun start() { | 77 | open fun start() { |
| @@ -27,7 +27,8 @@ open class TrackPublication( | @@ -27,7 +27,8 @@ open class TrackPublication( | ||
| 27 | internal set | 27 | internal set |
| 28 | var dimensions: Track.Dimensions? = null | 28 | var dimensions: Track.Dimensions? = null |
| 29 | internal set | 29 | internal set |
| 30 | - | 30 | + var source: Track.Source = Track.Source.UNKNOWN |
| 31 | + internal set | ||
| 31 | 32 | ||
| 32 | var participant: WeakReference<Participant> | 33 | var participant: WeakReference<Participant> |
| 33 | 34 | ||
| @@ -44,6 +45,7 @@ open class TrackPublication( | @@ -44,6 +45,7 @@ open class TrackPublication( | ||
| 44 | name = info.name | 45 | name = info.name |
| 45 | kind = Track.Kind.fromProto(info.type) | 46 | kind = Track.Kind.fromProto(info.type) |
| 46 | muted = info.muted | 47 | muted = info.muted |
| 48 | + source = Track.Source.fromProto(info.source) | ||
| 47 | if (kind == Track.Kind.VIDEO) { | 49 | if (kind == Track.Kind.VIDEO) { |
| 48 | simulcasted = info.simulcast | 50 | simulcasted = info.simulcast |
| 49 | dimensions = Track.Dimensions(info.width, info.height) | 51 | dimensions = Track.Dimensions(info.width, info.height) |
| @@ -2,6 +2,8 @@ package org.webrtc | @@ -2,6 +2,8 @@ package org.webrtc | ||
| 2 | 2 | ||
| 3 | /** | 3 | /** |
| 4 | * A helper to access package-protected methods used in [Camera2Session] | 4 | * A helper to access package-protected methods used in [Camera2Session] |
| 5 | + * | ||
| 6 | + * Note: cameraId as used in the Camera1XXX classes refers to the index within the list of cameras. | ||
| 5 | * @suppress | 7 | * @suppress |
| 6 | */ | 8 | */ |
| 7 | internal class Camera1Helper { | 9 | internal class Camera1Helper { |
| @@ -4,6 +4,9 @@ import android.hardware.camera2.CameraManager | @@ -4,6 +4,9 @@ import android.hardware.camera2.CameraManager | ||
| 4 | 4 | ||
| 5 | /** | 5 | /** |
| 6 | * A helper to access package-protected methods used in [Camera2Session] | 6 | * A helper to access package-protected methods used in [Camera2Session] |
| 7 | + * | ||
| 8 | + * Note: cameraId as used in the Camera2XXX classes refers to the id returned | ||
| 9 | + * by [CameraManager.getCameraIdList]. | ||
| 7 | * @suppress | 10 | * @suppress |
| 8 | */ | 11 | */ |
| 9 | internal class Camera2Helper { | 12 | internal class Camera2Helper { |
-
请 注册 或 登录 后发表评论