Add video player support to bskyembed with HLS.js
Adds real video playback to the bskyembed iframe embed system: - Lazy-load the video player using preact/compat lazy/Suspense so hls.js and all player code are in a separate chunk, keeping the default bundle small - Full video controls: play/pause, scrubber, volume, fullscreen, time display - HLS.js integration with bandwidth estimation and error handling - GIF presentation support with simplified controls - Thumbnail + play button shown until user clicks to activate the player - Ported patterns from the main app's web video player, adapted for Preact + Tailwind (no React Native, no ALF, no Lingui) https://claude.ai/code/session_01Mva57VZnFgdaQ7LPPoVNsP
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.15.25",
|
"@atproto/api": "^0.15.25",
|
||||||
|
"hls.js": "^1.6.2",
|
||||||
"preact": "^10.4.8"
|
"preact": "^10.4.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
AppBskyLabelerDefs,
|
AppBskyLabelerDefs,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {ComponentChildren, h} from 'preact'
|
import {ComponentChildren, h} from 'preact'
|
||||||
import {useMemo} from 'preact/hooks'
|
import {lazy, Suspense} from 'preact/compat'
|
||||||
|
import {useMemo, useState} from 'preact/hooks'
|
||||||
|
|
||||||
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
|
import infoIcon from '../../assets/circleInfo_stroke2_corner0_rounded.svg'
|
||||||
import playIcon from '../../assets/play_filled_corner2_rounded.svg'
|
import playIcon from '../../assets/play_filled_corner2_rounded.svg'
|
||||||
@@ -23,6 +24,8 @@ import {getVerificationState} from '../util/verification-state'
|
|||||||
import {Link} from './link'
|
import {Link} from './link'
|
||||||
import {VerificationCheck} from './verification-check'
|
import {VerificationCheck} from './verification-check'
|
||||||
|
|
||||||
|
const LazyVideoPlayer = lazy(() => import('./video-player'))
|
||||||
|
|
||||||
export function Embed({
|
export function Embed({
|
||||||
content,
|
content,
|
||||||
labels,
|
labels,
|
||||||
@@ -389,26 +392,66 @@ function GenericWithImageEmbed({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// just the thumbnail and a play button
|
|
||||||
function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
function VideoEmbed({content}: {content: AppBskyEmbedVideo.View}) {
|
||||||
let aspectRatio = 1
|
const [activated, setActivated] = useState(content.presentation === 'gif')
|
||||||
|
|
||||||
|
let aspectRatio = 1
|
||||||
if (content.aspectRatio) {
|
if (content.aspectRatio) {
|
||||||
const {width, height} = content.aspectRatio
|
const {width, height} = content.aspectRatio
|
||||||
aspectRatio = clamp(width / height, 1 / 1, 3 / 1)
|
aspectRatio = clamp(width / height, 1 / 1, 3 / 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activated) {
|
||||||
|
return (
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<VideoEmbedFallback
|
||||||
|
content={content}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
loading
|
||||||
|
/>
|
||||||
|
}>
|
||||||
|
<LazyVideoPlayer content={content} />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoEmbedFallback
|
||||||
|
content={content}
|
||||||
|
aspectRatio={aspectRatio}
|
||||||
|
onPress={() => setActivated(true)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function VideoEmbedFallback({
|
||||||
|
content,
|
||||||
|
aspectRatio,
|
||||||
|
onPress,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
content: AppBskyEmbedVideo.View
|
||||||
|
aspectRatio: number
|
||||||
|
onPress?: () => void
|
||||||
|
loading?: boolean
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full overflow-hidden rounded-xl aspect-square relative"
|
className="w-full overflow-hidden rounded-xl relative cursor-pointer"
|
||||||
style={{aspectRatio: `${aspectRatio} / 1`}}>
|
style={{aspectRatio: `${aspectRatio} / 1`}}
|
||||||
|
onClick={onPress}>
|
||||||
<img
|
<img
|
||||||
src={content.thumbnail}
|
src={content.thumbnail}
|
||||||
alt={content.alt}
|
alt={content.alt}
|
||||||
className="object-cover size-full"
|
className="object-cover size-full"
|
||||||
/>
|
/>
|
||||||
<div className="size-24 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-black/50 flex items-center justify-center">
|
<div className="size-24 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-black/50 flex items-center justify-center">
|
||||||
<img src={playIcon} className="object-cover size-3/5" />
|
{loading ? (
|
||||||
|
<div className="video-spinner" />
|
||||||
|
) : (
|
||||||
|
<img src={playIcon} className="object-cover size-3/5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,35 @@
|
|||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Video player spinner */
|
||||||
|
@keyframes video-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-spinner {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top-color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: video-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox scrubber workaround */
|
||||||
|
.force-no-clicks * {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Volume slider vertical orientation */
|
||||||
|
input[type='range'][orient='vertical'] {
|
||||||
|
writing-mode: vertical-lr;
|
||||||
|
direction: rtl;
|
||||||
|
appearance: slider-vertical;
|
||||||
|
-webkit-appearance: slider-vertical;
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='black' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
|
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='black' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|||||||
Reference in New Issue
Block a user