Skip to content

Commit

Permalink
Add Twitch player
Browse files Browse the repository at this point in the history
  • Loading branch information
cookpete committed Sep 2, 2017
1 parent 286e947 commit 288c18d
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 6 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ReactPlayer
[![Dependency Status](https://img.shields.io/david/CookPete/react-player.svg)](https://david-dm.org/CookPete/react-player)
[![devDependency Status](https://img.shields.io/david/dev/CookPete/react-player.svg)](https://david-dm.org/CookPete/react-player?type=dev)

A react component for playing a variety of URLs, including file paths, YouTube, Facebook, SoundCloud, Streamable, Vidme, Vimeo, Wistia and DailyMotion. Used by [rplayr](http://rplayr.com), an app to generate playlists from Reddit URLs.
A react component for playing a variety of URLs, including file paths, YouTube, Facebook, Twitch, SoundCloud, Streamable, Vidme, Vimeo, Wistia and DailyMotion.

The component parses a URL and loads in the appropriate markup and external SDKs to play media from [various sources](#supported-media). [Props](#props) can be passed in to control playback and react to events such as buffering or media ending.

Expand Down Expand Up @@ -180,6 +180,7 @@ Prop | Description
* Vidme videos are [resolved](https://docs.vid.me/#api-Video-DetailByURL) and played in a [`<video>`](https://developer.mozilla.org/en/docs/Web/HTML/Element/video) element using the track’s `complete_url` path
* Vimeo videos use the [Vimeo Player API](https://developer.vimeo.com/player/js-api)
* 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)
* [Supported file types](https://github.com/CookPete/react-player/blob/master/src/players/FilePlayer.js#L5-L6) are playing using [`<video>`](https://developer.mozilla.org/en/docs/Web/HTML/Element/video) or [`<audio>`](https://developer.mozilla.org/en/docs/Web/HTML/Element/audio) elements
* HLS streams are played using [hls.js](https://github.com/video-dev/hls.js)
Expand Down
3 changes: 3 additions & 0 deletions src/ReactPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Streamable from './players/Streamable'
import Vidme from './players/Vidme'
import Wistia from './players/Wistia'
import DailyMotion from './players/DailyMotion'
import Twitch from './players/Twitch'

export default class ReactPlayer extends Component {
static displayName = 'ReactPlayer'
Expand Down Expand Up @@ -98,6 +99,8 @@ export default class ReactPlayer extends Component {
players.push(Vidme)
} else if (Wistia.canPlay(url)) {
players.push(Wistia)
} else if (Twitch.canPlay(url)) {
players.push(Twitch)
} else if (url) {
// Fall back to FilePlayer if nothing else can play the URL
players.push(FilePlayer)
Expand Down
8 changes: 8 additions & 0 deletions src/demo/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ export default class App extends Component {
{this.renderLoadButton('https://vimeo.com/169599296', 'Test B')}
</td>
</tr>
<tr>
<th>Twitch</th>
<td>
{this.renderLoadButton('https://www.twitch.tv/videos/28946623', 'Test A')}
{this.renderLoadButton('https://www.twitch.tv/videos/12783852', 'Test B')}
{this.renderLoadButton('https://www.twitch.tv/kronovi', 'Test C')}
</td>
</tr>
<tr>
<th>Streamable</th>
<td>
Expand Down
6 changes: 1 addition & 5 deletions src/players/Facebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react'
import loadScript from 'load-script'

import Base from './Base'
import { randomString } from '../utils'

const SDK_URL = '//connect.facebook.net/en_US/sdk.js'
const SDK_GLOBAL = 'FB'
Expand Down Expand Up @@ -116,8 +117,3 @@ export default class YouTube extends Base {
)
}
}

// http://stackoverflow.com/a/38622545
function randomString () {
return Math.random().toString(36).substr(2, 5)
}
115 changes: 115 additions & 0 deletions src/players/Twitch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from 'react'
import loadScript from 'load-script'

import Base from './Base'
import { randomString } from '../utils'

const SDK_URL = '//player.twitch.tv/js/embed/v1.js'
const SDK_GLOBAL = 'Twitch'
const MATCH_VIDEO_URL = /^(?:https?:\/\/)?(?:www\.)twitch\.tv\/videos\/(\d+)($|\?)/
const MATCH_CHANNEL_URL = /^(?:https?:\/\/)?(?:www\.)twitch\.tv\/([a-z0-9_]+)($|\?)/
const PLAYER_ID_PREFIX = 'twitch-player-'

export default class YouTube extends Base {
static displayName = 'Twitch'
static canPlay (url) {
return MATCH_VIDEO_URL.test(url) || MATCH_CHANNEL_URL.test(url)
}
playerID = PLAYER_ID_PREFIX + randomString()
getSDK () {
if (window[SDK_GLOBAL]) {
return Promise.resolve(window[SDK_GLOBAL])
}
return new Promise((resolve, reject) => {
loadScript(SDK_URL, err => {
if (err) reject(err)
resolve(window[SDK_GLOBAL])
})
})
}
load (url) {
const { playsinline, onError } = this.props
const isChannel = MATCH_CHANNEL_URL.test(url)
const id = isChannel ? url.match(MATCH_CHANNEL_URL)[1] : url.match(MATCH_VIDEO_URL)[1]
if (this.isReady) {
if (isChannel) {
this.player.setChannel(id)
} else {
this.player.setVideo('v' + id)
}
return
}
if (this.loadingSDK) {
this.loadOnReady = url
return
}
this.loadingSDK = true
this.getSDK().then(Twitch => {
this.player = new Twitch.Player(this.playerID, {
video: isChannel ? '' : id,
channel: isChannel ? id : '',
height: '100%',
width: '100%',
playsinline: playsinline
})
const { READY, PLAY, PAUSE, ENDED } = Twitch.Player
this.player.addEventListener(READY, this.onReady)
this.player.addEventListener(PLAY, this.onPlay)
this.player.addEventListener(PAUSE, this.props.onPause)
this.player.addEventListener(ENDED, this.onEnded)
}, onError)
}
onEnded = () => {
const { loop, onEnded } = this.props
if (loop) {
this.seekTo(0)
}
onEnded()
}
call (method, ...args) {
if (!this.isReady || !this.player || !this.player[method]) return
return this.player[method](...args)
}
play () {
this.call('play')
}
pause () {
this.call('pause')
}
stop () {
this.call('pause')
}
seekTo (amount) {
const seconds = super.seekTo(amount)
this.call('seek', seconds)
}
setVolume (fraction) {
this.call('setVolume', fraction)
}
setPlaybackRate (rate) {
return null
}
getDuration () {
return this.call('getDuration')
}
getFractionPlayed () {
const time = this.call('getCurrentTime')
const duration = this.getDuration()
if (time && duration) {
return time / duration
}
return null
}
getFractionLoaded () {
return null
}
render () {
const style = {
width: '100%',
height: '100%'
}
return (
<div style={style} id={this.playerID} />
)
}
}
5 changes: 5 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,8 @@ function parseStartStamp (stamp) {
}
return seconds
}

// http://stackoverflow.com/a/38622545
export function randomString () {
return Math.random().toString(36).substr(2, 5)
}

0 comments on commit 288c18d

Please sign in to comment.