Repository navigation
fix(media): preview host files and stream videos across clients #9023
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
62e3571
fix(media): preview host files and stream videos across clients
juliusmarminge e67d3b1
fix(media): add video icons and paused previews
juliusmarminge 1dc4c2c
fix(media): secure file streams and renew preview capabilities
juliusmarminge b45ff76
fix(media): validate stream bounds and resolve network-path links
juliusmarminge f42882d
fix(media): keep preview states and error context consistent
juliusmarminge bde2af5
fix(media): avoid weak validators on mutable video streams
juliusmarminge af0ccf3
test(media): remove low-value rendering and wiring tests
juliusmarminge d84a531
fix(media): open workspace media links in the file viewer
juliusmarminge ef04cfe
fix(mobile): preserve UNC hosts in markdown file links
juliusmarminge d936826
fix(server): describe media file failures
juliusmarminge 5538b06
feat(media): add source tooltips and preview actions
juliusmarminge 50b1f5d
fix(web): use player controls for video downloads
juliusmarminge 8757284
fix(web): retain native recovery for failed videos
juliusmarminge 74b1818
chore: merge main into media preview branch
juliusmarminge 38a0c15
fix(web): refresh attachment URLs before opening
juliusmarminge cc36e83
fix(web): resolve protocol-relative media on desktop
juliusmarminge 0977bfc
fix(web): preserve expanded video aspect ratios
juliusmarminge File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| import { useIsFocused } from "@react-navigation/native"; | ||
| import { useEvent } from "expo"; | ||
| import { useVideoPlayer, VideoView } from "expo-video"; | ||
| import { useEffect, useRef, useState } from "react"; | ||
| import { ActivityIndicator, AppState, Pressable, View } from "react-native"; | ||
|
|
||
| import { loadMediaVideoSource } from "../lib/mediaVideoPlayback"; | ||
| import { AppText } from "./AppText"; | ||
| import { SymbolView } from "./AppSymbol"; | ||
|
|
||
| /** Loads only after Play or opening the viewer. Source replacement never starts playback itself. */ | ||
| function LoadedMediaVideo(props: { | ||
| readonly uri: string; | ||
| readonly playRequested: boolean; | ||
| readonly paused: boolean; | ||
| }) { | ||
| const focused = useIsFocused(); | ||
| const active = useRef(focused && AppState.currentState === "active"); | ||
| const [attempt, setAttempt] = useState(0); | ||
| const [loadError, setLoadError] = useState(false); | ||
| const player = useVideoPlayer(null, (player) => { | ||
| player.staysActiveInBackground = false; | ||
| player.bufferOptions = { preferredForwardBufferDuration: 5 }; | ||
| }); | ||
| const { status } = useEvent(player, "statusChange", { status: player.status }); | ||
|
|
||
| useEffect(() => { | ||
| active.current = focused && !props.paused && AppState.currentState === "active"; | ||
| if (!active.current) player.pause(); | ||
| const subscription = AppState.addEventListener("change", (state) => { | ||
| active.current = focused && !props.paused && state === "active"; | ||
| if (!active.current) player.pause(); | ||
| }); | ||
| return () => subscription.remove(); | ||
| }, [focused, player, props.paused]); | ||
|
|
||
| useEffect(() => { | ||
| const controller = new AbortController(); | ||
| setLoadError(false); | ||
| void loadMediaVideoSource(player, { | ||
| uri: props.uri, | ||
| signal: controller.signal, | ||
| playRequested: props.playRequested, | ||
| isActive: () => active.current, | ||
| }).catch(() => { | ||
| if (!controller.signal.aborted) setLoadError(true); | ||
| }); | ||
| return () => controller.abort(); | ||
| }, [player, props.uri, props.playRequested, attempt]); | ||
|
|
||
| return ( | ||
| <View collapsable={false} style={{ flex: 1 }}> | ||
| <VideoView | ||
| player={player} | ||
| style={{ width: "100%", height: "100%" }} | ||
| surfaceType="textureView" | ||
| nativeControls | ||
| contentFit="contain" | ||
| fullscreenOptions={{ enable: true }} | ||
| allowsPictureInPicture={false} | ||
| /> | ||
| {loadError || status === "error" ? ( | ||
| <View className="absolute inset-0 items-center justify-center gap-2 bg-black px-4"> | ||
| <AppText className="text-center text-sm text-white/80">Video unavailable</AppText> | ||
| <Pressable | ||
| accessibilityRole="button" | ||
| accessibilityLabel="Retry video" | ||
| onPress={() => setAttempt((value) => value + 1)} | ||
| className="min-h-11 justify-center px-4" | ||
| > | ||
| <AppText className="text-sm text-white">Retry</AppText> | ||
| </Pressable> | ||
| </View> | ||
| ) : status === "loading" || status === "idle" ? ( | ||
| <View pointerEvents="none" className="absolute inset-0 items-center justify-center"> | ||
| <ActivityIndicator color="#ffffff" accessibilityLabel="Loading video" /> | ||
| </View> | ||
| ) : null} | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| export function MediaVideoPlayer(props: { | ||
| readonly uri: string | null; | ||
| readonly name: string; | ||
| readonly unavailable?: boolean; | ||
| readonly expanded?: boolean; | ||
| readonly paused?: boolean; | ||
| readonly onExpand?: () => void; | ||
| }) { | ||
| const [playbackUri, setPlaybackUri] = useState<string | null>(null); | ||
| const uri = playbackUri ?? (props.expanded ? props.uri : null); | ||
|
|
||
| return ( | ||
| <View | ||
| collapsable={false} | ||
| className="overflow-hidden rounded-[10px] bg-black" | ||
| style={props.expanded ? { flex: 1 } : { width: "100%", maxWidth: 480, aspectRatio: 16 / 9 }} | ||
| > | ||
| {uri ? ( | ||
| <LoadedMediaVideo | ||
| uri={uri} | ||
| playRequested={playbackUri !== null} | ||
| paused={props.paused ?? false} | ||
| /> | ||
| ) : ( | ||
| <Pressable | ||
| accessibilityRole="button" | ||
| accessibilityLabel={`Play ${props.name}`} | ||
| accessibilityState={{ disabled: props.uri === null || props.unavailable === true }} | ||
| disabled={props.uri === null || props.unavailable === true} | ||
| onPress={() => setPlaybackUri(props.uri)} | ||
| className="flex-1 items-center justify-center gap-2 px-4" | ||
| > | ||
| {props.unavailable ? ( | ||
| <AppText className="text-sm text-white/80">Video unavailable</AppText> | ||
| ) : props.uri === null ? ( | ||
| <ActivityIndicator color="#ffffff" accessibilityLabel="Loading video" /> | ||
| ) : ( | ||
| <> | ||
| <SymbolView name="play" size={28} tintColor="#ffffff" type="monochrome" /> | ||
| <AppText className="text-center text-xs text-white/80" numberOfLines={2}> | ||
| {props.name} | ||
| </AppText> | ||
| </> | ||
| )} | ||
| </Pressable> | ||
| )} | ||
| {props.onExpand ? ( | ||
| <Pressable | ||
| accessibilityRole="button" | ||
| accessibilityLabel={`Expand ${props.name}`} | ||
| onPress={() => { | ||
| setPlaybackUri(null); | ||
| props.onExpand?.(); | ||
| }} | ||
| className="absolute right-1 top-1 min-h-11 min-w-11 items-center justify-center rounded-md bg-black/60 px-2" | ||
| > | ||
| <AppText className="text-xs text-white">Expand</AppText> | ||
| </Pressable> | ||
| ) : null} | ||
| </View> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.