Skip to content

Commit

Permalink
Merge pull request #3 from pldubouilh/tests
Browse files Browse the repository at this point in the history
Tests / refactor
  • Loading branch information
pldubouilh authored Feb 28, 2018
2 parents fd2fa14 + c177830 commit 4789a4a
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 196 deletions.
11 changes: 11 additions & 0 deletions .travis.yml
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
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
live-torrent
=============

[![Build Status](https://travis-ci.org/pldubouilh/live-torrent.svg?branch=master)](https://travis-ci.org/pldubouilh/live-torrent)


![ex](https://user-images.githubusercontent.com/760637/36377295-f08d0ff2-1576-11e8-97c0-dcb91246529d.png)


Expand All @@ -14,32 +18,37 @@ Yes please ! Live demo with sintel at [live.computer](https://live.computer)
# Install
npm i -g live-torrent

# Start with example live-feed
live-torrent -u http://wms.shared.streamshow.it/carinatv/carinatv/ -p playlist.m3u8
# Start with example live-feed. Manifest fetched from https://live.computer/manifest.m3u8
live-torrent -v -u https://live.computer -p manifest.m3u8

# ... or Create a Webtorrent enabled feed from a folder with .ts files
live-torrent -v -l -f feed

# Open browser at http://127.0.0.1:8008
```

### FAQ
> I have a regular feed already
live-torrent can convert your feed into a webtorrent enabled feed. Just install the CLI tool and start converting your feed.
live-torrent can convert your feed into a webtorrent enabled feed. The first example command above will download the feed at `https://live.computer/manifest.m3u8`, and generate a webtorrent-enabled HLS feed from it. Just open your web-browser at `http://127.0.0.1:8008` to have a look.

> I want to create a feed !
Have a look in the `server/` directory !
No problem - the second example up here will generate a feed for the directory `feed`, how simple ! New chunks added to the directory will be pushed to the manifest.

Have a look in the [feed directory](https://github.com/pldubouilh/live-torrent/tree/master/feed) for instructions on how to generate a sample feed from a mp4 file.

> How to implement on a website ?
Just host the script/sw yourself. Also, there are some limitations to the use of SW ; it needs to be served from HTTPS, and it should be located at the root of the domain (e.g. `https://live.computer/sw.js`). Also feel free to open an issue if something's acting weird :)
Just host the script, serviceworker and videoplayer on your site and you're good to go. Also, there are some limitations to the use of SW ; the site hosting the videoplayer needs to be served from HTTPS, and serviceworker should be located at the root of the domain (e.g. `https://live.computer/sw.js`). Also feel free to open an issue if something's acting weird :)

### How is it working ?
TLDR(ish); A server script parses the video manifest and generates torrent magnet links from the video chunks. The magnets are pushed on the manifest.

Now on the browser side, the videoplayer downloads the manifest, the serviceworker hijacks the request, extracts the magnet links, and tries to download the chunks via webtorrent. If it fails, it falls back to the manifest url (and then seed), otherwise, well p2p - yay !

Basically 3 different pieces are needed :
1. a server script that takes in a HLS feed and adds the magnet links to it
1. a server script to make a HLS manifest with magnet links in it
2. serviceworker to proxy the manifest/chunks requests
3. client script, that's the bit utilizing webtorrent (no webrtc in SW !)

Expand Down
146 changes: 58 additions & 88 deletions cli.js
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)
27 changes: 27 additions & 0 deletions client/fake-video.html
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>
15 changes: 7 additions & 8 deletions server/readme.md → feed/readme.md
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
```
81 changes: 81 additions & 0 deletions lib/streamMaker.js
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
Loading

0 comments on commit 4789a4a

Please sign in to comment.