An entry for the streamplace vod showcase
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(web): click-through seeking from preview to player

- Clicking on video preview opens player at that timestamp
- Player seeks to the clicked position after loading
- Works with HLS.js and native HLS playback
- Related videos in sidebar start from beginning

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+32 -18
+32 -18
apps/web/src/ui/UI4Editorial.tsx
··· 1066 1066 video: Video 1067 1067 metadata?: VideoMetadata 1068 1068 profile?: Profile 1069 - onClick: () => void 1069 + onClick: (startTime?: number) => void 1070 1070 } 1071 1071 1072 1072 function ThumbnailFallback({ title }: { title: string }) { ··· 1157 1157 } 1158 1158 })() : {} 1159 1159 1160 + const handleClick = useCallback(() => { 1161 + // If we have a preview position, seek to that time 1162 + onClick(previewFrame !== null ? previewTime : undefined) 1163 + }, [onClick, previewFrame, previewTime]) 1164 + 1160 1165 return ( 1161 - <article className="editorial-card" onClick={onClick}> 1166 + <article className="editorial-card" onClick={handleClick}> 1162 1167 <div 1163 1168 ref={mediaRef} 1164 1169 className="editorial-card-media" ··· 1224 1229 relatedVideos: Video[] 1225 1230 metadataMap: Map<string, VideoMetadata> 1226 1231 profileMap: Map<string, Profile> 1232 + startTime?: number 1227 1233 onClose: () => void 1228 - onPlayVideo: (video: Video) => void 1234 + onPlayVideo: (video: Video, startTime?: number) => void 1229 1235 } 1230 1236 1231 - function Player({ video, profile, relatedVideos, metadataMap, profileMap, onClose, onPlayVideo }: PlayerProps) { 1237 + function Player({ video, profile, relatedVideos, metadataMap, profileMap, startTime, onClose, onPlayVideo }: PlayerProps) { 1232 1238 const videoRef = useRef<HTMLVideoElement>(null) 1233 1239 const hlsRef = useRef<Hls | null>(null) 1234 - const startTime = useRef(Date.now()) 1240 + const watchStartTime = useRef(Date.now()) 1235 1241 1236 1242 useEffect(() => { 1237 1243 const el = videoRef.current ··· 1239 1245 1240 1246 const url = getPlaylistUrl(video.uri) 1241 1247 1248 + const seekToStart = () => { 1249 + if (startTime !== undefined && startTime > 0) { 1250 + el.currentTime = startTime 1251 + } 1252 + el.play() 1253 + } 1254 + 1242 1255 if (Hls.isSupported()) { 1243 1256 const hls = new Hls() 1244 1257 hls.loadSource(url) 1245 1258 hls.attachMedia(el) 1246 - hls.on(Hls.Events.MANIFEST_PARSED, () => el.play()) 1259 + hls.on(Hls.Events.MANIFEST_PARSED, seekToStart) 1247 1260 hlsRef.current = hls 1248 1261 } else if (el.canPlayType("application/vnd.apple.mpegurl")) { 1249 1262 el.src = url 1250 - el.play() 1263 + el.addEventListener('loadedmetadata', seekToStart, { once: true }) 1251 1264 } 1252 1265 1253 1266 return () => { 1254 - const watchedMs = Date.now() - startTime.current 1267 + const watchedMs = Date.now() - watchStartTime.current 1255 1268 const totalSeconds = video.duration / 1_000_000_000 1256 1269 recordWatch({ 1257 1270 uri: video.uri, ··· 1261 1274 }) 1262 1275 hlsRef.current?.destroy() 1263 1276 } 1264 - }, [video]) 1277 + }, [video, startTime]) 1265 1278 1266 1279 const displayName = profile?.displayName || profile?.handle || "Unknown Creator" 1267 1280 ··· 1365 1378 const [videos, setVideos] = useState<Video[]>([]) 1366 1379 const [metadataMap, setMetadataMap] = useState<Map<string, VideoMetadata>>(new Map()) 1367 1380 const [profileMap, setProfileMap] = useState<Map<string, Profile>>(new Map()) 1368 - const [playing, setPlaying] = useState<Video | null>(null) 1381 + const [playing, setPlaying] = useState<{ video: Video; startTime?: number } | null>(null) 1369 1382 const [continueWatching, setContinueWatching] = useState<Array<{ uri: string; progress: number }>>([]) 1370 1383 const [isLoading, setIsLoading] = useState(true) 1371 1384 const [activeTab, setActiveTab] = useState<'videos' | 'creators'>('videos') ··· 1488 1501 <div 1489 1502 key={video.uri} 1490 1503 className="editorial-continue-item" 1491 - onClick={() => setPlaying(video)} 1504 + onClick={() => setPlaying({ video })} 1492 1505 > 1493 1506 {thumb ? ( 1494 1507 <img className="editorial-continue-thumb" src={thumb} alt="" /> ··· 1543 1556 </div> 1544 1557 </div> 1545 1558 </div> 1546 - <div className="editorial-hero-media" onClick={() => setPlaying(featured)}> 1559 + <div className="editorial-hero-media" onClick={() => setPlaying({ video: featured })}> 1547 1560 {featuredMeta?.thumbnailUrl ? ( 1548 1561 <img 1549 1562 className="editorial-hero-img" ··· 1581 1594 video={video} 1582 1595 metadata={metadataMap.get(video.uri)} 1583 1596 profile={video.creator ? profileMap.get(video.creator) : undefined} 1584 - onClick={() => setPlaying(video)} 1597 + onClick={(startTime) => setPlaying({ video, startTime })} 1585 1598 /> 1586 1599 )) 1587 1600 )} ··· 1609 1622 video={video} 1610 1623 metadata={metadataMap.get(video.uri)} 1611 1624 profile={video.creator ? profileMap.get(video.creator) : undefined} 1612 - onClick={() => setPlaying(video)} 1625 + onClick={(startTime) => setPlaying({ video, startTime })} 1613 1626 /> 1614 1627 )) 1615 1628 )} ··· 1675 1688 1676 1689 {playing && ( 1677 1690 <Player 1678 - video={playing} 1679 - profile={playing.creator ? profileMap.get(playing.creator) : undefined} 1680 - relatedVideos={videos.filter((v) => v.uri !== playing.uri)} 1691 + video={playing.video} 1692 + profile={playing.video.creator ? profileMap.get(playing.video.creator) : undefined} 1693 + relatedVideos={videos.filter((v) => v.uri !== playing.video.uri)} 1681 1694 metadataMap={metadataMap} 1682 1695 profileMap={profileMap} 1696 + startTime={playing.startTime} 1683 1697 onClose={() => setPlaying(null)} 1684 - onPlayVideo={setPlaying} 1698 + onPlayVideo={(video, startTime) => setPlaying({ video, startTime })} 1685 1699 /> 1686 1700 )} 1687 1701 </div>