import BaseComponent, { BaseProps } from '../_base/baseComponent'; import React from 'react'; import cls from 'classnames'; import '@douyinfe/semi-foundation/audioPlayer/audioPlayer.scss'; import { cssClasses } from '@douyinfe/semi-foundation/audioPlayer/constants'; import Button from '../button'; import Dropdown from '../dropdown'; import Image from '../image'; import Tooltip from '../tooltip'; import Popover from '../popover'; import { IconAlertCircle, IconBackward, IconFastForward, IconPause, IconPlay, IconRefresh, IconRestart, IconVolume2, IconVolumnSilent } from '@douyinfe/semi-icons'; import AudioSlider from './audioSlider'; import AudioPlayerFoundation from '@douyinfe/semi-foundation/audioPlayer/foundation'; import { AudioPlayerAdapter } from '@douyinfe/semi-foundation/audioPlayer/foundation'; import { formatTime } from './utils'; type AudioSrc = string type AudioInfo = { title?: string; cover?: string; src: string } type AudioUrlArray = (AudioInfo | string)[]; type AudioUrl = AudioSrc | AudioInfo | AudioUrlArray export type AudioPlayerTheme = 'dark' | 'light' export interface AudioPlayerProps extends BaseProps { audioUrl: AudioUrl; autoPlay: boolean; showToolbar?: boolean; skipDuration?: number; theme?: AudioPlayerTheme; className?: string; style?: React.CSSProperties } export interface AudioPlayerState { isPlaying: boolean; currentIndex: number; totalTime: number; currentTime: number; currentRate: { label: string; value: number }; volume: number; error: boolean } const prefixCls = cssClasses.PREFIX; class AudioPlayer extends BaseComponent { audioRef = React.createRef(); static defaultProps: Partial = { autoPlay: false, showToolbar: true, skipDuration: 10, theme: 'dark', }; rateOptions = [ { label: '0.5x', value: 0.5 }, { label: '0.75x', value: 0.75 }, { label: '1.0x', value: 1 }, { label: '1.5x', value: 1.5 }, { label: '2.0x', value: 2 }, ]; foundation!: AudioPlayerFoundation; constructor(props: AudioPlayerProps) { super(props); this.state = { isPlaying: false, currentIndex: 0, totalTime: 0, currentTime: 0, currentRate: { label: '1.0x', value: 1 }, volume: 100, error: false, }; this.audioRef = React.createRef(); this.foundation = new AudioPlayerFoundation(this.adapter); } get adapter(): AudioPlayerAdapter { return { ...super.adapter, init: () => { if (this.audioRef.current) { this.audioRef.current.addEventListener('loadedmetadata', () => { this.foundation.initAudioState(); }); this.audioRef.current.addEventListener('error', () => { this.foundation.errorHandler(); }); this.audioRef.current.addEventListener('ended', () => { this.foundation.endHandler(); }); } }, destroy: () => { if (this.audioRef.current) { this.audioRef.current.removeEventListener('loadedmetadata', () => { this.foundation.initAudioState(); }); this.audioRef.current.removeEventListener('error', () => { this.foundation.errorHandler(); }); this.audioRef.current.removeEventListener('ended', () => { this.foundation.endHandler(); }); } }, handleStatusClick: () => { if (!this.audioRef.current) return; if (this.state.isPlaying) { this.audioRef.current.pause(); } else { this.audioRef.current.play(); } this.setState({ isPlaying: !this.state.isPlaying, }); }, getAudioRef: () => this.audioRef.current, resetAudioState: () => { this.setState({ isPlaying: true, currentTime: 0, currentRate: { label: '1.0x', value: 1 } }, () => { if (this.audioRef.current) { this.audioRef.current.currentTime = this.state.currentTime; this.audioRef.current.playbackRate = this.state.currentRate.value; this.audioRef.current.play(); } }); }, handleTimeUpdate: () => { if (!this.audioRef.current) return; this.setState({ currentTime: this.audioRef.current.currentTime, }); }, handleTrackChange: (direction: 'next' | 'prev') => { if (!this.audioRef.current) return; const { audioUrl } = this.props as AudioPlayerProps; const isAudioUrlArray = Array.isArray(audioUrl); if (isAudioUrlArray) { if (direction === 'next') { this.setState({ currentIndex: (this.state.currentIndex + 1) % audioUrl.length, error: false, }); } else { this.setState({ currentIndex: (this.state.currentIndex - 1 + audioUrl.length) % audioUrl.length, error: false, }); } } this.foundation.resetAudioState(); }, handleTimeChange: (value: number) => { if (!this.audioRef.current) return; this.audioRef.current.currentTime = value; this.setState({ currentTime: value, }); }, handleRefresh: () => { if (!this.audioRef.current) return; if (this.state.error) { this.audioRef.current.load(); } else { this.audioRef.current.currentTime = 0; this.setState({ currentTime: 0, }); } }, handleSpeedChange: (value: { label: string; value: number }) => { if (!this.audioRef.current) return; this.audioRef.current.playbackRate = value.value; this.setState({ currentRate: value, }); }, handleSeek: (direction: number) => { if (!this.audioRef.current) return; const { skipDuration = 10 } = this.props; const newTime = Math.min( Math.max(this.audioRef.current.currentTime + (direction * skipDuration), 0), this.audioRef.current.duration ); this.audioRef.current.currentTime = newTime; }, handleVolumeChange: (value: number) => { if (!this.audioRef.current) return; const volume = Math.floor(value); this.audioRef.current.volume = volume / 100; this.setState({ volume: volume, }); }, }; } componentDidMount() { this.foundation.init(); } componentWillUnmount() { this.foundation.destroy(); } handleStatusClick = () => { this.foundation.handleStatusClick(); } handleTrackChange = (direction: 'next' | 'prev') => { this.foundation.handleTrackChange(direction); } handleTimeChange = (value: number) => { this.foundation.handleTimeChange(value); } handleRefresh = () => { this.foundation.handleRefresh(); } handleSpeedChange = (value: { label: string; value: number }) => { this.foundation.handleSpeedChange(value); } handleSeek = (direction: number) => { this.foundation.handleSeek(direction); } handleTimeUpdate = () => { this.foundation.handleTimeUpdate(); } handleVolumeChange = (value: number) => { this.foundation.handleVolumeChange(value); } handleVolumeSilent = () => { if (!this.audioRef.current) return; this.audioRef.current.volume = this.state.volume === 0 ? 0.5 : 0; this.setState({ volume: this.state.volume === 0 ? 50 : 0, }); } getAudioInfo = (audioUrl: AudioUrl) => { const isAudioUrlArray = Array.isArray(audioUrl); if (isAudioUrlArray) { const audioInfo = audioUrl[this.state.currentIndex]; if (typeof audioInfo === 'string') { return { src: audioInfo, audioTitle: null, audioCover: null }; } else { return { src: audioInfo.src, audioTitle: audioInfo.title, audioCover: audioInfo.cover }; } } else if (typeof audioUrl === 'string') { return { src: audioUrl, audioTitle: null, audioCover: null }; } else { return { src: audioUrl.src, audioTitle: audioUrl.title, audioCover: audioUrl.cover }; } } renderControl = () => { const { error } = this.state; const isAudioUrlArray = Array.isArray(this.props.audioUrl); const iconClass = cls(`${prefixCls}-control-button-icon`); const circleStyle = { borderRadius: '50%', }; const transparentStyle = { background: 'transparent', }; const playStyle = { marginLeft: '1px', }; return
{isAudioUrlArray &&
; } renderInfo = () => { const { audioTitle, audioCover } = this.getAudioInfo(this.props.audioUrl); const { theme } = this.props; const { currentTime, totalTime, error } = this.state; return
{audioCover && }
{audioTitle &&
{audioTitle}{error && this.renderError()}
} {!error &&
{formatTime(currentTime)}
{formatTime(totalTime)}
}
; } renderToolbar = () => { const { volume, error } = this.state; const { skipDuration = 10, theme = 'dark' } = this.props; const iconClass = cls(`${prefixCls}-control-button-icon`); const transparentStyle = { background: 'transparent', }; const isVolumeSilent = volume === 0; return !error ? (
{volume}%
}>