[mirotalk] - Fix WebM duration to make it seekable
This commit is contained in:
+1
-1
@@ -45,7 +45,7 @@ dependencies: {
|
||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.5.59
|
||||
* @version 1.5.60
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mirotalk",
|
||||
"version": "1.5.59",
|
||||
"version": "1.5.60",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mirotalk",
|
||||
"version": "1.5.59",
|
||||
"version": "1.5.60",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@mattermost/client": "10.9.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mirotalk",
|
||||
"version": "1.5.59",
|
||||
"version": "1.5.60",
|
||||
"description": "A free WebRTC browser-based video call",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ let brand = {
|
||||
},
|
||||
about: {
|
||||
imageUrl: '../images/mirotalk-logo.gif',
|
||||
title: 'WebRTC P2P v1.5.59',
|
||||
title: 'WebRTC P2P v1.5.60',
|
||||
html: `
|
||||
<button
|
||||
id="support-button"
|
||||
|
||||
+36
-6
@@ -15,7 +15,7 @@
|
||||
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
|
||||
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
|
||||
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
|
||||
* @version 1.5.59
|
||||
* @version 1.5.60
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -635,6 +635,7 @@ let recScreenStream; // screen media to recording
|
||||
let recTimer;
|
||||
let recCodecs;
|
||||
let recElapsedTime;
|
||||
let recStartTs = null;
|
||||
let recPrioritizeH264 = false;
|
||||
let isStreamRecording = false;
|
||||
let isStreamRecordingPaused = false;
|
||||
@@ -7427,6 +7428,7 @@ function handleMediaRecorderStart(event) {
|
||||
recordStreamBtn.style.setProperty('color', '#ff4500');
|
||||
setTippy(recordStreamBtn, 'Stop recording', placement);
|
||||
if (isMobileDevice) elemDisplay(swapCameraBtn, false);
|
||||
recStartTs = performance.now();
|
||||
playSound('recStart');
|
||||
}
|
||||
|
||||
@@ -7519,16 +7521,25 @@ function resumeRecording() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebM duration fixer function
|
||||
* @returns {Function|null}
|
||||
*/
|
||||
function getWebmFixerFn() {
|
||||
const fn = window.FixWebmDuration;
|
||||
return typeof fn === 'function' ? fn : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download recorded stream
|
||||
*/
|
||||
function downloadRecordedStream() {
|
||||
async function downloadRecordedStream() {
|
||||
try {
|
||||
const type = recordedBlobs[0].type.includes('mp4') ? 'mp4' : 'webm';
|
||||
const blob = new Blob(recordedBlobs, { type: 'video/' + type });
|
||||
const rawBlob = new Blob(recordedBlobs, { type: 'video/' + type });
|
||||
const recFileName = getDataTimeString() + '-REC.' + type;
|
||||
const currentDevice = isMobileDevice ? 'MOBILE' : 'PC';
|
||||
const blobFileSize = bytesToSize(blob.size);
|
||||
const blobFileSize = bytesToSize(rawBlob.size);
|
||||
|
||||
const recordingInfo = `
|
||||
<br/>
|
||||
@@ -7553,7 +7564,26 @@ function downloadRecordedStream() {
|
||||
</div>`
|
||||
);
|
||||
|
||||
saveBlobToFile(blob, recFileName);
|
||||
// Fix WebM duration to make it seekable
|
||||
const fixWebmDuration = async (blob) => {
|
||||
if (type !== 'webm') return blob;
|
||||
try {
|
||||
const fix = getWebmFixerFn();
|
||||
const durationMs = recStartTs ? performance.now() - recStartTs : undefined;
|
||||
const fixed = await fix(blob, durationMs);
|
||||
return fixed || blob;
|
||||
} catch (e) {
|
||||
console.warn('WEBM duration fix failed, saving original blob:', e);
|
||||
return blob;
|
||||
} finally {
|
||||
recStartTs = null;
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const finalBlob = await fixWebmDuration(rawBlob);
|
||||
saveBlobToFile(finalBlob, recFileName);
|
||||
})();
|
||||
} catch (err) {
|
||||
userLog('error', 'Recording save failed: ' + err);
|
||||
}
|
||||
@@ -11318,7 +11348,7 @@ function showAbout() {
|
||||
Swal.fire({
|
||||
background: swBg,
|
||||
position: 'center',
|
||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.59',
|
||||
title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.5.60',
|
||||
imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about,
|
||||
customClass: { image: 'img-about' },
|
||||
html: `
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
'use strict';
|
||||
|
||||
// Minimal WebM duration fixer (no deps). Exposes: window.FixWebmDuration(blob, durationMs) -> Promise<Blob>
|
||||
(function () {
|
||||
// IDs chosen to match the simple VINT reader below
|
||||
const ID = {
|
||||
Segment: 0x8538067,
|
||||
Info: 0x549a966,
|
||||
TimecodeScale: 0xad7b1,
|
||||
Duration: 0x489,
|
||||
};
|
||||
|
||||
// Base element
|
||||
function WebmBase(name, type) {
|
||||
this.name = name || 'Unknown';
|
||||
this.type = type || 'Unknown';
|
||||
}
|
||||
WebmBase.prototype.updateBySource = function () {};
|
||||
WebmBase.prototype.setSource = function (source) {
|
||||
this.source = source;
|
||||
this.updateBySource();
|
||||
};
|
||||
WebmBase.prototype.updateByData = function () {};
|
||||
WebmBase.prototype.setData = function (data) {
|
||||
this.data = data;
|
||||
this.updateByData();
|
||||
};
|
||||
|
||||
// Uint (stored as hex string)
|
||||
function WebmUint() {
|
||||
WebmBase.call(this, 'Uint', 'Uint');
|
||||
}
|
||||
WebmUint.prototype = Object.create(WebmBase.prototype);
|
||||
WebmUint.prototype.constructor = WebmUint;
|
||||
const padHex = (h) => (h.length % 2 === 1 ? '0' + h : h);
|
||||
WebmUint.prototype.updateBySource = function () {
|
||||
this.data = '';
|
||||
for (let i = 0; i < this.source.length; i++) this.data += padHex(this.source[i].toString(16));
|
||||
};
|
||||
WebmUint.prototype.updateByData = function () {
|
||||
const len = this.data.length / 2;
|
||||
this.source = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) this.source[i] = parseInt(this.data.substr(i * 2, 2), 16);
|
||||
};
|
||||
WebmUint.prototype.getValue = function () {
|
||||
return parseInt(this.data, 16);
|
||||
};
|
||||
WebmUint.prototype.setValue = function (v) {
|
||||
this.setData(padHex(v.toString(16)));
|
||||
};
|
||||
|
||||
// Float (4 or 8 bytes)
|
||||
function WebmFloat() {
|
||||
WebmBase.call(this, 'Float', 'Float');
|
||||
}
|
||||
WebmFloat.prototype = Object.create(WebmBase.prototype);
|
||||
WebmFloat.prototype.constructor = WebmFloat;
|
||||
WebmFloat.prototype._arrType = function () {
|
||||
return this.source && this.source.length === 4 ? Float32Array : Float64Array;
|
||||
};
|
||||
WebmFloat.prototype.updateBySource = function () {
|
||||
const bytes = this.source.slice().reverse();
|
||||
const T = this._arrType();
|
||||
this.data = new T(bytes.buffer)[0];
|
||||
};
|
||||
WebmFloat.prototype.updateByData = function () {
|
||||
const T = this._arrType();
|
||||
const fa = new T([this.data]);
|
||||
const bytes = new Uint8Array(fa.buffer);
|
||||
this.source = bytes.reverse();
|
||||
};
|
||||
WebmFloat.prototype.getValue = function () {
|
||||
return this.data;
|
||||
};
|
||||
WebmFloat.prototype.setValue = function (v) {
|
||||
this.setData(v);
|
||||
};
|
||||
|
||||
// Container with VINT read/write
|
||||
function WebmContainer(name) {
|
||||
WebmBase.call(this, name || 'Container', 'Container');
|
||||
}
|
||||
WebmContainer.prototype = Object.create(WebmBase.prototype);
|
||||
WebmContainer.prototype.constructor = WebmContainer;
|
||||
WebmContainer.prototype.readByte = function () {
|
||||
return this.source[this.offset++];
|
||||
};
|
||||
WebmContainer.prototype.readVint = function () {
|
||||
const b0 = this.readByte();
|
||||
const bytes = 8 - b0.toString(2).length;
|
||||
let v = b0 - (1 << (7 - bytes));
|
||||
for (let i = 0; i < bytes; i++) {
|
||||
v = v * 256 + this.readByte();
|
||||
}
|
||||
return v;
|
||||
};
|
||||
WebmContainer.prototype.updateBySource = function () {
|
||||
this.data = [];
|
||||
for (this.offset = 0; this.offset < this.source.length; ) {
|
||||
const id = this.readVint();
|
||||
const len = this.readVint();
|
||||
const end = Math.min(this.offset + len, this.source.length);
|
||||
const bytes = this.source.slice(this.offset, end);
|
||||
|
||||
let ctor = WebmBase;
|
||||
if (id === ID.Segment || id === ID.Info) ctor = WebmContainer;
|
||||
else if (id === ID.TimecodeScale) ctor = WebmUint;
|
||||
else if (id === ID.Duration) ctor = WebmFloat;
|
||||
|
||||
const elem = new ctor();
|
||||
elem.setSource(bytes);
|
||||
this.data.push({ id, data: elem });
|
||||
this.offset = end;
|
||||
}
|
||||
};
|
||||
WebmContainer.prototype.writeVint = function (x, draft) {
|
||||
let bytes = 1,
|
||||
flag = 0x80;
|
||||
while (x >= flag && bytes < 8) {
|
||||
bytes++;
|
||||
flag *= 0x80;
|
||||
}
|
||||
if (!draft) {
|
||||
let val = flag + x;
|
||||
for (let i = bytes - 1; i >= 0; i--) {
|
||||
const c = val % 256;
|
||||
this.source[this.offset + i] = c;
|
||||
val = (val - c) / 256;
|
||||
}
|
||||
}
|
||||
this.offset += bytes;
|
||||
};
|
||||
WebmContainer.prototype.writeSections = function (draft) {
|
||||
this.offset = 0;
|
||||
for (const s of this.data) {
|
||||
const content = s.data.source;
|
||||
const len = content.length;
|
||||
this.writeVint(s.id, draft);
|
||||
this.writeVint(len, draft);
|
||||
if (!draft) this.source.set(content, this.offset);
|
||||
this.offset += len;
|
||||
}
|
||||
return this.offset;
|
||||
};
|
||||
WebmContainer.prototype.updateByData = function () {
|
||||
const len = this.writeSections(true);
|
||||
this.source = new Uint8Array(len);
|
||||
this.writeSections(false);
|
||||
};
|
||||
WebmContainer.prototype.getSectionById = function (id) {
|
||||
for (const s of this.data) if (s.id === id) return s.data;
|
||||
return null;
|
||||
};
|
||||
|
||||
// File = top-level container
|
||||
function WebmFile(src) {
|
||||
WebmContainer.call(this, 'File');
|
||||
this.setSource(src);
|
||||
}
|
||||
WebmFile.prototype = Object.create(WebmContainer.prototype);
|
||||
WebmFile.prototype.constructor = WebmFile;
|
||||
WebmFile.prototype.toBlob = function (mime) {
|
||||
return new Blob([this.source.buffer], { type: mime || 'video/webm' });
|
||||
};
|
||||
WebmFile.prototype.fixDuration = function (durationMs) {
|
||||
const segment = this.getSectionById(ID.Segment);
|
||||
if (!segment) return false;
|
||||
const info = segment.getSectionById(ID.Info);
|
||||
if (!info) return false;
|
||||
let scale = info.getSectionById(ID.TimecodeScale);
|
||||
if (!scale) return false;
|
||||
// Ensure 1ms scale for a simple duration (ms) value
|
||||
scale.setValue(1000000);
|
||||
|
||||
let dur = info.getSectionById(ID.Duration);
|
||||
if (dur) {
|
||||
if (dur.getValue() > 0) return false; // already valid
|
||||
dur.setValue(durationMs);
|
||||
} else {
|
||||
dur = new WebmFloat();
|
||||
dur.setValue(durationMs);
|
||||
info.data.push({ id: ID.Duration, data: dur });
|
||||
}
|
||||
// Rebuild buffers up the tree
|
||||
info.updateByData();
|
||||
segment.updateByData();
|
||||
this.updateByData();
|
||||
return true;
|
||||
};
|
||||
|
||||
async function FixWebmDuration(blob, durationMs) {
|
||||
if (!blob || blob.type.indexOf('webm') === -1) return blob;
|
||||
try {
|
||||
const buf = await blob.arrayBuffer();
|
||||
const file = new WebmFile(new Uint8Array(buf));
|
||||
if (file.fixDuration(Math.max(0, Number(durationMs) || 0))) {
|
||||
return file.toBlob(blob.type);
|
||||
}
|
||||
} catch (_) {
|
||||
/* ignore, fallback to original */
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
|
||||
window.FixWebmDuration = FixWebmDuration;
|
||||
})();
|
||||
@@ -1053,6 +1053,7 @@ access to use this app.
|
||||
<script defer src="../js/buttons.js"></script>
|
||||
<script defer src="../js/videoGrid.js"></script>
|
||||
<script defer src="../js/translate.js"></script>
|
||||
<script defer src="../js/fixWebmDuration.js"></script>
|
||||
<script defer src="../js/client.js"></script>
|
||||
<script defer src="../js/speechRecognition.js"></script>
|
||||
<script defer src="../js/nodeProcessor.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user