David Liu

sample app redesign

正在显示 20 个修改的文件 包含 253 行增加154 行删除
... ... @@ -108,16 +108,24 @@ internal constructor(
return super.getTrackPublicationByName(name) as? LocalTrackPublication
}
suspend fun setCameraEnabled(enabled: Boolean){
suspend fun setCameraEnabled(enabled: Boolean) {
setTrackEnabled(Track.Source.CAMERA, enabled)
}
suspend fun setMicrophoneEnabled(enabled: Boolean){
suspend fun setMicrophoneEnabled(enabled: Boolean) {
setTrackEnabled(Track.Source.MICROPHONE, enabled)
}
suspend fun setScreenShareEnabled(enabled: Boolean) {
setTrackEnabled(Track.Source.SCREEN_SHARE, enabled)
/**
* @param mediaProjectionPermissionResultData The resultData returned from launching
* [MediaProjectionManager.createScreenCaptureIntent()](https://developer.android.com/reference/android/media/projection/MediaProjectionManager#createScreenCaptureIntent()).
* @throws IllegalArgumentException if attempting to enable screenshare without [mediaProjectionPermissionResultData]
*/
suspend fun setScreenShareEnabled(
enabled: Boolean,
mediaProjectionPermissionResultData: Intent? = null
) {
setTrackEnabled(Track.Source.SCREEN_SHARE, enabled, mediaProjectionPermissionResultData)
}
private suspend fun setTrackEnabled(
... ...
package io.livekit.android.sample.util
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
fun <T> MutableLiveData<T>.hide(): LiveData<T> = this
\ No newline at end of file
... ...
package io.livekit.android.sample
import android.app.Activity
import android.media.AudioManager
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.ajalt.timberkt.Timber
import com.google.android.material.tabs.TabLayoutMediator
import com.snakydesign.livedataextensions.combineLatest
import com.xwray.groupie.GroupieAdapter
import io.livekit.android.room.track.LocalVideoTrack
... ... @@ -20,12 +23,23 @@ class CallActivity : AppCompatActivity() {
CallViewModel(args.url, args.token, application)
}
lateinit var binding: CallActivityBinding
var tabLayoutMediator: TabLayoutMediator? = null
val focusChangeListener = AudioManager.OnAudioFocusChangeListener {}
private var previousSpeakerphoneOn = true
private var previousMicrophoneMute = false
private val screenCaptureIntentLauncher =
registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val resultCode = result.resultCode
val data = result.data
if (resultCode != Activity.RESULT_OK || data == null) {
return@registerForActivityResult
}
viewModel.setScreenshare(true, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
... ... @@ -33,44 +47,71 @@ class CallActivity : AppCompatActivity() {
setContentView(binding.root)
// Viewpager setup
// Audience row setup
binding.audienceRow.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
val adapter = GroupieAdapter()
binding.viewPager.apply {
binding.audienceRow.apply {
this.adapter = adapter
}
combineLatest(
viewModel.room,
viewModel.remoteParticipants
viewModel.participants
) { room, participants -> room to participants }
.observe(this) {
tabLayoutMediator?.detach()
tabLayoutMediator = null
val (room, participants) = it
val items = participants.map { participant -> ParticipantItem(room, participant) }
adapter.update(items)
tabLayoutMediator =
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
tab.text = participants[position].identity
}
tabLayoutMediator?.attach()
}
// speaker view setup
viewModel.room.observe(this) { room ->
room.initVideoRenderer(binding.pipVideoView)
room.initVideoRenderer(binding.speakerView)
val videoTrack = room.localParticipant.videoTracks.values
.firstOrNull()
?.track as? LocalVideoTrack
videoTrack?.let {
it.addRenderer(binding.pipVideoView)
it.addRenderer(binding.speakerView)
}
}
// Controls setup
viewModel.videoEnabled.observe(this) { enabled ->
binding.camera.setOnClickListener { viewModel.setCameraEnabled(!enabled) }
binding.camera.setImageResource(
if (enabled) R.drawable.outline_videocam_24
else R.drawable.outline_videocam_off_24
)
binding.flipCamera.isEnabled = enabled
}
viewModel.micEnabled.observe(this) { enabled ->
binding.mic.setOnClickListener { viewModel.setMicEnabled(!enabled) }
binding.mic.setImageResource(
if (enabled) R.drawable.outline_mic_24
else R.drawable.outline_mic_off_24
)
}
binding.flipCamera.setOnClickListener { viewModel.flipCamera() }
viewModel.screenshareEnabled.observe(this) { enabled ->
binding.screenShare.setOnClickListener {
if (enabled) {
viewModel.setScreenshare(!enabled)
} else {
requestMediaProjection()
}
}
binding.screenShare.setImageResource(
if (enabled) R.drawable.baseline_cast_connected_24
else R.drawable.baseline_cast_24
)
}
// Grab audio focus for video call
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
with(audioManager) {
previousSpeakerphoneOn = isSpeakerphoneOn
... ... @@ -91,10 +132,19 @@ class CallActivity : AppCompatActivity() {
}
}
private fun requestMediaProjection() {
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
screenCaptureIntentLauncher.launch(mediaProjectionManager.createScreenCaptureIntent())
}
override fun onDestroy() {
super.onDestroy()
binding.pipVideoView.release()
// Release video views
binding.speakerView.release()
// Undo audio mode changes
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
with(audioManager) {
isSpeakerphoneOn = previousSpeakerphoneOn
... ...
package io.livekit.android.sample
import android.app.Application
import android.content.Intent
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.github.ajalt.timberkt.Timber
import com.snakydesign.livedataextensions.distinctUntilChanged
import io.livekit.android.ConnectOptions
import io.livekit.android.LiveKit
import io.livekit.android.events.RoomEvent
import io.livekit.android.events.collect
import io.livekit.android.room.Room
import io.livekit.android.room.RoomListener
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.CameraPosition
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import io.livekit.android.sample.util.hide
import kotlinx.coroutines.launch
class CallViewModel(
val url: String,
val token: String,
application: Application
) : AndroidViewModel(application), RoomListener {
) : AndroidViewModel(application) {
private val mutableRoom = MutableLiveData<Room>()
val room: LiveData<Room> = mutableRoom
private val mutableRemoteParticipants = MutableLiveData<List<RemoteParticipant>>()
val remoteParticipants: LiveData<List<RemoteParticipant>> = mutableRemoteParticipants
val room = mutableRoom.hide()
private val mutableParticipants = MutableLiveData<List<Participant>>()
val participants = mutableParticipants.hide()
private val mutableActiveSpeaker = MutableLiveData<Participant>()
val activeSpeaker = mutableActiveSpeaker.hide().distinctUntilChanged()
private val mutableVideoEnabled = MutableLiveData<Boolean>()
val videoEnabled = mutableVideoEnabled.hide().distinctUntilChanged()
private val mutableMicEnabled = MutableLiveData<Boolean>()
val micEnabled = mutableMicEnabled.hide().distinctUntilChanged()
private val mutableScreenshareEnabled = MutableLiveData<Boolean>()
val screenshareEnabled = mutableScreenshareEnabled.hide().distinctUntilChanged()
init {
viewModelScope.launch {
... ... @@ -31,9 +44,15 @@ class CallViewModel(
url,
token,
ConnectOptions(),
this@CallViewModel
null
)
launch {
room.events.collect {
handleRoomEvent(it)
}
}
val localParticipant = room.localParticipant
val audioTrack = localParticipant.createAudioTrack()
localParticipant.publishAudioTrack(audioTrack)
... ... @@ -42,49 +61,95 @@ class CallViewModel(
videoTrack.startCapture()
updateParticipants(room)
mutableActiveSpeaker.value = localParticipant
mutableRoom.value = room
mutableVideoEnabled.value =
!(localParticipant.getTrackPublication(Track.Source.CAMERA)?.muted ?: false)
mutableMicEnabled.value =
!(localParticipant.getTrackPublication(Track.Source.MICROPHONE)?.muted ?: false)
mutableScreenshareEnabled.value = false
}
}
private fun handleRoomEvent(event: RoomEvent) {
when (event) {
is RoomEvent.ParticipantConnected -> updateParticipants(event.room)
is RoomEvent.ParticipantDisconnected -> updateParticipants(event.room)
is RoomEvent.ActiveSpeakersChanged -> handleActiveSpeakersChanged(event.speakers)
}
}
private fun updateParticipants(room: Room) {
mutableRemoteParticipants.postValue(
room.remoteParticipants
.keys
.sortedBy { it }
.mapNotNull { room.remoteParticipants[it] }
mutableParticipants.postValue(
listOf(room.localParticipant) +
room.remoteParticipants
.keys
.sortedBy { it }
.mapNotNull { room.remoteParticipants[it] }
)
}
fun handleActiveSpeakersChanged(speakers: List<Participant>) {
// If old active speaker is still active, don't change.
if (speakers.isEmpty() || speakers.contains(mutableActiveSpeaker.value)) {
return
}
val newSpeaker = speakers.firstOrNull() ?: return
mutableActiveSpeaker.postValue(newSpeaker)
}
override fun onCleared() {
super.onCleared()
mutableRoom.value?.disconnect()
}
override fun onDisconnect(room: Room, error: Exception?) {
fun setCameraEnabled(enabled: Boolean) {
val localParticipant = room.value?.localParticipant ?: return
viewModelScope.launch {
localParticipant.setCameraEnabled(enabled)
mutableVideoEnabled.postValue(enabled)
}
}
override fun onParticipantConnected(
room: Room,
participant: RemoteParticipant
) {
updateParticipants(room)
fun setMicEnabled(enabled: Boolean) {
val localParticipant = room.value?.localParticipant ?: return
viewModelScope.launch {
localParticipant.setMicrophoneEnabled(enabled)
mutableMicEnabled.postValue(enabled)
}
}
override fun onParticipantDisconnected(
room: Room,
participant: RemoteParticipant
fun setScreenshare(
enabled: Boolean,
mediaProjectionPermissionResultData: Intent? = null
) {
updateParticipants(room)
}
val localParticipant = room.value?.localParticipant ?: return
override fun onFailedToConnect(room: Room, error: Exception) {
viewModelScope.launch {
localParticipant.setScreenShareEnabled(enabled, mediaProjectionPermissionResultData)
mutableScreenshareEnabled.postValue(enabled)
}
}
override fun onActiveSpeakersChanged(speakers: List<Participant>, room: Room) {
Timber.i { "active speakers changed ${speakers.count()}" }
}
fun flipCamera() {
val localParticipant = room.value?.localParticipant ?: return
val localVideoTrack = localParticipant
.getTrackPublication(Track.Source.CAMERA)
?.track as? LocalVideoTrack
?: return
override fun onMetadataChanged(participant: Participant, prevMetadata: String?, room: Room) {
Timber.i { "Participant metadata changed: ${participant.identity}" }
val currentOptions = localVideoTrack.options
val newPosition = when (currentOptions.position) {
CameraPosition.FRONT -> CameraPosition.BACK
CameraPosition.BACK -> CameraPosition.FRONT
null -> null
}
if (newPosition != null) {
localVideoTrack.restartTrack(options = currentOptions.copy(position = newPosition))
}
}
}
... ...
... ... @@ -5,6 +5,7 @@ import com.github.ajalt.timberkt.Timber
import com.xwray.groupie.viewbinding.BindableItem
import com.xwray.groupie.viewbinding.GroupieViewHolder
import io.livekit.android.room.Room
import io.livekit.android.room.participant.Participant
import io.livekit.android.room.participant.ParticipantListener
import io.livekit.android.room.participant.RemoteParticipant
import io.livekit.android.room.track.*
... ... @@ -12,7 +13,7 @@ import io.livekit.android.sample.databinding.ParticipantItemBinding
class ParticipantItem(
val room: Room,
val remoteParticipant: RemoteParticipant
val participant: Participant
) :
BindableItem<ParticipantItemBinding>() {
... ... @@ -27,13 +28,14 @@ class ParticipantItem(
override fun bind(viewBinding: ParticipantItemBinding, position: Int) {
viewBinding.run {
remoteParticipant.listener = object : ParticipantListener {
participant.listener = object : ParticipantListener {
override fun onTrackSubscribed(
track: Track,
publication: RemoteTrackPublication,
participant: RemoteParticipant
) {
if (track is VideoTrack) {
if (track !is VideoTrack) return
if (publication.source == Track.Source.CAMERA) {
setupVideoIfNeeded(track, viewBinding)
}
}
... ... @@ -54,9 +56,7 @@ class ParticipantItem(
}
private fun getVideoTrack(): VideoTrack? {
return remoteParticipant
.videoTracks.values
.firstOrNull()?.track as? VideoTrack
return participant.getTrackPublication(Track.Source.CAMERA)?.track as? VideoTrack
}
internal fun setupVideoIfNeeded(videoTrack: VideoTrack, viewBinding: ParticipantItemBinding) {
... ...
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
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"/>
</vector>
... ... @@ -4,58 +4,59 @@
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="48dp"
<io.livekit.android.renderer.TextureViewRenderer
android:id="@+id/speaker_view"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/audience_row"
app:layout_constraintTop_toTopOf="parent"
app:tabMode="auto" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/audience_row"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/tabs" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="200dp"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<io.livekit.android.renderer.TextureViewRenderer
android:id="@+id/pip_video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="16dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="30dp"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
android:layout_height="120dp"
app:layout_constraintBottom_toTopOf="@id/controls_box" />
<ImageButton
android:id="@+id/mic_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_round_mic_24" />
<Space
android:layout_width="20dp"
android:layout_height="1dp" />
<ImageButton
android:id="@+id/video_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@drawable/ic_round_videocam_24" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:id="@+id/controls_box"
android:layout_width="match_parent"
android:layout_height="60dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">
<ImageView
android:id="@+id/camera"
android:layout_width="@dimen/control_size"
android:layout_height="@dimen/control_size"
android:background="?android:attr/selectableItemBackground"
android:padding="@dimen/control_padding"
android:src="@drawable/outline_videocam_24" />
<ImageView
android:id="@+id/mic"
android:layout_width="@dimen/control_size"
android:layout_height="@dimen/control_size"
android:background="?android:attr/selectableItemBackground"
android:padding="@dimen/control_padding"
android:src="@drawable/outline_mic_24" />
<ImageView
android:id="@+id/flip_camera"
android:layout_width="@dimen/control_size"
android:layout_height="@dimen/control_size"
android:background="?android:attr/selectableItemBackground"
android:padding="@dimen/control_padding"
android:src="@drawable/outline_flip_camera_android_24" />
<ImageView
android:id="@+id/screen_share"
android:layout_width="@dimen/control_size"
android:layout_height="@dimen/control_size"
android:background="?android:attr/selectableItemBackground"
android:padding="@dimen/control_padding"
android:src="@drawable/baseline_cast_24" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
... ...
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<io.livekit.android.renderer.TextureViewRenderer
android:id="@+id/renderer"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
\ No newline at end of file
android:layout_width="0dp"
android:layout_height="match_parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
... ...
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="control_size">40dp</dimen>
<dimen name="control_padding">4dp</dimen>
</resources>
\ No newline at end of file
... ...
... ... @@ -6,7 +6,7 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">#000000</item>
<item name="android:windowBackground">@android:color/black</item>
</style>
</resources>
... ...