JohnCoates/Aerial

View on GitHub
Aerial/Source/Views/AerialView+Player.swift

Summary

Maintainability
C
7 hrs
Test Coverage
//
//  AerialView+Player.swift
//  Aerial
//
//  Created by Guillaume Louel on 06/12/2019.
//  Copyright © 2019 Guillaume Louel. All rights reserved.
//

import Foundation
import AVFoundation
import AVKit

extension AerialView {
    func setupPlayerLayer(withPlayer player: AVPlayer) {
        let displayDetection = DisplayDetection.sharedInstance

        self.layer = CALayer()
        guard let layer = self.layer else {
            errorLog("\(self.description) Couldn't create CALayer")
            return
        }
        self.wantsLayer = true
        layer.backgroundColor = NSColor.black.cgColor
        layer.needsDisplayOnBoundsChange = true
        layer.frame = self.bounds
        debugLog("\(self.description) setting up player layer with bounds/frame: \(layer.bounds) / \(layer.frame)")

        playerLayer = AVPlayerLayer(player: player)

        // Fill/fit is only available in 10.10+
        if #available(OSX 10.10, *) {
            if PrefsDisplays.aspectMode == .fill {
                playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
            } else {
                playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
            }
        }
        playerLayer.autoresizingMask = [CAAutoresizingMask.layerWidthSizable, CAAutoresizingMask.layerHeightSizable]

        // In case of span mode we need to compute the size of our layer
        if PrefsDisplays.viewingMode == .spanned && !isPreview {
            let zRect = displayDetection.getZeroedActiveSpannedRect()
            debugLog("foundScreen check : \(foundScreen.debugDescription)")
            
            if let scr = foundScreen {
                let tRect = CGRect(x: zRect.origin.x - scr.zeroedOrigin.x,
                                   y: zRect.origin.y - scr.zeroedOrigin.y,
                                   width: zRect.width,
                                   height: zRect.height)
                debugLog("tRect : \(tRect)")
                playerLayer.frame = tRect
            } else {
                debugLog("This is an unknown screen in span mode, workarounding...")
                
                if let alternateScreen = DisplayDetection.sharedInstance.alternateFindScreenWith(frame: self.frame) {
                    foundScreen = alternateScreen
                    debugLog("📺 alternate screen found : \(alternateScreen.description)")
                    let tRect = CGRect(x: zRect.origin.x - alternateScreen.zeroedOrigin.x,
                                       y: zRect.origin.y - alternateScreen.zeroedOrigin.y,
                                       width: zRect.width,
                                       height: zRect.height)
                    playerLayer.frame = tRect
                } else {
                    errorLog("No alternate screen found, reverting to single screen mode")
                    playerLayer.frame = layer.bounds
                }
            }
        } else {
            playerLayer.frame = layer.bounds

            // "true" mirrored mode
            let index = AerialView.instanciatedViews.firstIndex(of: self) ?? 0
            if index % 2 == 1 && PrefsDisplays.viewingMode == .mirrored {
                playerLayer.transform = CATransform3DMakeAffineTransform(CGAffineTransform(scaleX: -1, y: 1))
            }
        }
        layer.addSublayer(playerLayer)

        layer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0
        self.playerLayer.contentsScale = (self.window?.backingScaleFactor) ?? 1.0

        
        // The layers for descriptions, clock, message
        // On Sonoma we can't use the reported frame!
        if foundFrame != nil {
            layerManager.setupExtraLayers(layer: layer, frame: foundFrame!)
        } else {
            layerManager.setupExtraLayers(layer: layer, frame: self.frame)
        }
        // Make sure we set the retinaness here
        layerManager.setContentScale(scale: self.window?.backingScaleFactor ?? 1.0)

        // An extra layer to try and contravent a macOS graphics driver bug
        // This is useful on High Sierra+ on Intel Macs
        if #available(macOS 12.0, *) {
        } else {
            setupGlitchWorkaroundLayer(layer: layer)
        }
   }

    // MARK: - AVPlayerItem Notifications

    @objc func playerItemFailedtoPlayToEnd(_ aNotification: Notification) {
        warnLog("\(self.description) AVPlayerItemFailedToPlayToEndTimeNotification \(aNotification)")
        playNextVideo()
    }

    @objc func playerItemNewErrorLogEntryNotification(_ aNotification: Notification) {
        warnLog("\(self.description) AVPlayerItemNewErrorLogEntryNotification \(aNotification)")
    }

    @objc func playerItemPlaybackStalledNotification(_ aNotification: Notification) {
        warnLog("\(self.description) AVPlayerItemPlaybackStalledNotification \(aNotification)")
    }

    @objc func playerItemDidReachEnd(_ aNotification: Notification) {
        debugLog("\(self.description) played did reach end")
        debugLog("\(self.description) notification: \(aNotification)")

        if shouldLoop {
            debugLog("Rewinding video!")
            if let playerItem = aNotification.object as? AVPlayerItem {
                playerItem.seek(to: CMTime.zero, completionHandler: nil)
            }
        } else {
            playNextVideo()
            debugLog("\(self.description) playing next video for player \(String(describing: player))")
        }

    }

    // Video fade-in/out
    func addPlayerFades(view: AerialView, player: AVPlayer, video: AerialVideo) {
        if !Aerial.helper.underCompanion {
            // We only fade in/out if we have duration
            if video.duration > 0 && AerialView.shouldFade && !shouldLoop {
                let playbackSpeed = Double(PlaybackSpeed.forVideo(video.id))

                view.playerLayer.opacity = 0
                let fadeAnimation = CAKeyframeAnimation(keyPath: "opacity")
                fadeAnimation.values = [0, 1, 1, 0] as [Int]
                fadeAnimation.keyTimes = [0,
                                          AerialView.fadeDuration/(video.duration/playbackSpeed),
                                          1-(AerialView.fadeDuration/(video.duration/playbackSpeed)), 1 ] as [NSNumber]

                fadeAnimation.duration = video.duration/playbackSpeed
                fadeAnimation.calculationMode = CAAnimationCalculationMode.cubic
                view.playerLayer.add(fadeAnimation, forKey: "mainfade")
            } else {
                view.playerLayer.opacity = 1.0
            }
        } else {
            view.playerLayer.opacity = 1.0
        }
    }

    func removePlayerFades() {
        self.playerLayer.removeAllAnimations()
        self.playerLayer.opacity = 1.0
    }
    
    // This works pre Catalina as of right now
    func setupGlitchWorkaroundLayer(layer: CALayer) {
        debugLog("Using dot workaround for video driver corruption")

        let workaroundLayer = CATextLayer()
        workaroundLayer.frame = self.bounds
        workaroundLayer.opacity = 1.0
        workaroundLayer.font = NSFont(name: "Helvetica Neue Medium", size: 4)
        workaroundLayer.fontSize = 4
        workaroundLayer.string = "."

        let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: workaroundLayer.font as Any]

        // Calculate bounding box
        let attrString = NSAttributedString(string: workaroundLayer.string as! String, attributes: attributes)
        let rect = attrString.boundingRect(with: layer.visibleRect.size, options: NSString.DrawingOptions.usesLineFragmentOrigin)

        workaroundLayer.frame = rect
        workaroundLayer.position = CGPoint(x: 2, y: 2)
        workaroundLayer.anchorPoint = CGPoint(x: 0, y: 0)
        layer.addSublayer(workaroundLayer)
    }
}