mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
12 KiB
12 KiB
Subtitle Support
KSPlayer provides comprehensive subtitle support including embedded subtitles, external subtitle files, and online subtitle search.
SubtitleModel
SubtitleModel manages subtitle sources, selection, and rendering.
Properties
open class SubtitleModel: ObservableObject {
// Available subtitle sources
@Published public private(set) var subtitleInfos: [any SubtitleInfo]
// Current subtitle parts being displayed
@Published public private(set) var parts: [SubtitlePart]
// Global subtitle delay (seconds)
public var subtitleDelay: Double = 0.0
// Current media URL
public var url: URL?
// Selected subtitle
@Published public var selectedSubtitleInfo: (any SubtitleInfo)?
}
Static Styling Properties
SubtitleModel.textColor: Color = .white
SubtitleModel.textBackgroundColor: Color = .clear
SubtitleModel.textFontSize: CGFloat = SubtitleModel.Size.standard.rawValue
SubtitleModel.textBold: Bool = false
SubtitleModel.textItalic: Bool = false
SubtitleModel.textPosition: TextPosition = TextPosition()
Font Sizes
public enum Size {
case smaller // 12pt (iPhone), 20pt (iPad/Mac), 48pt (TV)
case standard // 16pt (iPhone), 26pt (iPad/Mac), 58pt (TV)
case large // 20pt (iPhone), 32pt (iPad/Mac), 68pt (TV)
}
Methods
// Add subtitle source
public func addSubtitle(info: any SubtitleInfo)
// Add subtitle data source
public func addSubtitle(dataSouce: SubtitleDataSouce)
// Search for subtitles online
public func searchSubtitle(query: String?, languages: [String])
// Get subtitle for current time (called internally)
public func subtitle(currentTime: TimeInterval) -> Bool
SubtitleInfo Protocol
Protocol for subtitle track information:
public protocol SubtitleInfo: KSSubtitleProtocol, AnyObject, Hashable, Identifiable {
var subtitleID: String { get }
var name: String { get }
var delay: TimeInterval { get set }
var isEnabled: Bool { get set }
}
KSSubtitleProtocol
public protocol KSSubtitleProtocol {
func search(for time: TimeInterval) -> [SubtitlePart]
}
URLSubtitleInfo
Subtitle from a URL:
public class URLSubtitleInfo: KSSubtitle, SubtitleInfo {
public private(set) var downloadURL: URL
public var delay: TimeInterval = 0
public private(set) var name: String
public let subtitleID: String
public var comment: String?
public var isEnabled: Bool
// Simple initializer
public convenience init(url: URL)
// Full initializer
public init(
subtitleID: String,
name: String,
url: URL,
userAgent: String? = nil
)
}
Example: Loading External Subtitle
let subtitleURL = URL(string: "https://example.com/subtitle.srt")!
let subtitleInfo = URLSubtitleInfo(url: subtitleURL)
// Add to subtitle model
subtitleModel.addSubtitle(info: subtitleInfo)
// Or select directly
subtitleModel.selectedSubtitleInfo = subtitleInfo
SubtitlePart
A single subtitle cue:
public class SubtitlePart: CustomStringConvertible, Identifiable {
public var start: TimeInterval
public var end: TimeInterval
public var origin: CGPoint = .zero
public let text: NSAttributedString?
public var image: UIImage? // For image-based subtitles (e.g., SUP)
public var textPosition: TextPosition?
public convenience init(_ start: TimeInterval, _ end: TimeInterval, _ string: String)
public init(_ start: TimeInterval, _ end: TimeInterval, attributedString: NSAttributedString?)
}
SubtitleDataSouce Protocol
Protocol for subtitle sources:
public protocol SubtitleDataSouce: AnyObject {
var infos: [any SubtitleInfo] { get }
}
FileURLSubtitleDataSouce
For file-based subtitle sources:
public protocol FileURLSubtitleDataSouce: SubtitleDataSouce {
func searchSubtitle(fileURL: URL?) async throws
}
SearchSubtitleDataSouce
For online subtitle search:
public protocol SearchSubtitleDataSouce: SubtitleDataSouce {
func searchSubtitle(query: String?, languages: [String]) async throws
}
CacheSubtitleDataSouce
For cached subtitles:
public protocol CacheSubtitleDataSouce: FileURLSubtitleDataSouce {
func addCache(fileURL: URL, downloadURL: URL)
}
Built-in Data Sources
URLSubtitleDataSouce
Simple URL-based subtitle source:
public class URLSubtitleDataSouce: SubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init(urls: [URL])
}
// Example
let subtitleSource = URLSubtitleDataSouce(urls: [
URL(string: "https://example.com/english.srt")!,
URL(string: "https://example.com/spanish.srt")!
])
DirectorySubtitleDataSouce
Searches for subtitles in the same directory as the video:
public class DirectorySubtitleDataSouce: FileURLSubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init()
public func searchSubtitle(fileURL: URL?) async throws
}
PlistCacheSubtitleDataSouce
Caches downloaded subtitle locations:
public class PlistCacheSubtitleDataSouce: CacheSubtitleDataSouce {
public static let singleton: PlistCacheSubtitleDataSouce
public var infos: [any SubtitleInfo]
public func searchSubtitle(fileURL: URL?) async throws
public func addCache(fileURL: URL, downloadURL: URL)
}
Online Subtitle Providers
ShooterSubtitleDataSouce
Shooter.cn subtitle search (for local files):
public class ShooterSubtitleDataSouce: FileURLSubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init()
public func searchSubtitle(fileURL: URL?) async throws
}
AssrtSubtitleDataSouce
Assrt.net subtitle search:
public class AssrtSubtitleDataSouce: SearchSubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init(token: String)
public func searchSubtitle(query: String?, languages: [String]) async throws
}
// Example
let assrtSource = AssrtSubtitleDataSouce(token: "your-api-token")
OpenSubtitleDataSouce
OpenSubtitles.com API:
public class OpenSubtitleDataSouce: SearchSubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init(apiKey: String, username: String? = nil, password: String? = nil)
// Search by query
public func searchSubtitle(query: String?, languages: [String]) async throws
// Search by IDs
public func searchSubtitle(
query: String?,
imdbID: Int,
tmdbID: Int,
languages: [String]
) async throws
// Search with custom parameters
public func searchSubtitle(queryItems: [String: String]) async throws
}
// Example
let openSubSource = OpenSubtitleDataSouce(apiKey: "your-api-key")
Configuring Default Data Sources
// Set default subtitle data sources
KSOptions.subtitleDataSouces = [
DirectorySubtitleDataSouce(),
PlistCacheSubtitleDataSouce.singleton
]
// Add online search
KSOptions.subtitleDataSouces.append(
OpenSubtitleDataSouce(apiKey: "your-key")
)
UIKit Integration
With VideoPlayerView
class VideoViewController: UIViewController {
let playerView = IOSVideoPlayerView()
func loadSubtitle(url: URL) {
let subtitleInfo = URLSubtitleInfo(url: url)
playerView.srtControl.addSubtitle(info: subtitleInfo)
playerView.srtControl.selectedSubtitleInfo = subtitleInfo
}
func selectSubtitle(at index: Int) {
let subtitles = playerView.srtControl.subtitleInfos
if index < subtitles.count {
playerView.srtControl.selectedSubtitleInfo = subtitles[index]
}
}
func disableSubtitles() {
playerView.srtControl.selectedSubtitleInfo = nil
}
}
Subtitle Styling
// Configure before creating player
SubtitleModel.textFontSize = 20
SubtitleModel.textColor = .yellow
SubtitleModel.textBackgroundColor = Color.black.opacity(0.5)
SubtitleModel.textBold = true
// Update during playback (VideoPlayerView only)
playerView.updateSrt()
SwiftUI Integration
With KSVideoPlayer.Coordinator
struct PlayerView: View {
@StateObject var coordinator = KSVideoPlayer.Coordinator()
var body: some View {
VStack {
KSVideoPlayer(coordinator: coordinator, url: url, options: KSOptions())
// Subtitle picker
Picker("Subtitle", selection: $coordinator.subtitleModel.selectedSubtitleInfo) {
Text("Off").tag(nil as (any SubtitleInfo)?)
ForEach(coordinator.subtitleModel.subtitleInfos, id: \.subtitleID) { info in
Text(info.name).tag(info as (any SubtitleInfo)?)
}
}
}
}
}
Adding External Subtitles
func addSubtitle(url: URL) {
let info = URLSubtitleInfo(url: url)
coordinator.subtitleModel.addSubtitle(info: info)
}
Searching Online Subtitles
func searchSubtitles(title: String) {
coordinator.subtitleModel.searchSubtitle(
query: title,
languages: ["en", "es"]
)
}
TextPosition
Subtitle text positioning:
public struct TextPosition {
public var verticalAlign: VerticalAlignment = .bottom
public var horizontalAlign: HorizontalAlignment = .center
public var leftMargin: CGFloat = 0
public var rightMargin: CGFloat = 0
public var verticalMargin: CGFloat = 10
}
// Configure position
SubtitleModel.textPosition = TextPosition(
verticalAlign: .bottom,
horizontalAlign: .center,
verticalMargin: 50
)
Supported Subtitle Formats
KSPlayer supports various subtitle formats through FFmpeg and built-in parsers:
- Text Formats: SRT, ASS/SSA, VTT, TTML
- Image Formats: SUP/PGS, VobSub (IDX/SUB)
- Embedded Subtitles: From MKV, MP4, etc.
Parsing Subtitles Manually
let subtitle = KSSubtitle()
// Parse from URL
Task {
try await subtitle.parse(url: subtitleURL)
print("Loaded \(subtitle.parts.count) subtitle cues")
}
// Parse from data
try subtitle.parse(data: subtitleData, encoding: .utf8)
// Search for subtitle at time
let parts = subtitle.search(for: currentTime)
Complete Example
class SubtitlePlayerController: UIViewController, KSPlayerLayerDelegate {
private var playerLayer: KSPlayerLayer!
private var subtitleModel = SubtitleModel()
private var subtitleLabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
setupSubtitleLabel()
// Configure subtitle sources
let subtitleSource = URLSubtitleDataSouce(urls: [
URL(string: "https://example.com/english.srt")!
])
subtitleModel.addSubtitle(dataSouce: subtitleSource)
// Create player
let url = URL(string: "https://example.com/video.mp4")!
playerLayer = KSPlayerLayer(url: url, options: KSOptions(), delegate: self)
subtitleModel.url = url
}
func player(layer: KSPlayerLayer, state: KSPlayerState) {
if state == .readyToPlay {
// Add embedded subtitles
if let subtitleDataSource = layer.player.subtitleDataSouce {
subtitleModel.addSubtitle(dataSouce: subtitleDataSource)
}
// Auto-select first subtitle
subtitleModel.selectedSubtitleInfo = subtitleModel.subtitleInfos.first
}
}
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
if subtitleModel.subtitle(currentTime: currentTime) {
updateSubtitleDisplay()
}
}
private func updateSubtitleDisplay() {
if let part = subtitleModel.parts.first {
subtitleLabel.attributedText = part.text
subtitleLabel.isHidden = false
} else {
subtitleLabel.isHidden = true
}
}
}