Compare commits

...

4 Commits

Author SHA1 Message Date
Lei Nelissen
e601b0d258 fix: only overflow direct play 2024-07-25 17:13:27 +02:00
Lei Nelissen
065515c25b chore: translation 2024-07-25 17:12:23 +02:00
Lei Nelissen
0cd6d5d05c fix: redundant console.log 2024-07-25 16:58:22 +02:00
Lei Nelissen
92c82b9f0a feat: add base codec info to player 2024-07-25 16:58:22 +02:00
14 changed files with 148 additions and 13 deletions

View File

@@ -0,0 +1,8 @@
<svg width="56" height="56" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M50.5303 36.5379C51.7368 36.5379 52.7016 35.573 52.7016 34.3935V21.6065C52.7016 20.427 51.7368 19.4352 50.5303 19.4352C49.2972 19.4352 48.3859 20.427 48.3859 21.6065V34.3935C48.3859 35.573 49.2972 36.5379 50.5303 36.5379Z" />
<path d="M41.5233 50.0488C42.7295 50.0488 43.6677 49.0839 43.6677 47.9043V8.09575C43.6677 6.91623 42.7295 5.92436 41.5233 5.92436C40.2633 5.92436 39.352 6.91623 39.352 8.09575V47.9043C39.352 49.0839 40.2633 50.0488 41.5233 50.0488Z" />
<path d="M32.4894 41.9261C33.7224 41.9261 34.6607 40.9879 34.6607 39.7817V16.2183C34.6607 15.012 33.7224 14.0469 32.4894 14.0469C31.256 14.0469 30.3447 15.012 30.3447 16.2183V39.7817C30.3447 40.9879 31.256 41.9261 32.4894 41.9261Z" />
<path d="M23.4553 56C24.6884 56 25.6535 55.0348 25.6535 53.8287V2.17137C25.6535 0.965053 24.6884 0 23.4553 0C22.249 0 21.3376 0.965053 21.3376 2.17137V53.8287C21.3376 55.0348 22.249 56 23.4553 56Z" />
<path d="M14.4481 45.1966C15.6812 45.1966 16.6195 44.2317 16.6195 43.0253V12.9746C16.6195 11.7683 15.6812 10.7764 14.4481 10.7764C13.2418 10.7764 12.3035 11.7683 12.3035 12.9746V43.0253C12.3035 44.2317 13.2418 45.1966 14.4481 45.1966Z" />
<path d="M5.41411 34.2326C6.67405 34.2326 7.61231 33.2675 7.61231 32.0613V23.9387C7.61231 22.7324 6.67405 21.7405 5.41411 21.7405C4.2078 21.7405 3.29636 22.7324 3.29636 23.9387V32.0613C3.29636 33.2675 4.2078 34.2326 5.41411 34.2326Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -76,5 +76,9 @@
"delete": "Delete", "delete": "Delete",
"cancel": "Cancel", "cancel": "Cancel",
"disc": "Disc", "disc": "Disc",
"lyrics": "Lyrics" "lyrics": "Lyrics",
"direct-play": "Direct play",
"transcoded": "Transcoded",
"khz": "kHz",
"kbps": "kbps"
} }

View File

@@ -75,5 +75,9 @@
"sleep-timer": "Slaaptimer", "sleep-timer": "Slaaptimer",
"delete": "Verwijder", "delete": "Verwijder",
"cancel": "Annuleer", "cancel": "Annuleer",
"disc": "Schijf" "disc": "Schijf",
"direct-play": "Direct afgespeeld",
"transcoded": "Getranscodeerd",
"khz": "kHz",
"kbps": "kbps"
} }

View File

@@ -75,4 +75,8 @@ export type LocaleKeys = 'play-next'
| 'delete' | 'delete'
| 'cancel' | 'cancel'
| 'disc' | 'disc'
| 'lyrics'; | 'lyrics'
| 'direct-play'
| 'transcoded'
| 'khz'
| 'kbps'

View File

@@ -19,6 +19,7 @@ const previous = () => TrackPlayer.skipToPrevious();
const Container = styled.View` const Container = styled.View`
align-items: center; align-items: center;
margin-top: 40px; margin-top: 40px;
margin-bottom: 52px;
`; `;
const Buttons = styled.View` const Buttons = styled.View`

View File

@@ -0,0 +1,79 @@
import { Text } from '@/components/Typography';
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';
import { useMemo } from 'react';
import { t } from '@/localisation';
const Container = styled.View`
flex-direction: row;
gap: 8px;
margin-top: 12px;
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;
`}
`;
/**
* This component displays information about the media that is being played
* back, such as the bitrate, sample rate, codec and whether it's transcoded.
*/
export default function MediaInformation() {
const styles = useDefaultStyles();
const { track, albumTrack } = useCurrentTrack();
const mediaStream = useMemo(() => (
albumTrack?.MediaStreams?.find((d) => d.Type === 'Audio')
), [albumTrack]);
if (!albumTrack || !track) {
return null;
}
return (
<Container>
<WaveformIcon fill={styles.icon.color} height={16} width={16} />
<Info>
<Label numberOfLines={1} overflow>
{track.isDirectPlay ? t('direct-play') : t('transcoded')}
</Label>
<Label numberOfLines={1}>
{track.isDirectPlay
? mediaStream?.Codec.toUpperCase()
: track.contentType?.replace('audio/', '').toUpperCase()
}
</Label>
{mediaStream && (
<>
<Label numberOfLines={1}>
{((track.isDirectPlay ? mediaStream.BitRate : track.bitRate) / 1000)
.toFixed(0)}
{t('kbps')}
</Label>
<Label numberOfLines={1}>
{(mediaStream.SampleRate / 1000).toFixed(1)}
{t('khz')}
</Label>
</>
)}
</Info>
</Container>
);
}

View File

@@ -13,7 +13,6 @@ import { t } from '@/localisation';
const Container = styled.View` const Container = styled.View`
align-self: flex-start; align-self: flex-start;
align-items: flex-start; align-items: flex-start;
margin-top: 52px;
padding: 8px; padding: 8px;
margin-left: -8px; margin-left: -8px;
flex: 0 1 auto; flex: 0 1 auto;

View File

@@ -12,6 +12,7 @@ import Timer from './components/Timer';
import styled from 'styled-components/native'; import styled from 'styled-components/native';
import { ColoredBlurView } from '@/components/Colors.tsx'; import { ColoredBlurView } from '@/components/Colors.tsx';
import LyricsPreview from './components/LyricsPreview.tsx'; import LyricsPreview from './components/LyricsPreview.tsx';
import MediaInformation from './components/MediaInformation';
const Group = styled.View` const Group = styled.View`
flex-direction: row; flex-direction: row;
@@ -28,6 +29,7 @@ export default function Player() {
<NowPlaying /> <NowPlaying />
<ConnectionNotice /> <ConnectionNotice />
<StreamStatus /> <StreamStatus />
<MediaInformation />
<ProgressBar /> <ProgressBar />
<MediaControls /> <MediaControls />
<Group> <Group>

View File

@@ -8,6 +8,29 @@ export interface UserData {
Key: string; 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 { export interface ArtistItem {
Name: string; Name: string;
Id: string; Id: string;
@@ -71,6 +94,7 @@ export interface AlbumTrack {
MediaType: string; MediaType: string;
HasLyrics: boolean; HasLyrics: boolean;
Lyrics: Lyrics | null; Lyrics: Lyrics | null;
MediaStreams: MediaStream[];
} }
export interface State { export interface State {

View File

@@ -61,6 +61,7 @@ export async function retrieveAlbumTracks(ItemId: string) {
const singleAlbumOptions = { const singleAlbumOptions = {
ParentId: ItemId, ParentId: ItemId,
SortBy: 'ParentIndexNumber,IndexNumber,SortName', SortBy: 'ParentIndexNumber,IndexNumber,SortName',
Fields: 'MediaStreams',
}; };
const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString(); const singleAlbumParams = new URLSearchParams(singleAlbumOptions).toString();

View File

@@ -15,6 +15,9 @@ function generateConfig(credentials: Credentials): RequestInit {
}; };
} }
/**
* Retrieve a copy of the store without getting caught in import cycles.
*/
export function asyncFetchStore() { export function asyncFetchStore() {
return require('@/store').default as Store; return require('@/store').default as Store;
} }
@@ -74,7 +77,7 @@ export async function fetchApi<T>(
const data = await response.json(); const data = await response.json();
throw data; throw data;
} catch { } catch {
throw response; throw new Error('FailedRequest');
} }
} }

View File

@@ -22,8 +22,9 @@ const baseTrackOptions: Record<string, string> = {
TranscodingContainer: 'aac', TranscodingContainer: 'aac',
AudioCodec: 'aac', AudioCodec: 'aac',
Container: 'mp3,aac', Container: 'mp3,aac',
audioBitRate: '320000',
...trackOptionsOsOverrides[Platform.OS], ...trackOptionsOsOverrides[Platform.OS],
}; } as const;
/** /**
* Generate the track streaming url from the trackId * 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 * Generate a track object from a Jellyfin ItemId so that
* react-native-track-player can easily consume it. * react-native-track-player can easily consume it.
*/ */
export function generateTrack(track: AlbumTrack): Track { export async function generateTrack(track: AlbumTrack): Promise<Track> {
// Also construct the URL for the stream // Also construct the URL for the stream
const url = generateTrackUrl(track.Id); const url = generateTrackUrl(track.Id);
const response = await fetch(url, { method: 'HEAD' });
return { return {
url, url,
backendId: track.Id, backendId: track.Id,
@@ -63,6 +66,9 @@ export function generateTrack(track: AlbumTrack): Track {
: getImage(track.Id), : getImage(track.Id),
hasLyrics: track.HasLyrics, hasLyrics: track.HasLyrics,
lyrics: track.Lyrics, lyrics: track.Lyrics,
contentType: response.headers.get('Content-Type') || undefined,
isDirectPlay: response.headers.has('Content-Length'),
bitRate: baseTrackOptions.audioBitRate,
}; };
} }

View File

@@ -27,9 +27,9 @@ export default function useCurrentTrack(): CurrentTrackResponse {
// Retrieve the current track from the queue using the index // Retrieve the current track from the queue using the index
const retrieveCurrentTrack = useCallback(async () => { const retrieveCurrentTrack = useCallback(async () => {
const queue = await TrackPlayer.getQueue(); const queue = await TrackPlayer.getQueue();
const currentTrackIndex = await TrackPlayer.getCurrentTrack(); const currentTrackIndex = await TrackPlayer.getActiveTrackIndex();
if (currentTrackIndex !== null) { if (currentTrackIndex !== undefined) {
setTrack(queue[currentTrackIndex] as Track); setTrack(queue[currentTrackIndex]);
setIndex(currentTrackIndex); setIndex(currentTrackIndex);
} else { } else {
setTrack(undefined); setTrack(undefined);

View File

@@ -49,7 +49,7 @@ export default function usePlayTracks() {
const queue = await TrackPlayer.getQueue(); const queue = await TrackPlayer.getQueue();
// Convert all trackIds to the relevant format for react-native-track-player // 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]; const track = tracks[trackId];
// GUARD: Check that the track actually exists in Redux // GUARD: Check that the track actually exists in Redux
@@ -58,7 +58,7 @@ export default function usePlayTracks() {
} }
// Retrieve the generated track from Jellyfin // 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 // Check if a downloaded version exists, and if so rewrite the URL
const download = downloads[trackId]; const download = downloads[trackId];
@@ -67,7 +67,7 @@ export default function usePlayTracks() {
} }
return generatedTrack; return generatedTrack;
}).filter((t): t is Track => typeof t !== 'undefined'); }))).filter((t): t is Track => typeof t !== 'undefined');
// Potentially shuffle all tracks // Potentially shuffle all tracks
const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks; const newTracks = shuffle ? shuffleArray(generatedTracks) : generatedTracks;