@douyinfe/semi-ui
Version:
A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.
474 lines • 15.8 kB
JavaScript
import BaseComponent from '../_base/baseComponent';
import React from 'react';
import cls from 'classnames';
import '@douyinfe/semi-foundation/lib/es/audioPlayer/audioPlayer.css';
import { cssClasses } from '@douyinfe/semi-foundation/lib/es/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/lib/es/audioPlayer/foundation';
import { formatTime } from './utils';
const prefixCls = cssClasses.PREFIX;
class AudioPlayer extends BaseComponent {
constructor(props) {
super(props);
this.audioRef = /*#__PURE__*/React.createRef();
this.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
}];
this.handleStatusClick = () => {
this.foundation.handleStatusClick();
};
this.handleTrackChange = direction => {
this.foundation.handleTrackChange(direction);
};
this.handleTimeChange = value => {
this.foundation.handleTimeChange(value);
};
this.handleRefresh = () => {
this.foundation.handleRefresh();
};
this.handleSpeedChange = value => {
this.foundation.handleSpeedChange(value);
};
this.handleSeek = direction => {
this.foundation.handleSeek(direction);
};
this.handleTimeUpdate = () => {
this.foundation.handleTimeUpdate();
};
this.handleVolumeChange = value => {
this.foundation.handleVolumeChange(value);
};
this.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
});
};
this.getAudioInfo = 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
};
}
};
this.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 /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-control`)
}, isAudioUrlArray && /*#__PURE__*/React.createElement(Tooltip, {
content: 'Previous',
autoAdjustOverflow: true,
showArrow: false
}, /*#__PURE__*/React.createElement(Button, {
style: Object.assign(Object.assign({}, circleStyle), transparentStyle),
size: 'large',
icon: /*#__PURE__*/React.createElement(IconRestart, {
size: 'large',
className: iconClass
}),
onClick: () => this.handleTrackChange('prev')
})), /*#__PURE__*/React.createElement(Button, {
style: circleStyle,
size: 'large',
disabled: error,
onClick: this.handleStatusClick,
icon: this.state.isPlaying ? /*#__PURE__*/React.createElement(IconPause, {
size: 'large'
}) : /*#__PURE__*/React.createElement(IconPlay, {
style: playStyle,
size: 'large'
}),
className: cls(`${cssClasses.PREFIX}-control-button-play`, {
[`${cssClasses.PREFIX}-control-button-play-disabled`]: error
})
}), isAudioUrlArray && /*#__PURE__*/React.createElement(Tooltip, {
content: 'Next',
autoAdjustOverflow: true,
showArrow: false
}, /*#__PURE__*/React.createElement(Button, {
style: Object.assign(Object.assign({}, circleStyle), transparentStyle),
size: 'large',
icon: /*#__PURE__*/React.createElement(IconRestart, {
size: 'large',
rotate: 180,
className: iconClass
}),
onClick: () => this.handleTrackChange('next')
})));
};
this.renderInfo = () => {
const {
audioTitle,
audioCover
} = this.getAudioInfo(this.props.audioUrl);
const {
theme
} = this.props;
const {
currentTime,
totalTime,
error
} = this.state;
return /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-info-container`)
}, audioCover && /*#__PURE__*/React.createElement(Image, {
src: audioCover,
width: 50,
height: 50
}), /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-info`)
}, audioTitle && /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-info-title`)
}, audioTitle, error && this.renderError()), !error && /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-info-time`)
}, /*#__PURE__*/React.createElement("span", {
style: {
width: '38px'
}
}, formatTime(currentTime)), /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-slider-container`)
}, /*#__PURE__*/React.createElement(AudioSlider, {
value: currentTime,
max: totalTime,
theme: theme,
onChange: this.handleTimeChange
})), /*#__PURE__*/React.createElement("span", {
style: {
width: '38px'
}
}, formatTime(totalTime)))));
};
this.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 ? (/*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-control`)
}, /*#__PURE__*/React.createElement(Popover, {
autoAdjustOverflow: true,
content: /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-control-volume`)
}, /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-control-volume-title`)
}, volume, "%"), /*#__PURE__*/React.createElement(AudioSlider, {
value: volume,
max: 100,
vertical: true,
height: 120,
theme: theme,
showTooltip: false,
onChange: this.handleVolumeChange
}))
}, /*#__PURE__*/React.createElement(Button, {
style: transparentStyle,
icon: !isVolumeSilent ? /*#__PURE__*/React.createElement(IconVolume2, {
className: iconClass
}) : /*#__PURE__*/React.createElement(IconVolumnSilent, {
className: iconClass
}),
onClick: this.handleVolumeSilent
})), /*#__PURE__*/React.createElement(Tooltip, {
content: `Backward ${skipDuration}s`,
autoAdjustOverflow: true,
showArrow: false
}, /*#__PURE__*/React.createElement(Button, {
style: transparentStyle,
icon: /*#__PURE__*/React.createElement(IconBackward, {
className: iconClass
}),
onClick: () => this.handleSeek(-1)
})), /*#__PURE__*/React.createElement(Tooltip, {
content: `Forward ${skipDuration}s`,
autoAdjustOverflow: true,
showArrow: false
}, /*#__PURE__*/React.createElement(Button, {
style: transparentStyle,
icon: /*#__PURE__*/React.createElement(IconFastForward, {
className: iconClass
}),
onClick: () => this.handleSeek(1)
})), /*#__PURE__*/React.createElement(Dropdown, {
className: cls(`${prefixCls}-control-speed-menu`),
render: /*#__PURE__*/React.createElement(Dropdown.Menu, null, this.rateOptions.map(option => (/*#__PURE__*/React.createElement(Dropdown.Item, {
className: cls(`${prefixCls}-control-speed-menu-item`),
key: option.value,
onClick: () => this.handleSpeedChange(option),
active: option.value === this.state.currentRate.value
}, option.label)))),
onChange: this.handleSpeedChange
}, /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-control-speed`)
}, /*#__PURE__*/React.createElement("span", null, this.state.currentRate.label))), /*#__PURE__*/React.createElement(Button, {
onClick: () => this.handleRefresh(),
style: transparentStyle,
icon: /*#__PURE__*/React.createElement(IconRefresh, {
style: {
transform: 'rotateY(180deg)'
},
className: iconClass
})
}))) : (/*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-control`)
}, /*#__PURE__*/React.createElement(Button, {
onClick: () => this.handleRefresh(),
style: transparentStyle,
icon: /*#__PURE__*/React.createElement(IconRefresh, {
style: {
transform: 'rotateY(180deg)'
},
className: iconClass
})
})));
};
this.renderError = () => /*#__PURE__*/React.createElement("div", {
className: cls(`${prefixCls}-error`)
}, /*#__PURE__*/React.createElement(IconAlertCircle, {
size: 'large'
}), "\u97F3\u9891\u52A0\u8F7D\u5931\u8D25");
this.state = {
isPlaying: false,
currentIndex: 0,
totalTime: 0,
currentTime: 0,
currentRate: {
label: '1.0x',
value: 1
},
volume: 100,
error: false
};
this.audioRef = /*#__PURE__*/React.createRef();
this.foundation = new AudioPlayerFoundation(this.adapter);
}
get adapter() {
return Object.assign(Object.assign({}, 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 => {
if (!this.audioRef.current) return;
const {
audioUrl
} = this.props;
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 => {
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 => {
if (!this.audioRef.current) return;
this.audioRef.current.playbackRate = value.value;
this.setState({
currentRate: value
});
},
handleSeek: direction => {
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 => {
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();
}
render() {
const {
audioUrl,
autoPlay,
className,
style,
showToolbar = true,
theme = 'dark'
} = this.props;
const src = this.getAudioInfo(audioUrl).src;
return /*#__PURE__*/React.createElement("div", {
className: cls(prefixCls, className, `${prefixCls}-${theme}`),
style: style
}, /*#__PURE__*/React.createElement("audio", {
src: src,
autoPlay: autoPlay,
className: cls(prefixCls, className),
style: style,
ref: this.audioRef,
onTimeUpdate: this.handleTimeUpdate
}, /*#__PURE__*/React.createElement("track", {
kind: "captions",
src: src
})), this.renderControl(), this.renderInfo(), showToolbar && this.renderToolbar());
}
}
AudioPlayer.defaultProps = {
autoPlay: false,
showToolbar: true,
skipDuration: 10,
theme: 'dark'
};
export default AudioPlayer;