mirror of
https://github.com/p-stream/p-stream.git
synced 2026-05-13 12:30:57 +00:00
Merge branch 'production' into production
This commit is contained in:
commit
fedd414629
9 changed files with 89 additions and 265 deletions
|
|
@ -735,10 +735,6 @@
|
||||||
"device": "device",
|
"device": "device",
|
||||||
"enabled": "Casting to device 🎬"
|
"enabled": "Casting to device 🎬"
|
||||||
},
|
},
|
||||||
"skipIntro": {
|
|
||||||
"feedback": "Was this skip helpful?",
|
|
||||||
"skip": "Skip Intro"
|
|
||||||
},
|
|
||||||
"menus": {
|
"menus": {
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"button": "Attempt download",
|
"button": "Attempt download",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useSkipTracking } from "@/components/player/hooks/useSkipTracking";
|
import { useSkipTracking } from "@/components/player/hooks/useSkipTracking";
|
||||||
|
|
@ -51,14 +50,6 @@ export function SkipIntroButton(props: {
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const { addSkipEvent } = useSkipTracking(20);
|
const { addSkipEvent } = useSkipTracking(20);
|
||||||
const [showFeedback, setShowFeedback] = useState(false);
|
|
||||||
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
|
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const pendingSkipDataRef = useRef<{
|
|
||||||
startTime: number;
|
|
||||||
endTime: number;
|
|
||||||
skipDuration: number;
|
|
||||||
} | null>(null);
|
|
||||||
const showingState = shouldShowSkipButton(time, props.skipTime);
|
const showingState = shouldShowSkipButton(time, props.skipTime);
|
||||||
const animation = showingState === "hover" ? "slide-up" : "fade";
|
const animation = showingState === "hover" ? "slide-up" : "fade";
|
||||||
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
|
let bottom = "bottom-[calc(6rem+env(safe-area-inset-bottom))]";
|
||||||
|
|
@ -68,19 +59,20 @@ export function SkipIntroButton(props: {
|
||||||
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
|
: "bottom-[calc(3rem+env(safe-area-inset-bottom))]";
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const handleSkip = useCallback(() => {
|
||||||
|
if (typeof props.skipTime === "number" && display) {
|
||||||
|
const startTime = time;
|
||||||
|
const endTime = props.skipTime;
|
||||||
|
const skipDuration = endTime - startTime;
|
||||||
|
|
||||||
const reportSkip = useCallback(
|
display.setTime(props.skipTime);
|
||||||
(confidence: number) => {
|
|
||||||
if (!pendingSkipDataRef.current) return;
|
|
||||||
|
|
||||||
const { startTime, endTime, skipDuration } = pendingSkipDataRef.current;
|
|
||||||
|
|
||||||
|
// Add manual skip event with high confidence (user explicitly clicked skip intro)
|
||||||
addSkipEvent({
|
addSkipEvent({
|
||||||
startTime,
|
startTime,
|
||||||
endTime,
|
endTime,
|
||||||
skipDuration,
|
skipDuration,
|
||||||
confidence,
|
confidence: 0.95, // High confidence for explicit user action
|
||||||
meta: meta
|
meta: meta
|
||||||
? {
|
? {
|
||||||
title:
|
title:
|
||||||
|
|
@ -95,99 +87,15 @@ export function SkipIntroButton(props: {
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`Skip intro reported: ${skipDuration}s total, confidence: ${confidence}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
pendingSkipDataRef.current = null;
|
|
||||||
setShowFeedback(false);
|
|
||||||
setFeedbackSubmitted(true);
|
|
||||||
if (timeoutRef.current) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
timeoutRef.current = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[addSkipEvent, meta],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleThumbsUp = useCallback(() => {
|
|
||||||
reportSkip(0.95);
|
|
||||||
}, [reportSkip]);
|
|
||||||
|
|
||||||
const handleThumbsDown = useCallback(() => {
|
|
||||||
reportSkip(0.7);
|
|
||||||
}, [reportSkip]);
|
|
||||||
|
|
||||||
const handleSkip = useCallback(() => {
|
|
||||||
if (typeof props.skipTime === "number" && display) {
|
|
||||||
const startTime = time;
|
|
||||||
const endTime = props.skipTime;
|
|
||||||
const skipDuration = endTime - startTime;
|
|
||||||
|
|
||||||
display.setTime(props.skipTime);
|
|
||||||
|
|
||||||
// Store skip data temporarily
|
|
||||||
pendingSkipDataRef.current = {
|
|
||||||
startTime,
|
|
||||||
endTime,
|
|
||||||
skipDuration,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show feedback UI
|
|
||||||
setShowFeedback(true);
|
|
||||||
setFeedbackSubmitted(false);
|
|
||||||
|
|
||||||
// Start 10-second timeout
|
|
||||||
timeoutRef.current = setTimeout(() => {
|
|
||||||
// Hide component immediately to prevent flicker
|
|
||||||
setShowFeedback(false);
|
|
||||||
setFeedbackSubmitted(true);
|
|
||||||
reportSkip(0.8);
|
|
||||||
}, 10000);
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Skip intro button used: ${skipDuration}s total`);
|
console.log(`Skip intro button used: ${skipDuration}s total`);
|
||||||
}
|
}
|
||||||
}, [props.skipTime, display, time, reportSkip]);
|
}, [props.skipTime, display, time, addSkipEvent, meta]);
|
||||||
|
|
||||||
// Reset feedback state when content changes
|
|
||||||
useEffect(() => {
|
|
||||||
setShowFeedback(false);
|
|
||||||
setFeedbackSubmitted(false);
|
|
||||||
if (timeoutRef.current) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
timeoutRef.current = null;
|
|
||||||
}
|
|
||||||
pendingSkipDataRef.current = null;
|
|
||||||
}, [meta?.tmdbId]);
|
|
||||||
|
|
||||||
// Cleanup timeout on unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (timeoutRef.current) {
|
|
||||||
clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!props.inControl) return null;
|
if (!props.inControl) return null;
|
||||||
|
|
||||||
let show = false;
|
let show = false;
|
||||||
// Don't show anything if feedback has been submitted
|
if (showingState === "always") show = true;
|
||||||
if (feedbackSubmitted) {
|
else if (showingState === "hover" && props.controlsShowing) show = true;
|
||||||
show = false;
|
|
||||||
} else if (showFeedback) {
|
|
||||||
// Always show feedback UI when active
|
|
||||||
show = true;
|
|
||||||
} else if (showingState === "always") {
|
|
||||||
// Show skip button when always visible
|
|
||||||
show = true;
|
|
||||||
} else if (showingState === "hover" && props.controlsShowing) {
|
|
||||||
// Show skip button on hover when controls are showing
|
|
||||||
show = true;
|
|
||||||
}
|
|
||||||
if (status !== "playing") show = false;
|
if (status !== "playing") show = false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -198,52 +106,17 @@ export function SkipIntroButton(props: {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames([
|
className={classNames([
|
||||||
"absolute bottom-0 right-0 transition-[bottom] duration-200 flex items-center",
|
"absolute bottom-0 right-0 transition-[bottom] duration-200 flex items-center space-x-3",
|
||||||
showFeedback ? "flex-col space-y-2" : "space-x-3",
|
|
||||||
bottom,
|
bottom,
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{showFeedback ? (
|
<Button
|
||||||
<>
|
onClick={handleSkip}
|
||||||
<div className="text-sm font-medium text-white">
|
className="bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText flex justify-center items-center"
|
||||||
{t("player.skipIntro.feedback")}
|
>
|
||||||
</div>
|
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} />
|
||||||
<div className="flex items-center space-x-3">
|
Skip Intro
|
||||||
<button
|
</Button>
|
||||||
type="button"
|
|
||||||
onClick={handleThumbsUp}
|
|
||||||
className={classNames(
|
|
||||||
"h-10 w-10 rounded-full flex items-center justify-center",
|
|
||||||
"bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText",
|
|
||||||
"scale-95 hover:scale-100 transition-all duration-200",
|
|
||||||
)}
|
|
||||||
aria-label="Thumbs up"
|
|
||||||
>
|
|
||||||
<Icon className="text-xl" icon={Icons.THUMBS_UP} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleThumbsDown}
|
|
||||||
className={classNames(
|
|
||||||
"h-10 w-10 rounded-full flex items-center justify-center",
|
|
||||||
"bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText",
|
|
||||||
"scale-95 hover:scale-100 transition-all duration-200",
|
|
||||||
)}
|
|
||||||
aria-label="Thumbs down"
|
|
||||||
>
|
|
||||||
<Icon className="text-xl" icon={Icons.THUMBS_DOWN} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
onClick={handleSkip}
|
|
||||||
className="bg-buttons-primary hover:bg-buttons-primaryHover text-buttons-primaryText flex justify-center items-center"
|
|
||||||
>
|
|
||||||
<Icon className="text-xl mr-1" icon={Icons.SKIP_EPISODE} />
|
|
||||||
{t("player.skipIntro.skip")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -471,8 +471,8 @@ export function CaptionSettingsView({
|
||||||
</span>
|
</span>
|
||||||
<CaptionDelay
|
<CaptionDelay
|
||||||
label={t("player.menus.subtitles.settings.delay")}
|
label={t("player.menus.subtitles.settings.delay")}
|
||||||
max={20}
|
max={40}
|
||||||
min={-20}
|
min={-40}
|
||||||
onChange={(v) => setDelay(v)}
|
onChange={(v) => setDelay(v)}
|
||||||
value={delay}
|
value={delay}
|
||||||
textTransformer={(s) => `${s}s`}
|
textTransformer={(s) => `${s}s`}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ function useIsIosHls() {
|
||||||
export function QualityView({ id }: { id: string }) {
|
export function QualityView({ id }: { id: string }) {
|
||||||
const router = useOverlayRouter(id);
|
const router = useOverlayRouter(id);
|
||||||
const isIosHls = useIsIosHls();
|
const isIosHls = useIsIosHls();
|
||||||
|
const sourceType = usePlayerStore((s) => s.source?.type);
|
||||||
const availableQualities = usePlayerStore((s) => s.qualities);
|
const availableQualities = usePlayerStore((s) => s.qualities);
|
||||||
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
const currentQuality = usePlayerStore((s) => s.currentQuality);
|
||||||
const switchQuality = usePlayerStore((s) => s.switchQuality);
|
const switchQuality = usePlayerStore((s) => s.switchQuality);
|
||||||
|
|
@ -50,14 +51,18 @@ export function QualityView({ id }: { id: string }) {
|
||||||
const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality);
|
const setLastChosenQuality = useQualityStore((s) => s.setLastChosenQuality);
|
||||||
const autoQuality = useQualityStore((s) => s.quality.automaticQuality);
|
const autoQuality = useQualityStore((s) => s.quality.automaticQuality);
|
||||||
|
|
||||||
|
// Auto quality only makes sense for HLS sources
|
||||||
|
const supportsAutoQuality = sourceType === "hls";
|
||||||
|
|
||||||
const change = useCallback(
|
const change = useCallback(
|
||||||
(q: SourceQuality) => {
|
(q: SourceQuality) => {
|
||||||
setLastChosenQuality(q);
|
setLastChosenQuality(q);
|
||||||
setAutomaticQuality(false);
|
// Don't disable auto quality when manually selecting a quality
|
||||||
|
// Keep auto quality enabled by default unless user explicitly toggles it
|
||||||
switchQuality(q);
|
switchQuality(q);
|
||||||
router.close();
|
router.close();
|
||||||
},
|
},
|
||||||
[router, switchQuality, setLastChosenQuality, setAutomaticQuality],
|
[router, switchQuality, setLastChosenQuality],
|
||||||
);
|
);
|
||||||
|
|
||||||
const changeAutomatic = useCallback(() => {
|
const changeAutomatic = useCallback(() => {
|
||||||
|
|
@ -90,12 +95,18 @@ export function QualityView({ id }: { id: string }) {
|
||||||
{qualityToString(v)}
|
{qualityToString(v)}
|
||||||
</SelectableLink>
|
</SelectableLink>
|
||||||
))}
|
))}
|
||||||
<Menu.Divider />
|
{supportsAutoQuality && (
|
||||||
<Menu.Link
|
<>
|
||||||
rightSide={<Toggle onClick={changeAutomatic} enabled={autoQuality} />}
|
<Menu.Divider />
|
||||||
>
|
<Menu.Link
|
||||||
{t("player.menus.quality.automaticLabel")}
|
rightSide={
|
||||||
</Menu.Link>
|
<Toggle onClick={changeAutomatic} enabled={autoQuality} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("player.menus.quality.automaticLabel")}
|
||||||
|
</Menu.Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Menu.SmallText>
|
<Menu.SmallText>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={
|
i18nKey={
|
||||||
|
|
|
||||||
|
|
@ -313,9 +313,24 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
||||||
const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]);
|
const quality = hlsLevelToQuality(hls.levels[hls.currentLevel]);
|
||||||
emit("changedquality", quality);
|
emit("changedquality", quality);
|
||||||
} else {
|
} else {
|
||||||
// When automatic quality is disabled, re-lock to preferred quality
|
// When automatic quality is disabled, check if current level matches preferred quality
|
||||||
// This prevents HLS.js from switching levels unexpectedly
|
const currentQuality = hlsLevelToQuality(
|
||||||
setupQualityForHls();
|
hls.levels[hls.currentLevel],
|
||||||
|
);
|
||||||
|
const preferredQualityLevel = getPreferredQuality(
|
||||||
|
hlsLevelsToQualities(hls.levels),
|
||||||
|
{
|
||||||
|
lastChosenQuality: preferenceQuality,
|
||||||
|
automaticQuality: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Only re-lock if the current level doesn't match our preferred quality
|
||||||
|
if (currentQuality !== preferredQualityLevel) {
|
||||||
|
setupQualityForHls();
|
||||||
|
} else {
|
||||||
|
// Emit the quality change since we're now at the correct level
|
||||||
|
emit("changedquality", currentQuality);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, () => {
|
hls.on(Hls.Events.SUBTITLE_TRACK_LOADED, () => {
|
||||||
|
|
|
||||||
|
|
@ -29,33 +29,8 @@ interface SkipTrackingResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate confidence score for automatic skip detection
|
* Hook that tracks manual skip events and monitors user behavior patterns for confidence adjustment.
|
||||||
* Based on skip duration and timing within the video
|
* Only processes skip events added via addSkipEvent (e.g., from skip intro button).
|
||||||
*/
|
|
||||||
function calculateSkipConfidence(
|
|
||||||
skipDuration: number,
|
|
||||||
startTime: number,
|
|
||||||
duration: number,
|
|
||||||
): number {
|
|
||||||
// Duration confidence: longer skips are more confident
|
|
||||||
// 20s = 0.4, 40s = 0.6, 60s+ = 0.85
|
|
||||||
const durationConfidence = Math.min(0.85, 0.4 + (skipDuration - 20) * 0.01);
|
|
||||||
|
|
||||||
// Timing confidence: earlier skips are more confident
|
|
||||||
// Start time as percentage of total duration
|
|
||||||
const startPercentage = startTime / duration;
|
|
||||||
// Higher confidence for earlier starts (0% = 1.0, 20% = 0.8)
|
|
||||||
const timingConfidence = Math.max(0.7, 1.0 - startPercentage * 0.75);
|
|
||||||
|
|
||||||
// Combine factors (weighted average)
|
|
||||||
return durationConfidence * 0.6 + timingConfidence * 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook that tracks rapid skipping sessions where users accumulate 20+ seconds of forward
|
|
||||||
* movement within a 5-second window. Sessions continue until 8 seconds pass without
|
|
||||||
* any forward movement, then report the total skip distance. Ignores skips that start
|
|
||||||
* after 20% of video duration (unlikely to be intro skipping).
|
|
||||||
*
|
*
|
||||||
* @param minSkipThreshold Minimum total forward movement in 5-second window to start session (default: 20)
|
* @param minSkipThreshold Minimum total forward movement in 5-second window to start session (default: 20)
|
||||||
* @param maxHistory Maximum number of skip events to keep in history (default: 50)
|
* @param maxHistory Maximum number of skip events to keep in history (default: 50)
|
||||||
|
|
@ -74,7 +49,6 @@ export function useSkipTracking(
|
||||||
// Get current player state
|
// Get current player state
|
||||||
const progress = usePlayerStore((s) => s.progress);
|
const progress = usePlayerStore((s) => s.progress);
|
||||||
const meta = usePlayerStore((s) => s.meta);
|
const meta = usePlayerStore((s) => s.meta);
|
||||||
const duration = progress.duration;
|
|
||||||
|
|
||||||
const clearHistory = useCallback(() => {
|
const clearHistory = useCallback(() => {
|
||||||
setSkipHistory([]);
|
setSkipHistory([]);
|
||||||
|
|
@ -149,60 +123,7 @@ export function useSkipTracking(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isInSkipSessionRef.current && recentEntries.length === 0) {
|
if (isInSkipSessionRef.current && recentEntries.length === 0) {
|
||||||
// Ignore skips that start after 20% of video duration (likely not intro skipping)
|
// Session ended - reset state but DON'T create skip event
|
||||||
const twentyPercentMark = duration * 0.2;
|
|
||||||
if (skipSessionStartRef.current > twentyPercentMark) {
|
|
||||||
// Reset session state without creating event
|
|
||||||
isInSkipSessionRef.current = false;
|
|
||||||
skipSessionStartRef.current = 0;
|
|
||||||
sessionTotalRef.current = 0;
|
|
||||||
skipWindowRef.current = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only report skips where end time is greater than start time
|
|
||||||
if (currentTime <= skipSessionStartRef.current) {
|
|
||||||
// Reset session state without creating event
|
|
||||||
isInSkipSessionRef.current = false;
|
|
||||||
skipSessionStartRef.current = 0;
|
|
||||||
sessionTotalRef.current = 0;
|
|
||||||
skipWindowRef.current = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create skip event for completed session
|
|
||||||
const skipEvent: SkipEvent = {
|
|
||||||
startTime: skipSessionStartRef.current,
|
|
||||||
endTime: currentTime,
|
|
||||||
skipDuration: sessionTotalRef.current,
|
|
||||||
timestamp: now,
|
|
||||||
confidence: calculateSkipConfidence(
|
|
||||||
sessionTotalRef.current,
|
|
||||||
skipSessionStartRef.current,
|
|
||||||
duration,
|
|
||||||
),
|
|
||||||
meta: meta
|
|
||||||
? {
|
|
||||||
title:
|
|
||||||
meta.type === "show" && meta.episode
|
|
||||||
? `${meta.title} - S${meta.season?.number || 0}E${meta.episode.number || 0}`
|
|
||||||
: meta.title,
|
|
||||||
type: meta.type === "movie" ? "Movie" : "TV Show",
|
|
||||||
tmdbId: meta.tmdbId,
|
|
||||||
seasonNumber: meta.season?.number,
|
|
||||||
episodeNumber: meta.episode?.number,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
setSkipHistory((prev) => {
|
|
||||||
const newHistory = [...prev, skipEvent];
|
|
||||||
return newHistory.length > maxHistory
|
|
||||||
? newHistory.slice(newHistory.length - maxHistory)
|
|
||||||
: newHistory;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset session state
|
|
||||||
isInSkipSessionRef.current = false;
|
isInSkipSessionRef.current = false;
|
||||||
skipSessionStartRef.current = 0;
|
skipSessionStartRef.current = 0;
|
||||||
sessionTotalRef.current = 0;
|
sessionTotalRef.current = 0;
|
||||||
|
|
@ -210,7 +131,7 @@ export function useSkipTracking(
|
||||||
}
|
}
|
||||||
|
|
||||||
previousTimeRef.current = currentTime;
|
previousTimeRef.current = currentTime;
|
||||||
}, [progress.time, duration, meta, minSkipThreshold, maxHistory]);
|
}, [progress.time, minSkipThreshold]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Monitor time changes every 100ms to catch rapid skipping
|
// Monitor time changes every 100ms to catch rapid skipping
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,13 @@ export function SkipTracker() {
|
||||||
return {
|
return {
|
||||||
skip,
|
skip,
|
||||||
originalConfidence: skip.confidence,
|
originalConfidence: skip.confidence,
|
||||||
startTime: progress.time,
|
startTime: skip.startTime,
|
||||||
endTime: skip.endTime,
|
endTime: skip.endTime,
|
||||||
hasBackwardMovement: false,
|
hasBackwardMovement: false,
|
||||||
timer,
|
timer,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[progress.time, sendSkipAnalytics],
|
[sendSkipAnalytics],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -518,23 +518,23 @@ export function FebboxSetup({
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
<div className="flex justify-between items-center gap-4 mt-6">
|
||||||
|
<div className="my-3">
|
||||||
|
<p className="max-w-[32rem] font-medium">
|
||||||
|
{t("fedapi.setup.useMp4")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Toggle
|
||||||
|
onClick={() =>
|
||||||
|
preferences.setFebboxUseMp4(!preferences.febboxUseMp4)
|
||||||
|
}
|
||||||
|
enabled={preferences.febboxUseMp4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex justify-between items-center gap-4 mt-6">
|
|
||||||
<div className="my-3">
|
|
||||||
<p className="max-w-[32rem] font-medium">
|
|
||||||
{t("fedapi.setup.useMp4")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Toggle
|
|
||||||
onClick={() =>
|
|
||||||
preferences.setFebboxUseMp4(!preferences.febboxUseMp4)
|
|
||||||
}
|
|
||||||
enabled={preferences.febboxUseMp4}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<Modal id={exampleModal.id}>
|
<Modal id={exampleModal.id}>
|
||||||
<ModalCard>
|
<ModalCard>
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,15 @@ export function selectQuality(
|
||||||
const availableQualities = Object.entries(source.qualities)
|
const availableQualities = Object.entries(source.qualities)
|
||||||
.filter((entry) => (entry[1].url.length ?? 0) > 0)
|
.filter((entry) => (entry[1].url.length ?? 0) > 0)
|
||||||
.map((entry) => entry[0]) as SourceQuality[];
|
.map((entry) => entry[0]) as SourceQuality[];
|
||||||
const quality = getPreferredQuality(availableQualities, qualityPreferences);
|
// For file sources (MP4), always use manual quality selection since they don't support switching
|
||||||
|
const manualQualityPreferences = {
|
||||||
|
...qualityPreferences,
|
||||||
|
automaticQuality: false,
|
||||||
|
};
|
||||||
|
const quality = getPreferredQuality(
|
||||||
|
availableQualities,
|
||||||
|
manualQualityPreferences,
|
||||||
|
);
|
||||||
if (quality) {
|
if (quality) {
|
||||||
const stream = source.qualities[quality];
|
const stream = source.qualities[quality];
|
||||||
if (stream) {
|
if (stream) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue