2021-05-29 15:37:37 +08:00
|
|
|
//import { InvisibleGlCanvas } from './gl-canvas.js';
|
|
|
|
import { safari_hack_InvisibleGlCanvas } from './gl-canvas.js';
|
2021-05-29 04:51:06 +08:00
|
|
|
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');
|
2021-06-05 06:50:03 +08:00
|
|
|
monitor_el.classList.add('d-none');
|
2021-05-29 04:51:06 +08:00
|
|
|
|
|
|
|
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');
|
2021-06-05 06:50:03 +08:00
|
|
|
monitor_el.classList.add('d-none');
|
2021-05-29 04:51:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2021-06-05 06:50:03 +08:00
|
|
|
// 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;
|
|
|
|
video.playsInline = true;
|
|
|
|
|
|
|
|
// Safari on iOS requires us to play() in the click handler and doesn't
|
|
|
|
// track async calls. So we play a blank video first. After that, the video
|
|
|
|
// element is blessed for script-driver playback.
|
|
|
|
video.src = 'empty.mp4';
|
|
|
|
monitor_el.src = 'empty.mp4';
|
|
|
|
await Promise.all([video.play(), monitor_el.play()]);
|
|
|
|
|
2021-05-29 04:51:06 +08:00
|
|
|
// capture video from webcam
|
2021-06-04 03:00:10 +08:00
|
|
|
const video_constraints = {
|
2021-06-05 06:50:03 +08:00
|
|
|
//width: 4096,
|
|
|
|
//height: 2160,
|
|
|
|
width: 1280,
|
|
|
|
height: 720,
|
2021-06-04 03:00:10 +08:00
|
|
|
frameRate: {
|
|
|
|
ideal: 30,
|
|
|
|
max: 30
|
2021-05-29 04:51:06 +08:00
|
|
|
}
|
2021-06-04 03:00:10 +08:00
|
|
|
};
|
|
|
|
try {
|
|
|
|
camera_stream = await navigator.mediaDevices.getUserMedia({
|
|
|
|
audio: true,
|
|
|
|
video: video_constraints
|
|
|
|
});
|
|
|
|
} catch (ex) {
|
|
|
|
// retry in case audio isn't available
|
|
|
|
console.warn("Failed to get user media, retrying without audio");
|
|
|
|
camera_stream = await navigator.mediaDevices.getUserMedia({
|
|
|
|
audio: false,
|
|
|
|
video: video_constraints
|
|
|
|
});
|
|
|
|
}
|
2021-05-29 04:51:06 +08:00
|
|
|
|
|
|
|
// 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
|
2021-05-29 15:37:37 +08:00
|
|
|
//gl_canvas = new InvisibleGlCanvas(document);
|
|
|
|
gl_canvas = new (await safari_hack_InvisibleGlCanvas())(document);
|
2021-05-29 04:51:06 +08:00
|
|
|
|
|
|
|
// 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);
|
2021-06-04 03:00:10 +08:00
|
|
|
|
|
|
|
// add audio if present
|
2021-06-04 15:09:56 +08:00
|
|
|
const audio_tracks = camera_stream.getAudioTracks();
|
|
|
|
if (audio_tracks.length > 0) {
|
|
|
|
canvas_stream.addTrack(audio_tracks[0]);
|
2021-06-04 03:00:10 +08:00
|
|
|
}
|
2021-05-29 04:51:06 +08:00
|
|
|
|
|
|
|
// 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 {
|
|
|
|
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();
|
2021-06-05 06:50:03 +08:00
|
|
|
monitor_el.classList.remove('d-none');
|
2021-05-29 04:51:06 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
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();
|
|
|
|
}
|