diff --git a/.gitignore b/.gitignore index f929c2f..da65407 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ lib/ es/ coverage/ .vscode/ +.DS_Store # yarn .yarn/ diff --git a/README.md b/README.md index 154ad08..10724f7 100644 --- a/README.md +++ b/README.md @@ -198,6 +198,45 @@ Then you can access the audio element like this: You can use [Media Source Extensions](https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API) and [Encrypted Media Extensions](https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API) with this player. You need to provide the complete duration, and also a onSeek and onEncrypted callbacks. The logic for feeding the audio buffer and providing the decryption keys (if using encryption) must be set in the consumer side. The player does not provide that logic. Check the [StoryBook example](https://github.com/lhz516/react-h5-audio-player/blob/master/stories/mse-eme-player.tsx) to understand better how to use. +### Using `` Elements + +You can use child `` elements instead of the `src` prop, for example [to provide different file types or codecs based on browser support](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#usage_notes). + +```jsx + + + + + + +``` + +When using `` elements in playlists, be sure to set a unique `key` +property for each element. + +```jsx +const srcs = [ + [ + { src: 'https://example.com/audio1.aac', type: 'audio/aac' }, + { src: 'https://example.com/audio1.mp3', type: 'audio/mpeg' }, + { src: 'https://example.com/audio1.wav', type: 'audio/wav' }, + ], + [ + { src: 'https://example.com/audio2.aac', type: 'audio/aac' }, + { src: 'https://example.com/audio2.mp3', type: 'audio/mpeg' }, + { src: 'https://example.com/audio2.wav', type: 'audio/wav' }, + ], +] + +const currentIndex = 0 + + + {srcs[currentIndex].map(({ src, type }) => ( + + ))} + +``` + ## Release Notes https://github.com/lhz516/react-h5-audio-player/releases diff --git a/src/ProgressBar.tsx b/src/ProgressBar.tsx index e2cc2a5..80ff747 100644 --- a/src/ProgressBar.tsx +++ b/src/ProgressBar.tsx @@ -82,13 +82,15 @@ class ProgressBar extends Component { // Get time info while dragging indicator by mouse or touch getCurrentProgress = (event: MouseEvent | TouchEvent): TimePosInfo => { const { audio, progressBar } = this.props + const audioSrc = audio.src || audio.currentSrc + // A single-file progressive download (non-blob) can have transient states // where currentTime is not yet finite. In those cases return zeros to avoid // NaN propagation. const isSingleFileProgressiveDownload = - audio.src.indexOf('blob:') !== 0 && typeof this.props.srcDuration === 'undefined' + audioSrc.indexOf('blob:') !== 0 && typeof this.props.srcDuration === 'undefined' - if (isSingleFileProgressiveDownload && (!audio.src || !isFinite(audio.currentTime) || !progressBar.current)) { + if (isSingleFileProgressiveDownload && (!audioSrc || !isFinite(audio.currentTime) || !progressBar.current)) { return { currentTime: 0, currentTimePos: '0%' } } diff --git a/src/index.test.js b/src/index.test.js index e5bf20f..efc1c40 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -702,4 +702,22 @@ describe('H5AudioPlayer', () => { }) }) }) + + describe('Multiple Source Elements', () => { + it('renders child elements', () => { + const srcAac = 'test-audio.aac' + const srcMp3 = 'test-audio.mp3' + + const { container } = render( + + + + + ) + + expect(screen.getByTestId('aac')).toBeInTheDocument() + expect(screen.getByTestId('mp3')).toBeInTheDocument() + expect(container.querySelector('audio')).not.toHaveAttribute('src') + }) + }) }) diff --git a/src/index.tsx b/src/index.tsx index ab82cf4..92d7824 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -7,6 +7,7 @@ import React, { CSSProperties, ReactElement, Key, + Children, } from 'react' import { Icon } from '@iconify/react' import ProgressBar from './ProgressBar' @@ -183,7 +184,7 @@ class H5AudioPlayer extends Component { togglePlay = (e: React.SyntheticEvent): void => { e.stopPropagation() const audio = this.audio.current - if ((audio.paused || audio.ended) && audio.src) { + if ((audio.paused || audio.ended) && (audio.src || audio.currentSrc)) { this.playAudioPromise() } else if (!audio.paused) { audio.pause() @@ -663,8 +664,26 @@ class H5AudioPlayer extends Component { } componentDidUpdate(prevProps: PlayerProps): void { - const { src, autoPlayAfterSrcChange } = this.props + const { src, autoPlayAfterSrcChange, children } = this.props + + let isAudioSrcChanged = false + if (prevProps.src !== src) { + isAudioSrcChanged = true + } else if (children && Children.count(children) > 0) { + const prevSrcs: string[] = [] + Children.toArray(prevProps.children).forEach((c) => { + if (isValidElement(c) && c.type === 'source') { + prevSrcs.push(c.key) + } + }) + + isAudioSrcChanged = Children.toArray(children).some( + (c) => isValidElement(c) && c.type === 'source' && !prevSrcs.includes(c.key) + ) + } + + if (isAudioSrcChanged) { if (autoPlayAfterSrcChange) { this.playAudioPromise() } else { diff --git a/stories/edge-cases.stories.js b/stories/edge-cases.stories.js index afb73bb..c4c5aac 100644 --- a/stories/edge-cases.stories.js +++ b/stories/edge-cases.stories.js @@ -1,5 +1,5 @@ import { action } from "storybook/actions"; -import { SAMPLE_MP3_URL } from "./utils"; +import { BRAHMS_FLAC_URL, BRAHMS_MP3_URL } from "./utils"; import AudioPlayer from "../src/index.tsx"; import React from "react"; @@ -40,3 +40,14 @@ export const InvalidSrc = { name: "Invalid src", }; + +export const ChildSourceElements = { + render: () => ( + + + + + ), + + name: "Source elements", +}; diff --git a/stories/playlist.stories.js b/stories/playlist.stories.js index a245826..55dc3d2 100644 --- a/stories/playlist.stories.js +++ b/stories/playlist.stories.js @@ -1,14 +1,47 @@ -import { SAMPLE_MP3_URL } from "./utils"; -import AudioPlayer from "../src/index.tsx"; +import { + SAMPLE_MP3_URL, + SAMPLE_MP3_URL_B, + SAMPLE_MP3_URL_C, + BRAHMS_OGG_URL, + BRAHMS_MP3_URL, + BRAHMS_FLAC_URL, + MOZART_OGG_URL, + MOZART_MP3_URL, + MOZART_FLAC_URL, +} from './utils.js' import PlayList from "./playlist.tsx"; import React from "react"; +const singleSourcePlaylist = [ + SAMPLE_MP3_URL, + SAMPLE_MP3_URL_B, + SAMPLE_MP3_URL_C, +] + +const multiSourcePlaylist = [ + [ + { src: BRAHMS_OGG_URL, type: 'audio/ogg' }, + { src: BRAHMS_MP3_URL, type: 'audio/mpeg' }, + { src: BRAHMS_FLAC_URL, type: 'audio/flac' }, + ], + [ + { src: MOZART_OGG_URL, type: 'audio/ogg' }, + { src: MOZART_MP3_URL, type: 'audio/mpeg' }, + { src: MOZART_FLAC_URL, type: 'audio/flac' }, + ] +] + export default { title: "Play List", component: PlayList, }; export const Playlist = { - render: () => , - name: "playlist", + render: () => , + name: "Playlist", +}; + +export const PlaylistWithSourceElements = { + render: () => , + name: "Playlist with elements", }; diff --git a/stories/playlist.tsx b/stories/playlist.tsx index c170758..cbcc754 100644 --- a/stories/playlist.tsx +++ b/stories/playlist.tsx @@ -1,47 +1,64 @@ import React, { Component } from 'react' import AudioPlayer from '../src/index' -const playlist = [ - { name: 'SoundHelix-Song-9', src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' }, - { name: 'SoundHelix-Song-4', src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3' }, - { name: 'SoundHelix-Song-8', src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3' }, -] +interface AudioSource { + src: string + type?: string +} + +interface PlaylistProps { + playlist: string[] | AudioSource[][] +} interface PlayListState { currentMusicIndex: number } -class PlayList extends Component { +class PlayList extends Component { + playlist: string[] | AudioSource[][] + state = { currentMusicIndex: 0, } + constructor(props: PlaylistProps) { + super(props) + this.playlist = props.playlist + } + handleClickPrevious = (): void => { this.setState((prevState) => ({ - currentMusicIndex: prevState.currentMusicIndex === 0 ? playlist.length - 1 : prevState.currentMusicIndex - 1, + currentMusicIndex: prevState.currentMusicIndex === 0 ? this.playlist.length - 1 : prevState.currentMusicIndex - 1, })) } handleClickNext = (): void => { this.setState((prevState) => ({ - currentMusicIndex: prevState.currentMusicIndex < playlist.length - 1 ? prevState.currentMusicIndex + 1 : 0, + currentMusicIndex: prevState.currentMusicIndex < this.playlist.length - 1 ? prevState.currentMusicIndex + 1 : 0, })) } render(): React.ReactNode { const { currentMusicIndex } = this.state + const track = this.playlist[currentMusicIndex] + const singleStringSrc = typeof track === 'string' ? track : null + const multipleSrcs: AudioSource[] | null = Array.isArray(track) ? track : null + return (

currentMusicIndex: {currentMusicIndex}

+ + {...(singleStringSrc ? { src: singleStringSrc } : {})} + > + {multipleSrcs && multipleSrcs.map(({ src, type }) => )} +
) } diff --git a/stories/utils.js b/stories/utils.js index 9479420..f4b8f46 100644 --- a/stories/utils.js +++ b/stories/utils.js @@ -1 +1,11 @@ -export const SAMPLE_MP3_URL = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' +export const SAMPLE_MP3_URL = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-9.mp3' +export const SAMPLE_MP3_URL_B = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3' +export const SAMPLE_MP3_URL_C = 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3' + +export const BRAHMS_OGG_URL = 'https://archive.org/download/MusopenCollectionAsFlac/Brahms_SymphonyNo.3inFMajor/JohannesBrahms-SymphonyNo.3InFMajorOp.90-03-PocoAllegretto.ogg' +export const BRAHMS_MP3_URL = 'https://archive.org/download/MusopenCollectionAsFlac/Brahms_SymphonyNo.3inFMajor/JohannesBrahms-SymphonyNo.3InFMajorOp.90-03-PocoAllegretto.mp3' +export const BRAHMS_FLAC_URL = 'https://archive.org/download/MusopenCollectionAsFlac/Brahms_SymphonyNo.3inFMajor/JohannesBrahms-SymphonyNo.3InFMajorOp.90-03-PocoAllegretto.flac' + +export const MOZART_OGG_URL = 'https://archive.org/download/MusopenCollectionAsFlac/Mozart_StringQuartetNo.15inDMinorK421/WolfgangAmadeusMozart-StringQuartetNo.15InDMinorK421-03-Minuetto.ogg' +export const MOZART_MP3_URL = 'https://archive.org/download/MusopenCollectionAsFlac/Mozart_StringQuartetNo.15inDMinorK421/WolfgangAmadeusMozart-StringQuartetNo.15InDMinorK421-03-Minuetto.mp3' +export const MOZART_FLAC_URL = 'https://archive.org/download/MusopenCollectionAsFlac/Mozart_StringQuartetNo.15inDMinorK421/WolfgangAmadeusMozart-StringQuartetNo.15InDMinorK421-03-Minuetto.flac'