// // PlayerContainerView.swift // Mastodon // // Created by xiaojian sun on 2021/3/10. // import os.log import AVKit import UIKit import Combine protocol PlayerContainerViewDelegate: AnyObject { func playerContainerView(_ playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) } final class PlayerContainerView: UIView { static let cornerRadius: CGFloat = ContentWarningOverlayView.cornerRadius private let container = UIView() private let touchBlockingView = TouchBlockingView() private var containerHeightLayoutConstraint: NSLayoutConstraint! let contentWarningOverlayView: ContentWarningOverlayView = { let contentWarningOverlayView = ContentWarningOverlayView() contentWarningOverlayView.update(cornerRadius: PlayerContainerView.cornerRadius) return contentWarningOverlayView }() let playerViewController = AVPlayerViewController() let blurhashOverlayImageView = UIImageView() let mediaTypeIndicatorView = MediaTypeIndicatorView() weak var delegate: PlayerContainerViewDelegate? private var isReadyForDisplayObservation: NSKeyValueObservation? let isReadyForDisplay = CurrentValueSubject(false) override init(frame: CGRect) { super.init(frame: frame) _init() } required init?(coder: NSCoder) { super.init(coder: coder) _init() } } extension PlayerContainerView { private func _init() { // accessibility accessibilityIgnoresInvertColors = true container.translatesAutoresizingMaskIntoConstraints = false addSubview(container) containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1) NSLayoutConstraint.activate([ container.topAnchor.constraint(equalTo: topAnchor), container.leadingAnchor.constraint(equalTo: leadingAnchor), trailingAnchor.constraint(equalTo: container.trailingAnchor), bottomAnchor.constraint(equalTo: container.bottomAnchor), containerHeightLayoutConstraint, ]) // will not influence full-screen playback playerViewController.view.layer.masksToBounds = true playerViewController.view.layer.cornerRadius = PlayerContainerView.cornerRadius playerViewController.view.layer.cornerCurve = .continuous blurhashOverlayImageView.translatesAutoresizingMaskIntoConstraints = false playerViewController.contentOverlayView!.addSubview(blurhashOverlayImageView) NSLayoutConstraint.activate([ blurhashOverlayImageView.topAnchor.constraint(equalTo: playerViewController.contentOverlayView!.topAnchor), blurhashOverlayImageView.leadingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.leadingAnchor), blurhashOverlayImageView.trailingAnchor.constraint(equalTo: playerViewController.contentOverlayView!.trailingAnchor), blurhashOverlayImageView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), ]) // mediaType mediaTypeIndicatorView.translatesAutoresizingMaskIntoConstraints = false playerViewController.contentOverlayView!.addSubview(mediaTypeIndicatorView) NSLayoutConstraint.activate([ mediaTypeIndicatorView.bottomAnchor.constraint(equalTo: playerViewController.contentOverlayView!.bottomAnchor), mediaTypeIndicatorView.rightAnchor.constraint(equalTo: playerViewController.contentOverlayView!.rightAnchor), mediaTypeIndicatorView.heightAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.height).priority(.required - 1), mediaTypeIndicatorView.widthAnchor.constraint(equalToConstant: MediaTypeIndicatorView.indicatorViewSize.width).priority(.required - 1), ]) isReadyForDisplayObservation = playerViewController.observe(\.isReadyForDisplay, options: [.initial, .new]) { [weak self] playerViewController, _ in guard let self = self else { return } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: isReadyForDisplay: %s", (#file as NSString).lastPathComponent, #line, #function, playerViewController.isReadyForDisplay.description) self.isReadyForDisplay.value = playerViewController.isReadyForDisplay } contentWarningOverlayView.delegate = self } } // MARK: - ContentWarningOverlayViewDelegate extension PlayerContainerView: ContentWarningOverlayViewDelegate { func contentWarningOverlayViewDidPressed(_ contentWarningOverlayView: ContentWarningOverlayView) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) delegate?.playerContainerView(self, contentWarningOverlayViewDidPressed: contentWarningOverlayView) } } extension PlayerContainerView { func reset() { // note: set playerViewController.player pause() and nil in data source configuration process make reloadData not break playing playerViewController.willMove(toParent: nil) playerViewController.view.removeFromSuperview() playerViewController.removeFromParent() blurhashOverlayImageView.image = nil container.subviews.forEach { subview in subview.removeFromSuperview() } } func setupPlayer(aspectRatio: CGSize, maxSize: CGSize, parent: UIViewController?) -> AVPlayerViewController { reset() touchBlockingView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(touchBlockingView) NSLayoutConstraint.activate([ touchBlockingView.topAnchor.constraint(equalTo: container.topAnchor), touchBlockingView.leadingAnchor.constraint(equalTo: container.leadingAnchor), touchBlockingView.bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) let rect = AVMakeRect( aspectRatio: aspectRatio, insideRect: CGRect(origin: .zero, size: maxSize) ).integral parent?.addChild(playerViewController) playerViewController.view.translatesAutoresizingMaskIntoConstraints = false touchBlockingView.addSubview(playerViewController.view) parent.flatMap { playerViewController.didMove(toParent: $0) } NSLayoutConstraint.activate([ playerViewController.view.topAnchor.constraint(equalTo: touchBlockingView.topAnchor), playerViewController.view.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor), playerViewController.view.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor), playerViewController.view.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor), touchBlockingView.widthAnchor.constraint(equalToConstant: rect.width).priority(.required - 1), ]) containerHeightLayoutConstraint.constant = rect.height containerHeightLayoutConstraint.isActive = true playerViewController.view.frame.size = rect.size contentWarningOverlayView.removeFromSuperview() contentWarningOverlayView.translatesAutoresizingMaskIntoConstraints = false addSubview(contentWarningOverlayView) NSLayoutConstraint.activate([ contentWarningOverlayView.topAnchor.constraint(equalTo: touchBlockingView.topAnchor), contentWarningOverlayView.leadingAnchor.constraint(equalTo: touchBlockingView.leadingAnchor), contentWarningOverlayView.trailingAnchor.constraint(equalTo: touchBlockingView.trailingAnchor), contentWarningOverlayView.bottomAnchor.constraint(equalTo: touchBlockingView.bottomAnchor) ]) bringSubviewToFront(mediaTypeIndicatorView) return playerViewController } func setMediaKind(kind: VideoPlayerViewModel.Kind) { mediaTypeIndicatorView.setMediaKind(kind: kind) } func setMediaIndicator(isHidden: Bool) { mediaTypeIndicatorView.alpha = isHidden ? 0 : 1 } }