164 lines
4.6 KiB
Swift
164 lines
4.6 KiB
Swift
import SwiftUI
|
|
import AVFoundation
|
|
import AVKit
|
|
|
|
// MARK: - 自定义 AVPlayerView 包装
|
|
|
|
#if os(macOS)
|
|
import AppKit
|
|
|
|
struct NativePlayerView: NSViewRepresentable {
|
|
let player: AVPlayer
|
|
|
|
func makeNSView(context: Context) -> AVPlayerView {
|
|
let view = AVPlayerView()
|
|
view.player = player
|
|
view.controlsStyle = .floating
|
|
view.showsFullScreenToggleButton = true
|
|
return view
|
|
}
|
|
|
|
func updateNSView(_ nsView: AVPlayerView, context: Context) {
|
|
nsView.player = player
|
|
}
|
|
}
|
|
#else
|
|
import UIKit
|
|
|
|
struct NativePlayerView: UIViewControllerRepresentable {
|
|
let player: AVPlayer
|
|
|
|
func makeUIViewController(context: Context) -> AVPlayerViewController {
|
|
let vc = AVPlayerViewController()
|
|
vc.player = player
|
|
vc.allowsPictureInPicturePlayback = true
|
|
return vc
|
|
}
|
|
|
|
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
|
|
uiViewController.player = player
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - VideoPlayerView
|
|
|
|
struct VideoPlayerView: View {
|
|
let url: String
|
|
let title: String
|
|
let episodeName: String?
|
|
let contentId: String
|
|
let episodeId: Int
|
|
|
|
@State private var viewModel = PlayerViewModel()
|
|
|
|
#if os(iOS)
|
|
@Environment(\.dismiss) private var dismiss
|
|
#endif
|
|
|
|
var body: some View {
|
|
#if os(macOS)
|
|
macOSPlayer
|
|
#else
|
|
iOSPlayer
|
|
#endif
|
|
}
|
|
|
|
#if os(iOS)
|
|
private var iOSPlayer: some View {
|
|
ZStack {
|
|
Color.black.ignoresSafeArea()
|
|
|
|
if let player = viewModel.player {
|
|
NativePlayerView(player: player)
|
|
.ignoresSafeArea()
|
|
} else {
|
|
ProgressView()
|
|
.tint(.white)
|
|
.foregroundStyle(.white)
|
|
}
|
|
|
|
VStack {
|
|
HStack {
|
|
Button {
|
|
viewModel.stop()
|
|
dismiss()
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.font(.title2)
|
|
.foregroundStyle(.white)
|
|
}
|
|
|
|
VStack(alignment: .leading) {
|
|
Text(title)
|
|
.font(.headline)
|
|
.foregroundStyle(.white)
|
|
if let episodeName {
|
|
Text(episodeName)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.white.opacity(0.8))
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Menu {
|
|
ForEach([0.5, 0.75, 1.0, 1.25, 1.5, 2.0], id: \.self) { rate in
|
|
Button {
|
|
viewModel.setRate(Float(rate))
|
|
} label: {
|
|
HStack {
|
|
Text("\(rate, specifier: "%.2g")x")
|
|
if viewModel.playbackRate == Float(rate) {
|
|
Image(systemName: "checkmark")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} label: {
|
|
Text("\(viewModel.playbackRate, specifier: "%.2g")x")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(.white.opacity(0.2), in: Capsule())
|
|
}
|
|
}
|
|
.padding()
|
|
.background(.black.opacity(0.4))
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
.statusBarHidden()
|
|
.persistentSystemOverlays(.hidden)
|
|
.onAppear {
|
|
viewModel.play(url: url, contentId: contentId, episodeId: episodeId)
|
|
}
|
|
.onDisappear {
|
|
viewModel.stop()
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if os(macOS)
|
|
private var macOSPlayer: some View {
|
|
Group {
|
|
if let player = viewModel.player {
|
|
NativePlayerView(player: player)
|
|
} else {
|
|
ProgressView()
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
.frame(minWidth: 640, minHeight: 400)
|
|
.onAppear {
|
|
viewModel.play(url: url, contentId: contentId, episodeId: episodeId)
|
|
}
|
|
.onDisappear {
|
|
viewModel.stop()
|
|
}
|
|
}
|
|
#endif
|
|
}
|