feat: add notification controls (#3723)
* feat(ios): add `showNotificationControls` prop * feat(android): add `showNotificationControls` prop * add docs * feat!: add `metadata` property to srouce This is breaking change for iOS/tvOS as we are moving some properties, but I believe that this will more readable and more user friendly * chore(ios): remove UI blocking function * code review changes for android * update example * fix readme * fix typos * update docs * fix typo * chore: improve sample metadata notification * update codegen types * rename properties * update tvOS example * reset metadata on source change * update docs --------- Co-authored-by: Olivier Bouillet <freeboub@gmail.com>
This commit is contained in:
28
ios/Video/DataStructures/CustomMetadata.swift
Normal file
28
ios/Video/DataStructures/CustomMetadata.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
struct CustomMetadata {
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let artist: String?
|
||||
let description: String?
|
||||
let imageUri: String?
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
init(_ json: NSDictionary?) {
|
||||
guard let json else {
|
||||
self.json = nil
|
||||
title = nil
|
||||
subtitle = nil
|
||||
artist = nil
|
||||
description = nil
|
||||
imageUri = nil
|
||||
return
|
||||
}
|
||||
|
||||
self.json = json
|
||||
title = json["title"] as? String ?? ""
|
||||
subtitle = json["subtitle"] as? String ?? ""
|
||||
artist = json["artist"] as? String ?? ""
|
||||
description = json["description"] as? String ?? ""
|
||||
imageUri = json["imageUri"] as? String ?? ""
|
||||
}
|
||||
}
|
@@ -8,11 +8,7 @@ struct VideoSource {
|
||||
let startPosition: Float64?
|
||||
let cropStart: Int64?
|
||||
let cropEnd: Int64?
|
||||
// Custom Metadata
|
||||
let title: String?
|
||||
let subtitle: String?
|
||||
let description: String?
|
||||
let customImageUri: String?
|
||||
let customMetadata: CustomMetadata?
|
||||
|
||||
let json: NSDictionary?
|
||||
|
||||
@@ -28,10 +24,7 @@ struct VideoSource {
|
||||
self.startPosition = nil
|
||||
self.cropStart = nil
|
||||
self.cropEnd = nil
|
||||
self.title = nil
|
||||
self.subtitle = nil
|
||||
self.description = nil
|
||||
self.customImageUri = nil
|
||||
self.customMetadata = nil
|
||||
return
|
||||
}
|
||||
self.json = json
|
||||
@@ -54,9 +47,6 @@ struct VideoSource {
|
||||
self.startPosition = json["startPosition"] as? Float64
|
||||
self.cropStart = (json["cropStart"] as? Float64).flatMap { Int64(round($0)) }
|
||||
self.cropEnd = (json["cropEnd"] as? Float64).flatMap { Int64(round($0)) }
|
||||
self.title = json["title"] as? String
|
||||
self.subtitle = json["subtitle"] as? String
|
||||
self.description = json["description"] as? String
|
||||
self.customImageUri = json["customImageUri"] as? String
|
||||
self.customMetadata = CustomMetadata(json["metadata"] as? NSDictionary)
|
||||
}
|
||||
}
|
||||
|
@@ -389,15 +389,21 @@ enum RCTVideoUtils {
|
||||
return item.copy() as! AVMetadataItem
|
||||
}
|
||||
|
||||
static func createImageMetadataItem(imageUri: String) -> Data? {
|
||||
if let uri = URL(string: imageUri),
|
||||
let imgData = try? Data(contentsOf: uri),
|
||||
let image = UIImage(data: imgData),
|
||||
let pngData = image.pngData() {
|
||||
return pngData
|
||||
static func createImageMetadataItem(imageUri: String) async -> Data? {
|
||||
guard let url = URL(string: imageUri) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
guard let image = UIImage(data: data), let pngData = image.pngData() else {
|
||||
return nil
|
||||
}
|
||||
return pngData
|
||||
} catch {
|
||||
print("Error fetching image data: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func getCurrentWindow() -> UIWindow? {
|
||||
|
281
ios/Video/NowPlayingInfoCenterManager.swift
Normal file
281
ios/Video/NowPlayingInfoCenterManager.swift
Normal file
@@ -0,0 +1,281 @@
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
class NowPlayingInfoCenterManager {
|
||||
static let shared = NowPlayingInfoCenterManager()
|
||||
|
||||
private let SEEK_INTERVAL_SECONDS: Double = 10
|
||||
|
||||
private weak var currentPlayer: AVPlayer?
|
||||
private var players = NSHashTable<AVPlayer>.weakObjects()
|
||||
|
||||
private var observers: [Int: NSKeyValueObservation] = [:]
|
||||
private var playbackObserver: Any?
|
||||
|
||||
private var playTarget: Any?
|
||||
private var pauseTarget: Any?
|
||||
private var skipForwardTarget: Any?
|
||||
private var skipBackwardTarget: Any?
|
||||
private var playbackPositionTarget: Any?
|
||||
private var seekTarget: Any?
|
||||
|
||||
private var receivingRemoveControlEvents = false {
|
||||
didSet {
|
||||
if receivingRemoveControlEvents {
|
||||
try? AVAudioSession.sharedInstance().setCategory(.playback)
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
} else {
|
||||
UIApplication.shared.endReceivingRemoteControlEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanup()
|
||||
}
|
||||
|
||||
func registerPlayer(player: AVPlayer) {
|
||||
if players.contains(player) {
|
||||
return
|
||||
}
|
||||
|
||||
if receivingRemoveControlEvents == false {
|
||||
receivingRemoveControlEvents = true
|
||||
}
|
||||
|
||||
if let oldObserver = observers[player.hashValue] {
|
||||
oldObserver.invalidate()
|
||||
}
|
||||
|
||||
observers[player.hashValue] = observePlayers(player: player)
|
||||
players.add(player)
|
||||
|
||||
if currentPlayer == nil {
|
||||
setCurrentPlayer(player: player)
|
||||
}
|
||||
}
|
||||
|
||||
func removePlayer(player: AVPlayer) {
|
||||
if !players.contains(player) {
|
||||
return
|
||||
}
|
||||
|
||||
if let observer = observers[players.hashValue] {
|
||||
observer.invalidate()
|
||||
}
|
||||
|
||||
observers.removeValue(forKey: players.hashValue)
|
||||
players.remove(player)
|
||||
|
||||
if currentPlayer == player {
|
||||
currentPlayer = nil
|
||||
updateMetadata()
|
||||
}
|
||||
|
||||
if players.allObjects.isEmpty {
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
public func cleanup() {
|
||||
observers.removeAll()
|
||||
players.removeAllObjects()
|
||||
|
||||
if let playbackObserver {
|
||||
currentPlayer?.removeTimeObserver(playbackObserver)
|
||||
}
|
||||
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
invalidateTargets(commandCenter)
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
receivingRemoveControlEvents = false
|
||||
}
|
||||
|
||||
private func setCurrentPlayer(player: AVPlayer) {
|
||||
if player == currentPlayer {
|
||||
return
|
||||
}
|
||||
|
||||
if let playbackObserver {
|
||||
currentPlayer?.removeTimeObserver(playbackObserver)
|
||||
}
|
||||
|
||||
currentPlayer = player
|
||||
registerTargets()
|
||||
|
||||
updateMetadata()
|
||||
|
||||
// one second observer
|
||||
playbackObserver = player.addPeriodicTimeObserver(
|
||||
forInterval: CMTime(value: 1, timescale: 4),
|
||||
queue: .global(),
|
||||
using: { [weak self] _ in
|
||||
self?.updatePlaybackInfo()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private func registerTargets() {
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
|
||||
invalidateTargets(commandCenter)
|
||||
|
||||
playTarget = commandCenter.playCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if player.rate == 0 {
|
||||
player.play()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
pauseTarget = commandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
if player.rate != 0 {
|
||||
player.pause()
|
||||
}
|
||||
|
||||
return .success
|
||||
}
|
||||
|
||||
skipBackwardTarget = commandCenter.skipBackwardCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
let newTime = player.currentTime() - CMTime(seconds: SEEK_INTERVAL_SECONDS, preferredTimescale: .max)
|
||||
player.seek(to: newTime)
|
||||
return .success
|
||||
}
|
||||
|
||||
skipForwardTarget = commandCenter.skipForwardCommand.addTarget { [weak self] _ in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
|
||||
let newTime = player.currentTime() + CMTime(seconds: SEEK_INTERVAL_SECONDS, preferredTimescale: .max)
|
||||
player.seek(to: newTime)
|
||||
return .success
|
||||
}
|
||||
|
||||
playbackPositionTarget = commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
|
||||
guard let self, let player = self.currentPlayer else {
|
||||
return .commandFailed
|
||||
}
|
||||
if let event = event as? MPChangePlaybackPositionCommandEvent {
|
||||
player.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: .max)) { _ in
|
||||
player.play()
|
||||
}
|
||||
return .success
|
||||
}
|
||||
return .commandFailed
|
||||
}
|
||||
}
|
||||
|
||||
private func invalidateTargets(_ commandCenter: MPRemoteCommandCenter) {
|
||||
commandCenter.playCommand.removeTarget(playTarget)
|
||||
commandCenter.pauseCommand.removeTarget(pauseTarget)
|
||||
commandCenter.skipForwardCommand.removeTarget(skipForwardTarget)
|
||||
commandCenter.skipBackwardCommand.removeTarget(skipBackwardTarget)
|
||||
commandCenter.changePlaybackPositionCommand.removeTarget(playbackPositionTarget)
|
||||
}
|
||||
|
||||
public func updateMetadata() {
|
||||
guard let player = currentPlayer, let currentItem = player.currentItem else {
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
invalidateTargets(commandCenter)
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
return
|
||||
}
|
||||
|
||||
// commonMetadata is metadata from asset, externalMetadata is custom metadata set by user
|
||||
let metadata = currentItem.asset.commonMetadata + currentItem.externalMetadata
|
||||
var nowPlayingInfo: [String: Any] = [:]
|
||||
|
||||
if let titleItem = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierTitle).first?.value {
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = titleItem
|
||||
} else {
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = ""
|
||||
}
|
||||
|
||||
if let artistItem = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierArtist).first?.value {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = artistItem
|
||||
} else {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = ""
|
||||
}
|
||||
|
||||
// I have some issue with this - setting artworkItem when it not set dont return nil but also is crashing application
|
||||
// this is very hacky workaround for it
|
||||
if let artworkItem = AVMetadataItem.metadataItems(from: metadata, filteredByIdentifier: .commonIdentifierArtwork).first?.value as? Data {
|
||||
if let image = UIImage(data: artworkItem) {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { _ in
|
||||
return image
|
||||
})
|
||||
}
|
||||
} else {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: UIImage().size, requestHandler: { _ in
|
||||
UIImage()
|
||||
})
|
||||
}
|
||||
|
||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
|
||||
private func updatePlaybackInfo() {
|
||||
guard let player = currentPlayer, let currentItem = player.currentItem else {
|
||||
return
|
||||
}
|
||||
|
||||
// We dont want to update playback if we did not set metadata yet
|
||||
if var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo {
|
||||
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = currentItem.duration.seconds
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentItem.currentTime().seconds.rounded()
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = player.rate
|
||||
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
}
|
||||
|
||||
private func findNewCurrentPlayer() {
|
||||
if let newPlayer = players.allObjects.first(where: {
|
||||
$0.rate != 0
|
||||
}) {
|
||||
setCurrentPlayer(player: newPlayer)
|
||||
}
|
||||
}
|
||||
|
||||
// We will observe players rate to find last active player that info will be displayed
|
||||
private func observePlayers(player: AVPlayer) -> NSKeyValueObservation {
|
||||
return player.observe(\.rate) { [weak self] player, change in
|
||||
guard let self else { return }
|
||||
|
||||
let rate = change.newValue
|
||||
|
||||
// case where there is new player that is not paused
|
||||
// In this case event is triggered by non currentPlayer
|
||||
if rate != 0 && currentPlayer != player {
|
||||
setCurrentPlayer(player: player)
|
||||
return
|
||||
}
|
||||
|
||||
// case where currentPlayer was paused
|
||||
// In this case event is triggeret by currentPlayer
|
||||
if rate == 0 && currentPlayer == player {
|
||||
findNewCurrentPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -66,6 +66,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
private var _filterEnabled = false
|
||||
private var _presentingViewController: UIViewController?
|
||||
private var _startPosition: Float64 = -1
|
||||
private var _showNotificationControls = false
|
||||
private var _pictureInPictureEnabled = false {
|
||||
didSet {
|
||||
#if os(iOS)
|
||||
@@ -244,6 +245,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self.removePlayerLayer()
|
||||
_playerObserver.clearPlayer()
|
||||
|
||||
if let player = _player {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
_pip = nil
|
||||
#endif
|
||||
@@ -358,54 +363,54 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
// MARK: - Player and source
|
||||
|
||||
func preparePlayerItem() async throws -> AVPlayerItem {
|
||||
guard let source = self._source else {
|
||||
guard let source = _source else {
|
||||
DebugLog("The source not exist")
|
||||
self.isSetSourceOngoing = false
|
||||
self.applyNextSource()
|
||||
isSetSourceOngoing = false
|
||||
applyNextSource()
|
||||
throw NSError(domain: "", code: 0, userInfo: nil)
|
||||
}
|
||||
|
||||
if let uri = source.uri, uri.starts(with: "ph://") {
|
||||
let photoAsset = await RCTVideoUtils.preparePHAsset(uri: uri)
|
||||
return await self.playerItemPrepareText(asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
|
||||
return await playerItemPrepareText(asset: photoAsset, assetOptions: nil, uri: source.uri ?? "")
|
||||
}
|
||||
|
||||
guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
|
||||
let asset = assetResult.asset,
|
||||
let assetOptions = assetResult.assetOptions else {
|
||||
DebugLog("Could not find video URL in source '\(String(describing: self._source))'")
|
||||
self.isSetSourceOngoing = false
|
||||
self.applyNextSource()
|
||||
DebugLog("Could not find video URL in source '\(String(describing: _source))'")
|
||||
isSetSourceOngoing = false
|
||||
applyNextSource()
|
||||
throw NSError(domain: "", code: 0, userInfo: nil)
|
||||
}
|
||||
|
||||
guard let assetResult = RCTVideoUtils.prepareAsset(source: source),
|
||||
let asset = assetResult.asset,
|
||||
let assetOptions = assetResult.assetOptions else {
|
||||
DebugLog("Could not find video URL in source '\(String(describing: self._source))'")
|
||||
self.isSetSourceOngoing = false
|
||||
self.applyNextSource()
|
||||
DebugLog("Could not find video URL in source '\(String(describing: _source))'")
|
||||
isSetSourceOngoing = false
|
||||
applyNextSource()
|
||||
throw NSError(domain: "", code: 0, userInfo: nil)
|
||||
}
|
||||
|
||||
if let startPosition = self._source?.startPosition {
|
||||
self._startPosition = startPosition / 1000
|
||||
if let startPosition = _source?.startPosition {
|
||||
_startPosition = startPosition / 1000
|
||||
}
|
||||
|
||||
#if USE_VIDEO_CACHING
|
||||
if self._videoCache.shouldCache(source: source, textTracks: self._textTracks) {
|
||||
return try await self._videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions)
|
||||
if _videoCache.shouldCache(source: source, textTracks: _textTracks) {
|
||||
return try await _videoCache.playerItemForSourceUsingCache(uri: source.uri, assetOptions: assetOptions)
|
||||
}
|
||||
#endif
|
||||
|
||||
if self._drm != nil || self._localSourceEncryptionKeyScheme != nil {
|
||||
self._resouceLoaderDelegate = RCTResourceLoaderDelegate(
|
||||
if _drm != nil || _localSourceEncryptionKeyScheme != nil {
|
||||
_resouceLoaderDelegate = RCTResourceLoaderDelegate(
|
||||
asset: asset,
|
||||
drm: self._drm,
|
||||
localSourceEncryptionKeyScheme: self._localSourceEncryptionKeyScheme,
|
||||
onVideoError: self.onVideoError,
|
||||
onGetLicense: self.onGetLicense,
|
||||
reactTag: self.reactTag
|
||||
drm: _drm,
|
||||
localSourceEncryptionKeyScheme: _localSourceEncryptionKeyScheme,
|
||||
onVideoError: onVideoError,
|
||||
onGetLicense: onGetLicense,
|
||||
reactTag: reactTag
|
||||
)
|
||||
}
|
||||
|
||||
@@ -413,53 +418,62 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func setupPlayer(playerItem: AVPlayerItem) async throws {
|
||||
if !self.isSetSourceOngoing {
|
||||
if !isSetSourceOngoing {
|
||||
DebugLog("setSrc has been canceled last step")
|
||||
return
|
||||
}
|
||||
|
||||
self._player?.pause()
|
||||
self._playerItem = playerItem
|
||||
self._playerObserver.playerItem = self._playerItem
|
||||
self.setPreferredForwardBufferDuration(self._preferredForwardBufferDuration)
|
||||
self.setPlaybackRange(playerItem, withCropStart: self._source?.cropStart, withCropEnd: self._source?.cropEnd)
|
||||
self.setFilter(self._filterName)
|
||||
if let maxBitRate = self._maxBitRate {
|
||||
self._playerItem?.preferredPeakBitRate = Double(maxBitRate)
|
||||
_player?.pause()
|
||||
_playerItem = playerItem
|
||||
_playerObserver.playerItem = _playerItem
|
||||
setPreferredForwardBufferDuration(_preferredForwardBufferDuration)
|
||||
setPlaybackRange(playerItem, withCropStart: _source?.cropStart, withCropEnd: _source?.cropEnd)
|
||||
setFilter(_filterName)
|
||||
if let maxBitRate = _maxBitRate {
|
||||
_playerItem?.preferredPeakBitRate = Double(maxBitRate)
|
||||
}
|
||||
|
||||
self._player = self._player ?? AVPlayer()
|
||||
if _player == nil {
|
||||
_player = AVPlayer()
|
||||
_player!.replaceCurrentItem(with: playerItem)
|
||||
|
||||
self._player?.replaceCurrentItem(with: playerItem)
|
||||
// We need to register player after we set current item and only for init
|
||||
NowPlayingInfoCenterManager.shared.registerPlayer(player: _player!)
|
||||
} else {
|
||||
_player?.replaceCurrentItem(with: playerItem)
|
||||
|
||||
self._playerObserver.player = self._player
|
||||
self.applyModifiers()
|
||||
self._player?.actionAtItemEnd = .none
|
||||
// later we can just call "updateMetadata:
|
||||
NowPlayingInfoCenterManager.shared.updateMetadata()
|
||||
}
|
||||
|
||||
_playerObserver.player = _player
|
||||
applyModifiers()
|
||||
_player?.actionAtItemEnd = .none
|
||||
|
||||
if #available(iOS 10.0, *) {
|
||||
self.setAutomaticallyWaitsToMinimizeStalling(self._automaticallyWaitsToMinimizeStalling)
|
||||
setAutomaticallyWaitsToMinimizeStalling(_automaticallyWaitsToMinimizeStalling)
|
||||
}
|
||||
|
||||
#if USE_GOOGLE_IMA
|
||||
if self._adTagUrl != nil {
|
||||
if _adTagUrl != nil {
|
||||
// Set up your content playhead and contentComplete callback.
|
||||
self._contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: self._player!)
|
||||
_contentPlayhead = IMAAVPlayerContentPlayhead(avPlayer: _player!)
|
||||
|
||||
self._imaAdsManager.setUpAdsLoader()
|
||||
_imaAdsManager.setUpAdsLoader()
|
||||
}
|
||||
#endif
|
||||
// Perform on next run loop, otherwise onVideoLoadStart is nil
|
||||
self.onVideoLoadStart?([
|
||||
onVideoLoadStart?([
|
||||
"src": [
|
||||
"uri": self._source?.uri ?? NSNull(),
|
||||
"type": self._source?.type ?? NSNull(),
|
||||
"isNetwork": NSNumber(value: self._source?.isNetwork ?? false),
|
||||
"uri": _source?.uri ?? NSNull(),
|
||||
"type": _source?.type ?? NSNull(),
|
||||
"isNetwork": NSNumber(value: _source?.isNetwork ?? false),
|
||||
],
|
||||
"drm": self._drm?.json ?? NSNull(),
|
||||
"target": self.reactTag,
|
||||
"drm": _drm?.json ?? NSNull(),
|
||||
"target": reactTag,
|
||||
])
|
||||
self.isSetSourceOngoing = false
|
||||
self.applyNextSource()
|
||||
isSetSourceOngoing = false
|
||||
applyNextSource()
|
||||
}
|
||||
|
||||
@objc
|
||||
@@ -478,6 +492,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self._player?.replaceCurrentItem(with: nil)
|
||||
self.isSetSourceOngoing = false
|
||||
self.applyNextSource()
|
||||
|
||||
if let player = self._player {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
|
||||
DebugLog("setSrc Stopping playback")
|
||||
return
|
||||
}
|
||||
@@ -500,6 +519,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self.onVideoError?(["error": error.localizedDescription])
|
||||
self.isSetSourceOngoing = false
|
||||
self.applyNextSource()
|
||||
|
||||
if let player = self._player {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -523,7 +546,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
|
||||
func playerItemPrepareText(asset: AVAsset!, assetOptions: NSDictionary?, uri: String) async -> AVPlayerItem {
|
||||
if (self._textTracks == nil) || self._textTracks?.isEmpty == true || (uri.hasSuffix(".m3u8")) {
|
||||
return self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
|
||||
return await self.playerItemPropegateMetadata(AVPlayerItem(asset: asset))
|
||||
}
|
||||
|
||||
// AVPlayer can't airplay AVMutableCompositions
|
||||
@@ -540,26 +563,30 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
self.setTextTracks(validTextTracks)
|
||||
}
|
||||
|
||||
return self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))
|
||||
return await self.playerItemPropegateMetadata(AVPlayerItem(asset: mixComposition))
|
||||
}
|
||||
|
||||
func playerItemPropegateMetadata(_ playerItem: AVPlayerItem!) -> AVPlayerItem {
|
||||
func playerItemPropegateMetadata(_ playerItem: AVPlayerItem!) async -> AVPlayerItem {
|
||||
var mapping: [AVMetadataIdentifier: Any] = [:]
|
||||
|
||||
if let title = _source?.title {
|
||||
if let title = _source?.customMetadata?.title {
|
||||
mapping[.commonIdentifierTitle] = title
|
||||
}
|
||||
|
||||
if let subtitle = _source?.subtitle {
|
||||
if let artist = _source?.customMetadata?.artist {
|
||||
mapping[.commonIdentifierArtist] = artist
|
||||
}
|
||||
|
||||
if let subtitle = _source?.customMetadata?.subtitle {
|
||||
mapping[.iTunesMetadataTrackSubTitle] = subtitle
|
||||
}
|
||||
|
||||
if let description = _source?.description {
|
||||
if let description = _source?.customMetadata?.description {
|
||||
mapping[.commonIdentifierDescription] = description
|
||||
}
|
||||
|
||||
if let customImageUri = _source?.customImageUri,
|
||||
let imageData = RCTVideoUtils.createImageMetadataItem(imageUri: customImageUri) {
|
||||
if let imageUri = _source?.customMetadata?.imageUri,
|
||||
let imageData = await RCTVideoUtils.createImageMetadataItem(imageUri: imageUri) {
|
||||
mapping[.commonIdentifierArtwork] = imageData
|
||||
}
|
||||
|
||||
@@ -1006,6 +1033,7 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
func createPlayerViewController(player: AVPlayer, withPlayerItem _: AVPlayerItem) -> RCTVideoPlayerViewController {
|
||||
let viewController = RCTVideoPlayerViewController()
|
||||
viewController.showsPlaybackControls = self._controls
|
||||
viewController.updatesNowPlayingInfoCenter = false
|
||||
viewController.rctDelegate = self
|
||||
viewController.preferredOrientation = _fullscreenOrientation
|
||||
|
||||
@@ -1057,6 +1085,21 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
func setShowNotificationControls(_ showNotificationControls: Bool) {
|
||||
guard let player = _player else {
|
||||
return
|
||||
}
|
||||
|
||||
if showNotificationControls {
|
||||
NowPlayingInfoCenterManager.shared.registerPlayer(player: player)
|
||||
} else {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
|
||||
_showNotificationControls = showNotificationControls
|
||||
}
|
||||
|
||||
@objc
|
||||
func setProgressUpdateInterval(_ progressUpdateInterval: Float) {
|
||||
_playerObserver.replaceTimeObserverIfSet(Float64(progressUpdateInterval))
|
||||
@@ -1182,7 +1225,11 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func removeFromSuperview() {
|
||||
_player?.pause()
|
||||
if let player = _player {
|
||||
player.pause()
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
|
||||
_player = nil
|
||||
_resouceLoaderDelegate = nil
|
||||
_playerObserver.clearPlayer()
|
||||
@@ -1354,6 +1401,10 @@ class RCTVideo: UIView, RCTVideoPlayerViewControllerDelegate, RCTPlayerObserverH
|
||||
}
|
||||
|
||||
func handlePlaybackFailed() {
|
||||
if let player = _player {
|
||||
NowPlayingInfoCenterManager.shared.removePlayer(player: player)
|
||||
}
|
||||
|
||||
guard let _playerItem else { return }
|
||||
onVideoError?(
|
||||
[
|
||||
|
@@ -37,6 +37,7 @@ RCT_EXPORT_VIEW_PROPERTY(progressUpdateInterval, float);
|
||||
RCT_EXPORT_VIEW_PROPERTY(restoreUserInterfaceForPIPStopCompletionHandler, BOOL);
|
||||
RCT_EXPORT_VIEW_PROPERTY(localSourceEncryptionKeyScheme, NSString);
|
||||
RCT_EXPORT_VIEW_PROPERTY(subtitleStyle, NSDictionary);
|
||||
RCT_EXPORT_VIEW_PROPERTY(showNotificationControls, BOOL)
|
||||
/* Should support: onLoadStart, onLoad, and onError to stay consistent with Image */
|
||||
RCT_EXPORT_VIEW_PROPERTY(onVideoLoadStart, RCTDirectEventBlock);
|
||||
RCT_EXPORT_VIEW_PROPERTY(onVideoLoad, RCTDirectEventBlock);
|
||||
|
Reference in New Issue
Block a user