Skip to content

Commit

Permalink
feat: Media stream info in now playing modal (#233)
Browse files Browse the repository at this point in the history
* feat: add base codec info to player

* fix: redundant console.log

* chore: translation

* fix: only overflow direct play
  • Loading branch information
leinelissen authored Jul 25, 2024
1 parent 189491b commit 0d09c6f
Show file tree
Hide file tree
Showing 14 changed files with 148 additions and 13 deletions.
8 changes: 8 additions & 0 deletions src/assets/icons/waveform.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/localisation/lang/en/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,9 @@
"delete": "Delete",
"cancel": "Cancel",
"disc": "Disc",
"lyrics": "Lyrics"
"lyrics": "Lyrics",
"direct-play": "Direct play",
"transcoded": "Transcoded",
"khz": "kHz",
"kbps": "kbps"
}
6 changes: 5 additions & 1 deletion src/localisation/lang/nl/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
6 changes: 5 additions & 1 deletion src/localisation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,8 @@ export type LocaleKeys = 'play-next'
| 'delete'
| 'cancel'
| 'disc'
| 'lyrics';
| 'lyrics'
| 'direct-play'
| 'transcoded'
| 'khz'
| 'kbps'
1 change: 1 addition & 0 deletions src/screens/modals/Player/components/MediaControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
79 changes: 79 additions & 0 deletions src/screens/modals/Player/components/MediaInformation.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
1 change: 0 additions & 1 deletion src/screens/modals/Player/components/Timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/screens/modals/Player/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +29,7 @@ export default function Player() {
<NowPlaying />
<ConnectionNotice />
<StreamStatus />
<MediaInformation />
<ProgressBar />
<MediaControls />
<Group>
Expand Down
24 changes: 24 additions & 0 deletions src/store/music/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -71,6 +94,7 @@ export interface AlbumTrack {
MediaType: string;
HasLyrics: boolean;
Lyrics: Lyrics | null;
MediaStreams: MediaStream[];
}

export interface State {
Expand Down
1 change: 1 addition & 0 deletions src/utility/JellyfinApi/album.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
5 changes: 4 additions & 1 deletion src/utility/JellyfinApi/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -74,7 +77,7 @@ export async function fetchApi<T>(
const data = await response.json();
throw data;
} catch {
throw response;
throw new Error('FailedRequest');
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/utility/JellyfinApi/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/utility/useCurrentTrack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/utility/usePlayTracks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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];
Expand All @@ -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;
Expand Down

0 comments on commit 0d09c6f

Please sign in to comment.