Playback modes

This commit is contained in:
Arkadiusz Fal 2022-07-11 00:24:56 +02:00
parent a632a4296d
commit e0620abf9f
13 changed files with 150 additions and 105 deletions

View File

@ -492,26 +492,7 @@ final class AVPlayerBackend: PlayerBackend {
model.prepareCurrentItemForHistory(finished: true) model.prepareCurrentItemForHistory(finished: true)
} }
if model.queue.isEmpty { eofPlaybackModeAction()
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(false)
#endif
if Defaults[.closeLastItemOnPlaybackEnd] {
model.resetQueue()
#if os(tvOS)
controller?.playerView.dismiss(animated: false) { [weak self] in
self?.controller?.dismiss(animated: true)
}
#else
model.hide()
#endif
}
} else {
if model.playingInPictureInPicture {
startPictureInPictureOnPlay = true
}
model.advanceToNextItem()
}
} }
private func addFrequentTimeObserver() { private func addFrequentTimeObserver() {

View File

@ -444,22 +444,7 @@ final class MPVBackend: PlayerBackend {
getClientUpdates() getClientUpdates()
if Defaults[.closeLastItemOnPlaybackEnd] { eofPlaybackModeAction()
model.prepareCurrentItemForHistory(finished: true)
}
if model.queue.isEmpty {
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(false)
#endif
if Defaults[.closeLastItemOnPlaybackEnd] {
model.resetQueue()
model.hide()
}
} else {
model.advanceToNextItem()
}
} }
func setNeedsDrawing(_ needsDrawing: Bool) { func setNeedsDrawing(_ needsDrawing: Bool) {

View File

@ -72,4 +72,30 @@ extension PlayerBackend {
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) { func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
seek(relative: time, completionHandler: completionHandler) seek(relative: time, completionHandler: completionHandler)
} }
func eofPlaybackModeAction() {
switch model.playbackMode {
case .queue, .shuffle:
if Defaults[.closeLastItemOnPlaybackEnd] {
model.prepareCurrentItemForHistory(finished: true)
}
if model.queue.isEmpty {
if Defaults[.closeLastItemOnPlaybackEnd] {
model.resetQueue()
model.hide()
}
} else {
model.advanceToNextItem()
}
case .loopOne:
model.backend.seek(to: .zero) { _ in
self.model.play()
}
case .related:
guard let item = model.autoplayItem else { return }
model.resetAutoplay()
model.advanceToItem(item)
}
}
} }

View File

@ -15,6 +15,23 @@ import SwiftyJSON
#endif #endif
final class PlayerModel: ObservableObject { final class PlayerModel: ObservableObject {
enum PlaybackMode: String, CaseIterable, Defaults.Serializable {
case queue, shuffle, loopOne, related
var systemImage: String {
switch self {
case .queue:
return "list.number"
case .shuffle:
return "shuffle"
case .loopOne:
return "repeat.1"
case .related:
return "infinity"
}
}
}
static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2] static let availableRates: [Float] = [0.5, 0.67, 0.8, 1, 1.25, 1.5, 2]
let logger = Logger(label: "stream.yattee.app") let logger = Logger(label: "stream.yattee.app")
@ -65,6 +82,11 @@ final class PlayerModel: ObservableObject {
@Published var restoredSegments = [Segment]() @Published var restoredSegments = [Segment]()
@Published var musicMode = false @Published var musicMode = false
@Published var playbackMode = PlaybackMode.queue { didSet { handlePlaybackModeChange() }}
@Published var autoplayItem: PlayerQueueItem?
@Published var autoplayItemSource: Video?
@Published var advancing = false
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI() @Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
@Published var isSeeking = false { didSet { @Published var isSeeking = false { didSet {
@ -160,6 +182,7 @@ final class PlayerModel: ObservableObject {
) )
Defaults[.activeBackend] = .mpv Defaults[.activeBackend] = .mpv
playbackMode = Defaults[.playbackMode]
} }
func show() { func show() {
@ -489,6 +512,7 @@ final class PlayerModel: ObservableObject {
backend.closeItem() backend.closeItem()
aspectRatio = VideoPlayerView.defaultAspectRatio aspectRatio = VideoPlayerView.defaultAspectRatio
resetAutoplay()
} }
func closePiP() { func closePiP() {
@ -518,9 +542,49 @@ final class PlayerModel: ObservableObject {
#endif #endif
DispatchQueue.main.async(qos: .background) { [weak self] in DispatchQueue.main.async(qos: .background) { [weak self] in
Defaults[.lastPlayed] = self?.currentItem guard let self = self else { return }
Defaults[.lastPlayed] = self.currentItem
if self.playbackMode == .related,
let video = self.currentVideo,
self.autoplayItemSource.isNil || self.autoplayItemSource?.videoID != video.videoID
{
self.setRelatedAutoplayItem()
} }
} }
}
func handlePlaybackModeChange() {
Defaults[.playbackMode] = playbackMode
guard playbackMode == .related else {
autoplayItem = nil
return
}
setRelatedAutoplayItem()
}
func setRelatedAutoplayItem() {
guard let video = currentVideo?.related.randomElement() else { return }
let item = PlayerQueueItem(video)
autoplayItem = item
autoplayItemSource = video
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
guard let self = self else { return }
self.accounts.api.loadDetails(item, completionHandler: { newItem in
guard newItem.videoID == self.autoplayItem?.videoID else { return }
self.autoplayItem = newItem
self.controls.objectWillChange.send()
})
}
}
func resetAutoplay() {
autoplayItem = nil
autoplayItemSource = nil
}
#if os(macOS) #if os(macOS)
var windowTitle: String { var windowTitle: String {

View File

@ -8,20 +8,16 @@ extension PlayerModel {
currentItem?.video currentItem?.video
} }
func play(_ videos: [Video], shuffling: Bool = false) { func play(_ videos: [Video]) {
let videosToPlay = shuffling ? videos.shuffled() : videos videos.forEach { video in
enqueueVideo(video, loadDetails: false)
guard let first = videosToPlay.first else {
return
} }
enqueueVideo(first, prepending: true) { _, item in #if os(iOS)
self.advanceToItem(item) onPresentPlayer = { [weak self] in self?.advanceToNextItem() }
} #else
advanceToNextItem()
videosToPlay.dropFirst().reversed().forEach { video in #endif
enqueueVideo(video, prepending: true, loadDetails: false)
}
show() show()
} }
@ -43,6 +39,8 @@ extension PlayerModel {
} }
func playItem(_ item: PlayerQueueItem, at time: CMTime? = nil) { func playItem(_ item: PlayerQueueItem, at time: CMTime? = nil) {
advancing = false
if !playingInPictureInPicture { if !playingInPictureInPicture {
backend.closeItem() backend.closeItem()
} }
@ -79,13 +77,42 @@ extension PlayerModel {
} }
func advanceToNextItem() { func advanceToNextItem() {
guard !advancing else {
return
}
advancing = true
prepareCurrentItemForHistory() prepareCurrentItemForHistory()
if let nextItem = queue.first { var nextItem: PlayerQueueItem?
switch playbackMode {
case .queue:
nextItem = queue.first
case .shuffle:
nextItem = queue.randomElement()
case .related:
nextItem = autoplayItem
case .loopOne:
nextItem = nil
}
resetAutoplay()
if let nextItem = nextItem {
advanceToItem(nextItem) advanceToItem(nextItem)
} }
} }
var isAdvanceToNextItemAvailable: Bool {
switch playbackMode {
case .loopOne:
return false
case .queue, .shuffle:
return !queue.isEmpty
case .related:
return !autoplayItem.isNil
}
}
func advanceToItem(_ newItem: PlayerQueueItem, at time: CMTime? = nil) { func advanceToItem(_ newItem: PlayerQueueItem, at time: CMTime? = nil) {
prepareCurrentItemForHistory() prepareCurrentItemForHistory()
@ -206,6 +233,7 @@ extension PlayerModel {
private func videoLoadFailureHandler(_ error: RequestError) { private func videoLoadFailureHandler(_ error: RequestError) {
navigation.presentAlert(title: "Could not load video", message: error.userMessage) navigation.presentAlert(title: "Could not load video", message: error.userMessage)
advancing = false
videoBeingOpened = nil videoBeingOpened = nil
currentItem = nil currentItem = nil
} }

View File

@ -43,22 +43,7 @@ extension PlayerModel {
self.pause() self.pause()
if Defaults[.closeLastItemOnPlaybackEnd] { self.backend.eofPlaybackModeAction()
self.prepareCurrentItemForHistory(finished: true)
}
if self.queue.isEmpty {
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(false)
#endif
if Defaults[.closeLastItemOnPlaybackEnd] {
self.resetQueue()
self.hide()
}
} else {
self.advanceToNextItem()
}
} }
return return

View File

@ -74,6 +74,7 @@ extension Defaults.Keys {
static let queue = Key<[PlayerQueueItem]>("queue", default: []) static let queue = Key<[PlayerQueueItem]>("queue", default: [])
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed") static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let playbackMode = Key<PlayerModel.PlaybackMode>("playbackMode", default: .queue)
static let saveHistory = Key<Bool>("saveHistory", default: true) static let saveHistory = Key<Bool>("saveHistory", default: true)
static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true) static let showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)

View File

@ -18,9 +18,6 @@ struct AppSidebarPlaylists: View {
Button("Play All") { Button("Play All") {
player.play(playlists.find(id: playlist.id)?.videos ?? []) player.play(playlists.find(id: playlist.id)?.videos ?? [])
} }
Button("Shuffle All") {
player.play(playlists.find(id: playlist.id)?.videos ?? [], shuffling: true)
}
Button("Edit") { Button("Edit") {
navigation.presentEditPlaylistForm(playlists.find(id: playlist.id)) navigation.presentEditPlaylistForm(playlists.find(id: playlist.id))
} }

View File

@ -275,6 +275,7 @@ struct PlayerControls: View {
Spacer() Spacer()
HStack(spacing: 20) { HStack(spacing: 20) {
playbackModeButton
restartVideoButton restartVideoButton
advanceToNextItemButton advanceToNextItemButton
#if !os(tvOS) #if !os(tvOS)
@ -286,6 +287,12 @@ struct PlayerControls: View {
.font(.system(size: 20)) .font(.system(size: 20))
} }
var playbackModeButton: some View {
button("Playback Mode", systemImage: player.playbackMode.systemImage, background: false) {
player.playbackMode = player.playbackMode.next()
}
}
var seekBackwardButton: some View { var seekBackwardButton: some View {
button("Seek Backward", systemImage: "gobackward.10", size: 25, cornerRadius: 5, background: false) { button("Seek Backward", systemImage: "gobackward.10", size: 25, cornerRadius: 5, background: false) {
player.backend.seek(relative: .secondsInDefaultTimescale(-10)) player.backend.seek(relative: .secondsInDefaultTimescale(-10))
@ -337,7 +344,7 @@ struct PlayerControls: View {
button("Next", systemImage: "forward.fill", size: 25, cornerRadius: 5, background: false) { button("Next", systemImage: "forward.fill", size: 25, cornerRadius: 5, background: false) {
player.advanceToNextItem() player.advanceToNextItem()
} }
.disabled(player.queue.isEmpty) .disabled(!player.isAdvanceToNextItemAvailable)
} }
func button( func button(

View File

@ -80,11 +80,7 @@ struct PlaylistsView: View {
Spacer() Spacer()
if currentPlaylist != nil { if currentPlaylist != nil {
HStack(spacing: 0) {
playButton playButton
shuffleButton
}
.offset(x: 10) .offset(x: 10)
} }
} }
@ -180,7 +176,6 @@ struct PlaylistsView: View {
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
playButton playButton
shuffleButton
} }
Spacer() Spacer()
@ -293,6 +288,7 @@ struct PlaylistsView: View {
private var playButton: some View { private var playButton: some View {
Button { Button {
player.playbackMode = .queue
player.play(items.compactMap(\.video)) player.play(items.compactMap(\.video))
} label: { } label: {
Image(systemName: "play") Image(systemName: "play")
@ -301,16 +297,6 @@ struct PlaylistsView: View {
} }
} }
private var shuffleButton: some View {
Button {
player.play(items.compactMap(\.video), shuffling: true)
} label: {
Image(systemName: "shuffle")
.padding(8)
.contentShape(Rectangle())
}
}
private var currentPlaylist: Playlist? { private var currentPlaylist: Playlist? {
model.find(id: selectedPlaylistID) ?? model.all.first model.find(id: selectedPlaylistID) ?? model.all.first
} }

View File

@ -86,8 +86,6 @@ struct ChannelPlaylistView: View {
playButton playButton
.labelStyle(.iconOnly) .labelStyle(.iconOnly)
shuffleButton
.labelStyle(.iconOnly)
} }
#endif #endif
VerticalCells(items: items) VerticalCells(items: items)
@ -119,7 +117,6 @@ struct ChannelPlaylistView: View {
} }
playButton playButton
shuffleButton
} }
} }
} }
@ -137,20 +134,13 @@ struct ChannelPlaylistView: View {
private var playButton: some View { private var playButton: some View {
Button { Button {
player.playbackMode = .queue
player.play(videos) player.play(videos)
} label: { } label: {
Label("Play All", systemImage: "play") Label("Play All", systemImage: "play")
} }
} }
private var shuffleButton: some View {
Button {
player.play(videos, shuffling: true)
} label: {
Label("Shuffle", systemImage: "shuffle")
}
}
private var videos: [Video] { private var videos: [Video] {
items.compactMap(\.video) items.compactMap(\.video)
} }

View File

@ -109,7 +109,7 @@ struct ControlsBar: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.disabled(model.queue.isEmpty) .disabled(!model.isAdvanceToNextItemAvailable)
Button { Button {
model.closeCurrentItem() model.closeCurrentItem()

View File

@ -65,16 +65,11 @@ struct PlaylistVideosView: View {
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title))) FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
Button { Button {
player.playbackMode = .queue
player.play(videos) player.play(videos)
} label: { } label: {
Label("Play All", systemImage: "play") Label("Play All", systemImage: "play")
} }
Button {
player.play(videos, shuffling: true)
} label: {
Label("Shuffle", systemImage: "shuffle")
}
} }
} }
} }