-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ An error occurred! See the Developer Console for details.
+
+
+
+
+
+
+ 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