Compare commits
4 Commits
main
...
feat/media
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e601b0d258 | ||
|
|
065515c25b | ||
|
|
0cd6d5d05c | ||
|
|
92c82b9f0a |
8
src/assets/icons/waveform.svg
Normal file
8
src/assets/icons/waveform.svg
Normal 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 |
@@ -76,5 +76,9 @@
|
||||
"delete": "Delete",
|
||||
"cancel": "Cancel",
|
||||
"disc": "Disc",
|
||||
"lyrics": "Lyrics"
|
||||
"lyrics": "Lyrics",
|
||||
"direct-play": "Direct play",
|
||||
"transcoded": "Transcoded",
|
||||
"khz": "kHz",
|
||||
"kbps": "kbps"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -75,4 +75,8 @@ export type LocaleKeys = 'play-next'
|
||||
| 'delete'
|
||||
| 'cancel'
|
||||
| 'disc'
|
||||
| 'lyrics';
|
||||
| 'lyrics'
|
||||
| 'direct-play'
|
||||
| 'transcoded'
|
||||
| 'khz'
|
||||
| 'kbps'
|
||||
|
||||
@@ -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`
|
||||
|
||||
79
src/screens/modals/Player/components/MediaInformation.tsx
Normal file
79
src/screens/modals/Player/components/MediaInformation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user