Support toggle front/back camera

This commit is contained in:
Valere 2020-06-18 23:05:18 +02:00
parent 77a01f0cd4
commit b27eead016
8 changed files with 208 additions and 68 deletions

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.call
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
@ -41,6 +42,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
}
callControlsSwitchCamera.clickableView.debouncedClicks {
callViewModel.handle(VectorCallViewActions.ToggleCamera)
dismiss()
}
callViewModel.observeViewEvents {
when (it) {
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
@ -55,19 +61,19 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
private fun showSoundDeviceChooser(available: List<CallAudioManager.SoundDevice>, current: CallAudioManager.SoundDevice) {
val soundDevices = available.map {
when (it) {
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> span {
text = getString(R.string.sound_device_wireless_headset)
textStyle = if (current == it) "bold" else "normal"
}
CallAudioManager.SoundDevice.PHONE -> span {
CallAudioManager.SoundDevice.PHONE -> span {
text = getString(R.string.sound_device_phone)
textStyle = if (current == it) "bold" else "normal"
}
CallAudioManager.SoundDevice.SPEAKER -> span {
CallAudioManager.SoundDevice.SPEAKER -> span {
text = getString(R.string.sound_device_speaker)
textStyle = if (current == it) "bold" else "normal"
}
CallAudioManager.SoundDevice.HEADSET -> span {
CallAudioManager.SoundDevice.HEADSET -> span {
text = getString(R.string.sound_device_headset)
textStyle = if (current == it) "bold" else "normal"
}
@ -77,13 +83,13 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
.setItems(soundDevices.toTypedArray()) { d, n ->
d.cancel()
when (soundDevices[n].toString()) {
getString(R.string.sound_device_phone) -> {
getString(R.string.sound_device_phone) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.PHONE))
}
getString(R.string.sound_device_speaker) -> {
getString(R.string.sound_device_speaker) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.SPEAKER))
}
getString(R.string.sound_device_headset) -> {
getString(R.string.sound_device_headset) -> {
callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.SoundDevice.HEADSET))
}
getString(R.string.sound_device_wireless_headset) -> {
@ -95,23 +101,16 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
.show()
}
// override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// return super.onCreateDialog(savedInstanceState).apply {
// window?.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// window?.decorView?.systemUiVisibility =
// View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
// View.SYSTEM_UI_FLAG_FULLSCREEN or
// View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
// }
// }
private fun renderState(state: VectorCallViewState) {
callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
callControlsSoundDevice.subTitle = when (state.soundDevice) {
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
CallAudioManager.SoundDevice.PHONE -> getString(R.string.sound_device_phone)
CallAudioManager.SoundDevice.SPEAKER -> getString(R.string.sound_device_speaker)
CallAudioManager.SoundDevice.HEADSET -> getString(R.string.sound_device_headset)
CallAudioManager.SoundDevice.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
}
callControlsSwitchCamera.isVisible = state.canSwitchCamera
callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back)
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.call
enum class CameraType {
FRONT,
BACK
}
data class CameraProxy(
val name: String,
val type: CameraType
)

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.call
// import im.vector.riotx.features.call.service.CallHeadsUpService
import android.app.KeyguardManager
import android.content.Context
import android.content.Intent
@ -161,13 +160,8 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
v.updatePadding(bottom = if (systemUiVisibility) insets.systemWindowInsetBottom else 0)
insets
}
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// // window.navigationBarColor = ContextCompat.getColor(this, R.color.riotx_background_light)
// // }
// for content intent when screen is locked
// window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
// window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
if (intent.hasExtra(MvRx.KEY_ARG)) {
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
@ -323,6 +317,10 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
peerConnectionManager.attachViewRenderers(pipRenderer, fullscreenRenderer,
intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
pipRenderer.setOnClickListener {
callViewModel.handle(VectorCallViewActions.ToggleCamera)
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {

View File

@ -48,6 +48,8 @@ data class VectorCallViewState(
val isAudioMuted: Boolean = false,
val isVideoEnabled: Boolean = true,
val isVideoCaptureInError: Boolean = false,
val isFrontCamera: Boolean = true,
val canSwitchCamera: Boolean = true,
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
@ -62,7 +64,8 @@ sealed class VectorCallViewActions : VectorViewModelAction {
object ToggleVideo : VectorCallViewActions()
data class ChangeAudioDevice(val device: CallAudioManager.SoundDevice) : VectorCallViewActions()
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed: VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
}
sealed class VectorCallViewEvents : VectorViewEvents {
@ -140,6 +143,15 @@ class VectorCallViewModel @AssistedInject constructor(
)
}
}
override fun onCameraChange(mgr: WebRtcPeerConnectionManager) {
setState {
copy(
canSwitchCamera = mgr.canSwitchCamera(),
isFrontCamera = mgr.currentCameraType() == CameraType.FRONT
)
}
}
}
init {
@ -160,7 +172,9 @@ class VectorCallViewModel @AssistedInject constructor(
callState = Success(mxCall.state),
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
soundDevice = webRtcPeerConnectionManager.audioManager.getCurrentSoundDevice(),
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices()
availableSoundDevices = webRtcPeerConnectionManager.audioManager.getAvailableSoundDevices(),
isFrontCamera = webRtcPeerConnectionManager.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcPeerConnectionManager.canSwitchCamera()
)
}
} ?: run {
@ -236,6 +250,9 @@ class VectorCallViewModel @AssistedInject constructor(
}
Unit
}
VectorCallViewActions.ToggleCamera -> {
webRtcPeerConnectionManager.switchCamera()
}
}.exhaustive
}

View File

@ -42,6 +42,7 @@ import org.webrtc.AudioSource
import org.webrtc.AudioTrack
import org.webrtc.Camera1Enumerator
import org.webrtc.Camera2Enumerator
import org.webrtc.CameraVideoCapturer
import org.webrtc.DataChannel
import org.webrtc.DefaultVideoDecoderFactory
import org.webrtc.DefaultVideoEncoderFactory
@ -54,7 +55,6 @@ import org.webrtc.RtpReceiver
import org.webrtc.SessionDescription
import org.webrtc.SurfaceTextureHelper
import org.webrtc.SurfaceViewRenderer
import org.webrtc.VideoCapturer
import org.webrtc.VideoSource
import org.webrtc.VideoTrack
import timber.log.Timber
@ -78,6 +78,7 @@ class WebRtcPeerConnectionManager @Inject constructor(
fun onCurrentCallChange(call: MxCall?)
fun onCaptureStateChanged(captureInError: Boolean)
fun onAudioDevicesChange(mgr: WebRtcPeerConnectionManager) {}
fun onCameraChange(mgr: WebRtcPeerConnectionManager) {}
}
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
@ -159,9 +160,10 @@ class WebRtcPeerConnectionManager @Inject constructor(
private var peerConnectionFactory: PeerConnectionFactory? = null
// private var localSdp: SessionDescription? = null
private var videoCapturer: CameraVideoCapturer? = null
private var videoCapturer: VideoCapturer? = null
private val availableCamera = ArrayList<CameraProxy>()
private var cameraInUse: CameraProxy? = null
var capturerIsInError = false
set(value) {
@ -413,51 +415,66 @@ class WebRtcPeerConnectionManager @Inject constructor(
// add video track if needed
if (callContext.mxCall.isVideoCall) {
availableCamera.clear()
val cameraIterator = if (Camera2Enumerator.isSupported(context)) Camera2Enumerator(context) else Camera1Enumerator(false)
// I don't realy know how that works if there are 2 front or 2 back cameras
val frontCamera = cameraIterator.deviceNames
?.firstOrNull { cameraIterator.isFrontFacing(it) }
?: cameraIterator.deviceNames?.first()
// TODO detect when no camera or no front camera
val videoCapturer = cameraIterator.createCapturer(frontCamera, object : CameraEventsHandlerAdapter() {
override fun onFirstFrameAvailable() {
super.onFirstFrameAvailable()
capturerIsInError = false
}
override fun onCameraClosed() {
// This could happen if you open the camera app in chat
// We then register in order to restart capture as soon as the camera is available again
Timber.v("## VOIP onCameraClosed")
this@WebRtcPeerConnectionManager.capturerIsInError = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val restarter = CameraRestarter(frontCamera ?: "", callContext.mxCall.callId)
callContext.cameraAvailabilityCallback = restarter
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
cameraManager.registerAvailabilityCallback(restarter, null)
?.let {
CameraProxy(it, CameraType.FRONT).also { availableCamera.add(it) }
}
}
})
val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast)
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
Timber.v("## VOIP Local video source created")
val backCamera = cameraIterator.deviceNames
?.firstOrNull { cameraIterator.isBackFacing(it) }
?.let {
CameraProxy(it, CameraType.BACK).also { availableCamera.add(it) }
}
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
// HD
videoCapturer.startCapture(1280, 720, 30)
val camera = frontCamera?.also { cameraInUse = frontCamera }
?: backCamera?.also { cameraInUse = backCamera }
?: null.also { cameraInUse = null }
this.videoCapturer = videoCapturer
if (camera != null) {
val videoCapturer = cameraIterator.createCapturer(camera.name, object : CameraEventsHandlerAdapter() {
override fun onFirstFrameAvailable() {
super.onFirstFrameAvailable()
capturerIsInError = false
}
val localVideoTrack = peerConnectionFactory!!.createVideoTrack("ARDAMSv0", videoSource)
Timber.v("## VOIP Local video track created")
localVideoTrack?.setEnabled(true)
override fun onCameraClosed() {
// This could happen if you open the camera app in chat
// We then register in order to restart capture as soon as the camera is available again
Timber.v("## VOIP onCameraClosed")
this@WebRtcPeerConnectionManager.capturerIsInError = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val restarter = CameraRestarter(cameraInUse?.name ?: "", callContext.mxCall.callId)
callContext.cameraAvailabilityCallback = restarter
val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
cameraManager.registerAvailabilityCallback(restarter, null)
}
}
})
callContext.localVideoSource = videoSource
callContext.localVideoTrack = localVideoTrack
val videoSource = peerConnectionFactory!!.createVideoSource(videoCapturer.isScreencast)
val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", rootEglBase!!.eglBaseContext)
Timber.v("## VOIP Local video source created")
localMediaStream?.addTrack(localVideoTrack)
videoCapturer.initialize(surfaceTextureHelper, context.applicationContext, videoSource!!.capturerObserver)
// HD
videoCapturer.startCapture(1280, 720, 30)
this.videoCapturer = videoCapturer
val localVideoTrack = peerConnectionFactory!!.createVideoTrack("ARDAMSv0", videoSource)
Timber.v("## VOIP Local video track created")
localVideoTrack?.setEnabled(true)
callContext.localVideoSource = videoSource
callContext.localVideoTrack = localVideoTrack
localMediaStream?.addTrack(localVideoTrack)
}
}
}
@ -661,6 +678,34 @@ class WebRtcPeerConnectionManager @Inject constructor(
currentCall?.localVideoTrack?.setEnabled(enabled)
}
fun switchCamera() {
Timber.v("## VOIP switchCamera")
if (currentCall != null && currentCall?.mxCall?.state is CallState.Connected && currentCall?.mxCall?.isVideoCall == true) {
videoCapturer?.switchCamera(object : CameraVideoCapturer.CameraSwitchHandler {
// Invoked on success. |isFrontCamera| is true if the new camera is front facing.
override fun onCameraSwitchDone(isFrontCamera: Boolean) {
Timber.v("## VOIP onCameraSwitchDone isFront $isFrontCamera")
cameraInUse = availableCamera.first { if (isFrontCamera) it.type == CameraType.FRONT else it.type == CameraType.BACK }
currentCallsListeners.forEach {
tryThis { it.onCameraChange(this@WebRtcPeerConnectionManager) }
}
}
override fun onCameraSwitchError(errorDescription: String?) {
Timber.v("## VOIP onCameraSwitchError isFront $errorDescription")
}
})
}
}
fun canSwitchCamera(): Boolean {
return availableCamera.size > 0
}
fun currentCameraType(): CameraType? {
return cameraInUse?.type
}
fun endCall() {
// Update service state
CallService.onNoActiveCall(context)

View File

@ -0,0 +1,42 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4,9L1,12L4,15"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M10,15L13,12L10,9"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M13,12H2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M23,7L16,12L23,17V7Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M1,7.5V7C1,5.8954 1.8954,5 3,5H14C15.1046,5 16,5.8954 16,7V17C16,18.1046 15.1046,19 14,19H3C1.8954,19 1,18.1046 1,17V16.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
</vector>

View File

@ -11,9 +11,18 @@
android:id="@+id/callControlsSoundDevice"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:actionTitle="Select sound device"
app:actionTitle="@string/call_select_sound_device"
tools:actionDescription="Speaker"
app:leftIcon="@drawable/ic_call_speaker_default"
app:tint="?attr/riotx_text_primary" />
<im.vector.riotx.core.ui.views.BottomSheetActionButton
android:id="@+id/callControlsSwitchCamera"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:actionTitle="@string/call_switch_camera"
tools:actionDescription="Front"
app:leftIcon="@drawable/ic_video_flip"
app:tint="?attr/riotx_text_primary" />
</LinearLayout>

View File

@ -219,6 +219,9 @@
<string name="sound_device_speaker">Speaker</string>
<string name="sound_device_headset">Headset</string>
<string name="sound_device_wireless_headset">Wireless Headset</string>
<string name="call_switch_camera">Switch Camera</string>
<string name="call_camera_front">Front</string>
<string name="call_camera_back">Back</string>
<string name="option_send_files">Send files</string>