mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
adjust confidence based on adjustments to prior skip
This commit is contained in:
parent
fcf9fbb56e
commit
cb1964492e
1 changed files with 116 additions and 26 deletions
|
|
@ -1,45 +1,95 @@
|
|||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import { useSkipTracking } from "@/components/player/hooks/useSkipTracking";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
// Import SkipEvent type
|
||||
type SkipEvent = NonNullable<ReturnType<typeof useSkipTracking>["latestSkip"]>;
|
||||
|
||||
/**
|
||||
* Component that tracks and reports completed skip sessions to analytics backend.
|
||||
* Sessions are detected when users accumulate 30+ seconds of forward movement
|
||||
* within a 5-second window and end after 8 seconds of no activity.
|
||||
* Ignores skips that start after 20% of video duration (unlikely to be intro skipping).
|
||||
*/
|
||||
interface PendingSkip {
|
||||
skip: SkipEvent;
|
||||
originalConfidence: number;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
hasBackwardMovement: boolean;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
export function SkipTracker() {
|
||||
const { latestSkip } = useSkipTracking(30);
|
||||
const lastLoggedSkipRef = useRef<number>(0);
|
||||
const [pendingSkips, setPendingSkips] = useState<PendingSkip[]>([]);
|
||||
const lastPlayerTimeRef = useRef<number>(0);
|
||||
|
||||
// Player metadata for context
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
const progress = usePlayerStore((s) => s.progress);
|
||||
const turnstileToken = "";
|
||||
|
||||
const sendSkipAnalytics = useCallback(async () => {
|
||||
if (!latestSkip) return;
|
||||
const sendSkipAnalytics = useCallback(
|
||||
async (skip: SkipEvent, adjustedConfidence: number) => {
|
||||
try {
|
||||
await fetch("https://skips.pstream.mov/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
start_time: skip.startTime,
|
||||
end_time: skip.endTime,
|
||||
skip_duration: skip.skipDuration,
|
||||
content_id: meta?.tmdbId,
|
||||
content_type: meta?.type,
|
||||
season_id: meta?.season?.tmdbId,
|
||||
episode_id: meta?.episode?.tmdbId,
|
||||
confidence: adjustedConfidence,
|
||||
turnstile_token: turnstileToken ?? "",
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send skip analytics:", error);
|
||||
}
|
||||
},
|
||||
[meta, turnstileToken],
|
||||
);
|
||||
|
||||
try {
|
||||
await fetch("https://skips.pstream.mov/send", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
start_time: latestSkip.startTime,
|
||||
end_time: latestSkip.endTime,
|
||||
skip_duration: latestSkip.skipDuration,
|
||||
content_id: meta?.tmdbId,
|
||||
content_type: meta?.type,
|
||||
season_id: meta?.season?.tmdbId,
|
||||
episode_id: meta?.episode?.tmdbId,
|
||||
confidence: latestSkip.confidence,
|
||||
turnstile_token: turnstileToken ?? "",
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to send skip analytics:", error);
|
||||
}
|
||||
}, [latestSkip, meta]);
|
||||
const createPendingSkip = useCallback(
|
||||
(skip: SkipEvent) => {
|
||||
const timer = setTimeout(() => {
|
||||
// Timer expired, send analytics with final confidence
|
||||
setPendingSkips((prev) => {
|
||||
const pendingSkip = prev.find(
|
||||
(p) => p.skip.timestamp === skip.timestamp,
|
||||
);
|
||||
if (!pendingSkip) return prev;
|
||||
|
||||
const adjustedConfidence = pendingSkip.hasBackwardMovement
|
||||
? Math.max(0.1, pendingSkip.originalConfidence * 0.5) // Reduce confidence by half if adjusted
|
||||
: pendingSkip.originalConfidence;
|
||||
|
||||
// Send analytics
|
||||
sendSkipAnalytics(pendingSkip.skip, adjustedConfidence);
|
||||
|
||||
// Remove from pending
|
||||
return prev.filter((p) => p.skip.timestamp !== skip.timestamp);
|
||||
});
|
||||
}, 10000); // 10 second delay
|
||||
|
||||
return {
|
||||
skip,
|
||||
originalConfidence: skip.confidence,
|
||||
startTime: progress.time,
|
||||
endTime: skip.endTime,
|
||||
hasBackwardMovement: false,
|
||||
timer,
|
||||
};
|
||||
},
|
||||
[progress.time, sendSkipAnalytics],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestSkip || !meta) return;
|
||||
|
|
@ -51,11 +101,51 @@ export function SkipTracker() {
|
|||
// eslint-disable-next-line no-console
|
||||
console.log(`Skip session completed: ${latestSkip.skipDuration}s total`);
|
||||
|
||||
// Send analytics data to backend
|
||||
sendSkipAnalytics();
|
||||
// Create pending skip with 10-second delay
|
||||
const pendingSkip = createPendingSkip(latestSkip);
|
||||
setPendingSkips((prev) => [...prev, pendingSkip]);
|
||||
|
||||
lastLoggedSkipRef.current = latestSkip.timestamp;
|
||||
}, [latestSkip, meta, sendSkipAnalytics]);
|
||||
}, [latestSkip, meta, createPendingSkip]);
|
||||
|
||||
// Monitor for backward movements during pending skip periods
|
||||
useEffect(() => {
|
||||
const currentTime = progress.time;
|
||||
|
||||
// Check for backward movement
|
||||
if (
|
||||
lastPlayerTimeRef.current > 0 &&
|
||||
currentTime < lastPlayerTimeRef.current
|
||||
) {
|
||||
// Backward movement detected, mark relevant pending skips as adjusted
|
||||
setPendingSkips((prev) =>
|
||||
prev.map((pending) => {
|
||||
// Check if we're within the monitoring period (between start and end time of skip)
|
||||
const isWithinSkipRange =
|
||||
currentTime >= pending.startTime && currentTime <= pending.endTime;
|
||||
if (isWithinSkipRange && !pending.hasBackwardMovement) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`Backward adjustment detected for skip, reducing confidence`,
|
||||
);
|
||||
return { ...pending, hasBackwardMovement: true };
|
||||
}
|
||||
return pending;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
lastPlayerTimeRef.current = currentTime;
|
||||
}, [progress.time]);
|
||||
|
||||
// Cleanup timers on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
pendingSkips.forEach((pending) => {
|
||||
clearTimeout(pending.timer);
|
||||
});
|
||||
};
|
||||
}, [pendingSkips]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue