@capgo/capacitor-video-player 8.2.0 → 8.2.2
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/android/src/main/java/com/capgo/videoplayer/FullscreenExoPlayerFragment.java +18 -8
- package/android/src/main/java/com/capgo/videoplayer/VideoPlayerPlugin.java +1 -1
- package/ios/Sources/VideoPlayerPlugin/FullscreenVideoPlayer.swift +12 -4
- package/ios/Sources/VideoPlayerPlugin/HLSSubtitleResourceLoader.swift +103 -37
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +1 -1
- package/ios/Tests/VideoPlayerPluginTests/HLSSubtitlePlaylistTests.swift +65 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerPluginTests.swift +16 -4
- package/package.json +3 -2
|
@@ -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
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
-
|
|
1626
|
-
|
|
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.
|
|
49
|
+
private final String pluginVersion = "8.2.2";
|
|
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";
|
|
@@ -100,8 +100,15 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
100
100
|
return
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
// Prefer the resource-loader path for any video with sidecar subtitles.
|
|
104
|
+
// That exposes selectable tracks in AVPlayerViewController (composition does not).
|
|
105
|
+
let assetResult = HLSVideoAssetFactory.makeAsset(
|
|
106
|
+
videoURL: url,
|
|
107
|
+
subtitleTracks: subtitleTracks
|
|
108
|
+
)
|
|
109
|
+
hlsResourceLoader = assetResult.resourceLoader
|
|
110
|
+
if assetResult.resourceLoader != nil {
|
|
111
|
+
let asset = applyFairPlayIfNeeded(to: assetResult.asset)
|
|
105
112
|
configurePlayer(with: AVPlayerItem(asset: asset))
|
|
106
113
|
completion()
|
|
107
114
|
return
|
|
@@ -126,8 +133,10 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
126
133
|
subtitleTracks: subtitleTracks
|
|
127
134
|
)
|
|
128
135
|
hlsResourceLoader = result.resourceLoader
|
|
129
|
-
|
|
136
|
+
return applyFairPlayIfNeeded(to: result.asset)
|
|
137
|
+
}
|
|
130
138
|
|
|
139
|
+
private func applyFairPlayIfNeeded(to asset: AVURLAsset) -> AVURLAsset {
|
|
131
140
|
if let certUrl = fairplayCertificateUrl, !certUrl.isEmpty,
|
|
132
141
|
let spcUrl = fairplayContentKeySpcUrl, !spcUrl.isEmpty {
|
|
133
142
|
let session = AVContentKeySession(keySystem: .fairPlayStreaming)
|
|
@@ -135,7 +144,6 @@ class FullscreenVideoPlayer: NSObject {
|
|
|
135
144
|
session.addContentKeyRecipient(asset)
|
|
136
145
|
self.contentKeySession = session
|
|
137
146
|
}
|
|
138
|
-
|
|
139
147
|
return asset
|
|
140
148
|
}
|
|
141
149
|
|
|
@@ -24,13 +24,18 @@ enum HLSVideoAssetFactory {
|
|
|
24
24
|
resolvedTracks.append((url: subtitleURL, language: track.language ?? "en"))
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
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 {
|
|
28
32
|
return (AVURLAsset(url: videoURL), nil)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
let resourceLoader = HLSSubtitleResourceLoader(
|
|
32
36
|
videoURL: videoURL,
|
|
33
|
-
subtitleTracks: resolvedTracks.map { HLSSubtitleTrack(url: $0.url, language: $0.language) }
|
|
37
|
+
subtitleTracks: resolvedTracks.map { HLSSubtitleTrack(url: $0.url, language: $0.language) },
|
|
38
|
+
isProgressive: !isHLSStream(videoURL)
|
|
34
39
|
)
|
|
35
40
|
guard let assetURL = HLSSubtitleResourceLoader.assetURL(for: videoURL) else {
|
|
36
41
|
return (AVURLAsset(url: videoURL), nil)
|
|
@@ -47,27 +52,37 @@ struct HLSSubtitleTrack {
|
|
|
47
52
|
let language: String
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
/// Injects external WebVTT subtitles into
|
|
51
|
-
///
|
|
55
|
+
/// Injects external WebVTT subtitles into playlists via AVAssetResourceLoaderDelegate.
|
|
56
|
+
///
|
|
57
|
+
/// - HLS multivariant playlists: inject `#EXT-X-MEDIA` subtitle tags.
|
|
58
|
+
/// - HLS media playlists: wrap in a synthetic multivariant playlist that references the media playlist.
|
|
59
|
+
/// - Progressive MP4/WebM: synthesize a multivariant + media playlist so multiple sidecar
|
|
60
|
+
/// tracks appear in AVPlayerViewController's subtitle options.
|
|
52
61
|
final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
53
62
|
static let videoScheme = "capgohls"
|
|
63
|
+
static let mediaPlaylistScheme = "capgohls-media"
|
|
54
64
|
static let subtitlePlaylistScheme = "capgohls-sub"
|
|
55
65
|
private static let subtitleGroupID = "capgosubs"
|
|
56
66
|
private static let mpegURLContentType = UTType.m3uPlaylist.identifier
|
|
67
|
+
private static let progressiveSegmentDuration = 86400.0
|
|
57
68
|
|
|
58
69
|
private let originalVideoURL: URL
|
|
59
70
|
private let subtitleTracks: [HLSSubtitleTrack]
|
|
71
|
+
private let isProgressive: Bool
|
|
60
72
|
|
|
61
73
|
private struct LoadedResource {
|
|
62
74
|
let data: Data
|
|
63
75
|
let contentType: String?
|
|
64
76
|
}
|
|
65
77
|
|
|
66
|
-
init(videoURL: URL, subtitleTracks: [HLSSubtitleTrack]) {
|
|
78
|
+
init(videoURL: URL, subtitleTracks: [HLSSubtitleTrack], isProgressive: Bool = false) {
|
|
67
79
|
self.originalVideoURL = videoURL
|
|
68
80
|
self.subtitleTracks = subtitleTracks
|
|
81
|
+
self.isProgressive = isProgressive
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
deinit {}
|
|
85
|
+
|
|
71
86
|
static func assetURL(for videoURL: URL) -> URL? {
|
|
72
87
|
guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
|
|
73
88
|
return nil
|
|
@@ -76,6 +91,14 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
76
91
|
return components.url
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
static func mediaPlaylistURL(for videoURL: URL) -> URL? {
|
|
95
|
+
guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
|
|
96
|
+
return nil
|
|
97
|
+
}
|
|
98
|
+
components.scheme = mediaPlaylistScheme
|
|
99
|
+
return components.url
|
|
100
|
+
}
|
|
101
|
+
|
|
79
102
|
static func subtitlePlaylistURL(for videoURL: URL, index: Int) -> URL? {
|
|
80
103
|
guard var components = URLComponents(url: videoURL, resolvingAgainstBaseURL: false) else {
|
|
81
104
|
return nil
|
|
@@ -111,10 +134,25 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
111
134
|
if requestURL.scheme == Self.subtitlePlaylistScheme {
|
|
112
135
|
let index = subtitleIndex(from: requestURL) ?? 0
|
|
113
136
|
let track = subtitleTracks[min(max(index, 0), subtitleTracks.count - 1)]
|
|
114
|
-
let data = Data(subtitleMediaPlaylist(for: track.url).utf8)
|
|
137
|
+
let data = Data(Self.subtitleMediaPlaylist(for: track.url).utf8)
|
|
115
138
|
return LoadedResource(data: data, contentType: Self.mpegURLContentType)
|
|
116
139
|
}
|
|
117
140
|
|
|
141
|
+
if requestURL.scheme == Self.mediaPlaylistScheme {
|
|
142
|
+
let data = Data(Self.progressiveMediaPlaylist(for: originalVideoURL).utf8)
|
|
143
|
+
return LoadedResource(data: data, contentType: Self.mpegURLContentType)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if isProgressive {
|
|
147
|
+
let playlist = Self.multivariantPlaylist(
|
|
148
|
+
mediaURI: Self.mediaPlaylistURL(for: originalVideoURL)?.absoluteString
|
|
149
|
+
?? originalVideoURL.absoluteString,
|
|
150
|
+
subtitleTracks: subtitleTracks,
|
|
151
|
+
videoURL: originalVideoURL
|
|
152
|
+
)
|
|
153
|
+
return LoadedResource(data: Data(playlist.utf8), contentType: Self.mpegURLContentType)
|
|
154
|
+
}
|
|
155
|
+
|
|
118
156
|
let fetchURL = originalURL(for: requestURL)
|
|
119
157
|
let (data, response) = try await URLSession.shared.data(from: fetchURL)
|
|
120
158
|
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
|
|
@@ -123,16 +161,22 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
123
161
|
|
|
124
162
|
let manifest = String(data: data, encoding: .utf8) ?? ""
|
|
125
163
|
|
|
126
|
-
|
|
127
|
-
let
|
|
128
|
-
|
|
164
|
+
if hasStreamInfTags(manifest) {
|
|
165
|
+
let modified = injectSubtitles(
|
|
166
|
+
into: manifest,
|
|
167
|
+
baseURL: response.url ?? fetchURL
|
|
168
|
+
)
|
|
169
|
+
return LoadedResource(data: Data(modified.utf8), contentType: Self.mpegURLContentType)
|
|
129
170
|
}
|
|
130
171
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
172
|
+
// Media playlist (no variants): wrap in a multivariant playlist so subtitle tags are selectable.
|
|
173
|
+
let mediaURI = (response.url ?? fetchURL).absoluteString
|
|
174
|
+
let playlist = Self.multivariantPlaylist(
|
|
175
|
+
mediaURI: mediaURI,
|
|
176
|
+
subtitleTracks: subtitleTracks,
|
|
177
|
+
videoURL: originalVideoURL
|
|
134
178
|
)
|
|
135
|
-
return LoadedResource(data: Data(
|
|
179
|
+
return LoadedResource(data: Data(playlist.utf8), contentType: Self.mpegURLContentType)
|
|
136
180
|
}
|
|
137
181
|
|
|
138
182
|
private func subtitleIndex(from requestURL: URL) -> Int? {
|
|
@@ -157,13 +201,6 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
157
201
|
manifest.contains("#EXT-X-STREAM-INF")
|
|
158
202
|
}
|
|
159
203
|
|
|
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
204
|
private func absoluteURI(for uri: String, relativeTo baseURL: URL) -> String {
|
|
168
205
|
let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
169
206
|
guard let url = URL(string: trimmed, relativeTo: baseURL)?.absoluteURL else {
|
|
@@ -172,18 +209,60 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
172
209
|
return url.absoluteString
|
|
173
210
|
}
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
212
|
+
static func multivariantPlaylist(
|
|
213
|
+
mediaURI: String,
|
|
214
|
+
subtitleTracks: [HLSSubtitleTrack],
|
|
215
|
+
videoURL: URL
|
|
216
|
+
) -> String {
|
|
217
|
+
var lines: [String] = ["#EXTM3U", "#EXT-X-VERSION:3"]
|
|
218
|
+
lines.append(contentsOf: subtitleMediaTags(subtitleTracks: subtitleTracks, videoURL: videoURL))
|
|
219
|
+
lines.append("#EXT-X-STREAM-INF:BANDWIDTH=2500000,SUBTITLES=\"\(subtitleGroupID)\"")
|
|
220
|
+
lines.append(mediaURI)
|
|
221
|
+
return lines.joined(separator: "\n") + "\n"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static func progressiveMediaPlaylist(for videoURL: URL) -> String {
|
|
225
|
+
"""
|
|
226
|
+
#EXTM3U
|
|
227
|
+
#EXT-X-VERSION:3
|
|
228
|
+
#EXT-X-TARGETDURATION:\(Int(progressiveSegmentDuration))
|
|
229
|
+
#EXT-X-MEDIA-SEQUENCE:0
|
|
230
|
+
#EXT-X-PLAYLIST-TYPE:VOD
|
|
231
|
+
#EXTINF:\(progressiveSegmentDuration),
|
|
232
|
+
\(videoURL.absoluteString)
|
|
233
|
+
#EXT-X-ENDLIST
|
|
234
|
+
"""
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static func subtitleMediaPlaylist(for subtitleURL: URL) -> String {
|
|
238
|
+
"""
|
|
239
|
+
#EXTM3U
|
|
240
|
+
#EXT-X-VERSION:3
|
|
241
|
+
#EXT-X-TARGETDURATION:\(Int(progressiveSegmentDuration))
|
|
242
|
+
#EXT-X-MEDIA-SEQUENCE:0
|
|
243
|
+
#EXT-X-PLAYLIST-TYPE:VOD
|
|
244
|
+
#EXTINF:\(progressiveSegmentDuration),
|
|
245
|
+
\(subtitleURL.absoluteString)
|
|
246
|
+
#EXT-X-ENDLIST
|
|
247
|
+
"""
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static func subtitleMediaTags(subtitleTracks: [HLSSubtitleTrack], videoURL: URL) -> [String] {
|
|
251
|
+
subtitleTracks.enumerated().compactMap { index, track -> String? in
|
|
252
|
+
guard let subtitlePlaylistURI = subtitlePlaylistURL(for: videoURL, index: index)?.absoluteString else {
|
|
178
253
|
return nil
|
|
179
254
|
}
|
|
180
255
|
let isDefault = index == 0 ? "YES" : "NO"
|
|
181
256
|
let displayName = Locale(identifier: track.language).localizedString(forLanguageCode: track.language)
|
|
182
257
|
?? track.language
|
|
183
258
|
return """
|
|
184
|
-
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="\(
|
|
259
|
+
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="\(subtitleGroupID)",NAME="\(displayName)",DEFAULT=\(isDefault),AUTOSELECT=YES,FORCED=NO,LANGUAGE="\(track.language)",URI="\(subtitlePlaylistURI)"
|
|
185
260
|
"""
|
|
186
261
|
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private func injectSubtitles(into manifest: String, baseURL: URL) -> String {
|
|
265
|
+
let mediaTags = Self.subtitleMediaTags(subtitleTracks: subtitleTracks, videoURL: originalVideoURL)
|
|
187
266
|
|
|
188
267
|
guard !mediaTags.isEmpty else {
|
|
189
268
|
return manifest
|
|
@@ -226,19 +305,6 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
|
|
|
226
305
|
return result.joined(separator: "\n")
|
|
227
306
|
}
|
|
228
307
|
|
|
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
308
|
private func fulfill(_ loadingRequest: AVAssetResourceLoadingRequest, with resource: LoadedResource) {
|
|
243
309
|
if let contentRequest = loadingRequest.contentInformationRequest {
|
|
244
310
|
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.
|
|
11
|
+
private let pluginVersion: String = "8.2.2"
|
|
12
12
|
public let identifier = "VideoPlayerPlugin"
|
|
13
13
|
public let jsName = "VideoPlayer"
|
|
14
14
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
}
|
|
@@ -3,13 +3,25 @@ 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
|
+
}
|
|
15
27
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-video-player",
|
|
3
|
-
"version": "8.2.
|
|
3
|
+
"version": "8.2.2",
|
|
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",
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"prepublishOnly": "bun run build",
|
|
50
50
|
"check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
|
|
51
51
|
"example:install": "cd example-app && bun install --frozen-lockfile",
|
|
52
|
-
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
|
|
52
|
+
"example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
|
|
53
|
+
"changelog:ai": "node scripts/generate-ai-changelog.mjs"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
56
|
"@capacitor/android": "^8.0.0",
|