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)
}
if model.queue.isEmpty {
#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()
}
eofPlaybackModeAction()
}
private func addFrequentTimeObserver() {

View File

@ -444,22 +444,7 @@ final class MPVBackend: PlayerBackend {
getClientUpdates()
if Defaults[.closeLastItemOnPlaybackEnd] {
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()
}
eofPlaybackModeAction()
}
func setNeedsDrawing(_ needsDrawing: Bool) {

View File

@ -72,4 +72,30 @@ extension PlayerBackend {
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
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
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]
let logger = Logger(label: "stream.yattee.app")
@ -65,6 +82,11 @@ final class PlayerModel: ObservableObject {
@Published var restoredSegments = [Segment]()
@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 isSeeking = false { didSet {
@ -160,6 +182,7 @@ final class PlayerModel: ObservableObject {
)
Defaults[.activeBackend] = .mpv
playbackMode = Defaults[.playbackMode]
}
func show() {
@ -489,6 +512,7 @@ final class PlayerModel: ObservableObject {
backend.closeItem()
aspectRatio = VideoPlayerView.defaultAspectRatio
resetAutoplay()
}
func closePiP() {
@ -518,10 +542,50 @@ final class PlayerModel: ObservableObject {
#endif
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)
var windowTitle: String {
currentVideo.isNil ? "Not playing" : "\(currentVideo!.title) - \(currentVideo!.author)"

View File

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

View File

@ -43,22 +43,7 @@ extension PlayerModel {
self.pause()
if Defaults[.closeLastItemOnPlaybackEnd] {
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()
}
self.backend.eofPlaybackModeAction()
}
return

View File

@ -74,6 +74,7 @@ extension Defaults.Keys {
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
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 showWatchingProgress = Key<Bool>("showWatchingProgress", default: true)

View File

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

View File

@ -275,6 +275,7 @@ struct PlayerControls: View {
Spacer()
HStack(spacing: 20) {
playbackModeButton
restartVideoButton
advanceToNextItemButton
#if !os(tvOS)
@ -286,6 +287,12 @@ struct PlayerControls: View {
.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 {
button("Seek Backward", systemImage: "gobackward.10", size: 25, cornerRadius: 5, background: false) {
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
@ -337,7 +344,7 @@ struct PlayerControls: View {
button("Next", systemImage: "forward.fill", size: 25, cornerRadius: 5, background: false) {
player.advanceToNextItem()
}
.disabled(player.queue.isEmpty)
.disabled(!player.isAdvanceToNextItemAvailable)
}
func button(

View File

@ -80,12 +80,8 @@ struct PlaylistsView: View {
Spacer()
if currentPlaylist != nil {
HStack(spacing: 0) {
playButton
shuffleButton
}
.offset(x: 10)
playButton
.offset(x: 10)
}
}
.padding(.horizontal)
@ -180,7 +176,6 @@ struct PlaylistsView: View {
.labelStyle(.iconOnly)
playButton
shuffleButton
}
Spacer()
@ -293,6 +288,7 @@ struct PlaylistsView: View {
private var playButton: some View {
Button {
player.playbackMode = .queue
player.play(items.compactMap(\.video))
} label: {
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? {
model.find(id: selectedPlaylistID) ?? model.all.first
}

View File

@ -86,8 +86,6 @@ struct ChannelPlaylistView: View {
playButton
.labelStyle(.iconOnly)
shuffleButton
.labelStyle(.iconOnly)
}
#endif
VerticalCells(items: items)
@ -119,7 +117,6 @@ struct ChannelPlaylistView: View {
}
playButton
shuffleButton
}
}
}
@ -137,20 +134,13 @@ struct ChannelPlaylistView: View {
private var playButton: some View {
Button {
player.playbackMode = .queue
player.play(videos)
} label: {
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] {
items.compactMap(\.video)
}

View File

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

View File

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