├── .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 | ![hypervision screenshot](screenshot.png) 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 |
112 |
113 | 114 |
115 |
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 | --------------------------------------------------------------------------------