/** * UI婵炴挸寮堕悡瀣闯? * 閻犳劗鍠曢惌妤冧焊閸℃瑥笑闁诡兛鐒﹀Σ褏浜搁崟顐㈢厒DOM闁挎稑濂旂粭宀勬偐閼哥鍋撴担渚悁闁荤偛妫滆闁? */ import { formatTime, formatTimestamp, toggleElement, toggleButtonState } from './utils.js'; import { mockCallSession } from './models.js'; import chatMessage from './chatmessage.js'; import store from './store.js'; import { buildUserCountLabel, createUserEntryElement, getCallTitle, getMediaStatusMeta, getNetworkQualityDisplay, getRemoteVideoPlaceholderText } from './renderer-ui.js'; const GRID_LAYOUT = { maxColumns: 3, breakpoints: [ { maxParticipants: 1, template: '1fr' }, { maxParticipants: 4, template: 'repeat(2, 1fr)' } ], defaultTemplate: 'repeat(3, 1fr)' }; function getGridTemplateColumns(participantCount) { for (const bp of GRID_LAYOUT.breakpoints) { if (participantCount <= bp.maxParticipants) { return bp.template; } } return GRID_LAYOUT.defaultTemplate; } class UIRenderer { constructor(stateManager) { this.stateManager = stateManager; this.unsubscribe = stateManager.subscribe(this.render.bind(this)); // 缂傚倹鎸搁悺?DOM 闁稿繐鍟扮粈? this.elements = { // 濠㈣埖鎸抽崕鎾椽鐏炵晫淇洪梺? header: document.querySelector('header'), footer: document.querySelector('footer'), // 濠㈣埖鐡峚rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹? participantGrid: document.getElementById('participantGrid'), // 濠㈣埖鎸抽崕鎾礃閸涱収鍟? headerTitle: document.getElementById('headerTitle'), callDuration: document.getElementById('callDuration'), encryptionBadge: document.getElementById('encryptionBadge'), unreadBadge: document.getElementById('unreadBadge'), remoteNetworkIndicator: document.getElementById('remoteNetworkIndicator'), remoteNetworkQuality: document.getElementById('remoteNetworkQuality'), // 閺夆晜绮庨顒傛喆閸℃侗鏆? remoteVideo: document.getElementById('remoteVideo'), remoteVideoPlaceholder: document.getElementById('remoteVideoPlaceholder'), networkStatus: document.getElementById('networkStatus'), networkStatusText: document.getElementById('networkStatusText'), connectingOverlay: document.getElementById('connectingOverlay'), // 闁哄牜鍓欏﹢瀵告喆閸℃侗鏆? localVideo: document.getElementById('localVideo'), localVideoPlaceholder: document.getElementById('localVideoPlaceholder'), localAudioWave: document.getElementById('localAudioWave'), localInitials: document.getElementById('localInitials'), // 濞撴皜鍡欑彾闁? sidebar: document.getElementById('sidebar'), chatContent: document.getElementById('chatContent'), userList: document.getElementById('userList'), localMediaStatus: document.getElementById('localMediaStatus'), localMuteIcon: document.querySelector('[data-field="localUser.muteIcon"]'), userCountDisplay: document.getElementById('userCountDisplay'), // 闁硅矇鍐ㄧ厬闁圭顦甸幐? micBtn: document.getElementById('micBtn'), videoBtn: document.getElementById('videoBtn'), recordBtn: document.getElementById('recordBtn'), connectionQuality: document.getElementById('connectionQuality') }; // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕? this.bindEventListeners(); // 閻犱降鍨藉Σ鍕偐閼哥鍋撴担绋跨秮闁? this.unsubscribe = stateManager.subscribe(this.render.bind(this)); // 閻犱降鍨藉Σ鍕槈閸喍绱栭柣妯垮煐閳ь兛绀佽ぐ澶愬礌? this.messageUnsubscribe = chatMessage.subscribe(this.renderMessageState.bind(this)); // 闁告帗绻傞~鎰板礌閺嶃劏顩柡? this.render(this.stateManager.getState(), { type: 'INIT' }); window.addEventListener('resize', () => { if (this.elements.remoteVideo && this.elements.remoteVideo.srcObject) { const stream = this.elements.remoteVideo.srcObject; const videoTracks = stream.getVideoTracks(); if (videoTracks.length > 0) { const resolution = this.getVideoResolution(videoTracks[0]); this.adjustVideoSize(this.elements.remoteVideo, resolution); } } }); } // 婵炴挸寮堕悡瀣槈閸喍绱栭柣妯垮煐閳ь兛绀佽ぐ澶愬礌? renderMessageState(messageState, changes) { switch (changes.type) { case 'NEW_MESSAGE': this.renderChatMessages(messageState.messages); this.renderUnreadCount(changes.unreadCount); break; case 'SIDEBAR_TOGGLE': this.renderSidebar(changes.isOpen); // 鐟滅増鎸烽弲鑸垫綇鐟欏嫮鍩夐柟鍨尭缁辨垿寮拋鍦闂佹彃绉堕悿鍡涘嫉椤忓浂鍤㈡繛鎴濈墛娴煎懐鎷嬮埄鍐╂ if (changes.isOpen) { this.renderUnreadCount(0); } else { this.renderUnreadCount(changes.unreadCount); } break; } } // 缂備焦鍨甸悾鐐鐎b晜顐介柣鈺傚灥閹宕? bindEventListeners() { // 濞存粌顑勫▎銏ゆ儎閹存繃鍎旈柛? } /** * 婵炴挸寮堕悡瀣棘鐟欏嫮銆?- 闁哄秷顫夊畵渚€鎮╅懜纰樺亾娴g缍侀柛鏍ㄧ墬濞插潡寮惂鐕? * @param {Object} state - 鐟滅増鎸告晶鐘虫償閺冨倹鏆忛柣妯垮煐閳? * @param {Object} changes - 闁绘鍩栭埀顑跨瑜板宕犻弽褜鍤犻悹? */ render(state, changes) { // 闁哄秷顫夊畵渚€宕eΟ鍝勵嚙缂侇偉顕ч悗鐑藉箥瑜戦、鎴炵▔瀹ュ懏鍊遍柣銊ュ鐟曞棝寮婚幘瀛樻儥濞? switch (changes.type) { case 'INIT': // 闁告帗绻傞~鎰板礌閺嶃劏顩柡?- 婵炴挸寮堕悡瀣箥閳ь剟寮垫繅姗ч柛蹇撳暟缁€? this.renderRemoteVideo(state.session.remoteUser); // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤? this.renderLocalVideo(state.session.localUser, state.localStream); // 婵炴挸寮堕悡瀣嫉椤掆偓濠€瀵告喆閸℃侗鏆? this.renderControlButtons(state.session.localUser.mediaState); // 婵炴挸寮堕悡瀣箳瑜嶉崺妤呭箰婢舵劖灏? this.renderChatMessages(chatMessage.getMessageState().messages); // 婵炴挸寮堕悡瀣嚂婵犲倶浜繛鎴濈墛娴? this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵? this.renderHeader(state.session); // 婵炴挸寮堕悡瀣緞閹绢喖鍔ュǎ鍥e墲娴? // 闁告帗绻傞~鎰板礌閺嶃劍顦ф俊顐熷亾闁哄被鍎寸换娆戠矙鐎n偆銈﹂柣妯垮煐閳ь兛绶ょ槐婵嬪及閸撗佷粵闁瑰瓨鐗犲▓锝夋寠韫囨挸绐楀ù锝呯Х閸庢寮? if (this.elements.remoteVideoPlaceholder) { if (state.remoteStream) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); // 闁哄牆顦崇换娆戠矙鐎n偆銈﹂柡鍐ㄧ埣濞堬綁鎸婅箛鎾崇獥濞达絽绉烽崕妤呭疾? } else { this.elements.remoteVideoPlaceholder.classList.remove('hidden'); // 闁哄啰濮剧换娆戠矙鐎n偆銈﹂柡鍐煐濡绮堥崫鍕獥濞达絽绉烽崕妤呭疾? } } break; case 'DURATION_UPDATE': // 闂侇偅淇洪惁浠嬪籍閸洘姣愰柡鍥х摠閺?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺? this.renderCallDuration(changes.duration); break; case 'LOCAL_MEDIA_CHANGE': // 闁哄牜鍓欏﹢瀛樺垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠囨儎缁嬪灝褰燯I this.renderControlButtons(state.session.localUser.mediaState); // 婵炴挸寮堕悡瀣箳瑜嶉崺妤呭箰婢舵劖灏? this.renderLocalVideo(state.session.localUser, state.localStream); // 婵炴挸寮堕悡瀣嫉椤掆偓濠€瀵告喆閸℃侗鏆? this.renderLocalUserStatus(state.session.localUser); // 婵炴挸寮堕悡瀣嫉椤掆偓濠€鎾偨閵婏箑鐓曢柣妯垮煐閳? this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵? break; case 'LOCAL_STREAM_OBTAINED': this.renderLocalStream(state.localStream); this.renderLocalVideo(state.session.localUser, state.localStream); break; case 'REMOTE_STREAM_OBTAINED': // 閺夆晜绮庨埢鐓幟规担钘夌闁告瑦鐗楅崹姘跺礉?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼寮伴崜褋浠? this.renderRemoteStream(changes.stream, changes.connectionId, changes.isHost); // 婵炴挸寮堕悡瀣交濠婂應鏌ゆ繛? // 鐟滅増鎹侀獮蹇涘矗閺嵮冪厒閺夆晜绮庨埢鐓幟规担瑙勵槯闁挎稑鐭傚▓锝夋寠韫囨氨绠鹃柟鎭掑劙閼垫垿骞撻幇顔轰粵 if (this.elements.connectingOverlay) { this.elements.connectingOverlay.classList.add('hidden'); } break; case 'REMOTE_MEDIA_CHANGE': // 閺夆晜绮庨埢鍏煎垔閹哄秶绉奸柣妯垮煐閳ь兛绀佽ぐ澶愬礌?- 闁哄洤鐡ㄩ弻濠冩交濠婂應鏌ら悷娆忔椤e爼宕畝鈧弫銈夊箣瀹勬澘鐏欓悶? this.renderRemoteVideo(state.session.remoteUser); // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤? this.renderUserList(state.session.localUser, state.session.remoteUser, state.participants); // 婵炴挸寮堕悡瀣偨閵婏箑鐓曢柛鎺擃殙閵? // Host缂佹棏鍨界槐鎵垝閹冩珯闁哄洤鐡ㄩ弻濠囧矗閹达腹鍋撴担瑙e亾閸楃瀽rticipant tile闁汇劌瀚畷鐗堟媴瀹ュ牆鍓归柡? // 闁告瑯浜濆﹢涓爋st缂佹棏鍨跺〒鍓佹啺娴e壊妲遍柣鐐叉晙articipantId閻庣數鎳撶花鏌ユ儍閸撳檮le闁告濮崇紞鍛箔閿旇偐绀凱articipant缂佹棏鍨遍惀鍛村嫉婢跺﹤寰撳ù鐘虫耿articipant闁汇劌瀚~瀣紣閹寸偟銈﹂柨? if (changes.participantId && state.session.localUser.isHost) { // 濞寸姴绐媋rticipants Map濞戞搩鍙€椤曚即宕i弽顒夊殙participant闁汇劌澧榠deo闁绘鍩栭埀顑跨筏缁辨繈鎳撳畝鍕remoteUser闁挎稑鐗嗛ˇ绺媋rticipant闁革妇鍎ゅ▍妾檈moteUser濞戞挸绉剁花璺ㄦ兜椤曞棛绀? const pInfo = state.participants[changes.participantId]; const showPlaceholder = pInfo ? !pInfo.mediaState.video : true; this.renderParticipantVideoPlaceholder(changes.participantId, showPlaceholder); } break; case 'USER_LIST_UPDATE': // 闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴寮寸€涙ɑ鐓€ - 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧? this.renderUserList(changes.localUser, changes.remoteUser, state.participants); break; case 'PARTICIPANTS_UPDATE': // Participants濞e洠鍓濇导鍛村矗濡搫顕?- 闂佹彃绉甸弻濠傘€掗崣澶屽帬闁活潿鍔嶉崺娑㈠礆濡ゅ嫨鈧啴鐛捄鐑樺€辨慨婵愭ile闁告艾绉惰ⅷ this.renderUserList(state.session.localUser, state.session.remoteUser, changes.participants || state.participants); // 闁告艾鏈鐐哄即鐎涙ɑ鐓€participant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒? this.syncParticipantTileNames(changes.participants || state.participants); break; case 'NETWORK_CHANGE': // 缂傚啯鍨圭划鍫曟偐閼哥鍋撴担绋跨秮闁?- 婵炴挸寮堕悡瀣磾閹寸姷鎹曢柣妯垮煐閳? this.renderNetworkStatus(changes.quality); break; case 'CALL_STATUS_CHANGE': // 闂侇偅淇洪惁浠嬫偐閼哥鍋撴担绋跨秮闁?- 婵炴挸寮堕悡瀣焻濮樺磭妯堥柣妯垮煐閳? this.renderCallStatus(changes.status); break; case 'CALL_ENDED': // 闂侇偅淇洪惁鐣岀磼閹惧瓨灏?- 婵炴挸寮堕悡瀣焻濮樺磭妯堢紓浣规尰濞碱偊鎮惧畝鍕〃 this.renderCallEnded(); break; case 'PARTICIPANT_LEFT': // participant缂佸倽顕х槐?- 闁哄洤鐡ㄩ弻濂濞达絽妫欓崺褔姊荤紙鐘电煗闁绘帟娉涢悺銊╁捶? this.renderParticipantLeft(changes.connectionId); break; case 'RESOLUTION_CHANGED': // 闁告帒妫滄ご鎼佹偝閸パ冪秮闁?- 闁哄洤鐡ㄩ弻濂濞戞搩鍘惧▓鎴﹀礆閸℃岸鍝洪柣婊冩喘閳ь剙顦懙鎴︽偐閼哥鍋? this.renderResolutionChanged(changes.resolution); break; } } // 婵炴挸寮堕悡瀣焻濮樺磭妯堥柣妯垮煐閳? renderCallStatus(status) { if (this.elements.connectingOverlay) { if (status === 'connecting') { this.elements.connectingOverlay.classList.remove('hidden'); } else { this.elements.connectingOverlay.classList.add('hidden'); } } } // 婵炴挸寮堕悡瀣緞閹绢喖鍔? renderHeader(session) { this.renderHeaderTitle(); if (this.elements.encryptionBadge) { toggleElement(this.elements.encryptionBadge, session.isEncrypted); } // 濠殿喖顑囩划鎾诲及閸撗佷粵缂傚啯鍨圭划鍫曟偐閼哥鍋撴担鐟扮樄缂佲偓閸濆嫭鐝ら柛婊冪焷瀹告繈鏌? if (this.elements.remoteNetworkIndicator) { this.elements.remoteNetworkIndicator.classList.remove('hidden'); } if (this.elements.remoteNetworkQuality) { this.elements.remoteNetworkQuality.classList.remove('hidden'); } this.renderCallDuration(session.duration); } renderHeaderTitle() { if (this.elements.headerTitle) { const connectionId = store.getConnectionId() || ''; // 闁哄牜浜g换娑㈠箳閵夛附顦у☉鎾崇У濡绮堥搹鐟邦劉婵℃妫濋崕鎾礆? this.elements.headerTitle.textContent = getCallTitle(connectionId); } } // 婵炴挸寮堕悡瀣焻濮樺磭妯堥柡鍐ㄧ埣閺? renderCallDuration(duration) { if (this.elements.callDuration) { this.elements.callDuration.textContent = formatTime(duration); } } // 婵炴挸寮堕悡瀣礆閸℃岸鍝洪柣婊冩搐瑜板宕? renderResolutionChanged(resolution) { if (!resolution) return; // 闁哄洤鐡ㄩ弻濠囧礆閸℃岸鍝洪柣婊冩喘閳ь剙顦甸妴宥夋儍閸曨垪鍋撴径澶庡幀闁绘鍩栭埀? const options = document.querySelectorAll('.resolution-option'); options.forEach(btn => { const btnRes = btn.dataset.resolution; const isActive = (resolution.height >= 1440 && btnRes === '1440') || (resolution.height >= 1080 && resolution.height < 1440 && btnRes === '1080') || (resolution.height >= 720 && resolution.height < 1080 && btnRes === '720') || (resolution.height < 720 && btnRes === '480'); btn.classList.toggle('active', isActive); }); // 闁哄洤鐡ㄩ弻濠呫亹閹惧啿顤呴柛鎺戞妞存悂鎮抽崶銊︾€柡? const currentResText = document.getElementById('currentResolutionText'); if (currentResText) { currentResText.textContent = `当前: ${resolution.label}`; } } // 婵炴挸寮堕悡瀣交濠婂拋浼傞悷娆忔椤? renderRemoteVideo(remoteUser) { this.renderUserList( this.stateManager.getState().session.localUser, remoteUser, this.stateManager.getState().participants ); if (this.elements.remoteVideoPlaceholder) { const shouldShowPlaceholder = !remoteUser.mediaState.video; const placeholderText = getRemoteVideoPlaceholderText(!shouldShowPlaceholder); toggleElement(this.elements.remoteVideoPlaceholder, shouldShowPlaceholder); if (this.elements.remoteVideo) { this.elements.remoteVideo.style.opacity = shouldShowPlaceholder ? '0' : '1'; } const placeholderContent = this.elements.remoteVideoPlaceholder.querySelector('.text-center'); if (placeholderContent) { const titleElement = placeholderContent.querySelector('p.text-white.text-lg.font-medium'); const subtitleElement = placeholderContent.querySelector('p.text-sm.text-gray-400'); if (titleElement) { titleElement.textContent = placeholderText.title; } if (subtitleElement) { subtitleElement.textContent = placeholderText.subtitle; } } } this.renderNetworkStatus(remoteUser.networkQuality); this.renderHeaderNetworkStatus(remoteUser.networkQuality); } renderHeaderNetworkStatus(networkQuality) { if (this.elements.remoteNetworkQuality) { const textElement = this.elements.remoteNetworkQuality.querySelector('span'); const iconElement = this.elements.remoteNetworkQuality.querySelector('i'); if (textElement && iconElement) { const display = getNetworkQualityDisplay(networkQuality); textElement.textContent = display.label; iconElement.className = display.headerIconClass; } } } renderLocalVideo(localUser, localStream) { if (this.elements.localVideoPlaceholder) { // 鐟滅増鎸婚惀鍛村嫉婢跺寒娼掑Λ鐗堝灦缁侊箓骞嬮弽顒夋綊濡増鍨甸崣褔姊婚鐔割槯闁哄嫬澧介妵姘跺础閻樿京绉寸紒? const shouldShowPlaceholder = !localStream || !localUser.mediaState.video; toggleElement(this.elements.localVideoPlaceholder, shouldShowPlaceholder); } if (this.elements.localAudioWave) { toggleElement(this.elements.localAudioWave, localUser.mediaState.isSpeaking); } // 闁告艾鏈鍌氥€掗崣澶屽帬闁哄牜鍓欏﹢鎾偨閵婏箑鐓曢柣妯垮煐閳? this.renderLocalUserStatus(localUser); } // 婵炴挸寮堕悡瀣嫉椤掆偓濠€瀵告喆閸℃侗鏆ユ繛? renderLocalStream(stream) { if (this.elements.localVideo && stream) { this.elements.localVideo.srcObject = stream; this.elements.localVideo.autoplay = true; this.elements.localVideo.muted = true; // 闁哄牜鍓欏﹢瀵告喆閸℃侗鏆ラ梻鍫熺懇閻撳爼鏁嶅畝鍕級闁稿繐绉村ú鏍ㄧ珶? console.log('srcObject set successfully:', this.elements.localVideo.srcObject); // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻? if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } } else { console.error('Either localVideo element or stream is missing'); } } // 婵炴挸寮堕悡瀣交濠婂應鏌ら悷娆忔椤h泛霉? renderRemoteStream(stream, connectionId, isHost) { if (isHost && connectionId) { // Host缂? 婵炴挸寮堕悡瀣礆?participant 閻熸瑥妫濋。鍓佺磾閹寸偟澹? this.renderParticipantStream(stream, connectionId); } else { // Participant缂? 婵炴挸寮堕悡瀣礆閺夊灝绀嬪☉鎾亾閺夆晜绮庨顒傛喆閸℃侗鏆ラ柨娑樻箽ost闁汇劌瀚弫楣冩椤喚绀? this.renderSingleRemoteStream(stream); } } // 婵炴挸寮堕悡濠琽st缂佹棏鍨冲▓鎴炲緞濮濆崋rticipant閻熸瑥妫濋。鍓佺磾閹寸偟澹? // 婵絽绻嬮柌娓沘rticipant tile闁哄嫬澧介妵姘辨嫚椤хΨrticipant閻庡湱鍋ゅ顖炴儍閸曨喚绠肩紒鏃戝灥椤锛愰幋鐐点偊 renderParticipantStream(stream, connectionId) { const grid = this.elements.participantGrid; if (!grid) return; // 闁哄嫬澧介妵姘辩磾閹寸偟澹愰柨娑樼焸濞堬綁鎸婅箛鎾崇閻犱警鍨电换娆戠博椤栨繍娼掑Λ? grid.classList.remove('hidden'); // 闁哄被鍎叉竟姗€骞嬮弽褍鐏$€点倝缂氶?connectionId 闁汇劌瀚~瀣紣閹寸偟澹愰悗? let tile = grid.querySelector(`[data-participant-id="${connectionId}"]`); if (!tile) { tile = document.createElement('div'); tile.className = 'relative bg-black/60 rounded-xl overflow-hidden flex items-center justify-center'; tile.dataset.participantId = connectionId; const video = document.createElement('video'); video.className = 'w-full h-full object-contain'; video.autoplay = true; video.playsinline = true; video.muted = false; // 濞戞挸绉瑰銈夋缁涘湱绀夐柟缁㈠幗閺備垢articipant闁汇劌瀚伴悡鑸碉紣? video.id = `participantVideo_${connectionId}`; tile.appendChild(video); // 闁告瑥鍊风粭宀勬嚀閸涢偊娼掑Λ鐗堝灥閸櫻囨⒒椤撶喐顦ч柣銊ュ瀹曠増鎷呭鍫濆壒闁哄拋鍨界槐娆愬緞瀹ュ洦鏆?remoteVideoPlaceholder 闁哄秴鍢茬槐锟犳晬? 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 = `

摄像头已关闭

`; tile.appendChild(placeholder); // 闁告瑥鍊风粭宀勬嚀閸涱厽鍊崇紒澶屽閻栵絿绮甸幘鍛濞村吋锚閸樻稒鎷呯捄銊︽殢participants濞戞搩鍘惧▓鎴︽儑閻旈鏉藉┑顔芥尭閹洟鏁? const pInfo = this.stateManager.getState().participants[connectionId]; const displayName = pInfo?.name || '参与者'; const label = document.createElement('div'); label.className = 'absolute bottom-3 left-3 glass px-3 py-1 rounded-full text-xs flex items-center gap-2'; label.innerHTML = `${displayName}`; tile.appendChild(label); // 闁革负鍔庨崵搴ㄥ冀閸ヮ亞妲? const liveTag = document.createElement('div'); liveTag.className = 'absolute top-3 right-3 bg-green-500/80 px-2 py-0.5 rounded-full text-xs flex items-center gap-1'; liveTag.innerHTML = `在线`; tile.appendChild(liveTag); grid.appendChild(tile); console.log(`Created participant video tile for ${connectionId}`); } if (tile) { // 閻熸瑥妫濋。鍫曞礂閸愵亞顦遍柡鍕⒔閵囨articipant闁汇劌瀚换娆戠博椤栨繍娼掑Λ鐗堝灦缁? const video = tile.querySelector('video'); if (video && stream) { // 闂侇剙鐏濋崢銈夋煂瀹ュ拋妲婚悹浣稿⒔閻ゅ棝宕ョ仦鑲╊伇婵炵繝绀侀顔炬寬閳藉懐绀勯梻濠冨▕椤e爼宕楅崼婵嗙厒闁靛棔娴囬~瀣紣閹存繃鍊甸柛鎺斿濡炲倸霉娴e壊鍤犻悹鐑囩磿濞村宕ュ畝瀣 if (video.srcObject === stream) { console.log(`Same stream for participant ${connectionId}, ensuring playback`); video.play().catch(e => console.log('Auto-play prevented:', e.message)); } else { video.srcObject = stream; video.play().catch(e => console.log('Auto-play prevented:', e.message)); console.log(`Set remote stream for participant tile ${connectionId}`); } } // 闂傚懏鍔樺Λ宀勫础閺囷紕鐔呴弶鈺傜矌椤忣剛鎲撮崱娑辨殽闁告粌鑻畷鐗堟媴瀹ュ浂鍎? const remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in'); if (remoteVideoDiv) { remoteVideoDiv.classList.add('hidden'); } } // 闁哄秷顫夊畵渚€宕i崒娆戠憿闁兼澘鎳忛弳鐔兼煂韫囨氨娈堕柡浣割嚟缂嶅寮介悡搴$仚闁? const tileCount = grid.querySelectorAll('[data-participant-id]').length; grid.style.gridTemplateColumns = getGridTemplateColumns(tileCount); // 闂傚懏鍔樺Λ灞炬交閻愭潙澶嶅☉鎿冨幗瑜颁胶绮? if (this.elements.connectingOverlay) { this.elements.connectingOverlay.classList.add('hidden'); } // 闂傚懏鍔樺Λ灞炬交濠婂拋浼傞悷娆忔椤e爼宕¢悩杈╃Т缂? if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); } } // 缂侇喗鍎抽崳顖炲即鐎涙ɑ鐓€闁圭娲ら悾绶媋rticipant tile闁汇劌瀚畷鐗堟媴瀹ュ牆鍓归柡? // participantId: 闁告瑦鍨块埀顑挎矡edia-state-changed闁汇劌鍩rticipant闁汇劌瀚换娑㈠箳椤у粚 // showPlaceholder: 闁哄嫷鍨伴幆渚€寮伴崜褋浠涢柛妤冨С缂嶅懘鎳楃仦鐐彲闁挎稑鐗愰~瀣紣閹存繂褰犻梻鍌ゅ幗濡炲倹绋夌花宄硊e闁? renderParticipantVideoPlaceholder(participantId, showPlaceholder) { const grid = this.elements.participantGrid; if (!grid) return; const tile = grid.querySelector(`[data-participant-id="${participantId}"]`); if (!tile) return; const placeholder = tile.querySelector('.participant-video-placeholder'); if (placeholder) { toggleElement(placeholder, showPlaceholder); console.log(`Updated placeholder for participant ${participantId}: ${showPlaceholder ? 'shown' : 'hidden'}`); } } // 闁告艾鏈鐐哄即鐎涙ɑ鐓€闁圭鍋撻柡鍫濄€宎rticipant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒? syncParticipantTileNames(participants) { if (!participants) return; const grid = this.elements.participantGrid; if (!grid) return; for (const [participantId, pInfo] of Object.entries(participants)) { this.updateParticipantTileName(participantId, pInfo.name); } } // 闁哄洤鐡ㄩ弻濠囧箰閸パ呮毎participant tile闁汇劌瀚幃鏇犵矓閻楀牏鍨肩紒? updateParticipantTileName(participantId, name) { const grid = this.elements.participantGrid; if (!grid) return; const tile = grid.querySelector(`[data-participant-id="${participantId}"]`); if (!tile) return; const label = tile.querySelector('.absolute.bottom-3'); if (label) { const nameSpan = label.querySelector('span'); if (nameSpan && name) { nameSpan.textContent = name; console.log(`Updated tile name for participant ${participantId}: ${name}`); } } } // 婵炴挸寮堕悡濠竌rticipant缂佹棏鍨冲▓鎴﹀础閺囨氨顏遍弶鈺傜矌椤忣剛鎲撮崱娑辨殽闁挎稑婀歰st闁汇垹顭峰浼存晬? renderSingleRemoteStream(stream) { if (!this.elements.remoteVideo || !stream) { console.error('Either remoteVideo element or stream is missing'); return; } console.log('Rendering remote stream:', stream, 'tracks:', stream.getTracks().map(t => `${t.kind}(${t.readyState})`)); // 闁稿繑濞婇弫顓熺┍椤旂⒈妲婚柨娑欏哺娴尖晠宕?srcObject = null 闁汇劌瀚伴崳鍝ョ磾椤斾茎浣割嚕? // 濠碘€冲€归悘?srcObject 鐎规瓕灏欑划锟犲及椤栨碍鍊卞☉鎾亾濞?stream 閻庣數顢婇挅鍕晬瀹€鍐惧殯闁哄嫬瀛╁Σ鎼佸触鐏炶偐顏辨繛缈犺兌濞堟垶娼妸鈺€澹曢柡鍥х摠閺屽﹪鏁嶉崼婵愭搐闂傚﹥濞婇。鍫曞礂閸繂鐓傞柨娑樼焷椤锛愰幋婵囧€甸柛鎺戝簻缁? // 婵炴潙绻楅~宥夊闯閵娿倗绐楅柤濂変簻婵晝鎷犻崱妤€鐒奸柡鍌滃閸у﹪宕濋悩鍨暠閺夌偑鍔戞禍楣冩晬鐏炵偓锟ラ梻鍥e亾闂佹彃绉堕悿?srcObject if (this.elements.remoteVideo.srcObject === stream) { console.log('Same stream object, track added - ensuring playback'); this.elements.remoteVideo.play().catch(e => console.log('Auto-play prevented:', e.message)); return; } // 濡絾鐗楅鑲╂媼閸撗呮瀭闁瑰瓨鐗楃粊锔锯偓鐢殿攰閽栧嫰宕eΟ鍝勵嚙闁挎稒姘ㄥú鍧楀箳閵夘煈鍟庣紓?srcObject闁挎稑鐗呯粭澶嬫媴鐠恒劍鏆?null 闂佹彃绉堕悿鍡椢熼垾宕囩闁? this.elements.remoteVideo.srcObject = stream; this.elements.remoteVideo.autoplay = true; this.elements.remoteVideo.playsinline = true; this.elements.remoteVideo.muted = false; // 缁绢収鍠曠换姘辨喆閸℃侗鏆ョ€殿喒鍋撳┑顔碱儐閹搁亶寮? this.elements.remoteVideo.play().catch(e => { console.log('Auto-play prevented, will retry on interaction:', e.message); }); // 闂傚懏鍔樺Λ宀勫棘椤撶偟纾婚弶鈺冨仦鐢鎲伴崱娆愮0閻? if (this.elements.disconnectedOverlay) { this.elements.disconnectedOverlay.classList.add('hidden'); } // 闁烩晜鍨甸幆澶屾喆閸℃侗鏆ラ弶鐐姂娴滈箖宕eΟ鍝勵嚙 const videoTracks = stream.getVideoTracks(); const audioTracks = stream.getAudioTracks(); console.log(`Stream has ${videoTracks.length} video tracks, ${audioTracks.length} audio tracks`); if (videoTracks.length > 0) { // 闁哄牆顦抽~瀣紣閹达絽缂撻梺顒佹惈缁变即姊鹃幇顖涱棏闁告濮崇紞鍛箔? if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.add('hidden'); } if (this.elements.connectingOverlay) { this.elements.connectingOverlay.classList.add('hidden'); } // 闁烩晜鍨甸幆澶屾喆閸℃侗鏆ラ弶鐐姂娴滈箖宕氶崱姘跺摵闁绘粌娲よぐ澶愬礌? const activeVideoTrack = videoTracks.find(track => track.readyState === 'live'); if (activeVideoTrack) { const resolution = this.getVideoResolution(activeVideoTrack); this.adjustVideoSize(this.elements.remoteVideo, resolution); activeVideoTrack.addEventListener('resize', () => { const newResolution = this.getVideoResolution(activeVideoTrack); this.adjustVideoSize(this.elements.remoteVideo, newResolution); }); } } else { // 闁告瑯浜濆﹢渚€妫呴幎绛嬫殽閺夌偑鍔戞禍楣冩晬閸綆娼掑Λ鐗堝灱瀵ゆ椽鏌嗛幘宕囨闁哄牜浜滈崺灞炬綇閹惧懐绀嗛柨娑欑煯缁楀寮伴崜褋浠涢柛妤冨С缂嶅懐绮敂鑲╃缂佹稑顦欢鐔烘喆閸℃侗鏆ラ弶鐐姂娴滈箖宕氶幏灞惧涧 // 濞戞挸绉烽鏇犵磾?srcObject = null闁挎稑濂旂换姘跺箰娓氣偓閻撹埖锛愰幋鐐村啊闁衡偓? console.log('Audio-only stream, waiting for video track...'); } } // 婵炴挸寮堕悡瀣嫉椤掆偓濠€鎾偨閵婏箑鐓曢柣妯垮煐閳? renderLocalUserStatus(localUser) { const mediaMeta = getMediaStatusMeta(localUser.mediaState); if (this.elements.localMediaStatus) { this.elements.localMediaStatus.textContent = mediaMeta.text; this.elements.localMediaStatus.className = mediaMeta.className; } if (this.elements.localMuteIcon) { if (mediaMeta.showMuteIcon) { this.elements.localMuteIcon.classList.remove('hidden'); this.elements.localMuteIcon.className = mediaMeta.muteIconClass; } else { this.elements.localMuteIcon.classList.add('hidden'); } } } renderUserList(localUser, remoteUser, participants) { if (!this.elements.userList) return; const participantsMap = participants || {}; const participantCount = Object.keys(participantsMap).length; const userCount = 1 + participantCount; if (this.elements.userCountDisplay) { this.elements.userCountDisplay.textContent = buildUserCountLabel(userCount); } this.elements.userList.innerHTML = ''; this.elements.userList.appendChild(createUserEntryElement({ user: localUser, role: 'local' })); if (participantCount > 0) { for (const [participantId, participant] of Object.entries(participantsMap)) { this.elements.userList.appendChild(createUserEntryElement({ user: participant, role: participant.role === 'host' ? 'host' : 'participant', id: participantId })); } } else if (remoteUser.status !== 'offline') { this.elements.userList.appendChild(createUserEntryElement({ user: remoteUser, role: 'remote' })); } } // 闁革腹鏆渆nderer.js濞戞搩鍘介崸濠囧礉閻樿櫕鐓欐繛? // 闁兼儳鍢茶ぐ鍥╂喆閸℃侗鏆ユ繛缈犵閸ㄥ孩娼忛妸褍鑺? getVideoResolution(track) { if (track && track.getSettings) { const settings = track.getSettings(); return { width: settings.width || 640, height: settings.height || 480 }; } return { width: 640, height: 480 }; // 濮掓稒顭堥濠氬磹? } // 閻犲鍟弳锝囨喆閸℃侗鏆ラ柛蹇撳暟缁€灞惧緞瑜嶉惃顒勭嵁鐠鸿櫣婀峰☉鎿冨幗濡绮? adjustVideoSize(videoElement, resolution) { if (!videoElement) return; const { width, height } = resolution; const aspectRatio = width / height; // 闁哄秷顫夊畵浣衡偓鍦嚀濞呮帗寰勮閻剟宕畝鍐炬綊濡増鍨甸鏃€顨囧Ο鑽ゆХ閻犲鍟弳锝囨喆閸℃侗鏆ラ柡鍕⒔閵? const container = videoElement.parentElement; const containerWidth = container.clientWidth; const containerHeight = container.clientHeight; // 闁告凹鍨抽弫銈囨兜椤戞寧顐介柛鏃傚█閳? videoElement.style.transform = 'translateZ(0)'; videoElement.style.willChange = 'transform'; // 閻犱礁澧介悿鍡欌偓鍦嚀濞呮帗绋夌弧绨俥x閻㈩垰鍟惇顒勬晬鐏炵厧鈻忛悷娆忔椤e墎浠﹂崨顒冨幀 container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; // 濞村吋锚鐎垫煡宕堕幆褍鍓兼繛鎾冲级閻? videoElement.style.imageRendering = 'auto'; // 缁绢収鍠曠换姘辨喆閸℃侗鏆ラ柛蹇撳暟缁€宀勫捶閵娿儺鍟囬柛锝冨妼閸炴潙顫㈤敐鍥b偓姗€寮伴崜褋浠? videoElement.style.maxWidth = '100%'; videoElement.style.maxHeight = '100%'; videoElement.style.objectFit = 'contain'; // 濞e洦绻冪€垫棃宕㈤悢濂夋綏婵絾鏌х欢銉╂晬鐏炶偐鐟濋悷浣风婢光偓 } // 婵炴挸寮堕悡瀣箳瑜嶉崺妤呭箰婢舵劖灏? renderControlButtons(mediaState) { if (this.elements.micBtn) { toggleButtonState(this.elements.micBtn, !mediaState.audio); } if (this.elements.videoBtn) { toggleButtonState(this.elements.videoBtn, !mediaState.video); } if (this.elements.recordBtn) { toggleButtonState(this.elements.recordBtn, mediaState.recording); } } // 婵炴挸寮堕悡瀣嚂婵犲倶浜繛鎴濈墛娴? renderChatMessages(messages) { if (!this.elements.chatContent) return; // 婵炴挸鎳愰埞鏍嚂婵犲倶浜柛鎰噹椤? this.elements.chatContent.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 = `闂侇偅淇洪惁钘夘嚕閳ь剚鎱?${formatTimestamp(startTime)}`; this.elements.chatContent.appendChild(startTimeElement); // 婵烇綀顕ф慨鐐测槈閸喍绱? messages.forEach(message => { const messageElement = this.createMessageElement(message); this.elements.chatContent.appendChild(messageElement); }); // 婵犲﹥鑹炬慨鈺呭礆閺夎法淇洪梺? this.elements.chatContent.scrollTop = this.elements.chatContent.scrollHeight; } // 闁告帗绋戠紓鎾斥槈閸喍绱栭柛蹇撳暟缁€? createMessageElement(message) { 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; let contentHTML = ''; if (message.type === 'file' && message.content.startsWith('data:image/')) { // 闁搞儱澧芥晶鏍р槈閸喍绱? contentHTML = `
${message.fileName || '图片'} ${message.fileName ? `
${message.fileName}
` : ''}
`; } else { // 闁哄倸娲﹀﹢鏉库槈閸喍绱? contentHTML = `
${message.content}
`; } messageDiv.innerHTML = `
${message.senderName} ${formatTimestamp(message.timestamp)}
${contentHTML}
`; return messageDiv; } // 婵炴挸寮堕悡瀣嫉椤忓浂鍤㈡繛鎴濈墛娴煎懘寮? renderUnreadCount(count) { if (this.elements.unreadBadge) { if (count > 0) { this.elements.unreadBadge.textContent = count; this.elements.unreadBadge.classList.remove('hidden'); } else { this.elements.unreadBadge.classList.add('hidden'); } } } // 婵炴挸寮堕悡瀣瑹瑜戠粩鐔煎冀? renderSidebar(isOpen) { if (this.elements.sidebar) { if (isOpen) { this.elements.sidebar.classList.remove('hidden'); } else { this.elements.sidebar.classList.add('hidden'); } } } // 婵炴挸寮堕悡瀣磾閹寸姷鎹曢柣妯垮煐閳? renderNetworkStatus(quality) { const display = getNetworkQualityDisplay(quality); if (this.elements.networkStatus && this.elements.networkStatusText) { toggleElement(this.elements.networkStatus, true); const networkStatus = this.elements.networkStatus; const networkStatusText = this.elements.networkStatusText; const existingIcon = networkStatus.querySelector('i'); if (existingIcon) { existingIcon.remove(); } const icon = document.createElement('i'); icon.className = display.statusIconClass; networkStatusText.textContent = display.label; networkStatusText.className = display.statusTextClass; networkStatus.insertBefore(icon, networkStatusText); } if (this.elements.connectionQuality) { this.elements.connectionQuality.textContent = `连接质量: ${display.label}`; this.elements.connectionQuality.className = `text-xs ${display.connectionTextClass}`; } this.updateHeaderNetworkIndicator(quality); this.renderHeaderNetworkStatus(quality); } updateHeaderNetworkIndicator(networkQuality) { if (!this.elements.remoteNetworkIndicator) return; this.elements.remoteNetworkIndicator.className = getNetworkQualityDisplay(networkQuality).indicatorClass; } renderCallEnded() { console.log('Call ended'); // 婵炴挸鎳愰幃濡廰rticipant缂傚啯鍨堕悧? const grid = this.elements.participantGrid; if (grid) { grid.querySelectorAll('[data-participant-id]').forEach(tile => { const video = tile.querySelector('video'); if (video) video.srcObject = null; tile.remove(); }); grid.classList.add('hidden'); } // 閻犲搫鐤囧ù鍡涘礆閹殿喚娉㈤柡澶屽枛閳ь剚淇洪惁浠嬫偩瀹€鍕〃 window.location.href = './endcall/endcall.html'; } // 婵炴挸寮堕悡濯漚rticipant缂佸倽顕х槐鎴︽晬閸ф攼st缂佹棏鍨界槐婵嬪箣閸ф锛熷ù鐘茬Ф閸斞呪偓娑櫭﹢顏堟晬? renderParticipantLeft(connectionId) { console.log(`Participant left: ${connectionId}, updating UI`); const grid = this.elements.participantGrid; if (grid) { const tile = grid.querySelector(`[data-participant-id="${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 remoteVideoDiv = this.elements.remoteVideo?.closest('.absolute.inset-0.video-fade-in'); if (remoteVideoDiv) { remoteVideoDiv.classList.remove('hidden'); } if (this.elements.remoteVideoPlaceholder) { this.elements.remoteVideoPlaceholder.classList.remove('hidden'); } } else { grid.style.gridTemplateColumns = getGridTemplateColumns(remainingTiles.length); } } if (this.elements.remoteNetworkIndicator) { this.elements.remoteNetworkIndicator.className = 'w-2 h-2 bg-gray-500 rounded-full'; } } // 闁兼儳鍢茶ぐ鍥偐閼哥鍋撴担瑙勭€柡? destroy() { if (this.unsubscribe) { this.unsubscribe(); } if (this.messageUnsubscribe) { this.messageUnsubscribe(); } } } export default UIRenderer;