Skip to content
This repository has been archived by the owner on May 8, 2024. It is now read-only.

Commit

Permalink
First usable version
Browse files Browse the repository at this point in the history
  • Loading branch information
heff committed Apr 3, 2020
1 parent 360137e commit 89e40ac
Show file tree
Hide file tree
Showing 7 changed files with 2,980 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.DS_Store

# Logs
logs
*.log
Expand Down
51 changes: 50 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,51 @@
# youtube-video-element
# `<youtube-video>`
A custom element (web component) for the YouTube player.

The element API matches the HTML5 `<video>` tag, so it can be easily swapped with other media, and be compatible with other UI components that work with the video tag.

## Example

```html
<html>
<head>
<script type="module" src="https://unpkg.com/youtube-video-element"></script>
</head>
<body>

<youtube-video controls src="https://www.youtube.com/watch?v=rubNgGj3pYo"></youtube-video>

</body>
</html>
```

## Installing

`<youtube-video>` is packaged as a javascript module (es6) only, which is supported by all evergreen browsers and Node v12+.

### Loading into your HTML using `<script>`

Note the `type="module"`, that's important.

> Modules are always loaded asynchronously by the browser, so it's ok to load them in the head :thumbsup:, and best for registering web components quickly.
```html
<head>
<script type="module" src="https://unpkg.com/youtube-video-element"></script>
</head>
```

### Adding to your app via `npm`

```bash
npm install youtube-video-element --save
```
Or yarn
```bash
yarn add youtube-video-element
```

Include in your app javascript (e.g. src/App.js)
```js
import 'player-chrome';
```
This will register the custom elements with the browser so they can be used as HTML.
8 changes: 8 additions & 0 deletions example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<html>
<head>
<script type="module" src="./index.js"></script>
</head>
<body>
<youtube-video controls src="https://www.youtube.com/watch?v=rubNgGj3pYo"></youtube-video>
</body>
</html>
271 changes: 271 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
// Build the HTML/CSS of the element
const template = document.createElement('template');

template.innerHTML = `
<style>
:host {
display: inline-block;
box-sizing: border-box;
position: relative;
width: 640px;
height: 360px;
background-color: #000;
}
iframe,
iframeContainer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
overflow: hidden;
}
</style>
<div id="iframeContainer"></div>
`;

function getIframeTemplate(params) {
const { id } = params;
const controls = params.controls ? 1 : 0;
const template = document.createElement('template');

template.innerHTML = `
<iframe
id="player"
type="text/html"
src="https://www.youtube.com/embed/${id}?enablejsapi=1&modestbranding=1&rel=0&showinfo=0&controls=${controls}"
frameborder="0"
allowfullscreen
allow="accelerometer; autoplay; encrypted-media; fullscreen; gyroscope; picture-in-picture; xr-spatial-tracking"
></iframe>
`;

return template;
}

/*
This video had an issue where it would start to play but then go back to paused.
Wondering if it's some sort of playlist issue?
https://www.youtube.com/watch?v=M7lc1UVf-VE
*/

function getIdFromURL(url) {
const regExp = /^.*(?:(?:youtu\.be\/|v\/|vi\/|u\/\w\/|embed\/)|(?:(?:watch)?\?v(?:i)?=|\&v(?:i)?=))([^#\&\?]*).*/;
const match = url.match(regExp);
return (match && match[1]) ? match[1] : url;
}

class YoutubeVideoElement extends HTMLElement {
constructor() {
super();

this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));

const src = this.getAttribute('src');

if (src) {
this.load();
}
}

// Handle multiple players with one YT API load
static ytReady = false;
static ytReadyQueue = [];
static onYTReady = function(callback) {
if (this.ytReady) {
callback();
} else {
this.ytReadyQueue.push(callback);
}
};
static handleYoutubeAPILoad() {
this.ytReady = true;
this.ytReadyQueue.forEach((callback) => {
callback();
});
this.ytReadyQueue = [];
}

load() {
// Destroy previous videos
this.ytPlayer = null;
this.shadowRoot.querySelector('#iframeContainer').innerHTML = '';

const src = this.getAttribute('src');

if (!src) {
// Should throw an code 4 error. We'll get ot that.
console.error('YoutubeVideoElement: No src was set when load() was called.');
return;
}

const iframeTemplate = getIframeTemplate({
id: getIdFromURL(src),
controls: !!this.hasAttribute('controls')
});

this.shadowRoot.querySelector('#iframeContainer').appendChild(iframeTemplate.content.cloneNode(true));
const iframe = this.shadowRoot.querySelector('iframe');

YoutubeVideoElement.onYTReady(()=>{
const onPlayerReady = (event) => {
this.dispatchEvent(new Event('volumechange'));

this.timeupdateInterval = setInterval(()=>{
this.dispatchEvent(new Event('timeupdate'));
}, 25);
}

const onPlayerStateChange = (event) => {
const state = event.data;

if (state == 1) {
this.dispatchEvent(new Event('play'));
} else if (state == 2) {
this.dispatchEvent(new Event('pause'));
}
}

const onPlayerError = (event) => {
console.log('onPlayerError', event.data, event);
}

this.ytPlayer = new YT.Player(iframe, {
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange,
'onError': onPlayerError
}
});

this.ytPlayer.addEventListener('onPlaybackRateChange', e => {
this.dispatchEvent(new Event('ratechange'));
});
});
}

connectedCallback() {
}

/* onStateChange
-1 (unstarted)
0 (ended)
1 (playing)
2 (paused)
3 (buffering)
5 (video cued).
*/

get paused() {
return !!([-1,0,2,5].indexOf(this.ytPlayer.getPlayerState()) > -1);
}

play() {
this.ytPlayer.playVideo();
}

pause() {
this.ytPlayer.pauseVideo();
}

get currentTime() {
return this.ytPlayer.getCurrentTime();
}

set currentTime(timeInSeconds) {
// allowSeekAhead is true here,though should technically be false
// when scrubbing w/ thumbnail previews
this.ytPlayer.seekTo(timeInSeconds, true);
this.dispatchEvent(new Event('timeupdate'));
}

get muted() {
if (this.ytPlayer) {
return this.ytPlayer.isMuted();
}

return false;
}

set muted(mute) {
if (mute) {
this.ytPlayer.mute()
} else {
this.ytPlayer.unMute()
}

// Leave time for post message API to update
setTimeout(() => {
this.dispatchEvent(new Event('volumechange'));
}, 100);
}

get volume() {
if (this.ytPlayer) {
return this.ytPlayer.getVolume() / 100;
}

return 1;
}

set volume(volume) {
this.ytPlayer.setVolume(volume * 100);

// Leave time for post message API to update
setTimeout(() => {
this.dispatchEvent(new Event('volumechange'));
}, 100);
}

get duration() {
return this.ytPlayer.getDuration();
}

get poster() {
const id = getIdFromURL(this.src);

if (id) {
// https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api?page=1&tab=votes#tab-top
return `https://i.ytimg.com/vi/${id}/maxresdefault.jpg`;
} else {
return null;
}
}

get playbackRate() {
return this.ytPlayer.getPlaybackRate();
}

set playbackRate(rate) {
this.ytPlayer.setPlaybackRate(rate);
}
}

function loadYoutubeAPI() {
if (window.onYouTubeIframeAPIReady) {
console.warn('YoutubeVideoElement: onYouTubeIframeAPIReady already defined. Overwriting.');
}

const YouTubeScriptTag = document.createElement('script');
YouTubeScriptTag.src = 'https://www.youtube.com/iframe_api';
const firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(YouTubeScriptTag, firstScriptTag);

window.onYouTubeIframeAPIReady = YoutubeVideoElement.handleYoutubeAPILoad.bind(YoutubeVideoElement);
}

if (window.customElements.get('youtube-video') || window.YoutubeVideoElement) {
console.debug('YoutubeVideoElement: <youtube-video> defined more than once.');
} else {
window.YoutubeVideoElement = YoutubeVideoElement;
window.customElements.define('youtube-video', YoutubeVideoElement);
loadYoutubeAPI();
}

export default YoutubeVideoElement;
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "youtube-video-element",
"version": "0.0.0",
"description": "Custom element (web component) for the YouTube player.",
"type": "module",
"main": "dist/youtube-video-element.js",
"files": [
"./dist/*",
"README.md"
],
"scripts": {
"build": "rm -rf dist && rollup --config",
"test": "echo \"Error: no test specified\" && exit 1",
"publish-patch": "yarn run build && np patch --no-tests"
},
"repository": {
"type": "git",
"url": "git+https://github.com/muxinc/youtube-video-element.git"
},
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"keywords": [
"youtube",
"video",
"player",
"web component",
"custom element"
],
"author": "@muxinc",
"license": "MIT",
"dependencies": {},
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.2",
"@rollup/plugin-node-resolve": "^7.1.1",
"np": "^6.2.0",
"rollup": "^2.2.0",
"rollup-plugin-babel-minify": "^10.0.0",
"rollup-plugin-terser": "^5.3.0"
}
}
17 changes: 17 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from "rollup-plugin-terser";
import pkg from './package.json'

export default {
input: 'index.js',
output: {
file: pkg.main,
format: 'es'
},
plugins: [
resolve(),
commonjs(),
terser(),
]
};
Loading

0 comments on commit 89e40ac

Please sign in to comment.