Skip to content

Commit

Permalink
Fix system video view overlay issues (#1122)
Browse files Browse the repository at this point in the history
  • Loading branch information
defagos authored Jan 23, 2025
1 parent e084021 commit 3ebfc89
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 61 deletions.
17 changes: 14 additions & 3 deletions Sources/Player/Extensions/AVPlayerViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@
import AVKit
import SwiftUI

private var kContentOverlayViewControllerKey: Void?

extension AVPlayerViewController {
private var contentOverlayViewController: UIViewController? {
get {
objc_getAssociatedObject(self, &kContentOverlayViewControllerKey) as? UIViewController
}
set {
objc_setAssociatedObject(self, &kContentOverlayViewControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

func stopPictureInPicture() {
guard allowsPictureInPicturePlayback else { return }
allowsPictureInPicturePlayback = false
Expand Down Expand Up @@ -37,15 +48,15 @@ extension AVPlayerViewController {

func setVideoOverlay<VideoOverlay>(_ videoOverlay: VideoOverlay) where VideoOverlay: View {
guard let contentOverlayView else { return }
if let hostController = children.compactMap({ $0 as? UIHostingController<VideoOverlay> }).first {
if let hostController = contentOverlayViewController as? UIHostingController<VideoOverlay> {
hostController.rootView = videoOverlay
}
else {
let hostController = UIHostingController(rootView: videoOverlay)
guard let hostView = hostController.view else { return }
addChild(hostController)
hostView.backgroundColor = .clear
contentOverlayView.addSubview(hostView)
hostController.didMove(toParent: self)
contentOverlayViewController = hostController

hostView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
Expand Down
8 changes: 4 additions & 4 deletions Sources/Player/PictureInPicture/SystemPictureInPicture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ final class SystemPictureInPicture: NSObject {

func dismantleHostViewController(_ hostViewController: PictureInPictureHostViewController) {
hostViewControllers.remove(hostViewController)
if !isActive && playerViewController == hostViewController.viewController {
if !isActive && playerViewController == hostViewController.playerViewController {
if let lastHostView = hostViewControllers.last {
playerViewController = lastHostView.viewController
playerViewController = lastHostView.playerViewController
}
else {
playerViewController = nil
Expand Down Expand Up @@ -130,8 +130,8 @@ extension SystemPictureInPicture: AVPlayerViewControllerDelegate {
}
// Wire the PiP controller to a valid source if the restored state is not bound to the player involved in
// the restoration.
else if !hostViewControllers.map(\.viewController).contains(self.playerViewController) {
self.playerViewController = hostViewControllers.last?.viewController
else if !hostViewControllers.map(\.playerViewController).contains(self.playerViewController) {
self.playerViewController = hostViewControllers.last?.playerViewController
}
}
}
8 changes: 3 additions & 5 deletions Sources/Player/UserInterface/BasicSystemVideoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,17 @@ struct BasicSystemVideoView<VideoOverlay>: UIViewControllerRepresentable where V
let contextualActions: [UIAction]
let videoOverlay: VideoOverlay

#if os(tvOS)
func makeCoordinator() -> AVPlayerViewControllerSpeedCoordinator {
func makeCoordinator() -> SystemVideoViewCoordinator {
.init()
}
#endif

func makeUIViewController(context: Context) -> AVPlayerViewController {
let controller = AVPlayerViewController()
controller.allowsPictureInPicturePlayback = false
#if os(iOS)
controller.updatesNowPlayingInfoCenter = false
#endif
context.coordinator.controller = controller
return controller
}

Expand All @@ -34,8 +33,7 @@ struct BasicSystemVideoView<VideoOverlay>: UIViewControllerRepresentable where V
uiViewController.setVideoOverlay(videoOverlay)
#if os(tvOS)
uiViewController.contextualActions = contextualActions
context.coordinator.player = player
context.coordinator.controller = uiViewController
#endif
context.coordinator.player = player
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ import AVKit
import UIKit

final class PictureInPictureHostViewController: UIViewController {
weak var viewController: AVPlayerViewController?
weak var playerViewController: AVPlayerViewController?

func addViewController(_ viewController: AVPlayerViewController) {
addChild(viewController)
view.addSubview(viewController.view)
func addViewController(_ playerViewController: AVPlayerViewController) {
addChild(playerViewController)
view.addSubview(playerViewController.view)

viewController.view.translatesAutoresizingMaskIntoConstraints = false
playerViewController.view.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
viewController.view.topAnchor.constraint(equalTo: view.topAnchor),
viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
playerViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
playerViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
playerViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
playerViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])

viewController.didMove(toParent: self)
self.viewController = viewController
playerViewController.didMove(toParent: self)
self.playerViewController = playerViewController
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,33 @@ import AVKit
import SwiftUI

// swiftlint:disable:next type_name
struct PictureInPictureSupportingSystemVideoView<VideoOvelay>: UIViewControllerRepresentable where VideoOvelay: View {
#if os(tvOS)
typealias Coordinator = AVPlayerViewControllerSpeedCoordinator
#else
typealias Coordinator = Void
#endif

struct PictureInPictureSupportingSystemVideoView<VideoOverlay>: UIViewControllerRepresentable where VideoOverlay: View {
let player: Player
let gravity: AVLayerVideoGravity
let contextualActions: [UIAction]
let videoOverlay: VideoOvelay
let videoOverlay: VideoOverlay

static func dismantleUIViewController(_ uiViewController: PictureInPictureHostViewController, coordinator: Coordinator) {
static func dismantleUIViewController(_ uiViewController: PictureInPictureHostViewController, coordinator: SystemVideoViewCoordinator) {
PictureInPicture.shared.system.dismantleHostViewController(uiViewController)
}

#if os(tvOS)
func makeCoordinator() -> Coordinator {
func makeCoordinator() -> SystemVideoViewCoordinator {
.init()
}
#endif

func makeUIViewController(context: Context) -> PictureInPictureHostViewController {
PictureInPicture.shared.system.makeHostViewController(for: player)
let controller = PictureInPicture.shared.system.makeHostViewController(for: player)
context.coordinator.controller = controller.playerViewController
return controller
}

func updateUIViewController(_ uiViewController: PictureInPictureHostViewController, context: Context) {
uiViewController.viewController?.player = player.systemPlayer
uiViewController.viewController?.videoGravity = gravity
uiViewController.viewController?.setVideoOverlay(videoOverlay)
uiViewController.playerViewController?.player = player.systemPlayer
uiViewController.playerViewController?.videoGravity = gravity
uiViewController.playerViewController?.setVideoOverlay(videoOverlay)
#if os(tvOS)
uiViewController.viewController?.contextualActions = contextualActions
context.coordinator.player = player
context.coordinator.controller = uiViewController.viewController
uiViewController.playerViewController?.contextualActions = contextualActions
#endif
context.coordinator.player = player
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,48 +8,37 @@ import AVKit
import Combine
import UIKit

@available(iOS, unavailable)
final class AVPlayerViewControllerSpeedCoordinator {
final class SystemVideoViewCoordinator {
var player: Player? {
didSet {
#if os(tvOS)
configurePlaybackSpeedPublisher(player: player, controller: controller)
#endif
}
}

var controller: AVPlayerViewController? {
didSet {
#if os(tvOS)
configurePlaybackSpeedPublisher(player: player, controller: controller)
#endif
}
}

private var cancellable: AnyCancellable?

private func configurePlaybackSpeedPublisher(player: Player?, controller: AVPlayerViewController?) {
guard let player, let controller else {
cancellable = nil
return
}
cancellable = player.playbackSpeedPublisher()
.map { speed in
guard let range = speed.range, range != 1...1 else { return [] }
return Self.speedMenuItems(for: player, range: range, speed: speed.effectiveValue)
}
.receiveOnMainThread()
.assign(to: \.transportBarCustomMenuItems, on: controller)
}
}

@available(iOS, unavailable)
private extension AVPlayerViewControllerSpeedCoordinator {
static func allowedSpeeds(from range: ClosedRange<Float>) -> Set<Float> {
private extension SystemVideoViewCoordinator {
private static func allowedSpeeds(from range: ClosedRange<Float>) -> Set<Float> {
Set(
AVPlaybackSpeed.systemDefaultSpeeds
.map(\.rate)
.filter { range.contains($0) }
)
}

static func speedMenuItems(
private static func speedMenuItems(
for player: Player,
range: ClosedRange<Float>,
speed: Float
Expand All @@ -69,4 +58,18 @@ private extension AVPlayerViewControllerSpeedCoordinator {
)
]
}

func configurePlaybackSpeedPublisher(player: Player?, controller: AVPlayerViewController?) {
guard let player, let controller else {
cancellable = nil
return
}
cancellable = player.playbackSpeedPublisher()
.map { speed in
guard let range = speed.range, range != 1...1 else { return [] }
return Self.speedMenuItems(for: player, range: range, speed: speed.effectiveValue)
}
.receiveOnMainThread()
.assign(to: \.transportBarCustomMenuItems, on: controller)
}
}

0 comments on commit 3ebfc89

Please sign in to comment.