SHA256
Починить двустороннее видео в звонках
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.5.13
|
client.version=1.5.21
|
||||||
server.version=1.4.9
|
server.version=1.4.9
|
||||||
|
|||||||
@@ -661,9 +661,14 @@ function getCallStateSnapshot() {
|
|||||||
canDecline: callPhase === 'incoming',
|
canDecline: callPhase === 'incoming',
|
||||||
canHangup: callPhase !== 'ended' && callPhase !== 'incoming',
|
canHangup: callPhase !== 'ended' && callPhase !== 'incoming',
|
||||||
canMute: callPhase === 'active' || callPhase === 'connecting' || callPhase === 'ringing' || callPhase === 'reconnecting',
|
canMute: callPhase === 'active' || callPhase === 'connecting' || callPhase === 'ringing' || callPhase === 'reconnecting',
|
||||||
hasLocalVideo: localVideoTracks.some((track) => track?.readyState === 'live' && track?.enabled !== false),
|
hasLocalVideo: Boolean(
|
||||||
hasRemoteVideo: remoteVideoTracks.some((track) => track?.readyState === 'live'),
|
call.cameraEnabled
|
||||||
localPreviewStream: call.localStream || null,
|
&& call.localVideoTrack
|
||||||
|
&& call.localVideoTrack.readyState === 'live'
|
||||||
|
&& call.localVideoTrack.enabled !== false
|
||||||
|
),
|
||||||
|
hasRemoteVideo: remoteVideoTracks.some((track) => track?.readyState === 'live' && !track?.muted),
|
||||||
|
localPreviewStream: call.cameraEnabled ? (call.localStream || null) : null,
|
||||||
remoteMediaStream: call.remoteMediaStream || null,
|
remoteMediaStream: call.remoteMediaStream || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -701,7 +706,18 @@ function setStatus(call, statusText, phase = '') {
|
|||||||
|
|
||||||
function buildActiveStatusText(call) {
|
function buildActiveStatusText(call) {
|
||||||
const route = String(call?.connectionRouteLabel || '').trim();
|
const route = String(call?.connectionRouteLabel || '').trim();
|
||||||
return route ? `Разговор идёт (${route})` : 'Разговор идёт';
|
const videoSlotMarker = call?.videoSlotReady ? ' +' : '';
|
||||||
|
return route ? `Разговор идёт (${route}${videoSlotMarker})` : 'Разговор идёт';
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshVideoSlotReady(call) {
|
||||||
|
if (!call?.pc) return false;
|
||||||
|
const localSdp = String(call.pc.localDescription?.sdp || '');
|
||||||
|
const remoteSdp = String(call.pc.remoteDescription?.sdp || '');
|
||||||
|
const hasVideoInLocal = localSdp.includes('\nm=video') || localSdp.startsWith('m=video');
|
||||||
|
const hasVideoInRemote = remoteSdp.includes('\nm=video') || remoteSdp.startsWith('m=video');
|
||||||
|
call.videoSlotReady = Boolean(hasVideoInLocal && hasVideoInRemote && call.videoTransceiver);
|
||||||
|
return call.videoSlotReady;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActiveStatus(call) {
|
function setActiveStatus(call) {
|
||||||
@@ -1416,6 +1432,75 @@ async function notifyPeerSessionsAboutConnectStart(call) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureReservedVideoTransceiver(call) {
|
||||||
|
const pc = call?.pc || null;
|
||||||
|
if (!pc) return null;
|
||||||
|
if (call.videoTransceiver) return call.videoTransceiver;
|
||||||
|
const transceivers = pc.getTransceivers?.() || [];
|
||||||
|
const existing = transceivers.find((tr) => tr?.mid !== null && tr?.mid !== undefined && (tr?.receiver?.track?.kind === 'video' || tr?.sender?.track?.kind === 'video'))
|
||||||
|
|| transceivers.find((tr) => tr?.receiver?.track?.kind === 'video' || tr?.sender?.track?.kind === 'video')
|
||||||
|
|| null;
|
||||||
|
if (existing) {
|
||||||
|
call.videoTransceiver = existing;
|
||||||
|
call.videoSender = existing?.sender || call.videoSender || null;
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
const created = pc.addTransceiver('video', { direction: 'sendrecv' });
|
||||||
|
call.videoTransceiver = created;
|
||||||
|
call.videoSender = created?.sender || null;
|
||||||
|
return created;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRemoteTrackObservers(call, track) {
|
||||||
|
if (!call || !track || typeof track !== 'object') return;
|
||||||
|
if (!call.remoteTrackObserverIds) {
|
||||||
|
call.remoteTrackObserverIds = new Set();
|
||||||
|
}
|
||||||
|
if (call.remoteTrackObserverIds.has(track.id)) return;
|
||||||
|
call.remoteTrackObserverIds.add(track.id);
|
||||||
|
track.onended = () => {
|
||||||
|
if (track.kind === 'video' && call.remoteMediaStream) {
|
||||||
|
try { call.remoteMediaStream.removeTrack(track); } catch {}
|
||||||
|
}
|
||||||
|
notifyCallState();
|
||||||
|
};
|
||||||
|
track.onmute = () => notifyCallState();
|
||||||
|
track.onunmute = () => notifyCallState();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRemoteMediaStream(call) {
|
||||||
|
if (call?.remoteMediaStream) return call.remoteMediaStream;
|
||||||
|
const stream = new MediaStream();
|
||||||
|
if (call) {
|
||||||
|
call.remoteMediaStream = stream;
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncRemoteReceiverVideoTracks(call) {
|
||||||
|
const pc = call?.pc || null;
|
||||||
|
if (!pc) return false;
|
||||||
|
const remoteStream = ensureRemoteMediaStream(call);
|
||||||
|
let changed = false;
|
||||||
|
const receivers = pc.getReceivers?.() || [];
|
||||||
|
receivers.forEach((receiver) => {
|
||||||
|
const track = receiver?.track || null;
|
||||||
|
if (!track || track.kind !== 'video' || track.readyState === 'ended') return;
|
||||||
|
const hasTrack = remoteStream.getVideoTracks().some((existing) => existing.id === track.id);
|
||||||
|
if (!hasTrack) {
|
||||||
|
try {
|
||||||
|
remoteStream.addTrack(track);
|
||||||
|
changed = true;
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
ensureRemoteTrackObservers(call, track);
|
||||||
|
});
|
||||||
|
if (call.remoteAudio) {
|
||||||
|
call.remoteAudio.srcObject = remoteStream;
|
||||||
|
}
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
async function ensurePeerConnection(call) {
|
async function ensurePeerConnection(call) {
|
||||||
if (call.pc) return call.pc;
|
if (call.pc) return call.pc;
|
||||||
|
|
||||||
@@ -1479,6 +1564,7 @@ async function ensurePeerConnection(call) {
|
|||||||
if (!call.connectedAtMs) {
|
if (!call.connectedAtMs) {
|
||||||
call.connectedAtMs = nowMs();
|
call.connectedAtMs = nowMs();
|
||||||
}
|
}
|
||||||
|
refreshVideoSlotReady(call);
|
||||||
setActiveStatus(call);
|
setActiveStatus(call);
|
||||||
startTransportProbe(call);
|
startTransportProbe(call);
|
||||||
void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
|
void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
|
||||||
@@ -1538,11 +1624,25 @@ async function ensurePeerConnection(call) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pc.onsignalingstatechange = () => {
|
||||||
|
const state = String(pc.signalingState || '');
|
||||||
|
recordCallTimeline(call, 'pc_signaling_state', `state=${state || ''}`);
|
||||||
|
if (state === 'stable') {
|
||||||
|
if (syncRemoteReceiverVideoTracks(call)) {
|
||||||
|
notifyCallState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state === 'stable' && call.pendingNegotiationReason) {
|
||||||
|
void renegotiateCall(call, `signaling_stable:${call.pendingNegotiationReason}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
|
||||||
call.localStream = stream;
|
call.localStream = stream;
|
||||||
call.audioSenders = [];
|
call.audioSenders = [];
|
||||||
call.videoSender = null;
|
call.videoSender = null;
|
||||||
|
call.videoTransceiver = null;
|
||||||
call.connectionRouteLabel = '';
|
call.connectionRouteLabel = '';
|
||||||
call.connectionRouteDetails = '';
|
call.connectionRouteDetails = '';
|
||||||
recordCallTimeline(call, 'local_media_ready', `tracks=${stream.getTracks().length}`);
|
recordCallTimeline(call, 'local_media_ready', `tracks=${stream.getTracks().length}`);
|
||||||
@@ -1553,6 +1653,24 @@ async function ensurePeerConnection(call) {
|
|||||||
call.audioSenders.push(sender);
|
call.audioSenders.push(sender);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const blackTrack = ensureBlackVideoTrack(call);
|
||||||
|
if (blackTrack) {
|
||||||
|
const existingVideoTracks = stream.getVideoTracks?.() || [];
|
||||||
|
existingVideoTracks.forEach((track) => {
|
||||||
|
if (track.id !== blackTrack.id) {
|
||||||
|
try { stream.removeTrack(track); } catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!existingVideoTracks.some((track) => track.id === blackTrack.id)) {
|
||||||
|
stream.addTrack(blackTrack);
|
||||||
|
}
|
||||||
|
const sender = pc.addTrack(blackTrack, stream);
|
||||||
|
call.videoSender = sender;
|
||||||
|
call.videoTransceiver = (pc.getTransceivers?.() || []).find((tr) => tr?.sender === sender) || null;
|
||||||
|
bindVideoSenderStream(call, sender);
|
||||||
|
} else {
|
||||||
|
ensureReservedVideoTransceiver(call);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus(call, `Нет доступа к микрофону: ${e?.message || 'unknown'}`, 'failed');
|
setStatus(call, `Нет доступа к микрофону: ${e?.message || 'unknown'}`, 'failed');
|
||||||
recordCallTimeline(call, 'local_media_failed', toErrorText(e), 'warn');
|
recordCallTimeline(call, 'local_media_failed', toErrorText(e), 'warn');
|
||||||
@@ -1573,18 +1691,18 @@ async function ensurePeerConnection(call) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
call.remoteMediaStream = remoteStream;
|
call.remoteMediaStream = remoteStream;
|
||||||
|
if (trackKind === 'video') {
|
||||||
|
call.videoEverActivated = true;
|
||||||
|
}
|
||||||
if (!call.remoteAudio) {
|
if (!call.remoteAudio) {
|
||||||
const audio = new Audio();
|
const audio = new Audio();
|
||||||
audio.autoplay = true;
|
audio.autoplay = true;
|
||||||
audio.playsInline = true;
|
audio.playsInline = true;
|
||||||
call.remoteAudio = audio;
|
call.remoteAudio = audio;
|
||||||
}
|
}
|
||||||
if (evt?.track) {
|
if (evt?.track) ensureRemoteTrackObservers(call, evt.track);
|
||||||
evt.track.onended = () => notifyCallState();
|
|
||||||
evt.track.onmute = () => notifyCallState();
|
|
||||||
evt.track.onunmute = () => notifyCallState();
|
|
||||||
}
|
|
||||||
call.remoteAudio.srcObject = remoteStream;
|
call.remoteAudio.srcObject = remoteStream;
|
||||||
|
syncRemoteReceiverVideoTracks(call);
|
||||||
void call.remoteAudio.play?.().catch?.(() => {});
|
void call.remoteAudio.play?.().catch?.(() => {});
|
||||||
notifyCallState();
|
notifyCallState();
|
||||||
};
|
};
|
||||||
@@ -1596,27 +1714,47 @@ async function ensurePeerConnection(call) {
|
|||||||
return pc;
|
return pc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function queueNegotiationReason(call, reason = '') {
|
||||||
|
if (!call) return '';
|
||||||
|
const normalized = String(reason || '').trim() || 'pending';
|
||||||
|
call.pendingNegotiationReason = normalized;
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
async function renegotiateCall(call, reason = '') {
|
async function renegotiateCall(call, reason = '') {
|
||||||
if (!call?.pc || !call.remoteSessionId) return false;
|
if (!call?.pc || !call.remoteSessionId) return false;
|
||||||
|
const queuedReason = queueNegotiationReason(call, reason);
|
||||||
if (call.negotiationInProgress) {
|
if (call.negotiationInProgress) {
|
||||||
recordCallTimeline(call, 'renegotiate_skipped_busy', `reason=${reason || '-'}`);
|
recordCallTimeline(call, 'renegotiate_queued_busy', `reason=${queuedReason || '-'}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
const pc = call.pc;
|
||||||
|
if (pc.signalingState !== 'stable') {
|
||||||
|
recordCallTimeline(call, 'renegotiate_queued_unstable', `state=${pc.signalingState}; reason=${queuedReason || '-'}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const activeReason = String(call.pendingNegotiationReason || queuedReason || 'pending').trim() || 'pending';
|
||||||
|
call.pendingNegotiationReason = '';
|
||||||
call.negotiationInProgress = true;
|
call.negotiationInProgress = true;
|
||||||
try {
|
try {
|
||||||
const pc = call.pc;
|
recordCallTimeline(call, 'renegotiate_start', `reason=${activeReason || '-'}`);
|
||||||
if (pc.signalingState !== 'stable' && pc.localDescription?.type === 'offer') {
|
|
||||||
recordCallTimeline(call, 'renegotiate_wait_unstable', `state=${pc.signalingState}; reason=${reason || '-'}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
recordCallTimeline(call, 'renegotiate_start', `reason=${reason || '-'}`);
|
|
||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
await pc.setLocalDescription(offer);
|
await pc.setLocalDescription(offer);
|
||||||
await sendSignal(call, TYPES.OFFER, JSON.stringify(offer));
|
await sendSignal(call, TYPES.OFFER, JSON.stringify(offer));
|
||||||
recordCallTimeline(call, 'renegotiate_offer_sent', `reason=${reason || '-'}`);
|
recordCallTimeline(call, 'renegotiate_offer_sent', `reason=${activeReason || '-'}`);
|
||||||
return true;
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (!call.pendingNegotiationReason) {
|
||||||
|
call.pendingNegotiationReason = activeReason;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
call.negotiationInProgress = false;
|
call.negotiationInProgress = false;
|
||||||
|
if (call.pendingNegotiationReason && call.pc?.signalingState === 'stable') {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
void renegotiateCall(call, `queued:${call.pendingNegotiationReason}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1633,6 +1771,7 @@ async function onAccept(call) {
|
|||||||
try {
|
try {
|
||||||
await notifyPeerSessionsAboutConnectStart(call);
|
await notifyPeerSessionsAboutConnectStart(call);
|
||||||
const pc = await ensurePeerConnection(call);
|
const pc = await ensurePeerConnection(call);
|
||||||
|
ensureReservedVideoTransceiver(call);
|
||||||
const offer = await pc.createOffer();
|
const offer = await pc.createOffer();
|
||||||
await pc.setLocalDescription(offer);
|
await pc.setLocalDescription(offer);
|
||||||
call.initialOfferSent = true;
|
call.initialOfferSent = true;
|
||||||
@@ -1734,8 +1873,10 @@ async function handleIncomingInvitePayload(payload, { source = 'ws' } = {}) {
|
|||||||
remoteMediaStream: null,
|
remoteMediaStream: null,
|
||||||
audioSenders: [],
|
audioSenders: [],
|
||||||
videoSender: null,
|
videoSender: null,
|
||||||
|
videoTransceiver: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
cameraEnabled: false,
|
cameraEnabled: false,
|
||||||
|
videoEverActivated: false,
|
||||||
connectionRouteLabel: '',
|
connectionRouteLabel: '',
|
||||||
reconnectInProgress: false,
|
reconnectInProgress: false,
|
||||||
reconnectAttempts: 0,
|
reconnectAttempts: 0,
|
||||||
@@ -1743,6 +1884,7 @@ async function handleIncomingInvitePayload(payload, { source = 'ws' } = {}) {
|
|||||||
debugRunId: '',
|
debugRunId: '',
|
||||||
debugRole: '',
|
debugRole: '',
|
||||||
pendingRemoteIceCandidates: [],
|
pendingRemoteIceCandidates: [],
|
||||||
|
pendingNegotiationReason: '',
|
||||||
initialOfferInProgress: false,
|
initialOfferInProgress: false,
|
||||||
initialOfferSent: false,
|
initialOfferSent: false,
|
||||||
timelineEvents: [],
|
timelineEvents: [],
|
||||||
@@ -1855,9 +1997,34 @@ async function ensureCameraTrack(call) {
|
|||||||
});
|
});
|
||||||
call.localStream.addTrack(videoTrack);
|
call.localStream.addTrack(videoTrack);
|
||||||
call.localVideoTrack = videoTrack;
|
call.localVideoTrack = videoTrack;
|
||||||
|
call.videoEverActivated = true;
|
||||||
return videoTrack;
|
return videoTrack;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureBlackVideoTrack(call) {
|
||||||
|
const existingTrack = call?.placeholderBlackVideoTrack || null;
|
||||||
|
if (existingTrack && existingTrack.readyState === 'live') {
|
||||||
|
existingTrack.enabled = true;
|
||||||
|
return existingTrack;
|
||||||
|
}
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 640;
|
||||||
|
canvas.height = 360;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx || typeof canvas.captureStream !== 'function') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ctx.fillStyle = '#000000';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
const stream = canvas.captureStream(1);
|
||||||
|
const track = stream.getVideoTracks?.()[0] || null;
|
||||||
|
if (!track) return null;
|
||||||
|
call.placeholderBlackCanvas = canvas;
|
||||||
|
call.placeholderBlackStream = stream;
|
||||||
|
call.placeholderBlackVideoTrack = track;
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
async function disableCameraTrack(call) {
|
async function disableCameraTrack(call) {
|
||||||
const videoTrack = call?.localVideoTrack || null;
|
const videoTrack = call?.localVideoTrack || null;
|
||||||
if (videoTrack) {
|
if (videoTrack) {
|
||||||
@@ -1868,37 +2035,71 @@ async function disableCameraTrack(call) {
|
|||||||
call.localVideoTrack = null;
|
call.localVideoTrack = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyCameraState(call, { renegotiate = true, reason = '' } = {}) {
|
function bindVideoSenderStream(call, sender) {
|
||||||
|
if (!call?.localStream || !sender || typeof sender.setStreams !== 'function') return;
|
||||||
|
try {
|
||||||
|
sender.setStreams(call.localStream);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyCameraState(call) {
|
||||||
if (!call?.pc) return false;
|
if (!call?.pc) return false;
|
||||||
const pc = call.pc;
|
const pc = call.pc;
|
||||||
const transceivers = pc.getTransceivers?.() || [];
|
let videoTransceiver = call.videoTransceiver || null;
|
||||||
let videoTransceiver = transceivers.find((tr) => tr?.sender?.track?.kind === 'video' || tr?.receiver?.track?.kind === 'video') || null;
|
if (!videoTransceiver) {
|
||||||
|
const transceivers = pc.getTransceivers?.() || [];
|
||||||
|
videoTransceiver = transceivers.find((tr) => tr?.sender?.track?.kind === 'video' || tr?.receiver?.track?.kind === 'video') || null;
|
||||||
|
call.videoTransceiver = videoTransceiver;
|
||||||
|
call.videoSender = videoTransceiver?.sender || call.videoSender || null;
|
||||||
|
}
|
||||||
if (call.cameraEnabled) {
|
if (call.cameraEnabled) {
|
||||||
const videoTrack = await ensureCameraTrack(call);
|
const videoTrack = await ensureCameraTrack(call);
|
||||||
if (!videoTransceiver) {
|
if (!videoTransceiver) {
|
||||||
const sender = pc.addTrack(videoTrack, call.localStream);
|
videoTransceiver = pc.addTransceiver('video', { direction: 'sendrecv' });
|
||||||
call.videoSender = sender;
|
call.videoTransceiver = videoTransceiver;
|
||||||
videoTransceiver = transceivers.find((tr) => tr?.sender === sender) || null;
|
call.videoSender = videoTransceiver?.sender || null;
|
||||||
} else {
|
}
|
||||||
|
try {
|
||||||
|
if (videoTransceiver && videoTransceiver.direction !== 'sendrecv') {
|
||||||
|
videoTransceiver.direction = 'sendrecv';
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
if (videoTransceiver?.sender) {
|
||||||
await videoTransceiver.sender.replaceTrack(videoTrack);
|
await videoTransceiver.sender.replaceTrack(videoTrack);
|
||||||
call.videoSender = videoTransceiver.sender;
|
call.videoSender = videoTransceiver.sender;
|
||||||
}
|
bindVideoSenderStream(call, videoTransceiver.sender);
|
||||||
if (videoTransceiver) {
|
} else {
|
||||||
try { videoTransceiver.direction = 'sendrecv'; } catch {}
|
const sender = pc.addTrack(videoTrack, call.localStream);
|
||||||
|
call.videoSender = sender;
|
||||||
|
bindVideoSenderStream(call, sender);
|
||||||
}
|
}
|
||||||
videoTrack.enabled = true;
|
videoTrack.enabled = true;
|
||||||
} else {
|
} else {
|
||||||
if (videoTransceiver) {
|
const blackTrack = ensureBlackVideoTrack(call);
|
||||||
try { await videoTransceiver.sender.replaceTrack(null); } catch {}
|
if (call.localStream && blackTrack) {
|
||||||
|
const existingVideoTracks = call.localStream.getVideoTracks?.() || [];
|
||||||
|
existingVideoTracks.forEach((track) => {
|
||||||
|
if (track.id !== blackTrack.id) {
|
||||||
|
try { call.localStream.removeTrack(track); } catch {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!existingVideoTracks.some((track) => track.id === blackTrack.id)) {
|
||||||
|
call.localStream.addTrack(blackTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (videoTransceiver && videoTransceiver.direction !== 'sendrecv') {
|
||||||
|
videoTransceiver.direction = 'sendrecv';
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
if (videoTransceiver?.sender) {
|
||||||
|
try { await videoTransceiver.sender.replaceTrack(blackTrack || null); } catch {}
|
||||||
call.videoSender = videoTransceiver.sender;
|
call.videoSender = videoTransceiver.sender;
|
||||||
try { videoTransceiver.direction = 'recvonly'; } catch {}
|
bindVideoSenderStream(call, videoTransceiver.sender);
|
||||||
}
|
}
|
||||||
await disableCameraTrack(call);
|
await disableCameraTrack(call);
|
||||||
}
|
}
|
||||||
notifyCallState();
|
notifyCallState();
|
||||||
if (renegotiate) {
|
|
||||||
await renegotiateCall(call, reason || (call.cameraEnabled ? 'camera_on' : 'camera_off'));
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1923,7 +2124,7 @@ export async function setCameraEnabled(enabled) {
|
|||||||
if (Boolean(call.cameraEnabled) === nextEnabled) return true;
|
if (Boolean(call.cameraEnabled) === nextEnabled) return true;
|
||||||
call.cameraEnabled = nextEnabled;
|
call.cameraEnabled = nextEnabled;
|
||||||
try {
|
try {
|
||||||
await applyCameraState(call, { renegotiate: true, reason: nextEnabled ? 'camera_on' : 'camera_off' });
|
await applyCameraState(call);
|
||||||
notifyCallState();
|
notifyCallState();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1993,13 +2194,16 @@ export async function startDebugConnectionAsResponder({ runId, callId, peerLogin
|
|||||||
remoteMediaStream: null,
|
remoteMediaStream: null,
|
||||||
audioSenders: [],
|
audioSenders: [],
|
||||||
videoSender: null,
|
videoSender: null,
|
||||||
|
videoTransceiver: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
cameraEnabled: false,
|
cameraEnabled: false,
|
||||||
|
videoEverActivated: false,
|
||||||
connectionRouteLabel: '',
|
connectionRouteLabel: '',
|
||||||
debugMode: true,
|
debugMode: true,
|
||||||
debugRunId: String(runId || '').trim(),
|
debugRunId: String(runId || '').trim(),
|
||||||
debugRole: 'responder',
|
debugRole: 'responder',
|
||||||
pendingRemoteIceCandidates: [],
|
pendingRemoteIceCandidates: [],
|
||||||
|
pendingNegotiationReason: '',
|
||||||
initialOfferInProgress: false,
|
initialOfferInProgress: false,
|
||||||
initialOfferSent: false,
|
initialOfferSent: false,
|
||||||
};
|
};
|
||||||
@@ -2036,13 +2240,16 @@ export async function startDebugConnectionAsInitiator({ runId, callId, peerLogin
|
|||||||
remoteMediaStream: null,
|
remoteMediaStream: null,
|
||||||
audioSenders: [],
|
audioSenders: [],
|
||||||
videoSender: null,
|
videoSender: null,
|
||||||
|
videoTransceiver: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
cameraEnabled: false,
|
cameraEnabled: false,
|
||||||
|
videoEverActivated: false,
|
||||||
connectionRouteLabel: '',
|
connectionRouteLabel: '',
|
||||||
debugMode: true,
|
debugMode: true,
|
||||||
debugRunId: String(runId || '').trim(),
|
debugRunId: String(runId || '').trim(),
|
||||||
debugRole: 'initiator',
|
debugRole: 'initiator',
|
||||||
pendingRemoteIceCandidates: [],
|
pendingRemoteIceCandidates: [],
|
||||||
|
pendingNegotiationReason: '',
|
||||||
initialOfferInProgress: false,
|
initialOfferInProgress: false,
|
||||||
initialOfferSent: false,
|
initialOfferSent: false,
|
||||||
timelineEvents: [],
|
timelineEvents: [],
|
||||||
@@ -2095,17 +2302,20 @@ export async function startOutgoingCall(peerLogin) {
|
|||||||
remoteMediaStream: null,
|
remoteMediaStream: null,
|
||||||
audioSenders: [],
|
audioSenders: [],
|
||||||
videoSender: null,
|
videoSender: null,
|
||||||
|
videoTransceiver: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
cameraEnabled: false,
|
cameraEnabled: false,
|
||||||
|
videoEverActivated: false,
|
||||||
connectionRouteLabel: '',
|
connectionRouteLabel: '',
|
||||||
reconnectInProgress: false,
|
reconnectInProgress: false,
|
||||||
reconnectAttempts: 0,
|
reconnectAttempts: 0,
|
||||||
debugMode: false,
|
debugMode: false,
|
||||||
debugRunId: '',
|
debugRunId: '',
|
||||||
debugRole: '',
|
debugRole: '',
|
||||||
pendingRemoteIceCandidates: [],
|
pendingRemoteIceCandidates: [],
|
||||||
initialOfferInProgress: false,
|
pendingNegotiationReason: '',
|
||||||
initialOfferSent: false,
|
initialOfferInProgress: false,
|
||||||
|
initialOfferSent: false,
|
||||||
timelineEvents: [],
|
timelineEvents: [],
|
||||||
timelineSeq: 0,
|
timelineSeq: 0,
|
||||||
timelineBaseMs: nowMs(),
|
timelineBaseMs: nowMs(),
|
||||||
@@ -2347,10 +2557,13 @@ export async function handleIncomingCallSignal(evt) {
|
|||||||
await pc.setLocalDescription({ type: 'rollback' });
|
await pc.setLocalDescription({ type: 'rollback' });
|
||||||
}
|
}
|
||||||
await pc.setRemoteDescription(incomingOffer);
|
await pc.setRemoteDescription(incomingOffer);
|
||||||
|
ensureReservedVideoTransceiver(call);
|
||||||
|
syncRemoteReceiverVideoTracks(call);
|
||||||
await flushPendingIceCandidates(call);
|
await flushPendingIceCandidates(call);
|
||||||
const answer = await pc.createAnswer();
|
const answer = await pc.createAnswer();
|
||||||
await pc.setLocalDescription(answer);
|
await pc.setLocalDescription(answer);
|
||||||
await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
|
await sendSignal(call, TYPES.ANSWER, JSON.stringify(answer));
|
||||||
|
refreshVideoSlotReady(call);
|
||||||
setStatus(call, 'Соединяем…', 'connecting');
|
setStatus(call, 'Соединяем…', 'connecting');
|
||||||
notifyCallState();
|
notifyCallState();
|
||||||
await emitDebug(call, 'info', 'offer_processed', 'answer sent');
|
await emitDebug(call, 'info', 'offer_processed', 'answer sent');
|
||||||
@@ -2382,7 +2595,10 @@ export async function handleIncomingCallSignal(evt) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(data)));
|
||||||
|
ensureReservedVideoTransceiver(call);
|
||||||
|
syncRemoteReceiverVideoTracks(call);
|
||||||
await flushPendingIceCandidates(call);
|
await flushPendingIceCandidates(call);
|
||||||
|
refreshVideoSlotReady(call);
|
||||||
recordCallTimeline(call, 'answer_applied', `from=${fromSessionId || '-'}`);
|
recordCallTimeline(call, 'answer_applied', `from=${fromSessionId || '-'}`);
|
||||||
setStatus(call, 'Соединяем…', 'connecting');
|
setStatus(call, 'Соединяем…', 'connecting');
|
||||||
notifyCallState();
|
notifyCallState();
|
||||||
|
|||||||
@@ -367,7 +367,8 @@ function applyExpandedState(snapshot) {
|
|||||||
mediaStageEl.hidden = incomingStage;
|
mediaStageEl.hidden = incomingStage;
|
||||||
const remoteStream = snapshot.remoteMediaStream || null;
|
const remoteStream = snapshot.remoteMediaStream || null;
|
||||||
const localStream = snapshot.localPreviewStream || null;
|
const localStream = snapshot.localPreviewStream || null;
|
||||||
if (remoteVideoEl.srcObject !== remoteStream) remoteVideoEl.srcObject = remoteStream;
|
const remoteVideoSource = snapshot.hasRemoteVideo ? remoteStream : null;
|
||||||
|
if (remoteVideoEl.srcObject !== remoteVideoSource) remoteVideoEl.srcObject = remoteVideoSource;
|
||||||
if (localVideoEl.srcObject !== localStream) localVideoEl.srcObject = localStream;
|
if (localVideoEl.srcObject !== localStream) localVideoEl.srcObject = localStream;
|
||||||
remoteVideoEl.hidden = !snapshot.hasRemoteVideo;
|
remoteVideoEl.hidden = !snapshot.hasRemoteVideo;
|
||||||
remoteVideoPlaceholderEl.hidden = snapshot.hasRemoteVideo;
|
remoteVideoPlaceholderEl.hidden = snapshot.hasRemoteVideo;
|
||||||
|
|||||||
@@ -118,7 +118,10 @@ export function parseDmTechBlocks(rawText = '') {
|
|||||||
const status = String(fields.status || '').trim().toLowerCase();
|
const status = String(fields.status || '').trim().toLowerCase();
|
||||||
if (status === 'completed') {
|
if (status === 'completed') {
|
||||||
const durationSec = Math.max(0, Math.floor(Number(fields.duration || 0)));
|
const durationSec = Math.max(0, Math.floor(Number(fields.duration || 0)));
|
||||||
callSummary = { status: 'completed', durationSec };
|
callSummary = {
|
||||||
|
status: 'completed',
|
||||||
|
durationSec,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
callSummary = {
|
callSummary = {
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
|||||||
Reference in New Issue
Block a user