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>
379 lines
10 KiB
Swift
379 lines
10 KiB
Swift
//
|
|
// PlayerDefines.swift
|
|
// KSPlayer
|
|
//
|
|
// Created by kintan on 2018/3/9.
|
|
//
|
|
|
|
import AVFoundation
|
|
import CoreMedia
|
|
import CoreServices
|
|
#if canImport(UIKit)
|
|
import UIKit
|
|
|
|
public extension KSOptions {
|
|
@MainActor
|
|
static var windowScene: UIWindowScene? {
|
|
UIApplication.shared.connectedScenes.first as? UIWindowScene
|
|
}
|
|
|
|
@MainActor
|
|
static var sceneSize: CGSize {
|
|
let window = windowScene?.windows.first
|
|
return window?.bounds.size ?? .zero
|
|
}
|
|
}
|
|
#else
|
|
import AppKit
|
|
import SwiftUI
|
|
|
|
public typealias UIView = NSView
|
|
public typealias UIPasteboard = NSPasteboard
|
|
public extension KSOptions {
|
|
static var sceneSize: CGSize {
|
|
NSScreen.main?.frame.size ?? .zero
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// extension MediaPlayerTrack {
|
|
// static func == (lhs: Self, rhs: Self) -> Bool {
|
|
// lhs.trackID == rhs.trackID
|
|
// }
|
|
// }
|
|
|
|
public enum DynamicRange: Int32 {
|
|
case sdr = 0
|
|
case hdr10 = 2
|
|
case hlg = 3
|
|
case dolbyVision = 5
|
|
|
|
#if canImport(UIKit)
|
|
var hdrMode: AVPlayer.HDRMode {
|
|
switch self {
|
|
case .sdr:
|
|
return AVPlayer.HDRMode(rawValue: 0)
|
|
case .hdr10:
|
|
return .hdr10 // 2
|
|
case .hlg:
|
|
return .hlg // 1
|
|
case .dolbyVision:
|
|
return .dolbyVision // 4
|
|
}
|
|
}
|
|
#endif
|
|
public static var availableHDRModes: [DynamicRange] {
|
|
#if os(macOS)
|
|
if NSScreen.main?.maximumPotentialExtendedDynamicRangeColorComponentValue ?? 1.0 > 1.0 {
|
|
return [.hdr10]
|
|
} else {
|
|
return [.sdr]
|
|
}
|
|
#else
|
|
let availableHDRModes = AVPlayer.availableHDRModes
|
|
if availableHDRModes == AVPlayer.HDRMode(rawValue: 0) {
|
|
return [.sdr]
|
|
} else {
|
|
var modes = [DynamicRange]()
|
|
if availableHDRModes.contains(.dolbyVision) {
|
|
modes.append(.dolbyVision)
|
|
}
|
|
if availableHDRModes.contains(.hdr10) {
|
|
modes.append(.hdr10)
|
|
}
|
|
if availableHDRModes.contains(.hlg) {
|
|
modes.append(.hlg)
|
|
}
|
|
return modes
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
extension DynamicRange: CustomStringConvertible {
|
|
public var description: String {
|
|
switch self {
|
|
case .sdr:
|
|
return "SDR"
|
|
case .hdr10:
|
|
return "HDR10"
|
|
case .hlg:
|
|
return "HLG"
|
|
case .dolbyVision:
|
|
return "Dolby Vision"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension DynamicRange {
|
|
var colorPrimaries: CFString {
|
|
switch self {
|
|
case .sdr:
|
|
return kCVImageBufferColorPrimaries_ITU_R_709_2
|
|
case .hdr10, .hlg, .dolbyVision:
|
|
return kCVImageBufferColorPrimaries_ITU_R_2020
|
|
}
|
|
}
|
|
|
|
var transferFunction: CFString {
|
|
switch self {
|
|
case .sdr:
|
|
return kCVImageBufferTransferFunction_ITU_R_709_2
|
|
case .hdr10:
|
|
return kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ
|
|
case .hlg, .dolbyVision:
|
|
return kCVImageBufferTransferFunction_ITU_R_2100_HLG
|
|
}
|
|
}
|
|
|
|
var yCbCrMatrix: CFString {
|
|
switch self {
|
|
case .sdr:
|
|
return kCVImageBufferYCbCrMatrix_ITU_R_709_2
|
|
case .hdr10, .hlg, .dolbyVision:
|
|
return kCVImageBufferYCbCrMatrix_ITU_R_2020
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
public enum DisplayEnum {
|
|
case plane
|
|
// swiftlint:disable identifier_name
|
|
case vr
|
|
// swiftlint:enable identifier_name
|
|
case vrBox
|
|
}
|
|
|
|
public struct VideoAdaptationState {
|
|
public struct BitRateState {
|
|
let bitRate: Int64
|
|
let time: TimeInterval
|
|
}
|
|
|
|
public let bitRates: [Int64]
|
|
public let duration: TimeInterval
|
|
public internal(set) var fps: Float
|
|
public internal(set) var bitRateStates: [BitRateState]
|
|
public internal(set) var currentPlaybackTime: TimeInterval = 0
|
|
public internal(set) var isPlayable: Bool = false
|
|
public internal(set) var loadedCount: Int = 0
|
|
}
|
|
|
|
public enum ClockProcessType {
|
|
case remain
|
|
case next
|
|
case dropNextFrame
|
|
case dropNextPacket
|
|
case dropGOPPacket
|
|
case flush
|
|
case seek
|
|
}
|
|
|
|
// 缓冲情况
|
|
public protocol CapacityProtocol {
|
|
var fps: Float { get }
|
|
var packetCount: Int { get }
|
|
var frameCount: Int { get }
|
|
var frameMaxCount: Int { get }
|
|
var isEndOfFile: Bool { get }
|
|
var mediaType: AVFoundation.AVMediaType { get }
|
|
}
|
|
|
|
extension CapacityProtocol {
|
|
var loadedTime: TimeInterval {
|
|
TimeInterval(packetCount + frameCount) / TimeInterval(fps)
|
|
}
|
|
}
|
|
|
|
public struct LoadingState {
|
|
public let loadedTime: TimeInterval
|
|
public let progress: TimeInterval
|
|
public let packetCount: Int
|
|
public let frameCount: Int
|
|
public let isEndOfFile: Bool
|
|
public let isPlayable: Bool
|
|
public let isFirst: Bool
|
|
public let isSeek: Bool
|
|
}
|
|
|
|
public let KSPlayerErrorDomain = "KSPlayerErrorDomain"
|
|
|
|
public enum KSPlayerErrorCode: Int {
|
|
case unknown
|
|
case formatCreate
|
|
case formatOpenInput
|
|
case formatOutputCreate
|
|
case formatWriteHeader
|
|
case formatFindStreamInfo
|
|
case readFrame
|
|
case codecContextCreate
|
|
case codecContextSetParam
|
|
case codecContextFindDecoder
|
|
case codesContextOpen
|
|
case codecVideoSendPacket
|
|
case codecAudioSendPacket
|
|
case codecVideoReceiveFrame
|
|
case codecAudioReceiveFrame
|
|
case auidoSwrInit
|
|
case codecSubtitleSendPacket
|
|
case videoTracksUnplayable
|
|
case subtitleUnEncoding
|
|
case subtitleUnParse
|
|
case subtitleFormatUnSupport
|
|
case subtitleParamsEmpty
|
|
}
|
|
|
|
extension KSPlayerErrorCode: CustomStringConvertible {
|
|
public var description: String {
|
|
switch self {
|
|
case .formatCreate:
|
|
return "avformat_alloc_context return nil"
|
|
case .formatOpenInput:
|
|
return "avformat can't open input"
|
|
case .formatOutputCreate:
|
|
return "avformat_alloc_output_context2 fail"
|
|
case .formatWriteHeader:
|
|
return "avformat_write_header fail"
|
|
case .formatFindStreamInfo:
|
|
return "avformat_find_stream_info return nil"
|
|
case .codecContextCreate:
|
|
return "avcodec_alloc_context3 return nil"
|
|
case .codecContextSetParam:
|
|
return "avcodec can't set parameters to context"
|
|
case .codesContextOpen:
|
|
return "codesContext can't Open"
|
|
case .codecVideoReceiveFrame:
|
|
return "avcodec can't receive video frame"
|
|
case .codecAudioReceiveFrame:
|
|
return "avcodec can't receive audio frame"
|
|
case .videoTracksUnplayable:
|
|
return "VideoTracks are not even playable."
|
|
case .codecSubtitleSendPacket:
|
|
return "avcodec can't decode subtitle"
|
|
case .subtitleUnEncoding:
|
|
return "Subtitle encoding format is not supported."
|
|
case .subtitleUnParse:
|
|
return "Subtitle parsing error"
|
|
case .subtitleFormatUnSupport:
|
|
return "Current subtitle format is not supported"
|
|
case .subtitleParamsEmpty:
|
|
return "Subtitle Params is empty"
|
|
case .auidoSwrInit:
|
|
return "swr_init swrContext fail"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
}
|
|
|
|
extension NSError {
|
|
convenience init(errorCode: KSPlayerErrorCode, userInfo: [String: Any] = [:]) {
|
|
var userInfo = userInfo
|
|
userInfo[NSLocalizedDescriptionKey] = errorCode.description
|
|
self.init(domain: KSPlayerErrorDomain, code: errorCode.rawValue, userInfo: userInfo)
|
|
}
|
|
|
|
convenience init(description: String) {
|
|
var userInfo = [String: Any]()
|
|
userInfo[NSLocalizedDescriptionKey] = description
|
|
self.init(domain: KSPlayerErrorDomain, code: 0, userInfo: userInfo)
|
|
}
|
|
}
|
|
|
|
#if !SWIFT_PACKAGE
|
|
extension Bundle {
|
|
static let module = Bundle(for: KSPlayerLayer.self).path(forResource: "KSPlayer_KSPlayer", ofType: "bundle").flatMap { Bundle(path: $0) } ?? Bundle.main
|
|
}
|
|
#endif
|
|
|
|
public enum TimeType {
|
|
case min
|
|
case hour
|
|
case minOrHour
|
|
case millisecond
|
|
}
|
|
|
|
public extension TimeInterval {
|
|
func toString(for type: TimeType) -> String {
|
|
Int(ceil(self)).toString(for: type)
|
|
}
|
|
}
|
|
|
|
public extension Int {
|
|
func toString(for type: TimeType) -> String {
|
|
var second = self
|
|
var min = second / 60
|
|
second -= min * 60
|
|
switch type {
|
|
case .min:
|
|
return String(format: "%02d:%02d", min, second)
|
|
case .hour:
|
|
let hour = min / 60
|
|
min -= hour * 60
|
|
return String(format: "%d:%02d:%02d", hour, min, second)
|
|
case .minOrHour:
|
|
let hour = min / 60
|
|
if hour > 0 {
|
|
min -= hour * 60
|
|
return String(format: "%d:%02d:%02d", hour, min, second)
|
|
} else {
|
|
return String(format: "%02d:%02d", min, second)
|
|
}
|
|
case .millisecond:
|
|
var time = self * 100
|
|
let millisecond = time % 100
|
|
time /= 100
|
|
let sec = time % 60
|
|
time /= 60
|
|
let min = time % 60
|
|
time /= 60
|
|
let hour = time % 60
|
|
if hour > 0 {
|
|
return String(format: "%d:%02d:%02d.%02d", hour, min, sec, millisecond)
|
|
} else {
|
|
return String(format: "%02d:%02d.%02d", min, sec, millisecond)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public extension FixedWidthInteger {
|
|
var kmFormatted: String {
|
|
Double(self).kmFormatted
|
|
}
|
|
}
|
|
|
|
open class AbstractAVIOContext {
|
|
let bufferSize: Int32
|
|
let writable: Bool
|
|
public init(bufferSize: Int32 = 32 * 1024, writable: Bool = false) {
|
|
self.bufferSize = bufferSize
|
|
self.writable = writable
|
|
}
|
|
|
|
open func read(buffer _: UnsafePointer<UInt8>?, size: Int32) -> Int32 {
|
|
size
|
|
}
|
|
|
|
open func write(buffer _: UnsafePointer<UInt8>?, size: Int32) -> Int32 {
|
|
size
|
|
}
|
|
|
|
/**
|
|
#define SEEK_SET 0 /* set file offset to offset */
|
|
#define SEEK_CUR 1 /* set file offset to current plus offset */
|
|
#define SEEK_END 2 /* set file offset to EOF plus offset */
|
|
*/
|
|
open func seek(offset: Int64, whence _: Int32) -> Int64 {
|
|
offset
|
|
}
|
|
|
|
open func fileSize() -> Int64 {
|
|
-1
|
|
}
|
|
|
|
open func close() {}
|
|
deinit {}
|
|
}
|