Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions apps/www/app/layout.config.tsx
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -33,28 +32,34 @@ const COMMON_LINKS: LinkItemType[] = [
export const baseOptions: BaseLayoutProps = {
links: [
{
icon: <SparkleIcon />,
icon: <IconRocket />,
text: "Quick Start",
type: "main",
url: "/docs/quick-start",
},
{
icon: <QuestionIcon />,
icon: <IconBook />,
text: "Introduction",
type: "main",
url: "/docs/introduction",
},
{
icon: <BlueprintIcon />,
text: "Concepts",
icon: <IconNewspaper2 />,
text: "Usage",
type: "main",
url: "/docs/concepts",
url: "/docs/usage",
},
{
icon: <RssIcon />,
text: "Events",
icon: <IconBlocks />,
text: "Blocks",
type: "main",
url: "/docs/events",
url: "/blocks/video-player",
},
Comment on lines +53 to +57

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Block documentation links are using the wrong base path.
Both sites use /blocks/video-player instead of the docs namespace, so navigation can break for readers.

  • apps/www/app/layout.config.tsx#L53-L57: change the Blocks nav URL to /docs/blocks/video-player.
  • apps/www/content/docs/quick-start.mdx#L26-L26: update the inline link target to /docs/blocks/video-player.
📍 Affects 2 files
  • apps/www/app/layout.config.tsx#L53-L57 (this comment)
  • apps/www/content/docs/quick-start.mdx#L26-L26
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/www/app/layout.config.tsx` around lines 53 - 57, Update the incorrect
block links that use "/blocks/video-player" to the docs namespace
"/docs/blocks/video-player": in apps/www/app/layout.config.tsx (lines 53-57)
change the navigation item's url value for the "Blocks" entry from
"/blocks/video-player" to "/docs/blocks/video-player"; in
apps/www/content/docs/quick-start.mdx (line 26) update the inline link target
from "/blocks/video-player" to "/docs/blocks/video-player".

{
icon: <IconBuildingBlocks />,
text: "Concepts",
type: "main",
url: "/docs/concepts",
},
...COMMON_LINKS,
],
Expand Down
2 changes: 2 additions & 0 deletions apps/www/components/mdx-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -21,6 +22,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
Attribution,
ComponentPreview,
License,
Mermaid,
pre: ({ ref: _ref, ...props }: React.ComponentPropsWithRef<typeof Pre>) => (
<CodeBlock {...props} keepBackground>
<Pre>{props.children}</Pre>
Expand Down
41 changes: 41 additions & 0 deletions apps/www/components/mdx/mermaid.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<figure className="not-prose my-8 overflow-hidden rounded-2xl border bg-fd-card/70 shadow-sm ring-1 ring-fd-border/40">
<div
className="
overflow-x-auto p-4
sm:p-6
"
dangerouslySetInnerHTML={{ __html: svg }}
/>
</figure>
)
} catch {
return (
<CodeBlock title="Mermaid">
<Pre>{chart}</Pre>
</CodeBlock>
)
}
}
7 changes: 3 additions & 4 deletions apps/www/components/players/audio-player/demo-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ export function AudioPlayerDemo({
}: AudioPlayerDemoProps) {
const [playlist, setPlaylist] = useState<AudioPlayerAsset[]>([])
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()
Expand All @@ -54,7 +53,7 @@ export function AudioPlayerDemo({
return (
<AudioPlayer
autoLoad={storeHydrated ? !hasPersistedAudioSelection : false}
playlist={playlist}
source={playlist}
>
{children}
</AudioPlayer>
Expand Down
83 changes: 56 additions & 27 deletions apps/www/components/stream-panel/use-stream-panel-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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"

Expand All @@ -37,6 +47,7 @@ export function useStreamPanelSync({
playerType?: StreamPanelPlayerType
} = {}) {
const playbackApi = useMediaFeatureApi<PlaybackStore>(PLAYBACK_FEATURE_KEY)
const events = useMediaEvents<AssetEvents>()
const mediaElement = useMediaStore((state) => state.mediaElement)
const player = usePlayerStore((state) => state.instance)

Expand Down Expand Up @@ -78,6 +89,7 @@ export function useStreamPanelSync({
context.signal,
blenderStreamCache
)
// DEV: The selected asset may change while the Blender stream URL is resolving.
context.signal.throwIfAborted()

await context.loadDefault(
Expand All @@ -86,6 +98,8 @@ export function useStreamPanelSync({
src: stream.playback.hls,
}
)
// DEV: loadDefault is async; avoid adding captions to a superseded player load.
context.signal.throwIfAborted()

try {
await addBlenderCaptions(context.player, stream)
Expand All @@ -112,26 +126,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<Asset>()

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
Expand Down Expand Up @@ -180,10 +199,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(
Expand All @@ -202,7 +221,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")
Expand All @@ -211,7 +233,7 @@ export function useStreamPanelSync({
playbackApi.getState().playback.setError(error)
})
},
[loadPlaylist, playbackApi, playerType, setContentSelection]
[assetOptions, loadSource, playbackApi, playerType, setContentSelection]
)

const restoreContentSelection = useCallback(
Expand All @@ -226,7 +248,7 @@ export function useStreamPanelSync({
)
if (preset) {
abortPlaylistRequest()
loadPlaylist([preset as unknown as Asset])
loadSource(preset as unknown as Asset, { loading: assetOptions })
return
}

Expand All @@ -237,7 +259,8 @@ export function useStreamPanelSync({
[
abortPlaylistRequest,
loadCustomStream,
loadPlaylist,
assetOptions,
loadSource,
loadPlaylistPreset,
playerType,
]
Expand Down Expand Up @@ -277,9 +300,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(
Expand Down
51 changes: 48 additions & 3 deletions apps/www/content/docs/blocks/audio-player.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ status: free
npx shadcn add @limeplay/audio-player
```

## Usage
## Minimal Usage

```tsx
import {
Expand All @@ -32,23 +32,68 @@ const playlist: AudioPlayerAsset[] = [
]

export function Player() {
return <AudioPlayer playlist={playlist} />
return <AudioPlayer source={playlist} />
}
```

Use `source` for media loading. Use `mediaProps` only for native audio attributes.

```tsx
<AudioPlayer
source={playlist}
mediaProps={{ autoPlay: true, loop: false }}
/>
```

## Single Track Usage

Pass one asset object when you do not need a queue.

```tsx
<AudioPlayer
source={{
artistName: "SoundHelix",
id: "soundhelix-1",
poster: "https://placehold.co/160x160/png",
src: "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
title: "SoundHelix Song 1",
}}
/>
Comment thread
Copilot marked this conversation as resolved.
```

## 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
<AudioPlayer
source={{ id: "track-123", title: "Track" }}
loading={{
resolveSource: async ({ asset, signal }) => {
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.
- Fixed timeline with elapsed, duration, hover time, and scrub support.
- 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

Expand Down
Loading
Loading