├── .gitignore ├── .npmignore ├── screenshot.png ├── .babelrc ├── .storybook └── config.js ├── stories └── index.js ├── package.json ├── .eslintrc.js ├── README.md └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | screenshot.png 3 | .storybook/ 4 | stories/ 5 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edlerd/react-h5-audio-player/master/screenshot.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react' 2 | import { setDefaults } from '@storybook/addon-info' 3 | 4 | // Docs: https://github.com/storybooks/storybook/tree/master/addons/info 5 | setDefaults({ 6 | header: true, 7 | }) 8 | 9 | configure(() => { 10 | require('../stories/index') 11 | }, module) 12 | -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { withInfo } from '@storybook/addon-info' 4 | import { action } from '@storybook/addon-actions' 5 | import Player from '../src' 6 | 7 | const SAMPLE_MP3_URL = 'https://ia802508.us.archive.org/5/items/testmp3testfile/mpthreetest.mp3' 8 | 9 | storiesOf('Player', module) 10 | .add( 11 | 'Basic', 12 | withInfo(`Basic usage 13 | `)(() => ), 14 | ) 15 | .add( 16 | 'Auto Play', 17 | withInfo(`Auto Play 18 | `)(() => ), 19 | ) 20 | .add( 21 | 'Action Logger', 22 | withInfo(`Auto Play 23 | `)(() => ( 24 | 39 | )), 40 | ) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-h5-audio-player", 3 | "version": "1.1.0", 4 | "description": "A React audio player with UI. Mobile compatible.", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "build": "babel src --out-dir lib", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "storybook": "start-storybook -p 9000" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+ssh://git@github.com/lhz516/react-h5-audio-player.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "component", 18 | "music", 19 | "audio", 20 | "player", 21 | "mobile" 22 | ], 23 | "author": "Hanz Luo", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/lhz516/react-h5-audio-player/issues" 27 | }, 28 | "homepage": "https://github.com/lhz516/react-h5-audio-player#readme", 29 | "peerDependencies": { 30 | "react": "^15.5.0 || ^16.0.0", 31 | "react-dom": "^15.5.0 || ^16.0.0", 32 | "prop-types": "^15.5.0 || ^16.0.0" 33 | }, 34 | "devDependencies": { 35 | "@babel/cli": "^7.1.2", 36 | "@babel/core": "^7.1.2", 37 | "@babel/plugin-proposal-class-properties": "^7.1.0", 38 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 39 | "@babel/preset-env": "^7.1.0", 40 | "@babel/preset-react": "^7.0.0", 41 | "@storybook/addon-actions": "^4.0.2", 42 | "@storybook/addon-info": "^4.0.2", 43 | "@storybook/addon-options": "^4.0.2", 44 | "@storybook/react": "^4.0.2", 45 | "babel-core": "^7.0.0-bridge.0", 46 | "babel-eslint": "^10.0.1", 47 | "babel-loader": "^8.0.4", 48 | "eslint": "^5.8.0", 49 | "eslint-plugin-react": "^7.11.1", 50 | "react": "^16.6.0", 51 | "react-dom": "^16.6.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: 'babel-eslint', 3 | env: { 4 | es6: true, 5 | browser: true, 6 | node: true, 7 | }, 8 | extends: ['eslint:recommended', 'plugin:react/recommended'], 9 | parserOptions: { 10 | allowImportExportEverywhere: true, 11 | ecmaFeatures: { 12 | experimentalObjectRestSpread: true, 13 | }, 14 | sourceType: 'module', 15 | }, 16 | rules: { 17 | 'arrow-parens': [2, 'as-needed', { requireForBlockBody: true }], 18 | camelcase: [2, { properties: 'never' }], 19 | curly: [2, 'multi-line'], 20 | indent: [2, 2, { SwitchCase: 1 }], 21 | quotes: [2, 'single'], 22 | semi: [2, 'never'], 23 | eqeqeq: 2, 24 | yoda: 2, 25 | 'block-spacing': 2, 26 | 'computed-property-spacing': 2, 27 | 'comma-dangle': [ 28 | 2, 29 | { 30 | arrays: 'always-multiline', 31 | objects: 'always-multiline', 32 | imports: 'always-multiline', 33 | exports: 'always-multiline', 34 | functions: 'always-multiline', 35 | }, 36 | ], 37 | 'func-call-spacing': 2, 38 | 'eol-last': 2, 39 | 'no-empty': [2, { allowEmptyCatch: true }], 40 | 'no-undefined': 2, 41 | 'no-use-before-define': [2, 'nofunc'], 42 | 'no-multi-assign': 2, 43 | 'no-useless-concat': 2, 44 | 'no-useless-return': 2, 45 | 'no-shadow-restricted-names': 2, 46 | 'no-multi-spaces': 2, 47 | 'no-multi-str': 2, 48 | 'no-unused-vars': 1, 49 | 'no-alert': 0, 50 | 'no-console': 1, 51 | 'no-useless-constructor': 1, 52 | 'no-constant-condition': [2, { checkLoops: false }], 53 | 'no-duplicate-imports': [2, { includeExports: true }], 54 | 'no-useless-computed-key': 2, 55 | 'no-useless-rename': 2, 56 | 'no-var': 2, 57 | 'space-before-blocks': 2, 58 | 'space-in-parens': 2, 59 | 'space-infix-ops': 2, 60 | 'space-unary-ops': [2, { words: true, nonwords: false }], 61 | 'space-before-function-paren': [ 62 | 2, 63 | { 64 | anonymous: 'never', 65 | named: 'never', 66 | asyncArrow: 'always', 67 | }, 68 | ], 69 | 'template-tag-spacing': 2, 70 | 'max-len': [ 71 | 2, 72 | { 73 | code: 120, 74 | tabWidth: 2, 75 | ignoreStrings: true, 76 | ignoreComments: true, 77 | ignoreTrailingComments: true, 78 | ignoreTemplateLiterals: true, 79 | ignoreRegExpLiterals: true, 80 | }, 81 | ], 82 | 'object-shorthand': 2, 83 | 'object-curly-spacing': [2, 'always'], 84 | 'prefer-const': 2, 85 | 'prefer-arrow-callback': 2, 86 | 'template-curly-spacing': [2, 'never'], 87 | 'react/prop-types': 0, 88 | }, 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React H5 Audio Player 2 | 3 | * Audio player component that provides consistent UI on different browsers. 4 | 5 | * Flexbox design with CSS shapes. Mobile friendly. No extra dependencies. 6 | 7 | ![screenshot](./screenshot.png) 8 | 9 | Supported browsers: Chrome, Firefox, Safari, Opera, Edge, IE (≥10) 10 | 11 | ### Breaking change from 0.x to 1.x 12 | 13 | In 1.x, we use `prop-types` package instead of using it directly in React. Thus we dropped support under `react@15.5.0`. The usage will remain the same. 14 | 15 | ## Installation 16 | 17 | `npm i --save react-h5-audio-player` 18 | 19 | ## Usage 20 | 21 | ```jsx 22 | import AudioPlayer from "react-h5-audio-player"; 23 | 24 | const Player = () => ( 25 | console.log("onPlay")} 29 | // other props here 30 | /> 31 | ); 32 | ``` 33 | 34 | ## Props 35 | 36 | ### HTML Audio Tag Native Attributes 37 | 38 | | Props | Type | Default | 39 | | -------- | :-----: | :-----: | 40 | | src | String | '' | 41 | | preload | String | 'auto' | 42 | | autoPlay | Boolean | false | 43 | | loop | Boolean | false | 44 | | muted | Boolean | false | 45 | | loop | Boolean | false | 46 | | volume | Number | 1.0 | 47 | 48 | More native attributes detail: [MDN Audio element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio) 49 | 50 | ### Other Props 51 | 52 | #### hidePlayer {Bool} [false] 53 | 54 | Indicates if the audio player is hidden. 55 | 56 | #### progressUpdateInterval {Number} [500] 57 | 58 | Indicates the interval that the progress bar UI updates. 59 | 60 | #### listenInterval {Number} [1000] 61 | 62 | Indicates how often to call the `onListened` prop during playback, in milliseconds. 63 | 64 | #### onAbort {Function (event)} 65 | 66 | Called when unloading the audio player, like when switching to a different src file. Passed the event. 67 | 68 | #### onCanPlay {Function (event)} 69 | 70 | Called when enough of the file has been downloaded to be able to start playing. 71 | 72 | #### onEnded {Function (event)} 73 | 74 | Called when playback has finished to the end of the file. Passed the event. 75 | 76 | #### onError {Function (event)} 77 | 78 | Called when the audio tag encounters an error. Passed the event. 79 | 80 | #### onListen {Function (currentTime)} 81 | 82 | Called every `listenInterval` milliseconds during playback. 83 | 84 | #### onPause {Function (event)} 85 | 86 | Called when the user pauses playback. Passed the event. 87 | 88 | #### onPlay {Function (event)} 89 | 90 | Called when the user taps play. 91 | 92 | #### onDragStart {Function (event)} 93 | 94 | Called when the user start dragging the time indicator. Passed the event. 95 | 96 | #### onDragMove {Function (event)} 97 | 98 | Called when the user is dragging the time indicator. Passed the event. 99 | 100 | #### onDragEnd {Function (event)} 101 | 102 | Called when the user finish dragging the time indicator. Passed the event. 103 | 104 | ## UI Overwrites 105 | 106 | React H5 Audio Player provides built-in class names for developers to overwrite. 107 | 108 | For example: 109 | 110 | ```sass 111 | // In a SASS or LESS file 112 | .react-h5-audio-player { 113 | .toggle-play-wrapper { 114 | .toggle-play-button { 115 | // Remember to use !important to overwrite inline styles. 116 | background-color: red !important; 117 | } 118 | } 119 | } 120 | ``` 121 | 122 | You can find more class names by inspecting element on you browser. 123 | 124 | To be compatible with some **old browsers**, you can add prefixers to flex container 125 | 126 | ```sass 127 | .react-h5-audio-player { 128 | .flex { 129 | display: -webkit-box; 130 | display: -webkit-flex; 131 | display: -ms-flexbox; 132 | display: flex; 133 | .toggle-play-wrapper { 134 | flex: 1 0 60px; 135 | -webkit-box-flex: 1 0 60px; 136 | -moz-box-flex: 1 0 60px; 137 | -ms-flex: 1 0 60px; 138 | } 139 | .progress-bar-wrapper { 140 | flex: 10 0 auto; 141 | -webkit-box-flex: 10 0 auto; 142 | -moz-box-flex: 10 0 auto; 143 | -ms-flex: 10 0 auto; 144 | } 145 | } 146 | } 147 | ``` 148 | 149 | ## Advanced Usage 150 | 151 | ### Access to the audio element 152 | 153 | You can get direct access to the underlying audio element. First get a ref to ReactAudioPlayer: 154 | 155 | ```jsx 156 | (this.player = c)} /> 157 | ``` 158 | 159 | Then you can access the audio element like this: 160 | 161 | `this.player.audio` 162 | 163 | ## How to contribute 164 | 165 | Issues and PR's are welcome. 166 | 167 | ## Credits 168 | 169 | This project is inspired by [React Audio Player](https://github.com/justinmc/react-audio-player). 170 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const style = { 5 | audioPlayerWrapper(hidePlayer) { 6 | return { 7 | display: hidePlayer ? 'none' : 'block', 8 | } 9 | }, 10 | flexWrapper: { 11 | boxSizing: 'border-box', 12 | height: '70px', 13 | display: 'flex', 14 | justifyContent: 'space-between', 15 | width: '100%', 16 | padding: '15px 0', 17 | backgroundColor: 'white', 18 | position: 'relative', 19 | zIndex: '100', 20 | boxShadow: '0 0 3px 0 rgba(0, 0, 0, 0.2)', 21 | }, 22 | pause: { 23 | boxSizing: 'content-box', 24 | display: 'block', 25 | width: '14px', 26 | height: '18px', 27 | borderLeft: '7px solid white', 28 | position: 'relative', 29 | zIndex: '1', 30 | left: '9px', 31 | backgroundColor: 'white', 32 | boxShadow: 'inset 7px 0 0 0 rgb(251, 86, 21)', 33 | }, 34 | play: { 35 | boxSizing: 'content-box', 36 | display: 'block', 37 | width: '0', 38 | height: '0', 39 | borderTop: '10px solid transparent', 40 | borderBottom: '10px solid transparent', 41 | borderLeft: '20px solid white', 42 | position: 'relative', 43 | zIndex: '1', 44 | left: '13px', 45 | }, 46 | togglePlayWrapper: { 47 | boxSizing: 'border-box', 48 | flex: '1 0 60px', 49 | position: 'relative', 50 | }, 51 | togglePlay: { 52 | boxSizing: 'border-box', 53 | position: 'absolute', 54 | left: '50%', 55 | marginLeft: '-20px', 56 | backgroundColor: '#FB5615', 57 | color: 'white', 58 | width: '40px', 59 | height: '40px', 60 | borderRadius: '50%', 61 | textAlign: 'center', 62 | paddingTop: '10px', 63 | }, 64 | progressBarWrapper: { 65 | height: '100%', 66 | display: 'flex', 67 | flexDirection: 'column', 68 | justifyContent: 'space-between', 69 | boxSizing: 'border-box', 70 | position: 'relative', 71 | flex: '10 0 auto', 72 | alignSelf: 'center', 73 | padding: '5px 4% 0 0', 74 | }, 75 | progressBar: { 76 | boxSizing: 'border-box', 77 | width: '100%', 78 | height: '5px', 79 | left: '0', 80 | background: '#e4e4e4', 81 | }, 82 | drag(left) { 83 | return { 84 | boxSizing: 'border-box', 85 | position: 'absolute', 86 | width: '20px', 87 | height: '20px', 88 | left, 89 | top: '-3px', 90 | background: 'skyblue', 91 | opacity: '0.8', 92 | borderRadius: '50px', 93 | boxShadow: '#fff 0 0 5px', 94 | cursor: 'pointer', 95 | } 96 | }, 97 | audioInfo: { 98 | display: 'flex', 99 | justifyContent: 'space-between', 100 | }, 101 | time: {}, 102 | volumeControl: { 103 | zIndex: 20, 104 | cursor: 'pointer', 105 | position: 'relative', 106 | width: 0, 107 | height: 0, 108 | borderBottom: '15px solid rgb(228, 228, 228)', 109 | borderLeft: '45px solid transparent', 110 | }, 111 | volume(currentVolume) { 112 | const height = 15 113 | return { 114 | zIndex: 19, 115 | position: 'absolute', 116 | left: '-45px', 117 | bottom: '-15px', 118 | width: 0, 119 | height: 0, 120 | borderBottom: `${height * currentVolume}px solid skyblue`, 121 | borderLeft: `${height * currentVolume * 3}px solid transparent`, 122 | } 123 | }, 124 | } 125 | 126 | class H5AudioPlayer extends React.Component { 127 | static propTypes = { 128 | /** 129 | * HTML5 Audio tag autoPlay property 130 | */ 131 | autoPlay: PropTypes.bool, 132 | /** 133 | * Display message when browser doesn't support 134 | */ 135 | children: PropTypes.element, 136 | /** 137 | * custom classNames 138 | */ 139 | className: PropTypes.string, 140 | /** 141 | * Set component `display` to none 142 | */ 143 | hidePlayer: PropTypes.bool, 144 | /** 145 | * The time interval to trigger onListen 146 | */ 147 | listenInterval: PropTypes.number, 148 | loop: PropTypes.bool, 149 | muted: PropTypes.bool, 150 | onAbort: PropTypes.func, 151 | onCanPlay: PropTypes.func, 152 | onCanPlayThrough: PropTypes.func, 153 | onEnded: PropTypes.func, 154 | onError: PropTypes.func, 155 | onListen: PropTypes.func, 156 | onPause: PropTypes.func, 157 | onPlay: PropTypes.func, 158 | onDragStart: PropTypes.func, 159 | onDragMove: PropTypes.func, 160 | onDragEnd: PropTypes.func, 161 | /** 162 | * HTML5 Audio tag preload property 163 | */ 164 | preload: PropTypes.oneOf(['auto', 'metadata', 'none']), 165 | /** 166 | * Pregress indicator refresh interval 167 | */ 168 | progressUpdateInterval: PropTypes.number, 169 | /** 170 | * HTML5 Audio tag src property 171 | */ 172 | src: PropTypes.string, 173 | title: PropTypes.string, 174 | volume: PropTypes.number, 175 | } 176 | 177 | static defaultProps = { 178 | autoPlay: false, 179 | hidePlayer: false, 180 | listenInterval: 1000, 181 | loop: false, 182 | muted: false, 183 | preload: 'auto', 184 | progressUpdateInterval: 500, 185 | src: '', 186 | volume: 1.0, 187 | } 188 | 189 | state = { 190 | duration: 0, 191 | currentTime: 0, 192 | currentVolume: this.props.volume, 193 | dragLeft: 0, 194 | isDragging: false, 195 | isPlaying: false, 196 | } 197 | 198 | componentDidMount() { 199 | // audio player object 200 | const audio = this.audio 201 | // progress bar slider object 202 | const slider = this.slider 203 | 204 | this.intervalId = setInterval(() => { 205 | const currentTime = this.audio.currentTime 206 | const duration = this.audio.duration 207 | const barWidth = this.bar.offsetWidth - 20 208 | const left = (barWidth * currentTime) / duration || 0 209 | if (!this.audio.paused && !this.state.isDragging && !!duration) { 210 | this.setState({ 211 | currentTime, 212 | duration, 213 | barWidth, 214 | dragLeft: left, 215 | }) 216 | } 217 | }, this.props.progressUpdateInterval) 218 | audio.addEventListener('error', (e) => { 219 | this.props.onError && this.props.onError(e) 220 | }) 221 | 222 | // When enough of the file has downloaded to start playing 223 | audio.addEventListener('canplay', (e) => { 224 | this.props.onCanPlay && this.props.onCanPlay(e) 225 | }) 226 | 227 | // When enough of the file has downloaded to play the entire file 228 | audio.addEventListener('canplaythrough', (e) => { 229 | this.props.onCanPlayThrough && this.props.onCanPlayThrough(e) 230 | }) 231 | 232 | // When audio play starts 233 | audio.addEventListener('play', (e) => { 234 | this.setState({ isPlaying: true }) 235 | this.setListenTrack() 236 | this.props.onPlay && this.props.onPlay(e) 237 | }) 238 | 239 | // When unloading the audio player (switching to another src) 240 | audio.addEventListener('abort', (e) => { 241 | this.clearListenTrack() 242 | this.props.onAbort && this.props.onAbort(e) 243 | }) 244 | 245 | // When the file has finished playing to the end 246 | audio.addEventListener('ended', (e) => { 247 | this.clearListenTrack() 248 | this.props.onEnded && this.props.onEnded(e) 249 | }) 250 | 251 | // When the user pauses playback 252 | audio.addEventListener('pause', (e) => { 253 | this.clearListenTrack() 254 | if (!this.audio) return 255 | this.setState({ isPlaying: false }) 256 | this.props.onPause && this.props.onPause(e) 257 | }) 258 | 259 | let dragX 260 | slider.addEventListener('dragstart', (e) => { 261 | if (!this.audio.src) { 262 | return 263 | } 264 | e.dataTransfer.setData('text', 'slider') 265 | if (e.dataTransfer.setDragImage) { 266 | const crt = slider.cloneNode(true) 267 | e.dataTransfer.setDragImage(crt, 0, 0) 268 | } 269 | this.audio.pause() 270 | document.addEventListener('dragover', (event) => { 271 | event = event || window.event 272 | dragX = event.pageX 273 | }) 274 | this.props.onDragStart && this.props.onDragStart(e) 275 | this.setState({ isDragging: true }) 276 | }) 277 | 278 | slider.addEventListener('touchstart', (e) => { 279 | this.setState({ isDragging: true }) 280 | this.props.onDragStart && this.props.onDragStart(e) 281 | setTimeout(() => this.audio.pause(), 0) 282 | }) 283 | slider.addEventListener('drag', (e) => { 284 | if (!this.audio.src) { 285 | return 286 | } 287 | if (dragX) { 288 | let dragLeft = dragX - this.bar.getBoundingClientRect().left 289 | if (dragLeft < 0) { 290 | dragLeft = 0 291 | } else if (dragLeft > this.bar.offsetWidth - 20) { 292 | dragLeft = this.bar.offsetWidth - 21 293 | } 294 | this.setState({ dragLeft }) 295 | this.props.onDragMove && this.props.onDragMove(e) 296 | } 297 | }) 298 | slider.addEventListener('touchmove', (e) => { 299 | let dragLeft = e.touches[0].clientX - this.bar.getBoundingClientRect().left 300 | if (dragLeft < 0) { 301 | dragLeft = 0 302 | } else if (dragLeft > this.bar.offsetWidth - 20) { 303 | dragLeft = this.bar.offsetWidth - 21 304 | } 305 | this.setState({ dragLeft }) 306 | this.props.onDragMove && this.props.onDragMove(e) 307 | }) 308 | slider.addEventListener('dragend', (e) => { 309 | if (!this.audio.src) { 310 | return 311 | } 312 | const audio = this.audio 313 | audio.currentTime = (audio.duration * this.state.dragLeft) / (this.bar.offsetWidth - 20) || 0 314 | audio.play() 315 | this.setState({ isDragging: false }) 316 | this.props.onDragEnd && this.props.onDragEnd(e) 317 | }) 318 | slider.addEventListener('touchend', (e) => { 319 | this.setState({ isDragging: false }) 320 | this.props.onDragEnd && this.props.onDragEnd(e) 321 | setTimeout(() => { 322 | const audio = this.audio 323 | audio.currentTime = (audio.duration * this.state.dragLeft) / (this.bar.offsetWidth - 20) 324 | audio.play() 325 | }, 0) 326 | }) 327 | } 328 | 329 | componentWillUnmount() { 330 | clearInterval(this.intervalId) 331 | } 332 | 333 | componentDidUpdate(prevProps) { 334 | const { src } = this.props 335 | if (src !== prevProps.src) { 336 | this.audio.play() 337 | } 338 | } 339 | 340 | togglePlay = () => { 341 | if (this.audio.paused && this.audio.src) { 342 | this.audio.play() 343 | } else if (!this.audio.paused) { 344 | this.audio.pause() 345 | } 346 | } 347 | 348 | volumnControlDrag = (e) => { 349 | if (e.clientX < 0) return 350 | const relativePos = e.clientX - this.volumeControl.getBoundingClientRect().left 351 | let currentVolume 352 | if (relativePos < 0) { 353 | currentVolume = 0 354 | } else if (relativePos > 45) { 355 | currentVolume = 1 356 | } else { 357 | currentVolume = relativePos / 45 358 | } 359 | e.currentTarget.style.cursor = 'pointer' 360 | if (e.dataTransfer) { 361 | e.dataTransfer.dropEffect = 'move' 362 | } 363 | this.audio.volume = currentVolume 364 | this.setState({ currentVolume }) 365 | } 366 | 367 | volumnControlDragOver = (e) => { 368 | e.dataTransfer.dropEffect = 'move' 369 | } 370 | 371 | volumnControlDragStart = (e) => { 372 | // e.target.style.cursor = 'pointer' 373 | e.dataTransfer.setData('text', 'volume') 374 | e.dataTransfer.effectAllowed = 'move' 375 | if (e.dataTransfer.setDragImage) { 376 | const crt = e.target.cloneNode(true) 377 | e.dataTransfer.setDragImage(crt, 0, 0) 378 | } 379 | } 380 | 381 | /** 382 | * Set an interval to call props.onListen every props.listenInterval time period 383 | */ 384 | setListenTrack = () => { 385 | if (!this.listenTracker) { 386 | const listenInterval = this.props.listenInterval 387 | this.listenTracker = setInterval(() => { 388 | this.props.onListen && this.props.onListen(this.audio.currentTime) 389 | }, listenInterval) 390 | } 391 | } 392 | 393 | /** 394 | * Clear the onListen interval 395 | */ 396 | clearListenTrack = () => { 397 | if (this.listenTracker) { 398 | clearInterval(this.listenTracker) 399 | this.listenTracker = null 400 | } 401 | } 402 | 403 | render() { 404 | const { className, volume, children, hidePlayer, src, preload, autoPlay, title = src, mute, loop } = this.props 405 | const { currentTime, currentVolume, duration, isPlaying, dragLeft } = this.state 406 | const incompatibilityMessage = children || ( 407 |

408 | Your browser does not support the audio element. 409 |

410 | ) 411 | 412 | let currentTimeMin = Math.floor(currentTime / 60) 413 | let currentTimeSec = Math.floor(currentTime % 60) 414 | let durationMin = Math.floor(duration / 60) 415 | let durationSec = Math.floor(duration % 60) 416 | const addHeadingZero = num => (num > 9 ? num.toString() : `0${num}`) 417 | 418 | currentTimeMin = addHeadingZero(currentTimeMin) 419 | currentTimeSec = addHeadingZero(currentTimeSec) 420 | durationMin = addHeadingZero(durationMin) 421 | durationSec = addHeadingZero(durationSec) 422 | 423 | return ( 424 |
425 |
426 | 440 | 449 |
450 |
{ 452 | this.bar = ref 453 | }} 454 | style={style.progressBar} 455 | /> 456 | {/*TODO: color change for sought part */} 457 |
458 |
{ 462 | this.slider = ref 463 | }} 464 | style={style.drag(dragLeft)} 465 | /> 466 |
467 |
468 | 469 | {currentTimeMin}:{currentTimeSec} 470 | 471 | / 472 | 473 | {durationMin}:{durationSec} 474 | 475 |
476 |
{ 478 | this.volumeControl = ref 479 | }} 480 | draggable="true" 481 | onDragStart={this.volumnControlDragStart} 482 | onDrag={this.volumnControlDrag} 483 | onDragOver={this.volumnControlDragOver} 484 | onMouseDown={this.volumnControlDrag} 485 | className="volume-controls" 486 | style={style.volumeControl} 487 | > 488 |
489 |
490 |
491 |
492 |
493 |
494 | ) 495 | } 496 | } 497 | 498 | export default H5AudioPlayer 499 | --------------------------------------------------------------------------------