mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 05:23:41 +00:00
CC support with Invidious and MPV
This commit is contained in:
parent
3718311a93
commit
e56ab3804e
@ -428,7 +428,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
keywords: json["keywords"].arrayValue.compactMap { $0.string },
|
||||
streams: extractStreams(from: json),
|
||||
related: extractRelated(from: json),
|
||||
chapters: extractChapters(from: description)
|
||||
chapters: extractChapters(from: description),
|
||||
captions: extractCaptions(from: json)
|
||||
)
|
||||
}
|
||||
|
||||
@ -566,4 +567,17 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
channel: Channel(id: channelId, name: author)
|
||||
)
|
||||
}
|
||||
|
||||
private func extractCaptions(from content: JSON) -> [Captions] {
|
||||
content["captions"].arrayValue.compactMap { details in
|
||||
guard let baseURL = account.url,
|
||||
let url = URL(string: baseURL + details["url"].stringValue) else { return nil }
|
||||
|
||||
return Captions(
|
||||
label: details["label"].stringValue,
|
||||
code: details["language_code"].stringValue,
|
||||
url: url
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
12
Model/Captions.swift
Normal file
12
Model/Captions.swift
Normal file
@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
struct Captions: Hashable, Identifiable {
|
||||
var id = UUID().uuidString
|
||||
let label: String
|
||||
let code: String
|
||||
let url: URL
|
||||
|
||||
var description: String {
|
||||
"\(label) (\(code))"
|
||||
}
|
||||
}
|
@ -19,6 +19,13 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
var stream: Stream?
|
||||
var video: Video?
|
||||
var captions: Captions? { didSet {
|
||||
guard let captions = captions else {
|
||||
client.removeSubs()
|
||||
return
|
||||
}
|
||||
addSubTrack(captions.url)
|
||||
}}
|
||||
var currentTime: CMTime?
|
||||
|
||||
var loadedVideo = false
|
||||
@ -155,11 +162,18 @@ final class MPVBackend: PlayerBackend {
|
||||
}
|
||||
#endif
|
||||
|
||||
var captions: Captions?
|
||||
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
|
||||
captions = video.captions.first { $0.code == captionsLanguageCode } ??
|
||||
video.captions.first { $0.code.contains(captionsLanguageCode) }
|
||||
}
|
||||
|
||||
let updateCurrentStream = {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.stream = stream
|
||||
self?.video = video
|
||||
self?.model.stream = stream
|
||||
self?.captions = captions
|
||||
}
|
||||
}
|
||||
|
||||
@ -211,7 +225,7 @@ final class MPVBackend: PlayerBackend {
|
||||
startPlaying()
|
||||
}
|
||||
|
||||
self.client.loadFile(url, time: time) { [weak self] _ in
|
||||
self.client.loadFile(url, sub: captions?.url, time: time) { [weak self] _ in
|
||||
self?.isLoadingVideo = true
|
||||
}
|
||||
} else {
|
||||
@ -223,7 +237,7 @@ final class MPVBackend: PlayerBackend {
|
||||
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
|
||||
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url
|
||||
|
||||
self.client?.loadFile(fileToLoad, audio: audioTrack, time: time) { [weak self] _ in
|
||||
self.client?.loadFile(fileToLoad, audio: audioTrack, sub: captions?.url, time: time) { [weak self] _ in
|
||||
self?.isLoadingVideo = true
|
||||
self?.pause()
|
||||
}
|
||||
@ -454,6 +468,11 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.addVideoTrack(url)
|
||||
}
|
||||
|
||||
func addSubTrack(_ url: URL) {
|
||||
client?.removeSubs()
|
||||
client?.addSubTrack(url)
|
||||
}
|
||||
|
||||
func setVideoToAuto() {
|
||||
client?.setVideoToAuto()
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func loadFile(_ url: URL, audio: URL? = nil, time: CMTime? = nil, completionHandler: ((Int32) -> Void)? = nil) {
|
||||
func loadFile(_ url: URL, audio: URL? = nil, sub: URL? = nil, time: CMTime? = nil, completionHandler: ((Int32) -> Void)? = nil) {
|
||||
var args = [url.absoluteString]
|
||||
var options = [String]()
|
||||
|
||||
@ -123,6 +123,10 @@ final class MPVClient: ObservableObject {
|
||||
options.append("audio-files-append=\"\(audioURL)\"")
|
||||
}
|
||||
|
||||
if let subURL = sub?.absoluteString {
|
||||
options.append("sub-files-append=\"\(subURL)\"")
|
||||
}
|
||||
|
||||
args.append(options.joined(separator: ","))
|
||||
|
||||
command("loadfile", args: args, returnValueCallback: completionHandler)
|
||||
@ -263,6 +267,14 @@ final class MPVClient: ObservableObject {
|
||||
command("video-add", args: [url.absoluteString])
|
||||
}
|
||||
|
||||
func addSubTrack(_ url: URL) {
|
||||
command("sub-add", args: [url.absoluteString])
|
||||
}
|
||||
|
||||
func removeSubs() {
|
||||
command("sub-remove")
|
||||
}
|
||||
|
||||
func setVideoToAuto() {
|
||||
setString("video", "1")
|
||||
}
|
||||
|
@ -47,6 +47,11 @@ final class PlayerControlsModel: ObservableObject {
|
||||
|
||||
func handleOverlayPresentationChange() {
|
||||
player?.backend.setNeedsNetworkStateUpdates(presentingControlsOverlay)
|
||||
if presentingControlsOverlay {
|
||||
removeTimer()
|
||||
} else {
|
||||
resetTimer()
|
||||
}
|
||||
}
|
||||
|
||||
func show() {
|
||||
|
@ -34,6 +34,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
var related = [Video]()
|
||||
var chapters = [Chapter]()
|
||||
|
||||
var captions = [Captions]()
|
||||
|
||||
init(
|
||||
id: String? = nil,
|
||||
videoID: String,
|
||||
@ -55,7 +57,8 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
keywords: [String] = [],
|
||||
streams: [Stream] = [],
|
||||
related: [Video] = [],
|
||||
chapters: [Chapter] = []
|
||||
chapters: [Chapter] = [],
|
||||
captions: [Captions] = []
|
||||
) {
|
||||
self.id = id ?? UUID().uuidString
|
||||
self.videoID = videoID
|
||||
@ -78,6 +81,7 @@ struct Video: Identifiable, Equatable, Hashable {
|
||||
self.streams = streams
|
||||
self.related = related
|
||||
self.chapters = chapters
|
||||
self.captions = captions
|
||||
}
|
||||
|
||||
var publishedDate: String? {
|
||||
|
@ -48,6 +48,7 @@ extension Defaults.Keys {
|
||||
static let roundedThumbnails = Key<Bool>("roundedThumbnails", default: true)
|
||||
static let thumbnailsQuality = Key<ThumbnailsQuality>("thumbnailsQuality", default: .highest)
|
||||
|
||||
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
|
||||
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
|
||||
static let quality = Key<ResolutionSetting>("quality", default: .best)
|
||||
static let playerSidebar = Key<PlayerSidebarSetting>("playerSidebar", default: PlayerSidebarSetting.defaultValue)
|
||||
|
@ -9,24 +9,27 @@ struct ControlsOverlay: View {
|
||||
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
backendButtons
|
||||
}
|
||||
qualityButton
|
||||
HStack {
|
||||
decreaseRateButton
|
||||
rateButton
|
||||
increaseRateButton
|
||||
}
|
||||
#if os(iOS)
|
||||
.foregroundColor(.white)
|
||||
#endif
|
||||
ScrollView {
|
||||
VStack(spacing: 6) {
|
||||
HStack {
|
||||
backendButtons
|
||||
}
|
||||
qualityButton
|
||||
captionsButton
|
||||
HStack {
|
||||
decreaseRateButton
|
||||
rateButton
|
||||
increaseRateButton
|
||||
}
|
||||
#if os(iOS)
|
||||
.foregroundColor(.white)
|
||||
#endif
|
||||
|
||||
if player.activeBackend == .mpv,
|
||||
showMPVPlaybackStats
|
||||
{
|
||||
mpvPlaybackStats
|
||||
if player.activeBackend == .mpv,
|
||||
showMPVPlaybackStats
|
||||
{
|
||||
mpvPlaybackStats
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -128,6 +131,60 @@ struct ControlsOverlay: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder private var captionsButton: some View {
|
||||
#if os(macOS)
|
||||
captionsPicker
|
||||
.labelsHidden()
|
||||
.frame(maxWidth: 300)
|
||||
#else
|
||||
Menu {
|
||||
captionsPicker
|
||||
.frame(width: 140, height: 30)
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
} label: {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "text.bubble")
|
||||
if let captions = captionsBinding.wrappedValue {
|
||||
Text(captions.code)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.frame(width: 140, height: 30)
|
||||
}
|
||||
.transaction { t in t.animation = .none }
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.primary)
|
||||
.frame(width: 140, height: 30)
|
||||
.modifier(ControlBackgroundModifier())
|
||||
.mask(RoundedRectangle(cornerRadius: 3))
|
||||
#endif
|
||||
}
|
||||
|
||||
@ViewBuilder private var captionsPicker: some View {
|
||||
let captions = player.currentVideo?.captions ?? []
|
||||
Picker("Captions", selection: captionsBinding) {
|
||||
if captions.isEmpty {
|
||||
Text("Not available")
|
||||
} else {
|
||||
Text("Disabled").tag(Captions?.none)
|
||||
}
|
||||
ForEach(captions) { caption in
|
||||
Text(caption.description).tag(Optional(caption))
|
||||
}
|
||||
}
|
||||
.disabled(captions.isEmpty)
|
||||
}
|
||||
|
||||
private var captionsBinding: Binding<Captions?> {
|
||||
.init(
|
||||
get: { player.mpvBackend.captions },
|
||||
set: {
|
||||
player.mpvBackend.captions = $0
|
||||
Defaults[.captionsLanguageCode] = $0?.code
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder private var rateButton: some View {
|
||||
#if os(macOS)
|
||||
ratePicker
|
||||
@ -185,6 +242,7 @@ struct ControlsOverlay: View {
|
||||
struct ControlsOverlay_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ControlsOverlay()
|
||||
.environmentObject(NetworkStateModel())
|
||||
.environmentObject(PlayerModel())
|
||||
.environmentObject(PlayerControlsModel())
|
||||
}
|
||||
|
@ -97,6 +97,7 @@ struct PlayerControls: View {
|
||||
#endif
|
||||
|
||||
ControlsOverlay()
|
||||
.frame(height: overlayHeight)
|
||||
.padding()
|
||||
.modifier(ControlBackgroundModifier(enabled: true))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
@ -127,6 +128,11 @@ struct PlayerControls: View {
|
||||
}
|
||||
}
|
||||
|
||||
var overlayHeight: Double {
|
||||
guard let player = player, player.playerSize.height.isFinite else { return 0 }
|
||||
return [0, [player.playerSize.height - 80, 140].min()!].max()!
|
||||
}
|
||||
|
||||
@ViewBuilder var controlsBackground: some View {
|
||||
if player.musicMode,
|
||||
let item = self.player.currentItem,
|
||||
|
@ -199,7 +199,8 @@ struct VideoPlayerView: View {
|
||||
.gesture(
|
||||
DragGesture(coordinateSpace: .global)
|
||||
.onChanged { value in
|
||||
guard player.presentingPlayer else { return }
|
||||
guard player.presentingPlayer,
|
||||
!playerControls.presentingControlsOverlay else { return }
|
||||
|
||||
let drag = value.translation.height
|
||||
|
||||
|
@ -397,7 +397,6 @@
|
||||
37732FF52703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
||||
37737786276F9858000521C1 /* Windows.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37737785276F9858000521C1 /* Windows.swift */; };
|
||||
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||
3774124927387D2300423605 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAF28F26740715007FC770 /* Channel.swift */; };
|
||||
3774124A27387D2300423605 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; };
|
||||
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0698127260B2100F7F6CB /* ThumbnailsModel.swift */; };
|
||||
@ -438,6 +437,9 @@
|
||||
3774127827387EB000423605 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 3774127727387EB000423605 /* Logging */; };
|
||||
3774127A27387EBC00423605 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 3774127927387EBC00423605 /* Defaults */; };
|
||||
3774127C27387EC800423605 /* Alamofire in Frameworks */ = {isa = PBXBuildFile; productRef = 3774127B27387EC800423605 /* Alamofire */; };
|
||||
3776ADD6287381240078EBC4 /* Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776ADD5287381240078EBC4 /* Captions.swift */; };
|
||||
3776ADD7287381240078EBC4 /* Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776ADD5287381240078EBC4 /* Captions.swift */; };
|
||||
3776ADD8287381240078EBC4 /* Captions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776ADD5287381240078EBC4 /* Captions.swift */; };
|
||||
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
||||
377A20AA2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
||||
377A20AB2693C9A2002842B8 /* TypedContentAccessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */; };
|
||||
@ -1048,6 +1050,7 @@
|
||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = "<group>"; };
|
||||
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||
37737785276F9858000521C1 /* Windows.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
||||
3776ADD5287381240078EBC4 /* Captions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Captions.swift; path = Model/Captions.swift; sourceTree = SOURCE_ROOT; };
|
||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
||||
377ABC3F286E4AD5009C986F /* InstancesManifest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InstancesManifest.swift; sourceTree = "<group>"; };
|
||||
377ABC43286E4B74009C986F /* ManifestedInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManifestedInstance.swift; sourceTree = "<group>"; };
|
||||
@ -1964,6 +1967,7 @@
|
||||
3751BA8127E69131007B1A60 /* ReturnYouTubeDislike */,
|
||||
37FB283F2721B20800A57617 /* Search */,
|
||||
374C0539272436DA009BDDBE /* SponsorBlock */,
|
||||
3776ADD5287381240078EBC4 /* Captions.swift */,
|
||||
37AAF28F26740715007FC770 /* Channel.swift */,
|
||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */,
|
||||
37520698285E8DD300CA655F /* Chapter.swift */,
|
||||
@ -2672,6 +2676,7 @@
|
||||
37130A5F277657300033018A /* PersistenceController.swift in Sources */,
|
||||
37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||
37BE0BD626A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */,
|
||||
3776ADD6287381240078EBC4 /* Captions.swift in Sources */,
|
||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||
37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||
37484C1926FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||
@ -2928,6 +2933,7 @@
|
||||
3782B9532755667600990149 /* String+Format.swift in Sources */,
|
||||
37E2EEAC270656EC00170416 /* BrowserPlayerControls.swift in Sources */,
|
||||
37BF662027308884008CCFB0 /* DropFavoriteOutside.swift in Sources */,
|
||||
3776ADD7287381240078EBC4 /* Captions.swift in Sources */,
|
||||
37E70924271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
||||
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */,
|
||||
373CFACC26966264003CB2C6 /* SearchQuery.swift in Sources */,
|
||||
@ -3057,7 +3063,6 @@
|
||||
3774125627387D2300423605 /* Segment.swift in Sources */,
|
||||
373C8FE7275B955100CB5936 /* CommentsPage.swift in Sources */,
|
||||
3774124D27387D2300423605 /* PlaylistsModel.swift in Sources */,
|
||||
3774123427387CC100423605 /* InvidiousAPI.swift in Sources */,
|
||||
3774124B27387D2300423605 /* ThumbnailsModel.swift in Sources */,
|
||||
3774125427387D2300423605 /* Store.swift in Sources */,
|
||||
3774125027387D2300423605 /* Video.swift in Sources */,
|
||||
@ -3119,6 +3124,7 @@
|
||||
371B7E632759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||
3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */,
|
||||
3776ADD8287381240078EBC4 /* Captions.swift in Sources */,
|
||||
37F0F4EC286F397E00C06C2E /* SettingsModel.swift in Sources */,
|
||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||
37BC50AA2778A84700510953 /* HistorySettings.swift in Sources */,
|
||||
|
Loading…
Reference in New Issue
Block a user