Facebook
From Karlo, 1 Week ago, written in Plain Text.
Embed
Download Paste or View Raw
Hits: 79
  1. //
  2. //  VideoPlayerContentView.swift
  3. //  WeSports
  4. //
  5. //  Created Jure Cular on 07.06.2022..
  6. //  Copyright © 2022 WeSports. All rights reserved.
  7. //
  8.  
  9. import AVFoundation
  10. import RxCocoa
  11. import RxRelay
  12. import RxSwift
  13. import UIKit
  14.  
  15. final class VideoPlayerContentView: UIView, VideoPlayerViewProtocol {
  16.     let muteVideoRelay = BehaviorRelay<Bool>(value: true)
  17.  
  18.     // MARK: - Private Properties -
  19.  
  20.     private let currentTimeRelay = BehaviorRelay<Double>(value: 0)
  21.     private let playerStatusRelay = BehaviorRelay<VideoPlayerStatus>(value: .loading)
  22.     private let bufferQueue: DispatchQueue = .init(label: "com.sportening.sample-buffer-queue")
  23.     private let displayLayer: AVSampleBufferDisplayLayer? = AVSampleBufferDisplayLayer()
  24.     private let audioRenderer: AVSampleBufferAudioRenderer? = AVSampleBufferAudioRenderer()
  25.     private let synchronizer = AVSampleBufferRenderSynchronizer()
  26.     private let sampleBufferProvider = SampleBufferProvider()
  27.     private let disposeBag = DisposeBag()
  28.     private var periodicObserverToken: Any?
  29.  
  30.     // MARK: - Init -
  31.  
  32.     override init(frame: CGRect) {
  33.         super.init(frame: frame)
  34.         setupRenderers()
  35.     }
  36.  
  37.     @available(*, unavailable)
  38.     required init?(coder _: NSCoder) {
  39.         fatalError("init(coder:) has not been implemented")
  40.     }
  41.  
  42.     // MARK: - UIView -
  43.  
  44.     override func layoutSubviews() {
  45.         super.layoutSubviews()
  46.         if let displayLayer = displayLayer, displayLayer.bounds != bounds {
  47.             displayLayer.bounds = bounds
  48.             displayLayer.position = center
  49.         }
  50.     }
  51.  
  52.     // MARK: - VideoViewProtocol -
  53.  
  54.     func set(url: URL) {
  55.         bufferQueue.async {
  56.             self.bind()
  57.             self.sampleBufferProvider.setup(for: url)
  58.         }
  59.     }
  60.  
  61.     // MARK: - Private Functions -
  62.  
  63.     private func pauseSynchronizer() {
  64.         // Synchronizes renderers and stops rendering audio and sound
  65.         synchronizer.rate = 0
  66.     }
  67.  
  68.     private func startSynchronizer() {
  69.         // Synchronizes renderers and starts rendering audio and sound at normal rate/speed
  70.         synchronizer.rate = 1
  71.     }
  72.  
  73.     private func setupRenderers() {
  74.         periodicObserverToken = synchronizer.addPeriodicTimeObserver(
  75.             forInterval: CMTime(seconds: 1, preferredTimescale: 1),
  76.             queue: bufferQueue
  77.         ) { [weak self] time in
  78.             self?.currentTimeRelay.accept(time.seconds)
  79.         }
  80.  
  81.         if let displayLayer = displayLayer {
  82.             synchronizer.addRenderer(displayLayer)
  83.             layer.addSublayer(displayLayer)
  84.         }
  85.         if let audioRenderer = audioRenderer {
  86.             synchronizer.addRenderer(audioRenderer)
  87.         }
  88.     }
  89.  
  90.     private func startRequestingVideo() {
  91.         if sampleBufferProvider.areVideoSamplesAvailable {
  92.             guard let displayLayer = displayLayer else {
  93.                 return
  94.             }
  95.  
  96.             // If display renderer is in failed status, flush it and remove the image
  97.             if displayLayer.status == AVQueuedSampleBufferRenderingStatus.failed {
  98.                  displayLayer.flushAndRemoveImage()
  99.              }
  100.  
  101.             displayLayer.requestMediaDataWhenReady(on: bufferQueue) { [weak self] in
  102.                 guard let self = self, let displayLayer = self.displayLayer else { return }
  103.                 if displayLayer.status == AVQueuedSampleBufferRenderingStatus.failed {
  104.                     displayLayer.flush()
  105.                 }
  106.  
  107.                 if let sampleBuffer = self.sampleBufferProvider.requestVideoBuffer() {
  108.                     displayLayer.enqueue(sampleBuffer)
  109.                 }
  110.             }
  111.         }
  112.     }
  113.  
  114.     private func startRequestingAudio() {
  115.         if sampleBufferProvider.areAudioSamplesAvailable {
  116.             guard let audioRenderer = audioRenderer else {
  117.                 return
  118.             }
  119.  
  120.             // If display audio is in failed status, flush sample buffers
  121.             if audioRenderer.status == .failed {
  122.                 audioRenderer.flush()
  123.             }
  124.  
  125.             audioRenderer.requestMediaDataWhenReady(on: bufferQueue) { [weak self] in
  126.                 guard let self = self, let audioRenderer = self.audioRenderer else { return }
  127.                 if audioRenderer.status == .failed {
  128.                     audioRenderer.flush()
  129.                 }
  130.                 if let sampleBuffer = self.sampleBufferProvider.requestAudioBuffer() {
  131.                     audioRenderer.enqueue(sampleBuffer)
  132.                 }
  133.             }
  134.         }
  135.     }
  136.  
  137.     private func stopRequestingVideo() {
  138.         guard let displayLayer = displayLayer else {
  139.             return
  140.         }
  141.  
  142.         displayLayer.stopRequestingMediaData()
  143.         displayLayer.flush()
  144.     }
  145.  
  146.     private func stopRequestingAudio() {
  147.         guard let audioRenderer = audioRenderer else {
  148.             return
  149.         }
  150.  
  151.         audioRenderer.stopRequestingMediaData()
  152.         audioRenderer
  153.             .flush(fromSourceTime: sampleBufferProvider.lastSampleBufferTime) { [weak audioRenderer] didSucceed in
  154.                 if !didSucceed {
  155.                     audioRenderer?.flush()
  156.                 }
  157.             }
  158.     }
  159.  
  160.     // MARK: - Setup Bindings -
  161.  
  162.     // swiftlint:disable cyclomatic_complexity
  163.     private func bind() {
  164.         muteVideoRelay
  165.             .bind { [weak self] isMuted in
  166.                 guard let self = self else { return }
  167.                 self.bufferQueue.async {
  168.                     self.audioRenderer?.isMuted = isMuted
  169.                 }
  170.             }
  171.             .disposed(by: disposeBag)
  172.  
  173.         // Observe player status to know when to start
  174.         sampleBufferProvider.status
  175.             .bind { [weak self] status in
  176.                 guard let self = self else { return }
  177.                 self.bufferQueue.async {
  178.                     switch status {
  179.                     case .readyToProvide:
  180.                         // Set initial display buffer
  181.                         self.setDisplayBuffer(self.sampleBufferProvider.requestVideoBuffer())
  182.                     case .providing:
  183.                         self.startSynchronizer()
  184.                         self.startRequestingAudio()
  185.                         self.startRequestingVideo()
  186.                     case .paused, .stopped:
  187.                         self.stopRequestingVideo()
  188.                         self.stopRequestingAudio()
  189.                         self.pauseSynchronizer()
  190.                     case .finished:
  191.                         self.stopRequestingVideo()
  192.                         self.stopRequestingAudio()
  193.                         self.finished()
  194.                     case .failed:
  195.                         self.stopRequestingAudio()
  196.                         self.stopRequestingVideo()
  197.                     default:
  198.                         break
  199.                     }
  200.                 }
  201.             }.disposed(by: disposeBag)
  202.  
  203.         sampleBufferProvider.status
  204.             .map { status -> VideoPlayerStatus in
  205.                 switch status {
  206.                 case .loading:
  207.                     return .loading
  208.                 case .readyToProvide:
  209.                     return .readyToPlay
  210.                 case .providing:
  211.                     return .playing
  212.                 case .paused:
  213.                     return .paused
  214.                 case .stopped:
  215.                     return .stopped
  216.                 case .finished:
  217.                     return .finished
  218.                 case .failed:
  219.                     return .failed
  220.                 }
  221.             }
  222.             .bind(to: playerStatusRelay)
  223.             .disposed(by: disposeBag)
  224.  
  225.         bindNotifications()
  226.         bindDisplayLayer()
  227.         bindAudioRendered()
  228.     }
  229.  
  230.     private func bindNotifications() {
  231.         // When app goes to background or resigns active and video is playing we need to stop requesting sample buffers.
  232.         NotificationCenter.default.rx
  233.             .notification(UIApplication.willResignActiveNotification)
  234.             .withLatestFrom(sampleBufferProvider.status)
  235.             .filter { $0 == .providing }
  236.             .subscribe(onNext: { [weak self] _ in
  237.                 guard let self = self else { return }
  238.                 self.bufferQueue.async {
  239.                     self.sampleBufferProvider.stop()
  240.                 }
  241.             })
  242.             .disposed(by: disposeBag)
  243.  
  244.         // Once app is active/in the foreground, requesting buffers needs to be restarted but also asset reader needs to
  245.         // be restarted too. If the reader isn't restarted sample buffers which have been read, but haven't been
  246.         // rendered would be lost (noticeable with audio).
  247.         Observable.merge(
  248.             NotificationCenter.default.rx
  249.                 .notification(UIApplication.didBecomeActiveNotification),
  250.             NotificationCenter.default.rx
  251.                 .notification(UIApplication.willEnterForegroundNotification)
  252.         )
  253.         .withLatestFrom(sampleBufferProvider.status)
  254.         .filter { $0 == .stopped }
  255.         .subscribe(onNext: { [weak self] _ in
  256.             guard let self = self else { return }
  257.             self.bufferQueue.async {
  258.                 // Restart reading asset when app comes back from background, because
  259.                 // audio sample buffers might have been read until end
  260.                 self.sampleBufferProvider.restartReadingAsset()
  261.                 self.sampleBufferProvider.startProvidingSampleBuffers()
  262.             }
  263.         })
  264.         .disposed(by: disposeBag)
  265.     }
  266.  
  267.     private func bindDisplayLayer() {
  268.         // When both YoutubeVideoPlayer and this view are on screen
  269.         // display layer might fail rendering:
  270.         //      - when youtube starts playing if youtube video isn't muted
  271.         //      - when youtube is paused and this view starts playing video while unmuted
  272.         //      - when youtube us paused and this view is playing and video gets unmuted
  273.         // In those cases we catch that status change and depending on the current view status we need to re-render the
  274.         // last frame and possibly restart requesting videos
  275.  
  276.         if let displayLayer = displayLayer {
  277.             displayLayer
  278.                 .rx.observeWeakly(AVQueuedSampleBufferRenderingStatus.self, "status")
  279.                 .withLatestFrom(sampleBufferProvider.status)
  280.                 .subscribe(onNext: { [weak self] status in
  281.                     guard
  282.                         let self = self, let displayLayer = self.displayLayer,
  283.                         displayLayer.status == .failed else { return }
  284.                     self.bufferQueue.async {
  285.                         // Display layer has failed presenting,
  286.                         // First we flush the display layer
  287.                         displayLayer.flush()
  288.  
  289.                         switch status {
  290.                         case .providing:
  291.                             // If video was playing we start requesting sample buffers again and start playing
  292.                             self.startRequestingVideo()
  293.                             self.startSynchronizer()
  294.                         case .paused, .finished:
  295.                             // If video was paused or finished we request the
  296.                             // last sample buffer and set it for display
  297.                             self.setDisplayBuffer(self.sampleBufferProvider.lastVideoSampleBuffer)
  298.                         default:
  299.                             break
  300.                         }
  301.                     }
  302.                 })
  303.                 .disposed(by: disposeBag)
  304.         }
  305.     }
  306.  
  307.     private func bindAudioRendered() {
  308.         if let audioRenderer = audioRenderer {
  309.             audioRenderer
  310.                 .rx.observeWeakly(AVQueuedSampleBufferRenderingStatus.self, "status")
  311.                 .withLatestFrom(sampleBufferProvider.status)
  312.                 .subscribe(onNext: { [weak self] status in
  313.                     guard
  314.                         let self = self, let audioRenderer = self.audioRenderer,
  315.                         audioRenderer.status == .failed else { return }
  316.                     self.bufferQueue.async {
  317.                         // Audio renderer has failed playing sample buffers
  318.                         // First we flush the sample buffers
  319.                         audioRenderer.flush()
  320.  
  321.                         // If audio was playing we start requesting sample buffers again
  322.                         // and start playing
  323.                         switch status {
  324.                         case .providing:
  325.                             self.startRequestingAudio()
  326.                             self.startSynchronizer()
  327.                         default:
  328.                             break
  329.                         }
  330.                     }
  331.                 })
  332.                 .disposed(by: disposeBag)
  333.         }
  334.     }
  335.  
  336.     private func setDisplayBuffer(_ sampleBuffer: CMSampleBuffer?) {
  337.         guard
  338.             let displayLayer = displayLayer,
  339.             displayLayer.isReadyForMoreMediaData,
  340.             let sampleBuffer = sampleBuffer
  341.         else {
  342.             return
  343.         }
  344.  
  345.         displayLayer.enqueue(sampleBuffer)
  346.     }
  347.  
  348.     private func finished() {
  349.         if let periodicObserverToken = periodicObserverToken {
  350.             synchronizer.removeTimeObserver(periodicObserverToken)
  351.             self.periodicObserverToken = nil
  352.         }
  353.         // Set the rate and time to 0 then flush the renderers for replayability
  354.         synchronizer.setRate(0, time: .zero)
  355.         audioRenderer?.flush()
  356.         displayLayer?.flush()
  357.     }
  358. }
  359.  
  360. extension VideoPlayerContentView: VideoPlayerProtocol {
  361.     var currentTime: Observable<Double> {
  362.         currentTimeRelay.asObservable()
  363.     }
  364.  
  365.     var playerStatus: Observable<VideoPlayerStatus> {
  366.         return playerStatusRelay.asObservable()
  367.     }
  368.  
  369.     func play() {
  370.         bufferQueue.async {
  371.             self.playerStatus
  372.                 .take(1)
  373.                 .flatMapCompletable { [weak self] status in
  374.                     guard let self = self else { return .empty() }
  375.                     if case .finished = status {
  376.                         return self.play(from: .zero)
  377.                     } else if case .paused = status {
  378.                         let time = self.synchronizer.currentTime()
  379.                         return self.play(from: time)
  380.                     } else if case .readyToPlay = status {
  381.                         self.sampleBufferProvider.startProvidingSampleBuffers()
  382.                     }
  383.                     return .empty()
  384.                 }
  385.                 .subscribe()
  386.                 .disposed(by: self.disposeBag)
  387.         }
  388.     }
  389.  
  390.     func pause() {
  391.         bufferQueue.async {
  392.             self.sampleBufferProvider.pause()
  393.         }
  394.     }
  395.  
  396.     private func play(from time: CMTime) -> Completable {
  397.         sampleBufferProvider.prepareToRead(from: time)
  398.         return playerStatus
  399.             .filter { $0 == .readyToPlay }
  400.             .take(1)
  401.             .flatMapCompletable { [weak self] _ in
  402.                 guard let self = self else { return .empty() }
  403.                 self.sampleBufferProvider.startProvidingSampleBuffers()
  404.                 return .empty()
  405.             }
  406.     }
  407. }