android: types and messages for webrtc calls (#609)

* android: webrtc calls

* string localizations, more types
This commit is contained in:
Evgeny Poberezkin
2022-05-07 13:23:20 +01:00
committed by GitHub
parent 29990765e7
commit fcb5c69281
12 changed files with 195 additions and 142 deletions

View File

@@ -17,8 +17,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.work.* import androidx.work.*
import chat.simplex.app.model.ChatModel import chat.simplex.app.model.*
import chat.simplex.app.model.NtfManager
import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.SplashView import chat.simplex.app.views.SplashView
import chat.simplex.app.views.WelcomeView import chat.simplex.app.views.WelcomeView
@@ -29,7 +28,6 @@ import chat.simplex.app.views.helpers.*
import chat.simplex.app.views.newchat.connectViaUri import chat.simplex.app.views.newchat.connectViaUri
import chat.simplex.app.views.newchat.withUriAction import chat.simplex.app.views.newchat.withUriAction
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
//import kotlinx.serialization.decodeFromString //import kotlinx.serialization.decodeFromString
class MainActivity: ComponentActivity() { class MainActivity: ComponentActivity() {

View File

@@ -10,6 +10,7 @@ import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.app.R import chat.simplex.app.R
import chat.simplex.app.ui.theme.SecretColor import chat.simplex.app.ui.theme.SecretColor
import chat.simplex.app.ui.theme.SimplexBlue import chat.simplex.app.ui.theme.SimplexBlue
import chat.simplex.app.views.call.*
import chat.simplex.app.views.helpers.generalGetString import chat.simplex.app.views.helpers.generalGetString
import kotlinx.datetime.* import kotlinx.datetime.*
import kotlinx.serialization.* import kotlinx.serialization.*
@@ -19,23 +20,29 @@ import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
class ChatModel(val controller: ChatController) { class ChatModel(val controller: ChatController) {
var currentUser = mutableStateOf<User?>(null) val currentUser = mutableStateOf<User?>(null)
var userCreated = mutableStateOf<Boolean?>(null) val userCreated = mutableStateOf<Boolean?>(null)
var chats = mutableStateListOf<Chat>() val chats = mutableStateListOf<Chat>()
var chatId = mutableStateOf<String?>(null) val chatId = mutableStateOf<String?>(null)
var chatItems = mutableStateListOf<ChatItem>() val chatItems = mutableStateListOf<ChatItem>()
var connReqInvitation: String? = null var connReqInvitation: String? = null
var terminalItems = mutableStateListOf<TerminalItem>() val terminalItems = mutableStateListOf<TerminalItem>()
var userAddress = mutableStateOf<String?>(null) val userAddress = mutableStateOf<String?>(null)
var userSMPServers = mutableStateOf<(List<String>)?>(null) val userSMPServers = mutableStateOf<(List<String>)?>(null)
// set when app opened from external intent // set when app opened from external intent
var clearOverlays = mutableStateOf<Boolean>(false) val clearOverlays = mutableStateOf<Boolean>(false)
// set when app is opened via contact or invitation URI // set when app is opened via contact or invitation URI
var appOpenUrl = mutableStateOf<Uri?>(null) val appOpenUrl = mutableStateOf<Uri?>(null)
var runServiceInBackground = mutableStateOf(true) val runServiceInBackground = mutableStateOf(true)
// current WebRTC call
val callInvitations = mutableStateMapOf<String, CallInvitation>()
val activeCallInvitation = mutableStateOf<ContactRef?>(null)
val activeCall = mutableStateOf<Call?>(null)
val callCommand = mutableStateOf<WCallCommand?>(null)
fun updateUserProfile(profile: Profile) { fun updateUserProfile(profile: Profile) {
val user = currentUser.value val user = currentUser.value
@@ -654,6 +661,13 @@ data class ChatItem (
else -> false else -> false
} }
val isCall: Boolean get() =
when (content) {
is CIContent.SndCall -> true
is CIContent.RcvCall -> true
else -> false
}
companion object { companion object {
fun getSampleData( fun getSampleData(
id: Long = 1, id: Long = 1,
@@ -709,26 +723,16 @@ data class ChatItem (
@Serializable @Serializable
sealed class CIDirection { sealed class CIDirection {
abstract val sent: Boolean @Serializable @SerialName("directSnd") class DirectSnd: CIDirection()
@Serializable @SerialName("directRcv") class DirectRcv: CIDirection()
@Serializable @SerialName("groupSnd") class GroupSnd: CIDirection()
@Serializable @SerialName("groupRcv") class GroupRcv(val groupMember: GroupMember): CIDirection()
@Serializable @SerialName("directSnd") val sent: Boolean get() = when(this) {
class DirectSnd: CIDirection() { is DirectSnd -> true
override val sent get() = true is DirectRcv -> false
} is GroupSnd -> true
is GroupRcv -> false
@Serializable @SerialName("directRcv")
class DirectRcv: CIDirection() {
override val sent get() = false
}
@Serializable @SerialName("groupSnd")
class GroupSnd: CIDirection() {
override val sent get() = true
}
@Serializable @SerialName("groupRcv")
class GroupRcv(val groupMember: GroupMember): CIDirection() {
override val sent get() = false
} }
} }
@@ -775,23 +779,12 @@ fun getTimestampText(t: Instant): String {
@Serializable @Serializable
sealed class CIStatus { sealed class CIStatus {
@Serializable @SerialName("sndNew") @Serializable @SerialName("sndNew") class SndNew: CIStatus()
class SndNew: CIStatus() @Serializable @SerialName("sndSent") class SndSent: CIStatus()
@Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus()
@Serializable @SerialName("sndSent") @Serializable @SerialName("sndError") class SndError(val agentError: AgentErrorType): CIStatus()
class SndSent: CIStatus() @Serializable @SerialName("rcvNew") class RcvNew: CIStatus()
@Serializable @SerialName("rcvRead") class RcvRead: CIStatus()
@Serializable @SerialName("sndErrorAuth")
class SndErrorAuth: CIStatus()
@Serializable @SerialName("sndError")
class SndError(val agentError: AgentErrorType): CIStatus()
@Serializable @SerialName("rcvNew")
class RcvNew: CIStatus()
@Serializable @SerialName("rcvRead")
class RcvRead: CIStatus()
} }
@Serializable @Serializable
@@ -806,29 +799,22 @@ interface ItemContent {
@Serializable @Serializable
sealed class CIContent: ItemContent { sealed class CIContent: ItemContent {
abstract override val text: String
abstract val msgContent: MsgContent? abstract val msgContent: MsgContent?
@Serializable @SerialName("sndMsgContent") @Serializable @SerialName("sndMsgContent") class SndMsgContent(override val msgContent: MsgContent): CIContent()
class SndMsgContent(override val msgContent: MsgContent): CIContent() { @Serializable @SerialName("rcvMsgContent") class RcvMsgContent(override val msgContent: MsgContent): CIContent()
override val text get() = msgContent.text @Serializable @SerialName("sndDeleted") class SndDeleted(val deleteMode: CIDeleteMode): CIContent() { override val msgContent get() = null }
} @Serializable @SerialName("rcvDeleted") class RcvDeleted(val deleteMode: CIDeleteMode): CIContent() { override val msgContent get() = null }
@Serializable @SerialName("sndCall") class SndCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent get() = null }
@Serializable @SerialName("rcvCall") class RcvCall(val status: CICallStatus, val duration: Int): CIContent() { override val msgContent get() = null }
@Serializable @SerialName("rcvMsgContent") override val text: String get() = when(this) {
class RcvMsgContent(override val msgContent: MsgContent): CIContent() { is SndMsgContent -> msgContent.text
override val text get() = msgContent.text is RcvMsgContent -> msgContent.text
} is SndDeleted -> generalGetString(R.string.deleted_description)
is RcvDeleted -> generalGetString(R.string.deleted_description)
@Serializable @SerialName("sndDeleted") is SndCall -> status.text(duration)
class SndDeleted(val deleteMode: CIDeleteMode): CIContent() { is RcvCall -> status.text(duration)
override val text get() = generalGetString(R.string.deleted_description)
override val msgContent get() = null
}
@Serializable @SerialName("rcvDeleted")
class RcvDeleted(val deleteMode: CIDeleteMode): CIContent() {
override val text get() = generalGetString(R.string.deleted_description)
override val msgContent get() = null
} }
} }
@@ -904,20 +890,11 @@ enum class CIFileStatus {
sealed class MsgContent { sealed class MsgContent {
abstract val text: String abstract val text: String
@Serializable(with = MsgContentSerializer::class) @Serializable(with = MsgContentSerializer::class) class MCText(override val text: String): MsgContent()
class MCText(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCLink(override val text: String, val preview: LinkPreview): MsgContent()
@Serializable(with = MsgContentSerializer::class) class MCImage(override val text: String, val image: String): MsgContent()
@Serializable(with = MsgContentSerializer::class) @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent()
class MCLink(override val text: String, val preview: LinkPreview): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
@Serializable(with = MsgContentSerializer::class)
class MCImage(override val text: String, val image: String): MsgContent()
@Serializable(with = MsgContentSerializer::class)
class MCFile(override val text: String): MsgContent()
@Serializable(with = MsgContentSerializer::class)
class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent()
val cmdString: String get() = when (this) { val cmdString: String get() = when (this) {
is MCText -> "text $text" is MCText -> "text $text"
@@ -1073,3 +1050,28 @@ class SndFileTransfer() {}
@Serializable @Serializable
class FileTransferMeta() {} class FileTransferMeta() {}
@Serializable
enum class CICallStatus {
@SerialName("pending") Pending,
@SerialName("missed") Missed,
@SerialName("rejected") Rejected,
@SerialName("accepted") Accepted,
@SerialName("negotiated") Negotiated,
@SerialName("progress") Progress,
@SerialName("ended") Ended,
@SerialName("error") Error;
fun text(sec: Int): String = when (this) {
Pending -> generalGetString(R.string.callstatus_calling)
Missed -> generalGetString(R.string.callstatus_missed)
Rejected -> generalGetString(R.string.callstatus_rejected)
Accepted -> generalGetString(R.string.callstatus_accepted)
Negotiated -> generalGetString(R.string.callstatus_connecting)
Progress -> generalGetString(R.string.callstatus_in_progress)
Ended -> String.format(generalGetString(R.string.callstatus_ended), duration(sec))
Error -> generalGetString(R.string.callstatus_error)
}
fun duration(sec: Int): String = "%02d:%02d".format(sec / 60, sec % 60)
}

View File

@@ -47,7 +47,7 @@ open class ChatController(private val ctrl: ChatCtrl, private val ntfManager: Nt
val chats = apiGetChats() val chats = apiGetChats()
chatModel.chats.clear() chatModel.chats.clear()
chatModel.chats.addAll(chats) chatModel.chats.addAll(chats)
chatModel.currentUser = mutableStateOf(user) chatModel.currentUser.value = user
chatModel.userCreated.value = true chatModel.userCreated.value = true
Log.d(TAG, "started chat") Log.d(TAG, "started chat")
} catch(e: Error) { } catch(e: Error) {
@@ -713,7 +713,7 @@ class APIResponse(val resp: CR, val corr: String? = null) {
json.decodeFromString(str) json.decodeFromString(str)
} catch(e: Exception) { } catch(e: Exception) {
try { try {
Log.d(TAG, e.localizedMessage) Log.d(TAG, e.localizedMessage ?: "")
val data = json.parseToJsonElement(str).jsonObject val data = json.parseToJsonElement(str).jsonObject
APIResponse( APIResponse(
resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)), resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)),

View File

@@ -27,7 +27,7 @@ import chat.simplex.app.TAG
import chat.simplex.app.views.helpers.TextEditor import chat.simplex.app.views.helpers.TextEditor
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
//@SuppressLint("JavascriptInterface") @SuppressLint("SetJavaScriptEnabled")
@Composable @Composable
fun VideoCallView(close: () -> Unit) { fun VideoCallView(close: () -> Unit) {
BackHandler(onBack = close) BackHandler(onBack = close)
@@ -57,8 +57,7 @@ fun VideoCallView(close: () -> Unit) {
} }
} }
val localContext = LocalContext.current val localContext = LocalContext.current
val iceCandidateCommand = remember { mutableStateOf("") } val commandToShow = remember { mutableStateOf("processCommand({command: {type: 'start', media: 'video'}})") } //, aesKey: 'FwW+t6UbnwHoapYOfN4mUBUuqR7UtvYWxW16iBqM29U='})") }
val commandToShow = remember { mutableStateOf("processCommand({type: 'start', media: 'video', aesKey: 'FwW+t6UbnwHoapYOfN4mUBUuqR7UtvYWxW16iBqM29U='})") }
val assetLoader = WebViewAssetLoader.Builder() val assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(localContext)) .addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(localContext))
.build() .build()
@@ -96,9 +95,7 @@ fun VideoCallView(close: () -> Unit) {
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean { override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
val rtnValue = super.onConsoleMessage(consoleMessage) val rtnValue = super.onConsoleMessage(consoleMessage)
val msg = consoleMessage?.message() as String val msg = consoleMessage?.message() as String
if (msg.startsWith("{\"action\":\"processIceCandidates\"")) { if (msg.startsWith("{")) {
iceCandidateCommand.value = "processCommand($msg)"
} else if (msg.startsWith("{")) {
commandToShow.value = "processCommand($msg)" commandToShow.value = "processCommand($msg)"
} }
return rtnValue return rtnValue
@@ -142,15 +139,9 @@ fun VideoCallView(close: () -> Unit) {
wv.evaluateJavascript(commandToShow.value, null) wv.evaluateJavascript(commandToShow.value, null)
commandToShow.value = "" commandToShow.value = ""
}) {Text("Send")} }) {Text("Send")}
Button( onClick = {
commandToShow.value = iceCandidateCommand.value
}) {Text("ICE")}
Button( onClick = { Button( onClick = {
commandToShow.value = "" commandToShow.value = ""
}) {Text("Clear")} }) {Text("Clear")}
Button( onClick = {
wv.evaluateJavascript("endCall()", null)
}) {Text("End Call")}
} }
} }
} }

View File

@@ -1,10 +1,47 @@
package chat.simplex.app.views.call package chat.simplex.app.views.call
import chat.simplex.app.model.CR import chat.simplex.app.R
import chat.simplex.app.model.User import chat.simplex.app.model.*
import chat.simplex.app.views.helpers.generalGetString
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
class Call(
val contact: Contact,
val callState: CallState,
val localMedia: CallMediaType,
val localCapabilities: CallCapabilities? = null,
val peerMedia: CallMediaType? = null,
val sharedKey: String? = null,
val audioEnabled: Boolean = true,
val videoEnabled: Boolean = localMedia == CallMediaType.Video
) {
val encrypted: Boolean get() = (localCapabilities?.encryption ?: false) && sharedKey != null
}
enum class CallState {
WaitCapabilities,
InvitationSent,
InvitationReceived,
OfferSent,
OfferReceived,
Negotiated,
Connected;
val text: String get() = when(this) {
WaitCapabilities -> generalGetString(R.string.callstate_starting)
InvitationSent -> generalGetString(R.string.callstate_waiting_for_answer)
InvitationReceived -> generalGetString(R.string.callstate_starting)
OfferSent -> generalGetString(R.string.callstate_waiting_for_confirmation)
OfferReceived -> generalGetString(R.string.callstate_received_answer)
Negotiated -> generalGetString(R.string.callstate_connecting)
Connected -> generalGetString(R.string.callstate_connected)
}
}
@Serializable class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand?)
@Serializable @Serializable
sealed class WCallCommand { sealed class WCallCommand {
@Serializable @SerialName("capabilities") class Capabilities(): WCallCommand() @Serializable @SerialName("capabilities") class Capabilities(): WCallCommand()
@@ -12,6 +49,7 @@ sealed class WCallCommand {
@Serializable @SerialName("accept") class Accept(val offer: String, val iceCandidates: List<String>, val media: CallMediaType, val aesKey: String? = null): WCallCommand() @Serializable @SerialName("accept") class Accept(val offer: String, val iceCandidates: List<String>, val media: CallMediaType, val aesKey: String? = null): WCallCommand()
@Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: List<String>): WCallCommand() @Serializable @SerialName("answer") class Answer (val answer: String, val iceCandidates: List<String>): WCallCommand()
@Serializable @SerialName("ice") class Ice(val iceCandidates: List<String>): WCallCommand() @Serializable @SerialName("ice") class Ice(val iceCandidates: List<String>): WCallCommand()
@Serializable @SerialName("media") class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("end") class End(): WCallCommand() @Serializable @SerialName("end") class End(): WCallCommand()
} }
@@ -30,17 +68,12 @@ sealed class WCallResponse {
@Serializable class Invalid(val str: String): WCallResponse() @Serializable class Invalid(val str: String): WCallResponse()
} }
@Serializable @Serializable class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession)
class WebRTCCallOffer(val callType: CallType, val rtcSession: WebRTCSession) @Serializable class WebRTCSession(val rtcSession: String, val rtcIceCandidates: List<String>)
@Serializable class WebRTCExtraInfo(val rtcIceCandidates: List<String>)
@Serializable @Serializable class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
class WebRTCSession(val rtcSession: String, val rtcIceCandidates: List<String>) @Serializable class CallInvitation(val peerMedia: CallMediaType?, val sharedKey: String?)
@Serializable class CallCapabilities(val encryption: Boolean)
@Serializable
class WebRTCExtraInfo(val rtcIceCandidates: List<String>)
@Serializable
class CallType(val media: CallMediaType, val capabilities: CallCapabilities)
enum class WebRTCCallStatus(val status: String) { enum class WebRTCCallStatus(val status: String) {
Connected("connected"), Connected("connected"),
@@ -54,9 +87,6 @@ enum class CallMediaType(val media: String) {
Audio("audio") Audio("audio")
} }
@Serializable
class CallCapabilities(val encryption: Boolean)
@Serializable @Serializable
class ConnectionState( class ConnectionState(
val connectionState: String, val connectionState: String,

View File

@@ -62,6 +62,8 @@ fun ChatItemView(
} }
} else if (cItem.isDeletedContent) { } else if (cItem.isDeletedContent) {
DeletedItemView(cItem, showMember = showMember) DeletedItemView(cItem, showMember = showMember)
} else if (cItem.isCall) {
FramedItemView(user, cItem, uriHandler, showMember = showMember, showMenu, receiveFile)
} }
if (cItem.isMsgContent) { if (cItem.isMsgContent) {
DropdownMenu( DropdownMenu(

View File

@@ -39,7 +39,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
suspend fun openChat(chatModel: ChatModel, cInfo: ChatInfo) { suspend fun openChat(chatModel: ChatModel, cInfo: ChatInfo) {
val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId)
if (chat != null) { if (chat != null) {
chatModel.chatItems = chat.chatItems.toMutableStateList() chatModel.chatItems.clear()
chatModel.chatItems.addAll(chat.chatItems)
chatModel.chatId.value = cInfo.id chatModel.chatId.value = cInfo.id
} }
} }

View File

@@ -275,4 +275,21 @@
<string name="you_can_also_connect_by_clicking_the_link">Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Open in mobile app</b>.</string> <string name="you_can_also_connect_by_clicking_the_link">Вы также можете соединиться, открыв ссылку там, где вы её получили. Если ссылка откроется в браузере, нажмите кнопку <b>Open in mobile app</b>.</string>
<string name="add_contact_to_start_new_chat">Добавьте контакт, чтобы начать разговор:</string> <string name="add_contact_to_start_new_chat">Добавьте контакт, чтобы начать разговор:</string>
<!-- CICallStatus -->
<string name="callstatus_calling">входящий звонок…</string>
<string name="callstatus_missed">пропущенный звонок</string>
<string name="callstatus_rejected">отклоненный звонок</string>
<string name="callstatus_accepted">принятый звонок</string>
<string name="callstatus_connecting">соединяется…</string>
<string name="callstatus_in_progress">активный звонок</string>
<string name="callstatus_ended">звонок завершён <xliff:g id="duration" example="01:15">%1$s!</xliff:g></string>
<string name="callstatus_error">ошибка соединения</string>
<!-- CallState -->
<string name="callstate_starting">инициализация…</string>
<string name="callstate_waiting_for_answer">ожидается ответ…</string>
<string name="callstate_waiting_for_confirmation">ожидается подтверждение…</string>
<string name="callstate_received_answer">получен ответ…</string>
<string name="callstate_connecting">соединяется…</string>
<string name="callstate_connected">соединено</string>
</resources> </resources>

View File

@@ -276,4 +276,21 @@
<string name="colored">colored</string> <string name="colored">colored</string>
<string name="secret">secret</string> <string name="secret">secret</string>
<!-- CICallStatus -->
<string name="callstatus_calling">calling…</string>
<string name="callstatus_missed">missed</string>
<string name="callstatus_rejected">rejected</string>
<string name="callstatus_accepted">accepted</string>
<string name="callstatus_connecting">connecting…</string>
<string name="callstatus_in_progress">in progress</string>
<string name="callstatus_ended">ended <xliff:g id="duration" example="01:15">%1$s!</xliff:g></string>
<string name="callstatus_error">error</string>
<!-- CallState -->
<string name="callstate_starting">starting…</string>
<string name="callstate_waiting_for_answer">waiting for answer…</string>
<string name="callstate_waiting_for_confirmation">waiting for confirmation…</string>
<string name="callstate_received_answer">received answer…</string>
<string name="callstate_connecting">connecting…</string>
<string name="callstate_connected">connected</string>
</resources> </resources>

View File

@@ -725,7 +725,6 @@ enum MsgContent {
} }
} }
// TODO define Encodable
extension MsgContent: Decodable { extension MsgContent: Decodable {
init(from decoder: Decoder) throws { init(from decoder: Decoder) throws {
do { do {
@@ -863,15 +862,14 @@ enum CICallStatus: String, Decodable {
func text(_ sec: Int) -> String { func text(_ sec: Int) -> String {
switch self { switch self {
case .pending: return "calling..." case .pending: return NSLocalizedString("calling…", comment: "call status")
case .negotiated: return "connecting..." case .missed: return NSLocalizedString("missed…", comment: "call status")
case .progress: return "in progress" case .rejected: return NSLocalizedString("rejected", comment: "call status")
case .ended: return "ended \(duration(sec))" case .accepted: return NSLocalizedString("accepted", comment: "call status")
default: return self.rawValue case .negotiated: return NSLocalizedString("connecting…", comment: "call status")
case .progress: return NSLocalizedString("in progress", comment: "call status")
case .ended: return String.localizedStringWithFormat(NSLocalizedString("ended %02d:%02d", comment: "call status"), sec / 60, sec % 60)
case .error: return NSLocalizedString("error", comment: "call status")
} }
} }
func duration(_ sec: Int) -> String {
String(format: "%02d:%02d", sec / 60, sec % 60)
}
} }

View File

@@ -81,12 +81,12 @@ enum CallState {
var text: LocalizedStringKey { var text: LocalizedStringKey {
switch self { switch self {
case .waitCapabilities: return "starting..." case .waitCapabilities: return "starting"
case .invitationSent: return "waiting for answer..." case .invitationSent: return "waiting for answer"
case .invitationReceived: return "starting..." case .invitationReceived: return "starting"
case .offerSent: return "waiting for confirmation..." case .offerSent: return "waiting for confirmation"
case .offerReceived: return "received answer..." case .offerReceived: return "received answer"
case .negotiated: return "connecting..." case .negotiated: return "connecting"
case .connected: return "connected" case .connected: return "connected"
} }
} }

View File

@@ -91,14 +91,15 @@ struct WebRTCView: UIViewRepresentable {
} }
//struct CallViewDebug: View { //struct CallViewDebug: View {
// @State var coordinator: WebRTCCoordinator? = nil // @State private var coordinator: WebRTCCoordinator? = nil
// @State var commandStr = "" // @State private var commandStr = ""
// @State private var webViewMsg: WCallResponse? = nil // @State private var webViewReady: Bool = false
// @State private var webViewMsg: WVAPIMessage? = nil
// @FocusState private var keyboardVisible: Bool // @FocusState private var keyboardVisible: Bool
// //
// var body: some View { // var body: some View {
// VStack(spacing: 30) { // VStack(spacing: 30) {
// WebRTCView(coordinator: $coordinator, webViewMsg: $webViewMsg).frame(maxHeight: 260) // WebRTCView(coordinator: $coordinator, webViewReady: $webViewReady, webViewMsg: $webViewMsg).frame(maxHeight: 260)
// .onChange(of: webViewMsg) { _ in // .onChange(of: webViewMsg) { _ in
// if let resp = webViewMsg { // if let resp = webViewMsg {
// commandStr = encodeJSON(resp) // commandStr = encodeJSON(resp)
@@ -126,13 +127,9 @@ struct WebRTCView: UIViewRepresentable {
// commandStr = "" // commandStr = ""
// } // }
// Button("Send") { // Button("Send") {
// do { // if let c = coordinator,
// if let c = coordinator, // let command: WCallCommand = decodeJSON(commandStr) {
// let command: WCallCommand = decodeJSON(commandStr) { // c.sendCommand(command: command)
// c.sendCommand(command: command)
// }
// } catch {
// print(error)
// } // }
// } // }
// } // }