Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ import { API_URL } from "@/config/index";
import useCamera, { cameraPermissions } from "@/hooks/meeting/useCamera";
import { API } from "@escolalms/sdk/lib";
import { IMeetRecording } from "@/components/Consultations/ConsultationCard/JitsyMeeting/types";
import {
JITSY_ANALYTICS_INTERVAL,
JITSY_TUTOR_INTERVAL,
} from "@/utils/constants";
import { JITSY_ANALYTICS_INTERVAL } from "@/utils/constants";
import { Text } from "@escolalms/components/lib/components/atoms/Typography/Text";
import { Button } from "@escolalms/components/lib/components/atoms/Button/Button";
import { useJitsyAnalyticsControl } from "@/hooks/meeting/useJitsyAnalyticsControl";
Expand Down Expand Up @@ -78,7 +75,7 @@ const JitsyMeeting: React.FC<JitsyMeetingProps> = ({
const { consultation, token } = useContext(EscolaLMSContext);
const { isStudent } = useRoles();
const { t } = useTranslation();
const { camera, getDataUrl, cameraAccessStatus } = useCamera();
const { camera, getDataUrl, cameraAccessStatus, stopCamera } = useCamera();
const userConsentedRef = useRef(false);
const isCameraMutedRef = useRef(false);

Expand All @@ -91,6 +88,8 @@ const JitsyMeeting: React.FC<JitsyMeetingProps> = ({
const workerRef = useRef<Worker | null>(null);
const apiRef = useRef<IJitsiMeetExternalApi | null>(null);
const recordingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const hasSentEndEventRef = useRef(false);

const { shouldRunAnalytics } = useJitsyAnalyticsControl({
modelType,
participantCount,
Expand Down Expand Up @@ -140,6 +139,13 @@ const JitsyMeeting: React.FC<JitsyMeetingProps> = ({
const sendRecordingEvent = useCallback(
async (action: "start-recording" | "end-recording") => {
if (isStudent) return;

if (
action === "end-recording" &&
(hasSentEndEventRef.current || !recordingIdRef.current)
) {
return;
}
try {
const payload = preparePayload(action);

Expand All @@ -156,6 +162,10 @@ const JitsyMeeting: React.FC<JitsyMeetingProps> = ({
}
);

if (action === "end-recording") {
recordingIdRef.current = null;
}

const result = await response.json();

if (action === "start-recording" && result?.data?.id) {
Expand Down Expand Up @@ -364,13 +374,25 @@ const JitsyMeeting: React.FC<JitsyMeetingProps> = ({
useEffect(() => {
const init = async () => {
try {
await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});

stream.getTracks().forEach((track) => {
track.stop();
console.log("Init test track stopped");
});

isStudent ? setShowModal(true) : setShowMeeting(true);
} catch {
} catch (err) {
console.warn("Camera access failed in init", err);
setShowMeeting(true);
}
};
init();

return () => {};
}, [isStudent]);

const getProperRoomName = () => {
Expand All @@ -393,6 +415,7 @@ const JitsyMeeting: React.FC<JitsyMeetingProps> = ({
}}
onReadyToClose={async () => {
stopAllIntervals();
stopCamera();
if (!isStudent && recordingIdRef.current) {
await sendRecordingEvent("end-recording");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,16 @@ export default function MeetingAnalyticsOverlay({
isArea: boolean
) => {
if (data.length === 0) return null;
const isSinglePoint = data.length === 1;

return (
<linearGradient id={id} x1="0" y1="0" x2="1" y2="0">
{data.map((point, index) => (
<stop
key={`${id}-${index}`}
offset={`${(index / (data.length - 1)) * 100}%`}
offset={
isSinglePoint ? "0%" : `${(index / (data.length - 1)) * 100}%`
}
stopColor={getColorByValue(point.value)}
stopOpacity={isArea ? 0.4 : 1}
/>
Expand All @@ -233,6 +237,14 @@ export default function MeetingAnalyticsOverlay({
val: 0,
};

useEffect(() => {
if (!shouldRunAnalytics) {
setAttentionData([]);
setEmotionHistory([]);
setShouldBreak(false);
}
}, [shouldRunAnalytics]);

return (
<OverlayRoot>
<Header>
Expand Down
9 changes: 4 additions & 5 deletions src/components/Webinars/Webinar/WebinarMeetModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import MeetingAnalyticsOverlay from "@/components/MeetingAnalyticsOverlay/Meetin
import { EndMeetingQuestionnairesModal } from "@/components/Consultations/ConsultationCard/EndMeetingQuestionnaires";
import { QuestionnaireModelType } from "@/types/questionnaire";
import { API } from "@escolalms/sdk/lib";
import { useRoles } from "@/hooks/useRoles";

interface Props {
onClose: () => void;
Expand All @@ -39,7 +38,6 @@ const WebinarMeetModal = ({ onClose, visible, webinarId, webinar }: Props) => {
const onCloseRef = useRef(onClose);
const { generateWebinarJitsy } = useContext(EscolaLMSContext);
const { t } = useTranslation();
const { isTutor } = useRoles();

useEffect(() => {
onCloseRef.current = onClose;
Expand Down Expand Up @@ -86,8 +84,9 @@ const WebinarMeetModal = ({ onClose, visible, webinarId, webinar }: Props) => {

const handleOnClose = useCallback(() => {
setIsEnded(true);
isTutor && onClose();
}, [isTutor, onClose]);
setWebinarMeetData(null);
onClose();
}, [onClose]);

return (
<>
Expand All @@ -114,7 +113,7 @@ const WebinarMeetModal = ({ onClose, visible, webinarId, webinar }: Props) => {
webinar={webinar}
participantCount={participantCount}
/>
{!loading && webinarMeetData && (
{visible && !loading && webinarMeetData && (
<JitsyMeeting
key={webinarId}
jitsyData={webinarMeetData}
Expand Down
21 changes: 18 additions & 3 deletions src/components/_App/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,16 @@ const Navbar = () => {
),
key: "menu-2",
},
{
title: (
<Link to={routeRoutes.webinars}>
<Text noMargin bold>
{t("Menu.Webinars")}
</Text>
</Link>
),
key: "menu-3",
},
{
title: (
<Link to={routeRoutes.courses}>
Expand All @@ -352,7 +362,7 @@ const Navbar = () => {
</Text>
</Link>
),
key: "menu-3",
key: "menu-4",
},
{
title: (
Expand All @@ -362,7 +372,7 @@ const Navbar = () => {
</Text>
</Link>
),
key: "menu-4",
key: "menu-5",
},
{
title: settings?.value?.config?.[metaDataKeys.termsPageMetaKey] && (
Expand All @@ -374,7 +384,7 @@ const Navbar = () => {
</Text>
</Link>
),
key: "menu-5",
key: "menu-6",
},

{
Expand Down Expand Up @@ -504,6 +514,11 @@ const Navbar = () => {
{t("MyProfilePage.MyConsultations")}
</NavLink>
</li>
<li>
<NavLink to={routeRoutes.myWebinars}>
{t("MyProfilePage.MyWebinars")}
</NavLink>
</li>
<li>
<NavLink to={routeRoutes.myCertificates}>
{t("Navbar.MyCertificates")}
Expand Down
60 changes: 38 additions & 22 deletions src/hooks/meeting/useCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,47 +130,63 @@ const useCamera = () => {

useEffect(() => {
const handlePermissionChange = async () => {
const permissionStatus = await navigator.permissions.query({
name: "camera" as PermissionName,
});
try {
const permissionStatus = await navigator.permissions.query({
name: "camera" as PermissionName,
});

// If permission is granted, restart the video track
if (permissionStatus.state === cameraPermissions.GRANTED) {
await restartVideoTrack();
}
const updateStatus = () => {
if (permissionStatus.state === cameraPermissions.DENIED) {
setHasCameraAccess(false);
setCameraAccessStatus(cameraPermissions.DENIED);
} else {
setCameraAccessStatus(permissionStatus.state as cameraPermissions);
}
};

if (permissionStatus.state === cameraPermissions.DENIED) {
setHasCameraAccess(false);
setCameraAccessStatus(cameraPermissions.DENIED);
permissionStatus.onchange = updateStatus;
updateStatus();
} catch (e) {
console.error("Permissions API not supported", e);
}

// Listen for permission changes
permissionStatus.onchange = async () => {
if (permissionStatus.state === cameraPermissions.GRANTED) {
await restartVideoTrack();
} else {
setHasCameraAccess(false);
setCameraAccessStatus(cameraPermissions.DENIED);
}
};
};

handlePermissionChange();

return () => {
// Clean up Workera
if (workerRef.current) {
workerRef.current.terminate();
}

if (streamRef.current) {
streamRef.current.getTracks().forEach((t) => t.stop());
}
};
}, [restartVideoTrack]);
}, []);

const stopCamera = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => {
track.stop();
});
streamRef.current = null;
}

if (videoRef.current) {
videoRef.current.srcObject = null;
videoRef.current.pause();
}

setHasCameraAccess(false);
}, []);

return {
camera,
hasCameraAccess,
restartVideoTrack,
getDataUrl,
cameraAccessStatus,
stopCamera,
};
};

Expand Down
5 changes: 4 additions & 1 deletion src/hooks/useAnalyticsWebsockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const useMeetingSockets = (
const [socketData, setSocketData] = useState<SocketDataState | null>(null);

useEffect(() => {
if (!modelId || !termUnix || !token) return;
if (!modelId || !termUnix || !token) {
setSocketData(null);
return;
}

const echo = getEchoInstance(token);
if (!echo) return;
Expand Down
Loading