前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AVKit框架详细解析(三) —— 基于视频播放器的画中画实现(二)

AVKit框架详细解析(三) —— 基于视频播放器的画中画实现(二)

原创
作者头像
conanma
修改2021-09-06 10:07:41
1.8K0
修改2021-09-06 10:07:41
举报
文章被收录于专栏:正则正则

源码

1. Swift

接着就是源码了

代码语言:javascript
复制
1. AppDelegate.swift
代码语言:javascript
复制
import AVKit
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
    let audioSession = AVAudioSession.sharedInstance()
    do {
      try audioSession.setCategory(.playback, mode: .moviePlayback)
    } catch {
      print("Failed to set audioSession category to playback")
    }

    return true
  }

  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(
      name: "Default Configuration",
      sessionRole: connectingSceneSession.role)
  }
}
代码语言:javascript
复制
2. SceneDelegate.swift
代码语言:javascript
复制
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = scene as? UIWindowScene else {
      return
    }

    window = UIWindow(windowScene: windowScene)
    window?.rootViewController = UINavigationController(rootViewController: CategoryListViewController())
    window?.makeKeyAndVisible()

    #if targetEnvironment(macCatalyst)
    if let titlebar = window?.windowScene?.titlebar {
      titlebar.titleVisibility = .hidden
      titlebar.toolbar = nil
    }
    #endif
  }
}
代码语言:javascript
复制
3. CategoryProvider.swift
代码语言:javascript
复制
import Foundation

class CategoryProvider: ObservableObject {
  @Published var categories: [Category] = []
  var currentContext: ProviderContext = .general
  var dataProvider: DataProvider

  init(dataProvider: DataProvider) {
    self.dataProvider = dataProvider
    refresh()
  }

  enum ProviderContext: Equatable {
    case general, favorites, lotsOfVideos

    var formattedName: String {
      switch self {
      case .general: return "All Videos"
      case .favorites: return "Favorites"
      case .lotsOfVideos: return "Lots of Videos"
      }
    }

    var missingCategoriesTitle: String {
      if self == .general || self == .lotsOfVideos {
        return "Couldn't find any videos..."
      } else {
        return "No Favorite Videos"
      }
    }

    var missingCategoriesDescription: String {
      if self == .general || self == .lotsOfVideos {
        return "No videos or categories were loaded. Has something gone wrong?"
      } else {
        return "You haven't favorited any videos."
      }
    }
  }

  public func refresh() {
    switch currentContext {
    case .general:
      categories = dataProvider.categories
    case .lotsOfVideos:
      categories = dataProvider.massiveCategoryList
    case .favorites:
      categories = dataProvider.categoriesWithFavoriteVideos
    }
  }
}
代码语言:javascript
复制
4. DataProvider.swift
代码语言:javascript
复制
import Foundation

class DataProvider: ObservableObject {
  @Published var categories: [Category] = []

  /// Compute new categories that only contain favorited videos. Based on the `categories` object.
  var categoriesWithFavoriteVideos: [Category] {
    return categories.filter { category in
      return !category.favoriteVideos.isEmpty
    }
  }

  var massiveCategoryList: [Category] {
    var categories: [Category] = []
    for _ in 0..<100 {
      categories.append(contentsOf: self.categories)
    }
    return categories
  }

  init() {
    categories = [
      Category(title: "SwiftUI", videos: [
        Video(
          title: "SwiftUI",
          description: "",
          thumbnailName: "swiftui")
      ]),
      Category(title: "UIKit", videos: [
        Video(
          title: "Demystifying Views in iOS",
          description: "",
          thumbnailName: "views"),
        Video(
          title: "Reproducing Popular iOS Controls",
          description: "",
          thumbnailName: "controls")
      ]),
      Category(title: "Frameworks", videos: [
        Video(
          title: "Fastlane for iOS",
          description: "",
          thumbnailName: "fastlane"),
        Video(
          title: "Beginning RxSwift",
          description: "",
          thumbnailName: "rxswift")
      ]),
      Category(title: "Miscellaneous", videos: [
        Video(
          title: "Data Structures & Algorithms in Swift",
          description: "",
          thumbnailName: "datastructures"),
        Video(
          title: "Beginning ARKit",
          description: "Learn about ARKit in this amazing tutorial!",
          thumbnailName: "arkit"),
        Video(
          title: "Machine Learning in iOS",
          description: "",
          thumbnailName: "machinelearning"),
        Video(
          title: "Push Notifications",
          description: "",
          thumbnailName: "notifications")
      ])
    ]
  }
}
代码语言:javascript
复制
5. Video.swift
代码语言:javascript
复制
import Foundation

class Video: Identifiable, Equatable, ObservableObject, Hashable {
  var id = UUID()
  var title: String
  var description: String
  var thumbnailName: String
  @Published var favorite = false

  init(
    title: String,
    description: String,
    thumbnailName: String
  ) {
    self.title = title
    self.description = description
    self.thumbnailName = thumbnailName
  }

  static func == (lhs: Video, rhs: Video) -> Bool {
    return lhs.id == rhs.id
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }
}
代码语言:javascript
复制
6. Category.swift
代码语言:javascript
复制
import Foundation

class Category: Identifiable, Hashable, Equatable {
  var id = UUID()
  var title: String
  var videos: [Video]
  var favoriteVideos: [Video] {
    return videos.filter { video in
      return video.favorite
    }
  }

  init(title: String, videos: [Video]) {
    self.title = title
    self.videos = videos
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(id)
  }

  static func == (lhs: Category, rhs: Category) -> Bool {
    return lhs.id == rhs.id
  }
}
代码语言:javascript
复制
7. CategoryListViewController.swift
代码语言:javascript
复制
import UIKit
import AVKit

class CategoryListViewController: UICollectionViewController {
  // MARK: - Properties
  private var dataProvider = DataProvider()
  private lazy var dataSource = makeDataSource()

  // MARK: - Value Types
  typealias DataSource = UICollectionViewDiffableDataSource<Category, Video>
  typealias Snapshot = NSDiffableDataSourceSnapshot<Category, Video>

  init() {
    super.init(collectionViewLayout: UICollectionViewLayout())
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    #if os(tvOS)
    collectionView.backgroundColor = .clear
    #else
    collectionView.backgroundColor = .systemBackground
    navigationItem.largeTitleDisplayMode = .automatic
    navigationController?.navigationBar.prefersLargeTitles = true
    #endif

    title = "Categories"

    configureLayout()
    applySnapshot(animatingDifferences: false)
  }

  // MARK: - Functions
  func makeDataSource() -> DataSource {
    collectionView.register(
      VideoCollectionViewCell.self,
      forCellWithReuseIdentifier: VideoCollectionViewCell.reuseIdentifier)

    let dataSource = DataSource(
      collectionView: collectionView
    ) { collectionView, indexPath, video -> UICollectionViewCell? in
      let cell = collectionView.dequeueReusableCell(
        withReuseIdentifier: "VideoCollectionViewCell",
        for: indexPath) as? VideoCollectionViewCell
      cell?.video = video
      cell?.layoutSubviews()
      return cell
    }
    dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in
      guard kind == UICollectionView.elementKindSectionHeader else {
        return nil
      }
      let section = self.dataSource.snapshot()
        .sectionIdentifiers[indexPath.section]
      let view = collectionView.dequeueReusableSupplementaryView(
        ofKind: kind,
        withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier,
        for: indexPath) as? SectionHeaderReusableView
      view?.titleLabel.text = section.title
      return view
    }
    return dataSource
  }

  func applySnapshot(animatingDifferences: Bool = true) {
    var snapshot = Snapshot()
    snapshot.appendSections(dataProvider.categories)

    dataProvider.categories.forEach { category in
      snapshot.appendItems(category.videos, toSection: category)
    }

    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
  }
}

// MARK: - UICollectionViewDataSource Implementation
extension CategoryListViewController {
  override func collectionView(
    _ collectionView: UICollectionView,
    didSelectItemAt indexPath: IndexPath
  ) {
    guard let videoURL = Bundle.main.url(forResource: "rick", withExtension: "mp4") else {
      return
    }

    let item = AVPlayerItem(url: videoURL)
    let player = AVQueuePlayer(
      playerItem: item)

    player.actionAtItemEnd = .pause

    presentPlayerController(with: player, customPlayer: true)
  }

  func presentPlayerController(with player: AVPlayer, customPlayer: Bool = false) {
    let controller: UIViewController

    if customPlayer {
      let customController = CustomPlayerViewController()
      customController.delegate = self
      customController.player = player
      controller = customController
    } else {
      let avController = AVPlayerViewController()
      avController.delegate = self
      avController.player = player
      controller = avController
    }

    present(controller, animated: true) {
      player.play()
    }
  }
}

// MARK: - Layout Handling
extension CategoryListViewController {
  private func configureLayout() {
    collectionView.register(
      SectionHeaderReusableView.self,
      forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
      withReuseIdentifier: SectionHeaderReusableView.reuseIdentifier)

    let layout = UICollectionViewCompositionalLayout { _, layoutEnvironment -> NSCollectionLayoutSection? in
      let isPhone = layoutEnvironment.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiom.phone
      let size = NSCollectionLayoutSize(
        widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
        heightDimension: NSCollectionLayoutDimension.absolute(isPhone ? 280 : 250))

      var itemCount = 1

      #if targetEnvironment(macCatalyst) || os(tvOS)
      let width = self.view.frame.width

      if width >= 1800 {
        itemCount = 6
      } else if width >= 1200 {
        itemCount = 4
      } else if width >= 1000 {
        itemCount = 3
      } else {
        itemCount = 2
      }

      #if os(tvOS)
      itemCount -= 1
      #endif

      #else
      let orientation = UIDevice.current.orientation
      if isPhone {
        itemCount = orientation == .landscapeRight || orientation == .landscapeLeft ? 2 : 1
      } else {
        itemCount = orientation == .portrait || orientation == .portraitUpsideDown ? 3 : 4
      }
      #endif

      let item = NSCollectionLayoutItem(layoutSize: size)
      let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitem: item, count: itemCount)
      let section = NSCollectionLayoutSection(group: group)
      section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
      #if os(tvOS)
      section.contentInsets.trailing = 20
      #endif
      // Supplementary header view setup
      let headerFooterSize = NSCollectionLayoutSize(
        widthDimension: .fractionalWidth(1.0),
        heightDimension: .estimated(20))
      let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
        layoutSize: headerFooterSize,
        elementKind: UICollectionView.elementKindSectionHeader,
        alignment: .top)
      section.boundarySupplementaryItems = [sectionHeader]
      return section
    }

    collectionView.collectionViewLayout = layout
  }

  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    coordinator.animate(alongsideTransition: { _ in
      self.collectionView.collectionViewLayout.invalidateLayout()
    }, completion: nil)
  }
}

extension CategoryListViewController: AVPlayerViewControllerDelegate {
  @objc func playerViewControllerShouldDismiss(_ playerViewController: AVPlayerViewController) -> Bool {
    if let presentedViewController = presentedViewController as? AVPlayerViewController,
      presentedViewController == playerViewController {
      return true
    }
    return false
  }

  @objc func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: AVPlayerViewController) -> Bool {
    // Dismiss the controller when PiP starts so that the user is returned to the item selection screen.
    return true
  }

  @objc func playerViewController(
    _ playerViewController: AVPlayerViewController,
    restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
  ) {
    restore(playerViewController: playerViewController, completionHandler: completionHandler)
  }
}

extension CategoryListViewController: CustomPlayerViewControllerDelegate {
  func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_ playerViewController: CustomPlayerViewController) -> Bool {
    // Dismiss the controller when PiP starts so that the user is returned to the item selection screen.
    return true
  }

  func playerViewController(
    _ playerViewController: CustomPlayerViewController,
    restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
  ) {
    restore(playerViewController: playerViewController, completionHandler: completionHandler)
  }
}

extension CategoryListViewController {
  func restore(playerViewController: UIViewController, completionHandler: @escaping (Bool) -> Void) {
    if let presentedViewController = presentedViewController {
      presentedViewController.dismiss(animated: false) { [weak self] in
        self?.present(playerViewController, animated: false) {
          completionHandler(true)
        }
      }
    } else {
      present(playerViewController, animated: false) {
        completionHandler(true)
      }
    }
  }
}
代码语言:javascript
复制
8. VideoCollectionViewCell.swift
代码语言:javascript
复制
import UIKit

class VideoCollectionViewCell: UICollectionViewCell {
  private lazy var thumbnailView: UIImageView = {
    let imageView = UIImageView()
    imageView.translatesAutoresizingMaskIntoConstraints = false
    imageView.layer.cornerRadius = 8
    imageView.layer.cornerCurve = .continuous
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = true
    return imageView
  }()

  private lazy var titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    label.numberOfLines = 1
    #if os(tvOS)
    label.font = UIFont.systemFont(ofSize: 28, weight: .semibold)
    #else
    label.font = UIFont.preferredFont(forTextStyle: .headline)
    #endif
    return label
  }()

  private lazy var subtitleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false
    #if os(tvOS)
    label.font = UIFont.systemFont(ofSize: 28, weight: .regular)
    #else
    label.font = UIFont.preferredFont(forTextStyle: .body)
    #endif
    return label
  }()

  var video: Video? {
    didSet {
      thumbnailView.layer.shadowOffset = CGSize(width: 2, height: 2)
      thumbnailView.layer.shadowRadius = 10
      thumbnailView.layer.shadowOpacity = 0.3

      thumbnailView.image = UIImage(named: video?.thumbnailName ?? "")
      titleLabel.text = video?.title
      subtitleLabel.text = video?.description
    }
  }

  public static var reuseIdentifier: String {
    return String(describing: self)
  }

  override init(frame: CGRect) {
    super.init(frame: frame)

    contentView.addSubview(thumbnailView)
    contentView.addSubview(titleLabel)
    contentView.addSubview(subtitleLabel)

    layoutSubviews()
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func layoutSubviews() {
    super.layoutSubviews()

    var spacing: CGFloat = 8
    #if os(tvOS)
    spacing = 12
    #endif

    NSLayoutConstraint.activate([
      thumbnailView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: spacing),
      thumbnailView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: spacing),
      thumbnailView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -spacing),
      thumbnailView.heightAnchor.constraint(lessThanOrEqualTo: contentView.heightAnchor, multiplier: 3 / 4),

      titleLabel.topAnchor.constraint(equalTo: thumbnailView.bottomAnchor, constant: 5),
      titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: spacing),
      titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -spacing),

      subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
      subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
      subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor)
    ])
  }

  #if os(tvOS)
  override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
    let propertyAnimator: UIViewPropertyAnimator

    if isFocused {
      propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeOut) {
        self.transform = CGAffineTransform(scaleX: 1.1, y: 1.1)
      }
    } else {
      propertyAnimator = UIViewPropertyAnimator(duration: 0.2, curve: .easeIn) {
        self.transform = .identity
      }
    }

    propertyAnimator.startAnimation()
  }
  #endif
}
代码语言:javascript
复制
9. SectionHeaderReusableView.swift
代码语言:javascript
复制
import UIKit

class SectionHeaderReusableView: UICollectionReusableView {
  static var reuseIdentifier: String {
    return String(describing: SectionHeaderReusableView.self)
  }

  lazy var titleLabel: UILabel = {
    let label = UILabel()
    label.translatesAutoresizingMaskIntoConstraints = false

    var textStyle = UIFont.TextStyle.title1
    #if os(tvOS)
    textStyle = .title3
    #endif
    label.font = UIFont.systemFont(
      ofSize: UIFont.preferredFont(forTextStyle: textStyle).pointSize,
      weight: .bold)
    label.adjustsFontForContentSizeCategory = true
    label.textColor = .label
    label.textAlignment = .left
    label.numberOfLines = 1
    label.setContentCompressionResistancePriority(
      .defaultHigh, for: .horizontal)
    return label
  }()

  override init(frame: CGRect) {
    super.init(frame: frame)
    var usingtvOS = false
    #if os(tvOS)
    usingtvOS = true
    #endif

    #if os(tvOS) || targetEnvironment(macCatalyst)
    backgroundColor = .clear
    #else
    backgroundColor = .systemBackground
    #endif
    addSubview(titleLabel)
    if UIDevice.current.userInterfaceIdiom == .pad || usingtvOS {
      NSLayoutConstraint.activate([
        titleLabel.leadingAnchor.constraint(
          equalTo: leadingAnchor,
          constant: 5),
        titleLabel.trailingAnchor.constraint(
          lessThanOrEqualTo: trailingAnchor,
          constant: -5)
      ])
    } else {
      NSLayoutConstraint.activate([
        titleLabel.leadingAnchor.constraint(
          equalTo: readableContentGuide.leadingAnchor),
        titleLabel.trailingAnchor.constraint(
          lessThanOrEqualTo: readableContentGuide.trailingAnchor)
      ])
    }
    NSLayoutConstraint.activate([
      titleLabel.topAnchor.constraint(
        equalTo: topAnchor,
        constant: 10),
      titleLabel.bottomAnchor.constraint(
        equalTo: bottomAnchor,
        constant: -10)
    ])
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
代码语言:javascript
复制
10. CustomPlayerViewController.swift
代码语言:javascript
复制
import UIKit
import AVKit

/// The Set of custom player controllers currently using or transitioning out of PiP
private var activeCustomPlayerViewControllers = Set<CustomPlayerViewController>()

public class CustomPlayerViewController: UIViewController {
  weak var delegate: CustomPlayerViewControllerDelegate?

  public var player: AVPlayer? {
    didSet {
      playerLayer = AVPlayerLayer(player: player)
    }
  }

  private var playerLayer: AVPlayerLayer?
  private var pictureInPictureController: AVPictureInPictureController?
  private var controlsView: CustomPlayerControlsView?

  init() {
    super.init(nibName: nil, bundle: nil)
    modalPresentationStyle = .fullScreen
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override public func viewDidLoad() {
    super.viewDidLoad()

    guard let playerLayer = playerLayer else {
      fatalError("Missing AVPlayerLayer")
    }

    view.backgroundColor = .black
    view.layer.addSublayer(playerLayer)

    pictureInPictureController = AVPictureInPictureController(
      playerLayer: playerLayer)
    pictureInPictureController?.delegate = self

    let tapGestureRecognizer = UITapGestureRecognizer(
      target: self,
      action: #selector(tapGestureHandler))


    #if os(tvOS)
    tapGestureRecognizer.allowedTouchTypes = [UITouch.TouchType.indirect].map { $0.rawValue as NSNumber
    }
    tapGestureRecognizer.allowedPressTypes = []
    #endif

    view.addGestureRecognizer(tapGestureRecognizer)
  }

  override public func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    playerLayer?.frame = view.bounds
  }

  @objc private func tapGestureHandler(recognizer: UITapGestureRecognizer) {
    switch recognizer.state {
    case .ended:
      if controlsView == nil {
        showControls()
      } else {
        hideControls()
      }
    default:
      break
    }
  }

  private func showControls() {
    let controlsView = CustomPlayerControlsView(player: player, pipController: pictureInPictureController)
    controlsView.delegate = self
    controlsView.translatesAutoresizingMaskIntoConstraints = false

    controlsView.alpha = 0.0

    let controlsViewHeight: CGFloat = 180.0

    view.addSubview(controlsView)
    NSLayoutConstraint.activate([
      controlsView.heightAnchor.constraint(equalToConstant: controlsViewHeight),
      controlsView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 90),
      controlsView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -90),
      controlsView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -60)
    ])

    UIView.animate(withDuration: 0.25) {
      controlsView.alpha = 1.0
    }

    self.controlsView = controlsView

    // Set the additional bottom safe area inset to the height of the custom UI so existing PiP windows avoid it.
    additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: controlsViewHeight, right: 0)
  }

  private func hideControls() {
    guard let controlsView = controlsView else { return }

    UIView.animate(withDuration: 0.25) {
      controlsView.alpha = 0.0
    } completion: { _ in
      controlsView.removeFromSuperview()
      self.controlsView = nil
    }

    // Reset the safe area inset to its default value.
    additionalSafeAreaInsets = .zero
  }
}

extension CustomPlayerViewController: CustomPlayerControlsViewDelegate {
  func controlsViewDidRequestStartPictureInPicture(
    _ controlsView: CustomPlayerControlsView
  ) {
    pictureInPictureController?.startPictureInPicture()
    hideControls()
  }

  func controlsViewDidRequestStopPictureInPicture(
    _ controlsView: CustomPlayerControlsView
  ) {
    pictureInPictureController?.stopPictureInPicture()
    hideControls()
  }

  func controlsViewDidRequestControlsDismissal(
    _ controlsView: CustomPlayerControlsView
  ) {
    hideControls()
  }

  func controlsViewDidRequestPlayerDismissal(
    _ controlsView: CustomPlayerControlsView
  ) {
    player?.rate = 0
    dismiss(animated: true)
  }
}

// MARK: - AVPictureInPictureDelegate

extension CustomPlayerViewController: AVPictureInPictureControllerDelegate {
  public func pictureInPictureControllerWillStartPictureInPicture(
    _ pictureInPictureController: AVPictureInPictureController
  ) {
    activeCustomPlayerViewControllers.insert(self)
  }

  public func pictureInPictureControllerDidStartPictureInPicture(
    _ pictureInPictureController: AVPictureInPictureController
  ) {
    dismiss(animated: true, completion: nil)
  }

  public func pictureInPictureController(
    _ pictureInPictureController: AVPictureInPictureController,
    failedToStartPictureInPictureWithError error: Error
  ) {
    activeCustomPlayerViewControllers.remove(self)
  }

  public func pictureInPictureControllerDidStopPictureInPicture(
    _ pictureInPictureController: AVPictureInPictureController
  ) {
    activeCustomPlayerViewControllers.remove(self)
  }

  public func pictureInPictureController(
    _ pictureInPictureController: AVPictureInPictureController,
    restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
  ) {
    delegate?.playerViewController(
      self,
      restoreUserInterfaceForPictureInPictureStopWithCompletionHandler: completionHandler)
  }
}

protocol CustomPlayerViewControllerDelegate: AnyObject {
  func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(
    _ playerViewController: CustomPlayerViewController
  ) -> Bool

  func playerViewController(
    _ playerViewController: CustomPlayerViewController,
    restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
  )
}
代码语言:javascript
复制
11. CustomPlayerControlsView.swift
代码语言:javascript
复制
import AVKit
import Combine
import UIKit

protocol CustomPlayerControlsViewDelegate: AnyObject {
  func controlsViewDidRequestStartPictureInPicture(_ controlsView: CustomPlayerControlsView)

  func controlsViewDidRequestStopPictureInPicture(_ controlsView: CustomPlayerControlsView)

  func controlsViewDidRequestControlsDismissal(_ controlsView: CustomPlayerControlsView)

  func controlsViewDidRequestPlayerDismissal(_ controlsView: CustomPlayerControlsView)
}

class CustomPlayerControlsView: UIView {
  weak var delegate: CustomPlayerControlsViewDelegate?

  private weak var player: AVPlayer?
  private weak var pipController: AVPictureInPictureController?

  private lazy var spacerView = UIView()

  private lazy var buttonStackView: UIStackView = {
    let stackView = UIStackView()
    stackView.translatesAutoresizingMaskIntoConstraints = false
    stackView.alignment = .center
    stackView.axis = .horizontal
    stackView.distribution = .fill
    stackView.spacing = 8
    return stackView
  }()

  private lazy var progressView: UIProgressView = {
    let progressView = UIProgressView()
    progressView.translatesAutoresizingMaskIntoConstraints = false
    return progressView
  }()

  private var progressTimer: Timer?
  private var canStopPictureInPictureCancellable: Cancellable?

  init(player: AVPlayer?, pipController: AVPictureInPictureController?) {
    self.player = player
    self.pipController = pipController

    super.init(frame: .zero)

    setupViewLayout()

    let progressTimer = Timer(
      timeInterval: 0.5,
      repeats: true
    ) { [weak self] _ in
      guard
        let player = player,
        let item = player.currentItem
      else {
        return
      }

      let progress = CMTimeGetSeconds(player.currentTime()) / CMTimeGetSeconds(item.duration)

      self?.progressView.progress = Float(progress)
    }
    self.progressTimer = progressTimer

    RunLoop.main.add(progressTimer, forMode: .default)

    let menuGestureRecognizer = UITapGestureRecognizer(
      target: self,
      action: #selector(menuGestureHandler))

    menuGestureRecognizer.allowedPressTypes = [UIPress.PressType.menu].map {
      $0.rawValue as NSNumber
    }
    addGestureRecognizer(menuGestureRecognizer)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  deinit {
    progressTimer?.invalidate()
    progressTimer = nil
    canStopPictureInPictureCancellable = nil
  }

  private func setupViewLayout() {
    #if os(tvOS)
    let canStopPiP = pipController?.canStopPictureInPicture ?? false
    #else
    let canStopPiP = false
    #endif

    spacerView.setContentHuggingPriority(.defaultLow, for: .horizontal)
    updatePiPButtons(canStopPiP: canStopPiP)

    #if os(tvOS)
    canStopPictureInPictureCancellable = pipController?.publisher(
      for: \.canStopPictureInPicture)
      .sink { [weak self] in
        self?.updatePiPButtons(canStopPiP: $0)
      }
    #endif

    addSubview(progressView)
    NSLayoutConstraint.activate([
      progressView.heightAnchor.constraint(equalToConstant: 10),
      progressView.leadingAnchor.constraint(equalTo: leadingAnchor),
      progressView.trailingAnchor.constraint(equalTo: trailingAnchor),
      progressView.bottomAnchor.constraint(equalTo: bottomAnchor)
    ])

    addSubview(buttonStackView)
    NSLayoutConstraint.activate([
      buttonStackView.heightAnchor.constraint(equalToConstant: 90),
      buttonStackView.bottomAnchor.constraint(equalTo: progressView.topAnchor, constant: -40),
      buttonStackView.widthAnchor.constraint(equalTo: progressView.widthAnchor, multiplier: 3 / 4),
      buttonStackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -10)
    ])
  }

  private func updatePiPButtons(canStopPiP: Bool) {
    let buttonSideLength: CGFloat = 50

    var buttons: [UIButton] = []

    let startButton = CustomPlayerCircularButtonView(symbolName: "pip.enter", height: buttonSideLength)
    startButton.addTarget(self, action: #selector(startButtonPressed), for: [.primaryActionTriggered, .touchUpInside])
    buttons.append(startButton)

    if canStopPiP {
      let stopButton = CustomPlayerCircularButtonView(symbolName: "pip.exit", height: buttonSideLength)
      stopButton.addTarget(self, action: #selector(stopButtonPressed), for: [.primaryActionTriggered, .touchUpInside])
      buttons.append(stopButton)
    }

    #if os(iOS)
    let closePlayerButton = CustomPlayerCircularButtonView(symbolName: "xmark", height: buttonSideLength)
    closePlayerButton.addTarget(self, action: #selector(closePlayerPressed), for: .touchUpInside)
    buttons.append(closePlayerButton)
    #endif

    let existingButtons = buttonStackView.arrangedSubviews
    for view in existingButtons {
      view.removeFromSuperview()
    }
    buttonStackView.addArrangedSubview(spacerView)
    for button in buttons {
      buttonStackView.addArrangedSubview(button)
      button.translatesAutoresizingMaskIntoConstraints = false
      NSLayoutConstraint.activate([
        button.widthAnchor.constraint(equalToConstant: buttonSideLength),
        button.heightAnchor.constraint(equalToConstant: buttonSideLength)
      ])
    }
  }

  override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) {
    if let nextButton = context.nextFocusedView as? CustomPlayerCircularButtonView {
      nextButton.backgroundColor = .white
      nextButton.tintColor = .darkGray
    }

    if let previousButton = context.previouslyFocusedView as? CustomPlayerCircularButtonView {
      previousButton.backgroundColor = .darkGray
      previousButton.tintColor = .white
    }
  }

  @objc private func startButtonPressed() {
    delegate?.controlsViewDidRequestStartPictureInPicture(self)
  }

  @objc private func stopButtonPressed() {
    delegate?.controlsViewDidRequestStopPictureInPicture(self)
  }

  @objc private func closePlayerPressed() {
    delegate?.controlsViewDidRequestPlayerDismissal(self)
  }

  @objc private func menuGestureHandler(recognizer: UITapGestureRecognizer) {
    switch recognizer.state {
    case .ended:
      delegate?.controlsViewDidRequestControlsDismissal(self)
    default:
      break
    }
  }
}
代码语言:javascript
复制
12. CustomPlayerCircularButtonView.swift
代码语言:javascript
复制
import UIKit

class CustomPlayerCircularButtonView: UIButton {
  init(symbolName: String, height: CGFloat) {
    super.init(frame: .zero)
    backgroundColor = .white.withAlphaComponent(0.5)
    layer.cornerRadius = height / 2
    tintColor = .black
    setImage(
      UIImage(systemName: symbolName)?
        .withRenderingMode(.alwaysTemplate),
      for: .normal)
    imageView?.contentMode = .scaleAspectFit
  }
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
代码语言:javascript
复制
13. AVPlayer+Extension.swift
代码语言:javascript
复制
import AVKit

extension AVPlayer {
  var isPlaying: Bool {
    return rate != 0 && error == nil
  }
}
代码语言:javascript
复制
14. ConditionalModifiers.swift
代码语言:javascript
复制
import SwiftUI

extension View {
  @ViewBuilder
  func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
    if conditional {
      content(self)
    } else {
      self
    }
  }
}
代码语言:javascript
复制
15. CurrentPlatform.swift
代码语言:javascript
复制
import Foundation

enum Platform {
  case macOS
  case iOS
  case tvOS
  case watchOS

  #if os(macOS)
  static let current = macOS
  #elseif os(iOS)
  static let current = iOS
  #elseif os(tvOS)
  static let current = tvOS
  #elseif os(watchOS)
  static let current = watchOS
  #else
  #error("Unsupported platform")
  #endif
}

后记

本篇主要讲述了基于视频播放器的画中画实现,感兴趣的给个赞或者关注~~~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Swift
  • 后记
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档