"use client"; import React, { useState, useEffect, useRef, useCallback } from "react"; import Login from "./components/Login"; import EmbedTimeline from "./components/EmbedTimeline"; import VideoPlayer from "./components/VideoPlayer"; import CustomDateRangePicker from "./components/DateRangePicker"; import "./App.css"; import StatusesDisplayHUD from "./components/StatusDisplay"; function App() { const original_data = useRef(null); const chartRef = useRef(null); const [dataResults, setDataResults] = useState(null); const [statusMessages, setStatusMessages] = useState([]); const [markerTime, setMarkerTime] = useState(0); const playerRef = useRef(null); const [drawerOpen, setDrawerOpen] = useState(false); const playerInstanceRef = useRef(null); const [selectedCamera, setSelectedCamera] = useState("Leopards 1"); const [selectedHighRes, setSelectedHighRes] = useState(false); const [videoPlaying, setVideoPlaying] = useState(true); // State for the values window.chartRef = chartRef; window.playerRef = playerRef; window.playerInstanceRef = playerInstanceRef; // Slider states const inputRef = useRef(null); const [sliderMin, setSliderMin] = useState(0.0); const [sliderMax, setSliderMax] = useState(1.0); // Date range states // const [startRange, setStartRange] = useState( new Date(new Date().getTime() - 1 * 24 * 60 * 60 * 1000), ); const [endRange, setEndRange] = useState(new Date()); // const [endRange, setEndRange] = useState(new Date(new Date().getTime() - 6 * 24 * 60 * 60 * 1000)); const [queryText, setQueryText] = useState("Two clouded leopards being aggressive"); const [sliderValue, setSliderValue] = useState(0); // State to track last submitted values const [lastSubmitted, setLastSubmitted] = useState({ startRange, endRange, queryText, selectedCamera, }); // // Check if any value has changed // const queryChanged = // startRange !== lastSubmitted.startRange || // endRange !== lastSubmitted.endRange || // queryText !== lastSubmitted.queryText; // Check if queryText is different from lastSubmitted.queryText const textChanged = queryText !== lastSubmitted.queryText; const startChanged = startRange.getTime() !== new Date(lastSubmitted.startRange).getTime(); const endChanged = endRange.getTime() !== new Date(lastSubmitted.endRange).getTime(); const cameraChanged = selectedCamera !== lastSubmitted.selectedCamera; const queryChanged = textChanged || startChanged || endChanged || cameraChanged; const streamComputeStatus = () => { fetch("api/return_status") .then((response) => { const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; // Accumulate partial text function read() { reader.read().then(({ done, value }) => { if (done) { if (buffer) { } setStatusMessages([]); return; } // Decode only the new chunk buffer += decoder.decode(value, { stream: true }); // If your server sends lines, split and log only complete lines: let lines = buffer.split("\n"); buffer = lines.pop(); // Save incomplete line for next chunk for (const line of lines) { if (line.trim()) { let c_line = JSON.parse(line); if (c_line["task"] !== "DONE_QUIT") { setStatusMessages((msgs) => [ ...msgs, c_line, ]); } } } read(); }); } read(); }) .catch((error) => { console.error("Error while streaming status:", error); }); }; // Function to resubmit fetch const handleResubmit = (doTestMode = false) => { setVideoPlaying(false); let startRangeUse; let endRangeUse; setDrawerOpen(false); if (doTestMode == true) { startRangeUse = new Date( new Date().getTime() - 2 * 24 * 60 * 60 * 1000, ); endRangeUse = new Date( new Date().getTime() - 1 * 24 * 60 * 60 * 1000, ); } else { startRangeUse = startRange; endRangeUse = endRange; } setStartRange(startRangeUse); setEndRange(endRangeUse); const params = new URLSearchParams(); params.append("startRange", startRangeUse.toISOString()); params.append("endRange", endRangeUse.toISOString()); params.append("threshold", 0.0); params.append("query", queryText); params.append("camera", selectedCamera); setDataResults({ videos: [], breaks: [] }); authenticatedFetch("api/videos.json?" + params.toString()) .then((res) => res.json()) .then((data) => { streamComputeStatus(); // Don't setDataResults here, since it's just {"status": ...} pollForResult(); // Start polling for the real result }); setLastSubmitted({ startRange: startRangeUse, endRange: endRangeUse, queryText, selectedCamera, }); }; function selectHighResFunc(selectedHighRes, setSelectedHighRes, toggleCheckbox = true) { console.log(selectedHighRes); const params = new URLSearchParams(); params.append("do_high_res", !selectedHighRes); authenticatedFetch("api/set_parameter?" + params.toString()) .then((res) => res.json()) .then((data) => { if (toggleCheckbox) { setSelectedHighRes(!selectedHighRes); } }); } function updateDataAndValue(newValue) { const floatValue = parseFloat(newValue); setSliderValue(floatValue); var newData = JSON.parse(JSON.stringify(original_data.current)); newData["videos"] = newData["videos"].filter( (vid) => vid["embed_scores"]["score"][1] >= floatValue, ); newData["threshold"] = floatValue; setDataResults(newData); } function setMarkerValueNonReactive(inputValue) { let chart = chartRef.current.getEchartsInstance(); let options = chart.getOption(); let mappers = options["mappers"]; let vv = { xAxis: mappers["real_to_virtual"](new Date(inputValue)), lineStyle: { type: "solid", color: "#FF0000", width: 2 }, label: { show: false, formatter: "Break", position: "bottom", color: "#888", fontSize: 10, }, }; let markLine = { symbol: ["none", "none"], data: [vv], lineStyle: { type: "dashed", color: "#FF0000", width: 2 }, silent: true, animation: false, }; // if ("markLine" in options["series"][1]) { if (false) { let vv_new = { xAxis: mappers["real_to_virtual"](new Date(inputValue)), }; let markLine_new = { data: [vv_new], }; chart.setOption( { series: [{}, { markLine: { data: [vv_new] } }], }, false, ["series.markLine"], ); } else { chart.setOption( { series: [{}, { markLine: markLine }], }, false, ["series.markLine"], ); } } const handleTimelineClick = useCallback((path, timeoffset) => { if (playerRef.current && playerInstanceRef.current) { const player = playerInstanceRef.current; const token = localStorage.getItem("access_token"); const authenticatedUrl = `api/${path}?token=${token}`; player.reset(); // Configure player for range request support const videoElement = player.el().querySelector("video"); if (videoElement) { videoElement.crossOrigin = "anonymous"; videoElement.preload = "metadata"; } player.src({ src: authenticatedUrl, type: "video/mp4", withCredentials: false, }); setVideoPlaying(true); // Ensure range headers are sent for seeking player.ready(() => { const tech = player.tech(); if (tech && tech.el_) { tech.el_.addEventListener("loadstart", () => { // Force range request capability if ( tech.el_.seekable && tech.el_.seekable.length === 0 ) { console.log( "Video doesn't support seeking - range headers may be needed", ); } }); } }); player.load(); // Add multiple event listeners for debugging player.one("loadedmetadata", () => { console.log("Video metadata loaded"); console.log("Video duration:", player.duration()); console.log("Time offset:", timeoffset); }); // Wait for the video to be ready for seeking player.one("canplay", () => { console.log("Video can start playing - setting time"); const duration = player.duration(); // Ensure timeoffset is valid const seekTime = Math.max( 0, Math.min(timeoffset, duration - 0.1), ); console.log("Seeking to:", seekTime, "of", duration); player.currentTime(seekTime); // Wait a bit before playing to ensure seek completed setTimeout(() => { const playPromise = player.play(); if (playPromise !== undefined) { playPromise .then(() => { console.log( "Video started playing at time:", player.currentTime(), ); }) .catch((error) => { console.error("Error playing video:", error); }); } }, 100); }); player.one("error", (e) => { console.error("Video error:", e); console.error("Player error:", player.error()); }); player.one("loadstart", () => { console.log("Load started"); }); player.one("canplay", () => { console.log("Video can start playing"); }); } else { console.error("Player ref not available"); } }, []); useEffect(() => { const params = new URLSearchParams(window.location.search); selectHighResFunc( selectedHighRes, setSelectedHighRes, false ) handleResubmit(params.get("test_mode") === "true"); }, []); // useEffect(() => { // if (startRange && endRange) { // handleResubmit(); // } // }, [startRange, endRange]); function pollForResult() { const poll = () => { authenticatedFetch("api/videos_result.json") .then((res) => res.json()) .then((data) => { if (data.status === "processing") { setTimeout(poll, 250); // Try again in 1 second } else { const max_value = Math.max( ...data["videos"].map( (vid) => vid["embed_scores"]["score"][1], ), ); setSliderMax(max_value); original_data.current = data; setDataResults(data); updateDataAndValue(sliderValue) } }); }; poll(); } const [isAuthenticated, setIsAuthenticated] = useState(false); const [checkingAuth, setCheckingAuth] = useState(true); // Check authentication on load useEffect(() => { const token = localStorage.getItem("access_token"); if (token) { // Verify token is still valid fetch("/api/videos_result.json", { headers: { Authorization: `Bearer ${token}`, }, }) .then((response) => { if (response.ok) { setIsAuthenticated(true); } else { localStorage.removeItem("access_token"); setIsAuthenticated(false); } }) .catch(() => { localStorage.removeItem("access_token"); setIsAuthenticated(false); }) .finally(() => { setCheckingAuth(false); }); } else { setCheckingAuth(false); } }, []); // Add authentication header to all API calls const authenticatedFetch = useCallback((url, options = {}) => { const token = localStorage.getItem("access_token"); const headers = { ...options.headers, Authorization: `Bearer ${token}`, }; return fetch(url, { ...options, headers }); }, []); const handleLoginSuccess = () => { setIsAuthenticated(true); handleResubmit(params.get("test_mode") === "true"); }; const handleLogout = () => { localStorage.removeItem("access_token"); setIsAuthenticated(false); }; if (checkingAuth) { return (