Skip to content

Commit

Permalink
Customizable Device Profiles (jellyfin#1169)
Browse files Browse the repository at this point in the history
* Rename ExperimentalSettingsView.swift to PlaybackQualitySettingsView.swift

Fix Merge

* Rename MaximumBitrateSettingsView.swift to PlaybackQualitySettingsView.swift

fix merge

* Re-implement on Main. Should now have all the Main changed. Added a new change to use the Device Profile as a Transcoding Profile.

* Part 1 -> Making VideoPlayerType into a struct (I Hope) correctly

* Part 1.1 -> Making VideoPlayerType into a struct (I Hope) correctly

* Remove unneeded Files

* Missing file + CustomDeviceProfileSelection -> CustomDeviceProfileAction Rename

* Change + to Appending

* Attempt to add StorageValues+User. Not sure if this is correct?

* Move the Array unwrapping to funcitons. Not required but this should help prevent accidently doing this wrong. Add subtitles back into the custom profiles since that somehow got dropped. Added a PlaybackCompatibility enum. This might need to work for more than just video

* Complete rewrite to allow multiple profiles, compatibility mode, and directplay.

* Hardward -> Hardware

* Update CustomDeviceProfileSettingsView.swift

Double Licensing

* It was actually really easy to implement iOS... Trash cans still look weird and small.

* Swipe to Delete instead of the edit button

* wip

* wip

* Linting

* tvOS Implementation

* wip

* wip

* cleanup

* Create Package.resolved

---------

Co-authored-by: Joseph Kribs <[email protected]>
Co-authored-by: Ethan Pippin <[email protected]>
  • Loading branch information
3 people authored Sep 2, 2024
1 parent 58dfdde commit f5bd1b8
Show file tree
Hide file tree
Showing 58 changed files with 2,843 additions and 550 deletions.
45 changes: 45 additions & 0 deletions Shared/Coordinators/CustomDeviceProfileCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Stinsen
import SwiftUI

final class CustomDeviceProfileCoordinator: NavigationCoordinatable {

let stack = NavigationStack(initial: \CustomDeviceProfileCoordinator.start)

@Root
var start = makeStart

@Route(.push)
var customDeviceProfileSettings = makeCustomDeviceProfileSettings
@Route(.push)
var editCustomDeviceProfile = makeEditCustomDeviceProfile
@Route(.push)
var createCustomDeviceProfile = makeCreateCustomDeviceProfile

func makeCustomDeviceProfileSettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> {
NavigationViewCoordinator(
PlaybackQualitySettingsCoordinator()
)
}

func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
}

func makeCreateCustomDeviceProfile() -> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator())
}

@ViewBuilder
func makeStart() -> some View {
CustomDeviceProfileSettingsView()
}
}
57 changes: 57 additions & 0 deletions Shared/Coordinators/EditCustomDeviceProfileCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Stinsen
import SwiftUI

final class EditCustomDeviceProfileCoordinator: NavigationCoordinatable {

let stack = NavigationStack(initial: \EditCustomDeviceProfileCoordinator.start)

@Root
var start = makeStart

// TODO: fix for tvOS

@Route(.push)
var customDeviceAudioEditor = makeCustomDeviceAudioEditor
@Route(.push)
var customDeviceVideoEditor = makeCustomDeviceVideoEditor
@Route(.push)
var customDeviceContainerEditor = makeCustomDeviceContainerEditor

private let profile: Binding<CustomDeviceProfile>?

init(profile: Binding<CustomDeviceProfile>? = nil) {
self.profile = profile
}

@ViewBuilder
func makeCustomDeviceAudioEditor(selection: Binding<[AudioCodec]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: AudioCodec.allCases)
.navigationTitle(L10n.audio)
}

@ViewBuilder
func makeCustomDeviceVideoEditor(selection: Binding<[VideoCodec]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: VideoCodec.allCases)
.navigationTitle(L10n.video)
}

@ViewBuilder
func makeCustomDeviceContainerEditor(selection: Binding<[MediaContainer]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: MediaContainer.allCases)
.navigationTitle(L10n.containers)
}

@ViewBuilder
func makeStart() -> some View {
CustomDeviceProfileSettingsView.EditCustomDeviceProfileView(profile: profile)
.navigationTitle(L10n.customProfile)
}
}
41 changes: 41 additions & 0 deletions Shared/Coordinators/PlaybackQualitySettingsCoordinator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Stinsen
import SwiftUI

final class PlaybackQualitySettingsCoordinator: NavigationCoordinatable {

let stack = NavigationStack(initial: \PlaybackQualitySettingsCoordinator.start)

@Root
var start = makeStart

@Route(.push)
var customDeviceProfileSettings = makeCustomDeviceProfileSettings

func makeCustomDeviceProfileSettings() -> NavigationViewCoordinator<CustomDeviceProfileCoordinator> {
NavigationViewCoordinator(
CustomDeviceProfileCoordinator()
)
}

func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
}

func makeCreateCustomDeviceProfile() -> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator())
}

@ViewBuilder
func makeStart() -> some View {
PlaybackQualitySettingsView()
}
}
73 changes: 54 additions & 19 deletions Shared/Coordinators/SettingsCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.push)
var nativePlayerSettings = makeNativePlayerSettings
@Route(.push)
var maximumBitrateSettings = makeMaximumBitrateSettings
var playbackQualitySettings = makePlaybackQualitySettings
@Route(.push)
var quickConnect = makeQuickConnectAuthorize
@Route(.push)
Expand All @@ -46,6 +46,13 @@ final class SettingsCoordinator: NavigationCoordinatable {
var serverDetail = makeServerDetail
@Route(.push)
var videoPlayerSettings = makeVideoPlayerSettings
@Route(.push)
var customDeviceProfileSettings = makeCustomDeviceProfileSettings

@Route(.modal)
var editCustomDeviceProfile = makeEditCustomDeviceProfile
@Route(.modal)
var createCustomDeviceProfile = makeCreateCustomDeviceProfile

#if DEBUG
@Route(.push)
Expand All @@ -59,13 +66,15 @@ final class SettingsCoordinator: NavigationCoordinatable {
@Route(.modal)
var experimentalSettings = makeExperimentalSettings
@Route(.modal)
var indicatorSettings = makeIndicatorSettings
@Route(.modal)
var log = makeLog
@Route(.modal)
var serverDetail = makeServerDetail
@Route(.modal)
var videoPlayerSettings = makeVideoPlayerSettings
@Route(.modal)
var maximumBitrateSettings = makeMaximumBitrateSettings
var playbackQualitySettings = makePlaybackQualitySettings
#endif

#if os(iOS)
Expand All @@ -75,8 +84,22 @@ final class SettingsCoordinator: NavigationCoordinatable {
}

@ViewBuilder
func makeMaximumBitrateSettings() -> some View {
MaximumBitrateSettingsView()
func makePlaybackQualitySettings() -> some View {
PlaybackQualitySettingsView()
}

@ViewBuilder
func makeCustomDeviceProfileSettings() -> some View {
CustomDeviceProfileSettingsView()
}

func makeEditCustomDeviceProfile(profile: Binding<CustomDeviceProfile>)
-> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator(profile: profile))
}

func makeCreateCustomDeviceProfile() -> NavigationViewCoordinator<EditCustomDeviceProfileCoordinator> {
NavigationViewCoordinator(EditCustomDeviceProfileCoordinator())
}

@ViewBuilder
Expand Down Expand Up @@ -123,27 +146,31 @@ final class SettingsCoordinator: NavigationCoordinatable {
EditServerView(server: server)
}

#if DEBUG
@ViewBuilder
func makeDebugSettings() -> some View {
DebugSettingsView()
}
#endif

func makeItemFilterDrawerSelector(selection: Binding<[ItemFilterType]>) -> some View {
OrderedSectionSelectorView(selection: selection, sources: ItemFilterType.allCases)
.navigationTitle(L10n.filters)
}

func makeVideoPlayerSettings() -> VideoPlayerSettingsCoordinator {
VideoPlayerSettingsCoordinator()
}

#if DEBUG
@ViewBuilder
func makeDebugSettings() -> some View {
DebugSettingsView()
}
#endif

#endif

#if os(tvOS)

func makeCustomizeViewsSettings() -> NavigationViewCoordinator<CustomizeSettingsCoordinator> {
NavigationViewCoordinator(CustomizeSettingsCoordinator())
func makeCustomizeViewsSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator(
BasicNavigationViewCoordinator {
CustomizeViewsSettings()
}
)
}

func makeExperimentalSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
Expand All @@ -154,20 +181,28 @@ final class SettingsCoordinator: NavigationCoordinatable {
)
}

func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
IndicatorSettingsView()
}
}

func makeServerDetail(server: ServerState) -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
EditServerView(server: server)
}
}

func makeVideoPlayerSettings() -> NavigationViewCoordinator<VideoPlayerSettingsCoordinator> {
NavigationViewCoordinator(VideoPlayerSettingsCoordinator())
NavigationViewCoordinator(
VideoPlayerSettingsCoordinator()
)
}

func makeMaximumBitrateSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
NavigationViewCoordinator {
MaximumBitrateSettingsView()
}
func makePlaybackQualitySettings() -> NavigationViewCoordinator<PlaybackQualitySettingsCoordinator> {
NavigationViewCoordinator(
PlaybackQualitySettingsCoordinator()
)
}
#endif

Expand Down
5 changes: 5 additions & 0 deletions Shared/Extensions/Array.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ extension Array {
return removeFirst()
}
}

// extension Array where Element: RawRepresentable<String> {
//
// var asCommaString: String {}
// }
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,19 @@ import JellyfinAPI
import Logging

extension BaseItemDto {

func videoPlayerViewModel(with mediaSource: MediaSourceInfo) async throws -> VideoPlayerViewModel {

let currentVideoPlayerType = Defaults[.VideoPlayer.videoPlayerType]
let currentVideoBitrate = Defaults[.VideoPlayer.appMaximumBitrate]
let currentVideoBitrate = Defaults[.VideoPlayer.Playback.appMaximumBitrate]
let compatibilityMode = Defaults[.VideoPlayer.Playback.compatibilityMode]

let maxBitrate = try await getMaxBitrate(for: currentVideoBitrate)
let profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: maxBitrate)
let profile = DeviceProfile.build(
for: currentVideoPlayerType,
compatibilityMode: compatibilityMode,
maxBitrate: maxBitrate
)

let userSession = Container.shared.currentUserSession()!

Expand Down Expand Up @@ -46,14 +53,17 @@ extension BaseItemDto {
}

func liveVideoPlayerViewModel(with mediaSource: MediaSourceInfo, logger: Logger) async throws -> VideoPlayerViewModel {

let currentVideoPlayerType = Defaults[.VideoPlayer.videoPlayerType]
let currentVideoBitrate = Defaults[.VideoPlayer.appMaximumBitrate]
let currentVideoBitrate = Defaults[.VideoPlayer.Playback.appMaximumBitrate]
let compatibilityMode = Defaults[.VideoPlayer.Playback.compatibilityMode]

let maxBitrate = try await getMaxBitrate(for: currentVideoBitrate)
var profile = DeviceProfile.build(for: currentVideoPlayerType, maxBitrate: maxBitrate)
if Defaults[.Experimental.liveTVForceDirectPlay] {
profile.directPlayProfiles = [DirectPlayProfile(type: .video)]
}
let profile = DeviceProfile.build(
for: currentVideoPlayerType,
compatibilityMode: compatibilityMode,
maxBitrate: maxBitrate
)

let userSession = Container.shared.currentUserSession()!

Expand Down Expand Up @@ -101,7 +111,7 @@ extension BaseItemDto {
}

private func getMaxBitrate(for bitrate: PlaybackBitrate) async throws -> Int {
let settingBitrate = Defaults[.VideoPlayer.appMaximumBitrateTest]
let settingBitrate = Defaults[.VideoPlayer.Playback.appMaximumBitrateTest]

guard bitrate != .auto else {
return try await testBitrate(with: settingBitrate.rawValue)
Expand Down
32 changes: 32 additions & 0 deletions Shared/Extensions/JellyfinAPI/CodecProfile.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
//

import Foundation
import JellyfinAPI

extension CodecProfile {

init(
codec: String? = nil,
container: String? = nil,
type: CodecType? = nil,
@ArrayBuilder<ProfileCondition> applyConditions: () -> [ProfileCondition] = { [] },
@ArrayBuilder<ProfileCondition> conditions: () -> [ProfileCondition] = { [] }
) {
let applyConditions = applyConditions()
let conditions = conditions()

self.init(
applyConditions: applyConditions.isEmpty ? nil : applyConditions,
codec: codec,
conditions: conditions.isEmpty ? nil : conditions,
container: container,
type: type
)
}
}
Loading

0 comments on commit f5bd1b8

Please sign in to comment.