Files
simvision/KSPlayer-main/Sources/KSPlayer/MEPlayer/Model.swift
Michael Simard 872354b834 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>
2026-01-21 22:12:08 -06:00

494 lines
17 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// Model.swift
// KSPlayer
//
// Created by kintan on 2018/3/9.
//
import AVFoundation
import CoreMedia
import Libavcodec
#if canImport(UIKit)
import UIKit
#endif
// MARK: enum
enum MESourceState {
case idle
case opening
case opened
case reading
case seeking
case paused
case finished
case closed
case failed
}
// MARK: delegate
public protocol OutputRenderSourceDelegate: AnyObject {
func getVideoOutputRender(force: Bool) -> VideoVTBFrame?
func getAudioOutputRender() -> AudioFrame?
func setAudio(time: CMTime, position: Int64)
func setVideo(time: CMTime, position: Int64)
}
protocol CodecCapacityDelegate: AnyObject {
func codecDidFinished(track: some CapacityProtocol)
}
protocol MEPlayerDelegate: AnyObject {
func sourceDidChange(loadingState: LoadingState)
func sourceDidOpened()
func sourceDidFailed(error: NSError?)
func sourceDidFinished()
func sourceDidChange(oldBitRate: Int64, newBitrate: Int64)
}
// MARK: protocol
public protocol ObjectQueueItem {
var timebase: Timebase { get }
var timestamp: Int64 { get set }
var duration: Int64 { get set }
// byte position
var position: Int64 { get set }
var size: Int32 { get set }
}
extension ObjectQueueItem {
var seconds: TimeInterval { cmtime.seconds }
var cmtime: CMTime { timebase.cmtime(for: timestamp) }
}
public protocol FrameOutput: AnyObject {
var renderSource: OutputRenderSourceDelegate? { get set }
func pause()
func flush()
func play()
}
protocol MEFrame: ObjectQueueItem {
var timebase: Timebase { get set }
}
// MARK: model
// for MEPlayer
public extension KSOptions {
/// VR
static var enableSensor = true
static var stackSize = 65536
static var isClearVideoWhereReplace = true
static var audioPlayerType: AudioOutput.Type = AudioEnginePlayer.self
static var videoPlayerType: (VideoOutput & UIView).Type = MetalPlayView.self
static var yadifMode = 1
static var deInterlaceAddIdet = false
static func colorSpace(ycbcrMatrix: CFString?, transferFunction: CFString?) -> CGColorSpace? {
switch ycbcrMatrix {
case kCVImageBufferYCbCrMatrix_ITU_R_709_2:
return CGColorSpace(name: CGColorSpace.itur_709)
case kCVImageBufferYCbCrMatrix_ITU_R_601_4:
return CGColorSpace(name: CGColorSpace.sRGB)
case kCVImageBufferYCbCrMatrix_ITU_R_2020:
if transferFunction == kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) {
return CGColorSpace(name: CGColorSpace.itur_2100_PQ)
} else if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, *) {
return CGColorSpace(name: CGColorSpace.itur_2020_PQ)
} else {
return CGColorSpace(name: CGColorSpace.itur_2020_PQ_EOTF)
}
} else if transferFunction == kCVImageBufferTransferFunction_ITU_R_2100_HLG {
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) {
return CGColorSpace(name: CGColorSpace.itur_2100_HLG)
} else {
return CGColorSpace(name: CGColorSpace.itur_2020)
}
} else {
return CGColorSpace(name: CGColorSpace.itur_2020)
}
default:
return CGColorSpace(name: CGColorSpace.sRGB)
}
}
static func colorSpace(colorPrimaries: CFString?) -> CGColorSpace? {
switch colorPrimaries {
case kCVImageBufferColorPrimaries_ITU_R_709_2:
return CGColorSpace(name: CGColorSpace.sRGB)
case kCVImageBufferColorPrimaries_DCI_P3:
if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, *) {
return CGColorSpace(name: CGColorSpace.displayP3_PQ)
} else {
return CGColorSpace(name: CGColorSpace.displayP3_PQ_EOTF)
}
case kCVImageBufferColorPrimaries_ITU_R_2020:
if #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) {
return CGColorSpace(name: CGColorSpace.itur_2100_PQ)
} else if #available(macOS 10.15.4, iOS 13.4, tvOS 13.4, *) {
return CGColorSpace(name: CGColorSpace.itur_2020_PQ)
} else {
return CGColorSpace(name: CGColorSpace.itur_2020_PQ_EOTF)
}
default:
return CGColorSpace(name: CGColorSpace.sRGB)
}
}
static func pixelFormat(planeCount: Int, bitDepth: Int32) -> [MTLPixelFormat] {
if planeCount == 3 {
if bitDepth > 8 {
return [.r16Unorm, .r16Unorm, .r16Unorm]
} else {
return [.r8Unorm, .r8Unorm, .r8Unorm]
}
} else if planeCount == 2 {
if bitDepth > 8 {
return [.r16Unorm, .rg16Unorm]
} else {
return [.r8Unorm, .rg8Unorm]
}
} else {
return [colorPixelFormat(bitDepth: bitDepth)]
}
}
static func colorPixelFormat(bitDepth: Int32) -> MTLPixelFormat {
if bitDepth == 10 {
return .bgr10a2Unorm
} else {
return .bgra8Unorm
}
}
}
enum MECodecState {
case idle
case decoding
case flush
case closed
case failed
case finished
}
public struct Timebase {
static let defaultValue = Timebase(num: 1, den: 1)
public let num: Int32
public let den: Int32
func getPosition(from seconds: TimeInterval) -> Int64 { Int64(seconds * TimeInterval(den) / TimeInterval(num)) }
func cmtime(for timestamp: Int64) -> CMTime { CMTime(value: timestamp * Int64(num), timescale: den) }
}
extension Timebase {
public var rational: AVRational { AVRational(num: num, den: den) }
init(_ rational: AVRational) {
num = rational.num
den = rational.den
}
}
final class Packet: ObjectQueueItem {
var duration: Int64 = 0
var timestamp: Int64 = 0
var position: Int64 = 0
var size: Int32 = 0
private(set) var corePacket = av_packet_alloc()
var timebase: Timebase {
assetTrack.timebase
}
var isKeyFrame: Bool {
if let corePacket {
return corePacket.pointee.flags & AV_PKT_FLAG_KEY == AV_PKT_FLAG_KEY
} else {
return false
}
}
var assetTrack: FFmpegAssetTrack! {
didSet {
guard let packet = corePacket?.pointee else {
return
}
timestamp = packet.pts == Int64.min ? packet.dts : packet.pts
position = packet.pos
duration = packet.duration
size = packet.size
}
}
deinit {
av_packet_unref(corePacket)
av_packet_free(&corePacket)
}
}
final class SubtitleFrame: MEFrame {
var timestamp: Int64 = 0
var timebase: Timebase
var duration: Int64 = 0
var position: Int64 = 0
var size: Int32 = 0
let part: SubtitlePart
init(part: SubtitlePart, timebase: Timebase) {
self.part = part
self.timebase = timebase
}
}
public final class AudioFrame: MEFrame {
public let dataSize: Int
public let audioFormat: AVAudioFormat
public internal(set) var timebase = Timebase.defaultValue
public var timestamp: Int64 = 0
public var duration: Int64 = 0
public var position: Int64 = 0
public var size: Int32 = 0
public var data: [UnsafeMutablePointer<UInt8>?]
public var numberOfSamples: UInt32 = 0
public init(dataSize: Int, audioFormat: AVAudioFormat) {
self.dataSize = dataSize
self.audioFormat = audioFormat
let count = audioFormat.isInterleaved ? 1 : audioFormat.channelCount
data = (0 ..< count).map { _ in
UnsafeMutablePointer<UInt8>.allocate(capacity: dataSize)
}
}
init(array: [AudioFrame]) {
audioFormat = array[0].audioFormat
timebase = array[0].timebase
timestamp = array[0].timestamp
position = array[0].position
var dataSize = 0
for frame in array {
duration += frame.duration
dataSize += frame.dataSize
size += frame.size
numberOfSamples += frame.numberOfSamples
}
self.dataSize = dataSize
let count = audioFormat.isInterleaved ? 1 : audioFormat.channelCount
data = (0 ..< count).map { _ in
UnsafeMutablePointer<UInt8>.allocate(capacity: dataSize)
}
var offset = 0
for frame in array {
for i in 0 ..< data.count {
data[i]?.advanced(by: offset).initialize(from: frame.data[i]!, count: frame.dataSize)
}
offset += frame.dataSize
}
}
deinit {
for i in 0 ..< data.count {
data[i]?.deinitialize(count: dataSize)
data[i]?.deallocate()
}
data.removeAll()
}
public func toFloat() -> [ContiguousArray<Float>] {
var array = [ContiguousArray<Float>]()
for i in 0 ..< data.count {
switch audioFormat.commonFormat {
case .pcmFormatInt16:
let capacity = dataSize / MemoryLayout<Int16>.size
data[i]?.withMemoryRebound(to: Int16.self, capacity: capacity) { src in
var des = ContiguousArray<Float>(repeating: 0, count: Int(capacity))
for j in 0 ..< capacity {
des[j] = max(-1.0, min(Float(src[j]) / 32767.0, 1.0))
}
array.append(des)
}
case .pcmFormatInt32:
let capacity = dataSize / MemoryLayout<Int32>.size
data[i]?.withMemoryRebound(to: Int32.self, capacity: capacity) { src in
var des = ContiguousArray<Float>(repeating: 0, count: Int(capacity))
for j in 0 ..< capacity {
des[j] = max(-1.0, min(Float(src[j]) / 2_147_483_647.0, 1.0))
}
array.append(des)
}
default:
let capacity = dataSize / MemoryLayout<Float>.size
data[i]?.withMemoryRebound(to: Float.self, capacity: capacity) { src in
var des = ContiguousArray<Float>(repeating: 0, count: Int(capacity))
for j in 0 ..< capacity {
des[j] = src[j]
}
array.append(ContiguousArray<Float>(des))
}
}
}
return array
}
public func toPCMBuffer() -> AVAudioPCMBuffer? {
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: numberOfSamples) else {
return nil
}
pcmBuffer.frameLength = pcmBuffer.frameCapacity
for i in 0 ..< min(Int(pcmBuffer.format.channelCount), data.count) {
switch audioFormat.commonFormat {
case .pcmFormatInt16:
let capacity = dataSize / MemoryLayout<Int16>.size
data[i]?.withMemoryRebound(to: Int16.self, capacity: capacity) { src in
pcmBuffer.int16ChannelData?[i].update(from: src, count: capacity)
}
case .pcmFormatInt32:
let capacity = dataSize / MemoryLayout<Int32>.size
data[i]?.withMemoryRebound(to: Int32.self, capacity: capacity) { src in
pcmBuffer.int32ChannelData?[i].update(from: src, count: capacity)
}
default:
let capacity = dataSize / MemoryLayout<Float>.size
data[i]?.withMemoryRebound(to: Float.self, capacity: capacity) { src in
pcmBuffer.floatChannelData?[i].update(from: src, count: capacity)
}
}
}
return pcmBuffer
}
public func toCMSampleBuffer() -> CMSampleBuffer? {
var outBlockListBuffer: CMBlockBuffer?
CMBlockBufferCreateEmpty(allocator: kCFAllocatorDefault, capacity: UInt32(data.count), flags: 0, blockBufferOut: &outBlockListBuffer)
guard let outBlockListBuffer else {
return nil
}
let sampleSize = Int(audioFormat.sampleSize)
let sampleCount = CMItemCount(numberOfSamples)
let dataByteSize = sampleCount * sampleSize
if dataByteSize > dataSize {
assertionFailure("dataByteSize: \(dataByteSize),render.dataSize: \(dataSize)")
}
for i in 0 ..< data.count {
var outBlockBuffer: CMBlockBuffer?
CMBlockBufferCreateWithMemoryBlock(
allocator: kCFAllocatorDefault,
memoryBlock: nil,
blockLength: dataByteSize,
blockAllocator: kCFAllocatorDefault,
customBlockSource: nil,
offsetToData: 0,
dataLength: dataByteSize,
flags: kCMBlockBufferAssureMemoryNowFlag,
blockBufferOut: &outBlockBuffer
)
if let outBlockBuffer {
CMBlockBufferReplaceDataBytes(
with: data[i]!,
blockBuffer: outBlockBuffer,
offsetIntoDestination: 0,
dataLength: dataByteSize
)
CMBlockBufferAppendBufferReference(
outBlockListBuffer,
targetBBuf: outBlockBuffer,
offsetToData: 0,
dataLength: CMBlockBufferGetDataLength(outBlockBuffer),
flags: 0
)
}
}
var sampleBuffer: CMSampleBuffer?
// sampleRatetimescaledurationinvalid
// let duration = CMTime(value: CMTimeValue(sampleCount), timescale: CMTimeScale(audioFormat.sampleRate))
let duration = CMTime.invalid
let timing = CMSampleTimingInfo(duration: duration, presentationTimeStamp: cmtime, decodeTimeStamp: .invalid)
let sampleSizeEntryCount: CMItemCount
let sampleSizeArray: [Int]?
if audioFormat.isInterleaved {
sampleSizeEntryCount = 1
sampleSizeArray = [sampleSize]
} else {
sampleSizeEntryCount = 0
sampleSizeArray = nil
}
CMSampleBufferCreateReady(allocator: kCFAllocatorDefault, dataBuffer: outBlockListBuffer, formatDescription: audioFormat.formatDescription, sampleCount: sampleCount, sampleTimingEntryCount: 1, sampleTimingArray: [timing], sampleSizeEntryCount: sampleSizeEntryCount, sampleSizeArray: sampleSizeArray, sampleBufferOut: &sampleBuffer)
return sampleBuffer
}
}
public final class VideoVTBFrame: MEFrame {
public var timebase = Timebase.defaultValue
// duration
public var duration: Int64 = 0
public var position: Int64 = 0
public var timestamp: Int64 = 0
public var size: Int32 = 0
public let fps: Float
public let isDovi: Bool
public var edrMetaData: EDRMetaData? = nil
var corePixelBuffer: PixelBufferProtocol?
init(fps: Float, isDovi: Bool) {
self.fps = fps
self.isDovi = isDovi
}
}
extension VideoVTBFrame {
#if !os(tvOS)
@available(iOS 16, *)
var edrMetadata: CAEDRMetadata? {
if var contentData = edrMetaData?.contentData, var displayData = edrMetaData?.displayData {
let data = Data(bytes: &displayData, count: MemoryLayout<MasteringDisplayMetadata>.stride)
let data2 = Data(bytes: &contentData, count: MemoryLayout<ContentLightMetadata>.stride)
return CAEDRMetadata.hdr10(displayInfo: data, contentInfo: data2, opticalOutputScale: 10000)
}
if var ambientViewingEnvironment = edrMetaData?.ambientViewingEnvironment {
let data = Data(bytes: &ambientViewingEnvironment, count: MemoryLayout<AmbientViewingEnvironment>.stride)
if #available(macOS 14.0, iOS 17.0, *) {
return CAEDRMetadata.hlg(ambientViewingEnvironment: data)
} else {
return CAEDRMetadata.hlg
}
}
if corePixelBuffer?.transferFunction == kCVImageBufferTransferFunction_SMPTE_ST_2084_PQ {
return CAEDRMetadata.hdr10(minLuminance: 0.1, maxLuminance: 1000, opticalOutputScale: 10000)
} else if corePixelBuffer?.transferFunction == kCVImageBufferTransferFunction_ITU_R_2100_HLG {
return CAEDRMetadata.hlg
}
return nil
}
#endif
}
public struct EDRMetaData {
var displayData: MasteringDisplayMetadata?
var contentData: ContentLightMetadata?
var ambientViewingEnvironment: AmbientViewingEnvironment?
}
public struct MasteringDisplayMetadata {
let display_primaries_r_x: UInt16
let display_primaries_r_y: UInt16
let display_primaries_g_x: UInt16
let display_primaries_g_y: UInt16
let display_primaries_b_x: UInt16
let display_primaries_b_y: UInt16
let white_point_x: UInt16
let white_point_y: UInt16
let minLuminance: UInt32
let maxLuminance: UInt32
}
public struct ContentLightMetadata {
let MaxCLL: UInt16
let MaxFALL: UInt16
}
// https://developer.apple.com/documentation/technotes/tn3145-hdr-video-metadata
public struct AmbientViewingEnvironment {
let ambient_illuminance: UInt32
let ambient_light_x: UInt16
let ambient_light_y: UInt16
}