// useWebRTC.ts
import { useState, useEffect, useRef, useCallback } from 'react'
interface useWebRTCReturn {
pc: RTCPeerConnection | null
localStream: MediaStream | null
ws: WebSocket | null
setURL: (wsUrl: string) => () => void
}
export function useWebRTC(): useWebRTCReturn {
const [pc, setPc] = useState<RTCPeerConnection | null>(null)
const [localStream, setLocalStream] = useState<MediaStream | null>(null)
const [ws, setWs] = useState<WebSocket | null>(null)
const iceServers: RTCIceServer[] = [
{ urls: "stun:stun.l.google.com:19302" },
{ urls: "stun:stun1.l.google.com:19302" },
{ urls: "stun:stun2.l.google.com:19302" },
{ urls: "stun:stun3.l.google.com:19302" },
{ urls: "stun:stun4.l.google.com:19302" }
]
// ICE候補を格納する配列(シグナリング送信用)
const iceCandidatesRef = useRef<RTCIceCandidateInit[]>([])
// WebSocket初期化
const setURL = useCallback((wsUrl: string) => {
const socket = new WebSocket(wsUrl)
socket.onopen = () => {
console.log("WebSocket connected")
// WebSocket接続後にWebRTCの初期化を開始
initWebRTC(socket)
}
socket.onmessage = (event: MessageEvent) => {
console.log("WebSocket message received:", event.data)
// ここでリモートからのシグナリング(アンサーやICE候補)を処理する
}
socket.onerror = (err) => {
console.error("WebSocket error:", err)
}
socket.onclose = () => {
console.log("WebSocket closed")
}
setWs(socket)
// クリーンアップ
return () => {
socket.close()
pc?.close()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// WebRTC初期化関数
const initWebRTC = useCallback((socket: WebSocket) => {
// ユーザーに映像・音声の許可をリクエスト
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
setLocalStream(stream)
const connection = new RTCPeerConnection({ iceServers })
// ローカルストリームの各トラックを追加
stream.getTracks().forEach(track => connection.addTrack(track, stream))
// ダミーデータチャネルを作成してICE候補収集を促進
connection.createDataChannel("dummy")
// ICE候補イベントのハンドラ
connection.onicecandidate = (event) => {
console.log("ICE candidate event:", event)
if (event.candidate) {
iceCandidatesRef.current.push(event.candidate.toJSON())
// 候補が見つかるたびに送信する(例:ここで個別送信)
socket.send(JSON.stringify({
type: "ice",
candidate: event.candidate.toJSON()
}))
} else {
// ICE候補の収集が完了(candidateがnull)
const signalingMessage = {
type: "offer",
sdp: connection.localDescription ? connection.localDescription.sdp : null,
iceCandidates: iceCandidatesRef.current
}
console.log("Signaling JSON message:", JSON.stringify(signalingMessage, null, 2))
socket.send(JSON.stringify(signalingMessage))
}
}
connection.onicegatheringstatechange = () => {
console.log("ICE gathering state:", connection.iceGatheringState)
}
// SDPオファー生成とローカル記述の設定
connection.createOffer()
.then(offer => {
console.log("Created SDP offer:", offer)
return connection.setLocalDescription(offer)
})
.then(() => {
// setLocalDescription()実行後、ICE候補収集が開始される
setPc(connection)
})
.catch(err => console.error("Error during SDP offer creation:", err))
})
.catch(err => {
console.error("Error getting user media:", err)
})
}, [iceServers])
return { pc, localStream, ws, setURL }
}