main 0b2f2a20ea41 cached
11 files
33.0 KB
9.1k tokens
25 symbols
1 requests
Download .txt
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
================================================
<component name="ProjectCodeStyleConfiguration">
  <state>
    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
  </state>
</component>

================================================
FILE: .idea/modules.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/youtube-to-html5.iml" filepath="$PROJECT_DIR$/.idea/youtube-to-html5.iml" />
    </modules>
  </component>
</project>

================================================
FILE: .idea/php.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="PhpProjectSharedConfiguration" php_language_level="7.2" />
</project>

================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>

================================================
FILE: .idea/youtube-to-html5.iml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
  <component name="NewModuleRootManager">
    <content url="file://$MODULE_DIR$" />
    <orderEntry type="inheritedJdk" />
    <orderEntry type="sourceFolder" forTests="false" />
  </component>
</module>

================================================
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
<script src="https://cdn.jsdelivr.net/npm/@thelevicole/youtube-to-html5-loader@5/dist/YouTubeToHtml5.js"></script>
```

### Initiating
First setup your HTML something like:
```html
<video data-yt2html5="YOUTUBE_URL_OR_ID_GOES_HERE"></video>
```
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);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})}();

================================================
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');
Download .txt
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
Download .txt
SYMBOL INDEX (25 symbols across 2 files)

FILE: dist/YouTubeToHtml5.js
  function t (line 1) | function t(t,n){var e="undefined"!=typeof Symbol&&t[Symbol.iterator]||t[...
  function o (line 1) | function o(t,o){(null==o||o>t.length)&&(o=t.length);for(var n=0,e=new Ar...
  function n (line 1) | function n(t){return n="function"==typeof Symbol&&"symbol"==typeof Symbo...
  function e (line 1) | function e(t,o){for(var n=0;n<o.length;n++){var e=o[n];e.enumerable=e.en...
  function r (line 1) | function r(t,o,n){return o in t?Object.defineProperty(t,o,{value:n,enume...
  function o (line 1) | function o(t){!function(t,o){if(!(t instanceof o))throw new TypeError("C...

FILE: src/YouTubeToHtml5.js
  class YouTubeToHtml5 (line 1) | class YouTubeToHtml5 {
    method constructor (line 30) | constructor(options) {
    method getOption (line 48) | getOption(name, defaultValue = null) {
    method getHooks (line 76) | getHooks(type, name) {
    method addHook (line 101) | addHook(type, hookMeta) {
    method addAction (line 132) | addAction(action, callback, priority = 10, global = false) {
    method doAction (line 146) | doAction(name, ...args) {
    method addFilter (line 159) | addFilter(filter, callback, priority = 10, global = false) {
    method applyFilters (line 175) | applyFilters(name, value, ...args) {
    method urlToId (line 187) | urlToId(url) {
    method getElements (line 198) | getElements(selector) {
    method requestUrl (line 221) | requestUrl(videoId) {
    method bulkSortBy (line 235) | bulkSortBy(a, b, processors) {
    method getStreamData (line 251) | getStreamData(response) {
    method canPlayType (line 364) | canPlayType(type) {
    method load (line 382) | load() {
    method loadSingle (line 394) | loadSingle(element) {
    method _actionLoadSuccess (line 430) | static _actionLoadSuccess(context, element, response) {
    method _actionLoadFailed (line 451) | static _actionLoadFailed(context, element, response) {
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (36K chars).
[
  {
    "path": ".gitignore",
    "chars": 3094,
    "preview": ".idea/workspace.xml\ntest/\nmix-manifest.json\n\n# Created by https://www.toptal.com/developers/gitignore/api/node,macos,lin"
  },
  {
    "path": ".idea/codeStyles/codeStyleConfig.xml",
    "chars": 149,
    "preview": "<component name=\"ProjectCodeStyleConfiguration\">\n  <state>\n    <option name=\"PREFERRED_PROJECT_CODE_STYLE\" value=\"Defaul"
  },
  {
    "path": ".idea/modules.xml",
    "chars": 284,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n   "
  },
  {
    "path": ".idea/php.xml",
    "chars": 149,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"PhpProjectSharedConfiguration\" php_langu"
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 180,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": ".idea/youtube-to-html5.iml",
    "chars": 281,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"NewModuleRootManager\">\n"
  },
  {
    "path": "README.md",
    "chars": 5530,
    "preview": "\n# Load YoutTube videos as HTML5 emebed element\n\n[![](https://data.jsdelivr.com/v1/package/npm/@thelevicole/youtube-to-h"
  },
  {
    "path": "dist/YouTubeToHtml5.js",
    "chars": 7856,
    "preview": "!function(){\"use strict\";function t(t,n){var e=\"undefined\"!=typeof Symbol&&t[Symbol.iterator]||t[\"@@iterator\"];if(!e){if"
  },
  {
    "path": "package.json",
    "chars": 1045,
    "preview": "{\n  \"name\": \"@thelevicole/youtube-to-html5-loader\",\n  \"version\": \"5.0.0\",\n  \"description\": \"A javascript library to load"
  },
  {
    "path": "src/YouTubeToHtml5.js",
    "chars": 15181,
    "preview": "class YouTubeToHtml5 {\n\n    static globalHooks = {};\n\n    static defaultOptions = {\n        endpoint: 'https://yt2html5."
  },
  {
    "path": "webpack.mix.js",
    "chars": 78,
    "preview": "const mix = require('laravel-mix');\n\nmix.js('src/YouTubeToHtml5.js', 'dist');\n"
  }
]

About this extraction

This page contains the full source code of the thelevicole/youtube-to-html5-loader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (33.0 KB), approximately 9.1k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!