正在显示
12 个修改的文件
包含
279 行增加
和
119 行删除
| @@ -39,9 +39,11 @@ ext { | @@ -39,9 +39,11 @@ ext { | ||
| 39 | minVersion : 21, | 39 | minVersion : 21, |
| 40 | ] | 40 | ] |
| 41 | versions = [ | 41 | versions = [ |
| 42 | - androidx_core: "1.2.0", | ||
| 43 | - dagger : "2.27", | ||
| 44 | - protobuf : "3.15.1", | 42 | + androidx_core : "1.2.0", |
| 43 | + androidx_lifecycle: "2.3.0", | ||
| 44 | + dagger : "2.27", | ||
| 45 | + groupie : "2.9.0", | ||
| 46 | + protobuf : "3.15.1", | ||
| 45 | ] | 47 | ] |
| 46 | generated = [ | 48 | generated = [ |
| 47 | protoSrc: "$projectDir/../protocol", | 49 | protoSrc: "$projectDir/../protocol", |
| @@ -236,7 +236,9 @@ constructor( | @@ -236,7 +236,9 @@ constructor( | ||
| 236 | Rtc.SignalResponse.MessageCase.TRACK_PUBLISHED -> { | 236 | Rtc.SignalResponse.MessageCase.TRACK_PUBLISHED -> { |
| 237 | listener?.onLocalTrackPublished(response.trackPublished) | 237 | listener?.onLocalTrackPublished(response.trackPublished) |
| 238 | } | 238 | } |
| 239 | - Rtc.SignalResponse.MessageCase.SPEAKER -> TODO() | 239 | + Rtc.SignalResponse.MessageCase.SPEAKER -> { |
| 240 | + listener?.onActiveSpeakersChanged(response.speaker.speakersList) | ||
| 241 | + } | ||
| 240 | Rtc.SignalResponse.MessageCase.MESSAGE_NOT_SET -> TODO() | 242 | Rtc.SignalResponse.MessageCase.MESSAGE_NOT_SET -> TODO() |
| 241 | else -> { | 243 | else -> { |
| 242 | Timber.v { "unhandled response type: ${response.messageCase.name}" } | 244 | Timber.v { "unhandled response type: ${response.messageCase.name}" } |
| @@ -225,7 +225,7 @@ constructor( | @@ -225,7 +225,7 @@ constructor( | ||
| 225 | listener?.onFailedToConnect(this, error) | 225 | listener?.onFailedToConnect(this, error) |
| 226 | } | 226 | } |
| 227 | 227 | ||
| 228 | - fun setupVideo(viewRenderer: SurfaceViewRenderer) { | 228 | + fun initVideoRenderer(viewRenderer: SurfaceViewRenderer) { |
| 229 | viewRenderer.init(eglBase.eglBaseContext, null) | 229 | viewRenderer.init(eglBase.eglBaseContext, null) |
| 230 | viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) | 230 | viewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) |
| 231 | viewRenderer.setEnableHardwareScaler(false /* enabled */); | 231 | viewRenderer.setEnableHardwareScaler(false /* enabled */); |
| @@ -28,7 +28,7 @@ class RemoteParticipant( | @@ -28,7 +28,7 @@ class RemoteParticipant( | ||
| 28 | val remoteDataTracks | 28 | val remoteDataTracks |
| 29 | get() = dataTracks.values.toList() | 29 | get() = dataTracks.values.toList() |
| 30 | 30 | ||
| 31 | - val listener: Listener? = null | 31 | + var listener: Listener? = null |
| 32 | 32 | ||
| 33 | var participantInfo: Model.ParticipantInfo? = null | 33 | var participantInfo: Model.ParticipantInfo? = null |
| 34 | 34 | ||
| @@ -237,53 +237,65 @@ class RemoteParticipant( | @@ -237,53 +237,65 @@ class RemoteParticipant( | ||
| 237 | } | 237 | } |
| 238 | 238 | ||
| 239 | interface Listener { | 239 | interface Listener { |
| 240 | - fun onPublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) | ||
| 241 | - fun onUnpublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) | ||
| 242 | - fun onPublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) | ||
| 243 | - fun onUnpublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) | ||
| 244 | - fun onPublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) | ||
| 245 | - fun onUnpublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) | ||
| 246 | - | ||
| 247 | - fun onEnable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) | ||
| 248 | - fun onDisable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) | ||
| 249 | - fun onEnable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) | ||
| 250 | - fun onDisable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) | ||
| 251 | - | ||
| 252 | - fun onSubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) | 240 | + fun onPublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {} |
| 241 | + fun onUnpublish(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {} | ||
| 242 | + fun onPublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {} | ||
| 243 | + fun onUnpublish(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {} | ||
| 244 | + fun onPublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {} | ||
| 245 | + fun onUnpublish(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {} | ||
| 246 | + | ||
| 247 | + fun onEnable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {} | ||
| 248 | + fun onDisable(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {} | ||
| 249 | + fun onEnable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {} | ||
| 250 | + fun onDisable(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {} | ||
| 251 | + | ||
| 252 | + fun onSubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) {} | ||
| 253 | fun onFailToSubscribe( | 253 | fun onFailToSubscribe( |
| 254 | audioTrack: RemoteAudioTrack, | 254 | audioTrack: RemoteAudioTrack, |
| 255 | exception: Exception, | 255 | exception: Exception, |
| 256 | participant: RemoteParticipant | 256 | participant: RemoteParticipant |
| 257 | - ) | 257 | + ) { |
| 258 | + } | ||
| 258 | 259 | ||
| 259 | - fun onUnsubscribe(audioTrack: RemoteAudioTrackPublication, participant: RemoteParticipant) | 260 | + fun onUnsubscribe( |
| 261 | + audioTrack: RemoteAudioTrackPublication, | ||
| 262 | + participant: RemoteParticipant | ||
| 263 | + ) { | ||
| 264 | + } | ||
| 260 | 265 | ||
| 261 | - fun onSubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) | 266 | + fun onSubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) {} |
| 262 | fun onFailToSubscribe( | 267 | fun onFailToSubscribe( |
| 263 | videoTrack: RemoteVideoTrack, | 268 | videoTrack: RemoteVideoTrack, |
| 264 | exception: Exception, | 269 | exception: Exception, |
| 265 | participant: RemoteParticipant | 270 | participant: RemoteParticipant |
| 266 | - ) | 271 | + ) { |
| 272 | + } | ||
| 267 | 273 | ||
| 268 | - fun onUnsubscribe(videoTrack: RemoteVideoTrackPublication, participant: RemoteParticipant) | 274 | + fun onUnsubscribe( |
| 275 | + videoTrack: RemoteVideoTrackPublication, | ||
| 276 | + participant: RemoteParticipant | ||
| 277 | + ) { | ||
| 278 | + } | ||
| 269 | 279 | ||
| 270 | - fun onSubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) | 280 | + fun onSubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {} |
| 271 | fun onFailToSubscribe( | 281 | fun onFailToSubscribe( |
| 272 | dataTrack: RemoteDataTrackPublication, | 282 | dataTrack: RemoteDataTrackPublication, |
| 273 | exception: Exception, | 283 | exception: Exception, |
| 274 | participant: RemoteParticipant | 284 | participant: RemoteParticipant |
| 275 | - ) | 285 | + ) { |
| 286 | + } | ||
| 276 | 287 | ||
| 277 | - fun onUnsubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) | 288 | + fun onUnsubscribe(dataTrack: RemoteDataTrackPublication, participant: RemoteParticipant) {} |
| 278 | fun onReceive( | 289 | fun onReceive( |
| 279 | data: ByteBuffer, | 290 | data: ByteBuffer, |
| 280 | dataTrack: RemoteDataTrackPublication, | 291 | dataTrack: RemoteDataTrackPublication, |
| 281 | participant: RemoteParticipant | 292 | participant: RemoteParticipant |
| 282 | - ) | 293 | + ) { |
| 294 | + } | ||
| 283 | 295 | ||
| 284 | //fun networkQualityDidChange(networkQualityLevel: NetworkQualityLevel, participant: remoteParticipant) | 296 | //fun networkQualityDidChange(networkQualityLevel: NetworkQualityLevel, participant: remoteParticipant) |
| 285 | - fun switchedOffVideo(track: RemoteVideoTrack, participant: RemoteParticipant) | ||
| 286 | - fun switchedOnVideo(track: RemoteVideoTrack, participant: RemoteParticipant) | 297 | + fun switchedOffVideo(track: RemoteVideoTrack, participant: RemoteParticipant) {} |
| 298 | + fun switchedOnVideo(track: RemoteVideoTrack, participant: RemoteParticipant) {} | ||
| 287 | // fun onChangePublishPriority(videoTrack: RemoteVideoTrackPublication, priority: PublishPriority, participant: RemoteParticipant) | 299 | // fun onChangePublishPriority(videoTrack: RemoteVideoTrackPublication, priority: PublishPriority, participant: RemoteParticipant) |
| 288 | // fun onChangePublishPriority(audioTrack: RemoteAudioTrackPublication, priority: PublishPriority, participant: RemoteParticipant) | 300 | // fun onChangePublishPriority(audioTrack: RemoteAudioTrackPublication, priority: PublishPriority, participant: RemoteParticipant) |
| 289 | // fun onChangePublishPriority(dataTrack: RemoteDataTrackPublication, priority: PublishPriority, participant: RemoteParticipant) | 301 | // fun onChangePublishPriority(dataTrack: RemoteDataTrackPublication, priority: PublishPriority, participant: RemoteParticipant) |
| @@ -24,6 +24,9 @@ android { | @@ -24,6 +24,9 @@ android { | ||
| 24 | sourceCompatibility java_version | 24 | sourceCompatibility java_version |
| 25 | targetCompatibility java_version | 25 | targetCompatibility java_version |
| 26 | } | 26 | } |
| 27 | + kotlinOptions { | ||
| 28 | + jvmTarget = java_version | ||
| 29 | + } | ||
| 27 | buildFeatures { | 30 | buildFeatures { |
| 28 | viewBinding = true | 31 | viewBinding = true |
| 29 | } | 32 | } |
| @@ -36,7 +39,14 @@ dependencies { | @@ -36,7 +39,14 @@ dependencies { | ||
| 36 | implementation 'com.google.android.material:material:1.3.0' | 39 | implementation 'com.google.android.material:material:1.3.0' |
| 37 | implementation 'androidx.appcompat:appcompat:1.2.0' | 40 | implementation 'androidx.appcompat:appcompat:1.2.0' |
| 38 | implementation 'androidx.core:core-ktx:1.3.2' | 41 | implementation 'androidx.core:core-ktx:1.3.2' |
| 42 | + implementation "androidx.activity:activity-ktx:1.2.1" | ||
| 39 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' | 43 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' |
| 44 | + implementation "androidx.viewpager2:viewpager2:1.0.0" | ||
| 45 | + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:${versions.androidx_lifecycle}" | ||
| 46 | + implementation "androidx.lifecycle:lifecycle-common-java8:${versions.androidx_lifecycle}" | ||
| 47 | + implementation "com.xwray:groupie:${versions.groupie}" | ||
| 48 | + implementation "com.xwray:groupie-viewbinding:${versions.groupie}" | ||
| 49 | + implementation 'com.snakydesign.livedataextensions:lives:1.3.0' | ||
| 40 | implementation 'com.github.ajalt:timberkt:1.5.1' | 50 | implementation 'com.github.ajalt:timberkt:1.5.1' |
| 41 | implementation project(":livekit-android-sdk") | 51 | implementation project(":livekit-android-sdk") |
| 42 | testImplementation 'junit:junit:4.12' | 52 | testImplementation 'junit:junit:4.12' |
| @@ -3,20 +3,21 @@ package io.livekit.android.sample | @@ -3,20 +3,21 @@ package io.livekit.android.sample | ||
| 3 | import android.os.Bundle | 3 | import android.os.Bundle |
| 4 | import android.os.Parcelable | 4 | import android.os.Parcelable |
| 5 | import androidx.appcompat.app.AppCompatActivity | 5 | import androidx.appcompat.app.AppCompatActivity |
| 6 | -import androidx.lifecycle.lifecycleScope | ||
| 7 | -import io.livekit.android.ConnectOptions | ||
| 8 | -import io.livekit.android.LiveKit | ||
| 9 | -import io.livekit.android.room.Room | ||
| 10 | -import io.livekit.android.room.participant.Participant | ||
| 11 | -import io.livekit.android.room.participant.RemoteParticipant | ||
| 12 | -import io.livekit.android.room.track.VideoTrack | 6 | +import com.snakydesign.livedataextensions.combineLatest |
| 7 | +import com.xwray.groupie.GroupieAdapter | ||
| 13 | import io.livekit.android.sample.databinding.CallActivityBinding | 8 | import io.livekit.android.sample.databinding.CallActivityBinding |
| 14 | -import kotlinx.coroutines.launch | ||
| 15 | import kotlinx.parcelize.Parcelize | 9 | import kotlinx.parcelize.Parcelize |
| 16 | 10 | ||
| 17 | class CallActivity : AppCompatActivity() { | 11 | class CallActivity : AppCompatActivity() { |
| 18 | 12 | ||
| 13 | + val viewModel: CallViewModel by viewModelByFactory { | ||
| 14 | + | ||
| 15 | + val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) | ||
| 16 | + ?: throw NullPointerException("args is null!") | ||
| 17 | + CallViewModel(args.url, args.token, application) | ||
| 18 | + } | ||
| 19 | lateinit var binding: CallActivityBinding | 19 | lateinit var binding: CallActivityBinding |
| 20 | + | ||
| 20 | override fun onCreate(savedInstanceState: Bundle?) { | 21 | override fun onCreate(savedInstanceState: Bundle?) { |
| 21 | super.onCreate(savedInstanceState) | 22 | super.onCreate(savedInstanceState) |
| 22 | 23 | ||
| @@ -24,69 +25,20 @@ class CallActivity : AppCompatActivity() { | @@ -24,69 +25,20 @@ class CallActivity : AppCompatActivity() { | ||
| 24 | 25 | ||
| 25 | setContentView(binding.root) | 26 | setContentView(binding.root) |
| 26 | 27 | ||
| 27 | - val args = intent.getParcelableExtra<BundleArgs>(KEY_ARGS) | ||
| 28 | - if (args == null) { | ||
| 29 | - finish() | ||
| 30 | - return | 28 | + val adapter = GroupieAdapter() |
| 29 | + binding.viewPager.apply { | ||
| 30 | + this.adapter = adapter | ||
| 31 | } | 31 | } |
| 32 | 32 | ||
| 33 | - lifecycleScope.launch { | ||
| 34 | - | ||
| 35 | - val room = LiveKit.connect( | ||
| 36 | - applicationContext, | ||
| 37 | - args.url, | ||
| 38 | - args.token, | ||
| 39 | - ConnectOptions(false), | ||
| 40 | - object : Room.Listener { | ||
| 41 | - | ||
| 42 | - var loadedParticipant = false | ||
| 43 | - override fun onConnect(room: Room) { | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | - override fun onDisconnect(room: Room, error: Exception?) { | ||
| 47 | - } | ||
| 48 | - | ||
| 49 | - override fun onParticipantConnected( | ||
| 50 | - room: Room, | ||
| 51 | - participant: RemoteParticipant | ||
| 52 | - ) { | ||
| 53 | - if (!loadedParticipant) { | ||
| 54 | - room.setupVideo(binding.fullscreenVideoView) | ||
| 55 | - participant.remoteVideoTracks | ||
| 56 | - .first() | ||
| 57 | - .track | ||
| 58 | - .let { it as? VideoTrack } | ||
| 59 | - ?.addRenderer(binding.fullscreenVideoView) | ||
| 60 | - } | ||
| 61 | - } | ||
| 62 | - | ||
| 63 | - override fun onParticipantDisconnected( | ||
| 64 | - room: Room, | ||
| 65 | - participant: RemoteParticipant | ||
| 66 | - ) { | ||
| 67 | - } | ||
| 68 | - | ||
| 69 | - override fun onFailedToConnect(room: Room, error: Exception) { | ||
| 70 | - } | ||
| 71 | - | ||
| 72 | - override fun onReconnecting(room: Room, error: Exception) { | ||
| 73 | - } | ||
| 74 | - | ||
| 75 | - override fun onReconnect(room: Room) { | ||
| 76 | - } | ||
| 77 | - | ||
| 78 | - override fun onStartRecording(room: Room) { | ||
| 79 | - } | ||
| 80 | - | ||
| 81 | - override fun onStopRecording(room: Room) { | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | - override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) { | ||
| 85 | - } | ||
| 86 | - | ||
| 87 | - } | ||
| 88 | - ) | ||
| 89 | - } | 33 | + combineLatest( |
| 34 | + viewModel.room, | ||
| 35 | + viewModel.remoteParticipants | ||
| 36 | + ) { room, participants -> room to participants } | ||
| 37 | + .observe(this) { | ||
| 38 | + val (room, participants) = it | ||
| 39 | + val items = participants.map { participant -> ParticipantItem(room, participant) } | ||
| 40 | + adapter.update(items) | ||
| 41 | + } | ||
| 90 | } | 42 | } |
| 91 | 43 | ||
| 92 | 44 |
| 1 | +package io.livekit.android.sample | ||
| 2 | + | ||
| 3 | +import android.app.Application | ||
| 4 | +import androidx.lifecycle.AndroidViewModel | ||
| 5 | +import androidx.lifecycle.LiveData | ||
| 6 | +import androidx.lifecycle.MutableLiveData | ||
| 7 | +import androidx.lifecycle.viewModelScope | ||
| 8 | +import io.livekit.android.ConnectOptions | ||
| 9 | +import io.livekit.android.LiveKit | ||
| 10 | +import io.livekit.android.room.Room | ||
| 11 | +import io.livekit.android.room.participant.Participant | ||
| 12 | +import io.livekit.android.room.participant.RemoteParticipant | ||
| 13 | +import kotlinx.coroutines.launch | ||
| 14 | + | ||
| 15 | +class CallViewModel( | ||
| 16 | + val url: String, | ||
| 17 | + val token: String, | ||
| 18 | + application: Application | ||
| 19 | +) : AndroidViewModel(application) { | ||
| 20 | + | ||
| 21 | + | ||
| 22 | + private val mutableRoom = MutableLiveData<Room>() | ||
| 23 | + val room: LiveData<Room> = mutableRoom | ||
| 24 | + private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>() | ||
| 25 | + val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants | ||
| 26 | + | ||
| 27 | + init { | ||
| 28 | + | ||
| 29 | + viewModelScope.launch { | ||
| 30 | + | ||
| 31 | + mutableRoom.value = LiveKit.connect( | ||
| 32 | + application, | ||
| 33 | + url, | ||
| 34 | + token, | ||
| 35 | + ConnectOptions(false), | ||
| 36 | + object : Room.Listener { | ||
| 37 | + override fun onConnect(room: Room) { | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + override fun onDisconnect(room: Room, error: Exception?) { | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + override fun onParticipantConnected( | ||
| 44 | + room: Room, | ||
| 45 | + participant: RemoteParticipant | ||
| 46 | + ) { | ||
| 47 | + updateParticipants(room) | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + override fun onParticipantDisconnected( | ||
| 51 | + room: Room, | ||
| 52 | + participant: RemoteParticipant | ||
| 53 | + ) { | ||
| 54 | + updateParticipants(room) | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + override fun onFailedToConnect(room: Room, error: Exception) { | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + override fun onReconnecting(room: Room, error: Exception) { | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + override fun onReconnect(room: Room) { | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + override fun onStartRecording(room: Room) { | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + override fun onStopRecording(room: Room) { | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) { | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + } | ||
| 76 | + ) | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + fun updateParticipants(room: Room) { | ||
| 81 | + mutableRemoteParticipants.value = room.remoteParticipants | ||
| 82 | + .keys | ||
| 83 | + .sortedBy { it.sid } | ||
| 84 | + .mapNotNull { room.remoteParticipants[it] } | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | +} |
| @@ -35,6 +35,6 @@ class MainActivity : AppCompatActivity() { | @@ -35,6 +35,6 @@ class MainActivity : AppCompatActivity() { | ||
| 35 | companion object { | 35 | companion object { |
| 36 | val URL = SpannableStringBuilder("192.168.11.2:7880") | 36 | val URL = SpannableStringBuilder("192.168.11.2:7880") |
| 37 | val TOKEN = | 37 | val TOKEN = |
| 38 | - SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg1NzgxNzMsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNTk4NjE3MywidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.O3UedhM9lwdPxsZJQoTfVk0qXc-0ukjV6oZCBIaRTck") | 38 | + SpannableStringBuilder("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MTg2MjY0NDAsImlzcyI6IkFQSXdMZWFoN2c0ZnVMWURZQUplYUtzU0UiLCJqdGkiOiJwaG9uZSIsIm5iZiI6MTYxNjAzNDQ0MCwidmlkZW8iOnsicm9vbSI6Im15cm9vbSIsInJvb21Kb2luIjp0cnVlfX0.QWN0B_DO8eSP2sJivnr_QzBud_sdIgJeWDQGQz67DvY") |
| 39 | } | 39 | } |
| 40 | } | 40 | } |
| 1 | +package io.livekit.android.sample | ||
| 2 | + | ||
| 3 | +import android.view.View | ||
| 4 | +import com.xwray.groupie.viewbinding.BindableItem | ||
| 5 | +import com.xwray.groupie.viewbinding.GroupieViewHolder | ||
| 6 | +import io.livekit.android.room.Room | ||
| 7 | +import io.livekit.android.room.participant.RemoteParticipant | ||
| 8 | +import io.livekit.android.room.track.RemoteVideoTrackPublication | ||
| 9 | +import io.livekit.android.room.track.VideoTrack | ||
| 10 | +import io.livekit.android.room.track.VideoTrackPublication | ||
| 11 | +import io.livekit.android.sample.databinding.ParticipantItemBinding | ||
| 12 | + | ||
| 13 | +class ParticipantItem( | ||
| 14 | + val room: Room, | ||
| 15 | + val remoteParticipant: RemoteParticipant | ||
| 16 | +) : | ||
| 17 | + BindableItem<ParticipantItemBinding>() { | ||
| 18 | + | ||
| 19 | + private var videoBound = false | ||
| 20 | + | ||
| 21 | + override fun initializeViewBinding(view: View): ParticipantItemBinding { | ||
| 22 | + return ParticipantItemBinding.bind(view) | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + override fun bind(viewBinding: ParticipantItemBinding, position: Int) { | ||
| 26 | + viewBinding.run { | ||
| 27 | + room.initVideoRenderer(renderer) | ||
| 28 | + | ||
| 29 | + val existingTrack = getVideoTrack() | ||
| 30 | + if (existingTrack != null) { | ||
| 31 | + setupVideoIfNeeded(existingTrack, viewBinding) | ||
| 32 | + } else { | ||
| 33 | + remoteParticipant.listener = object : RemoteParticipant.Listener { | ||
| 34 | + override fun onSubscribe( | ||
| 35 | + videoTrack: RemoteVideoTrackPublication, | ||
| 36 | + participant: RemoteParticipant | ||
| 37 | + ) { | ||
| 38 | + val track = videoTrack.videoTrack | ||
| 39 | + if (track != null) { | ||
| 40 | + setupVideoIfNeeded(track, viewBinding) | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + } | ||
| 45 | + } | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + private fun getVideoTrack(): VideoTrack? { | ||
| 49 | + return remoteParticipant | ||
| 50 | + .remoteVideoTracks | ||
| 51 | + .firstOrNull() | ||
| 52 | + .let { it as? VideoTrackPublication } | ||
| 53 | + ?.videoTrack | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + private fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) { | ||
| 57 | + if (videoBound) { | ||
| 58 | + return | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + videoTrack.addRenderer(viewBinding.renderer) | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + override fun unbind(viewHolder: GroupieViewHolder<ParticipantItemBinding>) { | ||
| 65 | + super.unbind(viewHolder) | ||
| 66 | + videoBound = false | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + override fun getLayout(): Int = R.layout.participant_item | ||
| 70 | +} |
| 1 | +package io.livekit.android.sample | ||
| 2 | + | ||
| 3 | +import androidx.activity.viewModels | ||
| 4 | +import androidx.fragment.app.FragmentActivity | ||
| 5 | +import androidx.lifecycle.ViewModel | ||
| 6 | +import androidx.lifecycle.ViewModelProvider | ||
| 7 | + | ||
| 8 | +typealias CreateViewModel<VM> = () -> VM | ||
| 9 | + | ||
| 10 | +inline fun <reified VM : ViewModel> FragmentActivity.viewModelByFactory( | ||
| 11 | + noinline create: CreateViewModel<VM> | ||
| 12 | +): Lazy<VM> { | ||
| 13 | + return viewModels { | ||
| 14 | + createViewModelFactoryFactory(create) | ||
| 15 | + } | ||
| 16 | +} | ||
| 17 | + | ||
| 18 | +fun <VM> createViewModelFactoryFactory( | ||
| 19 | + create: CreateViewModel<VM> | ||
| 20 | +): ViewModelProvider.Factory { | ||
| 21 | + return object : ViewModelProvider.Factory { | ||
| 22 | + override fun <T : ViewModel?> create(modelClass: Class<T>): T { | ||
| 23 | + @Suppress("UNCHECKED_CAST") | ||
| 24 | + return create() as? T | ||
| 25 | + ?: throw IllegalArgumentException("Unknown viewmodel class!") | ||
| 26 | + } | ||
| 27 | + } | ||
| 28 | +} |
| 1 | - | ||
| 2 | -<FrameLayout | ||
| 3 | - xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 4 | - xmlns:tools="http://schemas.android.com/tools" | 1 | +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| 5 | android:layout_width="match_parent" | 2 | android:layout_width="match_parent" |
| 6 | android:layout_height="match_parent"> | 3 | android:layout_height="match_parent"> |
| 7 | 4 | ||
| 8 | - <org.webrtc.SurfaceViewRenderer | ||
| 9 | - android:id="@+id/fullscreen_video_view" | ||
| 10 | - android:layout_width="wrap_content" | ||
| 11 | - android:layout_height="wrap_content" | ||
| 12 | - android:layout_gravity="center" /> | 5 | + <androidx.viewpager2.widget.ViewPager2 |
| 6 | + android:id="@+id/view_pager" | ||
| 7 | + android:layout_width="match_parent" | ||
| 8 | + android:layout_height="match_parent" /> | ||
| 13 | 9 | ||
| 14 | <org.webrtc.SurfaceViewRenderer | 10 | <org.webrtc.SurfaceViewRenderer |
| 15 | android:id="@+id/pip_video_view" | 11 | android:id="@+id/pip_video_view" |
| 16 | android:layout_height="144dp" | 12 | android:layout_height="144dp" |
| 17 | android:layout_width="wrap_content" | 13 | android:layout_width="wrap_content" |
| 18 | android:layout_gravity="bottom|end" | 14 | android:layout_gravity="bottom|end" |
| 19 | - android:layout_margin="16dp"/> | ||
| 20 | - | ||
| 21 | - <FrameLayout | ||
| 22 | - android:id="@+id/call_fragment_container" | ||
| 23 | - android:layout_width="match_parent" | ||
| 24 | - android:layout_height="match_parent" /> | ||
| 25 | - <FrameLayout | ||
| 26 | - android:id="@+id/hud_fragment_container" | ||
| 27 | - android:layout_width="match_parent" | ||
| 28 | - android:layout_height="match_parent" /> | 15 | + android:layout_margin="16dp" /> |
| 29 | 16 | ||
| 30 | </FrameLayout> | 17 | </FrameLayout> |
| 1 | +<?xml version="1.0" encoding="utf-8"?> | ||
| 2 | +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 3 | + android:layout_width="match_parent" | ||
| 4 | + android:layout_height="match_parent"> | ||
| 5 | + | ||
| 6 | + <org.webrtc.SurfaceViewRenderer | ||
| 7 | + android:id="@+id/renderer" | ||
| 8 | + android:layout_width="match_parent" | ||
| 9 | + android:layout_height="match_parent" /> | ||
| 10 | +</FrameLayout> |
-
请 注册 或 登录 后发表评论