From c3e5e8ac5a7e9c8480f8684f40f1f5d94b638302 Mon Sep 17 00:00:00 2001 From: Phan An Date: Tue, 3 Sep 2024 16:51:23 +0200 Subject: [PATCH] feat: allow bitrate options for mobile transcoding --- app/Http/Controllers/PlayController.php | 7 +- app/Values/UserPreferences.php | 9 +++ docs/usage/profile-preferences.md | 2 +- docs/usage/streaming.md | 15 +++- resources/assets/js/__tests__/UnitTestCase.ts | 1 + .../PreferencesForm.spec.ts | 11 ++- .../profile-preferences/PreferencesForm.vue | 23 +++++- .../__snapshots__/AllSongsScreen.spec.ts.snap | 79 +++++++++++++++++++ .../AlbumArtOverlay.spec.ts.snap | 2 + resources/assets/js/stores/commonStore.ts | 3 +- resources/assets/js/stores/songStore.spec.ts | 4 +- resources/assets/js/stores/songStore.ts | 2 +- resources/assets/js/types.d.ts | 1 + routes/web.base.php | 2 +- 14 files changed, 143 insertions(+), 18 deletions(-) diff --git a/app/Http/Controllers/PlayController.php b/app/Http/Controllers/PlayController.php index 81366fee..688c99a3 100644 --- a/app/Http/Controllers/PlayController.php +++ b/app/Http/Controllers/PlayController.php @@ -4,17 +4,20 @@ namespace App\Http\Controllers; use App\Http\Requests\SongPlayRequest; use App\Models\Song; +use App\Models\User; use App\Services\Streamer\Streamer; +use Illuminate\Contracts\Auth\Authenticatable; class PlayController extends Controller { - public function __invoke(SongPlayRequest $request, Song $song, ?bool $transcode = null, ?int $bitRate = null) + /** @param User $user */ + public function __invoke(Authenticatable $user, SongPlayRequest $request, Song $song, ?bool $transcode = null) { $this->authorize('access', $song); return (new Streamer(song: $song, config: [ 'transcode' => (bool) $transcode, - 'bit_rate' => $bitRate, + 'bit_rate' => $user->preferences->transcodeQuality, 'start_time' => (float) $request->time, ]))->stream(); } diff --git a/app/Values/UserPreferences.php b/app/Values/UserPreferences.php index 5b54f1fa..96d102f3 100644 --- a/app/Values/UserPreferences.php +++ b/app/Values/UserPreferences.php @@ -13,6 +13,7 @@ final class UserPreferences implements Arrayable, JsonSerializable 'show_now_playing_notification' => 'boolean', 'confirm_before_closing' => 'boolean', 'transcode_on_mobile' => 'boolean', + 'transcode_quality' => 'integer', 'show_album_art_overlay' => 'boolean', 'lyrics_zoom_level' => 'integer', 'make_uploads_public' => 'boolean', @@ -29,6 +30,7 @@ final class UserPreferences implements Arrayable, JsonSerializable 'show_now_playing_notification', 'confirm_before_closing', 'transcode_on_mobile', + 'transcode_quality', 'show_album_art_overlay', 'make_uploads_public', 'support_bar_no_bugging', @@ -50,6 +52,7 @@ final class UserPreferences implements Arrayable, JsonSerializable public bool $showNowPlayingNotification, public bool $confirmBeforeClosing, public bool $transcodeOnMobile, + public int $transcodeQuality, public bool $showAlbumArtOverlay, public bool $makeUploadsPublic, public bool $supportBarNoBugging, @@ -63,6 +66,10 @@ final class UserPreferences implements Arrayable, JsonSerializable Assert::oneOf($this->artistsViewMode, ['list', 'thumbnails']); Assert::oneOf($this->albumsViewMode, ['list', 'thumbnails']); Assert::oneOf($this->activeExtraPanelTab, [null, 'Lyrics', 'Artist', 'Album', 'YouTube']); + + if (!in_array($this->transcodeQuality, [64, 96, 128, 192, 256, 320], true)) { + $this->transcodeQuality = 128; + } } public static function fromArray(array $data): self @@ -77,6 +84,7 @@ final class UserPreferences implements Arrayable, JsonSerializable showNowPlayingNotification: $data['show_now_playing_notification'] ?? true, confirmBeforeClosing: $data['confirm_before_closing'] ?? false, transcodeOnMobile: $data['transcode_on_mobile'] ?? true, + transcodeQuality: $data['transcode_quality'] ?? 128, showAlbumArtOverlay: $data['show_album_art_overlay'] ?? true, makeUploadsPublic: $data['make_uploads_public'] ?? false, supportBarNoBugging: $data['support_bar_no_bugging'] ?? false, @@ -126,6 +134,7 @@ final class UserPreferences implements Arrayable, JsonSerializable 'confirm_before_closing' => $this->confirmBeforeClosing, 'show_album_art_overlay' => $this->showAlbumArtOverlay, 'transcode_on_mobile' => $this->transcodeOnMobile, + 'transcode_quality' => $this->transcodeQuality, 'make_uploads_public' => $this->makeUploadsPublic, 'lastfm_session_key' => $this->lastFmSessionKey, 'support_bar_no_bugging' => $this->supportBarNoBugging, diff --git a/docs/usage/profile-preferences.md b/docs/usage/profile-preferences.md index 4430f477..533ce99d 100644 --- a/docs/usage/profile-preferences.md +++ b/docs/usage/profile-preferences.md @@ -39,7 +39,7 @@ Koel allows you to set a couple of preferences: * Whether to show a notification whenever a new song starts playing * Whether to confirm before closing Koel’s browser tab * Whether to show a translucent, blurred overlay of the current album’s art -* Whether to transcode music on the fly (mobile only, useful if you have a slow network connection) +* Whether to transcode music to a lower bitrate (mobile only, useful if you have a slow connection) * Whether to set your uploaded music as public by default These preferences are saved immediately upon change and synced across all of your devices. diff --git a/docs/usage/streaming.md b/docs/usage/streaming.md index 2bfd8f6d..777825fc 100644 --- a/docs/usage/streaming.md +++ b/docs/usage/streaming.md @@ -26,10 +26,17 @@ If you're using [Koel mobile app](https://koel.dev/#mobile) and can't play the s Koel always uses the native PHP method if you're transcoding or streaming from a cloud storage. ::: -## Transcoding FLAC +## FLAC Transcoding -Koel supports transcoding FLAC to mp3 on the fly when streaming music. This behavior can be controlled via a `TRANSCODE_FLAC` setting in `.env` file: +By default, Koel streams FLAC files as-is, which means the lossless audio quality is preserved. +However, you can opt to have FLAC transcoded to mp3 to, for example, reduce bandwidth or support older devices. This behavior can be controlled via the `TRANSCODE_FLAC` setting in `.env` file: -* `false`: Disable FLAC transcoding. Koel will stream FLAC files as-is, producing the lossless audio quality. This is the default behavior. -* `true`: Enable FLAC transcoding. Koel will transcode FLAC to mp3 on the fly. You'll need to have [FFmpeg](https://ffmpeg.org/) installed on your server and set its executable path via the `FFMPEG_PATH` setting in the `.env` file. The transcoding quality can also be controlled via `OUTPUT_BIT_RATE` (defaults to `128`). +* `false`: Disable transcoding and stream FLAC files as-is, producing the lossless audio quality. This is the default behavior. +* `true`: Transcode FLAC to mp3 before streaming. You'll need to have [FFmpeg](https://ffmpeg.org/) installed on your server and set its executable path via the `FFMPEG_PATH` setting in the `.env` file. The transcoding quality can also be controlled via `OUTPUT_BIT_RATE` (defaults to `128`). +When transcoding is enabled, Koel caches the transcoded files to improve performance. If for any reason you want to clear the cache, run `php artisan cache:clear`. + +## Transcoding on Mobile + +On a mobile device where data usage is a concern, you might want to transcode all songs to a lower bitrate to save bandwidth. +This can be done via the [Preferences screen](./profile-preferences#preferences) and requires the same FFmpeg setup as FLAC transcoding. diff --git a/resources/assets/js/__tests__/UnitTestCase.ts b/resources/assets/js/__tests__/UnitTestCase.ts index ed03c0fa..3a583a93 100644 --- a/resources/assets/js/__tests__/UnitTestCase.ts +++ b/resources/assets/js/__tests__/UnitTestCase.ts @@ -56,6 +56,7 @@ export default abstract class UnitTestCase { commonStore.state.allows_download = true commonStore.state.uses_i_tunes = true commonStore.state.supports_batch_downloading = true + commonStore.state.supports_transcoding = true cb && cb() }) } diff --git a/resources/assets/js/components/profile-preferences/PreferencesForm.spec.ts b/resources/assets/js/components/profile-preferences/PreferencesForm.spec.ts index 3b40420c..9f626614 100644 --- a/resources/assets/js/components/profile-preferences/PreferencesForm.spec.ts +++ b/resources/assets/js/components/profile-preferences/PreferencesForm.spec.ts @@ -1,6 +1,7 @@ import { expect, it } from 'vitest' import { screen } from '@testing-library/vue' import isMobile from 'ismobilejs' +import { commonStore } from '@/stores' import UnitTestCase from '@/__tests__/UnitTestCase' import PreferencesForm from './PreferencesForm.vue' @@ -9,13 +10,19 @@ new class extends UnitTestCase { it('has "Transcode on mobile" option for mobile users', () => { isMobile.phone = true this.render(PreferencesForm) - screen.getByRole('checkbox', { name: 'Convert and play media at 128kbps on mobile' }) + screen.getByTestId('transcode_on_mobile') }) it('does not have "Transcode on mobile" option for non-mobile users', async () => { isMobile.phone = false this.render(PreferencesForm) - expect(screen.queryByRole('checkbox', { name: 'Convert and play media at 128kbps on mobile' })).toBeNull() + expect(screen.queryByTestId('transcode_on_mobile')).toBeNull() + }) + + it('does not have "Transcode on mobile" option if transcoding is not supported', async () => { + isMobile.phone = true + commonStore.state.supports_transcoding = false + expect(screen.queryByTestId('transcode_on_mobile')).toBeNull() }) } } diff --git a/resources/assets/js/components/profile-preferences/PreferencesForm.vue b/resources/assets/js/components/profile-preferences/PreferencesForm.vue index 27fff9af..469304f7 100644 --- a/resources/assets/js/components/profile-preferences/PreferencesForm.vue +++ b/resources/assets/js/components/profile-preferences/PreferencesForm.vue @@ -24,10 +24,22 @@ Confirm before closing Koel - +
- - Convert and play media at 128kbps on mobile + + Convert and play media at + + kbps on mobile
@@ -41,7 +53,8 @@