diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..7b57cb790
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,65 @@
+sudo: false
+language: node_js
+dist: trusty
+node_js:
+- "7"
+
+env:
+ - CXX=g++-4.8
+matrix:
+ include:
+ - os: linux
+ sudo: false
+ env: BROWSER=chrome BVER=stable
+ - os: linux
+ sudo: false
+ env: BROWSER=chrome BVER=beta
+ - os: linux
+ sudo: false
+ env: BROWSER=chrome BVER=unstable
+ - os: linux
+ sudo: false
+ env: BROWSER=firefox BVER=stable
+ - os: linux
+ sudo: false
+ env: BROWSER=firefox BVER=beta
+ - os: linux
+ sudo: false
+ env: BROWSER=firefox BVER=unstable
+ - os: osx
+ sudo: required
+ osx_image: xcode9.4
+ env: BROWSER=safari BVER=stable
+ - os: osx
+ sudo: required
+ osx_image: xcode11.2
+ env: BROWSER=safari BVER=unstable
+
+ fast_finish: true
+
+ allow_failures:
+ - os: linux
+ sudo: false
+ env: BROWSER=chrome BVER=unstable
+ - os: linux
+ sudo: false
+ env: BROWSER=firefox BVER=unstable
+
+before_script:
+ - ./node_modules/travis-multirunner/setup.sh
+ - export DISPLAY=:99.0
+ - if [ -f /etc/init.d/xvfb ]; then sh -e /etc/init.d/xvfb start; fi
+
+after_failure:
+ - for file in *.log; do echo $file; echo "======================"; cat $file; done || true
+
+notifications:
+ email:
+ -
+
+addons:
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ packages:
+ - g++-4.8
diff --git a/src/content/insertable-streams/video-recording/css/main.css b/src/content/insertable-streams/video-recording/css/main.css
new file mode 100644
index 000000000..6921cdb64
--- /dev/null
+++ b/src/content/insertable-streams/video-recording/css/main.css
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2015 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.
+ */
+
+button {
+ margin: 20px 10px 0 0;
+ min-width: 100px;
+}
+
+div#buttons {
+ margin: 0 0 20px 0;
+}
+
+div#status {
+ height: 2em;
+ margin: 1em 0 0 0;
+}
+
+video {
+ --width: 30%;
+ width: var(--width);
+ height: calc(var(--width) * 0.75);
+}
diff --git a/src/content/insertable-streams/video-recording/index.html b/src/content/insertable-streams/video-recording/index.html
new file mode 100644
index 000000000..2a030b62d
--- /dev/null
+++ b/src/content/insertable-streams/video-recording/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Insertable Streams - Mirror in a worker vs Mirror in main thread
+
+
+
+
+
+
+
+
+
+
+
+
+
+
This sample shows how to perform mirroring of a video stream using the experimental
+ mediacapture-transform API
+ in a Worker.
+
+ It also provides comparison between mirroring in the main thread using canvas and mirroring in a worker.
+
+
+
+
Original video
+
+
+
+
+
+
+
+
Mirrored With Canvas
+
+
+
+
+
+
+
+
Mirrored in a worker
+
+
+
+
+
+
+
+ Start
+ Stop
+ Slow Down Main Thread
+
+
+
+ Note : This sample is using an experimental API that has not yet been standardized. As
+ of 2022-11-21, this API is available in the latest version of Chrome based browsers.
+
+
View source on GitHub
+
+
+
+
+
+
+
+
+
diff --git a/src/content/insertable-streams/video-recording/js/main.js b/src/content/insertable-streams/video-recording/js/main.js
new file mode 100644
index 000000000..3f40fa535
--- /dev/null
+++ b/src/content/insertable-streams/video-recording/js/main.js
@@ -0,0 +1,154 @@
+/*
+ * 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';
+
+/* 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.');
+}
+
+const startButton = document.getElementById('startButton');
+const slowDownButton = document.getElementById('slowDownButton');
+const stopButton = document.getElementById('stopButton');
+const originalVideo = document.getElementById('originalVideo');
+const recordedOriginalVideo = document.getElementById('recordedOriginalVideo');
+const mirroredWithCanvasVideo = document.getElementById('mirroredWithCanvasVideo');
+const recordedMirroredWithCanvasVideo = document.getElementById('recordedMirroredWithCanvasVideo');
+const mirroredInWebWorkerVideo = document.getElementById('mirroredInWebWorkerVideo');
+const recordedMirroredInWorkerVideo = document.getElementById('recordedMirroredInWorkerVideo');
+
+const worker = new Worker('./js/worker.js', { name: 'Crop worker' });
+
+
+class VideoRecorder {
+ constructor(stream, outputVideoElement) {
+ this.videoElement = outputVideoElement;
+ this.mediaRecorder = new MediaRecorder(stream);
+
+ this.mediaRecorder.ondataavailable = (event) => {
+ if (event.data.size > 0) {
+ this.recordedBlob.push(event.data);
+ }
+ };
+
+ this.mediaRecorder.onerror = (e) => {
+ throw e;
+ };
+
+ this.recordedBlob = [];
+ }
+
+ start() {
+ this.mediaRecorder.start(1000);
+ }
+
+ stop() {
+ this.mediaRecorder.stop();
+ console.log('stopped');
+ const blob = new Blob(this.recordedBlob, { type: 'video/webm' });
+ const url = URL.createObjectURL(blob);
+ this.videoElement.src = url;
+ }
+}
+
+let recorders = [];
+
+startButton.addEventListener('click', async () => {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 1280, height: 720 } });
+ originalVideo.srcObject = stream;
+
+ const [track] = stream.getTracks();
+ const processor = new MediaStreamTrackProcessor({ track });
+ const { readable } = processor;
+
+ const generator = new MediaStreamTrackGenerator({ kind: 'video' });
+ const { writable } = generator;
+
+ const mediaStream = new MediaStream([generator]);
+ mirroredInWebWorkerVideo.srcObject = mediaStream;
+
+ const mirroredWithCanvasVideoStream = createMirroredCanvasStream(stream);
+ mirroredWithCanvasVideo.srcObject = mirroredWithCanvasVideoStream;
+
+ recorders.push(new VideoRecorder(stream, recordedOriginalVideo));
+ recorders.push(new VideoRecorder(mediaStream, recordedMirroredInWorkerVideo));
+ recorders.push(new VideoRecorder(mirroredWithCanvasVideoStream, recordedMirroredWithCanvasVideo));
+
+
+ recorders.forEach(recorder => recorder.start());
+
+ worker.postMessage({
+ operation: 'mirror',
+ readable,
+ writable,
+ }, [readable, writable]);
+});
+
+stopButton.addEventListener('click', () => {
+ recorders.forEach(recorder => recorder.stop());
+ recorders = [];
+});
+
+slowDownButton.addEventListener('click', () => {
+ console.time('slowDownButton');
+ let str = '';
+ for (let i = 0; i < 100000; i++) {
+ str += i.toString();
+ if (str[str.length - 1] === '0') {
+ str += '1';
+ }
+ }
+ console.timeEnd('slowDownButton');
+});
+
+
+function createMirroredCanvasStream(stream) {
+ const videoElement = document.createElement('video');
+ videoElement.playsInline = true;
+ videoElement.autoplay = true; // required in order for to successfully capture
+ videoElement.muted = true;
+ videoElement.srcObject = stream;
+
+ const videoTrack = stream.getVideoTracks()[0];
+ const { width, height } = videoTrack.getSettings();
+
+ const canvasElm = document.createElement('canvas');
+ canvasElm.width = width;
+ canvasElm.height = height;
+
+ const ctx = canvasElm.getContext('2d');
+
+ ctx.translate(canvasElm.width, 0);
+ ctx.scale(-1, 1);
+
+ function drawCanvas() {
+ ctx.drawImage(videoElement, 0, 0, canvasElm.width, canvasElm.height);
+ requestAnimationFrame(drawCanvas);
+ }
+ // our stepping criteria to recursively draw on canvas from frame
+ requestAnimationFrame(drawCanvas);
+
+ // testing this, we realized that Chrome makes the video super laggy if the this._preCanvasVideoElm
+ // is not in the DOM, and visible. We tried turning the opacity to 0, positioning the
+ // video offscreen, etc. But the only thing that makes the performance good is making
+ // it actually visible. So we make a 1px X 1px video in the top corner of the screen.
+ videoElement.style.width = '1px';
+ videoElement.style.height = '1px';
+ videoElement.style.position = 'absolute';
+ videoElement.style.zIndex = '9999999999999';
+ document.body.appendChild(videoElement);
+ videoElement.play();
+ const canvasStream = canvasElm.captureStream(30);
+
+ return canvasStream;
+}
diff --git a/src/content/insertable-streams/video-recording/js/worker.js b/src/content/insertable-streams/video-recording/js/worker.js
new file mode 100644
index 000000000..5e300cf4f
--- /dev/null
+++ b/src/content/insertable-streams/video-recording/js/worker.js
@@ -0,0 +1,44 @@
+/*
+ * 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 offscreenCanvas = new OffscreenCanvas(256, 256);
+const ctx = offscreenCanvas.getContext('2d');
+
+
+function transform(frame, controller) {
+ if (offscreenCanvas.width !== frame.displayWidth) {
+ offscreenCanvas.width = frame.displayWidth;
+ offscreenCanvas.height = frame.displayHeight;
+ ctx.translate(1280, 0);
+ ctx.scale(-1, 1);
+ }
+
+ // Draw frame to offscreen canvas with flipped x-axis.
+ ctx.drawImage(frame, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
+
+ const newFrame = new VideoFrame(offscreenCanvas, {
+ timestamp: frame.timestamp,
+ duration: frame.duration,
+ });
+ controller.enqueue(newFrame);
+ frame.close();
+}
+
+onmessage = async (event) => {
+ const {operation} = event.data;
+ if (operation === 'mirror') {
+ const {readable, writable} = event.data;
+ readable
+ .pipeThrough(new TransformStream({transform}))
+ .pipeTo(writable);
+ } else {
+ console.error('Unknown operation', operation);
+ }
+};