-
Notifications
You must be signed in to change notification settings - Fork 52
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to create get seekable webm from media-recoder example without using TypeScript? #14
Comments
Interestingly, getting errors at plnkr https://plnkr.co/edit/oxE8JTGKPWa21tHpzuCc?p=preview using same code from https://jsfiddle.net/ub2jej7c/
|
@legokichi Was able to make adjustments to jsfiddle to use at browser, though not able to successfully render |
@guest271314 here is a seekable webm creation code in vanilla js.
const ebml = require("ts-ebml");
navigator.mediaDevices.getUserMedia({video: true, audio: true}).then((stream)=>{
const decoder = new ebml.Decoder();
const reader = new ebml.Reader();
let tasks = Promise.resolve();
let webM = new Blob([], {type: "video/webm"});
const rec = new MediaRecorder(stream, { mimeType: 'video/webm; codecs="vp8, opus"'});
rec.addEventListener("dataavailable", ondataavailable);
function ondataavailable(ev){
console.log("data");
const chunk = ev.data;
webM = new Blob([webM, chunk], {type: chunk.type});
const task = ()=> readAsArrayBuffer(chunk).then((buf)=>{
const elms = decoder.decode(buf);
elms.forEach((elm)=>{ reader.read(elm); });
});
tasks = tasks.then(()=> task() );
}
rec.start(100);
return sleep(10*1000)
.then(()=>{
rec.stop();
rec.removeEventListener("dataavailable", ondataavailable);
rec.stream.getTracks().map((track) => { track.stop(); });
reader.stop();
return tasks; })
.then(()=>{
const refinedMetadataBuf = ebml.tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues);
return readAsArrayBuffer(webM).then((webMBuf)=>{
const body = webMBuf.slice(reader.metadataSize);
const refinedWebM = new Blob([refinedMetadataBuf, body], {type: webM.type});
const refined_video = document.createElement("video");
refined_video.src = URL.createObjectURL(refinedWebM);
refined_video.controls = true;
document.body.appendChild(refined_video);
return; }); });
});
function readAsArrayBuffer(blob) {
return new Promise((resolve, reject)=>{
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = ()=>{ resolve(reader.result); };
reader.onerror = (ev)=>{ reject(ev.error); };
});
}
function sleep(ms){
return new Promise((resolve)=> setTimeout(resolve, ms) );
} |
@legokichi Interesting results using |
@guest271314 This code is intended for long-term recording. In order not to create a huge ArrayBuffer, WebM output by the MediaRecorder API is divided into metadata, clusters, and mime type. And you can save this in IndexedDB one by one. Avoid using huge blobs and ArrayBuffer by using the MediaSource API for playing split long video files. const ebml = require("ts-ebml");
navigator.mediaDevices.getUserMedia({video: true, audio: true}).then((stream)=>{
stream.addEventListener("active", (ev)=>{ console.log(ev.type); });
stream.addEventListener("inactive", (ev)=>{ console.log(ev.type); });
stream.addEventListener("addtrack", (ev)=>{ console.log(ev.type); });
stream.addEventListener("removetrack", (ev)=>{ console.log(ev.type); });
const rec = new MediaRecorder(stream, { mimeType: 'video/webm; codecs="vp8, opus"'});
rec.addEventListener("dataavailable", (ev)=>{ console.log(ev.type); });
rec.addEventListener("pause", (ev)=>{ console.log(ev.type); });
rec.addEventListener("resume", (ev)=>{ console.log(ev.type); });
rec.addEventListener("start", (ev)=>{ console.log(ev.type); });
rec.addEventListener("stop", (ev)=>{ console.log(ev.type); });
rec.addEventListener("error", (ev)=>{ console.error(ev.type, ev); });
const decoder = new ebml.Decoder();
const reader = new ebml.Reader();
let tasks = Promise.resolve();
rec.addEventListener("dataavailable", ondataavailable);
function ondataavailable(ev){
console.log("data")
const chunk = ev.data;
const task = ()=> readAsArrayBuffer(chunk).then((buf)=>{
const elms = decoder.decode(buf);
elms.forEach((elm)=>{ reader.read(elm); });
});
tasks = tasks.then(()=> task() );
}
const clusters = [];
reader.addListener("cluster", (ev)=>{
console.log("cluster");
const buf = new ebml.Encoder().encode(ev.data);
clusters.push(buf);
});
rec.start(100);
return sleep(10*1000).then(()=>{
rec.stop();
rec.removeEventListener("dataavailable", ondataavailable);
rec.stream.getTracks().map((track) => { track.stop(); });
reader.stop();
return tasks;
}).then(()=>{
const refinedMetadataBuf = ebml.tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues);
return {metadata: refinedMetadataBuf, clusters, mimeType: rec.mimeType};
});
}).then((ctx)=>{
const {metadata, clusters, mimeType} = ctx;
const ms = new MediaSource();
ms.addEventListener('sourceopen', (ev)=>{ console.log(ev.type); });
ms.addEventListener('sourceended', (ev)=>{ console.log(ev.type); });
ms.addEventListener('sourceclose', (ev)=>{ console.log(ev.type); });
ms.sourceBuffers.addEventListener('addsourcebuffer', (ev)=>{ console.log(ev.type); });
ms.sourceBuffers.addEventListener('removesourcebuffer', (ev)=>{ console.log(ev.type); });
const video = document.createElement("video");
video.addEventListener('loadstart', (ev)=>{ console.log(ev.type); });
video.addEventListener('progress', (ev)=>{ console.log(ev.type); });
video.addEventListener('loadedmetadata', (ev)=>{ console.log(ev.type); });
video.addEventListener('loadeddata', (ev)=>{ console.log(ev.type); });
video.addEventListener('canplay', (ev)=>{ console.log(ev.type); });
video.addEventListener('canplaythrough', (ev)=>{ console.log(ev.type); });
video.addEventListener('playing', (ev)=>{ console.log(ev.type); });
video.addEventListener('waiting', (ev)=>{ console.log(ev.type); });
video.addEventListener('seeking', (ev)=>{ console.log(ev.type); });
video.addEventListener('seeked', (ev)=>{ console.log(ev.type); });
video.addEventListener('ended', (ev)=>{ console.log(ev.type); });
video.addEventListener('emptied', (ev)=>{ console.log(ev.type); });
video.addEventListener('stalled', (ev)=>{ console.log(ev.type); });
video.addEventListener('timeupdate', (ev)=>{ console.log(ev.type); }); // annoying
video.addEventListener('durationchange', (ev)=>{ console.log(ev.type); });
video.addEventListener('ratechange', (ev)=>{ console.log(ev.type); });
video.addEventListener('play', (ev)=>{ console.log(ev.type); });
video.addEventListener('pause', (ev)=>{ console.log(ev.type); });
video.addEventListener('error', (ev)=>{ console.warn(ev.type, ev); });
//video.srcObject = ms;
video.src = URL.createObjectURL(ms);
video.volume = 0;
video.controls = true;
video.autoplay = true;
document.body.appendChild(video);
return new Promise((resolve)=>{ ms.addEventListener('sourceopen', ()=>{ resolve({metadata, clusters, mimeType, video, ms}); }, {once: true}); });
}).then((ctx)=>{
const {video, ms, metadata, clusters, mimeType} = ctx;
const sb = ms.addSourceBuffer(mimeType);
sb.addEventListener('updatestart', (ev)=>{ console.log(ev.type); }); // annoying
sb.addEventListener('update', (ev)=>{ console.log(ev.type); }); // annoying
sb.addEventListener('updateend', (ev)=>{ console.log(ev.type); }); // annoying
sb.addEventListener('error', (ev)=>{ console.error(ev.type, ev); });
sb.addEventListener('abort', (ev)=>{ console.log(ev.type); });
return [metadata].concat(clusters).reduce((o, buf)=> o.then(()=> appendBuffer(video, sb, buf)), Promise.resolve());
});
function readAsArrayBuffer(blob) {
return new Promise((resolve, reject)=>{
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = ()=>{ resolve(reader.result); };
reader.onerror = (ev)=>{ reject(ev.error); };
});
}
function sleep(ms){
return new Promise((resolve)=> setTimeout(resolve, ms) );
}
function appendBuffer(video, sb, buf) {
return new Promise((resolve, reject)=>{
sb.appendBuffer(buf);
sb.addEventListener('updateend', ()=> resolve(), {once: true});
sb.addEventListener("error", (ev)=> reject(ev), {once: true});
}).then(()=>{
console.log("timestampOffset", sb.timestampOffset);
console.log("appendWindowStart", sb.appendWindowStart);
console.log("appendWindowEnd", sb.appendWindowEnd);
for(let i=0; i<sb.buffered.length; i++){
console.log("buffered", i, sb.buffered.start(i), sb.buffered.end(i));
}
for(let i=0; i<video.seekable.length; i++){
console.log("seekable", i, video.seekable.start(i), video.seekable.end(i));
}
console.log("webkitAudioDecodedByteCount", video["webkitAudioDecodedByteCount"]);
console.log("webkitVideoDecodedByteCount", video["webkitVideoDecodedByteCount"]);
console.log("webkitDecodedFrameCount", video["webkitDecodedFrameCount"]);
console.log("webkitDroppedFrameCount", video["webkitDroppedFrameCount"]);
if (video.buffered.length > 1) {
console.warn("MSE buffered has a gap!");
throw new Error("MSE buffered has a gap!");
}
});
} |
@legokichi Code have composed so far using
Using |
@legokichi Getting closer. Adjusted |
@legokichi Solved https://jsfiddle.net/258f9038/6/. Nice work. |
@legokichi Evidently did not update jsfiddle to include |
@legokichi fwiw the code that have composed so far https://github.com/guest271314/recordMediaFragments. There are several issues at Firefox, one of which is that the media fragments recorded into a single |
Here's a simple snippet for import {Decoder, Encoder, tools, Reader} from 'ts-ebml';
const readAsArrayBuffer = function(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsArrayBuffer(blob);
reader.onloadend = () => { resolve(reader.result); };
reader.onerror = (ev) => { reject(ev.error); };
});
}
const injectMetadata = function(blob) {
const decoder = new Decoder();
const reader = new Reader();
reader.logging = false;
reader.drop_default_duration = false;
readAsArrayBuffer(blob).then((buffer) => {
const elms = decoder.decode(buffer);
elms.forEach((elm) => { reader.read(elm); });
reader.stop();
var refinedMetadataBuf = tools.makeMetadataSeekable(
reader.metadatas, reader.duration, reader.cues);
var body = buffer.slice(reader.metadataSize);
const result = new Blob([refinedMetadataBuf, body],
{type: blob.type});
return result;
});
}
// usage: pass in a webm blob
var updatedBlob = injectMetadata(myBlob); |
@thijstriemstra The site that was using for ts-ebml is no longer providing the service https://wzrd.in/bundle/ts-ebml@latest/. Is there an online CDN for ts-ebml that you use? Note, at the code |
@guest271314 I use webpack to compile it into my project but perhaps unpkg.com works for you (e.g. https://unpkg.com/[email protected]/lib/index.js) |
@thijstriemstra Currently using https://cdn.webrtc-experiment.com/EBML.js. Can we mimic |
No Audio Duration |
Yes, I know this is closed, but is there any solution that doesn't require compiling your code to use browserify? I am using Thijs's code, but I cant just import ts-ebml since its client side js. |
There is a minimized version of |
The above solution doesn't work if the video file size exceeds 2GB, because it uses arrayBuffer internally and it has a size limit. Based on this, I wrapped |
FWIW a version of ts-ebml (minimized) that is a JavaScript module export https://github.com/guest271314/captureSystemAudio/blob/master/native_messaging/capture_system_audio/ts-ebml.min.js. Usage
|
Hello, since |
|
I don't think |
Have no experience using
TypeScript
. The code example using your library could possibly be a solution for w3c/media-source#191. How can we convert get seekable webm from media-recoder example to plain JavaScript capable of usage within browser?The text was updated successfully, but these errors were encountered: