This commit is contained in:
JinJiangHuang
2026-03-01 20:34:43 +08:00
parent 3f302f83b3
commit 54de47deb1
10 changed files with 487 additions and 211 deletions
+105 -18
View File
@@ -40,12 +40,34 @@ private struct MacOSPlayerView: NSViewRepresentable {
}
static func dismantleNSView(_ nsView: AVPlayerView, coordinator: ()) {
nsView.player?.pause()
nsView.player = nil
}
}
#endif
/// AVPlayer
@MainActor
final class SystemPlayerSessionController: ObservableObject {
fileprivate var player: AVPlayer?
fileprivate var mediaURLString: String?
func setPlayer(_ newPlayer: AVPlayer, urlString: String) {
if player !== newPlayer {
player?.pause()
player?.replaceCurrentItem(with: nil)
}
player = newPlayer
mediaURLString = urlString
}
func stop() {
player?.pause()
player?.replaceCurrentItem(with: nil)
player = nil
mediaURLString = nil
}
}
/// - Android PlayFragment
struct PlayerView: View {
let urlString: String
@@ -55,11 +77,22 @@ struct PlayerView: View {
var onToggleFullScreen: (() -> Void)? = nil
var canPlayNext: Bool = false
var onPlayNext: (() -> Void)? = nil
var systemController: SystemPlayerSessionController? = nil
var vlcController: VLCPlayerController? = nil
@AppStorage(HawkConfig.PLAY_TYPE) private var playTypeRaw = PlayerEngine.system.rawValue
@AppStorage(HawkConfig.PLAY_TYPE_VOD) private var vodPlayTypeRaw = -1
@AppStorage(HawkConfig.PLAY_TYPE) private var legacyPlayTypeRaw = PlayerEngine.system.rawValue
private var selectedEngine: PlayerEngine {
PlayerEngine.fromStoredValue(playTypeRaw)
let defaults = UserDefaults.standard
let rawValue: Int
if defaults.object(forKey: HawkConfig.PLAY_TYPE_VOD) != nil {
rawValue = vodPlayTypeRaw
} else if defaults.object(forKey: HawkConfig.PLAY_TYPE) != nil {
rawValue = legacyPlayTypeRaw
} else {
rawValue = PlayerEngine.system.rawValue
}
return PlayerEngine.fromStoredValue(rawValue)
}
var body: some View {
@@ -73,7 +106,8 @@ struct PlayerView: View {
onPlaybackEnded: onPlaybackEnded,
onToggleFullScreen: onToggleFullScreen,
canPlayNext: canPlayNext,
onPlayNext: onPlayNext
onPlayNext: onPlayNext,
sharedController: systemController
)
case .vlc:
VLCVodPlayerView(
@@ -89,6 +123,22 @@ struct PlayerView: View {
}
}
.id(selectedEngine.rawValue)
.onAppear {
if selectedEngine != .system {
systemController?.stop()
}
if selectedEngine != .vlc {
vlcController?.stop()
}
}
.onChange(of: selectedEngine) { _, newValue in
if newValue != .system {
systemController?.stop()
}
if newValue != .vlc {
vlcController?.stop()
}
}
}
}
@@ -101,6 +151,7 @@ struct AVPlayerContentView: View {
var onToggleFullScreen: (() -> Void)? = nil
var canPlayNext: Bool = false
var onPlayNext: (() -> Void)? = nil
var sharedController: SystemPlayerSessionController? = nil
@State private var player: AVPlayer?
@State private var playbackEndObserver: NSObjectProtocol?
@State private var timeObserverToken: Any?
@@ -195,7 +246,7 @@ struct AVPlayerContentView: View {
wakeUpControls()
}
.onDisappear {
cleanupPlayer()
cleanupPlayer(keepSharedPlayer: sharedController != nil)
controlsTimer?.invalidate()
osdTimer?.invalidate()
}
@@ -203,40 +254,60 @@ struct AVPlayerContentView: View {
private func setupPlayer() {
guard let url = URL(string: urlString) else { return }
let targetURLString = url.absoluteString
if let sharedController,
sharedController.mediaURLString == targetURLString,
let sharedPlayer = sharedController.player {
cleanupPlayer(keepSharedPlayer: true)
player = sharedPlayer
bindPlayerObservers(for: sharedPlayer)
reportProgress(for: sharedPlayer)
return
}
//
cleanupPlayer()
let playerItem = AVPlayerItem(url: url)
let newPlayer = AVPlayer(playerItem: playerItem)
if let sharedController {
sharedController.setPlayer(newPlayer, urlString: targetURLString)
}
//
player = newPlayer
bindPlayerObservers(for: newPlayer)
startPlayback(for: newPlayer)
}
private func bindPlayerObservers(for player: AVPlayer) {
playerObservers = [
newPlayer.observe(\.timeControlStatus, options: [.new]) { p, _ in
player.observe(\.timeControlStatus, options: [.new]) { p, _ in
let status = p.timeControlStatus
DispatchQueue.main.async { isPlaying = status == .playing }
},
newPlayer.observe(\.reasonForWaitingToPlay, options: [.new]) { p, _ in
player.observe(\.reasonForWaitingToPlay, options: [.new]) { p, _ in
let reason = p.reasonForWaitingToPlay
DispatchQueue.main.async { isPreparing = reason != nil }
},
newPlayer.observe(\.volume, options: [.new]) { p, _ in
player.observe(\.volume, options: [.new]) { p, _ in
let vol = Double(p.volume)
DispatchQueue.main.async { volume = vol }
},
newPlayer.observe(\.rate, options: [.new]) { p, _ in
player.observe(\.rate, options: [.new]) { p, _ in
let r = p.rate
DispatchQueue.main.async { rate = r }
}
]
observePlaybackProgress(for: newPlayer)
observePlaybackEnd(for: newPlayer)
player = newPlayer
startPlayback(for: newPlayer)
observePlaybackProgress(for: player)
observePlaybackEnd(for: player)
isPlaying = player.timeControlStatus == .playing
isPreparing = player.reasonForWaitingToPlay != nil
volume = Double(player.volume)
rate = player.rate
}
private func cleanupPlayer() {
private func detachPlayerObservers() {
if let token = timeObserverToken {
player?.removeTimeObserver(token)
timeObserverToken = nil
@@ -247,8 +318,24 @@ struct AVPlayerContentView: View {
}
playerObservers.forEach { $0.invalidate() }
playerObservers.removeAll()
player?.pause()
player?.replaceCurrentItem(with: nil)
}
private func cleanupPlayer(keepSharedPlayer: Bool = false) {
let currentPlayer = player
detachPlayerObservers()
guard let currentPlayer else { return }
if keepSharedPlayer, sharedController?.player === currentPlayer {
player = nil
return
}
currentPlayer.pause()
currentPlayer.replaceCurrentItem(with: nil)
if sharedController?.player === currentPlayer {
sharedController?.player = nil
sharedController?.mediaURLString = nil
}
player = nil
}
+15 -64
View File
@@ -14,6 +14,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
private static let maxVolume = 200
private static let drawableSizeChangeThreshold: CGFloat = 24
private static let drawableRebindMinimumInterval: TimeInterval = 1.2
// libvlc /
private static let stablePlaybackOptions: [String] = [
"--no-drop-late-frames",
"--no-skip-frames",
"--clock-synchro=0",
"--clock-jitter=0"
]
private static let playerInstanceSelector = NSSelectorFromString("playerInstance")
private static let libVLCStopAsync: LibVLCStopAsyncFunction? = {
// RTLD_DEFAULT Swift -2 C RTLD_DEFAULT
@@ -25,7 +32,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
}()
private typealias LibVLCStopAsyncFunction = @convention(c) (UnsafeMutableRawPointer?) -> Void
let mediaPlayer = VLCMediaPlayer()
let mediaPlayer = VLCMediaPlayer(options: VLCPlayerController.stablePlaybackOptions)
@Published var isPreparing = true
@Published var isPlaying = false
@Published var currentTimeSeconds: Double = 0
@@ -60,10 +67,6 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
private var bufferMode: VLCBufferMode = .defaultMode
private var bufferingFallbackWorkItem: DispatchWorkItem?
private var delayedPreparingWorkItem: DispatchWorkItem?
private var bufferingEventCountVod = 0
private var lastVodBufferingCountedAt: Date = .distantPast
private var hasAttemptedVodCacheBoost = false
private var useVodCacheBoost = false
private var currentMediaURLString: String?
private var currentMediaIsLive = false
private var currentMediaDecodeMode: VideoDecodeMode = .auto
@@ -73,16 +76,10 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
private var onPlaybackFailed: (() -> Void)?
private let progressUpdateIntervalVod: TimeInterval = 0.5
private let progressUpdateIntervalLive: TimeInterval = 1.0
private let bufferingFallbackThresholdVod: TimeInterval = 6.0
private let bufferingFallbackThresholdLive: TimeInterval = 4.0
private let bufferingConfirmDelayVod: TimeInterval = 0.35
private let bufferingIndicatorDelayVod: TimeInterval = 1.2
private let vodBufferingEventDebounceInterval: TimeInterval = 1.5
private let vodBufferingProgressAdvanceThreshold: Double = 0.25
private let vodCacheBoostTriggerCount = 2
private let vodCacheBoostExtraNetwork = 4500
private let vodCacheBoostExtraLive = 3500
private let vodCacheBoostExtraFile = 5500
private let progressPublishThreshold: Double = 0.25
private let durationPublishThreshold: Double = 0.5
private var lastNonZeroVolume = defaultVolume
@@ -163,25 +160,18 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
mediaPlayer.stop()
let media = VLCMedia(url: url)
var cacheConfig = Self.cacheConfig(isLive: isLive, bufferMode: bufferMode)
if !isLive && useVodCacheBoost {
cacheConfig.network += vodCacheBoostExtraNetwork
cacheConfig.live += vodCacheBoostExtraLive
cacheConfig.file += vodCacheBoostExtraFile
}
let cacheConfig = Self.cacheConfig(isLive: isLive, bufferMode: bufferMode)
let enableFrameDrop = isLive ? bufferMode.enableFrameDrop : false
let enableSkipFrames = isLive && bufferMode.enableFrameDrop
var mediaOptions: [String: Any] = [
"network-caching": cacheConfig.network,
"live-caching": cacheConfig.live,
"file-caching": cacheConfig.file,
"drop-late-frames": enableFrameDrop ? 1 : 0,
"skip-frames": enableSkipFrames ? 1 : 0,
"http-reconnect": 1
]
if !isLive {
mediaOptions["avcodec-hurry-up"] = 0
mediaOptions["clock-jitter"] = 5000000
mediaOptions["clock-synchro"] = 0
}
if let hwOption = decodeMode.vlcHardwareDecodeOption {
@@ -195,6 +185,9 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
}
media.addOptions(mediaOptions)
// 使 no- 0/1 libvlc
media.addOption(enableFrameDrop ? "drop-late-frames" : "no-drop-late-frames")
media.addOption(enableSkipFrames ? "skip-frames" : "no-skip-frames")
mediaPlayer.media = media
mediaPlayer.play()
@@ -624,13 +617,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
}
private func scheduleBufferingFallbackIfNeeded() {
guard isLive else { return }
guard bufferingFallbackWorkItem == nil else { return }
guard !hasAttemptedSoftDecodeFallback else { return }
if !isLive && !hasAttemptedVodCacheBoost { return }
guard decodeMode != .software else { return }
guard mediaPlayer.media != nil else { return }
let delay = isLive ? bufferingFallbackThresholdLive : bufferingFallbackThresholdVod
let delay = bufferingFallbackThresholdLive
let workItem = DispatchWorkItem { [weak self] in
Task { @MainActor [weak self] in
self?.attemptSoftDecodeFallbackIfNeeded()
@@ -680,7 +673,6 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
isPlaying = false
}
scheduleDelayedPreparingIndicatorForVod()
scheduleBufferingFallbackIfNeeded()
return
}
@@ -743,24 +735,11 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
guard !mediaPlayer.isPlaying else { return }
if !isInBufferingState {
isInBufferingState = true
markVodBufferingEventIfNeeded()
if shouldAttemptVodCacheBoost() {
attemptVodCacheBoostIfNeeded()
return
}
}
if isPlaying {
isPlaying = false
}
scheduleDelayedPreparingIndicatorForVod()
scheduleBufferingFallbackIfNeeded()
}
private func markVodBufferingEventIfNeeded() {
let now = Date()
guard now.timeIntervalSince(lastVodBufferingCountedAt) >= vodBufferingEventDebounceInterval else { return }
lastVodBufferingCountedAt = now
bufferingEventCountVod += 1
}
private func isStillStalledSinceBufferingStartedVod() -> Bool {
@@ -777,30 +756,6 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
}
}
private func shouldAttemptVodCacheBoost() -> Bool {
!isLive && !hasAttemptedVodCacheBoost && bufferingEventCountVod >= vodCacheBoostTriggerCount
}
private func attemptVodCacheBoostIfNeeded() {
guard !isLive else { return }
guard !hasAttemptedVodCacheBoost else { return }
guard let urlString = currentMediaURLString, let url = URL(string: urlString) else { return }
guard mediaPlayer.media != nil else { return }
hasAttemptedVodCacheBoost = true
useVodCacheBoost = true
cancelDelayedPreparingIndicator()
cancelBufferingFallbackTimer()
let resumePosition = max(currentSeconds(), 0)
play(
url: url,
startPosition: resumePosition,
isLive: false,
onProgressChanged: onProgressChanged,
onPlaybackEnded: onPlaybackEnded,
onPlaybackFailed: onPlaybackFailed
)
}
private func resetPlaybackRecoveryState() {
cancelBufferingFallbackTimer()
cancelDelayedPreparingIndicator()
@@ -809,10 +764,6 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
decodeModeOverride = nil
isInBufferingState = false
bufferingBaselineSecondsVod = 0
bufferingEventCountVod = 0
lastVodBufferingCountedAt = .distantPast
hasAttemptedVodCacheBoost = false
useVodCacheBoost = false
}
private func setPlaybackStatus(preparing: Bool, playing: Bool) {