import Foundation import UIKit import AmazonIVSPlayer @objc(AmazonIvsView) class AmazonIvsView: UIView, IVSPlayer.Delegate { @objc var onSeek: RCTDirectEventBlock? @objc var onData: RCTDirectEventBlock? @objc var onVideoStatistics: RCTDirectEventBlock? @objc var onPlayerStateChange: RCTDirectEventBlock? @objc var onDurationChange: RCTDirectEventBlock? @objc var onQualityChange: RCTDirectEventBlock? @objc var onRebuffering: RCTDirectEventBlock? @objc var onLoadStart: RCTDirectEventBlock? @objc var onLoad: RCTDirectEventBlock? @objc var onTextCue: RCTDirectEventBlock? @objc var onTextMetadataCue: RCTDirectEventBlock? @objc var onLiveLatencyChange: RCTDirectEventBlock? @objc var onProgress: RCTDirectEventBlock? @objc var onTimePoint: RCTDirectEventBlock? @objc var onError: RCTDirectEventBlock? private let player = IVSPlayer() private let playerView = IVSPlayerView() private var finishedLoading: Bool = false; private var progressObserverToken: Any? private var playerObserverToken: Any? private var timePointObserver: Any? private var oldQualities: [IVSQuality] = []; private var lastLiveLatency: Double?; private var lastBitrate: Int?; private var lastDuration: CMTime?; private var lastFramesDropped: Int?; private var lastFramesDecoded: Int?; private var _pipController: Any? = nil @available(iOS 15, *) private var pipController: AVPictureInPictureController? { get { return _pipController as! AVPictureInPictureController? } set { _pipController = newValue } } override init(frame: CGRect) { self.muted = player.muted self.loop = player.looping self.liveLowLatency = player.isLiveLowLatency self.autoQualityMode = player.autoQualityMode self.playbackRate = Double(player.playbackRate) self.logLevel = NSNumber(value: player.logLevel.rawValue) self.progressInterval = 1 self.volume = Double(player.volume) self.breakpoints = [] self.initialBufferDuration = 1 super.init(frame: frame) self.addSubview(self.playerView) self.playerView.autoresizingMask = [.flexibleHeight, .flexibleWidth] self.playerView.videoGravity = findResizeMode(mode: resizeMode) self.addProgressObserver() self.addPlayerObserver() self.addTimePointObserver() player.delegate = self self.playerView.player = player preparePictureInPicture() } deinit { self.removeProgressObserver() self.removePlayerObserver() self.removeTimePointObserver() } func load(urlString: String) { finishedLoading = false let url = URL(string: urlString) onLoadStart?(["": NSNull()]) player.load(url) } @objc var progressInterval: NSNumber { // TODO: Figure out why updatating observer does not work and results in multiple calls per second didSet { self.removeProgressObserver() self.addProgressObserver() } } @objc var loop: Bool { didSet { player.looping = loop } } @objc var muted: Bool { didSet { player.muted = muted } } @objc var playbackRate: Double { didSet { player.playbackRate = Float(playbackRate) } } @objc var liveLowLatency: Bool { didSet { player.setLiveLowLatencyEnabled(liveLowLatency) } } @objc var quality: NSDictionary? { didSet { let newQuality = findQuality(quality: quality) player.quality = newQuality } } @objc var autoQualityMode: Bool { didSet { player.autoQualityMode = autoQualityMode } } @objc var autoMaxQuality: NSDictionary? { didSet { let quality = findQuality(quality: autoMaxQuality) player.setAutoMaxQuality(quality) } } @objc var initialBufferDuration: Double { didSet { let parsedTime = CMTimeMakeWithSeconds(initialBufferDuration, preferredTimescale: 10) player.setInitialBufferDuration(parsedTime) } } private func findQuality(quality: NSDictionary?) -> IVSQuality? { let quality = player.qualities.first(where: { $0.name == quality?["name"] as? String && $0.codecs == quality?["codecs"] as? String && $0.bitrate == quality?["bitrate"] as? Int && $0.framerate == quality?["framerate"] as? Float && $0.width == quality?["width"] as? Int && $0.height == quality?["height"] as? Int }) return quality } private func getDuration(_ duration: CMTime) -> NSNumber? { let value: NSNumber? if duration.isNumeric { value = NSNumber(value: duration.seconds); } else { value = 0 } return value } @objc var streamUrl: String? { didSet { if let url = streamUrl, !streamUrl!.isEmpty { self.load(urlString: url) } } } @objc var volume: Double { didSet { player.volume = Float(volume) } } @objc var logLevel: NSNumber { didSet { switch logLevel { case 0: player.logLevel = IVSPlayer.LogLevel.debug case 1: player.logLevel = IVSPlayer.LogLevel.info case 2: player.logLevel = IVSPlayer.LogLevel.warning case 3: player.logLevel = IVSPlayer.LogLevel.error default: break } } } @objc var resizeMode: String? { didSet{ playerView.videoGravity = findResizeMode(mode: resizeMode) } } private func findResizeMode(mode: String?) -> AVLayerVideoGravity { switch mode { case "aspectFill": return AVLayerVideoGravity.resizeAspectFill case "aspectFit": return AVLayerVideoGravity.resizeAspect case "aspectZoom": return AVLayerVideoGravity.resize default: return AVLayerVideoGravity.resizeAspect } } @objc var breakpoints: NSArray { didSet { self.removeTimePointObserver() self.addTimePointObserver() } } @objc func play() { player.play() } @objc func pause() { player.pause() } @objc func seek(position: Double) { let parsedTime = CMTimeMakeWithSeconds(position, preferredTimescale: 1000000) player.seek(to: parsedTime) } @objc func setOrigin(origin: NSString){ let url = URL(string: origin as String) player.setOrigin(url) } @objc func togglePip() { guard #available(iOS 15, *), let pipController = pipController else { return } if pipController.isPictureInPictureActive { pipController.stopPictureInPicture() } else { pipController.startPictureInPicture() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func addPlayerObserver() { playerObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { [weak self] time in if self?.lastLiveLatency != self?.player.liveLatency.seconds { if let liveLatency = self?.player.liveLatency.seconds { let parsedValue = 1000*liveLatency self?.onLiveLatencyChange?(["liveLatency": parsedValue]) } else { self?.onLiveLatencyChange?(["liveLatency": NSNull()]) } self?.lastLiveLatency = self?.player.liveLatency.seconds } if self?.lastBitrate != self?.player.videoBitrate || self?.lastDuration != self?.player.duration || self?.lastFramesDecoded != self?.player.videoFramesDecoded || self?.lastFramesDropped != self?.player.videoFramesDropped || self?.onVideoStatistics != nil { let videoData: [String: Any] = [ "duration": self?.getDuration(self!.player.duration) ?? NSNull(), "bitrate": self?.player.videoBitrate ?? NSNull(), "framesDropped": self?.player.videoFramesDropped ?? NSNull(), "framesDecoded": self?.player.videoFramesDecoded ?? NSNull() ] self?.onVideoStatistics?(["videoData": videoData]) self?.lastBitrate = self?.player.videoBitrate self?.lastDuration = self?.player.duration self?.lastFramesDropped = self?.player.videoFramesDropped self?.lastFramesDecoded = self?.player.videoFramesDecoded } } } private func mapPlayerState(state: IVSPlayer.State) -> String { switch state { case IVSPlayer.State.playing: return "Playing" case IVSPlayer.State.buffering: return "Buffering" case IVSPlayer.State.ready: return "Ready" case IVSPlayer.State.idle: return "Idle" case IVSPlayer.State.ended: return "Ended" } } func addProgressObserver() { progressObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: Double(truncating: progressInterval), preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { [weak self] time in self?.onProgress?(["position": (self?.player.position.seconds ?? nil) as Any]) } } func addTimePointObserver() { timePointObserver = player.addBoundaryTimeObserver(forTimes: breakpoints as! [NSNumber], queue: .main) { [weak self] in self?.onTimePoint?(["position": (self?.player.position.seconds ?? nil) as Any]) } } func removePlayerObserver() { if let token = playerObserverToken { player.removeTimeObserver(token) self.playerObserverToken = nil } } func removeProgressObserver() { if let token = progressObserverToken { player.removeTimeObserver(token) self.progressObserverToken = nil } } func removeTimePointObserver() { if let token = timePointObserver { player.removeTimeObserver(token) self.timePointObserver = nil } } func player(_ player: IVSPlayer, didSeekTo time: CMTime) { onSeek?(["position": CMTimeGetSeconds(time)]) } func player(_ player: IVSPlayer, didChangeState state: IVSPlayer.State) { onPlayerStateChange?(["state": mapPlayerState(state: state)]) if state == IVSPlayer.State.playing, finishedLoading == false { let duration = getDuration(player.duration) onLoad?(["duration": duration ?? NSNull()]) finishedLoading = true } if state == IVSPlayer.State.ready { if player.qualities != oldQualities { let qualities: NSMutableArray = [] for quality in player.qualities { let qualityData: [String: Any] = [ "name": quality.name, "codecs": quality.codecs, "bitrate": quality.bitrate, "framerate": quality.framerate, "width": quality.width, "height": quality.height ] qualities.add(qualityData) } onData?(["playerData": [ "qualities": qualities, "version": player.version, "sessionId": player.sessionId ]]) } oldQualities = player.qualities } } func player(_ player: IVSPlayer, didChangeDuration duration: CMTime) { let parsedDuration = getDuration(duration) onDurationChange?(["duration": parsedDuration ?? NSNull()]) } func player(_ player: IVSPlayer, didChangeQuality quality: IVSQuality?) { if quality == nil { onQualityChange?(["quality": NSNull()]) } else { let qualityData: [String: Any] = [ "name": quality?.name ?? "", "codecs": quality?.codecs ?? "", "bitrate": quality?.bitrate ?? 0, "framerate": quality?.framerate ?? 0, "width": quality?.width ?? 0, "height": quality?.height ?? 0 ] onQualityChange?(["quality": qualityData]) } } func player(_ player: IVSPlayer, didOutputCue cue: IVSCue) { if let cue = cue as? IVSTextCue { let textCue: [String: Any] = [ "type": cue.type.rawValue, "line": cue.line, "size": cue.size, "position": cue.position, "text": cue.text, "textAlignment": cue.textAlignment ] onTextCue?(["textCue": textCue]) } if let cue = cue as? IVSTextMetadataCue { let textMetadataCue = [ "type": cue.type.rawValue, "text": cue.text, "textDescription": cue.textDescription ] onTextMetadataCue?(["textMetadataCue": textMetadataCue]) } } func playerWillRebuffer(_ player: IVSPlayer) { onRebuffering?(["": NSNull()]) } func player(_ player: IVSPlayer, didFailWithError error: Error) { onError?(["error": error.localizedDescription]) } private func preparePictureInPicture() { guard #available(iOS 15, *), AVPictureInPictureController.isPictureInPictureSupported() else { return } if let existingController = self.pipController { if existingController.ivsPlayerLayer == playerView.playerLayer { return } self.pipController = nil } guard let pipController = AVPictureInPictureController(ivsPlayerLayer: playerView.playerLayer) else { return } self.pipController = pipController pipController.canStartPictureInPictureAutomaticallyFromInline = true } }