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

137 lines
5.1 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.
//
// 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
}
}