Skip to content

Commit

Permalink
Fixes #685 subtitles menu issue
Browse files Browse the repository at this point in the history
  • Loading branch information
allenhumphreys committed Jun 8, 2023
1 parent 5a46ae9 commit a9654d5
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 81 deletions.
134 changes: 58 additions & 76 deletions PlayerUI/Views/PUIPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import Cocoa
import AVFoundation
import os.log
import AVKit
import Combine

public final class PUIPlayerView: NSView {

private let log = OSLog(subsystem: "PlayerUI", category: "PUIPlayerView")
private var cancellables: Set<AnyCancellable> = []

// MARK: - Public API

public weak var delegate: PUIPlayerViewDelegate?

public var isInPictureInPictureMode: Bool { pipController.isPictureInPictureActive }
public var isInPictureInPictureMode: Bool { pipController?.isPictureInPictureActive == true }

public weak var appearanceDelegate: PUIPlayerViewAppearanceDelegate? {
didSet {
Expand Down Expand Up @@ -55,9 +57,9 @@ public final class PUIPlayerView: NSView {
teardown(player: oldPlayer)
}

guard player != nil else { return }
guard let player else { return }

setupPlayer()
setupPlayer(player)
}
}

Expand All @@ -72,22 +74,19 @@ public final class PUIPlayerView: NSView {

public init(player: AVPlayer) {
self.player = player
self.pipController = AVPictureInPictureController(contentSource: .init(playerLayer: playerLayer))
if AVPictureInPictureController.isPictureInPictureSupported() {
self.pipController = AVPictureInPictureController(contentSource: .init(playerLayer: playerLayer))
} else {
self.pipController = nil
}

super.init(frame: .zero)

pipPossibleObservation = pipController.observe(
\AVPictureInPictureController.isPictureInPicturePossible, options: [.initial, .new]
) { [weak self] _, change in
self?.pipButton.isEnabled = change.newValue ?? false
}

pipController.delegate = self
wantsLayer = true
layer = PUIBoringLayer()
layer?.backgroundColor = NSColor.black.cgColor

setupPlayer()
setupPlayer(player)
setupControls()
}

Expand Down Expand Up @@ -208,30 +207,52 @@ public final class PUIPlayerView: NSView {

private let playerLayer = PUIBoringPlayerLayer()

private func setupPlayer() {
private func setupPlayer(_ player: AVPlayer) {
if let pipController {
pipPossibleObservation = pipController.observe(
\AVPictureInPictureController.isPictureInPicturePossible, options: [.initial, .new]
) { [weak self] _, change in
self?.pipButton.isEnabled = change.newValue ?? false
}
pipController.delegate = self
} else {
pipButton.isEnabled = false
}

elapsedTimeLabel.stringValue = elapsedTimeInitialValue
remainingTimeLabel.stringValue = remainingTimeInitialValue
timelineView.resetUI()

guard let player = player else { return }

playerLayer.player = player
playerLayer.videoGravity = .resizeAspect

player.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.initial, .new], context: nil)
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.volume), options: [.initial, .new], context: nil)
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: [.initial, .new], context: nil)
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: [.initial, .new], context: nil)
player.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.loadedTimeRanges), options: [.initial, .new], context: nil)

asset?.loadValuesAsynchronously(forKeys: ["duration"], completionHandler: durationBecameAvailable)

asset?.loadValuesAsynchronously(forKeys: ["availableMediaCharacteristicsWithMediaSelectionOptions"], completionHandler: { [weak self] in

if self?.asset?.statusOfValue(forKey: "availableMediaCharacteristicsWithMediaSelectionOptions", error: nil) == .loaded {
DispatchQueue.main.async { self?.updateSubtitleSelectionMenu() }
let options: NSKeyValueObservingOptions = [.initial, .new]
player.publisher(for: \.status, options: options).sink { [weak self] change in
self?.playerStatusChanged()
}.store(in: &cancellables)
player.publisher(for: \.volume, options: options).sink { [weak self] change in
self?.playerVolumeChanged()
}.store(in: &cancellables)
player.publisher(for: \.rate, options: options).sink { [weak self] change in
self?.updatePlayingState()
self?.updatePowerAssertion()
}.store(in: &cancellables)
player.publisher(for: \.currentItem, options: options).sink { [weak self] change in
if let playerItem = self?.player?.currentItem {
playerItem.audioTimePitchAlgorithm = .timeDomain
}
})
}.store(in: &cancellables)
player.publisher(for: \.currentItem?.loadedTimeRanges, options: [.initial, .new]).sink { [weak self] change in
self?.updateBufferedSegments()
}.store(in: &cancellables)

Task { [weak self] in
guard let asset = self?.asset else { return }
async let duration = asset.load(.duration)
async let legible = asset.loadMediaSelectionGroup(for: .legible)
self?.timelineView.mediaDuration = Double(CMTimeGetSeconds(try await duration))
self?.updateSubtitleSelectionMenu(subtitlesGroup: try await legible)
}

playerTimeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(0.5, preferredTimescale: 9000), queue: .main) { [weak self] currentTime in
self?.playerTimeDidChange(time: currentTime)
Expand All @@ -245,40 +266,11 @@ public final class PUIPlayerView: NSView {
oldValue.pause()
oldValue.cancelPendingPrerolls()

cancellables.removeAll()
if let observer = playerTimeObserver {
oldValue.removeTimeObserver(observer)
playerTimeObserver = nil
}

oldValue.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status))
oldValue.removeObserver(self, forKeyPath: #keyPath(AVPlayer.rate))
oldValue.removeObserver(self, forKeyPath: #keyPath(AVPlayer.volume))
oldValue.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem.loadedTimeRanges))
oldValue.removeObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem))
}

public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
DispatchQueue.main.async {
guard let keyPath = keyPath else { return }

switch keyPath {
case #keyPath(AVPlayer.status):
self.playerStatusChanged()
case #keyPath(AVPlayer.currentItem.loadedTimeRanges):
self.updateBufferedSegments()
case #keyPath(AVPlayer.volume):
self.playerVolumeChanged()
case #keyPath(AVPlayer.rate):
self.updatePlayingState()
self.updatePowerAssertion()
case #keyPath(AVPlayer.currentItem):
if let playerItem = self.player?.currentItem {
playerItem.audioTimePitchAlgorithm = .timeDomain
}
default:
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}

private func playerVolumeChanged() {
Expand Down Expand Up @@ -363,14 +355,6 @@ public final class PUIPlayerView: NSView {
}
}

private func durationBecameAvailable() {
guard let duration = asset?.duration else { return }

DispatchQueue.main.async {
self.timelineView.mediaDuration = Double(CMTimeGetSeconds(duration))
}
}

fileprivate func playerTimeDidChange(time: CMTime) {
guard let player = player else { return }
guard player.hasValidMediaDuration else { return }
Expand Down Expand Up @@ -1010,9 +994,9 @@ public final class PUIPlayerView: NSView {

@IBAction public func togglePip(_ sender: NSView?) {
if isInPictureInPictureMode {
pipController.stopPictureInPicture()
pipController?.stopPictureInPicture()
} else {
pipController.startPictureInPicture()
pipController?.startPictureInPicture()
}
}

Expand Down Expand Up @@ -1054,10 +1038,9 @@ public final class PUIPlayerView: NSView {
private var subtitlesMenu: NSMenu?
private var subtitlesGroup: AVMediaSelectionGroup?

private func updateSubtitleSelectionMenu() {
guard let playerItem = player?.currentItem else { return }

guard let subtitlesGroup = playerItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) else {
@MainActor
private func updateSubtitleSelectionMenu(subtitlesGroup: AVMediaSelectionGroup?) {
guard let subtitlesGroup else {
subtitlesButton.isHidden = true
return
}
Expand All @@ -1084,16 +1067,15 @@ public final class PUIPlayerView: NSView {
guard let option = sender.representedObject as? AVMediaSelectionOption else { return }

// reset all item's states
sender.menu?.items.forEach({ $0.state = .on })
sender.menu?.items.forEach({ $0.state = .off })

// The current language was clicked again, turn subtitles off
if option.extendedLanguageTag == player?.currentItem?.currentMediaSelection.selectedMediaOption(in: subtitlesGroup)?.extendedLanguageTag {
player?.currentItem?.select(nil, in: subtitlesGroup)
sender.state = .off
return
}

player?.currentItem?.select(option, in: subtitlesGroup)

sender.state = .on
}

Expand Down Expand Up @@ -1236,7 +1218,7 @@ public final class PUIPlayerView: NSView {
}
}

fileprivate let pipController: AVPictureInPictureController
private let pipController: AVPictureInPictureController?
private var pipPossibleObservation: Any?

// MARK: - Visibility management
Expand Down
8 changes: 3 additions & 5 deletions PlayerUI/Views/PUITimelineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,9 @@ public final class PUITimelineView: NSView {
}
}

public var mediaDuration: Double = 0 {
didSet {
needsLayout = true
}
}
@MainActor
@Invalidating(.layout)
public var mediaDuration: Double = 0

public var hasValidMediaDuration: Bool {
return AVPlayer.validateMediaDurationWithSeconds(mediaDuration)
Expand Down

0 comments on commit a9654d5

Please sign in to comment.