-
-
Notifications
You must be signed in to change notification settings - Fork 299
Expand file tree
/
Copy pathMpvPlayerCore.swift
More file actions
273 lines (231 loc) · 7.6 KB
/
Copy pathMpvPlayerCore.swift
File metadata and controls
273 lines (231 loc) · 7.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
import Cocoa
import Libmpv
import Metal
import QuartzCore
/// Core MPV player using Metal rendering on macOS.
class MpvPlayerCore: MpvPlayerCoreBase {
private weak var window: NSWindow?
private var playbackActivity: NSObjectProtocol?
private var isDisposed = false
func initialize(in window: NSWindow) -> Bool {
guard !isInitialized else {
print("[MpvPlayerCore] Already initialized")
return true
}
guard let contentView = window.contentView else {
print("[MpvPlayerCore] No content view")
return false
}
self.window = window
// Create and configure Metal layer for mpv rendering
let layer = MpvMetalLayer()
layer.device = MTLCreateSystemDefaultDevice()
layer.frame = contentView.bounds
if let screen = window.screen ?? NSScreen.main {
layer.contentsScale = screen.backingScaleFactor
}
layer.framebufferOnly = true
layer.isOpaque = true
layer.autoresizingMask = [.layerWidthSizable, .layerHeightSizable]
metalLayer = layer
contentView.wantsLayer = true
guard let contentLayer = contentView.layer else {
print("[MpvPlayerCore] No content layer")
metalLayer = nil
return false
}
withoutLayerAnimations {
contentLayer.insertSublayer(layer, at: 0)
layer.frame = contentView.bounds
}
guard setupMpv() else {
print("[MpvPlayerCore] Failed to setup MPV")
layer.removeFromSuperlayer()
metalLayer = nil
return false
}
let center = NotificationCenter.default
center.addObserver(
self,
selector: #selector(windowDidEnterFullScreen),
name: NSWindow.didEnterFullScreenNotification,
object: window
)
center.addObserver(
self,
selector: #selector(windowDidExitFullScreen),
name: NSWindow.didExitFullScreenNotification,
object: window
)
center.addObserver(
self,
selector: #selector(windowOcclusionDidChange),
name: NSWindow.didChangeOcclusionStateNotification,
object: window
)
isInitialized = true
print("[MpvPlayerCore] Initialized successfully with MPV")
return true
}
override func configurePlatformMpvOptions() {
guard let mpv else { return }
checkError(mpv_set_option_string(mpv, "ao", "avfoundation,coreaudio"))
}
func reattachMetalLayer() {
guard let metalLayer, let contentView = window?.contentView else { return }
contentView.wantsLayer = true
if let superlayer = contentView.layer, metalLayer.superlayer !== superlayer {
withoutLayerAnimations {
superlayer.backgroundColor = NSColor.black.cgColor
superlayer.isOpaque = true
superlayer.insertSublayer(metalLayer, at: 0)
metalLayer.frame = contentView.bounds
if let screen = window?.screen ?? NSScreen.main {
let scale = screen.backingScaleFactor
metalLayer.contentsScale = scale
metalLayer.drawableSize = CGSize(
width: contentView.bounds.width * scale,
height: contentView.bounds.height * scale
)
}
}
}
print("[MpvPlayerCore] Metal layer reattached to window")
}
func forceDraw() {
command(["seek", "0", "relative+exact"])
}
private var isVisible = false
private var pausedState = true
private var shouldRestoreOnWindowVisible = false
func setVisible(_ visible: Bool, restoreOnWindowVisible: Bool = false) {
guard metalLayer != nil, !isPipActive else { return }
if visible && isVisible && !shouldRestoreOnWindowVisible {
isBackgrounded = false
metalLayer?.isHidden = false
beginPlaybackActivity()
print("[MpvPlayerCore] setVisible(true) skipped - already visible")
return
}
isVisible = visible
shouldRestoreOnWindowVisible = !visible && restoreOnWindowVisible
isBackgrounded = !visible
if visible {
shouldRestoreOnWindowVisible = false
reattachMetalLayer()
beginPlaybackActivity()
} else {
endPlaybackActivity()
}
metalLayer?.isHidden = !visible
print("[MpvPlayerCore] setVisible(\(visible), restoreOnWindowVisible: \(restoreOnWindowVisible))")
}
func setPaused(_ paused: Bool) {
pausedState = paused
if paused {
endPlaybackActivity()
} else if isVisible {
beginPlaybackActivity()
}
}
func updateFrame(_ frame: CGRect? = nil) {
guard let metalLayer, !isPipActive else { return }
let targetFrame: CGRect
if let frame {
targetFrame = frame
} else if let contentView = window?.contentView {
targetFrame = contentView.bounds
} else {
return
}
withoutLayerAnimations {
metalLayer.frame = targetFrame
if let screen = window?.screen ?? NSScreen.main {
let scale = screen.backingScaleFactor
metalLayer.contentsScale = scale
metalLayer.drawableSize = CGSize(
width: targetFrame.width * scale,
height: targetFrame.height * scale
)
}
}
}
override func updateEDRMode(sigPeak: Double) {
guard let metalLayer else { return }
let hdrEnabled = self.hdrEnabled
var potentialHeadroom: CGFloat = 1.0
if let screen = window?.screen ?? NSScreen.main {
potentialHeadroom = screen.maximumPotentialExtendedDynamicRangeColorComponentValue
}
let shouldEnableEDR = hdrEnabled && sigPeak > 1.0 && potentialHeadroom > 1.0
withoutLayerAnimations {
metalLayer.wantsExtendedDynamicRangeContent = shouldEnableEDR
}
if shouldEnableEDR {
let peakNits = Int(potentialHeadroom * 80)
setPropertyAsync("target-peak", value: "\(peakNits)") { _ in }
} else {
setPropertyAsync("target-peak", value: "auto") { _ in }
}
print(
"[MpvPlayerCore] EDR mode: \(shouldEnableEDR) (hdrEnabled: \(hdrEnabled), sigPeak: \(sigPeak), potentialHeadroom: \(potentialHeadroom))"
)
}
func dispose() {
if isDisposed { return }
isDisposed = true
endPlaybackActivity()
NotificationCenter.default.removeObserver(self)
disposeSharedState(destroySynchronously: false)
metalLayer?.removeFromSuperlayer()
metalLayer = nil
isInitialized = false
print("[MpvPlayerCore] Disposed")
}
deinit {
dispose()
}
@objc private func windowDidEnterFullScreen(_ notification: Notification) {
guard !isPipActive else { return }
updateFrame()
}
@objc private func windowDidExitFullScreen(_ notification: Notification) {
guard !isPipActive else { return }
updateFrame()
}
@objc private func windowOcclusionDidChange(_ notification: Notification) {
guard mpv != nil, !isPipActive else { return }
let windowVisible = window?.occlusionState.contains(.visible) ?? true
if !windowVisible {
print("[MpvPlayerCore] Window occluded")
isBackgrounded = true
endPlaybackActivity()
} else {
print("[MpvPlayerCore] Window visible")
isBackgrounded = false
if !pausedState {
beginPlaybackActivity()
}
}
}
private func beginPlaybackActivity() {
guard playbackActivity == nil else { return }
playbackActivity = ProcessInfo.processInfo.beginActivity(
options: [.userInitiated, .latencyCritical],
reason: "Video playback"
)
print("[MpvPlayerCore] Began playback activity assertion")
}
private func endPlaybackActivity() {
guard let playbackActivity else { return }
ProcessInfo.processInfo.endActivity(playbackActivity)
self.playbackActivity = nil
print("[MpvPlayerCore] Ended playback activity assertion")
}
private func withoutLayerAnimations(_ updates: () -> Void) {
CATransaction.begin()
CATransaction.setDisableActions(true)
updates()
CATransaction.commit()
}
}