From 69e3c1ba2f98c504ad814d6c216a1fd2e08e10cb Mon Sep 17 00:00:00 2001 From: Cesar Alcazar Date: Mon, 19 Oct 2026 14:03:12 -0600 Subject: [PATCH] vycxexra8: Play feed videos from hlsUrl with a thumbnail fallback --- package-lock.json | 7 ++++ package.json | 1 + src/app/globals.css | 2 + src/components/PostCard.tsx | 7 +++- src/components/VideoPlayer.tsx | 69 ++++++++++++++++++++++++++++++++++ src/lib/feed-model.ts | 18 ++++++++- src/lib/video-playback.ts | 21 +++++++++++ tests/video-playback.test.ts | 55 +++++++++++++++++++++++++++ 8 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 src/components/VideoPlayer.tsx create mode 100644 src/lib/video-playback.ts create mode 100644 tests/video-playback.test.ts diff --git a/package-lock.json b/package-lock.json index 7f8544c..98a2697 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend-helper", "version": "0.1.0", "dependencies": { + "hls.js": "1.7.3", "next": "15.5.28", "react": "19.0.0", "react-dom": "19.0.0" @@ -4321,6 +4322,12 @@ "node": ">= 0.4" } }, + "node_modules/hls.js": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.3.tgz", + "integrity": "sha512-MsPlx6yVW4Qv4C7mEVou4/gk/5cN2dVxOTnrNjPSmyH2f0Ln+/UkG10Ax1PE6OvrmTVPKgQYozTtBaPOuCzq7A==", + "license": "Apache-2.0" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index e37a838..e50b341 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "vitest run" }, "dependencies": { + "hls.js": "1.7.3", "next": "15.5.28", "react": "19.0.0", "react-dom": "19.0.0" diff --git a/src/app/globals.css b/src/app/globals.css index e143da7..f586a20 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -24,6 +24,8 @@ button:disabled { opacity: .5; cursor: not-allowed; } .post-who time { color: var(--grey); font-size: 13px; } .post-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.5; } .post-media img { display: block; width: 100%; height: auto; border-radius: 8px; } +.post-video { background: #000; border-radius: 8px; overflow: hidden; } +.post-video video { display: block; width: 100%; max-height: 520px; background: #000; } .post-stats { display: flex; gap: 16px; color: var(--grey); font-size: 14px; } .heart { color: var(--red); } .feed-message { text-align: center; color: var(--grey); padding: 32px 16px; } diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 8b9e0a7..f36d04a 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -1,3 +1,4 @@ +import { VideoPlayer } from "@/components/VideoPlayer"; import { initials, relativeTime, type CardPost } from "@/lib/feed-model"; export function PostCard({ post, now }: { post: CardPost; now?: number }) { @@ -19,7 +20,11 @@ export function PostCard({ post, now }: { post: CardPost; now?: number }) { {post.text ?

{post.text}

: null} {post.media.map((m, i) => - m.src ? ( + m.kind === "video" && m.hlsUrl ? ( +
+ +
+ ) : m.src ? (
{/* eslint-disable-next-line @next/next/no-img-element */} {m.kind diff --git a/src/components/VideoPlayer.tsx b/src/components/VideoPlayer.tsx new file mode 100644 index 0000000..be96a78 --- /dev/null +++ b/src/components/VideoPlayer.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { choosePlayback, supportsNativeHls } from "@/lib/video-playback"; + +export interface VideoPlayerProps { + hlsUrl: string; + poster: string | null; + label: string; +} + +/** + * Real