add aac & vorbis/webm, add option to force full transcoding
Mserver/src/index.ts
@@ -121,9 +121,27 @@ const app = setup
const { cmd, mimeType } = convertWithFFmpeg(filePath, bitrate, query.format ?? MediaFormat["opus/ogg"]);
request.signal.addEventListener("abort", () => cmd.kill("SIGKILL"));
set.headers["content-type"] = mimeType;
return new Response(cmd.stdout);
if (query.disableChunkedTranscoding) {
console.log("Here")
const chunks: Uint8Array[] = [];
for await (const chunk of cmd.stdout) {
chunks.push(chunk);
}
const full = new Uint8Array(chunks.reduce((acc, val) => acc + val.length, 0));
let offset = 0;
for (const chunk of chunks) {
full.set(chunk, offset);
offset += chunk.length;
}
return full;
}
return cmd.stdout;
},
{
query: t.Optional(
t.Object({ bitrate: t.Number(), format: t.Enum(MediaFormat), disableChunkedTranscoding: t.Boolean() }),
),
},
{ query: t.Optional(t.Object({ bitrate: t.Number(), format: t.Enum(MediaFormat) })) },
)
.get(
"/list/*",
Mserver/src/utils.ts
@@ -160,6 +160,7 @@ export function convertWithFFmpeg(filePath: string, bitrate: number, format: Med
let codec = undefined;
let container = undefined;
let mimeType = undefined;
let flags: string[] = [];
switch (format) {
case MediaFormat["opus/ogg"]: {
codec = "libopus";
@@ -179,18 +180,31 @@ export function convertWithFFmpeg(filePath: string, bitrate: number, format: Med
mimeType = "audio/mpeg";
break;
}
case MediaFormat.vorbis: {
case MediaFormat["vorbis/ogg"]: {
codec = "libvorbis";
container = "ogg";
mimeType = 'audio/ogg; codecs="vorbis"';
break;
}
case MediaFormat["vorbis/webm"]: {
codec = "libvorbis";
container = "webm";
mimeType = 'audio/webm; codecs="vorbis"';
break;
}
case MediaFormat.aac: {
codec = "libfdk_aac";
container = "mp4";
mimeType = "audio/aac";
flags = ["-movflags", "frag_keyframe+empty_moov"];
break;
}
default: {
throw new Error(`Unsupported format: ${format}`);
}
}
const cmd = Bun.spawn(
["ffmpeg", "-i", filePath, "-vn", "-c:a", codec, "-f", container, "-b:a", bitrate.toString(), "-"],
["ffmpeg", "-i", filePath, "-vn", "-c:a", codec, "-f", container, "-b:a", bitrate.toString(), ...flags, "-"],
{
stdin: "ignore",
stdout: "pipe",
Mwebclient/src/App.tsx
@@ -12,8 +12,11 @@ import { MediaFormat } from "music-server-shared/types";
import PlaylistManager from "./components/PlaylistManager";
import { BUILD_HASH } from "./build-info";
//TODO: change download actions?
//TODO: AAC support
//TODO: search
//TODO: remember scroll position for parent directories
//TODO: merge fileblobs and filemetadata, try putting signals in the onlinetree
//TODO: fix touch highlighting in playlist
//TODO: better touch controls?
//TODO: Add documentation to functions
@@ -42,6 +45,7 @@ const App: Component = () => {
const [options, setOptions] = createSignal<AppOptions>({
bitrate: 128000,
enableTranscoding: true,
disableChunkedTranscoding: false,
maxTranscodes: 4,
maxDownloads: 2,
rememberDir: false,
Mwebclient/src/components/OptionsModal.tsx
@@ -71,6 +71,15 @@ export default function OptionsModal(props: OptionsModalProps) {
onChange={(e) => setNewOptions({ ...newOptions(), enableTranscoding: e.target.checked })}
/>
</div>
<div>
<label for="transcoding-buffer-checkbox">Disable chunked transcoding:</label>
<input
id="transcoding-buffer-checkbox"
type="checkbox"
checked={newOptions()?.disableChunkedTranscoding}
onChange={(e) => setNewOptions({ ...newOptions(), disableChunkedTranscoding: e.target.checked })}
/>
</div>
<div>
<label for="bitrate-select">Bitrate:</label>
<select
@@ -109,7 +118,9 @@ export default function OptionsModal(props: OptionsModalProps) {
<option>{MediaFormat["opus/ogg"]}</option>
<option>{MediaFormat["opus/webm"]}</option>
<option>{MediaFormat.mp3}</option>
<option>{MediaFormat.vorbis}</option>
<option>{MediaFormat["vorbis/ogg"]}</option>
<option>{MediaFormat["vorbis/webm"]}</option>
<option>{MediaFormat.aac}</option>
</select>
</div>
<div>
Mwebclient/src/offline.ts
@@ -204,7 +204,7 @@ export async function getSrc(path: string, options: AppOptions, offline: boolean
return undefined;
}
if (options.enableTranscoding) {
return `${joinPath("/transcode", path)}?bitrate=${options.bitrate}&format=${options.format}`;
return `${joinPath("/transcode", path)}?bitrate=${options.bitrate}&format=${options.format}&disableChunkedTranscoding=${options.disableChunkedTranscoding}`;
}
return joinPath("/download", path);
}
@@ -282,7 +282,9 @@ async function downloadFile(
await downloadSemaphore.acquire();
let res: Response;
if (options.enableTranscoding) {
res = await fetch(`${joinPath("/transcode", path)}?bitrate=${options.bitrate}&format=${options.format}`);
res = await fetch(
`${joinPath("/transcode", path)}?bitrate=${options.bitrate}&format=${options.format}&disableChunkedTranscoding=${options.disableChunkedTranscoding}`,
);
} else {
res = await fetch(joinPath("/download", path));
}