Есть предложение о добавлении метода .replaceTrack()
в MediaRecorder API, но в настоящее время спецификации все еще читаются
Если в любой момент дорожка добавляется или удаляется из набора дорожек потока, UA ДОЛЖЕН немедленно прекратить сбор данных, отбросить все собранные данные [...]
И вот что
Таким образом, мы все еще должны полагаться на уродливые хаки , чтобы сделать это сами ...
Вот один, , который кажется правильно работать только в Firefox, потому что я до сих пор не знаю, по каким причинам , используя MediaSource в качестве микшера.
Это работает следующим образом:
- захватывайте ваши видео
- записывать их все с помощью MediaRecorder для каждого видео
- перехватывать
dataavailable
этих MediaRecorder и подавать MediaSource с их порциями - захватывать поток видеоэлемента который воспроизводит этот MediaSource
- запись этого смешанного потока
* 103 7 * Однако вся эта установка добавляет
значительную задержку (не удивляйтесь, если вам придется подождать несколько секунд, прежде чем станет видно переключение источников), и это безумно тяжело для процессора ...
Помните, что ниже демо будет работать правильно, если страница размыта только в Firefox
{ // 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' );
const type = 'video/webm; codecs="vp8"';
if( !MediaSource.isTypeSupported( type ) ) {
throw new Error( 'Not Supported' );
}
let stopped = false;
let current = 0;
switcher_btn.onclick = (evt) => { current = +!current; };
console.log( 'loading videos, please wait' );
// see below for 'recordVid'
const recorders = await Promise.all( urls.map( (url, index) => recordVid( url, type ) ) );
const source = new MediaSource();
// create an offscreen video so it doesn't get paused when hidden
const mixed_vid = document.createElement( 'video' );
mixed_vid.autoplay = true;
mixed_vid.muted = true;
mixed_vid.src = URL.createObjectURL( source );
await waitForEvent( source, 'sourceopen' );
const buffer = source.addSourceBuffer( type );
buffer.mode = "sequence";
// init our requestData loop
appendBuffer();
mixed_vid.play();
await waitForEvent( mixed_vid, 'playing' );
console.clear();
// final recording part below
const mixed_stream = mixed_vid.captureStream();
// only for demo, so we can see what happens now
video_out.srcObject = mixed_stream;
const rec = new MediaRecorder( mixed_stream );
const chunks = [];
rec.ondataavailable = (evt) => chunks.push( evt.data );
rec.onstop = (evt) => {
stopped = true;
const final_file = new Blob( chunks );
recorders.forEach( (rec) => rec.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();
};
stop_btn.onclick = (evt) => rec.stop();
rec.start();
// requestData loop
async function appendBuffer() {
if( stopped ) { return; }
const chunks = await Promise.all( recorders.map( rec => rec.requestData() ) );
const chunk = chunks[ current ];
// first iteration is generally empty
if( !chunk.byteLength ) { setTimeout( appendBuffer, 100 ); return; }
buffer.appendBuffer( chunk );
await waitForEvent( buffer, 'update' );
appendBuffer();
};
}))
.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
** returns an object which exposes two method
** 'requestData()' returns a Promise resolved by the latest available chunk of data
** 'stop()' stops the video element and the recorder
*/
async function recordVid( url, type ) {
const player = await makeVid( url );
const stream = videoStream( player.captureStream() );
// const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const recorder = new MediaRecorder( stream, { mimeType: type } );
const chunks = [];
recorder.start( );
return {
requestData() {
recorder.requestData();
const data_prom = waitForEvent( recorder, "dataavailable" )
.then( (evt) => evt.data.arrayBuffer() );
return data_prom;
},
stop() { recorder.stop(); player.pause(); }
};
}
// removes the audio tracks from a MediaStream
function videoStream( mixed ) {
return new MediaStream( mixed.getVideoTracks() );
}
// 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>
Другой такой взлом - создание локального соединения RT C и запись принимающей стороны.
Хотя хотя на бумаге это должно было сработать, мой Firefox будет просто странно смешивать оба потока в чем-то, что я бы посоветовал избегать читателям epilepti c, а рекордеры Chrome производят однокадровое видео, возможно потому, что размер видео изменить ...
Итак, в данный момент это, похоже, нигде не работает , но здесь дело в том, что браузеры исправляют свои ошибки перед реализацией MediaRecorder.replaceTrack
.
{ // 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
// 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 (e) {
console.error(e);
}
return {
stream: await wait_for_stream,
async replaceTrack( new_track ) {
const sender = pc1.getSenders().find( ( { track } ) => track.kind == new_track.kind );
console.log( new_track );
return sender && sender.replaceTrack( new_track ) ||
Promise.reject('no such track');
}
}
}
// 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>
Тогда, на данный момент, наилучшим, вероятно, является еще go холст, с помощью Web Audio Timer Я сделал, когда страница размыта, хотя это не будет работать на Firefox.