feat: Working VoIP calling implementation (Flutter + Android)
Working video and audio calls, as well as android integration
This commit is contained in:
commit
96a7e211a0
60 changed files with 2445 additions and 0 deletions
18
android/src/main/AndroidManifest.xml
Normal file
18
android/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="xyz.nuark.liblinphone_flutter">
|
||||
<!-- Permissions for SIP calling -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Camera features -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
|
||||
<!-- Microphone feature -->
|
||||
<uses-feature android:name="android.hardware.microphone" android:required="true" />
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,228 @@
|
|||
package xyz.nuark.liblinphone_flutter
|
||||
|
||||
import android.app.Activity
|
||||
import android.util.Log
|
||||
import android.util.SparseArray
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import androidx.core.util.isNotEmpty
|
||||
import androidx.core.util.size
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
|
||||
import io.flutter.plugin.common.MethodChannel.Result
|
||||
import org.linphone.mediastream.video.capture.CaptureTextureView
|
||||
import xyz.nuark.liblinphone_flutter.views.LocalViewFactory
|
||||
import xyz.nuark.liblinphone_flutter.views.RemoteViewFactory
|
||||
|
||||
/** LiblinphoneFlutterPlugin */
|
||||
class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler {
|
||||
private lateinit var channel: MethodChannel
|
||||
|
||||
private var registrationEventsChannel: EventChannel.EventSink? = null
|
||||
private var callEventsChannel: EventChannel.EventSink? = null
|
||||
|
||||
private lateinit var activity: Activity
|
||||
|
||||
private var remoteViewCache = SparseArray<View>(1);
|
||||
private var localViewCache = SparseArray<View>(1);
|
||||
|
||||
private lateinit var linphoneBridge: LinphoneBridge
|
||||
|
||||
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
|
||||
flutterPluginBinding
|
||||
.platformViewRegistry
|
||||
.registerViewFactory("liblinphone_flutter.nuark.xyz/remote_view", RemoteViewFactory {
|
||||
Log.i(TAG, "onAttachedToEngine: caching RemoteView ${it.id}")
|
||||
remoteViewCache.put(0, it)
|
||||
})
|
||||
flutterPluginBinding
|
||||
.platformViewRegistry
|
||||
.registerViewFactory("liblinphone_flutter.nuark.xyz/local_view", LocalViewFactory {
|
||||
Log.i(TAG, "onAttachedToEngine: caching LocalView ${it.id}")
|
||||
localViewCache.put(0, it)
|
||||
})
|
||||
|
||||
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "liblinphone_flutter")
|
||||
channel.setMethodCallHandler(this)
|
||||
|
||||
EventChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
"liblinphone_flutter.nuark.xyz/registration_events"
|
||||
).setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(
|
||||
arguments: Any?,
|
||||
events: EventChannel.EventSink?
|
||||
) {
|
||||
registrationEventsChannel = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
registrationEventsChannel = null
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
EventChannel(
|
||||
flutterPluginBinding.binaryMessenger,
|
||||
"liblinphone_flutter.nuark.xyz/call_events"
|
||||
).setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(
|
||||
arguments: Any?,
|
||||
events: EventChannel.EventSink?
|
||||
) {
|
||||
callEventsChannel = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
callEventsChannel = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onMethodCall(
|
||||
call: MethodCall,
|
||||
result: Result
|
||||
) {
|
||||
when (call.method) {
|
||||
"checkPermissions" -> {
|
||||
result.success(linphoneBridge.checkPermissions())
|
||||
}
|
||||
|
||||
"initialize" -> {
|
||||
try {
|
||||
linphoneBridge = LinphoneBridge(
|
||||
activity,
|
||||
this::acquireRemoteView,
|
||||
this::acquireLocalView,
|
||||
{ registrationState ->
|
||||
registrationEventsChannel?.success(registrationState)
|
||||
},
|
||||
{ callState ->
|
||||
callEventsChannel?.success(callState)
|
||||
}
|
||||
)
|
||||
linphoneBridge.initializeLinphone()
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "initialize: ${e.message}")
|
||||
result.error("error", e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
"register" -> {
|
||||
try {
|
||||
val username = call.argument<String>("username")!!
|
||||
val password = call.argument<String>("password")!!
|
||||
val serverIp = call.argument<String>("serverIp")!!
|
||||
val serverPort = call.argument<Int>("serverPort")!!
|
||||
linphoneBridge.register(username, password, serverIp, serverPort)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "register: ${e.message}")
|
||||
result.error("error", e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
"unregister" -> {
|
||||
linphoneBridge.unregister()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
"makeCall" -> {
|
||||
try {
|
||||
val callTo = call.argument<String>("callTo")!!
|
||||
val isVideoEnabled = call.argument<Boolean>("isVideoEnabled")!!
|
||||
linphoneBridge.makeCall(callTo, isVideoEnabled)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "makeCall: ${e.message}")
|
||||
result.error("error", e.message, e)
|
||||
}
|
||||
}
|
||||
|
||||
"answerCall" -> {
|
||||
val res = linphoneBridge.answerCall()
|
||||
result.success(res)
|
||||
}
|
||||
|
||||
"hangupCall" -> {
|
||||
val res = linphoneBridge.hangupCall()
|
||||
result.success(res)
|
||||
}
|
||||
|
||||
"inCall" -> {
|
||||
val inCall = linphoneBridge.inCall()
|
||||
result.success(inCall)
|
||||
}
|
||||
|
||||
"callType" -> {
|
||||
val callType = linphoneBridge.callType()
|
||||
result.success(callType.ordinal)
|
||||
}
|
||||
|
||||
"toggleVideo" -> {
|
||||
val enabled = linphoneBridge.toggleVideo()
|
||||
result.success(enabled)
|
||||
}
|
||||
|
||||
"toggleMicrophone" -> {
|
||||
val enabled = linphoneBridge.toggleMicrophone()
|
||||
result.success(enabled)
|
||||
}
|
||||
|
||||
"stop" -> {
|
||||
linphoneBridge.stop()
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
|
||||
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivityForConfigChanges() {
|
||||
}
|
||||
|
||||
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
|
||||
activity = binding.activity
|
||||
}
|
||||
|
||||
override fun onDetachedFromActivity() {
|
||||
}
|
||||
|
||||
internal fun acquireLocalView(): CaptureTextureView? {
|
||||
Log.i(TAG, "acquireLocalView: ${localViewCache.size}")
|
||||
if (localViewCache.isNotEmpty()) {
|
||||
val widget = localViewCache.get(0)
|
||||
return widget as CaptureTextureView
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
internal fun acquireRemoteView(): TextureView? {
|
||||
Log.i(TAG, "acquireRemoteView: ${remoteViewCache.size}")
|
||||
if (remoteViewCache.isNotEmpty()) {
|
||||
val widget = remoteViewCache.get(0)
|
||||
return widget as TextureView
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "LiblinphoneFlutterPlugin"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
package xyz.nuark.liblinphone_flutter
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import android.view.TextureView
|
||||
import androidx.core.content.ContextCompat
|
||||
import org.linphone.core.Account
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.MediaDirection
|
||||
import org.linphone.core.PayloadType
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.RegistrationState
|
||||
import org.linphone.mediastream.video.capture.CaptureTextureView
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
class LinphoneBridge(
|
||||
private val activity: Activity,
|
||||
private val remoteViewAcquisitor: () -> TextureView?,
|
||||
private val localViewAcquisitor: () -> CaptureTextureView?,
|
||||
private val onRegistrationStateChanged: (Int) -> Unit,
|
||||
private val onCallStateChanged: (Int) -> Unit
|
||||
) {
|
||||
private lateinit var core: Core
|
||||
private var isRegistered = false
|
||||
private var currentCall: Call? = null
|
||||
|
||||
private var registrationState: RegistrationState by Delegates.observable(RegistrationState.None) { _, oldValue, newValue ->
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"registrationState delegate: oldValue: $oldValue, newValue: $newValue"
|
||||
)
|
||||
onRegistrationStateChanged(newValue.ordinal)
|
||||
}
|
||||
private var callState: Call.State by Delegates.observable(Call.State.Idle) { _, oldValue, newValue ->
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"callState delegate: oldValue: $oldValue, newValue: $newValue"
|
||||
)
|
||||
onCallStateChanged(newValue.ordinal)
|
||||
}
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
// override fun onRegistrationStateChanged(
|
||||
// core: Core,
|
||||
// proxyConfig: ProxyConfig,
|
||||
// state: RegistrationState,
|
||||
// message: String
|
||||
// ) {
|
||||
// registrationState = state
|
||||
// when (state) {
|
||||
// RegistrationState.Ok -> {
|
||||
// isRegistered = true
|
||||
// }
|
||||
//
|
||||
// RegistrationState.Failed -> {
|
||||
// isRegistered = false
|
||||
// }
|
||||
//
|
||||
// RegistrationState.None -> {
|
||||
// isRegistered = false
|
||||
// }
|
||||
//
|
||||
// RegistrationState.Progress -> {
|
||||
// isRegistered = false
|
||||
// }
|
||||
//
|
||||
// else -> {}
|
||||
// }
|
||||
// }
|
||||
|
||||
override fun onAccountRegistrationStateChanged(
|
||||
core: Core,
|
||||
account: Account,
|
||||
state: RegistrationState?,
|
||||
message: String
|
||||
) {
|
||||
registrationState = state ?: RegistrationState.None
|
||||
when (state) {
|
||||
RegistrationState.Ok -> {
|
||||
isRegistered = true
|
||||
}
|
||||
|
||||
RegistrationState.Failed -> {
|
||||
isRegistered = false
|
||||
}
|
||||
|
||||
RegistrationState.None -> {
|
||||
isRegistered = false
|
||||
}
|
||||
|
||||
RegistrationState.Progress -> {
|
||||
isRegistered = false
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
callState = state
|
||||
when (state) {
|
||||
Call.State.IncomingReceived -> {
|
||||
currentCall = call
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"onCallStateChanged: INCOMING_RECEIVED $call"
|
||||
)
|
||||
}
|
||||
|
||||
Call.State.Connected -> {
|
||||
Log.i(LiblinphoneFlutterPlugin.TAG, "onCallStateChanged: CONNECTED $call")
|
||||
|
||||
remoteViewAcquisitor()?.let {
|
||||
call.nativeVideoWindowId = it
|
||||
} ?: run {
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"onCallStateChanged: CONNECTED remoteViewAcquisitor found nothing"
|
||||
)
|
||||
}
|
||||
localViewAcquisitor()?.let {
|
||||
core.nativePreviewWindowId = it
|
||||
} ?: run {
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"onCallStateChanged: CONNECTED localViewAcquisitor found nothing"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Call.State.StreamsRunning -> {
|
||||
Log.i(LiblinphoneFlutterPlugin.TAG, "onCallStateChanged: STREAMS_RUNNING $call")
|
||||
remoteViewAcquisitor()?.let {
|
||||
call.nativeVideoWindowId = it
|
||||
} ?: run {
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"onCallStateChanged: STREAMS_RUNNING remoteViewAcquisitor found nothing"
|
||||
)
|
||||
}
|
||||
localViewAcquisitor()?.let {
|
||||
core.nativePreviewWindowId = it
|
||||
} ?: run {
|
||||
Log.i(
|
||||
LiblinphoneFlutterPlugin.TAG,
|
||||
"onCallStateChanged: STREAMS_RUNNING localViewAcquisitor found nothing"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Call.State.End -> {
|
||||
currentCall = null
|
||||
}
|
||||
|
||||
Call.State.Error -> {
|
||||
currentCall = null
|
||||
}
|
||||
|
||||
Call.State.Released -> {
|
||||
currentCall = null
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val neededPermissions = arrayOf(
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.CAMERA,
|
||||
Manifest.permission.MODIFY_AUDIO_SETTINGS
|
||||
)
|
||||
|
||||
fun checkPermissions(): Boolean {
|
||||
val missingPermissions = neededPermissions.filter { permission ->
|
||||
ContextCompat.checkSelfPermission(
|
||||
activity,
|
||||
permission
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
}.toTypedArray()
|
||||
if (missingPermissions.isNotEmpty()) {
|
||||
activity.requestPermissions(missingPermissions, 1)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun initializeLinphone() {
|
||||
val factory = Factory.instance()
|
||||
core = factory.createCore(null, null, activity.baseContext)
|
||||
core.addListener(coreListener)
|
||||
|
||||
// Enable video
|
||||
core.isVideoCaptureEnabled = true
|
||||
core.isVideoDisplayEnabled = true
|
||||
core.videoActivationPolicy.automaticallyInitiate = true
|
||||
core.videoActivationPolicy.automaticallyAccept = true
|
||||
|
||||
// https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus1000/sw/4_0/qos/configuration/guide/nexus1000v_qos/qos_6dscp_val.pdf
|
||||
core.sipDscp = 26 // AF31
|
||||
core.audioDscp = 46 // EF
|
||||
core.videoDscp = 36 // AF42
|
||||
|
||||
core.isVideoAdaptiveJittcompEnabled = true
|
||||
// core.isEchoCancellationEnabled = true // eh results, bad for audio
|
||||
|
||||
// good enough default
|
||||
core.uploadBandwidth = 512
|
||||
core.downloadBandwidth = 1500
|
||||
|
||||
val preferredAudio = listOf("opus", "pcmu", "pcma") // in order of preference
|
||||
val preferredVideo = listOf("h264", "vp8")
|
||||
|
||||
core.audioPayloadTypes.forEach { pt: PayloadType ->
|
||||
val mime = pt.mimeType.lowercase()
|
||||
val enable = preferredAudio.contains(mime)
|
||||
pt.enable(enable)
|
||||
}
|
||||
core.videoPayloadTypes.forEach { pt: PayloadType ->
|
||||
val mime = pt.mimeType.lowercase()
|
||||
val enable = preferredVideo.contains(mime)
|
||||
pt.enable(enable)
|
||||
}
|
||||
|
||||
core.getPayloadType("opus", -1, 0)?.let { it.normalBitrate = 32 } // 32 kbps for Opus
|
||||
core.getPayloadType("h264", -1, 0)?.let { it.normalBitrate = 600 } // 600 kbps for H264
|
||||
core.getPayloadType("vp8", -1, 0)?.let { it.normalBitrate = 600 } // 600 kbps for VP8
|
||||
|
||||
val preferredVidDef = factory.createVideoDefinition(720, 1280)
|
||||
core.preferredVideoDefinition = preferredVidDef
|
||||
core.preferredFramerate = 30f
|
||||
|
||||
core.start()
|
||||
}
|
||||
|
||||
fun register(username: String, password: String, serverIp: String, serverPort: Int) {
|
||||
val factory = Factory.instance()
|
||||
val authInfo = factory.createAuthInfo(username, null, password, null, null, serverIp)
|
||||
core.addAuthInfo(authInfo)
|
||||
|
||||
val identity = factory.createAddress("sip:$username@$serverIp")
|
||||
val server = factory.createAddress("sip:$serverIp:$serverPort")
|
||||
|
||||
val accountParams = core.createAccountParams().let {
|
||||
it.identityAddress = identity
|
||||
it.serverAddress = server
|
||||
it.isRegisterEnabled = true
|
||||
it
|
||||
}
|
||||
|
||||
val account = core.createAccount(accountParams)
|
||||
core.addAccount(account)
|
||||
core.defaultAccount = account
|
||||
}
|
||||
|
||||
fun unregister() {
|
||||
core.clearAccounts()
|
||||
core.clearAllAuthInfo()
|
||||
}
|
||||
|
||||
fun makeCall(callTo: String, isVideoEnabled: Boolean) {
|
||||
if (!isRegistered) {
|
||||
throw IllegalStateException("Not registered")
|
||||
}
|
||||
|
||||
val factory = Factory.instance()
|
||||
val remoteAddress = factory.createAddress(callTo)
|
||||
val callParams = core.createCallParams(null)
|
||||
if (remoteAddress == null) {
|
||||
throw IllegalStateException("Failed to create remote address")
|
||||
}
|
||||
if (callParams == null) {
|
||||
throw IllegalStateException("Failed to create call params")
|
||||
}
|
||||
|
||||
callParams.let {
|
||||
it.isVideoEnabled = isVideoEnabled
|
||||
it.videoDirection =
|
||||
if (isVideoEnabled) MediaDirection.SendRecv else MediaDirection.Inactive
|
||||
}
|
||||
|
||||
currentCall = core.inviteAddressWithParams(remoteAddress, callParams)
|
||||
}
|
||||
|
||||
fun answerCall(): Boolean {
|
||||
val res = currentCall?.let { call ->
|
||||
val callParams = core.createCallParams(call)
|
||||
callParams?.let {
|
||||
it.isVideoEnabled = true
|
||||
it.videoDirection = MediaDirection.SendRecv
|
||||
}
|
||||
call.acceptWithParams(callParams)
|
||||
|
||||
return@let true
|
||||
}
|
||||
|
||||
return res ?: false
|
||||
}
|
||||
|
||||
fun hangupCall(): Boolean {
|
||||
return currentCall?.terminate() == 0
|
||||
}
|
||||
|
||||
fun toggleVideo(): Boolean {
|
||||
currentCall?.let { call ->
|
||||
val params = core.createCallParams(call)
|
||||
params?.isVideoEnabled = !call.currentParams.isVideoEnabled
|
||||
call.update(params)
|
||||
}
|
||||
|
||||
return currentCall?.currentParams?.isVideoEnabled == true
|
||||
}
|
||||
|
||||
fun toggleMicrophone(): Boolean {
|
||||
core.isMicEnabled = !core.isMicEnabled
|
||||
return core.isMicEnabled
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
core.stop()
|
||||
}
|
||||
|
||||
fun inCall(): Boolean {
|
||||
return currentCall != null
|
||||
}
|
||||
|
||||
enum class CallType {
|
||||
Audio,
|
||||
Video,
|
||||
Unknown
|
||||
}
|
||||
|
||||
fun callType(): CallType {
|
||||
if (currentCall?.currentParams != null) {
|
||||
if (currentCall?.currentParams?.isVideoEnabled == true) {
|
||||
return CallType.Video
|
||||
} else {
|
||||
return CallType.Audio
|
||||
}
|
||||
}
|
||||
if (currentCall?.currentParams?.isVideoEnabled == true) {
|
||||
return CallType.Video
|
||||
}
|
||||
return CallType.Unknown
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package xyz.nuark.liblinphone_flutter.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import org.linphone.mediastream.video.capture.CaptureTextureView
|
||||
|
||||
internal class LocalView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
|
||||
private val textureView: CaptureTextureView
|
||||
|
||||
override fun getView(): View {
|
||||
return textureView
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
textureView.surfaceTexture?.release()
|
||||
}
|
||||
|
||||
init {
|
||||
textureView = CaptureTextureView(context)
|
||||
textureView.id = id
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package xyz.nuark.liblinphone_flutter.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
class LocalViewFactory(internal val cacher: (View) -> Unit) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
|
||||
val creationParams = args as Map<String?, Any?>?
|
||||
return LocalView(context, viewId, creationParams).also {
|
||||
cacher(it.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package xyz.nuark.liblinphone_flutter.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.view.TextureView
|
||||
import android.view.View
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
|
||||
internal class RemoteView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
|
||||
private val textureView: TextureView
|
||||
|
||||
override fun getView(): View {
|
||||
return textureView
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
textureView.surfaceTexture?.release()
|
||||
}
|
||||
|
||||
init {
|
||||
textureView = TextureView(context)
|
||||
textureView.id = id
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package xyz.nuark.liblinphone_flutter.views
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import io.flutter.plugin.platform.PlatformView
|
||||
import io.flutter.plugin.platform.PlatformViewFactory
|
||||
|
||||
class RemoteViewFactory(internal val cacher: (View) -> Unit) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
|
||||
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
|
||||
val creationParams = args as Map<String?, Any?>?
|
||||
return RemoteView(context, viewId, creationParams).also {
|
||||
cacher(it.view)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.example.liblinphone_flutter
|
||||
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import org.mockito.Mockito
|
||||
import kotlin.test.Test
|
||||
|
||||
/*
|
||||
* This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
|
||||
*
|
||||
* Once you have built the plugin's example app, you can run these tests from the command
|
||||
* line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
|
||||
* you can run them directly from IDEs that support JUnit such as Android Studio.
|
||||
*/
|
||||
|
||||
internal class LiblinphoneFlutterPluginTest {
|
||||
@Test
|
||||
fun onMethodCall_getPlatformVersion_returnsExpectedValue() {
|
||||
val plugin = LiblinphoneFlutterPlugin()
|
||||
|
||||
val call = MethodCall("getPlatformVersion", null)
|
||||
val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java)
|
||||
plugin.onMethodCall(call, mockResult)
|
||||
|
||||
Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue