mirror of
https://github.com/yattee/yattee.git
synced 2025-04-28 07:50:33 +05:30
Playback rate menu
This commit is contained in:
parent
320207e439
commit
0091af683f
@ -11,6 +11,7 @@ import SwiftUI
|
|||||||
import SwiftyJSON
|
import SwiftyJSON
|
||||||
|
|
||||||
final class PlayerModel: ObservableObject {
|
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")
|
let logger = Logger(label: "net.arekf.Pearvidious.ps")
|
||||||
|
|
||||||
private(set) var player = AVPlayer()
|
private(set) var player = AVPlayer()
|
||||||
@ -23,7 +24,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var presentingPlayer = false
|
@Published var presentingPlayer = false
|
||||||
|
|
||||||
@Published var stream: Stream?
|
@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 availableStreams = [Stream]() { didSet { rebuildTVMenu() } }
|
||||||
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||||
@ -418,6 +419,10 @@ final class PlayerModel: ObservableObject {
|
|||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if player.timeControlStatus == .playing, player.rate != self.currentRate {
|
||||||
|
player.rate = self.currentRate
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
if player.timeControlStatus == .playing {
|
if player.timeControlStatus == .playing {
|
||||||
ScreenSaverManager.shared.disable(reason: "Yattee is playing video")
|
ScreenSaverManager.shared.disable(reason: "Yattee is playing video")
|
||||||
@ -469,4 +474,12 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
currentArtwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! }
|
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))!)×"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,12 +42,33 @@ extension PlayerModel {
|
|||||||
self.restoreLastSkippedSegment()
|
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
|
#endif
|
||||||
|
|
||||||
func rebuildTVMenu() {
|
func rebuildTVMenu() {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
avPlayerViewController?.transportBarCustomMenuItems = [
|
avPlayerViewController?.transportBarCustomMenuItems = [
|
||||||
restoreLastSkippedSegmentAction,
|
restoreLastSkippedSegmentAction,
|
||||||
|
rateMenu,
|
||||||
streamsMenu
|
streamsMenu
|
||||||
].compactMap { $0 }
|
].compactMap { $0 }
|
||||||
#endif
|
#endif
|
||||||
|
@ -14,9 +14,15 @@ struct PlaybackBar: View {
|
|||||||
closeButton
|
closeButton
|
||||||
|
|
||||||
if player.currentItem != nil {
|
if player.currentItem != nil {
|
||||||
|
HStack {
|
||||||
Text(playbackStatus)
|
Text(playbackStatus)
|
||||||
.foregroundColor(.gray)
|
|
||||||
|
Text("•")
|
||||||
|
|
||||||
|
rateMenu
|
||||||
|
}
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -44,7 +50,6 @@ struct PlaybackBar: View {
|
|||||||
.frame(maxWidth: 180)
|
.frame(maxWidth: 180)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.environment(\.colorScheme, .dark)
|
|
||||||
.transaction { t in t.animation = .none }
|
.transaction { t in t.animation = .none }
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
@ -52,6 +57,7 @@ struct PlaybackBar: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.environment(\.colorScheme, .dark)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
.background(.black)
|
.background(.black)
|
||||||
@ -82,7 +88,8 @@ struct PlaybackBar: View {
|
|||||||
return "loading..."
|
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 {
|
if remainingSeconds < 60 {
|
||||||
return "less than a minute"
|
return "less than a minute"
|
||||||
@ -94,6 +101,29 @@ struct PlaybackBar: View {
|
|||||||
return "ends at \(timeFinishAtString)"
|
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 {
|
private var restoreLastSkippedSegmentButton: some View {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Button {
|
Button {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user