-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from pldubouilh/tests
Tests / refactor
- Loading branch information
Showing
11 changed files
with
359 additions
and
196 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
language: node_js | ||
node_js: | ||
- "8.1.3" | ||
cache: | ||
directories: | ||
- node_modules | ||
install: | ||
- npm install | ||
script: | ||
- npm run lint | ||
- npm run test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,115 +1,85 @@ | ||
#!/usr/bin/env node | ||
const parseTorrent = require('parse-torrent') | ||
const createTorrent = require('create-torrent') | ||
const request = require('request-promise-native') | ||
const WtManifest = require('./lib/wtManifest') | ||
const argv = require('yargs').argv | ||
const express = require('express') | ||
const app = express() | ||
|
||
let playlistLocation = '' | ||
let playlistName = '' | ||
let chunksLocation = '' | ||
const announceList = [['wss://tracker.openwebtorrent.com']] | ||
let manifest | ||
let sequence = 0 | ||
const fileToMagnet = {} | ||
const magnetsOrder = [] | ||
|
||
const help = ` | ||
🛰 Live-torrent 🛰 | ||
console.verbose = m => argv.v && (console.log('\x1Bc') || console.log(m)) | ||
|
||
const help = `🛰 Live-torrent 🛰 | ||
# To convert an existing stream to a live-torrent feed | ||
-u manifest location | ||
-p playlist name | ||
-c video chunk location - default same as url | ||
-r manifest refresh rate (in seconds) - default 5 | ||
-c video chunk location - default same as -u | ||
# To create a stream from a folder with HLS chunks | ||
-f folder with chunks location | ||
-l start from beggining and loop - default false | ||
# Misc | ||
-s add simple testpage to server - default true | ||
-v display manifest when generated - default false | ||
-r manifest refresh rate (in seconds) - default 2 | ||
eg. live-torrent -u http://wms.shared.streamshow.it/carinatv/carinatv/ -p playlist.m3u8 | ||
` | ||
const chunkName = url => url.match(/\d+(\.ts)/g)[0] | ||
eg. from existing feed | ||
live-torrent -v -u https://live.computer -p manifest.m3u8 | ||
eg. from local folder with ts files | ||
live-torrent -v -l -f feed/ | ||
` | ||
|
||
function die (msg, code) { | ||
console.log(help + '\n' + msg) | ||
console.log(msg.error ? msg.error : '\n' + msg) | ||
process.exit(code) | ||
} | ||
|
||
function computeMagnet (file, cn) { | ||
return new Promise((resolve, reject) => { | ||
file.name = cn | ||
createTorrent(file, { announceList }, (err, t) => { | ||
if (err) return console.log(err) | ||
const magnet = parseTorrent.toMagnetURI(parseTorrent(t)) | ||
resolve('###' + magnet) | ||
}) | ||
}) | ||
if (argv.h || argv.help || !((argv.p && argv.u) || argv.f)) { | ||
die(help, 0) | ||
} | ||
|
||
async function makeMagnet (fn) { | ||
// Extract chunk name. Return magnet if already computed | ||
const cn = chunkName(fn) | ||
if (fileToMagnet[cn]) return fileToMagnet[cn] | ||
|
||
// Fetch payload and compute magnet | ||
const payload = await request(chunksLocation + fn, { encoding: null }) | ||
const magnet = await computeMagnet(payload, cn) | ||
|
||
// Store magnet computed | ||
fileToMagnet[cn] = magnet | ||
magnetsOrder.push(cn) | ||
console.log('\nStarting server on port 8008\n') | ||
const sampleWebserver = typeof argv.s === 'undefined' ? true : (argv.s === 'true') | ||
const delay = parseInt(argv.r || 10) | ||
|
||
if (magnetsOrder.length > 10) { | ||
const oldMagnet = magnetsOrder.shift() | ||
delete fileToMagnet[oldMagnet] | ||
} | ||
} | ||
const manifestLocation = argv.u | ||
const playlistName = argv.p | ||
const chunksLocation = argv.c || argv.u | ||
|
||
async function makeAllMagnets (files) { | ||
return Promise.all(files.map(makeMagnet)) | ||
} | ||
const makeFromFolder = argv.f | ||
const loop = !!argv.l | ||
|
||
async function doManifest (path = '') { | ||
const _manifest = await request(playlistLocation + (path || playlistName)) | ||
const wtm = new WtManifest(chunksLocation, manifestLocation, playlistName, makeFromFolder, delay, loop) | ||
|
||
// Head over to the playlist, if what we got was a link to a playlist | ||
if (_manifest.includes('.m3u8')) { | ||
const m3u8 = _manifest.split('\n').find(l => l.includes('.m3u8')) | ||
return doManifest(m3u8) | ||
} | ||
app.get('*.m3u8', (req, res) => res.send(wtm.manifest)) | ||
|
||
// Split manifest and get sequenece number | ||
let split = _manifest.split('\n') | ||
const _sequence = split.filter(l => l.includes(`#EXT-X-MEDIA-SEQUENCE:`))[0].replace('#EXT-X-MEDIA-SEQUENCE:', '') | ||
if (_sequence === sequence) { | ||
return console.log('\nManifest unchanged\n') | ||
} | ||
if (sampleWebserver) app.use(express.static('client')) | ||
|
||
// Remove any existing magnet link from manifest (useful for testing) | ||
split = split.filter(l => !l.includes('magnet')) | ||
if (makeFromFolder) { | ||
app.use((req, res, next) => { | ||
res.header('Access-Control-Allow-Origin', '*') | ||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept') | ||
next() | ||
}) | ||
app.use(express.static(makeFromFolder)) | ||
} | ||
|
||
// Extract TS files and make magnet links | ||
const files = split.filter(l => l.includes('.ts')) | ||
await makeAllMagnets(files) | ||
const makeManifest = async (cb) => { | ||
try { | ||
await wtm.doManifest() | ||
} catch (e) { die(e, 1) } | ||
|
||
// Pop manifest back, inject magnet links alongside TS files | ||
manifest = split.map(l => l.includes('.ts') ? fileToMagnet[chunkName(l)] + '\n' + chunksLocation + l : l).join('\n') | ||
sequence = _sequence | ||
console.log(manifest) | ||
} | ||
if (!app.started) { | ||
app.started = true | ||
app.listen(8008) | ||
} | ||
|
||
if (argv.h || argv.help) { | ||
die('', 0) | ||
console.verbose(` | ||
${sampleWebserver ? '### Sample client fileserver running on http://127.0.0.1:8008' : ''} | ||
### Manifest at: http://127.0.0.1:8008/manifest.m3u8 | ||
### Manifest generated on: ${new Date()}\n\n${wtm.manifest}`) | ||
} | ||
|
||
if (argv.u && argv.p) { | ||
console.log('Starting server\n') | ||
app.get('*.m3u8', (req, res) => res.send(manifest)) | ||
app.use(express.static('client')) | ||
app.listen(8008) | ||
|
||
chunksLocation = argv.c || argv.u | ||
playlistLocation = argv.u | ||
playlistName = argv.p | ||
doManifest() | ||
setInterval(doManifest, (argv.r || 5) * 1000) | ||
} else { | ||
die('', 0) | ||
} | ||
makeManifest() | ||
setInterval(makeManifest, delay * 1000) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
<title>live-torrent demo</title> | ||
</head> | ||
|
||
<body> <h1>Tests</h1> </body> | ||
|
||
<script src="./build.js"></script> | ||
<script> | ||
const chunksDone = {} | ||
|
||
const dlManifest = async () => { | ||
const m = await fetch('manifest.m3u8') | ||
const t = await m.text() | ||
const last = t.split('\n').filter(l => l.endsWith('.ts'))[2] | ||
if (chunksDone[last] || !last) return | ||
chunksDone[last] = true | ||
fetch(last) | ||
} | ||
|
||
dlManifest() | ||
setInterval(dlManifest, 4000) | ||
</script> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,16 @@ | ||
Here's a quick howto on how to get started from a video file. | ||
Here's a quick howto to get started from a video file. | ||
|
||
```sh | ||
# Download test file, or use any mp4, h264 encoded file | ||
mkdir feed && cd feed | ||
cd feed | ||
wget http://peach.themazzone.com/durian/movies/sintel-1024-surround.mp4 | ||
|
||
|
||
# Convert to HLS (needs ffmpeg 3+) | ||
ffmpeg -i sintel-1024-surround.mp4 -codec: copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls chunks.m3u8 | ||
|
||
# Rename manifest | ||
mv chunks.m3u8 manifest.m3u8 | ||
|
||
# Start server script that will chop the HLS manifest into a live strean, and then serve your chunks and the client test page | ||
rm chunks.m3u8 | ||
cd .. | ||
node server/slicendice.js | ||
|
||
# Start feed from folder. Note the -l argument to loop over when video is over | ||
live-torrent -l -v -f feed | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
const fs = require('fs-extra') | ||
const getDuration = require('get-video-duration') | ||
|
||
const chunkName = url => url.match(/\d+(\.ts)/g)[0].replace('.ts', '') | ||
|
||
const now = () => new Date().getTime() | ||
|
||
function StreamMaker (loc, targetDuration = 10, loop = false) { | ||
this.chunk1 = 0 | ||
this.chunk2 = 1 | ||
this.chunk3 = 2 | ||
this.loop = loop | ||
this.targetDuration = targetDuration | ||
this.lastModif = now() | ||
this.loc = loc.endsWith('/') ? loc : loc + '/' | ||
|
||
fs.watch(loc, (ev, fn) => { this.lastModif = now() }) | ||
} | ||
|
||
StreamMaker.prototype.readLocal = async function (chunkNames) { | ||
// Read local folder, extract chunk ids and map ids to filenames | ||
const ls = await fs.readdir(this.loc) | ||
const ids = ls.filter(i => i.includes('.ts')).map(chunkName).map(i => parseInt(i)).sort((a, b) => a - b) | ||
|
||
const idToChunkname = {} | ||
ls.filter(i => i.includes('.ts')).forEach(el => { idToChunkname[chunkName(el)] = el }) | ||
return { idToChunkname, ids } | ||
} | ||
|
||
StreamMaker.prototype.loopFeed = async function () { | ||
// Make a looping stream out of the folder | ||
const { idToChunkname, ids } = await this.readLocal() | ||
|
||
this.chunk1 = this.chunk1 = this.chunk1 === (ids.length - 1) ? 0 : this.chunk1 + 1 | ||
this.chunk2 = this.chunk2 = this.chunk2 === (ids.length - 1) ? 0 : this.chunk2 + 1 | ||
this.chunk3 = this.chunk3 = this.chunk3 === (ids.length - 1) ? 0 : this.chunk3 + 1 | ||
|
||
return idToChunkname | ||
} | ||
|
||
StreamMaker.prototype.normalFeed = async function () { | ||
// Make a normal stream out of the folder. Just takes the last chunks and make a manifest ouf of them | ||
const { idToChunkname, ids } = await this.readLocal() | ||
|
||
// Remove last item from list if file is currently being written | ||
if (now() - this.lastModif < 200) ids.pop() | ||
|
||
this.chunk1 = ids[ids.length - 3] | ||
this.chunk2 = ids[ids.length - 2] | ||
this.chunk3 = ids[ids.length - 1] | ||
|
||
return idToChunkname | ||
} | ||
|
||
StreamMaker.prototype.makeLiveStream = async function () { | ||
const idToChunkname = this.loop ? await this.loopFeed() : await this.normalFeed() | ||
|
||
const chunkname1 = idToChunkname[this.chunk1] | ||
const chunkname2 = idToChunkname[this.chunk2] | ||
const chunkname3 = idToChunkname[this.chunk3] | ||
const dur1 = await getDuration(this.loc + chunkname1) | ||
const dur2 = await getDuration(this.loc + chunkname2) | ||
const dur3 = await getDuration(this.loc + chunkname3) | ||
|
||
const discontinuity = this.chunk3 !== this.chunk2 + 1 | ||
|
||
const manifest = `#EXTM3U | ||
#EXT-X-VERSION:3 | ||
#EXT-X-TARGETDURATION:${this.targetDuration} | ||
#EXT-X-MEDIA-SEQUENCE:${this.chunk1} | ||
#EXTINF: ${dur1}\n${chunkname1} | ||
#EXTINF: ${dur2}\n${chunkname2}${discontinuity ? '\n#EXT-X-DISCONTINUITY' : ''} | ||
#EXTINF: ${dur3}\n${chunkname3}` | ||
|
||
return manifest | ||
} | ||
|
||
// const sm = new StreamMaker('..//feed') | ||
// setInterval(() => sm.makeLiveStream(), 500) | ||
|
||
module.exports = StreamMaker |
Oops, something went wrong.