├── .gitignore
├── screenshot.png
├── index.html
├── components
├── player.js
├── input.js
├── button.js
├── link.js
├── home.js
├── settings.js
├── viewer.js
└── broadcast.js
├── app.js
├── README.md
├── package.json
├── style.css
├── LICENSE
├── lib
├── media-devices.js
├── broadcast.js
└── watch.js
└── index.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | streams
3 | bundle.js
4 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YerkoPalma/hypervision/master/screenshot.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | hypervision
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/components/player.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var css = require('sheetify')
3 |
4 | module.exports = function (state, emit) {
5 | var style = css`
6 | :host {
7 | width: 100%;
8 | video { width: 100%; }
9 | }
10 | `
11 |
12 | var el = html`
13 |
14 |
15 |
16 | `
17 |
18 | return el
19 | }
20 |
--------------------------------------------------------------------------------
/components/input.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var css = require('sheetify')
3 |
4 | module.exports = input
5 |
6 | function input (value, oninput) {
7 | var style = css`
8 | :host {
9 | font-size: 16px;
10 | padding: 0.4rem 0 0.4rem 0.65rem;
11 | width: 14rem;
12 | border: none;
13 | border-radius: 2px;
14 | font-weight: 500;
15 | -webkit-user-select: auto;
16 | }
17 | `
18 |
19 | return html``
20 | }
21 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var electron = require('electron')
2 | var path = require('path')
3 |
4 | var win = null
5 | var app = electron.app
6 | var BrowserWindow = electron.BrowserWindow
7 |
8 | app.on('ready', function () {
9 | console.log('The application is ready.')
10 |
11 | win = new BrowserWindow({
12 | width: 854,
13 | height: 650,
14 | minWidth: 550,
15 | minHeight: 200
16 | })
17 |
18 | win.loadURL('file://' + path.join(__dirname, 'index.html'))
19 | win.on('close', function () {
20 | win = null
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/components/button.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var css = require('sheetify')
3 |
4 | module.exports = button
5 |
6 | function button (color, text, onclick) {
7 | var style = css`
8 | :host {
9 | border: none;
10 | color: #FFFFFF;
11 | padding: 0.5rem 0.6rem 0.45rem 0.6rem;
12 | font-size: 18px;
13 | border-radius: 2px;
14 | text-decoration: none;
15 | display: inline-flex;
16 | flex-direction: row;
17 | align-items: center;
18 | justify-content: center;
19 | }
20 | `
21 |
22 | return html`
23 |
24 | ${ text }
25 |
26 | `
27 |
28 | function bgColor () {
29 | return `background: var(--color-${ color });`
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/components/link.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var css = require('sheetify')
3 |
4 | module.exports = link
5 |
6 | function link (color, text, location) {
7 | var style = css`
8 | :host {
9 | border: none;
10 | color: #FFFFFF;
11 | padding: 0.5rem 0.6rem 0.45rem 0.6rem;
12 | font-size: 18px;
13 | border-radius: 2px;
14 | text-decoration: none;
15 | display: inline-flex;
16 | flex-direction: row;
17 | align-items: center;
18 | justify-content: center;
19 | cursor: default;
20 | }
21 | `
22 |
23 | return html`
24 |
25 | ${ text }
26 |
27 | `
28 |
29 | function bgColor () {
30 | return `background: var(--color-${ color });`
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hypervision
2 |
3 | `hypervision` is a desktop application that lets you both watch and broadcast p2p live streams.
4 |
5 | When users connect to a stream, they distribute the data they receive amongst each other. This bypasses the need for a central server, and the huge amount of bandwidth required to deliver the same data to every user.
6 |
7 | `hypervision` is built on top of [hypercore](https://github.com/mafintosh/hypercore) & [hyperdiscovery](https://github.com/karissa/hyperdiscovery), both of which help facilitate the p2p networking which connects broadcasters and viewers together.
8 |
9 | 
10 |
11 | ## Installation
12 | ```
13 | git clone git://github.com/mafintosh/hypervision.git
14 | cd hypervision
15 |
16 | npm install
17 | npm run build
18 | npm start
19 | ```
20 |
21 | ## License
22 |
23 | MIT
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hypervision",
3 | "version": "0.0.0",
4 | "description": "P2P Television",
5 | "dependencies": {
6 | "browserify": "^14.1.0",
7 | "choo": "^5.0.4",
8 | "electron": "1.4.6",
9 | "end-of-stream": "^1.4.0",
10 | "getusermedia": "^2.0.0",
11 | "hypercore": "^5.4.4",
12 | "hyperdiscovery": "^1.2.0",
13 | "media-recorder-stream": "^2.1.1",
14 | "on-load": "^3.2.0",
15 | "pump": "^1.0.2",
16 | "sheetify": "^6.0.1",
17 | "sheetify-nested": "^1.0.2",
18 | "webm-cluster-stream": "^1.0.0"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "https://github.com/mafintosh/hypervision.git"
23 | },
24 | "scripts": {
25 | "start": "npm run build && electron app.js",
26 | "build": "node build.js"
27 | },
28 | "author": "Mathias Buus (@mafintosh)",
29 | "license": "MIT",
30 | "bugs": {
31 | "url": "https://github.com/mafintosh/hypervision/issues"
32 | },
33 | "homepage": "https://github.com/mafintosh/hypervision"
34 | }
35 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | /* Variables */
2 | :root {
3 | --color-red: #F9364E;
4 | --color-green: #3ABFA1;
5 | --color-pink: #FFA0AC;
6 |
7 | --color-grey: #999999;
8 | --color-white: #FFFFFF;
9 | --color-off-white: #F5F5F5;
10 | --color-font-black: #333333;
11 | --color-off-black: #222222;
12 | }
13 |
14 | /* Reset */
15 | html, body, div, span, a, video {
16 | margin: 0;
17 | padding: 0;
18 | border: 0;
19 | font-size: 100%;
20 | font: inherit;
21 | vertical-align: baseline;
22 | }
23 | body {
24 | line-height: 1;
25 | }
26 |
27 | /* Styles */
28 | body {
29 | color: var(--color-font-black);
30 | font-size: 18px;
31 | font-weight: 500;
32 | font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Helvetica", sans-serif;
33 | letter-spacing: -0.04rem;
34 | -webkit-font-smoothing: antialiased;
35 | -webkit-user-select: none;
36 | cursor: default;
37 | }
38 |
39 | main {
40 | display: flex;
41 | height: 100vh;
42 | background: var(--color-off-black);
43 | flex-direction: column;
44 | align-items: center;
45 | justify-content: center;
46 | box-sizing: border-box;
47 | }
48 |
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Mathias Buus
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/components/home.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var css = require('sheetify')
3 |
4 | var link= require('./link')
5 | var input = require('./input')
6 |
7 | var style = css`
8 | :host {
9 | background: var(--color-off-white);
10 | text-align: center;
11 |
12 | .title {
13 | font-size: 24px;
14 | margin-bottom: 3rem;
15 | letter-spacing: 0;
16 | }
17 |
18 | .watch { margin-bottom: 1.5rem; }
19 | .label { margin-bottom: 0.5rem; }
20 | }
21 | `
22 |
23 | module.exports = function (state, emit) {
24 | return html`
25 |
26 |
27 |
hypervision
28 |
p2p live streaming
29 |
30 |
31 |
32 |
Watch stream
33 | ${ input(state.hash, watch) }
34 |
35 |
36 |
37 |
Start broadcasting
38 | ${ link('pink', 'Go live', '/broadcast') }
39 |
40 |
41 | `
42 |
43 | // check for valid hash, then open stream
44 | function watch (e) {
45 | emit('watch', e.target.value)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/lib/media-devices.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get: get,
3 | start: start,
4 | stop: stop
5 | }
6 |
7 | // get list of media inputs connected to computer
8 | function get (done) {
9 | navigator.mediaDevices.enumerateDevices()
10 | .then(function (devices) {
11 | done(null, devices)
12 | })
13 | .catch(function (err) {
14 | done(err)
15 | })
16 | }
17 |
18 | function start (videoDevice, audioDevice, cb) {
19 | var videoOpts = { video: true }
20 | var audioOpts = { audio: true }
21 |
22 | if (videoDevice) {
23 | // if user has selected 'screen sharing'
24 | if (videoDevice.kind === 'screen') {
25 | videoOpts = {
26 | video: {
27 | mandatory: {
28 | chromeMediaSource: 'screen',
29 | maxWidth: 1920,
30 | maxHeight: 1080,
31 | maxFrameRate: 25
32 | }
33 | }
34 | }
35 | } else {
36 | videoOpts = { video: { deviceId: { exact: videoDevice.deviceId } } }
37 | }
38 | audioOpts = { audio: { deviceId: { exact: audioDevice.deviceId } } }
39 | }
40 |
41 | // add audio stream to video stream
42 | // (allows screen sharing with audio to work)
43 | navigator.webkitGetUserMedia(audioOpts, function (audioStream) {
44 | navigator.webkitGetUserMedia(videoOpts, function (mediaStream) {
45 | mediaStream.addTrack(audioStream.getAudioTracks()[0])
46 | cb(mediaStream)
47 | }, error)
48 | }, error)
49 | }
50 |
51 | // stop all media devices
52 | function stop () {
53 | var video = window.stream.getVideoTracks()
54 | var audio = window.stream.getAudioTracks()
55 |
56 | video.forEach(function (track) { track.stop() })
57 | audio.forEach(function (track) { track.stop() })
58 | }
59 |
60 |
61 | // error handling for `getUserMedia`
62 | function error (err) {
63 | if (err) console.log('err: ', err)
64 | }
65 |
--------------------------------------------------------------------------------
/lib/broadcast.js:
--------------------------------------------------------------------------------
1 | var recorder = require('media-recorder-stream')
2 | var hypercore = require('hypercore')
3 | var hyperdiscovery = require('hyperdiscovery')
4 | var desktopCapturer = require('electron').desktopCapturer
5 | var cluster = require('webm-cluster-stream')
6 | var pump = require('pump')
7 |
8 | module.exports = {
9 | start: start,
10 | stop: stop
11 | }
12 |
13 | // start broadcast
14 | function start (quality, media, cb) {
15 | // create bitrate options
16 | var video = (quality === 3) ? 800000 : (quality === 2) ? 500000 : 200000
17 | var audio = (quality === 3) ? 128000 : (quality === 2) ? 64000 : 32000
18 |
19 | // create MediaRecorder
20 | var opts = {
21 | interval: 1000,
22 | videoBitsPerSecond: video,
23 | audioBitsPerSecond: audio,
24 | }
25 |
26 | // create MediaRecorder stream
27 | var mediaRecorder = recorder(media, opts)
28 |
29 | // create a feed
30 | var feed = hypercore(`./streams/broadcasted/${ Date.now ()}`)
31 |
32 | // when feed is ready,
33 | // join p2p swarm & run callback
34 | var swarm
35 | feed.on('ready', function () {
36 | swarm = hyperdiscovery(feed, {live: true})
37 |
38 | var hash = feed.key.toString('hex')
39 | cb(mediaRecorder, hash)
40 | })
41 |
42 | // pipe MediaRecorder to webm transform
43 | // when MediaRecorder is destroyed, close feed & swarm
44 | var stream = pump(mediaRecorder, cluster(), function (err) {
45 | if (err) console.log('err: ', err)
46 | swarm.close()
47 | feed.close(function (err) { if (err) console.log('err: ', err) })
48 | })
49 |
50 | // append any new video to feed
51 | stream.on('data', function (data) {
52 | console.log(data.length, Math.floor(data.length / 16 / 1024), Math.floor(data.length / 10))
53 | feed.append(data)
54 | })
55 | }
56 |
57 | // stop broadcast
58 | function stop (recorder, cb) {
59 | recorder.stop()
60 | cb()
61 | }
62 |
63 |
--------------------------------------------------------------------------------
/lib/watch.js:
--------------------------------------------------------------------------------
1 | var http = require('http')
2 | var hypercore = require('hypercore')
3 | var hyperdiscovery = require('hyperdiscovery')
4 | var eos = require('end-of-stream')
5 |
6 | module.exports = {
7 | start: start
8 | }
9 |
10 | function start (hash, cb) {
11 | // create feed from stream hash
12 | var feed = hypercore(`./streams/viewed/${ Date.now ()}`, hash, {
13 | sparse: true
14 | })
15 |
16 | // when feed is ready, start watching the stream
17 | feed.on('ready', function () {
18 | feed.get(0, function () {})
19 |
20 | // join p2p swarm
21 | var swarm = hyperdiscovery(feed, {live: true})
22 |
23 | // create an http server to deliver video to user
24 | var server = http.createServer(function (req, res) {
25 | res.setHeader('Content-Type', 'video/webm')
26 | feed.get(0, function (err, data) {
27 | if (err) return res.end()
28 | res.write(data)
29 |
30 | var offset = feed.length
31 | var buf = 4
32 | while (buf-- && offset > 1) offset--
33 |
34 | var start = offset
35 |
36 | // start downloading data
37 | feed.download({start: start, linear: true})
38 |
39 | // when user stops watching stream, close everything down
40 | eos(res, function () {
41 | feed.undownload({start: start, linear: true})
42 | server.close()
43 | swarm.close()
44 | feed.close(function (err) { if (err) console.log('err: ', err) })
45 | })
46 |
47 | // keep piping new data from feed to response stream
48 | feed.get(offset, function loop (err, data) {
49 | if (err) return res.end()
50 | res.write(data, function () {
51 | feed.get(++offset, loop)
52 | })
53 | })
54 | })
55 | })
56 |
57 | // tune player into stream
58 | server.listen(0, function () {
59 | var port = server.address().port
60 | cb(port)
61 | })
62 | })
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var choo = require('choo')
2 | var css = require('sheetify')
3 |
4 | var app = choo()
5 |
6 | app.use(function (state, emitter) {
7 | // initial state
8 | state.hash = ''
9 | state.live = false
10 | state.quality = 3
11 | state.sources = {
12 | available: { video: [], audio: [] },
13 | selected: { video: null, audio: null }
14 | }
15 |
16 | // toggle on broadcast start/stop
17 | emitter.on('liveToggle', function (data) {
18 | emitter.emit('updateHash', data.live ? data.hash : '')
19 | state.live = data.live
20 |
21 | emitter.emit('render')
22 | })
23 |
24 | // sets broadcast bitrate
25 | emitter.on('qualityToggle', function () {
26 | var quality = state.quality
27 | state.quality = (quality === 1) ? 3 : (quality - 1)
28 |
29 | emitter.emit('render')
30 | })
31 |
32 | // sets available sources for broadcasting
33 | emitter.on('sourcesAvailable', function (data) {
34 | state.sources.available = {
35 | video: data.video,
36 | audio: data.audio
37 | }
38 |
39 | emitter.emit('render')
40 | })
41 |
42 | // select broadcast sources
43 | emitter.on('sourcesSelect', function (data) {
44 | state.sources.selected = {
45 | video: data.video,
46 | audio: data.audio
47 | }
48 |
49 | emitter.emit('pushState', '/broadcast')
50 | })
51 |
52 | // update stream hash
53 | emitter.on('updateHash', function (data) {
54 | state.hash = data
55 | })
56 |
57 | // watch stream
58 | emitter.on('watch', function (data) {
59 | emitter.emit('updateHash', data)
60 |
61 | if (state.hash.length === 64) {
62 | emitter.emit('redirect', '/view')
63 | }
64 | })
65 |
66 | // redirect utility
67 | emitter.on('redirect', function (data) {
68 | emitter.emit('pushState', data)
69 | })
70 | })
71 |
72 | // import base stylesheet
73 | css('./style.css')
74 |
75 | // routes
76 | app.route('/', require('./components/home'))
77 | app.route('/broadcast', require('./components/broadcast'))
78 | app.route('/view', require('./components/viewer'))
79 | app.route('/settings', require('./components/settings'))
80 |
81 | // start!
82 | document.body.appendChild(app.start())
83 |
--------------------------------------------------------------------------------
/components/settings.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var onload = require('on-load')
3 | var css = require('sheetify')
4 |
5 | var button = require('./button')
6 |
7 | var mediaDevices = require('../lib/media-devices')
8 |
9 | var $ = document.getElementById.bind(document)
10 |
11 | var style = css`
12 | :host {
13 | background: var(--color-off-white);
14 |
15 | section {
16 | margin: 0 0 1.5rem 0;
17 | }
18 |
19 | .title { margin: 0 0.75rem 0 0; }
20 |
21 | select {
22 | -webkit-appearance: none;
23 | background: var(--color-white);
24 | padding: 0.4rem 0 0.4rem 0.65rem;
25 | border-radius: 2px;
26 | border: none;
27 | width: 13rem;
28 | font-size: 16px;
29 | color: var(--color-font-black);
30 | letter-spacing: -0.04rem;
31 | font-weight: 500;
32 | }
33 | }
34 | `
35 |
36 | module.exports = function (state, emit) {
37 | var available = state.sources.available
38 | var selected = state.sources.selected
39 |
40 | var div = html`
41 |
42 |
43 |
44 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 | ${ button('grey', 'Done', done) }
57 |
58 | `
59 |
60 | // populate `
66 | } else {
67 | return html``
68 | }
69 | }
70 |
71 | // populate `
77 | } else {
78 | return html``
79 | }
80 | }
81 |
82 | // attach view lifecycle functions
83 | onload(div, load)
84 |
85 | // return function to router
86 | return div
87 |
88 | // when view finishes loading
89 | function load () {
90 | // if user hasn't previously selected any source inputs
91 | if (!state.sources.selected.video) {
92 | // get list of available source inputs
93 | mediaDevices.get(function (err, devices) {
94 | if (err) console.log('error: ', err)
95 |
96 | var videoDevices = []
97 | var audioDevices = []
98 |
99 | devices.forEach(function (device) {
100 | var kind = device.kind
101 |
102 | if (kind === 'videoinput') videoDevices.push(device)
103 | if (kind === 'audioinput') audioDevices.push(device)
104 | })
105 |
106 | // add a screen share video input to list
107 | videoDevices.push({
108 | deviceId: 'screen',
109 | kind: 'screen',
110 | label: 'Screen share'
111 | })
112 |
113 | emit('sourcesAvailable', {
114 | video: videoDevices,
115 | audio: audioDevices
116 | })
117 | })
118 | }
119 | }
120 |
121 | // when user closes settings menu, update state
122 | function done (e) {
123 | var video = $('videoinput').selectedIndex
124 | var audio = $('audioinput').selectedIndex
125 |
126 | var available = state.sources.available
127 |
128 | emit('sourcesSelect', {
129 | video: available.video[video],
130 | audio: available.audio[audio],
131 | })
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/components/viewer.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var onload = require('on-load')
3 | var css = require('sheetify')
4 |
5 | var button = require('./button')
6 | var player = require('./player')
7 |
8 | var watch = require('../lib/watch')
9 |
10 | var $ = document.getElementById.bind(document)
11 |
12 | module.exports = function (state, emit) {
13 | var style = css`
14 | :host {
15 | .preview { width: 100%; }
16 | video { width: 100%; }
17 |
18 | .overlay {
19 | position: fixed;
20 | height: 100vh;
21 | width: 100vw;
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: space-between;
25 | background: rgba(256, 256, 256, 0.3);
26 | transition: opacity 0.5s
27 | }
28 |
29 | header {
30 | display: flex;
31 | flex-direction: row;
32 | justify-content: space-between;
33 | padding: 1rem;
34 | }
35 |
36 | section {
37 | display: flex;
38 | flex-direction: row;
39 | }
40 |
41 | .fullscreen {
42 | color: white;
43 |
44 | text-align: center;
45 | width: 5.5rem;
46 | border-radius: 2px;
47 | padding: 0.5rem 0.65rem 0.5rem 0.6rem;
48 | margin: 0 1rem 0 0;
49 | }
50 |
51 | input[type=range] {
52 | -webkit-appearance: none;
53 | }
54 |
55 | input[type=range]:focus {
56 | outline: none;
57 | }
58 |
59 | input[type=range]::-webkit-slider-runnable-track {
60 | width: 100%;
61 | height: 5px;
62 | cursor: pointer;
63 | background: pink;
64 | border-radius: 1.3px;
65 | }
66 |
67 | input[type=range]::-webkit-slider-thumb {
68 | height: 15px;
69 | width: 15px;
70 | border-radius: 15px;
71 | background: #ffffff;
72 | cursor: pointer;
73 | -webkit-appearance: none;
74 | margin-top: -5.5px;
75 | }
76 |
77 | footer {
78 | display: flex;
79 | flex-direction: row;
80 | justify-content: space-between;
81 | padding: 1rem;
82 |
83 | .share {
84 | display: flex;
85 | flex-direction: row;
86 | align-items: center;
87 |
88 | span {
89 | padding: 0 1rem 0 0;
90 | }
91 |
92 | input {
93 | font-size: 16px;
94 | padding: 0.4rem 0 0.4rem 0.65rem;
95 | width: 14rem;
96 | border: none;
97 | border-radius: 2px;
98 | }
99 | }
100 | }
101 | }
102 | `
103 | var div = html`
104 |
105 | ${ player() }
106 |
107 |
108 |
109 | ${ button('grey', 'Fullscreen', fullscreen) }
110 |
111 |
116 |
117 |
124 |
125 |
126 | `
127 |
128 | // attach view lifecycle functions
129 | onload(div, load)
130 |
131 | // return function to router
132 | return div
133 |
134 | // play stream on load
135 | function load () {
136 | watch.start(state.hash, function (port) {
137 | $('player').volume = 0.75
138 | $('player').src = 'http://localhost:' + port + '/video.webm'
139 | })
140 | }
141 |
142 | // start jumbo vision
143 | function fullscreen () {
144 | $('player').webkitRequestFullscreen()
145 | }
146 |
147 | // when user's mouse enters window
148 | function hoverEnter () {
149 | $('overlay').style = "opacity: 1"
150 | }
151 |
152 | // when user's mouse leaves window
153 | function hoverLeave () {
154 | $('overlay').style = "opacity: 0"
155 | }
156 |
157 | // when user changes volume
158 | function volumeChange (e) {
159 | $('player').volume = e.target.value / 100
160 | }
161 |
162 | // exit stream & go back to menu
163 | function mainMenu () {
164 | $('player').src = ''
165 | emit('updateHash', '')
166 | emit('redirect', `/`)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/components/broadcast.js:
--------------------------------------------------------------------------------
1 | var html = require('choo/html')
2 | var onload = require('on-load')
3 | var css = require('sheetify')
4 |
5 | var mediaDevices = require('../lib/media-devices')
6 | var broadcast = require('../lib/broadcast')
7 | var button = require('./button')
8 | var link = require('./link')
9 |
10 | var $ = document.getElementById.bind(document)
11 |
12 | module.exports = function (state, emit) {
13 | var divStyle = css`
14 | :host {
15 | .preview { width: 100%; }
16 | video { width: 100%; }
17 |
18 | .overlay {
19 | position: fixed;
20 | height: 100vh;
21 | width: 100vw;
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: space-between;
25 | background: rgba(256, 256, 256, 0.3);
26 | transition: opacity 0.5s;
27 | }
28 |
29 | header {
30 | display: flex;
31 | flex-direction: row;
32 | justify-content: space-between;
33 | padding: 1rem;
34 |
35 | section {
36 | display: flex;
37 | flex-direction: row;
38 |
39 | > * { margin-right: 1rem; }
40 | :last-child { margin-right: 0; }
41 | }
42 | }
43 |
44 | footer {
45 | display: flex;
46 | flex-direction: row;
47 | justify-content: space-between;
48 | padding: 1rem;
49 |
50 | .share {
51 | display: flex;
52 | flex-direction: row;
53 | align-items: center;
54 |
55 | span { padding: 0 1rem 0 0; }
56 |
57 | input {
58 | font-size: 16px;
59 | padding: 0.4rem 0 0.4rem 0.65rem;
60 | width: 14rem;
61 | border: none;
62 | border-radius: 2px;
63 | }
64 | }
65 | }
66 | }
67 | `
68 |
69 | var div = html`
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | ${ button(state.live ? 'red' : 'grey', state.live ? 'ON AIR' : 'OFF AIR') }
78 | ${ button(state.live ? 'pink' : 'green', state.live ? 'Stop' : 'Start', state.live ? stop : start) }
79 |
80 |
81 | ${ button('pink', quality(), qualityToggle) }
82 | ${ link('pink', 'Settings', '/settings') }
83 |
84 |
85 |
92 |
93 |
94 | `
95 |
96 | // attach view lifecycle functions
97 | onload(div, load, unload)
98 |
99 | // return function to router
100 | return div
101 |
102 | // open media devices on entry
103 | function load () {
104 | var selected = state.sources.selected
105 |
106 | var video = selected.video
107 | var audio = selected.audio
108 |
109 | mediaDevices.start(video, audio, function (mediaStream) {
110 | window.stream = mediaStream
111 | $('player').volume = 0
112 | $('player').srcObject = mediaStream
113 | })
114 | }
115 |
116 | // stop media devices on exit
117 | function unload () {
118 | mediaDevices.stop()
119 | }
120 |
121 | // generate label for quality toggle button
122 | function quality () {
123 | var qual = state.quality
124 | return `${ ((qual === 1) ? 'Low' : (qual === 2) ? 'Medium' : 'High') } quality`
125 | }
126 |
127 | // start broadcast
128 | function start () {
129 | var quality = state.quality
130 | broadcast.start(quality, window.stream, function (mediaRecorder, hash) {
131 | window.recorder = mediaRecorder
132 | emit('liveToggle', { live: true, hash: hash })
133 | })
134 | }
135 |
136 | // stop broadcast
137 | function stop () {
138 | broadcast.stop(window.recorder, function () {
139 | emit('liveToggle', false)
140 | })
141 | }
142 |
143 | // when user changes stream quality
144 | function qualityToggle () {
145 | emit('qualityToggle')
146 | }
147 |
148 | // when user's mouse enters window
149 | function hoverEnter () {
150 | $('overlay').style = "opacity: 1"
151 | }
152 |
153 | // when user's mouse leaves window
154 | function hoverLeave () {
155 | $('overlay').style = "opacity: 0"
156 | }
157 | }
158 |
--------------------------------------------------------------------------------