mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-25 19:57:33 +00:00
feat: plugging CallManager.kt into view model and service, fixing up dependencies
This commit is contained in:
@@ -37,7 +37,6 @@ dependencies {
|
|||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.4.0'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
implementation 'androidx.lifecycle:lifecycle-common-java8:2.4.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
|
||||||
|
@@ -5,15 +5,21 @@ import android.content.BroadcastReceiver
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
|
import android.media.AudioManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.android.synthetic.main.activity_webrtc_tests.*
|
import kotlinx.android.synthetic.main.activity_webrtc_tests.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.messaging.messages.control.CallMessage
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||||
@@ -29,12 +35,9 @@ import org.webrtc.*
|
|||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection.Observer,
|
class WebRtcTestsActivity: PassphraseRequiredActionBarActivity() {
|
||||||
SdpObserver, RTCStatsCollectorCallback {
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val HD_VIDEO_WIDTH = 900
|
|
||||||
const val HD_VIDEO_HEIGHT = 1600
|
|
||||||
const val CALL_ID = "call_id_session"
|
const val CALL_ID = "call_id_session"
|
||||||
private const val LOCAL_TRACK_ID = "local_track"
|
private const val LOCAL_TRACK_ID = "local_track"
|
||||||
private const val LOCAL_STREAM_ID = "local_track"
|
private const val LOCAL_STREAM_ID = "local_track"
|
||||||
@@ -45,39 +48,13 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
const val EXTRA_SDP = "WebRtcTestsActivity_EXTRA_SDP"
|
const val EXTRA_SDP = "WebRtcTestsActivity_EXTRA_SDP"
|
||||||
const val EXTRA_ADDRESS = "WebRtcTestsActivity_EXTRA_ADDRESS"
|
const val EXTRA_ADDRESS = "WebRtcTestsActivity_EXTRA_ADDRESS"
|
||||||
const val EXTRA_CALL_ID = "WebRtcTestsActivity_EXTRA_CALL_ID"
|
const val EXTRA_CALL_ID = "WebRtcTestsActivity_EXTRA_CALL_ID"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val viewModel by viewModels<CallViewModel>()
|
private val viewModel by viewModels<CallViewModel>()
|
||||||
|
|
||||||
private val surfaceHelper by lazy { SurfaceTextureHelper.create(Thread.currentThread().name, viewModel.eglBaseContext) }
|
|
||||||
private val audioSource by lazy { connectionFactory.createAudioSource(MediaConstraints()) }
|
|
||||||
private val videoCapturer by lazy { createCameraCapturer(Camera2Enumerator(this)) }
|
|
||||||
|
|
||||||
private val acceptedCallMessageHashes = mutableSetOf<Int>()
|
private val acceptedCallMessageHashes = mutableSetOf<Int>()
|
||||||
|
|
||||||
private val candidates: MutableList<IceCandidate> = mutableListOf()
|
private val candidates: MutableList<IceCandidate> = mutableListOf()
|
||||||
private val iceDebouncer = Debouncer(2_000)
|
|
||||||
|
|
||||||
private var localCandidateType: String? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (value != null) {
|
|
||||||
// show it
|
|
||||||
local_candidate_info.isVisible = true
|
|
||||||
local_candidate_info.text = "local: $value"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var remoteCandidateType: String? = null
|
|
||||||
set(value) {
|
|
||||||
field = value
|
|
||||||
if (value != null) {
|
|
||||||
remote_candidate_info.isVisible = true
|
|
||||||
remote_candidate_info.text = "remote: $value"
|
|
||||||
}
|
|
||||||
// update text
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var callAddress: Address
|
private lateinit var callAddress: Address
|
||||||
private lateinit var callId: UUID
|
private lateinit var callId: UUID
|
||||||
@@ -96,16 +73,27 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
|
||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
setContentView(R.layout.activity_webrtc_tests)
|
setContentView(R.layout.activity_webrtc_tests)
|
||||||
|
volumeControlStream = AudioManager.STREAM_VOICE_CALL
|
||||||
|
|
||||||
|
initializeResources()
|
||||||
|
|
||||||
//TODO: better handling of permissions
|
|
||||||
Permissions.with(this)
|
Permissions.with(this)
|
||||||
.request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
|
.request(Manifest.permission.RECORD_AUDIO)
|
||||||
.onAllGranted {
|
.onAllGranted {
|
||||||
setupStreams()
|
setupStreams()
|
||||||
}
|
}
|
||||||
.execute()
|
.execute()
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
viewModel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
local_renderer.run {
|
local_renderer.run {
|
||||||
setEnableHardwareScaler(true)
|
setEnableHardwareScaler(true)
|
||||||
init(eglBase.eglBaseContext, null)
|
init(eglBase.eglBaseContext, null)
|
||||||
@@ -180,24 +168,8 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStatsDelivered(statsReport: RTCStatsReport?) {
|
private fun initializeResources() {
|
||||||
statsReport?.let { report ->
|
|
||||||
val usedConnection = report.statsMap.filter { (_,v) -> v.type == "candidate-pair" && v.members["writable"] == true }.asIterable().firstOrNull()?.value ?: return@let
|
|
||||||
|
|
||||||
usedConnection.members["remoteCandidateId"]?.let { candidate ->
|
|
||||||
runOnUiThread {
|
|
||||||
remoteCandidateType = report.statsMap[candidate]?.members?.get("candidateType") as? String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
usedConnection.members["localCandidateId"]?.let { candidate ->
|
|
||||||
runOnUiThread {
|
|
||||||
localCandidateType = report.statsMap[candidate]?.members?.get("candidateType") as? String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d("Loki-RTC", "report is: $report")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun endCall() {
|
private fun endCall() {
|
||||||
@@ -237,36 +209,6 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
peerConnection.addStream(stream)
|
peerConnection.addStream(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? {
|
|
||||||
val deviceNames = enumerator.deviceNames
|
|
||||||
|
|
||||||
// First, try to find front facing camera
|
|
||||||
Log.d("Loki-RTC-vid", "Looking for front facing cameras.")
|
|
||||||
for (deviceName in deviceNames) {
|
|
||||||
if (enumerator.isFrontFacing(deviceName)) {
|
|
||||||
Log.d("Loki-RTC-vid", "Creating front facing camera capturer.")
|
|
||||||
val videoCapturer = enumerator.createCapturer(deviceName, null)
|
|
||||||
if (videoCapturer != null) {
|
|
||||||
return videoCapturer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Front facing camera not found, try something else
|
|
||||||
Log.d("Loki-RTC-vid", "Looking for other cameras.")
|
|
||||||
for (deviceName in deviceNames) {
|
|
||||||
if (!enumerator.isFrontFacing(deviceName)) {
|
|
||||||
Log.d("Loki-RTC-vid", "Creating other camera capturer.")
|
|
||||||
val videoCapturer = enumerator.createCapturer(deviceName, null)
|
|
||||||
if (videoCapturer != null) {
|
|
||||||
return videoCapturer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
||||||
Log.d("Loki-RTC", "onSignalingChange: $p0")
|
Log.d("Loki-RTC", "onSignalingChange: $p0")
|
||||||
}
|
}
|
||||||
@@ -375,11 +317,4 @@ class WebRtcTestsActivity: PassphraseRequiredActionBarActivity(), PeerConnection
|
|||||||
Log.d("Loki-RTC", "onSetFailure: $p0")
|
Log.d("Loki-RTC", "onSetFailure: $p0")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CallMessage.iceCandidates(): List<IceCandidate> {
|
|
||||||
val candidateSize = sdpMids.size
|
|
||||||
return (0 until candidateSize).map { i ->
|
|
||||||
IceCandidate(sdpMids[i], sdpMLineIndexes[i], sdps[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@@ -16,6 +16,6 @@ interface CallComponent {
|
|||||||
fun get(context: Context) = ApplicationContext.getInstance(context).callComponent
|
fun get(context: Context) = ApplicationContext.getInstance(context).callComponent
|
||||||
}
|
}
|
||||||
|
|
||||||
fun callManagerCompat(): AudioManagerCompat
|
fun audioManagerCompat(): AudioManagerCompat
|
||||||
|
|
||||||
}
|
}
|
@@ -1,20 +1,23 @@
|
|||||||
package org.thoughtcrime.securesms.dependencies
|
package org.thoughtcrime.securesms.dependencies
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import org.session.libsession.database.CallDataProvider
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
import org.thoughtcrime.securesms.webrtc.CallManager
|
import org.thoughtcrime.securesms.webrtc.CallManager
|
||||||
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
|
import org.thoughtcrime.securesms.webrtc.audio.AudioManagerCompat
|
||||||
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||||
|
import org.thoughtcrime.securesms.webrtc.data.SessionCallDataProvider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
object CallModule {
|
abstract class CallModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -23,6 +26,10 @@ object CallModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideCallManager(@ApplicationContext context: Context, storage: Storage) =
|
fun provideCallManager(@ApplicationContext context: Context, storage: Storage) =
|
||||||
CallManager(context, storage)
|
CallManager(context)
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun bindCallDataProvider(sessionCallDataProvider: SessionCallDataProvider): CallDataProvider
|
||||||
|
|
||||||
}
|
}
|
@@ -122,10 +122,6 @@ object DatabaseModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
|
fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper)
|
||||||
|
|
||||||
// @Provides
|
|
||||||
// @Singleton
|
|
||||||
// fun provideCallDataProvider(storage: Storage) = SessionCallDataProvider(storage)
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAttachmentProvider(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): MessageDataProvider = DatabaseAttachmentProvider(context, openHelper)
|
fun provideAttachmentProvider(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper): MessageDataProvider = DatabaseAttachmentProvider(context, openHelper)
|
||||||
|
@@ -18,16 +18,9 @@ import kotlin.properties.Delegates
|
|||||||
import kotlin.properties.Delegates.observable
|
import kotlin.properties.Delegates.observable
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class WebRtcCallService: Service(), SignalAudioManager.EventListener {
|
class WebRtcCallService: Service() {
|
||||||
|
|
||||||
@Inject lateinit var callManager: CallManager
|
@Inject lateinit var callManager: CallManager
|
||||||
val signalAudioManager: SignalAudioManager by lazy {
|
|
||||||
SignalAudioManager(this, this, CallComponent.get(this).callManagerCompat())
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class CallState {
|
|
||||||
STATE_IDLE, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ACTION_UPDATE = "UPDATE"
|
private const val ACTION_UPDATE = "UPDATE"
|
||||||
@@ -81,10 +74,6 @@ class WebRtcCallService: Service(), SignalAudioManager.EventListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var state: CallState by observable(CallState.STATE_IDLE) { _, previousValue, newValue ->
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
@@ -103,8 +92,4 @@ class WebRtcCallService: Service(), SignalAudioManager.EventListener {
|
|||||||
// unregister network receiver
|
// unregister network receiver
|
||||||
// unregister power button
|
// unregister power button
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set<SignalAudioManager.AudioDevice>) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
}
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.webrtc
|
||||||
|
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
|
||||||
|
interface CallDataListener {
|
||||||
|
fun newCallMessage(callMessage: SignalServiceProtos.CallMessage)
|
||||||
|
}
|
@@ -2,44 +2,72 @@ package org.thoughtcrime.securesms.webrtc
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.android.mms.transaction.MessageSender
|
import com.android.mms.transaction.MessageSender
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
|
import org.session.libsignal.protos.SignalServiceProtos
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
import org.thoughtcrime.securesms.database.Storage
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
|
import org.thoughtcrime.securesms.dependencies.CallComponent
|
||||||
|
import org.thoughtcrime.securesms.service.WebRtcCallService
|
||||||
|
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
|
||||||
import org.webrtc.*
|
import org.webrtc.*
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CallManager(private val context: Context,
|
class CallManager(private val context: Context): PeerConnection.Observer,
|
||||||
private val storage: Storage): PeerConnection.Observer {
|
SignalAudioManager.EventListener,
|
||||||
|
CallDataListener {
|
||||||
|
|
||||||
|
enum class CallState {
|
||||||
|
STATE_IDLE, STATE_DIALING, STATE_ANSWERING, STATE_REMOTE_RINGING, STATE_LOCAL_RINGING, STATE_CONNECTED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val signalAudioManager: SignalAudioManager by lazy {
|
||||||
|
SignalAudioManager(context, this, CallComponent.get(context).audioManagerCompat())
|
||||||
|
}
|
||||||
|
|
||||||
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
private val serviceExecutor = Executors.newSingleThreadExecutor()
|
||||||
private val networkExecutor = Executors.newSingleThreadExecutor()
|
private val networkExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
private val eglBase: EglBase = EglBase.create()
|
private val eglBase: EglBase = EglBase.create()
|
||||||
|
|
||||||
private val connectionFactory by lazy {
|
private var peerConnectionWrapper: PeerConnectionWrapper? = null
|
||||||
|
|
||||||
val decoderFactory = DefaultVideoDecoderFactory(eglBase.eglBaseContext)
|
private val currentCallState: MutableStateFlow<CallState> = MutableStateFlow(CallState.STATE_IDLE)
|
||||||
val encoderFactory = DefaultVideoEncoderFactory(eglBase.eglBaseContext, true, true)
|
|
||||||
|
|
||||||
PeerConnectionFactory.builder()
|
private fun createCameraCapturer(enumerator: CameraEnumerator): CameraVideoCapturer? {
|
||||||
.setVideoDecoderFactory(decoderFactory)
|
val deviceNames = enumerator.deviceNames
|
||||||
.setVideoEncoderFactory(encoderFactory)
|
|
||||||
.setOptions(PeerConnectionFactory.Options())
|
// First, try to find front facing camera
|
||||||
.createPeerConnectionFactory()!!
|
Log.d("Loki-RTC-vid", "Looking for front facing cameras.")
|
||||||
|
for (deviceName in deviceNames) {
|
||||||
|
if (enumerator.isFrontFacing(deviceName)) {
|
||||||
|
Log.d("Loki-RTC-vid", "Creating front facing camera capturer.")
|
||||||
|
val videoCapturer = enumerator.createCapturer(deviceName, null)
|
||||||
|
if (videoCapturer != null) {
|
||||||
|
return videoCapturer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Front facing camera not found, try something else
|
||||||
|
Log.d("Loki-RTC-vid", "Looking for other cameras.")
|
||||||
|
for (deviceName in deviceNames) {
|
||||||
|
if (!enumerator.isFrontFacing(deviceName)) {
|
||||||
|
Log.d("Loki-RTC-vid", "Creating other camera capturer.")
|
||||||
|
val videoCapturer = enumerator.createCapturer(deviceName, null)
|
||||||
|
if (videoCapturer != null) {
|
||||||
|
return videoCapturer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private var peerConnection: PeerConnection? = null
|
override fun newCallMessage(callMessage: SignalServiceProtos.CallMessage) {
|
||||||
|
|
||||||
private fun getPeerConnection(): PeerConnection {
|
|
||||||
val stun = PeerConnection.IceServer.builder("stun:freyr.getsession.org:5349").setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK).createIceServer()
|
|
||||||
val turn = PeerConnection.IceServer.builder("turn:freyr.getsession.org:5349").setUsername("webrtc").setPassword("webrtc").setTlsCertPolicy(PeerConnection.TlsCertPolicy.TLS_CERT_POLICY_INSECURE_NO_CHECK).createIceServer()
|
|
||||||
val iceServers = mutableListOf(turn, stun)
|
|
||||||
val rtcConfig = PeerConnection.RTCConfiguration(iceServers).apply {
|
|
||||||
this.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.ENABLED
|
|
||||||
this.candidateNetworkPolicy = PeerConnection.CandidateNetworkPolicy.ALL
|
|
||||||
// this.iceTransportsType = PeerConnection.IceTransportsType.RELAY
|
|
||||||
}
|
|
||||||
rtcConfig.keyType = PeerConnection.KeyType.ECDSA
|
|
||||||
return connectionFactory.createPeerConnection(rtcConfig, this)!!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun networkChange(networkAvailable: Boolean) {
|
fun networkChange(networkAvailable: Boolean) {
|
||||||
@@ -54,9 +82,11 @@ class CallManager(private val context: Context,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fun callEnded() {
|
fun callEnded() {
|
||||||
peerConnection?.close()
|
peerConnectionWrapper?.()
|
||||||
peerConnection = null
|
peerConnectionWrapper = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAudioEnabled(isEnabled: Boolean) {
|
fun setAudioEnabled(isEnabled: Boolean) {
|
||||||
@@ -110,4 +140,16 @@ class CallManager(private val context: Context,
|
|||||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAudioDeviceChanged(activeDevice: SignalAudioManager.AudioDevice, devices: Set<SignalAudioManager.AudioDevice>) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CallMessage.iceCandidates(): List<IceCandidate> {
|
||||||
|
val candidateSize = sdpMids.size
|
||||||
|
return (0 until candidateSize).map { i ->
|
||||||
|
IceCandidate(sdpMids[i], sdpMLineIndexes[i], sdps[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@@ -6,23 +6,29 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.session.libsession.messaging.messages.control.CallMessage
|
||||||
import org.webrtc.*
|
import org.webrtc.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class CallViewModel @Inject constructor(
|
class CallViewModel @Inject constructor(private val callManager: CallManager): ViewModel() {
|
||||||
private val callManager: CallManager
|
|
||||||
): ViewModel(), PeerConnection.Observer {
|
|
||||||
|
|
||||||
sealed class StateEvent {
|
sealed class StateEvent {
|
||||||
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
|
data class AudioEnabled(val isEnabled: Boolean): StateEvent()
|
||||||
data class VideoEnabled(val isEnabled: Boolean): StateEvent()
|
data class VideoEnabled(val isEnabled: Boolean): StateEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val audioEnabledState = MutableStateFlow(StateEvent.AudioEnabled(true))
|
val audioEnabledState = MutableStateFlow(
|
||||||
private val videoEnabledState = MutableStateFlow(StateEvent.VideoEnabled(false))
|
callManager.audioEnabled.let { isEnabled ->
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val videoEnabledState = MutableStateFlow(
|
||||||
|
callManager.videoEnabled.let { isEnabled ->
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
private val peerConnection = callManager.getPeerConnection(this)
|
|
||||||
|
|
||||||
// set up listeners for establishing connection toggling video / audio
|
// set up listeners for establishing connection toggling video / audio
|
||||||
init {
|
init {
|
||||||
@@ -32,48 +38,4 @@ class CallViewModel @Inject constructor(
|
|||||||
.launchIn(viewModelScope)
|
.launchIn(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignalingChange(p0: PeerConnection.SignalingState?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceConnectionChange(p0: PeerConnection.IceConnectionState?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceConnectionReceivingChange(p0: Boolean) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceGatheringChange(p0: PeerConnection.IceGatheringState?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceCandidate(p0: IceCandidate?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onIceCandidatesRemoved(p0: Array<out IceCandidate>?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAddStream(p0: MediaStream?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRemoveStream(p0: MediaStream?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDataChannel(p0: DataChannel?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRenegotiationNeeded() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAddTrack(p0: RtpReceiver?, p1: Array<out MediaStream>?) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
@@ -0,0 +1,75 @@
|
|||||||
|
package org.thoughtcrime.securesms.webrtc
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.thoughtcrime.securesms.webrtc.video.Camera
|
||||||
|
import org.thoughtcrime.securesms.webrtc.video.CameraEventListener
|
||||||
|
import org.webrtc.*
|
||||||
|
|
||||||
|
class PeerConnectionWrapper(context: Context,
|
||||||
|
factory: PeerConnectionFactory,
|
||||||
|
observer: PeerConnection.Observer,
|
||||||
|
localRenderer: VideoSink,
|
||||||
|
cameraEventListener: CameraEventListener,
|
||||||
|
eglBase: EglBase,
|
||||||
|
relay: Boolean = false) {
|
||||||
|
|
||||||
|
private val peerConnection: PeerConnection
|
||||||
|
private val audioTrack: AudioTrack
|
||||||
|
private val audioSource: AudioSource
|
||||||
|
private val camera: Camera
|
||||||
|
private val videoSource: VideoSource?
|
||||||
|
private val videoTrack: VideoTrack?
|
||||||
|
|
||||||
|
init {
|
||||||
|
val stun = PeerConnection.IceServer.builder("stun:freyr.getsession.org:5349").createIceServer()
|
||||||
|
val turn = PeerConnection.IceServer.builder("turn:freyr.getsession.org:5349").setUsername("webrtc").setPassword("webrtc").createIceServer()
|
||||||
|
val iceServers = listOf(stun,turn)
|
||||||
|
|
||||||
|
val constraints = MediaConstraints().apply {
|
||||||
|
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
|
||||||
|
}
|
||||||
|
val audioConstraints = MediaConstraints().apply {
|
||||||
|
optional.add(MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val configuration = PeerConnection.RTCConfiguration(iceServers).apply {
|
||||||
|
bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE
|
||||||
|
rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE
|
||||||
|
if (relay) {
|
||||||
|
iceTransportsType = PeerConnection.IceTransportsType.RELAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection = factory.createPeerConnection(configuration, constraints, observer)!!
|
||||||
|
peerConnection.setAudioPlayout(false)
|
||||||
|
peerConnection.setAudioRecording(false)
|
||||||
|
|
||||||
|
val mediaStream = factory.createLocalMediaStream("ARDAMS")
|
||||||
|
audioSource = factory.createAudioSource(audioConstraints)
|
||||||
|
audioTrack = factory.createAudioTrack("ARDAMSa0", audioSource)
|
||||||
|
audioTrack.setEnabled(false)
|
||||||
|
mediaStream.addTrack(audioTrack)
|
||||||
|
|
||||||
|
camera = Camera(context, cameraEventListener)
|
||||||
|
if (camera.capturer != null) {
|
||||||
|
videoSource = factory.createVideoSource(false)
|
||||||
|
videoTrack = factory.createVideoTrack("ARDAMSv0", videoSource)
|
||||||
|
|
||||||
|
camera.capturer.initialize(
|
||||||
|
SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.eglBaseContext),
|
||||||
|
context,
|
||||||
|
videoSource.capturerObserver
|
||||||
|
)
|
||||||
|
|
||||||
|
videoTrack.addSink(localRenderer)
|
||||||
|
videoTrack.setEnabled(false)
|
||||||
|
mediaStream.addTrack(videoTrack)
|
||||||
|
} else {
|
||||||
|
videoSource = null
|
||||||
|
videoTrack = null
|
||||||
|
}
|
||||||
|
|
||||||
|
peerConnection.addStream(mediaStream)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -2,10 +2,10 @@ package org.thoughtcrime.securesms.webrtc.data
|
|||||||
|
|
||||||
import org.session.libsession.database.CallDataProvider
|
import org.session.libsession.database.CallDataProvider
|
||||||
import org.session.libsession.database.StorageProtocol
|
import org.session.libsession.database.StorageProtocol
|
||||||
|
import org.thoughtcrime.securesms.webrtc.CallManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SessionCallDataProvider @Inject constructor(private val storage: StorageProtocol): CallDataProvider {
|
class SessionCallDataProvider @Inject constructor(private val storage: StorageProtocol,
|
||||||
|
private val callManager: CallManager): CallDataProvider {
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@@ -0,0 +1,74 @@
|
|||||||
|
package org.thoughtcrime.securesms.webrtc.video
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.webrtc.video.CameraState.Direction.*
|
||||||
|
import org.webrtc.Camera2Enumerator
|
||||||
|
import org.webrtc.CameraEnumerator
|
||||||
|
import org.webrtc.CameraVideoCapturer
|
||||||
|
|
||||||
|
class Camera(context: Context,
|
||||||
|
private val cameraEventListener: CameraEventListener): CameraVideoCapturer.CameraSwitchHandler {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(Camera::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
val capturer: CameraVideoCapturer?
|
||||||
|
private val cameraCount: Int
|
||||||
|
private var activeDirection: CameraState.Direction = PENDING
|
||||||
|
var enabled: Boolean = false
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
capturer ?: return
|
||||||
|
try {
|
||||||
|
if (value) {
|
||||||
|
capturer.startCapture(1280,720,30)
|
||||||
|
} else {
|
||||||
|
capturer.stopCapture()
|
||||||
|
}
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Log.e(TAG,"Interrupted while stopping video capture")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
val enumerator = Camera2Enumerator(context)
|
||||||
|
cameraCount = enumerator.deviceNames.size
|
||||||
|
capturer = createVideoCapturer(enumerator, FRONT)?.apply {
|
||||||
|
activeDirection = FRONT
|
||||||
|
} ?: createVideoCapturer(enumerator, BACK)?.apply {
|
||||||
|
activeDirection = BACK
|
||||||
|
} ?: run {
|
||||||
|
activeDirection = NONE
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flip() {
|
||||||
|
if (capturer == null || cameraCount < 2) {
|
||||||
|
Log.w(TAG, "Tried to flip camera without capturer or less than 2 cameras")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
activeDirection = PENDING
|
||||||
|
capturer.switchCamera(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraSwitchDone(isFrontFacing: Boolean) {
|
||||||
|
activeDirection = if (isFrontFacing) FRONT else BACK
|
||||||
|
cameraEventListener.onCameraSwitchCompleted(CameraState(activeDirection, cameraCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCameraSwitchError(errorMessage: String?) {
|
||||||
|
Log.e(TAG,"onCameraSwitchError: $errorMessage")
|
||||||
|
cameraEventListener.onCameraSwitchCompleted(CameraState(activeDirection, cameraCount))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createVideoCapturer(enumerator: CameraEnumerator, direction: CameraState.Direction): CameraVideoCapturer? =
|
||||||
|
enumerator.deviceNames.firstOrNull { device ->
|
||||||
|
(direction == FRONT && enumerator.isFrontFacing(device)) ||
|
||||||
|
(direction == BACK && enumerator.isBackFacing(device))
|
||||||
|
}?.let { enumerator.createCapturer(it, null) }
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,18 @@
|
|||||||
|
package org.thoughtcrime.securesms.webrtc.video
|
||||||
|
|
||||||
|
interface CameraEventListener {
|
||||||
|
fun onCameraSwitchCompleted(newCameraState: CameraState)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CameraState(val activeDirection: Direction, val cameraCount: Int) {
|
||||||
|
companion object {
|
||||||
|
val UNKNOWN = CameraState(Direction.NONE, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val enabled: Boolean
|
||||||
|
get() = activeDirection != Direction.NONE
|
||||||
|
|
||||||
|
enum class Direction {
|
||||||
|
FRONT, BACK, NONE, PENDING
|
||||||
|
}
|
||||||
|
}
|
@@ -51,6 +51,6 @@ allprojects {
|
|||||||
|
|
||||||
project.ext {
|
project.ext {
|
||||||
androidMinimumSdkVersion = 23
|
androidMinimumSdkVersion = 23
|
||||||
androidCompileSdkVersion = 30
|
androidCompileSdkVersion = 31
|
||||||
}
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user