From 7908463d2dc1d3f59910432edd45460da152d426 Mon Sep 17 00:00:00 2001 From: Pete Cook Date: Tue, 26 Jan 2016 10:37:31 +0000 Subject: [PATCH] Support YouTube start time param --- src/players/YouTube.js | 11 ++++++--- src/utils.js | 32 +++++++++++++++++++++++++ test/karma/ReactPlayer.js | 9 +++++++- test/mocha/parseStartTime.js | 45 ++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 src/utils.js create mode 100644 test/mocha/parseStartTime.js diff --git a/src/players/YouTube.js b/src/players/YouTube.js index 120e8455..6480d702 100644 --- a/src/players/YouTube.js +++ b/src/players/YouTube.js @@ -2,6 +2,7 @@ import React from 'react' import loadScript from 'load-script' import Base from './Base' +import { parseStartTime } from '../utils' const SDK_URL = '//www.youtube.com/iframe_api' const SDK_GLOBAL = 'YT' @@ -48,7 +49,10 @@ export default class YouTube extends Base { load (url) { const id = url && url.match(MATCH_URL)[1] if (this.isReady) { - this.player.cueVideoById(id) + this.player.cueVideoById({ + videoId: id, + startSeconds: parseStartTime(url) + }) return } if (this.loadingSDK) { @@ -63,7 +67,8 @@ export default class YouTube extends Base { videoId: id, playerVars: { ...DEFAULT_PLAYER_VARS, - ...this.props.youtubeConfig.playerVars + ...this.props.youtubeConfig.playerVars, + start: parseStartTime(url) }, events: { onReady: () => { @@ -110,7 +115,7 @@ export default class YouTube extends Base { return this.player.getDuration() } getFractionPlayed () { - if (!this.isReady || !this.player.getCurrentTime) return null + if (!this.isReady || !this.getDuration()) return null return this.player.getCurrentTime() / this.getDuration() } getFractionLoaded () { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..98e62f8a --- /dev/null +++ b/src/utils.js @@ -0,0 +1,32 @@ +const MATCH_START_QUERY = /[\?&#](?:start|t)=([0-9hms]+)/ +const MATCH_START_STAMP = /(\d+)(h|m|s)/g +const MATCH_NUMERIC = /^\d+$/ + +// Parse YouTube URL for a start time param, ie ?t=1h14m30s +// and return the start time in seconds +export function parseStartTime (url) { + const match = url.match(MATCH_START_QUERY) + if (match) { + const stamp = match[1] + if (stamp.match(MATCH_START_STAMP)) { + return parseStartStamp(stamp) + } + if (MATCH_NUMERIC.test(stamp)) { + return parseInt(stamp, 10) + } + } + return 0 +} + +function parseStartStamp (stamp) { + let seconds = 0 + let array = MATCH_START_STAMP.exec(stamp) + while (array !== null) { + const [, count, period] = array + if (period === 'h') seconds += parseInt(count, 10) * 60 * 60 + if (period === 'm') seconds += parseInt(count, 10) * 60 + if (period === 's') seconds += parseInt(count, 10) + array = MATCH_START_STAMP.exec(stamp) + } + return seconds +} diff --git a/test/karma/ReactPlayer.js b/test/karma/ReactPlayer.js index 153b9fc6..f60686d7 100644 --- a/test/karma/ReactPlayer.js +++ b/test/karma/ReactPlayer.js @@ -4,7 +4,7 @@ import { render, unmountComponentAtNode } from 'react-dom' import ReactPlayer from '../../src/ReactPlayer' const { describe, it, beforeEach, afterEach } = window -const TEST_YOUTUBE_URL = 'https://www.youtube.com/watch?v=GlCmAC4MHek' +const TEST_YOUTUBE_URL = 'https://www.youtube.com/watch?v=M7lc1UVf-VE' const TEST_SOUNDCLOUD_URL = 'https://soundcloud.com/miami-nights-1984/accelerated' const TEST_VIMEO_URL = 'https://vimeo.com/90509568' const TEST_FILE_URL = 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.ogv' @@ -71,6 +71,13 @@ describe('ReactPlayer', () => { it('fires onError for Vimeo video', done => testError(TEST_VIMEO_ERROR, done)) it('fires onError for file', done => testError(TEST_FILE_ERROR, done)) + it('plays YouTube video at a specified time', done => { + const onProgress = state => { + if (state.played > 0.9) done() + } + render(, div) + }) + it('switches between media', function (done) { const renderFilePlayer = () => testPlay(TEST_FILE_URL, done) const renderVimeoPlayer = () => testPlay(TEST_VIMEO_URL, renderFilePlayer) diff --git a/test/mocha/parseStartTime.js b/test/mocha/parseStartTime.js new file mode 100644 index 00000000..8d7e20db --- /dev/null +++ b/test/mocha/parseStartTime.js @@ -0,0 +1,45 @@ +import { describe, it } from 'mocha' +import { expect } from 'chai' + +import { parseStartTime } from '../../src/utils' + +const YOUTUBE_URL = 'http://youtu.be/12345678901' + +describe('parseStartTime', () => { + it('parses seconds', () => { + expect(parseStartTime(YOUTUBE_URL + '?start=162')).to.equal(162) + }) + + it('parses stamps', () => { + expect(parseStartTime(YOUTUBE_URL + '?start=48s')).to.equal(48) + expect(parseStartTime(YOUTUBE_URL + '?start=3m15s')).to.equal(195) + expect(parseStartTime(YOUTUBE_URL + '?start=1h36m17s')).to.equal(5777) + }) + + it('parses with other params', () => { + expect(parseStartTime(YOUTUBE_URL + '?param=1&start=32')).to.equal(32) + }) + + it('parses using t', () => { + expect(parseStartTime(YOUTUBE_URL + '?t=32')).to.equal(32) + }) + + it('parses using a hash', () => { + expect(parseStartTime(YOUTUBE_URL + '#t=32')).to.equal(32) + expect(parseStartTime(YOUTUBE_URL + '#start=32')).to.equal(32) + }) + + it('parses using a hash', () => { + expect(parseStartTime(YOUTUBE_URL + '#t=32')).to.equal(32) + expect(parseStartTime(YOUTUBE_URL + '#start=32')).to.equal(32) + }) + + it('returns 0 for invalid stamps', () => { + expect(parseStartTime(YOUTUBE_URL)).to.equal(0) + expect(parseStartTime(YOUTUBE_URL + '?start=')).to.equal(0) + expect(parseStartTime(YOUTUBE_URL + '?start=hms')).to.equal(0) + expect(parseStartTime(YOUTUBE_URL + '?start=invalid')).to.equal(0) + expect(parseStartTime(YOUTUBE_URL + '?strat=32')).to.equal(0) + expect(parseStartTime(YOUTUBE_URL + '#s=32')).to.equal(0) + }) +})