@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.
- 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 +25 -5
- package/ios/Sources/VideoPlayerPlugin/HLSSubtitleResourceLoader.swift +111 -37
- package/ios/Sources/VideoPlayerPlugin/VideoPlayerPlugin.swift +1 -1
- package/ios/Sources/VideoPlayerPlugin/VideoSubtitleTrack.swift +7 -0
- package/ios/Tests/VideoPlayerPluginTests/HLSSubtitlePlaylistTests.swift +91 -0
- package/ios/Tests/VideoPlayerPluginTests/VideoPlayerPluginTests.swift +22 -4
- package/package.json +1 -1
|
@@ -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.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 =
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
51
|
-
///
|
|
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
|
-
|
|
127
|
-
let
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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="\(
|
|
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.
|
|
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
|
}
|