Hide video pillarboxing on web when the card fits (#11118)

This commit is contained in:
Spence Pope
2026-07-21 17:39:51 -04:00
committed by GitHub
parent 44b1ab08b5
commit 26ce5f0934
@@ -30,6 +30,13 @@ import * as VideoFallback from './VideoEmbedInner/VideoFallback'
const noop = () => {} const noop = () => {}
/**
* Minimum card width for the overlay controls (play, time, CC, volume,
* fullscreen) to fit without crowding. Narrower cards fall back to the
* full-width pillarbox.
*/
const MIN_CARD_WIDTH = 280
export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
const t = useTheme() const t = useTheme()
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
@@ -91,6 +98,21 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
constrained = Math.max(aspectRatio, ratio) constrained = Math.max(aspectRatio, ratio)
} }
const [containerWidth, setContainerWidth] = useState(0)
/*
* Portrait videos render at their own ratio instead of pillarboxed, but
* only when the resulting card fits the overlay controls. Videos taller
* than 1:2 would still show bars inside a ratio-fit card, and an unknown
* ratio can't be fit, so both keep the full-width pillarbox - a narrow
* card with black slices down the sides looks broken (see #9371).
*/
const cardWidth = containerWidth * Math.min(aspectRatio ?? 1, 1)
const fullBleed =
aspectRatio === undefined ||
aspectRatio < 1 / 2 ||
(containerWidth > 0 && cardWidth < MIN_CARD_WIDTH)
const contents = ( const contents = (
<div <div
ref={ref} ref={ref}
@@ -98,6 +120,7 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
display: 'flex', display: 'flex',
flex: 1, flex: 1,
cursor: 'default', cursor: 'default',
position: 'relative',
backgroundColor: t.palette.black, backgroundColor: t.palette.black,
backgroundImage: `url(${embed.thumbnail})`, backgroundImage: `url(${embed.thumbnail})`,
backgroundSize: 'contain', backgroundSize: 'contain',
@@ -105,6 +128,36 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
}} }}
onClick={evt => evt.stopPropagation()}> onClick={evt => evt.stopPropagation()}>
{fullBleed && embed.thumbnail && (
<>
{/* blurred backdrop fills the bars when the video is boxed */}
<div
aria-hidden
style={{
position: 'absolute',
inset: 0,
backgroundImage: `url(${embed.thumbnail})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
filter: 'blur(32px)',
// hide the transparent fade the blur creates at the edges
transform: 'scale(1.2)',
}}
/>
{/* redraw the sharp thumbnail above the blur */}
<div
aria-hidden
style={{
position: 'absolute',
inset: 0,
backgroundImage: `url(${embed.thumbnail})`,
backgroundSize: 'contain',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
}}
/>
</>
)}
<ErrorBoundary renderError={renderError} key={key}> <ErrorBoundary renderError={renderError} key={key}>
<OnlyNearScreen> <OnlyNearScreen>
<VideoEmbedInnerWeb <VideoEmbedInnerWeb
@@ -120,12 +173,14 @@ export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) {
) )
return ( return (
<View style={[a.pt_xs]}> <View
style={[a.pt_xs]}
onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}>
<ViewportObserver <ViewportObserver
sendPosition={isGif ? noop : sendPosition} sendPosition={isGif ? noop : sendPosition}
isAnyViewActive={currentActiveView !== null}> isAnyViewActive={currentActiveView !== null}>
<ConstrainedImage <ConstrainedImage
fullBleed fullBleed={fullBleed}
aspectRatio={constrained || 1} aspectRatio={constrained || 1}
// slightly smaller max height than images // slightly smaller max height than images
// images use 16 / 9, for reference // images use 16 / 9, for reference