blob: 6468b853aac5633b7333dfee3655059c5ceabe46 [file] [edit]
/*
* Copyright (c) 2020 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree.
*/
'use strict';
/* global MediaStreamTrackProcessor, MediaStreamTrackGenerator */
if (typeof MediaStreamTrackProcessor === 'undefined' ||
typeof MediaStreamTrackGenerator === 'undefined') {
alert(
'Your browser does not support the experimental MediaStreamTrack API ' +
'for Insertable Streams of Media. See the note at the bottom of the ' +
'page.');
}
/* global CameraSource */ // defined in camera-source.js
/* global CanvasTransform */ // defined in canvas-transform.js
/* global PeerConnectionSink */ // defined in peer-connection-sink.js
/* global PeerConnectionSource */ // defined in peer-connection-source.js
/* global Pipeline */ // defined in pipeline.js
/* global NullTransform, DropTransform, DelayTransform */ // defined in simple-transforms.js
/* global VideoSink */ // defined in video-sink.js
/* global VideoSource */ // defined in video-source.js
/* global WebGLTransform */ // defined in webgl-transform.js
/* global WebCodecTransform */ // defined in webcodec-transform.js
/**
* Allows inspecting objects in the console. See console log messages for
* attributes added to this debug object.
* @type {!Object<string,*>}
*/
let debug = {};
/**
* FrameTransformFn applies a transform to a frame and queues the output frame
* (if any) using the controller. The first argument is the input frame and the
* second argument is the stream controller.
* The VideoFrame should be destroyed as soon as it is no longer needed to free
* resources and maintain good performance.
* @typedef {function(
* !VideoFrame,
* !TransformStreamDefaultController<!VideoFrame>): !Promise<undefined>}
*/
let FrameTransformFn; // eslint-disable-line no-unused-vars
/**
* Creates a pair of MediaStreamTrackProcessor and MediaStreamTrackGenerator
* that applies transform to sourceTrack. This function is the core part of the
* sample, demonstrating how to use the new API.
* @param {!MediaStreamTrack} sourceTrack the video track to be transformed. The
* track can be from any source, e.g. getUserMedia, RTCTrackEvent, or
* captureStream on HTMLMediaElement or HTMLCanvasElement.
* @param {!FrameTransformFn} transform the transform to apply to sourceTrack;
* the transformed frames are available on the returned track. See the
* implementations of FrameTransform.transform later in this file for
* examples.
* @param {!AbortSignal} signal can be used to stop processing
* @return {!MediaStreamTrack} the result of sourceTrack transformed using
* transform.
*/
// eslint-disable-next-line no-unused-vars
function createProcessedMediaStreamTrack(sourceTrack, transform, signal) {
// Create the MediaStreamTrackProcessor.
/** @type {?MediaStreamTrackProcessor<!VideoFrame>} */
let processor;
try {
processor = new MediaStreamTrackProcessor(sourceTrack);
} catch (e) {
alert(`MediaStreamTrackProcessor failed: ${e}`);
throw e;
}
// Create the MediaStreamTrackGenerator.
/** @type {?MediaStreamTrackGenerator<!VideoFrame>} */
let generator;
try {
generator = new MediaStreamTrackGenerator('video');
} catch (e) {
alert(`MediaStreamTrackGenerator failed: ${e}`);
throw e;
}
const source = processor.readable;
const sink = generator.writable;
// Create a TransformStream using our FrameTransformFn. (Note that the
// "Stream" in TransformStream refers to the Streams API, specified by
// https://streams.spec.whatwg.org/, not the Media Capture and Streams API,
// specified by https://w3c.github.io/mediacapture-main/.)
/** @type {!TransformStream<!VideoFrame, !VideoFrame>} */
const transformer = new TransformStream({transform});
// Apply the transform to the processor's stream and send it to the
// generator's stream.
const promise = source.pipeThrough(transformer, {signal}).pipeTo(sink);
promise.catch((e) => {
if (signal.aborted) {
console.log(
'[createProcessedMediaStreamTrack] Shutting down streams after abort.');
} else {
console.error(
'[createProcessedMediaStreamTrack] Error from stream transform:', e);
}
source.cancel(e);
sink.abort(e);
});
debug['processor'] = processor;
debug['generator'] = generator;
debug['transformStream'] = transformer;
console.log(
'[createProcessedMediaStreamTrack] Created MediaStreamTrackProcessor, ' +
'MediaStreamTrackGenerator, and TransformStream.',
'debug.processor =', processor, 'debug.generator =', generator,
'debug.transformStream =', transformer);
return generator;
}
/**
* The current video pipeline. Initialized by initPipeline().
* @type {?Pipeline}
*/
let pipeline;
/**
* Sets up handlers for interacting with the UI elements on the page.
*/
function initUI() {
const sourceSelector = /** @type {!HTMLSelectElement} */ (
document.getElementById('sourceSelector'));
const sourceVisibleCheckbox = (/** @type {!HTMLInputElement} */ (
document.getElementById('sourceVisible')));
/**
* Updates the pipeline based on the current settings of the sourceSelector
* and sourceVisible UI elements. Unlike updatePipelineSource(), never
* re-initializes the pipeline.
*/
function updatePipelineSourceIfSet() {
const sourceType =
sourceSelector.options[sourceSelector.selectedIndex].value;
if (!sourceType) return;
console.log(`[UI] Selected source: ${sourceType}`);
let source;
switch (sourceType) {
case 'camera':
source = new CameraSource();
break;
case 'video':
source = new VideoSource();
break;
case 'pc':
source = new PeerConnectionSource(new CameraSource());
break;
default:
alert(`unknown source ${sourceType}`);
return;
}
source.setVisibility(sourceVisibleCheckbox.checked);
pipeline.updateSource(source);
}
/**
* Updates the pipeline based on the current settings of the sourceSelector
* and sourceVisible UI elements. If the "stopped" option is selected,
* reinitializes the pipeline instead.
*/
function updatePipelineSource() {
const sourceType =
sourceSelector.options[sourceSelector.selectedIndex].value;
if (!sourceType || !pipeline) {
initPipeline();
} else {
updatePipelineSourceIfSet();
}
}
sourceSelector.oninput = updatePipelineSource;
sourceSelector.disabled = false;
/**
* Updates the source visibility, if the source is already started.
*/
function updatePipelineSourceVisibility() {
console.log(`[UI] Changed source visibility: ${
sourceVisibleCheckbox.checked ? 'added' : 'removed'}`);
if (pipeline) {
const source = pipeline.getSource();
if (source) {
source.setVisibility(sourceVisibleCheckbox.checked);
}
}
}
sourceVisibleCheckbox.oninput = updatePipelineSourceVisibility;
sourceVisibleCheckbox.disabled = false;
const transformSelector = /** @type {!HTMLSelectElement} */ (
document.getElementById('transformSelector'));
/**
* Updates the pipeline based on the current settings of the transformSelector
* UI element.
*/
function updatePipelineTransform() {
if (!pipeline) {
return;
}
const transformType =
transformSelector.options[transformSelector.selectedIndex].value;
console.log(`[UI] Selected transform: ${transformType}`);
switch (transformType) {
case 'webgl':
pipeline.updateTransform(new WebGLTransform());
break;
case 'canvas2d':
pipeline.updateTransform(new CanvasTransform());
break;
case 'drop':
// Defined in simple-transforms.js.
pipeline.updateTransform(new DropTransform());
break;
case 'noop':
// Defined in simple-transforms.js.
pipeline.updateTransform(new NullTransform());
break;
case 'delay':
// Defined in simple-transforms.js.
pipeline.updateTransform(new DelayTransform());
break;
case 'webcodec':
// Defined in webcodec-transform.js
pipeline.updateTransform(new WebCodecTransform());
break;
default:
alert(`unknown transform ${transformType}`);
break;
}
}
transformSelector.oninput = updatePipelineTransform;
transformSelector.disabled = false;
const sinkSelector = (/** @type {!HTMLSelectElement} */ (
document.getElementById('sinkSelector')));
/**
* Updates the pipeline based on the current settings of the sinkSelector UI
* element.
*/
function updatePipelineSink() {
const sinkType = sinkSelector.options[sinkSelector.selectedIndex].value;
console.log(`[UI] Selected sink: ${sinkType}`);
switch (sinkType) {
case 'video':
pipeline.updateSink(new VideoSink());
break;
case 'pc':
pipeline.updateSink(new PeerConnectionSink());
break;
default:
alert(`unknown sink ${sinkType}`);
break;
}
}
sinkSelector.oninput = updatePipelineSink;
sinkSelector.disabled = false;
/**
* Initializes/reinitializes the pipeline. Called on page load and after the
* user chooses to stop the video source.
*/
function initPipeline() {
if (pipeline) pipeline.destroy();
pipeline = new Pipeline();
debug = {pipeline};
updatePipelineSourceIfSet();
updatePipelineTransform();
updatePipelineSink();
console.log(
'[initPipeline] Created new Pipeline.', 'debug.pipeline =', pipeline);
}
}
window.onload = initUI;