FEAT: Add StreamDashboardPage;
UPDATE: Add route to `StreamDashboardPage`; UPDATE: `VideoPlayer` for improved stream handling; UPDATE: `Navbar` to include navigation to `StreamDashboardPage`; UPDATE: Expand `stream_data` method to include `stream_key` in `streams.py`; REFACTOR: Format `streams.py`;
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import Logo from "../Layout/Logo";
|
||||
import Button, { ToggleButton } from "../Input/Button";
|
||||
import Sidebar from "./Sidebar";
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
LogIn as LogInIcon,
|
||||
LogOut as LogOutIcon,
|
||||
Settings as SettingsIcon,
|
||||
Radio as LiveIcon,
|
||||
} from "lucide-react";
|
||||
import SearchBar from "../Input/SearchBar";
|
||||
import AuthModal from "../Auth/AuthModal";
|
||||
@@ -15,6 +16,7 @@ import { useAuth } from "../../context/AuthContext";
|
||||
import QuickSettings from "../Settings/QuickSettings";
|
||||
import { useSidebar } from "../../context/SidebarContext";
|
||||
import { useQuickSettings } from "../../context/QuickSettingsContext";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
interface NavbarProps {
|
||||
variant?: "home" | "default";
|
||||
@@ -25,6 +27,7 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
const { showAuthModal, setShowAuthModal } = useAuthModal();
|
||||
const { showSideBar, setShowSideBar } = useSidebar();
|
||||
const { showQuickSettings, setShowQuickSettings } = useQuickSettings();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
console.log("Logging out...");
|
||||
@@ -76,8 +79,10 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
}`}
|
||||
>
|
||||
<Logo variant={variant} />
|
||||
|
||||
{/* Login / Logout Button */}
|
||||
<Button
|
||||
extraClasses={`absolute top-[75px] ${
|
||||
extraClasses={`absolute top-[2vh] ${
|
||||
showSideBar
|
||||
? "left-[16vw] duration-[0.5s]"
|
||||
: "left-[20px] duration-[1s]"
|
||||
@@ -102,7 +107,7 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
<>
|
||||
<ToggleButton
|
||||
onClick={() => handleSideBar()}
|
||||
extraClasses={`absolute group text-[1rem] top-[20px] ${
|
||||
extraClasses={`absolute group text-[1rem] top-[9vh] ${
|
||||
showSideBar
|
||||
? "left-[16vw] duration-[0.5s]"
|
||||
: "left-[20px] duration-[1s]"
|
||||
@@ -123,7 +128,7 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
|
||||
{/* Quick Settings Sidebar */}
|
||||
<ToggleButton
|
||||
extraClasses={`absolute group text-[1rem] top-[20px] ${
|
||||
extraClasses={`absolute group text-[1rem] top-[2vh] ${
|
||||
showQuickSettings ? "right-[21vw]" : "right-[20px]"
|
||||
} cursor-pointer`}
|
||||
onClick={() => handleQuickSettings()}
|
||||
@@ -132,15 +137,28 @@ const Navbar: React.FC<NavbarProps> = ({ variant = "default" }) => {
|
||||
<SettingsIcon className="h-15 w-15" />
|
||||
|
||||
{showQuickSettings && (
|
||||
<small className="absolute flex items-center top-0 mr-4 right-0 h-full w-full my-auto group-hover:right-full opacity-0 group-hover:opacity-100 text-white transition-all delay-200">
|
||||
Press Q
|
||||
</small>
|
||||
)}
|
||||
<small className="absolute flex items-center top-0 mr-4 right-0 h-full w-full my-auto group-hover:right-full opacity-0 group-hover:opacity-100 text-white transition-all delay-200">
|
||||
Press Q
|
||||
</small>
|
||||
)}
|
||||
</ToggleButton>
|
||||
<QuickSettings />
|
||||
|
||||
<SearchBar />
|
||||
|
||||
{/* Stream Button */}
|
||||
{isLoggedIn && !window.location.pathname.includes('go-live') && (
|
||||
<Button
|
||||
extraClasses={`${
|
||||
variant === "home" ? "absolute top-[2vh] right-[10vw]" : ""
|
||||
} flex flex-row items-center`}
|
||||
onClick={() => navigate("/go-live")}
|
||||
>
|
||||
<LiveIcon className="h-15 w-15 mr-2" />
|
||||
Go Live
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showAuthModal && <AuthModal onClose={() => setShowAuthModal(false)} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,31 +2,50 @@ import React, { useEffect, useRef } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import videojs from "video.js";
|
||||
import "video.js/dist/video-js.css";
|
||||
interface VideoPlayerProps {
|
||||
streamer?: string;
|
||||
extraClasses?: string;
|
||||
onStreamDetected?: (isStreamAvailable: boolean) => void;
|
||||
}
|
||||
|
||||
const VideoPlayer: React.FC = () => {
|
||||
const { streamerName } = useParams<{ streamerName: string }>(); // Get streamerName from URL
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
streamer,
|
||||
extraClasses,
|
||||
onStreamDetected,
|
||||
}) => {
|
||||
const { streamerName: urlStreamerName } = useParams<{
|
||||
streamerName: string;
|
||||
}>();
|
||||
const videoRef = useRef<HTMLDivElement>(null);
|
||||
const playerRef = useRef<videojs.Player | null>(null);
|
||||
|
||||
// Use URL param if available, otherwise fall back to prop
|
||||
const streamerName = urlStreamerName || streamer;
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoRef.current || !streamerName) return;
|
||||
|
||||
const streamUrl = `/stream/${streamerName}/index.m3u8`; // Updated URL with streamerName
|
||||
const streamUrl = `/stream/${streamerName}/index.m3u8`;
|
||||
|
||||
if (!playerRef.current) {
|
||||
const videoElement = document.createElement("video");
|
||||
videoElement.classList.add("video-js", "vjs-big-play-centered", "w-full", "h-full");
|
||||
videoElement.classList.add(
|
||||
"video-js",
|
||||
"vjs-big-play-centered",
|
||||
"w-full",
|
||||
"h-full"
|
||||
);
|
||||
videoElement.setAttribute("playsinline", "true");
|
||||
videoRef.current.appendChild(videoElement);
|
||||
|
||||
playerRef.current = videojs(videoElement, {
|
||||
controls: true,
|
||||
controls: false,
|
||||
autoplay: true,
|
||||
muted: true,
|
||||
muted: false,
|
||||
fluid: true,
|
||||
responsive: true,
|
||||
aspectRatio: "16:9",
|
||||
liveui: true,
|
||||
liveui: false,
|
||||
sources: [
|
||||
{
|
||||
src: streamUrl,
|
||||
@@ -35,12 +54,19 @@ const VideoPlayer: React.FC = () => {
|
||||
],
|
||||
});
|
||||
|
||||
// Handle stream errors & retry
|
||||
playerRef.current.on('loadeddata', () => {
|
||||
if (onStreamDetected) onStreamDetected(true);
|
||||
});
|
||||
|
||||
playerRef.current.on("error", () => {
|
||||
console.error(`Stream failed to load: ${streamUrl}`);
|
||||
if (onStreamDetected) onStreamDetected(false);
|
||||
setTimeout(() => {
|
||||
console.log("Retrying stream...");
|
||||
playerRef.current?.src({ src: streamUrl, type: "application/x-mpegURL" });
|
||||
playerRef.current?.src({
|
||||
src: streamUrl,
|
||||
type: "application/x-mpegURL",
|
||||
});
|
||||
playerRef.current?.play();
|
||||
}, 5000);
|
||||
});
|
||||
@@ -58,7 +84,10 @@ const VideoPlayer: React.FC = () => {
|
||||
}, [streamerName]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center bg-gray-900 rounded-lg">
|
||||
<div
|
||||
id="video-player"
|
||||
className={`${extraClasses} w-full h-full mx-auto flex justify-center items-center bg-gray-900 rounded-lg`}
|
||||
>
|
||||
<div ref={videoRef} className="w-full max-w-[160vh] self-center" />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user