媒体录像机切换视频轨道



我正在使用MediaRecorder API在Web应用程序中录制视频。该应用程序可以选择在相机和屏幕之间切换。我正在使用 Canvas 来增强流录制。该逻辑涉及从相机捕获流并将其重定向到视频元素。然后将此视频呈现在画布上,并将画布中的流传递给MediaRecorder。 我注意到的是,只要用户不切换/最小化 chrome 窗口,从屏幕切换到视频(反之亦然(就可以正常工作。画布呈现使用requestAnimationFrame,并在选项卡失去焦点后冻结。

有没有办法指示铬不要暂停requestAnimationFrame的执行?有没有其他方法可以在不影响MediaRecorder录制的情况下切换流?

更新:通读文档后,播放音频或具有活动 websocket 连接的选项卡不会受到限制。这是我们目前没有做的事情。这可能是一种解决方法,但希望社区有任何替代解决方案。(setTimeout 或 setInterval 限制太多,因此不使用它,而且会影响渲染质量(

更新2:我可以使用Worker解决此问题。工作线程不是将主 UI 线程用于请求动画帧,而是调用 API,并通过 postMessage 将通知发送到主线程。UI 线程完成渲染后,会将消息发送回工作线程。还有一个增量周期计算来限制来自工作人员的压倒性消息。

有一个正在进行的提案,将.replaceTrack()方法添加到MediaRecorder API,但目前,规范仍然读取

如果在任何时候,将曲目添加到流的轨道集或从中删除,UA 必须立即停止收集数据,丢弃它收集的任何数据 [...]

这就是实施的内容。


所以我们仍然必须依靠黑客来自己制作这个......

最好的方法可能是创建一个本地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>


否则,您仍然可以使用画布方式,使用我为页面模糊时制作的网络音频计时器,即使这在 Firefox 中不起作用,因为它们确实在内部挂钩到 rAF 以在录像机中推送新帧......

我遇到了同样的问题,并试图在没有太多复杂性(如 Canvas 或 SourceBuffer(的情况下弄清楚它。

我使用同一页面的对等连接来建立连接。建立连接后,您可以通过peerconnection.addTrack使用rtpSender,从那里您可以轻松切换。

我刚刚制作了一个库和一个演示,你可以找到: https://github.com/meething/StreamSwitcher/

最新更新