@capgo/capacitor-video-player 8.1.23 → 8.1.25

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.
@@ -14,10 +14,14 @@ class FullscreenVideoPlayer: NSObject {
14
14
  private var pipEnabled: Bool
15
15
  private var showControls: Bool
16
16
  private var chromecast: Bool
17
+ private var chromecastUrl: String?
17
18
  private var title: String?
18
19
  private var smallTitle: String?
19
20
  private var artwork: String?
20
21
  private var rate: Float
22
+ private var audioCategory: String?
23
+ private var didActivateAudioSession: Bool = false
24
+ private var didEmitExit: Bool = false
21
25
  private var timeObserver: Any?
22
26
  private var onPlay: (() -> Void)?
23
27
  private var onPause: (() -> Void)?
@@ -26,6 +30,8 @@ class FullscreenVideoPlayer: NSObject {
26
30
  private var onExit: ((Double) -> Void)?
27
31
  private var fairplayCertificateUrl: String?
28
32
  private var fairplayContentKeySpcUrl: String?
33
+ private var fairplayAssetId: String?
34
+ private var widevineLicenseUrl: String?
29
35
  private var contentKeySession: AVContentKeySession?
30
36
  private var castController: VideoPlayerCastController?
31
37
  private weak var presentingViewController: UIViewController?
@@ -42,13 +48,17 @@ class FullscreenVideoPlayer: NSObject {
42
48
  pipEnabled: Bool,
43
49
  showControls: Bool,
44
50
  chromecast: Bool,
51
+ chromecastUrl: String? = nil,
45
52
  title: String? = nil,
46
53
  smallTitle: String? = nil,
47
54
  artwork: String? = nil,
48
55
  subtitleUrl: String? = nil,
49
56
  subtitleLanguage: String? = nil,
50
57
  fairplayCertificateUrl: String? = nil,
51
- fairplayContentKeySpcUrl: String? = nil
58
+ fairplayContentKeySpcUrl: String? = nil,
59
+ fairplayAssetId: String? = nil,
60
+ widevineLicenseUrl: String? = nil,
61
+ audioCategory: String? = nil
52
62
  ) {
53
63
  self.playerId = playerId
54
64
  self.videoUrl = url
@@ -58,6 +68,7 @@ class FullscreenVideoPlayer: NSObject {
58
68
  self.pipEnabled = pipEnabled
59
69
  self.showControls = showControls
60
70
  self.chromecast = chromecast
71
+ self.chromecastUrl = chromecastUrl
61
72
  self.title = title
62
73
  self.smallTitle = smallTitle
63
74
  self.artwork = artwork
@@ -65,6 +76,9 @@ class FullscreenVideoPlayer: NSObject {
65
76
  self.subtitleLanguage = subtitleLanguage
66
77
  self.fairplayCertificateUrl = fairplayCertificateUrl
67
78
  self.fairplayContentKeySpcUrl = fairplayContentKeySpcUrl
79
+ self.fairplayAssetId = fairplayAssetId
80
+ self.widevineLicenseUrl = widevineLicenseUrl
81
+ self.audioCategory = audioCategory
68
82
  super.init()
69
83
  }
70
84
 
@@ -74,6 +88,8 @@ class FullscreenVideoPlayer: NSObject {
74
88
  return
75
89
  }
76
90
 
91
+ configureAudioSession()
92
+
77
93
  guard let subtitleUrlString = subtitleUrl,
78
94
  !subtitleUrlString.isEmpty,
79
95
  let subtitleURL = URL(string: subtitleUrlString) else {
@@ -113,7 +129,8 @@ class FullscreenVideoPlayer: NSObject {
113
129
  hlsResourceLoader = result.resourceLoader
114
130
  let asset = result.asset
115
131
 
116
- if let certUrl = fairplayCertificateUrl, !certUrl.isEmpty {
132
+ if let certUrl = fairplayCertificateUrl, !certUrl.isEmpty,
133
+ let spcUrl = fairplayContentKeySpcUrl, !spcUrl.isEmpty {
117
134
  let session = AVContentKeySession(keySystem: .fairPlayStreaming)
118
135
  session.setDelegate(self, queue: DispatchQueue.global(qos: .default))
119
136
  session.addContentKeyRecipient(asset)
@@ -132,9 +149,7 @@ class FullscreenVideoPlayer: NSObject {
132
149
  playerViewController?.player = player
133
150
  playerViewController?.showsPlaybackControls = showControls
134
151
  playerViewController?.allowsPictureInPicturePlayback = pipEnabled
135
- if pipEnabled {
136
- playerViewController?.delegate = self
137
- }
152
+ playerViewController?.delegate = self
138
153
 
139
154
  setupChromecast()
140
155
  setupObservers()
@@ -147,11 +162,19 @@ class FullscreenVideoPlayer: NSObject {
147
162
  return
148
163
  }
149
164
 
165
+ let castVideoUrl: String
166
+ if let chromecastUrl = chromecastUrl, !chromecastUrl.isEmpty {
167
+ castVideoUrl = chromecastUrl
168
+ } else {
169
+ castVideoUrl = videoUrl
170
+ }
171
+
150
172
  castController = VideoPlayerCastController(
151
- videoUrl: videoUrl,
173
+ videoUrl: castVideoUrl,
152
174
  title: title,
153
175
  smallTitle: smallTitle,
154
- artwork: artwork
176
+ artwork: artwork,
177
+ widevineLicenseUrl: widevineLicenseUrl
155
178
  )
156
179
  castController?.setOnPlay { [weak self] in
157
180
  self?.onPlay?()
@@ -235,23 +258,54 @@ class FullscreenVideoPlayer: NSObject {
235
258
 
236
259
  presentingViewController = viewController
237
260
  viewController.present(playerVC, animated: true) {
261
+ playerVC.presentationController?.delegate = self
238
262
  self.play()
239
263
  completion()
240
264
  }
241
265
  }
242
266
 
267
+ func show(on viewController: UIViewController, completion: @escaping () -> Void) {
268
+ guard let playerVC = playerViewController else {
269
+ completion()
270
+ return
271
+ }
272
+ if playerVC.presentingViewController != nil {
273
+ completion()
274
+ return
275
+ }
276
+ presentingViewController = viewController
277
+ viewController.present(playerVC, animated: true) {
278
+ playerVC.presentationController?.delegate = self
279
+ completion()
280
+ }
281
+ }
282
+
283
+ func hide(completion: @escaping () -> Void) {
284
+ guard let playerVC = playerViewController else {
285
+ completion()
286
+ return
287
+ }
288
+ if playerVC.presentingViewController == nil {
289
+ completion()
290
+ return
291
+ }
292
+ playerVC.dismiss(animated: true) {
293
+ completion()
294
+ }
295
+ }
296
+
243
297
  func dismiss() {
244
298
  let currentTime = getCurrentTime()
245
- castController?.detach(stopRemoteMedia: true)
246
299
  playerViewController?.dismiss(animated: true) { [weak self] in
247
- self?.cleanup()
248
- self?.onExit?(currentTime)
300
+ self?.emitExitIfNeeded(currentTime: currentTime)
249
301
  }
250
302
  }
251
303
 
252
- private func cleanup() {
253
- castController?.detach(stopRemoteMedia: false)
304
+ private func cleanup(stopRemoteMedia: Bool = false) {
305
+ castController?.detach(stopRemoteMedia: stopRemoteMedia)
254
306
  castController = nil
307
+ contentKeySession?.setDelegate(nil, queue: nil)
308
+ contentKeySession = nil
255
309
  hlsResourceLoader = nil
256
310
  if let observer = timeObserver {
257
311
  player?.removeObserver(self, forKeyPath: "rate")
@@ -264,6 +318,58 @@ class FullscreenVideoPlayer: NSObject {
264
318
  player = nil
265
319
  playerItem = nil
266
320
  playerViewController = nil
321
+ deactivateAudioSessionIfNeeded()
322
+ }
323
+
324
+ private func configureAudioSession() {
325
+ guard let audioCategory else { return }
326
+
327
+ let session = AVAudioSession.sharedInstance()
328
+ do {
329
+ switch audioCategory {
330
+ case "ambient":
331
+ try session.setCategory(.ambient, mode: .default, options: [.mixWithOthers])
332
+ case "playback":
333
+ try session.setCategory(.playback, mode: .default, options: [.mixWithOthers])
334
+ case "moviePlayback":
335
+ if #available(iOS 13.0, *) {
336
+ try session.setCategory(
337
+ .playback,
338
+ mode: .moviePlayback,
339
+ policy: .longFormVideo,
340
+ options: [.mixWithOthers]
341
+ )
342
+ } else {
343
+ try session.setCategory(.playback, mode: .moviePlayback, options: [.mixWithOthers])
344
+ }
345
+ default:
346
+ return
347
+ }
348
+
349
+ try session.setActive(true)
350
+ didActivateAudioSession = true
351
+ } catch {
352
+ print("Error configuring AVAudioSession: \(error)")
353
+ }
354
+ }
355
+
356
+ private func deactivateAudioSessionIfNeeded() {
357
+ guard didActivateAudioSession else { return }
358
+ do {
359
+ try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
360
+ didActivateAudioSession = false
361
+ } catch {
362
+ print("Error deactivating AVAudioSession: \(error)")
363
+ }
364
+ }
365
+
366
+ private func emitExitIfNeeded(currentTime: Double? = nil) {
367
+ guard !didEmitExit else { return }
368
+ didEmitExit = true
369
+
370
+ let time = currentTime ?? getCurrentTime()
371
+ cleanup(stopRemoteMedia: true)
372
+ onExit?(time)
267
373
  }
268
374
 
269
375
  // MARK: - Playback Control
@@ -396,6 +502,19 @@ class FullscreenVideoPlayer: NSObject {
396
502
  // MARK: - AVPlayerViewControllerDelegate (Picture in Picture)
397
503
 
398
504
  extension FullscreenVideoPlayer: AVPlayerViewControllerDelegate {
505
+ func playerViewControllerWillEndFullScreenPresentation(
506
+ _ playerViewController: AVPlayerViewController,
507
+ withAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
508
+ ) {
509
+ coordinator.animate(alongsideTransition: nil) { [weak self] _ in
510
+ self?.emitExitIfNeeded()
511
+ }
512
+ }
513
+
514
+ func playerViewControllerDidEndFullScreenPresentation(_ playerViewController: AVPlayerViewController) {
515
+ emitExitIfNeeded()
516
+ }
517
+
399
518
  func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(
400
519
  _ playerViewController: AVPlayerViewController
401
520
  ) -> Bool {
@@ -423,6 +542,14 @@ extension FullscreenVideoPlayer: AVPlayerViewControllerDelegate {
423
542
  }
424
543
  }
425
544
 
545
+ // MARK: - UIAdaptivePresentationControllerDelegate
546
+
547
+ extension FullscreenVideoPlayer: UIAdaptivePresentationControllerDelegate {
548
+ func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
549
+ emitExitIfNeeded()
550
+ }
551
+ }
552
+
426
553
  // MARK: - AVContentKeySessionDelegate (FairPlay DRM)
427
554
 
428
555
  extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
@@ -434,6 +561,61 @@ extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
434
561
  handleFairPlayKeyRequest(keyRequest)
435
562
  }
436
563
 
564
+ private func fairPlayContentIdentifierData(from identifier: Any?) -> Data? {
565
+ if let data = identifier as? Data {
566
+ return data.isEmpty ? nil : data
567
+ }
568
+
569
+ let identifierString: String?
570
+ if let url = identifier as? URL {
571
+ identifierString = url.absoluteString
572
+ } else if let string = identifier as? String {
573
+ identifierString = string
574
+ } else {
575
+ identifierString = nil
576
+ }
577
+
578
+ guard var string = identifierString?.trimmingCharacters(in: .whitespacesAndNewlines),
579
+ !string.isEmpty else {
580
+ return nil
581
+ }
582
+
583
+ if string.hasPrefix("skd://") {
584
+ string.removeFirst("skd://".count)
585
+ } else if string.hasPrefix("skd:") {
586
+ string.removeFirst("skd:".count)
587
+ }
588
+
589
+ while string.hasPrefix("/") {
590
+ string.removeFirst()
591
+ }
592
+
593
+ return string.data(using: .utf8)
594
+ }
595
+
596
+ private func normalizeFairPlayCkcData(_ data: Data) -> Data {
597
+ if let object = try? JSONSerialization.jsonObject(with: data),
598
+ let dict = object as? [String: Any] {
599
+ let candidateKeys = ["ckc", "CKC", "license", "License", "data"]
600
+ for key in candidateKeys {
601
+ if let value = dict[key] as? String,
602
+ let decoded = Data(base64Encoded: value) {
603
+ return decoded
604
+ }
605
+ }
606
+ }
607
+
608
+ if let string = String(data: data, encoding: .utf8) {
609
+ let trimmed = string.trimmingCharacters(in: .whitespacesAndNewlines)
610
+ .trimmingCharacters(in: CharacterSet(charactersIn: "\"'"))
611
+ if let decoded = Data(base64Encoded: trimmed) {
612
+ return decoded
613
+ }
614
+ }
615
+
616
+ return data
617
+ }
618
+
437
619
  private func handleFairPlayKeyRequest(_ keyRequest: AVContentKeyRequest) {
438
620
  guard let certUrlString = fairplayCertificateUrl,
439
621
  let certUrl = URL(string: certUrlString),
@@ -447,7 +629,17 @@ extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
447
629
  }
448
630
 
449
631
  // 1. Fetch the FairPlay certificate
450
- URLSession.shared.dataTask(with: certUrl) { certData, _, certError in
632
+ URLSession.shared.dataTask(with: certUrl) { [self] certData, certResponse, certError in
633
+ if let httpResponse = certResponse as? HTTPURLResponse,
634
+ !(200...299).contains(httpResponse.statusCode) {
635
+ keyRequest.processContentKeyResponseError(
636
+ NSError(domain: "VideoPlayer", code: -2, userInfo: [
637
+ NSLocalizedDescriptionKey: "Failed to fetch FairPlay certificate (HTTP \(httpResponse.statusCode))"
638
+ ])
639
+ )
640
+ return
641
+ }
642
+
451
643
  guard let certData = certData else {
452
644
  keyRequest.processContentKeyResponseError(
453
645
  certError ?? NSError(domain: "VideoPlayer", code: -2,
@@ -457,11 +649,17 @@ extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
457
649
  }
458
650
 
459
651
  // 2. Create SPC (Server Playback Context) using the certificate
460
- // contentIdentifier is nil here; override if your license server requires
461
- // a specific content ID extracted from the asset URL scheme
652
+ let contentIdentifier: Data? = {
653
+ if let assetId = fairplayAssetId?.trimmingCharacters(in: .whitespacesAndNewlines),
654
+ !assetId.isEmpty {
655
+ return assetId.data(using: .utf8)
656
+ }
657
+ return fairPlayContentIdentifierData(from: keyRequest.identifier)
658
+ }()
659
+
462
660
  keyRequest.makeStreamingContentKeyRequestData(
463
661
  forApp: certData,
464
- contentIdentifier: nil,
662
+ contentIdentifier: contentIdentifier,
465
663
  options: [AVContentKeyRequestProtocolVersionsKey: [1]]
466
664
  ) { spcData, spcError in
467
665
  guard let spcData = spcData else {
@@ -478,7 +676,17 @@ extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
478
676
  spcRequest.httpBody = spcData
479
677
  spcRequest.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
480
678
 
481
- URLSession.shared.dataTask(with: spcRequest) { ckcData, _, ckcError in
679
+ URLSession.shared.dataTask(with: spcRequest) { [self] ckcData, ckcResponse, ckcError in
680
+ if let httpResponse = ckcResponse as? HTTPURLResponse,
681
+ !(200...299).contains(httpResponse.statusCode) {
682
+ keyRequest.processContentKeyResponseError(
683
+ NSError(domain: "VideoPlayer", code: -3, userInfo: [
684
+ NSLocalizedDescriptionKey: "Failed to obtain FairPlay CKC (HTTP \(httpResponse.statusCode))"
685
+ ])
686
+ )
687
+ return
688
+ }
689
+
482
690
  guard let ckcData = ckcData else {
483
691
  keyRequest.processContentKeyResponseError(
484
692
  ckcError ?? NSError(domain: "VideoPlayer", code: -3,
@@ -488,7 +696,8 @@ extension FullscreenVideoPlayer: AVContentKeySessionDelegate {
488
696
  }
489
697
 
490
698
  // 4. Provide the CKC to AVFoundation to decrypt the content
491
- let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: ckcData)
699
+ let normalizedCkcData = normalizeFairPlayCkcData(ckcData)
700
+ let keyResponse = AVContentKeyResponse(fairPlayStreamingKeyResponseData: normalizedCkcData)
492
701
  keyRequest.processContentKeyResponse(keyResponse)
493
702
  }.resume()
494
703
  }
@@ -1,5 +1,6 @@
1
1
  import AVFoundation
2
2
  import Foundation
3
+ import UniformTypeIdentifiers
3
4
 
4
5
  enum HLSVideoAssetFactory {
5
6
  static func isHLSStream(_ url: URL) -> Bool {
@@ -43,11 +44,17 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
43
44
  static let videoScheme = "capgohls"
44
45
  static let subtitlePlaylistScheme = "capgohls-sub"
45
46
  private static let subtitleGroupID = "capgosubs"
47
+ private static let mpegURLContentType = UTType.m3uPlaylist.identifier
46
48
 
47
49
  private let originalVideoURL: URL
48
50
  private let subtitleURL: URL
49
51
  private let language: String
50
52
 
53
+ private struct LoadedResource {
54
+ let data: Data
55
+ let contentType: String?
56
+ }
57
+
51
58
  init(videoURL: URL, subtitleURL: URL, language: String) {
52
59
  self.originalVideoURL = videoURL
53
60
  self.subtitleURL = subtitleURL
@@ -80,8 +87,8 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
80
87
 
81
88
  Task {
82
89
  do {
83
- let data = try await loadData(for: requestURL)
84
- fulfill(loadingRequest, with: data)
90
+ let resource = try await loadData(for: requestURL)
91
+ fulfill(loadingRequest, with: resource)
85
92
  } catch {
86
93
  loadingRequest.finishLoading(with: error)
87
94
  }
@@ -90,25 +97,36 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
90
97
  return true
91
98
  }
92
99
 
93
- private func loadData(for requestURL: URL) async throws -> Data {
100
+ private func loadData(for requestURL: URL) async throws -> LoadedResource {
94
101
  if requestURL.scheme == Self.subtitlePlaylistScheme {
95
- return Data(subtitleMediaPlaylist().utf8)
102
+ let data = Data(subtitleMediaPlaylist().utf8)
103
+ return LoadedResource(data: data, contentType: Self.mpegURLContentType)
96
104
  }
97
105
 
98
106
  let fetchURL = originalURL(for: requestURL)
99
- let (data, _) = try await URLSession.shared.data(from: fetchURL)
107
+ let (data, response) = try await URLSession.shared.data(from: fetchURL)
108
+ if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
109
+ throw URLError(.badServerResponse)
110
+ }
111
+
100
112
  let manifest = String(data: data, encoding: .utf8) ?? ""
101
113
 
102
114
  guard hasStreamInfTags(manifest) else {
103
- return data
115
+ let contentType = Self.contentTypeIdentifier(from: response)
116
+ return LoadedResource(data: data, contentType: contentType)
104
117
  }
105
118
 
106
119
  guard let subtitlePlaylistURI = Self.subtitlePlaylistURL(for: originalVideoURL)?.absoluteString else {
107
- return data
120
+ let contentType = Self.contentTypeIdentifier(from: response) ?? Self.mpegURLContentType
121
+ return LoadedResource(data: data, contentType: contentType)
108
122
  }
109
123
 
110
- let modified = injectSubtitle(into: manifest, subtitlePlaylistURI: subtitlePlaylistURI)
111
- return Data(modified.utf8)
124
+ let modified = injectSubtitle(
125
+ into: manifest,
126
+ subtitlePlaylistURI: subtitlePlaylistURI,
127
+ baseURL: response.url ?? fetchURL
128
+ )
129
+ return LoadedResource(data: Data(modified.utf8), contentType: Self.mpegURLContentType)
112
130
  }
113
131
 
114
132
  private func originalURL(for requestURL: URL) -> URL {
@@ -125,7 +143,22 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
125
143
  manifest.contains("#EXT-X-STREAM-INF")
126
144
  }
127
145
 
128
- private func injectSubtitle(into manifest: String, subtitlePlaylistURI: String) -> String {
146
+ private static func contentTypeIdentifier(from response: URLResponse) -> String? {
147
+ guard let mimeType = (response as? HTTPURLResponse)?.mimeType else {
148
+ return nil
149
+ }
150
+ return UTType(mimeType: mimeType)?.identifier
151
+ }
152
+
153
+ private func absoluteURI(for uri: String, relativeTo baseURL: URL) -> String {
154
+ let trimmed = uri.trimmingCharacters(in: .whitespacesAndNewlines)
155
+ guard let url = URL(string: trimmed, relativeTo: baseURL)?.absoluteURL else {
156
+ return trimmed
157
+ }
158
+ return url.absoluteString
159
+ }
160
+
161
+ private func injectSubtitle(into manifest: String, subtitlePlaylistURI: String, baseURL: URL) -> String {
129
162
  let mediaTag = """
130
163
  #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="\(Self.subtitleGroupID)",NAME="Subtitles",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="\(language)",URI="\(subtitlePlaylistURI)"
131
164
  """
@@ -133,9 +166,10 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
133
166
  var lines = manifest.components(separatedBy: .newlines)
134
167
  var result: [String] = []
135
168
  var insertedMediaTag = false
169
+ var expectingVariantURI = false
136
170
 
137
171
  for line in lines {
138
- if line.hasPrefix("#EXT-X-STREAM-INF") {
172
+ if line.hasPrefix("#EXT-X-STREAM-INF") || line.hasPrefix("#EXT-X-I-FRAME-STREAM-INF") {
139
173
  if !insertedMediaTag {
140
174
  result.append(mediaTag)
141
175
  insertedMediaTag = true
@@ -146,9 +180,17 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
146
180
  } else {
147
181
  result.append("\(line),SUBTITLES=\"\(Self.subtitleGroupID)\"")
148
182
  }
183
+ expectingVariantURI = true
149
184
  continue
150
185
  }
151
186
 
187
+ if expectingVariantURI, !line.hasPrefix("#"), !line.isEmpty {
188
+ result.append(absoluteURI(for: line, relativeTo: baseURL))
189
+ expectingVariantURI = false
190
+ continue
191
+ }
192
+
193
+ expectingVariantURI = false
152
194
  result.append(line)
153
195
  }
154
196
 
@@ -172,14 +214,16 @@ final class HLSSubtitleResourceLoader: NSObject, AVAssetResourceLoaderDelegate {
172
214
  """
173
215
  }
174
216
 
175
- private func fulfill(_ loadingRequest: AVAssetResourceLoadingRequest, with data: Data) {
217
+ private func fulfill(_ loadingRequest: AVAssetResourceLoadingRequest, with resource: LoadedResource) {
176
218
  if let contentRequest = loadingRequest.contentInformationRequest {
177
- contentRequest.contentType = "application/vnd.apple.mpegurl"
178
- contentRequest.contentLength = Int64(data.count)
219
+ if let contentType = resource.contentType {
220
+ contentRequest.contentType = contentType
221
+ }
222
+ contentRequest.contentLength = Int64(resource.data.count)
179
223
  contentRequest.isByteRangeAccessSupported = false
180
224
  }
181
225
 
182
- loadingRequest.dataRequest?.respond(with: data)
226
+ loadingRequest.dataRequest?.respond(with: resource.data)
183
227
  loadingRequest.finishLoading()
184
228
  }
185
229
  }
@@ -20,6 +20,7 @@ final class VideoPlayerCastController: NSObject {
20
20
  private let title: String?
21
21
  private let smallTitle: String?
22
22
  private let artwork: String?
23
+ private let widevineLicenseUrl: String?
23
24
 
24
25
  private weak var player: AVPlayer?
25
26
  private weak var playerViewController: AVPlayerViewController?
@@ -52,11 +53,12 @@ final class VideoPlayerCastController: NSObject {
52
53
  return GCKCastContext.sharedInstance().sessionManager.currentCastSession?.remoteMediaClient
53
54
  }
54
55
 
55
- init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?) {
56
+ init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?, widevineLicenseUrl: String?) {
56
57
  self.videoUrl = videoUrl
57
58
  self.title = title
58
59
  self.smallTitle = smallTitle
59
60
  self.artwork = artwork
61
+ self.widevineLicenseUrl = widevineLicenseUrl
60
62
  super.init()
61
63
  }
62
64
 
@@ -529,6 +531,10 @@ private extension VideoPlayerCastController {
529
531
  builder.streamType = .buffered
530
532
  builder.contentType = contentType(for: url)
531
533
  builder.metadata = metadata
534
+ if let widevineLicenseUrl = widevineLicenseUrl,
535
+ !widevineLicenseUrl.isEmpty {
536
+ builder.customData = ["laurl": widevineLicenseUrl]
537
+ }
532
538
  if let duration = player?.currentItem?.duration.seconds,
533
539
  duration.isFinite,
534
540
  duration > 0 {
@@ -737,7 +743,7 @@ final class VideoPlayerCastController {
737
743
  return false
738
744
  }
739
745
 
740
- init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?) {
746
+ init(videoUrl: String, title: String?, smallTitle: String?, artwork: String?, widevineLicenseUrl: String?) {
741
747
  _ = videoUrl
742
748
  _ = title
743
749
  _ = smallTitle