diff --git a/app/build.gradle b/app/build.gradle index 864b367d8..db7b55e0a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,8 +61,8 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.3' //100ms.live SDK - implementation 'com.github.100mslive.android-sdk:lib:2.4.5' - implementation 'com.github.100mslive.android-sdk:virtualBackground:2.4.5' + implementation 'com.github.100mslive.android-sdk:lib:2.4.7' + implementation 'com.github.100mslive.android-sdk:virtualBackground:2.4.7' // Navigation implementation "androidx.navigation:navigation-fragment-ktx:2.4.0" diff --git a/app/src/main/java/live/hms/app2/ui/meeting/MeetingFragment.kt b/app/src/main/java/live/hms/app2/ui/meeting/MeetingFragment.kt index 591158c22..29909f07b 100644 --- a/app/src/main/java/live/hms/app2/ui/meeting/MeetingFragment.kt +++ b/app/src/main/java/live/hms/app2/ui/meeting/MeetingFragment.kt @@ -259,6 +259,8 @@ class MeetingFragment : Fragment() { override fun onPrepareOptionsMenu(menu: Menu) { super.onPrepareOptionsMenu(menu) + menu.findItem(R.id.hls_start).isVisible = meetingViewModel.isAllowedToHlsStream() + menu.findItem(R.id.hls_stop).isVisible = meetingViewModel.isAllowedToHlsStream() menu.findItem(R.id.raise_hand).isVisible = true menu.findItem(R.id.change_name).isVisible = true menu.findItem(R.id.pip_mode).isVisible = true @@ -278,7 +280,7 @@ class MeetingFragment : Fragment() { } menu.findItem(R.id.action_stop_streaming_and_recording).apply { - isVisible = meetingViewModel.isRecording.value == RecordingState.RECORDING || + isVisible = meetingViewModel.isAllowedToBrowserRecord() && meetingViewModel.isRecording.value == RecordingState.RECORDING || meetingViewModel.isRecording.value == RecordingState.STREAMING || meetingViewModel.isRecording.value == RecordingState.STREAMING_AND_RECORDING } @@ -292,7 +294,7 @@ class MeetingFragment : Fragment() { } menu.findItem(R.id.action_record_meeting).apply { - isVisible = true + isVisible = meetingViewModel.isAllowedToBrowserRecord() // If we're in a transitioning state, we prevent further clicks. // Checked or not checked depends on if it's currently recording or not. Checked if recording. @@ -602,6 +604,11 @@ class MeetingFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { meetingViewModel.events.collect { event -> when (event) { + is MeetingViewModel.Event.CameraSwitchEvent -> { + withContext(Dispatchers.Main) { + Toast.makeText(context, "Camera Switch ${event.message}", Toast.LENGTH_LONG).show() + } + } is MeetingViewModel.Event.RTMPError -> { withContext(Dispatchers.Main) { Toast.makeText(context, "RTMP error ${event.exception}", Toast.LENGTH_LONG).show() @@ -901,7 +908,7 @@ class MeetingFragment : Fragment() { meetingViewModel.setTitle(mode.titleResId) - if (mode == MeetingViewMode.AUDIO_ONLY) { + if (mode == MeetingViewMode.AUDIO_ONLY ||mode is MeetingViewMode.HLS) { binding.buttonToggleVideo.visibility = View.GONE } else { binding.buttonToggleVideo.visibility = View.VISIBLE diff --git a/app/src/main/java/live/hms/app2/ui/meeting/MeetingViewModel.kt b/app/src/main/java/live/hms/app2/ui/meeting/MeetingViewModel.kt index 7e31bd819..fef157643 100644 --- a/app/src/main/java/live/hms/app2/ui/meeting/MeetingViewModel.kt +++ b/app/src/main/java/live/hms/app2/ui/meeting/MeetingViewModel.kt @@ -124,6 +124,8 @@ class MeetingViewModel( val previewErrorLiveData : LiveData = previewErrorData val previewUpdateLiveData : LiveData>> = previewUpdateData + private val _peerCount = MutableLiveData() + val peerCount : LiveData = _peerCount fun setMeetingViewMode(mode: MeetingViewMode) { if (mode != meetingViewMode.value) { @@ -245,6 +247,10 @@ class MeetingViewModel( roomState.postValue(Pair(type,hmsRoom)) // This will keep the isRecording value updated correctly in preview. It will not be called after join. _isRecording.postValue(getRecordingState(hmsRoom)) + if(type == HMSRoomUpdate.ROOM_PEER_COUNT_UPDATED) { + _peerCount.postValue(hmsRoom.peerCount) + Log.d("PeerCountUpdated", "New peer count is : ${hmsRoom.peerCount}") + } } }) @@ -733,12 +739,22 @@ class MeetingViewModel( // NOTE: During audio-only calls, this switch-camera is ignored // as no camera in use - try { - HMSCoroutineScope.launch(Dispatchers.Main) { - hmsSDK.getLocalPeer()?.videoTrack?.switchCamera() - } - } catch (ex: HMSException) { - Log.e(TAG, "flipCamera: ${ex.description}", ex) + + viewModelScope.launch { + hmsSDK.getLocalPeer()?.videoTrack?.switchCamera(object : HMSActionResultListener { + override fun onError(error: HMSException) { + viewModelScope.launch { + _events.emit(Event.CameraSwitchEvent("Error: $error")) + } + } + + override fun onSuccess() { + viewModelScope.launch { + _events.emit(Event.CameraSwitchEvent("Success: Facing is now: ${hmsSDK.getLocalPeer()?.videoTrack?.settings?.cameraFacing}")) + } + } + + }) } } @@ -867,6 +883,14 @@ class MeetingViewModel( return hmsSDK.getLocalPeer()?.hmsRole?.permission?.unmute == true } + fun isAllowedToRtmpStream() : Boolean = + hmsSDK.getLocalPeer()?.hmsRole?.permission?.rtmpStreaming == true + + fun isAllowedToBrowserRecord() : Boolean = hmsSDK.getLocalPeer()?.hmsRole?.permission?.browserRecording == true + + fun isAllowedToHlsStream() : Boolean = + hmsSDK.getLocalPeer()?.hmsRole?.permission?.hlsStreaming == true + fun changeRole(remotePeerId: String, toRoleName: String, force: Boolean) { val hmsPeer = hmsSDK.getPeers().find { it.peerID == remotePeerId } val toRole = hmsSDK.getRoles().find { it.name == toRoleName } @@ -1145,6 +1169,7 @@ class MeetingViewModel( data class ServerRecordEvent(val message: String) : Event() data class HlsEvent(override val message : String) : MessageEvent(message) data class HlsRecordingEvent(override val message : String) : MessageEvent(message) + data class CameraSwitchEvent(override val message: String) : MessageEvent(message) } private val _isHandRaised = MutableLiveData(false) @@ -1189,7 +1214,7 @@ class MeetingViewModel( fun getStats(): Flow> = statsFlow - fun startHls(hlsUrl : String, recordingConfig : HMSHlsRecordingConfig) { + fun startHls(hlsUrl : String?, recordingConfig : HMSHlsRecordingConfig) { val config = HMSHLSConfig(listOf(HMSHLSMeetingURLVariant(hlsUrl)), recordingConfig) diff --git a/app/src/main/java/live/hms/app2/ui/meeting/participants/ParticipantsFragment.kt b/app/src/main/java/live/hms/app2/ui/meeting/participants/ParticipantsFragment.kt index 0f7448bad..f30fa764b 100644 --- a/app/src/main/java/live/hms/app2/ui/meeting/participants/ParticipantsFragment.kt +++ b/app/src/main/java/live/hms/app2/ui/meeting/participants/ParticipantsFragment.kt @@ -90,7 +90,10 @@ class ParticipantsFragment : Fragment() { meetingViewModel.peerLiveDate.observe(viewLifecycleOwner) { val peers = meetingViewModel.peers adapter.setItems(peers) - binding.participantCount.text = "${peers.size}" + } + + meetingViewModel.peerCount.observe(viewLifecycleOwner) { peerCount -> + binding.participantCount.text = "$peerCount" } meetingViewModel.state.observe(viewLifecycleOwner) { state -> diff --git a/app/src/main/java/live/hms/app2/ui/meeting/participants/RtmpRecordFragment.kt b/app/src/main/java/live/hms/app2/ui/meeting/participants/RtmpRecordFragment.kt index 96ccb3e0d..70ffdedeb 100644 --- a/app/src/main/java/live/hms/app2/ui/meeting/participants/RtmpRecordFragment.kt +++ b/app/src/main/java/live/hms/app2/ui/meeting/participants/RtmpRecordFragment.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope @@ -48,16 +49,16 @@ class RtmpRecordFragment : Fragment() { // Get a listener on the page for the urls. binding.addRtmpUrlButton.setOnClickListener { addItem() } - rtmpUrladapter.submitList(settings.rtmpUrlsList.toList()) - binding.rtmpUrls.layoutManager = LinearLayoutManager(context) - binding.rtmpUrls.adapter = rtmpUrladapter binding.startButton.setOnClickListener { startClicked() } val recordingTimesUseCase = RecordingTimesUseCase() + enableDisable() with(binding) { recording.text = recordingTimesUseCase.showRecordInfo(meetingViewModel.hmsSDK.getRoom()!!) rtmp.text = recordingTimesUseCase.showRtmpInfo(meetingViewModel.hmsSDK.getRoom()!!) sfu.text = recordingTimesUseCase.showServerInfo(meetingViewModel.hmsSDK.getRoom()!!) + shouldStartHls.isChecked = meetingViewModel.isAllowedToHlsStream() + rtmpStreamingSwitch.isChecked = meetingViewModel.isAllowedToRtmpStream() hlsSingleFilePerLayer.isEnabled = shouldStartHls.isChecked hlsVod.isEnabled = shouldStartHls.isChecked shouldStartHls.setOnCheckedChangeListener { buttonView, isChecked -> @@ -79,6 +80,32 @@ class RtmpRecordFragment : Fragment() { } } + private fun enableDisable() { + with(binding) { + enableDisableRtmp() + addRtmpUrlButton.isEnabled = meetingViewModel.isAllowedToRtmpStream() + shouldRecord.isEnabled = meetingViewModel.isAllowedToBrowserRecord() + shouldStartHls.isEnabled = meetingViewModel.isAllowedToHlsStream() + rtmpStreamingSwitch.isEnabled = meetingViewModel.isAllowedToRtmpStream() + } + } + + private fun enableDisableRtmp() { + // addRtmpUrlButton,rtmpUrlContainer,existingRtmpUrlsText,rtmpUrls + val enabled = meetingViewModel.isAllowedToRtmpStream() + with(binding) { + if(meetingViewModel.isAllowedToRtmpStream()) { + rtmpUrladapter.submitList(settings.rtmpUrlsList.toList()) + rtmpUrls.layoutManager = LinearLayoutManager(context) + rtmpUrls.adapter = rtmpUrladapter + } + addRtmpUrlButton.isVisible = enabled + rtmpUrlContainer.isVisible = enabled + existingRtmpUrlsText.isVisible = enabled + rtmpUrls.isVisible = enabled + } + } + private fun addItem() { val urlToAdd = binding.newRtmpUrl.editableText.toString() if (urlToAdd.isEmpty()) { @@ -115,7 +142,7 @@ class RtmpRecordFragment : Fragment() { val isRecording = binding.shouldRecord.isChecked val isHls = binding.shouldStartHls.isChecked val meetingUrl = binding.meetingUrl.text.toString() - val isRtmp = settings.rtmpUrlsList.toList().isNotEmpty() + val isRtmp = binding.rtmpStreamingSwitch.isChecked && settings.rtmpUrlsList.toList().isNotEmpty() && meetingViewModel.isAllowedToRtmpStream() val isHlsSingleFilePerLayer = binding.hlsSingleFilePerLayer.isChecked val isHlsVod = binding.hlsVod.isChecked @@ -135,10 +162,10 @@ class RtmpRecordFragment : Fragment() { return } - if (meetingUrl.isNullOrBlank()) { + if (meetingUrl.isNullOrBlank() && (isRecording || isRtmp) ) { Toast.makeText( requireContext(), - "A valid meeting url is required. $meetingUrl is invalid or not a role name", + "A valid meeting url is required for recording/rtmp. $meetingUrl is invalid or not a role name", Toast.LENGTH_LONG ).show() } else if((isRecording || isRtmp) && isHls) { diff --git a/app/src/main/res/layout/layout_rtmp_recording.xml b/app/src/main/res/layout/layout_rtmp_recording.xml index 6da327521..b253c88c3 100644 --- a/app/src/main/res/layout/layout_rtmp_recording.xml +++ b/app/src/main/res/layout/layout_rtmp_recording.xml @@ -34,7 +34,7 @@ android:inputType="number" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="Rtmp Width" /> + android:hint="@string/stream_width" /> + android:hint="@string/stream_height" /> + + Record Stop Recording/Stream Add new RTMP streaming url - RTMP Settings - Use Harware AEC + Stream Settings + Use Hardware AEC Enable Background Disable Background Show stats @@ -166,4 +166,7 @@ Audio mode : media unknown File Playing + Width + Height + RTMP streaming \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 704cf5991..f98686835 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,5 +19,5 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -100MS_APP_VERSION_CODE=349 -100MS_APP_VERSION_NAME=3.8.1 \ No newline at end of file +100MS_APP_VERSION_CODE=350 +100MS_APP_VERSION_NAME=3.8.2 \ No newline at end of file