diff --git a/src/components/player/atoms/settings/QualityView.tsx b/src/components/player/atoms/settings/QualityView.tsx index 4808dde2..139eb802 100644 --- a/src/components/player/atoms/settings/QualityView.tsx +++ b/src/components/player/atoms/settings/QualityView.tsx @@ -1,5 +1,6 @@ import { useCallback } from "react"; +import { Toggle } from "@/components/buttons/Toggle"; import { Icon, Icons } from "@/components/Icon"; import { Context } from "@/components/player/internals/ContextUtils"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; @@ -9,6 +10,7 @@ import { allQualities, qualityToString, } from "@/stores/player/utils/qualities"; +import { useQualityStore } from "@/stores/quality"; export function QualityOption(props: { children: React.ReactNode; @@ -41,13 +43,18 @@ export function QualityView({ id }: { id: string }) { const availableQualities = usePlayerStore((s) => s.qualities); const currentQuality = usePlayerStore((s) => s.currentQuality); const switchQuality = usePlayerStore((s) => s.switchQuality); + const setAutomaticQuality = useQualityStore((s) => s.setAutomaticQuality); + const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality); + const autoQuality = useQualityStore((s) => s.quality.automaticQuality); const change = useCallback( (q: SourceQuality) => { switchQuality(q); + setLastChosenQuality(q); + setAutomaticQuality(false); router.close(); }, - [router, switchQuality] + [router, switchQuality, setLastChosenQuality, setAutomaticQuality] ); const allVisibleQualities = allQualities.filter((t) => t !== "unknown"); @@ -73,7 +80,10 @@ export function QualityView({ id }: { id: string }) { Automatic quality - Toggle + setAutomaticQuality(!autoQuality)} + enabled={autoQuality} + /> You can try{" "} diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index b221cdb5..ea7b89d9 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -6,6 +6,7 @@ import { SourceSliceSource, selectQuality, } from "@/stores/player/utils/qualities"; +import { useQualityStore } from "@/stores/quality"; import { ValuesOf } from "@/utils/typeguard"; export const playerStatus = { @@ -118,7 +119,8 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ let qualities: string[] = []; if (stream.type === "file") qualities = Object.keys(stream.qualities); const store = get(); - const loadableStream = selectQuality(stream); + const qualityPreferences = useQualityStore.getState(); + const loadableStream = selectQuality(stream, qualityPreferences.quality); set((s) => { s.source = stream; diff --git a/src/stores/player/utils/qualities.ts b/src/stores/player/utils/qualities.ts index b83ef303..4082a9a2 100644 --- a/src/stores/player/utils/qualities.ts +++ b/src/stores/player/utils/qualities.ts @@ -1,3 +1,5 @@ +import { QualityStore } from "@/stores/quality"; + export type SourceQuality = "unknown" | "360" | "480" | "720" | "1080"; export type StreamType = "hls" | "mp4"; @@ -23,17 +25,56 @@ export type SourceSliceSource = }; const qualitySorting: Record = { - "1080": 40, + unknown: 0, "360": 10, "480": 20, "720": 30, - unknown: 0, + "1080": 40, }; const sortedQualities: SourceQuality[] = Object.entries(qualitySorting) .sort((a, b) => b[1] - a[1]) .map((v) => v[0] as SourceQuality); -export function selectQuality(source: SourceSliceSource): { +function getPreferredQuality( + availableQualites: SourceQuality[], + qualityPreferences: QualityStore["quality"] +) { + if ( + qualityPreferences.automaticQuality || + qualityPreferences.lastChosenQuality === null || + qualityPreferences.lastChosenQuality === "unknown" + ) + return sortedQualities.find((v) => availableQualites.includes(v)); + + // get preferred quality - not automatic or unknown + const chosenQualityIndex = sortedQualities.indexOf( + qualityPreferences.lastChosenQuality + ); + let nearestChoseQuality: undefined | SourceQuality; + + // check chosen quality or lower + for (let i = chosenQualityIndex; i < sortedQualities.length; i += 1) { + if (availableQualites.includes(sortedQualities[i])) { + nearestChoseQuality = sortedQualities[i]; + break; + } + } + if (nearestChoseQuality) return nearestChoseQuality; + + // chosen quality or lower doesn't exist, try higher + for (let i = chosenQualityIndex; i >= 0; i -= 1) { + if (availableQualites.includes(sortedQualities[i])) { + nearestChoseQuality = sortedQualities[i]; + break; + } + } + return nearestChoseQuality; +} + +export function selectQuality( + source: SourceSliceSource, + qualityPreferences: QualityStore["quality"] +): { stream: LoadableSource; quality: null | SourceQuality; } { @@ -43,13 +84,14 @@ export function selectQuality(source: SourceSliceSource): { quality: null, }; if (source.type === "file") { - const bestQuality = sortedQualities.find( - (v) => source.qualities[v] && (source.qualities[v]?.url.length ?? 0) > 0 - ); - if (bestQuality) { - const stream = source.qualities[bestQuality]; + const availableQualities = Object.entries(source.qualities) + .filter((entry) => (entry[1].url.length ?? 0) > 0) + .map((entry) => entry[0]) as SourceQuality[]; + const quality = getPreferredQuality(availableQualities, qualityPreferences); + if (quality) { + const stream = source.qualities[quality]; if (stream) { - return { stream, quality: bestQuality }; + return { stream, quality }; } } } diff --git a/src/stores/quality/index.ts b/src/stores/quality/index.ts new file mode 100644 index 00000000..e5c8e48d --- /dev/null +++ b/src/stores/quality/index.ts @@ -0,0 +1,38 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; +import { immer } from "zustand/middleware/immer"; + +import { SourceQuality } from "@/stores/player/utils/qualities"; + +export interface QualityStore { + quality: { + lastChosenQuality: SourceQuality | null; + automaticQuality: boolean; + }; + setLastChosenQuality(v: SourceQuality | null): void; + setAutomaticQuality(v: boolean): void; +} + +export const useQualityStore = create( + persist( + immer((set) => ({ + quality: { + automaticQuality: true, + lastChosenQuality: null, + }, + setLastChosenQuality(v) { + set((s) => { + s.quality.lastChosenQuality = v; + }); + }, + setAutomaticQuality(v) { + set((s) => { + s.quality.automaticQuality = v; + }); + }, + })), + { + name: "__MW::quality", + } + ) +);