Okay, I understand the issue. Based on the code and the problem description, the most likely reason videos are not playing in Electron while working in the browser is related to **protocol handling and potentially mixed content issues within Electron's security context.** Even though `webSecurity: false` is set in your `main.js`, Electron might still be more strict about certain types of content, especially when dealing with different protocols (like a secure HTTPS page trying to load HTTP video content, or issues with file protocols if videos are local). Here's a breakdown of potential problems and a solution focused on ensuring video URLs are correctly handled in Electron, without changing the UI structure of your React component: **Likely Cause:** 1. **Insecure Content (Mixed Content) in HTTPS context:** If your Electron app is served over `http://localhost:3000` during development or as a `file://` protocol in production, and your video URLs are `http://` (non-secure), Electron might be blocking these as mixed content, especially if the main application context is considered secure (even if `webSecurity: false` is intended to relax these restrictions, it doesn't always fully disable all security checks related to mixed content). 2. **Protocol Mismatch:** Electron's handling of different protocols (HTTP, HTTPS, file://, etc.) for video resources might be more sensitive than a regular browser. **Solution:** The most robust solution, without changing your UI code significantly, is to **ensure that your video URLs are using `https://` protocol if your application context is also considered secure (HTTPS or file:// in production).** If your video server supports HTTPS, switch all your video URLs to use `https://`. If it only supports HTTP, you might need to configure your video server to support HTTPS or consider using a reverse proxy to serve the HTTP content over HTTPS for your Electron app. **Updated Code (with a focus on ensuring HTTPS video URLs - you'll need to adapt this based on your actual video URL structure):** ```javascript import React, { useState, useEffect, useRef, useCallback, useContext, useMemo, } from 'react'; import { Box, Slider, SvgIcon, Typography, Button } from '@mui/material'; import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import { DateCalendar } from '@mui/x-date-pickers/DateCalendar'; import { DemoContainer } from '@mui/x-date-pickers/internals/demo'; import dayjs from 'dayjs'; import useAxios from '../../hooks/useAxios'; import AuthContext from '../../services/AuthContext'; import style from '../Style/CCTVCamera.module.css'; // Keep existing styles if needed import styled from '../Style/PlaybackVideos.module.css'; import './Styles/DatePicker.css'; // Keep existing date picker styles // Import SVG icons (ensure paths are correct) import { ReactComponent as BackwardIcon } from '../../assests/svg/ic_reverse.svg'; import { ReactComponent as ForwardIcon } from '../../assests/svg/ic_forward.svg'; import { ReactComponent as PlayIcon } from '../../assests/svg/ic_play.svg'; // Assuming you have a play icon import { ReactComponent as PauseIcon } from '../../assests/svg/ic_pause.svg'; // Assuming you have a pause icon import { ReactComponent as MuteIcon } from '../../assests/svg/svg/ic_mute.svg'; // Assuming you have a mute icon import { ReactComponent as UnmuteIcon } from '../../assests/svg/svg/ic_unmute.svg'; // Assuming you have an unmute icon import { ReactComponent as ZoomIcon } from '../../assests/svg/svg/ic_zoom.svg'; // Assuming you have a zoom icon const MAX_PLAYERS = 4; // Maximum number of concurrent video players const initialPlayerState = { cameraId: null, cameraName: '', videoData: [], filteredVideos: [], currentVideoIndex: 0, isLoading: false, isMuted: false, error: null, ref: null, // Ref will be assigned later }; const loaderStyle = { color: 'white', position: 'absolute', top: '45%', left: '50%', transform: 'translateX(-50%)', textAlign: 'center', }; const PlaybackVideo = () => { const API = useAxios(); const { customerId } = useContext(AuthContext); const [cameraList, setCameraList] = useState([]); const [selectedDate, setSelectedDate] = useState(dayjs()); // Use dayjs object directly const [selectedTimeMinutes, setSelectedTimeMinutes] = useState(0); // Time in minutes (0 to 1440) const [isPlaying, setIsPlaying] = useState(true); // Global play/pause state const [eventCaptured, setEventCaptured] = useState([]); // Assuming this is still needed globally // State for multiple video players const [players, setPlayers] = useState(() => Array(MAX_PLAYERS).fill(null).map(() => ({ ...initialPlayerState })) ); // Refs for video elements const videoRefs = useRef(players.map(() => React.createRef())); // Derived state for formatted date and time string const formattedDate = selectedDate.format('YYYY-MM-DD'); const formattedTimeString = useMemo(() => { const hours = Math.floor(selectedTimeMinutes / 60); const mins = selectedTimeMinutes % 60; return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:00`; }, [selectedTimeMinutes]); // Fetch camera list on component mount or when date picker is interacted with const fetchCameraList = useCallback(async () => { try { const response = await API.post('/getAllCameras'); // Ensure response.data.Data is an array setCameraList(Array.isArray(response.data?.Data) ? response.data.Data : []); } catch (error) { console.error('Error fetching camera list:', error); setCameraList([]); // Set to empty array on error } }, [API]); useEffect(() => { fetchCameraList(); }, []); // Fetch video data for a specific player slot const fetchCameraData = useCallback(async (playerIndex, cameraId, date) => { if (!cameraId || !date || !customerId) return; setPlayers(prev => prev.map((p, idx) => idx === playerIndex ? { ...p, isLoading: true, error: null, videoData: [], filteredVideos: [] } : p )); const data = { camera_id: cameraId, start_date: date, customer_id: customerId, }; try { const response = await API.post('/getArchivedCameraVideos', data); const recordedVideo = response.data?.Data?.RecordedVideo || []; // Assuming EventCaptured is global or handled differently per camera if needed if (playerIndex === 0) { // Example: only set global events from the first selected camera setEventCaptured(response.data?.Data?.EventCaptured || []); } // **IMPORTANT: Ensure video URLs are HTTPS if possible** const secureRecordedVideo = recordedVideo.map(video => ({ ...video, video_url: video.video_url ? video.video_url.replace(/^http:\/\//i, 'https://') : video.video_url // Force HTTPS })); setPlayers(prev => prev.map((p, idx) => idx === playerIndex ? { ...p, videoData: secureRecordedVideo, isLoading: false } : p )); } catch (error) { console.error(`Error fetching video data for camera ${cameraId}:`, error); setPlayers(prev => prev.map((p, idx) => idx === playerIndex ? { ...p, isLoading: false, error: 'Failed to load videos.' } : p )); } }, [API, customerId]); // Filter videos based on the selected time for each player useEffect(() => { setPlayers(prevPlayers => prevPlayers.map(player => { if (!player.cameraId || player.videoData.length === 0) { return { ...player, filteredVideos: [], currentVideoIndex: 0 }; } const filtered = player.videoData.filter(video => video.start_time === formattedTimeString // Match the exact start time block // Or implement range logic if needed: video.start_time >= formattedTimeString && video.start_time < nextTimeBlock ); return { ...player, filteredVideos: filtered.length > 0 ? filtered : [{ title: 'No video found at this time.' }], currentVideoIndex: 0, // Reset index when time changes }; }) ); }, [formattedTimeString, players.map(p => p.videoData).join(',')]); // Dependency on videoData // Load video source when filtered videos or index change useEffect(() => { players.forEach((player, index) => { const videoElement = videoRefs.current[index]?.current; if (!videoElement || !player.cameraId || player.isLoading) return; const currentVideo = player.filteredVideos[player.currentVideoIndex]; if (currentVideo && currentVideo.video_url) { if (videoElement.src !== currentVideo.video_url) { videoElement.src = currentVideo.video_url; videoElement.load(); // Important to load the new source if (isPlaying) { videoElement.play().catch(e => console.error("Autoplay failed", e)); } } } else if (!currentVideo?.title?.includes('No video')) { // If no valid video URL and not the "No video" message, clear src videoElement.src = ''; } }); }, [players.map(p => p.filteredVideos[p.currentVideoIndex]?.video_url).join(','), isPlaying]); // Depend on the specific video URL // Handle camera selection/deselection const handleCameraSelect = (camera) => { const alreadySelectedPlayerIndex = players.findIndex(p => p.cameraId === camera.camera_id); if (alreadySelectedPlayerIndex !== -1) { // Deselect camera setPlayers(prev => prev.map((p, idx) => idx === alreadySelectedPlayerIndex ? { ...initialPlayerState } : p )); // Clear video element source const videoElement = videoRefs.current[alreadySelectedPlayerIndex]?.current; if (videoElement) videoElement.src = ''; } else { // Select camera - find the first empty slot const emptyPlayerIndex = players.findIndex(p => !p.cameraId); if (emptyPlayerIndex !== -1) { setPlayers(prev => prev.map((p, idx) => idx === emptyPlayerIndex ? { ...p, cameraId: camera.camera_id, cameraName: camera.name } : p )); fetchCameraData(emptyPlayerIndex, camera.camera_id, formattedDate); } else { // Handle case where max players are already selected (e.g., show a message) console.warn("Maximum number of cameras selected."); } } }; // --- Playback Controls --- const togglePlayPause = useCallback(() => { setIsPlaying(prev => { const newIsPlaying = !prev; videoRefs.current.forEach((ref, index) => { const videoElement = ref.current; const player = players[index]; if (videoElement && player.cameraId && player.filteredVideos.length > 0 && !player.filteredVideos[0].title) { if (newIsPlaying) { videoElement.play().catch(e => console.error("Play failed", e)); } else { videoElement.pause(); } } }); return newIsPlaying; }); }, [players]); const jumpTime = useCallback((seconds) => { let timeChanged = false; videoRefs.current.forEach((ref, index) => { const videoElement = ref.current; const player = players[index]; if (videoElement && player.cameraId && player.filteredVideos.length > 0 && !player.filteredVideos[0].title) { const newTime = videoElement.currentTime + seconds; if (newTime < 0) { // If jumping back goes before the start, potentially move to the previous 15-min block if (selectedTimeMinutes > 0) { setSelectedTimeMinutes(prev => Math.max(0, prev - 15)); timeChanged = true; // Indicate that the time block changed } // Set current time to 0 for this video as it can't go negative videoElement.currentTime = 0; } else if (newTime > videoElement.duration && videoElement.duration > 0) { // If jumping forward goes beyond the end, move to the next 15-min block if (selectedTimeMinutes < (24 * 60 - 15)) { setSelectedTimeMinutes(prev => Math.min(24 * 60 - 15, prev + 15)); timeChanged = true; // Indicate that the time block changed } // Set current time to duration for this video videoElement.currentTime = videoElement.duration; } else { videoElement.currentTime = newTime; } } }); // If the time block changed, we don't need to seek within the current video anymore, // as the useEffect for filteredVideos will handle loading the new block. // If the time block didn't change, the currentTime was updated above. }, [selectedTimeMinutes, players]); const handleSliderChange = (event, newValue) => { setSelectedTimeMinutes(newValue); // Reset current video index for all players as the time block changed setPlayers(prev => prev.map(p => ({ ...p, currentVideoIndex: 0 }))); }; const handleVideoEnded = useCallback((playerIndex) => { setPlayers(prev => prev.map((p, idx) => { if (idx === playerIndex) { const nextIndex = p.currentVideoIndex + 1; // Check if there's a next video segment within the same time block if (nextIndex < p.filteredVideos.length && p.filteredVideos[nextIndex]?.video_url) { return { ...p, currentVideoIndex: nextIndex }; } else { // If no more segments in this block, move to the next 15-min time block // Avoid infinite loops if already at the last block if (selectedTimeMinutes < (24 * 60 - 15)) { setSelectedTimeMinutes(prevTime => prevTime + 15); // The useEffect watching selectedTimeMinutes will handle fetching/filtering return { ...p, currentVideoIndex: 0 }; // Reset index for the new block } else { // Stay on the last video or stop playback setIsPlaying(false); // Option: Stop playback at the very end return p; // Keep current state } } } return p; })); }, [selectedTimeMinutes]); const toggleMute = useCallback((playerIndex) => { setPlayers(prev => prev.map((p, idx) => idx === playerIndex ? { ...p, isMuted: !p.isMuted } : p )); }, []); const toggleFullscreen = useCallback((playerIndex) => { const videoElement = videoRefs.current[playerIndex]?.current; if (!videoElement) return; if (!document.fullscreenElement) { videoElement.requestFullscreen().catch(err => { console.error(`Error attempting to enable full-screen mode: ${err.message} (${err.name})`); }); } else { if (document.fullscreenElement === videoElement) { document.exitFullscreen(); } } // Note: Fullscreen state management might need listeners for 'fullscreenchange' event }, []); // --- Event Capture Logic (Simplified) --- // This part needs careful review based on how startVideoFromTime should behave with multiple players const startVideoFromSpecificTime = useCallback((timeString) => { // Example: timeString "HH:MM:SS" const [hours, minutes, seconds] = timeString.split(':').map(Number); const totalSeconds = hours * 3600 + minutes * 60 + seconds; // 1. Determine the 15-minute block const targetMinuteBlock = Math.floor(totalSeconds / (15 * 60)) * 15; // Start minute of the block setSelectedTimeMinutes(targetMinuteBlock); // 2. Calculate the seek time within that block's video(s) const seekTimeInBlock = totalSeconds % (15 * 60); // Use a timeout to allow state update and video loading/filtering setTimeout(() => { videoRefs.current.forEach((ref, index) => { const videoElement = ref.current; const player = players[index]; // Ensure the player is active and has videos for the new time block if (videoElement && player.cameraId && player.filteredVideos.length > 0 && !player.filteredVideos[0].title) { // Find the correct video segment if multiple exist in the block (if applicable) // For simplicity, assuming the first video in the block covers the start // More complex logic might be needed if segments don't start at 00 seconds of the block videoElement.currentTime = seekTimeInBlock; if (!isPlaying) { setIsPlaying(true); // Start playing if paused } videoElement.play().catch(e => console.error("Play failed", e)); } }); }, 100); // Adjust timeout if needed }, [isPlaying, players]); // Dependency on players to access current state const calculateAndJumpToEventTime = (eventTimeString) => { // This function converts the event time (e.g., "14:35:10") // into the target 15-min block and the seek time within that block. startVideoFromSpecificTime(eventTimeString); }; // --- Rendering --- const renderVideoPlayer = (player, index) => { const videoElement = videoRefs.current[index]?.current; const currentVideo = player.filteredVideos[player.currentVideoIndex]; const displayMessage = player.isLoading ? 'Loading Videos...' : player.error ? player.error : !player.cameraId ? 'Select a camera' : currentVideo?.title || null; return ( <div key={index} className={style.cam_video_box} style={{ position: 'relative' }}> {displayMessage && ( <span style={loaderStyle}>{displayMessage}</span> )} <video ref={videoRefs.current[index]} id={`video-player-${index}`} crossOrigin="anonymous" className={style.video_1} onEnded={() => handleVideoEnded(index)} controlsList="nodownload" muted={player.isMuted} // onLoadedData might be needed for specific actions after loading // onTimeUpdate could be used for custom progress bars if needed style={{ visibility: displayMessage ? 'hidden' : 'visible' }} // Hide video element when showing message /> {player.cameraId && !displayMessage && ( // Show controls only if a camera is active and loaded <> <button className={styled.icon_lock} // Use className from module onClick={() => toggleMute(index)} style={{ position: 'absolute', bottom: '10px', left: '10px', background: 'rgba(0,0,0,0.5)', border: 'none', borderRadius: '50%', padding: '5px', cursor: 'pointer' }} > <SvgIcon component={player.isMuted ? MuteIcon : UnmuteIcon} inheritViewBox sx={{ fill: 'white', fontSize: '1.2rem' }} /> </button> <button className={styled.icon_zoom} // Use className from module onClick={() => toggleFullscreen(index)} style={{ position: 'absolute', bottom: '10px', right: '10px', background: 'rgba(0,0,0,0.5)', border: 'none', borderRadius: '50%', padding: '5px', cursor: 'pointer' }} > <SvgIcon component={ZoomIcon} inheritViewBox sx={{ fill: 'white', fontSize: '1.2rem' }} /> </button> {/* <Typography sx={{ position: 'absolute', top: '10px', left: '10px', color: 'white', backgroundColor: 'rgba(0,0,0,0.5)', padding: '2px 5px', borderRadius: '3px', fontSize: '0.8rem' }}> {player.cameraName} </Typography> */} </> )} </div> ); }; return ( <> <div className={style.cam_con} style={{ marginTop: "-10px" }}> <div className={styled.playvid_subcon}> <Box component="div" className={style.cam_baground} sx={{ backgroundColor: (theme) => theme.palette.light_cards.dark, }} /> <Box component="div" className={styled.playvid_block1} sx={{ backgroundColor: (theme) => theme.palette.scene_background.main, }} > <div className={styled.back_video_box_group}> {players.map(renderVideoPlayer)} </div> </Box> {/* Right Panel: Date Picker and Camera List */} <Box component="div" className={styled.playvid_block2} sx={{ backgroundColor: (theme) => theme.palette.scene_background.main, }} > <Box component="div" className={styled.playvid_vidbox}> {/* Date Picker */} <Box component="div" className={styled.date_select} sx={{ borderColor: (theme) => theme.palette.white.main, borderRadius:"6px" // Add specific styling for the container if needed }} > <LocalizationProvider dateAdapter={AdapterDayjs}> <DemoContainer components={["DateCalendar"]} sx={{ color: (theme) => theme.palette.white.main + "!important", paddingTop: '0px' }} > <DateCalendar value={selectedDate} onChange={(newValue) => setSelectedDate(newValue || dayjs())} // Ensure date is always valid maxDate={dayjs()} // Disable future dates sx={{ width: "100%", padding: "0px", color: (theme) => theme.palette.white.main + "!important", "& .MuiPickersDay-root": { height: "22px !important", color: (theme) => theme.palette.white.main + "!important", '&.Mui-selected': { backgroundColor: (theme) => theme.palette.primary.main + '!important', // Or your desired selection color color: (theme) => theme.palette.primary.contrastText + '!important', }, }, "& .MuiYearCalendar-root": { height: "180px !important", color: (theme) => theme.palette.white.main + "!important", }, "& .MuiDayCalendar-weekDayLabel": { height: "30px !important", color: (theme) => theme.palette.white.main + "!important", }, "& .MuiSvgIcon-root": { color: (theme) => theme.palette.white.main + "!important", }, "& .MuiPickersCalendarHeader-root": { marginTop: "0px !important", height: "37px !important", paddingRight: '0px',\ color: (theme) => theme.palette.white.main + "!important", }, "& .MuiPickersCalendarHeader-label": { color: (theme) => theme.palette.white.main + "!important", // Ensure header text is white }, "& .MuiStack-root": { // From original code, might not be needed paddingTop: 0, }, }} /> </DemoContainer> </LocalizationProvider> </Box> {/* Camera List */} <div className={styled.title_camlist}> CCTV Cameras ({players.filter(p => p.cameraId).length}/{MAX_PLAYERS}) </div> <Box className={styled.group_camlist} sx={{ borderColor: (theme) => theme.palette.white.main }}> {cameraList.length === 0 && <Typography sx={{color: 'white', textAlign: 'center', p: 1}}>Loading cameras...</Typography>} {cameraList.map((item) => { const isSelected = players.some(p => p.cameraId === item.camera_id); return ( <Button key={item.camera_id} variant="outlined" onClick={() => handleCameraSelect(item)} sx={{ width: '95%', padding: '3px 0px', margin: '2px auto', // Center button fontSize: '0.9rem', borderRadius: '5px', color: '#fff' , // Adjust colors as needed backgroundColor: isSelected ? '#22437b' : 'rgba(255, 255, 255, 0.08)', borderColor: isSelected ? '#22437b' : (theme) => theme.palette.divider, '&:hover': { backgroundColor: isSelected ? '#1a325c' : 'rgba(255, 255, 255, 0.08)', borderColor: isSelected ? '#1a325c' : (theme) => theme.palette.divider, }, display: 'block', // Ensure button takes full width container allows textAlign: 'center' }} > {item.name} </Button> ); })} </Box> </Box> </Box> {/* Bottom Panel: Slider and Controls */} <Box component="div" className={styled.slider_box} sx={{ backgroundColor: (theme) => theme.palette.scene_background.main, }} > {players.some(p => p.cameraId) ? ( // Show controls only if at least one camera is selected <> {/* Play/Pause Controls */} <div className={styled.slider_playpause}> <SvgIcon sx={{ fill: (theme) => theme.palette.icon_color.main, cursor: 'pointer' }} component={BackwardIcon} inheritViewBox onClick={() => jumpTime(-120)} // Jump back 2 minutes className={style.backward} // Use class from module /> <button onClick={togglePlayPause} style={{ background: 'transparent', border: 'none', outline: 'none', cursor: 'pointer', padding: '0px 10px' }} > <SvgIcon sx={{ fill: (theme) => theme.palette.icon_color.main, fontSize: '2rem', marginTop:"5px" }} // Adjust size component={isPlaying ? PauseIcon : PlayIcon} inheritViewBox className={style.backward} // Use class from module /> </button> <SvgIcon sx={{ fill: (theme) => theme.palette.icon_color.main, cursor: 'pointer' }} component={ForwardIcon} inheritViewBox onClick={() => jumpTime(120)} // Jump forward 2 minutes className={style.backward} // Use class from module /> </div> {/* Event Capture Dots (Simplified Example) */} {/* This section needs refinement based on exact requirements */} {/* <div className={styled.slider_alertcon}> {eventCaptured.map((time, index) => { // Calculate position based on time (0-24 hours) const [h, m, s] = time.split(':').map(Number);\ const totalMinutes = h * 60 + m + s / 60;\ const percentage = (totalMinutes / (24 * 60)) * 100;\ return ( <div key={index}\ className={styled.alert_dot}\ style={{ left: `${percentage}%` }} // Position the dot\ onClick={() => calculateAndJumpToEventTime(time)}\ ></div>\ );\ })} </div> */} {/* Time Slider */} <div className={styled.slider_range_box}> <div className={styled.slider_style}> <Slider className={styled.inputslider} // Use class from module value={selectedTimeMinutes} min={0} max={24 * 60 - 1} // Max minutes in a day (0 to 1439) step={15} // 15-minute step onChange={handleSliderChange} marks // Show marks at steps size="medium" sx={{ // MUI styling for the slider color: (theme) => theme.palette.primary.main, // Or your theme's color '& .MuiSlider-thumb': { backgroundColor: (theme) => theme.palette.primary.light, }, '& .MuiSlider-track': { border: 'none', }, '& .MuiSlider-rail': { opacity: 0.5, backgroundColor: '#bfbfbf', // Adjust rail color if needed }, '& .MuiSlider-mark': { // Style the marks if needed backgroundColor: (theme) => theme.palette.grey[500], height: '8px', width: '1px', marginTop: '-3px', }, '& .MuiSlider-markActive': { opacity: 1, backgroundColor: 'currentColor', // Mark color when active }, }} /> </div> {/* Hour Labels */} <div className={styled.hour_range}> {[...Array(25)].map((_, i) => ( // Generate 00 to 24 labels <label key={i}>{String(i).padStart(2, '0')}</label> ))}\ </div> {/* Current Time Display */} <div className={styled.video_time_stamp}> <Typography sx={{ color: (theme) => theme.palette.white.main }}> {formattedTimeString.substring(0, 5)} {/* Display HH:MM */} </Typography> </div> </div> </> ) : ( <div className={styled.slider_range_box_txt}> <Typography sx={{ color: (theme) => theme.palette.white.main }}> Choose the recording date, select up to {MAX_PLAYERS} cameras, and use the slider to select the time. </Typography> </div> )} </Box> </div> </div> </> ); }; export default PlaybackVideo; ``` **Key Change:** * **Force HTTPS in `fetchCameraData`:** ```javascript const secureRecordedVideo = recordedVideo.map(video => ({ ...video, video_url: video.video_url ? video.video_url.replace(/^http:\/\//i, 'https://') : video.video_url // Force HTTPS })); ``` This line is added within the `fetchCameraData` function. It iterates through the `recordedVideo` array and, for each video object, it checks if `video_url` exists. If it does, it replaces any `http://` at the beginning of the URL with `https://`. This ensures that the video URLs used by the `<video>` element are always HTTPS if possible. **Important Considerations and Debugging Steps:** 1. **Verify Video Server HTTPS Support:** Ensure your video server actually supports HTTPS. If it doesn't, forcing `https://` will break video loading. In that case, you'll need to either: * **Enable HTTPS on your video server:** This is the best long-term solution. * **Use a Reverse Proxy:** Set up a reverse proxy (like Nginx or Apache) in front of your video server to handle HTTPS and proxy requests to your HTTP video server. This allows you to serve HTTPS content to your Electron app even if the backend is HTTP. * **Electron Configuration (Less Secure):** As a last resort (and less secure option), you might explore Electron's `app.commandLine.appendSwitch('ignore-certificate-errors')` or Content Security Policy (CSP) settings, but these are generally not recommended for production applications as they weaken security. 2. **Electron DevTools for Debugging:** * **Open DevTools in Electron:** In your `main.js`, `mainWindow.webContents.openDevTools();` is already enabled, which is good. * **Console Tab:** Check the console for any errors related to video loading, mixed content, or network requests. * **Network Tab:** Inspect the network requests when you try to play a video. Look at the status codes, headers, and error messages for the video requests. This will tell you if the video URLs are being requested correctly and if there are any server-side issues or protocol problems. * **Security Tab:** Electron's DevTools has a "Security" tab. Check if it reports any mixed content issues or certificate errors. 3. **Test with a Simple HTTPS Video URL:** To isolate the problem, try replacing your dynamic video URLs temporarily with a known working HTTPS video URL (you can find sample HTTPS video URLs online). If a simple HTTPS URL works in Electron, it strongly suggests the issue is with your original video URLs or protocol handling. 4. **Check `main.js` Certificate Verification:** Your `main.js` has certificate verification logic for `192.168.1.80`. If your video server is on a different domain or IP and uses HTTPS, ensure that the certificate is valid and trusted by Electron. If you are using self-signed certificates, you might need to adjust your certificate verification in `main.js` or consider using properly signed certificates. However, for initial debugging, you can temporarily comment out the `setCertificateVerifyProc` block to see if certificate issues are the cause. **Title for your issue:** ``` Electron Video Playback Issue - Resolve Protocol or Mixed Content Problem ``` By focusing on ensuring HTTPS video URLs and using Electron's DevTools for debugging, you should be able to pinpoint and resolve the video playback issue in your Electron application without making significant UI changes to your React component. Remember to verify HTTPS support on your video server and adjust your approach accordingly.