fix mse more, improve scanning speed for large mkv files
Mserver/src/ffmpeg.ts
@@ -1,6 +1,6 @@
import { AudioCodec, MediaContainer, VideoCodec, VideoEncodingSetting } from "music-server-shared/types";
import { createRFC6381String } from "music-server-shared/utils";
import { probeFile, readStream } from "./utils";
import { readStream } from "./utils";
function secondsToFfmpeg(seconds: number): string {
// Ensure seconds is non-negative
@@ -79,8 +79,6 @@ export function convertWithFFmpeg(
"make_zero",
"-movflags",
"+frag_keyframe+empty_moov+default_base_moof+faststart",
"-g",
"300",
];
}
@@ -97,8 +95,10 @@ export function convertWithFFmpeg(
//scale video down based on bitrate
"-vf",
`yadif=deint=1:mode=1${maxWidth ? `, scale='if(gt(iw,${maxWidth}),${maxWidth},iw)':-2` : ""}`,
//one keyframe ~ 5 seconds for 30 fps video
//important because if this is too long, the frontend might remove frames relevant for current playback when discarding old data from the sourcebuffer
"-g",
"600",
"150",
];
switch (requestedVideoCodec) {
@@ -199,8 +199,9 @@ export function convertWithFFmpeg(
: ["-vn"],
)
.concat(videoEncodingSettings)
.concat(["-c:a", audioCodec, "-b:a", audioBitrate.toString()])
.concat(["-c:a", audioCodec, "-b:a", audioBitrate.toString(), "-ac", "2"])
.concat(["-f", container, "-"]);
console.log(args.join(" "));
const cmd = Bun.spawn(args, {
stdin: "ignore",
stdout: "pipe",
Mserver/src/utils.ts
@@ -8,7 +8,6 @@ import {
coverRegex,
excludeExtension,
fileTypeCache,
mediaInfoDisabled,
mediaTypes,
musicRoot,
PathInfo,
@@ -36,6 +35,7 @@ const mediaInfo = await MediaInfo({
export async function findCover(targetPath: string): Promise<CoverResult> {
try {
if ((await stat(targetPath)).isFile()) {
//TODO
const parsed = await parseFile(targetPath, { duration: true, skipCovers: false });
if (!parsed.common.picture || parsed.common.picture.length === 0) return findCover(path.dirname(targetPath));
@@ -73,13 +73,22 @@ export async function findCover(targetPath: string): Promise<CoverResult> {
return { info: new ServerError(StatusMap["Not Found"], "Cover not found") };
}
function decodeString(str?: string) {
if (!str) return undefined;
if (typeof str === "object" && "@dt" in str && "#value" in str && str["@dt"] === "binary.base64") {
return Buffer.from(str["#value"], "base64").toString("utf-8");
}
return str;
// return Buffer.from(str, "utf8").toString("utf-8");
}
export async function probeFile(filePath: string): Promise<Metadata> {
const cached = probeCache.get(filePath);
if (cached) return cached;
const parsed = await parseFile(filePath, { duration: true, skipCovers: false });
let probeData: Metadata = {
duration: parsed.format.duration || 0,
const parsed = await parseFile(filePath, { duration: true, skipCovers: false, mkvUseIndex: true });
const probeData = {
title: parsed.common.title,
artist: parsed.common.artist,
album: parsed.common.album,
@@ -87,24 +96,36 @@ export async function probeFile(filePath: string): Promise<Metadata> {
trackNumber: parsed.common.track.no || undefined,
};
if (!mediaInfoDisabled) {
const file = Bun.file(filePath);
const fileSize = (await file.stat()).size;
const chunkReader = async (size: number, offset: number) => {
return await file.slice(offset, offset + size).bytes();
};
const info = await mediaInfo.analyzeData(fileSize, chunkReader);
probeData = {
...probeData,
audioCodec: info.media?.track.find((track) => track["@type"] === "Audio")?.Format,
audioBitrate: info.media?.track.find((track) => track["@type"] === "Audio")?.BitRate,
videoCodec: info.media?.track.find((track) => track["@type"] === "Video")?.Format,
videoBitrate: info.media?.track.find((track) => track["@type"] === "Video")?.BitRate,
container: info.media?.track.find((track) => track["@type"] === "General")?.Format,
};
const file = Bun.file(filePath);
const fileSize = (await file.stat()).size;
const chunkReader = async (size: number, offset: number) => {
return await file.slice(offset, offset + size).bytes();
};
const info = await mediaInfo.analyzeData(fileSize, chunkReader);
for (const track of info.media?.track || []) {
console.log(track);
}
probeCache.set(filePath, probeData);
return probeData;
const generalTrack = info.media?.track.find((track) => track["@type"] === "General");
const videoTrack = info.media?.track.find((track) => track["@type"] === "Video");
const audioTrack = info.media?.track.find((track) => track["@type"] === "Audio");
const probeResult = {
...probeData,
duration: generalTrack?.Duration || 0,
// title: decodeString(generalTrack?.Title),
// artist: generalTrack?.Album_Performer,
// album: generalTrack?.Album,
// genre: [generalTrack?.Genre || ""],
// trackNumber: generalTrack?.Track_Position,
container: generalTrack?.Format,
audioCodec: audioTrack?.Format,
audioBitrate: audioTrack?.BitRate,
videoCodec: videoTrack?.Format,
videoBitrate: videoTrack?.BitRate,
};
probeCache.set(filePath, probeResult);
return probeResult;
}
export async function getPathInfo(filePath: string): Promise<PathInfo | ServerError | undefined> {
Mwebclient/src/App.tsx
@@ -31,9 +31,9 @@ import { createRFC6381String, decodePath, encodePath } from "music-server-shared
import DownloadManager from "./components/DownloadManager";
import { SignIn } from "./components/SignIn";
//TODO: bypass music-metadata if it fails, maybe only use for fallback for files with weird encodings
//TODO: more file display options
//TODO: keep synced-aborted in downloadmanager
//TODO: bypass music-metadata if it fails
//TODO: Add documentation to functions
//TODO: allow syncing playlists to server
//TODO: video support
@@ -303,13 +303,14 @@ const App: Component = () => {
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 saveUpdate(() => {
if (sourceBuffer.buffered.length > 0 && videoElement.currentTime - sourceBuffer.buffered.start(0) > 20) {
return sourceBuffer.remove(0, videoElement.currentTime - 20);
}
});
await waitForBufferLimit();
}
}