diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/calls.js | 30 | ||||
| -rw-r--r-- | src/features/messages.js | 10 | ||||
| -rw-r--r-- | src/i18n.js | 1 |
3 files changed, 37 insertions, 4 deletions
diff --git a/src/calls.js b/src/calls.js index 6f18aec..e95b975 100644 --- a/src/calls.js +++ b/src/calls.js @@ -251,6 +251,32 @@ export function createCalls(env) { for (const t of call.local.getVideoTracks()) t.enabled = !on; changed(); } + // A voice call turned video: our camera into the call, by a new offer + // (NIP-AC's renegotiate). Into the video transceiver their camera already + // came by if there is one, else a new one. The other side sees us at once + // and gets the same button for theirs. + async function addVideo() { + const c = call; + if (!c || c.state !== 'connected' || !c.pc || !c.local || c.local.getVideoTracks().length) return; + try { + const fresh = await env.media('video-only', c.facing); + const track = fresh.getVideoTracks()[0]; + if (call !== c || c.state !== 'connected') { track.stop(); return; } + // a new stream object, so the preview picks the camera up + c.local = new MediaStream([...c.local.getTracks(), track]); + const tr = c.pc.getTransceivers().find((x) => !x.stopped && !x.sender.track && x.receiver.track && x.receiver.track.kind === 'video'); + if (tr) { await tr.sender.replaceTrack(track); tr.direction = 'sendrecv'; try { tr.sender.setStreams && tr.sender.setStreams(c.local); } catch {} } + else c.pc.addTrack(track, c.local); + c.type = 'video'; c.camOff = false; + changed(); + await renegotiate(c); + } catch {} + } + async function renegotiate(c) { + c.renegotiating = true; + await c.pc.setLocalDescription(await c.pc.createOffer()); + await sendFor(c, CALL.RENEGOTIATE, c.peer, c.pc.localDescription.sdp); + } // the other camera: the sender's track swapped, no new offer needed async function flip() { const c = call; @@ -357,6 +383,8 @@ export function createCalls(env) { await c.pc.setRemoteDescription({ type: 'offer', sdp: ev.content }); await c.pc.setLocalDescription(await c.pc.createAnswer()); await sendFor(c, CALL.ANSWER, c.peer, c.pc.localDescription.sdp); + // they turned their camera on: this is a video call now + if (c.type !== 'video' && /^m=video [1-9]/m.test(ev.content)) { c.type = 'video'; changed(); } } catch {} return; } @@ -365,7 +393,7 @@ export function createCalls(env) { return { get call() { return call; }, - start, accept, reject, hangup, setMuted, setCamOff, flip, handle, + start, accept, reject, hangup, setMuted, setCamOff, flip, addVideo, handle, // a sign-out or account switch: whatever was going on ends here stop() { if (call && call.state !== 'ended') hangup(); call = null; early.clear(); }, }; diff --git a/src/features/messages.js b/src/features/messages.js index 9604f6e..887f327 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -9532,6 +9532,7 @@ export function messagesFeature(ctx) { if (c.state === 'connected') setTimeout(scheduleRepaint, 1000); // the clock const video = c.type === 'video'; const remoteVideo = video && c.remote && c.remote.getVideoTracks().length > 0; + const camOn = !!c.local && c.local.getVideoTracks().length > 0; const sound = h('audio', { class: 'call-audio', 'data-key': 'call-audio:' + c.id, autoplay: '' }); const status = c.state === 'incoming' ? t(video ? 'callIncomingVideo' : 'callIncomingVoice') : c.state === 'starting' || c.state === 'offering' ? t('callCalling') @@ -9551,7 +9552,7 @@ export function messagesFeature(ctx) { avatar(c.peer, 'chat-avatar profile-avatar call-avatar', false), h('div', { class: 'call-name' }, displayName(c.peer)), h('div', { class: 'call-status' }, status)), - video && c.local ? h('video', { class: 'call-local' + (c.facing === 'user' ? ' mirror' : ''), 'data-key': 'call-local:' + c.id, autoplay: '', playsinline: '', muted: '' }) : null, + video && c.local && c.local.getVideoTracks().length ? h('video', { class: 'call-local' + (c.facing === 'user' ? ' mirror' : ''), 'data-key': 'call-local:' + c.id, autoplay: '', playsinline: '', muted: '' }) : null, c.state === 'incoming' ? h('div', { class: 'call-bar' }, btn('call-decline', 'hang', t('callDecline'), () => callsNow().reject()), @@ -9559,8 +9560,11 @@ export function messagesFeature(ctx) { : c.state === 'ended' ? null : h('div', { class: 'call-bar' }, btn('', c.muted ? 'micOff' : 'mic', t(c.muted ? 'callUnmute' : 'callMute'), () => callsNow().setMuted(!c.muted), c.muted), - video ? btn('', c.camOff ? 'camOff' : 'video', t(c.camOff ? 'callCamOn' : 'callCamOff'), () => callsNow().setCamOff(!c.camOff), c.camOff) : null, - video ? btn('', 'flip', t('callFlip'), () => callsNow().flip()) : null, + // a camera in the call: off/on and flip; none yet (a voice call, or + // theirs came on first): one tap puts ours in + camOn ? btn('', c.camOff ? 'camOff' : 'video', t(c.camOff ? 'callCamOn' : 'callCamOff'), () => callsNow().setCamOff(!c.camOff), c.camOff) : null, + camOn ? btn('', 'flip', t('callFlip'), () => callsNow().flip()) : null, + !camOn && c.state === 'connected' ? btn('call-add-video', 'video', t('callAddVideo'), () => callsNow().addVideo()) : null, btn('', 'min', t('callMinimize'), () => { ui.callMin = true; render(); }), btn('call-hang', 'hang', t('callHangUp'), () => callsNow().hangup()))); } diff --git a/src/i18n.js b/src/i18n.js index 613fae4..c2d0c86 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -537,6 +537,7 @@ const STR = { callUnmute: 'Unmute', callCamOff: 'Turn camera off', callCamOn: 'Turn camera on', + callAddVideo: 'Turn on camera', callFlip: 'Switch camera', callMinimize: 'Minimize', callEnded: 'Call ended', |
