Skip to content
Open
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
20 changes: 20 additions & 0 deletions apps/docs/content/components/(voice)/audio-player.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The `AudioPlayer` component provides a flexible and customizable audio playback
- Fully composable architecture with granular control components
- ButtonGroup integration for cohesive control layout
- Individual control components (play, seek, volume, etc.)
- Playback speed control with configurable rates
- Flexible layout with customizable control bars
- CSS custom properties for deep theming
- Shadcn/ui Button component styling
Expand Down Expand Up @@ -150,6 +151,25 @@ Seek forward button wrapped in a shadcn Button component.
}}
/>

### `<AudioPlayerPlaybackRateButton />`

Playback speed button, wrapped in a shadcn Button component.

<TypeTable
type={{
rates: {
description: "Playback rates to cycle through.",
type: "ArrayLike<number>",
default: "[0.5, 1, 1.2, 1.5, 1.7, 2]",
},
"...props": {
description:
"Any other props are spread to the MediaPlaybackRateButton component.",
type: 'Omit<React.ComponentProps<typeof MediaPlaybackRateButton>, "rates">',
},
}}
/>

### `<AudioPlayerTimeDisplay />`

Displays the current playback time, wrapped in ButtonGroupText.
Expand Down
63 changes: 63 additions & 0 deletions packages/elements/__tests__/audio-player.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerPlaybackRateButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
Expand Down Expand Up @@ -317,6 +318,60 @@ describe("audioPlayerMuteButton", () => {
});
});

describe("audioPlayerPlaybackRateButton", () => {
it("renders playback rate button", () => {
const { container } = render(<AudioPlayerPlaybackRateButton />);
const button = container.querySelector(
'[data-slot="audio-player-playback-rate-button"]'
);
expect(button).toBeInTheDocument();
});

it("uses default playback rates", () => {
const { container } = render(<AudioPlayerPlaybackRateButton />);
const button = container.querySelector(
'[data-slot="audio-player-playback-rate-button"]'
);
expect(button).toHaveAttribute("rates", "0.5 1 1.2 1.5 1.7 2");
});

it("accepts custom playback rates", () => {
const { container } = render(
<AudioPlayerPlaybackRateButton rates={[0.75, 1, 1.25]} />
);
const button = container.querySelector(
'[data-slot="audio-player-playback-rate-button"]'
);
expect(button).toHaveAttribute("rates", "0.75 1 1.25");
});

it("applies custom className", () => {
const { container } = render(
<AudioPlayerPlaybackRateButton className="custom-rate" />
);
const button = container.querySelector(
'[data-slot="audio-player-playback-rate-button"]'
);
expect(button).toHaveClass("custom-rate");
});

it("uses a wider text-friendly button size", () => {
const { container } = render(<AudioPlayerPlaybackRateButton />);
const button = container.querySelector(
'[data-slot="audio-player-playback-rate-button"]'
);
expect(button).toHaveClass("w-14");
});

it("prevents selecting playback rate text", () => {
const { container } = render(<AudioPlayerPlaybackRateButton />);
const button = container.querySelector(
'[data-slot="audio-player-playback-rate-button"]'
);
expect(button).toHaveClass("select-none");
});
});

describe("audioPlayerVolumeRange", () => {
it("renders volume range slider", () => {
const { container } = render(<AudioPlayerVolumeRange />);
Expand Down Expand Up @@ -348,6 +403,7 @@ const renderCompleteAudioPlayer = () =>
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerPlaybackRateButton />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
Expand Down Expand Up @@ -408,6 +464,13 @@ describe("integration tests", () => {
).toBeInTheDocument();
});

it("renders playback rate button", () => {
const { container } = renderCompleteAudioPlayer();
expect(
container.querySelector('[data-slot="audio-player-playback-rate-button"]')
).toBeInTheDocument();
});

it("handles AI SDK speech result data format", () => {
const mockSpeechData = {
base64: "dGVzdA==",
Expand Down
25 changes: 25 additions & 0 deletions packages/elements/src/audio-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
MediaDurationDisplay,
MediaMuteButton,
MediaPlayButton,
MediaPlaybackRateButton,
MediaSeekBackwardButton,
MediaSeekForwardButton,
MediaTimeDisplay,
Expand Down Expand Up @@ -198,6 +199,30 @@ export const AudioPlayerDurationDisplay = ({
</ButtonGroupText>
);

export type AudioPlayerPlaybackRateButtonProps = Omit<
ComponentProps<typeof MediaPlaybackRateButton>,
"rates"
> & {
rates?: ArrayLike<number>;
};

const DEFAULT_PLAYBACK_RATES = [0.5, 1, 1.2, 1.5, 1.7, 2];

export const AudioPlayerPlaybackRateButton = ({
className,
rates = DEFAULT_PLAYBACK_RATES,
...props
}: AudioPlayerPlaybackRateButtonProps) => (
<Button asChild size="sm" variant="outline">
<MediaPlaybackRateButton
className={cn("w-14 select-none bg-transparent px-2 tabular-nums", className)}
data-slot="audio-player-playback-rate-button"
rates={rates}
{...props}
/>
</Button>
);

export type AudioPlayerMuteButtonProps = ComponentProps<typeof MediaMuteButton>;

export const AudioPlayerMuteButton = ({
Expand Down
2 changes: 2 additions & 0 deletions packages/examples/src/audio-player-remote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerPlaybackRateButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
Expand All @@ -25,6 +26,7 @@ const Example = () => (
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerPlaybackRateButton />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
Expand Down
2 changes: 2 additions & 0 deletions packages/examples/src/audio-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerPlaybackRateButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
Expand Down Expand Up @@ -57,6 +58,7 @@ const Example = () => {
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerPlaybackRateButton />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
Expand Down
10 changes: 10 additions & 0 deletions skills/ai-elements/references/audio-player.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ npx ai-elements@latest add audio-player
- Fully composable architecture with granular control components
- ButtonGroup integration for cohesive control layout
- Individual control components (play, seek, volume, etc.)
- Playback speed control with configurable rates
- Flexible layout with customizable control bars
- CSS custom properties for deep theming
- Shadcn/ui Button component styling
Expand Down Expand Up @@ -93,6 +94,15 @@ Seek forward button wrapped in a shadcn Button component.
| `seekOffset` | `number` | `10` | The number of seconds to seek forward. |
| `...props` | `React.ComponentProps<typeof MediaSeekForwardButton>` | - | Any other props are spread to the MediaSeekForwardButton component. |

### `<AudioPlayerPlaybackRateButton />`

Playback speed button, wrapped in a shadcn Button component.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `rates` | `ArrayLike<number>` | `[0.5, 1, 1.2, 1.5, 1.7, 2]` | Playback rates to cycle through. |
| `...props` | `Omit<React.ComponentProps<typeof MediaPlaybackRateButton>, "rates">` | - | Any other props are spread to the MediaPlaybackRateButton component. |

### `<AudioPlayerTimeDisplay />`

Displays the current playback time, wrapped in ButtonGroupText.
Expand Down
2 changes: 2 additions & 0 deletions skills/ai-elements/scripts/audio-player-remote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerPlaybackRateButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
Expand All @@ -25,6 +26,7 @@ const Example = () => (
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerPlaybackRateButton />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
Expand Down
2 changes: 2 additions & 0 deletions skills/ai-elements/scripts/audio-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
AudioPlayerElement,
AudioPlayerMuteButton,
AudioPlayerPlayButton,
AudioPlayerPlaybackRateButton,
AudioPlayerSeekBackwardButton,
AudioPlayerSeekForwardButton,
AudioPlayerTimeDisplay,
Expand Down Expand Up @@ -57,6 +58,7 @@ const Example = () => {
<AudioPlayerTimeDisplay />
<AudioPlayerTimeRange />
<AudioPlayerDurationDisplay />
<AudioPlayerPlaybackRateButton />
<AudioPlayerMuteButton />
<AudioPlayerVolumeRange />
</AudioPlayerControlBar>
Expand Down