Skip to content

Commit 62dabf1

Browse files
authored
feat: add Mux player (#1748)
* feat: add Mux player * docs: add Mux player docs * fix(mux-player): add configurable version
1 parent 34c8f60 commit 62dabf1

File tree

6 files changed

+235
-1
lines changed

6 files changed

+235
-1
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ Key | Options
141141
`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
142142
`soundcloud` | `options`: Override the [default player options](https://developers.soundcloud.com/docs/api/html5-widget#params)
143143
`vimeo` | `playerOptions`: Override the [default params](https://developer.vimeo.com/player/sdk/embed)<br />`title`: Set the player `iframe` title attribute
144+
`mux` | `attributes`: Apply [element attributes](https://github.com/muxinc/elements/blob/main/packages/mux-player/REFERENCE.md#attributes)<br />`version`: Mux player version
144145
`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))
145146
`mixcloud` | `options`: Override the [default player options](https://www.mixcloud.com/developers/widget/#methods)
146147
`dailymotion` | `params`: Override the [default player vars](https://developer.dailymotion.com/player#player-parameters)
@@ -327,8 +328,8 @@ ReactPlayer `v2.0` changes single player imports and adds lazy loading players.
327328
* Facebook videos use the [Facebook Embedded Video Player API](https://developers.facebook.com/docs/plugins/embedded-video-player/api)
328329
* SoundCloud tracks use the [SoundCloud Widget API](https://developers.soundcloud.com/docs/api/html5-widget)
329330
* Streamable videos use [`Player.js`](https://github.com/embedly/player.js)
330-
* Vidme videos are [no longer supported](https://medium.com/vidme/goodbye-for-now-120b40becafa)
331331
* Vimeo videos use the [Vimeo Player API](https://developer.vimeo.com/player/sdk)
332+
* Mux videos use the [`<mux-player>`](https://github.com/muxinc/elements/blob/main/packages/mux-player/README.md) element
332333
* Wistia videos use the [Wistia Player API](https://wistia.com/doc/player-api)
333334
* Twitch videos use the [Twitch Interactive Frames API](https://dev.twitch.tv/docs/embed#interactive-frames-for-live-streams-and-vods)
334335
* DailyMotion videos use the [DailyMotion Player API](https://developer.dailymotion.com/player)

examples/react/src/App.js

+7
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ class App extends Component {
298298
{this.renderLoadButton('https://vimeo.com/169599296', 'Test B')}
299299
</td>
300300
</tr>
301+
<tr>
302+
<th>Mux</th>
303+
<td>
304+
{this.renderLoadButton('https://stream.mux.com/maVbJv2GSYNRgS02kPXOOGdJMWGU1mkA019ZUjYE7VU7k', 'Test A')}
305+
{this.renderLoadButton('https://stream.mux.com/Sc89iWAyNkhJ3P1rQ02nrEdCFTnfT01CZ2KmaEcxXfB008', 'Test B')}
306+
</td>
307+
</tr>
301308
<tr>
302309
<th>Twitch</th>
303310
<td>

src/patterns.js

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { isMediaStream, isBlobUrl } from './utils'
33
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\//
44
export const MATCH_URL_SOUNDCLOUD = /(?:soundcloud\.com|snd\.sc)\/[^.]+$/
55
export const MATCH_URL_VIMEO = /vimeo\.com\/(?!progressive_redirect).+/
6+
export const MATCH_URL_MUX = /stream\.mux\.com\/(\w+)/
67
export const MATCH_URL_FACEBOOK = /^https?:\/\/(www\.)?facebook\.com.*\/(video(s)?|watch|story)(\.php?|\/).+$/
78
export const MATCH_URL_FACEBOOK_WATCH = /^https?:\/\/fb\.watch\/.+$/
89
export const MATCH_URL_STREAMABLE = /streamable\.com\/([a-z0-9]+)$/
@@ -52,6 +53,7 @@ export const canPlay = {
5253
},
5354
soundcloud: url => MATCH_URL_SOUNDCLOUD.test(url) && !AUDIO_EXTENSIONS.test(url),
5455
vimeo: url => MATCH_URL_VIMEO.test(url) && !VIDEO_EXTENSIONS.test(url) && !HLS_EXTENSIONS.test(url),
56+
mux: url => MATCH_URL_MUX.test(url),
5557
facebook: url => MATCH_URL_FACEBOOK.test(url) || MATCH_URL_FACEBOOK_WATCH.test(url),
5658
streamable: url => MATCH_URL_STREAMABLE.test(url),
5759
wistia: url => MATCH_URL_WISTIA.test(url),

src/players/Mux.js

+210
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import React, { Component } from 'react'
2+
3+
import { canPlay, MATCH_URL_MUX } from '../patterns'
4+
5+
const SDK_URL = 'https://cdn.jsdelivr.net/npm/@mux/mux-player@VERSION/dist/mux-player.mjs'
6+
7+
export default class Mux extends Component {
8+
static displayName = 'Mux'
9+
static canPlay = canPlay.mux
10+
11+
componentDidMount () {
12+
this.props.onMount && this.props.onMount(this)
13+
this.addListeners(this.player)
14+
const playbackId = this.getPlaybackId(this.props.url) // Ensure src is set in strict mode
15+
if (playbackId) {
16+
this.player.playbackId = playbackId
17+
}
18+
}
19+
20+
componentWillUnmount () {
21+
this.player.playbackId = null
22+
this.removeListeners(this.player)
23+
}
24+
25+
addListeners (player) {
26+
const { playsinline } = this.props
27+
player.addEventListener('play', this.onPlay)
28+
player.addEventListener('waiting', this.onBuffer)
29+
player.addEventListener('playing', this.onBufferEnd)
30+
player.addEventListener('pause', this.onPause)
31+
player.addEventListener('seeked', this.onSeek)
32+
player.addEventListener('ended', this.onEnded)
33+
player.addEventListener('error', this.onError)
34+
player.addEventListener('ratechange', this.onPlayBackRateChange)
35+
player.addEventListener('enterpictureinpicture', this.onEnablePIP)
36+
player.addEventListener('leavepictureinpicture', this.onDisablePIP)
37+
player.addEventListener('webkitpresentationmodechanged', this.onPresentationModeChange)
38+
player.addEventListener('canplay', this.onReady)
39+
if (playsinline) {
40+
player.setAttribute('playsinline', '')
41+
}
42+
}
43+
44+
removeListeners (player) {
45+
player.removeEventListener('canplay', this.onReady)
46+
player.removeEventListener('play', this.onPlay)
47+
player.removeEventListener('waiting', this.onBuffer)
48+
player.removeEventListener('playing', this.onBufferEnd)
49+
player.removeEventListener('pause', this.onPause)
50+
player.removeEventListener('seeked', this.onSeek)
51+
player.removeEventListener('ended', this.onEnded)
52+
player.removeEventListener('error', this.onError)
53+
player.removeEventListener('ratechange', this.onPlayBackRateChange)
54+
player.removeEventListener('enterpictureinpicture', this.onEnablePIP)
55+
player.removeEventListener('leavepictureinpicture', this.onDisablePIP)
56+
player.removeEventListener('canplay', this.onReady)
57+
}
58+
59+
// Proxy methods to prevent listener leaks
60+
onReady = (...args) => this.props.onReady(...args)
61+
onPlay = (...args) => this.props.onPlay(...args)
62+
onBuffer = (...args) => this.props.onBuffer(...args)
63+
onBufferEnd = (...args) => this.props.onBufferEnd(...args)
64+
onPause = (...args) => this.props.onPause(...args)
65+
onEnded = (...args) => this.props.onEnded(...args)
66+
onError = (...args) => this.props.onError(...args)
67+
onPlayBackRateChange = (event) => this.props.onPlaybackRateChange(event.target.playbackRate)
68+
onEnablePIP = (...args) => this.props.onEnablePIP(...args)
69+
70+
onSeek = e => {
71+
this.props.onSeek(e.target.currentTime)
72+
}
73+
74+
async load (url) {
75+
const { onError, config } = this.props
76+
77+
if (!globalThis.customElements?.get('mux-player')) {
78+
try {
79+
await import(SDK_URL.replace('VERSION', config.version))
80+
this.props.onLoaded()
81+
} catch (error) {
82+
onError(error)
83+
}
84+
}
85+
86+
const [, id] = url.match(MATCH_URL_MUX)
87+
this.player.playbackId = id
88+
}
89+
90+
onDurationChange = () => {
91+
const duration = this.getDuration()
92+
this.props.onDuration(duration)
93+
}
94+
95+
play () {
96+
const promise = this.player.play()
97+
if (promise) {
98+
promise.catch(this.props.onError)
99+
}
100+
}
101+
102+
pause () {
103+
this.player.pause()
104+
}
105+
106+
stop () {
107+
this.player.playbackId = null
108+
}
109+
110+
seekTo (seconds, keepPlaying = true) {
111+
this.player.currentTime = seconds
112+
if (!keepPlaying) {
113+
this.pause()
114+
}
115+
}
116+
117+
setVolume (fraction) {
118+
this.player.volume = fraction
119+
}
120+
121+
mute = () => {
122+
this.player.muted = true
123+
}
124+
125+
unmute = () => {
126+
this.player.muted = false
127+
}
128+
129+
enablePIP () {
130+
if (this.player.requestPictureInPicture && document.pictureInPictureElement !== this.player) {
131+
this.player.requestPictureInPicture()
132+
}
133+
}
134+
135+
disablePIP () {
136+
if (document.exitPictureInPicture && document.pictureInPictureElement === this.player) {
137+
document.exitPictureInPicture()
138+
}
139+
}
140+
141+
setPlaybackRate (rate) {
142+
try {
143+
this.player.playbackRate = rate
144+
} catch (error) {
145+
this.props.onError(error)
146+
}
147+
}
148+
149+
getDuration () {
150+
if (!this.player) return null
151+
const { duration, seekable } = this.player
152+
// on iOS, live streams return Infinity for the duration
153+
// so instead we use the end of the seekable timerange
154+
if (duration === Infinity && seekable.length > 0) {
155+
return seekable.end(seekable.length - 1)
156+
}
157+
return duration
158+
}
159+
160+
getCurrentTime () {
161+
if (!this.player) return null
162+
return this.player.currentTime
163+
}
164+
165+
getSecondsLoaded () {
166+
if (!this.player) return null
167+
const { buffered } = this.player
168+
if (buffered.length === 0) {
169+
return 0
170+
}
171+
const end = buffered.end(buffered.length - 1)
172+
const duration = this.getDuration()
173+
if (end > duration) {
174+
return duration
175+
}
176+
return end
177+
}
178+
179+
getPlaybackId (url) {
180+
const [, id] = url.match(MATCH_URL_MUX)
181+
return id
182+
}
183+
184+
ref = player => {
185+
this.player = player
186+
}
187+
188+
render () {
189+
const { url, playing, loop, controls, muted, config, width, height } = this.props
190+
const style = {
191+
width: width === 'auto' ? width : '100%',
192+
height: height === 'auto' ? height : '100%'
193+
}
194+
if (controls === false) {
195+
style['--controls'] = 'none'
196+
}
197+
return (
198+
<mux-player
199+
ref={this.ref}
200+
playback-id={this.getPlaybackId(url)}
201+
style={style}
202+
preload='auto'
203+
autoPlay={playing || undefined}
204+
muted={muted ? '' : undefined}
205+
loop={loop ? '' : undefined}
206+
{...config.attributes}
207+
/>
208+
)
209+
}
210+
}

src/players/index.js

+6
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ export default [
2020
canPlay: canPlay.vimeo,
2121
lazyPlayer: lazy(() => import(/* webpackChunkName: 'reactPlayerVimeo' */'./Vimeo'))
2222
},
23+
{
24+
key: 'mux',
25+
name: 'Mux',
26+
canPlay: canPlay.mux,
27+
lazyPlayer: lazy(() => import(/* webpackChunkName: 'reactPlayerMux' */'./Mux'))
28+
},
2329
{
2430
key: 'facebook',
2531
name: 'Facebook',

src/props.js

+8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ export const propTypes = {
5050
playerOptions: object,
5151
title: string
5252
}),
53+
mux: shape({
54+
attributes: object,
55+
version: string
56+
}),
5357
file: shape({
5458
attributes: object,
5559
tracks: array,
@@ -165,6 +169,10 @@ export const defaultProps = {
165169
},
166170
title: null
167171
},
172+
mux: {
173+
attributes: {},
174+
version: '2'
175+
},
168176
file: {
169177
attributes: {},
170178
tracks: [],

0 commit comments

Comments
 (0)