Skip to content
This repository was archived by the owner on Nov 26, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules/
config/default.json
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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": [
Expand Down Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion bin/www
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
74 changes: 74 additions & 0 deletions config/example16.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
35 changes: 29 additions & 6 deletions public/javascripts/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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'));
Expand All @@ -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');
}
}
Expand Down
22 changes: 22 additions & 0 deletions public/stylesheets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ th {
min-height: 200px;
padding: 0px;
text-align: center;
position: relative;
}

.info {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -118,3 +135,8 @@ video {
background-color: #000;
}

/* Hide info row by default */
#infoRow {
display: none;
}

20 changes: 11 additions & 9 deletions routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) });
});

Expand Down
71 changes: 71 additions & 0 deletions routes/stream.js
Original file line number Diff line number Diff line change
@@ -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;
Loading