Files
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

215 lines
10 KiB
Swift

//
// MetalRender.swift
// KSPlayer-iOS
//
// Created by kintan on 2020/1/11.
//
import Accelerate
import CoreVideo
import Foundation
import Metal
import QuartzCore
import simd
class MetalRender {
static let device = MTLCreateSystemDefaultDevice()!
static let library: MTLLibrary = {
var library: MTLLibrary!
library = device.makeDefaultLibrary()
if library == nil {
library = try? device.makeDefaultLibrary(bundle: .module)
}
return library
}()
private let renderPassDescriptor = MTLRenderPassDescriptor()
private let commandQueue = MetalRender.device.makeCommandQueue()
private lazy var samplerState: MTLSamplerState? = {
let samplerDescriptor = MTLSamplerDescriptor()
samplerDescriptor.minFilter = .linear
samplerDescriptor.magFilter = .linear
return MetalRender.device.makeSamplerState(descriptor: samplerDescriptor)
}()
private lazy var colorConversion601VideoRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_ITU_R_601_4.pointee.videoRange.buffer
private lazy var colorConversion601FullRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_ITU_R_601_4.pointee.buffer
private lazy var colorConversion709VideoRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_ITU_R_709_2.pointee.videoRange.buffer
private lazy var colorConversion709FullRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_ITU_R_709_2.pointee.buffer
private lazy var colorConversionSMPTE240MVideoRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_SMPTE_240M_1995.videoRange.buffer
private lazy var colorConversionSMPTE240MFullRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_SMPTE_240M_1995.buffer
private lazy var colorConversion2020VideoRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_ITU_R_2020.videoRange.buffer
private lazy var colorConversion2020FullRangeMatrixBuffer: MTLBuffer? = kvImage_YpCbCrToARGBMatrix_ITU_R_2020.buffer
private lazy var colorOffsetVideoRangeMatrixBuffer: MTLBuffer? = {
var firstColumn = SIMD3<Float>(-16.0 / 255.0, -128.0 / 255.0, -128.0 / 255.0)
let buffer = MetalRender.device.makeBuffer(bytes: &firstColumn, length: MemoryLayout<SIMD3<Float>>.size)
buffer?.label = "colorOffset"
return buffer
}()
private lazy var colorOffsetFullRangeMatrixBuffer: MTLBuffer? = {
var firstColumn = SIMD3<Float>(0, -128.0 / 255.0, -128.0 / 255.0)
let buffer = MetalRender.device.makeBuffer(bytes: &firstColumn, length: MemoryLayout<SIMD3<Float>>.size)
buffer?.label = "colorOffset"
return buffer
}()
private lazy var leftShiftMatrixBuffer: MTLBuffer? = {
var firstColumn = SIMD3<UInt8>(1, 1, 1)
let buffer = MetalRender.device.makeBuffer(bytes: &firstColumn, length: MemoryLayout<SIMD3<UInt8>>.size)
buffer?.label = "leftShit"
return buffer
}()
private lazy var leftShiftSixMatrixBuffer: MTLBuffer? = {
var firstColumn = SIMD3<UInt8>(64, 64, 64)
let buffer = MetalRender.device.makeBuffer(bytes: &firstColumn, length: MemoryLayout<SIMD3<UInt8>>.size)
buffer?.label = "leftShit"
return buffer
}()
func clear(drawable: MTLDrawable) {
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
renderPassDescriptor.colorAttachments[0].loadAction = .clear
guard let commandBuffer = commandQueue?.makeCommandBuffer(),
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor)
else {
return
}
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
}
@MainActor
func draw(pixelBuffer: PixelBufferProtocol, display: DisplayEnum = .plane, drawable: CAMetalDrawable) {
let inputTextures = pixelBuffer.textures()
renderPassDescriptor.colorAttachments[0].texture = drawable.texture
guard !inputTextures.isEmpty, let commandBuffer = commandQueue?.makeCommandBuffer(), let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
return
}
encoder.pushDebugGroup("RenderFrame")
let state = display.pipeline(planeCount: pixelBuffer.planeCount, bitDepth: pixelBuffer.bitDepth)
encoder.setRenderPipelineState(state)
encoder.setFragmentSamplerState(samplerState, index: 0)
for (index, texture) in inputTextures.enumerated() {
texture.label = "texture\(index)"
encoder.setFragmentTexture(texture, index: index)
}
setFragmentBuffer(pixelBuffer: pixelBuffer, encoder: encoder)
display.set(encoder: encoder)
encoder.popDebugGroup()
encoder.endEncoding()
commandBuffer.present(drawable)
commandBuffer.commit()
commandBuffer.waitUntilCompleted()
}
private func setFragmentBuffer(pixelBuffer: PixelBufferProtocol, encoder: MTLRenderCommandEncoder) {
if pixelBuffer.planeCount > 1 {
let buffer: MTLBuffer?
let yCbCrMatrix = pixelBuffer.yCbCrMatrix
let isFullRangeVideo = pixelBuffer.isFullRangeVideo
if yCbCrMatrix == kCVImageBufferYCbCrMatrix_ITU_R_709_2 {
buffer = isFullRangeVideo ? colorConversion709FullRangeMatrixBuffer : colorConversion709VideoRangeMatrixBuffer
} else if yCbCrMatrix == kCVImageBufferYCbCrMatrix_SMPTE_240M_1995 {
buffer = isFullRangeVideo ? colorConversionSMPTE240MFullRangeMatrixBuffer : colorConversionSMPTE240MVideoRangeMatrixBuffer
} else if yCbCrMatrix == kCVImageBufferYCbCrMatrix_ITU_R_2020 {
buffer = isFullRangeVideo ? colorConversion2020FullRangeMatrixBuffer : colorConversion2020VideoRangeMatrixBuffer
} else {
buffer = isFullRangeVideo ? colorConversion601FullRangeMatrixBuffer : colorConversion601VideoRangeMatrixBuffer
}
encoder.setFragmentBuffer(buffer, offset: 0, index: 0)
let colorOffset = isFullRangeVideo ? colorOffsetFullRangeMatrixBuffer : colorOffsetVideoRangeMatrixBuffer
encoder.setFragmentBuffer(colorOffset, offset: 0, index: 1)
let leftShift = pixelBuffer.leftShift == 0 ? leftShiftMatrixBuffer : leftShiftSixMatrixBuffer
encoder.setFragmentBuffer(leftShift, offset: 0, index: 2)
}
}
static func makePipelineState(fragmentFunction: String, isSphere: Bool = false, bitDepth: Int32 = 8) -> MTLRenderPipelineState {
let descriptor = MTLRenderPipelineDescriptor()
descriptor.colorAttachments[0].pixelFormat = KSOptions.colorPixelFormat(bitDepth: bitDepth)
descriptor.vertexFunction = library.makeFunction(name: isSphere ? "mapSphereTexture" : "mapTexture")
descriptor.fragmentFunction = library.makeFunction(name: fragmentFunction)
let vertexDescriptor = MTLVertexDescriptor()
vertexDescriptor.attributes[0].format = .float4
vertexDescriptor.attributes[0].bufferIndex = 0
vertexDescriptor.attributes[0].offset = 0
vertexDescriptor.attributes[1].format = .float2
vertexDescriptor.attributes[1].bufferIndex = 1
vertexDescriptor.attributes[1].offset = 0
vertexDescriptor.layouts[0].stride = MemoryLayout<simd_float4>.stride
vertexDescriptor.layouts[1].stride = MemoryLayout<simd_float2>.stride
descriptor.vertexDescriptor = vertexDescriptor
// swiftlint:disable force_try
return try! library.device.makeRenderPipelineState(descriptor: descriptor)
// swftlint:enable force_try
}
static func texture(pixelBuffer: CVPixelBuffer) -> [MTLTexture] {
guard let iosurface = CVPixelBufferGetIOSurface(pixelBuffer)?.takeUnretainedValue() else {
return []
}
let formats = KSOptions.pixelFormat(planeCount: pixelBuffer.planeCount, bitDepth: pixelBuffer.bitDepth)
return (0 ..< pixelBuffer.planeCount).compactMap { index in
let width = pixelBuffer.widthOfPlane(at: index)
let height = pixelBuffer.heightOfPlane(at: index)
let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: formats[index], width: width, height: height, mipmapped: false)
return device.makeTexture(descriptor: descriptor, iosurface: iosurface, plane: index)
}
}
static func textures(formats: [MTLPixelFormat], widths: [Int], heights: [Int], buffers: [MTLBuffer?], lineSizes: [Int]) -> [MTLTexture] {
(0 ..< formats.count).compactMap { i in
guard let buffer = buffers[i] else {
return nil
}
let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: formats[i], width: widths[i], height: heights[i], mipmapped: false)
descriptor.storageMode = buffer.storageMode
return buffer.makeTexture(descriptor: descriptor, offset: 0, bytesPerRow: lineSizes[i])
}
}
}
// swiftlint:disable identifier_name
// private let kvImage_YpCbCrToARGBMatrix_ITU_R_601_4 = vImage_YpCbCrToARGBMatrix(Kr: 0.299, Kb: 0.114)
// private let kvImage_YpCbCrToARGBMatrix_ITU_R_709_2 = vImage_YpCbCrToARGBMatrix(Kr: 0.2126, Kb: 0.0722)
private let kvImage_YpCbCrToARGBMatrix_SMPTE_240M_1995 = vImage_YpCbCrToARGBMatrix(Kr: 0.212, Kb: 0.087)
private let kvImage_YpCbCrToARGBMatrix_ITU_R_2020 = vImage_YpCbCrToARGBMatrix(Kr: 0.2627, Kb: 0.0593)
extension vImage_YpCbCrToARGBMatrix {
/**
https://en.wikipedia.org/wiki/YCbCr
@textblock
| R | | 1 0 2-2Kr | | Y' |
| G | = | 1 -Kb * (2 - 2 * Kb) / Kg -Kr * (2 - 2 * Kr) / Kg | | Cb |
| B | | 1 2 - 2 * Kb 0 | | Cr |
@/textblock
*/
init(Kr: Float, Kb: Float) {
let Kg = 1 - Kr - Kb
self.init(Yp: 1, Cr_R: 2 - 2 * Kr, Cr_G: -Kr * (2 - 2 * Kr) / Kg, Cb_G: -Kb * (2 - 2 * Kb) / Kg, Cb_B: 2 - 2 * Kb)
}
var videoRange: vImage_YpCbCrToARGBMatrix {
vImage_YpCbCrToARGBMatrix(Yp: 255 / 219 * Yp, Cr_R: 255 / 224 * Cr_R, Cr_G: 255 / 224 * Cr_G, Cb_G: 255 / 224 * Cb_G, Cb_B: 255 / 224 * Cb_B)
}
var buffer: MTLBuffer? {
var matrix = simd_float3x3([Yp, Yp, Yp], [0.0, Cb_G, Cb_B], [Cr_R, Cr_G, 0.0])
let buffer = MetalRender.device.makeBuffer(bytes: &matrix, length: MemoryLayout<simd_float3x3>.size)
buffer?.label = "colorConversionMatrix"
return buffer
}
}
// swiftlint:enable identifier_name