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",
|
"delete": "Delete",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"disc": "Disc",
|
"disc": "Disc",
|
||||||
"lyrics": "Lyrics"
|
"lyrics": "Lyrics",
|
||||||
|
"direct-play": "Direct play",
|
||||||
|
"transcoded": "Transcoded",
|
||||||
|
"khz": "kHz",
|
||||||
|
"kbps": "kbps"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,8 @@ export type LocaleKeys = 'play-next'
|
|||||||
| 'delete'
|
| 'delete'
|
||||||
| 'cancel'
|
| 'cancel'
|
||||||
| 'disc'
|
| 'disc'
|
||||||
| 'lyrics';
|
| 'lyrics'
|
||||||
|
| 'direct-play'
|
||||||
|
| 'transcoded'
|
||||||
|
| 'khz'
|
||||||
|
| 'kbps'
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
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`
|
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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user