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:
220
KSPlayer-main/Sources/KSPlayer/Core/PlayerView.swift
Normal file
220
KSPlayer-main/Sources/KSPlayer/Core/PlayerView.swift
Normal file
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user