mirror of
https://github.com/movie-web/native-app.git
synced 2025-09-13 16:33:26 +00:00
feat: quality selector
This commit is contained in:
@@ -9,6 +9,7 @@ import { Controls } from "./Controls";
|
|||||||
import { DownloadButton } from "./DownloadButton";
|
import { DownloadButton } from "./DownloadButton";
|
||||||
import { PlaybackSpeedSelector } from "./PlaybackSpeedSelector";
|
import { PlaybackSpeedSelector } from "./PlaybackSpeedSelector";
|
||||||
import { ProgressBar } from "./ProgressBar";
|
import { ProgressBar } from "./ProgressBar";
|
||||||
|
import { QualitySelector } from "./QualitySelector";
|
||||||
import { SeasonSelector } from "./SeasonEpisodeSelector";
|
import { SeasonSelector } from "./SeasonEpisodeSelector";
|
||||||
import { SourceSelector } from "./SourceSelector";
|
import { SourceSelector } from "./SourceSelector";
|
||||||
import { mapMillisecondsToTime } from "./utils";
|
import { mapMillisecondsToTime } from "./utils";
|
||||||
@@ -80,6 +81,7 @@ export const BottomControls = () => {
|
|||||||
<SourceSelector />
|
<SourceSelector />
|
||||||
<AudioTrackSelector />
|
<AudioTrackSelector />
|
||||||
<PlaybackSpeedSelector />
|
<PlaybackSpeedSelector />
|
||||||
|
<QualitySelector />
|
||||||
<DownloadButton />
|
<DownloadButton />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
111
apps/expo/src/components/player/QualitySelector.tsx
Normal file
111
apps/expo/src/components/player/QualitySelector.tsx
Normal file
@@ -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 (
|
||||||
|
<>
|
||||||
|
<Controls>
|
||||||
|
<MWButton
|
||||||
|
type="secondary"
|
||||||
|
icon={
|
||||||
|
<MaterialIcons
|
||||||
|
name="hd"
|
||||||
|
size={24}
|
||||||
|
color={theme.buttonSecondaryText.val}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onPress={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
Quality
|
||||||
|
</MWButton>
|
||||||
|
</Controls>
|
||||||
|
|
||||||
|
<Settings.Sheet
|
||||||
|
forceRemoveScrollEnabled={open}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
>
|
||||||
|
<Settings.SheetOverlay />
|
||||||
|
<Settings.SheetHandle />
|
||||||
|
<Settings.SheetFrame>
|
||||||
|
<Settings.Header
|
||||||
|
icon={
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="close"
|
||||||
|
size={24}
|
||||||
|
color={theme.playerSettingsUnactiveText.val}
|
||||||
|
onPress={() => setOpen(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Quality settings"
|
||||||
|
/>
|
||||||
|
<Settings.Content>
|
||||||
|
{qualityMap?.map((quality) => (
|
||||||
|
<Settings.Item
|
||||||
|
key={quality.quality}
|
||||||
|
title={quality.quality}
|
||||||
|
iconRight={
|
||||||
|
quality.quality === currentQuality && (
|
||||||
|
<MaterialCommunityIcons
|
||||||
|
name="check-circle"
|
||||||
|
size={24}
|
||||||
|
color={theme.sheetItemSelected.val}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onPress={() => {
|
||||||
|
void videoRef.unloadAsync();
|
||||||
|
void videoRef.loadAsync(
|
||||||
|
{ uri: quality.url, headers: stream.headers },
|
||||||
|
{ shouldPlay: true },
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Settings.Content>
|
||||||
|
</Settings.SheetFrame>
|
||||||
|
</Settings.Sheet>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@@ -1,4 +1,3 @@
|
|||||||
import type { AVPlaybackSource } from "expo-av";
|
|
||||||
import type { SharedValue } from "react-native-reanimated";
|
import type { SharedValue } from "react-native-reanimated";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Dimensions, Platform } from "react-native";
|
import { Dimensions, Platform } from "react-native";
|
||||||
@@ -42,7 +41,6 @@ export const VideoPlayer = () => {
|
|||||||
const { currentSpeed } = usePlaybackSpeed();
|
const { currentSpeed } = usePlaybackSpeed();
|
||||||
const { synchronizePlayback } = useAudioTrack();
|
const { synchronizePlayback } = useAudioTrack();
|
||||||
const { dismissFullscreenPlayer } = usePlayer();
|
const { dismissFullscreenPlayer } = usePlayer();
|
||||||
const [videoSrc, setVideoSrc] = useState<AVPlaybackSource>();
|
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [resizeMode, setResizeMode] = useState(ResizeMode.CONTAIN);
|
const [resizeMode, setResizeMode] = useState(ResizeMode.CONTAIN);
|
||||||
const [hasStartedPlaying, setHasStartedPlaying] = useState(false);
|
const [hasStartedPlaying, setHasStartedPlaying] = useState(false);
|
||||||
@@ -57,6 +55,8 @@ export const VideoPlayer = () => {
|
|||||||
const videoRef = usePlayerStore((state) => state.videoRef);
|
const videoRef = usePlayerStore((state) => state.videoRef);
|
||||||
const asset = usePlayerStore((state) => state.asset);
|
const asset = usePlayerStore((state) => state.asset);
|
||||||
const setVideoRef = usePlayerStore((state) => state.setVideoRef);
|
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 setStatus = usePlayerStore((state) => state.setStatus);
|
||||||
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
const setIsIdle = usePlayerStore((state) => state.setIsIdle);
|
||||||
const toggleAudio = usePlayerStore((state) => state.toggleAudio);
|
const toggleAudio = usePlayerStore((state) => state.toggleAudio);
|
||||||
@@ -201,6 +201,7 @@ export const VideoPlayer = () => {
|
|||||||
hasStartedPlaying,
|
hasStartedPlaying,
|
||||||
router,
|
router,
|
||||||
selectedAudioTrack,
|
selectedAudioTrack,
|
||||||
|
setVideoSrc,
|
||||||
stream,
|
stream,
|
||||||
synchronizePlayback,
|
synchronizePlayback,
|
||||||
]);
|
]);
|
||||||
|
@@ -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 { Asset } from "expo-media-library";
|
||||||
|
|
||||||
import type { ScrapeMedia } from "@movie-web/provider-utils";
|
import type { ScrapeMedia } from "@movie-web/provider-utils";
|
||||||
@@ -30,11 +30,13 @@ export interface PlayerMeta {
|
|||||||
|
|
||||||
export interface VideoSlice {
|
export interface VideoSlice {
|
||||||
videoRef: Video | null;
|
videoRef: Video | null;
|
||||||
|
videoSrc: AVPlaybackSourceObject | null;
|
||||||
status: AVPlaybackStatus | null;
|
status: AVPlaybackStatus | null;
|
||||||
meta: PlayerMeta | null;
|
meta: PlayerMeta | null;
|
||||||
asset: Asset | null;
|
asset: Asset | null;
|
||||||
|
|
||||||
setVideoRef(ref: Video | null): void;
|
setVideoRef(ref: Video | null): void;
|
||||||
|
setVideoSrc(src: AVPlaybackSourceObject | null): void;
|
||||||
setStatus(status: AVPlaybackStatus | null): void;
|
setStatus(status: AVPlaybackStatus | null): void;
|
||||||
setMeta(meta: PlayerMeta | null): void;
|
setMeta(meta: PlayerMeta | null): void;
|
||||||
setAsset(asset: Asset | null): void;
|
setAsset(asset: Asset | null): void;
|
||||||
@@ -67,6 +69,7 @@ export const convertMetaToScrapeMedia = (meta: PlayerMeta): ScrapeMedia => {
|
|||||||
|
|
||||||
export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
|
export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
|
||||||
videoRef: null,
|
videoRef: null,
|
||||||
|
videoSrc: null,
|
||||||
status: null,
|
status: null,
|
||||||
meta: null,
|
meta: null,
|
||||||
asset: null,
|
asset: null,
|
||||||
@@ -74,6 +77,11 @@ export const createVideoSlice: MakeSlice<VideoSlice> = (set) => ({
|
|||||||
setVideoRef: (ref) => {
|
setVideoRef: (ref) => {
|
||||||
set({ videoRef: ref });
|
set({ videoRef: ref });
|
||||||
},
|
},
|
||||||
|
setVideoSrc: (src) => {
|
||||||
|
set((s) => {
|
||||||
|
s.videoSrc = src;
|
||||||
|
});
|
||||||
|
},
|
||||||
setStatus: (status) => {
|
setStatus: (status) => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.status = status;
|
s.status = status;
|
||||||
|
Reference in New Issue
Block a user