Merged in m3u-update (pull request #342)

mjpeg-player: updated player for m3u changes

Approved-by: Saxon Milton
Approved-by: Scott Barnard
This commit is contained in:
Trek Hopton 2020-01-28 05:11:52 +00:00
commit 4bbfa9ee6a
3 changed files with 252 additions and 125 deletions

View File

@ -1,34 +1,59 @@
<!DOCTYPE html> <!DOCTYPE html>
<!--
AUTHOR
Trek Hopton <trek@ausocean.org>
LICENSE
This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.
It is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License in gpl.txt.
If not, see http://www.gnu.org/licenses.
-->
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Mjpeg Player</title> <title>Mjpeg Player</title>
<script type="text/javascript" src="main.js"></script> <script type="module" src="./main.js"></script>
<script type="module" src="player.js"></script>
</head> </head>
<body style="height: 100%"> <body style="height: 100%">
<div class="card m-auto" style="width: 40rem;"> <div style="width: 40rem;">
<div class="card-body"> <div>
<div class="container-fluid"> <div>
<div class="form-group"> <div>
<input class="form-control-file" type="file" id="fileinput" onchange="play();"> <input type="file" id="fileInput">
</div>
</div>
<div class="container-fluid">
Frame Rate: <input type="text" id="rate" class="mb-3 mx-3" value="30"> fps
</div>
<div class="container-fluid">
<img src="" id="viewer" style="max-width:100%; height:auto;">
</div>
</div> </div>
</div>
<div>
<label>Playlist URL: </label>
<input type="url" id="url" placeholder="Enter URL">
<button id="urlBtn" type="submit">Load</button>
</div>
<div>
Frame Rate: <input type="text" id="rate" value="25"> fps
</div>
<div>
<img src="" id="viewer" style="max-width:100%; height:auto;">
</div>
</div> </div>
<footer id="sticky-footer" class="footer fixed-bottom py-4 bg-dark text-white-50" style="width: 100%;"> </div>
<div class="container text-center"> <footer id="sticky-footer" style="width: 100%;">
<small>&copy;2019 Australian Ocean Laboratory Limited (AusOcean) (<a rel="license" href="https://www.ausocean.org/license">License</a>)</small> <div>
</div> <small>&copy;2020 Australian Ocean Laboratory Limited (AusOcean) (<a rel="license" href="https://www.ausocean.org/license">License</a>)</small>
</footer> </div>
</footer>
</body> </body>
</html> </html>

View File

@ -1,12 +1,9 @@
/* /*
NAME
main.js
AUTHOR AUTHOR
Trek Hopton <trek@ausocean.org> Trek Hopton <trek@ausocean.org>
LICENSE LICENSE
This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the under the terms of the GNU General Public License as published by the
@ -22,54 +19,114 @@ LICENSE
If not, see http://www.gnu.org/licenses. If not, see http://www.gnu.org/licenses.
*/ */
// play will process and play the chosen target file. import Hls from "./hlsjs/hls.js";
function play() {
const viewer = document.getElementById('viewer');
const input = event.target.files[0];
const reader = new FileReader();
reader.onload = event => { let started = false;
const player = new Worker("player.js"); let player, viewer;
let rate = document.getElementById('rate'); // init gets DOM elements once the document has been loaded and adds listeners where necessary.
if (rate.value && rate.value > 0) { function init() {
player.postMessage({ msg: "setFrameRate", data: rate.value }); document.addEventListener('DOMContentLoaded', load);
} document.addEventListener('DOMContentLoaded', function () {
document.getElementById('urlBtn').addEventListener('click', load);
document.getElementById('fileInput').addEventListener('change', play);
viewer = document.getElementById('viewer');
}
);
}
player.onmessage = e => { init();
switch (e.data.msg) {
case "frame":
const blob = new Blob([new Uint8Array(e.data.data)], {
type: 'video/x-motion-jpeg'
});
const url = URL.createObjectURL(blob);
viewer.src = url;
break;
case "log":
console.log(e.data.data);
break;
case "stop":
break;
default:
console.error("unknown message from player");
break;
}
};
switch (input.name.split('.')[1]) { // load gets the URL from the URL input element or the browser's URL bar
case "mjpeg": // and creates an Hls instance to load the content from the URL.
case "mjpg": function load() {
player.postMessage({ msg: "loadMjpeg", data: event.target.result }, [event.target.result]); let url = document.getElementById('url').value;
break; if (url == "") {
case "ts": // Get everything following the ? from the browser's URL bar.
player.postMessage({ msg: "loadMtsMjpeg", data: event.target.result }, [event.target.result]); url = window.location.search.slice(1);
break; document.getElementById('url').value = url;
default: } else if (url[0] == '/') {
console.error("unknown file format"); url = window.location.protocol + '//' + window.location.host + url;
break; }
} if (url == "") {
}; return;
reader.onerror = error => reject(error); }
reader.readAsArrayBuffer(input);
let hls = new Hls();
hls.loadSource(url, append);
}
// append, on the first call, starts a player worker and passes it a frame rate and the video data,
// on subsequent calls it passes the video data to the player worker.
function append(data) {
if (!started) {
player = new Worker("player.js");
let rate = document.getElementById('rate');
if (rate.value && rate.value > 0) {
player.postMessage({ msg: "setFrameRate", data: rate.value });
}
player.onmessage = handleMessage;
player.postMessage({ msg: "loadMtsMjpeg", data: data }, [data]);
started = true;
} else {
player.postMessage({ msg: "appendMtsMjpeg", data: data }, [data]);
}
} }
// play will process and play the target file chosen with the file input element.
function play() {
const input = event.target.files[0];
const reader = new FileReader();
reader.onload = event => {
const player = new Worker("player.js");
let rate = document.getElementById('rate');
if (rate.value > 0) {
player.postMessage({ msg: "setFrameRate", data: rate.value });
}
player.onmessage = handleMessage;
switch (input.name.split('.')[1]) {
case "mjpeg":
case "mjpg":
player.postMessage({ msg: "loadMjpeg", data: event.target.result }, [event.target.result]);
break;
case "ts":
player.postMessage({ msg: "loadMtsMjpeg", data: event.target.result }, [event.target.result]);
break;
default:
console.error("unknown file format");
break;
}
};
reader.onerror = error => reject(error);
reader.readAsArrayBuffer(input);
}
// handleMessage handles messgaes from the player workers, its main job is to update the display when a frame is received.
function handleMessage(e) {
switch (e.data.msg) {
case "frame":
const blob = new Blob([new Uint8Array(e.data.data)], {
type: 'video/x-motion-jpeg'
});
const url = URL.createObjectURL(blob);
viewer.src = url;
break;
case "log":
console.log(e.data.data);
break;
case "stop":
console.log("stopped");
break;
default:
console.error("unknown message from player");
break;
}
}

View File

@ -1,12 +1,9 @@
/* /*
NAME
player.js
AUTHOR AUTHOR
Trek Hopton <trek@ausocean.org> Trek Hopton <trek@ausocean.org>
LICENSE LICENSE
This file is Copyright (C) 2019 the Australian Ocean Lab (AusOcean) This file is Copyright (C) 2020 the Australian Ocean Lab (AusOcean)
It is free software: you can redistribute it and/or modify them It is free software: you can redistribute it and/or modify them
under the terms of the GNU General Public License as published by the under the terms of the GNU General Public License as published by the
@ -22,68 +19,116 @@ LICENSE
If not, see http://www.gnu.org/licenses. If not, see http://www.gnu.org/licenses.
*/ */
let frameRate = 30; let frameRate = 25; //Keeps track of the frame rate, default is 25fps.
self.importScripts('./lex-mjpeg.js');
self.importScripts('./hlsjs/mts-demuxer.js');
const codecs = {
MJPEG: 1,
MTS_MJPEG: 2,
}
// onmessage is called whenever the main thread sends this worker a message.
onmessage = e => { onmessage = e => {
switch (e.data.msg) { switch (e.data.msg) {
case "setFrameRate": case "setFrameRate":
frameRate = e.data.data; frameRate = e.data.data;
break; break;
case "loadMjpeg": case "loadMjpeg":
self.importScripts('./lex-mjpeg.js'); player = new PlayerWorker();
let mjpeg = new Uint8Array(e.data.data); player.init(codecs.MJPEG);
let lex = new MJPEGLexer(mjpeg); player.append(e.data.data);
player = new Player(lex); player.setFrameRate(frameRate);
player.setFrameRate(frameRate); player.start();
player.start(); break;
break; case "loadMtsMjpeg":
case "loadMtsMjpeg": player = new PlayerWorker();
self.importScripts('./hlsjs/mts-demuxer.js'); player.init(codecs.MTS_MJPEG);
let mtsMjpeg = new Uint8Array(e.data.data); player.append(e.data.data);
let demux = new MTSDemuxer(); player.start();
let tracks = demux._getTracks(); break;
demux.append(mtsMjpeg); case "appendMtsMjpeg":
buf = new FrameBuffer(tracks.video.data); player.append(e.data.data);
player = new Player(buf); break;
player.setFrameRate(frameRate); //TODO: read frame rate from metadata. default:
player.start(); console.error("unknown message from main thread");
break; break;
default: }
console.error("unknown message from main thread");
break;
}
}; };
class Player { // PlayerWorker has a FrameBuffer to hold frames and once started, passes them one at a time to the main thread.
constructor(buffer) { class PlayerWorker {
this.buffer = buffer; init(codec) {
this.frameRate = frameRate; this.frameRate = frameRate;
this.codec = codec;
switch (codec) {
case codecs.MJPEG:
this.frameSrc = new MJPEGLexer();
break;
case codecs.MTS_MJPEG:
this.frameSrc = new FrameBuffer();
break;
} }
}
setFrameRate(rate) { setFrameRate(rate) {
this.frameRate = rate; this.frameRate = rate;
} }
start() { start() {
let frame = this.buffer.read(); let frame = this.frameSrc.read();
if (frame != null) { if (frame != null) {
postMessage({ msg: "frame", data: frame.buffer }, [frame.buffer]); postMessage({ msg: "frame", data: frame.buffer }, [frame.buffer]);
setTimeout(() => { this.start(); }, 1000 / this.frameRate); setTimeout(() => { this.start(); }, 1000 / this.frameRate);
} else { } else {
postMessage({ msg: "stop" }); postMessage({ msg: "stop" });
}
} }
}
append(data) {
this.frameSrc.append(data);
}
} }
// FrameBuffer allows an array of subarrays (MJPEG frames) to be read one at a time. // FrameBuffer allows an array of subarrays (MJPEG frames) to be read one at a time.
class FrameBuffer { class FrameBuffer {
constructor(src) { constructor() {
this.src = src; this.segments = [];
this.off = 0; this.off = { segment: 0, frame: 0 };
} this.demuxer = new MTSDemuxer();
}
// read returns the next single frame. // read returns the next single frame.
read() { read() {
return this.src[this.off++]; let off = this.off;
let prevOff = off;
if (this.incrementOff()) {
return this.segments[prevOff.segment][prevOff.frame];
} else {
return null;
} }
}
append(data) {
let demuxed = this.demuxer.demux(new Uint8Array(data));
this.segments.push(demuxed.data);
}
incrementOff() {
if (!this.segments || !this.segments[this.off.segment]) {
return false;
}
if (this.off.frame + 1 >= this.segments[this.off.segment].length) {
if (this.off.segment + 1 >= this.segments.length) {
return false;
} else {
this.off.segment++;
this.off.frame = 0;
return true;
}
} else {
this.off.frame++;
return true;
}
}
} }