/*
 *  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 VideoMirrorHelper */ // defined in video-mirror-helper.js

/**
 * Opens the device's camera with getUserMedia.
 * @implements {MediaStreamSource} in pipeline.js
 */
class CameraSource { // eslint-disable-line no-unused-vars
  constructor() {
    /**
     * @private @const {!VideoMirrorHelper} manages displaying the video stream
     *     in the page
     */
    this.videoMirrorHelper_ = new VideoMirrorHelper();
    /** @private {?MediaStream} camera stream, initialized in getMediaStream */
    this.stream_ = null;
    /** @private {string} */
    this.debugPath_ = '<unknown>';
  }
  /** @override */
  setDebugPath(path) {
    this.debugPath_ = path;
    this.videoMirrorHelper_.setDebugPath(`${path}.videoMirrorHelper_`);
  }
  /** @override */
  setVisibility(visible) {
    this.videoMirrorHelper_.setVisibility(visible);
  }
  /** @override */
  async getMediaStream() {
    if (this.stream_) return this.stream_;
    console.log('[CameraSource] Requesting camera.');
    this.stream_ =
        await navigator.mediaDevices.getUserMedia({audio: false, video: true});
    console.log(
        '[CameraSource] Received camera stream.',
        `${this.debugPath_}.stream_ =`, this.stream_);
    this.videoMirrorHelper_.setStream(this.stream_);
    return this.stream_;
  }
  /** @override */
  destroy() {
    console.log('[CameraSource] Stopping camera');
    this.videoMirrorHelper_.destroy();
    if (this.stream_) {
      this.stream_.getTracks().forEach(t => t.stop());
    }
  }
}

/*
 *  Copyright (c) 2021 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';

const TEXT_SOURCE =
    'https://raw.githubusercontent.com/w3c/mediacapture-insertable-streams/main/explainer.md';
const CANVAS_ASPECT_RATIO = 16 / 9;

/**
 * @param {number} x
 * @return {number} x rounded to the nearest even integer
 */
function roundToEven(x) {
  return 2 * Math.round(x / 2);
}

/**
 * Draws text on a Canvas.
 * @implements {MediaStreamSource} in pipeline.js
 */
class CanvasSource { // eslint-disable-line no-unused-vars
  constructor() {
    /** @private {boolean} */
    this.visibility_ = false;
    /**
     * @private {?HTMLCanvasElement} canvas element providing the MediaStream.
     */
    this.canvas_ = null;
    /**
     * @private {?CanvasRenderingContext2D} the 2D context used to draw the
     *     animation.
     */
    this.ctx_ = null;
    /**
     * @private {?MediaStream} the MediaStream from captureStream.
     */
    this.stream_ = null;
    /**
     * @private {?CanvasCaptureMediaStreamTrack} the capture track from
     *     canvas_, obtained from stream_. We manually request new animation
     *     frames on this track.
     */
    this.captureTrack_ = null;
    /** @private {number} requestAnimationFrame handle */
    this.requestAnimationFrameHandle_ = 0;
    /** @private {!Array<string>} text to render */
    this.text_ = ['WebRTC samples'];
    /** @private {string} */
    this.debugPath_ = '<unknown>';
    fetch(TEXT_SOURCE)
        .then(response => {
          if (response.ok) {
            return response.text();
          }
          throw new Error(`Request completed with status ${response.status}.`);
        })
        .then(text => {
          this.text_ = text.trim().split('\n');
        })
        .catch((e) => {
          console.log(`[CanvasSource] The request to retrieve ${
            TEXT_SOURCE} encountered an error: ${e}.`);
        });
  }
  /** @override */
  setDebugPath(path) {
    this.debugPath_ = path;
  }
  /** @override */
  setVisibility(visible) {
    this.visibility_ = visible;
    if (this.canvas_) {
      this.updateCanvasVisibility();
    }
  }
  /** @private */
  updateCanvasVisibility() {
    if (this.canvas_.parentNode && !this.visibility_) {
      this.canvas_.parentNode.removeChild(this.canvas_);
    } else if (!this.canvas_.parentNode && this.visibility_) {
      console.log('[CanvasSource] Adding source canvas to page.');
      const outputVideoContainer =
          document.getElementById('outputVideoContainer');
      outputVideoContainer.parentNode.insertBefore(
          this.canvas_, outputVideoContainer);
    }
  }
  /** @private */
  requestAnimationFrame() {
    this.requestAnimationFrameHandle_ =
        requestAnimationFrame(now => this.animate(now));
  }
  /**
   * @private
   * @param {number} now current animation timestamp
   */
  animate(now) {
    this.requestAnimationFrame();
    const ctx = this.ctx_;
    if (!this.canvas_ || !ctx || !this.captureTrack_) {
      return;
    }

    // Resize canvas based on displayed size; or if not visible, based on the
    // output video size.
    // VideoFrame prefers to have dimensions that are even numbers.
    if (this.visibility_) {
      this.canvas_.width = roundToEven(this.canvas_.clientWidth);
    } else {
      const outputVideoContainer =
          document.getElementById('outputVideoContainer');
      const outputVideo = outputVideoContainer.firstElementChild;
      if (outputVideo) {
        this.canvas_.width = roundToEven(outputVideo.clientWidth);
      }
    }
    this.canvas_.height = roundToEven(this.canvas_.width / CANVAS_ASPECT_RATIO);

    ctx.fillStyle = '#fff';
    ctx.fillRect(0, 0, this.canvas_.width, this.canvas_.height);

    const linesShown = 20;
    const millisecondsPerLine = 1000;
    const linesIncludingExtraBlank = this.text_.length + linesShown;
    const totalAnimationLength = linesIncludingExtraBlank * millisecondsPerLine;
    const currentFrame = now % totalAnimationLength;
    const firstLineIdx = Math.floor(
        linesIncludingExtraBlank * (currentFrame / totalAnimationLength) -
        linesShown);
    const lineFraction = (now % millisecondsPerLine) / millisecondsPerLine;

    const border = 20;
    const fontSize = (this.canvas_.height - 2 * border) / (linesShown + 1);
    ctx.font = `${fontSize}px sansserif`;

    const textWidth = this.canvas_.width - 2 * border;

    // first line
    if (firstLineIdx >= 0) {
      const fade = Math.floor(256 * lineFraction);
      ctx.fillStyle = `rgb(${fade},${fade},${fade})`;
      const position = (2 - lineFraction) * fontSize;
      ctx.fillText(this.text_[firstLineIdx], border, position, textWidth);
    }

    // middle lines
    for (let line = 2; line <= linesShown - 1; line++) {
      const lineIdx = firstLineIdx + line - 1;
      if (lineIdx >= 0 && lineIdx < this.text_.length) {
        ctx.fillStyle = 'black';
        const position = (line + 1 - lineFraction) * fontSize;
        ctx.fillText(this.text_[lineIdx], border, position, textWidth);
      }
    }

    // last line
    const lastLineIdx = firstLineIdx + linesShown - 1;
    if (lastLineIdx >= 0 && lastLineIdx < this.text_.length) {
      const fade = Math.floor(256 * (1 - lineFraction));
      ctx.fillStyle = `rgb(${fade},${fade},${fade})`;
      const position = (linesShown + 1 - lineFraction) * fontSize;
      ctx.fillText(this.text_[lastLineIdx], border, position, textWidth);
    }

    this.captureTrack_.requestFrame();
  }
  /** @override */
  async getMediaStream() {
    if (this.stream_) return this.stream_;

    console.log('[CanvasSource] Initializing 2D context for source animation.');
    this.canvas_ =
      /** @type {!HTMLCanvasElement} */ (document.createElement('canvas'));
    this.canvas_.classList.add('video', 'sourceVideo');
    // Generally video frames do not have an alpha channel. Even if the browser
    // supports it, there may be a performance cost, so we disable alpha.
    this.ctx_ = /** @type {?CanvasRenderingContext2D} */ (
      this.canvas_.getContext('2d', {alpha: false}));
    if (!this.ctx_) {
      throw new Error('Unable to create CanvasRenderingContext2D');
    }
    this.updateCanvasVisibility();
    this.stream_ = this.canvas_.captureStream(0);
    this.captureTrack_ = /** @type {!CanvasCaptureMediaStreamTrack} */ (
      this.stream_.getTracks()[0]);
    this.requestAnimationFrame();
    console.log(
        '[CanvasSource] Initialized canvas, context, and capture stream.',
        `${this.debugPath_}.canvas_ =`, this.canvas_,
        `${this.debugPath_}.ctx_ =`, this.ctx_, `${this.debugPath_}.stream_ =`,
        this.stream_, `${this.debugPath_}.captureTrack_ =`, this.captureTrack_);

    return this.stream_;
  }
  /** @override */
  destroy() {
    console.log('[CanvasSource] Stopping source animation');
    if (this.requestAnimationFrameHandle_) {
      cancelAnimationFrame(this.requestAnimationFrameHandle_);
    }
    if (this.canvas_) {
      if (this.canvas_.parentNode) {
        this.canvas_.parentNode.removeChild(this.canvas_);
      }
    }
  }
}

/*
 *  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';

/**
 * Applies a picture-frame effect using CanvasRenderingContext2D.
 * @implements {FrameTransform} in pipeline.js
 */
class CanvasTransform { // eslint-disable-line no-unused-vars
  constructor() {
    /**
     * @private {?OffscreenCanvas} canvas used to create the 2D context.
     *     Initialized in init.
     */
    this.canvas_ = null;
    /**
     * @private {?CanvasRenderingContext2D} the 2D context used to draw the
     *     effect. Initialized in init.
     */
    this.ctx_ = null;
    /**
     * @private {boolean} If false, pass VideoFrame directly to
     * CanvasRenderingContext2D.drawImage and create VideoFrame directly from
     * this.canvas_. If either of these operations fail (it's not supported in
     * Chrome <90 and broken in Chrome 90: https://crbug.com/1184128), we set
     * this field to true; in that case we create an ImageBitmap from the
     * VideoFrame and pass the ImageBitmap to drawImage on the input side and
     * create the VideoFrame using an ImageBitmap of the canvas on the output
     * side.
     */
    this.use_image_bitmap_ = false;
    /** @private {string} */
    this.debugPath_ = 'debug.pipeline.frameTransform_';
  }
  /** @override */
  async init() {
    console.log('[CanvasTransform] Initializing 2D context for transform');
    this.canvas_ = new OffscreenCanvas(1, 1);
    this.ctx_ = /** @type {?CanvasRenderingContext2D} */ (
      this.canvas_.getContext('2d', {alpha: false, desynchronized: true}));
    if (!this.ctx_) {
      throw new Error('Unable to create CanvasRenderingContext2D');
    }
    console.log(
        '[CanvasTransform] CanvasRenderingContext2D initialized.',
        `${this.debugPath_}.canvas_ =`, this.canvas_,
        `${this.debugPath_}.ctx_ =`, this.ctx_);
  }

  /** @override */
  async transform(frame, controller) {
    const ctx = this.ctx_;
    if (!this.canvas_ || !ctx) {
      frame.close();
      return;
    }
    const width = frame.displayWidth;
    const height = frame.displayHeight;
    this.canvas_.width = width;
    this.canvas_.height = height;
    const timestamp = frame.timestamp;

    if (!this.use_image_bitmap_) {
      try {
        // Supported for Chrome 90+.
        ctx.drawImage(frame, 0, 0);
      } catch (e) {
        // This should only happen on Chrome <90.
        console.log(
            '[CanvasTransform] Failed to draw VideoFrame directly. Falling ' +
                'back to ImageBitmap.',
            e);
        this.use_image_bitmap_ = true;
      }
    }
    if (this.use_image_bitmap_) {
      // Supported for Chrome <92.
      const inputBitmap = await frame.createImageBitmap();
      ctx.drawImage(inputBitmap, 0, 0);
      inputBitmap.close();
    }
    frame.close();

    ctx.shadowColor = '#000';
    ctx.shadowBlur = 20;
    ctx.lineWidth = 50;
    ctx.strokeStyle = '#000';
    ctx.strokeRect(0, 0, width, height);

    if (!this.use_image_bitmap_) {
      try {
        // alpha: 'discard' is needed in order to send frames to a PeerConnection.
        controller.enqueue(new VideoFrame(this.canvas_, {timestamp, alpha: 'discard'}));
      } catch (e) {
        // This should only happen on Chrome <91.
        console.log(
            '[CanvasTransform] Failed to create VideoFrame from ' +
                'OffscreenCanvas directly. Falling back to ImageBitmap.',
            e);
        this.use_image_bitmap_ = true;
      }
    }
    if (this.use_image_bitmap_) {
      const outputBitmap = await createImageBitmap(this.canvas_);
      const outputFrame = new VideoFrame(outputBitmap, {timestamp});
      outputBitmap.close();
      controller.enqueue(outputFrame);
    }
  }

  /** @override */
  destroy() {}
}

/*
 *  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.');
}

// In Chrome 88, VideoFrame.close() was called VideoFrame.destroy()
if (VideoFrame.prototype.close === undefined) {
  VideoFrame.prototype.close = VideoFrame.prototype.destroy;
}

/* global CameraSource */ // defined in camera-source.js
/* global CanvasSource */ // defined in canvas-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 closed 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 'canvas':
        source = new CanvasSource();
        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;

/*
 *  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';

/**
 * Sends a MediaStream to one end of an RTCPeerConnection and provides the
 * remote end as the resulting MediaStream.
 * In an actual video calling app, the two RTCPeerConnection objects would be
 * instantiated on different devices. However, in this sample, both sides of the
 * peer connection are local to allow the sample to be self-contained.
 * For more detailed samples using RTCPeerConnection, take a look at
 * https://webrtc.github.io/samples/.
 */
class PeerConnectionPipe { // eslint-disable-line no-unused-vars
  /**
   * @param {!MediaStream} inputStream stream to pipe over the peer connection
   * @param {string} debugPath the path to this object from the debug global var
   */
  constructor(inputStream, debugPath) {
    /**
     * @private @const {!RTCPeerConnection} the calling side of the peer
     *     connection, connected to inputStream_.
     */
    this.caller_ = new RTCPeerConnection(null);
    /**
     * @private @const {!RTCPeerConnection} the answering side of the peer
     *     connection, providing the stream returned by getMediaStream.
     */
    this.callee_ = new RTCPeerConnection(null);
    /** @private {string} */
    this.debugPath_ = debugPath;
    /**
     * @private @const {!Promise<!MediaStream>} the stream containing tracks
     *     from callee_, returned by getMediaStream.
     */
    this.outputStreamPromise_ = this.init_(inputStream);
  }
  /**
   * Sets the path to this object from the debug global var.
   * @param {string} path
   */
  setDebugPath(path) {
    this.debugPath_ = path;
  }
  /**
   * @param {!MediaStream} inputStream stream to pipe over the peer connection
   * @return {!Promise<!MediaStream>}
   * @private
   */
  async init_(inputStream) {
    console.log(
        '[PeerConnectionPipe] Initiating peer connection.',
        `${this.debugPath_} =`, this);
    this.caller_.onicecandidate = (/** !RTCPeerConnectionIceEvent*/ event) => {
      if (event.candidate) this.callee_.addIceCandidate(event.candidate);
    };
    this.callee_.onicecandidate = (/** !RTCPeerConnectionIceEvent */ event) => {
      if (event.candidate) this.caller_.addIceCandidate(event.candidate);
    };
    const outputStream = new MediaStream();
    const receiverStreamPromise = new Promise(resolve => {
      this.callee_.ontrack = (/** !RTCTrackEvent */ event) => {
        outputStream.addTrack(event.track);
        if (outputStream.getTracks().length == inputStream.getTracks().length) {
          resolve(outputStream);
        }
      };
    });
    inputStream.getTracks().forEach(track => {
      this.caller_.addTransceiver(track, {direction: 'sendonly'});
    });
    await this.caller_.setLocalDescription();
    await this.callee_.setRemoteDescription(
        /** @type {!RTCSessionDescription} */ (this.caller_.localDescription));
    await this.callee_.setLocalDescription();
    await this.caller_.setRemoteDescription(
        /** @type {!RTCSessionDescription} */ (this.callee_.localDescription));
    await receiverStreamPromise;
    console.log(
        '[PeerConnectionPipe] Peer connection established.',
        `${this.debugPath_}.caller_ =`, this.caller_,
        `${this.debugPath_}.callee_ =`, this.callee_);
    return receiverStreamPromise;
  }

  /**
   * Provides the MediaStream that has been piped through a peer connection.
   * @return {!Promise<!MediaStream>}
   */
  getOutputStream() {
    return this.outputStreamPromise_;
  }

  /** Frees any resources used by this object. */
  destroy() {
    console.log('[PeerConnectionPipe] Closing peer connection.');
    this.caller_.close();
    this.callee_.close();
  }
}

/*
 *  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 PeerConnectionPipe */ // defined in peer-connection-pipe.js
/* global VideoSink */ // defined in video-sink.js

/**
 * Sends the transformed video to one end of an RTCPeerConnection and displays
 * the remote end in a video element. In this sample, a PeerConnectionSink
 * represents processing the local user's camera input using a
 * MediaStreamTrackProcessor before sending it to a remote video call
 * participant. Contrast with a PeerConnectionSource.
 * @implements {MediaStreamSink} in pipeline.js
 */
class PeerConnectionSink { // eslint-disable-line no-unused-vars
  constructor() {
    /**
     * @private @const {!VideoSink} manages displaying the video stream in the
     *     page
     */
    this.videoSink_ = new VideoSink();
    /**
     * @private {?PeerConnectionPipe} handles piping the MediaStream through an
     *     RTCPeerConnection
     */
    this.pipe_ = null;
    /** @private {string} */
    this.debugPath_ = 'debug.pipeline.sink_';
    this.videoSink_.setDebugPath(`${this.debugPath_}.videoSink_`);
  }

  /** @override */
  async setMediaStream(stream) {
    console.log(
        '[PeerConnectionSink] Setting peer connection sink stream.', stream);
    if (this.pipe_) this.pipe_.destroy();
    this.pipe_ = new PeerConnectionPipe(stream, `${this.debugPath_}.pipe_`);
    const pipedStream = await this.pipe_.getOutputStream();
    console.log(
        '[PeerConnectionSink] Received callee peer connection stream.',
        pipedStream);
    await this.videoSink_.setMediaStream(pipedStream);
  }

  /** @override */
  destroy() {
    this.videoSink_.destroy();
    if (this.pipe_) this.pipe_.destroy();
  }
}

/*
 *  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 PeerConnectionPipe */ // defined in peer-connection-pipe.js
/* global VideoMirrorHelper */ // defined in video-mirror-helper.js

/**
 * Sends the original source video to one end of an RTCPeerConnection and
 * provides the remote end as the final source.
 * In this sample, a PeerConnectionSource represents receiving video from a
 * remote participant and locally processing it using a
 * MediaStreamTrackProcessor before displaying it on the screen. Contrast with a
 * PeerConnectionSink.
 * @implements {MediaStreamSource} in pipeline.js
 */
class PeerConnectionSource { // eslint-disable-line no-unused-vars
  /**
   * @param {!MediaStreamSource} originalSource original stream source, whose
   *     output is sent over the peer connection
   */
  constructor(originalSource) {
    /**
     * @private @const {!VideoMirrorHelper} manages displaying the video stream
     *     in the page
     */
    this.videoMirrorHelper_ = new VideoMirrorHelper();
    /**
     * @private @const {!MediaStreamSource} original stream source, whose output
     *     is sent on the sender peer connection. In an actual video calling
     *     app, this stream would be generated from the remote participant's
     *     camera. However, in this sample, both sides of the peer connection
     *     are local to allow the sample to be self-contained.
     */
    this.originalStreamSource_ = originalSource;
    /**
     * @private {?PeerConnectionPipe} handles piping the MediaStream through an
     *     RTCPeerConnection
     */
    this.pipe_ = null;
    /** @private {string} */
    this.debugPath_ = '<unknown>';
  }
  /** @override */
  setDebugPath(path) {
    this.debugPath_ = path;
    this.videoMirrorHelper_.setDebugPath(`${path}.videoMirrorHelper_`);
    this.originalStreamSource_.setDebugPath(`${path}.originalStreamSource_`);
    if (this.pipe_) this.pipe_.setDebugPath(`${path}.pipe_`);
  }
  /** @override */
  setVisibility(visible) {
    this.videoMirrorHelper_.setVisibility(visible);
  }

  /** @override */
  async getMediaStream() {
    if (this.pipe_) return this.pipe_.getOutputStream();

    console.log(
        '[PeerConnectionSource] Obtaining original source media stream.',
        `${this.debugPath_}.originalStreamSource_ =`,
        this.originalStreamSource_);
    const originalStream = await this.originalStreamSource_.getMediaStream();
    this.pipe_ =
        new PeerConnectionPipe(originalStream, `${this.debugPath_}.pipe_`);
    const outputStream = await this.pipe_.getOutputStream();
    console.log(
        '[PeerConnectionSource] Received callee peer connection stream.',
        outputStream);
    this.videoMirrorHelper_.setStream(outputStream);
    return outputStream;
  }

  /** @override */
  destroy() {
    this.videoMirrorHelper_.destroy();
    if (this.pipe_) this.pipe_.destroy();
    this.originalStreamSource_.destroy();
  }
}

/*
 *  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();
  }
}

/*
 *  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';

/**
 * Does nothing.
 * @implements {FrameTransform} in pipeline.js
 */
class NullTransform { // eslint-disable-line no-unused-vars
  /** @override */
  async init() {}
  /** @override */
  async transform(frame, controller) {
    controller.enqueue(frame);
  }
  /** @override */
  destroy() {}
}

/**
 * Drops frames at random.
 * @implements {FrameTransform} in pipeline.js
 */
class DropTransform { // eslint-disable-line no-unused-vars
  /** @override */
  async init() {}
  /** @override */
  async transform(frame, controller) {
    if (Math.random() < 0.5) {
      controller.enqueue(frame);
    } else {
      frame.close();
    }
  }
  /** @override */
  destroy() {}
}

/**
 * Delays all frames by 100ms.
 * @implements {FrameTransform} in pipeline.js
 */
class DelayTransform { // eslint-disable-line no-unused-vars
  /** @override */
  async init() {}
  /** @override */
  async transform(frame, controller) {
    await new Promise(resolve => setTimeout(resolve, 100));
    controller.enqueue(frame);
  }
  /** @override */
  destroy() {}
}

/*
 *  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';

/**
 * Helper to display a MediaStream in an HTMLVideoElement, based on the
 * visibility setting.
 */
class VideoMirrorHelper { // eslint-disable-line no-unused-vars
  constructor() {
    /** @private {boolean} */
    this.visibility_ = false;
    /** @private {?MediaStream} the stream to display */
    this.stream_ = null;
    /**
     * @private {?HTMLVideoElement} video element mirroring the camera stream.
     *    Set if visibility_ is true and stream_ is set.
     */
    this.video_ = null;
    /** @private {string} */
    this.debugPath_ = '<unknown>';
  }
  /**
   * Sets the path to this object from the debug global var.
   * @param {string} path
   */
  setDebugPath(path) {
    this.debugPath_ = path;
  }
  /**
   * Indicates if the video should be mirrored/displayed on the page.
   * @param {boolean} visible whether to add the video from the source stream to
   *     the page
   */
  setVisibility(visible) {
    this.visibility_ = visible;
    if (this.video_ && !this.visibility_) {
      this.video_.parentNode.removeChild(this.video_);
      this.video_ = null;
    }
    this.maybeAddVideoElement_();
  }

  /**
   * @param {!MediaStream} stream
   */
  setStream(stream) {
    this.stream_ = stream;
    this.maybeAddVideoElement_();
  }

  /** @private */
  maybeAddVideoElement_() {
    if (!this.video_ && this.visibility_ && this.stream_) {
      this.video_ =
        /** @type {!HTMLVideoElement} */ (document.createElement('video'));
      console.log(
          '[VideoMirrorHelper] Adding source video mirror.',
          `${this.debugPath_}.video_ =`, this.video_);
      this.video_.classList.add('video', 'sourceVideo');
      this.video_.srcObject = this.stream_;
      const outputVideoContainer =
          document.getElementById('outputVideoContainer');
      outputVideoContainer.parentNode.insertBefore(
          this.video_, outputVideoContainer);
      this.video_.play();
    }
  }

  /** Frees any resources used by this object. */
  destroy() {
    if (this.video_) {
      this.video_.pause();
      this.video_.srcObject = null;
      this.video_.parentNode.removeChild(this.video_);
    }
  }
}

/*
 *  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';

/**
 * Displays the output stream in a video element.
 * @implements {MediaStreamSink} in pipeline.js
 */
class VideoSink { // eslint-disable-line no-unused-vars
  constructor() {
    /**
     * @private {?HTMLVideoElement} output video element
     */
    this.video_ = null;
    /** @private {string} */
    this.debugPath_ = 'debug.pipeline.sink_';
  }
  /**
   * Sets the path to this object from the debug global var.
   * @param {string} path
   */
  setDebugPath(path) {
    this.debugPath_ = path;
  }
  /** @override */
  async setMediaStream(stream) {
    console.log('[VideoSink] Setting sink stream.', stream);
    if (!this.video_) {
      this.video_ =
        /** @type {!HTMLVideoElement} */ (document.createElement('video'));
      this.video_.classList.add('video', 'sinkVideo');
      document.getElementById('outputVideoContainer').appendChild(this.video_);
      console.log(
          '[VideoSink] Added video element to page.',
          `${this.debugPath_}.video_ =`, this.video_);
    }
    this.video_.srcObject = stream;
    this.video_.play();
  }
  /** @override */
  destroy() {
    if (this.video_) {
      console.log('[VideoSink] Stopping sink video');
      this.video_.pause();
      this.video_.srcObject = null;
      this.video_.parentNode.removeChild(this.video_);
    }
  }
}

/*
 *  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';

/**
 * Decodes and plays a video.
 * @implements {MediaStreamSource} in pipeline.js
 */
class VideoSource { // eslint-disable-line no-unused-vars
  constructor() {
    /** @private {boolean} */
    this.visibility_ = false;
    /** @private {?HTMLVideoElement} video element providing the MediaStream */
    this.video_ = null;
    /**
     * @private {?Promise<!MediaStream>} a Promise that resolves to the
     *     MediaStream from captureStream. Set iff video_ is set.
     */
    this.stream_ = null;
    /** @private {string} */
    this.debugPath_ = '<unknown>';
  }
  /** @override */
  setDebugPath(path) {
    this.debugPath_ = path;
  }
  /** @override */
  setVisibility(visible) {
    this.visibility_ = visible;
    if (this.video_) {
      this.updateVideoVisibility();
    }
  }
  /** @private */
  updateVideoVisibility() {
    if (this.video_.parentNode && !this.visibility_) {
      if (!this.video_.paused) {
        // Video playback is automatically paused when the element is removed
        // from the DOM. That is not the behavior we want.
        this.video_.onpause = async () => {
          this.video_.onpause = null;
          await this.video_.play();
        };
      }
      this.video_.parentNode.removeChild(this.video_);
    } else if (!this.video_.parentNode && this.visibility_) {
      console.log(
          '[VideoSource] Adding source video element to page.',
          `${this.debugPath_}.video_ =`, this.video_);
      const outputVideoContainer =
          document.getElementById('outputVideoContainer');
      outputVideoContainer.parentNode.insertBefore(
          this.video_, outputVideoContainer);
    }
  }
  /** @override */
  async getMediaStream() {
    if (this.stream_) return this.stream_;

    console.log('[VideoSource] Loading video');

    this.video_ =
      /** @type {!HTMLVideoElement} */ (document.createElement('video'));
    this.video_.classList.add('video', 'sourceVideo');
    this.video_.controls = true;
    this.video_.loop = true;
    this.video_.muted = true;
    // All browsers that support insertable streams also support WebM/VP8.
    this.video_.src = 'video-processing.webm';
    this.video_.load();
    this.video_.play();
    this.updateVideoVisibility();
    this.stream_ = new Promise((resolve, reject) => {
      this.video_.oncanplay = () => {
        if (!resolve || !reject) return;
        console.log('[VideoSource] Obtaining video capture stream');
        if (this.video_.captureStream) {
          resolve(this.video_.captureStream());
        } else if (this.video_.mozCaptureStream) {
          resolve(this.video_.mozCaptureStream());
        } else {
          const e = new Error('Stream capture is not supported');
          console.error(e);
          reject(e);
        }
        resolve = null;
        reject = null;
      };
    });
    await this.stream_;
    console.log(
        '[VideoSource] Received source video stream.',
        `${this.debugPath_}.stream_ =`, this.stream_);
    return this.stream_;
  }
  /** @override */
  destroy() {
    if (this.video_) {
      console.log('[VideoSource] Stopping source video');
      this.video_.pause();
      if (this.video_.parentNode) {
        this.video_.parentNode.removeChild(this.video_);
      }
    }
  }
}

/*
 *  Copyright (c) 2021 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';

/**
 * Encodes and decodes frames using the WebCodec API.
 * @implements {FrameTransform} in pipeline.js
 */
class WebCodecTransform { // eslint-disable-line no-unused-vars
  constructor() {
    // Encoder and decoder are initialized in init()
    this.decoder_ = null;
    this.encoder_ = null;
    this.controller_ = null;
  }
  /** @override */
  async init() {
    console.log('[WebCodecTransform] Initializing encoder and decoder');
    this.decoder_ = new VideoDecoder({
      output: frame => this.handleDecodedFrame(frame),
      error: this.error
    });
    this.encoder_ = new VideoEncoder({
      output: frame => this.handleEncodedFrame(frame),
      error: this.error
    });
    this.encoder_.configure({codec: 'vp8', width: 640, height: 480});
    this.decoder_.configure({codec: 'vp8', width: 640, height: 480});
  }

  /** @override */
  async transform(frame, controller) {
    if (!this.encoder_) {
      frame.close();
      return;
    }
    this.controller_ = controller;
    this.encoder_.encode(frame);
  }

  /** @override */
  destroy() {}

  /* Helper functions */
  handleEncodedFrame(encodedFrame) {
    this.decoder_.decode(encodedFrame);
  }

  handleDecodedFrame(videoFrame) {
    if (!this.controller_) {
      videoFrame.close();
      return;
    }
    this.controller_.enqueue(videoFrame);
  }

  error(e) {
    console.log('[WebCodecTransform] Bad stuff happened: ' + e);
  }
}

/*
 *  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';

/**
 * Applies a warp effect using WebGL.
 * @implements {FrameTransform} in pipeline.js
 */
class WebGLTransform { // eslint-disable-line no-unused-vars
  constructor() {
    // All fields are initialized in init()
    /** @private {?OffscreenCanvas} canvas used to create the WebGL context */
    this.canvas_ = null;
    /** @private {?WebGLRenderingContext} */
    this.gl_ = null;
    /** @private {?WebGLUniformLocation} location of inSampler */
    this.sampler_ = null;
    /** @private {?WebGLProgram} */
    this.program_ = null;
    /** @private {?WebGLTexture} input texture */
    this.texture_ = null;
    /**
     * @private {boolean} If false, pass VideoFrame directly to
     * WebGLRenderingContext.texImage2D and create VideoFrame directly from
     * this.canvas_. If either of these operations fail (it's not supported in
     * Chrome <90 and broken in Chrome 90: https://crbug.com/1184128), we set
     * this field to true; in that case we create an ImageBitmap from the
     * VideoFrame and pass the ImageBitmap to texImage2D on the input side and
     * create the VideoFrame using an ImageBitmap of the canvas on the output
     * side.
     */
    this.use_image_bitmap_ = false;
    /** @private {string} */
    this.debugPath_ = 'debug.pipeline.frameTransform_';
  }
  /** @override */
  async init() {
    console.log('[WebGLTransform] Initializing WebGL.');
    this.canvas_ = new OffscreenCanvas(1, 1);
    const gl = /** @type {?WebGLRenderingContext} */ (
      this.canvas_.getContext('webgl'));
    if (!gl) {
      alert(
          'Failed to create WebGL context. Check that WebGL is supported ' +
          'by your browser and hardware.');
      return;
    }
    this.gl_ = gl;
    const vertexShader = this.loadShader_(gl.VERTEX_SHADER, `
      precision mediump float;
      attribute vec3 g_Position;
      attribute vec2 g_TexCoord;
      varying vec2 texCoord;
      void main() {
        gl_Position = vec4(g_Position, 1.0);
        texCoord = g_TexCoord;
      }`);
    const fragmentShader = this.loadShader_(gl.FRAGMENT_SHADER, `
      precision mediump float;
      varying vec2 texCoord;
      uniform sampler2D inSampler;
      void main(void) {
        float boundary = distance(texCoord, vec2(0.5)) - 0.2;
        if (boundary < 0.0) {
          gl_FragColor = texture2D(inSampler, texCoord);
        } else {
          // Rotate the position
          float angle = 2.0 * boundary;
          vec2 rotation = vec2(sin(angle), cos(angle));
          vec2 fromCenter = texCoord - vec2(0.5);
          vec2 rotatedPosition = vec2(
            fromCenter.x * rotation.y + fromCenter.y * rotation.x,
            fromCenter.y * rotation.y - fromCenter.x * rotation.x) + vec2(0.5);
          gl_FragColor = texture2D(inSampler, rotatedPosition);
        }
      }`);
    if (!vertexShader || !fragmentShader) return;
    // Create the program object
    const programObject = gl.createProgram();
    gl.attachShader(programObject, vertexShader);
    gl.attachShader(programObject, fragmentShader);
    // Link the program
    gl.linkProgram(programObject);
    // Check the link status
    const linked = gl.getProgramParameter(programObject, gl.LINK_STATUS);
    if (!linked) {
      const infoLog = gl.getProgramInfoLog(programObject);
      gl.deleteProgram(programObject);
      throw new Error(`Error linking program:\n${infoLog}`);
    }
    gl.deleteShader(vertexShader);
    gl.deleteShader(fragmentShader);
    this.sampler_ = gl.getUniformLocation(programObject, 'inSampler');
    this.program_ = programObject;
    // Bind attributes
    const vertices = [1.0, -1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0];
    // Pass-through.
    const txtcoords = [1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0];
    // Mirror horizonally.
    // const txtcoords = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0];
    this.attributeSetFloats_('g_Position', 2, vertices);
    this.attributeSetFloats_('g_TexCoord', 2, txtcoords);
    // Initialize input texture
    this.texture_ = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, this.texture_);
    const pixel = new Uint8Array([0, 0, 255, 255]); // opaque blue
    gl.texImage2D(
        gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    console.log(
        '[WebGLTransform] WebGL initialized.', `${this.debugPath_}.canvas_ =`,
        this.canvas_, `${this.debugPath_}.gl_ =`, this.gl_);
  }

  /**
   * Creates and compiles a WebGLShader from the provided source code.
   * @param {number} type either VERTEX_SHADER or FRAGMENT_SHADER
   * @param {string} shaderSrc
   * @return {!WebGLShader}
   * @private
   */
  loadShader_(type, shaderSrc) {
    const gl = this.gl_;
    const shader = gl.createShader(type);
    // Load the shader source
    gl.shaderSource(shader, shaderSrc);
    // Compile the shader
    gl.compileShader(shader);
    // Check the compile status
    if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
      const infoLog = gl.getShaderInfoLog(shader);
      gl.deleteShader(shader);
      throw new Error(`Error compiling shader:\n${infoLog}`);
    }
    return shader;
  }

  /**
   * Sets a floating point shader attribute to the values in arr.
   * @param {string} attrName the name of the shader attribute to set
   * @param {number} vsize the number of components of the shader attribute's
   *   type
   * @param {!Array<number>} arr the values to set
   * @private
   */
  attributeSetFloats_(attrName, vsize, arr) {
    const gl = this.gl_;
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.STATIC_DRAW);
    const attr = gl.getAttribLocation(this.program_, attrName);
    gl.enableVertexAttribArray(attr);
    gl.vertexAttribPointer(attr, vsize, gl.FLOAT, false, 0, 0);
  }

  /** @override */
  async transform(frame, controller) {
    const gl = this.gl_;
    if (!gl || !this.canvas_) {
      frame.close();
      return;
    }
    const width = frame.displayWidth;
    const height = frame.displayHeight;
    if (this.canvas_.width !== width || this.canvas_.height !== height) {
      this.canvas_.width = width;
      this.canvas_.height = height;
      gl.viewport(0, 0, width, height);
    }
    const timestamp = frame.timestamp;
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, this.texture_);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    if (!this.use_image_bitmap_) {
      try {
        // Supported for Chrome 90+.
        gl.texImage2D(
            gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, frame);
      } catch (e) {
        // This should only happen on Chrome <90.
        console.log(
            '[WebGLTransform] Failed to upload VideoFrame directly. Falling ' +
                'back to ImageBitmap.',
            e);
        this.use_image_bitmap_ = true;
      }
    }
    if (this.use_image_bitmap_) {
      // Supported for Chrome <92.
      const inputBitmap =
            await frame.createImageBitmap({imageOrientation: 'flipY'});
      gl.texImage2D(
          gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, inputBitmap);
      inputBitmap.close();
    }
    frame.close();
    gl.useProgram(this.program_);
    gl.uniform1i(this.sampler_, 0);
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    gl.bindTexture(gl.TEXTURE_2D, null);
    if (!this.use_image_bitmap_) {
      try {
        // alpha: 'discard' is needed in order to send frames to a PeerConnection.
        controller.enqueue(new VideoFrame(this.canvas_, {timestamp, alpha: 'discard'}));
      } catch (e) {
        // This should only happen on Chrome <91.
        console.log(
            '[WebGLTransform] Failed to create VideoFrame from ' +
                'OffscreenCanvas directly. Falling back to ImageBitmap.',
            e);
        this.use_image_bitmap_ = true;
      }
    }
    if (this.use_image_bitmap_) {
      const outputBitmap = await createImageBitmap(this.canvas_);
      const outputFrame = new VideoFrame(outputBitmap, {timestamp});
      outputBitmap.close();
      controller.enqueue(outputFrame);
    }
  }

  /** @override */
  destroy() {
    if (this.gl_) {
      console.log('[WebGLTransform] Forcing WebGL context to be lost.');
      /** @type {!WEBGL_lose_context} */ (
        this.gl_.getExtension('WEBGL_lose_context'))
          .loseContext();
    }
  }
}
