Skip to content

Commit

Permalink
feat: add Mux player (#1748)
Browse files Browse the repository at this point in the history
* feat: add Mux player

* docs: add Mux player docs

* fix(mux-player): add configurable version
  • Loading branch information
luwes committed Feb 28, 2024
1 parent 34c8f60 commit 62dabf1
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ Key | Options
`facebook` | `appId`: Your own [Facebook app ID](https://developers.facebook.com/docs/apps/register#app-id)<br />`version`: Facebook SDK version<br />`playerId`: Override player ID for consistent server-side rendering (use with [`react-uid`](https://github.com/thearnica/react-uid))<br />`attributes`: Extra data attributes to pass to the `fb-video` element
`soundcloud` | `options`: Override the [default player options](https://developers.soundcloud.com/docs/api/html5-widget#params)
`vimeo` | `playerOptions`: Override the [default params](https://developer.vimeo.com/player/sdk/embed)<br />`title`: Set the player `iframe` title attribute
`mux` | `attributes`: Apply [element attributes](https://github.com/muxinc/elements/blob/main/packages/mux-player/REFERENCE.md#attributes)<br />`version`: Mux player version
`wistia` | `options`: Override the [default player options](https://wistia.com/doc/embed-options#options_list)<br />`playerId`: Override player ID for consistent server-side rendering (use with [`react-uid`](https://github.com/thearnica/react-uid))
`mixcloud` | `options`: Override the [default player options](https://www.mixcloud.com/developers/widget/#methods)
`dailymotion` | `params`: Override the [default player vars](https://developer.dailymotion.com/player#player-parameters)
Expand Down Expand Up @@ -327,8 +328,8 @@ ReactPlayer `v2.0` changes single player imports and adds lazy loading players.
* Facebook videos use the [Facebook Embedded Video Player API](https://developers.facebook.com/docs/plugins/embedded-video-player/api)
* SoundCloud tracks use the [SoundCloud Widget API](https://developers.soundcloud.com/docs/api/html5-widget)
* Streamable videos use [`Player.js`](https://github.com/embedly/player.js)
* Vidme videos are [no longer supported](https://medium.com/vidme/goodbye-for-now-120b40becafa)
* Vimeo videos use the [Vimeo Player API](https://developer.vimeo.com/player/sdk)
* Mux videos use the [`<mux-player>`](https://github.com/muxinc/elements/blob/main/packages/mux-player/README.md) element
* Wistia videos use the [Wistia Player API](https://wistia.com/doc/player-api)
* Twitch videos use the [Twitch Interactive Frames API](https://dev.twitch.tv/docs/embed#interactive-frames-for-live-streams-and-vods)
* DailyMotion videos use the [DailyMotion Player API](https://developer.dailymotion.com/player)
Expand Down
7 changes: 7 additions & 0 deletions examples/react/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,13 @@ class App extends Component {
{this.renderLoadButton('https://vimeo.com/169599296', 'Test B')}
</td>
</tr>
<tr>
<th>Mux</th>
<td>
{this.renderLoadButton('https://stream.mux.com/maVbJv2GSYNRgS02kPXOOGdJMWGU1mkA019ZUjYE7VU7k', 'Test A')}
{this.renderLoadButton('https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008', 'Test B')}
</td>
</tr>
<tr>
<th>Twitch</th>
<td>
Expand Down
2 changes: 2 additions & 0 deletions src/patterns.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isMediaStream, isBlobUrl } from './utils'
export const MATCH_URL_YOUTUBE = /(?:youtu\.be\/|youtube(?:-nocookie|education)?\.com\/(?:embed\/|v\/|watch\/|watch\?v=|watch\?.+&v=|shorts\/|live\/))((\w|-){11})|youtube\.com\/playlist\?list=|youtube\.com\/user\//
export const MATCH_URL_SOUNDCLOUD = /(?:soundcloud\.com|snd\.sc)\/[^.]+$/
export const MATCH_URL_VIMEO = /vimeo\.com\/(?!progressive_redirect).+/
export const MATCH_URL_MUX = /stream\.mux\.com\/(\w+)/
export const MATCH_URL_FACEBOOK = /^https?:\/\/(www\.)?facebook\.com.*\/(video(s)?|watch|story)(\.php?|\/).+$/
export const MATCH_URL_FACEBOOK_WATCH = /^https?:\/\/fb\.watch\/.+$/
export const MATCH_URL_STREAMABLE = /streamable\.com\/([a-z0-9]+)$/
Expand Down Expand Up @@ -52,6 +53,7 @@ export const canPlay = {
},
soundcloud: url => MATCH_URL_SOUNDCLOUD.test(url) && !AUDIO_EXTENSIONS.test(url),
vimeo: url => MATCH_URL_VIMEO.test(url) && !VIDEO_EXTENSIONS.test(url) && !HLS_EXTENSIONS.test(url),
mux: url => MATCH_URL_MUX.test(url),
facebook: url => MATCH_URL_FACEBOOK.test(url) || MATCH_URL_FACEBOOK_WATCH.test(url),
streamable: url => MATCH_URL_STREAMABLE.test(url),
wistia: url => MATCH_URL_WISTIA.test(url),
Expand Down
210 changes: 210 additions & 0 deletions src/players/Mux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { Component } from 'react'

import { canPlay, MATCH_URL_MUX } from '../patterns'

const SDK_URL = 'https://cdn.jsdelivr.net/npm/@mux/mux-player@VERSION/dist/mux-player.mjs'

export default class Mux extends Component {
static displayName = 'Mux'
static canPlay = canPlay.mux

componentDidMount () {
this.props.onMount && this.props.onMount(this)
this.addListeners(this.player)
const playbackId = this.getPlaybackId(this.props.url) // Ensure src is set in strict mode
if (playbackId) {
this.player.playbackId = playbackId
}
}

componentWillUnmount () {
this.player.playbackId = null
this.removeListeners(this.player)
}

addListeners (player) {
const { playsinline } = this.props
player.addEventListener('play', this.onPlay)
player.addEventListener('waiting', this.onBuffer)
player.addEventListener('playing', this.onBufferEnd)
player.addEventListener('pause', this.onPause)
player.addEventListener('seeked', this.onSeek)
player.addEventListener('ended', this.onEnded)
player.addEventListener('error', this.onError)
player.addEventListener('ratechange', this.onPlayBackRateChange)
player.addEventListener('enterpictureinpicture', this.onEnablePIP)
player.addEventListener('leavepictureinpicture', this.onDisablePIP)
player.addEventListener('webkitpresentationmodechanged', this.onPresentationModeChange)
player.addEventListener('canplay', this.onReady)
if (playsinline) {
player.setAttribute('playsinline', '')
}
}

removeListeners (player) {
player.removeEventListener('canplay', this.onReady)
player.removeEventListener('play', this.onPlay)
player.removeEventListener('waiting', this.onBuffer)
player.removeEventListener('playing', this.onBufferEnd)
player.removeEventListener('pause', this.onPause)
player.removeEventListener('seeked', this.onSeek)
player.removeEventListener('ended', this.onEnded)
player.removeEventListener('error', this.onError)
player.removeEventListener('ratechange', this.onPlayBackRateChange)
player.removeEventListener('enterpictureinpicture', this.onEnablePIP)
player.removeEventListener('leavepictureinpicture', this.onDisablePIP)
player.removeEventListener('canplay', this.onReady)
}

// Proxy methods to prevent listener leaks
onReady = (...args) => this.props.onReady(...args)
onPlay = (...args) => this.props.onPlay(...args)
onBuffer = (...args) => this.props.onBuffer(...args)
onBufferEnd = (...args) => this.props.onBufferEnd(...args)
onPause = (...args) => this.props.onPause(...args)
onEnded = (...args) => this.props.onEnded(...args)
onError = (...args) => this.props.onError(...args)
onPlayBackRateChange = (event) => this.props.onPlaybackRateChange(event.target.playbackRate)
onEnablePIP = (...args) => this.props.onEnablePIP(...args)

onSeek = e => {
this.props.onSeek(e.target.currentTime)
}

async load (url) {
const { onError, config } = this.props

if (!globalThis.customElements?.get('mux-player')) {
try {
await import(SDK_URL.replace('VERSION', config.version))
this.props.onLoaded()
} catch (error) {
onError(error)
}
}

const [, id] = url.match(MATCH_URL_MUX)
this.player.playbackId = id
}

onDurationChange = () => {
const duration = this.getDuration()
this.props.onDuration(duration)
}

play () {
const promise = this.player.play()
if (promise) {
promise.catch(this.props.onError)
}
}

pause () {
this.player.pause()
}

stop () {
this.player.playbackId = null
}

seekTo (seconds, keepPlaying = true) {
this.player.currentTime = seconds
if (!keepPlaying) {
this.pause()
}
}

setVolume (fraction) {
this.player.volume = fraction
}

mute = () => {
this.player.muted = true
}

unmute = () => {
this.player.muted = false
}

enablePIP () {
if (this.player.requestPictureInPicture && document.pictureInPictureElement !== this.player) {
this.player.requestPictureInPicture()
}
}

disablePIP () {
if (document.exitPictureInPicture && document.pictureInPictureElement === this.player) {
document.exitPictureInPicture()
}
}

setPlaybackRate (rate) {
try {
this.player.playbackRate = rate
} catch (error) {
this.props.onError(error)
}
}

getDuration () {
if (!this.player) return null
const { duration, seekable } = this.player
// on iOS, live streams return Infinity for the duration
// so instead we use the end of the seekable timerange
if (duration === Infinity && seekable.length > 0) {
return seekable.end(seekable.length - 1)
}
return duration
}

getCurrentTime () {
if (!this.player) return null
return this.player.currentTime
}

getSecondsLoaded () {
if (!this.player) return null
const { buffered } = this.player
if (buffered.length === 0) {
return 0
}
const end = buffered.end(buffered.length - 1)
const duration = this.getDuration()
if (end > duration) {
return duration
}
return end
}

getPlaybackId (url) {
const [, id] = url.match(MATCH_URL_MUX)
return id
}

ref = player => {
this.player = player
}

render () {
const { url, playing, loop, controls, muted, config, width, height } = this.props
const style = {
width: width === 'auto' ? width : '100%',
height: height === 'auto' ? height : '100%'
}
if (controls === false) {
style['--controls'] = 'none'
}
return (
<mux-player
ref={this.ref}
playback-id={this.getPlaybackId(url)}
style={style}
preload='auto'
autoPlay={playing || undefined}
muted={muted ? '' : undefined}
loop={loop ? '' : undefined}
{...config.attributes}
/>
)
}
}
6 changes: 6 additions & 0 deletions src/players/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export default [
canPlay: canPlay.vimeo,
lazyPlayer: lazy(() => import(/* webpackChunkName: 'reactPlayerVimeo' */'./Vimeo'))
},
{
key: 'mux',
name: 'Mux',
canPlay: canPlay.mux,
lazyPlayer: lazy(() => import(/* webpackChunkName: 'reactPlayerMux' */'./Mux'))
},
{
key: 'facebook',
name: 'Facebook',
Expand Down
8 changes: 8 additions & 0 deletions src/props.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export const propTypes = {
playerOptions: object,
title: string
}),
mux: shape({
attributes: object,
version: string
}),
file: shape({
attributes: object,
tracks: array,
Expand Down Expand Up @@ -165,6 +169,10 @@ export const defaultProps = {
},
title: null
},
mux: {
attributes: {},
version: '2'
},
file: {
attributes: {},
tracks: [],
Expand Down

0 comments on commit 62dabf1

Please sign in to comment.