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://data.jsdelivr.com/v1/package/npm/@thelevicole/youtube-to-html5-loader/badge)](https://www.jsdelivr.com/package/npm/@thelevicole/youtube-to-html5-loader) [![Latest Stable Version](https://img.shields.io/npm/v/@thelevicole/youtube-to-html5-loader)](https://www.npmjs.com/package/@thelevicole/youtube-to-html5-loader) [![Total Downloads](https://img.shields.io/npm/dt/@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');