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",
"cancel": "Cancel",
"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",
"delete": "Verwijder",
"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'
| 'cancel'
| 'disc'
| 'lyrics';
| 'lyrics'
| 'direct-play'
| 'transcoded'
| 'khz'
| 'kbps'

View File

@@ -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`

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`
align-self: flex-start;
align-items: flex-start;
margin-top: 52px;
padding: 8px;
margin-left: -8px;
flex: 0 1 auto;

View File

@@ -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() {
<NowPlaying />
<ConnectionNotice />
<StreamStatus />
<MediaInformation />
<ProgressBar />
<MediaControls />
<Group>

View File

@@ -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 {

View File

@@ -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();

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() {
return require('@/store').default as Store;
}
@@ -74,7 +77,7 @@ export async function fetchApi<T>(
const data = await response.json();
throw data;
} catch {
throw response;
throw new Error('FailedRequest');
}
}

View File

@@ -22,8 +22,9 @@ const baseTrackOptions: Record<string, string> = {
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<Track> {
// 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,
};
}

View File

@@ -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);

View File

@@ -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;