From 82cfefec84cef4d81dff0fc57812e24123829037 Mon Sep 17 00:00:00 2001 From: David Halls Date: Fri, 28 May 2021 21:51:06 +0100 Subject: [PATCH] Move files to site/ and catch errors --- example.html | 34 ---- example.js | 118 ------------- site/example.html | 43 +++++ site/example.js | 167 ++++++++++++++++++ site/ffmpeg-worker-hls.js | 1 + site/ffmpeg-worker-hls.wasm | 1 + gl-canvas.js => site/gl-canvas.js | 0 glsl-canvas.min.js => site/glsl-canvas.min.js | 0 .../glsl-canvas.min.js.map | 0 .../greyscale-shader.js | 0 hls-worker.js => site/hls-worker.js | 15 +- import-umd.js => site/import-umd.js | 0 12 files changed, 225 insertions(+), 154 deletions(-) delete mode 100644 example.html delete mode 100644 example.js create mode 100644 site/example.html create mode 100644 site/example.js create mode 120000 site/ffmpeg-worker-hls.js create mode 120000 site/ffmpeg-worker-hls.wasm rename gl-canvas.js => site/gl-canvas.js (100%) rename glsl-canvas.min.js => site/glsl-canvas.min.js (100%) rename glsl-canvas.min.js.map => site/glsl-canvas.min.js.map (100%) rename greyscale-shader.js => site/greyscale-shader.js (100%) rename hls-worker.js => site/hls-worker.js (87%) rename import-umd.js => site/import-umd.js (100%) diff --git a/example.html b/example.html deleted file mode 100644 index de9e3e3..0000000 --- a/example.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - -
-
-
-
- -
-
- - -
-
-
-
-
-
-
- -
-
- Loading... -
-
-
-
-
- - diff --git a/example.js b/example.js deleted file mode 100644 index 4ae0bd8..0000000 --- a/example.js +++ /dev/null @@ -1,118 +0,0 @@ -import { InvisibleGlCanvas } from './gl-canvas.js'; -import { HlsWorker } from './hls-worker.js'; -import shader from './greyscale-shader.js'; - -const ingestion_url_el = document.getElementById('ingestion-url'); -ingestion_url_el.value = localStorage.getItem('streamana-example-ingestion-url'); - -const go_live_el = document.getElementById('go-live'); -go_live_el.disabled = false; -go_live_el.addEventListener('click', function () { - if (this.checked) { - start(); - } else { - stop(); - } -}); - -const monitor_el = document.getElementById('monitor'); -const waiting_el = document.getElementById('waiting'); - -let hls_worker; - -async function start() { - const ingestion_url = ingestion_url_el.value.trim(); - if (!ingestion_url) { - go_live_el.checked = false; - return; - } - localStorage.setItem('streamana-example-ingestion-url', ingestion_url); - - go_live_el.disabled = true; - waiting_el.classList.remove('d-none'); - - // capture video from webcam - const camera_stream = await navigator.mediaDevices.getUserMedia({ - audio: true, - video: { - width: 4096, - height: 2160, - frameRate: { - ideal: 30, - max: 30 - } - } - }); - - // create video element which will be used for grabbing the frames to - // write to a canvas so we can apply webgl shaders - // also used to get the native video dimensions - const video = document.createElement('video'); - video.muted = true; - - // use glsl-canvas to make managing webgl stuff easier - // because it's not visible, client dimensions are zero so we - // need to substitute actual dimensions instead - const gl_canvas = new InvisibleGlCanvas(document); - - // as an example, greyscale the stream - gl_canvas.load(shader); - - // tell canvas to use frames from video - gl_canvas.setTexture('u_texture', video); - - // wait for video to load (must come after gl_canvas.setTexture() since it - // registers a loadeddata handler which then registers a play handler) - video.addEventListener('loadeddata', function () { - // make canvas same size as native video dimensions so every pixel is seen - gl_canvas.canvas.width = this.videoWidth; - gl_canvas.canvas.height = this.videoHeight; - - // start the camera video - this.play(); - - // capture video from the canvas - const canvas_stream = gl_canvas.canvas.captureStream(30); - canvas_stream.addTrack(camera_stream.getAudioTracks()[0]); - - // start HLS from the canvas stream to the ingestion URL - hls_worker = new HlsWorker(canvas_stream, ingestion_url); - hls_worker.addEventListener('run', () => console.log('HLS running')); - hls_worker.addEventListener('exit', ev => { - console.log('HLS exited with code', ev.detail); - for (let track of camera_stream.getTracks()) { - track.stop(); - } - gl_canvas.destroy(); - for (let track of canvas_stream.getTracks()) { - track.stop(); - } - monitor_el.srcObject = null; - go_live_el.disabled = false; - }); - hls_worker.addEventListener('error', ev => { - console.error('HLS errored', ev.detail); - }); - hls_worker.addEventListener('abort', ev => { - console.error('HLS aborted', ev.detail); - }); - hls_worker.addEventListener('start-video', () => { - // display the video locally so we can see what's going on - // note the video seems to set its height automatically to keep the - // correct aspect ratio - waiting_el.classList.add('d-none'); - monitor_el.srcObject = canvas_stream; - monitor_el.play(); - }); - - go_live_el.disabled = false; - }); - - // pass the stream from the camera to the video so it can render the frames - video.srcObject = camera_stream; -} - -function stop() { - go_live_el.disabled = true; - hls_worker.end(); -} diff --git a/site/example.html b/site/example.html new file mode 100644 index 0000000..bc444a2 --- /dev/null +++ b/site/example.html @@ -0,0 +1,43 @@ + + + + + + + + + + +
+ +
+
+ Loading... +
+
+
+ + diff --git a/site/example.js b/site/example.js new file mode 100644 index 0000000..eb203d7 --- /dev/null +++ b/site/example.js @@ -0,0 +1,167 @@ +import { InvisibleGlCanvas } from './gl-canvas.js'; +import { HlsWorker } from './hls-worker.js'; +import shader from './greyscale-shader.js'; + +const ingestion_url_el = document.getElementById('ingestion-url'); +ingestion_url_el.value = localStorage.getItem('streamana-example-ingestion-url'); + +const go_live_el = document.getElementById('go-live'); +go_live_el.disabled = false; +go_live_el.addEventListener('click', function () { + if (this.checked) { + start(); + } else { + stop(); + } +}); + +const monitor_el = document.getElementById('monitor'); +const waiting_el = document.getElementById('waiting'); +const error_alert_el = document.getElementById('error-alert'); +const error_alert_el_parent = error_alert_el.parentNode; +const error_alert_el_nextSibling = error_alert_el.nextSibling; +error_alert_el_parent.removeChild(error_alert_el); + +const ffmpeg_lib_url_el = document.getElementById('ffmpeg-lib-url'); +ffmpeg_lib_url_el.value = localStorage.getItem('streamana-ffmpeg-lib-url'); +ffmpeg_lib_url_el.addEventListener('input', function (e) { + localStorage.setItem('streamana-ffmpeg-lib-url', this.value); +}); + +let hls_worker; + +async function start() { + const ingestion_url = ingestion_url_el.value.trim(); + if (!ingestion_url) { + go_live_el.checked = false; + return; + } + localStorage.setItem('streamana-example-ingestion-url', ingestion_url); + + const ffmpeg_lib_url = ffmpeg_lib_url_el.value.trim() || + ffmpeg_lib_url_el.placeholder.trim(); + + go_live_el.disabled = true; + waiting_el.classList.remove('d-none'); + + if (error_alert_el.parentNode) { + error_alert_el_parent.removeChild(error_alert_el); + } + + let camera_stream, gl_canvas, canvas_stream, done = false; + function cleanup(err) { + if (done) { + return; + } + done = true; + if (err) { + console.error(err); + error_alert_el_parent.insertBefore(error_alert_el, error_alert_el_nextSibling); + error_alert_el.classList.add('show'); + } + if (camera_stream) { + for (let track of camera_stream.getTracks()) { + track.stop(); + } + } + if (gl_canvas) { + gl_canvas.destroy(); + } + if (canvas_stream) { + for (let track of canvas_stream.getTracks()) { + track.stop(); + } + } + monitor_el.srcObject = null; + go_live_el.checked = false; + go_live_el.disabled = false; + waiting_el.classList.add('d-none'); + } + + try { + // capture video from webcam + camera_stream = await navigator.mediaDevices.getUserMedia({ + audio: true, + video: { + width: 4096, + height: 2160, + frameRate: { + ideal: 30, + max: 30 + } + } + }); + + // create video element which will be used for grabbing the frames to + // write to a canvas so we can apply webgl shaders + // also used to get the native video dimensions + const video = document.createElement('video'); + video.muted = true; + + // use glsl-canvas to make managing webgl stuff easier + // because it's not visible, client dimensions are zero so we + // need to substitute actual dimensions instead + gl_canvas = new InvisibleGlCanvas(document); + + // as an example, greyscale the stream + gl_canvas.load(shader); + + // tell canvas to use frames from video + gl_canvas.setTexture('u_texture', video); + + // wait for video to load (must come after gl_canvas.setTexture() since it + // registers a loadeddata handler which then registers a play handler) + video.addEventListener('loadeddata', function () { + try { + // make canvas same size as native video dimensions so every pixel is seen + gl_canvas.canvas.width = this.videoWidth; + gl_canvas.canvas.height = this.videoHeight; + + // start the camera video + this.play(); + + // capture video from the canvas + canvas_stream = gl_canvas.canvas.captureStream(30); + canvas_stream.addTrack(camera_stream.getAudioTracks()[0]); + + // start HLS from the canvas stream to the ingestion URL + hls_worker = new HlsWorker(canvas_stream, ingestion_url, ffmpeg_lib_url); + hls_worker.addEventListener('run', () => console.log('HLS running')); + hls_worker.addEventListener('exit', ev => { + const msg = `HLS exited with status ${ev.detail}`; + if (ev.detail === 0) { + console.log(msg); + cleanup(); + } else { + console.error(msg); + cleanup(msg); + } + }); + hls_worker.addEventListener('error', cleanup); + hls_worker.addEventListener('abort', cleanup); + hls_worker.addEventListener('start-video', () => { + // display the video locally so we can see what's going on + // note the video seems to set its height automatically to keep the + // correct aspect ratio + waiting_el.classList.add('d-none'); + monitor_el.srcObject = canvas_stream; + monitor_el.play(); + }); + + go_live_el.disabled = false; + } catch (ex) { + cleanup(ex); + } + }); + + // pass the stream from the camera to the video so it can render the frames + video.srcObject = camera_stream; + } catch (ex) { + return cleanup(ex); + } +} + +function stop() { + go_live_el.disabled = true; + hls_worker.end(); +} diff --git a/site/ffmpeg-worker-hls.js b/site/ffmpeg-worker-hls.js new file mode 120000 index 0000000..a4fea27 --- /dev/null +++ b/site/ffmpeg-worker-hls.js @@ -0,0 +1 @@ +../ffmpeg.js/ffmpeg-worker-hls.js \ No newline at end of file diff --git a/site/ffmpeg-worker-hls.wasm b/site/ffmpeg-worker-hls.wasm new file mode 120000 index 0000000..3ae30a2 --- /dev/null +++ b/site/ffmpeg-worker-hls.wasm @@ -0,0 +1 @@ +../ffmpeg.js/ffmpeg-worker-hls.wasm \ No newline at end of file diff --git a/gl-canvas.js b/site/gl-canvas.js similarity index 100% rename from gl-canvas.js rename to site/gl-canvas.js diff --git a/glsl-canvas.min.js b/site/glsl-canvas.min.js similarity index 100% rename from glsl-canvas.min.js rename to site/glsl-canvas.min.js diff --git a/glsl-canvas.min.js.map b/site/glsl-canvas.min.js.map similarity index 100% rename from glsl-canvas.min.js.map rename to site/glsl-canvas.min.js.map diff --git a/greyscale-shader.js b/site/greyscale-shader.js similarity index 100% rename from greyscale-shader.js rename to site/greyscale-shader.js diff --git a/hls-worker.js b/site/hls-worker.js similarity index 87% rename from hls-worker.js rename to site/hls-worker.js index 2b730bb..0ff9e7c 100644 --- a/hls-worker.js +++ b/site/hls-worker.js @@ -1,6 +1,14 @@ export class HlsWorker extends EventTarget { - constructor(stream, ingestion_url) { + constructor(stream, ingestion_url, ffmpeg_lib_url) { super(); + + let exited = false; + onerror = e => { + if (!exited) { + this.dispatchEvent(new CustomEvent('error', { detail: e })); + } + }; + // set up video recording from the stream // note we don't start recording until ffmpeg has started (below) const recorder = new MediaRecorder(stream, { @@ -8,6 +16,7 @@ export class HlsWorker extends EventTarget { audioBitsPerSecond: 128 * 1000, videoBitsPerSecond: 2500 * 1000 }); + recorder.onerror = onerror; // push encoded data into the ffmpeg worker recorder.ondataavailable = async event => { @@ -19,7 +28,8 @@ export class HlsWorker extends EventTarget { }; // start ffmpeg in a Web Worker - this.worker = new Worker('ffmpeg.js/ffmpeg-worker-hls.js'); + this.worker = new Worker(ffmpeg_lib_url); + this.worker.onerror = onerror; this.worker.onmessage = e => { const msg = e.data; switch (msg.type) { @@ -55,6 +65,7 @@ export class HlsWorker extends EventTarget { recorder.start(1000); break; case 'exit': + exited = true; this.worker.terminate(); if (recorder.state !== 'inactive') { recorder.stop(); diff --git a/import-umd.js b/site/import-umd.js similarity index 100% rename from import-umd.js rename to site/import-umd.js