diff --git a/.gitignore b/.gitignore index c2658d7..f685cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +config/default.json diff --git a/README.md b/README.md index 3ac04a8..b959b63 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Start the Node express app: # ENV vars -To start a listener on a port other than 3000, set/export the `PORT` ENV var. +The application listens on port 3939 by default. To run on a different port, set/export the `PORT` environment variable. To start a HTTPs server, export: ``` SSL_KEY=/path/to/key @@ -37,15 +37,22 @@ If you wish to auto redirect from HTTP (TCP 80) to HTTPs, set/export: ``` REDIRECT_HTTP=true ``` +If the application is served behind a reverse proxy on a sub path you can set +the `BASE_PATH` environment variable to that path (e.g. `/mcr`). +This ensures that static assets and routes are mounted correctly when the +prefix is preserved by the proxy. **Note that you can only start a listener on TCP 80 and 443 as a super user since these are well known port.** # Usage When the Node express app is up and running you can direct your Chrome browser to: -> http://localhost:3000/?config=example.json` - -where `example.json` is a configuration file placed in the directory `config/` and can look like this: +> http://localhost:3939/?config=example.json` + +where `example.json` is a configuration file placed in the directory `config/`. +If the `config` parameter is omitted the application will instead look for +`config/default.json`. +It can look like this: ```json { "row0": [ @@ -74,7 +81,7 @@ For example, adding: Will play path/to/placeholder/videostream.mpd in the extra divs. If no `placeholder` is defined, these divs' `display` attribute will be set to `none`, effectively hiding them. -To toggle audio on or off click on the viewport that you want to listen to. A green border indicates for which viewport the audio is enabled. You can also use the keyboard keys 1-8. +To toggle audio on or off click on the viewport that you want to listen to. A green border indicates for which viewport the audio is enabled. You can also use the keyboard keys 1-8 to control the first eight viewports. ## Keyboard Shortcuts - SPACE - toggle play / pause for all viewports diff --git a/app.js b/app.js index 47d7a28..5552ca7 100644 --- a/app.js +++ b/app.js @@ -10,9 +10,18 @@ var cookieParser = require('cookie-parser'); var bodyParser = require('body-parser'); var routes = require('./routes/index'); +var streamRoutes = require('./routes/stream'); var app = express(); +// Optional base path used when the application is served behind a reverse proxy +// on a path prefix. Example: if BASE_PATH=/mcr the app will listen on +// https://example.com/mcr/. +var basePath = process.env.BASE_PATH || ''; +if (basePath !== '' && basePath.endsWith('/')) { + basePath = basePath.slice(0, -1); +} + // view engine setup app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); @@ -23,9 +32,10 @@ app.use(logger('dev')); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); -app.use(express.static(path.join(__dirname, 'public'))); +app.use(basePath, express.static(path.join(__dirname, 'public'))); -app.use('/', routes); +app.use(basePath || '/', routes); +app.use(basePath || '/', streamRoutes); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/bin/www b/bin/www index beedc83..061a04a 100755 --- a/bin/www +++ b/bin/www @@ -6,7 +6,7 @@ var https = require('https'); var http = require('http'); -app.set('port', process.env.PORT || 3000); +app.set('port', process.env.PORT || 3939); // If these ENV vars are all set, start Express over HTTPs if (process.env.SSL_KEY && process.env.SSL_CRT && process.env.SSL_CA) { diff --git a/config/example16.json b/config/example16.json new file mode 100644 index 0000000..a99f375 --- /dev/null +++ b/config/example16.json @@ -0,0 +1,74 @@ +{ + "row0": [ + { "title": "Example 1", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + }, + { "title": "Example 2", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + }, + { "title": "Example 3", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + }, + { "title": "Example 4", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + } + ], + "row1": [ + { "title": "Example 5", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + }, + { "title": "Example 6", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + }, + { "title": "Example 7", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + }, + { "title": "Example 8", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + } + ], + "row2": [ + { "title": "Example 9", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + }, + { "title": "Example 10", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + }, + { "title": "Example 11", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + }, + { "title": "Example 12", + "manifest": "https://content.jwplatform.com/manifests/vM7nH0Kl.m3u8", + "type": "hls" + } + ], + "row3": [ + { "title": "Example 13", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + }, + { "title": "Example 14", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + }, + { "title": "Example 15", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + }, + { "title": "Example 16", + "manifest": "https://www.bok.net/dash/tears_of_steel/cleartext/stream.mpd", + "type": "dash" + } + ] +} diff --git a/public/javascripts/viewer.js b/public/javascripts/viewer.js index c5c0169..d7e0aa7 100644 --- a/public/javascripts/viewer.js +++ b/public/javascripts/viewer.js @@ -4,6 +4,17 @@ // Author: Jonas Birme (Eyevinn Technology) var activeViewPort; var shakaPlayers = {}; +// Determine base path in case the application is hosted behind a proxy with a +// path prefix. This removes the trailing file or slash from the current +// location so that generated links work regardless of where the app is mounted. +var basePath = window.location.pathname.replace(/\/[^\/]*$/, ''); + +function getQueryParameter(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp('[\\?&]' + name + '=([^]*)'); + var results = regex.exec(window.location.search); + return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); +} function initHlsPlayer(conf, videoelemid, donecb) { var hlsconfig = { @@ -104,6 +115,13 @@ function initViewPortRow(row, numcols, config) { c = config['row'+row][i]; if (c) { initViewPort(c, videoelemid); + var linkElem = document.getElementById(videoelemid + '-view-link'); + if (linkElem) { + var cfg = getQueryParameter('config') || 'default.json'; + // Use the computed basePath so that the stream links work when the + // application is served behind a proxy on a path prefix. + linkElem.href = basePath + '/stream?config=' + encodeURIComponent(cfg) + '&row=' + row + '&col=' + i; + } }else if (config['placeholder'] !== undefined && config['placeholder'][0] !== undefined){ c = config['placeholder'][0]; initViewPort(c, videoelemid); @@ -139,10 +157,12 @@ function togglePlayback(videoelem) { } function togglePlaybackOnAllViewPorts() { - for(var i=0; i<2; i++) { + for(var i=0; i<4; i++) { for(var j=0; j<4; j++) { var videoelem = document.getElementById('vp'+i+j); - togglePlayback(videoelem); + if (videoelem) { + togglePlayback(videoelem); + } } } togglePlayback(document.getElementById('vpleft')); @@ -152,12 +172,15 @@ function togglePlaybackOnAllViewPorts() { function initMultiView(config) { if (config) { shaka.polyfill.installAll(); - initViewPortRow(0, 4, config); - initViewPortRow(1, 4, config); - if(config['row0'][0]) { + for(var r=0; r<4; r++) { + if(config['row'+r]) { + initViewPortRow(r, 4, config); + } + } + if(config['row0'] && config['row0'][0]) { initViewPort(config['row0'][0], 'vpleft'); } - if(config['row1'][0]) { + if(config['row1'] && config['row1'][0]) { initViewPort(config['row1'][0], 'vpright'); } } diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css index c54b58b..92ac870 100644 --- a/public/stylesheets/style.css +++ b/public/stylesheets/style.css @@ -49,6 +49,7 @@ th { min-height: 200px; padding: 0px; text-align: center; + position: relative; } .info { @@ -89,6 +90,22 @@ video { border: 3px solid green; } +.view-link { + position: absolute; + bottom: 10px; + right: 10px; + z-index: 20; + display: none; + padding: 2px 6px; + background-color: #000; + color: #fff; + border: 1px solid #fff; +} + +.vp:hover .view-link { + display: block; +} + .title { width: 90%; margin-left: auto; @@ -118,3 +135,8 @@ video { background-color: #000; } +/* Hide info row by default */ +#infoRow { + display: none; +} + diff --git a/routes/index.js b/routes/index.js index 9727399..b4f2227 100644 --- a/routes/index.js +++ b/routes/index.js @@ -10,21 +10,23 @@ const path = require('path'); function initiateDefaultConf() { return { "row0": [], - "row1": [] + "row1": [], + "row2": [], + "row3": [] }; } /* GET home page. */ router.get('/', function(req, res) { - conf = req.query.config; - var confobj = initiateDefaultConf(); - if(conf) { - var confpath = '../config/'+conf; - console.log("Loading config " + confpath); - if (fs.existsSync(path.join(__dirname, confpath))) { - var confobj = JSON.parse(fs.readFileSync(path.join(__dirname, confpath), 'utf8')); - } + let conf = req.query.config || 'default.json'; + let confobj = initiateDefaultConf(); + + const confpath = '../config/' + conf; + console.log('Loading config ' + confpath); + if (fs.existsSync(path.join(__dirname, confpath))) { + confobj = JSON.parse(fs.readFileSync(path.join(__dirname, confpath), 'utf8')); } + res.render('index', { title: 'Eyevinn Technology OTT Multiview', conf: JSON.stringify(confobj) }); }); diff --git a/routes/stream.js b/routes/stream.js new file mode 100644 index 0000000..9f7bca1 --- /dev/null +++ b/routes/stream.js @@ -0,0 +1,71 @@ +const express = require('express'); +const router = express.Router(); +const fs = require('fs'); +const path = require('path'); + +function initiateDefaultConf() { + return { + "row0": [], + "row1": [], + "row2": [], + "row3": [] + }; +} + +router.get('/stream', function(req, res) { + const conffile = req.query.config || 'default.json'; + const row = parseInt(req.query.row, 10); + const col = parseInt(req.query.col, 10); + + const confpath = '../config/' + conffile; + let confobj = initiateDefaultConf(); + if (fs.existsSync(path.join(__dirname, confpath))) { + confobj = JSON.parse(fs.readFileSync(path.join(__dirname, confpath), 'utf8')); + } + + if (isNaN(row) || isNaN(col) || !confobj['row' + row] || !confobj['row' + row][col]) { + res.status(404).send('Stream not found'); + return; + } + + const stream = confobj['row' + row][col]; + + const rows = Object.keys(confobj).filter(k => k.match(/^row\d+$/)).sort((a, b) => { + return parseInt(a.slice(3), 10) - parseInt(b.slice(3), 10); + }); + const positions = []; + rows.forEach((rk) => { + const rIdx = parseInt(rk.slice(3), 10); + const arr = confobj[rk]; + if (Array.isArray(arr)) { + arr.forEach((_, cIdx) => { + positions.push({ row: rIdx, col: cIdx }); + }); + } + }); + positions.sort((a, b) => { + if (a.row === b.row) { + return a.col - b.col; + } + return a.row - b.row; + }); + + const idx = positions.findIndex(p => p.row === row && p.col === col); + let prev, next; + if (idx > 0) { + prev = positions[idx - 1]; + } + if (idx >= 0 && idx < positions.length - 1) { + next = positions[idx + 1]; + } + + res.render('stream', { + title: stream.title, + stream, + prev, + next, + config: conffile + }); +}); + +module.exports = router; diff --git a/views/index.ejs b/views/index.ejs index c735cd5..c70ddb8 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -7,7 +7,7 @@
Click on viewport to activate audio | SPACE - pause/resume | F - toggle fullscreen | 1-8 - activate viewport
+Click on viewport to activate audio | SPACE - pause/resume | F - toggle fullscreen | 1-8 - activate viewport (first eight only)