@capgo/capacitor-video-player 8.2.1 → 8.2.3

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.
@@ -156,6 +156,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
156
156
  private Uri uri = null;
157
157
  private Uri sturi = null;
158
158
  private List<Uri> subtitleUris = new ArrayList<>();
159
+ private List<String> subtitleLanguages = new ArrayList<>();
159
160
  private ProgressBar Pbar;
160
161
  private View view;
161
162
  private ImageButton closeBtn;
@@ -178,7 +179,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
178
179
  private String stBackColor = "";
179
180
  private Integer stFontSize = 16;
180
181
  private boolean isInPictureInPictureMode = false;
181
- private TrackSelector trackSelector;
182
+ private DefaultTrackSelector trackSelector;
182
183
  // Current playback position (in milliseconds).
183
184
  private int mCurrentPosition;
184
185
  private int mDuration;
@@ -400,10 +401,12 @@ public class FullscreenExoPlayerFragment extends Fragment {
400
401
  if (!isInternal) {
401
402
  uri = Uri.parse(videoPath);
402
403
  subtitleUris = new ArrayList<>();
404
+ subtitleLanguages = new ArrayList<>();
403
405
  if (subtitleTracks != null) {
404
406
  for (VideoSubtitleTrack track : subtitleTracks) {
405
407
  if (track != null && track.url != null && !track.url.isEmpty()) {
406
408
  subtitleUris.add(Uri.parse(track.url));
409
+ subtitleLanguages.add(track.language != null ? track.language : "");
407
410
  }
408
411
  }
409
412
  }
@@ -411,6 +414,7 @@ public class FullscreenExoPlayerFragment extends Fragment {
411
414
  if (sturi == null && subTitle != null && !subTitle.isEmpty()) {
412
415
  sturi = Uri.parse(subTitle);
413
416
  subtitleUris.add(sturi);
417
+ subtitleLanguages.add(language != null ? language : "en");
414
418
  }
415
419
 
416
420
  stForeColor = subTitleOptions != null && subTitleOptions.has("foregroundColor")
@@ -892,6 +896,12 @@ public class FullscreenExoPlayerFragment extends Fragment {
892
896
  DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter.Builder(context).build();
893
897
  ExoTrackSelection.Factory videoTrackSelectionFactory = new AdaptiveTrackSelection.Factory();
894
898
  trackSelector = new DefaultTrackSelector(context, videoTrackSelectionFactory);
899
+ if (!subtitleUris.isEmpty()) {
900
+ String preferred = languageForTrack(0);
901
+ trackSelector.setParameters(
902
+ trackSelector.buildUponParameters().setPreferredTextLanguage(preferred).setSelectUndeterminedTextLanguage(true)
903
+ );
904
+ }
895
905
  LoadControl loadControl = new DefaultLoadControl();
896
906
  player = new ExoPlayer.Builder(context)
897
907
  .setSeekBackIncrementMs(10000)
@@ -946,11 +956,10 @@ public class FullscreenExoPlayerFragment extends Fragment {
946
956
 
947
957
  private void updateSubtitleButtonVisibility(Tracks tracks) {
948
958
  if (styledPlayerView == null) return;
949
- if (sturi != null) {
950
- styledPlayerView.setShowSubtitleButton(true);
951
- return;
952
- }
953
- styledPlayerView.setShowSubtitleButton(hasSupportedTextTrack(tracks));
959
+ boolean hasSidecar = !subtitleUris.isEmpty() || sturi != null;
960
+ boolean hasText = hasSidecar || hasSupportedTextTrack(tracks);
961
+ // CC button opens ExoPlayer's text-track picker when multiple languages exist.
962
+ styledPlayerView.setShowSubtitleButton(hasText);
954
963
  }
955
964
 
956
965
  private boolean hasSupportedTextTrack(Tracks tracks) {
@@ -1622,8 +1631,9 @@ public class FullscreenExoPlayerFragment extends Fragment {
1622
1631
  }
1623
1632
 
1624
1633
  private String languageForTrack(int index) {
1625
- if (subtitleTracks != null && index >= 0 && index < subtitleTracks.size()) {
1626
- String trackLanguage = subtitleTracks.get(index).language;
1634
+ // subtitleLanguages is built in lockstep with subtitleUris — never re-index raw subtitleTracks.
1635
+ if (index >= 0 && index < subtitleLanguages.size()) {
1636
+ String trackLanguage = subtitleLanguages.get(index);
1627
1637
  if (trackLanguage != null && !trackLanguage.isEmpty()) {
1628
1638
  return trackLanguage;
1629
1639
  }
@@ -46,7 +46,7 @@ import org.json.JSONObject;
46
46
  )
47
47
  public class VideoPlayerPlugin extends Plugin {
48
48
 
49
- private final String pluginVersion = "8.2.1";
49
+ private final String pluginVersion = "8.2.3";
50
50
 
51
51
  // Permission alias constants
52
52
  private static final String PERMISSION_DENIED_ERROR = "Unable to access media videos, user denied permission request";
@@ -81,7 +81,7 @@ class FullscreenVideoPlayer: NSObject {
81
81
  }
82
82
 
83
83
  func setupPlayer(completion: @escaping () -> Void) {
84
- guard let url = URL(string: videoUrl) else {
84
+ guard let url = Self.resolveMediaURL(videoUrl) else {
85
85
  completion()
86
86
  return
87
87
  }
@@ -100,8 +100,16 @@ class FullscreenVideoPlayer: NSObject {
100
100
  return
101
101
  }
102
102
 
103
- if HLSVideoAssetFactory.isHLSStream(url) {
104
- let asset = makeVideoAsset(url: url, subtitleTracks: subtitleTracks)
103
+ // Prefer the resource-loader path when makeAsset can safely wrap the stream
104
+ // (HLS / remote progressive). Local progressive file:// sources return a nil
105
+ // loader and must use composition so playback still starts (#78).
106
+ let assetResult = HLSVideoAssetFactory.makeAsset(
107
+ videoURL: url,
108
+ subtitleTracks: subtitleTracks
109
+ )
110
+ hlsResourceLoader = assetResult.resourceLoader
111
+ if assetResult.resourceLoader != nil {
112
+ let asset = applyFairPlayIfNeeded(to: assetResult.asset)
105
113
  configurePlayer(with: AVPlayerItem(asset: asset))
106
114
  completion()
107
115
  return
@@ -120,14 +128,27 @@ class FullscreenVideoPlayer: NSObject {
120
128
  }
121
129
  }
122
130
 
131
+ private static func resolveMediaURL(_ raw: String) -> URL? {
132
+ guard !raw.isEmpty else { return nil }
133
+ if let parsed = URL(string: raw), parsed.scheme != nil {
134
+ return parsed
135
+ }
136
+ if raw.hasPrefix("/") {
137
+ return URL(fileURLWithPath: raw)
138
+ }
139
+ return URL(string: raw)
140
+ }
141
+
123
142
  private func makeVideoAsset(url: URL, subtitleTracks: [VideoSubtitleTrack]) -> AVURLAsset {
124
143
  let result = HLSVideoAssetFactory.makeAsset(
125
144
  videoURL: url,
126
145
  subtitleTracks: subtitleTracks
127
146
  )
128
147
  hlsResourceLoader = result.resourceLoader
129
- let asset = result.asset
148
+ return applyFairPlayIfNeeded(to: result.asset)
149
+ }
130
150
 
151
+ private func applyFairPlayIfNeeded(to asset: AVURLAsset) -> AVURLAsset {
131
152
  if let certUrl = fairplayCertificateUrl, !certUrl.isEmpty,
132
153
  let spcUrl = fairplayContentKeySpcUrl, !spcUrl.isEmpty {
133
154
  let session = AVContentKeySession(keySystem: .fairPlayStreaming)
@@ -135,7 +156,6 @@ class FullscreenVideoPlayer: NSObject {
135
156
  session.addContentKeyRecipient(asset)
136
157
  self.contentKeySession = session
137
158
  }
138
-
139
159
  return asset
140
160
  }
141
161
 
@@ -24,13 +24,26 @@ enum HLSVideoAssetFactory {
24
24
  resolvedTracks.append((url: subtitleURL, language: track.language ?? "en"))
25
25
  }
26
26
 
27
- guard !resolvedTracks.isEmpty, isHLSStream(videoURL) else {
27
+ // Use the resource loader whenever sidecar subtitles are present so
28
+ // AVPlayerViewController can expose a native multi-language picker.
29
+ // AVMutableComposition can play one track but does not reliably expose
30
+ // selectable legible media-selection options.
31
+ guard !resolvedTracks.isEmpty else {
32
+ return (AVURLAsset(url: videoURL), nil)
33
+ }
34
+
35
+ // Local progressive files cannot be wrapped in synthetic HLS playlists:
36
+ // AVPlayer will not fetch file:// media/subtitle segments from a custom-scheme
37
+ // master playlist, so playback never becomes ready (#78). Fall back to the
38
+ // composition path in FullscreenVideoPlayer instead.
39
+ if videoURL.isFileURL && !isHLSStream(videoURL) {
28
40
  return (AVURLAsset(url: videoURL), nil)
29
41
  }
30
42
 
31
43
  let resourceLoader = HLSSubtitleResourceLoader(
32
44
  videoURL: videoURL,
33
- subtitleTracks: resolvedTracks.map { HLSSubtitleTrack(url: $0.url, language: $0.language) }
45
+ subtitleTracks: resolvedTracks.map { HLSSubtitleTrack(url: $0.url, language: $0.language) },
46
+ isProgressive: !isHLSStream(videoURL)
34
47
  )
35
48
  guard let assetURL = HLSSubtitleResourceLoader.assetURL(for: videoURL) else {
36
49
  return (AVURLAsset(url: videoURL), nil)
@@ -47,27 +60,37 @@ struct HLSSubtitleTrack {
47
60
  let language: String
48
61
  }
49
62
 
50
- /// Injects external WebVTT subtitles into HLS master playlists via AVAssetResourceLoaderDelegate.
51
- /// AVMutableComposition cannot demux HLS tracks, so sidecar subtitles must be wired through the manifest.
63
+ /// Injects external WebVTT subtitles into playlists via AVAssetResourceLoaderDelegate.
64
+ ///
65
+ /// - HLS multivariant playlists: inject `#EXT-X-MEDIA` subtitle tags.
66
+ /// - HLS media playlists: wrap in a synthetic multivariant playlist that references the media playlist.
67
+ /// - Progressive MP4/WebM: synthesize a multivariant + media playlist so multiple sidecar
68
+ /// tracks appear in AVPlayerViewController's subtitle options.
52
69
  final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
53
70
  static let videoScheme = "capgohls"
71
+ static let mediaPlaylistScheme = "capgohls-media"
54
72
  static let subtitlePlaylistScheme = "capgohls-sub"
55
73
  private static let subtitleGroupID = "capgosubs"
56
74
  private static let mpegURLContentType = UTType.m3uPlaylist.identifier
75
+ private static let progressiveSegmentDuration = 86400.0
57
76
 
58
77
  private let originalVideoURL: URL
59
78
  private let subtitleTracks: [HLSSubtitleTrack]
79
+ private let isProgressive: Bool
60
80
 
61
81
  private struct LoadedResource {
62
82
  let data: Data
63
83
  let contentType: String?
64
84
  }
65
85
 
66
- init(videoURL: URL, subtitleTracks: [HLSSubtitleTrack]) {
86
+ init(videoURL: URL, subtitleTracks: [HLSSubtitleTrack], isProgressive: Bool = false) {
67
87
  self.originalVideoURL = videoURL
68
88
  self.subtitleTracks = subtitleTracks
89
+ self.isProgressive = isProgressive
69
90
  }
70
91
 
92
+ deinit {}
93
+
71
94
  static func assetURL(for videoURL: URL) -> URL? {
72
95
  guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
73
96
  return nil
@@ -76,6 +99,14 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
76
99
  return components.url
77
100
  }
78
101
 
102
+ static func mediaPlaylistURL(for videoURL: URL) -> URL? {
103
+ guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
104
+ return nil
105
+ }
106
+ components.scheme = mediaPlaylistScheme
107
+ return components.url
108
+ }
109
+
79
110
  static func subtitlePlaylistURL(for videoURL: URL, index: Int) -> URL? {
80
111
  guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
81
112
  return nil
@@ -111,10 +142,25 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
111
142
  if requestURL.scheme == Self.subtitlePlaylistScheme {
112
143
  let index = subtitleIndex(from: requestURL) ?? 0
113
144
  let track = subtitleTracks[min(max(index, 0), subtitleTracks.count - 1)]
114
- let data = Data(subtitleMediaPlaylist(for: track.url).utf8)
145
+ let data = Data(Self.subtitleMediaPlaylist(for: track.url).utf8)
115
146
  return LoadedResource(data: data, contentType: Self.mpegURLContentType)
116
147
  }
117
148
 
149
+ if requestURL.scheme == Self.mediaPlaylistScheme {
150
+ let data = Data(Self.progressiveMediaPlaylist(for: originalVideoURL).utf8)
151
+ return LoadedResource(data: data, contentType: Self.mpegURLContentType)
152
+ }
153
+
154
+ if isProgressive {
155
+ let playlist = Self.multivariantPlaylist(
156
+ mediaURI: Self.mediaPlaylistURL(for: originalVideoURL)?.absoluteString
157
+ ?? originalVideoURL.absoluteString,
158
+ subtitleTracks: subtitleTracks,
159
+ videoURL: originalVideoURL
160
+ )
161
+ return LoadedResource(data: Data(playlist.utf8), contentType: Self.mpegURLContentType)
162
+ }
163
+
118
164
  let fetchURL = originalURL(for: requestURL)
119
165
  let (data, response) = try await URLSession.shared.data(from: fetchURL)
120
166
  if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
@@ -123,16 +169,22 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
123
169
 
124
170
  let manifest = String(data: data, encoding: .utf8) ?? ""
125
171
 
126
- guard hasStreamInfTags(manifest) else {
127
- let contentType = Self.contentTypeIdentifier(from: response)
128
- return LoadedResource(data: data, contentType: contentType)
172
+ if hasStreamInfTags(manifest) {
173
+ let modified = injectSubtitles(
174
+ into: manifest,
175
+ baseURL: response.url ?? fetchURL
176
+ )
177
+ return LoadedResource(data: Data(modified.utf8), contentType: Self.mpegURLContentType)
129
178
  }
130
179
 
131
- let modified = injectSubtitles(
132
- into: manifest,
133
- baseURL: response.url ?? fetchURL
180
+ // Media playlist (no variants): wrap in a multivariant playlist so subtitle tags are selectable.
181
+ let mediaURI = (response.url ?? fetchURL).absoluteString
182
+ let playlist = Self.multivariantPlaylist(
183
+ mediaURI: mediaURI,
184
+ subtitleTracks: subtitleTracks,
185
+ videoURL: originalVideoURL
134
186
  )
135
- return LoadedResource(data: Data(modified.utf8), contentType: Self.mpegURLContentType)
187
+ return LoadedResource(data: Data(playlist.utf8), contentType: Self.mpegURLContentType)
136
188
  }
137
189
 
138
190
  private func subtitleIndex(from requestURL: URL) -> Int? {
@@ -157,13 +209,6 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
157
209
  manifest.contains("#EXT-X-STREAM-INF")
158
210
  }
159
211
 
160
- private static func contentTypeIdentifier(from response: URLResponse) -> String? {
161
- guard let mimeType = (response as? HTTPURLResponse)?.mimeType else {
162
- return nil
163
- }
164
- return UTType(mimeType: mimeType)?.identifier
165
- }
166
-
167
212
  private func absoluteURI(for uri: String, relativeTo baseURL: URL) -> String {
168
213
  let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
169
214
  guard let url = URL(string: trimmed, relativeTo: baseURL)?.absoluteURL else {
@@ -172,18 +217,60 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
172
217
  return url.absoluteString
173
218
  }
174
219
 
175
- private func injectSubtitles(into manifest: String, baseURL: URL) -> String {
176
- let mediaTags = subtitleTracks.enumerated().compactMap { index, track -> String? in
177
- guard let subtitlePlaylistURI = Self.subtitlePlaylistURL(for: originalVideoURL, index: index)?.absoluteString else {
220
+ static func multivariantPlaylist(
221
+ mediaURI: String,
222
+ subtitleTracks: [HLSSubtitleTrack],
223
+ videoURL: URL
224
+ ) -> String {
225
+ var lines: [String] = ["#EXTM3U", "#EXT-X-VERSION:3"]
226
+ lines.append(contentsOf: subtitleMediaTags(subtitleTracks: subtitleTracks, videoURL: videoURL))
227
+ lines.append("#EXT-X-STREAM-INF:BANDWIDTH=2500000,SUBTITLES=\"\(subtitleGroupID)\"")
228
+ lines.append(mediaURI)
229
+ return lines.joined(separator: "\n") + "\n"
230
+ }
231
+
232
+ static func progressiveMediaPlaylist(for videoURL: URL) -> String {
233
+ """
234
+ #EXTM3U
235
+ #EXT-X-VERSION:3
236
+ #EXT-X-TARGETDURATION:\(Int(progressiveSegmentDuration))
237
+ #EXT-X-MEDIA-SEQUENCE:0
238
+ #EXT-X-PLAYLIST-TYPE:VOD
239
+ #EXTINF:\(progressiveSegmentDuration),
240
+ \(videoURL.absoluteString)
241
+ #EXT-X-ENDLIST
242
+ """
243
+ }
244
+
245
+ static func subtitleMediaPlaylist(for subtitleURL: URL) -> String {
246
+ """
247
+ #EXTM3U
248
+ #EXT-X-VERSION:3
249
+ #EXT-X-TARGETDURATION:\(Int(progressiveSegmentDuration))
250
+ #EXT-X-MEDIA-SEQUENCE:0
251
+ #EXT-X-PLAYLIST-TYPE:VOD
252
+ #EXTINF:\(progressiveSegmentDuration),
253
+ \(subtitleURL.absoluteString)
254
+ #EXT-X-ENDLIST
255
+ """
256
+ }
257
+
258
+ static func subtitleMediaTags(subtitleTracks: [HLSSubtitleTrack], videoURL: URL) -> [String] {
259
+ subtitleTracks.enumerated().compactMap { index, track -> String? in
260
+ guard let subtitlePlaylistURI = subtitlePlaylistURL(for: videoURL, index: index)?.absoluteString else {
178
261
  return nil
179
262
  }
180
263
  let isDefault = index == 0 ? "YES" : "NO"
181
264
  let displayName = Locale(identifier: track.language).localizedString(forLanguageCode: track.language)
182
265
  ?? track.language
183
266
  return """
184
- #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="\(Self.subtitleGroupID)",NAME="\(displayName)",DEFAULT=\(isDefault),AUTOSELECT=YES,FORCED=NO,LANGUAGE="\(track.language)",URI="\(subtitlePlaylistURI)"
267
+ #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="\(subtitleGroupID)",NAME="\(displayName)",DEFAULT=\(isDefault),AUTOSELECT=YES,FORCED=NO,LANGUAGE="\(track.language)",URI="\(subtitlePlaylistURI)"
185
268
  """
186
269
  }
270
+ }
271
+
272
+ private func injectSubtitles(into manifest: String, baseURL: URL) -> String {
273
+ let mediaTags = Self.subtitleMediaTags(subtitleTracks: subtitleTracks, videoURL: originalVideoURL)
187
274
 
188
275
  guard !mediaTags.isEmpty else {
189
276
  return manifest
@@ -226,19 +313,6 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
226
313
  return result.joined(separator: "\n")
227
314
  }
228
315
 
229
- private func subtitleMediaPlaylist(for subtitleURL: URL) -> String {
230
- """
231
- #EXTM3U
232
- #EXT-X-VERSION:3
233
- #EXT-X-TARGETDURATION:600
234
- #EXT-X-MEDIA-SEQUENCE:0
235
- #EXT-X-PLAYLIST-TYPE:VOD
236
- #EXTINF:600.0,
237
- \(subtitleURL.absoluteString)
238
- #EXT-X-ENDLIST
239
- """
240
- }
241
-
242
316
  private func fulfill(_ loadingRequest: AVAssetResourceLoadingRequest, with resource: LoadedResource) {
243
317
  if let contentRequest = loadingRequest.contentInformationRequest {
244
318
  if let contentType = resource.contentType {
@@ -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.2.1"
11
+ private let pluginVersion: String = "8.2.3"
12
12
  public let identifier = "VideoPlayerPlugin"
13
13
  public let jsName = "VideoPlayer"
14
14
  public let pluginMethods: [CAPPluginMethod] = [
@@ -6,6 +6,13 @@ struct VideoSubtitleTrack {
6
6
 
7
7
  var resolvedURL: URL? {
8
8
  guard !url.isEmpty else { return nil }
9
+ if let parsed = URL(string: url), parsed.scheme != nil {
10
+ return parsed
11
+ }
12
+ // Capacitor filesystem paths sometimes arrive without a file:// scheme.
13
+ if url.hasPrefix("/") {
14
+ return URL(fileURLWithPath: url)
15
+ }
9
16
  return URL(string: url)
10
17
  }
11
18
  }
@@ -0,0 +1,91 @@
1
+ import XCTest
2
+ @testable import VideoPlayerPlugin
3
+
4
+ final class HLSSubtitlePlaylistTests: XCTestCase {
5
+ func testMultivariantPlaylistIncludesAllSubtitleLanguages() {
6
+ let videoURL = URL(string: "https://example.com/video.mp4")!
7
+ let tracks = [
8
+ HLSSubtitleTrack(url: URL(string: "https://example.com/en.vtt")!, language: "en"),
9
+ HLSSubtitleTrack(url: URL(string: "https://example.com/fr.vtt")!, language: "fr"),
10
+ HLSSubtitleTrack(url: URL(string: "https://example.com/es.vtt")!, language: "es")
11
+ ]
12
+
13
+ let playlist = HLSSubtitleResourceLoader.multivariantPlaylist(
14
+ mediaURI: "capgohls-media://example.com/video.mp4",
15
+ subtitleTracks: tracks,
16
+ videoURL: videoURL
17
+ )
18
+
19
+ XCTAssertTrue(playlist.contains("#EXTM3U"))
20
+ XCTAssertTrue(playlist.contains("LANGUAGE=\"en\""))
21
+ XCTAssertTrue(playlist.contains("LANGUAGE=\"fr\""))
22
+ XCTAssertTrue(playlist.contains("LANGUAGE=\"es\""))
23
+ XCTAssertTrue(playlist.contains("DEFAULT=YES"))
24
+ XCTAssertTrue(playlist.contains("SUBTITLES=\"capgosubs\""))
25
+ XCTAssertTrue(playlist.contains("capgohls-media://example.com/video.mp4"))
26
+ XCTAssertEqual(playlist.components(separatedBy: "TYPE=SUBTITLES").count - 1, 3)
27
+ }
28
+
29
+ func testProgressiveMediaPlaylistPointsAtSourceVideo() {
30
+ let videoURL = URL(string: "https://example.com/movie.mp4")!
31
+ let playlist = HLSSubtitleResourceLoader.progressiveMediaPlaylist(for: videoURL)
32
+ let subtitle = HLSSubtitleResourceLoader.subtitleMediaPlaylist(
33
+ for: URL(string: "https://example.com/en.vtt")!
34
+ )
35
+
36
+ XCTAssertTrue(playlist.contains("#EXT-X-PLAYLIST-TYPE:VOD"))
37
+ XCTAssertTrue(playlist.contains(videoURL.absoluteString))
38
+ XCTAssertTrue(playlist.contains("#EXT-X-ENDLIST"))
39
+ XCTAssertTrue(playlist.contains("#EXT-X-TARGETDURATION:86400"))
40
+ XCTAssertTrue(playlist.contains("#EXTINF:86400.0,"))
41
+ XCTAssertTrue(subtitle.contains("#EXT-X-TARGETDURATION:86400"))
42
+ XCTAssertTrue(subtitle.contains("#EXTINF:86400.0,"))
43
+ }
44
+
45
+ func testMakeAssetUsesResourceLoaderForProgressiveSubtitles() {
46
+ let videoURL = URL(string: "https://example.com/movie.mp4")!
47
+ let tracks = [
48
+ VideoSubtitleTrack(url: "https://example.com/en.vtt", language: "en"),
49
+ VideoSubtitleTrack(url: "https://example.com/fr.vtt", language: "fr")
50
+ ]
51
+
52
+ let result = HLSVideoAssetFactory.makeAsset(videoURL: videoURL, subtitleTracks: tracks)
53
+
54
+ XCTAssertNotNil(result.resourceLoader)
55
+ XCTAssertEqual(result.asset.url.scheme, HLSSubtitleResourceLoader.videoScheme)
56
+ }
57
+
58
+ func testMakeAssetSkipsResourceLoaderWithoutSubtitles() {
59
+ let videoURL = URL(string: "https://example.com/movie.mp4")!
60
+ let result = HLSVideoAssetFactory.makeAsset(videoURL: videoURL, subtitleTracks: [])
61
+
62
+ XCTAssertNil(result.resourceLoader)
63
+ XCTAssertEqual(result.asset.url, videoURL)
64
+ }
65
+
66
+ func testMakeAssetSkipsResourceLoaderForLocalProgressiveSubtitles() {
67
+ let videoURL = URL(fileURLWithPath: "/var/mobile/Containers/Data/Application/video.mp4")
68
+ let tracks = [
69
+ VideoSubtitleTrack(url: "file:///var/mobile/Containers/Data/Application/subtitles-en.vtt", language: "en"),
70
+ VideoSubtitleTrack(url: "file:///var/mobile/Containers/Data/Application/subtitles-fr.vtt", language: "fr")
71
+ ]
72
+
73
+ let result = HLSVideoAssetFactory.makeAsset(videoURL: videoURL, subtitleTracks: tracks)
74
+
75
+ XCTAssertNil(result.resourceLoader)
76
+ XCTAssertEqual(result.asset.url, videoURL)
77
+ XCTAssertEqual(result.asset.url.scheme, "file")
78
+ }
79
+
80
+ func testMakeAssetUsesResourceLoaderForLocalHLSSubtitles() {
81
+ let videoURL = URL(fileURLWithPath: "/var/mobile/Containers/Data/Application/stream.m3u8")
82
+ let tracks = [
83
+ VideoSubtitleTrack(url: "file:///var/mobile/Containers/Data/Application/subtitles-en.vtt", language: "en")
84
+ ]
85
+
86
+ let result = HLSVideoAssetFactory.makeAsset(videoURL: videoURL, subtitleTracks: tracks)
87
+
88
+ XCTAssertNotNil(result.resourceLoader)
89
+ XCTAssertEqual(result.asset.url.scheme, HLSSubtitleResourceLoader.videoScheme)
90
+ }
91
+ }
@@ -3,13 +3,31 @@ import XCTest
3
3
 
4
4
  class VideoPlayerTests: XCTestCase {
5
5
  func testEcho() {
6
- // This is an example of a functional test case for a plugin.
7
- // Use XCTAssert and related functions to verify your tests produce the correct results.
8
-
9
6
  let implementation = VideoPlayer()
10
7
  let value = "Hello, World!"
11
8
  let result = implementation.echo(value)
12
-
13
9
  XCTAssertEqual(value, result)
14
10
  }
11
+
12
+ func testSubtitleTrackParserPrefersSubtitlesArray() {
13
+ let tracks = VideoSubtitleTrackParser.parse(
14
+ from: [
15
+ ["subtitle": "https://example.com/en.vtt", "language": "en"],
16
+ ["subtitle": "https://example.com/fr.vtt", "language": "fr"]
17
+ ],
18
+ legacySubtitle: "https://example.com/legacy.vtt",
19
+ legacyLanguage: "de"
20
+ )
21
+
22
+ XCTAssertEqual(tracks.count, 2)
23
+ XCTAssertEqual(tracks[0].url, "https://example.com/en.vtt")
24
+ XCTAssertEqual(tracks[0].language, "en")
25
+ XCTAssertEqual(tracks[1].language, "fr")
26
+ }
27
+
28
+ func testSubtitleTrackResolvesBareFilePath() {
29
+ let track = VideoSubtitleTrack(url: "/var/mobile/Containers/Data/Application/subtitles-en.vtt", language: "en")
30
+ XCTAssertEqual(track.resolvedURL?.isFileURL, true)
31
+ XCTAssertEqual(track.resolvedURL?.path, "/var/mobile/Containers/Data/Application/subtitles-en.vtt")
32
+ }
15
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-video-player",
3
- "version": "8.2.1",
3
+ "version": "8.2.3",
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",