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:
2026-01-21 22:12:08 -06:00
commit 872354b834
283 changed files with 338296 additions and 0 deletions

View File

@@ -0,0 +1,136 @@
//
// SubtitleDecode.swift
// KSPlayer
//
// Created by kintan on 2018/3/11.
//
import CoreGraphics
import Foundation
import Libavformat
#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif
class SubtitleDecode: DecodeProtocol {
private var codecContext: UnsafeMutablePointer<AVCodecContext>?
private let scale = VideoSwresample(dstFormat: AV_PIX_FMT_ARGB, isDovi: false)
private var subtitle = AVSubtitle()
private var startTime = TimeInterval(0)
private let assParse = AssParse()
required init(assetTrack: FFmpegAssetTrack, options: KSOptions) {
startTime = assetTrack.startTime.seconds
do {
codecContext = try assetTrack.createContext(options: options)
if let pointer = codecContext?.pointee.subtitle_header {
let subtitleHeader = String(cString: pointer)
_ = assParse.canParse(scanner: Scanner(string: subtitleHeader))
}
} catch {
KSLog(error as CustomStringConvertible)
}
}
func decode() {}
func decodeFrame(from packet: Packet, completionHandler: @escaping (Result<MEFrame, Error>) -> Void) {
guard let codecContext else {
return
}
var gotsubtitle = Int32(0)
_ = avcodec_decode_subtitle2(codecContext, &subtitle, &gotsubtitle, packet.corePacket)
if gotsubtitle == 0 {
return
}
let timestamp = packet.timestamp
var start = packet.assetTrack.timebase.cmtime(for: timestamp).seconds + TimeInterval(subtitle.start_display_time) / 1000.0
if start >= startTime {
start -= startTime
}
var duration = 0.0
if subtitle.end_display_time != UInt32.max {
duration = TimeInterval(subtitle.end_display_time - subtitle.start_display_time) / 1000.0
}
if duration == 0, packet.duration != 0 {
duration = packet.assetTrack.timebase.cmtime(for: packet.duration).seconds
}
var parts = text(subtitle: subtitle)
/// preSubtitleFrameend
/// endstart
if parts.isEmpty {
parts.append(SubtitlePart(0, 0, attributedString: nil))
}
for part in parts {
part.start = start
if duration == 0 {
part.end = .infinity
} else {
part.end = start + duration
}
let frame = SubtitleFrame(part: part, timebase: packet.assetTrack.timebase)
frame.timestamp = timestamp
completionHandler(.success(frame))
}
avsubtitle_free(&subtitle)
}
func doFlushCodec() {}
func shutdown() {
scale.shutdown()
avsubtitle_free(&subtitle)
if let codecContext {
avcodec_close(codecContext)
avcodec_free_context(&self.codecContext)
}
}
private func text(subtitle: AVSubtitle) -> [SubtitlePart] {
var parts = [SubtitlePart]()
var images = [(CGRect, CGImage)]()
var origin: CGPoint = .zero
var attributedString: NSMutableAttributedString?
for i in 0 ..< Int(subtitle.num_rects) {
guard let rect = subtitle.rects[i]?.pointee else {
continue
}
if i == 0 {
origin = CGPoint(x: Int(rect.x), y: Int(rect.y))
}
if let text = rect.text {
if attributedString == nil {
attributedString = NSMutableAttributedString()
}
attributedString?.append(NSAttributedString(string: String(cString: text)))
} else if let ass = rect.ass {
let scanner = Scanner(string: String(cString: ass))
if let group = assParse.parsePart(scanner: scanner) {
parts.append(group)
}
} else if rect.type == SUBTITLE_BITMAP {
if let image = scale.transfer(format: AV_PIX_FMT_PAL8, width: rect.w, height: rect.h, data: Array(tuple: rect.data), linesize: Array(tuple: rect.linesize))?.cgImage() {
images.append((CGRect(x: Int(rect.x), y: Int(rect.y), width: Int(rect.w), height: Int(rect.h)), image))
}
}
}
if images.count > 0 {
let part = SubtitlePart(0, 0, attributedString: nil)
if images.count > 1 {
origin = .zero
}
var image: UIImage?
// ,jpgtifiOS绿 heic线png
if let data = CGImage.combine(images: images)?.data(type: .png, quality: 0.2) {
image = UIImage(data: data)
}
part.image = image
part.origin = origin
parts.append(part)
}
if let attributedString {
parts.append(SubtitlePart(0, 0, attributedString: attributedString))
}
return parts
}
}