2021-10-16 22:48:58 +00:00
|
|
|
import Defaults
|
2021-08-22 19:13:33 +00:00
|
|
|
import Foundation
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
struct PlaybackBar: View {
|
2021-08-23 21:31:51 +00:00
|
|
|
@Environment(\.dismiss) private var dismiss
|
2021-10-05 20:20:09 +00:00
|
|
|
@Environment(\.inNavigationView) private var inNavigationView
|
|
|
|
|
2021-10-16 22:48:58 +00:00
|
|
|
@EnvironmentObject<InstancesModel> private var instances
|
2021-10-05 20:20:09 +00:00
|
|
|
@EnvironmentObject<PlayerModel> private var player
|
2021-08-23 21:31:51 +00:00
|
|
|
|
2021-08-22 19:13:33 +00:00
|
|
|
var body: some View {
|
|
|
|
HStack {
|
|
|
|
closeButton
|
|
|
|
|
2021-10-05 20:20:09 +00:00
|
|
|
if player.currentItem != nil {
|
2021-11-02 17:24:59 +00:00
|
|
|
HStack {
|
|
|
|
Text(playbackStatus)
|
|
|
|
|
|
|
|
Text("•")
|
|
|
|
|
|
|
|
rateMenu
|
|
|
|
}
|
|
|
|
.font(.caption2)
|
|
|
|
.foregroundColor(.gray)
|
2021-10-16 22:48:58 +00:00
|
|
|
|
|
|
|
Spacer()
|
|
|
|
|
|
|
|
HStack(spacing: 4) {
|
2021-10-23 16:49:45 +00:00
|
|
|
if !player.lastSkipped.isNil {
|
|
|
|
restoreLastSkippedSegmentButton
|
|
|
|
}
|
2021-10-24 18:01:08 +00:00
|
|
|
if player.live {
|
2021-10-16 22:48:58 +00:00
|
|
|
Image(systemName: "dot.radiowaves.left.and.right")
|
|
|
|
} else if player.isLoadingAvailableStreams || player.isLoadingStream {
|
|
|
|
Image(systemName: "bolt.horizontal.fill")
|
2021-11-07 16:52:42 +00:00
|
|
|
} else if !player.playerError.isNil {
|
|
|
|
Button {
|
|
|
|
player.presentingErrorDetails = true
|
|
|
|
} label: {
|
|
|
|
Image(systemName: "exclamationmark.circle.fill")
|
|
|
|
.foregroundColor(.red)
|
|
|
|
}
|
|
|
|
.buttonStyle(.plain)
|
2021-09-13 20:41:16 +00:00
|
|
|
}
|
2021-10-16 22:48:58 +00:00
|
|
|
|
|
|
|
streamControl
|
|
|
|
.disabled(player.isLoadingAvailableStreams)
|
|
|
|
.frame(alignment: .trailing)
|
|
|
|
.onChange(of: player.streamSelection) { selection in
|
|
|
|
guard !selection.isNil else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
player.upgradeToStream(selection!)
|
|
|
|
}
|
|
|
|
#if os(macOS)
|
|
|
|
.frame(maxWidth: 180)
|
|
|
|
#endif
|
2021-08-23 21:31:51 +00:00
|
|
|
}
|
2021-10-16 22:48:58 +00:00
|
|
|
.transaction { t in t.animation = .none }
|
2021-10-05 20:20:09 +00:00
|
|
|
.foregroundColor(.gray)
|
|
|
|
.font(.caption2)
|
|
|
|
} else {
|
|
|
|
Spacer()
|
2021-08-23 21:31:51 +00:00
|
|
|
}
|
2021-08-22 19:13:33 +00:00
|
|
|
}
|
2021-11-07 16:52:42 +00:00
|
|
|
.alert(player.playerError?.localizedDescription ?? "", isPresented: $player.presentingErrorDetails) {
|
|
|
|
Button("OK") {}
|
|
|
|
}
|
2021-11-02 17:24:59 +00:00
|
|
|
.environment(\.colorScheme, .dark)
|
2021-10-16 22:48:58 +00:00
|
|
|
.frame(minWidth: 0, maxWidth: .infinity)
|
2021-08-22 19:13:33 +00:00
|
|
|
.padding(4)
|
|
|
|
.background(.black)
|
|
|
|
}
|
|
|
|
|
2021-10-16 22:48:58 +00:00
|
|
|
private var closeButton: some View {
|
|
|
|
Button {
|
|
|
|
dismiss()
|
|
|
|
} label: {
|
|
|
|
Label(
|
|
|
|
"Close",
|
|
|
|
systemImage: inNavigationView ? "chevron.backward.circle.fill" : "chevron.down.circle.fill"
|
|
|
|
)
|
|
|
|
.labelStyle(.iconOnly)
|
|
|
|
}
|
|
|
|
.accessibilityLabel(Text("Close"))
|
|
|
|
.buttonStyle(.borderless)
|
|
|
|
.foregroundColor(.gray)
|
|
|
|
.keyboardShortcut(.cancelAction)
|
2021-08-22 19:13:33 +00:00
|
|
|
}
|
|
|
|
|
2021-10-16 22:48:58 +00:00
|
|
|
private var playbackStatus: String {
|
2021-10-05 20:20:09 +00:00
|
|
|
if player.live {
|
|
|
|
return "LIVE"
|
|
|
|
}
|
|
|
|
|
2021-10-24 18:01:08 +00:00
|
|
|
guard player.time != nil, player.time!.isValid, !player.currentVideo.isNil else {
|
2021-10-05 20:20:09 +00:00
|
|
|
return "loading..."
|
2021-08-22 19:13:33 +00:00
|
|
|
}
|
|
|
|
|
2021-11-02 17:24:59 +00:00
|
|
|
let videoLengthAtRate = player.currentVideo!.length / Double(player.currentRate)
|
|
|
|
let remainingSeconds = videoLengthAtRate - player.time!.seconds
|
2021-08-22 19:13:33 +00:00
|
|
|
|
2021-08-23 21:31:51 +00:00
|
|
|
if remainingSeconds < 60 {
|
|
|
|
return "less than a minute"
|
|
|
|
}
|
|
|
|
|
2021-08-22 19:13:33 +00:00
|
|
|
let timeFinishAt = Date.now.addingTimeInterval(remainingSeconds)
|
|
|
|
let timeFinishAtString = timeFinishAt.formatted(date: .omitted, time: .shortened)
|
|
|
|
|
2021-10-05 20:20:09 +00:00
|
|
|
return "ends at \(timeFinishAtString)"
|
2021-08-22 19:13:33 +00:00
|
|
|
}
|
|
|
|
|
2021-11-02 17:24:59 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-23 16:49:45 +00:00
|
|
|
private var restoreLastSkippedSegmentButton: some View {
|
2021-10-24 12:31:10 +00:00
|
|
|
HStack(spacing: 4) {
|
|
|
|
Button {
|
|
|
|
player.restoreLastSkippedSegment()
|
|
|
|
} label: {
|
|
|
|
HStack(spacing: 4) {
|
|
|
|
Image(systemName: "arrow.uturn.left.circle")
|
|
|
|
Text(player.lastSkipped!.title())
|
|
|
|
}
|
2021-10-23 16:49:45 +00:00
|
|
|
}
|
2021-10-24 12:31:10 +00:00
|
|
|
.buttonStyle(.plain)
|
|
|
|
|
|
|
|
Text("•")
|
2021-10-23 16:49:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-16 22:48:58 +00:00
|
|
|
private var streamControl: some View {
|
|
|
|
#if os(macOS)
|
|
|
|
Picker("", selection: $player.streamSelection) {
|
|
|
|
ForEach(instances.all) { instance in
|
|
|
|
let instanceStreams = availableStreamsForInstance(instance)
|
|
|
|
if !instanceStreams.values.isEmpty {
|
|
|
|
let kinds = Array(instanceStreams.keys).sorted { $0 < $1 }
|
|
|
|
|
|
|
|
Section(header: Text(instance.longDescription)) {
|
|
|
|
ForEach(kinds, id: \.self) { key in
|
|
|
|
ForEach(instanceStreams[key] ?? []) { stream in
|
|
|
|
Text(stream.quality).tag(Stream?.some(stream))
|
|
|
|
}
|
|
|
|
|
|
|
|
if kinds.count > 1 {
|
|
|
|
Divider()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
Menu {
|
|
|
|
ForEach(instances.all) { instance in
|
|
|
|
let instanceStreams = availableStreamsForInstance(instance)
|
|
|
|
if !instanceStreams.values.isEmpty {
|
|
|
|
let kinds = Array(instanceStreams.keys).sorted { $0 < $1 }
|
|
|
|
Picker("", selection: $player.streamSelection) {
|
|
|
|
ForEach(kinds, id: \.self) { key in
|
|
|
|
ForEach(instanceStreams[key] ?? []) { stream in
|
|
|
|
Text(stream.description).tag(Stream?.some(stream))
|
|
|
|
}
|
|
|
|
|
|
|
|
if kinds.count > 1 {
|
|
|
|
Divider()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
Text(player.streamSelection?.quality ?? "")
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
|
|
|
private func availableStreamsForInstance(_ instance: Instance) -> [Stream.Kind: [Stream]] {
|
2021-10-17 23:06:00 +00:00
|
|
|
let streams = player.availableStreamsSorted.filter { $0.instance == instance }
|
2021-10-16 22:48:58 +00:00
|
|
|
|
|
|
|
return Dictionary(grouping: streams, by: \.kind!)
|
|
|
|
}
|
2021-08-22 19:13:33 +00:00
|
|
|
}
|
2021-10-13 22:10:29 +00:00
|
|
|
|
|
|
|
struct PlaybackBar_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
|
|
|
PlaybackBar()
|
|
|
|
.injectFixtureEnvironmentObjects()
|
|
|
|
}
|
|
|
|
}
|