davidliu
Committed by GitHub

Allow setting of preferred video codec when publishing (#223)

  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project version="4">
  3 + <component name="KotlinJpsPluginSettings">
  4 + <option name="version" value="1.7.10" />
  5 + </component>
  6 +</project>
@@ -141,7 +141,7 @@ dependencies { @@ -141,7 +141,7 @@ dependencies {
141 lintPublish project(':livekit-lint') 141 lintPublish project(':livekit-lint')
142 142
143 testImplementation 'junit:junit:4.13.2' 143 testImplementation 'junit:junit:4.13.2'
144 - testImplementation 'org.robolectric:robolectric:4.6' 144 + testImplementation 'org.robolectric:robolectric:4.10.2'
145 testImplementation 'org.mockito:mockito-core:4.0.0' 145 testImplementation 'org.mockito:mockito-core:4.0.0'
146 testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" 146 testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
147 testImplementation 'androidx.test:core:1.4.0' 147 testImplementation 'androidx.test:core:1.4.0'
@@ -22,6 +22,8 @@ object InjectionNames { @@ -22,6 +22,8 @@ object InjectionNames {
22 */ 22 */
23 internal const val DISPATCHER_UNCONFINED = "dispatcher_unconfined" 23 internal const val DISPATCHER_UNCONFINED = "dispatcher_unconfined"
24 24
  25 + internal const val SENDER = "sender"
  26 +
25 internal const val OPTIONS_VIDEO_HW_ACCEL = "options_video_hw_accel" 27 internal const val OPTIONS_VIDEO_HW_ACCEL = "options_video_hw_accel"
26 28
27 // Overrides 29 // Overrides
@@ -17,6 +17,7 @@ import timber.log.Timber @@ -17,6 +17,7 @@ import timber.log.Timber
17 import javax.inject.Named 17 import javax.inject.Named
18 import javax.inject.Singleton 18 import javax.inject.Singleton
19 19
  20 +typealias CapabilitiesGetter = @JvmSuppressWildcards (MediaStreamTrack.MediaType) -> RtpCapabilities
20 21
21 @Module 22 @Module
22 object RTCModule { 23 object RTCModule {
@@ -193,6 +194,14 @@ object RTCModule { @@ -193,6 +194,14 @@ object RTCModule {
193 } 194 }
194 195
195 @Provides 196 @Provides
  197 + @Named(InjectionNames.SENDER)
  198 + fun senderCapabilitiesGetter(peerConnectionFactory: PeerConnectionFactory): CapabilitiesGetter {
  199 + return { mediaType: MediaStreamTrack.MediaType ->
  200 + peerConnectionFactory.getRtpSenderCapabilities(mediaType)
  201 + }
  202 + }
  203 +
  204 + @Provides
196 @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL) 205 @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)
197 fun videoHwAccel() = true 206 fun videoHwAccel() = true
198 } 207 }
@@ -7,6 +7,7 @@ import com.google.protobuf.ByteString @@ -7,6 +7,7 @@ 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.dagger.CapabilitiesGetter
10 import io.livekit.android.dagger.InjectionNames 11 import io.livekit.android.dagger.InjectionNames
11 import io.livekit.android.events.ParticipantEvent 12 import io.livekit.android.events.ParticipantEvent
12 import io.livekit.android.room.ConnectionState 13 import io.livekit.android.room.ConnectionState
@@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineDispatcher @@ -19,6 +20,7 @@ import kotlinx.coroutines.CoroutineDispatcher
19 import livekit.LivekitModels 20 import livekit.LivekitModels
20 import livekit.LivekitRtc 21 import livekit.LivekitRtc
21 import org.webrtc.* 22 import org.webrtc.*
  23 +import org.webrtc.RtpCapabilities.CodecCapability
22 import javax.inject.Named 24 import javax.inject.Named
23 import kotlin.math.max 25 import kotlin.math.max
24 26
@@ -36,6 +38,8 @@ internal constructor( @@ -36,6 +38,8 @@ internal constructor(
36 private val defaultsManager: DefaultsManager, 38 private val defaultsManager: DefaultsManager,
37 @Named(InjectionNames.DISPATCHER_DEFAULT) 39 @Named(InjectionNames.DISPATCHER_DEFAULT)
38 coroutineDispatcher: CoroutineDispatcher, 40 coroutineDispatcher: CoroutineDispatcher,
  41 + @Named(InjectionNames.SENDER)
  42 + private val capabilitiesGetter: CapabilitiesGetter,
39 ) : Participant("", null, coroutineDispatcher) { 43 ) : Participant("", null, coroutineDispatcher) {
40 44
41 var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults 45 var audioTrackCaptureDefaults: LocalAudioTrackOptions by defaultsManager::audioTrackCaptureDefaults
@@ -303,7 +307,46 @@ internal constructor( @@ -303,7 +307,46 @@ internal constructor(
303 return false 307 return false
304 } 308 }
305 309
306 - // TODO: enable setting preferred codec 310 +
  311 + if (options is VideoTrackPublishOptions && options.videoCodec != null) {
  312 + val targetCodec = options.videoCodec.lowercase()
  313 + val capabilities = capabilitiesGetter(MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO)
  314 + LKLog.v { "capabilities:" }
  315 + capabilities.codecs.forEach { codec ->
  316 + LKLog.v { "codec: ${codec.name}, ${codec.kind}, ${codec.mimeType}, ${codec.parameters}, ${codec.preferredPayloadType}" }
  317 + }
  318 +
  319 + val matched = mutableListOf<CodecCapability>()
  320 + val partialMatched = mutableListOf<CodecCapability>()
  321 + val unmatched = mutableListOf<CodecCapability>()
  322 +
  323 + for (codec in capabilities.codecs) {
  324 + val mimeType = codec.mimeType.lowercase()
  325 + if (mimeType == "audio/opus") {
  326 + matched.add(codec)
  327 + continue
  328 + }
  329 +
  330 + if (mimeType != "video/$targetCodec") {
  331 + unmatched.add(codec)
  332 + continue
  333 + }
  334 + // for h264 codecs that have sdpFmtpLine available, use only if the
  335 + // profile-level-id is 42e01f for cross-browser compatibility
  336 + if (targetCodec == "h264") {
  337 + if (codec.parameters["profile-level-id"] == "42e01f") {
  338 + matched.add(codec)
  339 + } else {
  340 + partialMatched.add(codec)
  341 + }
  342 + continue
  343 + } else {
  344 + matched.add(codec)
  345 + }
  346 + }
  347 + transceiver.setCodecPreferences(matched.plus(partialMatched).plus(unmatched))
  348 + }
  349 +
307 350
308 val publication = LocalTrackPublication( 351 val publication = LocalTrackPublication(
309 info = trackInfo, 352 info = trackInfo,
@@ -620,9 +663,6 @@ internal constructor( @@ -620,9 +663,6 @@ internal constructor(
620 interface Factory { 663 interface Factory {
621 fun create(dynacast: Boolean): LocalParticipant 664 fun create(dynacast: Boolean): LocalParticipant
622 } 665 }
623 -  
624 - companion object {  
625 - }  
626 } 666 }
627 667
628 internal fun LocalParticipant.publishTracksInfo(): List<LivekitRtc.TrackPublishedResponse> { 668 internal fun LocalParticipant.publishTracksInfo(): List<LivekitRtc.TrackPublishedResponse> {
@@ -643,18 +683,24 @@ interface TrackPublishOptions { @@ -643,18 +683,24 @@ interface TrackPublishOptions {
643 abstract class BaseVideoTrackPublishOptions { 683 abstract class BaseVideoTrackPublishOptions {
644 abstract val videoEncoding: VideoEncoding? 684 abstract val videoEncoding: VideoEncoding?
645 abstract val simulcast: Boolean 685 abstract val simulcast: Boolean
646 - //val videoCodec: VideoCodec? = null, 686 +
  687 + /**
  688 + * The video codec to use if available.
  689 + */
  690 + abstract val videoCodec: String?
647 } 691 }
648 692
649 data class VideoTrackPublishDefaults( 693 data class VideoTrackPublishDefaults(
650 override val videoEncoding: VideoEncoding? = null, 694 override val videoEncoding: VideoEncoding? = null,
651 - override val simulcast: Boolean = true 695 + override val simulcast: Boolean = true,
  696 + override val videoCodec: String? = null,
652 ) : BaseVideoTrackPublishOptions() 697 ) : BaseVideoTrackPublishOptions()
653 698
654 data class VideoTrackPublishOptions( 699 data class VideoTrackPublishOptions(
655 override val name: String? = null, 700 override val name: String? = null,
656 override val videoEncoding: VideoEncoding? = null, 701 override val videoEncoding: VideoEncoding? = null,
657 - override val simulcast: Boolean = true 702 + override val simulcast: Boolean = true,
  703 + override val videoCodec: String? = null,
658 ) : BaseVideoTrackPublishOptions(), TrackPublishOptions { 704 ) : BaseVideoTrackPublishOptions(), TrackPublishOptions {
659 constructor( 705 constructor(
660 name: String? = null, 706 name: String? = null,
@@ -662,7 +708,8 @@ data class VideoTrackPublishOptions( @@ -662,7 +708,8 @@ data class VideoTrackPublishOptions(
662 ) : this( 708 ) : this(
663 name, 709 name,
664 base.videoEncoding, 710 base.videoEncoding,
665 - base.simulcast 711 + base.simulcast,
  712 + base.videoCodec,
666 ) 713 )
667 } 714 }
668 715
1 package io.livekit.android.mock 1 package io.livekit.android.mock
2 2
3 -import org.webrtc.* 3 +import org.webrtc.DataChannel
  4 +import org.webrtc.IceCandidate
  5 +import org.webrtc.MediaConstraints
  6 +import org.webrtc.MediaStream
  7 +import org.webrtc.MediaStreamTrack
  8 +import org.webrtc.NativePeerConnectionFactory
  9 +import org.webrtc.PeerConnection
  10 +import org.webrtc.RTCStatsCollectorCallback
  11 +import org.webrtc.RTCStatsReport
  12 +import org.webrtc.RtcCertificatePem
  13 +import org.webrtc.RtpReceiver
  14 +import org.webrtc.RtpSender
  15 +import org.webrtc.RtpTransceiver
  16 +import org.webrtc.SdpObserver
  17 +import org.webrtc.SessionDescription
  18 +import org.webrtc.StatsObserver
4 19
5 private class MockNativePeerConnectionFactory : NativePeerConnectionFactory { 20 private class MockNativePeerConnectionFactory : NativePeerConnectionFactory {
6 override fun createNativePeerConnection(): Long = 0L 21 override fun createNativePeerConnection(): Long = 0L
@@ -14,6 +29,8 @@ class MockPeerConnection( @@ -14,6 +29,8 @@ class MockPeerConnection(
14 private var closed = false 29 private var closed = false
15 var localDesc: SessionDescription? = null 30 var localDesc: SessionDescription? = null
16 var remoteDesc: SessionDescription? = null 31 var remoteDesc: SessionDescription? = null
  32 +
  33 + private val transceivers = mutableListOf<RtpTransceiver>()
17 override fun getLocalDescription(): SessionDescription? = localDesc 34 override fun getLocalDescription(): SessionDescription? = localDesc
18 override fun setLocalDescription(observer: SdpObserver?, sdp: SessionDescription?) { 35 override fun setLocalDescription(observer: SdpObserver?, sdp: SessionDescription?) {
19 localDesc = sdp 36 localDesc = sdp
@@ -85,7 +102,7 @@ class MockPeerConnection( @@ -85,7 +102,7 @@ class MockPeerConnection(
85 } 102 }
86 103
87 override fun getTransceivers(): List<RtpTransceiver> { 104 override fun getTransceivers(): List<RtpTransceiver> {
88 - return emptyList() 105 + return transceivers
89 } 106 }
90 107
91 override fun addTrack(track: MediaStreamTrack?): RtpSender { 108 override fun addTrack(track: MediaStreamTrack?): RtpSender {
@@ -100,15 +117,19 @@ class MockPeerConnection( @@ -100,15 +117,19 @@ class MockPeerConnection(
100 return super.removeTrack(sender) 117 return super.removeTrack(sender)
101 } 118 }
102 119
103 - override fun addTransceiver(track: MediaStreamTrack?): RtpTransceiver {  
104 - return super.addTransceiver(track) 120 + override fun addTransceiver(track: MediaStreamTrack): RtpTransceiver {
  121 + val transceiver = MockRtpTransceiver.create(track, RtpTransceiver.RtpTransceiverInit())
  122 + transceivers.add(transceiver)
  123 + return transceiver
105 } 124 }
106 125
107 override fun addTransceiver( 126 override fun addTransceiver(
108 track: MediaStreamTrack, 127 track: MediaStreamTrack,
109 init: RtpTransceiver.RtpTransceiverInit? 128 init: RtpTransceiver.RtpTransceiverInit?
110 ): RtpTransceiver { 129 ): RtpTransceiver {
111 - return MockRtpTransceiver.create(track, init ?: RtpTransceiver.RtpTransceiverInit()) 130 + val transceiver = MockRtpTransceiver.create(track, init ?: RtpTransceiver.RtpTransceiverInit())
  131 + transceivers.add(transceiver)
  132 + return transceiver
112 } 133 }
113 134
114 override fun addTransceiver(mediaType: MediaStreamTrack.MediaType?): RtpTransceiver { 135 override fun addTransceiver(mediaType: MediaStreamTrack.MediaType?): RtpTransceiver {
@@ -177,6 +198,7 @@ class MockPeerConnection( @@ -177,6 +198,7 @@ class MockPeerConnection(
177 IceConnectionState.CHECKING -> PeerConnectionState.CONNECTING 198 IceConnectionState.CHECKING -> PeerConnectionState.CONNECTING
178 IceConnectionState.CONNECTED, 199 IceConnectionState.CONNECTED,
179 IceConnectionState.COMPLETED -> PeerConnectionState.CONNECTED 200 IceConnectionState.COMPLETED -> PeerConnectionState.CONNECTED
  201 +
180 IceConnectionState.DISCONNECTED -> PeerConnectionState.DISCONNECTED 202 IceConnectionState.DISCONNECTED -> PeerConnectionState.DISCONNECTED
181 IceConnectionState.FAILED -> PeerConnectionState.FAILED 203 IceConnectionState.FAILED -> PeerConnectionState.FAILED
182 IceConnectionState.CLOSED -> PeerConnectionState.CLOSED 204 IceConnectionState.CLOSED -> PeerConnectionState.CLOSED
@@ -216,6 +238,7 @@ class MockPeerConnection( @@ -216,6 +238,7 @@ class MockPeerConnection(
216 iceConnectionState = newState 238 iceConnectionState = newState
217 } 239 }
218 } 240 }
  241 +
219 IceConnectionState.FAILED, 242 IceConnectionState.FAILED,
220 IceConnectionState.DISCONNECTED, 243 IceConnectionState.DISCONNECTED,
221 IceConnectionState.CLOSED -> { 244 IceConnectionState.CLOSED -> {
  1 +package io.livekit.android.mock
  2 +
  3 +import android.content.Context
  4 +import org.webrtc.CapturerObserver
  5 +import org.webrtc.SurfaceTextureHelper
  6 +import org.webrtc.VideoCapturer
  7 +
  8 +class MockVideoCapturer : VideoCapturer {
  9 + override fun initialize(p0: SurfaceTextureHelper?, p1: Context?, p2: CapturerObserver?) {
  10 + }
  11 +
  12 + override fun startCapture(p0: Int, p1: Int, p2: Int) {
  13 + }
  14 +
  15 + override fun stopCapture() {
  16 + }
  17 +
  18 + override fun changeCaptureFormat(p0: Int, p1: Int, p2: Int) {
  19 + }
  20 +
  21 + override fun dispose() {
  22 + }
  23 +
  24 + override fun isScreencast(): Boolean {
  25 + return false
  26 + }
  27 +}
  1 +package io.livekit.android.mock
  2 +
  3 +import org.webrtc.VideoSource
  4 +
  5 +class MockVideoSource(nativeSource: Long = 100) : VideoSource(nativeSource) {
  6 +}
@@ -8,7 +8,8 @@ import okio.IOException @@ -8,7 +8,8 @@ import okio.IOException
8 8
9 class MockWebSocket( 9 class MockWebSocket(
10 private val request: Request, 10 private val request: Request,
11 - private val listener: WebSocketListener 11 + private val listener: WebSocketListener,
  12 + private val onSend: ((ByteString) -> Unit)?
12 ) : WebSocket { 13 ) : WebSocket {
13 14
14 var isClosed = false 15 var isClosed = false
@@ -45,6 +46,7 @@ class MockWebSocket( @@ -45,6 +46,7 @@ class MockWebSocket(
45 return false 46 return false
46 } 47 }
47 mutableSentRequests.add(bytes) 48 mutableSentRequests.add(bytes)
  49 + onSend?.invoke(bytes)
48 return !isClosed 50 return !isClosed
49 } 51 }
50 52
1 package io.livekit.android.mock 1 package io.livekit.android.mock
2 2
  3 +import io.livekit.android.util.toOkioByteString
  4 +import io.livekit.android.util.toPBByteString
  5 +import livekit.LivekitModels
  6 +import livekit.LivekitRtc
3 import okhttp3.Request 7 import okhttp3.Request
4 import okhttp3.WebSocket 8 import okhttp3.WebSocket
5 import okhttp3.WebSocketListener 9 import okhttp3.WebSocketListener
@@ -20,7 +24,25 @@ class MockWebSocketFactory : WebSocket.Factory { @@ -20,7 +24,25 @@ class MockWebSocketFactory : WebSocket.Factory {
20 */ 24 */
21 lateinit var listener: WebSocketListener 25 lateinit var listener: WebSocketListener
22 override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket { 26 override fun newWebSocket(request: Request, listener: WebSocketListener): WebSocket {
23 - this.ws = MockWebSocket(request, listener) 27 + this.ws = MockWebSocket(request, listener) { byteString ->
  28 + val signalRequest = LivekitRtc.SignalRequest.parseFrom(byteString.toPBByteString())
  29 + if (signalRequest.hasAddTrack()) {
  30 + val addTrack = signalRequest.addTrack
  31 + val trackPublished = with(LivekitRtc.SignalResponse.newBuilder()) {
  32 + trackPublished = with(LivekitRtc.TrackPublishedResponse.newBuilder()) {
  33 + cid = addTrack.cid
  34 + if (addTrack.type == LivekitModels.TrackType.AUDIO) {
  35 + track = TestData.LOCAL_AUDIO_TRACK
  36 + } else {
  37 + track = TestData.LOCAL_VIDEO_TRACK
  38 + }
  39 + build()
  40 + }
  41 + build()
  42 + }
  43 + this.listener.onMessage(this.ws, trackPublished.toOkioByteString())
  44 + }
  45 + }
24 this.listener = listener 46 this.listener = listener
25 this.request = request 47 this.request = request
26 48
@@ -9,6 +9,11 @@ object TestData { @@ -9,6 +9,11 @@ object TestData {
9 type = LivekitModels.TrackType.AUDIO 9 type = LivekitModels.TrackType.AUDIO
10 build() 10 build()
11 } 11 }
  12 + val LOCAL_VIDEO_TRACK = with(LivekitModels.TrackInfo.newBuilder()) {
  13 + sid = "local_video_track_sid"
  14 + type = LivekitModels.TrackType.VIDEO
  15 + build()
  16 + }
12 17
13 val REMOTE_AUDIO_TRACK = with(LivekitModels.TrackInfo.newBuilder()) { 18 val REMOTE_AUDIO_TRACK = with(LivekitModels.TrackInfo.newBuilder()) {
14 sid = "remote_audio_track_sid" 19 sid = "remote_audio_track_sid"
@@ -3,6 +3,7 @@ package io.livekit.android.mock.dagger @@ -3,6 +3,7 @@ package io.livekit.android.mock.dagger
3 import android.content.Context 3 import android.content.Context
4 import dagger.Module 4 import dagger.Module
5 import dagger.Provides 5 import dagger.Provides
  6 +import io.livekit.android.dagger.CapabilitiesGetter
6 import io.livekit.android.dagger.InjectionNames 7 import io.livekit.android.dagger.InjectionNames
7 import io.livekit.android.mock.MockEglBase 8 import io.livekit.android.mock.MockEglBase
8 import org.webrtc.* 9 import org.webrtc.*
@@ -34,6 +35,14 @@ object TestRTCModule { @@ -34,6 +35,14 @@ object TestRTCModule {
34 } 35 }
35 36
36 @Provides 37 @Provides
  38 + @Named(InjectionNames.SENDER)
  39 + fun senderCapabilitiesGetter(peerConnectionFactory: PeerConnectionFactory): CapabilitiesGetter {
  40 + return { mediaType: MediaStreamTrack.MediaType ->
  41 + peerConnectionFactory.getRtpSenderCapabilities(mediaType)
  42 + }
  43 + }
  44 +
  45 + @Provides
37 @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL) 46 @Named(InjectionNames.OPTIONS_VIDEO_HW_ACCEL)
38 fun videoHwAccel() = true 47 fun videoHwAccel() = true
39 } 48 }
@@ -300,16 +300,12 @@ class RoomMockE2ETest : MockE2ETest() { @@ -300,16 +300,12 @@ class RoomMockE2ETest : MockE2ETest() {
300 fun disconnectCleansLocalParticipant() = runTest { 300 fun disconnectCleansLocalParticipant() = runTest {
301 connect() 301 connect()
302 302
303 - val publishJob = launch {  
304 - room.localParticipant.publishAudioTrack(  
305 - LocalAudioTrack(  
306 - "",  
307 - MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)  
308 - ) 303 + room.localParticipant.publishAudioTrack(
  304 + LocalAudioTrack(
  305 + "",
  306 + MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)
309 ) 307 )
310 - }  
311 - wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.LOCAL_TRACK_PUBLISHED.toOkioByteString())  
312 - publishJob.join() 308 + )
313 309
314 val eventCollector = EventCollector(room.events, coroutineRule.scope) 310 val eventCollector = EventCollector(room.events, coroutineRule.scope)
315 room.disconnect() 311 room.disconnect()
@@ -6,7 +6,6 @@ import io.livekit.android.mock.MockPeerConnection @@ -6,7 +6,6 @@ import io.livekit.android.mock.MockPeerConnection
6 import io.livekit.android.room.track.LocalAudioTrack 6 import io.livekit.android.room.track.LocalAudioTrack
7 import io.livekit.android.util.toPBByteString 7 import io.livekit.android.util.toPBByteString
8 import kotlinx.coroutines.ExperimentalCoroutinesApi 8 import kotlinx.coroutines.ExperimentalCoroutinesApi
9 -import kotlinx.coroutines.launch  
10 import livekit.LivekitRtc 9 import livekit.LivekitRtc
11 import org.junit.Assert 10 import org.junit.Assert
12 import org.junit.Assert.assertEquals 11 import org.junit.Assert.assertEquals
@@ -91,16 +90,12 @@ class RoomReconnectionMockE2ETest : MockE2ETest() { @@ -91,16 +90,12 @@ class RoomReconnectionMockE2ETest : MockE2ETest() {
91 connect() 90 connect()
92 91
93 // publish track 92 // publish track
94 - val publishJob = launch {  
95 - room.localParticipant.publishAudioTrack(  
96 - LocalAudioTrack(  
97 - "",  
98 - MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)  
99 - ) 93 + room.localParticipant.publishAudioTrack(
  94 + LocalAudioTrack(
  95 + "",
  96 + MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)
100 ) 97 )
101 - }  
102 - simulateMessageFromServer(SignalClientTest.LOCAL_TRACK_PUBLISHED)  
103 - publishJob.join() 98 + )
104 99
105 prepareForReconnect() 100 prepareForReconnect()
106 disconnectPeerConnection() 101 disconnectPeerConnection()
@@ -6,17 +6,27 @@ import io.livekit.android.events.EventCollector @@ -6,17 +6,27 @@ import io.livekit.android.events.EventCollector
6 import io.livekit.android.events.ParticipantEvent 6 import io.livekit.android.events.ParticipantEvent
7 import io.livekit.android.events.RoomEvent 7 import io.livekit.android.events.RoomEvent
8 import io.livekit.android.mock.MockAudioStreamTrack 8 import io.livekit.android.mock.MockAudioStreamTrack
  9 +import io.livekit.android.mock.MockEglBase
  10 +import io.livekit.android.mock.MockVideoCapturer
  11 +import io.livekit.android.mock.MockVideoStreamTrack
  12 +import io.livekit.android.room.DefaultsManager
9 import io.livekit.android.room.SignalClientTest 13 import io.livekit.android.room.SignalClientTest
10 import io.livekit.android.room.track.LocalAudioTrack 14 import io.livekit.android.room.track.LocalAudioTrack
  15 +import io.livekit.android.room.track.LocalVideoTrack
  16 +import io.livekit.android.room.track.LocalVideoTrackOptions
  17 +import io.livekit.android.room.track.VideoCaptureParameter
11 import io.livekit.android.util.toOkioByteString 18 import io.livekit.android.util.toOkioByteString
12 import io.livekit.android.util.toPBByteString 19 import io.livekit.android.util.toPBByteString
13 import kotlinx.coroutines.ExperimentalCoroutinesApi 20 import kotlinx.coroutines.ExperimentalCoroutinesApi
14 -import kotlinx.coroutines.launch  
15 import livekit.LivekitRtc 21 import livekit.LivekitRtc
16 import org.junit.Assert.* 22 import org.junit.Assert.*
17 import org.junit.Test 23 import org.junit.Test
18 import org.junit.runner.RunWith 24 import org.junit.runner.RunWith
  25 +import org.mockito.Mockito
  26 +import org.mockito.Mockito.mock
  27 +import org.mockito.kotlin.argThat
19 import org.robolectric.RobolectricTestRunner 28 import org.robolectric.RobolectricTestRunner
  29 +import org.webrtc.VideoSource
20 30
21 @ExperimentalCoroutinesApi 31 @ExperimentalCoroutinesApi
22 @RunWith(RobolectricTestRunner::class) 32 @RunWith(RobolectricTestRunner::class)
@@ -26,16 +36,12 @@ class LocalParticipantMockE2ETest : MockE2ETest() { @@ -26,16 +36,12 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
26 fun disconnectCleansLocalParticipant() = runTest { 36 fun disconnectCleansLocalParticipant() = runTest {
27 connect() 37 connect()
28 38
29 - val publishJob = launch {  
30 - room.localParticipant.publishAudioTrack(  
31 - LocalAudioTrack(  
32 - "",  
33 - MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)  
34 - ) 39 + room.localParticipant.publishAudioTrack(
  40 + LocalAudioTrack(
  41 + "",
  42 + MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)
35 ) 43 )
36 - }  
37 - wsFactory.listener.onMessage(wsFactory.ws, SignalClientTest.LOCAL_TRACK_PUBLISHED.toOkioByteString())  
38 - publishJob.join() 44 + )
39 45
40 room.disconnect() 46 room.disconnect()
41 47
@@ -123,4 +129,55 @@ class LocalParticipantMockE2ETest : MockE2ETest() { @@ -123,4 +129,55 @@ class LocalParticipantMockE2ETest : MockE2ETest() {
123 participantEvents 129 participantEvents
124 ) 130 )
125 } 131 }
  132 +
  133 + private fun createLocalTrack() = LocalVideoTrack(
  134 + capturer = MockVideoCapturer(),
  135 + source = mock(VideoSource::class.java),
  136 + name = "",
  137 + options = LocalVideoTrackOptions(
  138 + isScreencast = false,
  139 + deviceId = null,
  140 + position = null,
  141 + captureParams = VideoCaptureParameter(width = 0, height = 0, maxFps = 0)
  142 + ),
  143 + rtcTrack = MockVideoStreamTrack(),
  144 + peerConnectionFactory = component.peerConnectionFactory(),
  145 + context = context,
  146 + eglBase = MockEglBase(),
  147 + defaultsManager = DefaultsManager(),
  148 + trackFactory = mock(LocalVideoTrack.Factory::class.java)
  149 + )
  150 +
  151 + @Test
  152 + fun publishSetCodecPreferencesH264() = runTest {
  153 + room.videoTrackPublishDefaults = room.videoTrackPublishDefaults.copy(videoCodec = "h264")
  154 + connect()
  155 +
  156 + room.localParticipant.publishVideoTrack(track = createLocalTrack())
  157 +
  158 + val peerConnection = component.rtcEngine().publisher.peerConnection
  159 + val transceiver = peerConnection.transceivers.first()
  160 +
  161 + Mockito.verify(transceiver).setCodecPreferences(argThat { codecs ->
  162 + val preferredCodec = codecs.first()
  163 + return@argThat preferredCodec.name.lowercase() == "h264" &&
  164 + preferredCodec.parameters["profile-level-id"] == "42e01f"
  165 + })
  166 + }
  167 +
  168 + @Test
  169 + fun publishSetCodecPreferencesVP8() = runTest {
  170 + room.videoTrackPublishDefaults = room.videoTrackPublishDefaults.copy(videoCodec = "vp8")
  171 + connect()
  172 +
  173 + room.localParticipant.publishVideoTrack(track = createLocalTrack())
  174 +
  175 + val peerConnection = component.rtcEngine().publisher.peerConnection
  176 + val transceiver = peerConnection.transceivers.first()
  177 +
  178 + Mockito.verify(transceiver).setCodecPreferences(argThat { codecs ->
  179 + val preferredCodec = codecs.first()
  180 + return@argThat preferredCodec.name.lowercase() == "vp8"
  181 + })
  182 + }
126 } 183 }
@@ -10,7 +10,6 @@ import io.livekit.android.room.SignalClientTest @@ -10,7 +10,6 @@ import io.livekit.android.room.SignalClientTest
10 import io.livekit.android.room.track.LocalAudioTrack 10 import io.livekit.android.room.track.LocalAudioTrack
11 import io.livekit.android.util.toOkioByteString 11 import io.livekit.android.util.toOkioByteString
12 import kotlinx.coroutines.ExperimentalCoroutinesApi 12 import kotlinx.coroutines.ExperimentalCoroutinesApi
13 -import kotlinx.coroutines.launch  
14 import org.junit.Assert.assertEquals 13 import org.junit.Assert.assertEquals
15 import org.junit.Test 14 import org.junit.Test
16 import org.junit.runner.RunWith 15 import org.junit.runner.RunWith
@@ -26,16 +25,12 @@ class ParticipantMockE2ETest : MockE2ETest() { @@ -26,16 +25,12 @@ class ParticipantMockE2ETest : MockE2ETest() {
26 connect() 25 connect()
27 26
28 // publish track 27 // publish track
29 - val publishJob = launch {  
30 - room.localParticipant.publishAudioTrack(  
31 - LocalAudioTrack(  
32 - "",  
33 - MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)  
34 - ) 28 + room.localParticipant.publishAudioTrack(
  29 + LocalAudioTrack(
  30 + "",
  31 + MockAudioStreamTrack(id = SignalClientTest.LOCAL_TRACK_PUBLISHED.trackPublished.cid)
35 ) 32 )
36 - }  
37 - simulateMessageFromServer(SignalClientTest.LOCAL_TRACK_PUBLISHED)  
38 - publishJob.join() 33 + )
39 34
40 val eventCollector = EventCollector(room.events, coroutineRule.scope) 35 val eventCollector = EventCollector(room.events, coroutineRule.scope)
41 // remote unpublish 36 // remote unpublish
1 package org.webrtc 1 package org.webrtc
2 2
3 import io.livekit.android.mock.MockPeerConnection 3 import io.livekit.android.mock.MockPeerConnection
  4 +import io.livekit.android.mock.MockVideoSource
  5 +import io.livekit.android.mock.MockVideoStreamTrack
4 6
5 class MockPeerConnectionFactory : PeerConnectionFactory(1L) { 7 class MockPeerConnectionFactory : PeerConnectionFactory(1L) {
6 override fun createPeerConnectionInternal( 8 override fun createPeerConnectionInternal(
@@ -11,4 +13,42 @@ class MockPeerConnectionFactory : PeerConnectionFactory(1L) { @@ -11,4 +13,42 @@ class MockPeerConnectionFactory : PeerConnectionFactory(1L) {
11 ): PeerConnection { 13 ): PeerConnection {
12 return MockPeerConnection(rtcConfig, observer) 14 return MockPeerConnection(rtcConfig, observer)
13 } 15 }
  16 +
  17 + override fun createVideoSource(isScreencast: Boolean, alignTimestamps: Boolean): VideoSource {
  18 + return MockVideoSource()
  19 + }
  20 +
  21 + override fun createVideoSource(isScreencast: Boolean): VideoSource {
  22 + return MockVideoSource()
  23 + }
  24 +
  25 + override fun createVideoTrack(id: String, source: VideoSource?): VideoTrack {
  26 + return MockVideoStreamTrack(id = id)
  27 + }
  28 +
  29 + override fun getRtpSenderCapabilities(mediaType: MediaStreamTrack.MediaType): RtpCapabilities {
  30 + return RtpCapabilities(
  31 + listOf(
  32 + RtpCapabilities.CodecCapability().apply {
  33 + name = "VP8"
  34 + mimeType = "video/VP8"
  35 + kind = MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO
  36 + parameters = emptyMap()
  37 + },
  38 + RtpCapabilities.CodecCapability().apply {
  39 + name = "H264"
  40 + mimeType = "video/H264"
  41 + kind = MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO
  42 + parameters = mapOf("profile-level-id" to "640c1f")
  43 + },
  44 + RtpCapabilities.CodecCapability().apply {
  45 + name = "H264"
  46 + mimeType = "video/H264"
  47 + kind = MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO
  48 + parameters = mapOf("profile-level-id" to "42e01f")
  49 + },
  50 + ),
  51 + emptyList()
  52 + )
  53 + }
14 } 54 }