diff --git a/apps/docs/content/components/(voice)/audio-player.mdx b/apps/docs/content/components/(voice)/audio-player.mdx index 1a56648b..4b5f8a16 100644 --- a/apps/docs/content/components/(voice)/audio-player.mdx +++ b/apps/docs/content/components/(voice)/audio-player.mdx @@ -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 @@ -150,6 +151,25 @@ Seek forward button wrapped in a shadcn Button component. }} /> +### `` + +Playback speed button, wrapped in a shadcn Button component. + +", + default: "[0.5, 1, 1.2, 1.5, 1.7, 2]", + }, + "...props": { + description: + "Any other props are spread to the MediaPlaybackRateButton component.", + type: 'Omit, "rates">', + }, + }} +/> + ### `` Displays the current playback time, wrapped in ButtonGroupText. diff --git a/packages/elements/__tests__/audio-player.test.tsx b/packages/elements/__tests__/audio-player.test.tsx index fbbf8ff6..dee0c0ab 100644 --- a/packages/elements/__tests__/audio-player.test.tsx +++ b/packages/elements/__tests__/audio-player.test.tsx @@ -7,6 +7,7 @@ import { AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, + AudioPlayerPlaybackRateButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, @@ -317,6 +318,60 @@ describe("audioPlayerMuteButton", () => { }); }); +describe("audioPlayerPlaybackRateButton", () => { + it("renders playback rate button", () => { + const { container } = render(); + const button = container.querySelector( + '[data-slot="audio-player-playback-rate-button"]' + ); + expect(button).toBeInTheDocument(); + }); + + it("uses default playback rates", () => { + const { container } = render(); + 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( + + ); + 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( + + ); + 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(); + 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(); + 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(); @@ -348,6 +403,7 @@ const renderCompleteAudioPlayer = () => + @@ -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==", diff --git a/packages/elements/src/audio-player.tsx b/packages/elements/src/audio-player.tsx index a340e76b..c860dded 100644 --- a/packages/elements/src/audio-player.tsx +++ b/packages/elements/src/audio-player.tsx @@ -13,6 +13,7 @@ import { MediaDurationDisplay, MediaMuteButton, MediaPlayButton, + MediaPlaybackRateButton, MediaSeekBackwardButton, MediaSeekForwardButton, MediaTimeDisplay, @@ -198,6 +199,30 @@ export const AudioPlayerDurationDisplay = ({ ); +export type AudioPlayerPlaybackRateButtonProps = Omit< + ComponentProps, + "rates" +> & { + rates?: ArrayLike; +}; + +const DEFAULT_PLAYBACK_RATES = [0.5, 1, 1.2, 1.5, 1.7, 2]; + +export const AudioPlayerPlaybackRateButton = ({ + className, + rates = DEFAULT_PLAYBACK_RATES, + ...props +}: AudioPlayerPlaybackRateButtonProps) => ( + +); + export type AudioPlayerMuteButtonProps = ComponentProps; export const AudioPlayerMuteButton = ({ diff --git a/packages/examples/src/audio-player-remote.tsx b/packages/examples/src/audio-player-remote.tsx index 4047356c..bf2bdd7e 100644 --- a/packages/examples/src/audio-player-remote.tsx +++ b/packages/examples/src/audio-player-remote.tsx @@ -7,6 +7,7 @@ import { AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, + AudioPlayerPlaybackRateButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, @@ -25,6 +26,7 @@ const Example = () => ( + diff --git a/packages/examples/src/audio-player.tsx b/packages/examples/src/audio-player.tsx index a97ade58..a0c91514 100644 --- a/packages/examples/src/audio-player.tsx +++ b/packages/examples/src/audio-player.tsx @@ -7,6 +7,7 @@ import { AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, + AudioPlayerPlaybackRateButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, @@ -57,6 +58,7 @@ const Example = () => { + diff --git a/skills/ai-elements/references/audio-player.md b/skills/ai-elements/references/audio-player.md index 2bd7b944..936f47f5 100644 --- a/skills/ai-elements/references/audio-player.md +++ b/skills/ai-elements/references/audio-player.md @@ -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 @@ -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` | - | Any other props are spread to the MediaSeekForwardButton component. | +### `` + +Playback speed button, wrapped in a shadcn Button component. + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `rates` | `ArrayLike` | `[0.5, 1, 1.2, 1.5, 1.7, 2]` | Playback rates to cycle through. | +| `...props` | `Omit, "rates">` | - | Any other props are spread to the MediaPlaybackRateButton component. | + ### `` Displays the current playback time, wrapped in ButtonGroupText. diff --git a/skills/ai-elements/scripts/audio-player-remote.tsx b/skills/ai-elements/scripts/audio-player-remote.tsx index 19d8d89d..ba1e8c5a 100644 --- a/skills/ai-elements/scripts/audio-player-remote.tsx +++ b/skills/ai-elements/scripts/audio-player-remote.tsx @@ -7,6 +7,7 @@ import { AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, + AudioPlayerPlaybackRateButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, @@ -25,6 +26,7 @@ const Example = () => ( + diff --git a/skills/ai-elements/scripts/audio-player.tsx b/skills/ai-elements/scripts/audio-player.tsx index bdfd8588..40a0dcc3 100644 --- a/skills/ai-elements/scripts/audio-player.tsx +++ b/skills/ai-elements/scripts/audio-player.tsx @@ -7,6 +7,7 @@ import { AudioPlayerElement, AudioPlayerMuteButton, AudioPlayerPlayButton, + AudioPlayerPlaybackRateButton, AudioPlayerSeekBackwardButton, AudioPlayerSeekForwardButton, AudioPlayerTimeDisplay, @@ -57,6 +58,7 @@ const Example = () => { +