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>
221 lines
6.1 KiB
Swift
221 lines
6.1 KiB
Swift
//
|
|
// PlayerView.swift
|
|
// VoiceNote
|
|
//
|
|
// Created by kintan on 2018/8/16.
|
|
//
|
|
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
#else
|
|
import AppKit
|
|
#endif
|
|
import AVFoundation
|
|
|
|
public enum PlayerButtonType: Int {
|
|
case play = 101
|
|
case pause
|
|
case back
|
|
case srt
|
|
case landscape
|
|
case replay
|
|
case lock
|
|
case rate
|
|
case definition
|
|
case pictureInPicture
|
|
case audioSwitch
|
|
case videoSwitch
|
|
}
|
|
|
|
public protocol PlayerControllerDelegate: AnyObject {
|
|
func playerController(state: KSPlayerState)
|
|
func playerController(currentTime: TimeInterval, totalTime: TimeInterval)
|
|
func playerController(finish error: Error?)
|
|
func playerController(maskShow: Bool)
|
|
func playerController(action: PlayerButtonType)
|
|
// `bufferedCount: 0` indicates first time loading
|
|
func playerController(bufferedCount: Int, consumeTime: TimeInterval)
|
|
func playerController(seek: TimeInterval)
|
|
}
|
|
|
|
open class PlayerView: UIView, KSPlayerLayerDelegate, KSSliderDelegate {
|
|
public typealias ControllerDelegate = PlayerControllerDelegate
|
|
public var playerLayer: KSPlayerLayer? {
|
|
didSet {
|
|
playerLayer?.delegate = self
|
|
}
|
|
}
|
|
|
|
public weak var delegate: ControllerDelegate?
|
|
public let toolBar = PlayerToolBar()
|
|
public let srtControl = SubtitleModel()
|
|
// Listen to play time change
|
|
public var playTimeDidChange: ((TimeInterval, TimeInterval) -> Void)?
|
|
public var backBlock: (() -> Void)?
|
|
public convenience init() {
|
|
#if os(macOS)
|
|
self.init(frame: .zero)
|
|
#else
|
|
self.init(frame: CGRect(origin: .zero, size: KSOptions.sceneSize))
|
|
#endif
|
|
}
|
|
|
|
override public init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
toolBar.timeSlider.delegate = self
|
|
toolBar.addTarget(self, action: #selector(onButtonPressed(_:)))
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
public required init?(coder _: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
@objc func onButtonPressed(_ button: UIButton) {
|
|
guard let type = PlayerButtonType(rawValue: button.tag) else { return }
|
|
|
|
#if os(macOS)
|
|
if let menu = button.menu,
|
|
let item = button.menu?.items.first(where: { $0.state == .on })
|
|
{
|
|
menu.popUp(positioning: item,
|
|
at: button.frame.origin,
|
|
in: self)
|
|
} else {
|
|
onButtonPressed(type: type, button: button)
|
|
}
|
|
#elseif os(tvOS)
|
|
onButtonPressed(type: type, button: button)
|
|
#else
|
|
if #available(iOS 14.0, *), button.menu != nil {
|
|
return
|
|
}
|
|
onButtonPressed(type: type, button: button)
|
|
#endif
|
|
}
|
|
|
|
open func onButtonPressed(type: PlayerButtonType, button: UIButton) {
|
|
var type = type
|
|
if type == .play, button.isSelected {
|
|
type = .pause
|
|
}
|
|
switch type {
|
|
case .back:
|
|
backBlock?()
|
|
case .play, .replay:
|
|
play()
|
|
case .pause:
|
|
pause()
|
|
default:
|
|
break
|
|
}
|
|
delegate?.playerController(action: type)
|
|
}
|
|
|
|
#if canImport(UIKit)
|
|
override open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
|
|
guard let presse = presses.first else {
|
|
return
|
|
}
|
|
switch presse.type {
|
|
case .playPause:
|
|
if let playerLayer, playerLayer.state.isPlaying {
|
|
pause()
|
|
} else {
|
|
play()
|
|
}
|
|
default: super.pressesBegan(presses, with: event)
|
|
}
|
|
}
|
|
#endif
|
|
open func play() {
|
|
becomeFirstResponder()
|
|
playerLayer?.play()
|
|
toolBar.playButton.isSelected = true
|
|
}
|
|
|
|
open func pause() {
|
|
playerLayer?.pause()
|
|
}
|
|
|
|
open func seek(time: TimeInterval, completion: @escaping ((Bool) -> Void)) {
|
|
playerLayer?.seek(time: time, autoPlay: KSOptions.isSeekedAutoPlay, completion: completion)
|
|
}
|
|
|
|
open func resetPlayer() {
|
|
pause()
|
|
totalTime = 0.0
|
|
}
|
|
|
|
open func set(url: URL, options: KSOptions) {
|
|
srtControl.url = url
|
|
toolBar.currentTime = 0
|
|
totalTime = 0
|
|
playerLayer = KSPlayerLayer(url: url, options: options)
|
|
}
|
|
|
|
// MARK: - KSSliderDelegate
|
|
|
|
open func slider(value: Double, event: ControlEvents) {
|
|
if event == .valueChanged {
|
|
toolBar.currentTime = value
|
|
} else if event == .touchUpInside {
|
|
seek(time: value) { [weak self] _ in
|
|
self?.delegate?.playerController(seek: value)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - KSPlayerLayerDelegate
|
|
|
|
open func player(layer: KSPlayerLayer, state: KSPlayerState) {
|
|
delegate?.playerController(state: state)
|
|
if state == .readyToPlay {
|
|
totalTime = layer.player.duration
|
|
toolBar.isSeekable = layer.player.seekable
|
|
toolBar.playButton.isSelected = true
|
|
} else if state == .playedToTheEnd || state == .paused || state == .error {
|
|
toolBar.playButton.isSelected = false
|
|
}
|
|
}
|
|
|
|
open func player(layer _: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
|
|
delegate?.playerController(currentTime: currentTime, totalTime: totalTime)
|
|
playTimeDidChange?(currentTime, totalTime)
|
|
toolBar.currentTime = currentTime
|
|
self.totalTime = totalTime
|
|
}
|
|
|
|
open func player(layer _: KSPlayerLayer, finish error: Error?) {
|
|
delegate?.playerController(finish: error)
|
|
}
|
|
|
|
open func player(layer _: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval) {
|
|
delegate?.playerController(bufferedCount: bufferedCount, consumeTime: consumeTime)
|
|
}
|
|
}
|
|
|
|
public extension PlayerView {
|
|
var totalTime: TimeInterval {
|
|
get {
|
|
toolBar.totalTime
|
|
}
|
|
set {
|
|
toolBar.totalTime = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
extension UIView {
|
|
var viewController: UIViewController? {
|
|
var next = next
|
|
while next != nil {
|
|
if let viewController = next as? UIViewController {
|
|
return viewController
|
|
}
|
|
next = next?.next
|
|
}
|
|
return nil
|
|
}
|
|
}
|