fix mse buffering
Mserver/src/ffmpeg.ts
@@ -79,8 +79,8 @@ export function convertWithFFmpeg(
"make_zero",
"-movflags",
"+frag_keyframe+empty_moov+default_base_moof+faststart",
// "-g",
// "60",
"-g",
"300",
];
}
Mwebclient/src/App.tsx
@@ -243,42 +243,78 @@ const App: Component = () => {
sourceBuffer.mode = "sequence";
sourceBuffer.timestampOffset = offsetSeconds;
const pendingBuffer: Uint8Array[] = [];
let isBuffering = false;
const processQueue = () => {
if (isBuffering || pendingBuffer.length === 0) return;
const buffer = pendingBuffer.shift() as Uint8Array;
if (thisMediaSource.readyState !== "open") return;
isBuffering = true;
sourceBuffer.appendBuffer(buffer);
};
sourceBuffer.addEventListener("updateend", () => {
isBuffering = false;
processQueue();
});
try {
mediaSourceAbortController = new AbortController();
const response = await fetch(srcUrl, { credentials: "same-origin", signal: mediaSourceAbortController.signal });
if (!response.ok || !response.body) throw new Error("Failed to fetch stream");
const reader = response.body.getReader();
function waitUntilUpdateDone() {
return new Promise((resolve) => {
if (!sourceBuffer.updating) resolve(undefined);
return sourceBuffer.addEventListener("updateend", resolve, { once: true });
});
}
//wait until playback time is progressed far enough to appending to the buffer again
//keep only 30 seconds of buffer ahead
function waitForBufferLimit() {
return new Promise((resolve) => {
const cb = () => {
try {
if (
sourceBuffer.buffered.length === 0 ||
sourceBuffer.buffered.end(0) - videoElement.currentTime < 30
) {
resolve(undefined);
videoElement.removeEventListener("timeupdate", cb);
return true;
}
return false;
} catch (e) {
videoElement.removeEventListener("timeupdate", cb);
resolve(undefined);
}
};
//check once immediately before starting the event listener
if (!cb()) {
videoElement.addEventListener("timeupdate", cb);
}
});
}
async function saveUpdate(action: () => void) {
while (true) {
try {
if (thisMediaSource.readyState !== "open") return;
await waitUntilUpdateDone();
action();
return;
} catch (e) {
if (!(e instanceof Error && e.name === "InvalidStateError")) throw e;
}
}
}
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
// Copy value to prevent buffer overwriting
pendingBuffer.push(Uint8Array.from(value));
processQueue();
if (done || thisMediaSource.readyState !== "open") break;
const CHUNK_SIZE = 1000000; // 1MB chunks
for (let offset = 0; offset < value.length; offset += CHUNK_SIZE) {
const chunk = value.slice(offset, offset + CHUNK_SIZE);
await saveUpdate(() => sourceBuffer.appendBuffer(chunk));
//keep last 20 seconds of buffer
if (sourceBuffer.buffered.length > 0 && videoElement.currentTime - sourceBuffer.buffered.start(0) > 20) {
await saveUpdate(() => sourceBuffer.remove(0, videoElement.currentTime - 20));
}
await waitForBufferLimit();
}
}
// Wait for queue to drain
while (pendingBuffer.length > 0 || isBuffering) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
// Wait for buffer to finish updating
await waitUntilUpdateDone();
} catch (error) {
if (!(error instanceof Error && error.name === "AbortError")) {
console.error(error);
@@ -517,7 +553,7 @@ const App: Component = () => {
if (activeElementTag === "input") {
return;
}
if (e.key === " ") {
if (e.key === " " && document.fullscreenElement !== videoElement) {
togglePlay();
e.preventDefault();
} else if (e.key === "ArrowLeft") {