MediaRecorder 切换视频轨道
Posted
技术标签:
【中文标题】MediaRecorder 切换视频轨道【英文标题】:MediaRecorder switch video tracks 【发布时间】:2021-12-20 21:52:24 【问题描述】:我正在使用MediaRecorder API
在网络应用程序中录制视频。该应用程序可以选择在相机和屏幕之间切换。我正在使用 Canvas 来增强流记录。该逻辑涉及从相机捕获流并将其重定向到视频元素。然后在画布上渲染此视频,并将来自画布的流传递给MediaRecorder
。
我注意到的是,只要用户不切换/最小化 chrome 窗口,从屏幕切换到视频(反之亦然)就可以正常工作。画布渲染使用requestAnimationFrame
,并在选项卡失去焦点后冻结。
有什么办法可以指示chrome不要暂停requestAnimationFrame
的执行?有没有其他方法可以在不影响MediaRecorder
录制的情况下切换流?
更新: 通读文档后,播放音频或具有活动 websocket 连接的选项卡不会受到限制。这是我们目前没有做的事情。这可能是一种解决方法,但希望社区提供任何替代解决方案。 (setTimeout 或 setInterval 过于节流,因此不使用它,而且会影响渲染质量)
更新 2: 我可以使用 Worker 来解决这个问题。工作人员调用 API 并通过 postMessage 将通知发送到主线程,而不是为 requestAnimationFrame 使用主 UI 线程。 UI Thread 完成渲染后,会向 Worker 发送一条消息。还有一个增量周期计算来限制来自工作人员的大量消息。
【问题讨论】:
可以包含您的代码的一部分吗? 嗨@clota974 感谢您的评论。调用 requestAnimationFrame 的代码/逻辑与在线提供的示例非常相似。我的问题是,当 chrome 窗口失去焦点或被最小化时,我需要一种解决方法/解决方案来不限制 requestAnimationFrame。 ***.com/questions/40687010/… 虽然我不确定那里接受的黑客仍然有效......(最后不作为骗子关闭,因为可能确实有方法可以在不依赖画布的情况下切换流) 嗨@Kaiido,感谢您的链接。但正如您在回答中提到的,它是否停止从画布捕获流到媒体流? 通常不会。但实际上,浏览器可能不会在画布上绘画……所以这并不可靠。但是我可能有一个解决方案......来自 Chrome。 【参考方案1】:正在进行proposal 向 MediaRecorder API 添加.replaceTrack()
方法,但目前the specs 仍在读取中
如果在任何时候,将轨道添加到流的轨道集中或从流的轨道集中删除,UA 必须立即停止收集数据,丢弃它收集的任何数据 [...]
这就是实现的。
所以我们仍然必须依靠 hack 自己制作这个......
最好的可能是创建一个本地RTC连接,并记录接收端。
// creates a mixable stream
async function mixableStream( initial_track )
const source_stream = new MediaStream( [] );
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
pc1.onicecandidate = (evt) => pc2.addIceCandidate( evt.candidate );
pc2.onicecandidate = (evt) => pc1.addIceCandidate( evt.candidate );
const wait_for_stream = waitForEvent( pc2, 'track')
.then( evt => new MediaStream( [ evt.track ] ) );
pc1.addTrack( initial_track, source_stream );
await waitForEvent( pc1, 'negotiationneeded' );
try
await pc1.setLocalDescription( await pc1.createOffer() );
await pc2.setRemoteDescription( pc1.localDescription );
await pc2.setLocalDescription( await pc2.createAnswer() );
await pc1.setRemoteDescription( pc2.localDescription );
catch ( err )
console.error( err );
return
stream: await wait_for_stream,
async replaceTrack( new_track )
const sender = pc1.getSenders().find( ( track ) => track.kind == new_track.kind );
return sender && sender.replaceTrack( new_track ) ||
Promise.reject( "no such track" );
// remap unstable FF version
const proto = htmlMediaElement.prototype;
if( !proto.captureStream ) proto.captureStream = proto.mozCaptureStream;
waitForEvent( document.getElementById( 'starter' ), 'click' )
.then( (evt) => evt.target.parentNode.remove() )
.then( (async() =>
const urls = [
"2/22/Volcano_Lava_Sample.webm",
"/a/a4/BBH_gravitational_lensing_of_gw150914.webm"
].map( (suffix) => "https://upload.wikimedia.org/wikipedia/commons/" + suffix );
const switcher_btn = document.getElementById( 'switcher' );
const stop_btn = document.getElementById( 'stopper' );
const video_out = document.getElementById( 'out' );
let current = 0;
// see below for 'recordVid'
const video_tracks = await Promise.all( urls.map( (url, index) => getVideoTracks( url ) ) );
const mixable_stream = await mixableStream( video_tracks[ current ].track );
switcher_btn.onclick = async (evt) =>
current = +!current;
await mixable_stream.replaceTrack( video_tracks[ current ].track );
;
// final recording part below
// only for demo, so we can see what happens now
video_out.srcObject = mixable_stream.stream;
const rec = new MediaRecorder( mixable_stream.stream );
const chunks = [];
rec.ondataavailable = (evt) => chunks.push( evt.data );
rec.onerror = console.log;
rec.onstop = (evt) =>
const final_file = new Blob( chunks );
video_tracks.forEach( (track) => track.stop() );
// only for demo, since we did set its srcObject
video_out.srcObject = null;
video_out.src = URL.createObjectURL( final_file );
switcher_btn.remove();
stop_btn.remove();
const anchor = document.createElement( 'a' );
anchor.download = 'file.webm';
anchor.textContent = 'download';
anchor.href = video_out.src;
document.body.prepend( anchor );
;
stop_btn.onclick = (evt) => rec.stop();
rec.start();
))
.catch( console.error )
// some helpers below
// returns a video loaded to given url
function makeVid( url )
const vid = document.createElement('video');
vid.crossOrigin = true;
vid.loop = true;
vid.muted = true;
vid.src = url;
return vid.play()
.then( (_) => vid );
/* Records videos from given url
** @method stop() ::pauses the linked <video>
** @property track ::the video track
*/
async function getVideoTracks( url )
const player = await makeVid( url );
const track = player.captureStream().getVideoTracks()[ 0 ];
return
track,
stop() player.pause();
;
// Promisifies EventTarget.addEventListener
function waitForEvent( target, type )
return new Promise( (res) => target.addEventListener( type, res, once: true ) );
video max-height: 100vh; max-width: 100vw; vertical-align: top;
.overlay
background: #ded;
position: fixed;
z-index: 999;
height: 100vh;
width: 100vw;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
<div class="overlay">
<button id="starter">start demo</button>
</div>
<button id="switcher">switch source</button>
<button id="stopper">stop recording</button>
<video id="out" muted controls autoplay></video>
否则,您仍然可以使用画布方式,使用我为页面模糊时所做的Web Audio Timer,即使这在 Firefox 中不起作用,因为它们在内部与 rAF 挂钩以在记录器中推送新帧。 .
【讨论】:
我曾尝试使用 addXXX 和 removeXXX 方法切换音轨,但使用此方法后,MediaRecorder 会停止录制流。 @CuriousMind 有时间做一个 hack,它只适用于 Firefox,因为我不知道是什么原因(没有更多时间挖掘......) 我非常感谢您的努力。我也告诉你一个秘密。通过将 requestAnimationFrame 委托给 worker,然后在主线程中处理 web worker 消息,我得到了一些工作。它正在与 Chrome 一起使用。发现这篇文章很有用 (threejsfundamentals.org/threejs/lessons/…) 我无法使用 OffscreenCanvas,但我喜欢 webworker 处理的东西。 OffscreenCanvas 没有多大帮助,MediaRecorder 在 Workers 中不可用。请注意,虽然 WebWorker 计时器现在可能正在工作,但实际上并没有强制浏览器保持主线程处于活动状态,或者唤醒它来处理消息。他们可以很好地存储消息并在唤醒时处理这些消息。这就是我猜想在不久的将来Page lifecycle API 会在这里发生什么。虽然他们必须无缝处理来自 AudioAPI 的事件,以避免难看的点击和其他问题。 谢谢@Kaiido,我正在浏览文档。你说的是对的。 MediaRecorder 在 web worker 中不起作用,并且 OffscreenCanvas 对我不起作用(因为我允许将流从相机切换到屏幕,反之亦然,视频控件不能作为 Transferable 对象转移到 worker)。虽然此时我可以使用工作人员并且正在进行消息交换,但它可能会失败。【参考方案2】:我遇到了同样的问题,并试图在没有太多复杂性(如 Canvas 或 SourceBuffer)的情况下解决它。
我使用 PeerConnection 为同一页面建立连接。建立连接后,您可以通过 peerconnection.addTrack 使用 rtpSender 并从那里轻松切换。
我刚刚制作了一个库和一个演示,您可以找到: https://github.com/meething/StreamSwitcher/
【讨论】:
我想试试这个。如果屏幕上没有视频对象怎么办?我正在使用视频会议代码来完成所有这些工作,但有一个单独的本地媒体流来保存 HQ 质量。该代码看起来像是在视频元素上使用了 srcObject。我可以更新媒体记录器对象吗? 嗨@John,这个问题不是很清楚,您能否详细说明您的问题以更清楚地说明您想要实现的目标? 在遇到相同问题后,我在反应项目中使用了@QVDev 解决方案。如果有人想使用代码,这里有一个代码框:codesandbox.io/s/mediarecorderexample-msxm1以上是关于MediaRecorder 切换视频轨道的主要内容,如果未能解决你的问题,请参考以下文章
在 MediaSource HTML5 中播放 MediaRecorder 块——视频冻结
如何通过 CSS 为 HTML5 视频中的文本轨道设置样式?
Android 开发 MediaRecorder使用Camera1配合录制视频
Error处理: android.media.MediaRecorder.start(Native Method) 报错:start failed: -19