@capgo/capacitor-video-player 8.2.4 → 8.2.5

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.
@@ -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.4";
49
+ private final String pluginVersion = "8.2.5";
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";
@@ -22,6 +22,7 @@ class FullscreenVideoPlayer: NSObject {
22
22
  private var rate: Float
23
23
  private var audioCategory: String?
24
24
  private var didActivateAudioSession: Bool = false
25
+ private var didEmitReady: Bool = false
25
26
  private var didEmitExit: Bool = false
26
27
  private var timeObserver: Any?
27
28
  private var onPlay: (() -> Void)?
@@ -38,6 +39,11 @@ class FullscreenVideoPlayer: NSObject {
38
39
  private weak var presentingViewController: UIViewController?
39
40
  private var subtitleTracks: [VideoSubtitleTrack] = []
40
41
  private var hlsResourceLoader: HLSSubtitleResourceLoader?
42
+ private var subtitleButton: UIButton?
43
+ private var subtitleSelectionObserver: NSObjectProtocol?
44
+ private var nonHlsSubtitleModeEnabled: Bool = false
45
+ private var selectedNonHlsSubtitleIndex: Int? = nil
46
+ private var nonHlsBootstrapLoadId: UUID?
41
47
 
42
48
  init(
43
49
  playerId: String,
@@ -92,14 +98,59 @@ class FullscreenVideoPlayer: NSObject {
92
98
  guard let subtitleURL = track.resolvedURL else { return nil }
93
99
  return (subtitleURL, track.language)
94
100
  }
101
+ let isLikelyHlsSource = Self.isLikelyHLS(videoUrl)
95
102
 
96
103
  guard !resolvedTracks.isEmpty else {
104
+ nonHlsSubtitleModeEnabled = false
105
+ selectedNonHlsSubtitleIndex = nil
106
+ nonHlsBootstrapLoadId = nil
97
107
  let asset = makeVideoAsset(url: url, subtitleTracks: [])
98
108
  configurePlayer(with: AVPlayerItem(asset: asset))
99
109
  completion()
100
110
  return
101
111
  }
102
112
 
113
+ // For non-HLS media (e.g. MP4 + sidecar subtitles), initialize with only the
114
+ // first usable subtitle track and switch tracks later via the custom subtitle button.
115
+ if !isLikelyHlsSource {
116
+ nonHlsSubtitleModeEnabled = true
117
+ let firstValidIndex = validNonHlsSubtitleTracks.first?.index
118
+ selectedNonHlsSubtitleIndex = firstValidIndex
119
+ let baseAsset = makeVideoAsset(url: url, subtitleTracks: [])
120
+ configurePlayer(with: AVPlayerItem(asset: baseAsset))
121
+ completion()
122
+
123
+ guard let firstValidIndex else {
124
+ return
125
+ }
126
+
127
+ let bootstrapLoadId = UUID()
128
+ nonHlsBootstrapLoadId = bootstrapLoadId
129
+
130
+ Task { [weak self] in
131
+ guard let self else { return }
132
+ let initialTrack = [self.subtitleTracks[firstValidIndex]]
133
+ let item = await ProgressiveVideoPlayerItemFactory.createPlayerItem(
134
+ videoAsset: baseAsset,
135
+ subtitleTracks: initialTrack
136
+ )
137
+ await MainActor.run {
138
+ guard self.nonHlsSubtitleModeEnabled,
139
+ self.nonHlsBootstrapLoadId == bootstrapLoadId,
140
+ self.selectedNonHlsSubtitleIndex == firstValidIndex else {
141
+ return
142
+ }
143
+ self.replaceCurrentItemPreservingPlayback(with: item)
144
+ self.refreshSubtitleButton()
145
+ }
146
+ }
147
+ return
148
+ }
149
+
150
+ nonHlsSubtitleModeEnabled = false
151
+ selectedNonHlsSubtitleIndex = nil
152
+ nonHlsBootstrapLoadId = nil
153
+
103
154
  // Prefer the resource-loader path when makeAsset can safely wrap the stream
104
155
  // (HLS / remote progressive). Local progressive file:// sources return a nil
105
156
  // loader and must use composition so playback still starts (#78).
@@ -172,6 +223,378 @@ class FullscreenVideoPlayer: NSObject {
172
223
 
173
224
  setupChromecast()
174
225
  setupObservers()
226
+ setupSubtitleSelectionObserver()
227
+ }
228
+
229
+ private func setupSubtitleSelectionObserver() {
230
+ guard subtitleSelectionObserver == nil, let playerItem else { return }
231
+ subtitleSelectionObserver = NotificationCenter.default.addObserver(
232
+ forName: AVPlayerItem.mediaSelectionDidChangeNotification,
233
+ object: playerItem,
234
+ queue: .main
235
+ ) { [weak self] _ in
236
+ self?.refreshSubtitleButton()
237
+ }
238
+ }
239
+
240
+ private static func isLikelyHLS(_ value: String) -> Bool {
241
+ guard let url = resolveMediaURL(value) else { return false }
242
+ return HLSVideoAssetFactory.isHLSStream(url)
243
+ }
244
+
245
+ private var validNonHlsSubtitleTracks: [(index: Int, track: VideoSubtitleTrack)] {
246
+ subtitleTracks.enumerated().compactMap { index, track in
247
+ guard track.resolvedURL != nil else { return nil }
248
+ return (index, track)
249
+ }
250
+ }
251
+
252
+ private func shouldShowCustomSubtitleButton() -> Bool {
253
+ guard showControls else { return false }
254
+ // HLS usually has native subtitle selection UI; this custom button targets MP4 + sidecar subtitles.
255
+ return !Self.isLikelyHLS(videoUrl)
256
+ }
257
+
258
+ private func shouldShowNonHlsSubtitleButton() -> Bool {
259
+ return nonHlsSubtitleModeEnabled && validNonHlsSubtitleTracks.count > 1 && shouldShowCustomSubtitleButton()
260
+ }
261
+
262
+ private func refreshSubtitleButton() {
263
+ DispatchQueue.main.async { [weak self] in
264
+ self?.installOrUpdateSubtitleButtonIfNeeded()
265
+ }
266
+ }
267
+
268
+ private func installOrUpdateSubtitleButtonIfNeeded() {
269
+ guard shouldShowCustomSubtitleButton(),
270
+ let playerVC = playerViewController else {
271
+ subtitleButton?.removeFromSuperview()
272
+ subtitleButton = nil
273
+ return
274
+ }
275
+
276
+ if subtitleButton == nil {
277
+ guard let overlayView = playerVC.contentOverlayView else {
278
+ return
279
+ }
280
+ let button = UIButton(type: .system)
281
+ button.translatesAutoresizingMaskIntoConstraints = false
282
+ if #available(iOS 13.0, *) {
283
+ button.setImage(UIImage(systemName: "captions.bubble"), for: .normal)
284
+ } else {
285
+ button.setTitle("Sub", for: .normal)
286
+ }
287
+ button.tintColor = .white
288
+ button.backgroundColor = UIColor.black.withAlphaComponent(0.45)
289
+ button.layer.cornerRadius = 22
290
+ button.clipsToBounds = true
291
+ button.contentHorizontalAlignment = .center
292
+ button.contentVerticalAlignment = .center
293
+
294
+ if #available(iOS 14.0, *) {
295
+ button.showsMenuAsPrimaryAction = true
296
+ } else {
297
+ button.addTarget(self, action: #selector(showSubtitleActionSheet), for: .touchUpInside)
298
+ }
299
+
300
+ overlayView.addSubview(button)
301
+ NSLayoutConstraint.activate([
302
+ button.topAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.topAnchor, constant: 60),
303
+ button.leadingAnchor.constraint(equalTo: overlayView.safeAreaLayoutGuide.leadingAnchor, constant: 16),
304
+ button.widthAnchor.constraint(equalToConstant: 44),
305
+ button.heightAnchor.constraint(equalToConstant: 44)
306
+ ])
307
+ subtitleButton = button
308
+ }
309
+ subtitleButton?.isHidden = false
310
+ subtitleButton?.alpha = 1
311
+
312
+ if shouldShowNonHlsSubtitleButton() {
313
+ if #available(iOS 14.0, *) {
314
+ subtitleButton?.menu = buildNonHlsSubtitleMenu()
315
+ }
316
+ updateNonHlsSubtitleButtonAccessibility()
317
+ return
318
+ }
319
+
320
+ guard let playerItem,
321
+ let group = playerItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible),
322
+ !group.options.isEmpty else {
323
+ subtitleButton?.removeFromSuperview()
324
+ subtitleButton = nil
325
+ return
326
+ }
327
+
328
+ if #available(iOS 14.0, *) {
329
+ subtitleButton?.menu = buildSubtitleMenu(group: group)
330
+ }
331
+ updateSubtitleButtonAccessibility(group: group)
332
+ }
333
+
334
+ private func updateSubtitleButtonAccessibility(group: AVMediaSelectionGroup) {
335
+ let selectedOption = playerItem?.selectedMediaOption(in: group)
336
+ let currentTitle = selectedOption.map(Self.subtitleDisplayName) ?? "Off"
337
+ subtitleButton?.accessibilityLabel = "Subtitles"
338
+ subtitleButton?.accessibilityValue = currentTitle
339
+ }
340
+
341
+ @available(iOS 14.0, *)
342
+ private func buildSubtitleMenu(group: AVMediaSelectionGroup) -> UIMenu {
343
+ let selectedOption = playerItem?.selectedMediaOption(in: group)
344
+
345
+ let offAction = UIAction(
346
+ title: "Off",
347
+ state: selectedOption == nil ? .on : .off
348
+ ) { [weak self] _ in
349
+ guard let self, let playerItem = self.playerItem else { return }
350
+ playerItem.select(nil, in: group)
351
+ self.refreshSubtitleButton()
352
+ }
353
+
354
+ let trackActions = group.options.map { option in
355
+ UIAction(
356
+ title: Self.subtitleDisplayName(option),
357
+ state: option == selectedOption ? .on : .off
358
+ ) { [weak self] _ in
359
+ guard let self, let playerItem = self.playerItem else { return }
360
+ playerItem.select(option, in: group)
361
+ self.refreshSubtitleButton()
362
+ }
363
+ }
364
+
365
+ return UIMenu(title: "Subtitles", children: [offAction] + trackActions)
366
+ }
367
+
368
+ @available(iOS 14.0, *)
369
+ private func buildNonHlsSubtitleMenu() -> UIMenu {
370
+ let offAction = UIAction(
371
+ title: "Off",
372
+ state: selectedNonHlsSubtitleIndex == nil ? .on : .off
373
+ ) { [weak self] _ in
374
+ self?.switchNonHlsSubtitleTrack(index: nil)
375
+ }
376
+
377
+ let trackActions = validNonHlsSubtitleTracks.map { index, track in
378
+ UIAction(
379
+ title: Self.nonHlsSubtitleDisplayName(track: track, index: index),
380
+ state: selectedNonHlsSubtitleIndex == index ? .on : .off
381
+ ) { [weak self] _ in
382
+ self?.switchNonHlsSubtitleTrack(index: index)
383
+ }
384
+ }
385
+
386
+ return UIMenu(title: "Subtitles", children: [offAction] + trackActions)
387
+ }
388
+
389
+ @objc
390
+ private func showSubtitleActionSheet() {
391
+ if shouldShowNonHlsSubtitleButton() {
392
+ showNonHlsSubtitleActionSheet()
393
+ return
394
+ }
395
+
396
+ guard let playerVC = playerViewController,
397
+ let playerItem,
398
+ let group = playerItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible),
399
+ !group.options.isEmpty else {
400
+ return
401
+ }
402
+
403
+ let sheet = UIAlertController(title: "Subtitles", message: nil, preferredStyle: .actionSheet)
404
+ let selectedOption = playerItem.selectedMediaOption(in: group)
405
+
406
+ let offTitle = selectedOption == nil ? "Off ✓" : "Off"
407
+ sheet.addAction(UIAlertAction(title: offTitle, style: .default) { [weak self] _ in
408
+ guard let self else { return }
409
+ playerItem.select(nil, in: group)
410
+ self.refreshSubtitleButton()
411
+ })
412
+
413
+ for option in group.options {
414
+ let isSelected = option == selectedOption
415
+ let title = isSelected ? "\(Self.subtitleDisplayName(option)) ✓" : Self.subtitleDisplayName(option)
416
+ sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
417
+ guard let self else { return }
418
+ playerItem.select(option, in: group)
419
+ self.refreshSubtitleButton()
420
+ })
421
+ }
422
+
423
+ sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
424
+ if let popover = sheet.popoverPresentationController,
425
+ let subtitleButton {
426
+ popover.sourceView = subtitleButton
427
+ popover.sourceRect = subtitleButton.bounds
428
+ }
429
+ playerVC.present(sheet, animated: true)
430
+ }
431
+
432
+ private func showNonHlsSubtitleActionSheet() {
433
+ guard let playerVC = playerViewController else { return }
434
+
435
+ let sheet = UIAlertController(title: "Subtitles", message: nil, preferredStyle: .actionSheet)
436
+ let offTitle = selectedNonHlsSubtitleIndex == nil ? "Off ✓" : "Off"
437
+ sheet.addAction(UIAlertAction(title: offTitle, style: .default) { [weak self] _ in
438
+ self?.switchNonHlsSubtitleTrack(index: nil)
439
+ })
440
+
441
+ for (index, track) in validNonHlsSubtitleTracks {
442
+ let isSelected = selectedNonHlsSubtitleIndex == index
443
+ let label = Self.nonHlsSubtitleDisplayName(track: track, index: index)
444
+ let title = isSelected ? "\(label) ✓" : label
445
+ sheet.addAction(UIAlertAction(title: title, style: .default) { [weak self] _ in
446
+ self?.switchNonHlsSubtitleTrack(index: index)
447
+ })
448
+ }
449
+
450
+ sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
451
+ if let popover = sheet.popoverPresentationController,
452
+ let subtitleButton {
453
+ popover.sourceView = subtitleButton
454
+ popover.sourceRect = subtitleButton.bounds
455
+ }
456
+ playerVC.present(sheet, animated: true)
457
+ }
458
+
459
+ private static func subtitleDisplayName(_ option: AVMediaSelectionOption) -> String {
460
+ let optionDisplayName = option.displayName.trimmingCharacters(in: .whitespacesAndNewlines)
461
+ if !optionDisplayName.isEmpty {
462
+ return localizedLanguageName(from: optionDisplayName) ?? optionDisplayName
463
+ }
464
+ if let tag = option.extendedLanguageTag, !tag.isEmpty {
465
+ return localizedLanguageName(from: tag) ?? tag
466
+ }
467
+ if let localeId = option.locale?.identifier, !localeId.isEmpty {
468
+ return localizedLanguageName(from: localeId) ?? localeId
469
+ }
470
+ return "Unknown"
471
+ }
472
+
473
+ private static func nonHlsSubtitleDisplayName(track: VideoSubtitleTrack, index: Int) -> String {
474
+ if let language = track.language?.trimmingCharacters(in: .whitespacesAndNewlines),
475
+ !language.isEmpty {
476
+ return localizedLanguageName(from: language) ?? language
477
+ }
478
+ return "Subtitle \(index + 1)"
479
+ }
480
+
481
+ private static func localizedLanguageName(from rawValue: String) -> String? {
482
+ let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
483
+ guard !trimmed.isEmpty else { return nil }
484
+
485
+ let normalized = trimmed.replacingOccurrences(of: "_", with: "-")
486
+
487
+ if let identifierName = Locale.current.localizedString(forIdentifier: normalized),
488
+ !identifierName.isEmpty,
489
+ identifierName.caseInsensitiveCompare(trimmed) != .orderedSame {
490
+ return identifierName
491
+ }
492
+
493
+ if let languageCodeName = Locale.current.localizedString(forLanguageCode: normalized.lowercased()),
494
+ !languageCodeName.isEmpty,
495
+ languageCodeName.caseInsensitiveCompare(trimmed) != .orderedSame {
496
+ return languageCodeName
497
+ }
498
+
499
+ return nil
500
+ }
501
+
502
+ private func updateNonHlsSubtitleButtonAccessibility() {
503
+ let title: String
504
+ if let index = selectedNonHlsSubtitleIndex,
505
+ subtitleTracks.indices.contains(index) {
506
+ title = Self.nonHlsSubtitleDisplayName(track: subtitleTracks[index], index: index)
507
+ } else {
508
+ title = "Off"
509
+ }
510
+ subtitleButton?.accessibilityLabel = "Subtitles"
511
+ subtitleButton?.accessibilityValue = title
512
+ }
513
+
514
+ private func switchNonHlsSubtitleTrack(index: Int?) {
515
+ guard nonHlsSubtitleModeEnabled,
516
+ selectedNonHlsSubtitleIndex != index else {
517
+ refreshSubtitleButton()
518
+ return
519
+ }
520
+ guard let videoURL = Self.resolveMediaURL(videoUrl) else { return }
521
+
522
+ if let index, !validNonHlsSubtitleTracks.contains(where: { $0.index == index }) {
523
+ return
524
+ }
525
+
526
+ selectedNonHlsSubtitleIndex = index
527
+ nonHlsBootstrapLoadId = nil
528
+
529
+ Task {
530
+ let baseAsset = makeVideoAsset(url: videoURL, subtitleTracks: [])
531
+ let trackSelection: [VideoSubtitleTrack]
532
+ if let index {
533
+ trackSelection = [subtitleTracks[index]]
534
+ } else {
535
+ trackSelection = []
536
+ }
537
+
538
+ let newItem = await ProgressiveVideoPlayerItemFactory.createPlayerItem(
539
+ videoAsset: baseAsset,
540
+ subtitleTracks: trackSelection
541
+ )
542
+
543
+ await MainActor.run {
544
+ guard self.nonHlsSubtitleModeEnabled,
545
+ self.selectedNonHlsSubtitleIndex == index else {
546
+ return
547
+ }
548
+ self.replaceCurrentItemPreservingPlayback(with: newItem)
549
+ self.refreshSubtitleButton()
550
+ }
551
+ }
552
+ }
553
+
554
+ private func replaceCurrentItemPreservingPlayback(with newItem: AVPlayerItem) {
555
+ guard let player else { return }
556
+
557
+ let previousTime = player.currentTime()
558
+ let wasPlaying = player.rate > 0
559
+ let targetRate = rate
560
+
561
+ let oldItem = playerItem
562
+ removePlayerItemObservers(oldItem)
563
+ playerItem = newItem
564
+ player.replaceCurrentItem(with: newItem)
565
+ addPlayerItemObservers(newItem)
566
+ setupSubtitleSelectionObserver()
567
+
568
+ player.seek(to: previousTime) { _ in
569
+ if wasPlaying {
570
+ player.play()
571
+ player.rate = targetRate
572
+ }
573
+ }
574
+ }
575
+
576
+ private func addPlayerItemObservers(_ item: AVPlayerItem) {
577
+ item.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
578
+ NotificationCenter.default.addObserver(
579
+ self,
580
+ selector: #selector(playerDidFinishPlaying),
581
+ name: .AVPlayerItemDidPlayToEndTime,
582
+ object: item
583
+ )
584
+ }
585
+
586
+ private func removePlayerItemObservers(_ item: AVPlayerItem?) {
587
+ guard let item else { return }
588
+ item.removeObserver(self, forKeyPath: "status")
589
+ NotificationCenter.default.removeObserver(
590
+ self,
591
+ name: .AVPlayerItemDidPlayToEndTime,
592
+ object: item
593
+ )
594
+ if let subtitleSelectionObserver {
595
+ NotificationCenter.default.removeObserver(subtitleSelectionObserver)
596
+ self.subtitleSelectionObserver = nil
597
+ }
175
598
  }
176
599
 
177
600
  private func setupChromecast() {
@@ -210,16 +633,9 @@ class FullscreenVideoPlayer: NSObject {
210
633
  private func setupObservers() {
211
634
  guard let player = player else { return }
212
635
 
213
- // Observe player status
214
- playerItem?.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
215
-
216
- // Observe playback end
217
- NotificationCenter.default.addObserver(
218
- self,
219
- selector: #selector(playerDidFinishPlaying),
220
- name: .AVPlayerItemDidPlayToEndTime,
221
- object: playerItem
222
- )
636
+ if let playerItem {
637
+ addPlayerItemObservers(playerItem)
638
+ }
223
639
 
224
640
  // Observe time updates
225
641
  let interval = CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
@@ -235,7 +651,8 @@ class FullscreenVideoPlayer: NSObject {
235
651
  if keyPath == "status" {
236
652
  if let item = object as? AVPlayerItem {
237
653
  if item.status == .readyToPlay {
238
- onReady?()
654
+ refreshSubtitleButton()
655
+ emitReadyIfNeeded()
239
656
  }
240
657
  }
241
658
  } else if keyPath == "rate" {
@@ -278,6 +695,7 @@ class FullscreenVideoPlayer: NSObject {
278
695
  presentingViewController = viewController
279
696
  viewController.present(playerVC, animated: true) {
280
697
  playerVC.presentationController?.delegate = self
698
+ self.refreshSubtitleButton()
281
699
  self.play()
282
700
  completion()
283
701
  }
@@ -321,8 +739,15 @@ class FullscreenVideoPlayer: NSObject {
321
739
  }
322
740
 
323
741
  private func cleanup(stopRemoteMedia: Bool = false) {
742
+ nonHlsBootstrapLoadId = nil
324
743
  castController?.detach(stopRemoteMedia: stopRemoteMedia)
325
744
  castController = nil
745
+ if let subtitleSelectionObserver {
746
+ NotificationCenter.default.removeObserver(subtitleSelectionObserver)
747
+ self.subtitleSelectionObserver = nil
748
+ }
749
+ subtitleButton?.removeFromSuperview()
750
+ subtitleButton = nil
326
751
  contentKeySession?.setDelegate(nil, queue: nil)
327
752
  contentKeySession = nil
328
753
  hlsResourceLoader = nil
@@ -331,7 +756,7 @@ class FullscreenVideoPlayer: NSObject {
331
756
  player?.removeTimeObserver(observer)
332
757
  timeObserver = nil
333
758
  }
334
- playerItem?.removeObserver(self, forKeyPath: "status")
759
+ removePlayerItemObservers(playerItem)
335
760
  NotificationCenter.default.removeObserver(self)
336
761
  player?.pause()
337
762
  player = nil
@@ -391,6 +816,12 @@ class FullscreenVideoPlayer: NSObject {
391
816
  onExit?(time)
392
817
  }
393
818
 
819
+ private func emitReadyIfNeeded() {
820
+ guard !didEmitReady, let onReady else { return }
821
+ didEmitReady = true
822
+ onReady()
823
+ }
824
+
394
825
  // MARK: - Playback Control
395
826
 
396
827
  func play() {
@@ -503,6 +934,9 @@ class FullscreenVideoPlayer: NSObject {
503
934
 
504
935
  func setOnReady(_ callback: @escaping () -> Void) {
505
936
  self.onReady = callback
937
+ if playerItem?.status == .readyToPlay {
938
+ emitReadyIfNeeded()
939
+ }
506
940
  }
507
941
 
508
942
  func setOnEnd(_ callback: @escaping () -> Void) {
@@ -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.4"
11
+ private let pluginVersion: String = "8.2.5"
12
12
  public let identifier = "VideoPlayerPlugin"
13
13
  public let jsName = "VideoPlayer"
14
14
  public let pluginMethods: [CAPPluginMethod] = [
@@ -77,6 +77,16 @@ final class HLSSubtitlePlaylistTests: XCTestCase {
77
77
  XCTAssertEqual(result.asset.url.scheme, "file")
78
78
  }
79
79
 
80
+ func testIsHLSStreamDetectsMpegurlQuery() {
81
+ let url = URL(string: "https://cdn.example.com/video?format=mpegurl")!
82
+ XCTAssertTrue(HLSVideoAssetFactory.isHLSStream(url))
83
+ }
84
+
85
+ func testIsHLSStreamRejectsProgressiveMp4() {
86
+ let url = URL(string: "https://cdn.example.com/video.mp4")!
87
+ XCTAssertFalse(HLSVideoAssetFactory.isHLSStream(url))
88
+ }
89
+
80
90
  func testMakeAssetUsesResourceLoaderForLocalHLSSubtitles() {
81
91
  let videoURL = URL(fileURLWithPath: "/var/mobile/Containers/Data/Application/stream.m3u8")
82
92
  let tracks = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-video-player",
3
- "version": "8.2.4",
3
+ "version": "8.2.5",
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",