Repository: thelevicole/youtube-to-html5-loader
Branch: main
Commit: 0b2f2a20ea41
Files: 11
Total size: 33.0 KB
Directory structure:
gitextract_kcwpn6ue/
├── .gitignore
├── .idea/
│ ├── codeStyles/
│ │ └── codeStyleConfig.xml
│ ├── modules.xml
│ ├── php.xml
│ ├── vcs.xml
│ └── youtube-to-html5.iml
├── README.md
├── dist/
│ └── YouTubeToHtml5.js
├── package.json
├── src/
│ └── YouTubeToHtml5.js
└── webpack.mix.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.idea/workspace.xml
test/
mix-manifest.json
# Created by https://www.toptal.com/developers/gitignore/api/node,macos,linux,windows
# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,linux,windows
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/node,macos,linux,windows
.idea/workspace.xml
================================================
FILE: .idea/codeStyles/codeStyleConfig.xml
================================================
================================================
FILE: .idea/modules.xml
================================================
================================================
FILE: .idea/php.xml
================================================
================================================
FILE: .idea/vcs.xml
================================================
================================================
FILE: .idea/youtube-to-html5.iml
================================================
================================================
FILE: README.md
================================================
# Load YoutTube videos as HTML5 emebed element
[](https://www.jsdelivr.com/package/npm/@thelevicole/youtube-to-html5-loader)
[](https://www.npmjs.com/package/@thelevicole/youtube-to-html5-loader)
[](https://www.npmjs.com/package/@thelevicole/youtube-to-html5-loader)
## Get started
### Load library
First you need to include the library in your project, this can be achieved via NPM or jsDeliver.
#### NPM
```
npm i @thelevicole/youtube-to-html5-loader
```
```javascript
import YouTubeToHtml5 from '@thelevicole/youtube-to-html5-loader'
```
#### jsDeliver
```html
```
### Initiating
First setup your HTML something like:
```html
```
And then simply initiate the library with:
```javascript
new YouTubeToHtml5();
```
### Options
There are a number of options that can be passed to the constructor these are:
| Option | Description | Type | Default |
|--|--|--|--|
| `endpoint` | This is the API url thats used for retrieving data. More information to come. | `string` | `https://yt2html5.com/?id=` |
| `selector` | The DOM selector used for finding video elements. | `string` | `video[data-yt2html5]` |
| `attribute` | This is the attribute where your YouTube id/url is stored on the element. | `string` | `data-yt2html5` |
| `formats` | Filter the API results by specific formats. For example `[ '1080p', '720p' ]` will only allow 1080p and 720p formats. An asterix will allow all streaming formats. | `string|array` | `*` |
| `autoload` | Whether or not to load all videos on library init. | `boolean` | `true` |
| `withAudio` | Whether or not to only load streams with audio. | `boolean` | `true` |
| `withVideo` | Whether or not to only load streams with video. | `boolean` | `true` |
### Changing the API endpoint and custom server
This package uses a man-in-the-middle server (yt2html.com) to handle the API requests. This can cause issues as YouTube often blocks the host causing the library to not work. A solution to this is to host your own man-in-the-middle server and change the libraries API endpoint.
Simply modify the libraries global endpoint with the below snippet. Make sure to place before any `YouTubeToHtml5()` initiations.
```javascript
YouTubeToHtml5.defaultOptions.endpoint = 'http://myserver.com/?id=';
```
The server source can be found here: [thelevicole/youtube-to-html5-server](https://github.com/thelevicole/youtube-to-html5-server)
### Hooks
The library has a hook mechanism for filters and actions. If you've worked with WordPress before you'll be familiar with this concept.
> Note: You'll need to disable auto loading when using any hooks. First create an instance, then bind your hooks and finally call the `.load()` method.
#### Filters
Modify and return values.
##### Request URL
You might want to modify the request URL on each element load. You can do this with the `request.url` filter. For example:
```javascript
const controller = new YouTubeToHtml5({
autoload: false
});
controller.addFilter('request.url', function(url) {
return `${url}&cache_bust=${(new Date()).getTime()}`;
});
controller.load();
```
#### Actions
Run code every time the action is called.
##### Before each load
```javascript
const controller = new YouTubeToHtml5({
autoload: false
});
controller.addAction('load.before', function(element, data) {
element.classList.add('is-loading');
});
controller.load();
```
##### After each load
```javascript
const controller = new YouTubeToHtml5({
autoload: false
});
controller.addAction('load.after', function(element, data) {
element.classList.remove('is-loading');
});
controller.load();
```
##### After a successful load
```javascript
const controller = new YouTubeToHtml5({
autoload: false
});
controller.addAction('load.success', function(element, data) {
element.classList.addClass('is-playable');
});
controller.load();
```
##### After a failed load
```javascript
const controller = new YouTubeToHtml5({
autoload: false
});
controller.addAction('load.failed', function(element, data) {
element.classList.add('is-unplayable');
});
controller.load();
```
### jQuery
The library now includes a simply jQuery plugin which can be used like so...
```js
$('video[data-yt2html5]').youtubeToHtml5();
```
The `.youtubeToHtml5()` plugin returns the `YouTubeToHtml5` class instance so adding hooks etc is just as described above...
```js
const controller = $('video[data-yt2html5]').youtubeToHtml5({
autoload: false
});
controller.addAction('load.failed', function(element, data) {
element.classList.add('is-unplayable');
});
controller.load();
```
## Accepted URL patterns
Below is a list of varying YouTube url patterns, which include http/s and www/non-www.
```
youtube.com/watch?v=ScMzIvxBSi4
youtube.com/watch?vi=ScMzIvxBSi4
youtube.com/v/ScMzIvxBSi4
youtube.com/vi/ScMzIvxBSi4
youtube.com/?v=ScMzIvxBSi4
youtube.com/?vi=ScMzIvxBSi4
youtu.be/ScMzIvxBSi4
youtube.com/embed/ScMzIvxBSi4
youtube.com/v/ScMzIvxBSi4
youtube.com/watch?v=ScMzIvxBSi4&wtv=wtv
youtube.com/watch?dev=inprogress&v=ScMzIvxBSi4&feature=related
m.youtube.com/watch?v=ScMzIvxBSi4
youtube-nocookie.com/embed/ScMzIvxBSi4
```
================================================
FILE: dist/YouTubeToHtml5.js
================================================
!function(){"use strict";function t(t,n){var e="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!e){if(Array.isArray(t)||(e=function(t,n){if(!t)return;if("string"==typeof t)return o(t,n);var e=Object.prototype.toString.call(t).slice(8,-1);"Object"===e&&t.constructor&&(e=t.constructor.name);if("Map"===e||"Set"===e)return Array.from(t);if("Arguments"===e||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(e))return o(t,n)}(t))||n&&t&&"number"==typeof t.length){e&&(t=e);var r=0,i=function(){};return{s:i,n:function(){return r>=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,u=!0,l=!1;return{s:function(){e=e.call(t)},n:function(){var t=e.next();return u=t.done,t},e:function(t){l=!0,a=t},f:function(){try{u||null==e.return||e.return()}finally{if(l)throw a}}}}function o(t,o){(null==o||o>t.length)&&(o=t.length);for(var n=0,e=new Array(o);n1&&void 0!==arguments[1]?arguments[1]:null;!o&&t in this.class.defaultOptions&&(o=this.class.defaultOptions[t]);var n=t in this.options?this.options[t]:o;return n=this.applyFilters("option",n,t),n=this.applyFilters("option.".concat(t),n)}},{key:"getHooks",value:function(t,o){var n=[];if(t in this.class.globalHooks){var e=this.class.globalHooks[t];e=(e=e.filter((function(t){return t.name===o}))).sort((function(t,o){return t.priority-o.priority})),n=n.concat(e)}if(t in this.hooks){var r=this.hooks[t];r=(r=r.filter((function(t){return t.name===o}))).sort((function(t,o){return t.priority-o.priority})),n=n.concat(r)}return n}},{key:"addHook",value:function(t,o){t in this.class.globalHooks||(this.class.globalHooks[t]=[]),t in this.hooks||(this.hooks[t]=[]),"global"in o&&o.global?this.class.globalHooks[t].push(o):this.hooks[t].push(o)}},{key:"addAction",value:function(t,o){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:10,e=arguments.length>3&&void 0!==arguments[3]&&arguments[3];this.addHook("actions",{name:t,callback:o,priority:n,global:e})}},{key:"doAction",value:function(t){for(var o=this,n=arguments.length,e=new Array(n>1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:10,e=arguments.length>3&&void 0!==arguments[3]&&arguments[3];this.addHook("filters",{name:t,callback:o,priority:n,global:e})}},{key:"applyFilters",value:function(t,o){for(var n=this,e=arguments.length,r=new Array(e>2?e-2:0),i=2;i0)},function(t){return+(t.hasVideo&&t.hasAudio)},function(t){return+t.hasVideo},function(t){return parseInt(t.format)||0},function(t){return t._raw.bitrate||0},function(t){return t._raw.audioBitrate||0},function(t){return["mp4v","avc1","Sorenson H.283","MPEG-4 Visual","VP8","VP9","H.264"].findIndex((function(o){return t._raw.codecs&&t._raw.codecs.includes(o)}))},function(t){return["mp4a","mp3","vorbis","aac","opus","flac"].findIndex((function(o){return t._raw.codecs&&t._raw.codecs.includes(o)}))}])})),this.getOption("withAudio")&&(e=e.filter((function(t){return t.hasAudio}))),this.getOption("withVideo")&&(e=e.filter((function(t){return t.hasVideo})));var r=this.getOption("formats");return"*"!==r&&(e=e.filter((function(t){return Array.from(r).includes(t.format)}))),e}},{key:"canPlayType",value:function(t){var o,n=(o=/^audio/i.test(t)?document.createElement("audio"):document.createElement("video"))&&"function"==typeof o.canPlayType?o.canPlayType(t):"unknown";return n||"no"}},{key:"load",value:function(){var t=this,o=this.getElements(this.getOption("selector"));o&&o.length&&o.forEach((function(o){return t.loadSingle(o)}))}},{key:"loadSingle",value:function(t){var o=this,n=this.getOption("attribute");if(t.getAttribute(n)){var e=this.urlToId(t.getAttribute(n)),r=this.requestUrl(e);this.doAction("load.before",t),fetch(r).then((function(n){n.json().then((function(n){return o.doAction("load.success",t,n)}))})).catch((function(n){n.json().then((function(n){return o.doAction("load.failed",t,n)}))})).finally((function(){o.doAction("load.after",t)}))}}}],u=[{key:"_actionLoadSuccess",value:function(t,o,n){var e=t.getStreamData(n),r=(e=e.filter((function(t){return t.type===o.tagName.toLowerCase()}))).shift();r&&(o.src=r.url)}},{key:"_actionLoadFailed",value:function(t,o,n){console.warn("".concat(t.class," was unable to load video."))}}],a&&e(i.prototype,a),u&&e(i,u),Object.defineProperty(i,"prototype",{writable:!1}),o}();r(i,"globalHooks",{}),r(i,"defaultOptions",{endpoint:"https://yt2html5.com/?id=",selector:"video[data-yt2html5]",attribute:"data-yt2html5",formats:"*",autoload:!0,withAudio:!1,withVideo:!0}),window.YouTubeToHtml5=i,"undefined"!=typeof jQuery&&(jQuery.fn.youtubeToHtml5=function(){var t=this,o=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n="autoload"in o?o.autoload:i.defaultOptions.autoload;o.autoload=!1;var e=new i(o);return e.addFilter("elements",(function(){return Array.from(t)})),n&&e.load(),e})}();
================================================
FILE: package.json
================================================
{
"name": "@thelevicole/youtube-to-html5-loader",
"version": "5.0.0",
"description": "A javascript library to load YoutTube videos as HTML5 emebed elements.",
"main": "dist/YouTubeToHtml5.js",
"scripts": {
"dev": "npm run development",
"development": "mix",
"watch": "mix watch",
"watch-poll": "mix watch -- --watch-options-poll=1000",
"hot": "mix watch --hot",
"prod": "npm run production",
"production": "mix --production"
},
"keywords": [
"youtube",
"html5-video",
"youtube-api",
"video"
],
"devDependencies": {
"laravel-mix": "^6.0.49"
},
"browserslist": [
"last 3 version",
"> 1%"
],
"repository": {
"type": "git",
"url": "git+https://github.com/thelevicole/youtube-to-html5-loader.git"
},
"author": {
"name": "Levi Cole",
"email": "dev@thelevicole.com"
},
"license": "ISC",
"bugs": {
"url": "https://github.com/thelevicole/youtube-to-html5-loader/issues"
},
"homepage": "https://thelevicole.com/youtube-to-html5-loader/"
}
================================================
FILE: src/YouTubeToHtml5.js
================================================
class YouTubeToHtml5 {
static globalHooks = {};
static defaultOptions = {
endpoint: 'https://yt2html5.com/?id=',
selector: 'video[data-yt2html5]',
attribute: 'data-yt2html5',
formats: '*', // Accepts an array of formats e.g. [ '1080p', '720p', '320p' ] or a single format '1080p'. Asterix for all.
autoload: true,
withAudio: false,
withVideo: true
}
class = YouTubeToHtml5;
options = {};
hooks = {};
/**
* @param {{
* endpoint: string,
* selector: string,
* attribute: string,
* formats: string|array,
* autoload: boolean,
* withAudio: boolean,
* withVideo: boolean
* }} options
*/
constructor(options) {
this.options = options;
// Add default load actions.
this.addAction('load.success', this.class._actionLoadSuccess, 0);
this.addAction('load.failed', this.class._actionLoadFailed, 0);
if (this.getOption('autoload')) {
this.load();
}
}
/**
* Get a user or default option.
* @param {string} name
* @param defaultValue
* @returns {*}
*/
getOption(name, defaultValue = null) {
if (!defaultValue && name in this.class.defaultOptions) {
defaultValue = this.class.defaultOptions[name];
}
var value = name in this.options ? this.options[name] : defaultValue;
/**
* Apply value filters to all regardless of option name.
* @example instance.addFilter('option', function(value, name) { return value + 500; });
*/
value = this.applyFilters(`option`, value, name );
/**
* Apply value filters to option named only.
* @example instance.addFilter('setting.delay', function(value) { return value + 500; });
*/
value = this.applyFilters(`option.${name}`, value );
return value;
}
/**
* Get hooks by type and name. Ordered by priority.
* @param {string} type
* @param {string} name
* @returns {array}
*/
getHooks(type, name) {
let hooks = [];
if (type in this.class.globalHooks) {
let globalHooks = this.class.globalHooks[type];
globalHooks = globalHooks.filter(el => el.name === name);
globalHooks = globalHooks.sort((a, b) => a.priority - b.priority);
hooks = hooks.concat(globalHooks);
}
if (type in this.hooks) {
let localHooks = this.hooks[ type ];
localHooks = localHooks.filter(el => el.name === name);
localHooks = localHooks.sort((a, b) => a.priority - b.priority);
hooks = hooks.concat(localHooks);
}
return hooks;
}
/**
* Register a hook.
* @param {string} type
* @param {object} hookMeta
*/
addHook(type, hookMeta) {
// Create new global hook type array.
if (!(type in this.class.globalHooks)) {
this.class.globalHooks[type] = [];
}
// Create new local hook type array.
if (!(type in this.hooks)) {
this.hooks[type] = [];
}
// Add to global.
if ('global' in hookMeta && hookMeta.global) {
this.class.globalHooks[type].push(hookMeta);
}
// Else, add to local.
else {
this.hooks[type].push(hookMeta);
}
}
/**
* Add action callback.
* @param {string} action Name of action to trigger callback on.
* @param {function} callback
* @param {number} priority
* @param {boolean} global True if this action should apply to all instances.
*/
addAction(action, callback, priority = 10, global = false) {
this.addHook('actions', {
name: action,
callback: callback,
priority: priority,
global: global
});
}
/**
* Trigger an action.
* @param {string} name Name of action to run.
* @param {*} args Arguments passed to the callback function.
*/
doAction(name, ...args) {
this.getHooks('actions', name).forEach(hook => {
hook.callback.apply(this, args);
});
}
/**
* Register filter.
* @param {string} filter Name of filter to trigger callback on.
* @param {function} callback
* @param {number} priority
* @param {boolean} global True if this action should apply to all instances.
*/
addFilter(filter, callback, priority = 10, global = false) {
this.addHook('filters', {
name: filter,
callback: callback,
priority: priority,
global: global
});
}
/**
* Apply all named filters to a value.
* @param {string} name Name of action to run.
* @param {*} value The value to be mutated.
* @param {*} args Arguments passed to the callback function.
* @returns {*}
*/
applyFilters(name, value, ...args) {
this.getHooks('filters', name).forEach(hook => {
value = hook.callback.apply(this, [value].concat(args));
});
return value;
}
/**
* Extract the Youtube ID from a URL. Returns full value if no matches.
* @param {string} url
* @returns {string}
*/
urlToId(url) {
const regex = /^(?:http(?:s)?:\/\/)?(?:www\.)?(?:m\.)?(?:youtu\.be\/|(?:(?:youtube-nocookie\.com\/|youtube\.com\/)(?:(?:watch)?\?(?:.*&)?v(?:i)?=|(?:embed|v|vi|user)\/)))([a-zA-Z0-9\-_]*)/;
const matches = url.match(regex);
return Array.isArray(matches) && matches[1] ? matches[1] : url;
}
/**
* Get list of elements found with the selector.
* @param {NodeList|HTMLCollection|string} selector
* @returns {array}
*/
getElements(selector) {
var elements = null;
if (selector) {
if (NodeList.prototype.isPrototypeOf(selector) || HTMLCollection.prototype.isPrototypeOf(selector)) {
elements = selector;
} else if (typeof selector === 'object' && 'nodeType' in selector && selector.nodeType) {
elements = [selector];
} else {
elements = document.querySelectorAll(this.getOption('selector'));
}
}
elements = Array.from(elements || '');
return this.applyFilters('elements', elements);
}
/**
* Build API url from video id.
* @param {string} videoId
* @returns {string}
*/
requestUrl(videoId) {
const endpoint = this.getOption('endpoint');
const url = endpoint + videoId;
return this.applyFilters('request.url', url, endpoint, videoId);
}
/**
* Sort formats by a list of functions.
*
* @param {object} a
* @param {object} b
* @param {function[]} processors
* @returns {number}
*/
bulkSortBy(a, b, processors) {
let result = 0;
for (let fn of processors) {
const diff = fn(b) - fn(a);
result += diff;
}
return result;
}
/**
* Get stream data from API response.
* @param {object} response
* @returns {array}
*/
getStreamData(response) {
const data = response?.data || {};
let streams = [];
// Build streams array
Array.from(data.formats || '').forEach(stream => {
let thisData = {
_raw: stream,
itag: stream.itag,
url: stream.url,
format: stream.qualityLabel,
type: 'unknown',
mime: 'unknown',
hasAudio: stream.hasAudio,
hasVideo: stream.hasVideo,
browserSupport: 'unknown'
};
if (!thisData.format) {
// Add audio format fallback
if (thisData.hasAudio && !thisData.hasVideo) {
thisData.format = `${stream.audioBitrate}kbps`;
}
}
// Extract stream data from mimetype.
if ('mimeType' in stream) {
const mimeParts = stream.mimeType.match(/^(audio|video)(?:\/([^;]+);)?/i);
// Set media type (video, audo)
if (mimeParts[1]) {
thisData.type = mimeParts[ 1 ];
}
// Set media mime (mp4, ogg...etc)
if (mimeParts[2]) {
thisData.mime = mimeParts[2];
}
// Set browser support rating
thisData.browserSupport = this.canPlayType(`${thisData.type}/${thisData.mime}`);
}
streams.push(thisData);
});
// Sort streams by playability and quality
streams.sort((a, b) => {
return this.bulkSortBy(a, b, [
format => {
return {
'unknown': -1,
'no': -1,
'maybe': 0,
'probably': 1
}[format.browserSupport];
},
format => +!!format._raw.isHLS,
format => +!!format._raw.isDashMPD,
format => +(format._raw.contentLength > 0),
format => +(format.hasVideo && format.hasAudio),
format => +format.hasVideo,
format => parseInt(format.format) || 0,
format => format._raw.bitrate || 0,
format => format._raw.audioBitrate || 0,
format => [
'mp4v',
'avc1',
'Sorenson H.283',
'MPEG-4 Visual',
'VP8',
'VP9',
'H.264',
].findIndex(encoding => format._raw.codecs && format._raw.codecs.includes(encoding)),
format => [
'mp4a',
'mp3',
'vorbis',
'aac',
'opus',
'flac',
].findIndex(encoding => format._raw.codecs && format._raw.codecs.includes(encoding))
]);
});
// Only return streams with audio
if (this.getOption('withAudio')) {
streams = streams.filter(item => item.hasAudio);
}
// Only return streams with video
if (this.getOption('withVideo')) {
streams = streams.filter(item => item.hasVideo);
}
const allowedFormats = this.getOption('formats');
// Filter streams further by allowed formats.
if (allowedFormats !== '*') {
streams = streams.filter(item => Array.from(allowedFormats).includes(item.format));
}
return streams;
}
/**
* Check if a given mime type can be played by the browser.
* @link https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/canPlayType
* @param {string} type For example "video/mp4"
* @returns {CanPlayTypeResult|string} probably, maybe, no, unkown
*/
canPlayType(type) {
var phantomEl;
if (/^audio/i.test(type)) {
phantomEl = document.createElement('audio');
} else {
phantomEl = document.createElement('video');
}
const value = phantomEl && typeof phantomEl.canPlayType === 'function' ? phantomEl.canPlayType(type) : 'unknown';
return value ? value : 'no';
}
/**
* Run our full process. Loops through each element matching the selector.
*/
load() {
const elements = this.getElements(this.getOption('selector'));
if (elements && elements.length) {
elements.forEach(element => this.loadSingle(element) );
}
}
/**
* Process a single element.
* @param {Element} element
*/
loadSingle(element) {
/**
* Attribute name for grabbing YouTube identifier/url.
*
* @type {string}
*/
const attribute = this.getOption('attribute');
// Check if element has attribute value
if (element.getAttribute(attribute)) {
// Extract video id from attribute value.
const videoId = this.urlToId(element.getAttribute(attribute));
// Build request url.
const requestUrl = this.requestUrl(videoId);
this.doAction('load.before', element);
fetch(requestUrl).then(response => {
response.json().then(json => this.doAction('load.success', element, json));
}).catch(response => {
response.json().then(json => this.doAction('load.failed', element, json));
}).finally(() => {
this.doAction('load.after', element)
});
}
}
/**
* Parse raw YouTube response into usable data.
* @param {YouTubeToHtml5} context
* @param {Element} element
* @param {object} response
*/
static _actionLoadSuccess(context, element, response) {
let streams = context.getStreamData(response);
// Limit to element tag name (video/audio)
streams = streams.filter(item => item.type === element.tagName.toLowerCase());
// Get the top priority stream
const stream = streams.shift();
if (stream) {
element.src = stream.url;
}
}
/**
* Handle failed response.
* @param {YouTubeToHtml5} context
* @param {Element} element
* @param {object} response
*/
static _actionLoadFailed(context, element, response) {
console.warn(`${context.class} was unable to load video.`);
}
}
/**
* Add class to the window's global scope.
*
* @type {YouTubeToHtml5}
*/
window.YouTubeToHtml5 = YouTubeToHtml5;
/**
* Add jQuery plugin if exists.
*/
if (typeof jQuery !== 'undefined') {
(function($) {
/**
*
* @param {{
* endpoint: string,
* formats: string|array,
* autoload: boolean,
* withAudio: boolean,
* withVideo: boolean
* }} options
* @return {YouTubeToHtml5}
*/
$.fn.youtubeToHtml5 = function(options = {}) {
// Cache user default autoload option.
const isAutoload = 'autoload' in options ? options.autoload : YouTubeToHtml5.defaultOptions.autoload;
// For jQuery we will need to make some modifications before we process loading.
options.autoload = false;
// Create new instance.
const controller = new YouTubeToHtml5(options);
// Overide core elements with jQuery selected elements.
controller.addFilter('elements', () => Array.from(this));
// Now we can autoload.
if (isAutoload) {
controller.load();
}
// Return controller instance.
return controller;
}
})(jQuery);
}
/**
* Export module.
*/
export default YouTubeToHtml5;
================================================
FILE: webpack.mix.js
================================================
const mix = require('laravel-mix');
mix.js('src/YouTubeToHtml5.js', 'dist');