Skip to content

Commit

Permalink
feat: add useYouTube hook
Browse files Browse the repository at this point in the history
  • Loading branch information
tjallingt committed Apr 19, 2020
1 parent 145d771 commit ca732d6
Show file tree
Hide file tree
Showing 8 changed files with 481 additions and 77 deletions.
46 changes: 0 additions & 46 deletions example/example.js

This file was deleted.

62 changes: 59 additions & 3 deletions example/src/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
import React, { useState } from 'react';
import React, { Fragment, useState } from 'react';
import ReactDOM from 'react-dom';
import YouTube from 'react-youtube';
import YouTube, { useYouTube } from 'react-youtube';

import './styles.css';

const VIDEOS = ['XxVg_s8xAms', '-DX3vJiqxm4'];

function YouTubeHookExample() {
const [videoIndex, setVideoIndex] = useState(0);
const [width, setWidth] = useState(600);
const [hidden, setHidden] = useState(false);
const [autoplay, setAutoplay] = useState(false);

const { targetRef, player } = useYouTube({
videoId: VIDEOS[videoIndex],
autoplay,
width,
height: width * (9 / 16),
});

return (
<div className="App">
<div style={{ display: 'flex', marginBottom: '1em' }}>
<button type="button" onClick={() => player.seekTo(120)}>
Seek to 2 minutes
</button>
<button type="button" onClick={() => setVideoIndex((videoIndex + 1) % VIDEOS.length)}>
Change video
</button>
<label>
<input
type="range"
min="300"
max="1080"
value={width}
onChange={(event) => setWidth(event.currentTarget.value)}
/>
Width ({width}px)
</label>
<button type="button" onClick={() => setHidden(!hidden)}>
{hidden ? 'Show' : 'Hide'}
</button>
<label>
<input
type="checkbox"
value={autoplay}
onChange={(event) => setAutoplay(event.currentTarget.checked === false)}
/>
Autoplaying
</label>
</div>

{hidden ? 'mysterious' : <div className="container" ref={targetRef} />}
</div>
);
}

function YouTubeComponentExample() {
const [player, setPlayer] = useState(0);
const [videoIndex, setVideoIndex] = useState(0);
Expand Down Expand Up @@ -57,4 +107,10 @@ function YouTubeComponentExample() {
);
}

ReactDOM.render(<YouTubeComponentExample />, document.getElementById('app'));
ReactDOM.render(
<Fragment>
<YouTubeComponentExample />
<YouTubeHookExample />
</Fragment>,
document.getElementById('app'),
);
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "7.9.0",
"description": "React.js powered YouTube player component",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"module": "dist/index.mjs",
"types": "index.d.ts",
"files": [
"dist",
Expand Down Expand Up @@ -78,8 +78,8 @@
"scripts": {
"test": "jest",
"test:ci": "jest --ci --runInBand",
"compile:cjs": "babel src/YouTube.js --out-file dist/index.js",
"compile:es": "cross-env BABEL_ENV=es babel src/YouTube.js --out-file dist/index.esm.js",
"compile:cjs": "babel src --out-dir dist",
"compile:es": "cross-env BABEL_ENV=es babel src --out-dir dist --out-file-extension .mjs",
"compile": "npm-run-all --parallel compile:*",
"prepublishOnly": "npm run compile",
"lint": "eslint src example",
Expand Down
19 changes: 19 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import YouTube from './YouTube.js';
import useYouTube from './useYouTube.js';

export default YouTube;
export { useYouTube };

/**
* Expose PlayerState constants for convenience. These constants can also be
* accessed through the global YT object after the YouTube IFrame API is instantiated.
* https://developers.google.com/youtube/iframe_api_reference#onStateChange
*/
export const PlayerState = {
UNSTARTED: -1,
ENDED: 0,
PLAYING: 1,
PAUSED: 2,
BUFFERING: 3,
CUED: 5,
};
226 changes: 226 additions & 0 deletions src/useYouTube.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { useState, useEffect, useRef } from 'react';

function loadScript(url) {
return new Promise((resolve, reject) => {
const script = Object.assign(document.createElement('script'), {
type: 'text/javascript',
charset: 'utf8',
src: url,
async: true,
onerror() {
reject(new Error(`Failed to load: ${url}`));
},
onload() {
resolve();
},
});

document.head.appendChild(script);
});
}

function loadYouTubeIframeApi() {
return new Promise((resolve, reject) => {
const previous = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = () => {
if (previous) previous();
resolve();
};

const protocol = window.location.protocol === 'http:' ? 'http:' : 'https:';
loadScript(`${protocol}//www.youtube.com/iframe_api`).catch(reject);
});
}

function getYouTubeApi() {
if (window.YT && window.YT.Player && window.YT.Player instanceof Function) {
return window.YT;
}
return null;
}

/*
Available options that you can pass to the YouTube Iframe API are
- videoId updated by Player#cueVideoById
- width updated by Player#setSize
- height updated by Player#setSize
- playerVars
- autoplay Player#loadVideoById/Playlist
- cc_lang_pref [static]
- cc_load_policy [static]
- color [static]
- controls [static]
- disablekb [static]
- enablejsapi [static]
- end Player#cue/load* should set endSeconds
- fs [static]
- hl [static]
- iv_load_policy [static]
- list [static]
- listType [static]
- loop updated by Player#setLoop
- modestbranding [static]
- origin [static]
- playlist updated by Player#cue/loadPlaylist
- playsinline [static]
- rel [static]
- start Player#cue/load* should set startSeconds
- widget_referrer [static]
- events [note]
- onReady
- onStateChange
- onPlaybackQualityChange
- onPlaybackRateChange
- onError
- onApiChange
[note] youtube-player fixes the very strange Player#addEventListener behaviour
but does this by overwriting the events property so we can't set these immediately.
*/

/*
type Config = {
videoId: string,
autoplay: boolean,
startSeconds: number,
endSeconds: number,
width: number,
height: number,
onReady: (event: any) => void,
onStateChange: (event: any) => void,
onPlaybackQualityChange: (event: any) => void,
onPlaybackRateChange: (event: any) => void,
onError: (event: any) => void,
onApiChange: (event: any) => void,
};
*/

export default function useYouTube(config, playerVars) {
const [YouTubeApi, setYouTubeApi] = useState(getYouTubeApi);

const [target, setTarget] = useState(null);
const [player, setPlayer] = useState(null);
const configRef = useRef(config);

useEffect(() => {
configRef.current = config;
}, [config]);

useEffect(() => {
if (target === null) return undefined;

const element = target.appendChild(document.createElement('div'));

// TODO: use suspense
if (YouTubeApi === null) {
loadYouTubeIframeApi()
.then(() => setYouTubeApi(getYouTubeApi))
.catch((error) => {
console.error(error);
// TODO: throw so it can be handled by an error boundary
});
return undefined;
}

// NOTE: The YouTube player replaces `element`.
// trying to access it after this point results in unexpected behaviour
const instance = new YouTubeApi.Player(element, {
videoId: configRef.current.videoId,
width: configRef.current.width,
height: configRef.current.height,
playerVars,
events: {
onReady(event) {
setPlayer(instance);

if (typeof configRef.current.onReady === 'function') {
configRef.current.onReady(event);
}
},
onStateChange(event) {
if (typeof configRef.current.onStateChange === 'function') {
configRef.current.onStateChange(event);
}
},
onPlaybackQualityChange(event) {
if (typeof configRef.current.onPlaybackQualityChange === 'function') {
configRef.current.onPlaybackQualityChange(event);
}
},
onPlaybackRateChange(event) {
if (typeof configRef.current.onPlaybackRateChange === 'function') {
configRef.current.onPlaybackRateChange(event);
}
},
onError(event) {
if (typeof configRef.current.onError === 'function') {
configRef.current.onError(event);
}
},
onApiChange(event) {
if (typeof configRef.current.onApiChange === 'function') {
configRef.current.onApiChange(event);
}
},
},
});

return () => {
instance.getIframe().remove();
// TODO: figure out why calling instance.destroy() causes cross origin errors
setPlayer(null);
};
}, [YouTubeApi, target, playerVars]);

// videoId, autoplay, startSeconds, endSeconds
useEffect(() => {
if (player === null) return;

if (!config.videoId) {
player.stopVideo();
return;
}

if (configRef.current.autoplay) {
player.loadVideoById({
videoId: config.videoId,
startSeconds: configRef.current.startSeconds,
endSeconds: configRef.current.endSeconds,
});
return;
}

player.cueVideoById({
videoId: config.videoId,
startSeconds: configRef.current.startSeconds,
endSeconds: configRef.current.endSeconds,
});
}, [player, config.videoId]);

// width, height
useEffect(() => {
if (player === null) return;

if (config.width !== undefined && config.height !== undefined) {
// calling setSize with width and height set to undefined
// makes the player smaller than the default
player.setSize(config.width, config.height);
}
}, [player, config.width, config.height]);

return {
player,
targetRef: setTarget,
};
}
2 changes: 1 addition & 1 deletion src/Youtube.test.js → tests/Youtube.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, queryByAttribute } from '@testing-library/react';
import YouTube from './YouTube';
import YouTube from '../src/YouTube';

import Player, { playerMock } from './__mocks__/youtube-player';

Expand Down
File renamed without changes.
Loading

0 comments on commit ca732d6

Please sign in to comment.