├── .gitignore ├── LICENSE ├── README.md ├── examples └── basic_usage.html ├── package.json └── src └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | *-lock.json 91 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Henry Rodrick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hlsjs-ipfs-loader 2 | A [js-ipfs](https://github.com/ipfs/js-ipfs) loader for the 3 | [hls.js](https://github.com/video-dev/hls.js) JavaScript HLS client 4 | 5 | ## Building 6 | 7 | To build hlsjs-ipfs-loader, make sure you have the latest version of npm 8 | installed. Then simply run the following commands from the hlsjs-ipfs-loader 9 | project root: 10 | ``` 11 | npm install 12 | npm run build 13 | ``` 14 | 15 | This will write a self-contained JS bundle to dist/index.js 16 | 17 | ## Browser example 18 | Include script tags for HLS.js, js-ipfs and hlsjs-ipfs-loader: 19 | ``` 20 | 21 | 22 | 23 | ``` 24 | 25 | After including these dependencies, add your own script that hooks everything 26 | up. Please see [this example](examples/basic_usage.html) for more details. 27 | 28 | You can also [click here](https://moshisushi.github.io/ipfs_hls_example.html) to see this example in action in your browser, without checking out any code. 29 | 30 | NOTE: Chrome's strict security policies block the example from running 31 | locally, so you will either have to spin up a local web server to host it from 32 | (unless you want to explicitly fiddle with the policies), 33 | or run it in Firefox where this restriction currently doesn't exist. 34 | -------------------------------------------------------------------------------- /examples/basic_usage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hlsjs-ipfs-loader", 3 | "version": "0.3.1", 4 | "description": "IPFS loader for hls.js", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "aegir build" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/moshisushi/hlsjs-ipfs-loader.git" 12 | }, 13 | "author": "Henry Rodrick ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/moshisushi/hlsjs-ipfs-loader/issues" 17 | }, 18 | "homepage": "https://github.com/moshisushi/hlsjs-ipfs-loader#readme", 19 | "devDependencies": { 20 | "aegir": "^35.0.3" 21 | }, 22 | "files": [ 23 | "dist", 24 | "examples", 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class HlsjsIPFSLoader { 4 | constructor(config) { 5 | this._abortFlag = [ false ]; 6 | this.ipfs = config.ipfs 7 | this.hash = config.ipfsHash 8 | if (config.debug === false) { 9 | this.debug = function() {} 10 | } else if (config.debug === true) { 11 | this.debug = console.log 12 | } else { 13 | this.debug = config.debug 14 | } 15 | if(config.m3u8provider) { 16 | this.m3u8provider = config.m3u8provider; 17 | } else { 18 | this.m3u8provider = null; 19 | } 20 | if(config.tsListProvider) { 21 | this.tsListProvider = config.tsListProvider; 22 | } else { 23 | this.tsListProvider = null; 24 | } 25 | } 26 | 27 | destroy() { 28 | } 29 | 30 | abort() { 31 | this._abortFlag[0] = true; 32 | } 33 | 34 | load(context, config, callbacks) { 35 | this.context = context 36 | this.config = config 37 | this.callbacks = callbacks 38 | this.stats = { trequest: performance.now(), retry: 0 } 39 | this.retryDelay = config.retryDelay 40 | this.loadInternal() 41 | } 42 | /** 43 | * Call this by getting the HLSIPFSLoader instance from hls.js hls.coreComponents[0].loaders.manifest.setM3U8Provider() 44 | * @param {function} provider 45 | */ 46 | setM3U8Provider(provider) { 47 | this.m3u8provider = provider; 48 | } 49 | /** 50 | * 51 | * @param {function} provider 52 | */ 53 | setTsListProvider(provider) { 54 | this.tsListProvider = provider; 55 | } 56 | 57 | loadInternal() { 58 | const { stats, context, callbacks } = this 59 | 60 | stats.tfirst = Math.max(performance.now(), stats.trequest) 61 | stats.loaded = 0 62 | 63 | //When using absolute path (https://example.com/index.html) vs https://example.com/ 64 | const urlParts = window.location.href.split("/") 65 | if(urlParts[urlParts.length - 1] !== "") { 66 | urlParts[urlParts.length - 1] = "" 67 | } 68 | const filename = context.url.replace(urlParts.join("/"), "") 69 | 70 | const options = {} 71 | if (Number.isFinite(context.rangeStart)) { 72 | options.offset = context.rangeStart; 73 | if (Number.isFinite(context.rangeEnd)) { 74 | options.length = context.rangeEnd - context.rangeStart; 75 | } 76 | } 77 | 78 | if(filename.split(".")[1] === "m3u8" && this.m3u8provider !== null) { 79 | const res = this.m3u8provider(); 80 | let data; 81 | if(Buffer.isBuffer(res)) { 82 | data = buf2str(res) 83 | } else { 84 | data = res; 85 | } 86 | const response = { url: context.url, data: data } 87 | callbacks.onSuccess(response, stats, context) 88 | return; 89 | } 90 | if(filename.split(".")[1] === "m3u8" && this.tsListProvider !== null) { 91 | var tslist = this.tsListProvider(); 92 | var hash = tslist[filename]; 93 | if(hash) { 94 | this.cat(hash).then(res => { 95 | let data; 96 | if(Buffer.isBuffer(res)) { 97 | data = buf2str(res) 98 | } else { 99 | data = res; 100 | } 101 | stats.loaded = stats.total = data.length 102 | stats.tload = Math.max(stats.tfirst, performance.now()) 103 | const response = { url: context.url, data: data } 104 | callbacks.onSuccess(response, stats, context) 105 | }); 106 | } 107 | return; 108 | } 109 | this._abortFlag[0] = false; 110 | getFile(this.ipfs, this.hash, filename, options, this.debug, this._abortFlag).then(res => { 111 | const data = (context.responseType === 'arraybuffer') ? res : buf2str(res) 112 | stats.loaded = stats.total = data.length 113 | stats.tload = Math.max(stats.tfirst, performance.now()) 114 | const response = { url: context.url, data: data } 115 | callbacks.onSuccess(response, stats, context) 116 | }, console.error) 117 | } 118 | } 119 | async function getFile(ipfs, rootHash, filename, options, debug, abortFlag) { 120 | debug(`Fetching hash for '${rootHash}/${filename}'`) 121 | const path = `${rootHash}/${filename}` 122 | try { 123 | return await cat(path, options, ipfs, debug, abortFlag) 124 | } catch(ex) { 125 | throw new Error(`File not found: ${rootHash}/${filename}`) 126 | } 127 | } 128 | 129 | function buf2str(buf) { 130 | return new TextDecoder().decode(buf) 131 | } 132 | 133 | async function cat (cid, options, ipfs, debug, abortFlag) { 134 | const parts = [] 135 | let length = 0, offset = 0 136 | 137 | for await (const buf of ipfs.cat(cid, options)) { 138 | parts.push(buf) 139 | length += buf.length 140 | if (abortFlag[0]) { 141 | debug('Cancel reading from ipfs') 142 | break 143 | } 144 | } 145 | 146 | const value = new Uint8Array(length) 147 | for (const buf of parts) { 148 | value.set(buf, offset) 149 | offset += buf.length 150 | } 151 | 152 | debug(`Received data for file '${cid}' size: ${value.length} in ${parts.length} blocks`) 153 | return value 154 | } 155 | 156 | exports = module.exports = HlsjsIPFSLoader 157 | --------------------------------------------------------------------------------