This commit is contained in:
Avently
2024-09-04 00:58:06 +09:00
parent 26e41e9283
commit 5a21f9c668
19 changed files with 316 additions and 208 deletions
@@ -119,7 +119,7 @@ actual fun ActiveCallView() {
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
val callType = CallType(call.initialCallType, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
@@ -132,7 +132,7 @@ actual fun ActiveCallView() {
activeCallWaitDeliveryReceipt(scope)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Starting is delayed to make Android <= 11 working good with Bluetooth
@@ -185,16 +185,19 @@ actual fun ActiveCallView() {
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
is WCallCommand.Media -> {
updateActiveCall(call) {
when (cmd.media) {
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable, localMedia = if (cmd.enable) CallMediaType.Video else CallMediaType.Audio)
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
val sources = it.localMediaSources
when (cmd.source) {
CallMediaSource.Mic -> it.copy(localMediaSources = sources.copy(mic = cmd.enable))
CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable))
CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
}
}
}
is WCallCommand.Camera -> {
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
if (!call.audioEnabled) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
if (!call.localMediaSources.mic) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
}
}
is WCallCommand.End -> {
@@ -248,9 +251,9 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceM
devices = remember { callAudioDeviceManager.devices }.value,
currentDevice = remember { callAudioDeviceManager.currentDevice },
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = !call.localMediaSources.camera)) },
toggleSound = {
val enableSpeaker = callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
val preferredInternalDevice = callAudioDeviceManager.devices.value.firstOrNull { it.type == if (enableSpeaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }
@@ -365,7 +368,7 @@ private fun ActiveCallOverlayLayout(
IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
}
if (call.videoEnabled) {
if (call.localMediaSources.camera) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
} else {
@@ -390,7 +393,7 @@ private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, e
@Composable
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
if (call.audioEnabled) {
if (call.localMediaSources.mic) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
} else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio)
@@ -762,7 +765,7 @@ fun PreviewActiveCallOverlayVideo() {
userProfile = Profile.sampleData,
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
initialCallType = CallMediaType.Video,
peerMediaSources = CallMediaSources(),
callUUID = "",
connectionInfo = ConnectionInfo(
@@ -792,7 +795,7 @@ fun PreviewActiveCallOverlayAudio() {
userProfile = Profile.sampleData,
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
initialCallType = CallMediaType.Audio,
peerMediaSources = CallMediaSources(),
callUUID = "",
connectionInfo = ConnectionInfo(
@@ -54,8 +54,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
val media = call.peerMediaSources ?: call.localMedia
if (media == CallMediaType.Video) {
if (call.supportsVideo()) {
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
} else {
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
@@ -49,7 +49,7 @@ class CallManager(val chatModel: ChatModel) {
contact = invitation.contact,
callUUID = invitation.callUUID,
callState = CallState.InvitationAccepted,
localMedia = invitation.callType.media,
initialCallType = invitation.callType.media,
sharedKey = invitation.sharedKey,
)
showCallView.value = true
@@ -2,6 +2,7 @@ package chat.simplex.common.views.call
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
@@ -15,12 +16,11 @@ data class Call(
val contact: Contact,
val callUUID: String?,
val callState: CallState,
val localMedia: CallMediaType,
val initialCallType: CallMediaType,
val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video && appPlatform.isAndroid),
val localCapabilities: CallCapabilities? = null,
val peerMediaSources: CallMediaSources = CallMediaSources(),
val sharedKey: String? = null,
val audioEnabled: Boolean = true,
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
var localCamera: VideoCamera = VideoCamera.User,
val connectionInfo: ConnectionInfo? = null,
var connectedAt: Instant? = null,
@@ -35,6 +35,9 @@ data class Call(
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
}
val localMedia: CallMediaType
get() = if (localMediaSources.camera) CallMediaType.Video else CallMediaType.Audio
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
fun supportsVideo(): Boolean = peerMediaSources.hasVideo() || localMedia == CallMediaType.Video
@@ -84,7 +87,7 @@ sealed class WCallCommand {
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("media") data class Media(val source: CallMediaSource, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
@@ -545,7 +545,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
if (chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile)
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
@@ -6,6 +6,15 @@
<script src="../lz-string.min.js"></script>
</head>
<body>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
@@ -15,15 +24,6 @@
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="local-video-stream"
class="inline"
@@ -39,7 +39,7 @@
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
style="visibility: hidden"
></video>
</body>
<footer>
@@ -12,6 +12,17 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 60px;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
@@ -136,7 +136,16 @@ const processCommand = (function () {
const remoteStream = new MediaStream();
const remoteScreenStream = new MediaStream();
const localCamera = VideoCamera.User;
const localStream = await getLocalMediaStream(mediaType, localCamera);
let localStream;
try {
localStream = await getLocalMediaStream(mediaType, localCamera);
}
catch (e) {
if (isDesktop) {
window.alert("Permission denied. Please, allow mic and video to make the call working.");
}
throw e;
}
const localScreenStream = new MediaStream();
if (isDesktop) {
localStream
@@ -355,12 +364,12 @@ const processCommand = (function () {
if (!activeCall) {
resp = { type: "error", message: "media: call not started" };
}
else if (!activeCall.cameraTrackWasSetBefore && command.media == CallMediaType.Video && command.enable) {
else if (!activeCall.cameraTrackWasSetBefore && command.source == CallMediaSource.Camera && command.enable) {
await startSendingCamera(activeCall, activeCall.localCamera);
resp = { type: "ok" };
}
else {
enableMedia(activeCall.localStream, command.media, command.enable);
enableMedia(activeCall.localStream, command.source, command.enable);
resp = { type: "ok" };
}
break;
@@ -629,7 +638,7 @@ const processCommand = (function () {
// videos.localScreen.pause()
// videos.localScreen.srcObject = call.localScreenStream
videos.localScreen.play();
videos.localScreen.style.display = "block";
videos.localScreen.style.visibility = "visible";
}
else {
pc.getTransceivers().forEach((elem) => {
@@ -642,7 +651,7 @@ const processCommand = (function () {
t.stop();
for (const t of call.localScreenStream.getTracks())
call.localScreenStream.removeTrack(t);
videos.localScreen.style.display = "none";
videos.localScreen.style.visibility = "hidden";
}
}
async function replaceMedia(call, camera) {
@@ -727,56 +736,62 @@ const processCommand = (function () {
const videos = getVideoElements();
if (!videos)
throw Error("no video elements");
if (activeCall) {
const source = mediaSourceFromTransceiverMid(transceiverMid);
console.log("LALAL ON MUTE/UNMUTE", mute, source, transceiverMid);
const sources = activeCall.peerMediaSources;
if (source == CallMediaSource.Mic && activeCall.peerMediaSources.mic == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
};
sources.mic = !mute;
activeCall.peerMediaSources = sources;
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.Camera && activeCall.peerMediaSources.camera == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
};
sources.camera = !mute;
activeCall.peerMediaSources = sources;
videos.remote.style.display = !mute ? "block" : "none";
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.ScreenAudio && activeCall.peerMediaSources.screenAudio == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
};
sources.screenAudio = !mute;
activeCall.peerMediaSources = sources;
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.ScreenVideo && activeCall.peerMediaSources.screenVideo == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
};
sources.screenVideo = !mute;
activeCall.peerMediaSources = sources;
videos.remoteScreen.style.display = !mute ? "block" : "none";
sendMessageToNative({ resp: resp });
}
if (!activeCall)
return;
const source = mediaSourceFromTransceiverMid(transceiverMid);
console.log("LALAL ON MUTE/UNMUTE", mute, source, transceiverMid);
const sources = activeCall.peerMediaSources;
if (source == CallMediaSource.Mic && activeCall.peerMediaSources.mic == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
};
sources.mic = !mute;
activeCall.peerMediaSources = sources;
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.Camera && activeCall.peerMediaSources.camera == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
};
sources.camera = !mute;
activeCall.peerMediaSources = sources;
videos.remote.style.visibility = !mute ? "visible" : "hidden";
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.ScreenAudio && activeCall.peerMediaSources.screenAudio == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
};
sources.screenAudio = !mute;
activeCall.peerMediaSources = sources;
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.ScreenVideo && activeCall.peerMediaSources.screenVideo == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
};
sources.screenVideo = !mute;
activeCall.peerMediaSources = sources;
videos.remoteScreen.style.visibility = !mute ? "visible" : "hidden";
sendMessageToNative({ resp: resp });
}
if (activeCall.peerMediaSources.screenVideo) {
videos.remote.className = "collapsed";
}
else {
videos.remote.className = "inline";
}
}
function getLocalMediaStream(mediaType, facingMode) {
@@ -861,20 +876,23 @@ const processCommand = (function () {
// video.style.opacity = "1"
// }
// }
function enableMedia(s, media, enable) {
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks();
function enableMedia(s, source, enable) {
const tracks = source == CallMediaSource.Camera ? s.getVideoTracks() : s.getAudioTracks();
for (const t of tracks)
activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection.getTransceivers().forEach((elem) => {
if (enable) {
t.enabled = true;
elem.sender.replaceTrack(t);
}
else {
t.enabled = false;
elem.sender.replaceTrack(null);
if ((t.kind == CallMediaType.Audio && mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Mic) ||
(t.kind == CallMediaType.Video && mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Camera)) {
if (enable) {
t.enabled = true;
elem.sender.replaceTrack(t);
}
else {
t.enabled = false;
elem.sender.replaceTrack(null);
}
}
});
if (media == CallMediaType.Video && activeCall) {
if (source == CallMediaSource.Camera && activeCall) {
activeCall.localMediaSources.camera = enable;
}
}
@@ -892,6 +910,10 @@ function toggleRemoteVideoFitFill() {
const remote = document.getElementById("remote-video-stream");
remote.style.objectFit = remote.style.objectFit != "contain" ? "contain" : "cover";
}
function toggleRemoteScreenVideoFitFill() {
const remoteScreen = document.getElementById("remote-screen-video-stream");
remoteScreen.style.objectFit = remoteScreen.style.objectFit != "contain" ? "contain" : "cover";
}
function toggleMedia(s, media) {
let res = false;
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks();
@@ -7,20 +7,20 @@
<script src="/lz-string.min.js"></script>
</head>
<body>
<video
id="remote-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
@@ -38,7 +38,7 @@
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
style="visibility: hidden"
></video>
<div id="progress"></div>
@@ -50,19 +50,19 @@
<img src="/desktop/images/ic_phone_in_talk.svg" />
</div>
<p id="manage-call">
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
<button id="toggle-screen" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleScreenManually()">
<img src="/desktop/images/ic_screen_share.svg" />
</button>
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
<button id="toggle-audio" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleAudioManually()">
<img src="/desktop/images/ic_mic.svg" />
</button>
<button id="end-call" onclick="javascript:endCallManually()">
<img src="/desktop/images/ic_call_end_filled.svg" />
</button>
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
<button id="toggle-speaker" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleSpeakerManually()">
<img src="/desktop/images/ic_volume_up.svg" />
</button>
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
<button id="toggle-video" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleVideoManually()">
<img src="/desktop/images/ic_videocam_off.svg" />
</button>
</p>
@@ -12,6 +12,17 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 20%;
max-height: 20%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 60px;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
@@ -44,7 +44,9 @@ function toggleSpeakerManually() {
}
function toggleVideoManually() {
if (activeCall) {
const apiCall = { command: { type: "media", media: CallMediaType.Video, enable: activeCall.localMediaSources.camera != true } };
const apiCall = {
command: { type: "media", source: CallMediaSource.Camera, enable: activeCall.localMediaSources.camera != true },
};
reactOnMessageFromServer(apiCall);
processCommand(apiCall).then(() => {
var _a;
@@ -82,7 +84,7 @@ function reactOnMessageFromServer(msg) {
document.getElementById("info-block").className = msg.command.media;
break;
case "media":
const className = (msg.command.media == CallMediaType.Video && msg.command.enable) ||
const className = (msg.command.source == CallMediaSource.Camera && msg.command.enable) ||
(activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources.camera) ||
(activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources.screenVideo)
? "video"
@@ -34,14 +34,14 @@ actual fun ActiveCallView() {
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
val callType = CallType(call.initialCallType, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
}
is WCallResponse.Answer -> withBGApi {
@@ -86,15 +86,18 @@ actual fun ActiveCallView() {
is WCallCommand.Answer ->
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
is WCallCommand.Media -> {
when (cmd.media) {
CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
val sources = call.localMediaSources
when (cmd.source) {
CallMediaSource.Mic -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(mic = cmd.enable))
CallMediaSource.Camera -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(camera = cmd.enable))
CallMediaSource.ScreenAudio -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
CallMediaSource.ScreenVideo -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
}
}
is WCallCommand.Camera -> {
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
if (!call.audioEnabled) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
if (!call.localMediaSources.mic) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
}
}
is WCallCommand.End ->
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Composable
actual fun ActiveCallInteractiveArea(call: Call) {
val showMenu = remember { mutableStateOf(false) }
val media = call.peerMediaSources ?: call.localMedia
CompositionLocalProvider(
LocalIndication provides NoIndication
) {
@@ -56,7 +55,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp)
.align(Alignment.TopEnd)
) {
if (media == CallMediaType.Video) {
if (call.supportsVideo()) {
Icon(
painterResource(MR.images.ic_videocam_filled),
stringResource(MR.strings.icon_descr_video_call),
@@ -6,6 +6,15 @@
<script src="../lz-string.min.js"></script>
</head>
<body>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
@@ -15,15 +24,6 @@
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="local-video-stream"
class="inline"
@@ -39,7 +39,7 @@
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
style="visibility: hidden"
></video>
</body>
<footer>
@@ -12,6 +12,17 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 60px;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
+92 -61
View File
@@ -134,7 +134,7 @@ interface WCallIceCandidates extends IWCallCommand, IWCallResponse {
interface WCEnableMedia extends IWCallCommand {
type: "media"
media: CallMediaType
source: CallMediaSource
enable: boolean
}
@@ -377,7 +377,15 @@ const processCommand = (function () {
const remoteStream = new MediaStream()
const remoteScreenStream = new MediaStream()
const localCamera = VideoCamera.User
const localStream = await getLocalMediaStream(mediaType, localCamera)
let localStream: MediaStream
try {
localStream = await getLocalMediaStream(mediaType, localCamera)
} catch (e) {
if (isDesktop) {
window.alert("Permissions denied. Please, allow mic and video to make the call working and repeat again.")
}
throw e
}
const localScreenStream = new MediaStream()
if (isDesktop) {
localStream
@@ -596,11 +604,11 @@ const processCommand = (function () {
case "media":
if (!activeCall) {
resp = {type: "error", message: "media: call not started"}
} else if (!activeCall.cameraTrackWasSetBefore && command.media == CallMediaType.Video && command.enable) {
} else if (!activeCall.cameraTrackWasSetBefore && command.source == CallMediaSource.Camera && command.enable) {
await startSendingCamera(activeCall, activeCall.localCamera)
resp = {type: "ok"}
} else {
enableMedia(activeCall.localStream, command.media, command.enable)
enableMedia(activeCall.localStream, command.source, command.enable)
resp = {type: "ok"}
}
break
@@ -902,7 +910,7 @@ const processCommand = (function () {
// videos.localScreen.pause()
// videos.localScreen.srcObject = call.localScreenStream
videos.localScreen.play()
videos.localScreen.style.display = "block"
videos.localScreen.style.visibility = "visible"
} else {
pc.getTransceivers().forEach((elem) => {
const source = mediaSourceFromTransceiverMid(elem.mid)
@@ -912,7 +920,7 @@ const processCommand = (function () {
})
for (const t of call.localScreenStream.getTracks()) t.stop()
for (const t of call.localScreenStream.getTracks()) call.localScreenStream.removeTrack(t)
videos.localScreen.style.display = "none"
videos.localScreen.style.visibility = "hidden"
}
}
@@ -1002,53 +1010,57 @@ const processCommand = (function () {
function onMediaMuteUnmute(transceiverMid: string | null, mute: boolean) {
const videos = getVideoElements()
if (!videos) throw Error("no video elements")
if (activeCall) {
const source = mediaSourceFromTransceiverMid(transceiverMid)
console.log("LALAL ON MUTE/UNMUTE", mute, source, transceiverMid)
const sources = activeCall.peerMediaSources
if (source == CallMediaSource.Mic && activeCall.peerMediaSources.mic == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
}
sources.mic = !mute
activeCall.peerMediaSources = sources
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.Camera && activeCall.peerMediaSources.camera == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
}
sources.camera = !mute
activeCall.peerMediaSources = sources
videos.remote.style.display = !mute ? "block" : "none"
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.ScreenAudio && activeCall.peerMediaSources.screenAudio == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
}
sources.screenAudio = !mute
activeCall.peerMediaSources = sources
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.ScreenVideo && activeCall.peerMediaSources.screenVideo == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
}
sources.screenVideo = !mute
activeCall.peerMediaSources = sources
videos.remoteScreen.style.display = !mute ? "block" : "none"
sendMessageToNative({resp: resp})
if (!activeCall) return
const source = mediaSourceFromTransceiverMid(transceiverMid)
console.log("LALAL ON MUTE/UNMUTE", mute, source, transceiverMid)
const sources = activeCall.peerMediaSources
if (source == CallMediaSource.Mic && activeCall.peerMediaSources.mic == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
}
sources.mic = !mute
activeCall.peerMediaSources = sources
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.Camera && activeCall.peerMediaSources.camera == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
}
sources.camera = !mute
activeCall.peerMediaSources = sources
videos.remote.style.visibility = !mute ? "visible" : "hidden"
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.ScreenAudio && activeCall.peerMediaSources.screenAudio == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Audio,
source: source,
enabled: !mute,
}
sources.screenAudio = !mute
activeCall.peerMediaSources = sources
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.ScreenVideo && activeCall.peerMediaSources.screenVideo == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
}
sources.screenVideo = !mute
activeCall.peerMediaSources = sources
videos.remoteScreen.style.visibility = !mute ? "visible" : "hidden"
sendMessageToNative({resp: resp})
}
if (activeCall.peerMediaSources.screenVideo) {
videos.remote.className = "collapsed"
} else {
videos.remote.className = "inline"
}
}
@@ -1057,6 +1069,15 @@ const processCommand = (function () {
return navigator.mediaDevices.getUserMedia(constraints)
}
function getEmptyStream(mediaType: CallMediaType, pc: RTCPeerConnection): MediaStream {
const stream = new MediaStream()
pc.addTransceiver("audio", {streams: [stream]})
if (mediaType == CallMediaType.Video) {
pc.addTransceiver("video", {streams: [stream]})
}
return stream
}
function getLocalScreenCaptureStream(): Promise<MediaStream> {
const constraints: any /* DisplayMediaStreamConstraints */ = {
video: {
@@ -1154,19 +1175,24 @@ const processCommand = (function () {
// }
// }
function enableMedia(s: MediaStream, media: CallMediaType, enable: boolean) {
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks()
function enableMedia(s: MediaStream, source: CallMediaSource, enable: boolean) {
const tracks = source == CallMediaSource.Camera ? s.getVideoTracks() : s.getAudioTracks()
for (const t of tracks)
activeCall?.connection.getTransceivers().forEach((elem) => {
if (enable) {
t.enabled = true
elem.sender.replaceTrack(t)
} else {
t.enabled = false
elem.sender.replaceTrack(null)
if (
(t.kind == CallMediaType.Audio && mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Mic) ||
(t.kind == CallMediaType.Video && mediaSourceFromTransceiverMid(elem.mid) == CallMediaSource.Camera)
) {
if (enable) {
t.enabled = true
elem.sender.replaceTrack(t)
} else {
t.enabled = false
elem.sender.replaceTrack(null)
}
}
})
if (media == CallMediaType.Video && activeCall) {
if (source == CallMediaSource.Camera && activeCall) {
activeCall.localMediaSources.camera = enable
}
}
@@ -1187,6 +1213,11 @@ function toggleRemoteVideoFitFill() {
remote.style.objectFit = remote.style.objectFit != "contain" ? "contain" : "cover"
}
function toggleRemoteScreenVideoFitFill() {
const remoteScreen = document.getElementById("remote-screen-video-stream")!
remoteScreen.style.objectFit = remoteScreen.style.objectFit != "contain" ? "contain" : "cover"
}
function toggleMedia(s: MediaStream, media: CallMediaType): boolean {
let res = false
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks()
@@ -7,20 +7,20 @@
<script src="/lz-string.min.js"></script>
</head>
<body>
<video
id="remote-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
@@ -38,7 +38,7 @@
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="display: none"
style="visibility: hidden"
></video>
<div id="progress"></div>
@@ -50,19 +50,19 @@
<img src="/desktop/images/ic_phone_in_talk.svg" />
</div>
<p id="manage-call">
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
<button id="toggle-screen" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleScreenManually()">
<img src="/desktop/images/ic_screen_share.svg" />
</button>
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
<button id="toggle-audio" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleAudioManually()">
<img src="/desktop/images/ic_mic.svg" />
</button>
<button id="end-call" onclick="javascript:endCallManually()">
<img src="/desktop/images/ic_call_end_filled.svg" />
</button>
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
<button id="toggle-speaker" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleSpeakerManually()">
<img src="/desktop/images/ic_volume_up.svg" />
</button>
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
<button id="toggle-video" style="display: none; width: 44px; height: 44px" onclick="javascript:toggleVideoManually()">
<img src="/desktop/images/ic_videocam_off.svg" />
</button>
</p>
@@ -12,6 +12,17 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 20%;
max-height: 20%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 60px;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
@@ -51,7 +51,9 @@ function toggleSpeakerManually() {
function toggleVideoManually() {
if (activeCall) {
const apiCall: WVAPICall = {command: {type: "media", media: CallMediaType.Video, enable: activeCall.localMediaSources.camera != true}}
const apiCall: WVAPICall = {
command: {type: "media", source: CallMediaSource.Camera, enable: activeCall.localMediaSources.camera != true},
}
reactOnMessageFromServer(apiCall as any)
processCommand(apiCall).then(() => {
enableVideoIcon(activeCall?.localMediaSources?.camera == true)
@@ -90,7 +92,7 @@ function reactOnMessageFromServer(msg: WVApiMessage) {
break
case "media":
const className =
(msg.command.media == CallMediaType.Video && msg.command.enable) ||
(msg.command.source == CallMediaSource.Camera && msg.command.enable) ||
activeCall?.peerMediaSources.camera ||
activeCall?.peerMediaSources.screenVideo
? "video"