From d92731b1ebf006ab795cf726452a7f15a49ba618 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 29 Aug 2024 15:58:22 +0100 Subject: [PATCH] [Video] Lexicon implementation (#4881) * implement AppBskyEmbedVideo lexicon in player * add alt to native player * add prerelease package * update prerelease * add video embed view manually from record * fix type error on example video * black bg + use aspect ratio on web * add video to feeds * fix video overflowing aspect ratio * remove prerelease package --------- Co-authored-by: Samuel Newman <10959775+mozzius@users.noreply.github.com> --- package.json | 2 +- src/view/com/post-thread/PostThread.tsx | 1 + src/view/com/posts/FeedItem.tsx | 40 +++++----- src/view/com/util/post-embeds/VideoEmbed.tsx | 68 +++++++++++------ .../com/util/post-embeds/VideoEmbed.web.tsx | 27 +++++-- .../VideoEmbedInner/VideoEmbedInnerNative.tsx | 29 ++++++- .../VideoEmbedInner/VideoEmbedInnerWeb.tsx | 75 ++++++++++--------- src/view/com/util/post-embeds/index.tsx | 11 +++ yarn.lock | 49 ++++++++++-- 9 files changed, 211 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index 4a791ca2..fe43922a 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web" }, "dependencies": { - "@atproto/api": "0.13.3", + "@atproto/api": "0.13.5", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index d5740f87..4c4b0080 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -428,6 +428,7 @@ export function PostThread({uri}: {uri: string | undefined}) { (item.ctx.depth < 0 && !!item.parent) || item.ctx.depth > 1 const hasUnrevealedParents = index === 0 && skeleton?.parents && maxParents < skeleton.parents.length + return ( ) } diff --git a/src/view/com/util/post-embeds/VideoEmbed.tsx b/src/view/com/util/post-embeds/VideoEmbed.tsx index b2bcd851..378952f5 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.tsx @@ -1,21 +1,25 @@ import React, {useCallback, useState} from 'react' import {View} from 'react-native' +import {Image} from 'expo-image' +import {AppBskyEmbedVideo} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' +import {clamp} from '#/lib/numbers' +import {useGate} from '#/lib/statsig/statsig' +import {VideoEmbedInnerNative} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative' import {atoms as a, useTheme} from '#/alf' -import {Button, ButtonIcon} from '#/components/Button' +import {Button} from '#/components/Button' import {Play_Filled_Corner2_Rounded as PlayIcon} from '#/components/icons/Play' import {VisibilityView} from '../../../../../modules/expo-bluesky-swiss-army' import {ErrorBoundary} from '../ErrorBoundary' import {useActiveVideoNative} from './ActiveVideoNativeContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' -export function VideoEmbed({source}: {source: string}) { +export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { const t = useTheme() const {activeSource, setActiveSource} = useActiveVideoNative() - const isActive = source === activeSource + const isActive = embed.playlist === activeSource const {_} = useLingui() const [key, setKey] = useState(0) @@ -25,39 +29,61 @@ export function VideoEmbed({source}: {source: string}) { ), [key], ) + const gate = useGate() + + if (!gate('videos')) { + return null + } + + let aspectRatio = 16 / 9 + + if (embed.aspectRatio) { + const {width, height} = embed.aspectRatio + aspectRatio = width / height + aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) + } return ( { - if (isActive) { - setActiveSource(source) + onChangeStatus={isVisible => { + if (isVisible) { + setActiveSource(embed.playlist) } }}> {isActive ? ( - + ) : ( - + <> + {embed.alt} + + )} diff --git a/src/view/com/util/post-embeds/VideoEmbed.web.tsx b/src/view/com/util/post-embeds/VideoEmbed.web.tsx index c0d774ab..409f2c7b 100644 --- a/src/view/com/util/post-embeds/VideoEmbed.web.tsx +++ b/src/view/com/util/post-embeds/VideoEmbed.web.tsx @@ -1,19 +1,23 @@ import React, {useCallback, useEffect, useRef, useState} from 'react' import {View} from 'react-native' +import {AppBskyEmbedVideo} from '@atproto/api' import {Trans} from '@lingui/macro' +import {clamp} from '#/lib/numbers' +import {useGate} from '#/lib/statsig/statsig' import { HLSUnsupportedError, VideoEmbedInnerWeb, -} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' +} from '#/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb' import {atoms as a, useTheme} from '#/alf' import {ErrorBoundary} from '../ErrorBoundary' import {useActiveVideoWeb} from './ActiveVideoWebContext' import * as VideoFallback from './VideoEmbedInner/VideoFallback' -export function VideoEmbed({source}: {source: string}) { +export function VideoEmbed({embed}: {embed: AppBskyEmbedVideo.View}) { const t = useTheme() const ref = useRef(null) + const gate = useGate() const {active, setActive, sendPosition, currentActiveView} = useActiveVideoWeb() const [onScreen, setOnScreen] = useState(false) @@ -43,12 +47,25 @@ export function VideoEmbed({source}: {source: string}) { [key], ) + if (!gate('videos')) { + return null + } + + let aspectRatio = 16 / 9 + + if (embed.aspectRatio) { + const {width, height} = embed.aspectRatio + // min: 3/1, max: square + aspectRatio = clamp(width / height, 1 / 1, 3 / 1) + } + return ( @@ -61,7 +78,7 @@ export function VideoEmbed({source}: {source: string}) { sendPosition={sendPosition} isAnyViewActive={currentActiveView !== null}> (null) const isScreenFocused = useIsFocused() @@ -47,13 +54,23 @@ export function VideoEmbedInnerNative() { ref.current?.enterFullscreen() }, []) + let aspectRatio = 16 / 9 + + if (embed.aspectRatio) { + const {width, height} = embed.aspectRatio + aspectRatio = width / height + aspectRatio = clamp(aspectRatio, 1 / 1, 3 / 1) + } + return ( - + { PlatformInfo.setAudioCategory(AudioCategory.Playback) PlatformInfo.setAudioActive(true) @@ -65,13 +82,17 @@ export function VideoEmbedInnerNative() { player.muted = true if (!player.playing) player.play() }} + accessibilityLabel={ + embed.alt ? _(msg`Video: ${embed.alt}`) : _(msg`Video`) + } + accessibilityHint="" /> - + ) } -function Controls({ +function VideoControls({ player, enterFullscreen, }: { diff --git a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx index c0021d9b..77295c00 100644 --- a/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx +++ b/src/view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb.tsx @@ -1,31 +1,27 @@ -import React, {useEffect, useRef, useState} from 'react' +import React, {useEffect, useId, useRef, useState} from 'react' import {View} from 'react-native' +import {AppBskyEmbedVideo} from '@atproto/api' import Hls from 'hls.js' import {atoms as a} from '#/alf' import {Controls} from './VideoWebControls' export function VideoEmbedInnerWeb({ - source, + embed, active, setActive, onScreen, }: { - source: string - active?: boolean - setActive?: () => void - onScreen?: boolean + embed: AppBskyEmbedVideo.View + active: boolean + setActive: () => void + onScreen: boolean }) { - if (active == null || setActive == null || onScreen == null) { - throw new Error( - 'active, setActive, and onScreen are required VideoEmbedInner props on web.', - ) - } - const containerRef = useRef(null) const ref = useRef(null) const [focused, setFocused] = useState(false) const [hasSubtitleTrack, setHasSubtitleTrack] = useState(false) + const figId = useId() const hlsRef = useRef(undefined) @@ -37,7 +33,7 @@ export function VideoEmbedInnerWeb({ hlsRef.current = hls hls.attachMedia(ref.current) - hls.loadSource(source) + hls.loadSource(embed.playlist) // initial value, later on it's managed by Controls hls.autoLevelCapping = 0 @@ -53,29 +49,40 @@ export function VideoEmbedInnerWeb({ hls.detachMedia() hls.destroy() } - }, [source]) + }, [embed.playlist]) return ( - -
-