Files
justice/www/html/realtimechat/public/js/room.js
T

319 lines
12 KiB
JavaScript

(function () {
const BASE = '/realtimechat';
const params = new URLSearchParams(location.search);
const roomId = params.get('room');
const password = params.get('password') || '';
const socket = io({ path: BASE + '/socket.io' });
let localStream = null;
const peers = new Map();
const names = new Map();
// ลดเสียงสะท้อนและเสียงรบกวนจากไมค์ (ใช้ทั้งตอนเริ่มและตอนเปลี่ยนอุปกรณ์)
function audioConstraints(deviceId) {
const base = { echoCancellation: true, noiseSuppression: true, autoGainControl: true };
if (deviceId) base.deviceId = { exact: deviceId };
return base;
}
const pendingOffers = new Map();
const pendingIceByFrom = new Map();
if (!roomId) {
document.getElementById('form-create')?.addEventListener('submit', (e) => {
e.preventDefault();
const id = (document.getElementById('room-id').value || '').trim();
const pw = document.getElementById('room-password').value || '';
const nick = (document.getElementById('nick').value || '').trim();
socket.emit('create-room', { roomId: id || undefined, password: pw }, (res) => {
if (res && res.ok) {
let url = 'room.html?room=' + encodeURIComponent(res.roomId);
if (pw) url += '&password=' + encodeURIComponent(pw);
if (nick) url += '&nick=' + encodeURIComponent(nick);
window.location.href = url;
}
else alert(res?.error || 'สร้างห้องไม่ได้');
});
});
document.getElementById('form-join')?.addEventListener('submit', (e) => {
e.preventDefault();
const id = document.getElementById('join-id').value.trim();
const pw = document.getElementById('join-password').value || '';
const nick = (document.getElementById('nick').value || '').trim();
socket.emit('join-room', { roomId: id, password: pw }, (res) => {
if (res && res.ok) {
let url = 'room.html?room=' + encodeURIComponent(id);
if (pw) url += '&password=' + encodeURIComponent(pw);
if (nick) url += '&nick=' + encodeURIComponent(nick);
window.location.href = url;
}
else alert(res?.error || 'เข้าร่วมไม่ได้');
});
});
return;
}
const nick = params.get('nick') || '';
const videosEl = document.getElementById('videos');
const roomLabel = document.getElementById('room-label');
const chatMessages = document.getElementById('chat-messages');
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
if (roomLabel) roomLabel.textContent = 'ห้อง: ' + roomId;
document.getElementById('btn-copy')?.addEventListener('click', () => {
const url = location.origin + BASE + '/room.html?room=' + encodeURIComponent(roomId);
navigator.clipboard.writeText(url).then(() => alert('คัดลอกลิงก์แล้ว'));
});
document.getElementById('btn-leave')?.addEventListener('click', (e) => { e.preventDefault(); location.href = BASE + '/index.html'; });
const btnMute = document.getElementById('btn-mute');
const btnVideo = document.getElementById('btn-video');
const micSelect = document.getElementById('mic-select');
const camSelect = document.getElementById('cam-select');
function updateMuteVideoLabels() {
if (!localStream) return;
const audioTrack = localStream.getAudioTracks()[0];
const videoTrack = localStream.getVideoTracks()[0];
if (btnMute) btnMute.textContent = audioTrack && !audioTrack.enabled ? 'เปิดไมค์' : 'ปิดไมค์';
if (btnVideo) btnVideo.textContent = videoTrack && !videoTrack.enabled ? 'เปิดกล้อง' : 'ปิดกล้อง';
}
btnMute?.addEventListener('click', () => {
if (!localStream) return;
const t = localStream.getAudioTracks()[0];
if (t) { t.enabled = !t.enabled; updateMuteVideoLabels(); }
});
btnVideo?.addEventListener('click', () => {
if (!localStream) return;
const t = localStream.getVideoTracks()[0];
if (t) { t.enabled = !t.enabled; updateMuteVideoLabels(); }
});
async function fillDeviceSelects() {
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const mics = devices.filter(d => d.kind === 'audioinput');
const cams = devices.filter(d => d.kind === 'videoinput');
const curAudio = localStream?.getAudioTracks()[0]?.getSettings()?.deviceId;
const curVideo = localStream?.getVideoTracks()[0]?.getSettings()?.deviceId;
if (micSelect) {
micSelect.innerHTML = '';
mics.forEach(d => {
const opt = document.createElement('option');
opt.value = d.deviceId;
opt.textContent = d.label || 'ไมค์ ' + (micSelect.options.length + 1);
if (d.deviceId === curAudio) opt.selected = true;
micSelect.appendChild(opt);
});
}
if (camSelect) {
camSelect.innerHTML = '';
cams.forEach(d => {
const opt = document.createElement('option');
opt.value = d.deviceId;
opt.textContent = d.label || 'กล้อง ' + (camSelect.options.length + 1);
if (d.deviceId === curVideo) opt.selected = true;
camSelect.appendChild(opt);
});
}
} catch (e) { console.error('enumerateDevices', e); }
}
async function switchDevice(kind, deviceId) {
if (!localStream) return;
try {
const stream = await navigator.mediaDevices.getUserMedia(
kind === 'audio' ? { audio: audioConstraints(deviceId), video: false } : { video: deviceId ? { deviceId: { exact: deviceId } } : true, audio: false }
);
const newTrack = stream.getTracks()[0];
const oldTrack = kind === 'audio' ? localStream.getAudioTracks()[0] : localStream.getVideoTracks()[0];
if (oldTrack) {
localStream.removeTrack(oldTrack);
oldTrack.stop();
}
localStream.addTrack(newTrack);
peers.forEach((pc) => {
const sender = pc.getSenders().find(s => s.track && s.track.kind === kind);
if (sender) sender.replaceTrack(newTrack);
});
const localWrap = videosEl.querySelector('[data-peer-id="' + socket.id + '"]');
const localVid = localWrap?.querySelector('video');
if (localVid) localVid.srcObject = localStream;
updateMuteVideoLabels();
} catch (e) { console.error('switchDevice', e); }
}
micSelect?.addEventListener('change', () => switchDevice('audio', micSelect.value));
camSelect?.addEventListener('change', () => switchDevice('video', camSelect.value));
function addVideo(id, stream, isLocal) {
const vid = document.createElement('video');
vid.autoplay = true;
vid.playsInline = true;
if (isLocal) {
vid.muted = true;
vid.volume = 0;
} else {
vid.muted = false;
}
if (stream) {
vid.srcObject = stream;
vid.play().catch(() => {});
}
vid.style.border = isLocal ? '3px solid #7aa2f7' : '2px solid #9ece6a';
const wrap = document.createElement('div');
wrap.dataset.peerId = id;
wrap.appendChild(vid);
const label = names.get(id) || (isLocal ? (nick || 'คุณ') : id.slice(0, 8));
wrap.appendChild(document.createElement('span')).textContent = label;
videosEl.appendChild(wrap);
return vid;
}
async function startLocal() {
try {
localStream = await navigator.mediaDevices.getUserMedia({
audio: audioConstraints(),
video: true
});
addVideo(socket.id, localStream, true);
await fillDeviceSelects();
updateMuteVideoLabels();
} catch (e) {
console.error('getUserMedia', e);
}
}
function createPeer(remoteId) {
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
});
if (localStream) localStream.getTracks().forEach(t => pc.addTrack(t, localStream));
pc.onicecandidate = (e) => { if (e.candidate) socket.emit('webrtc-ice', { to: remoteId, candidate: e.candidate }); };
pc.ontrack = (e) => {
const wrap = videosEl.querySelector('[data-peer-id="' + remoteId + '"]');
const vid = wrap ? wrap.querySelector('video') : null;
const stream = e.streams[0];
if (vid) {
vid.srcObject = stream;
vid.play().catch(() => {});
} else {
addVideo(remoteId, stream, false);
}
};
return pc;
}
async function handleOffer(from, offer) {
let pc = peers.get(from);
if (!pc) {
pc = createPeer(from);
peers.set(from, pc);
}
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
socket.emit('webrtc-answer', { to: from, answer: pc.localDescription });
const pending = pendingIceByFrom.get(from);
if (pending) { pending.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c))); pendingIceByFrom.delete(from); }
}
async function handleAnswer(from, answer) {
let pc = peers.get(from);
if (!pc) return;
await pc.setRemoteDescription(new RTCSessionDescription(answer));
const pending = pendingIceByFrom.get(from);
if (pending) { pending.forEach(c => pc.addIceCandidate(new RTCIceCandidate(c))); pendingIceByFrom.delete(from); }
}
socket.on('user-joined', async (data) => {
if (data.nickname) names.set(data.id, data.nickname);
addVideo(data.id, null, false);
const pc = createPeer(data.id);
peers.set(data.id, pc);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
if (!localStream) {
pendingOffers.set(data.id, offer);
return;
}
socket.emit('webrtc-offer', { to: data.id, offer });
});
socket.on('user-left', (data) => {
const pc = peers.get(data.id);
if (pc) { pc.close(); peers.delete(data.id); }
pendingOffers.delete(data.id);
pendingIceByFrom.delete(data.id);
names.delete(data.id);
const wrap = videosEl.querySelector('[data-peer-id="' + data.id + '"]');
if (wrap) wrap.remove();
});
socket.on('webrtc-offer', async (data) => {
if (!localStream) {
pendingOffers.set(data.from, data.offer);
return;
}
await handleOffer(data.from, data.offer);
});
socket.on('webrtc-answer', (data) => {
if (pendingOffers.has(data.from)) pendingOffers.delete(data.from);
handleAnswer(data.from, data.answer);
});
socket.on('webrtc-ice', (data) => {
let pc = peers.get(data.from);
if (!pc) {
if (!pendingIceByFrom.has(data.from)) pendingIceByFrom.set(data.from, []);
pendingIceByFrom.get(data.from).push(data.candidate);
return;
}
pc.addIceCandidate(new RTCIceCandidate(data.candidate)).catch(() => {});
});
socket.on('chat', (data) => {
const div = document.createElement('div');
div.className = 'chat-msg';
const label = data.nickname || names.get(data.id) || (data.id === socket.id ? (nick || 'คุณ') : data.id.slice(0, 8));
div.textContent = label + ': ' + (data.text || '');
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
});
chatForm?.addEventListener('submit', (e) => {
e.preventDefault();
const t = (chatInput.value || '').trim();
if (t) { socket.emit('chat', t); chatInput.value = ''; }
});
async function doJoin() {
// เปิดไมค์/กล้องให้พร้อมก่อน แล้วค่อย join ห้อง
await startLocal();
socket.emit('join-room', { roomId, password, nickname: nick || undefined }, async (res) => {
if (!res || !res.ok) {
alert(res?.error || 'ไม่พบห้อง');
location.href = BASE + '/index.html';
return;
}
names.set(socket.id, nick || 'คุณ');
(res.peers || []).forEach((p) => {
names.set(p.id, p.nickname || p.id.slice(0, 8));
});
for (const [from, offer] of pendingOffers) await handleOffer(from, offer);
pendingOffers.clear();
});
}
if (socket.connected) doJoin(); else socket.once('connect', doJoin);
})();