Newer
Older
mobile.raikyakun.app / node / src / hooks / useWebRTC.tsx
nutrition 14 hours ago 3 KB 利便性向上
// 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 }
}