mishig HF Staff commited on
Commit
520fe8f
·
verified ·
1 Parent(s): 3a26e9c

Sync from GitHub via hub-sync

Browse files
Files changed (2) hide show
  1. next.config.ts +6 -0
  2. src/components/videos-player.tsx +0 -425
next.config.ts CHANGED
@@ -9,6 +9,12 @@ const nextConfig: NextConfig = {
9
  ignoreDuringBuilds: true,
10
  },
11
  transpilePackages: ["three"],
 
 
 
 
 
 
12
  generateBuildId: () => packageJson.version,
13
  };
14
 
 
9
  ignoreDuringBuilds: true,
10
  },
11
  transpilePackages: ["three"],
12
+ // Avoid the 200-800ms cold-start cost of barrel-file imports.
13
+ // react-icons re-exports thousands of icon components from /fa, etc.;
14
+ // recharts and @huggingface/hub also have wide entry surfaces.
15
+ experimental: {
16
+ optimizePackageImports: ["react-icons", "recharts", "@huggingface/hub"],
17
+ },
18
  generateBuildId: () => packageJson.version,
19
  };
20
 
src/components/videos-player.tsx DELETED
@@ -1,425 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useRef, useState, useMemo, useCallback } from "react";
4
- import { useTime } from "../context/time-context";
5
- import { FaExpand, FaCompress, FaTimes, FaEye } from "react-icons/fa";
6
- import type { VideoInfo } from "@/types";
7
-
8
- type VideoPlayerProps = {
9
- videosInfo: VideoInfo[];
10
- onVideosReady?: () => void;
11
- };
12
-
13
- const videoCleanupHandlers = new WeakMap<HTMLVideoElement, () => void>();
14
- const videoReadyHandlers = new WeakMap<HTMLVideoElement, EventListener>();
15
-
16
- const VIDEO_SYNC_TOLERANCE = 0.2;
17
-
18
- export const VideosPlayer = ({
19
- videosInfo,
20
- onVideosReady,
21
- }: VideoPlayerProps) => {
22
- const { currentTime, seek, isPlaying, setIsPlaying } = useTime();
23
- const videoRefs = useRef<HTMLVideoElement[]>([]);
24
- const [hiddenVideos, setHiddenVideos] = useState<string[]>([]);
25
-
26
- const hiddenSet = useMemo(() => new Set(hiddenVideos), [hiddenVideos]);
27
-
28
- const firstVisibleIdx = videosInfo.findIndex(
29
- (video) => !hiddenSet.has(video.filename),
30
- );
31
- const visibleCount = videosInfo.filter(
32
- (video) => !hiddenSet.has(video.filename),
33
- ).length;
34
- const [enlargedVideo, setEnlargedVideo] = useState<string | null>(null);
35
- const prevHiddenVideosRef = useRef<string[]>([]);
36
- const videoContainerRefs = useRef<Record<string, HTMLDivElement | null>>({});
37
- const [showHiddenMenu, setShowHiddenMenu] = useState(false);
38
- const hiddenMenuRef = useRef<HTMLDivElement | null>(null);
39
- const showHiddenBtnRef = useRef<HTMLButtonElement | null>(null);
40
- const [videoCodecError, setVideoCodecError] = useState(false);
41
-
42
- // Tracks the last time value set by the primary video's onTimeUpdate.
43
- // If currentTime differs from this, an external source (slider/chart click) changed it.
44
- const lastVideoTimeRef = useRef(0);
45
-
46
- // Initialize video refs
47
- useEffect(() => {
48
- videoRefs.current = videoRefs.current.slice(0, videosInfo.length);
49
- }, [videosInfo]);
50
-
51
- // When videos get unhidden, sync their time and resume playback
52
- useEffect(() => {
53
- const prevHidden = prevHiddenVideosRef.current;
54
- const newlyUnhidden = prevHidden.filter(
55
- (filename) => !hiddenSet.has(filename),
56
- );
57
- if (newlyUnhidden.length > 0) {
58
- const unhiddenNames = new Set(newlyUnhidden);
59
- videosInfo.forEach((video, idx) => {
60
- if (unhiddenNames.has(video.filename)) {
61
- const ref = videoRefs.current[idx];
62
- if (ref) {
63
- ref.currentTime = currentTime;
64
- if (isPlaying) {
65
- ref.play().catch(() => {});
66
- }
67
- }
68
- }
69
- });
70
- }
71
- prevHiddenVideosRef.current = hiddenVideos;
72
- }, [hiddenVideos, hiddenSet, isPlaying, videosInfo, currentTime]);
73
-
74
- // Check video codec support
75
- useEffect(() => {
76
- const checkCodecSupport = () => {
77
- const dummyVideo = document.createElement("video");
78
- const canPlayVideos = dummyVideo.canPlayType(
79
- 'video/mp4; codecs="av01.0.05M.08"',
80
- );
81
- setVideoCodecError(!canPlayVideos);
82
- };
83
-
84
- checkCodecSupport();
85
- }, []);
86
-
87
- // Handle play/pause — skip hidden videos to avoid wasting decoder time
88
- useEffect(() => {
89
- videoRefs.current.forEach((video, idx) => {
90
- if (!video || hiddenSet.has(videosInfo[idx]?.filename)) return;
91
- if (isPlaying) {
92
- video.play().catch(() => console.error("Error playing video"));
93
- } else {
94
- video.pause();
95
- }
96
- });
97
- }, [isPlaying, hiddenSet, videosInfo]);
98
-
99
- // Minimize enlarged video on Escape key
100
- useEffect(() => {
101
- if (!enlargedVideo) return;
102
- const handleKeyDown = (e: KeyboardEvent) => {
103
- if (e.key === "Escape") {
104
- setEnlargedVideo(null);
105
- }
106
- };
107
- window.addEventListener("keydown", handleKeyDown);
108
- // Scroll enlarged video into view
109
- const ref = videoContainerRefs.current[enlargedVideo];
110
- if (ref) {
111
- ref.scrollIntoView();
112
- }
113
- return () => {
114
- window.removeEventListener("keydown", handleKeyDown);
115
- };
116
- }, [enlargedVideo]);
117
-
118
- // Close hidden videos dropdown on outside click
119
- useEffect(() => {
120
- if (!showHiddenMenu) return;
121
- function handleClick(e: MouseEvent) {
122
- const menu = hiddenMenuRef.current;
123
- const btn = showHiddenBtnRef.current;
124
- if (
125
- menu &&
126
- !menu.contains(e.target as Node) &&
127
- btn &&
128
- !btn.contains(e.target as Node)
129
- ) {
130
- setShowHiddenMenu(false);
131
- }
132
- }
133
- document.addEventListener("mousedown", handleClick);
134
- return () => document.removeEventListener("mousedown", handleClick);
135
- }, [showHiddenMenu]);
136
-
137
- // Close dropdown if no hidden videos
138
- useEffect(() => {
139
- if (hiddenVideos.length === 0 && showHiddenMenu) {
140
- setShowHiddenMenu(false);
141
- }
142
- // Minimize if enlarged video is hidden
143
- if (enlargedVideo && hiddenVideos.includes(enlargedVideo)) {
144
- setEnlargedVideo(null);
145
- }
146
- }, [hiddenVideos, showHiddenMenu, enlargedVideo]);
147
-
148
- // Sync all video times when currentTime changes.
149
- // For the primary video, only seek when the change came from an external source
150
- // (slider drag, chart click, etc.) — detected by comparing against lastVideoTimeRef.
151
- useEffect(() => {
152
- const isExternalSeek =
153
- Math.abs(currentTime - lastVideoTimeRef.current) > 0.3;
154
-
155
- videoRefs.current.forEach((video, index) => {
156
- if (!video) return;
157
- if (hiddenSet.has(videosInfo[index]?.filename)) return;
158
- if (index === firstVisibleIdx && !isExternalSeek) return;
159
-
160
- const videoInfo = videosInfo[index];
161
- if (videoInfo?.isSegmented) {
162
- const segmentStart = videoInfo.segmentStart || 0;
163
- const segmentTime = segmentStart + currentTime;
164
- if (Math.abs(video.currentTime - segmentTime) > VIDEO_SYNC_TOLERANCE) {
165
- video.currentTime = segmentTime;
166
- }
167
- } else {
168
- if (Math.abs(video.currentTime - currentTime) > VIDEO_SYNC_TOLERANCE) {
169
- video.currentTime = currentTime;
170
- }
171
- }
172
- });
173
- }, [currentTime, videosInfo, firstVisibleIdx, hiddenSet]);
174
-
175
- // Stable per-index timeupdate handlers avoid findIndex scan on every event
176
- const makeTimeUpdateHandler = useCallback(
177
- (index: number) => {
178
- return () => {
179
- const video = videoRefs.current[index];
180
- if (!video || !video.duration) return;
181
- const videoInfo = videosInfo[index];
182
-
183
- if (videoInfo?.isSegmented) {
184
- const segmentStart = videoInfo.segmentStart || 0;
185
- const globalTime = Math.max(0, video.currentTime - segmentStart);
186
- lastVideoTimeRef.current = globalTime;
187
- seek(globalTime);
188
- } else {
189
- lastVideoTimeRef.current = video.currentTime;
190
- seek(video.currentTime);
191
- }
192
- };
193
- },
194
- [videosInfo, seek],
195
- );
196
-
197
- // Handle video ready and setup segmentation
198
- useEffect(() => {
199
- let videosReadyCount = 0;
200
- const onCanPlayThrough = (videoIndex: number) => {
201
- const video = videoRefs.current[videoIndex];
202
- const videoInfo = videosInfo[videoIndex];
203
-
204
- // Setup video segmentation for v3.0 chunked videos
205
- if (video && videoInfo?.isSegmented) {
206
- const segmentStart = videoInfo.segmentStart || 0;
207
- const segmentEnd = videoInfo.segmentEnd || video.duration || 0;
208
-
209
- // Set initial time to segment start if not already set
210
- if (
211
- video.currentTime < segmentStart ||
212
- video.currentTime > segmentEnd
213
- ) {
214
- video.currentTime = segmentStart;
215
- }
216
-
217
- // Add event listener to handle segment boundaries
218
- const handleTimeUpdate = () => {
219
- if (video.currentTime > segmentEnd) {
220
- video.currentTime = segmentStart;
221
- if (!video.loop) {
222
- video.pause();
223
- }
224
- }
225
- };
226
-
227
- video.addEventListener("timeupdate", handleTimeUpdate);
228
-
229
- videoCleanupHandlers.set(video, () => {
230
- video.removeEventListener("timeupdate", handleTimeUpdate);
231
- });
232
- }
233
-
234
- videosReadyCount += 1;
235
- if (videosReadyCount === videosInfo.length) {
236
- if (typeof onVideosReady === "function") {
237
- onVideosReady();
238
- setIsPlaying(true);
239
- }
240
- }
241
- };
242
-
243
- videoRefs.current.forEach((video, index) => {
244
- if (video) {
245
- // If already ready, call the handler immediately
246
- if (video.readyState >= 4) {
247
- onCanPlayThrough(index);
248
- } else {
249
- const readyHandler = () => onCanPlayThrough(index);
250
- video.addEventListener("canplaythrough", readyHandler);
251
- videoReadyHandlers.set(video, readyHandler);
252
- }
253
- }
254
- });
255
-
256
- return () => {
257
- videoRefs.current.forEach((video) => {
258
- if (!video) return;
259
- const readyHandler = videoReadyHandlers.get(video);
260
- if (readyHandler) {
261
- video.removeEventListener("canplaythrough", readyHandler);
262
- videoReadyHandlers.delete(video);
263
- }
264
- const cleanup = videoCleanupHandlers.get(video);
265
- if (cleanup) {
266
- cleanup();
267
- videoCleanupHandlers.delete(video);
268
- }
269
- });
270
- };
271
- }, [videosInfo, onVideosReady, setIsPlaying]);
272
-
273
- return (
274
- <>
275
- {/* Error message */}
276
- {videoCodecError && (
277
- <div className="font-medium text-amber-400">
278
- <p>
279
- Videos could NOT play because{" "}
280
- <a
281
- href="https://en.wikipedia.org/wiki/AV1"
282
- target="_blank"
283
- className="underline"
284
- >
285
- AV1
286
- </a>{" "}
287
- decoding is not available on your browser.
288
- </p>
289
- <ul className="list-inside list-decimal">
290
- <li>
291
- If iPhone:{" "}
292
- <span className="italic">
293
- It is supported with A17 chip or higher.
294
- </span>
295
- </li>
296
- <li>
297
- If Mac with Safari:{" "}
298
- <span className="italic">
299
- It is supported on most browsers except Safari with M1 chip or
300
- higher and on Safari with M3 chip or higher.
301
- </span>
302
- </li>
303
- <li>
304
- Other:{" "}
305
- <span className="italic">
306
- Contact the maintainers on LeRobot discord channel:
307
- </span>
308
- <a
309
- href="https://discord.com/invite/s3KuuzsPFb"
310
- target="_blank"
311
- className="underline"
312
- >
313
- https://discord.com/invite/s3KuuzsPFb
314
- </a>
315
- </li>
316
- </ul>
317
- </div>
318
- )}
319
-
320
- {/* Show Hidden Videos Button */}
321
- {hiddenVideos.length > 0 && (
322
- <div className="relative">
323
- <button
324
- ref={showHiddenBtnRef}
325
- className="flex items-center gap-2 rounded bg-[var(--surface-1)] px-3 py-2 text-sm text-slate-100 hover:bg-white/5 border border-white/10"
326
- onClick={() => setShowHiddenMenu((prev) => !prev)}
327
- >
328
- <FaEye /> Show Hidden Videos ({hiddenVideos.length})
329
- </button>
330
- {showHiddenMenu && (
331
- <div
332
- ref={hiddenMenuRef}
333
- className="absolute left-0 mt-2 w-max rounded border border-white/10 bg-[var(--surface-0)] shadow-lg p-2 z-50"
334
- >
335
- <div className="mb-2 text-xs text-slate-300">
336
- Restore hidden videos:
337
- </div>
338
- {hiddenVideos.map((filename) => (
339
- <button
340
- key={filename}
341
- className="block w-full text-left px-2 py-1 rounded hover:bg-white/5 text-slate-100"
342
- onClick={() =>
343
- setHiddenVideos((prev: string[]) =>
344
- prev.filter((v: string) => v !== filename),
345
- )
346
- }
347
- >
348
- {filename}
349
- </button>
350
- ))}
351
- </div>
352
- )}
353
- </div>
354
- )}
355
-
356
- {/* Videos */}
357
- <div className="flex flex-wrap gap-x-2 gap-y-6">
358
- {videosInfo.map((video, idx) => {
359
- if (hiddenVideos.includes(video.filename) || videoCodecError)
360
- return null;
361
- const isEnlarged = enlargedVideo === video.filename;
362
- return (
363
- <div
364
- key={video.filename}
365
- ref={(el) => {
366
- videoContainerRefs.current[video.filename] = el;
367
- }}
368
- className={`${isEnlarged ? "z-40 fixed inset-0 bg-black bg-opacity-90 flex flex-col items-center justify-center" : "max-w-96"}`}
369
- style={isEnlarged ? { height: "100vh", width: "100vw" } : {}}
370
- >
371
- <p className="truncate w-full rounded-t-xl bg-gray-800 px-2 text-sm text-gray-300 flex items-center justify-between">
372
- <span>{video.filename}</span>
373
- <span className="flex gap-1">
374
- <button
375
- title={isEnlarged ? "Minimize" : "Enlarge"}
376
- className="ml-2 p-1 hover:bg-white/5 rounded focus:outline-none focus:ring-0"
377
- onClick={() =>
378
- setEnlargedVideo(isEnlarged ? null : video.filename)
379
- }
380
- >
381
- {isEnlarged ? <FaCompress /> : <FaExpand />}
382
- </button>
383
- <button
384
- title="Hide Video"
385
- className="ml-1 p-1 hover:bg-white/5 rounded focus:outline-none focus:ring-0"
386
- onClick={() =>
387
- setHiddenVideos((prev: string[]) => [
388
- ...prev,
389
- video.filename,
390
- ])
391
- }
392
- disabled={visibleCount === 1}
393
- >
394
- <FaTimes />
395
- </button>
396
- </span>
397
- </p>
398
- <video
399
- ref={(el) => {
400
- if (el) videoRefs.current[idx] = el;
401
- }}
402
- muted
403
- loop
404
- preload="auto"
405
- crossOrigin="anonymous"
406
- className={`w-full object-contain ${isEnlarged ? "max-h-[90vh] max-w-[90vw]" : ""}`}
407
- onTimeUpdate={
408
- idx === firstVisibleIdx
409
- ? makeTimeUpdateHandler(idx)
410
- : undefined
411
- }
412
- style={isEnlarged ? { zIndex: 41 } : {}}
413
- >
414
- <source src={video.url} type="video/mp4" />
415
- Your browser does not support the video tag.
416
- </video>
417
- </div>
418
- );
419
- })}
420
- </div>
421
- </>
422
- );
423
- };
424
-
425
- export default VideosPlayer;