From 0091af683fba4d61145c713a02185805babb7954 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 2 Nov 2021 18:24:59 +0100 Subject: [PATCH] Playback rate menu --- Model/Player/PlayerModel.swift | 15 ++++++++++++- Model/Player/PlayerTVMenu.swift | 21 +++++++++++++++++ Shared/Player/PlaybackBar.swift | 40 ++++++++++++++++++++++++++++----- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 845e4747..8aac99be 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -11,6 +11,7 @@ import SwiftUI import SwiftyJSON final class PlayerModel: ObservableObject { + static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2] let logger = Logger(label: "net.arekf.Pearvidious.ps") private(set) var player = AVPlayer() @@ -23,7 +24,7 @@ final class PlayerModel: ObservableObject { @Published var presentingPlayer = false @Published var stream: Stream? - @Published var currentRate: Float? + @Published var currentRate: Float = 1.0 { didSet { player.rate = currentRate } } @Published var availableStreams = [Stream]() { didSet { rebuildTVMenu() } } @Published var streamSelection: Stream? { didSet { rebuildTVMenu() } } @@ -418,6 +419,10 @@ final class PlayerModel: ObservableObject { self.objectWillChange.send() } + if player.timeControlStatus == .playing, player.rate != self.currentRate { + player.rate = self.currentRate + } + #if os(macOS) if player.timeControlStatus == .playing { ScreenSaverManager.shared.disable(reason: "Yattee is playing video") @@ -469,4 +474,12 @@ final class PlayerModel: ObservableObject { currentArtwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! } } + + func rateLabel(_ rate: Float) -> String { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 2 + + return "\(formatter.string(from: NSNumber(value: rate))!)×" + } } diff --git a/Model/Player/PlayerTVMenu.swift b/Model/Player/PlayerTVMenu.swift index 2eaf6d31..8cd5f524 100644 --- a/Model/Player/PlayerTVMenu.swift +++ b/Model/Player/PlayerTVMenu.swift @@ -42,12 +42,33 @@ extension PlayerModel { self.restoreLastSkippedSegment() } } + + private var rateMenu: UIMenu { + UIMenu(title: "Playback rate", image: UIImage(systemName: rateMenuSystemImage), children: rateMenuActions) + } + + private var rateMenuSystemImage: String { + [0.0, 1.0].contains(currentRate) ? "speedometer" : (currentRate < 1.0 ? "tortoise.fill" : "hare.fill") + } + + private var rateMenuActions: [UIAction] { + PlayerModel.availableRates.map { rate in + let image = currentRate == Float(rate) ? UIImage(systemName: "checkmark") : nil + + return UIAction(title: rateLabel(rate), image: image) { _ in + DispatchQueue.main.async { + self.currentRate = rate + } + } + } + } #endif func rebuildTVMenu() { #if os(tvOS) avPlayerViewController?.transportBarCustomMenuItems = [ restoreLastSkippedSegmentAction, + rateMenu, streamsMenu ].compactMap { $0 } #endif diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift index 6c3fa051..b002cfe0 100644 --- a/Shared/Player/PlaybackBar.swift +++ b/Shared/Player/PlaybackBar.swift @@ -14,9 +14,15 @@ struct PlaybackBar: View { closeButton if player.currentItem != nil { - Text(playbackStatus) - .foregroundColor(.gray) - .font(.caption2) + HStack { + Text(playbackStatus) + + Text("•") + + rateMenu + } + .font(.caption2) + .foregroundColor(.gray) Spacer() @@ -44,7 +50,6 @@ struct PlaybackBar: View { .frame(maxWidth: 180) #endif } - .environment(\.colorScheme, .dark) .transaction { t in t.animation = .none } .foregroundColor(.gray) .font(.caption2) @@ -52,6 +57,7 @@ struct PlaybackBar: View { Spacer() } } + .environment(\.colorScheme, .dark) .frame(minWidth: 0, maxWidth: .infinity) .padding(4) .background(.black) @@ -82,7 +88,8 @@ struct PlaybackBar: View { return "loading..." } - let remainingSeconds = player.currentVideo!.length - player.time!.seconds + let videoLengthAtRate = player.currentVideo!.length / Double(player.currentRate) + let remainingSeconds = videoLengthAtRate - player.time!.seconds if remainingSeconds < 60 { return "less than a minute" @@ -94,6 +101,29 @@ struct PlaybackBar: View { return "ends at \(timeFinishAtString)" } + private var rateMenu: some View { + #if os(macOS) + ratePicker + .labelsHidden() + .frame(maxWidth: 70) + #else + Menu { + ratePicker + } label: { + Text(player.rateLabel(player.currentRate)) + } + + #endif + } + + private var ratePicker: some View { + Picker("", selection: $player.currentRate) { + ForEach(PlayerModel.availableRates, id: \.self) { rate in + Text(player.rateLabel(rate)).tag(rate) + } + } + } + private var restoreLastSkippedSegmentButton: some View { HStack(spacing: 4) { Button {