@capgo/capacitor-video-player 8.1.16 → 8.1.18

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.
@@ -38,7 +38,7 @@ import java.util.Map;
38
38
  )
39
39
  public class VideoPlayerPlugin extends Plugin {
40
40
 
41
- private final String pluginVersion = "8.1.16";
41
+ private final String pluginVersion = "8.1.18";
42
42
 
43
43
  // Permission alias constants
44
44
  private static final String PERMISSION_DENIED_ERROR = "Unable to access media videos, user denied permission request";
@@ -28,6 +28,9 @@ class FullscreenVideoPlayer: NSObject {
28
28
  private var fairplayContentKeySpcUrl: String?
29
29
  private var contentKeySession: AVContentKeySession?
30
30
  private var castController: VideoPlayerCastController?
31
+ private weak var presentingViewController: UIViewController?
32
+ private var subtitleUrl: String?
33
+ private var subtitleLanguage: String?
31
34
 
32
35
  init(
33
36
  playerId: String,
@@ -41,6 +44,8 @@ class FullscreenVideoPlayer: NSObject {
41
44
  title: String? = nil,
42
45
  smallTitle: String? = nil,
43
46
  artwork: String? = nil,
47
+ subtitleUrl: String? = nil,
48
+ subtitleLanguage: String? = nil,
44
49
  fairplayCertificateUrl: String? = nil,
45
50
  fairplayContentKeySpcUrl: String? = nil
46
51
  ) {
@@ -55,19 +60,41 @@ class FullscreenVideoPlayer: NSObject {
55
60
  self.title = title
56
61
  self.smallTitle = smallTitle
57
62
  self.artwork = artwork
63
+ self.subtitleUrl = subtitleUrl
64
+ self.subtitleLanguage = subtitleLanguage
58
65
  self.fairplayCertificateUrl = fairplayCertificateUrl
59
66
  self.fairplayContentKeySpcUrl = fairplayContentKeySpcUrl
60
67
  super.init()
61
68
  }
62
69
 
63
- func setupPlayer() {
70
+ func setupPlayer(completion: @escaping () -> Void) {
64
71
  guard let url = URL(string: videoUrl) else {
72
+ completion()
73
+ return
74
+ }
75
+
76
+ let asset = makeVideoAsset(url: url)
77
+
78
+ guard let subtitleUrlString = subtitleUrl,
79
+ !subtitleUrlString.isEmpty,
80
+ let subtitleURL = URL(string: subtitleUrlString) else {
81
+ configurePlayer(with: AVPlayerItem(asset: asset))
82
+ completion()
65
83
  return
66
84
  }
67
85
 
86
+ Task {
87
+ let item = await createPlayerItem(videoAsset: asset, subtitleURL: subtitleURL)
88
+ await MainActor.run {
89
+ self.configurePlayer(with: item)
90
+ completion()
91
+ }
92
+ }
93
+ }
94
+
95
+ private func makeVideoAsset(url: URL) -> AVURLAsset {
68
96
  let asset = AVURLAsset(url: url)
69
97
 
70
- // Configure FairPlay DRM if certificate URL is provided
71
98
  if let certUrl = fairplayCertificateUrl, !certUrl.isEmpty {
72
99
  let session = AVContentKeySession(keySystem: .fairPlayStreaming)
73
100
  session.setDelegate(self, queue: DispatchQueue.global(qos: .default))
@@ -75,23 +102,112 @@ class FullscreenVideoPlayer: NSObject {
75
102
  self.contentKeySession = session
76
103
  }
77
104
 
78
- // Create player item with asset
79
- playerItem = AVPlayerItem(asset: asset)
105
+ return asset
106
+ }
107
+
108
+ private func createPlayerItem(videoAsset: AVURLAsset, subtitleURL: URL) async -> AVPlayerItem {
109
+ let subtitleAsset = AVURLAsset(url: subtitleURL)
110
+
111
+ do {
112
+ let videoDuration = try await videoAsset.load(.duration)
113
+ let composition = AVMutableComposition()
114
+
115
+ try await insertTracks(
116
+ from: videoAsset,
117
+ mediaTypes: [.video, .audio],
118
+ duration: videoDuration,
119
+ into: composition
120
+ )
121
+
122
+ var subtitleAdded = false
123
+ let subtitleTracks = try await subtitleAsset.loadTracks(withMediaType: .text)
124
+ if let subtitleTrack = subtitleTracks.first {
125
+ let compositionTrack = composition.addMutableTrack(
126
+ withMediaType: .text,
127
+ preferredTrackID: kCMPersistentTrackID_Invalid
128
+ )
129
+ let subtitleDuration = try await subtitleAsset.load(.duration)
130
+ let duration = CMTimeCompare(subtitleDuration, videoDuration) < 0 ? subtitleDuration : videoDuration
131
+ try compositionTrack?.insertTimeRange(
132
+ CMTimeRange(start: .zero, duration: duration),
133
+ of: subtitleTrack,
134
+ at: CMTime.zero
135
+ )
136
+ subtitleAdded = true
137
+ }
138
+
139
+ let playerItem = AVPlayerItem(asset: composition)
140
+
141
+ if subtitleAdded {
142
+ selectSubtitle(in: playerItem, language: subtitleLanguage)
143
+ }
144
+
145
+ return playerItem
146
+ } catch {
147
+ return AVPlayerItem(asset: videoAsset)
148
+ }
149
+ }
150
+
151
+ private func selectSubtitle(in playerItem: AVPlayerItem, language: String?) {
152
+ guard let group = playerItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
153
+ return
154
+ }
155
+
156
+ let selectedOption = selectSubtitleOption(in: group, language: language)
157
+ ?? group.defaultOption
158
+ ?? group.options.first
159
+
160
+ if let selectedOption {
161
+ playerItem.select(selectedOption, in: group)
162
+ }
163
+ }
164
+
165
+ private func insertTracks(
166
+ from asset: AVURLAsset,
167
+ mediaTypes: [AVMediaType],
168
+ duration: CMTime,
169
+ into composition: AVMutableComposition
170
+ ) async throws {
171
+ for mediaType in mediaTypes {
172
+ let tracks = try await asset.loadTracks(withMediaType: mediaType)
173
+ guard let sourceTrack = tracks.first else { continue }
174
+
175
+ let compositionTrack = composition.addMutableTrack(
176
+ withMediaType: mediaType,
177
+ preferredTrackID: kCMPersistentTrackID_Invalid
178
+ )
179
+ try compositionTrack?.insertTimeRange(
180
+ CMTimeRange(start: .zero, duration: duration),
181
+ of: sourceTrack,
182
+ at: .zero
183
+ )
184
+ }
185
+ }
80
186
 
81
- // Create player
187
+ private func selectSubtitleOption(in group: AVMediaSelectionGroup, language: String?) -> AVMediaSelectionOption? {
188
+ guard let language, !language.isEmpty else { return nil }
189
+
190
+ return group.options.first { option in
191
+ option.extendedLanguageTag == language
192
+ || option.locale?.identifier == language
193
+ || option.locale?.languageCode == language
194
+ }
195
+ }
196
+
197
+ private func configurePlayer(with item: AVPlayerItem) {
198
+ playerItem = item
82
199
  player = AVPlayer(playerItem: playerItem)
83
200
  player?.rate = rate
84
201
 
85
- // Create player view controller
86
202
  playerViewController = AVPlayerViewController()
87
203
  playerViewController?.player = player
88
204
  playerViewController?.showsPlaybackControls = showControls
89
-
90
- // Picture in Picture support
91
205
  playerViewController?.allowsPictureInPicturePlayback = pipEnabled
92
- setupChromecast()
206
+ if pipEnabled {
207
+ playerViewController?.delegate = self
208
+ }
93
209
 
94
- // Setup observers
210
+ setupChromecast()
95
211
  setupObservers()
96
212
  }
97
213
 
@@ -188,6 +304,7 @@ class FullscreenVideoPlayer: NSObject {
188
304
  return
189
305
  }
190
306
 
307
+ presentingViewController = viewController
191
308
  viewController.present(playerVC, animated: true) {
192
309
  self.play()
193
310
  completion()
@@ -346,6 +463,36 @@ class FullscreenVideoPlayer: NSObject {
346
463
  }
347
464
  }
348
465
 
466
+ // MARK: - AVPlayerViewControllerDelegate (Picture in Picture)
467
+
468
+ extension FullscreenVideoPlayer: AVPlayerViewControllerDelegate {
469
+ func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(
470
+ _ playerViewController: AVPlayerViewController
471
+ ) -> Bool {
472
+ false
473
+ }
474
+
475
+ func playerViewController(
476
+ _ playerViewController: AVPlayerViewController,
477
+ restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
478
+ ) {
479
+ guard let presentingViewController else {
480
+ completionHandler(false)
481
+ return
482
+ }
483
+
484
+ if playerViewController.presentingViewController == nil {
485
+ DispatchQueue.main.async {
486
+ presentingViewController.present(playerViewController, animated: false) {
487
+ completionHandler(true)
488
+ }
489
+ }
490
+ } else {
491
+ completionHandler(true)
492
+ }
493
+ }
494
+ }
495
+
349
496
  // MARK: - AVContentKeySessionDelegate (FairPlay DRM)
350
497
 
351
498
  extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
@@ -8,7 +8,7 @@ import AVKit
8
8
  */
9
9
  @objc(VideoPlayerPlugin)
10
10
  public class VideoPlayerPlugin: CAPPlugin, CAPBridgedPlugin {
11
- private let pluginVersion: String = "8.1.16"
11
+ private let pluginVersion: String = "8.1.18"
12
12
  public let identifier = "VideoPlayerPlugin"
13
13
  public let jsName = "VideoPlayer"
14
14
  public let pluginMethods: [CAPPluginMethod] = [
@@ -79,6 +79,8 @@ extension VideoPlayerPlugin {
79
79
  let title = call.getString("title")
80
80
  let smallTitle = call.getString("smallTitle")
81
81
  let artwork = call.getString("artwork")
82
+ let subtitle = call.getString("subtitle")
83
+ let language = call.getString("language")
82
84
 
83
85
  // Extract FairPlay DRM options if provided
84
86
  let drm = call.getObject("drm")
@@ -99,6 +101,8 @@ extension VideoPlayerPlugin {
99
101
  title: title,
100
102
  smallTitle: smallTitle,
101
103
  artwork: artwork,
104
+ subtitleUrl: subtitle,
105
+ subtitleLanguage: language,
102
106
  fairplayCertificateUrl: fairplayCertificateUrl,
103
107
  fairplayContentKeySpcUrl: fairplayContentKeySpcUrl
104
108
  )
@@ -114,19 +118,20 @@ extension VideoPlayerPlugin {
114
118
  return
115
119
  }
116
120
 
117
- player.setupPlayer()
118
- self.configureCallbacks(for: player, playerId: playerId)
121
+ player.setupPlayer {
122
+ self.configureCallbacks(for: player, playerId: playerId)
119
123
 
120
- // Store player
121
- self.videoPlayers[playerId] = player
122
- self.currentPlayerId = playerId
124
+ // Store player
125
+ self.videoPlayers[playerId] = player
126
+ self.currentPlayerId = playerId
123
127
 
124
- player.present(on: viewController) {
125
- call.resolve([
126
- "result": true,
127
- "method": "initPlayer",
128
- "value": playerId
129
- ])
128
+ player.present(on: viewController) {
129
+ call.resolve([
130
+ "result": true,
131
+ "method": "initPlayer",
132
+ "value": playerId
133
+ ])
134
+ }
130
135
  }
131
136
  }
132
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-video-player",
3
- "version": "8.1.16",
3
+ "version": "8.1.18",
4
4
  "description": "Capacitor plugin to play video in native player",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",