diff --git a/src/assets/icons/waveform.svg b/src/assets/icons/waveform.svg new file mode 100644 index 0000000..e39cc74 --- /dev/null +++ b/src/assets/icons/waveform.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/screens/modals/Player/components/MediaControls.tsx b/src/screens/modals/Player/components/MediaControls.tsx index 3a71753..bc366ce 100644 --- a/src/screens/modals/Player/components/MediaControls.tsx +++ b/src/screens/modals/Player/components/MediaControls.tsx @@ -19,6 +19,7 @@ const previous = () => TrackPlayer.skipToPrevious(); const Container = styled.View` align-items: center; margin-top: 40px; + margin-bottom: 52px; `; const Buttons = styled.View` diff --git a/src/screens/modals/Player/components/MediaInformation.tsx b/src/screens/modals/Player/components/MediaInformation.tsx new file mode 100644 index 0000000..3d759df --- /dev/null +++ b/src/screens/modals/Player/components/MediaInformation.tsx @@ -0,0 +1,72 @@ +import { Text } from '@/components/Typography'; +import { useTypedSelector } from '@/store'; +import useCurrentTrack from '@/utility/useCurrentTrack'; +import React from 'react-native'; +import WaveformIcon from '@/assets/icons/waveform.svg'; +import useDefaultStyles from '@/components/Colors'; +import styled, { css } from 'styled-components/native'; + +const Container = styled.View` + flex-direction: row; + gap: 8px; + margin-top: 8px; + margin-bottom: 16px; +`; + +const Info = styled.View` + flex-direction: row; + justify-content: space-between; + gap: 8px; + flex-grow: 1; + flex-shrink: 1; +`; + +const Label = styled(Text)<{ overflow?: boolean }>` + opacity: 0.5; + font-size: 13px; + + ${(props) => props?.overflow && css` + flex: 0 1 auto; + `} +`; + +export default function MediaInformation() { + const styles = useDefaultStyles(); + const { track } = useCurrentTrack(); + const { entities } = useTypedSelector((state) => state.music.tracks); + + if (!track) { + return null; + } + + const albumTrack = entities[track.backendId]; + const mediaStream = albumTrack.MediaStreams?.find((d) => d.Type === 'Audio'); + + console.log(mediaStream); + + return ( + + + + + + {mediaStream && ( + <> + + + + )} + + + ); +} \ No newline at end of file diff --git a/src/screens/modals/Player/components/Timer.tsx b/src/screens/modals/Player/components/Timer.tsx index 1b1afb5..cdbab18 100644 --- a/src/screens/modals/Player/components/Timer.tsx +++ b/src/screens/modals/Player/components/Timer.tsx @@ -13,7 +13,6 @@ import { t } from '@/localisation'; const Container = styled.View` align-self: flex-start; align-items: flex-start; - margin-top: 52px; padding: 8px; margin-left: -8px; flex: 0 1 auto; diff --git a/src/screens/modals/Player/index.tsx b/src/screens/modals/Player/index.tsx index aeea5a8..2c1cf35 100644 --- a/src/screens/modals/Player/index.tsx +++ b/src/screens/modals/Player/index.tsx @@ -12,6 +12,7 @@ import Timer from './components/Timer'; import styled from 'styled-components/native'; import { ColoredBlurView } from '@/components/Colors.tsx'; import LyricsPreview from './components/LyricsPreview.tsx'; +import MediaInformation from './components/MediaInformation'; const Group = styled.View` flex-direction: row; @@ -28,6 +29,7 @@ export default function Player() { + diff --git a/src/store/music/types.ts b/src/store/music/types.ts index 99a683c..109d0dd 100644 --- a/src/store/music/types.ts +++ b/src/store/music/types.ts @@ -8,6 +8,29 @@ export interface UserData { Key: string; } +export interface MediaStream { + Codec: string + TimeBase: string + VideoRange: string + VideoRangeType: string + AudioSpatialFormat: string + DisplayTitle: string + IsInterlaced: boolean + ChannelLayout: string + BitRate: number + Channels: number + SampleRate: number + IsDefault: boolean + IsForced: boolean + IsHearingImpaired: boolean + Type: string + Index: number + IsExternal: boolean + IsTextSubtitleStream: boolean + SupportsExternalStream: boolean + Level: number +} + export interface ArtistItem { Name: string; Id: string; @@ -71,6 +94,7 @@ export interface AlbumTrack { MediaType: string; HasLyrics: boolean; Lyrics: Lyrics | null; + MediaStreams: MediaStream[]; } export interface State { diff --git a/src/utility/JellyfinApi/album.ts b/src/utility/JellyfinApi/album.ts index 11e9754..97004b0 100644 --- a/src/utility/JellyfinApi/album.ts +++ b/src/utility/JellyfinApi/album.ts @@ -61,6 +61,7 @@ export async function retrieveAlbumTracks(ItemId: string) { const singleAlbumOptions = { ParentId: ItemId, SortBy: 'ParentIndexNumber,IndexNumber,SortName', + Fields: 'MediaStreams', }; const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString(); diff --git a/src/utility/JellyfinApi/lib.ts b/src/utility/JellyfinApi/lib.ts index 8e30ac2..19e1453 100644 --- a/src/utility/JellyfinApi/lib.ts +++ b/src/utility/JellyfinApi/lib.ts @@ -15,6 +15,9 @@ function generateConfig(credentials: Credentials): RequestInit { }; } +/** + * Retrieve a copy of the store without getting caught in import cycles. + */ export function asyncFetchStore() { return require('@/store').default as Store; } @@ -74,7 +77,7 @@ export async function fetchApi( const data = await response.json(); throw data; } catch { - throw response; + throw new Error('FailedRequest'); } } diff --git a/src/utility/JellyfinApi/track.ts b/src/utility/JellyfinApi/track.ts index 73f05bb..8fa218f 100644 --- a/src/utility/JellyfinApi/track.ts +++ b/src/utility/JellyfinApi/track.ts @@ -22,8 +22,9 @@ const baseTrackOptions: Record = { TranscodingContainer: 'aac', AudioCodec: 'aac', Container: 'mp3,aac', + audioBitRate: '320000', ...trackOptionsOsOverrides[Platform.OS], -}; +} as const; /** * Generate the track streaming url from the trackId @@ -47,10 +48,12 @@ export function generateTrackUrl(trackId: string) { * Generate a track object from a Jellyfin ItemId so that * react-native-track-player can easily consume it. */ -export function generateTrack(track: AlbumTrack): Track { +export async function generateTrack(track: AlbumTrack): Promise { // Also construct the URL for the stream const url = generateTrackUrl(track.Id); + const response = await fetch(url, { method: 'HEAD' }); + return { url, backendId: track.Id, @@ -63,6 +66,9 @@ export function generateTrack(track: AlbumTrack): Track { : getImage(track.Id), hasLyrics: track.HasLyrics, lyrics: track.Lyrics, + contentType: response.headers.get('Content-Type') || undefined, + isDirectPlay: response.headers.has('Content-Length'), + bitRate: baseTrackOptions.audioBitRate, }; } diff --git a/src/utility/useCurrentTrack.ts b/src/utility/useCurrentTrack.ts index 703eb74..3145b9d 100644 --- a/src/utility/useCurrentTrack.ts +++ b/src/utility/useCurrentTrack.ts @@ -27,9 +27,9 @@ export default function useCurrentTrack(): CurrentTrackResponse { // Retrieve the current track from the queue using the index const retrieveCurrentTrack = useCallback(async () => { const queue = await TrackPlayer.getQueue(); - const currentTrackIndex = await TrackPlayer.getCurrentTrack(); - if (currentTrackIndex !== null) { - setTrack(queue[currentTrackIndex] as Track); + const currentTrackIndex = await TrackPlayer.getActiveTrackIndex(); + if (currentTrackIndex !== undefined) { + setTrack(queue[currentTrackIndex]); setIndex(currentTrackIndex); } else { setTrack(undefined); diff --git a/src/utility/usePlayTracks.ts b/src/utility/usePlayTracks.ts index 15d4fc8..0f0cc40 100644 --- a/src/utility/usePlayTracks.ts +++ b/src/utility/usePlayTracks.ts @@ -49,7 +49,7 @@ export default function usePlayTracks() { const queue = await TrackPlayer.getQueue(); // Convert all trackIds to the relevant format for react-native-track-player - const generatedTracks = trackIds.map((trackId) => { + const generatedTracks = (await Promise.all(trackIds.map(async (trackId) => { const track = tracks[trackId]; // GUARD: Check that the track actually exists in Redux @@ -58,7 +58,7 @@ export default function usePlayTracks() { } // Retrieve the generated track from Jellyfin - const generatedTrack = generateTrack(track); + const generatedTrack = await generateTrack(track); // Check if a downloaded version exists, and if so rewrite the URL const download = downloads[trackId]; @@ -67,7 +67,7 @@ export default function usePlayTracks() { } return generatedTrack; - }).filter((t): t is Track => typeof t !== 'undefined'); + }))).filter((t): t is Track => typeof t !== 'undefined'); // Potentially shuffle all tracks const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;