File size: 5,597 Bytes
d2aea71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// assets/js/video_segment.js
const DEFAULT_FPS = 30;

/**
 * Playback rules by label (case-insensitive):
 *
 * time                      : video start -> endFrame
 * success                   : entire video
 * failure                   : entire video
 * safety conflict           : startFrame -> endFrame
 * safety avoidance          : startFrame -> end of video
 * passive wait              : startFrame -> endFrame
 * redundant retrieval       : startFrame -> end of video
 * task model uncertainty    : startFrame -> end of video
 * capability miscalibration : startFrame -> endFrame
 * missed grab               : (startFrame-10) -> (endFrame+50 or end of video)
 * slippage                  : (startFrame-10) -> (endFrame+50 or end of video)
 *
 * Manual control requirements:
 *  1) If user pauses, DO NOT auto-resume.
 *  2) If user seeks/scrubs anywhere, allow it. When playback reaches the window end,
 *     it loops back to the window start (and continues only if user is playing).
 */

function normalizeLabel(label) {
    return String(label || "").trim().toLowerCase();
}

function computeWindowFrames(label, startFrame, endFrame) {
    const lab = normalizeLabel(label);

    let s = Number(startFrame);
    let e = Number(endFrame);

    if (!Number.isFinite(s)) s = 0;
    if (!Number.isFinite(e)) e = 0;
    if (s > e) [s, e] = [e, s];

    let start = s;
    let end = e;
    let endIsVideo = false;

    const isTime = lab === "time";
    const isSuccess = lab === "success";
    const isFailure = lab === "failure";

    const isSafetyConflict = lab === "safety conflict";
    const isSafetyAvoidance = lab === "safety avoidance";
    const isPassiveWait = lab === "passive wait";
    const isRedundantRetrieval = lab === "redundant retrieval";
    const isTMU = lab === "task model uncertainty";
    const isCapMis = lab === "capability miscalibration";
    const isMissedGrab = lab === "missed grab";
    const isSlippage = lab === "slippage";

    if (isTime) {
        start = 0;
        end = e;
        endIsVideo = false;
    } else if (isSuccess || isFailure) {
        start = 0;
        endIsVideo = true;
    } else if (isSafetyConflict || isPassiveWait || isCapMis) {
        start = s;
        end = e;
        endIsVideo = false;
    } else if (isSafetyAvoidance || isRedundantRetrieval || isTMU) {
        start = s;
        endIsVideo = true;
    } else if (isMissedGrab || isSlippage) {
        start = Math.max(0, s - 10);
        end = e + 50; // will clamp to duration
        endIsVideo = false;
    } else {
        // Unknown label: just use annotated segment
        start = s;
        end = e;
        endIsVideo = false;
    }

    start = Math.max(0, Math.floor(start));
    if (!endIsVideo) end = Math.max(start, Math.floor(end));

    return { startFrameAdj: start, endFrameAdj: end, endIsVideo };
}

/**
 * Attaches labeled looping to a <video>, while preserving manual user control.
 *
 * - Autoplays (muted) so the page is lively.
 * - If user pauses, we do NOT force play().
 * - If user seeks anywhere, we allow it.
 * - When playback crosses the computed end time, we jump back to the start time.
 *   If the video was playing, we keep playing. If paused, it stays paused.
 */
function attachLabeledLoop(videoEl, { label, startFrame, endFrame, fps = DEFAULT_FPS }) {
    const { startFrameAdj, endFrameAdj, endIsVideo } = computeWindowFrames(label, startFrame, endFrame);

    const startSec = Math.max(0, startFrameAdj / fps);
    let endSec = Math.max(startSec, endFrameAdj / fps);

    // Autoplay compatibility: muted + playsInline
    videoEl.muted = true;
    videoEl.playsInline = true;

    // Track whether a pause was user-initiated (so we never override it)
    let userPaused = false;

    function safePlay() {
        const p = videoEl.play();
        if (p && typeof p.catch === "function") p.catch(() => { });
    }

    function jumpToStart(keepPaused) {
        try {
            videoEl.currentTime = startSec;
        } catch (_) { }
        if (!keepPaused) safePlay();
    }

    // Setup final endSec based on duration once known
    videoEl.addEventListener("loadedmetadata", () => {
        const dur = Number.isFinite(videoEl.duration) ? videoEl.duration : null;

        if (dur != null) {
            if (endIsVideo) {
                endSec = dur;
            } else if (endSec > dur) {
                endSec = dur;
            }
        }

        // Start at window start and autoplay (muted)
        userPaused = false;
        jumpToStart(false);
    });

    // If user pauses, respect it
    videoEl.addEventListener("pause", () => {
        // If it paused because it reached the natural end (rare), userPaused still ok
        userPaused = true;
    });

    // If user hits play, allow it (resume normal looping)
    videoEl.addEventListener("play", () => {
        userPaused = false;
    });

    // Core loop logic: when reaching endSec, loop back to startSec
    videoEl.addEventListener("timeupdate", () => {
        // epsilon to prevent jitter
        if (endSec <= startSec + 0.01) return;

        if (videoEl.currentTime >= endSec - 0.02) {
            // Loop back, but DO NOT resume if user paused
            const keepPaused = videoEl.paused || userPaused;
            jumpToStart(keepPaused);
        }
    });

    // IMPORTANT CHANGE: allow free seeking/scrubbing.
    // No snapping during seek; we only enforce the window when time reaches endSec.
}

window.DEFAULT_FPS = DEFAULT_FPS;
window.attachLabeledLoop = attachLabeledLoop;
window.computeWindowFrames = computeWindowFrames;