diff --git a/packages/www/components/AppPlayer/index.tsx b/packages/www/components/AppPlayer/index.tsx index 4c46ad011b..69d4185f34 100644 --- a/packages/www/components/AppPlayer/index.tsx +++ b/packages/www/components/AppPlayer/index.tsx @@ -2,13 +2,26 @@ import { Player } from "@livepeer/react"; import mux from "mux-embed"; import { memo, useCallback } from "react"; -interface AppPlayerProps { - playbackUrl: string; +type AppPlayerProps = { + playbackUrl?: string; + playbackId?: string; autoPlay?: boolean; type: "asset" | "stream"; -} +} & ( + | { + playbackUrl: string; + } + | { + playbackId: string; + } +); -const AppPlayer = ({ playbackUrl, autoPlay = true, type }: AppPlayerProps) => { +const AppPlayer = ({ + playbackUrl, + playbackId, + autoPlay = true, + type, +}: AppPlayerProps) => { const mediaElementRef = useCallback((element: HTMLMediaElement) => { mux.monitor(element, { debug: false, @@ -25,11 +38,12 @@ const AppPlayer = ({ playbackUrl, autoPlay = true, type }: AppPlayerProps) => { return ( ); }; diff --git a/packages/www/components/AssetDetails/AssetSharePopup.tsx b/packages/www/components/AssetDetails/AssetSharePopup.tsx index 1d4489a01a..25f0e797b7 100644 --- a/packages/www/components/AssetDetails/AssetSharePopup.tsx +++ b/packages/www/components/AssetDetails/AssetSharePopup.tsx @@ -10,6 +10,7 @@ import { } from "@livepeer/design-system"; import { CodeIcon, Link1Icon } from "@radix-ui/react-icons"; import { CopyToClipboard } from "react-copy-to-clipboard"; +import { isStaging } from "lib/utils"; const buttonLinkCss = { display: "flex", @@ -29,7 +30,9 @@ const AssetSharePopup = ({ onEmbedVideoClick, }: AssetSharePopupProps) => { const [openSnackbar] = useSnackbar(); - const copyString = `https://lvpr.tv?v=${playbackId}`; + const copyString = isStaging() + ? `https://monster.lvpr.tv?v=${playbackId}` + : `https://lvpr.tv?v=${playbackId}`; return ( diff --git a/packages/www/components/AssetDetails/EmbedVideoDialog.tsx b/packages/www/components/AssetDetails/EmbedVideoDialog.tsx index cdd161174a..0bd64dc866 100644 --- a/packages/www/components/AssetDetails/EmbedVideoDialog.tsx +++ b/packages/www/components/AssetDetails/EmbedVideoDialog.tsx @@ -9,10 +9,11 @@ import { TextField, useSnackbar, } from "@livepeer/design-system"; +import { isStaging } from "lib/utils"; import { CopyToClipboard } from "react-copy-to-clipboard"; -const embedStringForAsset = (playbackId: string) => - ``; +const embedStringForAsset = (embedUrl: string) => + ``; export type EmbedVideoDialog = { isOpen: boolean; @@ -25,7 +26,10 @@ const EmbedVideoDialog = ({ onOpenChange, playbackId, }: EmbedVideoDialog) => { - const embedString = embedStringForAsset(playbackId); + const embedUrl = isStaging() + ? `https://monster.lvpr.tv?v=${playbackId}` + : `https://lvpr.tv?v=${playbackId}`; + const embedString = embedStringForAsset(embedUrl); const [openSnackbar] = useSnackbar(); const onCopy = () => { diff --git a/packages/www/components/StreamDetails/StreamChildrenHeadingBox.tsx b/packages/www/components/StreamDetails/StreamChildrenHeadingBox.tsx index c874d4229f..ed6beebb27 100644 --- a/packages/www/components/StreamDetails/StreamChildrenHeadingBox.tsx +++ b/packages/www/components/StreamDetails/StreamChildrenHeadingBox.tsx @@ -22,7 +22,7 @@ export type StreamChildrenHeadingBoxProps = { stream: Stream; user: User; activeTab: string; - setSwitchTab: Dispatch>; + setSwitchTab: Dispatch>; invalidateStream: () => void; }; @@ -43,7 +43,7 @@ const StreamChildrenHeadingBox = ({ mb: "$4", width: "100%", }}> - + setSwitchTab("Overview")} @@ -54,14 +54,28 @@ const StreamChildrenHeadingBox = ({ textDecoration: "none", borderBottom: "2px solid", borderColor: activeTab === "Overview" ? "$green9" : "transparent", - mr: "$5", "&:hover": { textDecoration: "none", }, }}> Overview - + setSwitchTab("Details")} + css={{ + textDecoration: "none", + pb: "$2", + width: "100%", + cursor: "default", + borderBottom: "2px solid", + borderColor: activeTab === "Details" ? "$green9" : "transparent", + "&:hover": { + textDecoration: "none", + }, + }}> + Details + setSwitchTab("Health")} diff --git a/packages/www/components/StreamDetails/StreamDetailsTab.tsx b/packages/www/components/StreamDetails/StreamDetailsTab.tsx new file mode 100644 index 0000000000..a8aa58f204 --- /dev/null +++ b/packages/www/components/StreamDetails/StreamDetailsTab.tsx @@ -0,0 +1,35 @@ +import { Text } from "@livepeer/design-system"; +import MultistreamTargetsTable from "components/StreamDetails/MultistreamTargetsTable"; +import SessionsTable from "./SessionsTable"; + +const StreamDetailsTab = ({ id, stream, streamHealth, invalidateStream }) => { + return ( + <> + + No targets + + } + tableLayout="auto" + border + /> + + No sessions + + } + tableLayout="auto" + border + /> + + ); +}; + +export default StreamDetailsTab; diff --git a/packages/www/components/StreamDetails/StreamHeadingBox.tsx b/packages/www/components/StreamDetails/StreamHeadingBox.tsx index d95e2ec573..c8374623ca 100644 --- a/packages/www/components/StreamDetails/StreamHeadingBox.tsx +++ b/packages/www/components/StreamDetails/StreamHeadingBox.tsx @@ -21,7 +21,6 @@ const StreamHeadingBox = ({ align="end" css={{ pb: "$3", - mb: "$5", width: "100%", }}> diff --git a/packages/www/components/StreamDetails/StreamDetailsBox.tsx b/packages/www/components/StreamDetails/StreamOverviewBox.tsx similarity index 70% rename from packages/www/components/StreamDetails/StreamDetailsBox.tsx rename to packages/www/components/StreamDetails/StreamOverviewBox.tsx index f6b42197ab..b6724a759e 100644 --- a/packages/www/components/StreamDetails/StreamDetailsBox.tsx +++ b/packages/www/components/StreamDetails/StreamOverviewBox.tsx @@ -4,7 +4,6 @@ import RelativeTime from "../RelativeTime"; import ShowURL from "../ShowURL"; import Record from "./Record"; import { QuestionMarkCircledIcon as Help } from "@radix-ui/react-icons"; -import { useState } from "react"; import { Stream } from "livepeer"; const Cell = ({ children, css = {} }) => { @@ -15,22 +14,17 @@ const Cell = ({ children, css = {} }) => { ); }; -export type StreamDetailsBoxProps = { +export type StreamOverviewBoxProps = { stream: Stream & { suspended?: boolean }; - globalIngestUrl: string; - globalSrtIngestUrl: string; globalPlaybackUrl: string; invalidateStream: () => void; }; -const StreamDetailsBox = ({ +const StreamOverviewBox = ({ stream, - globalIngestUrl, - globalSrtIngestUrl, globalPlaybackUrl, invalidateStream, -}: StreamDetailsBoxProps) => { - const [keyRevealed, setKeyRevealed] = useState(false); +}: StreamOverviewBoxProps) => { return ( <> - Details + Overview - Stream name - {stream.name} Stream ID - Stream key - - {keyRevealed ? ( - - - - ) : ( - - )} - - RTMP ingest URL - - - - SRT ingest URL - - - Playback ID @@ -108,26 +73,6 @@ const StreamDetailsBox = ({ anchor={false} /> - Record sessions - - - - - - - When enabled, transcoded streaming sessions will be recorded - and stored by Livepeer Studio. Each recorded session will - have a recording .m3u8 URL for playback and an MP4 download - link. This feature is currently free. - - }> - - - - Created at Status {stream.isActive ? "Active" : "Idle"} - Suspended - {stream.suspended ? "Suspended" : "Normal"} + Record sessions + + + + + + + When enabled, transcoded streaming sessions will be recorded + and stored by Livepeer Studio. Each recorded session will + have a recording .m3u8 URL for playback and an MP4 download + link. This feature is currently free. + + }> + + + + ); }; -export default StreamDetailsBox; +export default StreamOverviewBox; diff --git a/packages/www/components/StreamDetails/StreamOverviewTab.tsx b/packages/www/components/StreamDetails/StreamOverviewTab.tsx index e3f4f2f5c8..b0e5cbd569 100644 --- a/packages/www/components/StreamDetails/StreamOverviewTab.tsx +++ b/packages/www/components/StreamDetails/StreamOverviewTab.tsx @@ -1,32 +1,39 @@ -import SessionsTable from "./SessionsTable"; -import MultistreamTargetsTable from "components/StreamDetails/MultistreamTargetsTable"; -import { Text } from "@livepeer/design-system"; +import { useApi } from "hooks"; +import { useEffect, useState } from "react"; +import StreamOverviewBox from "./StreamOverviewBox"; const StreamOverviewTab = ({ id, stream, streamHealth, invalidateStream }) => { + const { getIngest } = useApi(); + + const [ingest, setIngest] = useState(null); + + useEffect(() => { + getIngest(true) + .then((ingest) => { + if (Array.isArray(ingest)) { + ingest.sort((a, b) => a.base.localeCompare(b.base)); + } + if ((ingest?.length ?? 0) > 0) { + setIngest(ingest?.[0]); + } + }) + .catch((err) => console.error(err)); // todo: surface this + }, [id]); + + const playbackId = (stream || {}).playbackId || ""; + + let globalPlaybackUrl = ""; + + if (ingest) { + globalPlaybackUrl = `${ingest?.playback ?? ""}/${playbackId}/index.m3u8`; + } + return ( <> - - No targets - - } - tableLayout="auto" - border - /> - - No sessions - - } - tableLayout="auto" - border /> ); diff --git a/packages/www/components/StreamDetails/StreamPlayerBox/ActiveStream.tsx b/packages/www/components/StreamDetails/StreamPlayerBox/ActiveStream.tsx index 56124bfedc..14e2addbb7 100644 --- a/packages/www/components/StreamDetails/StreamPlayerBox/ActiveStream.tsx +++ b/packages/www/components/StreamDetails/StreamPlayerBox/ActiveStream.tsx @@ -1,7 +1,7 @@ import AppPlayer from "components/AppPlayer"; import { Badge, Box, Status } from "@livepeer/design-system"; -const ActiveStream = ({ playbackUrl }: { playbackUrl: string }) => ( +const ActiveStream = ({ playbackId }: { playbackId: string }) => ( <> ( Active - + ); diff --git a/packages/www/components/StreamDetails/StreamPlayerBox/IdleStream.tsx b/packages/www/components/StreamDetails/StreamPlayerBox/IdleStream.tsx deleted file mode 100644 index 87f4437203..0000000000 --- a/packages/www/components/StreamDetails/StreamPlayerBox/IdleStream.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Badge, Box, Status } from "@livepeer/design-system"; - -const IdleStream = () => ( - - - - - - Idle - - -); - -export default IdleStream; diff --git a/packages/www/components/StreamDetails/StreamPlayerBox/index.tsx b/packages/www/components/StreamDetails/StreamPlayerBox/index.tsx index 43ec9cfb2b..b3a850d5c7 100644 --- a/packages/www/components/StreamDetails/StreamPlayerBox/index.tsx +++ b/packages/www/components/StreamDetails/StreamPlayerBox/index.tsx @@ -1,21 +1,61 @@ -import { Box, Flex, Button } from "@livepeer/design-system"; +import { + Badge, + Box, + Button, + Flex, + Status, + Tooltip, +} from "@livepeer/design-system"; +import { Broadcast as LivepeerBroadcast } from "@livepeer/react"; import { Share2Icon } from "@radix-ui/react-icons"; import { Stream } from "livepeer"; import AssetSharePopup from "../../AssetDetails/AssetSharePopup"; + +import { useEffect, useMemo, useState } from "react"; +import { FiKey, FiVideo } from "react-icons/fi"; import ActiveStream from "./ActiveStream"; -import IdleStream from "./IdleStream"; +import StreamSetupBox from "../StreamSetupBox"; +import { FaKey, FaVideo } from "react-icons/fa"; export type StreamPlayerBoxProps = { stream: Stream; - globalPlaybackUrl: string; onEmbedVideoClick: () => void; + globalIngestUrl: string; + globalSrtIngestUrl: string; + globalPlaybackUrl: string; + invalidateStream: () => void; }; const StreamPlayerBox = ({ stream, - globalPlaybackUrl, onEmbedVideoClick, + globalIngestUrl, + globalSrtIngestUrl, + globalPlaybackUrl, + invalidateStream, }: StreamPlayerBoxProps) => { + const [activeTab, setSwitchTab] = useState<"Browser" | "Streaming Software">( + "Browser" + ); + const [showBroadcast, setShowBroadcast] = useState(false); + + const isStreamActiveFromExternal = useMemo( + () => !showBroadcast && stream.isActive, + [showBroadcast, stream.isActive] + ); + + useEffect(() => { + if (isStreamActiveFromExternal) { + setSwitchTab("Streaming Software"); + } + }, [isStreamActiveFromExternal]); + + useEffect(() => { + if (showBroadcast) { + setSwitchTab("Browser"); + } + }, [showBroadcast]); + return ( - {stream.isActive ? ( - - ) : ( - - )} - - - + + {showBroadcast ? ( + + ) : stream.isActive ? ( + + ) : ( + <> + + + + + Idle + + + )} + + + + + + { + if (!isStreamActiveFromExternal) setSwitchTab("Browser"); + }} + css={{ + display: "flex", + gap: "$1", + m: "$1", + p: "$2", + borderRadius: "$1", + width: "100%", + cursor: isStreamActiveFromExternal ? "not-allowed" : "default", + textDecoration: "none", + alignItems: "center", + justifyContent: "center", + color: activeTab === "Browser" ? "$neutral1" : "inherit", + backgroundColor: + activeTab === "Browser" ? "$neutral12" : "transparent", + }}> + + Browser + + setSwitchTab("Streaming Software")} + css={{ + display: "flex", + gap: "$1", + m: "$1", + p: "$2", + borderRadius: "$1", + width: "100%", + cursor: "default", + textDecoration: "none", + alignItems: "center", + justifyContent: "center", + color: activeTab === "Streaming Software" ? "$neutral1" : "inherit", + backgroundColor: + activeTab === "Streaming Software" ? "$neutral12" : "transparent", + }}> + + Streaming Software + + + ); }; diff --git a/packages/www/components/StreamDetails/StreamSetupBox.tsx b/packages/www/components/StreamDetails/StreamSetupBox.tsx new file mode 100644 index 0000000000..c810c8cd77 --- /dev/null +++ b/packages/www/components/StreamDetails/StreamSetupBox.tsx @@ -0,0 +1,111 @@ +import { Box, Flex, Heading, Link, Text } from "@livepeer/design-system"; +import { Stream } from "livepeer"; +import ClipButton from "../Clipping/ClipButton"; +import ShowURL from "../ShowURL"; + +export type StreamSetupBoxProps = { + activeTab: "Browser" | "Streaming Software"; + stream: Stream & { suspended?: boolean }; + globalIngestUrl: string; + globalSrtIngestUrl: string; + globalPlaybackUrl: string; + invalidateStream: () => void; +}; + +const StreamSetupBox = ({ + activeTab, + stream, + globalIngestUrl, + globalSrtIngestUrl, + invalidateStream, +}: StreamSetupBoxProps) => { + return ( + <> + + + + {activeTab === "Streaming Software" + ? "Streaming software setup" + : "Go live from the browser"} + + + {activeTab === "Streaming Software" ? ( + <> + Copy and paste the stream key into your streaming software. Use + either the RTMP or SRT ingest, depending on your use-case. The + RTMP ingest is more common with OBS users.{" "} + + Check out our docs for more details. + + + ) : ( + 'Check that your camera and microphone inputs are properly working before clicking the "Go live" button above.' + )} + + {activeTab === "Streaming Software" && ( + <> + + Stream key + + + + + + RTMP ingest + + + + + + SRT ingest + + + + + + )} + + + + ); +}; + +export default StreamSetupBox; diff --git a/packages/www/layouts/streamDetail.tsx b/packages/www/layouts/streamDetail.tsx index 15bdc3699d..5717d30688 100644 --- a/packages/www/layouts/streamDetail.tsx +++ b/packages/www/layouts/streamDetail.tsx @@ -6,7 +6,7 @@ import { useEffect, useState, useMemo } from "react"; import Spinner from "components/Spinner"; import { Variant as StatusVariant } from "components/StatusBadge"; import StreamPlayerBox from "components/StreamDetails/StreamPlayerBox/"; -import StreamDetailsBox from "components/StreamDetails/StreamDetailsBox"; +import StreamSetupBox from "components/StreamDetails/StreamSetupBox"; import StreamHeadingBox from "components/StreamDetails/StreamHeadingBox"; import StreamChildrenHeadingBox from "components/StreamDetails/StreamChildrenHeadingBox"; import EmbedVideoDialog from "components/AssetDetails/EmbedVideoDialog"; @@ -129,7 +129,7 @@ const StreamDetail = ({ /> - + {stream ? ( <> @@ -150,12 +150,8 @@ const StreamDetail = ({ stream={stream} globalPlaybackUrl={globalPlaybackUrl} onEmbedVideoClick={() => setEmbedVideoDialogOpen(true)} - /> - diff --git a/packages/www/package.json b/packages/www/package.json index 6e2f860ac9..6157c76473 100644 --- a/packages/www/package.json +++ b/packages/www/package.json @@ -22,7 +22,7 @@ "@emotion/styled": "^10.0.23", "@livepeer.studio/api": "^0.15.0", "@livepeer/design-system": "^1.0.17", - "@livepeer/react": "^2.5.7", + "@livepeer/react": "^2.6.1", "@mdx-js/loader": "^1.6.16", "@mdx-js/mdx": "^1.6.16", "@mdx-js/react": "^1.6.16", diff --git a/packages/www/pages/_app.tsx b/packages/www/pages/_app.tsx index de7eead80c..bbfd6affa1 100644 --- a/packages/www/pages/_app.tsx +++ b/packages/www/pages/_app.tsx @@ -66,8 +66,11 @@ const livepeerClient = createReactClient({ // eventually should move to using JWT from user's login apiKey: "", baseUrl: isStaging() - ? "https://livepeer.monster" - : "https://livepeer.studio", + ? "https://livepeer.monster/api" + : "https://livepeer.studio/api", + webrtcIngestBaseUrl: isStaging() + ? "https://webrtc.livepeer.monster/webrtc" + : "https://webrtc.livepeer.studio/webrtc", }), }); @@ -75,6 +78,20 @@ const livepeerTheme: ThemeConfig = { colors: { accent: "$colors$green10", }, + fontSizes: { + timeFontSize: "0.85rem", + timeFontSizeMd: "0.85rem", + timeFontSizeSm: "0.85rem", + titleFontSize: "0.9rem", + titleFontSizeMd: "0.9rem", + titleFontSizeSm: "0.9rem", + errorTitleFontSize: "1.3rem", + errorTitleFontSizeMd: "1.3rem", + errorTitleFontSizeSm: "1.3rem", + errorTextFontSize: "0.85rem", + errorTextFontSizeMd: "0.85rem", + errorTextFontSizeSm: "0.85rem", + }, }; const App = ({ Component, pageProps }) => { diff --git a/packages/www/pages/dashboard/streams/[id]/index.tsx b/packages/www/pages/dashboard/streams/[id]/index.tsx index 29d018596b..06f2c75b8c 100644 --- a/packages/www/pages/dashboard/streams/[id]/index.tsx +++ b/packages/www/pages/dashboard/streams/[id]/index.tsx @@ -6,6 +6,7 @@ import { useApi, useAnalyzer } from "hooks"; import StreamDetail from "layouts/streamDetail"; import StreamHealthTab from "components/StreamDetails/StreamHealthTab"; import StreamOverviewTab from "components/StreamDetails/StreamOverviewTab"; +import StreamDetailsTab from "components/StreamDetails/StreamDetailsTab"; const refetchInterval = 5 * 1000; @@ -14,9 +15,9 @@ const StreamDetails = () => { const queryClient = useQueryClient(); const { getStream } = useApi(); const { getHealth } = useAnalyzer(); - const [currentTab, setCurrentTab] = useState<"Overview" | "Health">( - "Overview" - ); + const [currentTab, setCurrentTab] = useState< + "Overview" | "Details" | "Health" + >("Overview"); const [embedVideoDialogOpen, setEmbedVideoDialogOpen] = useState(false); const { query } = router; @@ -63,6 +64,13 @@ const StreamDetails = () => { streamHealth={streamHealth} invalidateStream={invalidateStream} /> + ) : currentTab === "Details" ? ( + ) : (