mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
fix(voice): record the real length and write it into the file
The recording clock counted setInterval ticks, which a background tab throttles away: a recording that kept going looked frozen at 13 seconds and was saved with that length. It now reads the wall clock instead. MediaRecorder also writes WebM with no usable duration. Chrome omits the element entirely, Firefox reserves a Duration of 0.0 it never fills in, so players had no length to show and played past the end of the seek bar. lib/webm-duration.ts stamps the recorded length into Segment > Info when the recording stops, in place where the browser reserved room for it.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Builds WebM bytes in the shapes MediaRecorder emits them. The two browsers
|
||||
* differ in ways this code has to survive:
|
||||
*
|
||||
* Chrome writes no SeekHead and no Duration at all, with compact 1-byte
|
||||
* element sizes.
|
||||
* Firefox writes an empty SeekHead, a Duration reserved as 0.0 that it never
|
||||
* fills in, and 8-byte element sizes.
|
||||
*
|
||||
* Nothing here is a real audio stream; these tests only care about the
|
||||
* container fields a player reads to learn how long a recording is.
|
||||
*/
|
||||
|
||||
const UNKNOWN_SIZE = [0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
|
||||
const INFO_ID = [0x15, 0x49, 0xa9, 0x66];
|
||||
const SEEK_HEAD_ID = [0x11, 0x4d, 0x9b, 0x74];
|
||||
|
||||
/** A Duration of 0.0 in an 8-byte float, the placeholder Firefox leaves behind. */
|
||||
export const DURATION_PLACEHOLDER = [0x44, 0x89, 0x88, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
|
||||
export function ebmlElement(id: number[], content: number[], sizeWidth = 1): number[] {
|
||||
if (sizeWidth === 1) {
|
||||
if (content.length >= 127) throw new Error('a 1-byte size holds at most 126 bytes');
|
||||
return [...id, 0x80 | content.length, ...content];
|
||||
}
|
||||
|
||||
// Division, not `>>`: JS shifts wrap at 32 bits and an 8-byte size needs 56.
|
||||
const size = Array.from({ length: sizeWidth }, (_, i) =>
|
||||
i === 0
|
||||
? 0x100 >> sizeWidth
|
||||
: Math.floor(content.length / 2 ** (8 * (sizeWidth - 1 - i))) & 0xff
|
||||
);
|
||||
return [...id, ...size, ...content];
|
||||
}
|
||||
|
||||
export function timecodeScaleElement(nanoseconds: number): number[] {
|
||||
return ebmlElement(
|
||||
[0x2a, 0xd7, 0xb1],
|
||||
[(nanoseconds >> 16) & 0xff, (nanoseconds >> 8) & 0xff, nanoseconds & 0xff]
|
||||
);
|
||||
}
|
||||
|
||||
export const CLUSTER_BYTES = ebmlElement([0x1f, 0x43, 0xb6, 0x75], [0xe7, 0x81, 0x00]);
|
||||
|
||||
export function buildLiveWebm({
|
||||
info = timecodeScaleElement(1_000_000),
|
||||
seekHead = 'none',
|
||||
sizeWidth = 1,
|
||||
}: {
|
||||
info?: number[];
|
||||
seekHead?: 'none' | 'empty' | 'entries';
|
||||
sizeWidth?: number;
|
||||
} = {}): Uint8Array {
|
||||
const header = ebmlElement([0x1a, 0x45, 0xdf, 0xa3], [0x42, 0x86, 0x81, 0x01]);
|
||||
const seekHeadBytes =
|
||||
seekHead === 'none'
|
||||
? []
|
||||
: ebmlElement(SEEK_HEAD_ID, seekHead === 'empty' ? [] : [0x53, 0xac, 0x81, 0xa1], sizeWidth);
|
||||
const segmentBody = [
|
||||
...seekHeadBytes,
|
||||
...ebmlElement(INFO_ID, info, sizeWidth),
|
||||
...CLUSTER_BYTES,
|
||||
];
|
||||
return new Uint8Array([...header, 0x18, 0x53, 0x80, 0x67, ...UNKNOWN_SIZE, ...segmentBody]);
|
||||
}
|
||||
|
||||
/** Walks the file the way a player would and pulls Segment > Info > Duration. */
|
||||
export function readWebmDuration(bytes: Uint8Array): number | null {
|
||||
for (let i = 0; i < bytes.length - 4; i++) {
|
||||
if (!INFO_ID.every((byte, offset) => bytes[i + offset] === byte)) continue;
|
||||
|
||||
const sizeWidth = 8 - Math.floor(Math.log2(bytes[i + 4]));
|
||||
const infoStart = i + 4 + sizeWidth;
|
||||
let infoSize = bytes[i + 4] & (0xff >> sizeWidth);
|
||||
for (let k = 1; k < sizeWidth; k++) infoSize = infoSize * 0x100 + bytes[i + 4 + k];
|
||||
|
||||
for (let j = infoStart; j < infoStart + infoSize - 1; j++) {
|
||||
if (bytes[j] === 0x44 && bytes[j + 1] === 0x89) {
|
||||
const width = bytes[j + 2] & 0x7f;
|
||||
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||
return width === 4 ? view.getFloat32(j + 3) : view.getFloat64(j + 3);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user