// 拡大表示画面: 教師からの映像/画像を受信して表示 new Vue({ el: '#app', data: { showToolbar: true, targetName: '拡大表示', teacherEmail: '', myMail: '', roomPath: '', roomRef: null, pc: null, timeoutHide: null }, mounted() { // URLパラメータから教師メール、生徒メール、クラス名を取得 const urlParams = new URLSearchParams(window.location.search); this.teacherEmail = urlParams.get('teacher') || ''; this.myMail = urlParams.get('student') || ''; const className = urlParams.get('class') || ''; if (!this.teacherEmail || !this.myMail || !className) { alert('不正な起動です。コントローラから開いてください。'); window.close(); } const domain = this.teacherEmail.split('@')[1]; const safeDomain = domain.replace(/\./g, '_'); this.roomPath = `classes/${safeDomain}/${className}`; this.roomRef = firebase.database().ref(this.roomPath); this.setupWebRTC(); this.setupSignaling(); // ツールバー自動非表示 setTimeout(() => this.hideToolbarDelayed(), 3000); }, methods: { async setupWebRTC() { const config = { iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }; this.pc = new RTCPeerConnection(config); this.pc.ontrack = (event) => { const videoEl = document.getElementById('remoteVideo'); videoEl.srcObject = event.streams[0]; videoEl.style.display = 'block'; document.getElementById('remoteImage').style.display = 'none'; }; this.pc.onicecandidate = (event) => { if (event.candidate) { this.sendSignal({ type: 'candidate', candidate: event.candidate }); } }; // データチャネル受信 this.pc.ondatachannel = (event) => { const dc = event.channel; dc.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.cmd === 'lock') { this.applyLock(msg.params); } else if (msg.cmd === 'unlock') { this.releaseLock(); } }; }; // Offerを受信するために、受信側はダミーのオファーを送る? // 簡易実装: コントローラからのOfferを待つので、先にデータチャネルを作成しておく const dc = this.pc.createDataChannel('command'); dc.onopen = () => console.log('DataChannel open'); dc.onmessage = (ev) => { const msg = JSON.parse(ev.data); if (msg.cmd === 'lock') this.applyLock(msg.params); else if (msg.cmd === 'unlock') this.releaseLock(); }; const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); this.sendSignal({ type: 'offer', sdp: offer.sdp }); }, sendSignal(data) { const signalRef = this.roomRef.child('signaling'); signalRef.push({ from: this.myMail, to: this.teacherEmail, type: data.type, data: data }); }, setupSignaling() { this.roomRef.child('signaling').on('child_added', async (snap) => { const msg = snap.val(); if (msg.to === this.myMail && msg.from === this.teacherEmail) { const { type, data } = msg; if (type === 'offer') { await this.pc.setRemoteDescription(new RTCSessionDescription({ type: 'offer', sdp: data.sdp })); const answer = await this.pc.createAnswer(); await this.pc.setLocalDescription(answer); this.sendSignal({ type: 'answer', sdp: answer.sdp }); } else if (type === 'answer') { await this.pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: data.sdp })); } else if (type === 'candidate') { await this.pc.addIceCandidate(new RTCIceCandidate(data.candidate)); } else if (type === 'command') { // コマンド直接受信 (fallback) if (data.cmd === 'lock') this.applyLock(data.params); else if (data.cmd === 'unlock') this.releaseLock(); } snap.ref.remove(); } }); }, applyLock(params) { // 実際のロック画面表示(簡易) if (params.type === 'text') { document.body.style.backgroundColor = params.bg_color; document.body.style.color = params.font_color; document.body.innerHTML = `
${params.text}
`; } else { // 画像表示 const imgUrl = `./assets/lock_${params.image}.png`; // 実際の画像パス document.body.innerHTML = ``; } document.body.style.overflow = 'hidden'; }, releaseLock() { // ロック解除 → 再読み込みなど location.reload(); }, hideToolbarDelayed() { if (this.timeoutHide) clearTimeout(this.timeoutHide); this.timeoutHide = setTimeout(() => { this.showToolbar = false; }, 3000); }, closeWindow() { window.close(); } } });