diff --git a/client/public/renderer-chat.js b/client/public/renderer-chat.js new file mode 100644 index 0000000..c164ca1 --- /dev/null +++ b/client/public/renderer-chat.js @@ -0,0 +1,61 @@ +export function createMessageElement(message, formatTimestamp) { + const messageDiv = document.createElement('div'); + let messageClass = 'chat-bubble'; + + if (message.type === 'system') { + messageClass += ' message-system'; + } else if (message.isSelf) { + messageClass += ' message-self'; + } else { + messageClass += ' message-other'; + } + + messageDiv.className = messageClass; + messageDiv.dataset.messageId = message.id; + + const contentHTML = message.type === 'file' && message.content.startsWith('data:image/') + ? ` +
+ ` + : ` + + `; + + messageDiv.innerHTML = ` + + + `; + + return messageDiv; +} + +export function renderChatMessagesInto(container, messages, formatTimestamp) { + if (!container) return; + + container.innerHTML = ''; + + const startTimeElement = document.createElement('div'); + startTimeElement.className = 'text-center text-xs text-gray-500 my-4'; + const startTime = messages[0]?.timestamp || new Date().toISOString(); + startTimeElement.textContent = `\u901a\u8bdd\u5f00\u59cb ${formatTimestamp(startTime)}`; + container.appendChild(startTimeElement); + + messages.forEach(message => { + container.appendChild(createMessageElement(message, formatTimestamp)); + }); + + container.scrollTop = container.scrollHeight; +} diff --git a/client/public/renderer-media.js b/client/public/renderer-media.js new file mode 100644 index 0000000..ca3c43a --- /dev/null +++ b/client/public/renderer-media.js @@ -0,0 +1,188 @@ +import { createParticipantTile, getParticipantTile } from './renderer-participant-grid.js'; + +export function getVideoResolution(track) { + if (track && track.getSettings) { + const settings = track.getSettings(); + return { + width: settings.width || 640, + height: settings.height || 480 + }; + } + + return { width: 640, height: 480 }; +} + +export function adjustVideoSize(videoElement) { + if (!videoElement) return; + + const container = videoElement.parentElement; + if (!container) return; + + videoElement.style.transform = 'translateZ(0)'; + videoElement.style.willChange = 'transform'; + container.style.display = 'flex'; + container.style.alignItems = 'center'; + container.style.justifyContent = 'center'; + videoElement.style.imageRendering = 'auto'; + videoElement.style.maxWidth = '100%'; + videoElement.style.maxHeight = '100%'; + videoElement.style.objectFit = 'contain'; +} + +export function renderParticipantStreamMedia({ + grid, + stream, + connectionId, + displayName, + getGridTemplateColumns, + remoteVideo, + connectingOverlay, + remoteVideoPlaceholder +}) { + if (!grid) return; + + grid.classList.remove('hidden'); + + let tile = getParticipantTile(grid, connectionId); + if (!tile) { + tile = createParticipantTile(connectionId, displayName); + grid.appendChild(tile); + console.log(`Created participant video tile for ${connectionId}`); + } + + const video = tile.querySelector('video'); + if (video && stream) { + if (video.srcObject === stream) { + console.log(`Same stream for participant ${connectionId}, ensuring playback`); + video.play().catch(error => console.log('Auto-play prevented:', error.message)); + } else { + video.srcObject = stream; + video.play().catch(error => console.log('Auto-play prevented:', error.message)); + console.log(`Set remote stream for participant tile ${connectionId}`); + } + } + + const remoteVideoContainer = remoteVideo?.closest('.absolute.inset-0.video-fade-in'); + if (remoteVideoContainer) { + remoteVideoContainer.classList.add('hidden'); + } + + const tileCount = grid.querySelectorAll('[data-participant-id]').length; + grid.style.gridTemplateColumns = getGridTemplateColumns(tileCount); + + if (connectingOverlay) { + connectingOverlay.classList.add('hidden'); + } + if (remoteVideoPlaceholder) { + remoteVideoPlaceholder.classList.add('hidden'); + } +} + +export function renderSingleRemoteStreamMedia({ + remoteVideo, + stream, + disconnectedOverlay, + remoteVideoPlaceholder, + connectingOverlay +}) { + if (!remoteVideo || !stream) { + console.error('Either remoteVideo element or stream is missing'); + return; + } + + console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(track => `${track.kind}(${track.readyState})`)); + + if (remoteVideo.srcObject === stream) { + console.log('Same stream object, track added - ensuring playback'); + remoteVideo.play().catch(error => console.log('Auto-play prevented:', error.message)); + return; + } + + remoteVideo.srcObject = stream; + remoteVideo.autoplay = true; + remoteVideo.playsinline = true; + remoteVideo.muted = false; + remoteVideo.play().catch(error => { + console.log('Auto-play prevented, will retry on interaction:', error.message); + }); + + if (disconnectedOverlay) { + disconnectedOverlay.classList.add('hidden'); + } + + const videoTracks = stream.getVideoTracks(); + const audioTracks = stream.getAudioTracks(); + console.log(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`); + + if (videoTracks.length === 0) { + console.log('Audio-only stream, waiting for video track...'); + return; + } + + if (remoteVideoPlaceholder) { + remoteVideoPlaceholder.classList.add('hidden'); + } + if (connectingOverlay) { + connectingOverlay.classList.add('hidden'); + } + + const activeVideoTrack = videoTracks.find(track => track.readyState === 'live'); + if (!activeVideoTrack) return; + + adjustVideoSize(remoteVideo, getVideoResolution(activeVideoTrack)); + activeVideoTrack.addEventListener('resize', () => { + adjustVideoSize(remoteVideo, getVideoResolution(activeVideoTrack)); + }); +} + +export function clearParticipantGrid(grid) { + if (!grid) return; + + grid.querySelectorAll('[data-participant-id]').forEach(tile => { + const video = tile.querySelector('video'); + if (video) { + video.srcObject = null; + } + tile.remove(); + }); + grid.classList.add('hidden'); +} + +export function removeParticipantTile({ + grid, + connectionId, + getGridTemplateColumns, + remoteVideo, + remoteVideoPlaceholder, + remoteNetworkIndicator +}) { + if (!grid) return; + + const tile = getParticipantTile(grid, connectionId); + if (tile) { + const video = tile.querySelector('video'); + if (video) { + video.srcObject = null; + } + tile.remove(); + console.log(`Removed participant video tile for ${connectionId}`); + } + + const remainingTiles = grid.querySelectorAll('[data-participant-id]'); + if (remainingTiles.length === 0) { + grid.classList.add('hidden'); + const remoteVideoContainer = remoteVideo?.closest('.absolute.inset-0.video-fade-in'); + if (remoteVideoContainer) { + remoteVideoContainer.classList.remove('hidden'); + } + if (remoteVideoPlaceholder) { + remoteVideoPlaceholder.classList.remove('hidden'); + } + } else { + grid.style.gridTemplateColumns = getGridTemplateColumns(remainingTiles.length); + } + + if (remoteNetworkIndicator) { + remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full'; + } +} diff --git a/client/public/renderer-participant-grid.js b/client/public/renderer-participant-grid.js new file mode 100644 index 0000000..2d2b8f1 --- /dev/null +++ b/client/public/renderer-participant-grid.js @@ -0,0 +1,64 @@ +function createParticipantPlaceholder() { + const placeholder = document.createElement('div'); + placeholder.className = 'participant-video-placeholder absolute inset-0 flex items-center justify-center bg-gradient-to-br from-indigo-900/80 to-purple-900/80 hidden'; + placeholder.innerHTML = ` +\u6444\u50cf\u5934\u5df2\u5173\u95ed
+摄像头已关闭
-