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