From e5ccf25b45a56f2b4f30ff2bf3886f7623adb161 Mon Sep 17 00:00:00 2001 From: winoffrg Date: Sat, 13 Jun 2026 17:37:15 +0530 Subject: [PATCH 1/5] fix: player controls improvements --- apps/www/package.json | 2 +- .../components/playback-controls.tsx | 30 ++++++---- .../components/playback-mode-controls.tsx | 55 ++++++++++++++++--- .../audio-player/components/playlist.tsx | 29 +++++++--- .../audio-player/components/track-info.tsx | 46 ++++++---------- .../components/bottom-controls.tsx | 4 ++ .../playlist-navigation-controls.tsx | 32 +++++++++++ bun.lock | 4 +- 8 files changed, 143 insertions(+), 59 deletions(-) create mode 100644 apps/www/registry/default/blocks/video-player/components/playlist-navigation-controls.tsx diff --git a/apps/www/package.json b/apps/www/package.json index 939b3d89..29a56099 100644 --- a/apps/www/package.json +++ b/apps/www/package.json @@ -65,7 +65,7 @@ "remark-code-import": "^1.2.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", - "shaka-player": "^4.16.32", + "shaka-player": "^4.16.34", "tailwind-merge": "^3.6.0", "ts-morph": "27.0.2", "tw-animate-css": "^1.4.0", diff --git a/apps/www/registry/default/blocks/audio-player/components/playback-controls.tsx b/apps/www/registry/default/blocks/audio-player/components/playback-controls.tsx index fa27d261..52846041 100644 --- a/apps/www/registry/default/blocks/audio-player/components/playback-controls.tsx +++ b/apps/www/registry/default/blocks/audio-player/components/playback-controls.tsx @@ -16,7 +16,11 @@ import { RECOMMENDED_PLAYER_BUFFERING_THROTTLE_MS } from "@/registry/default/hoo import { usePlaylist } from "@/registry/default/hooks/use-playlist" import { PlaybackControl } from "@/registry/default/ui/playback-control" -export function PlaybackControls() { +export function PlaybackControls({ + showNavigation = true, +}: { + showNavigation?: boolean +}) { const status = usePlaybackStore((state) => state.status) const { hasNext, hasPrevious, next, previous } = usePlaylist() @@ -29,13 +33,15 @@ export function PlaybackControls() { return (
- + {showNavigation ? ( + + ) : null} - + {showNavigation ? ( + + ) : null}
) } diff --git a/apps/www/registry/default/blocks/audio-player/components/playback-mode-controls.tsx b/apps/www/registry/default/blocks/audio-player/components/playback-mode-controls.tsx index 2901e400..4f8fb415 100644 --- a/apps/www/registry/default/blocks/audio-player/components/playback-mode-controls.tsx +++ b/apps/www/registry/default/blocks/audio-player/components/playback-mode-controls.tsx @@ -1,6 +1,9 @@ "use client" import { AnimatePresence, motion } from "motion/react" +import { useEffect, useMemo } from "react" + +import type { RepeatMode } from "@/registry/default/hooks/use-playlist" import { cn } from "@/lib/utils" import { Button } from "@/registry/default/blocks/audio-player/components/button" @@ -16,19 +19,46 @@ import { usePlaylistStore, } from "@/registry/default/hooks/use-playlist" -export function RepeatControl() { - const { cycleRepeatMode } = usePlaylist() +type RepeatControlVariant = "asset" | "playlist" + +const REPEAT_MODES: Record = { + asset: ["off", "one"], + playlist: ["off", "all", "one"], +} + +export function RepeatControl({ + variant = "playlist", +}: { + variant?: RepeatControlVariant +}) { + const { setRepeatMode } = usePlaylist() const repeatMode = usePlaylistStore((state) => state.repeatMode) - const isActive = repeatMode !== "off" + const modes = REPEAT_MODES[variant] + const normalizedRepeatMode = useMemo( + () => normalizeRepeatMode(repeatMode, modes), + [modes, repeatMode] + ) + const isActive = normalizedRepeatMode !== "off" + + useEffect(() => { + if (repeatMode === normalizedRepeatMode) return + setRepeatMode(normalizedRepeatMode) + }, [normalizedRepeatMode, repeatMode, setRepeatMode]) + + const handleCycleRepeatMode = () => { + const modeIndex = modes.indexOf(normalizedRepeatMode) + const nextMode = modes[(modeIndex + 1) % modes.length] ?? "off" + setRepeatMode(nextMode) + } return ( ) } + +function normalizeRepeatMode( + mode: RepeatMode, + modes: RepeatMode[] +): RepeatMode { + if (modes.includes(mode)) return mode + if (mode === "all" && modes.includes("one")) return "one" + return modes[0] ?? "off" +} diff --git a/apps/www/registry/default/blocks/audio-player/components/playlist.tsx b/apps/www/registry/default/blocks/audio-player/components/playlist.tsx index de1cc938..e8659fde 100644 --- a/apps/www/registry/default/blocks/audio-player/components/playlist.tsx +++ b/apps/www/registry/default/blocks/audio-player/components/playlist.tsx @@ -4,18 +4,21 @@ import { CardsThreeIcon, PlayIcon } from "@phosphor-icons/react" import { Volume2Icon } from "lucide-react" import { useCallback, useMemo, useRef } from "react" -import type { AudioPlayerAsset } from "@/registry/default/blocks/audio-player/components/audio-source" - import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { cn } from "@/lib/utils" -import { useAudioSource } from "@/registry/default/blocks/audio-player/components/audio-source" +import { + type AudioPlayerAsset, + getAudioAssetMetadata, + useAudioSource, +} from "@/registry/default/blocks/audio-player/components/audio-source" import { Button } from "@/registry/default/blocks/audio-player/components/button" import { usePlayerStore } from "@/registry/default/hooks/use-player" import { usePlaylistStore } from "@/registry/default/hooks/use-playlist" +import { LimeplayLogo } from "@/registry/default/ui/limeplay-logo" export function Playlist() { const currentItem = usePlaylistStore( @@ -161,6 +164,8 @@ function TrackRow({ preloaded: boolean setSize: number }) { + const metadata = getAudioAssetMetadata(asset, "Untitled track") + return ( + ) +} + +function useHasPlaylist() { + return usePlaylistStore((state) => state.queue.length > 1) +} diff --git a/bun.lock b/bun.lock index e21a0bc2..6f1cd22f 100644 --- a/bun.lock +++ b/bun.lock @@ -65,7 +65,7 @@ "remark-code-import": "^1.2.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", - "shaka-player": "^4.16.32", + "shaka-player": "^4.16.34", "tailwind-merge": "^3.6.0", "ts-morph": "27.0.2", "tw-animate-css": "^1.4.0", @@ -1834,7 +1834,7 @@ "shadcn": ["shadcn@4.8.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.26.0", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "tailwind-merge": "^3.0.1", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-LAm3I/1FdoU/zu5GVG8Hbna4X9zlzEG5TeeCPXqsopkjvGk8QUF9OFhqeRN8oM6Oh/ynUI/yQHZxQAO3Ymcqsg=="], - "shaka-player": ["shaka-player@4.16.32", "", {}, "sha512-IctqLYNczkX3PzH/iG/N+m4r12WOgqIPnuvqIXRZlQssMAmKaaUP8pB2MGX9l7SwSG7/PUwQVtIlbt+o/BZl+A=="], + "shaka-player": ["shaka-player@4.16.34", "", {}, "sha512-magCRFwu1TsWvUJEQm6DlKK6u8EsP7WJzpIThN/gBiIhvedTI8lFxHew4qYPLMwXZ305RRpSEDZHJ/KwuOz/og=="], "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], From 85691df9e67b13d180c05587183359e1bb58b602 Mon Sep 17 00:00:00 2001 From: winoffrg Date: Sat, 13 Jun 2026 17:37:47 +0530 Subject: [PATCH 2/5] fix: player controls improvements --- .../default/blocks/audio-player/components/track-info.tsx | 2 +- .../blocks/video-player/components/bottom-controls.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/www/registry/default/blocks/audio-player/components/track-info.tsx b/apps/www/registry/default/blocks/audio-player/components/track-info.tsx index 7a7ac5b9..0dcb4055 100644 --- a/apps/www/registry/default/blocks/audio-player/components/track-info.tsx +++ b/apps/www/registry/default/blocks/audio-player/components/track-info.tsx @@ -26,7 +26,7 @@ export function TrackInfo() { /> ) : ( - + )}
diff --git a/apps/www/registry/default/blocks/video-player/components/bottom-controls.tsx b/apps/www/registry/default/blocks/video-player/components/bottom-controls.tsx index ec12e811..c58f02e1 100644 --- a/apps/www/registry/default/blocks/video-player/components/bottom-controls.tsx +++ b/apps/www/registry/default/blocks/video-player/components/bottom-controls.tsx @@ -3,9 +3,7 @@ import { PictureInPictureControl } from "@/registry/default/blocks/video-player/ import { PlaybackRateControl } from "@/registry/default/blocks/video-player/components/playback-rate-control" import { PlaybackStateControl } from "@/registry/default/blocks/video-player/components/playback-state-control" import { Playlist } from "@/registry/default/blocks/video-player/components/playlist" -import { - PlaylistNextControl, -} from "@/registry/default/blocks/video-player/components/playlist-navigation-controls" +import { PlaylistNextControl } from "@/registry/default/blocks/video-player/components/playlist-navigation-controls" import { TimelineSliderControl } from "@/registry/default/blocks/video-player/components/timeline-slider-control" import { VolumeGroupControl } from "@/registry/default/blocks/video-player/components/volume-group-control" import * as Layout from "@/registry/default/ui/player-layout" From 751fb204fa9a302c981902bb700244bc7403db28 Mon Sep 17 00:00:00 2001 From: winoffrg Date: Sun, 14 Jun 2026 03:38:05 +0530 Subject: [PATCH 3/5] feat: usage revamp and documentation update --- apps/www/app/layout.config.tsx | 33 +- apps/www/components/mdx-components.tsx | 2 + apps/www/components/mdx/mermaid.tsx | 41 ++ .../players/audio-player/demo-player.tsx | 7 +- .../stream-panel/use-stream-panel-sync.ts | 80 +- apps/www/content/docs/blocks/audio-player.mdx | 51 +- apps/www/content/docs/blocks/meta.json | 3 +- apps/www/content/docs/blocks/video-player.mdx | 68 +- .../docs/components/media-provider.mdx | 2 +- apps/www/content/docs/concepts.mdx | 137 ++-- apps/www/content/docs/events.mdx | 117 --- apps/www/content/docs/hooks/use-asset.mdx | 121 +++- .../docs/hooks/use-playback-source.mdx | 36 +- apps/www/content/docs/hooks/use-playback.mdx | 2 +- apps/www/content/docs/hooks/use-player.mdx | 8 +- apps/www/content/docs/quick-start.mdx | 41 +- apps/www/content/docs/usage.mdx | 289 ++++++++ apps/www/package.json | 4 +- .../audio-player/components/audio-source.tsx | 266 ++++--- .../audio-player/components/controls.tsx | 24 +- .../audio-player/components/media-player.tsx | 36 +- .../audio-player/components/playlist.tsx | 21 +- .../audio-player/hooks/use-playlist-asset.ts | 5 +- .../video-player/components/media-player.tsx | 43 +- .../default/blocks/video-player/page.tsx | 2 +- apps/www/registry/default/hooks/use-asset.ts | 684 ++++++++++++------ .../default/hooks/use-playback-source.ts | 91 ++- apps/www/registry/default/hooks/use-player.ts | 15 +- bun.lock | 9 +- 29 files changed, 1504 insertions(+), 734 deletions(-) create mode 100644 apps/www/components/mdx/mermaid.tsx delete mode 100644 apps/www/content/docs/events.mdx create mode 100644 apps/www/content/docs/usage.mdx diff --git a/apps/www/app/layout.config.tsx b/apps/www/app/layout.config.tsx index 80fc38fe..91830451 100644 --- a/apps/www/app/layout.config.tsx +++ b/apps/www/app/layout.config.tsx @@ -1,11 +1,10 @@ import type { BaseLayoutProps, LinkItemType } from "fumadocs-ui/layouts/shared" -import { - BlueprintIcon, - QuestionIcon, - SparkleIcon, -} from "@phosphor-icons/react/dist/ssr" -import { RssIcon } from "lucide-react" +import { IconBlocks } from "@central-icons-react/round-filled-radius-0-stroke-1/IconBlocks" +import { IconBook } from "@central-icons-react/round-filled-radius-0-stroke-1/IconBook" +import { IconBuildingBlocks } from "@central-icons-react/round-filled-radius-0-stroke-1/IconBuildingBlocks" +import { IconNewspaper2 } from "@central-icons-react/round-filled-radius-0-stroke-1/IconNewspaper2" +import { IconRocket } from "@central-icons-react/round-filled-radius-0-stroke-1/IconRocket" import Image from "next/image" import { Icons } from "@/components/icons" @@ -33,28 +32,34 @@ const COMMON_LINKS: LinkItemType[] = [ export const baseOptions: BaseLayoutProps = { links: [ { - icon: , + icon: , text: "Quick Start", type: "main", url: "/docs/quick-start", }, { - icon: , + icon: , text: "Introduction", type: "main", url: "/docs/introduction", }, { - icon: , - text: "Concepts", + icon: , + text: "Usage", type: "main", - url: "/docs/concepts", + url: "/docs/usage", }, { - icon: , - text: "Events", + icon: , + text: "Blocks", type: "main", - url: "/docs/events", + url: "/blocks/video-player", + }, + { + icon: , + text: "Concepts", + type: "main", + url: "/docs/concepts", }, ...COMMON_LINKS, ], diff --git a/apps/www/components/mdx-components.tsx b/apps/www/components/mdx-components.tsx index 8e36d5bf..a8a10415 100644 --- a/apps/www/components/mdx-components.tsx +++ b/apps/www/components/mdx-components.tsx @@ -8,6 +8,7 @@ import * as TabsComponents from "fumadocs-ui/components/tabs" import defaultComponents from "fumadocs-ui/mdx" import { ComponentPreview } from "@/components/component-preview" +import { Mermaid } from "@/components/mdx/mermaid" const generator = createGenerator() @@ -21,6 +22,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { Attribution, ComponentPreview, License, + Mermaid, pre: ({ ref: _ref, ...props }: React.ComponentPropsWithRef) => (
{props.children}
diff --git a/apps/www/components/mdx/mermaid.tsx b/apps/www/components/mdx/mermaid.tsx new file mode 100644 index 00000000..257b3f8b --- /dev/null +++ b/apps/www/components/mdx/mermaid.tsx @@ -0,0 +1,41 @@ +import { renderMermaidSVG } from "beautiful-mermaid" +import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock" + +export async function Mermaid({ chart }: { chart: string }) { + try { + const svg = renderMermaidSVG(chart, { + accent: "var(--color-fd-primary)", + bg: "transparent", + border: "color-mix(in oklab, var(--color-fd-border) 86%, transparent)", + fg: "var(--color-fd-foreground)", + font: "var(--font-sans), ui-sans-serif, system-ui, sans-serif", + interactive: true, + layerSpacing: 52, + line: "color-mix(in oklab, var(--color-fd-muted-foreground) 58%, transparent)", + muted: "var(--color-fd-muted-foreground)", + nodeSpacing: 36, + padding: 32, + surface: + "color-mix(in oklab, var(--color-fd-card) 92%, var(--color-fd-primary) 8%)", + transparent: true, + }) + + return ( +
+
+
+ ) + } catch { + return ( + +
{chart}
+
+ ) + } +} diff --git a/apps/www/components/players/audio-player/demo-player.tsx b/apps/www/components/players/audio-player/demo-player.tsx index b52d23d2..e00e1585 100644 --- a/apps/www/components/players/audio-player/demo-player.tsx +++ b/apps/www/components/players/audio-player/demo-player.tsx @@ -28,9 +28,8 @@ export function AudioPlayerDemo({ }: AudioPlayerDemoProps) { const [playlist, setPlaylist] = useState([]) const storeHydrated = useStreamPanelStoreHydrated() - const hasPersistedAudioSelection = useStreamPanelStore((s) => - Boolean(s.contentSelections.audio) - ) + const audioSelection = useStreamPanelStore((s) => s.contentSelections.audio) + const hasPersistedAudioSelection = Boolean(audioSelection) useEffect(() => { const abortController = new AbortController() @@ -54,7 +53,7 @@ export function AudioPlayerDemo({ return ( {children} 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 d49ac601..e6f6c002 100644 --- a/apps/www/components/stream-panel/use-stream-panel-sync.ts +++ b/apps/www/components/stream-panel/use-stream-panel-sync.ts @@ -2,7 +2,11 @@ import { useCallback, useEffect, useMemo, useRef } from "react" -import type { Asset, UseAssetOptions } from "@/registry/default/hooks/use-asset" +import type { + Asset, + AssetEvents, + UseAssetOptions, +} from "@/registry/default/hooks/use-asset" import type { PlaybackStore } from "@/registry/default/hooks/use-playback" import { @@ -22,12 +26,18 @@ import { useStreamPanelStoreHydrated, } from "@/components/stream-panel/use-stream-panel" import { getPresetsForType, type StreamPreset } from "@/lib/stream-presets" -import { useAsset } from "@/registry/default/hooks/use-asset" +import { + AssetRecoveryAction, + useAsset, +} from "@/registry/default/hooks/use-asset" import { useMediaStore } from "@/registry/default/hooks/use-media" import { PLAYBACK_FEATURE_KEY } from "@/registry/default/hooks/use-playback" import { usePlayerStore } from "@/registry/default/hooks/use-player" import { useVolumeStore } from "@/registry/default/hooks/use-volume" -import { useMediaFeatureApi } from "@/registry/default/ui/media-provider" +import { + useMediaEvents, + useMediaFeatureApi, +} from "@/registry/default/ui/media-provider" const DEFAULT_VIDEO_PRESET_ID = "mux-big-buck-bunny" @@ -37,6 +47,7 @@ export function useStreamPanelSync({ playerType?: StreamPanelPlayerType } = {}) { const playbackApi = useMediaFeatureApi(PLAYBACK_FEATURE_KEY) + const events = useMediaEvents() const mediaElement = useMediaStore((state) => state.mediaElement) const player = usePlayerStore((state) => state.instance) @@ -112,26 +123,31 @@ export function useStreamPanelSync({ }) }, }, - onAssetChange: (event) => { - const selection = - useStreamPanelStore.getState().contentSelections[playerType] - if (!selection || selection.kind !== "playlist") return - if (selection.index === event.currentIndex) return - - setContentSelection(playerType, { - ...selection, - index: event.currentIndex, - }) - }, - onLoadError: (_asset, error) => { - playbackApi.getState().playback.setError(error) - return "stop" + recover: { + loadError: (_asset, error) => { + playbackApi.getState().playback.setError(error) + return AssetRecoveryAction.Stop + }, }, }), - [blenderStreamCache, playbackApi, playerType, setContentSelection] + [blenderStreamCache, playbackApi] ) - const { loadPlaylist } = useAsset(assetOptions) + const { loadSource } = useAsset() + + useEffect(() => { + return events.on("assetchange", (event) => { + const selection = + useStreamPanelStore.getState().contentSelections[playerType] + if (!selection || selection.kind !== "playlist") return + if (selection.index === event.currentIndex) return + + setContentSelection(playerType, { + ...selection, + index: event.currentIndex, + }) + }) + }, [events, playerType, setContentSelection]) useEffect(() => { if (!mediaElement) return @@ -180,10 +196,10 @@ export function useStreamPanelSync({ src, type: playerType, } - loadPlaylist([asset as unknown as Asset]) + loadSource(asset as unknown as Asset, { loading: assetOptions }) return asset }, - [loadPlaylist, playbackApi, playerType] + [assetOptions, loadSource, playbackApi, playerType] ) const loadPlaylistPreset = useCallback( @@ -202,7 +218,10 @@ export function useStreamPanelSync({ index, kind: "playlist", }) - loadPlaylist(assets, index) + loadSource(assets, { + initialIndex: index, + loading: assetOptions, + }) }) .catch((error: unknown) => { if (error instanceof DOMException && error.name === "AbortError") @@ -211,7 +230,7 @@ export function useStreamPanelSync({ playbackApi.getState().playback.setError(error) }) }, - [loadPlaylist, playbackApi, playerType, setContentSelection] + [assetOptions, loadSource, playbackApi, playerType, setContentSelection] ) const restoreContentSelection = useCallback( @@ -226,7 +245,7 @@ export function useStreamPanelSync({ ) if (preset) { abortPlaylistRequest() - loadPlaylist([preset as unknown as Asset]) + loadSource(preset as unknown as Asset, { loading: assetOptions }) return } @@ -237,7 +256,8 @@ export function useStreamPanelSync({ [ abortPlaylistRequest, loadCustomStream, - loadPlaylist, + assetOptions, + loadSource, loadPlaylistPreset, playerType, ] @@ -277,9 +297,15 @@ export function useStreamPanelSync({ (preset: StreamPreset, kind: StreamPanelContentKind = "stream") => { abortPlaylistRequest() setContentSelection(playerType, { id: preset.id, index: 0, kind }) - loadPlaylist([preset as unknown as Asset]) + loadSource(preset as unknown as Asset, { loading: assetOptions }) }, - [abortPlaylistRequest, loadPlaylist, playerType, setContentSelection] + [ + abortPlaylistRequest, + assetOptions, + loadSource, + playerType, + setContentSelection, + ] ) const handlePlaylistPresetChange = useCallback( diff --git a/apps/www/content/docs/blocks/audio-player.mdx b/apps/www/content/docs/blocks/audio-player.mdx index cf266382..32eca7bd 100644 --- a/apps/www/content/docs/blocks/audio-player.mdx +++ b/apps/www/content/docs/blocks/audio-player.mdx @@ -13,7 +13,7 @@ status: free npx shadcn add @limeplay/audio-player ``` -## Usage +## Minimal Usage ```tsx import { @@ -32,10 +32,53 @@ const playlist: AudioPlayerAsset[] = [ ] export function Player() { - return + return } ``` +Use `source` for media loading. Use `mediaProps` only for native audio attributes. + +```tsx + +``` + +## Single Track Usage + +Pass one asset object when you do not need a queue. + +```tsx + +``` + +## Custom Loading + +The audio block includes an opinionated default resolver for `src` and `playbackUrls.primary`. Use `loading.resolveSource` when your app needs signed URLs, token refresh, or a different URL shape. + +```tsx + { + const response = await fetch(`/api/tracks/${asset.id}/source`, { signal }) + const source = await response.json() + + return source.url + }, + }} +/> +``` + ## Features - Playlist queue with track artwork, title, genre, and duration. @@ -43,12 +86,14 @@ export function Player() { - Previous, play/pause, next, volume, mute, repeat, and shuffle controls. - Playback URL resolution for direct `src` values or `playbackUrls.primary` endpoints. - Automatic skip/reload behavior for recoverable load and playback failures. +- Shared `source` and `loading` contract used by all source-driven blocks. ## Notes - `AudioPlayer` does not ship with bundled tracks. Pass a playlist from your app. - Use `duration` in milliseconds when you want stable duration labels before metadata loads. -- Use `resolveSource` for signed audio URLs, token refresh, or custom playback URL APIs. +- Do not pass `mediaProps.src`; pass `source` instead. +- For the full block loading model, see [Usage](/docs/usage). ## API Reference diff --git a/apps/www/content/docs/blocks/meta.json b/apps/www/content/docs/blocks/meta.json index 4be6b723..67f56810 100644 --- a/apps/www/content/docs/blocks/meta.json +++ b/apps/www/content/docs/blocks/meta.json @@ -1,5 +1,6 @@ { "title": "Blocks", - "description": "Headless showcase pages for registry blocks.", + "description": "Ready-to-use player blocks with shared source loading APIs.", + "pages": ["video-player", "audio-player"], "defaultOpen": true } diff --git a/apps/www/content/docs/blocks/video-player.mdx b/apps/www/content/docs/blocks/video-player.mdx index 4ab680e7..d9606258 100644 --- a/apps/www/content/docs/blocks/video-player.mdx +++ b/apps/www/content/docs/blocks/video-player.mdx @@ -13,7 +13,7 @@ status: free npx shadcn add @limeplay/video-player ``` -## Usage +## Minimal Usage ```tsx import { VideoPlayer } from "@/components/limeplay/video-player/components/media-player" @@ -22,10 +22,70 @@ const src = "https://ad391cc0d55b44c6a86d232548adc225.mediatailor.us-east-1.amazonaws.com/v1/master/d02fedbbc5a68596164208dd24e9b48aa60dadc7/singssai/master.m3u8" export function Player() { - return + return } ``` +Use `source` for media loading. Use `mediaProps` only for native video attributes. + +```tsx + +``` + +## Playlist Usage + +Pass an array to `source` when the player should manage a queue. + +```tsx +import { + VideoPlayer, + type VideoPlayerAsset, +} from "@/components/limeplay/video-player/components/media-player" + +const playlist: VideoPlayerAsset[] = [ + { + id: "episode-1", + poster: "/posters/episode-1.jpg", + src: "https://example.com/episode-1.mpd", + title: "Episode 1", + }, + { + id: "episode-2", + poster: "/posters/episode-2.jpg", + src: "https://example.com/episode-2.mpd", + title: "Episode 2", + }, +] + +export function Player() { + return +} +``` + +## Custom Loading + +Use `loading.resolveSource` when your app needs signed URLs, token refresh, or source lookup before Shaka loads the asset. + +```tsx + { + const response = await fetch(`/api/assets/${asset.id}/source`, { signal }) + const source = await response.json() + + return { + config: source.config, + src: source.url, + } + }, + }} +/> +``` + ## Features - Single-source and playlist-backed loading APIs. @@ -33,12 +93,14 @@ export function Player() { - Timeline scrubbing with buffered progress feedback. - Volume, mute, captions, playback rate, and picture-in-picture controls. - Shaka-backed playback for HLS, DASH, and DRM-capable streams. +- Shared `source` and `loading` contract used by all source-driven blocks. ## Notes - `VideoPlayer` renders a video element internally. You do not need an `as` prop. - Pass `mediaProps` for native video options like `muted`, `playsInline`, or `autoPlay`. -- Use `resolveSource` when your app needs signed URLs, token refresh, or custom asset loading. +- Do not pass `mediaProps.src`; pass `source` instead. +- For the full block loading model, see [Usage](/docs/usage). ## API Reference diff --git a/apps/www/content/docs/components/media-provider.mdx b/apps/www/content/docs/components/media-provider.mdx index 3d6b86a9..d2663e87 100644 --- a/apps/www/content/docs/components/media-provider.mdx +++ b/apps/www/content/docs/components/media-provider.mdx @@ -117,7 +117,7 @@ function Logger() { } ``` -See [Events](/docs/events) for the full event reference. +See [Concepts](/docs/concepts#event-and-action-bridge) for the event model. ## Multi-Instance diff --git a/apps/www/content/docs/concepts.mdx b/apps/www/content/docs/concepts.mdx index 16663808..0cadaafb 100644 --- a/apps/www/content/docs/concepts.mdx +++ b/apps/www/content/docs/concepts.mdx @@ -5,15 +5,17 @@ index: true The key ideas behind Limeplay's architecture. -## Features +## Feature Model -Every capability — playback, volume, timeline, captions — is a **feature**. A feature is a self-contained unit that provides: +Limeplay is built from **features**. A feature owns one capability, such as playback, volume, timeline, playlist, player setup, or asset loading. -- **Store slice** — A namespaced piece of state (e.g. `volume.level`, `playback.paused`) -- **Setup component** — An internal component that attaches DOM event listeners and syncs state (auto-mounted by `MediaProvider`) -- **Events** — Typed events emitted when state changes (e.g. `volumechange`, `seek`) +Each feature can provide: -You compose features with `createMediaKit`: +- A namespaced store slice, such as `playback.paused` or `volume.level`. +- A setup component that syncs native media or Shaka Player state. +- Typed events for state changes, such as `play`, `seek`, or `assetloaded`. + +You compose the features you need with `createMediaKit`. ```tsx title="lib/media.ts" "use client" @@ -26,97 +28,94 @@ import { volumeFeature } from "@/hooks/limeplay/use-volume" export const media = createMediaKit({ features: [mediaFeature(), playbackFeature(), volumeFeature()] as const, }) -``` - -This returns a `MediaProvider` component and typed hooks. Each feature's Setup component is auto-mounted inside `MediaProvider` — no manual wiring required. -### Available Features +export const { MediaProvider, useMediaApi, useMediaEvents, useMediaStore } = + media +``` -| Feature | Key | Factory | Events | -| -------------------------------------------------------- | ------------------ | --------------------------- | ---------------------------------------------------------------- | -| [Media](/docs/concepts) | `media` | `mediaFeature()` | — | -| [Player](/docs/hooks/use-player) | `player` | `playerFeature()` | `playerready`, `playererror`, `playbackerror`, `bufferingchange` | -| [Playback](/docs/hooks/use-playback) | `playback` | `playbackFeature()` | `play`, `pause`, `ended`, `buffering`, `statuschange` | -| [Volume](/docs/hooks/use-volume) | `volume` | `volumeFeature()` | `volumechange`, `mute` | -| [Timeline](/docs/hooks/use-timeline) | `timeline` | `timelineFeature()` | `timeupdate`, `durationchange`, `seek` | -| [Playlist](/docs/hooks/use-playlist) | `playlist` | `playlistFeature()` | `playlistchange` | -| [Playback Rate](/docs/hooks/use-playback-rate) | `playbackRate` | `playbackRateFeature()` | `ratechange` | -| [Picture-in-Picture](/docs/hooks/use-picture-in-picture) | `pictureInPicture` | `pictureInPictureFeature()` | `enterpictureinpicture`, `leavepictureinpicture` | -| [Captions](/docs/hooks/use-captions) | `captions` | `captionsFeature()` | — | -| [Asset](/docs/hooks/use-asset) | `asset` | `assetFeature()` | — | +Use this page to understand the model. Use the [Hooks](/docs/hooks) section when you need exact APIs. ## Store -State is managed by [Zustand](https://zustand.docs.pmnd.rs/) with [Immer](https://immerjs.github.io/immer/) middleware. Each `MediaProvider` creates an isolated store instance via React Context, so multiple players on the same page maintain independent state. - -### Reading State +Each `MediaProvider` creates an isolated Zustand store. Multiple players on the same page do not share state unless you explicitly wire them together. -Use the typed selector hooks to subscribe to specific fields: +Read state with selectors so components only re-render for the fields they use. ```tsx -// Per-feature convenience hook -const level = useVolumeStore((s) => s.level) -const paused = usePlaybackStore((s) => s.paused) - -// Or the unified hook from createMediaKit -const level = media.useMediaStore((s) => s.volume.level) +const paused = usePlaybackStore((state) => state.paused) +const volume = useVolumeStore((state) => state.volume) ``` -Components only re-render when the selected value changes. - -### Mutating State - -For imperative mutations (effects, callbacks), use the store API: +For imperative code, use the media API returned by your kit. ```tsx -const api = media.useMediaApi() - -// Read without subscribing -api.getState().volume.level +const api = useMediaApi() -// Mutate with Immer draft api.setState(({ volume }) => { volume.level = 0.5 }) ``` -## Event & Action Bridge - -Limeplay separates concerns into two bridges: - -### Event Bridge +## Event And Action Bridge -Each feature's Setup component attaches native DOM event listeners to the `