expo-mpv 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +5 -0
- package/README.md +33 -0
- package/android/build.gradle +18 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/mpv/ExpoMpvModule.kt +50 -0
- package/android/src/main/java/expo/modules/mpv/ExpoMpvView.kt +30 -0
- package/build/ExpoMpv.types.d.ts +128 -0
- package/build/ExpoMpv.types.d.ts.map +1 -0
- package/build/ExpoMpv.types.js +2 -0
- package/build/ExpoMpv.types.js.map +1 -0
- package/build/ExpoMpvModule.d.ts +7 -0
- package/build/ExpoMpvModule.d.ts.map +1 -0
- package/build/ExpoMpvModule.js +4 -0
- package/build/ExpoMpvModule.js.map +1 -0
- package/build/ExpoMpvView.d.ts +5 -0
- package/build/ExpoMpvView.d.ts.map +1 -0
- package/build/ExpoMpvView.js +25 -0
- package/build/ExpoMpvView.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +6 -0
- package/build/index.js.map +1 -0
- package/bun.lock +2142 -0
- package/expo-module.config.json +6 -0
- package/ios/ExpoMpv.podspec +56 -0
- package/ios/ExpoMpvModule.swift +105 -0
- package/ios/ExpoMpvView.swift +589 -0
- package/ios/download-mpvkit.sh +85 -0
- package/package.json +43 -0
- package/src/ExpoMpv.types.ts +145 -0
- package/src/ExpoMpvModule.ts +8 -0
- package/src/ExpoMpvView.tsx +33 -0
- package/src/index.ts +5 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Libmpv
|
|
3
|
+
|
|
4
|
+
class ExpoMpvView: ExpoView {
|
|
5
|
+
// MARK: - Metal Layer
|
|
6
|
+
|
|
7
|
+
private let metalLayer = MetalLayer()
|
|
8
|
+
|
|
9
|
+
// MARK: - MPV
|
|
10
|
+
|
|
11
|
+
private var mpv: OpaquePointer?
|
|
12
|
+
private lazy var queue = DispatchQueue(label: "com.expo.mpv.event", qos: .userInitiated)
|
|
13
|
+
|
|
14
|
+
// MARK: - State
|
|
15
|
+
|
|
16
|
+
private var isInitialized = false
|
|
17
|
+
private var pendingSource: String?
|
|
18
|
+
private var progressTimer: Timer?
|
|
19
|
+
|
|
20
|
+
// MARK: - Event Dispatchers
|
|
21
|
+
|
|
22
|
+
let onPlaybackStateChange = EventDispatcher()
|
|
23
|
+
let onProgress = EventDispatcher()
|
|
24
|
+
let onLoad = EventDispatcher()
|
|
25
|
+
let onError = EventDispatcher()
|
|
26
|
+
let onEnd = EventDispatcher()
|
|
27
|
+
let onBuffer = EventDispatcher()
|
|
28
|
+
let onSeek = EventDispatcher()
|
|
29
|
+
let onVolumeChange = EventDispatcher()
|
|
30
|
+
|
|
31
|
+
// MARK: - Init
|
|
32
|
+
|
|
33
|
+
required init(appContext: AppContext? = nil) {
|
|
34
|
+
super.init(appContext: appContext)
|
|
35
|
+
clipsToBounds = true
|
|
36
|
+
backgroundColor = .black
|
|
37
|
+
setupMetalLayer()
|
|
38
|
+
setupMpv()
|
|
39
|
+
setupNotifications()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
deinit {
|
|
43
|
+
stopProgressTimer()
|
|
44
|
+
NotificationCenter.default.removeObserver(self)
|
|
45
|
+
destroy()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: - Layout
|
|
49
|
+
|
|
50
|
+
override func layoutSubviews() {
|
|
51
|
+
super.layoutSubviews()
|
|
52
|
+
metalLayer.frame = bounds
|
|
53
|
+
let scale = window?.screen.nativeScale ?? UIScreen.main.nativeScale
|
|
54
|
+
let w = bounds.width * scale
|
|
55
|
+
let h = bounds.height * scale
|
|
56
|
+
if w > 1 && h > 1 {
|
|
57
|
+
metalLayer.drawableSize = CGSize(width: w, height: h)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// MARK: - Metal Layer Setup
|
|
62
|
+
|
|
63
|
+
private func setupMetalLayer() {
|
|
64
|
+
metalLayer.contentsScale = UIScreen.main.nativeScale
|
|
65
|
+
metalLayer.framebufferOnly = true
|
|
66
|
+
metalLayer.backgroundColor = UIColor.black.cgColor
|
|
67
|
+
metalLayer.pixelFormat = .bgra8Unorm
|
|
68
|
+
metalLayer.device = MTLCreateSystemDefaultDevice()
|
|
69
|
+
if metalLayer.device == nil {
|
|
70
|
+
log("WARNING: MTLCreateSystemDefaultDevice() returned nil — Metal not available")
|
|
71
|
+
}
|
|
72
|
+
layer.addSublayer(metalLayer)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// MARK: - MPV Setup
|
|
76
|
+
|
|
77
|
+
private func setupMpv() {
|
|
78
|
+
log("Creating mpv instance...")
|
|
79
|
+
|
|
80
|
+
mpv = mpv_create()
|
|
81
|
+
guard let mpv = mpv else {
|
|
82
|
+
let msg = "Failed to create mpv instance"
|
|
83
|
+
log("ERROR: \(msg)")
|
|
84
|
+
DispatchQueue.main.async { self.onError(["error": msg]) }
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Enable mpv log messages so we can see what's happening
|
|
89
|
+
checkError(mpv_request_log_messages(mpv, "v"), label: "request_log_messages")
|
|
90
|
+
|
|
91
|
+
// Pass the CAMetalLayer as the wid (window ID)
|
|
92
|
+
// mpv expects the raw pointer value as an Int64
|
|
93
|
+
var wid = unsafeBitCast(metalLayer, to: Int64.self)
|
|
94
|
+
checkError(mpv_set_option(mpv, "wid", MPV_FORMAT_INT64, &wid), label: "set wid")
|
|
95
|
+
|
|
96
|
+
// Rendering configuration: gpu-next + vulkan + moltenvk → Metal
|
|
97
|
+
setOptionString("vo", "gpu-next")
|
|
98
|
+
setOptionString("gpu-api", "vulkan")
|
|
99
|
+
setOptionString("gpu-context", "moltenvk")
|
|
100
|
+
setOptionString("hwdec", "videotoolbox-copy")
|
|
101
|
+
|
|
102
|
+
// General options
|
|
103
|
+
setOptionString("keep-open", "yes")
|
|
104
|
+
setOptionString("idle", "yes")
|
|
105
|
+
setOptionString("input-default-bindings", "no")
|
|
106
|
+
setOptionString("input-vo-keyboard", "no")
|
|
107
|
+
|
|
108
|
+
// Initialize mpv
|
|
109
|
+
log("Initializing mpv...")
|
|
110
|
+
let initResult = mpv_initialize(mpv)
|
|
111
|
+
guard initResult == 0 else {
|
|
112
|
+
let errStr = String(cString: mpv_error_string(initResult))
|
|
113
|
+
let msg = "mpv_initialize failed: \(errStr) (\(initResult))"
|
|
114
|
+
log("ERROR: \(msg)")
|
|
115
|
+
DispatchQueue.main.async { self.onError(["error": msg]) }
|
|
116
|
+
mpv_destroy(mpv)
|
|
117
|
+
self.mpv = nil
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isInitialized = true
|
|
122
|
+
log("mpv initialized successfully")
|
|
123
|
+
|
|
124
|
+
// Observe properties
|
|
125
|
+
mpv_observe_property(mpv, 0, "pause", MPV_FORMAT_FLAG)
|
|
126
|
+
mpv_observe_property(mpv, 1, "duration", MPV_FORMAT_DOUBLE)
|
|
127
|
+
mpv_observe_property(mpv, 2, "time-pos", MPV_FORMAT_DOUBLE)
|
|
128
|
+
mpv_observe_property(mpv, 3, "paused-for-cache", MPV_FORMAT_FLAG)
|
|
129
|
+
mpv_observe_property(mpv, 4, "eof-reached", MPV_FORMAT_FLAG)
|
|
130
|
+
mpv_observe_property(mpv, 5, "volume", MPV_FORMAT_DOUBLE)
|
|
131
|
+
mpv_observe_property(mpv, 6, "mute", MPV_FORMAT_FLAG)
|
|
132
|
+
mpv_observe_property(mpv, 7, "speed", MPV_FORMAT_DOUBLE)
|
|
133
|
+
mpv_observe_property(mpv, 8, "demuxer-cache-duration", MPV_FORMAT_DOUBLE)
|
|
134
|
+
mpv_observe_property(mpv, 9, "video-params/w", MPV_FORMAT_INT64)
|
|
135
|
+
mpv_observe_property(mpv, 10, "video-params/h", MPV_FORMAT_INT64)
|
|
136
|
+
|
|
137
|
+
// Set wakeup callback for the event loop
|
|
138
|
+
let rawSelf = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
|
139
|
+
mpv_set_wakeup_callback(mpv, { ctx in
|
|
140
|
+
guard let ctx = ctx else { return }
|
|
141
|
+
let view = Unmanaged<ExpoMpvView>.fromOpaque(ctx).takeUnretainedValue()
|
|
142
|
+
view.readEvents()
|
|
143
|
+
}, rawSelf)
|
|
144
|
+
|
|
145
|
+
// Load pending source if any
|
|
146
|
+
if let source = pendingSource {
|
|
147
|
+
pendingSource = nil
|
|
148
|
+
loadFile(source)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// MARK: - App Lifecycle
|
|
153
|
+
|
|
154
|
+
private func setupNotifications() {
|
|
155
|
+
NotificationCenter.default.addObserver(
|
|
156
|
+
self,
|
|
157
|
+
selector: #selector(appDidEnterBackground),
|
|
158
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
159
|
+
object: nil
|
|
160
|
+
)
|
|
161
|
+
NotificationCenter.default.addObserver(
|
|
162
|
+
self,
|
|
163
|
+
selector: #selector(appWillEnterForeground),
|
|
164
|
+
name: UIApplication.willEnterForegroundNotification,
|
|
165
|
+
object: nil
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@objc private func appDidEnterBackground() {
|
|
170
|
+
guard mpv != nil else { return }
|
|
171
|
+
mpv_set_property_string(mpv, "vid", "no")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@objc private func appWillEnterForeground() {
|
|
175
|
+
guard mpv != nil else { return }
|
|
176
|
+
mpv_set_property_string(mpv, "vid", "auto")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// MARK: - Progress Timer
|
|
180
|
+
|
|
181
|
+
private func startProgressTimer() {
|
|
182
|
+
stopProgressTimer()
|
|
183
|
+
DispatchQueue.main.async { [weak self] in
|
|
184
|
+
self?.progressTimer = Timer.scheduledTimer(withTimeInterval: 0.25, repeats: true) { [weak self] _ in
|
|
185
|
+
self?.emitProgressEvent()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private func stopProgressTimer() {
|
|
191
|
+
progressTimer?.invalidate()
|
|
192
|
+
progressTimer = nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func emitProgressEvent() {
|
|
196
|
+
guard mpv != nil else { return }
|
|
197
|
+
let position = getDouble("time-pos")
|
|
198
|
+
let duration = getDouble("duration")
|
|
199
|
+
let cachedDuration = getDouble("demuxer-cache-duration")
|
|
200
|
+
|
|
201
|
+
guard position.isFinite && duration.isFinite else { return }
|
|
202
|
+
|
|
203
|
+
onProgress([
|
|
204
|
+
"position": position,
|
|
205
|
+
"duration": duration,
|
|
206
|
+
"bufferedDuration": cachedDuration.isFinite ? cachedDuration : 0,
|
|
207
|
+
])
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// MARK: - Event Loop
|
|
211
|
+
|
|
212
|
+
private func readEvents() {
|
|
213
|
+
queue.async { [weak self] in
|
|
214
|
+
guard let self = self, self.mpv != nil else { return }
|
|
215
|
+
|
|
216
|
+
while true {
|
|
217
|
+
let event = mpv_wait_event(self.mpv, 0)
|
|
218
|
+
guard let event = event else { break }
|
|
219
|
+
|
|
220
|
+
if event.pointee.event_id == MPV_EVENT_NONE {
|
|
221
|
+
break
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
switch event.pointee.event_id {
|
|
225
|
+
case MPV_EVENT_PROPERTY_CHANGE:
|
|
226
|
+
self.handlePropertyChange(event)
|
|
227
|
+
|
|
228
|
+
case MPV_EVENT_LOG_MESSAGE:
|
|
229
|
+
if let data = event.pointee.data {
|
|
230
|
+
let msg = data.assumingMemoryBound(to: mpv_event_log_message.self).pointee
|
|
231
|
+
if let text = msg.text {
|
|
232
|
+
let logText = String(cString: text).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
233
|
+
let prefix = msg.prefix.map { String(cString: $0) } ?? "?"
|
|
234
|
+
let level = msg.level.map { String(cString: $0) } ?? "?"
|
|
235
|
+
self.log("[\(prefix)] [\(level)] \(logText)")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case MPV_EVENT_FILE_LOADED:
|
|
240
|
+
self.log("EVENT: file-loaded")
|
|
241
|
+
DispatchQueue.main.async {
|
|
242
|
+
let duration = self.getDouble("duration")
|
|
243
|
+
let width = self.getInt("video-params/w")
|
|
244
|
+
let height = self.getInt("video-params/h")
|
|
245
|
+
self.log("Media loaded: duration=\(duration) size=\(width)x\(height)")
|
|
246
|
+
self.onLoad([
|
|
247
|
+
"duration": duration.isFinite ? duration : 0,
|
|
248
|
+
"width": width,
|
|
249
|
+
"height": height,
|
|
250
|
+
])
|
|
251
|
+
self.startProgressTimer()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case MPV_EVENT_START_FILE:
|
|
255
|
+
self.log("EVENT: start-file")
|
|
256
|
+
|
|
257
|
+
case MPV_EVENT_END_FILE:
|
|
258
|
+
if let data = event.pointee.data {
|
|
259
|
+
let endFile = data.assumingMemoryBound(to: mpv_event_end_file.self).pointee
|
|
260
|
+
self.log("EVENT: end-file reason=\(endFile.reason) error=\(endFile.error)")
|
|
261
|
+
DispatchQueue.main.async {
|
|
262
|
+
self.stopProgressTimer()
|
|
263
|
+
let reason: String
|
|
264
|
+
switch endFile.reason {
|
|
265
|
+
case MPV_END_FILE_REASON_EOF:
|
|
266
|
+
reason = "ended"
|
|
267
|
+
case MPV_END_FILE_REASON_ERROR:
|
|
268
|
+
reason = "error"
|
|
269
|
+
let errStr = String(cString: mpv_error_string(endFile.error))
|
|
270
|
+
let msg = "Playback error: \(errStr) (code \(endFile.error))"
|
|
271
|
+
self.log("ERROR: \(msg)")
|
|
272
|
+
self.onError(["error": msg])
|
|
273
|
+
case MPV_END_FILE_REASON_STOP:
|
|
274
|
+
reason = "stopped"
|
|
275
|
+
default:
|
|
276
|
+
reason = "unknown"
|
|
277
|
+
}
|
|
278
|
+
self.onEnd(["reason": reason])
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
case MPV_EVENT_SHUTDOWN:
|
|
283
|
+
self.log("EVENT: shutdown")
|
|
284
|
+
self.mpv = nil
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
case MPV_EVENT_SEEK:
|
|
288
|
+
DispatchQueue.main.async {
|
|
289
|
+
self.onSeek([:])
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
default:
|
|
293
|
+
let eventName = mpv_event_name(event.pointee.event_id)
|
|
294
|
+
if let eventName = eventName {
|
|
295
|
+
self.log("EVENT: \(String(cString: eventName))")
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func handlePropertyChange(_ event: UnsafePointer<mpv_event>) {
|
|
303
|
+
guard let data = event.pointee.data else { return }
|
|
304
|
+
let prop = data.assumingMemoryBound(to: mpv_event_property.self).pointee
|
|
305
|
+
|
|
306
|
+
guard let cName = prop.name else { return }
|
|
307
|
+
let name = String(cString: cName)
|
|
308
|
+
|
|
309
|
+
switch name {
|
|
310
|
+
case "pause":
|
|
311
|
+
if prop.format == MPV_FORMAT_FLAG, let flagPtr = prop.data {
|
|
312
|
+
let paused = flagPtr.assumingMemoryBound(to: Int32.self).pointee != 0
|
|
313
|
+
DispatchQueue.main.async {
|
|
314
|
+
self.onPlaybackStateChange([
|
|
315
|
+
"state": paused ? "paused" : "playing",
|
|
316
|
+
"isPlaying": !paused,
|
|
317
|
+
])
|
|
318
|
+
if paused {
|
|
319
|
+
self.stopProgressTimer()
|
|
320
|
+
} else {
|
|
321
|
+
self.startProgressTimer()
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case "paused-for-cache":
|
|
327
|
+
if prop.format == MPV_FORMAT_FLAG, let flagPtr = prop.data {
|
|
328
|
+
let buffering = flagPtr.assumingMemoryBound(to: Int32.self).pointee != 0
|
|
329
|
+
DispatchQueue.main.async {
|
|
330
|
+
self.onBuffer(["isBuffering": buffering])
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
case "volume":
|
|
335
|
+
if prop.format == MPV_FORMAT_DOUBLE, let dataPtr = prop.data {
|
|
336
|
+
let volume = dataPtr.assumingMemoryBound(to: Double.self).pointee
|
|
337
|
+
DispatchQueue.main.async {
|
|
338
|
+
self.onVolumeChange(["volume": volume, "muted": self.getFlag("mute")])
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
case "mute":
|
|
343
|
+
if prop.format == MPV_FORMAT_FLAG, let flagPtr = prop.data {
|
|
344
|
+
let muted = flagPtr.assumingMemoryBound(to: Int32.self).pointee != 0
|
|
345
|
+
DispatchQueue.main.async {
|
|
346
|
+
self.onVolumeChange(["volume": self.getDouble("volume"), "muted": muted])
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
default:
|
|
351
|
+
break
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// MARK: - Public API
|
|
356
|
+
|
|
357
|
+
func loadFile(_ url: String) {
|
|
358
|
+
guard isInitialized, mpv != nil else {
|
|
359
|
+
log("loadFile deferred (not initialized yet): \(url)")
|
|
360
|
+
pendingSource = url
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
log("loadFile: \(url)")
|
|
364
|
+
commandAsync("loadfile", args: [url, "replace"])
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
func play() {
|
|
368
|
+
setFlag("pause", false)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
func pause() {
|
|
372
|
+
setFlag("pause", true)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
func togglePlay() {
|
|
376
|
+
let isPaused = getFlag("pause")
|
|
377
|
+
setFlag("pause", !isPaused)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
func stop() {
|
|
381
|
+
commandAsync("stop")
|
|
382
|
+
stopProgressTimer()
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func seekTo(_ position: Double) {
|
|
386
|
+
commandAsync("seek", args: [String(position), "absolute"])
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
func seekBy(_ offset: Double) {
|
|
390
|
+
commandAsync("seek", args: [String(offset), "relative"])
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
func setSpeed(_ speed: Double) {
|
|
394
|
+
setDouble("speed", speed)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
func setVolume(_ volume: Double) {
|
|
398
|
+
setDouble("volume", volume)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
func setMuted(_ muted: Bool) {
|
|
402
|
+
setFlag("mute", muted)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
func setLooping(_ loop: Bool) {
|
|
406
|
+
guard mpv != nil else { return }
|
|
407
|
+
mpv_set_property_string(mpv, "loop-file", loop ? "inf" : "no")
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
func setSubtitleTrack(_ trackId: Int) {
|
|
411
|
+
setInt("sid", Int64(trackId))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
func setAudioTrack(_ trackId: Int) {
|
|
415
|
+
setInt("aid", Int64(trackId))
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
func getPlaybackInfo() -> [String: Any] {
|
|
419
|
+
let position = getDouble("time-pos")
|
|
420
|
+
let duration = getDouble("duration")
|
|
421
|
+
let isPaused = getFlag("pause")
|
|
422
|
+
let speed = getDouble("speed")
|
|
423
|
+
let volume = getDouble("volume")
|
|
424
|
+
let muted = getFlag("mute")
|
|
425
|
+
|
|
426
|
+
return [
|
|
427
|
+
"position": position.isFinite ? position : 0,
|
|
428
|
+
"duration": duration.isFinite ? duration : 0,
|
|
429
|
+
"isPlaying": !isPaused,
|
|
430
|
+
"speed": speed,
|
|
431
|
+
"volume": volume,
|
|
432
|
+
"muted": muted,
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
func destroy() {
|
|
437
|
+
stopProgressTimer()
|
|
438
|
+
if let mpv = mpv {
|
|
439
|
+
log("Destroying mpv...")
|
|
440
|
+
mpv_set_wakeup_callback(mpv, nil, nil)
|
|
441
|
+
mpv_terminate_destroy(mpv)
|
|
442
|
+
self.mpv = nil
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// MARK: - MPV Helpers
|
|
447
|
+
|
|
448
|
+
private func commandAsync(_ command: String, args: [String] = []) {
|
|
449
|
+
guard mpv != nil else {
|
|
450
|
+
log("commandAsync ignored (mpv is nil): \(command)")
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Build null-terminated args string for mpv_command_string is simplest,
|
|
455
|
+
// but mpv_command_string doesn't exist. Use mpv_command with proper memory management.
|
|
456
|
+
let allArgs = [command] + args
|
|
457
|
+
// Create C strings that live long enough
|
|
458
|
+
var cStrings = allArgs.map { strdup($0) }
|
|
459
|
+
cStrings.append(nil) // null-terminate
|
|
460
|
+
|
|
461
|
+
// Create array of const pointers
|
|
462
|
+
let result = cStrings.withUnsafeMutableBufferPointer { buffer -> Int32 in
|
|
463
|
+
// Build an array of UnsafePointer<CChar>? from UnsafeMutablePointer<CChar>?
|
|
464
|
+
var constPtrs = buffer.map { UnsafePointer($0) }
|
|
465
|
+
return constPtrs.withUnsafeMutableBufferPointer { constBuffer in
|
|
466
|
+
mpv_command(mpv, constBuffer.baseAddress)
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Free strdup'd strings
|
|
471
|
+
for ptr in cStrings {
|
|
472
|
+
if let ptr = ptr { free(ptr) }
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if result < 0 {
|
|
476
|
+
let errStr = String(cString: mpv_error_string(result))
|
|
477
|
+
log("ERROR: command '\(command) \(args.joined(separator: " "))' failed: \(errStr) (\(result))")
|
|
478
|
+
DispatchQueue.main.async {
|
|
479
|
+
self.onError(["error": "Command '\(command)' failed: \(errStr)"])
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
log("Command OK: \(command) \(args.joined(separator: " "))")
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private func setOptionString(_ name: String, _ value: String) {
|
|
487
|
+
guard let mpv = mpv else { return }
|
|
488
|
+
let result = mpv_set_option_string(mpv, name, value)
|
|
489
|
+
if result < 0 {
|
|
490
|
+
let errStr = String(cString: mpv_error_string(result))
|
|
491
|
+
log("ERROR: set option '\(name)'='\(value)' failed: \(errStr)")
|
|
492
|
+
} else {
|
|
493
|
+
log("Option: \(name) = \(value)")
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private func getDouble(_ name: String) -> Double {
|
|
498
|
+
guard mpv != nil else { return 0 }
|
|
499
|
+
var data: Double = 0
|
|
500
|
+
mpv_get_property(mpv, name, MPV_FORMAT_DOUBLE, &data)
|
|
501
|
+
return data
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private func getInt(_ name: String) -> Int64 {
|
|
505
|
+
guard mpv != nil else { return 0 }
|
|
506
|
+
var data: Int64 = 0
|
|
507
|
+
mpv_get_property(mpv, name, MPV_FORMAT_INT64, &data)
|
|
508
|
+
return data
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private func getFlag(_ name: String) -> Bool {
|
|
512
|
+
guard mpv != nil else { return false }
|
|
513
|
+
var data: Int32 = 0
|
|
514
|
+
mpv_get_property(mpv, name, MPV_FORMAT_FLAG, &data)
|
|
515
|
+
return data != 0
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private func setDouble(_ name: String, _ value: Double) {
|
|
519
|
+
guard mpv != nil else { return }
|
|
520
|
+
var data = value
|
|
521
|
+
let result = mpv_set_property(mpv, name, MPV_FORMAT_DOUBLE, &data)
|
|
522
|
+
if result < 0 {
|
|
523
|
+
let errStr = String(cString: mpv_error_string(result))
|
|
524
|
+
log("ERROR: set property '\(name)'=\(value) failed: \(errStr)")
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private func setInt(_ name: String, _ value: Int64) {
|
|
529
|
+
guard mpv != nil else { return }
|
|
530
|
+
var data = value
|
|
531
|
+
let result = mpv_set_property(mpv, name, MPV_FORMAT_INT64, &data)
|
|
532
|
+
if result < 0 {
|
|
533
|
+
let errStr = String(cString: mpv_error_string(result))
|
|
534
|
+
log("ERROR: set property '\(name)'=\(value) failed: \(errStr)")
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private func setFlag(_ name: String, _ flag: Bool) {
|
|
539
|
+
guard mpv != nil else { return }
|
|
540
|
+
var data: Int32 = flag ? 1 : 0
|
|
541
|
+
let result = mpv_set_property(mpv, name, MPV_FORMAT_FLAG, &data)
|
|
542
|
+
if result < 0 {
|
|
543
|
+
let errStr = String(cString: mpv_error_string(result))
|
|
544
|
+
log("ERROR: set flag '\(name)'=\(flag) failed: \(errStr)")
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
@discardableResult
|
|
549
|
+
private func checkError(_ status: Int32, label: String = "") -> Bool {
|
|
550
|
+
if status < 0 {
|
|
551
|
+
let errStr = String(cString: mpv_error_string(status))
|
|
552
|
+
log("ERROR [\(label)]: \(errStr) (\(status))")
|
|
553
|
+
return false
|
|
554
|
+
}
|
|
555
|
+
return true
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private func log(_ message: String) {
|
|
559
|
+
NSLog("[ExpoMpv] %@", message)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// MARK: - MetalLayer
|
|
564
|
+
|
|
565
|
+
private class MetalLayer: CAMetalLayer {
|
|
566
|
+
// Guard against MoltenVK 1×1 drawableSize bug
|
|
567
|
+
override var drawableSize: CGSize {
|
|
568
|
+
get { super.drawableSize }
|
|
569
|
+
set {
|
|
570
|
+
if Int(newValue.width) > 1 && Int(newValue.height) > 1 {
|
|
571
|
+
super.drawableSize = newValue
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// EDR/HDR content must be set on main thread
|
|
577
|
+
override var wantsExtendedDynamicRangeContent: Bool {
|
|
578
|
+
get { super.wantsExtendedDynamicRangeContent }
|
|
579
|
+
set {
|
|
580
|
+
if Thread.isMainThread {
|
|
581
|
+
super.wantsExtendedDynamicRangeContent = newValue
|
|
582
|
+
} else {
|
|
583
|
+
DispatchQueue.main.sync {
|
|
584
|
+
super.wantsExtendedDynamicRangeContent = newValue
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
XCFW_DIR="${SCRIPT_DIR}/Frameworks"
|
|
6
|
+
MPVKIT_VERSION="0.41.0"
|
|
7
|
+
LOCKFILE="${XCFW_DIR}/.version"
|
|
8
|
+
|
|
9
|
+
# Check if already downloaded at the right version
|
|
10
|
+
if [ -f "${LOCKFILE}" ] && [ "$(cat ${LOCKFILE})" = "${MPVKIT_VERSION}" ]; then
|
|
11
|
+
echo "✅ MPVKit ${MPVKIT_VERSION} xcframeworks already present."
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
echo "🔽 Downloading MPVKit ${MPVKIT_VERSION} xcframeworks..."
|
|
16
|
+
rm -rf "${XCFW_DIR}"
|
|
17
|
+
mkdir -p "${XCFW_DIR}"
|
|
18
|
+
|
|
19
|
+
MPVKIT_URL="https://github.com/mpvkit/MPVKit/releases/download/${MPVKIT_VERSION}"
|
|
20
|
+
|
|
21
|
+
download_xcframework() {
|
|
22
|
+
local name=$1
|
|
23
|
+
local url=$2
|
|
24
|
+
echo " 📦 ${name}..."
|
|
25
|
+
curl -sL "${url}" -o "${XCFW_DIR}/${name}.zip"
|
|
26
|
+
unzip -q -o "${XCFW_DIR}/${name}.zip" -d "${XCFW_DIR}/"
|
|
27
|
+
rm -f "${XCFW_DIR}/${name}.zip"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Core MPVKit - libmpv
|
|
31
|
+
download_xcframework "Libmpv" "${MPVKIT_URL}/Libmpv.xcframework.zip"
|
|
32
|
+
|
|
33
|
+
# FFmpeg frameworks
|
|
34
|
+
for lib in Libavcodec Libavdevice Libavformat Libavfilter Libavutil Libswresample Libswscale; do
|
|
35
|
+
download_xcframework "${lib}" "${MPVKIT_URL}/${lib}.xcframework.zip"
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
# OpenSSL
|
|
39
|
+
OPENSSL_VERSION="3.3.5"
|
|
40
|
+
for lib in Libcrypto Libssl; do
|
|
41
|
+
download_xcframework "${lib}" "https://github.com/mpvkit/openssl-build/releases/download/${OPENSSL_VERSION}/${lib}.xcframework.zip"
|
|
42
|
+
done
|
|
43
|
+
|
|
44
|
+
# GnuTLS
|
|
45
|
+
GNUTLS_VERSION="3.8.11"
|
|
46
|
+
for lib in gmp nettle hogweed gnutls; do
|
|
47
|
+
download_xcframework "${lib}" "https://github.com/mpvkit/gnutls-build/releases/download/${GNUTLS_VERSION}/${lib}.xcframework.zip"
|
|
48
|
+
done
|
|
49
|
+
|
|
50
|
+
# Libass and dependencies
|
|
51
|
+
LIBASS_VERSION="0.17.4"
|
|
52
|
+
for lib in Libunibreak Libfreetype Libfribidi Libharfbuzz Libass; do
|
|
53
|
+
download_xcframework "${lib}" "https://github.com/mpvkit/libass-build/releases/download/${LIBASS_VERSION}/${lib}.xcframework.zip"
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
# MoltenVK (Vulkan on Metal)
|
|
57
|
+
download_xcframework "MoltenVK" "https://github.com/mpvkit/moltenvk-build/releases/download/1.4.1/MoltenVK.xcframework.zip"
|
|
58
|
+
|
|
59
|
+
# Shaderc
|
|
60
|
+
download_xcframework "Libshaderc_combined" "https://github.com/mpvkit/libshaderc-build/releases/download/2025.5.0/Libshaderc_combined.xcframework.zip"
|
|
61
|
+
|
|
62
|
+
# lcms2
|
|
63
|
+
download_xcframework "lcms2" "https://github.com/mpvkit/lcms2-build/releases/download/2.17.0/lcms2.xcframework.zip"
|
|
64
|
+
|
|
65
|
+
# Libplacebo
|
|
66
|
+
download_xcframework "Libplacebo" "https://github.com/mpvkit/libplacebo-build/releases/download/7.351.0-2512/Libplacebo.xcframework.zip"
|
|
67
|
+
|
|
68
|
+
# Libdav1d
|
|
69
|
+
download_xcframework "Libdav1d" "https://github.com/mpvkit/libdav1d-build/releases/download/1.5.2-xcode/Libdav1d.xcframework.zip"
|
|
70
|
+
|
|
71
|
+
# Libuchardet
|
|
72
|
+
download_xcframework "Libuchardet" "https://github.com/mpvkit/libuchardet-build/releases/download/0.0.8-xcode/Libuchardet.xcframework.zip"
|
|
73
|
+
|
|
74
|
+
# Libbluray
|
|
75
|
+
download_xcframework "Libbluray" "https://github.com/mpvkit/libbluray-build/releases/download/1.4.0/Libbluray.xcframework.zip"
|
|
76
|
+
|
|
77
|
+
# Libdovi
|
|
78
|
+
download_xcframework "Libdovi" "https://github.com/mpvkit/libdovi-build/releases/download/3.3.2/Libdovi.xcframework.zip"
|
|
79
|
+
|
|
80
|
+
# Libuavs3d (uAVS3 decoder)
|
|
81
|
+
download_xcframework "Libuavs3d" "https://github.com/mpvkit/libuavs3d-build/releases/download/1.2.1-xcode/Libuavs3d.xcframework.zip"
|
|
82
|
+
|
|
83
|
+
echo "${MPVKIT_VERSION}" > "${LOCKFILE}"
|
|
84
|
+
echo ""
|
|
85
|
+
echo "✅ All MPVKit ${MPVKIT_VERSION} xcframeworks downloaded to ${XCFW_DIR}"
|