Newer
Older
mobile.raikyakun.app / node / src / app / call / page.tsx
nutrition 11 hours ago 3 KB 直接入力対応
'use client'
import { Suspense, useEffect, useRef } from 'react'
import { useSearchParams, useRouter } from 'next/navigation'
import { IconButton, Typography } from '@mui/material'
import {
	Mic as MicIcon,
	MicOff as MicOffIcon,
	Videocam as VideocamIcon,
	VideocamOff as VideocamOffIcon,
	CallEnd as CallEndIcon,
} from '@mui/icons-material'
import { useWebRTC } from '@/hooks'
import './page.css'

// useSearchParams() を使うため Suspense 境界の内側に置く (静的 prerender 要件)。
function CallPageInner() {
	const searchParams = useSearchParams()
	const router = useRouter()
	const callId = searchParams.get('callId') ?? ''
	const accessKey = searchParams.get('accessKey') ?? ''

	const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? ''
	const wsUrl = callId && accessKey
		? baseUrl.replace(/^https?:\/\//, 'wss://') + `/api/mobile/${accessKey}/call/${callId}`
		: ''

	const { callState, localStream, remoteStream, isMuted, isCameraOff, toggleMute, toggleCamera, hangup } =
		useWebRTC({ callId, wsUrl, onHangup: () => {} })

	const remoteVideoRef = useRef<HTMLVideoElement>(null)
	const localVideoRef = useRef<HTMLVideoElement>(null)

	useEffect(() => {
		if (remoteVideoRef.current && remoteStream) {
			remoteVideoRef.current.srcObject = remoteStream
		}
	}, [remoteStream])

	useEffect(() => {
		if (localVideoRef.current && localStream) {
			localVideoRef.current.srcObject = localStream
		}
	}, [localStream])

	// 通話終了・失敗時は2秒後に前のページへ戻る
	useEffect(() => {
		if (callState === 'ended' || callState === 'failed') {
			const timer = setTimeout(() => router.back(), 2000)
			return () => clearTimeout(timer)
		}
	}, [callState, router])

	const statusText: Record<string, string> = {
		connecting: '接続中…',
		failed: '接続に失敗しました',
		ended: '通話終了',
	}

	const isEnded = callState === 'ended' || callState === 'failed'

	return (
		<div className="callContainer">
			{/* リモート映像(全画面) */}
			<video
				ref={remoteVideoRef}
				className="remoteVideo"
				autoPlay
				playsInline
			/>

			{/* ローカル映像(右下小窓) */}
			<video
				ref={localVideoRef}
				className="localVideo"
				autoPlay
				playsInline
				muted
			/>

			{/* 状態テキスト */}
			{callState !== 'connected' && statusText[callState] && (
				<div className="statusOverlay">
					<Typography sx={{ fontSize: '4vw', fontWeight: 'bold' }}>
						{statusText[callState]}
					</Typography>
				</div>
			)}

			{/* コントロールバー */}
			<div className="controlBar">
				<IconButton
					onClick={toggleMute}
					disabled={isEnded}
					sx={{ color: isMuted ? '#f44336' : '#fff', background: 'rgba(255,255,255,0.15)', '&:hover': { background: 'rgba(255,255,255,0.25)' } }}
					size="large"
				>
					{isMuted ? <MicOffIcon fontSize="large" /> : <MicIcon fontSize="large" />}
				</IconButton>

				<IconButton
					onClick={toggleCamera}
					disabled={isEnded}
					sx={{ color: isCameraOff ? '#f44336' : '#fff', background: 'rgba(255,255,255,0.15)', '&:hover': { background: 'rgba(255,255,255,0.25)' } }}
					size="large"
				>
					{isCameraOff ? <VideocamOffIcon fontSize="large" /> : <VideocamIcon fontSize="large" />}
				</IconButton>

				<IconButton
					onClick={hangup}
					disabled={isEnded}
					sx={{ color: '#fff', background: '#f44336', '&:hover': { background: '#d32f2f' }, width: 64, height: 64 }}
					size="large"
				>
					<CallEndIcon fontSize="large" />
				</IconButton>
			</div>
		</div>
	)
}

export default function CallPage() {
	return (
		<Suspense fallback={<div className="callContainer" />}>
			<CallPageInner />
		</Suspense>
	)
}