looood / src /hooks /use-agora.ts
looda3131's picture
Clean push without any binary history
cc276cc
"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";
/**
* Hardcoded App ID for absolute reliability.
*/
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 };
};