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 #if !os(tvOS) vc.allowsPictureInPicturePlayback = true #endif 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(macOS) @Environment(\.dismiss) private var dismiss #endif var body: some View { #if os(macOS) macOSPlayer #elseif os(tvOS) tvOSPlayer #else iOSPlayer #endif } #if os(tvOS) private var tvOSPlayer: some View { Group { if let player = viewModel.player { NativePlayerView(player: player) .ignoresSafeArea() } else { ZStack { Color.black.ignoresSafeArea() ProgressView() .tint(.white) } } } .onAppear { viewModel.play(url: url, contentId: contentId, episodeId: episodeId) } .onDisappear { viewModel.stop() } } #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 }