Skip to content

Commit

Permalink
🔧 chore: move streaming logic to rust-side
Browse files Browse the repository at this point in the history
Signed-off-by: SimonShiki <[email protected]>
  • Loading branch information
SimonShiki committed Jan 1, 2025
1 parent e44d097 commit 18c1f2b
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 220 deletions.
348 changes: 253 additions & 95 deletions src-tauri/src/audio.rs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src-tauri/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use serde::Serialize;
use std::fmt::{Display, Formatter};
use tauri::Error as TauriError;
use reqwest::Error as ReqwestError;

#[derive(Debug, Serialize)]
pub enum AppError {
Expand Down Expand Up @@ -39,3 +40,10 @@ impl From<TauriError> for AppError {
AppError::TauriError(error.to_string())
}
}

// Add From implementation for ReqwestError
impl From<ReqwestError> for AppError {
fn from(error: ReqwestError) -> Self {
AppError::NetworkError(error.to_string())
}
}
7 changes: 4 additions & 3 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use media_control::MediaControlState;
use rodio::OutputStream;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Condvar, Mutex, Once};
use std::time::Duration;
use tauri::{image::Image, Emitter, Manager};
use tauri::{
menu::{MenuBuilder, MenuItemBuilder},
Expand All @@ -27,6 +28,9 @@ pub async fn run() {
is_stream_ended: Arc::new(AtomicBool::new(false)),
decoder: Arc::new(Mutex::new(None)),
data_available: Arc::new((Mutex::new(false), Condvar::new())),
seek_target: Arc::new(Mutex::new(None)),
buffered_duration: Arc::new(Mutex::new(Duration::from_secs(0))),
progress_tx: Arc::new(Mutex::new(None)),
};
let media_control_state = MediaControlState {
media_controls: Arc::new(Mutex::new(None)),
Expand Down Expand Up @@ -103,7 +107,6 @@ pub async fn run() {
})
.invoke_handler(tauri::generate_handler![
audio::play_local_file,
audio::play_arraybuffer,
audio::play_url_stream,
audio::get_music_status,
audio::pause,
Expand All @@ -118,8 +121,6 @@ pub async fn run() {
media_control::update_playback_status,
local_scanner::get_song_buffer,
local_scanner::scan_folder,
cache_manager::get_cached_song,
cache_manager::cache_song,
cache_manager::get_cache_size,
cache_manager::clear_cache,
])
Expand Down
7 changes: 1 addition & 6 deletions src/components/now-playing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ import { useState, useCallback, useEffect } from 'react';
import Slider from './base/slider';
import Tooltip from './base/tooltip';
import Lyrics from './lyrics';
import { focusAtom } from 'jotai-optics';
import { settingsJotai } from '../jotais/settings';
import PlaylistTooltip from './playlist-tooltip';

const playModeIconMap: Record<PlayMode, string> = {
Expand All @@ -30,8 +28,6 @@ function formatMilliseconds (ms: number): string {
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}

const streamingJotai = focusAtom(settingsJotai, (optic) => optic.prop('streaming'));

export default function NowPlaying () {
const [globalFullscreen, setGlobalFullscreen] = useAtom(nowPlayingPageJotai);
const [localFullscreen, setLocalFullscreen] = useState(globalFullscreen);
Expand All @@ -42,7 +38,6 @@ export default function NowPlaying () {
const song = useAtomValue(currentSongJotai);
const progress = useAtomValue(progressJotai);
const buffering = useAtomValue(bufferingJotai);
const streaming = useAtomValue(streamingJotai);

const [isAnimating, setIsAnimating] = useState(false);

Expand Down Expand Up @@ -130,7 +125,7 @@ export default function NowPlaying () {
<div className='flex flex-row gap-6 items-center px-6'>
<span className='color-outline-pri font-size-sm'>{formatMilliseconds(progress * 1000)}</span>
<div className='w-full'>
<Slider value={Math.min(progress * 1000 / song.duration! * 100, 100)} disabled={buffering || (streaming && song.storage !== 'local')} onChange={handleChangePlayProgress} step={0.01} />
<Slider value={Math.min(progress * 1000 / song.duration! * 100, 100)} disabled={buffering} onChange={handleChangePlayProgress} step={0.01} />
</div>
<span className='color-outline-pri font-size-sm'>{formatMilliseconds(song.duration!)}</span>
</div>
Expand Down
1 change: 0 additions & 1 deletion src/jotais/play.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,3 @@ export const currentSongJotai = focusAtom(nowPlayingJotai, (optic) => optic.prop
export const playingJotai = focusAtom(nowPlayingJotai, (optic) => optic.prop('playing'));
export const volumeJotai = atomWithStorage('volume', 1);
export const bufferingJotai = atom(false);
export const streamingJotai = atom(false);
10 changes: 7 additions & 3 deletions src/jotais/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ export interface Song<From extends string> {

export interface AbstractStorage {
scan(): Promise<void>;
getMusicURL?(id: string | number): Promise<string>;
getMusicBuffer?(id: string | number): Promise<number[]>;
}

export interface RemoteStorage extends AbstractStorage {
getMusicURL(id: string | number): Promise<string>;
}

type StorageInstance = Local | RemoteStorage

export interface StorageMeta {
identifer: string;
instance: AbstractStorage;
instance: StorageInstance;
scanned: boolean;
songList: Song<StorageMeta['identifer']>[];
}
Expand Down
18 changes: 2 additions & 16 deletions src/pages/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@ import Card from '../components/base/card';
import Input from '../components/base/input';
import Select from '../components/base/select';
import Switch from '../components/base/switch';
import { localeJotai, settingsJotai, storagesConfigJotai } from '../jotais/settings';
import { localeJotai, storagesConfigJotai } from '../jotais/settings';
import { SetStateAction, useAtom, useAtomValue, useSetAtom, WritableAtom } from 'jotai';
import type { LocalConfig } from '../storages/local';
import { useCallback, useEffect, useState } from 'react';
import { open } from '@tauri-apps/plugin-dialog';
import { storagesJotai } from '../jotais/storage';
import Tooltip from '../components/base/tooltip';
import Modal from '../components/base/modal';
import md5 from 'md5';
import type { NCMConfig, NCMQuality } from '../storages/ncm';
import Spinner from '../components/base/spinner';
import { FormattedMessage, useIntl } from 'react-intl';
import { langMap } from '../../locales';
import { clearCache, getCacheSize } from '../utils/cache.';
import { clearCache, getCacheSize } from '../utils/cache';
import { nowPlayingBarJotai } from '../jotais/play';

const streamingJotai = focusAtom(settingsJotai, (optic) => optic.prop('streaming'));
const localStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('local')) as unknown as WritableAtom<LocalConfig, [SetStateAction<LocalConfig>], void>;
const ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('ncm')) as unknown as WritableAtom<NCMConfig, [SetStateAction<NCMConfig>], void>;
const ncmCookieJotai = focusAtom(ncmStorageConfigJotai, (optic) => optic.prop('cookie'));
Expand Down Expand Up @@ -50,7 +48,6 @@ export default function Settings () {
const localScanned = useAtomValue(localScannedJotai);
const setNCMCookie = useSetAtom(ncmCookieJotai);
const [ncmLoggedIn, setNCMloggedIn] = useAtom(ncmLoggedInJotai);
const [streaming, setStreaming] = useAtom(streamingJotai);
const barOpen = useAtomValue(nowPlayingBarJotai);
const [ncmProfile, setNCMProfile] = useAtom(ncmProfileJotai);
const [ncmAPI, setNcmAPI] = useAtom(ncmApiJotai);
Expand Down Expand Up @@ -181,17 +178,6 @@ export default function Settings () {
<span className='color-text-pri dark:color-text-dark-pri font-size-sm my-2'>
<FormattedMessage defaultMessage='Play' />
</span>
<Card className='flex flex-col gap-2 color-text-pri dark:color-text-dark-pri'>
<div className='flex flex-row items-center gap-4'>
<span className='i-fluent:stream-24-regular w-5 h-5' />
<span className='grow-1'>
<FormattedMessage defaultMessage='Use streaming (Experimental)' />
</span>
<Tooltip content={intl.formatMessage({ defaultMessage: 'Streaming does not currently support adjusting playback progress'})} placement='left' tooltipClassName='min-w-50'>
<Switch checked={streaming} onChange={setStreaming} />
</Tooltip>
</div>
</Card>
<Card className='flex flex-col gap-2 color-text-pri dark:color-text-dark-pri'>
<div className='flex flex-row items-center gap-4'>
<span className='i-fluent:database-arrow-down-20-regular w-5 h-5' />
Expand Down
15 changes: 2 additions & 13 deletions src/storages/ncm.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { focusAtom } from 'jotai-optics';
import { StorageConfig, storagesConfigJotai } from '../jotais/settings';
import sharedStore from '../jotais/shared-store';
import { AbstractStorage, Song, storagesJotai } from '../jotais/storage';
import { RemoteStorage, Song, storagesJotai } from '../jotais/storage';
import { mergeDeep } from '../utils/merge-deep';
import type { SetStateAction, WritableAtom } from 'jotai';
import { backendStorage } from '../utils/local-utitity';
import { fetchBuffer } from '../utils/chunk-transformer';
import { currentSongJotai } from '../jotais/play';
import { mergeLyrics } from '../utils/lyric-parser';
import { cacheSong, getCachedSong } from '../utils/cache.';

interface NCMSearchResult {
id: number;
Expand Down Expand Up @@ -282,7 +280,7 @@ const defaultConfig: NCMConfig = {
syncSonglist: false
};

export class NCM implements AbstractStorage {
export class NCM implements RemoteStorage {
private ncmStorageConfigJotai = focusAtom(storagesConfigJotai, (optic) => optic.prop('ncm')) as unknown as WritableAtom<NCMConfig, [SetStateAction<NCMConfig>], void>;
private songlistJotai?: WritableAtom<Song<'ncm'>[], [Song<'ncm'>[]], void>;
private scannedJotai?: WritableAtom<boolean, boolean[], void>;
Expand Down Expand Up @@ -400,15 +398,6 @@ export class NCM implements AbstractStorage {
return song.url as string;
}

async getMusicBuffer (id: number, quality = this.config.defaultQuality) {
const cached = await getCachedSong(id);
if (cached) return cached;
const url = await this.getMusicURL(id, quality);
const buffer = await fetchBuffer(url);
cacheSong(id, buffer);
return buffer;
}

async search (keyword: string, limit = 10, page = 1) {
const offset = (page - 1) * limit;
const res = await fetch(`${this.config.api}search?keywords=${keyword}&limit=${limit}&offset=${offset}`);
Expand Down
17 changes: 0 additions & 17 deletions src/utils/cache..ts

This file was deleted.

9 changes: 9 additions & 0 deletions src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { invoke } from '@tauri-apps/api/core';

export async function getCacheSize () {
return await invoke<number>('get_cache_size');
}

export async function clearCache () {
await invoke('clear_cache');
}
17 changes: 0 additions & 17 deletions src/utils/chunk-transformer.ts

This file was deleted.

Loading

0 comments on commit 18c1f2b

Please sign in to comment.