import { type Accessor, batch, createEffect, createSignal, For, Match, on, type Setter, Show, Switch } from "solid-js"; import { type AppOptions, AUDIO_BITRATES, FileDisplay, StreamingMode, VIDEO_BITRATES } from "../types"; import "./OptionsModal.css"; import "./Dialog.css"; import "toastify-js/src/toastify.css"; import { AudioCodec, MediaContainer, VideoCodec, VideoEncodingSetting } from "music-server-shared/types"; import { audioCodecHasBitrate } from "music-server-shared/utils"; import { createStore } from "solid-js/store"; import { checkAudioContainerSupport, checkSupport, checkVideoContainerSupport, type Combination, containerAudioCodecs, containerVideoCodecs, hasMediaSource, playableSettings, selectableVideoCodecs, type Support, } from "../capabilities"; import { handleUnauthorized, toast } from "../utils"; interface OptionsModalProps { showOptions: Accessor; setShowOptions: Setter; options: AppOptions; setOptions: (options: AppOptions) => void; } const STREAMING_MODE_HINTS: Record = { [StreamingMode.mse]: "Media Source Extensions: the transcode is streamed into a SourceBuffer. The only mode that can seek within a transcode, but limited to the containers and codecs that support MSE.", [StreamingMode.progressive]: "The transcode is streamed straight to the player as it is produced. Plays whatever the browser can decode, but cannot seek.", [StreamingMode.buffered]: "The server transcodes the whole file before sending anything, so the response is a normal sized download instead of a chunked one. Much slower to start, and only needed for browsers that refuse chunked media sources (Safari).", }; type Profile = "videoSettings" | "audioSettings"; function bitrateLabel(bitrate: number): string { return bitrate < 1_000_000 ? `${bitrate / 1000} kbps` : `${bitrate / 1_000_000} Mbps`; } function SupportedOption(props: { value: string; support: Support }) { return ( ); } export default function OptionsModal(props: OptionsModalProps) { //clone, otherwise it shares the reference const [newOptions, setNewOptions] = createStore({ ...props.options }); const [optionsPage, setOptionsPage] = createSignal(1); let dialogElement!: HTMLDialogElement; let formElement!: HTMLFormElement; const videoSupport = () => checkSupport(newOptions.videoSettings); const audioSupport = () => checkSupport(newOptions.audioSettings); //keep the selection playable, so the greyed-out entries in the lists are only ever ones the user could //pick and never the one they are already on. writing a value a store already holds notifies nobody, so //once the selection is playable this re-runs, finds the same values and settles function coerceProfile(profile: Profile) { const playable = playableSettings(newOptions[profile]); batch(() => { setNewOptions(profile, "streamingMode", playable.streamingMode); setNewOptions(profile, "container", playable.container); setNewOptions(profile, "audioCodec", playable.audioCodec); //only videoSettings has a video codec, which is exactly when playableSettings returns one if (playable.videoCodec !== undefined) setNewOptions("videoSettings", "videoCodec", playable.videoCodec); }); } createEffect(() => coerceProfile("videoSettings")); createEffect(() => coerceProfile("audioSettings")); createEffect( on(props.showOptions, () => { if (props.showOptions()) { dialogElement.showModal(); setNewOptions(props.options); setOptionsPage(1); } }), ); function handleSave(e: Event) { e.preventDefault(); if (!formElement.reportValidity()) return; toast("Saved changes", "success"); props.setOptions(newOptions); handleClose(); } function handleClose() { dialogElement.close(); props.setShowOptions(false); } async function deleteServerCache() { const response = await fetch("/reset-cache", { method: "POST", credentials: "same-origin" }).catch(() => {}); if (handleUnauthorized(response)) return; if (response?.status !== 200) { toast("Clearing server cache failed", "error"); } else { toast("Server cache cleared", "info"); } } return ( e.target.nodeName === "DIALOG" && handleClose()}>
Player Options

{newOptions.enableTranscoding && ( )} {newOptions.enableTranscoding && ( )}
setNewOptions("enableTranscoding", e.target.checked)} />
setNewOptions("languages", e.target.value)} disabled={!newOptions.enableTranscoding} />
setNewOptions("maxTranscodes", e.target.valueAsNumber)} />
setNewOptions("maxDownloads", e.target.valueAsNumber)} />
setNewOptions("hideVolume", e.target.checked)} />
setNewOptions("hideCover", e.target.checked)} />
setNewOptions("hidePlaylistCover", e.target.checked)} />
setNewOptions("progressBarHeight", e.target.valueAsNumber)} />
setNewOptions("rememberDir", e.target.checked)} />
setNewOptions("duplicateWarning", e.target.checked)} />
setNewOptions("transcodeCovers", e.target.checked)} />
Transcoding settings for videos
0}>
Transcoding settings for music
); }