Skip to content
Closed
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
32 changes: 11 additions & 21 deletions apps/www/app/(home)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Script from "next/script"

import { Footer } from "@/components/footer"
import { Header } from "@/components/header"
import { VideoBackground } from "@/components/video-background"
Expand All @@ -10,24 +8,16 @@ export default function RootLayout({
children: React.ReactNode
}>) {
return (
<>
{process.env.NODE_ENV === "development" && (
<Script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
/>
)}
<main
className={`
light w-dvw scrollbar-gutter-auto overscroll-contain bg-linear-to-br from-white to-neutral-200
md:scrollbar-gutter-stable
`}
>
<VideoBackground />
<Header />
{children}
<Footer />
</main>
</>
<main
className={`
light w-dvw scrollbar-gutter-auto overscroll-contain bg-linear-to-br from-white to-neutral-200
md:scrollbar-gutter-stable
`}
>
<VideoBackground />
<Header />
{children}
<Footer />
</main>
)
}
8 changes: 8 additions & 0 deletions apps/www/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { UserJotProvider } from "@userjot/next"
import { Analytics } from "@vercel/analytics/react"
import { SpeedInsights } from "@vercel/speed-insights/next"
import { Inter } from "next/font/google"
import Script from "next/script"

import { JsonLd } from "@/components/json-ld"
import {
Expand Down Expand Up @@ -90,6 +91,13 @@ export default function Layout({ children }: { children: ReactNode }) {
type="text/markdown"
/>
<UserJotProvider projectId="cmjs634l4043b15ldylgedgwi" />
{process.env.NODE_ENV === "development" && (
<Script
Comment thread
WINOFFRG marked this conversation as resolved.
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
strategy="beforeInteractive"
/>
)}
Comment thread
WINOFFRG marked this conversation as resolved.
</head>
<body className="antialiased">
<JsonLd />
Expand Down
2 changes: 2 additions & 0 deletions apps/www/content/docs/blocks/audio-player.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,15 @@ The audio block includes an opinionated default resolver for `src` and `playback
- 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.
- Browser Media Session metadata, playback state, position, and media-key handlers.
- 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.
- Do not pass `mediaProps.src`; pass `source` instead.
- Media Session metadata is derived from the active track title, artist, album, and poster.
- For the full block loading model, see [Usage](/docs/usage).

## API Reference
Expand Down
2 changes: 2 additions & 0 deletions apps/www/content/docs/blocks/video-player.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,15 @@ Use `loading.resolveSource` when your app needs signed URLs, token refresh, or s
- 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.
- Browser Media Session metadata, playback state, position, and media-key handlers.
- 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`.
- Do not pass `mediaProps.src`; pass `source` instead.
- Media Session metadata is derived from the active asset `title`, `description`, and `poster`.
- For the full block loading model, see [Usage](/docs/usage).

## API Reference
Expand Down
1 change: 1 addition & 0 deletions apps/www/content/docs/hooks/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Standalone hooks that don't require feature registration:
| Hook | Description |
| ---------------------------------------------------------------- | ------------------------------------------- |
| [`use-seek`](/docs/hooks/use-seek) | Seek by offset (±N seconds) |
| [`use-media-session`](/docs/hooks/use-media-session) | Sync browser Media Session metadata and controls |
| [`use-controls-visibility`](/docs/hooks/use-controls-visibility) | Auto-hide controls based on player activity |
| [`use-idle`](/docs/hooks/use-idle) | Detect user inactivity |
| [`use-interval`](/docs/hooks/use-interval) | `setInterval` with auto-cleanup |
Expand Down
1 change: 1 addition & 0 deletions apps/www/content/docs/hooks/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"use-playlist",
"use-playback-rate",
"use-picture-in-picture",
"use-media-session",
"use-captions",
"use-seek",
"use-controls-visibility",
Expand Down
200 changes: 200 additions & 0 deletions apps/www/content/docs/hooks/use-media-session.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
---
title: use-media-session
description: Utility hook for synchronizing browser Media Session metadata, playback state, position, and action handlers.
---

## Installation

```npm
npx shadcn add @limeplay/use-media-session
```

## Feature Registration

`use-media-session` is a utility hook, not a media feature. It does not need to
be registered in `createMediaKit`.

Use it inside a client component that can read your current media state:

```tsx title="components/player/media-session.tsx"
"use client"

import * as React from "react"

import { useMediaSession } from "@/hooks/limeplay/use-media-session"

export function MediaSessionController() {
const mediaSession = useMediaSession()

React.useEffect(() => {
mediaSession.setMetadata({
artist: "Creator",
artwork: [{ sizes: "512x512", src: "/poster.jpg" }],
title: "Current asset",
})

return () => mediaSession.clearMetadata()
}, [mediaSession])

return null
}
```

## Store

`useMediaSession` does not create a store slice. It wraps
`navigator.mediaSession` and no-ops safely when the browser does not support the
API.

### State

| Field | Type | Description |
| ----------- | --------- | --------------------------------------------------------- |
| `supported` | `boolean` | Whether `navigator.mediaSession` is available. |
| Metadata | Browser | Stored on `navigator.mediaSession.metadata`. |
| Position | Browser | Stored through `navigator.mediaSession.setPositionState`. |

### Actions

| Method | Description |
| ----------------------------------- | ----------------------------------------------------------------- |
| `setMetadata(metadata)` | Sets browser media metadata using `MediaMetadata`. |
| `clearMetadata()` | Clears browser media metadata. |
| `setPlaybackState(state)` | Sets browser playback state to `none`, `paused`, or `playing`. |
| `setPositionState(state)` | Sets duration, playback rate, and position when values are valid. |
| `clearPositionState()` | Clears browser position state. |
| `setActionHandler(action, handler)` | Registers a browser media action handler and returns a cleanup. |

## Sync Hook

Use `useMediaSessionSync` when a component can derive the current metadata,
playback state, position, and handlers from existing player stores.
Pass playback `status` to `getMediaSessionPlaybackState` so loading and buffering
are reported as paused while the current position remains explicitly published.
This prevents the platform from falling back to an independently advancing
media-element clock. Guard `onPlay` with `canStartMediaSessionPlayback` so
platform controls can retry loading or buffering media, but cannot start before
initialization or from an unrecovered error state.

```tsx title="components/player/media-session.tsx"
import {
canStartMediaSessionPlayback,
getMediaSessionPlaybackState,
getMediaSessionPositionState,
useMediaSessionActionHandlers,
useMediaSessionSync,
} from "@/hooks/limeplay/use-media-session"

interface Asset {
creator?: string
poster?: string
title?: string
}

interface CustomMediaSessionControllerProps {
asset: Asset | null
currentTime: number
duration: number
onPause: () => void
onPlay: () => Promise<void>
onSeek: (time: number) => void
playbackRate: number
status: string
}

function CustomMediaSessionController({
asset,
currentTime,
duration,
onPause,
onPlay,
onSeek,
playbackRate,
status,
}: CustomMediaSessionControllerProps) {
const active = Boolean(asset)
const actions = useMediaSessionActionHandlers({
getCurrentTime: () => currentTime,
onPause,
onPlay: () => {
if (!canStartMediaSessionPlayback(status)) return

return onPlay()
},
onSeek,
})

useMediaSessionSync({
actions,
active,
metadata: asset
? {
artist: asset.creator,
artwork: asset.poster ? [{ src: asset.poster }] : [],
title: asset.title,
}
: null,
playbackState: getMediaSessionPlaybackState({
active,
status,
}),
position: getMediaSessionPositionState({
active,
currentTime,
duration,
playbackRate,
}),
})

return null
}
```

## Action Handlers

`useMediaSessionActionHandlers` builds the common media action map used by the
default blocks.

| Action | Option | Description |
| ------------------------------ | ----------------------------------------------------- | ------------------------------------------------------- |
| `play` | `onPlay` | Starts playback. |
| `pause` | `onPause` | Pauses playback. |
| `seekto` | `onSeek` | Seeks to the requested absolute media time. |
| `seekbackward` / `seekforward` | `getCurrentTime`, `onSeek` | Seeks relative to the current media time. |
| `nexttrack` | `canGoNext`, `onNextTrack` | Moves to the next playlist item when available. |
| `previoustrack` | `canGoPrevious`, `onPreviousTrack` | Moves to the previous playlist item when available. |
| `enterpictureinpicture` | `canEnterPictureInPicture`, `onEnterPictureInPicture` | Lets supported browsers request PiP from Media Session. |
| `skipad` | `onSkipAd` | Handles ad-skip requests when provided. |
| `stop` | `onStop` | Handles stop requests when provided. |

## Events

`use-media-session` does not emit Limeplay media events.

| Event | Payload | When |
| ----- | ------- | --------------------------------------------------------------- |
| None | — | Browser Media Session callbacks call the handlers you register. |

## Browser Behavior

Media Session support varies by browser and action. Unsupported browsers and
unsupported actions no-op safely. The `enterpictureinpicture` action is mainly a
browser integration hook for automatic or browser-initiated Picture-in-Picture;
it is not guaranteed to appear as a visible operating-system media control.

## API Reference

<AutoTypeTable
path="./registry/default/hooks/use-media-session.ts"
name="UseMediaSessionReturn"
/>

<AutoTypeTable
path="./registry/default/hooks/use-media-session.ts"
name="UseMediaSessionSyncOptions"
/>

<AutoTypeTable
path="./registry/default/hooks/use-media-session.ts"
name="UseMediaSessionActionHandlersOptions"
/>
10 changes: 10 additions & 0 deletions apps/www/registry/collection/registry-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export const blocks: Registry["items"] = [
path: `${VIDEO_PLAYER_SRC_URL}/components/bottom-controls.tsx`,
type: "registry:component",
},
{
path: `${VIDEO_PLAYER_SRC_URL}/components/media-session-controller.tsx`,
type: "registry:component",
},
{
path: `${VIDEO_PLAYER_SRC_URL}/components/button.tsx`,
type: "registry:component",
Expand Down Expand Up @@ -133,6 +137,7 @@ export const blocks: Registry["items"] = [
"use-playlist",
"use-asset",
"use-media",
"use-media-session",
"use-controls-visibility",
"use-playback-source",
],
Expand Down Expand Up @@ -186,6 +191,10 @@ export const blocks: Registry["items"] = [
path: "blocks/audio-player/components/fixed-timeline-control.tsx",
type: "registry:component",
},
{
path: "blocks/audio-player/components/media-session-controller.tsx",
type: "registry:component",
},
{
path: "blocks/audio-player/components/track-info.tsx",
type: "registry:component",
Expand Down Expand Up @@ -237,6 +246,7 @@ export const blocks: Registry["items"] = [
"use-asset",
"use-media",
"use-playback-source",
"use-media-session",
"limeplay-logo",
"utils",
],
Expand Down
11 changes: 11 additions & 0 deletions apps/www/registry/collection/registry-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ export const hooks: Registry["items"] = [
registryDependencies: ["media-provider"],
type: "registry:hook",
},
{
files: [
{
path: "hooks/use-media-session.ts",
target: `${TARGET_BASE_PATH}/use-media-session.ts`,
type: "registry:hook",
},
],
name: "use-media-session",
type: "registry:hook",
},
{
dependencies: ["lodash.clamp", "zustand"],
devDependencies: ["@types/lodash.clamp"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,8 @@ export function TimelineControl() {
<TimelineSlider.Thumb
className={cn(
"absolute top-1/2 size-0 -translate-y-1/2 rounded-full bg-rose-600!",
`
transition-[height,width]
group-hover/timeline:size-4
`
"transition-[height,width] duration-150 ease-in-out",
"group-hover/timeline:size-4"
)}
/>

Expand All @@ -56,6 +54,12 @@ export function TimelineControl() {
group-hover/timeline:opacity-100
`}
showWithHover
style={
{
"--lp-timeline-thumb-left":
"clamp(calc((5ch + 1rem) / 2), var(--lp-timeline-thumb-position), calc(100% - calc((5ch + 1rem) / 2)))",
} as unknown as React.CSSProperties
}
>
<HoverTime />
</TimelineSlider.Thumb>
Expand Down
Loading
Loading