mirror of
https://github.com/davedoesdev/streamana.git
synced 2025-10-05 05:26:52 +08:00
Bring over changes from dash branch
This commit is contained in:
20
README.adoc
20
README.adoc
@@ -3,7 +3,7 @@
|
|||||||
== Description
|
== Description
|
||||||
|
|
||||||
Streamana is a Web page which streams your camera and microphone to YouTube Live
|
Streamana is a Web page which streams your camera and microphone to YouTube Live
|
||||||
(or any other HLS receiver). It uses https://github.com/davedoesdev/webm-muxer.js[webm-muxer.js] and
|
(or any other HLS or DASH receiver). It uses https://github.com/davedoesdev/webm-muxer.js[webm-muxer.js] and
|
||||||
https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js].
|
https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js].
|
||||||
|
|
||||||
== Demo
|
== Demo
|
||||||
@@ -15,7 +15,9 @@ Use Chrome 95 or later.
|
|||||||
.. Click _CREATE_ and then select _Go Live_ from the drop-down menu.
|
.. Click _CREATE_ and then select _Go Live_ from the drop-down menu.
|
||||||
.. Under _Select stream key_, select _Create new stream key_.
|
.. Under _Select stream key_, select _Create new stream key_.
|
||||||
.. Give your key a name.
|
.. Give your key a name.
|
||||||
.. You must select _HLS_ as the streaming protocol.
|
.. You must select _HLS_ as the streaming protocol. Note: YouTube DASH ingestion is only available
|
||||||
|
by using the Youtube API. See https://developers.google.com/youtube/v3/live/guides/encoding-with-dash#url-structure[here]
|
||||||
|
for more details.
|
||||||
.. Click _CREATE_.
|
.. Click _CREATE_.
|
||||||
.. Make sure the key you created is selected.
|
.. Make sure the key you created is selected.
|
||||||
.. Click _COPY_ next to _Stream URL_.
|
.. Click _COPY_ next to _Stream URL_.
|
||||||
@@ -34,7 +36,7 @@ You can also change various options:
|
|||||||
** Lock the camera to portrait mode (where available, e.g. mobile phones).
|
** Lock the camera to portrait mode (where available, e.g. mobile phones).
|
||||||
** Zoom the camera to fill the page.
|
** Zoom the camera to fill the page.
|
||||||
** Select a different version of https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js] to perform
|
** Select a different version of https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js] to perform
|
||||||
the HLS encoding.
|
the HLS or DASH encoding.
|
||||||
|
|
||||||
== Customisation
|
== Customisation
|
||||||
|
|
||||||
@@ -46,16 +48,16 @@ so you can change this to add video effects or overlays. The shader already hand
|
|||||||
resizing and rotating the video in `main()`. The optional greyscale conversion is in
|
resizing and rotating the video in `main()`. The optional greyscale conversion is in
|
||||||
the `tpix()` function.
|
the `tpix()` function.
|
||||||
|
|
||||||
The page's functionality is defined in link:site/streamana.js[] and link:site/hls.js[].
|
The page's functionality is defined in link:site/streamana.js[] and link:site/streamer.js[].
|
||||||
|
|
||||||
link:site/hls.js[] exports a class, `HLS`, which does the heavy lifting:
|
link:site/streamer.js[] exports a class, `Streamer`, which does the heavy lifting:
|
||||||
|
|
||||||
* The constructor takes the following arguments:
|
* The constructor takes the following arguments:
|
||||||
** The https://developer.mozilla.org/en-US/docs/Web/API/MediaStream[`MediaStream`]
|
** The https://developer.mozilla.org/en-US/docs/Web/API/MediaStream[`MediaStream`]
|
||||||
containing your video and audio tracks. Note that link:site/streamana.js[] supplies
|
containing your video and audio tracks. Note that link:site/streamana.js[] supplies
|
||||||
blank video when the camera is hidden and silent audio when the microphone is muted.
|
blank video when the camera is hidden and silent audio when the microphone is muted.
|
||||||
** The ingestion URL.
|
** The ingestion URL.
|
||||||
** The URL of `ffmpeg-worker-hls.js` in https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js].
|
** The URL of `ffmpeg-worker-hls.js` or `ffmpeg-worker-dash.js` in https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js].
|
||||||
This allows your application (or the end user if required) to supply its own version,
|
This allows your application (or the end user if required) to supply its own version,
|
||||||
in accordance with LGPL.
|
in accordance with LGPL.
|
||||||
** The desired video frame rate.
|
** The desired video frame rate.
|
||||||
@@ -63,7 +65,7 @@ link:site/hls.js[] exports a class, `HLS`, which does the heavy lifting:
|
|||||||
* Call the `async start()` function to start streaming.
|
* Call the `async start()` function to start streaming.
|
||||||
* Call the `end()` function to stop streaming.
|
* Call the `end()` function to stop streaming.
|
||||||
|
|
||||||
`HLS` extends from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget[`EventTarget`]
|
`Streamer` extends from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget[`EventTarget`]
|
||||||
and dispatches the following events:
|
and dispatches the following events:
|
||||||
|
|
||||||
* `start` when streaming has started.
|
* `start` when streaming has started.
|
||||||
@@ -80,7 +82,7 @@ Note that https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js] is licensed under
|
|||||||
Streamana runs it inside a Web Worker and communicates with it via message passing.
|
Streamana runs it inside a Web Worker and communicates with it via message passing.
|
||||||
The end user can replace the version used by changing the URL in the user interface.
|
The end user can replace the version used by changing the URL in the user interface.
|
||||||
|
|
||||||
Note also that the https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js] HLS distribution
|
Note also that the https://github.com/davedoesdev/ffmpeg.js[ffmpeg.js] HLS and DASH
|
||||||
contains no H.264 or MP4 code. All encoding is done by the browser using
|
distributions contain no H.264 or MP4 code. All encoding is done by the browser using
|
||||||
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder[`MediaRecorder`] or
|
https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder[`MediaRecorder`] or
|
||||||
https://www.w3.org/TR/webcodecs/[WebCodecs].
|
https://www.w3.org/TR/webcodecs/[WebCodecs].
|
||||||
|
@@ -29,11 +29,12 @@ export class GlCanvas extends Canvas {
|
|||||||
}
|
}
|
||||||
}), options);
|
}), options);
|
||||||
this.update_limiter = new UpdateLimiter();
|
this.update_limiter = new UpdateLimiter();
|
||||||
|
this.destroyed = false;
|
||||||
}
|
}
|
||||||
// Allow rendering loop to be driven externally (e.g. by the audio encoder)
|
// Allow rendering loop to be driven externally (e.g. by the audio encoder)
|
||||||
// to avoid requestAnimationFrame (or indeed setInterval) throttling.
|
// to avoid requestAnimationFrame (or indeed setInterval) throttling.
|
||||||
onLoop() {
|
onLoop() {
|
||||||
if (this.update_limiter.check()) {
|
if (this.update_limiter.check() && !this.destroyed) {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.checkRender();
|
this.checkRender();
|
||||||
// Make sure we don't hog the main thread. Software rendering will take
|
// Make sure we don't hog the main thread. Software rendering will take
|
||||||
@@ -50,14 +51,25 @@ export class GlCanvas extends Canvas {
|
|||||||
}
|
}
|
||||||
// Prevent errors after destruction
|
// Prevent errors after destruction
|
||||||
destroy() {
|
destroy() {
|
||||||
|
this.destroyed = true;
|
||||||
|
if (this.gl) {
|
||||||
super.destroy();
|
super.destroy();
|
||||||
|
}
|
||||||
this.uniforms = {
|
this.uniforms = {
|
||||||
createTexture() {
|
createTexture() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
create() {}
|
create() {},
|
||||||
|
update() {}
|
||||||
|
};
|
||||||
|
this.textures = {
|
||||||
|
createOrUpdate() {
|
||||||
|
return {
|
||||||
|
then() {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
values: {}
|
||||||
};
|
};
|
||||||
this.textures = {};
|
|
||||||
this.buffers = {
|
this.buffers = {
|
||||||
values: {}
|
values: {}
|
||||||
};
|
};
|
||||||
|
@@ -8,7 +8,7 @@ export class MuxReceiver extends EventTarget {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
start({ ffmpeg_lib_url, ffmpeg_args, base_url }) {
|
start({ ffmpeg_lib_url, ffmpeg_args, base_url, protocol }) {
|
||||||
this.worker = new Worker(ffmpeg_lib_url);
|
this.worker = new Worker(ffmpeg_lib_url);
|
||||||
this.worker.onerror = this.onerror.bind(this);
|
this.worker.onerror = this.onerror.bind(this);
|
||||||
this.worker.onmessage = e => {
|
this.worker.onmessage = e => {
|
||||||
@@ -48,7 +48,8 @@ export class MuxReceiver extends EventTarget {
|
|||||||
case 'start-stream':
|
case 'start-stream':
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'base-url',
|
type: 'base-url',
|
||||||
data: base_url
|
data: base_url,
|
||||||
|
protocol
|
||||||
});
|
});
|
||||||
// falls through
|
// falls through
|
||||||
case 'sending':
|
case 'sending':
|
||||||
|
@@ -65,9 +65,22 @@
|
|||||||
<input id="zoom-video" class="form-check-input" type="checkbox">
|
<input id="zoom-video" class="form-check-input" type="checkbox">
|
||||||
<label for="zoom-video" class="form-check-label">Minimize vertical bars in local video display</label>
|
<label for="zoom-video" class="form-check-label">Minimize vertical bars in local video display</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="pt-4">
|
||||||
|
<label class="form-label">Ingestion protocol</label>
|
||||||
|
<div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input id="protocol-hls" name="protocol" class="form-check-input" type="radio" value="ffmpeg-worker-hls.js">
|
||||||
|
<label for="protocol-hls" class="form-check-label">HLS (H.264)</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input id="protocol-dash" name="protocol" class="form-check-input" type="radio" value="ffmpeg-worker-dash.js">
|
||||||
|
<label for="protocol-dash" class="form-check-label">DASH (VP9)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<label for="ffmpeg-lib-url" class="form-label">FFmpeg library URL</label>
|
<label for="ffmpeg-lib-url" class="form-label">FFmpeg library URL</label>
|
||||||
<input id="ffmpeg-lib-url" class="form-control" placeholder="ffmpeg-worker-hls.js" type="text">
|
<input id="ffmpeg-lib-url" class="form-control" type="text">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="error-alert" class="alert alert-danger alert-dismissible fade mb-0 flex-grow-0" role="alert">
|
<div id="error-alert" class="alert alert-danger alert-dismissible fade mb-0 flex-grow-0" role="alert">
|
||||||
|
@@ -1,17 +1,14 @@
|
|||||||
import { GlCanvas } from './gl-canvas.js';
|
import { GlCanvas } from './gl-canvas.js';
|
||||||
import {
|
import {
|
||||||
HLS,
|
get_default_config_from_url,
|
||||||
video_encoder_codec,
|
Streamer
|
||||||
videoBitsPerSecond
|
} from './streamer.js';
|
||||||
} from './hls.js';
|
|
||||||
import shader from './shader.js';
|
import shader from './shader.js';
|
||||||
import {
|
import {
|
||||||
supported_video_configs,
|
supported_video_configs,
|
||||||
max_video_config,
|
max_video_config,
|
||||||
} from './resolution.js';
|
} from './resolution.js';
|
||||||
|
|
||||||
const target_frame_rate = 30;
|
|
||||||
|
|
||||||
const ingestion_url_el = document.getElementById('ingestion-url');
|
const ingestion_url_el = document.getElementById('ingestion-url');
|
||||||
ingestion_url_el.value = localStorage.getItem('streamana-ingestion-url');
|
ingestion_url_el.value = localStorage.getItem('streamana-ingestion-url');
|
||||||
|
|
||||||
@@ -39,25 +36,34 @@ const initial_ffmpeg_lib_url = (localStorage.getItem('streamana-ffmpeg-lib-url')
|
|||||||
if (initial_ffmpeg_lib_url) {
|
if (initial_ffmpeg_lib_url) {
|
||||||
ffmpeg_lib_url_el.value = initial_ffmpeg_lib_url;
|
ffmpeg_lib_url_el.value = initial_ffmpeg_lib_url;
|
||||||
}
|
}
|
||||||
ffmpeg_lib_url_el.addEventListener('input', function () {
|
ffmpeg_lib_url_el.addEventListener('change', function () {
|
||||||
localStorage.setItem('streamana-ffmpeg-lib-url', this.value);
|
const value = this.value.trim();
|
||||||
|
localStorage.setItem('streamana-ffmpeg-lib-url', value);
|
||||||
|
if (value) {
|
||||||
|
protocol_hls_el.disabled = true;
|
||||||
|
protocol_dash_el.disabled = true;
|
||||||
|
} else {
|
||||||
|
protocol_hls_el.disabled = false;
|
||||||
|
protocol_dash_el.disabled = false;
|
||||||
|
}
|
||||||
|
set_ingestion();
|
||||||
});
|
});
|
||||||
|
|
||||||
const zoom_video_el = document.getElementById('zoom-video');
|
const zoom_video_el = document.getElementById('zoom-video');
|
||||||
zoom_video_el.checked = !!localStorage.getItem('streamana-zoom-video');
|
zoom_video_el.checked = !!localStorage.getItem('streamana-zoom-video');
|
||||||
zoom_video_el.addEventListener('input', function () {
|
zoom_video_el.addEventListener('change', function () {
|
||||||
localStorage.setItem('streamana-zoom-video', this.checked ? 'true' : '');
|
localStorage.setItem('streamana-zoom-video', this.checked ? 'true' : '');
|
||||||
});
|
});
|
||||||
|
|
||||||
const lock_portrait_el = document.getElementById('lock-portrait');
|
const lock_portrait_el = document.getElementById('lock-portrait');
|
||||||
lock_portrait_el.checked = !!localStorage.getItem('streamana-lock-portrait');
|
lock_portrait_el.checked = !!localStorage.getItem('streamana-lock-portrait');
|
||||||
lock_portrait_el.addEventListener('input', function () {
|
lock_portrait_el.addEventListener('change', function () {
|
||||||
localStorage.setItem('streamana-lock-portrait', this.checked ? 'true' : '');
|
localStorage.setItem('streamana-lock-portrait', this.checked ? 'true' : '');
|
||||||
});
|
});
|
||||||
|
|
||||||
const greyscale_el = document.getElementById('greyscale');
|
const greyscale_el = document.getElementById('greyscale');
|
||||||
greyscale_el.checked = !!localStorage.getItem('streamana-greyscale');
|
greyscale_el.checked = !!localStorage.getItem('streamana-greyscale');
|
||||||
greyscale_el.addEventListener('input', function () {
|
greyscale_el.addEventListener('change', function () {
|
||||||
localStorage.setItem('streamana-greyscale', this.checked ? 'true' : '');
|
localStorage.setItem('streamana-greyscale', this.checked ? 'true' : '');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,13 +112,44 @@ camera_el.addEventListener('click', camera_save);
|
|||||||
|
|
||||||
const camera_swap_el = document.getElementById('camera-swap');
|
const camera_swap_el = document.getElementById('camera-swap');
|
||||||
|
|
||||||
|
const protocol_hls_el = document.getElementById('protocol-hls');
|
||||||
|
const protocol_dash_el = document.getElementById('protocol-dash');
|
||||||
|
const resolution_el = document.getElementById('resolution');
|
||||||
|
|
||||||
|
let streamer_config;
|
||||||
let video_config;
|
let video_config;
|
||||||
|
const video_configs = new Map();
|
||||||
|
|
||||||
|
function set_ingestion_protocol(protocol) {
|
||||||
|
if (protocol === 'dash') {
|
||||||
|
protocol_hls_el.checked = false;
|
||||||
|
protocol_dash_el.checked = true;
|
||||||
|
ffmpeg_lib_url_el.placeholder = protocol_dash_el.value;
|
||||||
|
} else {
|
||||||
|
protocol_hls_el.checked = true;
|
||||||
|
protocol_dash_el.checked = false;
|
||||||
|
ffmpeg_lib_url_el.placeholder = protocol_hls_el.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_ingestion_protocol(localStorage.getItem('streamana-ingestion-protocol'));
|
||||||
|
|
||||||
|
async function set_ingestion() {
|
||||||
|
const ffmpeg_lib_url = ffmpeg_lib_url_el.value.trim() ||
|
||||||
|
ffmpeg_lib_url_el.placeholder.trim();
|
||||||
|
|
||||||
|
streamer_config = get_default_config_from_url(ffmpeg_lib_url);
|
||||||
|
|
||||||
|
set_ingestion_protocol(streamer_config.protocol);
|
||||||
|
localStorage.setItem('streamana-ingestion-protocol', streamer_config.protocol);
|
||||||
|
|
||||||
|
video_config = null;
|
||||||
let preferred_resolution = localStorage.getItem('streamana-resolution');
|
let preferred_resolution = localStorage.getItem('streamana-resolution');
|
||||||
if (preferred_resolution) {
|
if (preferred_resolution) {
|
||||||
video_config = await max_video_config({
|
video_config = await max_video_config({
|
||||||
...JSON.parse(preferred_resolution),
|
...JSON.parse(preferred_resolution),
|
||||||
codec: video_encoder_codec,
|
...streamer_config.video,
|
||||||
bitrate: videoBitsPerSecond
|
...streamer_config.webcodecs.video
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
if (!video_config) {
|
if (!video_config) {
|
||||||
@@ -120,15 +157,15 @@ if (!video_config) {
|
|||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
ratio: 16/9,
|
ratio: 16/9,
|
||||||
codec: video_encoder_codec,
|
...streamer_config.video,
|
||||||
bitrate: videoBitsPerSecond
|
...streamer_config.webcodecs.video
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
const resolution_el = document.getElementById('resolution');
|
|
||||||
const video_configs = new Map();
|
resolution_el.innerHTML = '';
|
||||||
for (let config of (await supported_video_configs({
|
for (let config of (await supported_video_configs({
|
||||||
codec: video_encoder_codec,
|
...streamer_config.video,
|
||||||
bitrate: videoBitsPerSecond
|
...streamer_config.webcodecs.video
|
||||||
}, true)).filter(c => c.ratio >= 1)) {
|
}, true)).filter(c => c.ratio >= 1)) {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.innerHTML = `${config.width}x${config.height} — ${config.label}`;
|
option.innerHTML = `${config.width}x${config.height} — ${config.label}`;
|
||||||
@@ -136,7 +173,21 @@ for (let config of (await supported_video_configs({
|
|||||||
resolution_el.appendChild(option);
|
resolution_el.appendChild(option);
|
||||||
video_configs.set(option.innerText, config);
|
video_configs.set(option.innerText, config);
|
||||||
}
|
}
|
||||||
resolution_el.addEventListener('change', function (ev) {
|
}
|
||||||
|
|
||||||
|
await set_ingestion();
|
||||||
|
|
||||||
|
protocol_hls_el.addEventListener('change', function () {
|
||||||
|
ffmpeg_lib_url_el.placeholder = protocol_hls_el.value;
|
||||||
|
set_ingestion();
|
||||||
|
});
|
||||||
|
|
||||||
|
protocol_dash_el.addEventListener('change', function () {
|
||||||
|
ffmpeg_lib_url_el.placeholder = protocol_dash_el.value;
|
||||||
|
set_ingestion();
|
||||||
|
});
|
||||||
|
|
||||||
|
resolution_el.addEventListener('change', function () {
|
||||||
video_config = video_configs.get(this.value);
|
video_config = video_configs.get(this.value);
|
||||||
localStorage.setItem('streamana-resolution', JSON.stringify({
|
localStorage.setItem('streamana-resolution', JSON.stringify({
|
||||||
width: video_config.width,
|
width: video_config.width,
|
||||||
@@ -145,7 +196,7 @@ resolution_el.addEventListener('change', function (ev) {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
let hls;
|
let streamer;
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
const ingestion_url = ingestion_url_el.value.trim();
|
const ingestion_url = ingestion_url_el.value.trim();
|
||||||
@@ -172,6 +223,8 @@ async function start() {
|
|||||||
lock_portrait_el.disabled = true;
|
lock_portrait_el.disabled = true;
|
||||||
zoom_video_el.disabled = true;
|
zoom_video_el.disabled = true;
|
||||||
resolution_el.disabled = true;
|
resolution_el.disabled = true;
|
||||||
|
protocol_hls_el.disabled = true;
|
||||||
|
protocol_dash_el.disabled = true;
|
||||||
waiting_el.classList.remove('d-none');
|
waiting_el.classList.remove('d-none');
|
||||||
mic_el.removeEventListener('click', mic_save);
|
mic_el.removeEventListener('click', mic_save);
|
||||||
camera_el.removeEventListener('click', camera_save);
|
camera_el.removeEventListener('click', camera_save);
|
||||||
@@ -218,7 +271,7 @@ async function start() {
|
|||||||
camera_icon_el.classList.add('off');
|
camera_icon_el.classList.add('off');
|
||||||
}
|
}
|
||||||
camera_el.addEventListener('click', camera_save);
|
camera_el.addEventListener('click', camera_save);
|
||||||
greyscale_el.removeEventListener('input', greyscale);
|
greyscale_el.removeEventListener('change', greyscale);
|
||||||
camera_swap_el.classList.add('d-none');
|
camera_swap_el.classList.add('d-none');
|
||||||
camera_swap_el.removeEventListener('click', about_face);
|
camera_swap_el.removeEventListener('click', about_face);
|
||||||
canvas_el_parent.classList.add('mx-auto');
|
canvas_el_parent.classList.add('mx-auto');
|
||||||
@@ -267,8 +320,9 @@ async function start() {
|
|||||||
track.stop();
|
track.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hls) {
|
if (streamer) {
|
||||||
hls.end(!!err);
|
streamer.end(!!err);
|
||||||
|
streamer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
go_live_el.checked = false;
|
go_live_el.checked = false;
|
||||||
@@ -279,6 +333,8 @@ async function start() {
|
|||||||
lock_portrait_el.disabled = false;
|
lock_portrait_el.disabled = false;
|
||||||
zoom_video_el.disabled = false;
|
zoom_video_el.disabled = false;
|
||||||
resolution_el.disabled = false;
|
resolution_el.disabled = false;
|
||||||
|
protocol_hls_el.disabled = ffmpeg_lib_url_el.value.trim();
|
||||||
|
protocol_dash_el.disabled = ffmpeg_lib_url_el.value.trim();;
|
||||||
waiting_el.classList.add('d-none');
|
waiting_el.classList.add('d-none');
|
||||||
canvas_el.classList.add('d-none');
|
canvas_el.classList.add('d-none');
|
||||||
}
|
}
|
||||||
@@ -356,7 +412,7 @@ async function start() {
|
|||||||
camera_swap_el.removeEventListener('click', about_face);
|
camera_swap_el.removeEventListener('click', about_face);
|
||||||
|
|
||||||
async function finish() {
|
async function finish() {
|
||||||
await hls.start();
|
await streamer.start();
|
||||||
mic_el.addEventListener('click', media_toggle);
|
mic_el.addEventListener('click', media_toggle);
|
||||||
camera_el.addEventListener('click', media_toggle);
|
camera_el.addEventListener('click', media_toggle);
|
||||||
camera_swap_el.addEventListener('click', about_face);
|
camera_swap_el.addEventListener('click', about_face);
|
||||||
@@ -375,8 +431,8 @@ async function start() {
|
|||||||
width: video_config.width,
|
width: video_config.width,
|
||||||
height: video_config.height,
|
height: video_config.height,
|
||||||
frameRate: {
|
frameRate: {
|
||||||
ideal: target_frame_rate,
|
ideal: streamer_config.video.framerate,
|
||||||
max: target_frame_rate
|
max: streamer_config.video.framerate
|
||||||
},
|
},
|
||||||
facingMode: requested_facing_mode
|
facingMode: requested_facing_mode
|
||||||
};
|
};
|
||||||
@@ -534,7 +590,7 @@ async function start() {
|
|||||||
|
|
||||||
// tell shader whether to greyscale
|
// tell shader whether to greyscale
|
||||||
gl_canvas.setUniform('u_greyscale', greyscale_el.checked);
|
gl_canvas.setUniform('u_greyscale', greyscale_el.checked);
|
||||||
greyscale_el.addEventListener('input', greyscale);
|
greyscale_el.addEventListener('change', greyscale);
|
||||||
|
|
||||||
// tell shader camera hasn't started
|
// tell shader camera hasn't started
|
||||||
gl_canvas.setUniform('u_active', false);
|
gl_canvas.setUniform('u_active', false);
|
||||||
@@ -572,7 +628,7 @@ async function start() {
|
|||||||
// capture video from the canvas
|
// capture video from the canvas
|
||||||
// Note: Safari on iOS doesn't get any data, might be related to
|
// Note: Safari on iOS doesn't get any data, might be related to
|
||||||
// https://bugs.webkit.org/show_bug.cgi?id=181663
|
// https://bugs.webkit.org/show_bug.cgi?id=181663
|
||||||
canvas_stream = canvas_el.captureStream(target_frame_rate);
|
canvas_stream = canvas_el.captureStream(streamer_config.video.framerate);
|
||||||
|
|
||||||
// add audio to canvas stream
|
// add audio to canvas stream
|
||||||
audio_dest = audio_context.createMediaStreamDestination();
|
audio_dest = audio_context.createMediaStreamDestination();
|
||||||
@@ -593,11 +649,15 @@ async function start() {
|
|||||||
audio_source = silence;
|
audio_source = silence;
|
||||||
audio_source.connect(audio_dest);
|
audio_source.connect(audio_dest);
|
||||||
|
|
||||||
// HLS from the canvas stream to the ingestion URL
|
// Stream from the canvas stream to the ingestion URL
|
||||||
hls = new HLS(canvas_stream, audio_context, ingestion_url, ffmpeg_lib_url, target_frame_rate, lock_portrait);
|
streamer = new Streamer(canvas_stream,
|
||||||
hls.addEventListener('run', () => console.log('HLS running'));
|
audio_context,
|
||||||
hls.addEventListener('exit', ev => {
|
ingestion_url,
|
||||||
const msg = `HLS exited with status ${ev.detail.code}`;
|
streamer_config,
|
||||||
|
lock_portrait);
|
||||||
|
streamer.addEventListener('run', () => console.log('Streamer running'));
|
||||||
|
streamer.addEventListener('exit', ev => {
|
||||||
|
const msg = `Streamer exited with status ${ev.detail.code}`;
|
||||||
if (ev.detail.code === 0) {
|
if (ev.detail.code === 0) {
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
cleanup();
|
cleanup();
|
||||||
@@ -605,8 +665,8 @@ async function start() {
|
|||||||
cleanup(msg);
|
cleanup(msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hls.addEventListener('error', cleanup);
|
streamer.addEventListener('error', cleanup);
|
||||||
hls.addEventListener('start', function () {
|
streamer.addEventListener('start', function () {
|
||||||
if (done) {
|
if (done) {
|
||||||
this.end(true);
|
this.end(true);
|
||||||
}
|
}
|
||||||
@@ -616,7 +676,7 @@ async function start() {
|
|||||||
go_live_el.disabled = false;
|
go_live_el.disabled = false;
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
hls.addEventListener('update', update);
|
streamer.addEventListener('update', update);
|
||||||
|
|
||||||
await start_media(facing_mode);
|
await start_media(facing_mode);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@@ -626,5 +686,5 @@ async function start() {
|
|||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
go_live_el.disabled = true;
|
go_live_el.disabled = true;
|
||||||
hls.end();
|
streamer.end();
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,71 @@
|
|||||||
import { UpdateLimiter } from './update-limiter.js';
|
import { UpdateLimiter } from './update-limiter.js';
|
||||||
import { MuxReceiver } from './mux-receiver.js';
|
import { MuxReceiver } from './mux-receiver.js';
|
||||||
|
|
||||||
const audioBitsPerSecond = 128 * 1000;
|
|
||||||
export const videoBitsPerSecond = 2500 * 1000;
|
|
||||||
const key_frame_interval = 3;
|
const key_frame_interval = 3;
|
||||||
|
|
||||||
export const video_encoder_codec = 'avc1.42E01E' /*'avc1.42001E'*/;
|
export function get_default_config_from_url(ffmpeg_lib_url) {
|
||||||
|
const protocol = ffmpeg_lib_url.indexOf('worker-dash') >= 0 ? 'dash' : 'hls';
|
||||||
|
return {
|
||||||
|
ffmpeg_lib_url,
|
||||||
|
protocol,
|
||||||
|
video: {
|
||||||
|
bitrate: 2500 * 1000,
|
||||||
|
framerate: 30
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
bitrate: 128 * 1000
|
||||||
|
},
|
||||||
|
media_recorder: {
|
||||||
|
video: {
|
||||||
|
codec: protocol === 'dash' ? 'vp9' : 'H264',
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
codec: 'opus'
|
||||||
|
},
|
||||||
|
webm: true,
|
||||||
|
mp4: false // requires ffmpeg-worker-hls.js or ffmpeg-worker-dash.js
|
||||||
|
// to be configured with MP4 support (which is not the default)
|
||||||
|
},
|
||||||
|
webcodecs: {
|
||||||
|
video: {
|
||||||
|
...(protocol === 'dash' ? {
|
||||||
|
codec: 'vp09.00.10.08.01'
|
||||||
|
} : {
|
||||||
|
codec: 'avc1.42E01E' /*'avc1.42001E'*/,
|
||||||
|
avc: { format: 'annexb' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
codec: 'opus' /*'pcm'*/,
|
||||||
|
},
|
||||||
|
webm_muxer: {
|
||||||
|
video: {
|
||||||
|
codec: protocol === 'dash' ? 'V_VP9' : 'V_MPEG4/ISO/AVC'
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
codec: 'A_OPUS',
|
||||||
|
bit_depth: 0 // 32 for pcm */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ffmpeg: {
|
||||||
|
video: {
|
||||||
|
codec: protocol === 'dash' ? 'libvpx-vp9' : 'libx264'
|
||||||
|
},
|
||||||
|
audio: {
|
||||||
|
codec: protocol === 'dash' ? 'libopus' : 'aac'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class HLS extends EventTarget {
|
export class Streamer extends EventTarget {
|
||||||
constructor(stream, audio_context, base_url, ffmpeg_lib_url, frame_rate, rotate) {
|
constructor(stream, audio_context, base_url, config, rotate) {
|
||||||
super();
|
super();
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
this.audio_context = audio_context;
|
this.audio_context = audio_context;
|
||||||
this.base_url = base_url;
|
this.base_url = base_url;
|
||||||
this.ffmpeg_lib_url = ffmpeg_lib_url;
|
this.config = config;
|
||||||
this.frame_rate = frame_rate;
|
|
||||||
if (rotate) {
|
if (rotate) {
|
||||||
this.ffmpeg_metadata = ['-metadata:s:v:0', 'rotate=-90'];
|
this.ffmpeg_metadata = ['-metadata:s:v:0', 'rotate=-90'];
|
||||||
} else {
|
} else {
|
||||||
@@ -30,25 +81,47 @@ export class HLS extends EventTarget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mrcfg = this.config.media_recorder;
|
||||||
|
|
||||||
|
const mp4 = async () => {
|
||||||
|
if (mfcrg.mp4) {
|
||||||
|
// try MediaRecorder MP4 - this should work on Safari MacOS and iOS,
|
||||||
|
// producing H.264 video and AAC audio
|
||||||
|
await this.media_recorder('video/mp4');
|
||||||
|
console.log("Using MediaRecorder MP4 (H264,aac)");
|
||||||
|
} else {
|
||||||
|
throw new Error('no supported encoding methods');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const webcodecs = async () => {
|
||||||
|
const wccfg = this.config.webcodecs;
|
||||||
|
if (wccfg) {
|
||||||
try {
|
try {
|
||||||
// first try WebM/H264 MediaRecorder - this should work on Chrome Linux and Windows
|
// try WebCodecs - this should work on Chrome including Android
|
||||||
await this.media_recorder('video/webm;codecs=H264');
|
await this.webcodecs();
|
||||||
console.log("Using MediaRecorder WebM/h264");
|
|
||||||
} catch (ex) {
|
|
||||||
console.warn(ex.toString());
|
|
||||||
try {
|
|
||||||
// next try WebCodecs - this should work on Chrome including Android
|
|
||||||
await this.webcodecs(video_encoder_codec,
|
|
||||||
'opus' /*'pcm'*/,
|
|
||||||
{ avc: { format: 'annexb' } });
|
|
||||||
console.log("Using WebCodecs");
|
console.log("Using WebCodecs");
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
console.warn(ex.toString());
|
console.warn(ex.toString());
|
||||||
// finally try MP4 - this should work on Safari MacOS and iOS, producing H264
|
await mp4();
|
||||||
// this assumes ffmpeg-hls.js has been configured with MP4 support
|
|
||||||
await this.media_recorder('video/mp4');
|
|
||||||
console.log("Using MediaRecorder MP4");
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
await mp4();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (mrcfg.webm) {
|
||||||
|
try {
|
||||||
|
// try MediaRecorder WebM - this should work on Chrome Linux and Windows
|
||||||
|
const codecs = `${mrcfg.video.codec},${mrcfg.audio.codec}`;
|
||||||
|
await this.media_recorder(`video/webm;codecs=${codecs}`);
|
||||||
|
console.log(`Using MediaRecorder WebM (${codecs})`);
|
||||||
|
} catch (ex) {
|
||||||
|
console.warn(ex.toString());
|
||||||
|
await webcodecs();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await webcodecs();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.started = true;
|
this.started = true;
|
||||||
@@ -59,7 +132,7 @@ export class HLS extends EventTarget {
|
|||||||
await this.audio_context.audioWorklet.addModule('./dummy-worklet.js');
|
await this.audio_context.audioWorklet.addModule('./dummy-worklet.js');
|
||||||
this.dummy_processor = new AudioWorkletNode(this.audio_context, 'dummy-processor', {
|
this.dummy_processor = new AudioWorkletNode(this.audio_context, 'dummy-processor', {
|
||||||
processorOptions: {
|
processorOptions: {
|
||||||
update_rate: this.frame_rate
|
update_rate: this.config.video.framerate
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.dummy_processor.onerror = onerror;
|
this.dummy_processor.onerror = onerror;
|
||||||
@@ -73,6 +146,30 @@ export class HLS extends EventTarget {
|
|||||||
this.dummy_processor.disconnect();
|
this.dummy_processor.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
receiver_args(video_codec, audio_codec) {
|
||||||
|
return {
|
||||||
|
ffmpeg_lib_url: this.config.ffmpeg_lib_url,
|
||||||
|
ffmpeg_args: [
|
||||||
|
'-i', '/work/stream1',
|
||||||
|
'-map', '0:v',
|
||||||
|
'-map', '0:a',
|
||||||
|
...(video_codec === this.config.ffmpeg.video.codec ||
|
||||||
|
video_codec === 'copy' ?
|
||||||
|
['-c:v', 'copy'] : // pass through the video data (no decoding or encoding)
|
||||||
|
['-c:v', this.config.ffmpeg.video.codec, // re-encode video
|
||||||
|
'-b:v', this.config.video.bitrate.toString()]), // set video bitrate
|
||||||
|
...this.ffmpeg_metadata,
|
||||||
|
...(audio_codec === this.config.ffmpeg.audio.codec ||
|
||||||
|
audio_codec === 'copy' ?
|
||||||
|
['-c:a', 'copy'] : // pass through the audio data
|
||||||
|
['-c:a', this.config.ffmpeg.audio.codec, // re-encode audio
|
||||||
|
'-b:a', this.config.audio.bitrate.toString()]) // set audio bitrate
|
||||||
|
],
|
||||||
|
base_url: this.base_url,
|
||||||
|
protocol: this.config.protocol
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async media_recorder(mimeType) {
|
async media_recorder(mimeType) {
|
||||||
const onerror = this.onerror.bind(this);
|
const onerror = this.onerror.bind(this);
|
||||||
|
|
||||||
@@ -80,8 +177,8 @@ export class HLS extends EventTarget {
|
|||||||
// note we don't start recording until ffmpeg has started (below)
|
// note we don't start recording until ffmpeg has started (below)
|
||||||
const recorder = new MediaRecorder(this.stream, {
|
const recorder = new MediaRecorder(this.stream, {
|
||||||
mimeType,
|
mimeType,
|
||||||
audioBitsPerSecond,
|
videoBitsPerSecond: this.config.video.bitrate,
|
||||||
videoBitsPerSecond
|
audioBitsPerSecond: this.config.audio.bitrate
|
||||||
});
|
});
|
||||||
recorder.onerror = onerror;
|
recorder.onerror = onerror;
|
||||||
|
|
||||||
@@ -101,27 +198,71 @@ export class HLS extends EventTarget {
|
|||||||
|
|
||||||
await this.start_dummy_processor();
|
await this.start_dummy_processor();
|
||||||
|
|
||||||
|
let video_codec, audio_codec;
|
||||||
|
if (recorder.mimeType === 'video/mp4') {
|
||||||
|
video_codec = 'libx264';
|
||||||
|
audio_codec = 'aac';
|
||||||
|
} else {
|
||||||
|
switch (this.config.media_recorder.video.codec.toLowerCase()) {
|
||||||
|
case 'av1':
|
||||||
|
video_codec = 'libaom-av1';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'h264':
|
||||||
|
video_codec = 'libx264';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'vp8':
|
||||||
|
video_codec = 'libvpx';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'vp9':
|
||||||
|
video_codec = 'libvpx-vp9';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
video_codec = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (this.config.media_recorder.audio.codec.toLowerCase()) {
|
||||||
|
case 'flac':
|
||||||
|
audio_codec = 'flac';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'mp3':
|
||||||
|
audio_codec = 'libmp3lame';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'opus':
|
||||||
|
audio_codec = 'libopus';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'vorbis':
|
||||||
|
audio_codec = 'libvorbis';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pcm':
|
||||||
|
audio_codec = 'f32le';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (audio_codes.startsWith('mp4a')) {
|
||||||
|
audio_codec = 'aac';
|
||||||
|
} else {
|
||||||
|
audio_codec = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// start the ffmpeg worker
|
// start the ffmpeg worker
|
||||||
this.receiver = new MuxReceiver();
|
this.receiver = new MuxReceiver();
|
||||||
this.receiver.addEventListener('message', e => {
|
this.receiver.addEventListener('message', e => {
|
||||||
const msg = e.detail;
|
const msg = e.detail;
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'ready':
|
case 'ready':
|
||||||
this.receiver.start({
|
this.receiver.start(this.receiver_args(video_codec, audio_codec));
|
||||||
ffmpeg_lib_url: this.ffmpeg_lib_url,
|
|
||||||
ffmpeg_args: [
|
|
||||||
'-i', '/work/stream1',
|
|
||||||
'-map', '0:v',
|
|
||||||
'-map', '0:a',
|
|
||||||
'-c:v', 'copy', // pass through the video data (h264, no decoding or encoding)
|
|
||||||
...this.ffmpeg_metadata,
|
|
||||||
...(recorder.mimeType === 'video/mp4' ?
|
|
||||||
['-c:a', 'copy'] : // assume already AAC
|
|
||||||
['-c:a', 'aac', // re-encode audio as AAC-LC
|
|
||||||
'-b:a', audioBitsPerSecond.toString()]) // set audio bitrate
|
|
||||||
],
|
|
||||||
base_url: this.base_url
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
@@ -153,7 +294,7 @@ export class HLS extends EventTarget {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async webcodecs(video_codec, audio_codec, video_config, audio_config) {
|
async webcodecs() {
|
||||||
const onerror = this.onerror.bind(this);
|
const onerror = this.onerror.bind(this);
|
||||||
|
|
||||||
const video_track = this.stream.getVideoTracks()[0];
|
const video_track = this.stream.getVideoTracks()[0];
|
||||||
@@ -209,13 +350,11 @@ export class HLS extends EventTarget {
|
|||||||
readable: video_readable,
|
readable: video_readable,
|
||||||
key_frame_interval,
|
key_frame_interval,
|
||||||
config: {
|
config: {
|
||||||
codec: video_codec,
|
...this.config.video,
|
||||||
bitrate: videoBitsPerSecond,
|
...this.config.webcodecs.video,
|
||||||
framerate: this.frame_rate,
|
|
||||||
latencyMode: 'realtime',
|
latencyMode: 'realtime',
|
||||||
width: video_settings.width,
|
width: video_settings.width,
|
||||||
height: video_settings.height,
|
height: video_settings.height,
|
||||||
...video_config
|
|
||||||
},
|
},
|
||||||
}, [video_readable]);
|
}, [video_readable]);
|
||||||
|
|
||||||
@@ -224,11 +363,10 @@ export class HLS extends EventTarget {
|
|||||||
audio: true,
|
audio: true,
|
||||||
readable: audio_readable,
|
readable: audio_readable,
|
||||||
config: {
|
config: {
|
||||||
codec: audio_codec,
|
...this.config.audio,
|
||||||
bitrate: audioBitsPerSecond,
|
...this.config.webcodecs.audio,
|
||||||
sampleRate: audio_settings.sampleRate,
|
sampleRate: audio_settings.sampleRate,
|
||||||
numberOfChannels: audio_settings.channelCount,
|
numberOfChannels: audio_settings.channelCount,
|
||||||
...audio_config
|
|
||||||
},
|
},
|
||||||
}, [audio_readable]);
|
}, [audio_readable]);
|
||||||
|
|
||||||
@@ -257,6 +395,60 @@ export class HLS extends EventTarget {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let video_codec;
|
||||||
|
switch (this.config.webcodecs.video.codec) {
|
||||||
|
case 'V_AV1':
|
||||||
|
video_codec = 'libaom-av1';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'V_MPEG4/ISO/AVC':
|
||||||
|
video_codec = 'libx264';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'V_VP8':
|
||||||
|
video_codec = 'libvpx';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'V_VP9':
|
||||||
|
video_codec = 'libvpx-vp9';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
video_codec = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let audio_codec;
|
||||||
|
switch (this.config.webcodecs.audio.codec) {
|
||||||
|
case 'A_FLAC':
|
||||||
|
audio_codec = 'flac';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'A_MPEG/L3':
|
||||||
|
audio_codec = 'libmp3lame';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'A_OPUS':
|
||||||
|
audio_codec = 'libopus';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'A_VORBIS':
|
||||||
|
audio_codec = 'libvorbis';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'A_PCM/FLOAT/IEEE':
|
||||||
|
audio_codec = 'f32le';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (audio_codes.startsWith('A_AAC')) {
|
||||||
|
audio_codec = 'aac';
|
||||||
|
} else {
|
||||||
|
audio_codec = null;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
this.worker.postMessage({
|
this.worker.postMessage({
|
||||||
type: 'start',
|
type: 'start',
|
||||||
webm_metadata: {
|
webm_metadata: {
|
||||||
@@ -264,28 +456,19 @@ export class HLS extends EventTarget {
|
|||||||
video: {
|
video: {
|
||||||
width: video_settings.width,
|
width: video_settings.width,
|
||||||
height: video_settings.height,
|
height: video_settings.height,
|
||||||
frame_rate: this.frame_rate,
|
frame_rate: this.config.video.framerate,
|
||||||
codec_id: 'V_MPEG4/ISO/AVC'
|
codec_id: this.config.webcodecs.webm_muxer.video.codec
|
||||||
},
|
},
|
||||||
audio: {
|
audio: {
|
||||||
sample_rate: audio_settings.sampleRate,
|
sample_rate: audio_settings.sampleRate,
|
||||||
channels: audio_settings.channelCount,
|
channels: audio_settings.channelCount,
|
||||||
codec_id: 'A_OPUS'
|
bit_depth: this.config.webcodecs.webm_muxer.audio.bit_depth,
|
||||||
|
codec_id: this.config.webcodecs.webm_muxer.audio.codec
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
webm_receiver: './mux-receiver.js',
|
webm_receiver: './mux-receiver.js',
|
||||||
webm_receiver_data: { name: 'stream1' },
|
webm_receiver_data: { name: 'stream1' },
|
||||||
ffmpeg_lib_url: this.ffmpeg_lib_url,
|
...this.receiver_args(video_codec, audio_codec)
|
||||||
base_url: this.base_url,
|
|
||||||
ffmpeg_args: [
|
|
||||||
'-i', '/work/stream1',
|
|
||||||
'-map', '0:v',
|
|
||||||
'-map', '0:a',
|
|
||||||
'-c:v', 'copy', // pass through the video data (h264, no decoding or encoding)
|
|
||||||
...this.ffmpeg_metadata,
|
|
||||||
'-c:a', 'aac', // re-encode audio as AAC-LC
|
|
||||||
'-b:a', audioBitsPerSecond.toString() // set audio bitrate
|
|
||||||
]
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue
Block a user