CC support with Invidious and MPV

This commit is contained in:
Arkadiusz Fal 2022-07-05 19:20:25 +02:00
parent 3718311a93
commit e56ab3804e
11 changed files with 163 additions and 25 deletions

View File

@ -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
View 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))"
}
}

View File

@ -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()
}

View File

@ -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")
}

View File

@ -47,6 +47,11 @@ final class PlayerControlsModel: ObservableObject {
func handleOverlayPresentationChange() {
player?.backend.setNeedsNetworkStateUpdates(presentingControlsOverlay)
if presentingControlsOverlay {
removeTimer()
} else {
resetTimer()
}
}
func show() {

View File

@ -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? {

View File

@ -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)

View File

@ -9,11 +9,13 @@ struct ControlsOverlay: View {
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
var body: some View {
ScrollView {
VStack(spacing: 6) {
HStack {
backendButtons
}
qualityButton
captionsButton
HStack {
decreaseRateButton
rateButton
@ -30,6 +32,7 @@ struct ControlsOverlay: View {
}
}
}
}
private var backendButtons: some View {
ForEach(PlayerBackendType.allCases, id: \.self) { backend in
@ -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())
}

View File

@ -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,

View File

@ -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

View File

@ -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 */,