一起录制视频和屏幕并用 Javascript 覆盖

2024-01-12

我想在网络摄像头旁边记录用户的屏幕并将结果显示为叠加层,如下所示:

我假设在录制时我可以在两个单独的视频元素中显示多个流并用 CSS 覆盖它们。

然而,我该如何save结果是两个视频的叠加?


这可以在纯 JS 中实现,如下所示 -

  1. 通过 getUserMedia() 获取网络摄像头流
  2. 通过 getDisplayMedia() 获取屏幕共享流
  3. 使用一些数学和画布操作合并两个流
  4. 使用canvas.captureStream()生成复合视频流。
  5. 使用 AudioContext 合并音频剪辑(如果同时使用麦克风和系统音频,则尤其需要)。
  6. 使用 MediaStream 构造函数创建一个新流,使用 - 来自新流的视频 + 来自 audioContext 目标节点的音频,如下 -

new MediaStream([...newStream.getVideoTracks(), ...audioDestination.stream.getTracks()]);

  1. 根据需要使用新生成的 MediaStream(即替换 RTCPeerConnection 等)。
  2. 在此示例中 - MediaRecorder API 用于录制生成的复合/画中画视频。
  3. 单击“记录结果流”按钮后,记录开始。单击“停止录制并下载结果流”按钮即可下载最终结果。

PS:该代码片段将无法获取相机/屏幕共享+请使用此代码笔链接 https://codepen.io/BRAiNCHiLD95/pen/BaJZOyR看看它的实际效果。

let localCamStream,
  localScreenStream,
  localOverlayStream,
  rafId,
  cam,
  screen,
  mediaRecorder,
  audioContext,
  audioDestination;
let mediaWrapperDiv = document.getElementById("mediaWrapper");
let startWebcamBtn = document.getElementById("startWebcam");
let startScreenShareBtn = document.getElementById("startScreenShare");
let mergeStreamsBtn = document.getElementById("mergeStreams");
let startRecordingBtn = document.getElementById("startRecording");
let stopRecordingBtn = document.getElementById("stopRecording");
let stopAllStreamsBtn = document.getElementById("stopAllStreams");
let canvasElement = document.createElement("canvas");
let canvasCtx = canvasElement.getContext("2d");
let encoderOptions = {
  mimeType: "video/webm; codecs=vp9"
};
let recordedChunks = [];
let audioTracks = [];

/**
 * Internal Polyfill to simulate
 * window.requestAnimationFrame
 * since the browser will kill canvas
 * drawing when tab is inactive
 */
const requestVideoFrame = function(callback) {
  return window.setTimeout(function() {
    callback(Date.now());
  }, 1000 / 60); // 60 fps - just like requestAnimationFrame
};

/**
 * Internal polyfill to simulate
 * window.cancelAnimationFrame
 */
const cancelVideoFrame = function(id) {
  clearTimeout(id);
};

async function startWebcamFn() {
  localCamStream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: {
      deviceId: {
        exact: "communications"
      }
    }
  });
  if (localCamStream) {
    cam = await attachToDOM("justWebcam", localCamStream);
  }
}

async function startScreenShareFn() {
  localScreenStream = await navigator.mediaDevices.getDisplayMedia({
    video: true,
    audio: true
  });
  if (localScreenStream) {
    screen = await attachToDOM("justScreenShare", localScreenStream);
  }
}

async function stopAllStreamsFn() {
  [
    ...(localCamStream ? localCamStream.getTracks() : []),
    ...(localScreenStream ? localScreenStream.getTracks() : []),
    ...(localOverlayStream ? localOverlayStream.getTracks() : [])
  ].map((track) => track.stop());
  localCamStream = null;
  localScreenStream = null;
  localOverlayStream = null;
  cancelVideoFrame(rafId);
  mediaWrapperDiv.innerHTML = "";
  document.getElementById("recordingState").innerHTML = "";
}

async function makeComposite() {
  if (cam && screen) {
    canvasCtx.save();
    canvasElement.setAttribute("width", `${screen.videoWidth}px`);
    canvasElement.setAttribute("height", `${screen.videoHeight}px`);
    canvasCtx.clearRect(0, 0, screen.videoWidth, screen.videoHeight);
    canvasCtx.drawImage(screen, 0, 0, screen.videoWidth, screen.videoHeight);
    canvasCtx.drawImage(
      cam,
      0,
      Math.floor(screen.videoHeight - screen.videoHeight / 4),
      Math.floor(screen.videoWidth / 4),
      Math.floor(screen.videoHeight / 4)
    ); // this is just a rough calculation to offset the webcam stream to bottom left
    let imageData = canvasCtx.getImageData(
      0,
      0,
      screen.videoWidth,
      screen.videoHeight
    ); // this makes it work
    canvasCtx.putImageData(imageData, 0, 0); // properly on safari/webkit browsers too
    canvasCtx.restore();
    rafId = requestVideoFrame(makeComposite);
  }
}

async function mergeStreamsFn() {
  document.getElementById("mutingStreams").style.display = "block";
  await makeComposite();
  audioContext = new AudioContext();
  audioDestination = audioContext.createMediaStreamDestination();
  let fullVideoStream = canvasElement.captureStream();
  let existingAudioStreams = [
    ...(localCamStream ? localCamStream.getAudioTracks() : []),
    ...(localScreenStream ? localScreenStream.getAudioTracks() : [])
  ];
  audioTracks.push(
    audioContext.createMediaStreamSource(
      new MediaStream([existingAudioStreams[0]])
    )
  );
  if (existingAudioStreams.length > 1) {
    audioTracks.push(
      audioContext.createMediaStreamSource(
        new MediaStream([existingAudioStreams[1]])
      )
    );
  }
  audioTracks.map((track) => track.connect(audioDestination));
  console.log(audioDestination.stream);
  localOverlayStream = new MediaStream([...fullVideoStream.getVideoTracks()]);
  let fullOverlayStream = new MediaStream([
    ...fullVideoStream.getVideoTracks(),
    ...audioDestination.stream.getTracks()
  ]);
  console.log(localOverlayStream, existingAudioStreams);
  if (localOverlayStream) {
    overlay = await attachToDOM("pipOverlayStream", localOverlayStream);
    mediaRecorder = new MediaRecorder(fullOverlayStream, encoderOptions);
    mediaRecorder.ondataavailable = handleDataAvailable;
    overlay.volume = 0;
    cam.volume = 0;
    screen.volume = 0;
    cam.style.display = "none";
    // localCamStream.getAudioTracks().map(track => { track.enabled = false });
    screen.style.display = "none";
    // localScreenStream.getAudioTracks().map(track => { track.enabled = false });
  }
}

async function startRecordingFn() {
  mediaRecorder.start();
  console.log(mediaRecorder.state);
  console.log("recorder started");
  document.getElementById("pipOverlayStream").style.border = "10px solid red";
  document.getElementById(
    "recordingState"
  ).innerHTML = `${mediaRecorder.state}...`;
}

async function attachToDOM(id, stream) {
  let videoElem = document.createElement("video");
  videoElem.id = id;
  videoElem.width = 640;
  videoElem.height = 360;
  videoElem.autoplay = true;
  videoElem.setAttribute("playsinline", true);
  videoElem.srcObject = new MediaStream(stream.getTracks());
  mediaWrapperDiv.appendChild(videoElem);
  return videoElem;
}

function handleDataAvailable(event) {
  console.log("data-available");
  if (event.data.size > 0) {
    recordedChunks.push(event.data);
    console.log(recordedChunks);
    download();
  } else {}
}

function download() {
  var blob = new Blob(recordedChunks, {
    type: "video/webm"
  });
  var url = URL.createObjectURL(blob);
  var a = document.createElement("a");
  document.body.appendChild(a);
  a.style = "display: none";
  a.href = url;
  a.download = "result.webm";
  a.click();
  window.URL.revokeObjectURL(url);
}

function stopRecordingFn() {
  mediaRecorder.stop();
  document.getElementById(
    "recordingState"
  ).innerHTML = `${mediaRecorder.state}...`;
}

startWebcamBtn.addEventListener("click", startWebcamFn);
startScreenShareBtn.addEventListener("click", startScreenShareFn);
mergeStreamsBtn.addEventListener("click", mergeStreamsFn);
stopAllStreamsBtn.addEventListener("click", stopAllStreamsFn);
startRecordingBtn.addEventListener("click", startRecordingFn);
stopRecordingBtn.addEventListener("click", stopRecordingFn);
div#mediaWrapper,
div#buttonWrapper {
  display: flex;
  flex: 1 1 100%;
  flex-basis: row nowrap;
}

div#mediaWrapper video {
  border: 1px solid black;
  margin: 1px;
  max-width: 33%;
  height: auto;
}

div#mediaWrapper video#pipOverlayStream {
  max-width: 100% !important;
}

div#buttonWrapper button {
  border-radius: 0.25rem;
  color: #ffffff;
  display: inline-block;
  font-size: 1rem;
  font-weight: bold;
  line-height: 1.6;
  padding: 0.375rem 0.75rem;
  text-align: center;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  vertical-align: middle;
  margin: 5px;
  cursor: pointer;
}

div#buttonWrapper button#startWebcam {
  background-color: #007bff;
  border: 1px solid #007bff;
}

div#buttonWrapper button#startScreenShare {
  background-color: #17a2b8;
  border: 1px solid #17a2b8;
}

div#buttonWrapper button#mergeStreams {
  background-color: #28a745;
  border: 1px solid #28a745;
}

div#buttonWrapper button#startRecording {
  background-color: #17a2b8;
  border: 1px solid #17a2b8;
}

div#buttonWrapper button#stopRecording {
  background-color: #000000;
  border: 1px solid #000000;
}

div#buttonWrapper button#stopAllStreams {
  background-color: #dc3545;
  border: 1px solid #dc3545;
}
<main>
  <p>This demo is a proof-of-concept solution for this <a href="https://stackoverflow.com/questions/71557879" target="_blank" rel="noopener noreferrer">StackOverflow question</a> and <a href="https://stackoverflow.com/questions/37404860" target="_blank"
      rel="noopener noreferrer">also this one</a> - as long as you make the required changes<br>i.e. <b>mimeType: "video/mp4; codecs=h264"</b> instead of <b>mimeType: "video/webm; codecs=vp9"</b><br>AND<br><b>type: "video/mp4"</b> instead of <b>type: "video/webm"</b><br>AND<br><b>result.mp4</b>    instead of <b>result.webm</b></p>
  <h2>Click on "Start Webcam" to get started. </h2>
  <h3>
    Core Idea:<br>
    <ol>
      <li>Fetch Webcam Stream via getUserMedia()</li>
      <li>Fetch Screen Share Stream via getDisplayMedia()</li>
      <li>Merge Both Stream using some math & canvas operations</li>
      <li>Use canvas.captureStream() to generate the composite video stream.</li>
      <li>Use AudioContext to merge audio clips (especially needed if using both microphone & system audio).</li>
      <li>Use MediaStream constructor to create a new stream using - the video from the new stream + audio from audioContext Destination Node as follows -<br><br>
        <code>new MediaStream([...newStream.getVideoTracks(), ...audioDestination.stream.getTracks()]);</code>
      </li><br>
      <li>Use newly generated MediaStream as required (i.e. replace in RTCPeerConnection, etc.).</li>
      <li>In this example - MediaRecorder API is used to record the resulting composite/picture-in-picture video. Recording begins when the "Record Resulting Stream" button is clicked. The final result can be downloaded upon clicking the "Stop Recording and
        Download Resulting Stream" button</li>
    </ol>
  </h3>
  <div id="mediaWrapper"></div>
  <div id="buttonWrapper">
    <button id="startWebcam" title="Start Webcam">Start Webcam</button>
    <button id="startScreenShare" title="Start Screen Share">Start Screen Share</button>
    <button id="mergeStreams" title="Merge Streams">Merge Streams</button>
    <button id="startRecording" title="Record Resulting Stream">Record Resulting Stream</button>
    <button id="stopRecording" title="Stop Recording and Download Resulting Stream">Stop Recording and Download Resulting Stream</button>
    <button id="stopAllStreams" title="Stop All Streams">Stop All Streams</button>
  </div>

  <div id="helpText">
    <h1 id="recordingState"></h1><br>
    <h2 id="mutingStreams">
      Note: In a WebRTC setting, you wouldn't be hearing your own voice or the screen share audio via the "video" tag. The same has been simulated here by ensuring that all video tags have a "volume = 0". Removing this will create a loopback hell which you
      do not want.<br><br> Another way to avoid this issue is to ensure that the video tags are created with ONLY video stream tracks using <em style="color: blue;">new MediaStream([ source.getVideoTracks() ])</em> during the srcObject assignment.
    </h2>
    <h1>
      Remember to send the correct stream (with both audio and video) to the rest of the peers though.
    </h1>
  </div>
</main>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一起录制视频和屏幕并用 Javascript 覆盖 的相关文章

随机推荐

  • 动态创建变量是个好主意吗?

    最近发现了如何通过这个方法在python中动态创建变量 vars my variable Some Value 从而创建变量my variable 我的问题是 这是个好主意吗 或者我应该总是提前声明变量 我认为如果可能的话最好使用字典 va
  • 视频流基础设施

    我们想建立一个实时视频聊天网站 并正在寻找基本的架构建议和 或针对要使用的特定框架的推荐 以下是该网站的基本功能 大多数流媒体将由一个人通过网络摄像头等进行现场直播 通常由 1 10 人观看 但最多可能有 100 多名观众 音频和视频不必是
  • Spark 对分隔数据进行排序

    我是 Spark 新手 您能告诉我以下代码有什么问题吗 val rawData USA E001 ABC DE 19850607 IT 100 UK E005 CHAN CL 19870512 OP 200 USA E003 XYZ AB
  • 获取上周唯一的最新数据并对某些列求和

    仅获取上周的最新数据并对某些列求和 我用数据 实际结果和预期做了一个例子 http rextester com HMB12638 http rextester com HMB12638 Taking first as example use
  • Select2限制标签数量

    有没有办法限制用户可以使用 Select2 添加到输入字段的标签数量 I have tags select2 containerCssClass supplierTags placeholder Usual suppliers minimu
  • 如何使用 Bash 获取屏幕会话中的命令历史记录?

    如果我开始一个屏幕会话screen dmS name 我如何使用脚本访问该屏幕会话的命令历史记录 Using the the last executed command appears even in screen 我在系统上使用默认的 b
  • 推理 Big O 的正式定义时遇到一些困难

    我的教授最近回顾了 Big O 的正式定义 老实说 即使他向几个不同的学生解释了它 我们似乎仍然没有理解它的核心 理解上的问题主要出现在我们经历的以下例子中 到目前为止 我的推理如下 当您将函数的最高项乘以常数时 您会得到一个新函数 该新函
  • 循环变量覆盖全局变量

    在Python中 为什么循环变量会覆盖已经定义的全局变量 将循环变量放入模块的全局命名空间而不是仅用于循环的新本地命名空间似乎违反直觉 这是一个显示我正在谈论的内容的示例 c 3 14 print before loop c format
  • 如何从 Blazor 中的另一个组件重新渲染组件?

    我有一个电子商务Blazor服务器项目 我想重新渲染推车组件将商品添加到购物车后产品组件 我尝试将 Cart 组件继承到 Product 组件 并运行 Cart 组件的公共方法来重新渲染其组件 添加到购物车方法产品组件 产品 剃须刀 pub
  • 如何对一周中每一天的每个小时进行分组和绘图

    我需要帮助弄清楚如何绘制子图 以便与显示的数据框进行轻松比较 Date A B C 2017 03 22 15 00 00 obj1 value a other 1 2017 03 22 14 00 00 obj2 value ns oth
  • Jenkins 管道:kubectl:未找到

    我有以下 Jenkinsfile node stage Apply Kubernetes files withKubeConfig credentialsId jenkins deployer serverUrl https 192 168
  • 当 AngularJS 控制器加载时运行一次

    我有一些事情只需要在控制器加载时完成一次 最好的方法是什么 我读过一些有关 运行块 的内容 但我不太明白它是如何工作的 一些伪代码 when app resolove some stuff load a view controllerA C
  • 如何使用 SnakeYaml 加载自定义对象列表

    我一直在尝试将以下 yaml 反序列化为List
  • Jenkins 蓝海:Maven 看不到 Java

    即使路径存在 我也收到错误 var jenkins home tools hudson model JDK jdk8 bin java 未找到 edi debatcher master LNI22Y2C5V3VECCBCFPVB3ZUWJJ
  • 如何检查双精度数是否可以放入浮点数而不转换为无穷大

    是否有一种标准方法来检查 64 位浮点数是否可以转换为 32 位浮点数而不转换为 Infinity 我知道事后可以检查 Inf Convert ToSingle 不会这样做 为了测试是否double value d将转换为float在不产生
  • 如何强制 PhpMailer 5.2 使用 TLS 1.2

    最近我使用的第三方电子邮件服务提供商发生了变化 他们禁用了对 TLS 1 0 和 TLS 1 1 的支持 我为仍然使用 php 5 3 和 phpmailer 5 2 的古老系统提供支持 我的测试表明 TLS 1 2 已启用 但是 禁用 T
  • 使用带有自定义键的 HashMap

    快速问题 如果我想使用HashMap以自定义类为键 must我覆盖hashCode功能 如果我不重写该函数 它将如何工作 如果您不覆盖 hashCode AND equals 您将获得默认行为 即每个对象都是不同的 无论其内容如何
  • WF4 InstancePersistenceCommand 中断

    我有一个 Windows 服务 正在运行工作流程 工作流程是从数据库加载的 XAML 用户可以使用重新托管的设计器定义自己的工作流程 它配置有一个 SQLWorkflowInstanceStore 实例 以便在空闲时保留工作流 它基本上源自
  • 有什么方法可以判断任意 .docx 文件是否采用 Strict Office Open XML 格式与过渡格式? (ECMA-376)

    我在网上搜索过 没有找到任何程序或工具可以区分那些编码为严格 ECMA 376 和非严格 ECMA 376 的 docx 文件 对于 xlsx 文件也是如此 大多数讨论都集中在给定应用程序支持哪些格式 例如LibreOffice 但不知道如
  • 一起录制视频和屏幕并用 Javascript 覆盖

    我想在网络摄像头旁边记录用户的屏幕并将结果显示为叠加层 如下所示 我假设在录制时我可以在两个单独的视频元素中显示多个流并用 CSS 覆盖它们 然而 我该如何save结果是两个视频的叠加 这可以在纯 JS 中实现 如下所示 通过 getUse