SHA256
UI: стадии и звук звонка
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.349
|
client.version=1.2.350
|
||||||
server.version=1.2.319
|
server.version=1.2.319
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ function nowMs() {
|
|||||||
return Date.now();
|
return Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isMobileBrowser() {
|
||||||
|
const uaDataMobile = navigator?.userAgentData?.mobile;
|
||||||
|
if (typeof uaDataMobile === 'boolean') return uaDataMobile;
|
||||||
|
const ua = String(navigator?.userAgent || '');
|
||||||
|
return /Android|iPhone|iPad|iPod|Mobile/i.test(ua);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canRouteAudioOutputInBrowser() {
|
||||||
|
const mediaProto = globalThis?.HTMLMediaElement?.prototype;
|
||||||
|
return Boolean(
|
||||||
|
window.isSecureContext
|
||||||
|
&& isMobileBrowser()
|
||||||
|
&& mediaProto
|
||||||
|
&& typeof mediaProto.setSinkId === 'function'
|
||||||
|
&& navigator?.mediaDevices
|
||||||
|
&& typeof navigator.mediaDevices.selectAudioOutput === 'function'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeTimelineText(value) {
|
function sanitizeTimelineText(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
@@ -548,6 +567,12 @@ function getCallStateSnapshot() {
|
|||||||
if (!call) return null;
|
if (!call) return null;
|
||||||
if (call.localUiDismissed) return null;
|
if (call.localUiDismissed) return null;
|
||||||
const callPhase = String(call.phase || '').trim();
|
const callPhase = String(call.phase || '').trim();
|
||||||
|
const canToggleSpeaker = Boolean(
|
||||||
|
callPhase !== 'incoming'
|
||||||
|
&& callPhase !== 'ended'
|
||||||
|
&& canRouteAudioOutputInBrowser()
|
||||||
|
&& call.remoteAudio
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
callId: call.callId,
|
callId: call.callId,
|
||||||
peerLogin: call.peerLogin || '',
|
peerLogin: call.peerLogin || '',
|
||||||
@@ -557,6 +582,8 @@ function getCallStateSnapshot() {
|
|||||||
startedAtMs: Number(call.startedAtMs || 0),
|
startedAtMs: Number(call.startedAtMs || 0),
|
||||||
connectedAtMs: Number(call.connectedAtMs || 0),
|
connectedAtMs: Number(call.connectedAtMs || 0),
|
||||||
muted: Boolean(call.muted),
|
muted: Boolean(call.muted),
|
||||||
|
speakerEnabled: Boolean(call.speakerEnabled),
|
||||||
|
canToggleSpeaker,
|
||||||
canAnswer: callPhase === 'incoming',
|
canAnswer: callPhase === 'incoming',
|
||||||
canDecline: callPhase === 'incoming',
|
canDecline: callPhase === 'incoming',
|
||||||
canHangup: callPhase !== 'ended' && callPhase !== 'incoming',
|
canHangup: callPhase !== 'ended' && callPhase !== 'incoming',
|
||||||
@@ -1400,8 +1427,10 @@ async function ensurePeerConnection(call) {
|
|||||||
recordCallTimeline(call, 'remote_track_received', `streams=${evt?.streams?.length || 0}`);
|
recordCallTimeline(call, 'remote_track_received', `streams=${evt?.streams?.length || 0}`);
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
audio.autoplay = true;
|
audio.autoplay = true;
|
||||||
|
audio.playsInline = true;
|
||||||
audio.srcObject = evt.streams[0];
|
audio.srcObject = evt.streams[0];
|
||||||
call.remoteAudio = audio;
|
call.remoteAudio = audio;
|
||||||
|
notifyCallState();
|
||||||
};
|
};
|
||||||
|
|
||||||
call.pc = pc;
|
call.pc = pc;
|
||||||
@@ -1602,6 +1631,36 @@ export async function toggleMicMuted() {
|
|||||||
await setMicMuted(!call.muted);
|
await setMicMuted(!call.muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function toggleSpeakerMode() {
|
||||||
|
const call = getActiveCall();
|
||||||
|
if (!call || !call.remoteAudio || !canRouteAudioOutputInBrowser()) return false;
|
||||||
|
const audio = call.remoteAudio;
|
||||||
|
if (Boolean(call.speakerEnabled)) {
|
||||||
|
try {
|
||||||
|
await audio.setSinkId('');
|
||||||
|
call.speakerEnabled = false;
|
||||||
|
notifyCallState();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = call.speakerSinkId ? { deviceId: call.speakerSinkId } : undefined;
|
||||||
|
const selected = await navigator.mediaDevices.selectAudioOutput(options);
|
||||||
|
const deviceId = String(selected?.deviceId || '').trim();
|
||||||
|
if (!deviceId) return false;
|
||||||
|
await audio.setSinkId(deviceId);
|
||||||
|
call.speakerSinkId = deviceId;
|
||||||
|
call.speakerEnabled = true;
|
||||||
|
notifyCallState();
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function startDebugConnectionAsResponder({ runId, callId, peerLogin, peerSessionId }) {
|
export async function startDebugConnectionAsResponder({ runId, callId, peerLogin, peerSessionId }) {
|
||||||
const cleanCallId = String(callId || '').trim();
|
const cleanCallId = String(callId || '').trim();
|
||||||
const cleanPeerLogin = String(peerLogin || '').trim();
|
const cleanPeerLogin = String(peerLogin || '').trim();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
hangupActiveCall,
|
hangupActiveCall,
|
||||||
setMicMuted,
|
setMicMuted,
|
||||||
subscribeCallState,
|
subscribeCallState,
|
||||||
|
toggleSpeakerMode,
|
||||||
} from './call-service.js';
|
} from './call-service.js';
|
||||||
|
|
||||||
const MINIMIZED_BAR_HEIGHT_PX = 44;
|
const MINIMIZED_BAR_HEIGHT_PX = 44;
|
||||||
@@ -14,6 +15,7 @@ let panelEl = null;
|
|||||||
let headerEl = null;
|
let headerEl = null;
|
||||||
let titleEl = null;
|
let titleEl = null;
|
||||||
let statusEl = null;
|
let statusEl = null;
|
||||||
|
let speakerBtn = null;
|
||||||
let muteBtn = null;
|
let muteBtn = null;
|
||||||
let acceptBtn = null;
|
let acceptBtn = null;
|
||||||
let declineBtn = null;
|
let declineBtn = null;
|
||||||
@@ -22,6 +24,7 @@ let minimizeBtn = null;
|
|||||||
let barEl = null;
|
let barEl = null;
|
||||||
let barTextEl = null;
|
let barTextEl = null;
|
||||||
let barActionsEl = null;
|
let barActionsEl = null;
|
||||||
|
let barSpeakerBtn = null;
|
||||||
let barMuteBtn = null;
|
let barMuteBtn = null;
|
||||||
let barHangupBtn = null;
|
let barHangupBtn = null;
|
||||||
let unbind = null;
|
let unbind = null;
|
||||||
@@ -76,6 +79,12 @@ function getIconSvg(name) {
|
|||||||
if (name === 'minimize') {
|
if (name === 'minimize') {
|
||||||
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M7.5 10.5 12 6l4.5 4.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 6v12M7.5 10.5 12 6l4.5 4.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||||
}
|
}
|
||||||
|
if (name === 'speaker-on') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 14h4l5 4V6L8 10H4z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M16 9.5a4.5 4.5 0 0 1 0 5" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M18.8 7a8 8 0 0 1 0 10" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
|
||||||
|
}
|
||||||
|
if (name === 'speaker-off') {
|
||||||
|
return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 10H4v4h4l5 4V6z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/><path d="M16 8l4 8" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M20 8l-4 8" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
|
||||||
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +104,13 @@ function updateMuteButtons(muted) {
|
|||||||
if (barMuteBtn) barMuteBtn.dataset.muted = muted ? '1' : '0';
|
if (barMuteBtn) barMuteBtn.dataset.muted = muted ? '1' : '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSpeakerButtons(enabled) {
|
||||||
|
const iconName = enabled ? 'speaker-on' : 'speaker-off';
|
||||||
|
const label = enabled ? 'Выключить громкую связь' : 'Включить громкую связь';
|
||||||
|
setButtonIcon(speakerBtn, iconName, label);
|
||||||
|
setButtonIcon(barSpeakerBtn, iconName, label);
|
||||||
|
}
|
||||||
|
|
||||||
async function handleMuteClick(event) {
|
async function handleMuteClick(event) {
|
||||||
stopEvent(event);
|
stopEvent(event);
|
||||||
const nowMuted = event.currentTarget?.dataset?.muted === '1';
|
const nowMuted = event.currentTarget?.dataset?.muted === '1';
|
||||||
@@ -111,6 +127,11 @@ async function handleEndCallClick(event) {
|
|||||||
await hangupActiveCall();
|
await hangupActiveCall();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSpeakerClick(event) {
|
||||||
|
stopEvent(event);
|
||||||
|
await toggleSpeakerMode();
|
||||||
|
}
|
||||||
|
|
||||||
function setMinimized(nextValue) {
|
function setMinimized(nextValue) {
|
||||||
minimized = Boolean(nextValue);
|
minimized = Boolean(nextValue);
|
||||||
render();
|
render();
|
||||||
@@ -152,6 +173,11 @@ function ensureUi() {
|
|||||||
barActionsEl = document.createElement('div');
|
barActionsEl = document.createElement('div');
|
||||||
barActionsEl.className = 'call-minimized-bar-actions';
|
barActionsEl.className = 'call-minimized-bar-actions';
|
||||||
|
|
||||||
|
barSpeakerBtn = document.createElement('button');
|
||||||
|
barSpeakerBtn.type = 'button';
|
||||||
|
barSpeakerBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
|
||||||
|
barSpeakerBtn.addEventListener('click', handleSpeakerClick);
|
||||||
|
|
||||||
barMuteBtn = document.createElement('button');
|
barMuteBtn = document.createElement('button');
|
||||||
barMuteBtn.type = 'button';
|
barMuteBtn.type = 'button';
|
||||||
barMuteBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
|
barMuteBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--secondary';
|
||||||
@@ -162,7 +188,7 @@ function ensureUi() {
|
|||||||
barHangupBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--danger';
|
barHangupBtn.className = 'call-icon-btn call-icon-btn--bar call-icon-btn--danger';
|
||||||
barHangupBtn.addEventListener('click', handleEndCallClick);
|
barHangupBtn.addEventListener('click', handleEndCallClick);
|
||||||
|
|
||||||
barActionsEl.append(barMuteBtn, barHangupBtn);
|
barActionsEl.append(barSpeakerBtn, barMuteBtn, barHangupBtn);
|
||||||
barEl.append(barTextEl, barActionsEl);
|
barEl.append(barTextEl, barActionsEl);
|
||||||
|
|
||||||
overlayEl = document.createElement('div');
|
overlayEl = document.createElement('div');
|
||||||
@@ -194,6 +220,11 @@ function ensureUi() {
|
|||||||
const controls = document.createElement('div');
|
const controls = document.createElement('div');
|
||||||
controls.className = 'call-overlay-controls';
|
controls.className = 'call-overlay-controls';
|
||||||
|
|
||||||
|
speakerBtn = document.createElement('button');
|
||||||
|
speakerBtn.type = 'button';
|
||||||
|
speakerBtn.className = 'call-icon-btn call-icon-btn--secondary';
|
||||||
|
speakerBtn.addEventListener('click', handleSpeakerClick);
|
||||||
|
|
||||||
muteBtn = document.createElement('button');
|
muteBtn = document.createElement('button');
|
||||||
muteBtn.type = 'button';
|
muteBtn.type = 'button';
|
||||||
muteBtn.className = 'call-icon-btn call-icon-btn--secondary';
|
muteBtn.className = 'call-icon-btn call-icon-btn--secondary';
|
||||||
@@ -209,8 +240,8 @@ function ensureUi() {
|
|||||||
|
|
||||||
declineBtn = document.createElement('button');
|
declineBtn = document.createElement('button');
|
||||||
declineBtn.type = 'button';
|
declineBtn.type = 'button';
|
||||||
declineBtn.className = 'call-icon-btn call-icon-btn--danger';
|
declineBtn.className = 'destructive-btn call-decline-btn';
|
||||||
setButtonIcon(declineBtn, 'hangup', 'Отклонить звонок');
|
declineBtn.textContent = 'Отклонить';
|
||||||
declineBtn.addEventListener('click', async () => {
|
declineBtn.addEventListener('click', async () => {
|
||||||
await declineIncomingCall();
|
await declineIncomingCall();
|
||||||
});
|
});
|
||||||
@@ -221,7 +252,7 @@ function ensureUi() {
|
|||||||
setButtonIcon(hangupBtn, 'hangup', 'Положить трубку');
|
setButtonIcon(hangupBtn, 'hangup', 'Положить трубку');
|
||||||
hangupBtn.addEventListener('click', handleEndCallClick);
|
hangupBtn.addEventListener('click', handleEndCallClick);
|
||||||
|
|
||||||
controls.append(muteBtn, acceptBtn, declineBtn, hangupBtn);
|
controls.append(speakerBtn, muteBtn, acceptBtn, declineBtn, hangupBtn);
|
||||||
headerEl.append(titleEl, minimizeBtn);
|
headerEl.append(titleEl, minimizeBtn);
|
||||||
panelEl.append(headerEl, statusEl, controls);
|
panelEl.append(headerEl, statusEl, controls);
|
||||||
overlayEl.append(panelEl);
|
overlayEl.append(panelEl);
|
||||||
@@ -236,15 +267,20 @@ function applyExpandedState(snapshot) {
|
|||||||
titleEl.textContent = `Звонок: ${snapshot.peerLogin || 'пользователь'}`;
|
titleEl.textContent = `Звонок: ${snapshot.peerLogin || 'пользователь'}`;
|
||||||
statusEl.textContent = snapshot.statusText || '';
|
statusEl.textContent = snapshot.statusText || '';
|
||||||
|
|
||||||
|
const incomingStage = Boolean(snapshot.canAnswer || snapshot.canDecline);
|
||||||
const muted = Boolean(snapshot.muted);
|
const muted = Boolean(snapshot.muted);
|
||||||
updateMuteButtons(muted);
|
updateMuteButtons(muted);
|
||||||
|
updateSpeakerButtons(Boolean(snapshot.speakerEnabled));
|
||||||
|
|
||||||
muteBtn.hidden = !snapshot.canMute;
|
speakerBtn.hidden = incomingStage || !snapshot.canToggleSpeaker;
|
||||||
|
speakerBtn.disabled = !snapshot.canToggleSpeaker;
|
||||||
|
|
||||||
|
muteBtn.hidden = incomingStage || !snapshot.canMute;
|
||||||
muteBtn.disabled = !snapshot.canMute;
|
muteBtn.disabled = !snapshot.canMute;
|
||||||
|
|
||||||
acceptBtn.hidden = !snapshot.canAnswer;
|
acceptBtn.hidden = !snapshot.canAnswer;
|
||||||
declineBtn.hidden = !snapshot.canDecline;
|
declineBtn.hidden = !snapshot.canDecline;
|
||||||
hangupBtn.hidden = !snapshot.canHangup;
|
hangupBtn.hidden = incomingStage || !snapshot.canHangup;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMinimizedState(snapshot) {
|
function applyMinimizedState(snapshot) {
|
||||||
@@ -253,6 +289,9 @@ function applyMinimizedState(snapshot) {
|
|||||||
barTextEl.textContent = buildBarText(snapshot);
|
barTextEl.textContent = buildBarText(snapshot);
|
||||||
const muted = Boolean(snapshot.muted);
|
const muted = Boolean(snapshot.muted);
|
||||||
updateMuteButtons(muted);
|
updateMuteButtons(muted);
|
||||||
|
updateSpeakerButtons(Boolean(snapshot.speakerEnabled));
|
||||||
|
barSpeakerBtn.hidden = !snapshot.canToggleSpeaker;
|
||||||
|
barSpeakerBtn.disabled = !snapshot.canToggleSpeaker;
|
||||||
barMuteBtn.hidden = !snapshot.canMute;
|
barMuteBtn.hidden = !snapshot.canMute;
|
||||||
barMuteBtn.disabled = !snapshot.canMute;
|
barMuteBtn.disabled = !snapshot.canMute;
|
||||||
const canEnd = snapshot.canHangup || snapshot.canDecline;
|
const canEnd = snapshot.canHangup || snapshot.canDecline;
|
||||||
|
|||||||
@@ -1903,6 +1903,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.call-decline-btn {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
.call-accept-btn {
|
.call-accept-btn {
|
||||||
border: 1px solid rgba(122, 230, 166, 0.64);
|
border: 1px solid rgba(122, 230, 166, 0.64);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
|
|||||||
Reference in New Issue
Block a user