react-h5-audio-player
Version:
A customizable React audio player. Written in TypeScript. Mobile compatible. Keyboard friendly
509 lines (508 loc) • 19 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "RHAP_UI", {
enumerable: true,
get: function () {
return _constants.RHAP_UI;
}
});
exports.default = void 0;
var _react = _interopRequireWildcard(require("react"));
var _react2 = require("@iconify/react");
var _ProgressBar = _interopRequireDefault(require("./ProgressBar"));
var _CurrentTime = _interopRequireDefault(require("./CurrentTime"));
var _Duration = _interopRequireDefault(require("./Duration"));
var _VolumeBar = _interopRequireDefault(require("./VolumeBar"));
var _constants = require("./constants");
var _utils = require("./utils");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
class H5AudioPlayer extends _react.Component {
static defaultI18nAriaLabels = {
player: 'Audio player',
progressControl: 'Audio progress control',
volumeControl: 'Volume control',
play: 'Play',
pause: 'Pause',
rewind: 'Rewind',
forward: 'Forward',
previous: 'Previous',
next: 'Skip',
loop: 'Disable loop',
loopOff: 'Enable loop',
volume: 'Mute',
volumeMute: 'Unmute'
};
audio = (() => (0, _react.createRef)())();
progressBar = (() => (0, _react.createRef)())();
container = (() => (0, _react.createRef)())();
lastVolume = (() => this.props.volume ?? 1)();
togglePlay = e => {
e.stopPropagation();
const audio = this.audio.current;
if ((audio.paused || audio.ended) && audio.src) {
this.playAudioPromise();
} else if (!audio.paused) {
audio.pause();
}
};
playAudioPromise = () => {
if (this.audio.current.error) {
this.audio.current.load();
}
const playPromise = this.audio.current.play();
if (playPromise) {
playPromise.then(null).catch(err => {
const {
onPlayError
} = this.props;
onPlayError && onPlayError(new Error(err));
});
} else {
this.forceUpdate();
}
};
isPlaying = () => {
const audio = this.audio.current;
if (!audio) return false;
return !audio.paused && !audio.ended;
};
handlePlay = e => {
this.forceUpdate();
this.props.onPlay && this.props.onPlay(e);
};
handlePause = e => {
if (!this.audio) return;
this.forceUpdate();
this.props.onPause && this.props.onPause(e);
};
handleEnded = e => {
if (!this.audio) return;
this.forceUpdate();
this.props.onEnded && this.props.onEnded(e);
};
handleAbort = e => {
this.props.onAbort && this.props.onAbort(e);
};
handleClickVolumeButton = () => {
const audio = this.audio.current;
if (audio.volume > 0) {
this.lastVolume = audio.volume;
audio.volume = 0;
} else {
audio.volume = this.lastVolume;
}
};
handleMuteChange = () => {
this.forceUpdate();
};
handleClickLoopButton = () => {
this.audio.current.loop = !this.audio.current.loop;
this.forceUpdate();
};
handleClickRewind = () => {
const {
progressJumpSteps,
progressJumpStep
} = this.props;
const jumpStep = progressJumpSteps.backward || progressJumpStep;
this.setJumpTime(-jumpStep);
};
handleClickForward = () => {
const {
progressJumpSteps,
progressJumpStep
} = this.props;
const jumpStep = progressJumpSteps.forward || progressJumpStep;
this.setJumpTime(jumpStep);
};
setJumpTime = time => {
const audio = this.audio.current;
const {
duration,
currentTime: prevTime
} = audio;
if (audio.readyState === audio.HAVE_NOTHING || audio.readyState === audio.HAVE_METADATA || !isFinite(duration) || !isFinite(prevTime)) {
try {
audio.load();
} catch (err) {
return this.props.onChangeCurrentTimeError && this.props.onChangeCurrentTimeError(err);
}
}
let currentTime = prevTime + time / 1000;
if (currentTime < 0) {
audio.currentTime = 0;
currentTime = 0;
} else if (currentTime > duration) {
audio.currentTime = duration;
currentTime = duration;
} else {
audio.currentTime = currentTime;
}
};
setJumpVolume = volume => {
let newVolume = this.audio.current.volume + volume;
if (newVolume < 0) newVolume = 0;else if (newVolume > 1) newVolume = 1;
this.audio.current.volume = newVolume;
};
handleKeyDown = e => {
if (this.props.hasDefaultKeyBindings ?? true) {
switch (e.key) {
case ' ':
if (e.target === this.container.current || e.target === this.progressBar.current) {
e.preventDefault();
this.togglePlay(e);
}
break;
case 'ArrowLeft':
this.handleClickRewind();
break;
case 'ArrowRight':
this.handleClickForward();
break;
case 'ArrowUp':
e.preventDefault();
this.setJumpVolume(this.props.volumeJumpStep);
break;
case 'ArrowDown':
e.preventDefault();
this.setJumpVolume(-this.props.volumeJumpStep);
break;
case 'l':
this.handleClickLoopButton();
break;
case 'm':
this.handleClickVolumeButton();
break;
}
}
};
renderUIModules = modules => {
return modules.map((comp, i) => this.renderUIModule(comp, i));
};
renderUIModule = (comp, key) => {
const {
defaultCurrentTime = '--:--',
progressUpdateInterval = 20,
showDownloadProgress = true,
showFilledProgress = true,
showFilledVolume = false,
defaultDuration = '--:--',
customIcons = {},
showSkipControls = false,
onClickPrevious,
onClickNext,
onChangeCurrentTimeError,
showJumpControls = true,
customAdditionalControls = [_constants.RHAP_UI.LOOP],
customVolumeControls = [_constants.RHAP_UI.VOLUME],
muted = false,
timeFormat = 'auto',
volume: volumeProp = 1,
loop: loopProp = false,
mse,
i18nAriaLabels = H5AudioPlayer.defaultI18nAriaLabels
} = this.props;
switch (comp) {
case _constants.RHAP_UI.CURRENT_TIME:
return _react.default.createElement("div", {
key: key,
id: "rhap_current-time",
className: "rhap_time rhap_current-time"
}, _react.default.createElement(_CurrentTime.default, {
audio: this.audio.current,
isLeftTime: false,
defaultCurrentTime: defaultCurrentTime,
timeFormat: timeFormat
}));
case _constants.RHAP_UI.CURRENT_LEFT_TIME:
return _react.default.createElement("div", {
key: key,
id: "rhap_current-left-time",
className: "rhap_time rhap_current-left-time"
}, _react.default.createElement(_CurrentTime.default, {
audio: this.audio.current,
isLeftTime: true,
defaultCurrentTime: defaultCurrentTime,
timeFormat: timeFormat
}));
case _constants.RHAP_UI.PROGRESS_BAR:
return _react.default.createElement(_ProgressBar.default, {
key: key,
ref: this.progressBar,
audio: this.audio.current,
progressUpdateInterval: progressUpdateInterval,
showDownloadProgress: showDownloadProgress,
showFilledProgress: showFilledProgress,
onSeek: mse && mse.onSeek,
onChangeCurrentTimeError: onChangeCurrentTimeError,
srcDuration: mse && mse.srcDuration,
i18nProgressBar: i18nAriaLabels.progressControl
});
case _constants.RHAP_UI.DURATION:
return _react.default.createElement("div", {
key: key,
className: "rhap_time rhap_total-time"
}, mse && mse.srcDuration ? (0, _utils.getDisplayTimeBySeconds)(mse.srcDuration, mse.srcDuration, this.props.timeFormat) : _react.default.createElement(_Duration.default, {
audio: this.audio.current,
defaultDuration: defaultDuration,
timeFormat: timeFormat
}));
case _constants.RHAP_UI.ADDITIONAL_CONTROLS:
return _react.default.createElement("div", {
key: key,
className: "rhap_additional-controls"
}, this.renderUIModules(customAdditionalControls));
case _constants.RHAP_UI.MAIN_CONTROLS:
{
const isPlaying = this.isPlaying();
let actionIcon;
if (isPlaying) {
actionIcon = customIcons.pause ? customIcons.pause : _react.default.createElement(_react2.Icon, {
icon: "mdi:pause-circle"
});
} else {
actionIcon = customIcons.play ? customIcons.play : _react.default.createElement(_react2.Icon, {
icon: "mdi:play-circle"
});
}
return _react.default.createElement("div", {
key: key,
className: "rhap_main-controls"
}, showSkipControls && _react.default.createElement("button", {
"aria-label": i18nAriaLabels.previous,
className: "rhap_button-clear rhap_main-controls-button rhap_skip-button",
type: "button",
onClick: onClickPrevious
}, customIcons.previous ? customIcons.previous : _react.default.createElement(_react2.Icon, {
icon: "mdi:skip-previous"
})), showJumpControls && _react.default.createElement("button", {
"aria-label": i18nAriaLabels.rewind,
className: "rhap_button-clear rhap_main-controls-button rhap_rewind-button",
type: "button",
onClick: this.handleClickRewind
}, customIcons.rewind ? customIcons.rewind : _react.default.createElement(_react2.Icon, {
icon: "mdi:rewind"
})), _react.default.createElement("button", {
"aria-label": isPlaying ? i18nAriaLabels.pause : i18nAriaLabels.play,
className: "rhap_button-clear rhap_main-controls-button rhap_play-pause-button",
type: "button",
onClick: this.togglePlay
}, actionIcon), showJumpControls && _react.default.createElement("button", {
"aria-label": i18nAriaLabels.forward,
className: "rhap_button-clear rhap_main-controls-button rhap_forward-button",
type: "button",
onClick: this.handleClickForward
}, customIcons.forward ? customIcons.forward : _react.default.createElement(_react2.Icon, {
icon: "mdi:fast-forward"
})), showSkipControls && _react.default.createElement("button", {
"aria-label": i18nAriaLabels.next,
className: "rhap_button-clear rhap_main-controls-button rhap_skip-button",
type: "button",
onClick: onClickNext
}, customIcons.next ? customIcons.next : _react.default.createElement(_react2.Icon, {
icon: "mdi:skip-next"
})));
}
case _constants.RHAP_UI.VOLUME_CONTROLS:
return _react.default.createElement("div", {
key: key,
className: "rhap_volume-controls"
}, this.renderUIModules(customVolumeControls));
case _constants.RHAP_UI.LOOP:
{
const loop = this.audio.current ? this.audio.current.loop : loopProp;
let loopIcon;
if (loop) {
loopIcon = customIcons.loop ? customIcons.loop : _react.default.createElement(_react2.Icon, {
icon: "mdi:repeat"
});
} else {
loopIcon = customIcons.loopOff ? customIcons.loopOff : _react.default.createElement(_react2.Icon, {
icon: "mdi:repeat-off"
});
}
return _react.default.createElement("button", {
key: key,
"aria-label": loop ? i18nAriaLabels.loop : i18nAriaLabels.loopOff,
className: "rhap_button-clear rhap_repeat-button",
type: "button",
onClick: this.handleClickLoopButton
}, loopIcon);
}
case _constants.RHAP_UI.VOLUME:
{
const {
volume = muted ? 0 : volumeProp
} = this.audio.current || {};
let volumeIcon;
if (volume) {
volumeIcon = customIcons.volume ? customIcons.volume : _react.default.createElement(_react2.Icon, {
icon: "mdi:volume-high"
});
} else {
volumeIcon = customIcons.volume ? customIcons.volumeMute : _react.default.createElement(_react2.Icon, {
icon: "mdi:volume-mute"
});
}
return _react.default.createElement("div", {
key: key,
className: "rhap_volume-container"
}, _react.default.createElement("button", {
"aria-label": volume ? i18nAriaLabels.volume : i18nAriaLabels.volumeMute,
onClick: this.handleClickVolumeButton,
type: "button",
className: "rhap_button-clear rhap_volume-button"
}, volumeIcon), _react.default.createElement(_VolumeBar.default, {
audio: this.audio.current,
volume: volume,
onMuteChange: this.handleMuteChange,
showFilledVolume: showFilledVolume,
i18nVolumeControl: i18nAriaLabels.volumeControl
}));
}
default:
if (!(0, _react.isValidElement)(comp)) {
return null;
}
return comp.key ? comp : (0, _react.cloneElement)(comp, {
key
});
}
};
componentDidMount() {
this.forceUpdate();
const audio = this.audio.current;
if (this.props.muted) {
audio.volume = 0;
} else {
audio.volume = this.lastVolume;
}
audio.addEventListener('error', e => {
const target = e.target;
if (target.error && target.currentTime === target.duration) {
return this.props.onEnded && this.props.onEnded(e);
}
this.props.onError && this.props.onError(e);
});
audio.addEventListener('canplay', e => {
this.props.onCanPlay && this.props.onCanPlay(e);
});
audio.addEventListener('canplaythrough', e => {
this.props.onCanPlayThrough && this.props.onCanPlayThrough(e);
});
audio.addEventListener('play', this.handlePlay);
audio.addEventListener('abort', this.handleAbort);
audio.addEventListener('ended', this.handleEnded);
audio.addEventListener('playing', e => {
this.props.onPlaying && this.props.onPlaying(e);
});
audio.addEventListener('seeking', e => {
this.props.onSeeking && this.props.onSeeking(e);
});
audio.addEventListener('seeked', e => {
this.props.onSeeked && this.props.onSeeked(e);
});
audio.addEventListener('waiting', e => {
this.props.onWaiting && this.props.onWaiting(e);
});
audio.addEventListener('emptied', e => {
this.props.onEmptied && this.props.onEmptied(e);
});
audio.addEventListener('stalled', e => {
this.props.onStalled && this.props.onStalled(e);
});
audio.addEventListener('suspend', e => {
this.props.onSuspend && this.props.onSuspend(e);
});
audio.addEventListener('loadstart', e => {
this.props.onLoadStart && this.props.onLoadStart(e);
});
audio.addEventListener('loadedmetadata', e => {
this.props.onLoadedMetaData && this.props.onLoadedMetaData(e);
});
audio.addEventListener('loadeddata', e => {
this.props.onLoadedData && this.props.onLoadedData(e);
});
audio.addEventListener('pause', this.handlePause);
audio.addEventListener('timeupdate', (0, _utils.throttle)(e => {
this.props.onListen && this.props.onListen(e);
}, this.props.listenInterval));
audio.addEventListener('volumechange', e => {
this.props.onVolumeChange && this.props.onVolumeChange(e);
});
audio.addEventListener('encrypted', e => {
const {
mse
} = this.props;
mse && mse.onEcrypted && mse.onEcrypted(e);
});
}
componentDidUpdate(prevProps) {
const {
src,
autoPlayAfterSrcChange
} = this.props;
if (prevProps.src !== src) {
if (autoPlayAfterSrcChange) {
this.playAudioPromise();
} else {
this.forceUpdate();
}
}
}
render() {
const {
className = '',
src,
loop: loopProp = false,
preload = 'auto',
autoPlay = false,
crossOrigin,
mediaGroup,
header,
footer,
layout = 'stacked',
customProgressBarSection = [_constants.RHAP_UI.CURRENT_TIME, _constants.RHAP_UI.PROGRESS_BAR, _constants.RHAP_UI.DURATION],
customControlsSection = [_constants.RHAP_UI.ADDITIONAL_CONTROLS, _constants.RHAP_UI.MAIN_CONTROLS, _constants.RHAP_UI.VOLUME_CONTROLS],
children,
style,
i18nAriaLabels = H5AudioPlayer.defaultI18nAriaLabels
} = this.props;
const loop = this.audio.current ? this.audio.current.loop : loopProp;
const loopClass = loop ? 'rhap_loop--on' : 'rhap_loop--off';
const isPlayingClass = this.isPlaying() ? 'rhap_play-status--playing' : 'rhap_play-status--paused';
return _react.default.createElement("div", {
role: "group",
tabIndex: 0,
"aria-label": i18nAriaLabels.player,
className: `rhap_container ${loopClass} ${isPlayingClass} ${className}`,
onKeyDown: this.handleKeyDown,
ref: this.container,
style: style
}, _react.default.createElement("audio", {
src: src,
controls: false,
loop: loop,
autoPlay: autoPlay,
preload: preload,
crossOrigin: crossOrigin,
mediaGroup: mediaGroup,
ref: this.audio
}, children), header && _react.default.createElement("div", {
className: "rhap_header"
}, header), _react.default.createElement("div", {
className: `rhap_main ${(0, _utils.getMainLayoutClassName)(layout)}`
}, _react.default.createElement("div", {
className: "rhap_progress-section"
}, this.renderUIModules(customProgressBarSection)), _react.default.createElement("div", {
className: "rhap_controls-section"
}, this.renderUIModules(customControlsSection))), footer && _react.default.createElement("div", {
className: "rhap_footer"
}, footer));
}
}
var _default = exports.default = H5AudioPlayer;