Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix system video view overlay issues #1122

Merged
merged 7 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}