android, desktop: calls switching from audio to video and back

This commit is contained in:
Avently
2024-08-30 23:32:28 +09:00
parent 564b137f95
commit 4ca07338ea
11 changed files with 726 additions and 115 deletions
@@ -162,6 +162,16 @@ actual fun ActiveCallView() {
is WCallResponse.Connected -> {
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
}
is WCallResponse.PeerMedia -> {
updateActiveCall(call) {
val sources = it.peerMediaSources
when (r.source) {
CallMediaSource.Mic -> it.copy(peerMediaSources = sources.copy(mic = r.enabled))
CallMediaSource.Camera -> it.copy(peerMediaSources = sources.copy(camera = r.enabled))
CallMediaSource.Screen -> it.copy(peerMediaSources = sources.copy(screen = r.enabled))
}
}
}
is WCallResponse.End -> {
withBGApi { chatModel.callManager.endCall(call) }
}
@@ -175,7 +185,7 @@ actual fun ActiveCallView() {
is WCallCommand.Media -> {
updateActiveCall(call) {
when (cmd.media) {
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable)
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable, localMedia = if (cmd.enable) CallMediaType.Video else CallMediaType.Audio)
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
}
}
@@ -293,9 +303,9 @@ private fun ActiveCallOverlayLayout(
flipCamera: () -> Unit
) {
Column {
val media = call.peerMedia ?: call.localMedia
val supportsVideo = call.supportsVideo()
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
if (media == CallMediaType.Video) {
if (supportsVideo) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
}
@@ -327,29 +337,12 @@ private fun ActiveCallOverlayLayout(
}
}
when (media) {
CallMediaType.Video -> {
when (supportsVideo) {
true -> {
VideoCallInfoView(call)
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
ToggleAudioButton(call, enabled, toggleAudio)
SelectSoundDevice()
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) {
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 {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
}
}
}
CallMediaType.Audio -> {
false -> {
Spacer(Modifier.fillMaxHeight().weight(1f))
Column(
Modifier.fillMaxWidth(),
@@ -359,24 +352,24 @@ private fun ActiveCallOverlayLayout(
ProfileImage(size = 192.dp, image = call.contact.profile.image)
AudioCallInfoView(call)
}
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
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))
}
}
Box(Modifier.padding(start = 32.dp)) {
ToggleAudioButton(call, enabled, toggleAudio)
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) {
SelectSoundDevice()
}
}
}
}
}
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
ToggleAudioButton(call, enabled, toggleAudio)
SelectSoundDevice()
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) {
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 {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
}
}
}
@@ -769,7 +762,7 @@ fun PreviewActiveCallOverlayVideo() {
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video,
peerMediaSources = CallMediaSources(),
callUUID = "",
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
@@ -799,7 +792,7 @@ fun PreviewActiveCallOverlayAudio() {
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio,
peerMediaSources = CallMediaSources(),
callUUID = "",
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
@@ -54,7 +54,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
val media = call.peerMedia ?: call.localMedia
val media = call.peerMediaSources ?: call.localMedia
if (media == CallMediaType.Video) {
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
} else {
@@ -2409,7 +2409,7 @@ object ChatController {
// TODO askConfirmation?
// TODO check encryption is compatible
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey)
val useRelay = appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
Log.d(TAG, ".callOffer iceServers $iceServers")
@@ -17,7 +17,7 @@ data class Call(
val callState: CallState,
val localMedia: CallMediaType,
val localCapabilities: CallCapabilities? = null,
val peerMedia: CallMediaType? = null,
val peerMediaSources: CallMediaSources = CallMediaSources(),
val sharedKey: String? = null,
val audioEnabled: Boolean = true,
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
@@ -37,7 +37,7 @@ data class Call(
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
fun supportsVideo(): Boolean = peerMediaSources.hasVideo() || localMedia == CallMediaType.Video
}
@@ -68,6 +68,14 @@ enum class CallState {
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable data class CallMediaSources(
val mic: Boolean = false,
val camera: Boolean = false,
val screen: Boolean = false
) {
fun hasVideo() = camera || screen
}
@Serializable
sealed class WCallCommand {
@Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
@@ -90,6 +98,7 @@ sealed class WCallResponse {
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("peerMedia") data class PeerMedia(val media: CallMediaType, val source: CallMediaSource, val enabled: Boolean): WCallResponse()
@Serializable @SerialName("end") object End: WCallResponse()
@Serializable @SerialName("ended") object Ended: WCallResponse()
@Serializable @SerialName("ok") object Ok: WCallResponse()
@@ -165,6 +174,13 @@ enum class CallMediaType {
@SerialName("audio") Audio
}
@Serializable
enum class CallMediaSource {
@SerialName("mic") Mic,
@SerialName("camera") Camera,
@SerialName("screen") Screen
}
@Serializable
enum class VideoCamera {
@SerialName("user") User,
@@ -6,6 +6,13 @@ var CallMediaType;
CallMediaType["Audio"] = "audio";
CallMediaType["Video"] = "video";
})(CallMediaType || (CallMediaType = {}));
var CallMediaSource;
(function (CallMediaSource) {
CallMediaSource["Mic"] = "mic";
CallMediaSource["Camera"] = "camera";
CallMediaSource["Screen"] = "screen";
CallMediaSource["Unknown"] = "unknown";
})(CallMediaSource || (CallMediaSource = {}));
var VideoCamera;
(function (VideoCamera) {
VideoCamera["User"] = "user";
@@ -131,6 +138,7 @@ const processCommand = (function () {
.filter((elem) => elem.kind == "video")
.forEach((elem) => (elem.enabled = false));
}
// Will become video when any video tracks will be added
const iceCandidates = getIceCandidates(pc, config);
const call = {
connection: pc,
@@ -139,9 +147,14 @@ const processCommand = (function () {
localCamera,
localStream,
remoteStream,
peerMediaSources: {
mic: false,
camera: false,
screen: false,
},
aesKey,
screenShareEnabled: false,
cameraEnabled: true,
cameraEnabled: !isDesktop,
};
await setupMediaStreams(call);
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
@@ -232,6 +245,13 @@ const processCommand = (function () {
const aesKey = encryption ? command.aesKey : undefined;
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey);
const pc = activeCall.connection;
if (media == CallMediaType.Audio) {
console.log("LALAL ADDING TRANSCEIVER for video");
// For camera. So the first video in the list is for camera
pc.addTransceiver("video", { streams: [activeCall.localStream] });
}
// For screenshare. So the second video in the list is for screenshare
pc.addTransceiver("video", { streams: [activeCall.localStream] });
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// for debugging, returning the command for callee to use
@@ -269,7 +289,11 @@ const processCommand = (function () {
const pc = activeCall.connection;
// console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
pc.getTransceivers().forEach((elem) => (elem.direction = "sendrecv"));
console.log("LALAL TRANSCE", pc.getTransceivers());
let answer = await pc.createAnswer();
console.log("LALAL SDP", answer, answer.sdp);
// answer!.sdp = answer.sdp?.replace("a=recvonly", "a=sendrecv")
await pc.setLocalDescription(answer);
addIceCandidates(pc, remoteIceCandidates);
// same as command for caller to use
@@ -295,6 +319,7 @@ const processCommand = (function () {
const answer = parse(command.answer);
const remoteIceCandidates = parse(command.iceCandidates);
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
console.log("LALAL SDP2", answer, answer.sdp);
await pc.setRemoteDescription(new RTCSessionDescription(answer));
addIceCandidates(pc, remoteIceCandidates);
resp = { type: "ok" };
@@ -314,8 +339,9 @@ const processCommand = (function () {
if (!activeCall) {
resp = { type: "error", message: "media: call not started" };
}
else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video) {
resp = { type: "error", message: "media: no video" };
else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video && command.enable) {
await startSendingVideo(activeCall, activeCall.localCamera);
resp = { type: "ok" };
}
else {
enableMedia(activeCall.localStream, command.media, command.enable);
@@ -399,6 +425,12 @@ const processCommand = (function () {
call.worker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: "text/javascript" })));
call.worker.onerror = ({ error, filename, lineno, message }) => console.log({ error, filename, lineno, message });
// call.worker.onmessage = ({data}) => console.log(JSON.stringify({message: data}))
call.worker.onmessage = ({ data }) => {
console.log(JSON.stringify({ message: data }));
const transceiverMid = data.transceiverMid;
const mute = data.mute;
onMediaMuteUnmute(transceiverMid, mute);
};
}
}
}
@@ -413,8 +445,9 @@ const processCommand = (function () {
}
if (call.aesKey && call.key) {
console.log("set up encryption for sending");
for (const sender of pc.getSenders()) {
setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key);
for (const transceiver of pc.getTransceivers()) {
const sender = transceiver.sender;
setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key, transceiver.sender.track.kind == "video" ? CallMediaType.Video : CallMediaType.Audio, transceiver.mid);
}
}
}
@@ -422,16 +455,49 @@ const processCommand = (function () {
// Pull tracks from remote stream as they arrive add them to remoteStream video
const pc = call.connection;
pc.ontrack = (event) => {
console.log("LALAL ON TRACK ", event);
try {
if (call.aesKey && call.key) {
console.log("set up decryption for receiving");
setupPeerTransform(TransformOperation.Decrypt, event.receiver, call.worker, call.aesKey, call.key);
setupPeerTransform(TransformOperation.Decrypt, event.receiver, call.worker, call.aesKey, call.key, event.receiver.track.kind == "video" ? CallMediaType.Video : CallMediaType.Audio, event.transceiver.mid);
}
for (const stream of event.streams) {
for (const track of stream.getTracks()) {
call.remoteStream.addTrack(track);
// const source = mediaSourceFromTransceiverMid(event.transceiver.mid)
// const sources = call.peerMediaSources
// if (source == CallMediaSource.Mic) {
// sources.mic = true
// } else if (source == CallMediaSource.Camera) {
// sources.camera = true
// } else if (source == CallMediaSource.Screen) {
// sources.screen = true
// }
// call.peerMediaSources = sources
if (event.streams.length > 0) {
for (const stream of event.streams) {
for (const track of stream.getTracks()) {
call.remoteStream.addTrack(track);
// const resp: WRPeerMedia = {
// type: "peerMedia",
// media: track.kind == "audio" ? CallMediaType.Audio : CallMediaType.Video,
// source: source,
// enabled: track.enabled,
// }
// console.log("LALAL ADDED REMOTE", track, track.kind)
// sendMessageToNative({resp: resp})
}
}
}
else {
const track = event.track;
call.remoteStream.addTrack(track);
// const resp: WRPeerMedia = {
// type: "peerMedia",
// media: track.kind == "audio" ? CallMediaType.Audio : CallMediaType.Video,
// source: source,
// enabled: track.enabled,
// }
// console.log("LALAL ADDED REMOTE", track, track.kind)
// sendMessageToNative({resp: resp})
}
console.log(`ontrack success`);
}
catch (e) {
@@ -479,6 +545,42 @@ const processCommand = (function () {
}
}
}
async function startSendingVideo(call, camera) {
console.log("LALAL STARTING SENDING VIDEO");
const videos = getVideoElements();
if (!videos)
throw Error("no video elements");
const pc = call.connection;
// Taking the first video transceiver and use it for sending video from camera. Following tracks are for other purposes
const tc = pc.getTransceivers().find((tc) => tc.receiver.track.kind == "video" && tc.direction == "sendrecv");
console.log(pc.getTransceivers().map((elem) => { var _a, _b; return "" + ((_a = elem.sender.track) === null || _a === void 0 ? void 0 : _a.kind) + " " + ((_b = elem.receiver.track) === null || _b === void 0 ? void 0 : _b.kind) + " " + elem.direction; }));
let localStream;
try {
localStream = await getLocalMediaStream(CallMediaType.Video, camera);
for (const t of localStream.getVideoTracks()) {
console.log("LALAL TC", tc, pc.getTransceivers());
call.localStream.addTrack(t);
tc === null || tc === void 0 ? void 0 : tc.sender.replaceTrack(t);
localStream.removeTrack(t);
// when adding track a `sender` will be created on that track automatically
//pc.addTrack(t, call.localStream)
console.log("LALAL ADDED VIDEO TRACK " + t);
}
call.localMedia = CallMediaType.Video;
call.cameraEnabled = true;
}
catch (e) {
return;
}
const sender = tc === null || tc === void 0 ? void 0 : tc.sender;
console.log("LALAL SENDER " + sender + " " + (sender === null || sender === void 0 ? void 0 : sender.getParameters()));
if (call.aesKey && call.key && sender) {
setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key, CallMediaType.Video, tc.mid);
}
// Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture
videos.local.play();
console.log("LALAL SENDING VIDEO");
}
async function replaceMedia(call, camera) {
const videos = getVideoElements();
if (!videos)
@@ -535,20 +637,39 @@ const processCommand = (function () {
for (const t of tracks)
sender.replaceTrack(t);
}
function setupPeerTransform(operation, peer, worker, aesKey, key) {
function mediaSourceFromTransceiverMid(mid) {
switch (mid) {
case "0":
return CallMediaSource.Mic;
case "1":
return CallMediaSource.Camera;
case "2":
return CallMediaSource.Screen;
default:
return CallMediaSource.Unknown;
}
}
function setupPeerTransform(operation, peer, worker, aesKey, key, media, transceiverMid) {
console.log("LALAL MEDIA " + media + " " + transceiverMid);
if (worker && "RTCRtpScriptTransform" in window) {
console.log(`${operation} with worker & RTCRtpScriptTransform`);
peer.transform = new RTCRtpScriptTransform(worker, { operation, aesKey });
peer.transform = new RTCRtpScriptTransform(worker, { operation, aesKey, media, transceiverMid });
}
else if ("createEncodedStreams" in peer) {
const { readable, writable } = peer.createEncodedStreams();
if (worker) {
console.log(`${operation} with worker`);
worker.postMessage({ operation, readable, writable, aesKey }, [readable, writable]);
worker.postMessage({ operation, readable, writable, aesKey, media, transceiverMid }, [
readable,
writable,
]);
}
else {
console.log(`${operation} without worker`);
const transform = callCrypto.transformFrame[operation](key);
const onMediaMuteUnmuteConst = (mute) => {
onMediaMuteUnmute(transceiverMid, mute);
};
const transform = callCrypto.transformFrame[operation](key, onMediaMuteUnmuteConst);
readable.pipeThrough(new TransformStream({ transform })).pipeTo(writable);
}
}
@@ -556,6 +677,46 @@ const processCommand = (function () {
console.log(`no ${operation}`);
}
}
function onMediaMuteUnmute(transceiverMid, mute) {
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;
sendMessageToNative({ resp: resp });
}
else if (source == CallMediaSource.Screen && activeCall.peerMediaSources.screen == mute) {
const resp = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
};
sources.screen = !mute;
activeCall.peerMediaSources = sources;
sendMessageToNative({ resp: resp });
}
}
}
function getLocalMediaStream(mediaType, facingMode) {
const constraints = callMediaConstraints(mediaType, facingMode);
return navigator.mediaDevices.getUserMedia(constraints);
@@ -703,6 +864,7 @@ function callCryptoFunction() {
: new Uint8Array(0);
frame.data = concatN(initial, ciphertext, iv).buffer;
controller.enqueue(frame);
// console.log("LALAL ENCRYPT", frame.data.byteLength)
}
catch (e) {
console.log(`encryption error ${e}`);
@@ -710,7 +872,9 @@ function callCryptoFunction() {
}
};
}
function decryptFrame(key) {
function decryptFrame(key, onMediaMuteUnmute) {
let wasMuted = true;
let lastBytes = [];
return async (frame, controller) => {
const data = new Uint8Array(frame.data);
const n = initialPlainTextRequired[frame.type] || 1;
@@ -723,6 +887,23 @@ function callCryptoFunction() {
: new Uint8Array(0);
frame.data = concatN(initial, plaintext).buffer;
controller.enqueue(frame);
lastBytes.push(frame.data.byteLength);
const sliced = lastBytes.slice(-20, lastBytes.length);
const average = sliced.reduce((prev, value) => value + prev, 0) / Math.max(1, sliced.length);
if (lastBytes.length > 20) {
console.log("LALAL REPLACED", lastBytes.length, sliced.length);
lastBytes = sliced;
}
console.log("LALAL DECRYPT", frame.type, frame.data.byteLength, average);
// frame.type is undefined for audio stream, but defined for video
if (frame.type && wasMuted && average > 200) {
wasMuted = false;
onMediaMuteUnmute(false);
}
else if (frame.type && !wasMuted && average < 200) {
wasMuted = true;
onMediaMuteUnmute(true);
}
}
catch (e) {
console.log(`decryption error ${e}`);
@@ -828,9 +1009,9 @@ function workerFunction() {
if ("RTCTransformEvent" in self) {
self.addEventListener("rtctransform", async ({ transformer }) => {
try {
const { operation, aesKey } = transformer.options;
const { operation, aesKey, transceiverMid } = transformer.options;
const { readable, writable } = transformer;
await setupTransform({ operation, aesKey, readable, writable });
await setupTransform({ operation, aesKey, transceiverMid, readable, writable });
self.postMessage({ result: "setupTransform success" });
}
catch (e) {
@@ -838,9 +1019,12 @@ function workerFunction() {
}
});
}
async function setupTransform({ operation, aesKey, readable, writable }) {
async function setupTransform({ operation, aesKey, transceiverMid, readable, writable }) {
const key = await callCrypto.decodeAesKey(aesKey);
const transform = callCrypto.transformFrame[operation](key);
const onMediaMuteUnmute = (mute) => {
self.postMessage({ transceiverMid: transceiverMid, mute: mute });
};
const transform = callCrypto.transformFrame[operation](key, onMediaMuteUnmute);
readable.pipeThrough(new TransformStream({ transform })).pipeTo(writable);
}
}
@@ -9,6 +9,7 @@ socket.addEventListener("open", (_event) => {
sendMessageToNative = (msg) => {
console.log("Message to server");
socket.send(JSON.stringify(msg));
reactOnMessageToServer(msg);
};
});
socket.addEventListener("message", (event) => {
@@ -43,17 +44,19 @@ function toggleSpeakerManually() {
}
function toggleVideoManually() {
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
let res;
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled) {
activeCall.cameraEnabled = !activeCall.cameraEnabled;
res = activeCall.cameraEnabled;
enableVideoIcon(activeCall.cameraEnabled);
// } else if (activeCall.localMedia == CallMediaType.Video) {
// enableVideoIcon(toggleMedia(activeCall.localStream, CallMediaType.Video))
}
else {
res = toggleMedia(activeCall.localStream, CallMediaType.Video);
const apiCall = { command: { type: "media", media: CallMediaType.Video, enable: activeCall.cameraEnabled != true } };
reactOnMessageFromServer(apiCall);
processCommand(apiCall).then(() => {
enableVideoIcon((activeCall === null || activeCall === void 0 ? void 0 : activeCall.cameraEnabled) == true);
});
}
document.getElementById("toggle-video").innerHTML = res
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />';
}
}
async function toggleScreenManually() {
@@ -65,6 +68,11 @@ async function toggleScreenManually() {
: '<img src="/desktop/images/ic_screen_share.svg" />';
}
}
function enableVideoIcon(enabled) {
document.getElementById("toggle-video").innerHTML = enabled
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />';
}
function reactOnMessageFromServer(msg) {
var _a;
switch ((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) {
@@ -75,23 +83,41 @@ function reactOnMessageFromServer(msg) {
case "start":
document.getElementById("toggle-audio").style.display = "inline-block";
document.getElementById("toggle-speaker").style.display = "inline-block";
if (msg.command.media == CallMediaType.Video) {
document.getElementById("toggle-video").style.display = "inline-block";
document.getElementById("toggle-screen").style.display = "inline-block";
}
document.getElementById("toggle-video").style.display = "inline-block";
document.getElementById("toggle-screen").style.display = "inline-block";
document.getElementById("info-block").className = msg.command.media;
break;
case "media":
const className = (msg.command.media == CallMediaType.Video && msg.command.enable) ||
(activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources.camera) ||
(activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources.screen)
? "video"
: "audio";
document.getElementById("info-block").className = className;
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
break;
case "description":
updateCallInfoView(msg.command.state, msg.command.description);
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection.connectionState) == "connected") {
document.getElementById("progress").style.display = "none";
if (document.getElementById("info-block").className == CallMediaType.Audio) {
document.getElementById("audio-call-icon").style.display = "block";
}
document.getElementById("audio-call-icon").style.display =
document.getElementById("info-block").className == CallMediaType.Audio ? "block" : "none";
}
break;
}
}
function reactOnMessageToServer(msg) {
var _a;
switch ((_a = msg.resp) === null || _a === void 0 ? void 0 : _a.type) {
case "peerMedia":
const className = (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) == CallMediaType.Video || (activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources.camera) || (activeCall === null || activeCall === void 0 ? void 0 : activeCall.peerMediaSources.screen)
? "video"
: "audio";
document.getElementById("info-block").className = className;
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
break;
}
}
function updateCallInfoView(state, description) {
document.getElementById("state").innerText = state;
document.getElementById("description").innerText = description;
@@ -65,6 +65,14 @@ actual fun ActiveCallView() {
is WCallResponse.Connected -> {
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
}
is WCallResponse.PeerMedia -> {
val sources = call.peerMediaSources
chatModel.activeCall.value = when (r.source) {
CallMediaSource.Mic -> call.copy(peerMediaSources = sources.copy(mic = r.enabled))
CallMediaSource.Camera -> call.copy(peerMediaSources = sources.copy(camera = r.enabled))
CallMediaSource.Screen -> call.copy(peerMediaSources = sources.copy(screen = r.enabled))
}
}
is WCallResponse.End -> {
withBGApi { chatModel.callManager.endCall(call) }
}
@@ -24,7 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Composable
actual fun ActiveCallInteractiveArea(call: Call) {
val showMenu = remember { mutableStateOf(false) }
val media = call.peerMedia ?: call.localMedia
val media = call.peerMediaSources ?: call.localMedia
CompositionLocalProvider(
LocalIndication provides NoIndication
) {
@@ -0,0 +1,110 @@
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007bf0edd03035, pid=626666, tid=626706
#
# JRE version: OpenJDK Runtime Environment (17.0.12+7) (build 17.0.12+7)
# Java VM: OpenJDK 64-Bit Server VM (17.0.12+7, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
# Problematic frame:
# C [libc.so.6+0x9a035] __pthread_rwlock_rdlock+0x15
#
# Core dump will be written. Default location: Core dumps may be processed with "/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h" (or dumping to /mnt/Dev/apps/simplex/apps/multiplatform/desktop/core.626666)
#
# If you would like to submit a bug report, please visit:
# https://bugreport.java.com/bugreport/crash.jsp
#
--------------- S U M M A R Y ------------
Command Line: -Dcompose.application.configure.swing.globals=true -Dcompose.application.resources.dir=/mnt/Dev/apps/simplex/apps/multiplatform/desktop/build/compose/tmp/prepareAppResources -Dfile.encoding=UTF-8 -Duser.country=US -Duser.language=en -Duser.variant chat.simplex.desktop.MainKt
Host: 12th Gen Intel(R) Core(TM) i7-12700H, 20 cores, 38G, Manjaro Linux
Time: Fri Aug 30 22:33:15 2024 KST elapsed time: 110.587221 seconds (0d 0h 1m 50s)
--------------- T H R E A D ---------------
Current thread is native thread
Stack: [0x00007bf042401000,0x00007bf042c01000], sp=0x00007bf042bfb820, free space=8170k
Native frames: (J=compiled Java code, j=interpreted, Vv=VM code, C=native code)
C [libc.so.6+0x9a035] __pthread_rwlock_rdlock+0x15
C [libcrypto.so.3+0x18645e] CRYPTO_THREAD_read_lock+0xe
C [libcrypto.so.3+0x1b36fa] RAND_get_rand_method+0x3a
C [libcrypto.so.3+0x1b4c97] RAND_bytes_ex+0x27
siginfo: si_signo: 11 (SIGSEGV), si_code: 1 (SEGV_MAPERR), si_addr: 0x0000000000000018
Registers:
RAX=0x0000000000000001, RBX=0x0000000000000000, RCX=0x0000000000000000, RDX=0x0000000000000050
RSP=0x00007bf042bfb820, RBP=0x00007bf042bfb840, RSI=0x00007bf04c3b0ab0, RDI=0x0000000000000000
R8 =0x0000000000000fa0, R9 =0x0000000000000000, R10=0x0000000000000001, R11=0x00007bf042bfb880
R12=0x0000000000000050, R13=0x00007befe40c8898, R14=0x0000000000000000, R15=0x0000000000000000
RIP=0x00007bf0edd03035, EFLAGS=0x0000000000010202, CSGSFS=0x002b000000000033, ERR=0x0000000000000004
TRAPNO=0x000000000000000e
Top of Stack: (sp=0x00007bf042bfb820)
0x00007bf042bfb820: 0000000000000fa0 00007befe40c8898
0x00007bf042bfb830: 0000000000000050 00007befe40c8898
0x00007bf042bfb840: 00007bf042bfb850 00007bf04c38645e
0x00007bf042bfb850: 00007bf042bfb870 00007bf04c3b36fa
0x00007bf042bfb860: 00007befe40c8898 0000000000000050
0x00007bf042bfb870: 00007bf042bfb8b0 00007bf04c3b4c97
0x00007bf042bfb880: 00007befe40c8898 00007befe40c8898
0x00007bf042bfb890: 0000000000000fb0 00007befe40a5728
0x00007bf042bfb8a0: 0000000000000fa0 00007befe40c78f8
0x00007bf042bfb8b0: 0000000000000050 00007bf055d655cd
0x00007bf042bfb8c0: 0000000000000fb0 0000000000000001
0x00007bf042bfb8d0: 0000000000000ff0 00007bf055d645de
0x00007bf042bfb8e0: 00007befe40c8898 00007befe40dd000
0x00007bf042bfb8f0: 0000000000000fa0 00007befe40c8998
0x00007bf042bfb900: 0000000100001000 00007befe40c88a8
0x00007bf042bfb910: 0000000000000000 00007befe40a5728
0x00007bf042bfb920: 00007befe40c78f8 0000000000000ff0
0x00007bf042bfb930: 0000000000000001 00007befe40c78e8
0x00007bf042bfb940: 0000000000000000 00007bf055d650f1
0x00007bf042bfb950: 00007befe40c78f8 00007bf055d651a6
0x00007bf042bfb960: 0000000000000000 00007befe40dd000
0x00007bf042bfb970: 0000000000000010 0000000100000010
0x00007bf042bfb980: 00007befe40c78a8 3f5d6a2928082800
0x00007bf042bfb990: 0000000000000000 00007befe40de028
0x00007bf042bfb9a0: 00007befe40b99d8 0000000000000001
0x00007bf042bfb9b0: 0000000000000000 00007befe40dcff0
0x00007bf042bfb9c0: 0000000000000000 00007bf055d892d4
0x00007bf042bfb9d0: 05000000d763a120 3f5d6a2928082800
0x00007bf042bfb9e0: 00007befe40b99d8 0000000000000000
0x00007bf042bfb9f0: 0000000000000000 00007befe40b9cec
0x00007bf042bfba00: 00007befe409a808 00007bf055d9042f
0x00007bf042bfba10: 00007bf042bfba20 3f5d6a2928082800
Instructions: (pc=0x00007bf0edd03035)
0x00007bf0edd02f35: f7 0f 84 31 fd ff ff 48 8d 3d ed ba 11 00 e8 68
0x00007bf0edd02f45: f5 fe ff 0f 1f 84 00 00 00 00 00 83 c0 16 83 e0
0x00007bf0edd02f55: f7 75 e4 e9 3f ff ff ff 45 85 ff 0f 84 07 fd ff
0x00007bf0edd02f65: ff 8b 43 04 48 8d 53 04 3d 01 00 00 80 74 30 8d
0x00007bf0edd02f75: 48 ff f0 0f b1 0a 75 f0 3d 01 00 00 80 0f 85 e5
0x00007bf0edd02f85: fc ff ff 8b 13 b8 03 00 00 00 83 ca 02 e9 9b fc
0x00007bf0edd02f95: ff ff 83 c0 16 83 e0 f7 75 9d e9 cd fe ff ff 31
0x00007bf0edd02fa5: c9 eb cf 85 c0 0f 85 40 ff ff ff 89 4b 0c e9 6d
0x00007bf0edd02fb5: fe ff ff 0f 1f 84 00 00 00 00 00 f3 0f 1e fa 90
0x00007bf0edd02fc5: 31 c0 c3 0f 1f 84 00 00 00 00 00 f3 0f 1e fa 48
0x00007bf0edd02fd5: 85 f6 48 8d 05 aa 06 12 00 66 0f ef c0 48 c7 47
0x00007bf0edd02fe5: 30 00 00 00 00 48 0f 44 f0 0f 11 47 10 0f 11 07
0x00007bf0edd02ff5: 0f 11 47 20 8b 06 8b 56 04 89 47 30 31 c0 85 d2
0x00007bf0edd03005: 0f 95 c0 89 47 1c 31 c0 c3 66 2e 0f 1f 84 00 00
0x00007bf0edd03015: 00 00 00 0f 1f 84 00 00 00 00 00 f3 0f 1e fa 55
0x00007bf0edd03025: 48 89 e5 41 55 41 54 53 48 89 fb 48 83 ec 08 90
0x00007bf0edd03035: 8b 57 18 64 8b 04 25 d0 02 00 00 39 c2 0f 84 08
0x00007bf0edd03045: 01 00 00 83 7f 30 02 74 32 b8 08 00 00 00 f0 0f
0x00007bf0edd03055: c1 03 83 c0 08 85 c0 0f 88 fe 00 00 00 a8 01 75
0x00007bf0edd03065: 7a 31 d2 90 48 83 c4 08 89 d0 5b 41 5c 41 5d 5d
0x00007bf0edd03075: c3 66 2e 0f 1f 84 00 00 00 00 00 8b 37 89 f0 83
0x00007bf0edd03085: e0 03 83 f8 02 75 c2 89 f0 c1 e8 03 74 bb 89 f2
0x00007bf0edd03095: 89 f0 83 ca 04 f0 0f b1 13 89 c6 74 0b eb de 0f
0x00007bf0edd030a5: 1f 40 00 83 f8 4b 74 bb 8b 33 40 f6 c6 04 74 cd
0x00007bf0edd030b5: 44 8b 4b 1c 45 31 c0 48 89 df 45 85 c9 41 0f 95
0x00007bf0edd030c5: c0 31 d2 31 c9 41 c1 e0 07 e8 ad 69 ff ff 89 c2
0x00007bf0edd030d5: 83 f8 6e 75 ce eb 8c 0f 1f 40 00 89 c2 83 e2 03
0x00007bf0edd030e5: 83 fa 01 0f 85 89 00 00 00 89 c2 83 f2 01 f0 0f
0x00007bf0edd030f5: b1 13 75
+267 -22
View File
@@ -26,6 +26,7 @@ type WCallResponse =
| WCallIceCandidates
| WRConnection
| WRCallConnected
| WRPeerMedia
| WRCallEnd
| WRCallEnded
| WROk
@@ -34,13 +35,31 @@ type WCallResponse =
type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "description" | "layout" | "end"
type WCallResponseTag = "capabilities" | "offer" | "answer" | "ice" | "connection" | "connected" | "end" | "ended" | "ok" | "error"
type WCallResponseTag =
| "capabilities"
| "offer"
| "answer"
| "ice"
| "connection"
| "connected"
| "peerMedia"
| "end"
| "ended"
| "ok"
| "error"
enum CallMediaType {
Audio = "audio",
Video = "video",
}
enum CallMediaSource {
Mic = "mic",
Camera = "camera",
Screen = "screen",
Unknown = "unknown",
}
enum VideoCamera {
User = "user",
Environment = "environment",
@@ -52,6 +71,12 @@ enum LayoutType {
RemoteVideo = "remoteVideo",
}
interface CallMediaSources {
mic: boolean
camera: boolean
screen: boolean
}
interface IWCallCommand {
type: WCallCommandTag
}
@@ -151,6 +176,13 @@ interface WRCallConnected extends IWCallResponse {
connectionInfo: ConnectionInfo
}
interface WRPeerMedia extends IWCallResponse {
type: "peerMedia"
media: CallMediaType
source: CallMediaSource
enabled: boolean
}
interface WRCallEnd extends IWCallResponse {
type: "end"
}
@@ -206,6 +238,7 @@ interface Call {
localCamera: VideoCamera
localStream: MediaStream
remoteStream: MediaStream
peerMediaSources: CallMediaSources
screenShareEnabled: boolean
cameraEnabled: boolean
aesKey?: string
@@ -341,17 +374,23 @@ const processCommand = (function () {
.filter((elem) => elem.kind == "video")
.forEach((elem) => (elem.enabled = false))
}
// Will become video when any video tracks will be added
const iceCandidates = getIceCandidates(pc, config)
const call = {
const call: Call = {
connection: pc,
iceCandidates,
localMedia: mediaType,
localCamera,
localStream,
remoteStream,
peerMediaSources: {
mic: false,
camera: false,
screen: false,
},
aesKey,
screenShareEnabled: false,
cameraEnabled: true,
cameraEnabled: !isDesktop,
}
await setupMediaStreams(call)
let connectionTimeout: number | undefined = setTimeout(connectionHandler, answerTimeout)
@@ -443,6 +482,14 @@ const processCommand = (function () {
const aesKey = encryption ? command.aesKey : undefined
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey)
const pc = activeCall.connection
if (media == CallMediaType.Audio) {
console.log("LALAL ADDING TRANSCEIVER for video")
// For camera. So the first video in the list is for camera
pc.addTransceiver("video", {streams: [activeCall.localStream]})
}
// For screenshare. So the second video in the list is for screenshare
pc.addTransceiver("video", {streams: [activeCall.localStream]})
const offer = await pc.createOffer()
await pc.setLocalDescription(offer)
// for debugging, returning the command for callee to use
@@ -478,7 +525,11 @@ const processCommand = (function () {
const pc = activeCall.connection
// console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
await pc.setRemoteDescription(new RTCSessionDescription(offer))
const answer = await pc.createAnswer()
pc.getTransceivers().forEach((elem) => (elem.direction = "sendrecv"))
console.log("LALAL TRANSCE", pc.getTransceivers())
let answer = await pc.createAnswer()
console.log("LALAL SDP", answer, answer.sdp)
// answer!.sdp = answer.sdp?.replace("a=recvonly", "a=sendrecv")
await pc.setLocalDescription(answer)
addIceCandidates(pc, remoteIceCandidates)
// same as command for caller to use
@@ -501,6 +552,8 @@ const processCommand = (function () {
const answer: RTCSessionDescriptionInit = parse(command.answer)
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
console.log("LALAL SDP2", answer, answer.sdp)
await pc.setRemoteDescription(new RTCSessionDescription(answer))
addIceCandidates(pc, remoteIceCandidates)
resp = {type: "ok"}
@@ -518,8 +571,9 @@ const processCommand = (function () {
case "media":
if (!activeCall) {
resp = {type: "error", message: "media: call not started"}
} else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video) {
resp = {type: "error", message: "media: no video"}
} else if (activeCall.localMedia == CallMediaType.Audio && command.media == CallMediaType.Video && command.enable) {
await startSendingVideo(activeCall, activeCall.localCamera)
resp = {type: "ok"}
} else {
enableMedia(activeCall.localStream, command.media, command.enable)
resp = {type: "ok"}
@@ -600,6 +654,12 @@ const processCommand = (function () {
call.worker = new Worker(URL.createObjectURL(new Blob([workerCode], {type: "text/javascript"})))
call.worker.onerror = ({error, filename, lineno, message}: ErrorEvent) => console.log({error, filename, lineno, message})
// call.worker.onmessage = ({data}) => console.log(JSON.stringify({message: data}))
call.worker.onmessage = ({data}) => {
console.log(JSON.stringify({message: data}))
const transceiverMid: string = data.transceiverMid
const mute: boolean = data.mute
onMediaMuteUnmute(transceiverMid, mute)
}
}
}
}
@@ -616,8 +676,17 @@ const processCommand = (function () {
if (call.aesKey && call.key) {
console.log("set up encryption for sending")
for (const sender of pc.getSenders() as RTCRtpSenderWithEncryption[]) {
setupPeerTransform(TransformOperation.Encrypt, sender, call.worker, call.aesKey, call.key)
for (const transceiver of pc.getTransceivers()) {
const sender = transceiver.sender as RTCRtpSenderWithEncryption
setupPeerTransform(
TransformOperation.Encrypt,
sender,
call.worker,
call.aesKey,
call.key,
transceiver.sender.track!.kind == "video" ? CallMediaType.Video : CallMediaType.Audio,
transceiver.mid
)
}
}
}
@@ -626,15 +695,56 @@ const processCommand = (function () {
// Pull tracks from remote stream as they arrive add them to remoteStream video
const pc = call.connection
pc.ontrack = (event) => {
console.log("LALAL ON TRACK ", event)
try {
if (call.aesKey && call.key) {
console.log("set up decryption for receiving")
setupPeerTransform(TransformOperation.Decrypt, event.receiver as RTCRtpReceiverWithEncryption, call.worker, call.aesKey, call.key)
setupPeerTransform(
TransformOperation.Decrypt,
event.receiver as RTCRtpReceiverWithEncryption,
call.worker,
call.aesKey,
call.key,
event.receiver.track.kind == "video" ? CallMediaType.Video : CallMediaType.Audio,
event.transceiver.mid
)
}
for (const stream of event.streams) {
for (const track of stream.getTracks()) {
call.remoteStream.addTrack(track)
// const source = mediaSourceFromTransceiverMid(event.transceiver.mid)
// const sources = call.peerMediaSources
// if (source == CallMediaSource.Mic) {
// sources.mic = true
// } else if (source == CallMediaSource.Camera) {
// sources.camera = true
// } else if (source == CallMediaSource.Screen) {
// sources.screen = true
// }
// call.peerMediaSources = sources
if (event.streams.length > 0) {
for (const stream of event.streams) {
for (const track of stream.getTracks()) {
call.remoteStream.addTrack(track)
// const resp: WRPeerMedia = {
// type: "peerMedia",
// media: track.kind == "audio" ? CallMediaType.Audio : CallMediaType.Video,
// source: source,
// enabled: track.enabled,
// }
// console.log("LALAL ADDED REMOTE", track, track.kind)
// sendMessageToNative({resp: resp})
}
}
} else {
const track = event.track
call.remoteStream.addTrack(track)
// const resp: WRPeerMedia = {
// type: "peerMedia",
// media: track.kind == "audio" ? CallMediaType.Audio : CallMediaType.Video,
// source: source,
// enabled: track.enabled,
// }
// console.log("LALAL ADDED REMOTE", track, track.kind)
// sendMessageToNative({resp: resp})
}
console.log(`ontrack success`)
} catch (e) {
@@ -683,6 +793,51 @@ const processCommand = (function () {
}
}
async function startSendingVideo(call: Call, camera: VideoCamera): Promise<void> {
console.log("LALAL STARTING SENDING VIDEO")
const videos = getVideoElements()
if (!videos) throw Error("no video elements")
const pc = call.connection
// Taking the first video transceiver and use it for sending video from camera. Following tracks are for other purposes
const tc = pc.getTransceivers().find((tc) => tc.receiver.track.kind == "video" && tc.direction == "sendrecv")
console.log(pc.getTransceivers().map((elem) => "" + elem.sender.track?.kind + " " + elem.receiver.track?.kind + " " + elem.direction))
let localStream: MediaStream
try {
localStream = await getLocalMediaStream(CallMediaType.Video, camera)
for (const t of localStream.getVideoTracks()) {
console.log("LALAL TC", tc, pc.getTransceivers())
call.localStream.addTrack(t)
tc?.sender.replaceTrack(t)
localStream.removeTrack(t)
// when adding track a `sender` will be created on that track automatically
//pc.addTrack(t, call.localStream)
console.log("LALAL ADDED VIDEO TRACK " + t)
}
call.localMedia = CallMediaType.Video
call.cameraEnabled = true
} catch (e: any) {
return
}
const sender = tc?.sender
console.log("LALAL SENDER " + sender + " " + sender?.getParameters())
if (call.aesKey && call.key && sender) {
setupPeerTransform(
TransformOperation.Encrypt,
sender as RTCRtpSenderWithEncryption,
call.worker,
call.aesKey,
call.key,
CallMediaType.Video,
tc.mid
)
}
// Without doing it manually Firefox shows black screen but video can be played in Picture-in-Picture
videos.local.play()
console.log("LALAL SENDING VIDEO")
}
async function replaceMedia(call: Call, camera: VideoCamera): Promise<void> {
const videos = getVideoElements()
if (!videos) throw Error("no video elements")
@@ -734,24 +889,46 @@ const processCommand = (function () {
if (sender) for (const t of tracks) sender.replaceTrack(t)
}
function mediaSourceFromTransceiverMid(mid: string | null) {
switch (mid) {
case "0":
return CallMediaSource.Mic
case "1":
return CallMediaSource.Camera
case "2":
return CallMediaSource.Screen
default:
return CallMediaSource.Unknown
}
}
function setupPeerTransform(
operation: TransformOperation,
peer: RTCRtpReceiverWithEncryption | RTCRtpSenderWithEncryption,
worker: Worker | undefined,
aesKey: string,
key: CryptoKey
key: CryptoKey,
media: CallMediaType,
transceiverMid: string | null
) {
console.log("LALAL MEDIA " + media + " " + transceiverMid)
if (worker && "RTCRtpScriptTransform" in window) {
console.log(`${operation} with worker & RTCRtpScriptTransform`)
peer.transform = new RTCRtpScriptTransform(worker, {operation, aesKey})
peer.transform = new RTCRtpScriptTransform(worker, {operation, aesKey, media, transceiverMid})
} else if ("createEncodedStreams" in peer) {
const {readable, writable} = peer.createEncodedStreams()
if (worker) {
console.log(`${operation} with worker`)
worker.postMessage({operation, readable, writable, aesKey}, [readable, writable] as unknown as Transferable[])
worker.postMessage({operation, readable, writable, aesKey, media, transceiverMid}, [
readable,
writable,
] as unknown as Transferable[])
} else {
console.log(`${operation} without worker`)
const transform = callCrypto.transformFrame[operation](key)
const onMediaMuteUnmuteConst = (mute: boolean) => {
onMediaMuteUnmute(transceiverMid, mute)
}
const transform = callCrypto.transformFrame[operation](key, onMediaMuteUnmuteConst)
readable.pipeThrough(new TransformStream({transform})).pipeTo(writable)
}
} else {
@@ -759,6 +936,45 @@ const processCommand = (function () {
}
}
function onMediaMuteUnmute(transceiverMid: string | null, mute: boolean) {
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
sendMessageToNative({resp: resp})
} else if (source == CallMediaSource.Screen && activeCall.peerMediaSources.screen == mute) {
const resp: WRPeerMedia = {
type: "peerMedia",
media: CallMediaType.Video,
source: source,
enabled: !mute,
}
sources.screen = !mute
activeCall.peerMediaSources = sources
sendMessageToNative({resp: resp})
}
}
}
function getLocalMediaStream(mediaType: CallMediaType, facingMode: VideoCamera): Promise<MediaStream> {
const constraints = callMediaConstraints(mediaType, facingMode)
return navigator.mediaDevices.getUserMedia(constraints)
@@ -902,7 +1118,10 @@ function changeLayout(layout: LayoutType) {
}
}
type TransformFrameFunc = (key: CryptoKey) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void>
type TransformFrameFunc = (
key: CryptoKey,
onMediaMuteUnmute: (mute: boolean) => void
) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void>
interface CallCrypto {
transformFrame: {[x in TransformOperation]: TransformFrameFunc}
@@ -936,6 +1155,7 @@ function callCryptoFunction(): CallCrypto {
: new Uint8Array(0)
frame.data = concatN(initial, ciphertext, iv).buffer
controller.enqueue(frame)
// console.log("LALAL ENCRYPT", frame.data.byteLength)
} catch (e) {
console.log(`encryption error ${e}`)
throw e
@@ -943,7 +1163,12 @@ function callCryptoFunction(): CallCrypto {
}
}
function decryptFrame(key: CryptoKey): (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> {
function decryptFrame(
key: CryptoKey,
onMediaMuteUnmute: (mute: boolean) => void
): (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void> {
let wasMuted = true
let lastBytes: number[] = []
return async (frame, controller) => {
const data = new Uint8Array(frame.data)
const n = initialPlainTextRequired[frame.type] || 1
@@ -956,6 +1181,22 @@ function callCryptoFunction(): CallCrypto {
: new Uint8Array(0)
frame.data = concatN(initial, plaintext).buffer
controller.enqueue(frame)
lastBytes.push(frame.data.byteLength)
const sliced = lastBytes.slice(-20, lastBytes.length)
const average = sliced.reduce((prev, value) => value + prev, 0) / Math.max(1, sliced.length)
if (lastBytes.length > 20) {
console.log("LALAL REPLACED", lastBytes.length, sliced.length)
lastBytes = sliced
}
console.log("LALAL DECRYPT", frame.type, frame.data.byteLength, average)
// frame.type is undefined for audio stream, but defined for video
if (frame.type && wasMuted && average > 200) {
wasMuted = false
onMediaMuteUnmute(false)
} else if (frame.type && !wasMuted && average < 200) {
wasMuted = true
onMediaMuteUnmute(true)
}
} catch (e) {
console.log(`decryption error ${e}`)
throw e
@@ -1076,6 +1317,7 @@ function workerFunction() {
readable: ReadableStream<RTCEncodedVideoFrame>
writable: WritableStream<RTCEncodedVideoFrame>
aesKey: string
transceiverMid: string | null
}
// encryption with createEncodedStreams support
@@ -1087,9 +1329,9 @@ function workerFunction() {
if ("RTCTransformEvent" in self) {
self.addEventListener("rtctransform", async ({transformer}: any) => {
try {
const {operation, aesKey} = transformer.options
const {operation, aesKey, transceiverMid} = transformer.options
const {readable, writable} = transformer
await setupTransform({operation, aesKey, readable, writable})
await setupTransform({operation, aesKey, transceiverMid, readable, writable})
self.postMessage({result: "setupTransform success"})
} catch (e) {
self.postMessage({message: `setupTransform error: ${(e as Error).message}`})
@@ -1097,9 +1339,12 @@ function workerFunction() {
})
}
async function setupTransform({operation, aesKey, readable, writable}: Transform): Promise<void> {
async function setupTransform({operation, aesKey, transceiverMid, readable, writable}: Transform): Promise<void> {
const key = await callCrypto.decodeAesKey(aesKey)
const transform = callCrypto.transformFrame[operation](key)
const onMediaMuteUnmute = (mute: boolean) => {
self.postMessage({transceiverMid: transceiverMid, mute: mute})
}
const transform = callCrypto.transformFrame[operation](key, onMediaMuteUnmute)
readable.pipeThrough(new TransformStream({transform})).pipeTo(writable)
}
}
+42 -13
View File
@@ -10,6 +10,7 @@ socket.addEventListener("open", (_event) => {
sendMessageToNative = (msg: WVApiMessage) => {
console.log("Message to server")
socket.send(JSON.stringify(msg))
reactOnMessageToServer(msg)
}
})
@@ -50,16 +51,18 @@ function toggleSpeakerManually() {
function toggleVideoManually() {
if (activeCall?.localMedia) {
let res: boolean
if (activeCall?.screenShareEnabled) {
activeCall.cameraEnabled = !activeCall.cameraEnabled
res = activeCall.cameraEnabled
enableVideoIcon(activeCall.cameraEnabled)
// } else if (activeCall.localMedia == CallMediaType.Video) {
// enableVideoIcon(toggleMedia(activeCall.localStream, CallMediaType.Video))
} else {
res = toggleMedia(activeCall.localStream, CallMediaType.Video)
const apiCall: WVAPICall = {command: {type: "media", media: CallMediaType.Video, enable: activeCall.cameraEnabled != true}}
reactOnMessageFromServer(apiCall as any)
processCommand(apiCall).then(() => {
enableVideoIcon(activeCall?.cameraEnabled == true)
})
}
document.getElementById("toggle-video")!!.innerHTML = res
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />'
}
}
@@ -73,6 +76,12 @@ async function toggleScreenManually() {
}
}
function enableVideoIcon(enabled: boolean) {
document.getElementById("toggle-video")!!.innerHTML = enabled
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />'
}
function reactOnMessageFromServer(msg: WVApiMessage) {
switch (msg.command?.type) {
case "capabilities":
@@ -82,24 +91,44 @@ function reactOnMessageFromServer(msg: WVApiMessage) {
case "start":
document.getElementById("toggle-audio")!!.style.display = "inline-block"
document.getElementById("toggle-speaker")!!.style.display = "inline-block"
if (msg.command.media == CallMediaType.Video) {
document.getElementById("toggle-video")!!.style.display = "inline-block"
document.getElementById("toggle-screen")!!.style.display = "inline-block"
}
document.getElementById("toggle-video")!!.style.display = "inline-block"
document.getElementById("toggle-screen")!!.style.display = "inline-block"
document.getElementById("info-block")!!.className = msg.command.media
break
case "media":
const className =
(msg.command.media == CallMediaType.Video && msg.command.enable) ||
activeCall?.peerMediaSources.camera ||
activeCall?.peerMediaSources.screen
? "video"
: "audio"
document.getElementById("info-block")!!.className = className
document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none"
break
case "description":
updateCallInfoView(msg.command.state, msg.command.description)
if (activeCall?.connection.connectionState == "connected") {
document.getElementById("progress")!.style.display = "none"
if (document.getElementById("info-block")!!.className == CallMediaType.Audio) {
document.getElementById("audio-call-icon")!.style.display = "block"
}
document.getElementById("audio-call-icon")!.style.display =
document.getElementById("info-block")!!.className == CallMediaType.Audio ? "block" : "none"
}
break
}
}
function reactOnMessageToServer(msg: WVApiMessage) {
switch (msg.resp?.type) {
case "peerMedia":
const className =
activeCall?.localMedia == CallMediaType.Video || activeCall?.peerMediaSources.camera || activeCall?.peerMediaSources.screen
? "video"
: "audio"
document.getElementById("info-block")!!.className = className
document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none"
break
}
}
function updateCallInfoView(state: string, description: string) {
document.getElementById("state")!!.innerText = state
document.getElementById("description")!!.innerText = description