Twitch |
diff --git a/src/patterns.js b/src/patterns.js
index 8549eb49..25a96c84 100644
--- a/src/patterns.js
+++ b/src/patterns.js
@@ -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]+)$/
@@ -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),
diff --git a/src/players/Mux.js b/src/players/Mux.js
new file mode 100644
index 00000000..693f3048
--- /dev/null
+++ b/src/players/Mux.js
@@ -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 (
+
+ )
+ }
+}
diff --git a/src/players/index.js b/src/players/index.js
index f1c31bdc..ba2e25fe 100644
--- a/src/players/index.js
+++ b/src/players/index.js
@@ -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',
diff --git a/src/props.js b/src/props.js
index a029e293..62c9b17a 100644
--- a/src/props.js
+++ b/src/props.js
@@ -50,6 +50,10 @@ export const propTypes = {
playerOptions: object,
title: string
}),
+ mux: shape({
+ attributes: object,
+ version: string
+ }),
file: shape({
attributes: object,
tracks: array,
@@ -165,6 +169,10 @@ export const defaultProps = {
},
title: null
},
+ mux: {
+ attributes: {},
+ version: '2'
+ },
file: {
attributes: {},
tracks: [],
|