Initial commit: SimVision tvOS streaming app
Features: - VOD library with movie grouping and version detection - TV show library with season/episode organization - TMDB integration for trending shows and recently aired episodes - Recent releases section with TMDB release date sorting - Watch history tracking with continue watching - Playlist caching (12-hour TTL) for offline support - M3U playlist parsing with XStream API support - Authentication with credential storage Technical: - SwiftUI for tvOS - Actor-based services for thread safety - Persistent caching for playlists, TMDB data, and watch history - KSPlayer integration for video playback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
225
KSPlayer-main/Sources/KSPlayer/Video/BrightnessVolume.swift
Normal file
225
KSPlayer-main/Sources/KSPlayer/Video/BrightnessVolume.swift
Normal file
@@ -0,0 +1,225 @@
|
||||
//
|
||||
// BrightnessVolume.swift
|
||||
// KSPlayer
|
||||
//
|
||||
// Created by kintan on 2017/11/3.
|
||||
//
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
open class BrightnessVolume {
|
||||
private var brightnessObservation: NSKeyValueObservation?
|
||||
public static let shared = BrightnessVolume()
|
||||
public var progressView: BrightnessVolumeViewProtocol & UIView = ProgressView()
|
||||
init() {
|
||||
#if !os(tvOS) && !os(xrOS)
|
||||
brightnessObservation = UIScreen.main.observe(\.brightness, options: .new) { [weak self] _, change in
|
||||
guard KSOptions.enableBrightnessGestures else { return }
|
||||
if let self, let value = change.newValue {
|
||||
self.appearView()
|
||||
self.progressView.setProgress(Float(value), type: 0)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
let name = NSNotification.Name(rawValue: "AVSystemController_SystemVolumeDidChangeNotification")
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(volumeIsChanged(notification:)), name: name, object: nil)
|
||||
progressView.alpha = 0.0
|
||||
}
|
||||
|
||||
public func move(to view: UIView) {
|
||||
progressView.move(to: view)
|
||||
}
|
||||
|
||||
@objc private func volumeIsChanged(notification: NSNotification) {
|
||||
guard KSOptions.enableVolumeGestures else { return }
|
||||
if let changeReason = notification.userInfo?["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String, changeReason == "ExplicitVolumeChange" {
|
||||
if let volume = notification.userInfo?["AVSystemController_AudioVolumeNotificationParameter"] as? CGFloat {
|
||||
appearView()
|
||||
progressView.setProgress(Float(volume), type: 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func appearView() {
|
||||
if progressView.alpha == 0.0 {
|
||||
progressView.alpha = 1.0
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) { [weak self] () in
|
||||
self?.disAppearView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func disAppearView() {
|
||||
if progressView.alpha == 1.0 {
|
||||
UIView.animate(withDuration: 0.8) { [weak self] () in
|
||||
self?.progressView.alpha = 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
brightnessObservation?.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
public protocol BrightnessVolumeViewProtocol {
|
||||
// type: 0 brightness type: 1 volume
|
||||
func setProgress(_ progress: Float, type: UInt)
|
||||
func move(to view: UIView)
|
||||
}
|
||||
|
||||
private final class SystemView: UIVisualEffectView {
|
||||
private let stackView = UIStackView()
|
||||
private let imageView = UIImageView()
|
||||
private let titleLabel = UILabel()
|
||||
private lazy var brightnessImage = UIImage(systemName: "sun.max")
|
||||
private lazy var volumeImage = UIImage(systemName: "speaker.wave.3.fill")
|
||||
private convenience init() {
|
||||
self.init(effect: UIBlurEffect(style: .extraLight))
|
||||
clipsToBounds = true
|
||||
cornerRadius = 10
|
||||
imageView.image = brightnessImage
|
||||
contentView.addSubview(imageView)
|
||||
titleLabel.font = .systemFont(ofSize: 16)
|
||||
titleLabel.textColor = UIColor(red: 0.25, green: 0.22, blue: 0.21, alpha: 1)
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.text = "亮度"
|
||||
contentView.addSubview(titleLabel)
|
||||
let longView = UIView()
|
||||
longView.backgroundColor = titleLabel.textColor
|
||||
contentView.addSubview(longView)
|
||||
stackView.alignment = .center
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.axis = .horizontal
|
||||
stackView.spacing = 1
|
||||
longView.addSubview(stackView)
|
||||
for _ in 0 ..< 16 {
|
||||
let tipView = UIView()
|
||||
tipView.backgroundColor = .white
|
||||
stackView.addArrangedSubview(tipView)
|
||||
tipView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
tipView.heightAnchor.constraint(equalTo: stackView.heightAnchor),
|
||||
])
|
||||
}
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
longView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.widthAnchor.constraint(equalToConstant: 79),
|
||||
imageView.heightAnchor.constraint(equalToConstant: 76),
|
||||
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 5),
|
||||
titleLabel.widthAnchor.constraint(equalTo: widthAnchor),
|
||||
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
titleLabel.heightAnchor.constraint(equalToConstant: 30),
|
||||
longView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 13),
|
||||
longView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -13),
|
||||
longView.heightAnchor.constraint(equalToConstant: 7),
|
||||
longView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16),
|
||||
stackView.leadingAnchor.constraint(equalTo: longView.leadingAnchor, constant: 1),
|
||||
stackView.trailingAnchor.constraint(equalTo: longView.trailingAnchor, constant: -1),
|
||||
stackView.topAnchor.constraint(equalTo: longView.topAnchor, constant: 1),
|
||||
stackView.bottomAnchor.constraint(equalTo: longView.bottomAnchor, constant: -1),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
extension SystemView: BrightnessVolumeViewProtocol {
|
||||
public func setProgress(_ progress: Float, type: UInt) {
|
||||
if type == 0 {
|
||||
imageView.image = brightnessImage
|
||||
titleLabel.text = NSLocalizedString("brightness", comment: "")
|
||||
} else {
|
||||
imageView.image = volumeImage
|
||||
titleLabel.text = NSLocalizedString("volume", comment: "")
|
||||
}
|
||||
let level = Int(progress * Float(stackView.arrangedSubviews.count))
|
||||
for i in 0 ..< stackView.arrangedSubviews.count {
|
||||
let view = stackView.arrangedSubviews[i]
|
||||
if i <= level, level > 0 {
|
||||
view.alpha = 1
|
||||
} else {
|
||||
view.alpha = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func move(to view: UIView) {
|
||||
if superview != view {
|
||||
removeFromSuperview()
|
||||
view.addSubview(self)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
heightAnchor.constraint(equalToConstant: 155),
|
||||
widthAnchor.constraint(equalToConstant: 155),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ProgressView: UIView {
|
||||
private lazy var brightnessImage = UIImage(systemName: "sun.max")
|
||||
private lazy var volumeImage = UIImage(systemName: "speaker.fill")
|
||||
private lazy var brightnessOffImage = UIImage(systemName: "sun.min")
|
||||
private lazy var volumeOffImage = UIImage(systemName: "speaker.slash.fill")
|
||||
private let progressView = UIProgressView()
|
||||
private let imageView = UIImageView()
|
||||
|
||||
override init(frame _: CGRect) {
|
||||
super.init(frame: .zero)
|
||||
addSubview(progressView)
|
||||
addSubview(imageView)
|
||||
progressView.progressTintColor = UIColor.white
|
||||
progressView.trackTintColor = UIColor.white.withAlphaComponent(0.5)
|
||||
progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
progressView.centerRotate(byDegrees: -90)
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
progressView.widthAnchor.constraint(equalToConstant: 115),
|
||||
progressView.heightAnchor.constraint(equalToConstant: 2),
|
||||
progressView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
progressView.topAnchor.constraint(equalTo: topAnchor, constant: 57),
|
||||
imageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
imageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension ProgressView: BrightnessVolumeViewProtocol {
|
||||
func setProgress(_ progress: Float, type: UInt) {
|
||||
progressView.setProgress(progress, animated: false)
|
||||
if progress == 0 {
|
||||
imageView.image = type == 0 ? brightnessOffImage : volumeOffImage
|
||||
} else {
|
||||
imageView.image = type == 0 ? brightnessImage : volumeImage
|
||||
}
|
||||
}
|
||||
|
||||
func move(to view: UIView) {
|
||||
if superview != view {
|
||||
removeFromSuperview()
|
||||
view.addSubview(self)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
trailingAnchor.constraint(equalTo: view.safeTrailingAnchor, constant: -10),
|
||||
centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
heightAnchor.constraint(equalToConstant: 150),
|
||||
widthAnchor.constraint(equalToConstant: 24),
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
474
KSPlayer-main/Sources/KSPlayer/Video/IOSVideoPlayerView.swift
Normal file
474
KSPlayer-main/Sources/KSPlayer/Video/IOSVideoPlayerView.swift
Normal file
@@ -0,0 +1,474 @@
|
||||
//
|
||||
// IOSVideoPlayerView.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by kintan on 2018/10/31.
|
||||
//
|
||||
#if canImport(UIKit) && canImport(CallKit)
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreServices
|
||||
import MediaPlayer
|
||||
import UIKit
|
||||
|
||||
open class IOSVideoPlayerView: VideoPlayerView {
|
||||
private weak var originalSuperView: UIView?
|
||||
private var originalframeConstraints: [NSLayoutConstraint]?
|
||||
private var originalFrame = CGRect.zero
|
||||
private var originalOrientations: UIInterfaceOrientationMask?
|
||||
private weak var fullScreenDelegate: PlayerViewFullScreenDelegate?
|
||||
private var isVolume = false
|
||||
private let volumeView = BrightnessVolume()
|
||||
public var volumeViewSlider = UXSlider()
|
||||
public var backButton = UIButton()
|
||||
public var airplayStatusView: UIView = AirplayStatusView()
|
||||
#if !os(xrOS)
|
||||
public var routeButton = AVRoutePickerView()
|
||||
#endif
|
||||
private let routeDetector = AVRouteDetector()
|
||||
/// Image view to show video cover
|
||||
public var maskImageView = UIImageView()
|
||||
public var landscapeButton: UIControl = UIButton()
|
||||
override open var isMaskShow: Bool {
|
||||
didSet {
|
||||
fullScreenDelegate?.player(isMaskShow: isMaskShow, isFullScreen: landscapeButton.isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(xrOS)
|
||||
private var brightness: CGFloat = UIScreen.main.brightness {
|
||||
didSet {
|
||||
UIScreen.main.brightness = brightness
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
override open func customizeUIComponents() {
|
||||
super.customizeUIComponents()
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
subtitleLabel.font = .systemFont(ofSize: 14)
|
||||
}
|
||||
insertSubview(maskImageView, at: 0)
|
||||
maskImageView.contentMode = .scaleAspectFit
|
||||
toolBar.addArrangedSubview(landscapeButton)
|
||||
landscapeButton.tag = PlayerButtonType.landscape.rawValue
|
||||
landscapeButton.addTarget(self, action: #selector(onButtonPressed(_:)), for: .touchUpInside)
|
||||
landscapeButton.tintColor = .white
|
||||
if let landscapeButton = landscapeButton as? UIButton {
|
||||
landscapeButton.setImage(UIImage(systemName: "arrow.up.left.and.arrow.down.right"), for: .normal)
|
||||
landscapeButton.setImage(UIImage(systemName: "arrow.down.right.and.arrow.up.left"), for: .selected)
|
||||
}
|
||||
backButton.tag = PlayerButtonType.back.rawValue
|
||||
backButton.setImage(UIImage(systemName: "chevron.left"), for: .normal)
|
||||
backButton.addTarget(self, action: #selector(onButtonPressed(_:)), for: .touchUpInside)
|
||||
backButton.tintColor = .white
|
||||
navigationBar.insertArrangedSubview(backButton, at: 0)
|
||||
|
||||
addSubview(airplayStatusView)
|
||||
volumeView.move(to: self)
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
let tmp = MPVolumeView(frame: CGRect(x: -100, y: -100, width: 0, height: 0))
|
||||
if let first = (tmp.subviews.first { $0 is UISlider }) as? UISlider {
|
||||
volumeViewSlider = first
|
||||
}
|
||||
#endif
|
||||
backButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
landscapeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
maskImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
maskImageView.topAnchor.constraint(equalTo: topAnchor),
|
||||
maskImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
maskImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
maskImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
backButton.widthAnchor.constraint(equalToConstant: 25),
|
||||
landscapeButton.widthAnchor.constraint(equalToConstant: 30),
|
||||
airplayStatusView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
airplayStatusView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
#if !os(xrOS)
|
||||
routeButton.isHidden = true
|
||||
navigationBar.addArrangedSubview(routeButton)
|
||||
routeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
routeButton.widthAnchor.constraint(equalToConstant: 25),
|
||||
])
|
||||
#endif
|
||||
addNotification()
|
||||
}
|
||||
|
||||
override open func resetPlayer() {
|
||||
super.resetPlayer()
|
||||
maskImageView.alpha = 1
|
||||
maskImageView.image = nil
|
||||
panGesture.isEnabled = false
|
||||
#if !os(xrOS)
|
||||
routeButton.isHidden = !routeDetector.multipleRoutesDetected
|
||||
#endif
|
||||
}
|
||||
|
||||
override open func onButtonPressed(type: PlayerButtonType, button: UIButton) {
|
||||
if type == .back, viewController is PlayerFullScreenViewController {
|
||||
updateUI(isFullScreen: false)
|
||||
return
|
||||
}
|
||||
super.onButtonPressed(type: type, button: button)
|
||||
if type == .lock {
|
||||
button.isSelected.toggle()
|
||||
isMaskShow = !button.isSelected
|
||||
button.alpha = 1.0
|
||||
} else if type == .landscape {
|
||||
updateUI(isFullScreen: !landscapeButton.isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
open func isHorizonal() -> Bool {
|
||||
playerLayer?.player.naturalSize.isHorizonal ?? true
|
||||
}
|
||||
|
||||
open func updateUI(isFullScreen: Bool) {
|
||||
guard let viewController else {
|
||||
return
|
||||
}
|
||||
landscapeButton.isSelected = isFullScreen
|
||||
let isHorizonal = isHorizonal()
|
||||
viewController.navigationController?.interactivePopGestureRecognizer?.isEnabled = !isFullScreen
|
||||
if isFullScreen {
|
||||
if viewController is PlayerFullScreenViewController {
|
||||
return
|
||||
}
|
||||
originalSuperView = superview
|
||||
originalframeConstraints = frameConstraints
|
||||
if let originalframeConstraints {
|
||||
NSLayoutConstraint.deactivate(originalframeConstraints)
|
||||
}
|
||||
originalFrame = frame
|
||||
originalOrientations = viewController.supportedInterfaceOrientations
|
||||
let fullVC = PlayerFullScreenViewController(isHorizonal: isHorizonal)
|
||||
fullScreenDelegate = fullVC
|
||||
fullVC.view.addSubview(self)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
topAnchor.constraint(equalTo: fullVC.view.readableTopAnchor),
|
||||
leadingAnchor.constraint(equalTo: fullVC.view.leadingAnchor),
|
||||
trailingAnchor.constraint(equalTo: fullVC.view.trailingAnchor),
|
||||
bottomAnchor.constraint(equalTo: fullVC.view.bottomAnchor),
|
||||
])
|
||||
fullVC.modalPresentationStyle = .fullScreen
|
||||
fullVC.modalPresentationCapturesStatusBarAppearance = true
|
||||
fullVC.transitioningDelegate = self
|
||||
viewController.present(fullVC, animated: true) {
|
||||
KSOptions.supportedInterfaceOrientations = fullVC.supportedInterfaceOrientations
|
||||
}
|
||||
} else {
|
||||
guard viewController is PlayerFullScreenViewController else {
|
||||
return
|
||||
}
|
||||
let presentingVC = viewController.presentingViewController ?? viewController
|
||||
if let originalOrientations {
|
||||
KSOptions.supportedInterfaceOrientations = originalOrientations
|
||||
}
|
||||
presentingVC.dismiss(animated: true) {
|
||||
self.originalSuperView?.addSubview(self)
|
||||
if let constraints = self.originalframeConstraints, !constraints.isEmpty {
|
||||
NSLayoutConstraint.activate(constraints)
|
||||
} else {
|
||||
self.translatesAutoresizingMaskIntoConstraints = true
|
||||
self.frame = self.originalFrame
|
||||
}
|
||||
}
|
||||
}
|
||||
let isLandscape = isFullScreen && isHorizonal
|
||||
updateUI(isLandscape: isLandscape)
|
||||
}
|
||||
|
||||
open func updateUI(isLandscape: Bool) {
|
||||
if isLandscape {
|
||||
topMaskView.isHidden = KSOptions.topBarShowInCase == .none
|
||||
} else {
|
||||
topMaskView.isHidden = KSOptions.topBarShowInCase != .always
|
||||
}
|
||||
toolBar.playbackRateButton.isHidden = false
|
||||
toolBar.srtButton.isHidden = srtControl.subtitleInfos.isEmpty
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
if isLandscape {
|
||||
landscapeButton.isHidden = true
|
||||
toolBar.srtButton.isHidden = srtControl.subtitleInfos.isEmpty
|
||||
} else {
|
||||
toolBar.srtButton.isHidden = true
|
||||
if let image = maskImageView.image {
|
||||
landscapeButton.isHidden = image.size.width < image.size.height
|
||||
} else {
|
||||
landscapeButton.isHidden = false
|
||||
}
|
||||
}
|
||||
toolBar.playbackRateButton.isHidden = !isLandscape
|
||||
} else {
|
||||
landscapeButton.isHidden = true
|
||||
}
|
||||
lockButton.isHidden = !isLandscape
|
||||
judgePanGesture()
|
||||
}
|
||||
|
||||
override open func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||
super.player(layer: layer, state: state)
|
||||
if state == .readyToPlay {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.maskImageView.alpha = 0.0
|
||||
}
|
||||
}
|
||||
judgePanGesture()
|
||||
}
|
||||
|
||||
override open func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
||||
airplayStatusView.isHidden = !layer.player.isExternalPlaybackActive
|
||||
super.player(layer: layer, currentTime: currentTime, totalTime: totalTime)
|
||||
}
|
||||
|
||||
override open func set(resource: KSPlayerResource, definitionIndex: Int = 0, isSetUrl: Bool = true) {
|
||||
super.set(resource: resource, definitionIndex: definitionIndex, isSetUrl: isSetUrl)
|
||||
maskImageView.image(url: resource.cover)
|
||||
}
|
||||
|
||||
override open func change(definitionIndex: Int) {
|
||||
Task {
|
||||
let image = await playerLayer?.player.thumbnailImageAtCurrentTime()
|
||||
if let image {
|
||||
self.maskImageView.image = UIImage(cgImage: image)
|
||||
self.maskImageView.alpha = 1
|
||||
}
|
||||
super.change(definitionIndex: definitionIndex)
|
||||
}
|
||||
}
|
||||
|
||||
override open func panGestureBegan(location point: CGPoint, direction: KSPanDirection) {
|
||||
if direction == .vertical {
|
||||
if point.x > bounds.size.width / 2 {
|
||||
isVolume = true
|
||||
tmpPanValue = volumeViewSlider.value
|
||||
} else {
|
||||
isVolume = false
|
||||
}
|
||||
} else {
|
||||
super.panGestureBegan(location: point, direction: direction)
|
||||
}
|
||||
}
|
||||
|
||||
override open func panGestureChanged(velocity point: CGPoint, direction: KSPanDirection) {
|
||||
if direction == .vertical {
|
||||
if isVolume {
|
||||
if KSOptions.enableVolumeGestures {
|
||||
tmpPanValue += panValue(velocity: point, direction: direction, currentTime: Float(toolBar.currentTime), totalTime: Float(totalTime))
|
||||
tmpPanValue = max(min(tmpPanValue, 1), 0)
|
||||
volumeViewSlider.value = tmpPanValue
|
||||
}
|
||||
} else if KSOptions.enableBrightnessGestures {
|
||||
#if !os(xrOS)
|
||||
brightness += CGFloat(panValue(velocity: point, direction: direction, currentTime: Float(toolBar.currentTime), totalTime: Float(totalTime)))
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
super.panGestureChanged(velocity: point, direction: direction)
|
||||
}
|
||||
}
|
||||
|
||||
open func judgePanGesture() {
|
||||
if landscapeButton.isSelected || UIDevice.current.userInterfaceIdiom == .pad {
|
||||
panGesture.isEnabled = isPlayed && !replayButton.isSelected
|
||||
} else {
|
||||
panGesture.isEnabled = toolBar.playButton.isSelected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSVideoPlayerView: UIViewControllerTransitioningDelegate {
|
||||
public func animationController(forPresented _: UIViewController, presenting _: UIViewController, source _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
if let originalSuperView, let animationView = playerLayer?.player.view {
|
||||
return PlayerTransitionAnimator(containerView: originalSuperView, animationView: animationView)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func animationController(forDismissed _: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
if let originalSuperView, let animationView = playerLayer?.player.view {
|
||||
return PlayerTransitionAnimator(containerView: originalSuperView, animationView: animationView, isDismiss: true)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - private functions
|
||||
|
||||
extension IOSVideoPlayerView {
|
||||
private func addNotification() {
|
||||
// NotificationCenter.default.addObserver(self, selector: #selector(orientationChanged), name: UIApplication.didChangeStatusBarOrientationNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(routesAvailableDidChange), name: .AVRouteDetectorMultipleRoutesDetectedDidChange, object: nil)
|
||||
}
|
||||
|
||||
@objc private func routesAvailableDidChange(notification _: Notification) {
|
||||
#if !os(xrOS)
|
||||
routeButton.isHidden = !routeDetector.multipleRoutesDetected
|
||||
#endif
|
||||
}
|
||||
|
||||
@objc private func orientationChanged(notification _: Notification) {
|
||||
guard isHorizonal() else {
|
||||
return
|
||||
}
|
||||
updateUI(isFullScreen: UIApplication.isLandscape)
|
||||
}
|
||||
}
|
||||
|
||||
public class AirplayStatusView: UIView {
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
let airplayicon = UIImageView(image: UIImage(systemName: "airplayvideo"))
|
||||
addSubview(airplayicon)
|
||||
let airplaymessage = UILabel()
|
||||
airplaymessage.backgroundColor = .clear
|
||||
airplaymessage.textColor = .white
|
||||
airplaymessage.font = .systemFont(ofSize: 14)
|
||||
airplaymessage.text = NSLocalizedString("AirPlay 投放中", comment: "")
|
||||
airplaymessage.textAlignment = .center
|
||||
addSubview(airplaymessage)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
airplayicon.translatesAutoresizingMaskIntoConstraints = false
|
||||
airplaymessage.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
widthAnchor.constraint(equalToConstant: 100),
|
||||
heightAnchor.constraint(equalToConstant: 115),
|
||||
airplayicon.topAnchor.constraint(equalTo: topAnchor),
|
||||
airplayicon.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
airplayicon.widthAnchor.constraint(equalToConstant: 100),
|
||||
airplayicon.heightAnchor.constraint(equalToConstant: 100),
|
||||
airplaymessage.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
airplaymessage.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
airplaymessage.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
airplaymessage.heightAnchor.constraint(equalToConstant: 15),
|
||||
])
|
||||
isHidden = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
public extension KSOptions {
|
||||
/// func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
|
||||
static var supportedInterfaceOrientations = UIInterfaceOrientationMask.portrait
|
||||
}
|
||||
|
||||
extension UIApplication {
|
||||
static var isLandscape: Bool {
|
||||
UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - menu
|
||||
|
||||
extension IOSVideoPlayerView {
|
||||
override open var canBecomeFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override open func canPerformAction(_ action: Selector, withSender _: Any?) -> Bool {
|
||||
if action == #selector(IOSVideoPlayerView.openFileAction) {
|
||||
return true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc fileprivate func openFileAction(_: AnyObject) {
|
||||
let documentPicker = UIDocumentPickerViewController(documentTypes: [kUTTypeAudio, kUTTypeMovie, kUTTypePlainText] as [String], in: .open)
|
||||
documentPicker.delegate = self
|
||||
viewController?.present(documentPicker, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension IOSVideoPlayerView: UIDocumentPickerDelegate {
|
||||
public func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
if let url = urls.first {
|
||||
if url.isMovie || url.isAudio {
|
||||
set(url: url, options: KSOptions())
|
||||
} else {
|
||||
srtControl.selectedSubtitleInfo = URLSubtitleInfo(url: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@MainActor
|
||||
public class MenuController {
|
||||
public init(with builder: UIMenuBuilder) {
|
||||
builder.remove(menu: .format)
|
||||
builder.insertChild(MenuController.openFileMenu(), atStartOfMenu: .file)
|
||||
// builder.insertChild(MenuController.openURLMenu(), atStartOfMenu: .file)
|
||||
// builder.insertChild(MenuController.navigationMenu(), atStartOfMenu: .file)
|
||||
}
|
||||
|
||||
class func openFileMenu() -> UIMenu {
|
||||
let openCommand = UIKeyCommand(input: "O", modifierFlags: .command, action: #selector(IOSVideoPlayerView.openFileAction(_:)))
|
||||
openCommand.title = NSLocalizedString("Open File", comment: "")
|
||||
let openMenu = UIMenu(title: "",
|
||||
image: nil,
|
||||
identifier: UIMenu.Identifier("com.example.apple-samplecode.menus.openFileMenu"),
|
||||
options: .displayInline,
|
||||
children: [openCommand])
|
||||
return openMenu
|
||||
}
|
||||
|
||||
// class func openURLMenu() -> UIMenu {
|
||||
// let openCommand = UIKeyCommand(input: "O", modifierFlags: [.command, .shift], action: #selector(IOSVideoPlayerView.openURLAction(_:)))
|
||||
// openCommand.title = NSLocalizedString("Open URL", comment: "")
|
||||
// let openMenu = UIMenu(title: "",
|
||||
// image: nil,
|
||||
// identifier: UIMenu.Identifier("com.example.apple-samplecode.menus.openURLMenu"),
|
||||
// options: .displayInline,
|
||||
// children: [openCommand])
|
||||
// return openMenu
|
||||
// }
|
||||
// class func navigationMenu() -> UIMenu {
|
||||
// let arrowKeyChildrenCommands = Arrows.allCases.map { arrow in
|
||||
// UIKeyCommand(title: arrow.localizedString(),
|
||||
// image: nil,
|
||||
// action: #selector(IOSVideoPlayerView.navigationMenuAction(_:)),
|
||||
// input: arrow.command,
|
||||
// modifierFlags: .command)
|
||||
// }
|
||||
// return UIMenu(title: NSLocalizedString("NavigationTitle", comment: ""),
|
||||
// image: nil,
|
||||
// identifier: UIMenu.Identifier("com.example.apple-samplecode.menus.navigationMenu"),
|
||||
// options: [],
|
||||
// children: arrowKeyChildrenCommands)
|
||||
// }
|
||||
|
||||
enum Arrows: String, CaseIterable {
|
||||
case rightArrow
|
||||
case leftArrow
|
||||
case upArrow
|
||||
case downArrow
|
||||
func localizedString() -> String {
|
||||
NSLocalizedString("\(rawValue)", comment: "")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var command: String {
|
||||
switch self {
|
||||
case .rightArrow:
|
||||
return UIKeyCommand.inputRightArrow
|
||||
case .leftArrow:
|
||||
return UIKeyCommand.inputLeftArrow
|
||||
case .upArrow:
|
||||
return UIKeyCommand.inputUpArrow
|
||||
case .downArrow:
|
||||
return UIKeyCommand.inputDownArrow
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
99
KSPlayer-main/Sources/KSPlayer/Video/KSMenu.swift
Normal file
99
KSPlayer-main/Sources/KSPlayer/Video/KSMenu.swift
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// KSMenu.swift
|
||||
// KSPlayer
|
||||
//
|
||||
// Created by Alanko5 on 15/12/2022.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
extension UIMenu {
|
||||
func updateActionState(actionTitle: String? = nil) -> UIMenu {
|
||||
for action in children {
|
||||
guard let action = action as? UIAction else {
|
||||
continue
|
||||
}
|
||||
action.state = action.title == actionTitle ? .on : .off
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
@available(tvOS 15.0, *)
|
||||
convenience init?<U>(title: String, current: U?, list: [U], addDisabled: Bool = false, titleFunc: (U) -> String, completition: @escaping (String, U?) -> Void) {
|
||||
if list.count < (addDisabled ? 1 : 2) {
|
||||
return nil
|
||||
}
|
||||
var actions = list.map { value in
|
||||
let item = UIAction(title: titleFunc(value)) { item in
|
||||
completition(item.title, value)
|
||||
}
|
||||
|
||||
if let current, titleFunc(value) == titleFunc(current) {
|
||||
item.state = .on
|
||||
}
|
||||
return item
|
||||
}
|
||||
if addDisabled {
|
||||
actions.insert(UIAction(title: "Disabled") { item in
|
||||
completition(item.title, nil)
|
||||
}, at: 0)
|
||||
}
|
||||
|
||||
self.init(title: title, children: actions)
|
||||
}
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
extension UIButton {
|
||||
@available(iOS 14.0, *)
|
||||
func setMenu<U>(title: String, current: U?, list: [U], addDisabled: Bool = false, titleFunc: (U) -> String, completition handler: @escaping (U?) -> Void) {
|
||||
menu = UIMenu(title: title, current: current, list: list, addDisabled: addDisabled, titleFunc: titleFunc) { [weak self] title, value in
|
||||
guard let self else { return }
|
||||
handler(value)
|
||||
self.menu = self.menu?.updateActionState(actionTitle: title)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if canImport(UIKit)
|
||||
|
||||
#else
|
||||
public typealias UIMenu = NSMenu
|
||||
|
||||
public final class UIAction: NSMenuItem {
|
||||
private let handler: (UIAction) -> Void
|
||||
init(title: String, handler: @escaping (UIAction) -> Void) {
|
||||
self.handler = handler
|
||||
super.init(title: title, action: #selector(menuPressed), keyEquivalent: "")
|
||||
state = .off
|
||||
target = self
|
||||
}
|
||||
|
||||
@objc private func menuPressed() {
|
||||
handler(self)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
extension UIMenu {
|
||||
var children: [NSMenuItem] {
|
||||
items
|
||||
}
|
||||
|
||||
convenience init(title: String, children: [UIAction]) {
|
||||
self.init(title: title)
|
||||
for item in children {
|
||||
addItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
147
KSPlayer-main/Sources/KSPlayer/Video/KSPlayerItem.swift
Normal file
147
KSPlayer-main/Sources/KSPlayer/Video/KSPlayerItem.swift
Normal file
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// KSPlayerItem.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by kintan on 16/5/21.
|
||||
//
|
||||
//
|
||||
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import MediaPlayer
|
||||
|
||||
public class KSPlayerResource: Equatable, Hashable {
|
||||
public static func == (lhs: KSPlayerResource, rhs: KSPlayerResource) -> Bool {
|
||||
lhs.definitions == rhs.definitions
|
||||
}
|
||||
|
||||
public let name: String
|
||||
public let definitions: [KSPlayerResourceDefinition]
|
||||
public let cover: URL?
|
||||
public let subtitleDataSouce: SubtitleDataSouce?
|
||||
public var nowPlayingInfo: KSNowPlayableMetadata?
|
||||
public let extinf: [String: String]?
|
||||
/**
|
||||
Player recource item with url, used to play single difinition video
|
||||
|
||||
- parameter name: video name
|
||||
- parameter url: video url
|
||||
- parameter cover: video cover, will show before playing, and hide when play
|
||||
- parameter subtitleURLs: video subtitles
|
||||
*/
|
||||
public convenience init(url: URL, options: KSOptions = KSOptions(), name: String = "", cover: URL? = nil, subtitleURLs: [URL]? = nil, extinf: [String: String]? = nil) {
|
||||
let definition = KSPlayerResourceDefinition(url: url, definition: "", options: options)
|
||||
let subtitleDataSouce: URLSubtitleDataSouce?
|
||||
if let subtitleURLs {
|
||||
subtitleDataSouce = URLSubtitleDataSouce(urls: subtitleURLs)
|
||||
} else {
|
||||
subtitleDataSouce = nil
|
||||
}
|
||||
|
||||
self.init(name: name, definitions: [definition], cover: cover, subtitleDataSouce: subtitleDataSouce, extinf: extinf)
|
||||
}
|
||||
|
||||
/**
|
||||
Play resouce with multi definitions
|
||||
|
||||
- parameter name: video name
|
||||
- parameter definitions: video definitions
|
||||
- parameter cover: video cover
|
||||
- parameter subtitle: video subtitle
|
||||
*/
|
||||
public init(name: String, definitions: [KSPlayerResourceDefinition], cover: URL? = nil, subtitleDataSouce: SubtitleDataSouce? = nil, extinf: [String: String]? = nil) {
|
||||
self.name = name
|
||||
self.cover = cover
|
||||
self.subtitleDataSouce = subtitleDataSouce
|
||||
self.definitions = definitions
|
||||
self.extinf = extinf
|
||||
nowPlayingInfo = KSNowPlayableMetadata(title: name)
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(definitions)
|
||||
}
|
||||
}
|
||||
|
||||
extension KSPlayerResource: Identifiable {
|
||||
public var id: KSPlayerResource { self }
|
||||
}
|
||||
|
||||
public struct KSPlayerResourceDefinition: Hashable {
|
||||
public static func == (lhs: KSPlayerResourceDefinition, rhs: KSPlayerResourceDefinition) -> Bool {
|
||||
lhs.url == rhs.url
|
||||
}
|
||||
|
||||
public let url: URL
|
||||
public let definition: String
|
||||
public let options: KSOptions
|
||||
public init(url: URL) {
|
||||
self.init(url: url, definition: url.lastPathComponent)
|
||||
}
|
||||
|
||||
/**
|
||||
Video recource item with defination name and specifying options
|
||||
|
||||
- parameter url: video url
|
||||
- parameter definition: url deifination
|
||||
- parameter options: specifying options for the initialization of the AVURLAsset
|
||||
*/
|
||||
public init(url: URL, definition: String, options: KSOptions = KSOptions()) {
|
||||
self.url = url
|
||||
self.definition = definition
|
||||
self.options = options
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(url)
|
||||
}
|
||||
}
|
||||
|
||||
extension KSPlayerResourceDefinition: Identifiable {
|
||||
public var id: Self { self }
|
||||
}
|
||||
|
||||
public struct KSNowPlayableMetadata {
|
||||
private let mediaType: MPNowPlayingInfoMediaType?
|
||||
private let isLiveStream: Bool?
|
||||
private let title: String
|
||||
private let artist: String?
|
||||
private let artwork: MPMediaItemArtwork?
|
||||
private let albumArtist: String?
|
||||
private let albumTitle: String?
|
||||
var nowPlayingInfo: [String: Any] {
|
||||
var nowPlayingInfo = [String: Any]()
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = mediaType?.rawValue
|
||||
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = isLiveStream
|
||||
nowPlayingInfo[MPMediaItemPropertyTitle] = title
|
||||
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
|
||||
if #available(OSX 10.13.2, *) {
|
||||
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
|
||||
}
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = albumArtist
|
||||
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = albumTitle
|
||||
return nowPlayingInfo
|
||||
}
|
||||
|
||||
public init(mediaType: MPNowPlayingInfoMediaType? = nil, isLiveStream: Bool? = nil, title: String, artist: String? = nil,
|
||||
artwork: MPMediaItemArtwork? = nil, albumArtist: String? = nil, albumTitle: String? = nil)
|
||||
{
|
||||
self.mediaType = mediaType
|
||||
self.isLiveStream = isLiveStream
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
self.artwork = artwork
|
||||
self.albumArtist = albumArtist
|
||||
self.albumTitle = albumTitle
|
||||
}
|
||||
|
||||
public init(mediaType: MPNowPlayingInfoMediaType? = nil, isLiveStream: Bool? = nil, title: String, artist: String? = nil, image: UIImage, albumArtist: String? = nil, albumTitle: String? = nil) {
|
||||
self.mediaType = mediaType
|
||||
self.isLiveStream = isLiveStream
|
||||
self.title = title
|
||||
self.artist = artist
|
||||
self.albumArtist = albumArtist
|
||||
self.albumTitle = albumTitle
|
||||
artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
||||
}
|
||||
}
|
||||
209
KSPlayer-main/Sources/KSPlayer/Video/MacVideoPlayerView.swift
Normal file
209
KSPlayer-main/Sources/KSPlayer/Video/MacVideoPlayerView.swift
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// MacVideoPlayerView.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by kintan on 2018/10/31.
|
||||
//
|
||||
#if !canImport(UIKit)
|
||||
|
||||
import AppKit
|
||||
import AVFoundation
|
||||
|
||||
public extension NSPasteboard.PasteboardType {
|
||||
static let nsURL = NSPasteboard.PasteboardType("NSURL")
|
||||
static let nsFilenames = NSPasteboard.PasteboardType("NSFilenamesPboardType")
|
||||
}
|
||||
|
||||
public extension NSDraggingInfo {
|
||||
@MainActor
|
||||
func getUrl() -> URL? {
|
||||
guard let types = draggingPasteboard.types else { return nil }
|
||||
|
||||
if types.contains(.nsFilenames) {
|
||||
guard let paths = draggingPasteboard.propertyList(forType: .nsFilenames) as? [String] else { return nil }
|
||||
let urls = paths.map { URL(fileURLWithPath: $0) }
|
||||
return urls.first
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
open class MacVideoPlayerView: VideoPlayerView {
|
||||
override open func customizeUIComponents() {
|
||||
super.customizeUIComponents()
|
||||
registerForDraggedTypes([.nsFilenames, .nsURL, .string])
|
||||
}
|
||||
|
||||
override open func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
||||
super.player(layer: layer, state: state)
|
||||
if state == .readyToPlay {
|
||||
let naturalSize = layer.player.naturalSize
|
||||
window?.aspectRatio = naturalSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MacVideoPlayerView {
|
||||
override open func updateTrackingAreas() {
|
||||
for trackingArea in trackingAreas {
|
||||
removeTrackingArea(trackingArea)
|
||||
}
|
||||
let trackingArea = NSTrackingArea(rect: bounds, options: [.mouseEnteredAndExited, .mouseMoved, .activeInKeyWindow], owner: self, userInfo: nil)
|
||||
addTrackingArea(trackingArea)
|
||||
}
|
||||
|
||||
override open func mouseEntered(with _: NSEvent) {
|
||||
isMaskShow = true
|
||||
}
|
||||
|
||||
override open func mouseMoved(with _: NSEvent) {
|
||||
isMaskShow = true
|
||||
}
|
||||
|
||||
override open func mouseExited(with _: NSEvent) {
|
||||
isMaskShow = false
|
||||
}
|
||||
|
||||
override open func scrollWheel(with event: NSEvent) {
|
||||
if event.phase.contains(.began) {
|
||||
if event.scrollingDeltaX != 0 {
|
||||
scrollDirection = .horizontal
|
||||
tmpPanValue = toolBar.timeSlider.value
|
||||
} else if event.scrollingDeltaY != 0 {
|
||||
scrollDirection = .vertical
|
||||
tmpPanValue = 1
|
||||
}
|
||||
} else if event.phase.contains(.changed) {
|
||||
let delta = scrollDirection == .horizontal ? event.scrollingDeltaX : event.scrollingDeltaY
|
||||
if scrollDirection == .horizontal {
|
||||
tmpPanValue += Float(delta / 10000) * Float(totalTime)
|
||||
showSeekToView(second: Double(tmpPanValue), isAdd: delta > 0)
|
||||
} else {
|
||||
if KSOptions.enableVolumeGestures {
|
||||
tmpPanValue -= Float(delta / 1000)
|
||||
tmpPanValue = max(min(tmpPanValue, 1), 0)
|
||||
}
|
||||
}
|
||||
} else if event.phase.contains(.ended) {
|
||||
if scrollDirection == .horizontal {
|
||||
slider(value: Double(tmpPanValue), event: .touchUpInside)
|
||||
hideSeekToView()
|
||||
} else {
|
||||
if KSOptions.enableVolumeGestures {
|
||||
playerLayer?.player.playbackVolume = tmpPanValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open var acceptsFirstResponder: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override open func keyDown(with event: NSEvent) {
|
||||
if let specialKey = event.specialKey {
|
||||
if specialKey == .rightArrow {
|
||||
slider(value: Double(toolBar.timeSlider.value) + 0.01 * totalTime, event: .touchUpInside)
|
||||
} else if specialKey == .leftArrow {
|
||||
slider(value: Double(toolBar.timeSlider.value) - 0.01 * totalTime, event: .touchUpInside)
|
||||
}
|
||||
} else if let character = event.characters?.first {
|
||||
if character == " " {
|
||||
onButtonPressed(toolBar.playButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override open func draggingEntered(_: NSDraggingInfo) -> NSDragOperation {
|
||||
.copy
|
||||
}
|
||||
|
||||
override open func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
|
||||
if let url = sender.getUrl() {
|
||||
if url.isMovie || url.isAudio {
|
||||
set(resource: KSPlayerResource(url: url, options: KSOptions()))
|
||||
return true
|
||||
} else {
|
||||
srtControl.selectedSubtitleInfo = URLSubtitleInfo(url: url)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class UIActivityIndicatorView: UIView {
|
||||
private let loadingView = NSView()
|
||||
private let progressLabel = UILabel()
|
||||
public var progress: Double = 0 {
|
||||
didSet {
|
||||
print("new progress: \(progress)")
|
||||
progressLabel.stringValue = "\(Int(progress * 100))%"
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame frameRect: CGRect) {
|
||||
super.init(frame: frameRect)
|
||||
wantsLayer = true
|
||||
backingLayer?.backgroundColor = UIColor(white: 0, alpha: 0.2).cgColor
|
||||
setupLoadingView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func setupLoadingView() {
|
||||
loadingView.wantsLayer = true
|
||||
addSubview(loadingView)
|
||||
let imageView = NSImageView()
|
||||
imageView.image = KSOptions.image(named: "loading")
|
||||
loadingView.addSubview(imageView)
|
||||
imageView.imageScaling = .scaleAxesIndependently
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
loadingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
widthAnchor.constraint(equalToConstant: 110),
|
||||
heightAnchor.constraint(equalToConstant: 110),
|
||||
loadingView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
loadingView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
loadingView.widthAnchor.constraint(equalTo: widthAnchor),
|
||||
loadingView.heightAnchor.constraint(equalTo: heightAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: loadingView.bottomAnchor),
|
||||
imageView.leadingAnchor.constraint(equalTo: loadingView.leadingAnchor),
|
||||
imageView.heightAnchor.constraint(equalTo: widthAnchor),
|
||||
imageView.widthAnchor.constraint(equalTo: heightAnchor),
|
||||
])
|
||||
progressLabel.alignment = .center
|
||||
progressLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
|
||||
addSubview(progressLabel)
|
||||
progressLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
progressLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||
progressLabel.topAnchor.constraint(equalTo: loadingView.bottomAnchor, constant: 20),
|
||||
progressLabel.widthAnchor.constraint(equalToConstant: 100),
|
||||
progressLabel.heightAnchor.constraint(equalToConstant: 22),
|
||||
])
|
||||
startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
extension UIActivityIndicatorView: LoadingIndector {
|
||||
func startAnimating() {
|
||||
loadingView.backingLayer?.position = CGPoint(x: loadingView.layer!.frame.midX, y: loadingView.layer!.frame.midY)
|
||||
loadingView.backingLayer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
rotationAnimation.duration = 1.0
|
||||
rotationAnimation.repeatCount = MAXFLOAT
|
||||
rotationAnimation.fromValue = 0.0
|
||||
rotationAnimation.toValue = Float.pi * -2
|
||||
loadingView.backingLayer?.add(rotationAnimation, forKey: "loading")
|
||||
}
|
||||
|
||||
func stopAnimating() {
|
||||
loadingView.backingLayer?.removeAnimation(forKey: "loading")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// PlayerFullScreenViewController.swift
|
||||
// KSPlayer
|
||||
//
|
||||
// Created by kintan on 2021/8/20.
|
||||
//
|
||||
#if canImport(UIKit) && !os(tvOS)
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol PlayerViewFullScreenDelegate: AnyObject {
|
||||
func player(isMaskShow: Bool, isFullScreen: Bool)
|
||||
}
|
||||
|
||||
class PlayerFullScreenViewController: UIViewController {
|
||||
private let isHorizonal: Bool
|
||||
private var statusHiden = false
|
||||
init(isHorizonal: Bool) {
|
||||
self.isHorizonal = isHorizonal
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
KSOptions.supportedInterfaceOrientations = isHorizonal ? .landscapeRight : .portrait
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
navigationController?.navigationBar.isHidden = true
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
KSOptions.supportedInterfaceOrientations == .all
|
||||
}
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
.all
|
||||
}
|
||||
|
||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
.lightContent
|
||||
}
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
statusHiden
|
||||
}
|
||||
}
|
||||
|
||||
extension PlayerFullScreenViewController: PlayerViewFullScreenDelegate {
|
||||
func player(isMaskShow: Bool, isFullScreen: Bool) {
|
||||
if isFullScreen {
|
||||
statusHiden = !isMaskShow
|
||||
setNeedsFocusUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// PlayerTransitionAnimator.swift
|
||||
// KSPlayer
|
||||
//
|
||||
// Created by kintan on 2021/8/20.
|
||||
//
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
|
||||
class PlayerTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
private let isDismiss: Bool
|
||||
private let containerView: UIView
|
||||
private let animationView: UIView
|
||||
private let fromCenter: CGPoint
|
||||
init(containerView: UIView, animationView: UIView, isDismiss: Bool = false) {
|
||||
self.containerView = containerView
|
||||
self.animationView = animationView
|
||||
self.isDismiss = isDismiss
|
||||
fromCenter = containerView.superview?.convert(containerView.center, to: nil) ?? .zero
|
||||
super.init()
|
||||
}
|
||||
|
||||
func transitionDuration(using _: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
0.3
|
||||
}
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
let animationSuperView = animationView.superview
|
||||
let animationViewIndex = animationSuperView?.subviews.firstIndex(of: animationView) ?? 0
|
||||
let initSize = animationView.frame.size
|
||||
let animationFrameConstraints = animationView.frameConstraints
|
||||
guard let presentedView = transitionContext.view(forKey: isDismiss ? .from : .to) else {
|
||||
return
|
||||
}
|
||||
if isDismiss {
|
||||
containerView.layoutIfNeeded()
|
||||
presentedView.bounds = containerView.bounds
|
||||
presentedView.removeFromSuperview()
|
||||
} else {
|
||||
if let viewController = transitionContext.viewController(forKey: .to) {
|
||||
presentedView.frame = transitionContext.finalFrame(for: viewController)
|
||||
}
|
||||
}
|
||||
presentedView.layoutIfNeeded()
|
||||
transitionContext.containerView.addSubview(animationView)
|
||||
animationView.translatesAutoresizingMaskIntoConstraints = true
|
||||
guard let transform = transitionContext.viewController(forKey: .from)?.view.transform else {
|
||||
return
|
||||
}
|
||||
animationView.transform = CGAffineTransform(scaleX: initSize.width / animationView.frame.size.width, y: initSize.height / animationView.frame.size.height).concatenating(transform)
|
||||
let toCenter = transitionContext.containerView.center
|
||||
let fromCenter = transform == .identity ? fromCenter : fromCenter.reverse
|
||||
animationView.center = isDismiss ? toCenter : fromCenter
|
||||
UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: .curveEaseInOut) {
|
||||
self.animationView.transform = .identity
|
||||
self.animationView.center = self.isDismiss ? fromCenter : toCenter
|
||||
} completion: { _ in
|
||||
animationSuperView?.insertSubview(self.animationView, at: animationViewIndex)
|
||||
if !animationFrameConstraints.isEmpty {
|
||||
self.animationView.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate(animationFrameConstraints)
|
||||
}
|
||||
if !self.isDismiss {
|
||||
transitionContext.containerView.addSubview(presentedView)
|
||||
}
|
||||
transitionContext.completeTransition(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
75
KSPlayer-main/Sources/KSPlayer/Video/SeekView.swift
Normal file
75
KSPlayer-main/Sources/KSPlayer/Video/SeekView.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// SeekView.swift
|
||||
// KSPlayer-iOS
|
||||
//
|
||||
// Created by kintan on 2018/11/14.
|
||||
//
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
public protocol SeekViewProtocol {
|
||||
func set(text: String, isAdd: Bool)
|
||||
}
|
||||
|
||||
class SeekView: UIView {
|
||||
private let seekToViewImage = UIImageView()
|
||||
private let seekToLabel = UILabel()
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
addSubview(seekToViewImage)
|
||||
addSubview(seekToLabel)
|
||||
seekToLabel.font = .systemFont(ofSize: 13)
|
||||
seekToLabel.textColor = UIColor(red: 0.9098, green: 0.9098, blue: 0.9098, alpha: 1.0)
|
||||
backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.7)
|
||||
cornerRadius = 4
|
||||
clipsToBounds = true
|
||||
isHidden = true
|
||||
if #available(macOS 11.0, *) {
|
||||
seekToViewImage.image = UIImage(systemName: "forward.fill")
|
||||
}
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
seekToViewImage.translatesAutoresizingMaskIntoConstraints = false
|
||||
seekToLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
seekToViewImage.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 15),
|
||||
seekToViewImage.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
seekToViewImage.widthAnchor.constraint(equalToConstant: 25),
|
||||
seekToViewImage.heightAnchor.constraint(equalToConstant: 15),
|
||||
seekToLabel.leadingAnchor.constraint(equalTo: seekToViewImage.trailingAnchor, constant: 10),
|
||||
seekToLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
var backgroundColor: UIColor? {
|
||||
get {
|
||||
if let layer, let cgColor = layer.backgroundColor {
|
||||
return UIColor(cgColor: cgColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
set {
|
||||
backingLayer?.backgroundColor = newValue?.cgColor
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extension SeekView: SeekViewProtocol {
|
||||
public func set(text: String, isAdd: Bool) {
|
||||
seekToLabel.text = text
|
||||
if !isAdd {
|
||||
seekToViewImage.backingLayer?.position = CGPoint(x: seekToViewImage.backingLayer!.frame.midX, y: seekToViewImage.backingLayer!.frame.midY)
|
||||
seekToViewImage.backingLayer?.anchorPoint = CGPoint(x: 0.5, y: 0.5)
|
||||
}
|
||||
seekToViewImage.centerRotate(byDegrees: isAdd ? 0.0 : 180)
|
||||
}
|
||||
}
|
||||
1054
KSPlayer-main/Sources/KSPlayer/Video/VideoPlayerView.swift
Normal file
1054
KSPlayer-main/Sources/KSPlayer/Video/VideoPlayerView.swift
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user