Merge remote-tracking branch 'origin/main' into Improve-style-consistency
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import * as PlatformInfo from './src/PlatformInfo'
|
||||
import {AudioCategory} from './src/PlatformInfo/types'
|
||||
import * as Referrer from './src/Referrer'
|
||||
import * as SharedPrefs from './src/SharedPrefs'
|
||||
import VisibilityView from './src/VisibilityView'
|
||||
|
||||
export {PlatformInfo, Referrer, SharedPrefs, VisibilityView}
|
||||
export {AudioCategory, PlatformInfo, Referrer, SharedPrefs, VisibilityView}
|
||||
|
||||
@@ -7,5 +7,36 @@ public class ExpoPlatformInfoModule: Module {
|
||||
Function("getIsReducedMotionEnabled") {
|
||||
return UIAccessibility.isReduceMotionEnabled
|
||||
}
|
||||
|
||||
Function("setAudioCategory") { (audioCategoryString: String) in
|
||||
let audioCategory = AVAudioSession.Category(rawValue: audioCategoryString)
|
||||
try? AVAudioSession.sharedInstance().setCategory(audioCategory)
|
||||
}
|
||||
|
||||
Function("setAudioActive") { (active: Bool) in
|
||||
var categoryOptions: AVAudioSession.CategoryOptions
|
||||
let currentCategory = AVAudioSession.sharedInstance().category
|
||||
|
||||
if active {
|
||||
categoryOptions = [.mixWithOthers]
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
} else {
|
||||
categoryOptions = [.duckOthers]
|
||||
try? AVAudioSession
|
||||
.sharedInstance()
|
||||
.setActive(
|
||||
false,
|
||||
options: [.notifyOthersOnDeactivation]
|
||||
)
|
||||
}
|
||||
|
||||
try? AVAudioSession
|
||||
.sharedInstance()
|
||||
.setCategory(
|
||||
currentCategory,
|
||||
mode: .default,
|
||||
options: categoryOptions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
import {Platform} from 'react-native'
|
||||
import {requireNativeModule} from 'expo-modules-core'
|
||||
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
const NativeModule = requireNativeModule('ExpoPlatformInfo')
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
return NativeModule.getIsReducedMotionEnabled()
|
||||
}
|
||||
|
||||
export function setAudioActive(active: boolean): void {
|
||||
if (Platform.OS !== 'ios') return
|
||||
NativeModule.setAudioActive(active)
|
||||
}
|
||||
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
if (Platform.OS !== 'ios') return
|
||||
NativeModule.setAudioCategory(audioCategory)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the app's audio should mix with other apps' audio. Will also resume background music playback when `false`
|
||||
* if it was previously playing.
|
||||
* @param mixWithOthers
|
||||
* @see https://developer.apple.com/documentation/avfaudio/avaudiosession/setactiveoptions/1616603-notifyothersondeactivation
|
||||
*/
|
||||
export function setAudioActive(active: boolean): void {
|
||||
throw new NotImplementedError({active})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the audio category for the app.
|
||||
* @param audioCategory
|
||||
* @platform ios
|
||||
*/
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
throw new NotImplementedError({audioCategory})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import {NotImplementedError} from '../NotImplemented'
|
||||
import {AudioCategory} from './types'
|
||||
|
||||
export function getIsReducedMotionEnabled(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false
|
||||
}
|
||||
return window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
}
|
||||
|
||||
export function setAudioActive(active: boolean): void {
|
||||
throw new NotImplementedError({active})
|
||||
}
|
||||
|
||||
export function setAudioCategory(audioCategory: AudioCategory): void {
|
||||
throw new NotImplementedError({audioCategory})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Sets the audio session category on iOS. In general, we should only need to use this for the `playback` and `ambient`
|
||||
* categories. This enum however includes other categories that are available in the native API for clarity and
|
||||
* potential future use.
|
||||
* @see https://developer.apple.com/documentation/avfoundation/avaudiosession/category
|
||||
* @platform ios
|
||||
*/
|
||||
export enum AudioCategory {
|
||||
Ambient = 'AVAudioSessionCategoryAmbient',
|
||||
Playback = 'AVAudioSessionCategoryPlayback',
|
||||
_SoloAmbient = 'AVAudioSessionCategorySoloAmbient',
|
||||
_Record = 'AVAudioSessionCategoryRecord',
|
||||
_PlayAndRecord = 'AVAudioSessionCategoryPlayAndRecord',
|
||||
_MultiRoute = 'AVAudioSessionCategoryMultiRoute',
|
||||
}
|
||||
+1
-1
@@ -139,7 +139,7 @@
|
||||
"expo-system-ui": "~3.0.4",
|
||||
"expo-task-manager": "~11.8.1",
|
||||
"expo-updates": "~0.25.14",
|
||||
"expo-video": "^1.1.10",
|
||||
"expo-video": "^1.2.4",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
|
||||
setShowPreviousButton(!requireLinearPlayback)
|
||||
setShowNextButton(!requireLinearPlayback)
|
||||
setTimeBarInteractive(requireLinearPlayback)
|
||||
+ setShowSubtitleButton(true)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
||||
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
+ val fullscreenButton =
|
||||
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
fullscreenButton?.visibility = if (visible) {
|
||||
android.view.View.VISIBLE
|
||||
} else {
|
||||
@@ -0,0 +1,135 @@
|
||||
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
index 9905e13..47342ff 100644
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/PlayerViewExtension.kt
|
||||
@@ -11,6 +11,7 @@ internal fun PlayerView.applyRequiresLinearPlayback(requireLinearPlayback: Boole
|
||||
setShowPreviousButton(!requireLinearPlayback)
|
||||
setShowNextButton(!requireLinearPlayback)
|
||||
setTimeBarInteractive(requireLinearPlayback)
|
||||
+ setShowSubtitleButton(true)
|
||||
}
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
@@ -27,7 +28,8 @@ internal fun PlayerView.setTimeBarInteractive(interactive: Boolean) {
|
||||
|
||||
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
|
||||
internal fun PlayerView.setFullscreenButtonVisibility(visible: Boolean) {
|
||||
- val fullscreenButton = findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
+ val fullscreenButton =
|
||||
+ findViewById<android.widget.ImageButton>(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
fullscreenButton?.visibility = if (visible) {
|
||||
android.view.View.VISIBLE
|
||||
} else {
|
||||
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||
index ec3da2a..5a1397a 100644
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoModule.kt
|
||||
@@ -43,7 +43,9 @@ class VideoModule : Module() {
|
||||
View(VideoView::class) {
|
||||
Events(
|
||||
"onPictureInPictureStart",
|
||||
- "onPictureInPictureStop"
|
||||
+ "onPictureInPictureStop",
|
||||
+ "onEnterFullscreen",
|
||||
+ "onExitFullscreen"
|
||||
)
|
||||
|
||||
Prop("player") { view: VideoView, player: VideoPlayer ->
|
||||
diff --git a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||
index a951d80..3932535 100644
|
||||
--- a/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||
+++ b/node_modules/expo-video/android/src/main/java/expo/modules/video/VideoView.kt
|
||||
@@ -36,6 +36,8 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||
val playerView: PlayerView = PlayerView(context.applicationContext)
|
||||
val onPictureInPictureStart by EventDispatcher<Unit>()
|
||||
val onPictureInPictureStop by EventDispatcher<Unit>()
|
||||
+ val onEnterFullscreen by EventDispatcher()
|
||||
+ val onExitFullscreen by EventDispatcher()
|
||||
|
||||
var willEnterPiP: Boolean = false
|
||||
var isInFullscreen: Boolean = false
|
||||
@@ -154,6 +156,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||
@Suppress("DEPRECATION")
|
||||
currentActivity.overridePendingTransition(0, 0)
|
||||
}
|
||||
+ onEnterFullscreen(mapOf())
|
||||
isInFullscreen = true
|
||||
}
|
||||
|
||||
@@ -162,6 +165,7 @@ class VideoView(context: Context, appContext: AppContext) : ExpoView(context, ap
|
||||
val fullScreenButton: ImageButton = playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen)
|
||||
fullScreenButton.setImageResource(androidx.media3.ui.R.drawable.exo_icon_fullscreen_enter)
|
||||
videoPlayer?.changePlayerView(playerView)
|
||||
+ this.onExitFullscreen(mapOf())
|
||||
isInFullscreen = false
|
||||
}
|
||||
|
||||
diff --git a/node_modules/expo-video/build/VideoView.types.d.ts b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||
index cb9ca6d..60e9f4e 100644
|
||||
--- a/node_modules/expo-video/build/VideoView.types.d.ts
|
||||
+++ b/node_modules/expo-video/build/VideoView.types.d.ts
|
||||
@@ -89,5 +89,8 @@ export interface VideoViewProps extends ViewProps {
|
||||
* @platform ios 16.0+
|
||||
*/
|
||||
allowsVideoFrameAnalysis?: boolean;
|
||||
+
|
||||
+ onEnterFullscreen?: () => void;
|
||||
+ onExitFullscreen?: () => void;
|
||||
}
|
||||
//# sourceMappingURL=VideoView.types.d.ts.map
|
||||
diff --git a/node_modules/expo-video/ios/VideoModule.swift b/node_modules/expo-video/ios/VideoModule.swift
|
||||
index c537a12..e4a918f 100644
|
||||
--- a/node_modules/expo-video/ios/VideoModule.swift
|
||||
+++ b/node_modules/expo-video/ios/VideoModule.swift
|
||||
@@ -16,7 +16,9 @@ public final class VideoModule: Module {
|
||||
View(VideoView.self) {
|
||||
Events(
|
||||
"onPictureInPictureStart",
|
||||
- "onPictureInPictureStop"
|
||||
+ "onPictureInPictureStop",
|
||||
+ "onEnterFullscreen",
|
||||
+ "onExitFullscreen"
|
||||
)
|
||||
|
||||
Prop("player") { (view, player: VideoPlayer?) in
|
||||
diff --git a/node_modules/expo-video/ios/VideoView.swift b/node_modules/expo-video/ios/VideoView.swift
|
||||
index f4579e4..10c5908 100644
|
||||
--- a/node_modules/expo-video/ios/VideoView.swift
|
||||
+++ b/node_modules/expo-video/ios/VideoView.swift
|
||||
@@ -41,6 +41,8 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
|
||||
let onPictureInPictureStart = EventDispatcher()
|
||||
let onPictureInPictureStop = EventDispatcher()
|
||||
+ let onEnterFullscreen = EventDispatcher()
|
||||
+ let onExitFullscreen = EventDispatcher()
|
||||
|
||||
public override var bounds: CGRect {
|
||||
didSet {
|
||||
@@ -163,6 +165,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
_ playerViewController: AVPlayerViewController,
|
||||
willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
|
||||
) {
|
||||
+ onEnterFullscreen()
|
||||
isFullscreen = true
|
||||
}
|
||||
|
||||
@@ -179,6 +182,7 @@ public final class VideoView: ExpoView, AVPlayerViewControllerDelegate {
|
||||
if wasPlaying {
|
||||
self.player?.pointer.play()
|
||||
}
|
||||
+ self.onExitFullscreen()
|
||||
self.isFullscreen = false
|
||||
}
|
||||
}
|
||||
diff --git a/node_modules/expo-video/src/VideoView.types.ts b/node_modules/expo-video/src/VideoView.types.ts
|
||||
index 29fe5db..e1fbf59 100644
|
||||
--- a/node_modules/expo-video/src/VideoView.types.ts
|
||||
+++ b/node_modules/expo-video/src/VideoView.types.ts
|
||||
@@ -100,4 +100,7 @@ export interface VideoViewProps extends ViewProps {
|
||||
* @platform ios 16.0+
|
||||
*/
|
||||
allowsVideoFrameAnalysis?: boolean;
|
||||
+
|
||||
+ onEnterFullscreen?: () => void;
|
||||
+ onExitFullscreen?: () => void;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
## uwu woad beawing, do not wemove
|
||||
|
||||
## `expo-video` Patch
|
||||
|
||||
This patch adds two props to `VideoView`: `onEnterFullscreen` and `onExitFullscreen` which do exactly what they say on
|
||||
the tin.
|
||||
@@ -61,6 +61,7 @@ import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as TourProvider} from '#/tours'
|
||||
import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider'
|
||||
import {AudioCategory, PlatformInfo} from '../modules/expo-bluesky-swiss-army'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
@@ -157,6 +158,8 @@ function App() {
|
||||
const [isReady, setReady] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||
PlatformInfo.setAudioActive(true)
|
||||
initPersistedState().then(() => setReady(true))
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ function DialogError({details}: {details?: string}) {
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner style={a.gap_md} label={_(msg`An error occured`)}>
|
||||
<Dialog.ScrollableInner style={a.gap_md} label={_(msg`An error has occurred`)}>
|
||||
<Dialog.Close />
|
||||
<ErrorScreen
|
||||
title={_(msg`Oh no!`)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {View} from 'react-native'
|
||||
// @ts-expect-error no type definition
|
||||
import ProgressPie from 'react-native-progress/Pie'
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -46,7 +47,9 @@ export function VideoTranscodeProgress({
|
||||
color={t.atoms.text.color}
|
||||
progress={progress}
|
||||
/>
|
||||
<Text>Compressing...</Text>
|
||||
<Text>
|
||||
<Trans>Compressing...</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useMemo, useState} from 'react'
|
||||
import React, {memo, useId, useMemo, useState} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
@@ -137,7 +137,6 @@ let FeedItemInner = ({
|
||||
const {openComposer} = useComposerControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const gate = useGate()
|
||||
|
||||
const href = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
@@ -356,9 +355,7 @@ let FeedItemInner = ({
|
||||
postAuthor={post.author}
|
||||
onOpenEmbed={onOpenEmbed}
|
||||
/>
|
||||
{gate('video_debug') && (
|
||||
<VideoEmbed source="https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8" />
|
||||
)}
|
||||
<VideoDebug />
|
||||
<PostCtrls
|
||||
post={post}
|
||||
record={record}
|
||||
@@ -501,6 +498,19 @@ function ReplyToLabel({
|
||||
)
|
||||
}
|
||||
|
||||
function VideoDebug() {
|
||||
const gate = useGate()
|
||||
const id = useId()
|
||||
|
||||
if (!gate('video_debug')) return null
|
||||
|
||||
return (
|
||||
<VideoEmbed
|
||||
source={`https://lumi.jazco.dev/watch/did:plc:q6gjnaw2blty4crticxkmujt/Qmc8w93UpTa2adJHg4ZhnDPrBs1EsbzrekzPcqF5SwusuZ/playlist.m3u8?ignore_me_just_testing_frontend_stuff=${id}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
outer: {
|
||||
paddingLeft: 10,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import React, {useCallback, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {VideoEmbedInnerNative} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerNative'
|
||||
@@ -8,13 +8,23 @@ import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} 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 {useActiveVideoView} from './ActiveVideoContext'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
export function VideoEmbed({source}: {source: string}) {
|
||||
const t = useTheme()
|
||||
const {active, setActive} = useActiveVideoView({source})
|
||||
const {_} = useLingui()
|
||||
|
||||
const [key, setKey] = useState(0)
|
||||
const renderError = useCallback(
|
||||
(error: unknown) => (
|
||||
<VideoError error={error} retry={() => setKey(key + 1)} />
|
||||
),
|
||||
[key],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -25,27 +35,42 @@ export function VideoEmbed({source}: {source: string}) {
|
||||
t.atoms.bg_contrast_25,
|
||||
a.my_xs,
|
||||
]}>
|
||||
<VisibilityView
|
||||
enabled={true}
|
||||
onChangeStatus={isActive => {
|
||||
if (isActive) {
|
||||
setActive()
|
||||
}
|
||||
}}>
|
||||
{active ? (
|
||||
<VideoEmbedInnerNative />
|
||||
) : (
|
||||
<Button
|
||||
style={[a.flex_1, t.atoms.bg_contrast_25]}
|
||||
onPress={setActive}
|
||||
label={_(msg`Play video`)}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="large">
|
||||
<ButtonIcon icon={PlayIcon} />
|
||||
</Button>
|
||||
)}
|
||||
</VisibilityView>
|
||||
<ErrorBoundary renderError={renderError} key={key}>
|
||||
<VisibilityView
|
||||
enabled={true}
|
||||
onChangeStatus={isActive => {
|
||||
if (isActive) {
|
||||
setActive()
|
||||
}
|
||||
}}>
|
||||
{active ? (
|
||||
<VideoEmbedInnerNative />
|
||||
) : (
|
||||
<Button
|
||||
style={[a.flex_1, t.atoms.bg_contrast_25]}
|
||||
onPress={setActive}
|
||||
label={_(msg`Play video`)}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="large">
|
||||
<ButtonIcon icon={PlayIcon} />
|
||||
</Button>
|
||||
)}
|
||||
</VisibilityView>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function VideoError({retry}: {error: unknown; retry: () => void}) {
|
||||
return (
|
||||
<VideoFallback.Container>
|
||||
<VideoFallback.Text>
|
||||
<Trans>
|
||||
An error occurred while loading the video. Please try again later.
|
||||
</Trans>
|
||||
</VideoFallback.Text>
|
||||
<VideoFallback.RetryButton onPress={retry} />
|
||||
</VideoFallback.Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {
|
||||
HLSUnsupportedError,
|
||||
VideoEmbedInnerWeb,
|
||||
} from 'view/com/util/post-embeds/VideoEmbedInner/VideoEmbedInnerWeb'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ErrorBoundary} from '../ErrorBoundary'
|
||||
import {useActiveVideoView} from './ActiveVideoContext'
|
||||
import * as VideoFallback from './VideoEmbedInner/VideoFallback'
|
||||
|
||||
export function VideoEmbed({source}: {source: string}) {
|
||||
const t = useTheme()
|
||||
@@ -138,32 +136,11 @@ function ViewportObserver({
|
||||
}
|
||||
|
||||
function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const isHLS = error instanceof HLSUnsupportedError
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.px_lg,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
a.rounded_sm,
|
||||
a.gap_lg,
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
a.text_center,
|
||||
t.atoms.text_contrast_high,
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
{maxWidth: 300},
|
||||
]}>
|
||||
<VideoFallback.Container>
|
||||
<VideoFallback.Text>
|
||||
{isHLS ? (
|
||||
<Trans>
|
||||
Your browser does not support the video format. Please try a
|
||||
@@ -174,19 +151,8 @@ function VideoError({error, retry}: {error: unknown; retry: () => void}) {
|
||||
An error occurred while loading the video. Please try again later.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{!isHLS && (
|
||||
<Button
|
||||
onPress={retry}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
variant="solid"
|
||||
label={_(msg`Retry`)}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</VideoFallback.Text>
|
||||
{!isHLS && <VideoFallback.RetryButton onPress={retry} />}
|
||||
</VideoFallback.Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,76 @@
|
||||
import React, {useEffect, useRef, useState} from 'react'
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import Animated, {FadeInDown, FadeOutDown} from 'react-native-reanimated'
|
||||
import {VideoPlayer, VideoView} from 'expo-video'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIsFocused} from '@react-navigation/native'
|
||||
|
||||
import {useVideoPlayer} from 'view/com/util/post-embeds/VideoPlayerContext'
|
||||
import {android, atoms as a} from '#/alf'
|
||||
import {HITSLOP_30} from '#/lib/constants'
|
||||
import {useAppState} from '#/lib/hooks/useAppState'
|
||||
import {useVideoPlayer} from '#/view/com/util/post-embeds/VideoPlayerContext'
|
||||
import {android, atoms as a, useTheme} from '#/alf'
|
||||
import {Mute_Stroke2_Corner0_Rounded as MuteIcon} from '#/components/icons/Mute'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as UnmuteIcon} from '#/components/icons/Speaker'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
AudioCategory,
|
||||
PlatformInfo,
|
||||
} from '../../../../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
export function VideoEmbedInnerNative() {
|
||||
const player = useVideoPlayer()
|
||||
const ref = useRef<VideoView>(null)
|
||||
const isScreenFocused = useIsFocused()
|
||||
const isAppFocused = useAppState()
|
||||
const prevFocusedRef = useRef(isAppFocused)
|
||||
|
||||
// resume video when coming back from background
|
||||
useEffect(() => {
|
||||
if (isAppFocused !== prevFocusedRef.current) {
|
||||
prevFocusedRef.current = isAppFocused
|
||||
if (isAppFocused === 'active') {
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}, [isAppFocused, player])
|
||||
|
||||
// pause the video when the screen is not focused
|
||||
useEffect(() => {
|
||||
if (!isScreenFocused) {
|
||||
let wasPlaying = player.playing
|
||||
player.pause()
|
||||
|
||||
return () => {
|
||||
if (wasPlaying) player.play()
|
||||
}
|
||||
}
|
||||
}, [isScreenFocused, player])
|
||||
|
||||
const enterFullscreen = useCallback(() => {
|
||||
ref.current?.enterFullscreen()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={[a.flex_1, a.relative]} collapsable={false}>
|
||||
<View style={[a.flex_1, a.relative]}>
|
||||
<VideoView
|
||||
ref={ref}
|
||||
player={player}
|
||||
style={a.flex_1}
|
||||
style={[a.flex_1, a.rounded_sm]}
|
||||
nativeControls={true}
|
||||
onEnterFullscreen={() => {
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Playback)
|
||||
PlatformInfo.setAudioActive(false)
|
||||
player.muted = false
|
||||
}}
|
||||
onExitFullscreen={() => {
|
||||
PlatformInfo.setAudioCategory(AudioCategory.Ambient)
|
||||
PlatformInfo.setAudioActive(true)
|
||||
player.muted = true
|
||||
if (!player.playing) player.play()
|
||||
}}
|
||||
/>
|
||||
<Controls
|
||||
player={player}
|
||||
enterFullscreen={() => ref.current?.enterFullscreen()}
|
||||
/>
|
||||
<Controls player={player} enterFullscreen={enterFullscreen} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -33,6 +82,9 @@ function Controls({
|
||||
player: VideoPlayer
|
||||
enterFullscreen: () => void
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const t = useTheme()
|
||||
const [isMuted, setIsMuted] = useState(player.muted)
|
||||
const [duration, setDuration] = useState(() => Math.floor(player.duration))
|
||||
const [currentTime, setCurrentTime] = useState(() =>
|
||||
Math.floor(player.currentTime),
|
||||
@@ -47,50 +99,121 @@ function Controls({
|
||||
// duration gets reset to 0 on loop
|
||||
if (player.duration) setDuration(Math.floor(player.duration))
|
||||
setCurrentTime(Math.floor(player.currentTime))
|
||||
|
||||
// how often should we update the time?
|
||||
// 1000 gets out of sync with the video time
|
||||
}, 250)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const sub = player.addListener('volumeChange', ({isMuted}) => {
|
||||
setIsMuted(isMuted)
|
||||
})
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
sub.remove()
|
||||
}
|
||||
}, [player])
|
||||
|
||||
if (isNaN(timeRemaining)) {
|
||||
return null
|
||||
}
|
||||
const onPressFullscreen = useCallback(() => {
|
||||
switch (player.status) {
|
||||
case 'idle':
|
||||
case 'loading':
|
||||
case 'readyToPlay': {
|
||||
if (!player.playing) player.play()
|
||||
enterFullscreen()
|
||||
break
|
||||
}
|
||||
case 'error': {
|
||||
player.replay()
|
||||
break
|
||||
}
|
||||
}
|
||||
}, [player, enterFullscreen])
|
||||
|
||||
const toggleMuted = useCallback(() => {
|
||||
const muted = !player.muted
|
||||
// We want to set this to the _inverse_ of the new value, because we actually want for the audio to be mixed when
|
||||
// the video is muted, and vice versa.
|
||||
const mix = !muted
|
||||
const category = muted ? AudioCategory.Ambient : AudioCategory.Playback
|
||||
|
||||
PlatformInfo.setAudioCategory(category)
|
||||
PlatformInfo.setAudioActive(mix)
|
||||
player.muted = muted
|
||||
}, [player])
|
||||
|
||||
// show countdown when:
|
||||
// 1. timeRemaining is a number - was seeing NaNs
|
||||
// 2. duration is greater than 0 - means metadata has loaded
|
||||
// 3. we're less than 5 second into the video
|
||||
const showTime = !isNaN(timeRemaining) && duration > 0 && currentTime <= 5
|
||||
|
||||
return (
|
||||
<View style={[a.absolute, a.inset_0]}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75',
|
||||
{showTime && (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(300)}
|
||||
exiting={FadeOutDown.duration(500)}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
position: 'absolute',
|
||||
left: 5,
|
||||
bottom: 5,
|
||||
minHeight: 20,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
style={[
|
||||
{color: t.palette.white, fontSize: 12},
|
||||
a.font_bold,
|
||||
android({lineHeight: 1.25}),
|
||||
]}>
|
||||
{minutes}:{seconds}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={onPressFullscreen}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel={_(msg`Video`)}
|
||||
accessibilityHint={_(msg`Tap to enter full screen`)}
|
||||
accessibilityRole="button"
|
||||
/>
|
||||
{duration > 0 && (
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(300)}
|
||||
style={{
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
position: 'absolute',
|
||||
left: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
]}
|
||||
pointerEvents="none">
|
||||
<Text
|
||||
style={[
|
||||
{color: 'white', fontSize: 12},
|
||||
a.font_bold,
|
||||
android({lineHeight: 1.25}),
|
||||
]}>
|
||||
{minutes}:{seconds}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={enterFullscreen}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel="Video"
|
||||
accessibilityHint="Tap to enter full screen"
|
||||
accessibilityRole="button"
|
||||
/>
|
||||
right: 5,
|
||||
minHeight: 20,
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<Pressable
|
||||
onPress={toggleMuted}
|
||||
style={a.flex_1}
|
||||
accessibilityLabel={isMuted ? _(msg`Muted`) : _(msg`Unmuted`)}
|
||||
accessibilityHint={_(msg`Tap to toggle sound`)}
|
||||
accessibilityRole="button"
|
||||
hitSlop={HITSLOP_30}>
|
||||
{isMuted ? (
|
||||
<MuteIcon width={14} fill={t.palette.white} />
|
||||
) : (
|
||||
<UnmuteIcon width={14} fill={t.palette.white} />
|
||||
)}
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import {Text as TypoText} from '#/components/Typography'
|
||||
|
||||
export function Container({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.px_lg,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
a.rounded_sm,
|
||||
a.gap_lg,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function Text({children}: {children: React.ReactNode}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<TypoText
|
||||
style={[
|
||||
a.text_center,
|
||||
t.atoms.text_contrast_high,
|
||||
a.text_md,
|
||||
a.leading_snug,
|
||||
{maxWidth: 300},
|
||||
]}>
|
||||
{children}
|
||||
</TypoText>
|
||||
)
|
||||
}
|
||||
|
||||
export function RetryButton({onPress}: {onPress: () => void}) {
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
size="small"
|
||||
color="secondary_inverted"
|
||||
variant="solid"
|
||||
label={_(msg`Retry`)}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export function VideoPlayerProvider({
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
const player = useExpoVideoPlayer(source, player => {
|
||||
player.loop = true
|
||||
player.muted = true
|
||||
player.play()
|
||||
})
|
||||
|
||||
|
||||
@@ -12302,10 +12302,10 @@ expo-updates@~0.25.14:
|
||||
ignore "^5.3.1"
|
||||
resolve-from "^5.0.0"
|
||||
|
||||
expo-video@^1.1.10:
|
||||
version "1.1.10"
|
||||
resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-1.1.10.tgz#b47c0d40c21f401236639424bd25d70c09316b7b"
|
||||
integrity sha512-k9ecpgtwAK8Ut8enm8Jv398XkB/uVOyLLqk80M/d8pH9EN5CVrBQ7iEzWlR3quvVUFM7Uf5wRukJ4hk3mZ8NCg==
|
||||
expo-video@^1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/expo-video/-/expo-video-1.2.4.tgz#787342aded4295a1b6864f59227d178b93e1bb53"
|
||||
integrity sha512-pBK9mt7vYAbuPQjCSQxHQ7xrNjbmRheJep7JIStEg57O183/JRfP2blKuXniiSt1HBdZYPdoQnGRa3jGMXB9pg==
|
||||
|
||||
expo-web-browser@~13.0.3:
|
||||
version "13.0.3"
|
||||
|
||||
Reference in New Issue
Block a user