blob: 7c9709f5bde407e1f9acebe76f40fb9d1ec6ed32 [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 createProcessedMediaStreamTrack */ // defined in main.js
/**
* Wrapper around createProcessedMediaStreamTrack to apply transform to a
* MediaStream.
* @param {!MediaStream} sourceStream the video stream to be transformed. The
* first video track will be used.
* @param {!FrameTransformFn} transform the transform to apply to the
* sourceStream.
* @param {!AbortSignal} signal can be used to stop processing
* @return {!MediaStream} holds a single video track of the transformed video
* frames
*/
function createProcessedMediaStream(sourceStream, transform, signal) {
// For this sample, we're only dealing with video tracks.
/** @type {!MediaStreamTrack} */
const sourceTrack = sourceStream.getVideoTracks()[0];
const processedTrack =
createProcessedMediaStreamTrack(sourceTrack, transform, signal);
// Create a new MediaStream to hold our processed track.
const processedStream = new MediaStream();
processedStream.addTrack(processedTrack);
return processedStream;
}
/**
* Interface implemented by all video sources the user can select. A common
* interface allows the user to choose a source independently of the transform
* and sink.
* @interface
*/
class MediaStreamSource { // eslint-disable-line no-unused-vars
/**
* Sets the path to this object from the debug global var.
* @param {string} path
*/
setDebugPath(path) {}
/**
* Indicates if the source video should be mirrored/displayed on the page. If
* false (the default), any element producing frames will not be a child of
* the document.
* @param {boolean} visible whether to add the raw source video to the page
*/
setVisibility(visible) {}
/**
* Initializes and returns the MediaStream for this source.
* @return {!Promise<!MediaStream>}
*/
async getMediaStream() {}
/** Frees any resources used by this object. */
destroy() {}
}
/**
* Interface implemented by all video transforms that the user can select. A
* common interface allows the user to choose a transform independently of the
* source and sink.
* @interface
*/
class FrameTransform { // eslint-disable-line no-unused-vars
/** Initializes state that is reused across frames. */
async init() {}
/**
* Applies the transform to frame. Queues the output frame (if any) using the
* controller.
* @param {!VideoFrame} frame the input frame
* @param {!TransformStreamDefaultController<!VideoFrame>} controller
*/
async transform(frame, controller) {}
/** Frees any resources used by this object. */
destroy() {}
}
/**
* Interface implemented by all video sinks that the user can select. A common
* interface allows the user to choose a sink independently of the source and
* transform.
* @interface
*/
class MediaStreamSink { // eslint-disable-line no-unused-vars
/**
* @param {!MediaStream} stream
*/
async setMediaStream(stream) {}
/** Frees any resources used by this object. */
destroy() {}
}
/**
* Assembles a MediaStreamSource, FrameTransform, and MediaStreamSink together.
*/
class Pipeline { // eslint-disable-line no-unused-vars
constructor() {
/** @private {?MediaStreamSource} set by updateSource*/
this.source_ = null;
/** @private {?FrameTransform} set by updateTransform */
this.frameTransform_ = null;
/** @private {?MediaStreamSink} set by updateSink */
this.sink_ = null;
/** @private {!AbortController} may used to stop all processing */
this.abortController_ = new AbortController();
/**
* @private {?MediaStream} set in maybeStartPipeline_ after all of source_,
* frameTransform_, and sink_ are set
*/
this.processedStream_ = null;
}
/** @return {?MediaStreamSource} */
getSource() {
return this.source_;
}
/**
* Sets a new source for the pipeline.
* @param {!MediaStreamSource} mediaStreamSource
*/
async updateSource(mediaStreamSource) {
if (this.source_) {
this.abortController_.abort();
this.abortController_ = new AbortController();
this.source_.destroy();
this.processedStream_ = null;
}
this.source_ = mediaStreamSource;
this.source_.setDebugPath('debug.pipeline.source_');
console.log(
'[Pipeline] Updated source.',
'debug.pipeline.source_ = ', this.source_);
await this.maybeStartPipeline_();
}
/** @private */
async maybeStartPipeline_() {
if (this.processedStream_ || !this.source_ || !this.frameTransform_ ||
!this.sink_) {
return;
}
const sourceStream = await this.source_.getMediaStream();
await this.frameTransform_.init();
try {
this.processedStream_ = createProcessedMediaStream(
sourceStream, async (frame, controller) => {
if (this.frameTransform_) {
await this.frameTransform_.transform(frame, controller);
}
}, this.abortController_.signal);
} catch (e) {
this.destroy();
return;
}
await this.sink_.setMediaStream(this.processedStream_);
console.log(
'[Pipeline] Pipeline started.',
'debug.pipeline.abortController_ =', this.abortController_);
}
/**
* Sets a new transform for the pipeline.
* @param {!FrameTransform} frameTransform
*/
async updateTransform(frameTransform) {
if (this.frameTransform_) this.frameTransform_.destroy();
this.frameTransform_ = frameTransform;
console.log(
'[Pipeline] Updated frame transform.',
'debug.pipeline.frameTransform_ = ', this.frameTransform_);
if (this.processedStream_) {
await this.frameTransform_.init();
} else {
await this.maybeStartPipeline_();
}
}
/**
* Sets a new sink for the pipeline.
* @param {!MediaStreamSink} mediaStreamSink
*/
async updateSink(mediaStreamSink) {
if (this.sink_) this.sink_.destroy();
this.sink_ = mediaStreamSink;
console.log(
'[Pipeline] Updated sink.', 'debug.pipeline.sink_ = ', this.sink_);
if (this.processedStream_) {
await this.sink_.setMediaStream(this.processedStream_);
} else {
await this.maybeStartPipeline_();
}
}
/** Frees any resources used by this object. */
destroy() {
console.log('[Pipeline] Destroying Pipeline');
this.abortController_.abort();
if (this.source_) this.source_.destroy();
if (this.frameTransform_) this.frameTransform_.destroy();
if (this.sink_) this.sink_.destroy();
}
}