正在显示
20 个修改的文件
包含
253 行增加
和
154 行删除
| @@ -108,16 +108,24 @@ internal constructor( | @@ -108,16 +108,24 @@ internal constructor( | ||
| 108 | return super.getTrackPublicationByName(name) as? LocalTrackPublication | 108 | return super.getTrackPublicationByName(name) as? LocalTrackPublication |
| 109 | } | 109 | } |
| 110 | 110 | ||
| 111 | - suspend fun setCameraEnabled(enabled: Boolean){ | 111 | + suspend fun setCameraEnabled(enabled: Boolean) { |
| 112 | setTrackEnabled(Track.Source.CAMERA, enabled) | 112 | setTrackEnabled(Track.Source.CAMERA, enabled) |
| 113 | } | 113 | } |
| 114 | 114 | ||
| 115 | - suspend fun setMicrophoneEnabled(enabled: Boolean){ | 115 | + suspend fun setMicrophoneEnabled(enabled: Boolean) { |
| 116 | setTrackEnabled(Track.Source.MICROPHONE, enabled) | 116 | setTrackEnabled(Track.Source.MICROPHONE, enabled) |
| 117 | } | 117 | } |
| 118 | 118 | ||
| 119 | - suspend fun setScreenShareEnabled(enabled: Boolean) { | ||
| 120 | - setTrackEnabled(Track.Source.SCREEN_SHARE, enabled) | 119 | + /** |
| 120 | + * @param mediaProjectionPermissionResultData The resultData returned from launching | ||
| 121 | + * [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()). | ||
| 122 | + * @throws IllegalArgumentException if attempting to enable screenshare without [mediaProjectionPermissionResultData] | ||
| 123 | + */ | ||
| 124 | + suspend fun setScreenShareEnabled( | ||
| 125 | + enabled: Boolean, | ||
| 126 | + mediaProjectionPermissionResultData: Intent? = null | ||
| 127 | + ) { | ||
| 128 | + setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, mediaProjectionPermissionResultData) | ||
| 121 | } | 129 | } |
| 122 | 130 | ||
| 123 | private suspend fun setTrackEnabled( | 131 | private suspend fun setTrackEnabled( |
| 1 | package io.livekit.android.sample | 1 | package io.livekit.android.sample |
| 2 | 2 | ||
| 3 | +import android.app.Activity | ||
| 3 | import android.media.AudioManager | 4 | import android.media.AudioManager |
| 5 | +import android.media.projection.MediaProjectionManager | ||
| 4 | import android.os.Bundle | 6 | import android.os.Bundle |
| 5 | import android.os.Parcelable | 7 | import android.os.Parcelable |
| 8 | +import androidx.activity.result.contract.ActivityResultContracts | ||
| 6 | import androidx.appcompat.app.AppCompatActivity | 9 | import androidx.appcompat.app.AppCompatActivity |
| 10 | +import androidx.recyclerview.widget.LinearLayoutManager | ||
| 7 | import com.github.ajalt.timberkt.Timber | 11 | import com.github.ajalt.timberkt.Timber |
| 8 | -import com.google.android.material.tabs.TabLayoutMediator | ||
| 9 | import com.snakydesign.livedataextensions.combineLatest | 12 | import com.snakydesign.livedataextensions.combineLatest |
| 10 | import com.xwray.groupie.GroupieAdapter | 13 | import com.xwray.groupie.GroupieAdapter |
| 11 | import io.livekit.android.room.track.LocalVideoTrack | 14 | import io.livekit.android.room.track.LocalVideoTrack |
| @@ -20,12 +23,23 @@ class CallActivity : AppCompatActivity() { | @@ -20,12 +23,23 @@ class CallActivity : AppCompatActivity() { | ||
| 20 | CallViewModel(args.url, args.token, application) | 23 | CallViewModel(args.url, args.token, application) |
| 21 | } | 24 | } |
| 22 | lateinit var binding: CallActivityBinding | 25 | lateinit var binding: CallActivityBinding |
| 23 | - var tabLayoutMediator: TabLayoutMediator? = null | ||
| 24 | val focusChangeListener = AudioManager.OnAudioFocusChangeListener {} | 26 | val focusChangeListener = AudioManager.OnAudioFocusChangeListener {} |
| 25 | 27 | ||
| 26 | private var previousSpeakerphoneOn = true | 28 | private var previousSpeakerphoneOn = true |
| 27 | private var previousMicrophoneMute = false | 29 | private var previousMicrophoneMute = false |
| 28 | - | 30 | + |
| 31 | + private val screenCaptureIntentLauncher = | ||
| 32 | + registerForActivityResult( | ||
| 33 | + ActivityResultContracts.StartActivityForResult() | ||
| 34 | + ) { result -> | ||
| 35 | + val resultCode = result.resultCode | ||
| 36 | + val data = result.data | ||
| 37 | + if (resultCode != Activity.RESULT_OK || data == null) { | ||
| 38 | + return@registerForActivityResult | ||
| 39 | + } | ||
| 40 | + viewModel.setScreenshare(true, data) | ||
| 41 | + } | ||
| 42 | + | ||
| 29 | override fun onCreate(savedInstanceState: Bundle?) { | 43 | override fun onCreate(savedInstanceState: Bundle?) { |
| 30 | super.onCreate(savedInstanceState) | 44 | super.onCreate(savedInstanceState) |
| 31 | 45 | ||
| @@ -33,44 +47,71 @@ class CallActivity : AppCompatActivity() { | @@ -33,44 +47,71 @@ class CallActivity : AppCompatActivity() { | ||
| 33 | 47 | ||
| 34 | setContentView(binding.root) | 48 | setContentView(binding.root) |
| 35 | 49 | ||
| 36 | - // Viewpager setup | 50 | + // Audience row setup |
| 51 | + binding.audienceRow.layoutManager = | ||
| 52 | + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) | ||
| 37 | val adapter = GroupieAdapter() | 53 | val adapter = GroupieAdapter() |
| 38 | 54 | ||
| 39 | - binding.viewPager.apply { | 55 | + binding.audienceRow.apply { |
| 40 | this.adapter = adapter | 56 | this.adapter = adapter |
| 41 | } | 57 | } |
| 42 | 58 | ||
| 43 | combineLatest( | 59 | combineLatest( |
| 44 | viewModel.room, | 60 | viewModel.room, |
| 45 | - viewModel.remoteParticipants | 61 | + viewModel.participants |
| 46 | ) { room, participants -> room to participants } | 62 | ) { room, participants -> room to participants } |
| 47 | .observe(this) { | 63 | .observe(this) { |
| 48 | - tabLayoutMediator?.detach() | ||
| 49 | - tabLayoutMediator = null | ||
| 50 | 64 | ||
| 51 | val (room, participants) = it | 65 | val (room, participants) = it |
| 52 | val items = participants.map { participant -> ParticipantItem(room, participant) } | 66 | val items = participants.map { participant -> ParticipantItem(room, participant) } |
| 53 | adapter.update(items) | 67 | adapter.update(items) |
| 54 | - | ||
| 55 | - tabLayoutMediator = | ||
| 56 | - TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position -> | ||
| 57 | - tab.text = participants[position].identity | ||
| 58 | - } | ||
| 59 | - tabLayoutMediator?.attach() | ||
| 60 | } | 68 | } |
| 61 | 69 | ||
| 70 | + // speaker view setup | ||
| 62 | viewModel.room.observe(this) { room -> | 71 | viewModel.room.observe(this) { room -> |
| 63 | - room.initVideoRenderer(binding.pipVideoView) | 72 | + room.initVideoRenderer(binding.speakerView) |
| 64 | val videoTrack = room.localParticipant.videoTracks.values | 73 | val videoTrack = room.localParticipant.videoTracks.values |
| 65 | .firstOrNull() | 74 | .firstOrNull() |
| 66 | ?.track as? LocalVideoTrack | 75 | ?.track as? LocalVideoTrack |
| 67 | 76 | ||
| 68 | videoTrack?.let { | 77 | videoTrack?.let { |
| 69 | - it.addRenderer(binding.pipVideoView) | 78 | + it.addRenderer(binding.speakerView) |
| 70 | } | 79 | } |
| 80 | + } | ||
| 71 | 81 | ||
| 82 | + // Controls setup | ||
| 83 | + viewModel.videoEnabled.observe(this) { enabled -> | ||
| 84 | + binding.camera.setOnClickListener { viewModel.setCameraEnabled(!enabled) } | ||
| 85 | + binding.camera.setImageResource( | ||
| 86 | + if (enabled) R.drawable.outline_videocam_24 | ||
| 87 | + else R.drawable.outline_videocam_off_24 | ||
| 88 | + ) | ||
| 89 | + binding.flipCamera.isEnabled = enabled | ||
| 90 | + } | ||
| 91 | + viewModel.micEnabled.observe(this) { enabled -> | ||
| 92 | + binding.mic.setOnClickListener { viewModel.setMicEnabled(!enabled) } | ||
| 93 | + binding.mic.setImageResource( | ||
| 94 | + if (enabled) R.drawable.outline_mic_24 | ||
| 95 | + else R.drawable.outline_mic_off_24 | ||
| 96 | + ) | ||
| 97 | + } | ||
| 72 | 98 | ||
| 99 | + binding.flipCamera.setOnClickListener { viewModel.flipCamera() } | ||
| 100 | + viewModel.screenshareEnabled.observe(this) { enabled -> | ||
| 101 | + binding.screenShare.setOnClickListener { | ||
| 102 | + if (enabled) { | ||
| 103 | + viewModel.setScreenshare(!enabled) | ||
| 104 | + } else { | ||
| 105 | + requestMediaProjection() | ||
| 106 | + } | ||
| 107 | + } | ||
| 108 | + binding.screenShare.setImageResource( | ||
| 109 | + if (enabled) R.drawable.baseline_cast_connected_24 | ||
| 110 | + else R.drawable.baseline_cast_24 | ||
| 111 | + ) | ||
| 73 | } | 112 | } |
| 113 | + | ||
| 114 | + // Grab audio focus for video call | ||
| 74 | val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager | 115 | val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager |
| 75 | with(audioManager) { | 116 | with(audioManager) { |
| 76 | previousSpeakerphoneOn = isSpeakerphoneOn | 117 | previousSpeakerphoneOn = isSpeakerphoneOn |
| @@ -91,10 +132,19 @@ class CallActivity : AppCompatActivity() { | @@ -91,10 +132,19 @@ class CallActivity : AppCompatActivity() { | ||
| 91 | } | 132 | } |
| 92 | } | 133 | } |
| 93 | 134 | ||
| 135 | + private fun requestMediaProjection() { | ||
| 136 | + val mediaProjectionManager = | ||
| 137 | + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager | ||
| 138 | + screenCaptureIntentLauncher.launch(mediaProjectionManager.createScreenCaptureIntent()) | ||
| 139 | + } | ||
| 140 | + | ||
| 94 | override fun onDestroy() { | 141 | override fun onDestroy() { |
| 95 | super.onDestroy() | 142 | super.onDestroy() |
| 96 | 143 | ||
| 97 | - binding.pipVideoView.release() | 144 | + // Release video views |
| 145 | + binding.speakerView.release() | ||
| 146 | + | ||
| 147 | + // Undo audio mode changes | ||
| 98 | val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager | 148 | val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager |
| 99 | with(audioManager) { | 149 | with(audioManager) { |
| 100 | isSpeakerphoneOn = previousSpeakerphoneOn | 150 | isSpeakerphoneOn = previousSpeakerphoneOn |
| 1 | package io.livekit.android.sample | 1 | package io.livekit.android.sample |
| 2 | 2 | ||
| 3 | import android.app.Application | 3 | import android.app.Application |
| 4 | +import android.content.Intent | ||
| 4 | import androidx.lifecycle.AndroidViewModel | 5 | import androidx.lifecycle.AndroidViewModel |
| 5 | -import androidx.lifecycle.LiveData | ||
| 6 | import androidx.lifecycle.MutableLiveData | 6 | import androidx.lifecycle.MutableLiveData |
| 7 | import androidx.lifecycle.viewModelScope | 7 | import androidx.lifecycle.viewModelScope |
| 8 | -import com.github.ajalt.timberkt.Timber | 8 | +import com.snakydesign.livedataextensions.distinctUntilChanged |
| 9 | import io.livekit.android.ConnectOptions | 9 | import io.livekit.android.ConnectOptions |
| 10 | import io.livekit.android.LiveKit | 10 | import io.livekit.android.LiveKit |
| 11 | +import io.livekit.android.events.RoomEvent | ||
| 12 | +import io.livekit.android.events.collect | ||
| 11 | import io.livekit.android.room.Room | 13 | import io.livekit.android.room.Room |
| 12 | -import io.livekit.android.room.RoomListener | ||
| 13 | import io.livekit.android.room.participant.Participant | 14 | import io.livekit.android.room.participant.Participant |
| 14 | -import io.livekit.android.room.participant.RemoteParticipant | 15 | +import io.livekit.android.room.track.CameraPosition |
| 16 | +import io.livekit.android.room.track.LocalVideoTrack | ||
| 17 | +import io.livekit.android.room.track.Track | ||
| 18 | +import io.livekit.android.sample.util.hide | ||
| 15 | import kotlinx.coroutines.launch | 19 | import kotlinx.coroutines.launch |
| 16 | 20 | ||
| 17 | class CallViewModel( | 21 | class CallViewModel( |
| 18 | val url: String, | 22 | val url: String, |
| 19 | val token: String, | 23 | val token: String, |
| 20 | application: Application | 24 | application: Application |
| 21 | -) : AndroidViewModel(application), RoomListener { | 25 | +) : AndroidViewModel(application) { |
| 22 | private val mutableRoom = MutableLiveData<Room>() | 26 | 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 | 27 | + val room = mutableRoom.hide() |
| 28 | + private val mutableParticipants = MutableLiveData<List<Participant>>() | ||
| 29 | + val participants = mutableParticipants.hide() | ||
| 30 | + private val mutableActiveSpeaker = MutableLiveData<Participant>() | ||
| 31 | + val activeSpeaker = mutableActiveSpeaker.hide().distinctUntilChanged() | ||
| 32 | + | ||
| 33 | + private val mutableVideoEnabled = MutableLiveData<Boolean>() | ||
| 34 | + val videoEnabled = mutableVideoEnabled.hide().distinctUntilChanged() | ||
| 35 | + private val mutableMicEnabled = MutableLiveData<Boolean>() | ||
| 36 | + val micEnabled = mutableMicEnabled.hide().distinctUntilChanged() | ||
| 37 | + private val mutableScreenshareEnabled = MutableLiveData<Boolean>() | ||
| 38 | + val screenshareEnabled = mutableScreenshareEnabled.hide().distinctUntilChanged() | ||
| 26 | 39 | ||
| 27 | init { | 40 | init { |
| 28 | viewModelScope.launch { | 41 | viewModelScope.launch { |
| @@ -31,9 +44,15 @@ class CallViewModel( | @@ -31,9 +44,15 @@ class CallViewModel( | ||
| 31 | url, | 44 | url, |
| 32 | token, | 45 | token, |
| 33 | ConnectOptions(), | 46 | ConnectOptions(), |
| 34 | - this@CallViewModel | 47 | + null |
| 35 | ) | 48 | ) |
| 36 | 49 | ||
| 50 | + launch { | ||
| 51 | + room.events.collect { | ||
| 52 | + handleRoomEvent(it) | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + | ||
| 37 | val localParticipant = room.localParticipant | 56 | val localParticipant = room.localParticipant |
| 38 | val audioTrack = localParticipant.createAudioTrack() | 57 | val audioTrack = localParticipant.createAudioTrack() |
| 39 | localParticipant.publishAudioTrack(audioTrack) | 58 | localParticipant.publishAudioTrack(audioTrack) |
| @@ -42,49 +61,95 @@ class CallViewModel( | @@ -42,49 +61,95 @@ class CallViewModel( | ||
| 42 | videoTrack.startCapture() | 61 | videoTrack.startCapture() |
| 43 | 62 | ||
| 44 | updateParticipants(room) | 63 | updateParticipants(room) |
| 64 | + mutableActiveSpeaker.value = localParticipant | ||
| 45 | mutableRoom.value = room | 65 | mutableRoom.value = room |
| 66 | + | ||
| 67 | + mutableVideoEnabled.value = | ||
| 68 | + !(localParticipant.getTrackPublication(Track.Source.CAMERA)?.muted ?: false) | ||
| 69 | + mutableMicEnabled.value = | ||
| 70 | + !(localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.muted ?: false) | ||
| 71 | + mutableScreenshareEnabled.value = false | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + private fun handleRoomEvent(event: RoomEvent) { | ||
| 76 | + when (event) { | ||
| 77 | + is RoomEvent.ParticipantConnected -> updateParticipants(event.room) | ||
| 78 | + is RoomEvent.ParticipantDisconnected -> updateParticipants(event.room) | ||
| 79 | + is RoomEvent.ActiveSpeakersChanged -> handleActiveSpeakersChanged(event.speakers) | ||
| 46 | } | 80 | } |
| 47 | } | 81 | } |
| 48 | 82 | ||
| 49 | private fun updateParticipants(room: Room) { | 83 | private fun updateParticipants(room: Room) { |
| 50 | - mutableRemoteParticipants.postValue( | ||
| 51 | - room.remoteParticipants | ||
| 52 | - .keys | ||
| 53 | - .sortedBy { it } | ||
| 54 | - .mapNotNull { room.remoteParticipants[it] } | 84 | + mutableParticipants.postValue( |
| 85 | + listOf(room.localParticipant) + | ||
| 86 | + room.remoteParticipants | ||
| 87 | + .keys | ||
| 88 | + .sortedBy { it } | ||
| 89 | + .mapNotNull { room.remoteParticipants[it] } | ||
| 55 | ) | 90 | ) |
| 56 | } | 91 | } |
| 57 | 92 | ||
| 93 | + fun handleActiveSpeakersChanged(speakers: List<Participant>) { | ||
| 94 | + // If old active speaker is still active, don't change. | ||
| 95 | + if (speakers.isEmpty() || speakers.contains(mutableActiveSpeaker.value)) { | ||
| 96 | + return | ||
| 97 | + } | ||
| 98 | + val newSpeaker = speakers.firstOrNull() ?: return | ||
| 99 | + mutableActiveSpeaker.postValue(newSpeaker) | ||
| 100 | + } | ||
| 101 | + | ||
| 58 | override fun onCleared() { | 102 | override fun onCleared() { |
| 59 | super.onCleared() | 103 | super.onCleared() |
| 60 | mutableRoom.value?.disconnect() | 104 | mutableRoom.value?.disconnect() |
| 61 | } | 105 | } |
| 62 | 106 | ||
| 63 | - override fun onDisconnect(room: Room, error: Exception?) { | 107 | + fun setCameraEnabled(enabled: Boolean) { |
| 108 | + val localParticipant = room.value?.localParticipant ?: return | ||
| 109 | + | ||
| 110 | + viewModelScope.launch { | ||
| 111 | + localParticipant.setCameraEnabled(enabled) | ||
| 112 | + mutableVideoEnabled.postValue(enabled) | ||
| 113 | + } | ||
| 64 | } | 114 | } |
| 65 | 115 | ||
| 66 | - override fun onParticipantConnected( | ||
| 67 | - room: Room, | ||
| 68 | - participant: RemoteParticipant | ||
| 69 | - ) { | ||
| 70 | - updateParticipants(room) | 116 | + fun setMicEnabled(enabled: Boolean) { |
| 117 | + val localParticipant = room.value?.localParticipant ?: return | ||
| 118 | + | ||
| 119 | + viewModelScope.launch { | ||
| 120 | + localParticipant.setMicrophoneEnabled(enabled) | ||
| 121 | + mutableMicEnabled.postValue(enabled) | ||
| 122 | + } | ||
| 71 | } | 123 | } |
| 72 | 124 | ||
| 73 | - override fun onParticipantDisconnected( | ||
| 74 | - room: Room, | ||
| 75 | - participant: RemoteParticipant | 125 | + fun setScreenshare( |
| 126 | + enabled: Boolean, | ||
| 127 | + mediaProjectionPermissionResultData: Intent? = null | ||
| 76 | ) { | 128 | ) { |
| 77 | - updateParticipants(room) | ||
| 78 | - } | 129 | + val localParticipant = room.value?.localParticipant ?: return |
| 79 | 130 | ||
| 80 | - override fun onFailedToConnect(room: Room, error: Exception) { | 131 | + viewModelScope.launch { |
| 132 | + localParticipant.setScreenShareEnabled(enabled, mediaProjectionPermissionResultData) | ||
| 133 | + mutableScreenshareEnabled.postValue(enabled) | ||
| 134 | + } | ||
| 81 | } | 135 | } |
| 82 | 136 | ||
| 83 | - override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) { | ||
| 84 | - Timber.i { "active speakers changed ${speakers.count()}" } | ||
| 85 | - } | 137 | + fun flipCamera() { |
| 138 | + val localParticipant = room.value?.localParticipant ?: return | ||
| 139 | + val localVideoTrack = localParticipant | ||
| 140 | + .getTrackPublication(Track.Source.CAMERA) | ||
| 141 | + ?.track as? LocalVideoTrack | ||
| 142 | + ?: return | ||
| 86 | 143 | ||
| 87 | - override fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) { | ||
| 88 | - Timber.i { "Participant metadata changed: ${participant.identity}" } | 144 | + val currentOptions = localVideoTrack.options |
| 145 | + val newPosition = when (currentOptions.position) { | ||
| 146 | + CameraPosition.FRONT -> CameraPosition.BACK | ||
| 147 | + CameraPosition.BACK -> CameraPosition.FRONT | ||
| 148 | + null -> null | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + if (newPosition != null) { | ||
| 152 | + localVideoTrack.restartTrack(options = currentOptions.copy(position = newPosition)) | ||
| 153 | + } | ||
| 89 | } | 154 | } |
| 90 | } | 155 | } |
| @@ -5,6 +5,7 @@ import com.github.ajalt.timberkt.Timber | @@ -5,6 +5,7 @@ import com.github.ajalt.timberkt.Timber | ||
| 5 | import com.xwray.groupie.viewbinding.BindableItem | 5 | import com.xwray.groupie.viewbinding.BindableItem |
| 6 | import com.xwray.groupie.viewbinding.GroupieViewHolder | 6 | import com.xwray.groupie.viewbinding.GroupieViewHolder |
| 7 | import io.livekit.android.room.Room | 7 | import io.livekit.android.room.Room |
| 8 | +import io.livekit.android.room.participant.Participant | ||
| 8 | import io.livekit.android.room.participant.ParticipantListener | 9 | import io.livekit.android.room.participant.ParticipantListener |
| 9 | import io.livekit.android.room.participant.RemoteParticipant | 10 | import io.livekit.android.room.participant.RemoteParticipant |
| 10 | import io.livekit.android.room.track.* | 11 | import io.livekit.android.room.track.* |
| @@ -12,7 +13,7 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding | @@ -12,7 +13,7 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding | ||
| 12 | 13 | ||
| 13 | class ParticipantItem( | 14 | class ParticipantItem( |
| 14 | val room: Room, | 15 | val room: Room, |
| 15 | - val remoteParticipant: RemoteParticipant | 16 | + val participant: Participant |
| 16 | ) : | 17 | ) : |
| 17 | BindableItem<ParticipantItemBinding>() { | 18 | BindableItem<ParticipantItemBinding>() { |
| 18 | 19 | ||
| @@ -27,13 +28,14 @@ class ParticipantItem( | @@ -27,13 +28,14 @@ class ParticipantItem( | ||
| 27 | override fun bind(viewBinding: ParticipantItemBinding, position: Int) { | 28 | override fun bind(viewBinding: ParticipantItemBinding, position: Int) { |
| 28 | viewBinding.run { | 29 | viewBinding.run { |
| 29 | 30 | ||
| 30 | - remoteParticipant.listener = object : ParticipantListener { | 31 | + participant.listener = object : ParticipantListener { |
| 31 | override fun onTrackSubscribed( | 32 | override fun onTrackSubscribed( |
| 32 | track: Track, | 33 | track: Track, |
| 33 | publication: RemoteTrackPublication, | 34 | publication: RemoteTrackPublication, |
| 34 | participant: RemoteParticipant | 35 | participant: RemoteParticipant |
| 35 | ) { | 36 | ) { |
| 36 | - if (track is VideoTrack) { | 37 | + if (track !is VideoTrack) return |
| 38 | + if (publication.source == Track.Source.CAMERA) { | ||
| 37 | setupVideoIfNeeded(track, viewBinding) | 39 | setupVideoIfNeeded(track, viewBinding) |
| 38 | } | 40 | } |
| 39 | } | 41 | } |
| @@ -54,9 +56,7 @@ class ParticipantItem( | @@ -54,9 +56,7 @@ class ParticipantItem( | ||
| 54 | } | 56 | } |
| 55 | 57 | ||
| 56 | private fun getVideoTrack(): VideoTrack? { | 58 | private fun getVideoTrack(): VideoTrack? { |
| 57 | - return remoteParticipant | ||
| 58 | - .videoTracks.values | ||
| 59 | - .firstOrNull()?.track as? VideoTrack | 59 | + return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack |
| 60 | } | 60 | } |
| 61 | 61 | ||
| 62 | internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) { | 62 | internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) { |
| 1 | -<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | - android:width="24dp" | ||
| 3 | - android:height="24dp" | ||
| 4 | - android:viewportWidth="24" | ||
| 5 | - android:viewportHeight="24" | ||
| 6 | - android:tint="?attr/colorControlNormal"> | ||
| 7 | - <path | ||
| 8 | - android:fillColor="@android:color/white" | ||
| 9 | - android:pathData="M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.91,11c-0.49,0 -0.9,0.36 -0.98,0.85C16.52,14.2 14.47,16 12,16s-4.52,-1.8 -4.93,-4.15c-0.08,-0.49 -0.49,-0.85 -0.98,-0.85 -0.61,0 -1.09,0.54 -1,1.14 0.49,3 2.89,5.35 5.91,5.78L11,20c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2.08c3.02,-0.43 5.42,-2.78 5.91,-5.78 0.1,-0.6 -0.39,-1.14 -1,-1.14z"/> | ||
| 10 | -</vector> |
| 1 | -<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | - android:width="24dp" | ||
| 3 | - android:height="24dp" | ||
| 4 | - android:viewportWidth="24" | ||
| 5 | - android:viewportHeight="24" | ||
| 6 | - android:tint="?attr/colorControlNormal"> | ||
| 7 | - <path | ||
| 8 | - android:fillColor="@android:color/white" | ||
| 9 | - android:pathData="M15,10.6L15,5c0,-1.66 -1.34,-3 -3,-3 -1.54,0 -2.79,1.16 -2.96,2.65L15,10.6zM18.08,11c-0.41,0 -0.77,0.3 -0.83,0.71 -0.05,0.32 -0.12,0.64 -0.22,0.93l1.27,1.27c0.3,-0.6 0.52,-1.25 0.63,-1.94 0.07,-0.51 -0.33,-0.97 -0.85,-0.97zM3.71,3.56c-0.39,0.39 -0.39,1.02 0,1.41L9,10.27v0.43c0,1.19 0.6,2.32 1.63,2.91 0.75,0.43 1.41,0.44 2.02,0.31l1.66,1.66c-0.71,0.33 -1.5,0.52 -2.31,0.52 -2.54,0 -4.88,-1.77 -5.25,-4.39 -0.06,-0.41 -0.42,-0.71 -0.83,-0.71 -0.52,0 -0.92,0.46 -0.85,0.97 0.46,2.96 2.96,5.3 5.93,5.75L11,20c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-2.28c0.91,-0.13 1.77,-0.45 2.55,-0.9l3.49,3.49c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L5.12,3.56c-0.39,-0.39 -1.02,-0.39 -1.41,0z"/> | ||
| 10 | -</vector> |
| 1 | -<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | - android:width="24dp" | ||
| 3 | - android:height="24dp" | ||
| 4 | - android:viewportWidth="24" | ||
| 5 | - android:viewportHeight="24" | ||
| 6 | - android:tint="?attr/colorControlNormal"> | ||
| 7 | - <path | ||
| 8 | - android:fillColor="@android:color/white" | ||
| 9 | - android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l2.29,2.29c0.63,0.63 1.71,0.18 1.71,-0.71V8.91c0,-0.89 -1.08,-1.34 -1.71,-0.71L17,10.5z"/> | ||
| 10 | -</vector> |
| 1 | -<vector xmlns:android="http://schemas.android.com/apk/res/android" | ||
| 2 | - android:width="24dp" | ||
| 3 | - android:height="24dp" | ||
| 4 | - android:viewportWidth="24" | ||
| 5 | - android:viewportHeight="24" | ||
| 6 | - android:tint="?attr/colorControlNormal"> | ||
| 7 | - <path | ||
| 8 | - android:fillColor="@android:color/white" | ||
| 9 | - android:pathData="M21,14.2V8.91c0,-0.89 -1.08,-1.34 -1.71,-0.71L17,10.5V7c0,-0.55 -0.45,-1 -1,-1h-5.61l8.91,8.91c0.62,0.63 1.7,0.18 1.7,-0.71zM2.71,2.56c-0.39,0.39 -0.39,1.02 0,1.41L4.73,6H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.21,0 0.39,-0.08 0.55,-0.18l2.48,2.48c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L4.12,2.56c-0.39,-0.39 -1.02,-0.39 -1.41,0z"/> | ||
| 10 | -</vector> |
| @@ -4,58 +4,59 @@ | @@ -4,58 +4,59 @@ | ||
| 4 | android:layout_height="match_parent" | 4 | android:layout_height="match_parent" |
| 5 | android:keepScreenOn="true"> | 5 | android:keepScreenOn="true"> |
| 6 | 6 | ||
| 7 | - <com.google.android.material.tabs.TabLayout | ||
| 8 | - android:id="@+id/tabs" | ||
| 9 | - android:layout_width="match_parent" | ||
| 10 | - android:layout_height="48dp" | 7 | + <io.livekit.android.renderer.TextureViewRenderer |
| 8 | + android:id="@+id/speaker_view" | ||
| 9 | + android:layout_width="0dp" | ||
| 10 | + android:layout_height="0dp" | ||
| 11 | + app:layout_constraintBottom_toTopOf="@id/audience_row" | ||
| 11 | app:layout_constraintTop_toTopOf="parent" | 12 | app:layout_constraintTop_toTopOf="parent" |
| 12 | - app:tabMode="auto" /> | 13 | + app:layout_constraintEnd_toEndOf="parent" |
| 14 | + app:layout_constraintStart_toStartOf="parent" /> | ||
| 13 | 15 | ||
| 14 | - <androidx.viewpager2.widget.ViewPager2 | ||
| 15 | - android:id="@+id/view_pager" | 16 | + <androidx.recyclerview.widget.RecyclerView |
| 17 | + android:id="@+id/audience_row" | ||
| 16 | android:layout_width="match_parent" | 18 | android:layout_width="match_parent" |
| 17 | - android:layout_height="0dp" | ||
| 18 | - app:layout_constraintBottom_toBottomOf="parent" | ||
| 19 | - app:layout_constraintTop_toBottomOf="@id/tabs" /> | ||
| 20 | - | ||
| 21 | - <androidx.constraintlayout.widget.ConstraintLayout | ||
| 22 | - android:layout_width="200dp" | ||
| 23 | - android:layout_height="200dp" | ||
| 24 | - app:layout_constraintBottom_toBottomOf="parent" | ||
| 25 | - app:layout_constraintEnd_toEndOf="parent"> | ||
| 26 | - | ||
| 27 | - <io.livekit.android.renderer.TextureViewRenderer | ||
| 28 | - android:id="@+id/pip_video_view" | ||
| 29 | - android:layout_width="match_parent" | ||
| 30 | - android:layout_height="match_parent" | ||
| 31 | - android:layout_margin="16dp" /> | ||
| 32 | - | ||
| 33 | - <LinearLayout | ||
| 34 | - android:layout_width="wrap_content" | ||
| 35 | - android:layout_height="wrap_content" | ||
| 36 | - android:layout_margin="30dp" | ||
| 37 | - android:orientation="horizontal" | ||
| 38 | - app:layout_constraintBottom_toBottomOf="parent" | ||
| 39 | - app:layout_constraintEnd_toEndOf="parent" | ||
| 40 | - app:layout_constraintStart_toStartOf="parent"> | 19 | + android:layout_height="120dp" |
| 20 | + app:layout_constraintBottom_toTopOf="@id/controls_box" /> | ||
| 41 | 21 | ||
| 42 | - <ImageButton | ||
| 43 | - android:id="@+id/mic_button" | ||
| 44 | - android:layout_width="48dp" | ||
| 45 | - android:layout_height="48dp" | ||
| 46 | - android:src="@drawable/ic_round_mic_24" /> | ||
| 47 | - | ||
| 48 | - <Space | ||
| 49 | - android:layout_width="20dp" | ||
| 50 | - android:layout_height="1dp" /> | ||
| 51 | - | ||
| 52 | - <ImageButton | ||
| 53 | - android:id="@+id/video_button" | ||
| 54 | - android:layout_width="48dp" | ||
| 55 | - android:layout_height="48dp" | ||
| 56 | - android:src="@drawable/ic_round_videocam_24" /> | ||
| 57 | - | ||
| 58 | - </LinearLayout> | ||
| 59 | - | ||
| 60 | - </androidx.constraintlayout.widget.ConstraintLayout> | 22 | + <LinearLayout |
| 23 | + android:id="@+id/controls_box" | ||
| 24 | + android:layout_width="match_parent" | ||
| 25 | + android:layout_height="60dp" | ||
| 26 | + android:gravity="center" | ||
| 27 | + android:orientation="horizontal" | ||
| 28 | + app:layout_constraintBottom_toBottomOf="parent"> | ||
| 29 | + | ||
| 30 | + <ImageView | ||
| 31 | + android:id="@+id/camera" | ||
| 32 | + android:layout_width="@dimen/control_size" | ||
| 33 | + android:layout_height="@dimen/control_size" | ||
| 34 | + android:background="?android:attr/selectableItemBackground" | ||
| 35 | + android:padding="@dimen/control_padding" | ||
| 36 | + android:src="@drawable/outline_videocam_24" /> | ||
| 37 | + | ||
| 38 | + <ImageView | ||
| 39 | + android:id="@+id/mic" | ||
| 40 | + android:layout_width="@dimen/control_size" | ||
| 41 | + android:layout_height="@dimen/control_size" | ||
| 42 | + android:background="?android:attr/selectableItemBackground" | ||
| 43 | + android:padding="@dimen/control_padding" | ||
| 44 | + android:src="@drawable/outline_mic_24" /> | ||
| 45 | + | ||
| 46 | + <ImageView | ||
| 47 | + android:id="@+id/flip_camera" | ||
| 48 | + android:layout_width="@dimen/control_size" | ||
| 49 | + android:layout_height="@dimen/control_size" | ||
| 50 | + android:background="?android:attr/selectableItemBackground" | ||
| 51 | + android:padding="@dimen/control_padding" | ||
| 52 | + android:src="@drawable/outline_flip_camera_android_24" /> | ||
| 53 | + | ||
| 54 | + <ImageView | ||
| 55 | + android:id="@+id/screen_share" | ||
| 56 | + android:layout_width="@dimen/control_size" | ||
| 57 | + android:layout_height="@dimen/control_size" | ||
| 58 | + android:background="?android:attr/selectableItemBackground" | ||
| 59 | + android:padding="@dimen/control_padding" | ||
| 60 | + android:src="@drawable/baseline_cast_24" /> | ||
| 61 | + </LinearLayout> | ||
| 61 | </androidx.constraintlayout.widget.ConstraintLayout> | 62 | </androidx.constraintlayout.widget.ConstraintLayout> |
| 1 | <?xml version="1.0" encoding="utf-8"?> | 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" | 2 | +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" |
| 3 | + xmlns:app="http://schemas.android.com/apk/res-auto" | ||
| 4 | + android:layout_width="wrap_content" | ||
| 4 | android:layout_height="match_parent"> | 5 | android:layout_height="match_parent"> |
| 5 | 6 | ||
| 6 | <io.livekit.android.renderer.TextureViewRenderer | 7 | <io.livekit.android.renderer.TextureViewRenderer |
| 7 | android:id="@+id/renderer" | 8 | android:id="@+id/renderer" |
| 8 | - android:layout_width="match_parent" | ||
| 9 | - android:layout_height="match_parent" /> | ||
| 10 | -</FrameLayout> | ||
| 9 | + android:layout_width="0dp" | ||
| 10 | + android:layout_height="match_parent" | ||
| 11 | + app:layout_constraintDimensionRatio="1:1" | ||
| 12 | + app:layout_constraintStart_toStartOf="parent" | ||
| 13 | + app:layout_constraintTop_toTopOf="parent" /> | ||
| 14 | +</androidx.constraintlayout.widget.ConstraintLayout> |
sample-app/src/main/res/values/dimens.xml
0 → 100644
| @@ -6,7 +6,7 @@ | @@ -6,7 +6,7 @@ | ||
| 6 | <item name="colorPrimary">@color/colorPrimary</item> | 6 | <item name="colorPrimary">@color/colorPrimary</item> |
| 7 | <item name="colorPrimaryDark">@color/colorPrimaryDark</item> | 7 | <item name="colorPrimaryDark">@color/colorPrimaryDark</item> |
| 8 | <item name="colorAccent">@color/colorAccent</item> | 8 | <item name="colorAccent">@color/colorAccent</item> |
| 9 | - <item name="android:windowBackground">#000000</item> | 9 | + <item name="android:windowBackground">@android:color/black</item> |
| 10 | </style> | 10 | </style> |
| 11 | 11 | ||
| 12 | </resources> | 12 | </resources> |
-
请 注册 或 登录 后发表评论