Committed by
GitHub
Allow setting of preferred video codec when publishing (#223)
正在显示
17 个修改的文件
包含
296 行增加
和
55 行删除
.idea/kotlinc.xml
0 → 100644
| @@ -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 | +} |
| @@ -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() |
livekit-android-sdk/src/test/java/io/livekit/android/room/participant/LocalParticipantMockE2ETest.kt
| @@ -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 | } |
-
请 注册 或 登录 后发表评论