Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ lib/
es/
coverage/
.vscode/
.DS_Store

# yarn
.yarn/
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<source>` Elements

You can use child `<source>` 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
<AudioPlayer>
<source src="https://example.com/audio.aac" type="audio/aac" />
<source src="https://example.com/audio.ogg" type="audio/ogg" />
<source src="https://example.com/audio.mp3" type="audio/mpeg" />
<source src="https://example.com/audio.wav" type="audio/wav" />
</AudioPlayer>
```

When using `<source>` 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

<AudioPlayer>
{srcs[currentIndex].map(({ src, type }) => (
<source key={src} src={src} type={type} />
))}
</AudioPlayer>
```

## Release Notes

https://github.com/lhz516/react-h5-audio-player/releases
Expand Down
6 changes: 4 additions & 2 deletions src/ProgressBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,15 @@ class ProgressBar extends Component<ProgressBarProps, ProgressBarState> {
// 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%' }
}

Expand Down
18 changes: 18 additions & 0 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -702,4 +702,22 @@ describe('H5AudioPlayer', () => {
})
})
})

describe('Multiple Source Elements', () => {
it('renders child <source> elements', () => {
const srcAac = 'test-audio.aac'
const srcMp3 = 'test-audio.mp3'

const { container } = render(
<H5AudioPlayer>
<source src={srcAac} type="audio/aac" data-testid="aac" />
<source src={srcMp3} type="audio/mpeg" data-testid="mp3" />
</H5AudioPlayer>
)

expect(screen.getByTestId('aac')).toBeInTheDocument()
expect(screen.getByTestId('mp3')).toBeInTheDocument()
expect(container.querySelector('audio')).not.toHaveAttribute('src')
})
})
})
23 changes: 21 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, {
CSSProperties,
ReactElement,
Key,
Children,
} from 'react'
import { Icon } from '@iconify/react'
import ProgressBar from './ProgressBar'
Expand Down Expand Up @@ -183,7 +184,7 @@ class H5AudioPlayer extends Component<PlayerProps> {
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()
Expand Down Expand Up @@ -663,8 +664,26 @@ class H5AudioPlayer extends Component<PlayerProps> {
}

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) {
Copy link
Copy Markdown
Author

@mysticflute mysticflute Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not the best idea to check the children like this, but it does add to the convenience for autoplay.

alternatively i could remove it, leaving the dev to implement autoplay when using <source> elements, perhaps an approach with useEffect with the playlist index changes.

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 {
Expand Down
13 changes: 12 additions & 1 deletion stories/edge-cases.stories.js
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -40,3 +40,14 @@ export const InvalidSrc = {

name: "Invalid src",
};

export const ChildSourceElements = {
render: () => (
<AudioPlayer>
<source src={BRAHMS_FLAC_URL} type="audio/flac" />
<source src={BRAHMS_MP3_URL} type="audio/mpeg" />
</AudioPlayer>
),

name: "Source elements",
};
41 changes: 37 additions & 4 deletions stories/playlist.stories.js
Original file line number Diff line number Diff line change
@@ -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: () => <PlayList />,
name: "playlist",
render: () => <PlayList playlist={singleSourcePlaylist} />,
name: "Playlist",
};

export const PlaylistWithSourceElements = {
render: () => <PlayList playlist={multiSourcePlaylist} />,
name: "Playlist with <source> elements",
};
37 changes: 27 additions & 10 deletions stories/playlist.tsx
Original file line number Diff line number Diff line change
@@ -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<null, PlayListState> {
class PlayList extends Component<PlaylistProps, PlayListState> {
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 (
<div>
<p>currentMusicIndex: {currentMusicIndex}</p>

<AudioPlayer
onEnded={this.handleClickNext}
autoPlayAfterSrcChange={true}
showSkipControls={true}
showJumpControls={false}
src={playlist[currentMusicIndex].src}
onClickPrevious={this.handleClickPrevious}
onClickNext={this.handleClickNext}
/>
{...(singleStringSrc ? { src: singleStringSrc } : {})}
>
{multipleSrcs && multipleSrcs.map(({ src, type }) => <source key={src} src={src} type={type} />)}
</AudioPlayer>
</div>
)
}
Expand Down
12 changes: 11 additions & 1 deletion stories/utils.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.