mirror of
https://github.com/yattee/yattee.git
synced 2024-12-23 05:53:41 +00:00
Playback modes
This commit is contained in:
parent
a632a4296d
commit
e0620abf9f
@ -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() {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)"
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ struct ControlsBar: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.disabled(model.queue.isEmpty)
|
||||
.disabled(!model.isAdvanceToNextItemAvailable)
|
||||
|
||||
Button {
|
||||
model.closeCurrentItem()
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user