davidliu
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 个修改的文件 包含 551 行增加129 行删除
package io.livekit.android
import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.VideoTrackPublishDefaults
import io.livekit.android.room.track.LocalAudioTrackOptions
import io.livekit.android.room.track.LocalVideoTrackOptions
import org.webrtc.PeerConnection
class ConnectOptions(
var autoSubscribe: Boolean = true
data class ConnectOptions(
val autoSubscribe: Boolean = true,
val iceServers: List<PeerConnection.IceServer>? = null,
val rtcConfig: PeerConnection.RTCConfiguration? = null,
/**
* capture and publish audio track on connect, defaults to false
*/
val audio: Boolean = false,
/**
* capture and publish video track on connect, defaults to false
*/
val video: Boolean = false,
val audioTrackCaptureDefaults: LocalAudioTrackOptions? = null,
val videoTrackCaptureDefaults: LocalVideoTrackOptions? = null,
val audioTrackPublishDefaults: AudioTrackPublishDefaults? = null,
val videoTrackPublishDefaults: VideoTrackPublishDefaults? = null,
) {
internal var reconnect: Boolean = false
}
... ...
... ... @@ -48,6 +48,28 @@ class LiveKit {
room.listener = listener
room.connect(url, token, options)
options?.audioTrackCaptureDefaults?.let {
room.localParticipant.audioTrackCaptureDefaults = it
}
options?.videoTrackCaptureDefaults?.let {
room.localParticipant.videoTrackCaptureDefaults = it
}
options?.audioTrackPublishDefaults?.let {
room.localParticipant.audioTrackPublishDefaults = it
}
options?.videoTrackPublishDefaults?.let {
room.localParticipant.videoTrackPublishDefaults = it
}
if (options?.audio == true) {
val audioTrack = room.localParticipant.createAudioTrack()
room.localParticipant.publishAudioTrack(audioTrack)
}
if (options?.video == true) {
val videoTrack = room.localParticipant.createVideoTrack()
room.localParticipant.publishVideoTrack(videoTrack)
}
return room
}
... ...
package io.livekit.android.room
import io.livekit.android.room.participant.AudioTrackPublishDefaults
import io.livekit.android.room.participant.VideoTrackPublishDefaults
import io.livekit.android.room.track.LocalAudioTrackOptions
import io.livekit.android.room.track.LocalVideoTrackOptions
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultsManager
@Inject
constructor() {
var audioTrackCaptureDefaults: LocalAudioTrackOptions = LocalAudioTrackOptions()
var audioTrackPublishDefaults: AudioTrackPublishDefaults = AudioTrackPublishDefaults()
var videoTrackCaptureDefaults: LocalVideoTrackOptions = LocalVideoTrackOptions()
var videoTrackPublishDefaults: VideoTrackPublishDefaults = VideoTrackPublishDefaults()
}
\ No newline at end of file
... ...
package io.livekit.android.room
import android.content.Context
import android.hardware.camera2.CameraManager
import android.os.Handler
import android.os.Looper
import org.webrtc.Camera1Enumerator
import org.webrtc.Camera2Enumerator
object DeviceManager {
enum class Kind {
// Only camera input currently, audio input/output only has one option atm.
CAMERA;
}
private val defaultDevices = mutableMapOf<Kind, String>()
private val listeners =
mutableMapOf<Kind, MutableList<OnDeviceAvailabilityChangeListener>>()
private var hasSetupListeners = false
@Synchronized
internal fun setupListenersIfNeeded(context: Context) {
if (hasSetupListeners) {
return
}
hasSetupListeners = true
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
cameraManager.registerAvailabilityCallback(object : CameraManager.AvailabilityCallback() {
override fun onCameraAvailable(cameraId: String) {
notifyListeners(Kind.CAMERA)
}
override fun onCameraUnavailable(cameraId: String) {
notifyListeners(Kind.CAMERA)
}
override fun onCameraAccessPrioritiesChanged() {
notifyListeners(Kind.CAMERA)
}
}, Handler(Looper.getMainLooper()))
}
fun getDefaultDevice(kind: Kind): String? {
return defaultDevices[kind]
}
fun setDefaultDevice(kind: Kind, deviceId: String?) {
if (deviceId != null) {
defaultDevices[kind] = deviceId
} else {
defaultDevices.remove(kind)
}
}
/**
* @return the list of device ids for [kind]
*/
fun getDevices(context: Context, kind: Kind): List<String> {
return when (kind) {
Kind.CAMERA -> {
val cameraEnumerator = if (Camera2Enumerator.isSupported(context)) {
Camera2Enumerator(context)
} else {
Camera1Enumerator()
}
cameraEnumerator.deviceNames.toList()
}
}
}
fun registerOnDeviceAvailabilityChange(
kind: Kind,
listener: OnDeviceAvailabilityChangeListener
) {
if (listeners[kind] == null) {
listeners[kind] = mutableListOf()
}
listeners[kind]!!.add(listener)
}
fun unregisterOnDeviceAvailabilityChange(
kind: Kind,
listener: OnDeviceAvailabilityChangeListener
) {
listeners[kind]?.remove(listener)
}
private fun notifyListeners(kind: Kind) {
listeners[kind]?.forEach {
it.onDeviceAvailabilityChanged(kind)
}
}
interface OnDeviceAvailabilityChangeListener {
fun onDeviceAvailabilityChanged(kind: Kind)
}
}
\ No newline at end of file
... ...
... ... @@ -3,10 +3,7 @@ package io.livekit.android.room
import android.os.SystemClock
import io.livekit.android.ConnectOptions
import io.livekit.android.dagger.InjectionNames
import io.livekit.android.room.track.DataPublishReliability
import io.livekit.android.room.track.Track
import io.livekit.android.room.track.TrackException
import io.livekit.android.room.track.TrackPublication
import io.livekit.android.room.util.*
import io.livekit.android.util.CloseableCoroutineScope
import io.livekit.android.util.Either
... ... @@ -21,7 +18,6 @@ import livekit.LivekitRtc
import org.webrtc.*
import java.net.ConnectException
import java.nio.ByteBuffer
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
... ... @@ -101,7 +97,7 @@ internal constructor(
isSubscriberPrimary = joinResponse.subscriberPrimary
if (!this::publisher.isInitialized) {
configure(joinResponse)
configure(joinResponse, options)
}
// create offer
if (!this.isSubscriberPrimary) {
... ... @@ -111,44 +107,51 @@ internal constructor(
return joinResponse
}
private fun configure(joinResponse: LivekitRtc.JoinResponse) {
private fun configure(joinResponse: LivekitRtc.JoinResponse, connectOptions: ConnectOptions?) {
if (this::publisher.isInitialized || this::subscriber.isInitialized) {
// already configured
return
}
// update ICE servers before creating PeerConnection
val iceServers = mutableListOf<PeerConnection.IceServer>()
for (serverInfo in joinResponse.iceServersList) {
val username = serverInfo.username ?: ""
val credential = serverInfo.credential ?: ""
iceServers.add(
PeerConnection.IceServer
.builder(serverInfo.urlsList)
.setUsername(username)
.setPassword(credential)
.createIceServer()
)
}
val iceServers = if (connectOptions?.iceServers != null) {
connectOptions.iceServers
} else {
val servers = mutableListOf<PeerConnection.IceServer>()
for (serverInfo in joinResponse.iceServersList) {
val username = serverInfo.username ?: ""
val credential = serverInfo.credential ?: ""
servers.add(
PeerConnection.IceServer
.builder(serverInfo.urlsList)
.setUsername(username)
.setPassword(credential)
.createIceServer()
)
}
if (iceServers.isEmpty()) {
iceServers.addAll(SignalClient.DEFAULT_ICE_SERVERS)
}
joinResponse.iceServersList.forEach {
LKLog.v { "username = \"${it.username}\"" }
LKLog.v { "credential = \"${it.credential}\"" }
LKLog.v { "urls: " }
it.urlsList.forEach {
LKLog.v { " $it" }
if (servers.isEmpty()) {
servers.addAll(SignalClient.DEFAULT_ICE_SERVERS)
}
servers
}
// Setup peer connections
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
enableDtlsSrtp = true
val rtcConfig = connectOptions?.rtcConfig?.apply {
val mergedServers = this.iceServers.toMutableList()
iceServers.forEach { server ->
if (!mergedServers.contains(server)) {
mergedServers.add(server)
}
}
}
?: PeerConnection.RTCConfiguration(iceServers).apply {
sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN
continualGatheringPolicy =
PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY
enableDtlsSrtp = true
}
publisher = pctFactory.create(
rtcConfig,
... ...
... ... @@ -24,7 +24,8 @@ constructor(
@Assisted private val context: Context,
private val engine: RTCEngine,
private val eglBase: EglBase,
private val localParticipantFactory: LocalParticipant.Factory
private val localParticipantFactory: LocalParticipant.Factory,
private val defaultsManager: DefaultsManager,
) : RTCEngine.Listener, ParticipantListener, ConnectivityManager.NetworkCallback() {
init {
engine.listener = this
... ... @@ -51,6 +52,11 @@ constructor(
var metadata: String? = null
private set
var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults
var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults
var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults
var videoTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::videoTrackPublishDefaults
lateinit var localParticipant: LocalParticipant
private set
private val mutableRemoteParticipants = mutableMapOf<String, RemoteParticipant>()
... ... @@ -583,6 +589,16 @@ interface RoomListener {
* @param quality the new connection quality
*/
fun onConnectionQualityChanged(participant: Participant, quality: ConnectionQuality) {}
companion object {
fun getDefaultDevice(kind: DeviceManager.Kind): String? {
return DeviceManager.getDefaultDevice(kind)
}
fun setDefaultDevice(kind: DeviceManager.Kind, deviceId: String?) {
DeviceManager.setDefaultDevice(kind, deviceId)
}
}
}
sealed class RoomException(message: String? = null, cause: Throwable? = null) :
... ...
... ... @@ -7,12 +7,16 @@ import com.google.protobuf.ByteString
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.room.DefaultsManager
import io.livekit.android.room.RTCEngine
import io.livekit.android.room.track.*
import io.livekit.android.util.LKLog
import livekit.LivekitModels
import livekit.LivekitRtc
import org.webrtc.*
import org.webrtc.EglBase
import org.webrtc.PeerConnectionFactory
import org.webrtc.RtpParameters
import org.webrtc.RtpTransceiver
import kotlin.math.abs
import kotlin.math.roundToInt
... ... @@ -26,8 +30,14 @@ internal constructor(
private val context: Context,
private val eglBase: EglBase,
private val screencastVideoTrackFactory: LocalScreencastVideoTrack.Factory,
) :
Participant(info.sid, info.identity) {
private val videoTrackFactory: LocalVideoTrack.Factory,
private val defaultsManager: DefaultsManager
) : Participant(info.sid, info.identity) {
var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults
var audioTrackPublishDefaults: AudioTrackPublishDefaults by defaultsManager::audioTrackPublishDefaults
var videoTrackCaptureDefaults: LocalVideoTrackOptions by defaultsManager::videoTrackCaptureDefaults
var videoTrackPublishDefaults: VideoTrackPublishDefaults by defaultsManager::videoTrackPublishDefaults
init {
updateFromInfo(info)
... ... @@ -45,18 +55,9 @@ internal constructor(
*/
fun createAudioTrack(
name: String = "",
options: LocalAudioTrackOptions = LocalAudioTrackOptions(),
options: LocalAudioTrackOptions = audioTrackCaptureDefaults,
): LocalAudioTrack {
val audioConstraints = MediaConstraints()
val items = listOf(
MediaConstraints.KeyValuePair("googEchoCancellation", options.echoCancellation.toString()),
MediaConstraints.KeyValuePair("googAutoGainControl", options.autoGainControl.toString()),
MediaConstraints.KeyValuePair("googHighpassFilter", options.highPassFilter.toString()),
MediaConstraints.KeyValuePair("googNoiseSuppression", options.noiseSuppression.toString()),
MediaConstraints.KeyValuePair("googTypingNoiseDetection", options.typingNoiseDetection.toString()),
)
audioConstraints.optional.addAll(items)
return LocalAudioTrack.createTrack(context, peerConnectionFactory, audioConstraints, name)
return LocalAudioTrack.createTrack(context, peerConnectionFactory, options, name)
}
/**
... ... @@ -66,14 +67,15 @@ internal constructor(
*/
fun createVideoTrack(
name: String = "",
options: LocalVideoTrackOptions = LocalVideoTrackOptions(),
options: LocalVideoTrackOptions = videoTrackCaptureDefaults.copy(),
): LocalVideoTrack {
return LocalVideoTrack.createTrack(
peerConnectionFactory,
context,
name,
options,
eglBase
eglBase,
videoTrackFactory,
)
}
... ... @@ -98,9 +100,62 @@ internal constructor(
)
}
override fun getTrackPublication(source: Track.Source): LocalTrackPublication? {
return super.getTrackPublication(source) as? LocalTrackPublication
}
override fun getTrackPublicationByName(name: String): LocalTrackPublication? {
return super.getTrackPublicationByName(name) as? LocalTrackPublication
}
private suspend fun setTrackEnabled(
source: Track.Source,
enabled: Boolean,
mediaProjectionPermissionResultData: Intent? = null
) {
val pub = getTrackPublication(source)
if (enabled) {
if (pub != null) {
pub.muted = false
} else {
when (source) {
Track.Source.CAMERA -> {
val track = createVideoTrack()
publishVideoTrack(track)
}
Track.Source.MICROPHONE -> {
val track = createAudioTrack()
publishAudioTrack(track)
}
Track.Source.SCREEN_SHARE -> {
if (mediaProjectionPermissionResultData == null) {
throw IllegalArgumentException("Media Projection permission result data is required to create a screen share track.")
}
val track =
createScreencastTrack(mediaProjectionPermissionResultData = mediaProjectionPermissionResultData)
publishVideoTrack(track)
}
}
}
} else {
pub?.track?.let { track ->
// screenshare cannot be muted, unpublish instead
if (pub.source == Track.Source.SCREEN_SHARE) {
unpublishTrack(track)
} else {
pub.muted = true
}
}
}
}
suspend fun publishAudioTrack(
track: LocalAudioTrack,
options: AudioTrackPublishOptions = AudioTrackPublishOptions(),
options: AudioTrackPublishOptions = AudioTrackPublishOptions(
null,
audioTrackPublishDefaults
),
publishListener: PublishListener? = null
) {
if (localTrackPublications.any { it.track == track }) {
... ... @@ -140,7 +195,7 @@ internal constructor(
suspend fun publishVideoTrack(
track: LocalVideoTrack,
options: VideoTrackPublishOptions = VideoTrackPublishOptions(),
options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults),
publishListener: PublishListener? = null
) {
if (localTrackPublications.any { it.track == track }) {
... ... @@ -339,7 +394,7 @@ internal constructor(
for (ti in info.tracksList) {
val publication = this.tracks[ti.sid] as? LocalTrackPublication ?: continue
if (ti.muted != publication.muted) {
publication.setMuted(ti.muted)
publication.muted = ti.muted
}
}
}
... ... @@ -382,15 +437,53 @@ interface TrackPublishOptions {
val name: String?
}
abstract class BaseVideoTrackPublishOptions {
abstract val videoEncoding: VideoEncoding?
abstract val simulcast: Boolean
//val videoCodec: VideoCodec? = null,
}
data class VideoTrackPublishDefaults(
override val videoEncoding: VideoEncoding? = null,
override val simulcast: Boolean = false
) : BaseVideoTrackPublishOptions()
data class VideoTrackPublishOptions(
override val name: String? = null,
val videoEncoding: VideoEncoding? = null,
//val videoCodec: VideoCodec? = null,
val simulcast: Boolean = false
) : TrackPublishOptions
override val videoEncoding: VideoEncoding? = null,
override val simulcast: Boolean = false
) : BaseVideoTrackPublishOptions(), TrackPublishOptions {
constructor(
name: String? = null,
base: BaseVideoTrackPublishOptions
) : this(
name,
base.videoEncoding,
base.simulcast
)
}
abstract class BaseAudioTrackPublishOptions {
abstract val audioBitrate: Int?
abstract val dtx: Boolean
}
data class AudioTrackPublishDefaults(
override val audioBitrate: Int? = null,
override val dtx: Boolean = true
) : BaseAudioTrackPublishOptions()
data class AudioTrackPublishOptions(
override val name: String? = null,
val audioBitrate: Int? = null,
val dtx: Boolean = true
) : TrackPublishOptions
\ No newline at end of file
override val audioBitrate: Int? = null,
override val dtx: Boolean = true
) : BaseAudioTrackPublishOptions(), TrackPublishOptions {
constructor(
name: String? = null,
base: BaseAudioTrackPublishOptions
) : this(
name,
base.audioBitrate,
base.dtx
)
}
\ No newline at end of file
... ...
... ... @@ -63,8 +63,50 @@ open class Participant(var sid: String, identity: String? = null) {
when (publication.kind) {
Track.Kind.AUDIO -> audioTracks[publication.sid] = publication
Track.Kind.VIDEO -> videoTracks[publication.sid] = publication
else -> {}
else -> {
}
}
}
/**
* Retrieves the first track that matches the source, or null
*/
open fun getTrackPublication(source: Track.Source): TrackPublication? {
if (source == Track.Source.UNKNOWN) {
return null
}
for ((_, pub) in tracks) {
if (pub.source == source) {
return pub
}
// Alternative heuristics for finding track if source is unknown
if (pub.source == Track.Source.UNKNOWN) {
if (source == Track.Source.MICROPHONE && pub.kind == Track.Kind.AUDIO) {
return pub
}
if (source == Track.Source.CAMERA && pub.kind == Track.Kind.VIDEO && pub.name != "screen") {
return pub
}
if (source == Track.Source.SCREEN_SHARE && pub.kind == Track.Kind.VIDEO && pub.name == "screen") {
return pub
}
}
}
return null
}
/**
* Retrieves the first track that matches [name], or null
*/
open fun getTrackPublicationByName(name: String): TrackPublication? {
for ((_, pub) in tracks) {
if (pub.name == name) {
return pub
}
}
return null
}
/**
... ...
... ... @@ -33,7 +33,7 @@ class LocalAudioTrack(
internal fun createTrack(
context: Context,
factory: PeerConnectionFactory,
audioConstraints: MediaConstraints = MediaConstraints(),
options: LocalAudioTrackOptions = LocalAudioTrackOptions(),
name: String = ""
): LocalAudioTrack {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) !=
... ... @@ -42,6 +42,16 @@ class LocalAudioTrack(
throw SecurityException("Record audio permissions are required to create an audio track.")
}
val audioConstraints = MediaConstraints()
val items = listOf(
MediaConstraints.KeyValuePair("googEchoCancellation", options.echoCancellation.toString()),
MediaConstraints.KeyValuePair("googAutoGainControl", options.autoGainControl.toString()),
MediaConstraints.KeyValuePair("googHighpassFilter", options.highPassFilter.toString()),
MediaConstraints.KeyValuePair("googNoiseSuppression", options.noiseSuppression.toString()),
MediaConstraints.KeyValuePair("googTypingNoiseDetection", options.typingNoiseDetection.toString()),
)
audioConstraints.optional.addAll(items)
val audioSource = factory.createAudioSource(audioConstraints)
val rtcAudioTrack =
factory.createAudioTrack(UUID.randomUUID().toString(), audioSource)
... ...
package io.livekit.android.room.track
class LocalAudioTrackOptions(
var noiseSuppression: Boolean = true,
var echoCancellation: Boolean = true,
var autoGainControl: Boolean = true,
var highPassFilter: Boolean = true,
var typingNoiseDetection: Boolean = true,
data class LocalAudioTrackOptions(
val noiseSuppression: Boolean = true,
val echoCancellation: Boolean = true,
val autoGainControl: Boolean = true,
val highPassFilter: Boolean = true,
val typingNoiseDetection: Boolean = true,
)
... ...
... ... @@ -7,6 +7,7 @@ import android.media.projection.MediaProjection
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.room.DefaultsManager
import io.livekit.android.room.track.screencapture.ScreenCaptureConnection
import org.webrtc.*
import java.util.*
... ... @@ -23,6 +24,8 @@ constructor(
peerConnectionFactory: PeerConnectionFactory,
context: Context,
eglBase: EglBase,
defaultsManager: DefaultsManager,
videoTrackFactory: LocalVideoTrack.Factory,
) : LocalVideoTrack(
capturer,
source,
... ... @@ -31,7 +34,9 @@ constructor(
rtcTrack,
peerConnectionFactory,
context,
eglBase
eglBase,
defaultsManager,
videoTrackFactory
) {
private val serviceConnection = ScreenCaptureConnection(context)
... ...
... ... @@ -13,27 +13,29 @@ class LocalTrackPublication(
* Mute or unmute the current track. Muting the track would stop audio or video from being
* transmitted to the server, and notify other participants in the room.
*/
fun setMuted(muted: Boolean) {
if (muted == this.muted) {
return
override var muted: Boolean
get() = super.muted
set(muted) {
if (muted == this.muted) {
return
}
val mediaTrack = track ?: return
mediaTrack.rtcTrack.setEnabled(!muted)
super.muted = muted
// send updates to server
val participant = this.participant.get() as? LocalParticipant ?: return
participant.engine.updateMuteStatus(sid, muted)
if (muted) {
participant.listener?.onTrackMuted(this, participant)
participant.internalListener?.onTrackMuted(this, participant)
} else {
participant.listener?.onTrackUnmuted(this, participant)
participant.internalListener?.onTrackUnmuted(this, participant)
}
}
val mediaTrack = track ?: return
mediaTrack.rtcTrack.setEnabled(!muted)
this.muted = muted
// send updates to server
val participant = this.participant.get() as? LocalParticipant ?: return
participant.engine.updateMuteStatus(sid, muted)
if (muted) {
participant.listener?.onTrackMuted(this, participant)
participant.internalListener?.onTrackMuted(this, participant)
} else {
participant.listener?.onTrackUnmuted(this, participant)
participant.internalListener?.onTrackUnmuted(this, participant)
}
}
}
... ...
package io.livekit.android.room.track
data class LocalTrackPublicationOptions(val placeholder: Unit)
enum class DataPublishReliability {
RELIABLE,
LOSSY,
... ...
... ... @@ -5,6 +5,10 @@ import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraManager
import androidx.core.content.ContextCompat
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.livekit.android.room.DefaultsManager
import io.livekit.android.room.track.video.Camera1CapturerWithSize
import io.livekit.android.room.track.video.Camera2CapturerWithSize
import io.livekit.android.room.track.video.VideoCapturerWithSize
... ... @@ -18,15 +22,19 @@ import java.util.*
*
* [startCapture] should be called before use.
*/
open class LocalVideoTrack(
private var capturer: VideoCapturer,
private var source: VideoSource,
name: String,
var options: LocalVideoTrackOptions,
rtcTrack: org.webrtc.VideoTrack,
open class LocalVideoTrack
@AssistedInject
constructor(
@Assisted private var capturer: VideoCapturer,
@Assisted private var source: VideoSource,
@Assisted name: String,
@Assisted var options: LocalVideoTrackOptions,
@Assisted rtcTrack: org.webrtc.VideoTrack,
private val peerConnectionFactory: PeerConnectionFactory,
private val context: Context,
private val eglBase: EglBase,
private val defaultsManager: DefaultsManager,
private val trackFactory: Factory,
) : VideoTrack(name, rtcTrack) {
override var rtcTrack: org.webrtc.VideoTrack = rtcTrack
... ... @@ -61,13 +69,18 @@ open class LocalVideoTrack(
super.stop()
}
fun restartTrack(options: LocalVideoTrackOptions = LocalVideoTrackOptions()) {
fun setDeviceId(deviceId: String) {
restartTrack(options.copy(deviceId = deviceId))
}
fun restartTrack(options: LocalVideoTrackOptions = defaultsManager.videoTrackCaptureDefaults.copy()) {
val newTrack = createTrack(
peerConnectionFactory,
context,
name,
options,
eglBase
eglBase,
trackFactory
)
val oldCapturer = capturer
... ... @@ -95,6 +108,17 @@ open class LocalVideoTrack(
sender?.setTrack(newTrack.rtcTrack, true)
}
@AssistedFactory
interface Factory {
fun create(
capturer: VideoCapturer,
source: VideoSource,
name: String,
options: LocalVideoTrackOptions,
rtcTrack: org.webrtc.VideoTrack,
): LocalVideoTrack
}
companion object {
internal fun createTrack(
... ... @@ -103,6 +127,7 @@ open class LocalVideoTrack(
name: String,
options: LocalVideoTrackOptions,
rootEglBase: EglBase,
trackFactory: Factory
): LocalVideoTrack {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) !=
... ... @@ -112,7 +137,7 @@ open class LocalVideoTrack(
}
val source = peerConnectionFactory.createVideoSource(options.isScreencast)
val capturer = createVideoCapturer(context, options.position) ?: TODO()
val (capturer, newOptions) = createVideoCapturer(context, options) ?: TODO()
capturer.initialize(
SurfaceTextureHelper.create("VideoCaptureThread", rootEglBase.eglBaseContext),
context,
... ... @@ -120,41 +145,44 @@ open class LocalVideoTrack(
)
val track = peerConnectionFactory.createVideoTrack(UUID.randomUUID().toString(), source)
return LocalVideoTrack(
return trackFactory.create(
capturer = capturer,
source = source,
options = options,
options = newOptions,
name = name,
rtcTrack = track,
peerConnectionFactory = peerConnectionFactory,
context = context,
eglBase = rootEglBase,
rtcTrack = track
)
}
private fun createVideoCapturer(context: Context, position: CameraPosition): VideoCapturer? {
val videoCapturer: VideoCapturer? = if (Camera2Enumerator.isSupported(context)) {
createCameraCapturer(context, Camera2Enumerator(context), position)
private fun createVideoCapturer(
context: Context,
options: LocalVideoTrackOptions
): Pair<VideoCapturer, LocalVideoTrackOptions>? {
val pair = if (Camera2Enumerator.isSupported(context)) {
createCameraCapturer(context, Camera2Enumerator(context), options)
} else {
createCameraCapturer(context, Camera1Enumerator(true), position)
createCameraCapturer(context, Camera1Enumerator(true), options)
}
if (videoCapturer == null) {
if (pair == null) {
LKLog.d { "Failed to open camera" }
return null
}
return videoCapturer
return pair
}
private fun createCameraCapturer(
context: Context,
enumerator: CameraEnumerator,
position: CameraPosition
): VideoCapturer? {
options: LocalVideoTrackOptions
): Pair<VideoCapturerWithSize, LocalVideoTrackOptions>? {
val deviceNames = enumerator.deviceNames
var targetDeviceName: String? = null
var targetVideoCapturer: VideoCapturer? = null
for (deviceName in deviceNames) {
if (enumerator.isFrontFacing(deviceName) && position == CameraPosition.FRONT) {
if ((options.deviceId != null && deviceName == options.deviceId)
|| (enumerator.isFrontFacing(deviceName) && options.position == CameraPosition.FRONT)
) {
LKLog.v { "Creating front facing camera capturer." }
val videoCapturer = enumerator.createCapturer(deviceName, null)
if (videoCapturer != null) {
... ... @@ -162,7 +190,9 @@ open class LocalVideoTrack(
targetVideoCapturer = videoCapturer
break
}
} else if (enumerator.isBackFacing(deviceName) && position == CameraPosition.BACK) {
} else if ((options.deviceId != null && deviceName == options.deviceId)
|| (enumerator.isBackFacing(deviceName) && options.position == CameraPosition.BACK)
) {
LKLog.v { "Creating back facing camera capturer." }
val videoCapturer = enumerator.createCapturer(deviceName, null)
if (videoCapturer != null) {
... ... @@ -173,19 +203,40 @@ open class LocalVideoTrack(
}
}
// back fill any missing information
val newOptions = options.copy(
deviceId = targetDeviceName,
position = enumerator.getCameraPosition(targetDeviceName!!)
)
if (targetVideoCapturer is Camera1Capturer) {
return Camera1CapturerWithSize(targetVideoCapturer, targetDeviceName)
return Pair(
Camera1CapturerWithSize(targetVideoCapturer, targetDeviceName),
newOptions
)
}
if (targetVideoCapturer is Camera2Capturer) {
return Camera2CapturerWithSize(
targetVideoCapturer,
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
targetDeviceName
return Pair(
Camera2CapturerWithSize(
targetVideoCapturer,
context.getSystemService(Context.CAMERA_SERVICE) as CameraManager,
targetDeviceName
),
newOptions
)
}
return null
}
fun CameraEnumerator.getCameraPosition(deviceName: String): CameraPosition? {
if (isBackFacing(deviceName)) {
return CameraPosition.BACK
} else if (isFrontFacing(deviceName)) {
return CameraPosition.FRONT
}
return null
}
}
}
... ...
... ... @@ -2,10 +2,15 @@ package io.livekit.android.room.track
import org.webrtc.RtpParameters
class LocalVideoTrackOptions(
var isScreencast: Boolean = false,
var position: CameraPosition = CameraPosition.FRONT,
var captureParams: VideoCaptureParameter = VideoPreset169.QHD.capture
data class LocalVideoTrackOptions(
val isScreencast: Boolean = false,
/**
* Preferred deviceId to capture from. If not set or found,
* will prefer a camera according to [position]
*/
val deviceId: String? = null,
val position: CameraPosition? = CameraPosition.FRONT,
val captureParams: VideoCaptureParameter = VideoPreset169.QHD.capture
)
data class VideoCaptureParameter(
... ...
... ... @@ -44,6 +44,34 @@ open class Track(
}
}
enum class Source {
CAMERA,
MICROPHONE,
SCREEN_SHARE,
UNKNOWN;
fun toProto(): LivekitModels.TrackSource {
return when (this) {
CAMERA -> LivekitModels.TrackSource.CAMERA
MICROPHONE -> LivekitModels.TrackSource.MICROPHONE
SCREEN_SHARE -> LivekitModels.TrackSource.SCREEN_SHARE
UNKNOWN -> LivekitModels.TrackSource.UNKNOWN
}
}
companion object {
fun fromProto(source: LivekitModels.TrackSource): Source {
return when (source) {
LivekitModels.TrackSource.CAMERA -> CAMERA
LivekitModels.TrackSource.MICROPHONE -> MICROPHONE
LivekitModels.TrackSource.SCREEN_SHARE -> SCREEN_SHARE
else -> UNKNOWN
}
}
}
}
data class Dimensions(var width: Int, var height: Int)
open fun start() {
... ...
... ... @@ -27,7 +27,8 @@ open class TrackPublication(
internal set
var dimensions: Track.Dimensions? = null
internal set
var source: Track.Source = Track.Source.UNKNOWN
internal set
var participant: WeakReference<Participant>
... ... @@ -44,6 +45,7 @@ open class TrackPublication(
name = info.name
kind = Track.Kind.fromProto(info.type)
muted = info.muted
source = Track.Source.fromProto(info.source)
if (kind == Track.Kind.VIDEO) {
simulcasted = info.simulcast
dimensions = Track.Dimensions(info.width, info.height)
... ...
... ... @@ -2,6 +2,8 @@ package org.webrtc
/**
* A helper to access package-protected methods used in [Camera2Session]
*
* Note: cameraId as used in the Camera1XXX classes refers to the index within the list of cameras.
* @suppress
*/
internal class Camera1Helper {
... ...
... ... @@ -4,6 +4,9 @@ import android.hardware.camera2.CameraManager
/**
* A helper to access package-protected methods used in [Camera2Session]
*
* Note: cameraId as used in the Camera2XXX classes refers to the id returned
* by [CameraManager.getCameraIdList].
* @suppress
*/
internal class Camera2Helper {
... ...
... ... @@ -54,7 +54,8 @@ class RoomTest {
context,
rtcEngine,
eglBase,
localParticantFactory
localParticantFactory,
DefaultsManager()
)
}
... ...