Files
simvision/KSPlayer-main/Sources/KSPlayer/MEPlayer/VideoToolboxDecode.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

217 lines
9.8 KiB
Swift
Raw Permalink 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.
//
// VideoToolboxDecode.swift
// KSPlayer
//
// Created by kintan on 2018/3/10.
//
import FFmpegKit
import Libavformat
#if canImport(VideoToolbox)
import VideoToolbox
class VideoToolboxDecode: DecodeProtocol {
private var session: DecompressionSession {
didSet {
VTDecompressionSessionInvalidate(oldValue.decompressionSession)
}
}
private let options: KSOptions
private var startTime = Int64(0)
private var lastPosition = Int64(0)
private var needReconfig = false
init(options: KSOptions, session: DecompressionSession) {
self.options = options
self.session = session
}
func decodeFrame(from packet: Packet, completionHandler: @escaping (Result<MEFrame, Error>) -> Void) {
if needReconfig {
//
session = DecompressionSession(assetTrack: session.assetTrack, options: options)!
doFlushCodec()
needReconfig = false
}
guard let corePacket = packet.corePacket?.pointee, let data = corePacket.data else {
return
}
do {
let sampleBuffer = try session.formatDescription.getSampleBuffer(isConvertNALSize: session.assetTrack.isConvertNALSize, data: data, size: Int(corePacket.size))
let flags: VTDecodeFrameFlags = [
._EnableAsynchronousDecompression,
]
var flagOut = VTDecodeInfoFlags.frameDropped
let timestamp = packet.timestamp
let packetFlags = corePacket.flags
let duration = corePacket.duration
let size = corePacket.size
let status = VTDecompressionSessionDecodeFrame(session.decompressionSession, sampleBuffer: sampleBuffer, flags: flags, infoFlagsOut: &flagOut) { [weak self] status, infoFlags, imageBuffer, _, _ in
guard let self, !infoFlags.contains(.frameDropped) else {
return
}
guard status == noErr else {
if status == kVTInvalidSessionErr || status == kVTVideoDecoderMalfunctionErr || status == kVTVideoDecoderBadDataErr {
if packet.isKeyFrame {
completionHandler(.failure(NSError(errorCode: .codecVideoReceiveFrame, avErrorCode: status)))
} else {
//
self.needReconfig = true
}
}
return
}
let frame = VideoVTBFrame(fps: session.assetTrack.nominalFrameRate, isDovi: session.assetTrack.dovi != nil)
frame.corePixelBuffer = imageBuffer
frame.timebase = session.assetTrack.timebase
if packet.isKeyFrame, packetFlags & AV_PKT_FLAG_DISCARD != 0, self.lastPosition > 0 {
self.startTime = self.lastPosition - timestamp
}
self.lastPosition = max(self.lastPosition, timestamp)
frame.position = packet.position
frame.timestamp = self.startTime + timestamp
frame.duration = duration
frame.size = size
self.lastPosition += frame.duration
completionHandler(.success(frame))
}
if status == noErr {
if !flags.contains(._EnableAsynchronousDecompression) {
VTDecompressionSessionWaitForAsynchronousFrames(session.decompressionSession)
}
} else if status == kVTInvalidSessionErr || status == kVTVideoDecoderMalfunctionErr || status == kVTVideoDecoderBadDataErr {
if packet.isKeyFrame {
throw NSError(errorCode: .codecVideoReceiveFrame, avErrorCode: status)
} else {
//
needReconfig = true
}
}
} catch {
completionHandler(.failure(error))
}
}
func doFlushCodec() {
lastPosition = 0
startTime = 0
}
func shutdown() {
VTDecompressionSessionInvalidate(session.decompressionSession)
}
func decode() {
lastPosition = 0
startTime = 0
}
}
class DecompressionSession {
fileprivate let formatDescription: CMFormatDescription
fileprivate let decompressionSession: VTDecompressionSession
fileprivate var assetTrack: FFmpegAssetTrack
init?(assetTrack: FFmpegAssetTrack, options: KSOptions) {
self.assetTrack = assetTrack
guard let pixelFormatType = assetTrack.pixelFormatType, let formatDescription = assetTrack.formatDescription else {
return nil
}
self.formatDescription = formatDescription
#if os(macOS)
VTRegisterProfessionalVideoWorkflowVideoDecoders()
if #available(macOS 11.0, *) {
VTRegisterSupplementalVideoDecoderIfAvailable(formatDescription.mediaSubType.rawValue)
}
#endif
// VTDecompressionSessionCanAcceptFormatDescription(<#T##session: VTDecompressionSession##VTDecompressionSession#>, formatDescription: <#T##CMFormatDescription#>)
let attributes: NSMutableDictionary = [
kCVPixelBufferPixelFormatTypeKey: pixelFormatType,
kCVPixelBufferMetalCompatibilityKey: true,
kCVPixelBufferWidthKey: assetTrack.codecpar.width,
kCVPixelBufferHeightKey: assetTrack.codecpar.height,
kCVPixelBufferIOSurfacePropertiesKey: NSDictionary(),
]
var session: VTDecompressionSession?
// swiftlint:disable line_length
let status = VTDecompressionSessionCreate(allocator: kCFAllocatorDefault, formatDescription: formatDescription, decoderSpecification: CMFormatDescriptionGetExtensions(formatDescription), imageBufferAttributes: attributes, outputCallback: nil, decompressionSessionOut: &session)
// swiftlint:enable line_length
guard status == noErr, let decompressionSession = session else {
return nil
}
if #available(iOS 14.0, tvOS 14.0, macOS 11.0, *) {
VTSessionSetProperty(decompressionSession, key: kVTDecompressionPropertyKey_PropagatePerFrameHDRDisplayMetadata,
value: kCFBooleanTrue)
}
if let destinationDynamicRange = options.availableDynamicRange(nil) {
let pixelTransferProperties = [kVTPixelTransferPropertyKey_DestinationColorPrimaries: destinationDynamicRange.colorPrimaries,
kVTPixelTransferPropertyKey_DestinationTransferFunction: destinationDynamicRange.transferFunction,
kVTPixelTransferPropertyKey_DestinationYCbCrMatrix: destinationDynamicRange.yCbCrMatrix]
VTSessionSetProperty(decompressionSession,
key: kVTDecompressionPropertyKey_PixelTransferProperties,
value: pixelTransferProperties as CFDictionary)
}
self.decompressionSession = decompressionSession
}
}
#endif
extension CMFormatDescription {
fileprivate func getSampleBuffer(isConvertNALSize: Bool, data: UnsafeMutablePointer<UInt8>, size: Int) throws -> CMSampleBuffer {
if isConvertNALSize {
var ioContext: UnsafeMutablePointer<AVIOContext>?
let status = avio_open_dyn_buf(&ioContext)
if status == 0 {
var nalSize: UInt32 = 0
let end = data + size
var nalStart = data
while nalStart < end {
nalSize = UInt32(nalStart[0]) << 16 | UInt32(nalStart[1]) << 8 | UInt32(nalStart[2])
avio_wb32(ioContext, nalSize)
nalStart += 3
avio_write(ioContext, nalStart, Int32(nalSize))
nalStart += Int(nalSize)
}
var demuxBuffer: UnsafeMutablePointer<UInt8>?
let demuxSze = avio_close_dyn_buf(ioContext, &demuxBuffer)
return try createSampleBuffer(data: demuxBuffer, size: Int(demuxSze))
} else {
throw NSError(errorCode: .codecVideoReceiveFrame, avErrorCode: status)
}
} else {
return try createSampleBuffer(data: data, size: size)
}
}
private func createSampleBuffer(data: UnsafeMutablePointer<UInt8>?, size: Int) throws -> CMSampleBuffer {
var blockBuffer: CMBlockBuffer?
var sampleBuffer: CMSampleBuffer?
// swiftlint:disable line_length
var status = CMBlockBufferCreateWithMemoryBlock(allocator: kCFAllocatorDefault, memoryBlock: data, blockLength: size, blockAllocator: kCFAllocatorNull, customBlockSource: nil, offsetToData: 0, dataLength: size, flags: 0, blockBufferOut: &blockBuffer)
if status == noErr {
status = CMSampleBufferCreate(allocator: kCFAllocatorDefault, dataBuffer: blockBuffer, dataReady: true, makeDataReadyCallback: nil, refcon: nil, formatDescription: self, sampleCount: 1, sampleTimingEntryCount: 0, sampleTimingArray: nil, sampleSizeEntryCount: 0, sampleSizeArray: nil, sampleBufferOut: &sampleBuffer)
if let sampleBuffer {
return sampleBuffer
}
}
throw NSError(errorCode: .codecVideoReceiveFrame, avErrorCode: status)
// swiftlint:enable line_length
}
}
extension CMVideoCodecType {
var avc: String {
switch self {
case kCMVideoCodecType_MPEG4Video:
return "esds"
case kCMVideoCodecType_H264:
return "avcC"
case kCMVideoCodecType_HEVC:
return "hvcC"
case kCMVideoCodecType_VP9:
return "vpcC"
default: return "avcC"
}
}
}