diff --git a/apps/www/components/blocks/preview-pane.tsx b/apps/www/components/blocks/preview-pane.tsx index df31b81..ab268ea 100644 --- a/apps/www/components/blocks/preview-pane.tsx +++ b/apps/www/components/blocks/preview-pane.tsx @@ -2,7 +2,7 @@ import { domAnimation, LazyMotion, m } from "motion/react" import { usePathname, useRouter, useSearchParams } from "next/navigation" -import React, { useCallback, useLayoutEffect, useState } from "react" +import React, { useCallback, useLayoutEffect, useRef, useState } from "react" import { StreamPanelProvider } from "@/components/stream-panel" import { useThemeToggle } from "@/components/theme-toggle" @@ -25,6 +25,7 @@ export function BlockPreviewWithToolbar({ const [expanded, setExpanded] = useState(() => { return searchParams.get(EXPANDED_QUERY_PARAM) === "true" }) + const expandedRef = useRef(expanded) const { isDark, toggleTheme } = useThemeToggle({ blur: false, start: "top-right", @@ -51,11 +52,10 @@ export function BlockPreviewWithToolbar({ ) const handleExpandToggle = useCallback(() => { - setExpanded((previousExpanded) => { - const nextExpanded = !previousExpanded - updateExpandedQuery(nextExpanded) - return nextExpanded - }) + const nextExpanded = !expandedRef.current + expandedRef.current = nextExpanded + setExpanded(nextExpanded) + updateExpandedQuery(nextExpanded) }, [updateExpandedQuery]) const handleReload = useCallback(() => { @@ -63,7 +63,9 @@ export function BlockPreviewWithToolbar({ }, []) useLayoutEffect(() => { - setExpanded(searchParams.get(EXPANDED_QUERY_PARAM) === "true") + const nextExpanded = searchParams.get(EXPANDED_QUERY_PARAM) === "true" + expandedRef.current = nextExpanded + setExpanded(nextExpanded) }, [pathname, searchParams]) // DEV: This controls the left doc section to be hidden when preview is expanded diff --git a/apps/www/components/players/video-player/player-container.tsx b/apps/www/components/players/video-player/player-container.tsx index 9d4af1d..cada5d2 100644 --- a/apps/www/components/players/video-player/player-container.tsx +++ b/apps/www/components/players/video-player/player-container.tsx @@ -3,7 +3,7 @@ import type { RefObject } from "react" import { ChevronDownIcon, MonitorPlayIcon, RotateCwIcon } from "lucide-react" -import { useRef } from "react" +import { useMemo, useRef } from "react" import { useFullscreen, useToggle } from "react-use" import { @@ -27,6 +27,10 @@ export function VideoPlayerContainer() { const { isPortrait } = useOrientation() const isMobilePortrait = isMobile && isPortrait const playerRef = useRef(null) + const controlsVisibility = useVideoPlayerControlsVisibility({ + disabled: isMobilePortrait, + isMobile, + }) return ( @@ -50,6 +54,7 @@ export function VideoPlayerContainer() { muted: true, }} ref={playerRef} + {...controlsVisibility} > @@ -165,6 +170,22 @@ function useSelectedStreamName() { return ( getPresetsForType("video").find((preset) => preset.id === selection.id) - ?.name ?? "Custom Stream" + ?.title ?? "Custom Stream" + ) +} + +function useVideoPlayerControlsVisibility({ + disabled, + isMobile, +}: { + disabled: boolean + isMobile: boolean +}) { + return useMemo( + () => ({ + controlsHideDelay: disabled ? 0 : 1800, + hideCursorOnIdle: !disabled && !isMobile, + }), + [disabled, isMobile] ) } diff --git a/apps/www/components/stream-panel/content-catalog.ts b/apps/www/components/stream-panel/content-catalog.ts index 4ebdf97..f528046 100644 --- a/apps/www/components/stream-panel/content-catalog.ts +++ b/apps/www/components/stream-panel/content-catalog.ts @@ -42,6 +42,7 @@ export interface BlenderOpenFilmImages { backdrop?: string logo?: string poster?: string + thumbnail?: string } export interface BlenderStreamResponse extends BlenderPlaylistItem { @@ -141,6 +142,7 @@ const BlenderOpenFilmImagesSchema = z.object({ backdrop: z.string().optional(), logo: z.string().optional(), poster: z.string().optional(), + thumbnail: z.string().optional(), }) const BlenderStreamCaptionSchema = z.object({ @@ -421,7 +423,7 @@ function toBlenderOpenFilmAsset( duration: item.duration, id: item.id, images: item.images, - poster: item.images?.backdrop ?? item.images?.poster, + poster: item.images?.thumbnail ?? item.images?.poster, source: "blender-open-film", subtitle: item.subtitle, title: item.title, diff --git a/apps/www/components/stream-panel/panel-popover.tsx b/apps/www/components/stream-panel/panel-popover.tsx index 9113a7e..ed5bfc1 100644 --- a/apps/www/components/stream-panel/panel-popover.tsx +++ b/apps/www/components/stream-panel/panel-popover.tsx @@ -98,7 +98,7 @@ export function StreamPanel({ } return ( - presets.find((preset) => preset.id === contentSelection.id)?.name ?? + presets.find((preset) => preset.id === contentSelection.id)?.title ?? STREAM_PANEL_EMPTY_CONTENT_LABEL ) }, [contentSelection, playlistPresets, presets]) diff --git a/apps/www/components/stream-panel/presets-overlay.tsx b/apps/www/components/stream-panel/presets-overlay.tsx index ee49846..3de506b 100644 --- a/apps/www/components/stream-panel/presets-overlay.tsx +++ b/apps/www/components/stream-panel/presets-overlay.tsx @@ -138,7 +138,7 @@ export function PresetsOverlay({ >
- {preset.name} + {preset.title}
{preset.description ? ( diff --git a/apps/www/components/stream-panel/use-stream-panel-sync.ts b/apps/www/components/stream-panel/use-stream-panel-sync.ts index eab7e86..1ce490f 100644 --- a/apps/www/components/stream-panel/use-stream-panel-sync.ts +++ b/apps/www/components/stream-panel/use-stream-panel-sync.ts @@ -278,8 +278,8 @@ export function useStreamPanelSync({ format: "progressive", group: "Special", id, - name: "Custom Stream", src, + title: "Custom Stream", type: playerType, } loadSource(asset as unknown as Asset, { loading: assetOptions }) diff --git a/apps/www/lib/stream-presets.ts b/apps/www/lib/stream-presets.ts index 1b550d2..7dbfb88 100644 --- a/apps/www/lib/stream-presets.ts +++ b/apps/www/lib/stream-presets.ts @@ -36,11 +36,10 @@ export interface StreamPreset { format: "dash" | "hls" | "progressive" group: StreamGroup id: string - name: string poster?: string src: string thumbnail?: string - title?: string + title: string type: "audio" | "video" } @@ -53,9 +52,9 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "HLS", id: "mux-big-buck-bunny", - name: "Big Buck Bunny", poster: "https://files.vidstack.io/sprite-fight/poster.webp", src: "https://stream.mux.com/VZtzUzGRv02OhRnZCxcNg49OilvolTqdnFLEqBsTwaxU.m3u8", + title: "Big Buck Bunny", type: "video", }, { @@ -72,8 +71,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "HLS", id: "apple-advanced-hls", - name: "Apple Advanced Stream", src: "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8", + title: "Apple Advanced Stream", type: "video", }, { @@ -82,8 +81,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "HLS", id: "apple-bipbop-hls", - name: "Apple Bipbop", src: "https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8", + title: "Apple Bipbop", type: "video", }, { @@ -92,8 +91,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "HLS", id: "shaka-bbb-dark-truths-hls", - name: "Dark Truths", src: "https://storage.googleapis.com/shaka-demo-assets/bbb-dark-truths-hls/hls.m3u8", + title: "Dark Truths", type: "video", }, @@ -105,8 +104,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DASH", id: "shaka-angel-one-dash", - name: "Angel One", src: "https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd", + title: "Angel One", type: "video", }, { @@ -115,8 +114,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DASH", id: "shaka-sintel-4k", - name: "Sintel 4K", src: "https://storage.googleapis.com/shaka-demo-assets/sintel/dash.mpd", + title: "Sintel 4K", type: "video", }, { @@ -125,10 +124,10 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DASH", id: "bitmovin-dash", - name: "Art of Motion", src: "https://cdn.bitmovin.com/content/assets/art-of-motion-dash-hls-progressive/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd", thumbnail: "https://cdn.bitmovin.com/content/assets/art-of-motion-dash-hls-progressive/thumbnails/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.vtt", + title: "Art of Motion", type: "video", }, { @@ -137,8 +136,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DASH", id: "shaka-tears-surround", - name: "Tears of Steel", src: "https://storage.googleapis.com/shaka-demo-assets/tos-surround/dash.mpd", + title: "Tears of Steel", type: "video", }, @@ -150,8 +149,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Live", id: "dash-if-live", - name: "DASH-IF Live Sim", src: "https://livesim2.dashif.org/livesim2/utc_head/testpic_2s/Manifest.mpd", + title: "DASH-IF Live Sim", type: "video", }, { @@ -160,8 +159,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Live", id: "shaka-live-dash", - name: "Shaka Player History", src: "https://storage.googleapis.com/shaka-live-assets/player-source.mpd", + title: "Shaka Player History", type: "video", }, { @@ -170,8 +169,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "Live", id: "shaka-live-hls", - name: "Shaka Player History (HLS)", src: "https://storage.googleapis.com/shaka-live-assets/player-source.m3u8", + title: "Shaka Player History (HLS)", type: "video", }, { @@ -180,8 +179,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Live", id: "dash-if-ll-live", - name: "Low Latency DASH", src: "https://livesim2.dashif.org/livesim2/chunkdur_1/ato_7/testpic4_8s/Manifest300.mpd", + title: "Low Latency DASH", type: "video", }, @@ -200,8 +199,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DRM", id: "shaka-angel-widevine", - name: "Angel One", src: "https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd", + title: "Angel One", type: "video", }, { @@ -218,8 +217,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DRM", id: "shaka-angel-clearkey", - name: "Angel One", src: "https://storage.googleapis.com/shaka-demo-assets/angel-one-clearkey/dash.mpd", + title: "Angel One", type: "video", }, { @@ -235,8 +234,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "DRM", id: "shaka-angel-hls-widevine", - name: "Angel One (HLS)", src: "https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8", + title: "Angel One (HLS)", type: "video", }, { @@ -258,8 +257,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "DRM", id: "shaka-sintel-widevine", - name: "Sintel 4K", src: "https://storage.googleapis.com/shaka-demo-assets/sintel-widevine/dash.mpd", + title: "Sintel 4K", type: "video", }, { @@ -268,8 +267,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "DRM", id: "bitmovin-hls-aes128", - name: "Art of Motion", src: "https://cdn.bitmovin.com/content/assets/art-of-motion_drm/m3u8s/11331.m3u8", + title: "Art of Motion", type: "video", }, @@ -281,8 +280,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Audio", id: "shaka-dig-uke", - name: "Dig the Uke", src: "https://storage.googleapis.com/shaka-demo-assets/dig-the-uke-clear/dash.mpd", + title: "Dig the Uke", type: "audio", }, { @@ -291,8 +290,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "hls", group: "Audio", id: "apple-hls-audio-aac", - name: "HLS Audio (AAC)", src: "https://storage.googleapis.com/shaka-demo-assets/raw-hls-audio-only/manifest.m3u8", + title: "HLS Audio (AAC)", type: "audio", }, @@ -304,8 +303,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "progressive", group: "Progressive", id: "mp4-bunny-progressive", - name: "Big Buck Bunny", src: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", + title: "Big Buck Bunny", type: "video", }, { @@ -314,8 +313,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "progressive", group: "Progressive", id: "mp4-elephants-dream", - name: "Elephants Dream", src: "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4", + title: "Elephants Dream", type: "video", }, @@ -327,8 +326,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Special", id: "shaka-sintel-trick", - name: "Sintel", src: "https://storage.googleapis.com/shaka-demo-assets/sintel-trickplay/dash.mpd", + title: "Sintel", type: "video", }, { @@ -337,8 +336,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Special", id: "dash-if-thumbnails", - name: "Big Buck Bunny", src: "https://dash.akamaized.net/akamai/bbb_30fps/bbb_with_tiled_thumbnails.mpd", + title: "Big Buck Bunny", type: "video", }, { @@ -347,8 +346,8 @@ export const STREAM_PRESETS: StreamPreset[] = [ format: "dash", group: "Special", id: "bitmovin-vr", - name: "VR Playhouse", src: "https://cdn.bitmovin.com/content/assets/playhouse-vr/mpds/105560.mpd", + title: "VR Playhouse", type: "video", }, ] diff --git a/apps/www/next-env.d.ts b/apps/www/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/apps/www/next-env.d.ts +++ b/apps/www/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/www/registry/collection/registry-blocks.ts b/apps/www/registry/collection/registry-blocks.ts index c2d9650..6db69ff 100644 --- a/apps/www/registry/collection/registry-blocks.ts +++ b/apps/www/registry/collection/registry-blocks.ts @@ -24,6 +24,10 @@ export const blocks: Registry["items"] = [ path: `${VIDEO_PLAYER_SRC_URL}/components/media-player.tsx`, type: "registry:component", }, + { + path: `${VIDEO_PLAYER_SRC_URL}/components/asset-metadata-overlay.tsx`, + type: "registry:component", + }, { path: `${VIDEO_PLAYER_SRC_URL}/components/volume-state-control.tsx`, type: "registry:component", diff --git a/apps/www/registry/default/blocks/video-player/components/asset-metadata-overlay.tsx b/apps/www/registry/default/blocks/video-player/components/asset-metadata-overlay.tsx new file mode 100644 index 0000000..364a626 --- /dev/null +++ b/apps/www/registry/default/blocks/video-player/components/asset-metadata-overlay.tsx @@ -0,0 +1,46 @@ +"use client" + +import type { VideoPlayerAsset } from "@/registry/default/blocks/video-player/components/media-player" + +import { useAsset } from "@/registry/default/hooks/use-asset" +import { ControlsTopContainer } from "@/registry/default/ui/player-layout" + +export function AssetMetadataOverlay() { + const { currentItem } = useAsset() + const asset = currentItem?.properties + const description = asset?.description?.trim() + const title = asset?.title?.trim() + + if (!title && !description) return null + + return ( + <> +
+ +
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+
+ + ) +} diff --git a/apps/www/registry/default/blocks/video-player/components/media-player.tsx b/apps/www/registry/default/blocks/video-player/components/media-player.tsx index a935762..10b21df 100644 --- a/apps/www/registry/default/blocks/video-player/components/media-player.tsx +++ b/apps/www/registry/default/blocks/video-player/components/media-player.tsx @@ -10,6 +10,7 @@ import type { } from "@/registry/default/hooks/use-asset" import { cn } from "@/lib/utils" +import { AssetMetadataOverlay } from "@/registry/default/blocks/video-player/components/asset-metadata-overlay" import { BottomControls } from "@/registry/default/blocks/video-player/components/bottom-controls" import { Button } from "@/registry/default/blocks/video-player/components/button" import { MediaProvider } from "@/registry/default/blocks/video-player/lib/media-kit" @@ -28,13 +29,16 @@ export interface VideoPlayerAsset extends Asset { description?: string poster?: string title?: string + year?: string } export interface VideoPlayerProps { autoLoad?: boolean children?: React.ReactNode className?: string + controlsHideDelay?: number debug?: boolean + hideCursorOnIdle?: boolean initialIndex?: number loading?: UseAssetOptions /** @@ -51,7 +55,9 @@ export const VideoPlayer = React.forwardRef( autoLoad, children, className, + controlsHideDelay, debug, + hideCursorOnIdle, initialIndex, loading, mediaProps, @@ -71,7 +77,12 @@ export const VideoPlayer = React.forwardRef( source={source} sourceKey={sourceKey} /> - + @@ -87,6 +98,7 @@ export const VideoPlayer = React.forwardRef( {children} + diff --git a/apps/www/registry/default/blocks/video-player/components/playlist.tsx b/apps/www/registry/default/blocks/video-player/components/playlist.tsx index 81f9e62..e116d23 100644 --- a/apps/www/registry/default/blocks/video-player/components/playlist.tsx +++ b/apps/www/registry/default/blocks/video-player/components/playlist.tsx @@ -1,7 +1,7 @@ "use client" import { CardsThreeIcon, PlayIcon } from "@phosphor-icons/react" -import { useMemo } from "react" +import { useEffect, useMemo } from "react" import type { VideoPlayerAsset } from "@/registry/default/blocks/video-player/components/media-player" @@ -15,7 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Button } from "@/registry/default/blocks/video-player/components/button" -import { useAssetStore } from "@/registry/default/hooks/use-asset" +import { useMediaStore } from "@/registry/default/hooks/use-media" import { usePlayerStore } from "@/registry/default/hooks/use-player" import { usePlaylistStore } from "@/registry/default/hooks/use-playlist" @@ -24,10 +24,8 @@ export function Playlist() { (state) => state.currentItem as null | { id: string; properties: VideoPlayerAsset } ) - const preloadAsset = useAssetStore((state) => state.preloadAsset) as ( - asset: VideoPlayerAsset - ) => Promise - const preloadManagers = usePlayerStore((state) => state.preloadManagers) + const containerRef = usePlayerStore((state) => state.containerRef) + const setForceIdle = useMediaStore((state) => state.setForceIdle) const queue = usePlaylistStore( (state) => state.queue as { id: string; properties: VideoPlayerAsset }[] ) @@ -45,20 +43,28 @@ export function Playlist() { ) }, [queue, shuffle, shuffleOrder]) + useEffect(() => { + return () => { + setForceIdle(false) + } + }, [setForceIdle]) + if (orderedItems.length < 2) return null const handleAssetSelect = async (assetId: string) => { await skipToId(assetId) } - const handleAssetHover = async (assetId: string, asset: VideoPlayerAsset) => { - if (!preloadManagers.has(assetId) && currentItem?.id !== assetId) { - await preloadAsset(asset) - } + const dropdownCollisionProps: { + collisionBoundary?: Element + collisionPadding?: number + } = { + collisionBoundary: containerRef ?? undefined, + collisionPadding: 12, } return ( - +