| "use client"; |
|
|
| import { useState, useEffect, useRef, useCallback } from "react"; |
| import type { |
| IAgoraRTCClient, |
| IAgoraRTCRemoteUser, |
| IMicrophoneAudioTrack, |
| ICameraVideoTrack, |
| VideoEncoderConfiguration, |
| } from "agora-rtc-sdk-ng"; |
| import { useToast } from "@/hooks/use-toast"; |
| import { VideoQuality } from "@/lib/types"; |
| import { Capacitor } from "@capacitor/core"; |
|
|
| |
| |
| |
| const AGORA_APP_ID = "8a6eeabbf65e46f7963fe39e045da21b"; |
|
|
| const qualityProfiles: { [key in VideoQuality]: VideoEncoderConfiguration } = { |
| 'low': '120p_1', |
| 'medium': '240p_1', |
| 'high': '480p_1', |
| }; |
|
|
| const qualityCycle: VideoQuality[] = ['low', 'medium', 'high']; |
|
|
| export const useAgora = ( |
| channelName: string | null, |
| uid: number, |
| callType: 'video' | 'audio', |
| isActive: boolean |
| ) => { |
| const { toast } = useToast(); |
| |
| const agoraClientRef = useRef<IAgoraRTCClient | null>(null); |
| const [localVideoTrack, setLocalVideoTrack] = useState<ICameraVideoTrack | null>(null); |
| const localVideoTrackRef = useRef<ICameraVideoTrack | null>(null); |
| const localAudioTrackRef = useRef<IMicrophoneAudioTrack | null>(null); |
| const [remoteUsers, setRemoteUsers] = useState<IAgoraRTCRemoteUser[]>([]); |
| const [isJoined, setIsJoined] = useState(false); |
| const [isAudioMuted, setIsAudioMuted] = useState(false); |
| const [isVideoMuted, setIsVideoMuted] = useState(callType === 'audio'); |
| const [isLoading, setIsLoading] = useState(false); |
| const [error, setError] = useState<string | null>(null); |
| const [networkQuality, setNetworkQuality] = useState<{ uplink: number, downlink: number }>({ uplink: 0, downlink: 0 }); |
| const [currentQuality, setCurrentQuality] = useState<VideoQuality>('medium'); |
| const connectionState = useRef<'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'RECONNECTING' | 'DISCONNECTING'>('DISCONNECTED'); |
| |
| useEffect(() => { |
| localVideoTrackRef.current = localVideoTrack; |
| }, [localVideoTrack]); |
|
|
| const handleUserPublished = async (user: IAgoraRTCRemoteUser, mediaType: "audio" | "video") => { |
| if (connectionState.current !== 'CONNECTED') return; |
| await agoraClientRef.current!.subscribe(user, mediaType); |
| if (mediaType === "audio" && user.audioTrack) user.audioTrack.play(); |
| setRemoteUsers(Array.from(agoraClientRef.current!.remoteUsers)); |
| }; |
|
|
| const handleUserUnpublished = (user: IAgoraRTCRemoteUser) => setRemoteUsers(Array.from(agoraClientRef.current!.remoteUsers)); |
| const handleUserLeft = (user: IAgoraRTCRemoteUser) => setRemoteUsers(prev => prev.filter(u => u.uid !== user.uid)); |
| const handleNetworkQuality = (stats: any) => setNetworkQuality({ uplink: stats.uplinkNetworkQuality, downlink: stats.downlinkNetworkQuality }); |
| const handleConnectionStateChange = (curState: any) => { connectionState.current = curState; }; |
|
|
| const leave = useCallback(async () => { |
| if(localAudioTrackRef.current) { localAudioTrackRef.current.stop(); localAudioTrackRef.current.close(); localAudioTrackRef.current = null; } |
| if(localVideoTrackRef.current) { localVideoTrackRef.current.stop(); localVideoTrackRef.current.close(); setLocalVideoTrack(null); } |
| setRemoteUsers([]); |
| setIsJoined(false); |
| const client = agoraClientRef.current; |
| if (client && connectionState.current !== 'DISCONNECTED') { |
| client.off("user-published", handleUserPublished); |
| client.off("user-unpublished", handleUserUnpublished); |
| client.off("user-left", handleUserLeft); |
| client.off("network-quality", handleNetworkQuality); |
| client.off("connection-state-change", handleConnectionStateChange); |
| connectionState.current = 'DISCONNECTING'; |
| try { await client.leave(); } catch (e) { console.error("Agora Leave Error:", e); } finally { agoraClientRef.current = null; } |
| } |
| connectionState.current = 'DISCONNECTED'; |
| }, []); |
|
|
| const join = useCallback(async () => { |
| const AgoraRTC = (await import('agora-rtc-sdk-ng')).default; |
| if (connectionState.current !== 'DISCONNECTED' || !channelName) return; |
| const client = AgoraRTC.createClient({ codec: "h264", mode: "rtc" }); |
| agoraClientRef.current = client; |
| try { |
| connectionState.current = 'CONNECTING'; |
| setIsLoading(true); |
| setError(null); |
| client.on("user-published", handleUserPublished); |
| client.on("user-unpublished", handleUserUnpublished); |
| client.on("user-left", handleUserLeft); |
| client.on("network-quality", handleNetworkQuality); |
| client.on("connection-state-change", handleConnectionStateChange); |
| const isNative = Capacitor.isNativePlatform(); |
| const baseUrl = isNative ? process.env.NEXT_PUBLIC_API_BASE_URL : ''; |
| const response = await fetch(`${baseUrl}/api/agora/token`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ channelName, uid }), |
| }); |
| const data = await response.json(); |
| if (!response.ok) throw new Error(data.error || 'Token failure'); |
| await client.join(AGORA_APP_ID, channelName, data.token, uid); |
| const videoConfig = qualityProfiles[currentQuality]; |
| const [audioTrack, videoTrack] = await AgoraRTC.createMicrophoneAndCameraTracks({}, { encoderConfig: videoConfig }); |
| localAudioTrackRef.current = audioTrack; |
| setLocalVideoTrack(videoTrack); |
| if (callType === 'audio') { await client.publish([audioTrack]); } else { await client.publish([audioTrack, videoTrack]); } |
| connectionState.current = 'CONNECTED'; |
| setIsJoined(true); |
| } catch (err: any) { |
| setError(err.message || "Join failed"); |
| await leave(); |
| } finally { |
| setIsLoading(false); |
| } |
| }, [channelName, uid, callType, leave, currentQuality]); |
|
|
| useEffect(() => { |
| if (isActive && typeof window !== 'undefined') join(); |
| return () => { if (isActive && typeof window !== 'undefined') leave(); }; |
| }, [isActive, channelName, uid, callType, join, leave]); |
|
|
| const toggleAudio = useCallback(async () => { |
| if (localAudioTrackRef.current) { |
| const nextState = !isAudioMuted; |
| await localAudioTrackRef.current.setMuted(nextState); |
| setIsAudioMuted(nextState); |
| } |
| }, [isAudioMuted]); |
|
|
| const toggleVideo = useCallback(async () => { |
| if (localVideoTrack) { |
| const nextState = !isVideoMuted; |
| try { await localVideoTrack.setEnabled(!nextState); setIsVideoMuted(nextState); } catch (e) { |
| toast({ variant: 'destructive', title: 'Video Error', description: 'Could not toggle video.' }); |
| } |
| } |
| }, [isVideoMuted, localVideoTrack, toast]); |
|
|
| const switchCamera = useCallback(async () => { |
| const AgoraRTC = (await import('agora-rtc-sdk-ng')).default; |
| if (!localVideoTrackRef.current) return; |
| try { |
| const devices = await AgoraRTC.getCameras(); |
| if (devices.length <= 1) return; |
| const currentCamera = localVideoTrackRef.current.getTrackLabel(); |
| const index = devices.findIndex(d => d.label === currentCamera); |
| await localVideoTrackRef.current.setDevice(devices[(index + 1) % devices.length].deviceId); |
| } catch (e) { toast({ variant: "destructive", title: "Switch Failed" }); } |
| }, [toast]); |
|
|
| const setNextVideoQuality = useCallback(async () => { |
| if (!localVideoTrackRef.current || isVideoMuted) return; |
| const nextQuality = qualityCycle[(qualityCycle.indexOf(currentQuality) + 1) % qualityCycle.length]; |
| try { |
| await localVideoTrackRef.current.setEncoderConfiguration(qualityProfiles[nextQuality]); |
| setCurrentQuality(nextQuality); |
| } catch (e) { toast({ variant: 'destructive', title: 'Quality Failed' }); } |
| }, [currentQuality, isVideoMuted, toast]); |
|
|
| return { localVideoTrack, remoteUsers, isJoined, isAudioMuted, isVideoMuted, isLoading, error, networkQuality, toggleAudio, toggleVideo, switchCamera, currentQuality, setNextVideoQuality }; |
| }; |
|
|