├── .babelrc ├── .dockerignore ├── .env-template ├── .gitignore ├── Dockerfile ├── README.md ├── app.js ├── bin └── www ├── client ├── index.html ├── index.js └── styles.css ├── docker-compose.yml ├── ipfs ├── Dockerfile └── start_ipfs.sh ├── package-lock.json ├── package.json ├── redis └── redis.conf ├── routes └── index.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": ["@babel/plugin-proposal-class-properties"] 7 | } 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /.env-template: -------------------------------------------------------------------------------- 1 | IPFS_HOST=ipfs 2 | REDIS_PSWD= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | mydb 4 | .env 5 | public 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | RUN apt-get update -y && apt-get install -y ffmpeg git build-essential 3 | RUN apt-get update -yq \ 4 | && apt-get install curl gnupg -yq \ 5 | && curl -sL https://deb.nodesource.com/setup_8.x | bash \ 6 | && apt-get install nodejs -yq 7 | WORKDIR /usr/src/app 8 | COPY package*.json ./ 9 | RUN npm install nodemon -g 10 | RUN npm install webpack -g 11 | RUN npm install webpack-cli -g 12 | RUN npm install 13 | COPY . . 14 | EXPOSE 3000 15 | CMD [ "npm", "start" ] 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipfs-hls-converter 2 | 3 | Takes an IPFS hash, converts it to HLS (Apple's HTTP Live Streaming) and reuploads the file to IPFS, requires docker. 4 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // @format 2 | require('dotenv').config(); 3 | var createError = require('http-errors'); 4 | var express = require('express'); 5 | var path = require('path'); 6 | var cookieParser = require('cookie-parser'); 7 | var logger = require('morgan'); 8 | 9 | var indexRouter = require('./routes/index'); 10 | 11 | var app = express(); 12 | 13 | // view engine setup 14 | app.set('views', path.join(__dirname, 'views')); 15 | app.set('view engine', 'jade'); 16 | 17 | app.use(logger('dev')); 18 | app.use(express.json()); 19 | app.use(express.urlencoded({extended: false})); 20 | app.use(cookieParser()); 21 | 22 | const isDeveloping = process.env.NODE_ENV !== 'production'; 23 | 24 | if (isDeveloping) { 25 | let webpack = require('webpack'); 26 | let webpackMiddleware = require('webpack-dev-middleware'); 27 | let config = require('./webpack.config.js'); 28 | let compiler = webpack(config); 29 | // serve the content using webpack 30 | app.use(webpackMiddleware(compiler, { 31 | publicPath: '/', 32 | })); 33 | } else { 34 | // serve the content using static directory 35 | app.use(express.static(staticPath)); 36 | } 37 | 38 | app.get('/', (req, res) => { 39 | res.send('Hello world\n'); 40 | }); 41 | app.use('/hashes/:ipfsHash', indexRouter.ipfsHashes); 42 | module.exports = app; 43 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @format 3 | 4 | /** 5 | * Module dependencies. 6 | */ 7 | 8 | var app = require('../app'); 9 | var debug = require('debug')('ipfs-convert:server'); 10 | var http = require('http'); 11 | var webpackHotMiddleware = require('webpack-middleware-hmr/server.js'); 12 | let webpack = require('webpack'); 13 | let webpackMiddleware = require('webpack-dev-middleware'); 14 | let config = require('../webpack.config.js'); 15 | let compiler = webpack(config); 16 | 17 | /** 18 | * Get port from environment and store in Express. 19 | */ 20 | 21 | var port = normalizePort(process.env.PORT || '3000'); 22 | app.set('port', port); 23 | 24 | /** 25 | * Create HTTP server. 26 | */ 27 | 28 | var server = http.createServer(app); 29 | 30 | /** 31 | * Listen on provided port, on all network interfaces. 32 | */ 33 | 34 | server.listen(port); 35 | server.on('error', onError); 36 | server.on('listening', onListening); 37 | 38 | webpackHotMiddleware(server, compiler) 39 | 40 | /** 41 | * Normalize a port into a number, string, or false. 42 | */ 43 | 44 | function normalizePort(val) { 45 | var port = parseInt(val, 10); 46 | 47 | if (isNaN(port)) { 48 | // named pipe 49 | return val; 50 | } 51 | 52 | if (port >= 0) { 53 | // port number 54 | return port; 55 | } 56 | 57 | return false; 58 | } 59 | 60 | /** 61 | * Event listener for HTTP server "error" event. 62 | */ 63 | 64 | function onError(error) { 65 | if (error.syscall !== 'listen') { 66 | throw error; 67 | } 68 | 69 | var bind = typeof port === 'string' 70 | ? 'Pipe ' + port 71 | : 'Port ' + port; 72 | 73 | // handle specific listen errors with friendly messages 74 | switch (error.code) { 75 | case 'EACCES': 76 | console.error(bind + ' requires elevated privileges'); 77 | process.exit(1); 78 | break; 79 | case 'EADDRINUSE': 80 | console.error(bind + ' is already in use'); 81 | process.exit(1); 82 | break; 83 | default: 84 | throw error; 85 | } 86 | } 87 | 88 | /** 89 | * Event listener for HTTP server "listening" event. 90 | */ 91 | 92 | function onListening() { 93 | var addr = server.address(); 94 | var bind = typeof addr === 'string' 95 | ? 'pipe ' + addr 96 | : 'port ' + addr.port; 97 | debug('Listening on ' + bind); 98 | } 99 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IPFS HLS Converter 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | // @format 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import css from './styles.css'; 5 | import Highlight from 'react-highlight'; 6 | import {Line} from 'rc-progress'; 7 | 8 | class App extends React.Component { 9 | state = { 10 | status: '', 11 | intervals: [], 12 | }; 13 | handleClick = () => { 14 | const {intervals} = this.state; 15 | 16 | const intervalId = setInterval(() => { 17 | var Http = new XMLHttpRequest(); 18 | var url = '../hashes/' + document.querySelector('.value').value; 19 | Http.open('GET', url); 20 | Http.send(); 21 | Http.onreadystatechange = e => { 22 | const data = JSON.parse(Http.responseText); 23 | this.setState(data); 24 | }; 25 | }, 1000); 26 | 27 | intervals.push(intervalId); 28 | this.setState({intervals, status: ''}); 29 | }; 30 | 31 | componentDidUpdate(prevProps, prevState) { 32 | const {status, intervals} = this.state; 33 | if ( 34 | (status === 'error' && intervals.length !== 0) || 35 | (status === 'finished' && intervals.length !== 0) 36 | ) { 37 | intervals.forEach(clearInterval); 38 | this.setState({intervals: []}); 39 | } 40 | } 41 | 42 | progressBar() { 43 | const {percentage} = this.state; 44 | if (percentage && percentage !== 100) { 45 | return ( 46 | 57 | ); 58 | } 59 | } 60 | 61 | status() { 62 | const {files, status, error} = this.state; 63 | if (files) { 64 | const hash = files[files.length - 1].hash; 65 | return ( 66 |

67 | 68 | Check out your file: {hash} 69 | 70 |

71 | ); 72 | } 73 | if (status === 'error' && error) { 74 | return

Processing threw an error: {error}

; 75 | } 76 | if (status === 'downloading') { 77 | return

Downloading the file and piping it to FFMPEG

; 78 | } 79 | } 80 | 81 | render() { 82 | const {files} = this.state; 83 | return ( 84 |
85 |

IPFS HLS Converter

86 |

87 | This website converts any IPFS-hosted file to an{' '} 88 | 91 | HLS file 92 | {' '} 93 | and reuploads it to IPFS. 94 |

95 | 100 | 101 | Convert 102 | 103 | {this.progressBar()} 104 | {this.status()} 105 |

API (free)

106 |

HTTP GET /hashes/:IPFSHash/

107 |

108 | When an IPFS hash is requested to be converted a subprocess is spawned 109 | that downloads the file and converts it with FFMPEG. During this time, 110 | the endpoint can be continously be queried by a script (polling). The 111 | `status` property will indicate the status of the processing. 112 |

113 | 114 | {` 115 | { 116 | "_id": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGb", 117 | "_rev": "19-f8918be4b43a18ebebb2962b226f7ff9", 118 | "duration": "00:00:23.10", 119 | "files": [ 120 | { 121 | "hash": "QmSHAwFbJiufnWQwCtQMevzAWjK8gqQECvuo3mpxMutM9y", 122 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH/master.m3u8", 123 | "size": 246 124 | }, 125 | { 126 | "hash": "QmeghHDm1DzoJa8mLE6LD8QTcSRnxmsUkqgLwpvrDqDVh3", 127 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH/master0.ts", 128 | "size": 3478125 129 | }, 130 | { 131 | "hash": "Qmca9scyDaG5yLcXP3oQKeVMC65h9pMv29PRYZJ3SGJbpF", 132 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH/master1.ts", 133 | "size": 800943 134 | }, 135 | { 136 | "hash": "QmcQ6ttJZfV6PfgFc3bK7nmUhVcwXdMDAA6gpwWM5TPfKF", 137 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH/master2.ts", 138 | "size": 1537468 139 | }, 140 | { 141 | "hash": "QmY1m5xM9Ce6GMvYtZdqYjt1aUibHTkhrPRJrQkdLemC5L", 142 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH/master3.ts", 143 | "size": 1314307 144 | }, 145 | { 146 | "hash": "QmfNgCUG35h9XZZaAxSWoubaxAfkFoc9eeGWDbABhK9W7c", 147 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH/master4.ts", 148 | "size": 149286 149 | }, 150 | { 151 | "hash": "QmNuH4tuWm6CV6hXjDctEoy66hRcchG8qeFxL2w8FWPkVV", 152 | "path": "QmYRh3EmbVCTyA5vi1YJeKN3xQiyyogwf6t36LzMTN9FGbHTyhaIShBH", 153 | "size": 7280704 154 | } 155 | ], 156 | "percentage": 100, 157 | "progress": "00:00:23.10", 158 | "status": "finished" 159 | } 160 | `} 161 | 162 |

Embedd your movie into your website

163 |

164 | It's important to use the folder hash (usually the last in the array 165 | `files`) as `testhash`'s value. 166 |

167 |