feat: Replace react-h5-audio-player with @arraypress/waveform-player and update AudioPlayer component to support waveform visualization

This commit is contained in:
2026-02-06 18:50:06 +08:00
parent 26e40039a9
commit 8f5cfd8093
10 changed files with 149 additions and 129 deletions

View File

@@ -8,92 +8,58 @@
background: transparent;
}
.audioPlayerWrapper :global(.rhap_container) {
.waveformContainer {
flex: 1;
background-color: transparent;
box-shadow: none;
min-width: 0;
}
.audioPlayerWrapper :global(.waveform-player) {
background: transparent;
border: none;
padding: 0;
}
.audioPlayerWrapper :global(.rhap_main) {
--rhap_theme-color: hsl(var(--primary));
--rhap_background-color: transparent;
--rhap_bar-color: hsl(var(--secondary));
--rhap_time-color: hsl(var(--muted-foreground));
.audioPlayerWrapper :global(.waveform-btn) {
color: hsl(var(--foreground));
border-color: hsl(var(--border));
background: transparent;
transition: all 150ms ease;
}
.audioPlayerWrapper :global(.rhap_progress-indicator),
.audioPlayerWrapper :global(.rhap_volume-indicator) {
background: hsl(var(--primary));
.audioPlayerWrapper :global(.waveform-btn:hover) {
color: hsl(var(--primary));
border-color: hsl(var(--primary));
}
.audioPlayerWrapper :global(.rhap_progress-filled),
.audioPlayerWrapper :global(.rhap_volume-bar) {
background-color: hsl(var(--primary));
}
.audioPlayerWrapper :global(.rhap_progress-bar),
.audioPlayerWrapper :global(.rhap_volume-container) {
background-color: hsl(var(--secondary));
}
.audioPlayerWrapper :global(.rhap_progress-bar) {
height: 6px;
border-radius: 3px;
transition: height 0.15s ease;
}
.audioPlayerWrapper :global(.rhap_progress-bar):hover {
height: 7px;
}
.audioPlayerWrapper :global(.rhap_progress-filled) {
.audioPlayerWrapper :global(.waveform-canvas) {
border-radius: 3px;
}
.audioPlayerWrapper :global(.rhap_progress-indicator) {
width: 14px;
height: 14px;
top: -4px;
margin-left: -7px;
transition: transform 0.15s ease, box-shadow 0.15s ease;
.audioPlayerWrapper :global(.waveform-info) {
position: relative;
justify-content: center;
}
.audioPlayerWrapper :global(.rhap_progress-indicator):hover {
transform: scale(1.1);
.audioPlayerWrapper :global(.waveform-text) {
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.audioPlayerWrapper :global(.rhap_progress-container) {
margin: 0 0.5rem;
.audioPlayerWrapper :global(.waveform-title) {
text-align: center;
font-size: 0.8125rem;
}
.audioPlayerWrapper :global(.rhap_horizontal .rhap_controls-section) {
margin-left: 0;
}
.audioPlayerWrapper :global(.rhap_time) {
.audioPlayerWrapper :global(.waveform-time) {
color: hsl(var(--muted-foreground));
font-size: 0.875rem;
font-weight: 500;
}
.audioPlayerWrapper :global(.rhap_button-clear) {
color: hsl(var(--foreground));
font-size: 1.25rem;
}
.audioPlayerWrapper :global(.rhap_button-clear):hover {
color: hsl(var(--primary));
}
.audioPlayerWrapper :global(.rhap_main-controls-button) {
width: 40px;
height: 40px;
}
.audioPlayerWrapper :global(.rhap_main-controls-button svg) {
width: 22px;
height: 22px;
position: absolute;
right: 0;
}
.downloadButton {

View File

@@ -1,7 +1,7 @@
import { useRef, useState, useEffect, useCallback, memo } from 'react'
import { useTranslation } from 'react-i18next'
import AudioPlayerLib from 'react-h5-audio-player'
import 'react-h5-audio-player/lib/styles.css'
import WaveformPlayer from '@arraypress/waveform-player'
import '@arraypress/waveform-player/dist/waveform-player.css'
import { Button } from '@/components/ui/button'
import { Download } from 'lucide-react'
import apiClient from '@/lib/api'
@@ -10,14 +10,17 @@ import styles from './AudioPlayer.module.css'
interface AudioPlayerProps {
audioUrl: string
jobId: number
text?: string
}
const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
const AudioPlayer = memo(({ audioUrl, jobId, text }: AudioPlayerProps) => {
const { t } = useTranslation('common')
const [blobUrl, setBlobUrl] = useState<string>('')
const [isLoading, setIsLoading] = useState(false)
const [loadError, setLoadError] = useState<string | null>(null)
const previousAudioUrlRef = useRef<string>('')
const containerRef = useRef<HTMLDivElement>(null)
const playerInstanceRef = useRef<WaveformPlayer | null>(null)
useEffect(() => {
if (!audioUrl || audioUrl === previousAudioUrlRef.current) return
@@ -65,6 +68,49 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
}
}, [])
useEffect(() => {
if (!containerRef.current || !blobUrl) return
const truncateText = (str: string, maxLength: number = 30) => {
if (!str) return ''
return str.length > maxLength ? str.substring(0, maxLength) + '...' : str
}
const player = new WaveformPlayer(containerRef.current, {
url: blobUrl,
waveformStyle: 'mirror',
height: 60,
barWidth: 3,
barSpacing: 1,
samples: 200,
showTime: true,
showPlaybackSpeed: false,
autoplay: false,
enableMediaSession: true,
title: text ? truncateText(text) : undefined,
})
playerInstanceRef.current = player
setTimeout(() => {
if (containerRef.current) {
const buttons = containerRef.current.querySelectorAll('button')
buttons.forEach(btn => {
if (!btn.hasAttribute('type')) {
btn.setAttribute('type', 'button')
}
})
}
}, 0)
return () => {
if (playerInstanceRef.current) {
playerInstanceRef.current.destroy()
playerInstanceRef.current = null
}
}
}, [blobUrl, text])
const handleDownload = useCallback(() => {
const link = document.createElement('a')
link.href = blobUrl || audioUrl
@@ -94,27 +140,16 @@ const AudioPlayer = memo(({ audioUrl, jobId }: AudioPlayerProps) => {
return (
<div className={styles.audioPlayerWrapper}>
<AudioPlayerLib
src={blobUrl}
layout="horizontal"
customAdditionalControls={[
<Button
key="download"
type="button"
variant="ghost"
size="icon"
onClick={handleDownload}
className={styles.downloadButton}
>
<Download className="h-4 w-4" />
</Button>
]}
customVolumeControls={[]}
showJumpControls={false}
volume={1}
preload="metadata"
autoPlayAfterSrcChange={false}
/>
<div ref={containerRef} className={styles.waveformContainer} />
<Button
type="button"
variant="ghost"
size="icon"
onClick={handleDownload}
className={styles.downloadButton}
>
<Download className="h-4 w-4" />
</Button>
</div>
)
})

View File

@@ -219,7 +219,7 @@ const JobDetailDialog = memo(({ job, open, onOpenChange }: JobDetailDialogProps)
<Separator />
<div className="space-y-2">
<h3 className="font-semibold text-sm">{t('job:audioPlayback')}</h3>
<AudioPlayer audioUrl={audioUrl} jobId={job.id} />
<AudioPlayer audioUrl={audioUrl} jobId={job.id} text={job.parameters?.text} />
</div>
</>
)}

View File

@@ -542,6 +542,7 @@ const CustomVoiceForm = forwardRef<CustomVoiceFormHandle>((_props, ref) => {
<AudioPlayer
audioUrl={memoizedAudioUrl}
jobId={currentJob.id}
text={currentJob.parameters?.text}
/>
</div>
)}

View File

@@ -422,6 +422,7 @@ function VoiceCloneForm() {
<AudioPlayer
audioUrl={memoizedAudioUrl}
jobId={currentJob.id}
text={currentJob.parameters?.text}
/>
</div>
)}

View File

@@ -457,6 +457,7 @@ const VoiceDesignForm = forwardRef<VoiceDesignFormHandle>((_props, ref) => {
<AudioPlayer
audioUrl={memoizedAudioUrl}
jobId={currentJob.id}
text={currentJob.parameters?.text}
/>
<Button
type="button"

View File

@@ -0,0 +1,35 @@
declare module '@arraypress/waveform-player' {
export interface WaveformPlayerOptions {
url?: string
waveformStyle?: 'bars' | 'mirror' | 'line' | 'blocks' | 'dots' | 'seekbar'
height?: number
barWidth?: number
barSpacing?: number
samples?: number
waveformColor?: string
progressColor?: string
buttonColor?: string
showTime?: boolean
showPlaybackSpeed?: boolean
playbackRate?: number
autoplay?: boolean
enableMediaSession?: boolean
title?: string
subtitle?: string
onLoad?: (player: WaveformPlayer) => void
onPlay?: (player: WaveformPlayer) => void
onPause?: (player: WaveformPlayer) => void
onEnd?: (player: WaveformPlayer) => void
onTimeUpdate?: (current: number, total: number, player: WaveformPlayer) => void
}
export default class WaveformPlayer {
constructor(container: HTMLElement, options?: WaveformPlayerOptions)
play(): void
pause(): void
togglePlay(): void
seekTo(seconds: number): void
setVolume(level: number): void
destroy(): void
}
}