From da78bac20580c041d18dc73a435617682e7ce51d Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Tue, 7 Jun 2022 23:27:48 +0200 Subject: [PATCH] Add initial version of music mode --- Model/Player/Backends/MPVBackend.swift | 4 ++ Model/Player/PiPDelegate.swift | 2 +- Model/Player/PlayerControlsModel.swift | 19 +++++-- Model/Player/PlayerModel.swift | 42 +++++++++++++- Shared/Player/Controls/PlayerControls.swift | 30 ++++++++-- Shared/Player/VideoPlayerView.swift | 63 +++++++++++---------- Shared/Videos/VideoCell.swift | 4 ++ Shared/Views/VideoContextMenuView.swift | 19 +++++++ 8 files changed, 142 insertions(+), 41 deletions(-) diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 32c452e3..95975c19 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -62,6 +62,10 @@ final class MPVBackend: PlayerBackend { private var controlsUpdates = false private var timeObserverThrottle = Throttle(interval: 2) + var tracks: Int { + client?.tracksCount ?? -1 + } + init(model: PlayerModel, controls: PlayerControlsModel? = nil) { self.model = model self.controls = controls diff --git a/Model/Player/PiPDelegate.swift b/Model/Player/PiPDelegate.swift index 7f4db8fa..d618d0b8 100644 --- a/Model/Player/PiPDelegate.swift +++ b/Model/Player/PiPDelegate.swift @@ -50,7 +50,7 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate { } #endif - if !player.currentItem.isNil { + if !player.currentItem.isNil, !player.musicMode { player?.show() } diff --git a/Model/Player/PlayerControlsModel.swift b/Model/Player/PlayerControlsModel.swift index 5c460a82..27147ff4 100644 --- a/Model/Player/PlayerControlsModel.swift +++ b/Model/Player/PlayerControlsModel.swift @@ -68,9 +68,15 @@ final class PlayerControlsModel: ObservableObject { } func hide() { - player?.backend.stopControlsUpdates() + guard let player = player, + !player.musicMode + else { + return + } - guard !(player?.currentItem.isNil ?? true) else { + player.backend.stopControlsUpdates() + + guard !player.currentItem.isNil else { return } @@ -83,9 +89,7 @@ final class PlayerControlsModel: ObservableObject { } func toggle() { - withAnimation(PlayerControls.animation) { - presentingControls.toggle() - } + presentingControls ? hide() : show() } func reset() { @@ -101,6 +105,11 @@ final class PlayerControlsModel: ObservableObject { #endif removeTimer() + + guard !player.musicMode else { + return + } + timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in withAnimation(PlayerControls.animation) { [weak self] in self?.presentingControls = false diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index a2270699..69f2cd06 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -62,6 +62,7 @@ final class PlayerModel: ObservableObject { @Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } } @Published var restoredSegments = [Segment]() + @Published var musicMode = false @Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI() #if os(iOS) @@ -114,7 +115,7 @@ final class PlayerModel: ObservableObject { self.avPlayerBackend = AVPlayerBackend(model: self, controls: controls) self.mpvBackend = MPVBackend(model: self) - self.activeBackend = Defaults[.activeBackend] + Defaults[.activeBackend] = .mpv } func show() { @@ -361,6 +362,12 @@ final class PlayerModel: ObservableObject { return } + if to == .mpv { + addVideoTrackFromStream() + } else { + musicMode = false + } + inactiveBackends().forEach { $0.pause() } let fromBackend: PlayerBackend = from == .appleAVPlayer ? avPlayerBackend : mpvBackend @@ -561,4 +568,37 @@ final class PlayerModel: ObservableObject { func setNeedsDrawing(_ needsDrawing: Bool) { backends.forEach { $0.setNeedsDrawing(needsDrawing) } } + + func toggleMusicMode() { + musicMode.toggle() + + if musicMode { + if playingInPictureInPicture { + avPlayerBackend.pause() + avPlayerBackend.switchToMPVOnPipClose = false + closePiP() + } + changeActiveBackend(from: .appleAVPlayer, to: .mpv) + controls.presentingControls = true + controls.removeTimer() + mpvBackend.setVideoToNo() + } else { + addVideoTrackFromStream() + mpvBackend.setVideoToAuto() + + controls.resetTimer() + } + } + + func addVideoTrackFromStream() { + if let videoTrackURL = stream?.videoAsset?.url, + mpvBackend.tracks < 2 + { + logger.info("adding video track") + + mpvBackend.addVideoTrack(videoTrackURL) + } + + mpvBackend.setVideoToAuto() + } } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 4d0b9a2a..5c6511a6 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -1,10 +1,12 @@ import Foundation +import SDWebImageSwiftUI import SwiftUI struct PlayerControls: View { static let animation = Animation.easeInOut(duration: 0.2) private var player: PlayerModel! + private var thumbnails: ThumbnailsModel! @EnvironmentObject private var model @@ -20,8 +22,9 @@ struct PlayerControls: View { @FocusState private var focusedField: Field? #endif - init(player: PlayerModel) { + init(player: PlayerModel, thumbnails: ThumbnailsModel) { self.player = player + self.thumbnails = thumbnails } var body: some View { @@ -86,10 +89,26 @@ struct PlayerControls: View { } #else .background(PlayerGestures()) + .background(controlsBackground) #endif .environment(\.colorScheme, .dark) } + @ViewBuilder var controlsBackground: some View { + if player.musicMode, + let item = self.player.currentItem, + let url = thumbnails.best(item.video) + { + WebImage(url: url) + .resizable() + .placeholder { + Rectangle().fill(Color("PlaceholderColor")) + } + .retryOnAppear(true) + .indicator(.activity) + } + } + var timeline: some View { TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0) } @@ -186,9 +205,11 @@ struct PlayerControls: View { closeVideoButton + button("Music Mode", systemImage: "music.note", active: player.musicMode, action: player.toggleMusicMode) + .disabled(player.activeBackend == .appleAVPlayer) + Spacer() #endif -// button("Music Mode", systemImage: "music.note") } } @@ -355,6 +376,7 @@ struct PlayerControls: View { systemImage: String = "arrow.up.left.and.arrow.down.right", size: Double = 30, cornerRadius: Double = 3, + active: Bool = false, action: @escaping () -> Void = {} ) -> some View { Button { @@ -367,7 +389,7 @@ struct PlayerControls: View { .contentShape(Rectangle()) } .buttonStyle(.plain) - .foregroundColor(.primary) + .foregroundColor(active ? .accentColor : .primary) .frame(width: size, height: size) #if os(macOS) .background(VisualEffectBlur(material: .hudWindow)) @@ -396,7 +418,7 @@ struct PlayerControls_Previews: PreviewProvider { let view = ZStack { Color.gray - PlayerControls(player: PlayerModel()) + PlayerControls(player: PlayerModel(), thumbnails: ThumbnailsModel()) .injectFixtureEnvironmentObjects() .environmentObject(model) } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 0854b98a..12b971c0 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -48,6 +48,7 @@ struct VideoPlayerView: View { @EnvironmentObject private var accounts @EnvironmentObject private var playerControls @EnvironmentObject private var player + @EnvironmentObject private var thumbnails var body: some View { #if os(macOS) @@ -126,9 +127,7 @@ struct VideoPlayerView: View { #else GeometryReader { geometry in VStack(spacing: 0) { - if player.currentItem.isNil { - playerPlaceholder(geometry: geometry) - } else if player.playingInPictureInPicture { + if player.playingInPictureInPicture { pictureInPicturePlaceholder(geometry: geometry) } else { playerView @@ -140,6 +139,7 @@ struct VideoPlayerView: View { fullScreen: playerControls.playingFullscreen ) ) + .overlay(playerPlaceholder(geometry: geometry)) #endif } } @@ -273,7 +273,7 @@ struct VideoPlayerView: View { PlayerGestures() #endif - PlayerControls(player: player) + PlayerControls(player: player, thumbnails: thumbnails) } #if os(iOS) .onAppear { @@ -298,38 +298,41 @@ struct VideoPlayerView: View { #endif } - func playerPlaceholder(geometry: GeometryProxy) -> some View { - ZStack(alignment: .topLeading) { - HStack { - Spacer() - VStack { + @ViewBuilder func playerPlaceholder(geometry: GeometryProxy) -> some View { + if player.currentItem.isNil { + ZStack(alignment: .topLeading) { + HStack { Spacer() - VStack(spacing: 10) { - #if !os(tvOS) - Image(systemName: "ticket") - .font(.system(size: 120)) - #endif + VStack { + Spacer() + VStack(spacing: 10) { + #if !os(tvOS) + Image(systemName: "ticket") + .font(.system(size: 120)) + #endif + } + Spacer() } + .foregroundColor(.gray) Spacer() } - .foregroundColor(.gray) - Spacer() - } - #if os(iOS) - Button { - player.hide() - } label: { - Image(systemName: "xmark") - .font(.system(size: 40)) - } - .buttonStyle(.plain) - .padding(10) - .foregroundColor(.gray) - #endif + #if os(iOS) + Button { + player.hide() + } label: { + Image(systemName: "xmark") + .font(.system(size: 40)) + } + .buttonStyle(.plain) + .padding(10) + .foregroundColor(.gray) + #endif + } + .background(Color.black) + .contentShape(Rectangle()) + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio) } - .contentShape(Rectangle()) - .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / Self.defaultAspectRatio) } func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View { diff --git a/Shared/Videos/VideoCell.swift b/Shared/Videos/VideoCell.swift index 8b842d21..8b9fd826 100644 --- a/Shared/Videos/VideoCell.swift +++ b/Shared/Videos/VideoCell.swift @@ -65,6 +65,10 @@ struct VideoCell: View { return } + if player.musicMode { + player.toggleMusicMode() + } + if watchingNow { if !player.playingInPictureInPicture { player.show() diff --git a/Shared/Views/VideoContextMenuView.swift b/Shared/Views/VideoContextMenuView.swift index 6338fc27..d8c8ae93 100644 --- a/Shared/Views/VideoContextMenuView.swift +++ b/Shared/Views/VideoContextMenuView.swift @@ -57,6 +57,9 @@ struct VideoContextMenuView: View { #if os(iOS) playNowInPictureInPictureButton #endif + #if !os(tvOS) + playNowInMusicMode + #endif } Section { @@ -131,6 +134,10 @@ struct VideoContextMenuView: View { private var playNowButton: some View { Button { + if player.musicMode { + player.toggleMusicMode() + } + player.play(video) } label: { Label("Play Now", systemImage: "play") @@ -149,6 +156,18 @@ struct VideoContextMenuView: View { } } + private var playNowInMusicMode: some View { + Button { + if !player.musicMode { + player.toggleMusicMode() + } + + player.play(video, at: watch?.timeToRestart, showingPlayer: false) + } label: { + Label("Play Music", systemImage: "music.note") + } + } + private var playNextButton: some View { Button { player.playNext(video)