From ea435d91defd6b07fc04126ebe4e6e5470aa11f5 Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Sun, 24 Mar 2024 17:36:14 +0100 Subject: [PATCH] feat: quality selector --- .../src/components/player/BottomControls.tsx | 2 + .../src/components/player/QualitySelector.tsx | 111 ++++++++++++++++++ .../src/components/player/VideoPlayer.tsx | 5 +- apps/expo/src/stores/player/slices/video.ts | 10 +- 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 apps/expo/src/components/player/QualitySelector.tsx diff --git a/apps/expo/src/components/player/BottomControls.tsx b/apps/expo/src/components/player/BottomControls.tsx index 846f10c..539fe26 100644 --- a/apps/expo/src/components/player/BottomControls.tsx +++ b/apps/expo/src/components/player/BottomControls.tsx @@ -9,6 +9,7 @@ import { Controls } from "./Controls"; import { DownloadButton } from "./DownloadButton"; import { PlaybackSpeedSelector } from "./PlaybackSpeedSelector"; import { ProgressBar } from "./ProgressBar"; +import { QualitySelector } from "./QualitySelector"; import { SeasonSelector } from "./SeasonEpisodeSelector"; import { SourceSelector } from "./SourceSelector"; import { mapMillisecondsToTime } from "./utils"; @@ -80,6 +81,7 @@ export const BottomControls = () => { + diff --git a/apps/expo/src/components/player/QualitySelector.tsx b/apps/expo/src/components/player/QualitySelector.tsx new file mode 100644 index 0000000..3f05c31 --- /dev/null +++ b/apps/expo/src/components/player/QualitySelector.tsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"; +import { useTheme } from "tamagui"; + +import { constructFullUrl } from "~/lib/url"; +import { usePlayerStore } from "~/stores/player/store"; +import { MWButton } from "../ui/Button"; +import { Controls } from "./Controls"; +import { Settings } from "./settings/Sheet"; + +export const QualitySelector = () => { + const theme = useTheme(); + const [open, setOpen] = useState(false); + const videoRef = usePlayerStore((state) => state.videoRef); + const videoSrc = usePlayerStore((state) => state.videoSrc); + const stream = usePlayerStore((state) => state.interface.currentStream); + const hlsTracks = usePlayerStore((state) => state.interface.hlsTracks); + + if (!videoRef || !videoSrc || !stream) return null; + let qualityMap: { quality: string; url: string }[]; + let currentQuality: string | undefined; + + if (stream.type === "file") { + const { qualities } = stream; + + currentQuality = Object.keys(qualities).find( + (key) => qualities[key as keyof typeof qualities]!.url === videoSrc.uri, + ); + + qualityMap = Object.keys(qualities).map((key: string) => ({ + quality: key, + url: qualities[key as keyof typeof qualities]!.url, + })); + } else if (stream.type === "hls") { + if (!hlsTracks?.video) return null; + + qualityMap = hlsTracks.video.map((video) => ({ + quality: + (video.properties[0]?.attributes.resolution as string) ?? "unknown", + url: constructFullUrl(stream.playlist, video.uri), + })); + } else { + return null; + } + + return ( + <> + + + } + onPress={() => setOpen(true)} + > + Quality + + + + + + + + setOpen(false)} + /> + } + title="Quality settings" + /> + + {qualityMap?.map((quality) => ( + + ) + } + onPress={() => { + void videoRef.unloadAsync(); + void videoRef.loadAsync( + { uri: quality.url, headers: stream.headers }, + { shouldPlay: true }, + ); + }} + /> + ))} + + + + + ); +}; diff --git a/apps/expo/src/components/player/VideoPlayer.tsx b/apps/expo/src/components/player/VideoPlayer.tsx index 181e028..d927336 100644 --- a/apps/expo/src/components/player/VideoPlayer.tsx +++ b/apps/expo/src/components/player/VideoPlayer.tsx @@ -1,4 +1,3 @@ -import type { AVPlaybackSource } from "expo-av"; import type { SharedValue } from "react-native-reanimated"; import { useEffect, useState } from "react"; import { Dimensions, Platform } from "react-native"; @@ -42,7 +41,6 @@ export const VideoPlayer = () => { const { currentSpeed } = usePlaybackSpeed(); const { synchronizePlayback } = useAudioTrack(); const { dismissFullscreenPlayer } = usePlayer(); - const [videoSrc, setVideoSrc] = useState(); const [isLoading, setIsLoading] = useState(true); const [resizeMode, setResizeMode] = useState(ResizeMode.CONTAIN); const [hasStartedPlaying, setHasStartedPlaying] = useState(false); @@ -57,6 +55,8 @@ export const VideoPlayer = () => { const videoRef = usePlayerStore((state) => state.videoRef); const asset = usePlayerStore((state) => state.asset); const setVideoRef = usePlayerStore((state) => state.setVideoRef); + const videoSrc = usePlayerStore((state) => state.videoSrc) ?? undefined; + const setVideoSrc = usePlayerStore((state) => state.setVideoSrc); const setStatus = usePlayerStore((state) => state.setStatus); const setIsIdle = usePlayerStore((state) => state.setIsIdle); const toggleAudio = usePlayerStore((state) => state.toggleAudio); @@ -201,6 +201,7 @@ export const VideoPlayer = () => { hasStartedPlaying, router, selectedAudioTrack, + setVideoSrc, stream, synchronizePlayback, ]); diff --git a/apps/expo/src/stores/player/slices/video.ts b/apps/expo/src/stores/player/slices/video.ts index 26a9516..4e57f9b 100644 --- a/apps/expo/src/stores/player/slices/video.ts +++ b/apps/expo/src/stores/player/slices/video.ts @@ -1,4 +1,4 @@ -import type { AVPlaybackStatus, Video } from "expo-av"; +import type { AVPlaybackSourceObject, AVPlaybackStatus, Video } from "expo-av"; import type { Asset } from "expo-media-library"; import type { ScrapeMedia } from "@movie-web/provider-utils"; @@ -30,11 +30,13 @@ export interface PlayerMeta { export interface VideoSlice { videoRef: Video | null; + videoSrc: AVPlaybackSourceObject | null; status: AVPlaybackStatus | null; meta: PlayerMeta | null; asset: Asset | null; setVideoRef(ref: Video | null): void; + setVideoSrc(src: AVPlaybackSourceObject | null): void; setStatus(status: AVPlaybackStatus | null): void; setMeta(meta: PlayerMeta | null): void; setAsset(asset: Asset | null): void; @@ -67,6 +69,7 @@ export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => { export const createVideoSlice: MakeSlice = (set) => ({ videoRef: null, + videoSrc: null, status: null, meta: null, asset: null, @@ -74,6 +77,11 @@ export const createVideoSlice: MakeSlice = (set) => ({ setVideoRef: (ref) => { set({ videoRef: ref }); }, + setVideoSrc: (src) => { + set((s) => { + s.videoSrc = src; + }); + }, setStatus: (status) => { set((s) => { s.status = status;