mirror of
https://github.com/yattee/yattee.git
synced 2025-01-23 13:17:04 +00:00
Add initial version of music mode
This commit is contained in:
parent
cf6077aaf3
commit
89713d815a
@ -62,6 +62,10 @@ final class MPVBackend: PlayerBackend {
|
|||||||
private var controlsUpdates = false
|
private var controlsUpdates = false
|
||||||
private var timeObserverThrottle = Throttle(interval: 2)
|
private var timeObserverThrottle = Throttle(interval: 2)
|
||||||
|
|
||||||
|
var tracks: Int {
|
||||||
|
client?.tracksCount ?? -1
|
||||||
|
}
|
||||||
|
|
||||||
init(model: PlayerModel, controls: PlayerControlsModel? = nil) {
|
init(model: PlayerModel, controls: PlayerControlsModel? = nil) {
|
||||||
self.model = model
|
self.model = model
|
||||||
self.controls = controls
|
self.controls = controls
|
||||||
|
@ -50,7 +50,7 @@ final class PiPDelegate: NSObject, AVPictureInPictureControllerDelegate {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if !player.currentItem.isNil {
|
if !player.currentItem.isNil, !player.musicMode {
|
||||||
player?.show()
|
player?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,9 +68,15 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hide() {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +89,7 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toggle() {
|
func toggle() {
|
||||||
withAnimation(PlayerControls.animation) {
|
presentingControls ? hide() : show()
|
||||||
presentingControls.toggle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func reset() {
|
func reset() {
|
||||||
@ -101,6 +105,11 @@ final class PlayerControlsModel: ObservableObject {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
removeTimer()
|
removeTimer()
|
||||||
|
|
||||||
|
guard !player.musicMode else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
|
timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: false) { _ in
|
||||||
withAnimation(PlayerControls.animation) { [weak self] in
|
withAnimation(PlayerControls.animation) { [weak self] in
|
||||||
self?.presentingControls = false
|
self?.presentingControls = false
|
||||||
|
@ -62,6 +62,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
||||||
@Published var restoredSegments = [Segment]()
|
@Published var restoredSegments = [Segment]()
|
||||||
|
|
||||||
|
@Published var musicMode = false
|
||||||
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@ -114,7 +115,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
self.avPlayerBackend = AVPlayerBackend(model: self, controls: controls)
|
self.avPlayerBackend = AVPlayerBackend(model: self, controls: controls)
|
||||||
self.mpvBackend = MPVBackend(model: self)
|
self.mpvBackend = MPVBackend(model: self)
|
||||||
|
|
||||||
self.activeBackend = Defaults[.activeBackend]
|
Defaults[.activeBackend] = .mpv
|
||||||
}
|
}
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
@ -361,6 +362,12 @@ final class PlayerModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if to == .mpv {
|
||||||
|
addVideoTrackFromStream()
|
||||||
|
} else {
|
||||||
|
musicMode = false
|
||||||
|
}
|
||||||
|
|
||||||
inactiveBackends().forEach { $0.pause() }
|
inactiveBackends().forEach { $0.pause() }
|
||||||
|
|
||||||
let fromBackend: PlayerBackend = from == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
let fromBackend: PlayerBackend = from == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
||||||
@ -561,4 +568,37 @@ final class PlayerModel: ObservableObject {
|
|||||||
func setNeedsDrawing(_ needsDrawing: Bool) {
|
func setNeedsDrawing(_ needsDrawing: Bool) {
|
||||||
backends.forEach { $0.setNeedsDrawing(needsDrawing) }
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import SDWebImageSwiftUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PlayerControls: View {
|
struct PlayerControls: View {
|
||||||
static let animation = Animation.easeInOut(duration: 0.2)
|
static let animation = Animation.easeInOut(duration: 0.2)
|
||||||
|
|
||||||
private var player: PlayerModel!
|
private var player: PlayerModel!
|
||||||
|
private var thumbnails: ThumbnailsModel!
|
||||||
|
|
||||||
@EnvironmentObject<PlayerControlsModel> private var model
|
@EnvironmentObject<PlayerControlsModel> private var model
|
||||||
|
|
||||||
@ -20,8 +22,9 @@ struct PlayerControls: View {
|
|||||||
@FocusState private var focusedField: Field?
|
@FocusState private var focusedField: Field?
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
init(player: PlayerModel) {
|
init(player: PlayerModel, thumbnails: ThumbnailsModel) {
|
||||||
self.player = player
|
self.player = player
|
||||||
|
self.thumbnails = thumbnails
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -86,10 +89,26 @@ struct PlayerControls: View {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
.background(PlayerGestures())
|
.background(PlayerGestures())
|
||||||
|
.background(controlsBackground)
|
||||||
#endif
|
#endif
|
||||||
.environment(\.colorScheme, .dark)
|
.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 {
|
var timeline: some View {
|
||||||
TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0)
|
TimelineView(duration: durationBinding, current: currentTimeBinding, cornerRadius: 0)
|
||||||
}
|
}
|
||||||
@ -186,9 +205,11 @@ struct PlayerControls: View {
|
|||||||
|
|
||||||
closeVideoButton
|
closeVideoButton
|
||||||
|
|
||||||
|
button("Music Mode", systemImage: "music.note", active: player.musicMode, action: player.toggleMusicMode)
|
||||||
|
.disabled(player.activeBackend == .appleAVPlayer)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
#endif
|
#endif
|
||||||
// button("Music Mode", systemImage: "music.note")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +376,7 @@ struct PlayerControls: View {
|
|||||||
systemImage: String = "arrow.up.left.and.arrow.down.right",
|
systemImage: String = "arrow.up.left.and.arrow.down.right",
|
||||||
size: Double = 30,
|
size: Double = 30,
|
||||||
cornerRadius: Double = 3,
|
cornerRadius: Double = 3,
|
||||||
|
active: Bool = false,
|
||||||
action: @escaping () -> Void = {}
|
action: @escaping () -> Void = {}
|
||||||
) -> some View {
|
) -> some View {
|
||||||
Button {
|
Button {
|
||||||
@ -367,7 +389,7 @@ struct PlayerControls: View {
|
|||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.foregroundColor(.primary)
|
.foregroundColor(active ? .accentColor : .primary)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.background(VisualEffectBlur(material: .hudWindow))
|
.background(VisualEffectBlur(material: .hudWindow))
|
||||||
@ -396,7 +418,7 @@ struct PlayerControls_Previews: PreviewProvider {
|
|||||||
let view = ZStack {
|
let view = ZStack {
|
||||||
Color.gray
|
Color.gray
|
||||||
|
|
||||||
PlayerControls(player: PlayerModel())
|
PlayerControls(player: PlayerModel(), thumbnails: ThumbnailsModel())
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
.environmentObject(model)
|
.environmentObject(model)
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ struct VideoPlayerView: View {
|
|||||||
@EnvironmentObject<AccountsModel> private var accounts
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
@EnvironmentObject<PlayerControlsModel> private var playerControls
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
@EnvironmentObject<ThumbnailsModel> private var thumbnails
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
@ -126,9 +127,7 @@ struct VideoPlayerView: View {
|
|||||||
#else
|
#else
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if player.currentItem.isNil {
|
if player.playingInPictureInPicture {
|
||||||
playerPlaceholder(geometry: geometry)
|
|
||||||
} else if player.playingInPictureInPicture {
|
|
||||||
pictureInPicturePlaceholder(geometry: geometry)
|
pictureInPicturePlaceholder(geometry: geometry)
|
||||||
} else {
|
} else {
|
||||||
playerView
|
playerView
|
||||||
@ -140,6 +139,7 @@ struct VideoPlayerView: View {
|
|||||||
fullScreen: playerControls.playingFullscreen
|
fullScreen: playerControls.playingFullscreen
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.overlay(playerPlaceholder(geometry: geometry))
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,7 +273,7 @@ struct VideoPlayerView: View {
|
|||||||
PlayerGestures()
|
PlayerGestures()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
PlayerControls(player: player)
|
PlayerControls(player: player, thumbnails: thumbnails)
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@ -298,38 +298,41 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
@ViewBuilder func playerPlaceholder(geometry: GeometryProxy) -> some View {
|
||||||
ZStack(alignment: .topLeading) {
|
if player.currentItem.isNil {
|
||||||
HStack {
|
ZStack(alignment: .topLeading) {
|
||||||
Spacer()
|
HStack {
|
||||||
VStack {
|
|
||||||
Spacer()
|
Spacer()
|
||||||
VStack(spacing: 10) {
|
VStack {
|
||||||
#if !os(tvOS)
|
Spacer()
|
||||||
Image(systemName: "ticket")
|
VStack(spacing: 10) {
|
||||||
.font(.system(size: 120))
|
#if !os(tvOS)
|
||||||
#endif
|
Image(systemName: "ticket")
|
||||||
|
.font(.system(size: 120))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.foregroundColor(.gray)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.foregroundColor(.gray)
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
Button {
|
Button {
|
||||||
player.hide()
|
player.hide()
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "xmark")
|
Image(systemName: "xmark")
|
||||||
.font(.system(size: 40))
|
.font(.system(size: 40))
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
#endif
|
#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 {
|
func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View {
|
||||||
|
@ -65,6 +65,10 @@ struct VideoCell: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if player.musicMode {
|
||||||
|
player.toggleMusicMode()
|
||||||
|
}
|
||||||
|
|
||||||
if watchingNow {
|
if watchingNow {
|
||||||
if !player.playingInPictureInPicture {
|
if !player.playingInPictureInPicture {
|
||||||
player.show()
|
player.show()
|
||||||
|
@ -57,6 +57,9 @@ struct VideoContextMenuView: View {
|
|||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
playNowInPictureInPictureButton
|
playNowInPictureInPictureButton
|
||||||
#endif
|
#endif
|
||||||
|
#if !os(tvOS)
|
||||||
|
playNowInMusicMode
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
@ -131,6 +134,10 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
private var playNowButton: some View {
|
private var playNowButton: some View {
|
||||||
Button {
|
Button {
|
||||||
|
if player.musicMode {
|
||||||
|
player.toggleMusicMode()
|
||||||
|
}
|
||||||
|
|
||||||
player.play(video)
|
player.play(video)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Play Now", systemImage: "play")
|
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 {
|
private var playNextButton: some View {
|
||||||
Button {
|
Button {
|
||||||
player.playNext(video)
|
player.playNext(video)
|
||||||
|
Loading…
Reference in New Issue
Block a user