@mux/mux-react-native-player 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/MuxReactNativePlayer.podspec +37 -0
- package/README.md +134 -0
- package/android/build.gradle +33 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/mux/reactnativeplayer/MuxReactNativePlayerModule.kt +135 -0
- package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoRecords.kt +174 -0
- package/android/src/main/java/com/mux/reactnativeplayer/MuxVideoView.kt +452 -0
- package/android/src/main/res/layout/mux_video_player_view.xml +6 -0
- package/assets/MuxRobot_02.gif +0 -0
- package/assets/MuxRobot_02@2x.gif +0 -0
- package/assets/MuxRobot_03.gif +0 -0
- package/assets/MuxRobot_03@2x.gif +0 -0
- package/assets/MuxRobot_04.gif +0 -0
- package/assets/MuxRobot_04@2x.gif +0 -0
- package/assets/MuxRobot_05.gif +0 -0
- package/assets/MuxRobot_05@2x.gif +0 -0
- package/build/MuxVideoControls.d.ts +21 -0
- package/build/MuxVideoControls.d.ts.map +1 -0
- package/build/MuxVideoControls.js +1032 -0
- package/build/MuxVideoPlayer.d.ts +59 -0
- package/build/MuxVideoPlayer.d.ts.map +1 -0
- package/build/MuxVideoPlayer.js +265 -0
- package/build/MuxVideoView.d.ts +39 -0
- package/build/MuxVideoView.d.ts.map +1 -0
- package/build/MuxVideoView.js +254 -0
- package/build/NativeMuxVideoView.d.ts +5 -0
- package/build/NativeMuxVideoView.d.ts.map +1 -0
- package/build/NativeMuxVideoView.js +4 -0
- package/build/index.d.ts +6 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/normalizeSource.d.ts +7 -0
- package/build/normalizeSource.d.ts.map +1 -0
- package/build/normalizeSource.js +76 -0
- package/build/screenOrientation.d.ts +3 -0
- package/build/screenOrientation.d.ts.map +1 -0
- package/build/screenOrientation.js +38 -0
- package/build/types.d.ts +170 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +1 -0
- package/expo-module.config.json +13 -0
- package/ios/MuxReactNativePlayerModule.swift +139 -0
- package/ios/MuxVideoRecords.swift +212 -0
- package/ios/MuxVideoView.swift +502 -0
- package/package.json +69 -0
- package/plugin/index.d.ts +11 -0
- package/plugin/index.js +1 -0
- package/plugin/withMuxReactNativePlayer.js +203 -0
- package/src/MuxVideoControls.tsx +1772 -0
- package/src/MuxVideoPlayer.ts +338 -0
- package/src/MuxVideoView.tsx +412 -0
- package/src/NativeMuxVideoView.ts +15 -0
- package/src/index.ts +32 -0
- package/src/normalizeSource.ts +101 -0
- package/src/screenOrientation.ts +46 -0
- package/src/types.ts +228 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import AVKit
|
|
3
|
+
import ExpoModulesCore
|
|
4
|
+
import MuxPlayerSwift
|
|
5
|
+
import UIKit
|
|
6
|
+
|
|
7
|
+
final class MuxVideoView: ExpoView {
|
|
8
|
+
private let onStatusChange = EventDispatcher()
|
|
9
|
+
private let onPlayingChange = EventDispatcher()
|
|
10
|
+
private let onTimeUpdate = EventDispatcher()
|
|
11
|
+
private let onSourceLoad = EventDispatcher()
|
|
12
|
+
private let onSourceError = EventDispatcher()
|
|
13
|
+
|
|
14
|
+
private let playerViewController = AVPlayerViewController()
|
|
15
|
+
private var sourceFingerprint: String?
|
|
16
|
+
private var currentPlaybackId: String?
|
|
17
|
+
private var didEmitSourceLoad = false
|
|
18
|
+
private var didLoadLegibleGroup = false
|
|
19
|
+
private var didReachEnd = false
|
|
20
|
+
private var muted = false
|
|
21
|
+
private var volume: Float = 1
|
|
22
|
+
private var loop = false
|
|
23
|
+
private var playbackRate: Float = 1
|
|
24
|
+
private var shouldPlay = false
|
|
25
|
+
private var timeUpdateInterval: TimeInterval = 0.5
|
|
26
|
+
private var startupBufferDuration: TimeInterval = 0
|
|
27
|
+
private var timeUpdateTimer: Timer?
|
|
28
|
+
private var statusObservation: NSKeyValueObservation?
|
|
29
|
+
private var timeControlObservation: NSKeyValueObservation?
|
|
30
|
+
|
|
31
|
+
required init(appContext: AppContext? = nil) {
|
|
32
|
+
super.init(appContext: appContext)
|
|
33
|
+
|
|
34
|
+
playerViewController.view.frame = bounds
|
|
35
|
+
playerViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
36
|
+
playerViewController.showsPlaybackControls = true
|
|
37
|
+
playerViewController.videoGravity = .resizeAspect
|
|
38
|
+
addSubview(playerViewController.view)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
deinit {
|
|
42
|
+
releasePlayer()
|
|
43
|
+
stopTimeUpdates()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override func didMoveToWindow() {
|
|
47
|
+
super.didMoveToWindow()
|
|
48
|
+
|
|
49
|
+
if window == nil {
|
|
50
|
+
stopTimeUpdates()
|
|
51
|
+
} else {
|
|
52
|
+
startTimeUpdates()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func setSource(_ source: MuxVideoSourceRecord?) {
|
|
57
|
+
guard let source else {
|
|
58
|
+
release()
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
guard source.fingerprint != sourceFingerprint else {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
releasePlayer()
|
|
67
|
+
sourceFingerprint = source.fingerprint
|
|
68
|
+
currentPlaybackId = source.playbackId
|
|
69
|
+
didEmitSourceLoad = false
|
|
70
|
+
didLoadLegibleGroup = false
|
|
71
|
+
didReachEnd = false
|
|
72
|
+
sendStatusChange(status: "loading")
|
|
73
|
+
|
|
74
|
+
playerViewController.prepare(
|
|
75
|
+
playbackID: source.playbackId,
|
|
76
|
+
playbackOptions: source.toPlaybackOptions(),
|
|
77
|
+
monitoringOptions: source.toMonitoringOptions()
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
observePlayer()
|
|
81
|
+
applyPlayerConfiguration()
|
|
82
|
+
startTimeUpdates()
|
|
83
|
+
|
|
84
|
+
if shouldPlay {
|
|
85
|
+
playerViewController.player?.play()
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func setNativeControls(_ enabled: Bool) {
|
|
90
|
+
playerViewController.showsPlaybackControls = enabled
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func setContentFit(_ contentFit: String) {
|
|
94
|
+
switch contentFit {
|
|
95
|
+
case "cover":
|
|
96
|
+
playerViewController.videoGravity = .resizeAspectFill
|
|
97
|
+
case "fill":
|
|
98
|
+
playerViewController.videoGravity = .resize
|
|
99
|
+
default:
|
|
100
|
+
playerViewController.videoGravity = .resizeAspect
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func setAllowsPictureInPicture(_ enabled: Bool) {
|
|
105
|
+
playerViewController.allowsPictureInPicturePlayback = enabled
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func setTimeUpdateEventInterval(_ interval: Double) {
|
|
109
|
+
timeUpdateInterval = max(0.1, interval)
|
|
110
|
+
stopTimeUpdates()
|
|
111
|
+
startTimeUpdates()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func setStartupBufferDuration(_ duration: Double) {
|
|
115
|
+
startupBufferDuration = max(0, duration)
|
|
116
|
+
playerViewController.player?.currentItem?.preferredForwardBufferDuration = startupBufferDuration
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func setPlayWhenReady(_ playWhenReady: Bool) {
|
|
120
|
+
if shouldPlay == playWhenReady {
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if playWhenReady {
|
|
125
|
+
play()
|
|
126
|
+
} else {
|
|
127
|
+
pause()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func setMuted(_ muted: Bool) {
|
|
132
|
+
self.muted = muted
|
|
133
|
+
playerViewController.player?.isMuted = muted
|
|
134
|
+
sendStatusChange()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func setVolume(_ volume: Double) {
|
|
138
|
+
self.volume = Float(min(1, max(0, volume)))
|
|
139
|
+
playerViewController.player?.volume = self.volume
|
|
140
|
+
sendStatusChange()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
func setLoop(_ loop: Bool) {
|
|
144
|
+
self.loop = loop
|
|
145
|
+
sendStatusChange()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
func setPlaybackRate(_ rate: Double) {
|
|
149
|
+
playbackRate = Float(min(4, max(0.25, rate)))
|
|
150
|
+
if playerViewController.player?.rate ?? 0 > 0 {
|
|
151
|
+
playerViewController.player?.rate = playbackRate
|
|
152
|
+
}
|
|
153
|
+
sendStatusChange()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func setCaptionTrack(_ trackId: String?) {
|
|
157
|
+
guard
|
|
158
|
+
let item = playerViewController.player?.currentItem,
|
|
159
|
+
let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: .legible)
|
|
160
|
+
else {
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
guard let trackId else {
|
|
165
|
+
item.select(nil, in: group)
|
|
166
|
+
sendStatusChange()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
guard
|
|
171
|
+
let index = Int(trackId),
|
|
172
|
+
group.options.indices.contains(index)
|
|
173
|
+
else {
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
item.select(group.options[index], in: group)
|
|
178
|
+
sendStatusChange()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func play() {
|
|
182
|
+
shouldPlay = true
|
|
183
|
+
didReachEnd = false
|
|
184
|
+
startPlaybackIfPossible()
|
|
185
|
+
sendStatusChange()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
func pause() {
|
|
189
|
+
shouldPlay = false
|
|
190
|
+
playerViewController.player?.pause()
|
|
191
|
+
sendStatusChange()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
func replay() {
|
|
195
|
+
shouldPlay = true
|
|
196
|
+
didReachEnd = false
|
|
197
|
+
seekTo(0)
|
|
198
|
+
play()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func seekBy(_ seconds: Double) {
|
|
202
|
+
seekTo(currentTimeSeconds() + seconds)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func seekTo(_ seconds: Double) {
|
|
206
|
+
didReachEnd = false
|
|
207
|
+
let target = CMTime(seconds: max(0, seconds), preferredTimescale: 600)
|
|
208
|
+
playerViewController.player?.seek(to: target, toleranceBefore: .zero, toleranceAfter: .zero)
|
|
209
|
+
sendStatusChange()
|
|
210
|
+
sendTimeUpdate()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func release() {
|
|
214
|
+
releasePlayer()
|
|
215
|
+
sourceFingerprint = nil
|
|
216
|
+
currentPlaybackId = nil
|
|
217
|
+
didEmitSourceLoad = false
|
|
218
|
+
didLoadLegibleGroup = false
|
|
219
|
+
didReachEnd = false
|
|
220
|
+
shouldPlay = false
|
|
221
|
+
sendStatusChange(status: "idle")
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private func observePlayer() {
|
|
225
|
+
guard let player = playerViewController.player else {
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
statusObservation = player.observe(\.currentItem?.status, options: [.initial, .new]) { [weak self] _, _ in
|
|
230
|
+
DispatchQueue.main.async {
|
|
231
|
+
self?.handlePlayerStatusUpdate()
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
timeControlObservation = player.observe(\.timeControlStatus, options: [.new]) { [weak self] _, _ in
|
|
236
|
+
DispatchQueue.main.async {
|
|
237
|
+
self?.sendStatusChange()
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
NotificationCenter.default.addObserver(
|
|
242
|
+
self,
|
|
243
|
+
selector: #selector(handlePlaybackEnded),
|
|
244
|
+
name: .AVPlayerItemDidPlayToEndTime,
|
|
245
|
+
object: player.currentItem
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
NotificationCenter.default.addObserver(
|
|
249
|
+
self,
|
|
250
|
+
selector: #selector(handlePlaybackFailed(_:)),
|
|
251
|
+
name: .AVPlayerItemFailedToPlayToEndTime,
|
|
252
|
+
object: player.currentItem
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private func handlePlayerStatusUpdate() {
|
|
257
|
+
guard let item = playerViewController.player?.currentItem else {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
switch item.status {
|
|
262
|
+
case .readyToPlay:
|
|
263
|
+
if !didEmitSourceLoad {
|
|
264
|
+
didEmitSourceLoad = true
|
|
265
|
+
onSourceLoad([
|
|
266
|
+
"playbackId": currentPlaybackId ?? "",
|
|
267
|
+
"duration": durationSeconds(),
|
|
268
|
+
"captionTracks": captionTracksPayload(),
|
|
269
|
+
"selectedCaptionTrackId": selectedCaptionTrackId() ?? NSNull(),
|
|
270
|
+
])
|
|
271
|
+
}
|
|
272
|
+
loadLegibleGroupIfNeeded(for: item)
|
|
273
|
+
if shouldPlay && playerViewController.player?.rate == 0 {
|
|
274
|
+
startPlaybackIfPossible()
|
|
275
|
+
}
|
|
276
|
+
sendStatusChange()
|
|
277
|
+
case .failed:
|
|
278
|
+
let message = item.error?.localizedDescription ?? "Mux playback failed."
|
|
279
|
+
onSourceError([
|
|
280
|
+
"playbackId": currentPlaybackId ?? "",
|
|
281
|
+
"message": message,
|
|
282
|
+
"code": item.error.map { "\(($0 as NSError).code)" } ?? "",
|
|
283
|
+
])
|
|
284
|
+
sendStatusChange(status: "error", error: message)
|
|
285
|
+
default:
|
|
286
|
+
sendStatusChange(status: "loading")
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
@objc private func handlePlaybackEnded() {
|
|
291
|
+
if loop {
|
|
292
|
+
seekTo(0)
|
|
293
|
+
play()
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
didReachEnd = true
|
|
298
|
+
shouldPlay = false
|
|
299
|
+
sendStatusChange(status: "ended")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@objc private func handlePlaybackFailed(_ notification: Notification) {
|
|
303
|
+
let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error
|
|
304
|
+
let message = error?.localizedDescription ?? "Mux playback failed."
|
|
305
|
+
onSourceError([
|
|
306
|
+
"playbackId": currentPlaybackId ?? "",
|
|
307
|
+
"message": message,
|
|
308
|
+
"code": error.map { "\(($0 as NSError).code)" } ?? "",
|
|
309
|
+
])
|
|
310
|
+
sendStatusChange(status: "error", error: message)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private func applyPlayerConfiguration() {
|
|
314
|
+
let player = playerViewController.player
|
|
315
|
+
player?.automaticallyWaitsToMinimizeStalling = true
|
|
316
|
+
player?.currentItem?.preferredForwardBufferDuration = startupBufferDuration
|
|
317
|
+
player?.isMuted = muted
|
|
318
|
+
player?.volume = volume
|
|
319
|
+
if shouldPlay {
|
|
320
|
+
startPlaybackIfPossible()
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private func startPlaybackIfPossible() {
|
|
325
|
+
guard let player = playerViewController.player else {
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
player.automaticallyWaitsToMinimizeStalling = true
|
|
330
|
+
player.currentItem?.preferredForwardBufferDuration = startupBufferDuration
|
|
331
|
+
player.play()
|
|
332
|
+
|
|
333
|
+
if playbackRate != 1, player.currentItem?.status == .readyToPlay {
|
|
334
|
+
player.rate = playbackRate
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private func releasePlayer() {
|
|
339
|
+
NotificationCenter.default.removeObserver(self)
|
|
340
|
+
statusObservation = nil
|
|
341
|
+
timeControlObservation = nil
|
|
342
|
+
playerViewController.stopMonitoring()
|
|
343
|
+
playerViewController.player?.pause()
|
|
344
|
+
playerViewController.player = nil
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private func startTimeUpdates() {
|
|
348
|
+
stopTimeUpdates()
|
|
349
|
+
guard window != nil else {
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
timeUpdateTimer = Timer.scheduledTimer(withTimeInterval: timeUpdateInterval, repeats: true) { [weak self] _ in
|
|
353
|
+
self?.sendTimeUpdate()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private func stopTimeUpdates() {
|
|
358
|
+
timeUpdateTimer?.invalidate()
|
|
359
|
+
timeUpdateTimer = nil
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private func sendTimeUpdate() {
|
|
363
|
+
onTimeUpdate([
|
|
364
|
+
"currentTime": currentTimeSeconds(),
|
|
365
|
+
"duration": durationSeconds(),
|
|
366
|
+
"bufferedPosition": bufferedPositionSeconds(),
|
|
367
|
+
])
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private func sendStatusChange(status: String? = nil, error: String? = nil) {
|
|
371
|
+
var payload: [String: Any] = [
|
|
372
|
+
"status": status ?? inferStatus(),
|
|
373
|
+
"currentTime": currentTimeSeconds(),
|
|
374
|
+
"duration": durationSeconds(),
|
|
375
|
+
"bufferedPosition": bufferedPositionSeconds(),
|
|
376
|
+
"muted": muted,
|
|
377
|
+
"volume": Double(volume),
|
|
378
|
+
"loop": loop,
|
|
379
|
+
"playbackRate": Double(playbackRate),
|
|
380
|
+
"captionTracks": captionTracksPayload(),
|
|
381
|
+
"selectedCaptionTrackId": selectedCaptionTrackId() ?? NSNull(),
|
|
382
|
+
]
|
|
383
|
+
|
|
384
|
+
if let error {
|
|
385
|
+
payload["error"] = error
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
onStatusChange(payload)
|
|
389
|
+
onPlayingChange(["isPlaying": payload["status"] as? String == "playing"])
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private func inferStatus() -> String {
|
|
393
|
+
guard let player = playerViewController.player else {
|
|
394
|
+
return currentPlaybackId == nil ? "idle" : "loading"
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if didReachEnd {
|
|
398
|
+
return "ended"
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if player.currentItem?.status == .failed {
|
|
402
|
+
return "error"
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if player.currentItem?.status != .readyToPlay {
|
|
406
|
+
return "loading"
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if player.timeControlStatus == .waitingToPlayAtSpecifiedRate {
|
|
410
|
+
return "buffering"
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return player.rate > 0 ? "playing" : "paused"
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private func currentTimeSeconds() -> Double {
|
|
417
|
+
guard let seconds = playerViewController.player?.currentTime().seconds, seconds.isFinite else {
|
|
418
|
+
return 0
|
|
419
|
+
}
|
|
420
|
+
return max(0, seconds)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private func durationSeconds() -> Double {
|
|
424
|
+
guard let duration = playerViewController.player?.currentItem?.duration.seconds, duration.isFinite else {
|
|
425
|
+
return 0
|
|
426
|
+
}
|
|
427
|
+
return max(0, duration)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private func bufferedPositionSeconds() -> Double {
|
|
431
|
+
guard let range = playerViewController.player?.currentItem?.loadedTimeRanges.first?.timeRangeValue else {
|
|
432
|
+
return 0
|
|
433
|
+
}
|
|
434
|
+
let end = CMTimeGetSeconds(CMTimeAdd(range.start, range.duration))
|
|
435
|
+
return end.isFinite ? max(0, end) : 0
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private func loadLegibleGroupIfNeeded(for item: AVPlayerItem) {
|
|
439
|
+
guard !didLoadLegibleGroup else {
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
didLoadLegibleGroup = true
|
|
443
|
+
let asset = item.asset
|
|
444
|
+
let requestedFingerprint = sourceFingerprint
|
|
445
|
+
asset.loadValuesAsynchronously(forKeys: ["availableMediaCharacteristicsWithMediaSelectionOptions"]) { [weak self] in
|
|
446
|
+
DispatchQueue.main.async {
|
|
447
|
+
guard let self, self.sourceFingerprint == requestedFingerprint else {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
self.sendStatusChange()
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private func captionTracksPayload() -> [[String: Any]] {
|
|
456
|
+
guard
|
|
457
|
+
let item = playerViewController.player?.currentItem,
|
|
458
|
+
let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: .legible)
|
|
459
|
+
else {
|
|
460
|
+
return []
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return group.options.enumerated().map { index, option in
|
|
464
|
+
var payload: [String: Any] = [
|
|
465
|
+
"id": "\(index)",
|
|
466
|
+
"label": option.displayName,
|
|
467
|
+
"kind": captionTrackKind(option),
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
if let language = option.extendedLanguageTag ?? option.locale?.identifier {
|
|
471
|
+
payload["language"] = language
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return payload
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private func selectedCaptionTrackId() -> String? {
|
|
479
|
+
guard
|
|
480
|
+
let item = playerViewController.player?.currentItem,
|
|
481
|
+
let group = item.asset.mediaSelectionGroup(forMediaCharacteristic: .legible),
|
|
482
|
+
let selected = item.currentMediaSelection.selectedMediaOption(in: group),
|
|
483
|
+
let index = group.options.firstIndex(where: { $0 === selected })
|
|
484
|
+
else {
|
|
485
|
+
return nil
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return "\(index)"
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private func captionTrackKind(_ option: AVMediaSelectionOption) -> String {
|
|
492
|
+
if option.hasMediaCharacteristic(.containsOnlyForcedSubtitles) {
|
|
493
|
+
return "forced"
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if option.hasMediaCharacteristic(.transcribesSpokenDialogForAccessibility) {
|
|
497
|
+
return "captions"
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return "subtitles"
|
|
501
|
+
}
|
|
502
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mux/mux-react-native-player",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mux-owned React Native video player powered by Mux Player for iOS and Android.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"types": "build/index.d.ts",
|
|
8
|
+
"react-native": "src/index.ts",
|
|
9
|
+
"source": "src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./build/index.d.ts",
|
|
13
|
+
"react-native": "./src/index.ts",
|
|
14
|
+
"default": "./build/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./plugin": "./plugin/index.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"android/build.gradle",
|
|
20
|
+
"android/src",
|
|
21
|
+
"assets/*.gif",
|
|
22
|
+
"build/**/*.{js,d.ts,d.ts.map}",
|
|
23
|
+
"ios/*.{h,m,mm,swift}",
|
|
24
|
+
"plugin",
|
|
25
|
+
"src",
|
|
26
|
+
"expo-module.config.json",
|
|
27
|
+
"MuxReactNativePlayer.podspec",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"prepack": "npm run build",
|
|
33
|
+
"test": "vitest run",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
35
|
+
"check:plugin": "node --check plugin/withMuxReactNativePlayer.js",
|
|
36
|
+
"example:install": "npm --prefix example install",
|
|
37
|
+
"example:robots": "npm --prefix example run robots",
|
|
38
|
+
"example:ios:prebuild": "npm --prefix example run prebuild:ios -- --clean",
|
|
39
|
+
"example:ios": "npm --prefix example run ios",
|
|
40
|
+
"example:ios:build": "npm --prefix example run ios:build",
|
|
41
|
+
"android": "expo run:android",
|
|
42
|
+
"ios": "expo run:ios"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"mux",
|
|
46
|
+
"react-native",
|
|
47
|
+
"expo",
|
|
48
|
+
"video",
|
|
49
|
+
"player"
|
|
50
|
+
],
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"expo": "*",
|
|
53
|
+
"expo-linear-gradient": "*",
|
|
54
|
+
"expo-modules-core": "*",
|
|
55
|
+
"react": "*",
|
|
56
|
+
"react-native": "*"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@expo/config-plugins": "^55.0.8",
|
|
60
|
+
"@types/react": "^19.2.14",
|
|
61
|
+
"expo": "^55.0.23",
|
|
62
|
+
"expo-linear-gradient": "^55.0.13",
|
|
63
|
+
"expo-modules-core": "^55.0.25",
|
|
64
|
+
"react": "19.2.0",
|
|
65
|
+
"react-native": "0.83.6",
|
|
66
|
+
"typescript": "~5.9.2",
|
|
67
|
+
"vitest": "^3.1.4"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConfigPlugin } from '@expo/config-plugins';
|
|
2
|
+
|
|
3
|
+
export type MuxReactNativePlayerPluginProps = {
|
|
4
|
+
enableBackgroundAudio?: boolean;
|
|
5
|
+
enablePictureInPicture?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
declare const withMuxReactNativePlayer: ConfigPlugin<MuxReactNativePlayerPluginProps | void>;
|
|
9
|
+
|
|
10
|
+
export default withMuxReactNativePlayer;
|
|
11
|
+
export { withMuxReactNativePlayer };
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./withMuxReactNativePlayer');
|