mirror of
https://github.com/jellyfin/Swiftfin.git
synced 2024-11-23 14:10:01 +00:00
Support select subtitle font (#498)
This commit is contained in:
parent
38af1cd54a
commit
48a03d8462
@ -11,7 +11,6 @@ import Stinsen
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class SettingsCoordinator: NavigationCoordinatable {
|
final class SettingsCoordinator: NavigationCoordinatable {
|
||||||
|
|
||||||
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
let stack = NavigationStack(initial: \SettingsCoordinator.start)
|
||||||
|
|
||||||
@Root
|
@Root
|
||||||
@ -32,6 +31,8 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
@Route(.push)
|
@Route(.push)
|
||||||
var quickConnect = makeQuickConnectSettings
|
var quickConnect = makeQuickConnectSettings
|
||||||
|
@Route(.push)
|
||||||
|
var fontPicker = makeFontPicker
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@ -71,6 +72,12 @@ final class SettingsCoordinator: NavigationCoordinatable {
|
|||||||
let viewModel = QuickConnectSettingsViewModel()
|
let viewModel = QuickConnectSettingsViewModel()
|
||||||
QuickConnectSettingsView(viewModel: viewModel)
|
QuickConnectSettingsView(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func makeFontPicker() -> some View {
|
||||||
|
FontPickerView()
|
||||||
|
.navigationTitle(L10n.subtitleFont)
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
@ -44,137 +44,137 @@ extension BaseItemDto {
|
|||||||
var viewModels: [VideoPlayerViewModel] = []
|
var viewModels: [VideoPlayerViewModel] = []
|
||||||
|
|
||||||
for currentMediaSource in mediaSources {
|
for currentMediaSource in mediaSources {
|
||||||
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
||||||
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
||||||
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
||||||
|
|
||||||
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
||||||
|
|
||||||
let defaultSubtitleStream = subtitleStreams
|
let defaultSubtitleStream = subtitleStreams
|
||||||
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||||
|
|
||||||
// MARK: Build Streams
|
// MARK: Build Streams
|
||||||
|
|
||||||
let directStreamURL: URL
|
let directStreamURL: URL
|
||||||
let transcodedStreamURL: URLComponents?
|
let transcodedStreamURL: URLComponents?
|
||||||
var hlsStreamURL: URL
|
var hlsStreamURL: URL
|
||||||
let mediaSourceID: String
|
let mediaSourceID: String
|
||||||
let streamType: ServerStreamType
|
let streamType: ServerStreamType
|
||||||
|
|
||||||
if mediaSources.count > 1 {
|
if mediaSources.count > 1 {
|
||||||
mediaSourceID = currentMediaSource.id!
|
mediaSourceID = currentMediaSource.id!
|
||||||
} else {
|
} else {
|
||||||
mediaSourceID = self.id!
|
mediaSourceID = self.id!
|
||||||
}
|
|
||||||
|
|
||||||
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
|
||||||
itemId: self.id!,
|
|
||||||
_static: true,
|
|
||||||
tag: self.etag,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
minSegments: 6,
|
|
||||||
mediaSourceId: mediaSourceID
|
|
||||||
)
|
|
||||||
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
|
||||||
|
|
||||||
if let transcodeURL = currentMediaSource.transcodingUrl {
|
|
||||||
streamType = .transcode
|
|
||||||
transcodedStreamURL = URLComponents(
|
|
||||||
string: SessionManager.main.currentLogin.server.currentURI
|
|
||||||
.appending(transcodeURL)
|
|
||||||
)!
|
|
||||||
} else {
|
|
||||||
streamType = .direct
|
|
||||||
transcodedStreamURL = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
|
||||||
itemId: id ?? "",
|
|
||||||
mediaSourceId: id ?? "",
|
|
||||||
_static: true,
|
|
||||||
tag: currentMediaSource.eTag,
|
|
||||||
deviceProfileId: nil,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
segmentContainer: "ts",
|
|
||||||
segmentLength: nil,
|
|
||||||
minSegments: 2,
|
|
||||||
deviceId: UIDevice.vendorUUIDString,
|
|
||||||
audioCodec: audioStreams
|
|
||||||
.compactMap(\.codec)
|
|
||||||
.joined(separator: ","),
|
|
||||||
breakOnNonKeyFrames: true,
|
|
||||||
requireAvc: true,
|
|
||||||
transcodingMaxAudioChannels: 6,
|
|
||||||
videoCodec: videoStream?.codec,
|
|
||||||
videoStreamIndex: videoStream?.index,
|
|
||||||
enableAdaptiveBitrateStreaming: true
|
|
||||||
)
|
|
||||||
|
|
||||||
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
|
||||||
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
|
||||||
|
|
||||||
hlsStreamURL = hlsStreamComponents.url!
|
|
||||||
|
|
||||||
// MARK: VidoPlayerViewModel Creation
|
|
||||||
|
|
||||||
var subtitle: String?
|
|
||||||
|
|
||||||
// MARK: Attach media content to self
|
|
||||||
|
|
||||||
var modifiedSelfItem = self
|
|
||||||
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
|
||||||
|
|
||||||
// TODO: other forms of media subtitle
|
|
||||||
if self.itemType == .episode {
|
|
||||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
|
||||||
subtitle = "\(seriesName) - \(episodeLocator)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let subtitlesEnabled = defaultSubtitleStream != nil
|
|
||||||
|
|
||||||
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
|
||||||
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
|
||||||
|
|
||||||
let overlayType = Defaults[.overlayType]
|
|
||||||
|
|
||||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
|
||||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
|
||||||
|
|
||||||
var fileName: String?
|
|
||||||
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
|
||||||
fileName = String(lastInPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(
|
|
||||||
item: modifiedSelfItem,
|
|
||||||
title: modifiedSelfItem.name ?? "",
|
|
||||||
subtitle: subtitle,
|
|
||||||
directStreamURL: directStreamURL,
|
|
||||||
transcodedStreamURL: transcodedStreamURL?.url,
|
|
||||||
hlsStreamURL: hlsStreamURL,
|
|
||||||
streamType: streamType,
|
|
||||||
response: response,
|
|
||||||
audioStreams: audioStreams,
|
|
||||||
subtitleStreams: subtitleStreams,
|
|
||||||
chapters: modifiedSelfItem.chapters ?? [],
|
|
||||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
|
||||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
|
||||||
subtitlesEnabled: subtitlesEnabled,
|
|
||||||
autoplayEnabled: autoplayEnabled,
|
|
||||||
overlayType: overlayType,
|
|
||||||
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
|
||||||
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
|
||||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
|
||||||
container: currentMediaSource.container ?? "",
|
|
||||||
filename: fileName,
|
|
||||||
versionName: currentMediaSource.name
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModels.append(videoPlayerViewModel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewModels
|
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
||||||
|
itemId: self.id!,
|
||||||
|
_static: true,
|
||||||
|
tag: self.etag,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
minSegments: 6,
|
||||||
|
mediaSourceId: mediaSourceID
|
||||||
|
)
|
||||||
|
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
||||||
|
|
||||||
|
if let transcodeURL = currentMediaSource.transcodingUrl {
|
||||||
|
streamType = .transcode
|
||||||
|
transcodedStreamURL = URLComponents(
|
||||||
|
string: SessionManager.main.currentLogin.server.currentURI
|
||||||
|
.appending(transcodeURL)
|
||||||
|
)!
|
||||||
|
} else {
|
||||||
|
streamType = .direct
|
||||||
|
transcodedStreamURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
|
mediaSourceId: id ?? "",
|
||||||
|
_static: true,
|
||||||
|
tag: currentMediaSource.eTag,
|
||||||
|
deviceProfileId: nil,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
segmentContainer: "ts",
|
||||||
|
segmentLength: nil,
|
||||||
|
minSegments: 2,
|
||||||
|
deviceId: UIDevice.vendorUUIDString,
|
||||||
|
audioCodec: audioStreams
|
||||||
|
.compactMap(\.codec)
|
||||||
|
.joined(separator: ","),
|
||||||
|
breakOnNonKeyFrames: true,
|
||||||
|
requireAvc: true,
|
||||||
|
transcodingMaxAudioChannels: 6,
|
||||||
|
videoCodec: videoStream?.codec,
|
||||||
|
videoStreamIndex: videoStream?.index,
|
||||||
|
enableAdaptiveBitrateStreaming: true
|
||||||
|
)
|
||||||
|
|
||||||
|
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
||||||
|
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||||
|
|
||||||
|
hlsStreamURL = hlsStreamComponents.url!
|
||||||
|
|
||||||
|
// MARK: VidoPlayerViewModel Creation
|
||||||
|
|
||||||
|
var subtitle: String?
|
||||||
|
|
||||||
|
// MARK: Attach media content to self
|
||||||
|
|
||||||
|
var modifiedSelfItem = self
|
||||||
|
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
||||||
|
|
||||||
|
// TODO: other forms of media subtitle
|
||||||
|
if self.itemType == .episode {
|
||||||
|
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||||
|
subtitle = "\(seriesName) - \(episodeLocator)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtitlesEnabled = defaultSubtitleStream != nil
|
||||||
|
|
||||||
|
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||||
|
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||||
|
|
||||||
|
let overlayType = Defaults[.overlayType]
|
||||||
|
|
||||||
|
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||||
|
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||||
|
|
||||||
|
var fileName: String?
|
||||||
|
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
||||||
|
fileName = String(lastInPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: modifiedSelfItem,
|
||||||
|
title: modifiedSelfItem.name ?? "",
|
||||||
|
subtitle: subtitle,
|
||||||
|
directStreamURL: directStreamURL,
|
||||||
|
transcodedStreamURL: transcodedStreamURL?.url,
|
||||||
|
hlsStreamURL: hlsStreamURL,
|
||||||
|
streamType: streamType,
|
||||||
|
response: response,
|
||||||
|
audioStreams: audioStreams,
|
||||||
|
subtitleStreams: subtitleStreams,
|
||||||
|
chapters: modifiedSelfItem.chapters ?? [],
|
||||||
|
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||||
|
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||||
|
subtitlesEnabled: subtitlesEnabled,
|
||||||
|
autoplayEnabled: autoplayEnabled,
|
||||||
|
overlayType: overlayType,
|
||||||
|
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||||
|
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||||
|
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||||
|
container: currentMediaSource.container ?? "",
|
||||||
|
filename: fileName,
|
||||||
|
versionName: currentMediaSource.name
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModels.append(videoPlayerViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewModels
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
@ -211,137 +211,137 @@ extension BaseItemDto {
|
|||||||
var viewModels: [VideoPlayerViewModel] = []
|
var viewModels: [VideoPlayerViewModel] = []
|
||||||
|
|
||||||
for currentMediaSource in mediaSources {
|
for currentMediaSource in mediaSources {
|
||||||
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first
|
||||||
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? []
|
||||||
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? []
|
||||||
|
|
||||||
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! })
|
||||||
|
|
||||||
let defaultSubtitleStream = subtitleStreams
|
let defaultSubtitleStream = subtitleStreams
|
||||||
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
.first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 })
|
||||||
|
|
||||||
// MARK: Build Streams
|
// MARK: Build Streams
|
||||||
|
|
||||||
let directStreamURL: URL
|
let directStreamURL: URL
|
||||||
let transcodedStreamURL: URLComponents?
|
let transcodedStreamURL: URLComponents?
|
||||||
var hlsStreamURL: URL
|
var hlsStreamURL: URL
|
||||||
let mediaSourceID: String
|
let mediaSourceID: String
|
||||||
let streamType: ServerStreamType
|
let streamType: ServerStreamType
|
||||||
|
|
||||||
if mediaSources.count > 1 {
|
if mediaSources.count > 1 {
|
||||||
mediaSourceID = currentMediaSource.id!
|
mediaSourceID = currentMediaSource.id!
|
||||||
} else {
|
} else {
|
||||||
mediaSourceID = self.id!
|
mediaSourceID = self.id!
|
||||||
}
|
|
||||||
|
|
||||||
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
|
||||||
itemId: self.id!,
|
|
||||||
_static: true,
|
|
||||||
tag: self.etag,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
minSegments: 6,
|
|
||||||
mediaSourceId: mediaSourceID
|
|
||||||
)
|
|
||||||
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
|
||||||
|
|
||||||
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
|
||||||
streamType = .transcode
|
|
||||||
transcodedStreamURL = URLComponents(
|
|
||||||
string: SessionManager.main.currentLogin.server.currentURI
|
|
||||||
.appending(transcodeURL)
|
|
||||||
)!
|
|
||||||
} else {
|
|
||||||
streamType = .direct
|
|
||||||
transcodedStreamURL = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
|
||||||
itemId: id ?? "",
|
|
||||||
mediaSourceId: id ?? "",
|
|
||||||
_static: true,
|
|
||||||
tag: currentMediaSource.eTag,
|
|
||||||
deviceProfileId: nil,
|
|
||||||
playSessionId: response.playSessionId,
|
|
||||||
segmentContainer: "ts",
|
|
||||||
segmentLength: nil,
|
|
||||||
minSegments: 2,
|
|
||||||
deviceId: UIDevice.vendorUUIDString,
|
|
||||||
audioCodec: audioStreams
|
|
||||||
.compactMap(\.codec)
|
|
||||||
.joined(separator: ","),
|
|
||||||
breakOnNonKeyFrames: true,
|
|
||||||
requireAvc: true,
|
|
||||||
transcodingMaxAudioChannels: 6,
|
|
||||||
videoCodec: videoStream?.codec,
|
|
||||||
videoStreamIndex: videoStream?.index,
|
|
||||||
enableAdaptiveBitrateStreaming: true
|
|
||||||
)
|
|
||||||
|
|
||||||
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
|
||||||
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
|
||||||
|
|
||||||
hlsStreamURL = hlsStreamComponents.url!
|
|
||||||
|
|
||||||
// MARK: VidoPlayerViewModel Creation
|
|
||||||
|
|
||||||
var subtitle: String?
|
|
||||||
|
|
||||||
// MARK: Attach media content to self
|
|
||||||
|
|
||||||
var modifiedSelfItem = self
|
|
||||||
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
|
||||||
|
|
||||||
// TODO: other forms of media subtitle
|
|
||||||
if self.itemType == .episode {
|
|
||||||
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
|
||||||
subtitle = "\(seriesName) - \(episodeLocator)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let subtitlesEnabled = defaultSubtitleStream != nil
|
|
||||||
|
|
||||||
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
|
||||||
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
|
||||||
|
|
||||||
let overlayType = Defaults[.overlayType]
|
|
||||||
|
|
||||||
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
|
||||||
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
|
||||||
|
|
||||||
var fileName: String?
|
|
||||||
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
|
||||||
fileName = String(lastInPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoPlayerViewModel = VideoPlayerViewModel(
|
|
||||||
item: modifiedSelfItem,
|
|
||||||
title: modifiedSelfItem.name ?? "",
|
|
||||||
subtitle: subtitle,
|
|
||||||
directStreamURL: directStreamURL,
|
|
||||||
transcodedStreamURL: transcodedStreamURL?.url,
|
|
||||||
hlsStreamURL: hlsStreamURL,
|
|
||||||
streamType: streamType,
|
|
||||||
response: response,
|
|
||||||
audioStreams: audioStreams,
|
|
||||||
subtitleStreams: subtitleStreams,
|
|
||||||
chapters: modifiedSelfItem.chapters ?? [],
|
|
||||||
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
|
||||||
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
|
||||||
subtitlesEnabled: subtitlesEnabled,
|
|
||||||
autoplayEnabled: autoplayEnabled,
|
|
||||||
overlayType: overlayType,
|
|
||||||
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
|
||||||
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
|
||||||
shouldShowAutoPlay: shouldShowAutoPlay,
|
|
||||||
container: currentMediaSource.container ?? "",
|
|
||||||
filename: fileName,
|
|
||||||
versionName: currentMediaSource.name
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModels.append(videoPlayerViewModel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return viewModels
|
let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder(
|
||||||
|
itemId: self.id!,
|
||||||
|
_static: true,
|
||||||
|
tag: self.etag,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
minSegments: 6,
|
||||||
|
mediaSourceId: mediaSourceID
|
||||||
|
)
|
||||||
|
directStreamURL = URL(string: directStreamBuilder.URLString)!
|
||||||
|
|
||||||
|
if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] {
|
||||||
|
streamType = .transcode
|
||||||
|
transcodedStreamURL = URLComponents(
|
||||||
|
string: SessionManager.main.currentLogin.server.currentURI
|
||||||
|
.appending(transcodeURL)
|
||||||
|
)!
|
||||||
|
} else {
|
||||||
|
streamType = .direct
|
||||||
|
transcodedStreamURL = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder(
|
||||||
|
itemId: id ?? "",
|
||||||
|
mediaSourceId: id ?? "",
|
||||||
|
_static: true,
|
||||||
|
tag: currentMediaSource.eTag,
|
||||||
|
deviceProfileId: nil,
|
||||||
|
playSessionId: response.playSessionId,
|
||||||
|
segmentContainer: "ts",
|
||||||
|
segmentLength: nil,
|
||||||
|
minSegments: 2,
|
||||||
|
deviceId: UIDevice.vendorUUIDString,
|
||||||
|
audioCodec: audioStreams
|
||||||
|
.compactMap(\.codec)
|
||||||
|
.joined(separator: ","),
|
||||||
|
breakOnNonKeyFrames: true,
|
||||||
|
requireAvc: true,
|
||||||
|
transcodingMaxAudioChannels: 6,
|
||||||
|
videoCodec: videoStream?.codec,
|
||||||
|
videoStreamIndex: videoStream?.index,
|
||||||
|
enableAdaptiveBitrateStreaming: true
|
||||||
|
)
|
||||||
|
|
||||||
|
var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)!
|
||||||
|
hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken)
|
||||||
|
|
||||||
|
hlsStreamURL = hlsStreamComponents.url!
|
||||||
|
|
||||||
|
// MARK: VidoPlayerViewModel Creation
|
||||||
|
|
||||||
|
var subtitle: String?
|
||||||
|
|
||||||
|
// MARK: Attach media content to self
|
||||||
|
|
||||||
|
var modifiedSelfItem = self
|
||||||
|
modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams
|
||||||
|
|
||||||
|
// TODO: other forms of media subtitle
|
||||||
|
if self.itemType == .episode {
|
||||||
|
if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() {
|
||||||
|
subtitle = "\(seriesName) - \(episodeLocator)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let subtitlesEnabled = defaultSubtitleStream != nil
|
||||||
|
|
||||||
|
let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode
|
||||||
|
let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay
|
||||||
|
|
||||||
|
let overlayType = Defaults[.overlayType]
|
||||||
|
|
||||||
|
let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode
|
||||||
|
let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode
|
||||||
|
|
||||||
|
var fileName: String?
|
||||||
|
if let lastInPath = currentMediaSource.path?.split(separator: "/").last {
|
||||||
|
fileName = String(lastInPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoPlayerViewModel = VideoPlayerViewModel(
|
||||||
|
item: modifiedSelfItem,
|
||||||
|
title: modifiedSelfItem.name ?? "",
|
||||||
|
subtitle: subtitle,
|
||||||
|
directStreamURL: directStreamURL,
|
||||||
|
transcodedStreamURL: transcodedStreamURL?.url,
|
||||||
|
hlsStreamURL: hlsStreamURL,
|
||||||
|
streamType: streamType,
|
||||||
|
response: response,
|
||||||
|
audioStreams: audioStreams,
|
||||||
|
subtitleStreams: subtitleStreams,
|
||||||
|
chapters: modifiedSelfItem.chapters ?? [],
|
||||||
|
selectedAudioStreamIndex: defaultAudioStream?.index ?? -1,
|
||||||
|
selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1,
|
||||||
|
subtitlesEnabled: subtitlesEnabled,
|
||||||
|
autoplayEnabled: autoplayEnabled,
|
||||||
|
overlayType: overlayType,
|
||||||
|
shouldShowPlayPreviousItem: shouldShowPlayPreviousItem,
|
||||||
|
shouldShowPlayNextItem: shouldShowPlayNextItem,
|
||||||
|
shouldShowAutoPlay: shouldShowAutoPlay,
|
||||||
|
container: currentMediaSource.container ?? "",
|
||||||
|
filename: fileName,
|
||||||
|
versionName: currentMediaSource.name
|
||||||
|
)
|
||||||
|
|
||||||
|
viewModels.append(videoPlayerViewModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewModels
|
||||||
}
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
// Copyright (c) 2022 Jellyfin & Jellyfin Contributors
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
import TVVLCKit
|
import TVVLCKit
|
||||||
#else
|
#else
|
||||||
@ -22,4 +23,14 @@ extension VLCMediaPlayer {
|
|||||||
with: size.textRendererFontSize
|
with: size.textRendererFontSize
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Applies font to the player
|
||||||
|
///
|
||||||
|
/// This is pretty hacky until VLCKit 4 has a public API to support this
|
||||||
|
func setSubtitleFont(fontName: String) {
|
||||||
|
perform(
|
||||||
|
Selector(("setTextRendererFont:")),
|
||||||
|
with: fontName
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -396,6 +396,8 @@ internal enum L10n {
|
|||||||
internal static var studio: String { return L10n.tr("Localizable", "studio") }
|
internal static var studio: String { return L10n.tr("Localizable", "studio") }
|
||||||
/// Studios
|
/// Studios
|
||||||
internal static var studios: String { return L10n.tr("Localizable", "studios") }
|
internal static var studios: String { return L10n.tr("Localizable", "studios") }
|
||||||
|
/// Subtitle Font
|
||||||
|
internal static var subtitleFont: String { return L10n.tr("Localizable", "subtitleFont") }
|
||||||
/// Subtitles
|
/// Subtitles
|
||||||
internal static var subtitles: String { return L10n.tr("Localizable", "subtitles") }
|
internal static var subtitles: String { return L10n.tr("Localizable", "subtitles") }
|
||||||
/// Subtitle Size
|
/// Subtitle Size
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
extension SwiftfinStore {
|
extension SwiftfinStore {
|
||||||
enum Defaults {
|
enum Defaults {
|
||||||
@ -69,6 +70,11 @@ extension Defaults.Keys {
|
|||||||
)
|
)
|
||||||
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
static let autoplayEnabled = Key<Bool>("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
static let resumeOffset = Key<Bool>("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
static let subtitleFontName = Key<String>(
|
||||||
|
"subtitleFontName",
|
||||||
|
default: UIFont.systemFont(ofSize: 14).fontName,
|
||||||
|
suite: SwiftfinStore.Defaults.generalSuite
|
||||||
|
)
|
||||||
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
|
static let subtitleSize = Key<SubtitleSize>("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite)
|
||||||
|
|
||||||
// Should show video player items
|
// Should show video player items
|
||||||
|
@ -221,6 +221,7 @@
|
|||||||
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; };
|
62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; };
|
||||||
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; };
|
62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; };
|
||||||
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; };
|
62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; };
|
||||||
|
62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C83B07288C6A630004ED0C /* FontPicker.swift */; };
|
||||||
62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; };
|
62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; };
|
||||||
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; };
|
62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; };
|
||||||
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; };
|
62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; };
|
||||||
@ -732,6 +733,7 @@
|
|||||||
62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = "<group>"; };
|
62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = "<group>"; };
|
||||||
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = "<group>"; };
|
62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = "<group>"; };
|
||||||
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = "<group>"; };
|
62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
62C83B07288C6A630004ED0C /* FontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPicker.swift; sourceTree = "<group>"; };
|
||||||
62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = "<group>"; };
|
62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = "<group>"; };
|
||||||
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = "<group>"; };
|
||||||
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = "<group>"; };
|
||||||
@ -1658,6 +1660,7 @@
|
|||||||
E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */,
|
E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */,
|
||||||
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
5338F74D263B61370014BF09 /* ConnectToServerView.swift */,
|
||||||
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
5389276D263C25100035E14B /* ContinueWatchingView.swift */,
|
||||||
|
62C83B07288C6A630004ED0C /* FontPicker.swift */,
|
||||||
625CB56E2678C23300530A6E /* HomeView.swift */,
|
625CB56E2678C23300530A6E /* HomeView.swift */,
|
||||||
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
|
E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */,
|
||||||
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
E14F7D0A26DB3714007C3AE6 /* ItemView */,
|
||||||
@ -2414,6 +2417,7 @@
|
|||||||
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */,
|
||||||
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
|
E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */,
|
||||||
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
621338932660107500A81A2A /* StringExtensions.swift in Sources */,
|
||||||
|
62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */,
|
||||||
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */,
|
||||||
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
|
E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */,
|
||||||
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
|
E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */,
|
||||||
|
35
Swiftfin/Views/FontPicker.swift
Normal file
35
Swiftfin/Views/FontPicker.swift
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// 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) 2022 Jellyfin & Jellyfin Contributors
|
||||||
|
//
|
||||||
|
|
||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct FontPickerView: UIViewControllerRepresentable {
|
||||||
|
func makeUIViewController(context: Context) -> UIFontPickerViewController {
|
||||||
|
let configuration = UIFontPickerViewController.Configuration()
|
||||||
|
configuration.includeFaces = true
|
||||||
|
|
||||||
|
let fontViewController = UIFontPickerViewController(configuration: configuration)
|
||||||
|
fontViewController.delegate = context.coordinator
|
||||||
|
return fontViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIFontPickerViewController, context: Context) {}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator()
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIFontPickerViewControllerDelegate {
|
||||||
|
func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
|
||||||
|
guard let descriptor = viewController.selectedFontDescriptor else { return }
|
||||||
|
Defaults[.subtitleFontName] = descriptor.postscriptName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -48,6 +48,8 @@ struct SettingsView: View {
|
|||||||
var resumeOffset
|
var resumeOffset
|
||||||
@Default(.subtitleSize)
|
@Default(.subtitleSize)
|
||||||
var subtitleSize
|
var subtitleSize
|
||||||
|
@Default(.subtitleFontName)
|
||||||
|
var subtitleFontName
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@ -185,6 +187,20 @@ struct SettingsView: View {
|
|||||||
Text(appearance.localizedName).tag(appearance.rawValue)
|
Text(appearance.localizedName).tag(appearance.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
settingsRouter.route(to: \.fontPicker)
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
L10n.subtitleFont.text
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
Text(subtitleFontName)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Picker(L10n.subtitleSize, selection: $subtitleSize) {
|
Picker(L10n.subtitleSize, selection: $subtitleSize) {
|
||||||
ForEach(SubtitleSize.allCases, id: \.self) { size in
|
ForEach(SubtitleSize.allCases, id: \.self) { size in
|
||||||
Text(size.label).tag(size.rawValue)
|
Text(size.label).tag(size.rawValue)
|
||||||
|
@ -568,6 +568,7 @@ extension VLCPlayerViewController {
|
|||||||
vlcMediaPlayer.delegate = self
|
vlcMediaPlayer.delegate = self
|
||||||
vlcMediaPlayer.drawable = videoContentView
|
vlcMediaPlayer.drawable = videoContentView
|
||||||
|
|
||||||
|
vlcMediaPlayer.setSubtitleFont(fontName: Defaults[.subtitleFontName])
|
||||||
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
|
vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize])
|
||||||
|
|
||||||
stopOverlayDismissTimer()
|
stopOverlayDismissTimer()
|
||||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user