Files
streamyfin/docs/ks-player/SubtitleSupport.md

491 lines
12 KiB
Markdown

# 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
```swift
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
```swift
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
```swift
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
```swift
// 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:
```swift
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
```swift
public protocol KSSubtitleProtocol {
func search(for time: TimeInterval) -> [SubtitlePart]
}
```
## URLSubtitleInfo
Subtitle from a URL:
```swift
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
```swift
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:
```swift
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:
```swift
public protocol SubtitleDataSouce: AnyObject {
var infos: [any SubtitleInfo] { get }
}
```
### FileURLSubtitleDataSouce
For file-based subtitle sources:
```swift
public protocol FileURLSubtitleDataSouce: SubtitleDataSouce {
func searchSubtitle(fileURL: URL?) async throws
}
```
### SearchSubtitleDataSouce
For online subtitle search:
```swift
public protocol SearchSubtitleDataSouce: SubtitleDataSouce {
func searchSubtitle(query: String?, languages: [String]) async throws
}
```
### CacheSubtitleDataSouce
For cached subtitles:
```swift
public protocol CacheSubtitleDataSouce: FileURLSubtitleDataSouce {
func addCache(fileURL: URL, downloadURL: URL)
}
```
## Built-in Data Sources
### URLSubtitleDataSouce
Simple URL-based subtitle source:
```swift
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:
```swift
public class DirectorySubtitleDataSouce: FileURLSubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init()
public func searchSubtitle(fileURL: URL?) async throws
}
```
### PlistCacheSubtitleDataSouce
Caches downloaded subtitle locations:
```swift
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):
```swift
public class ShooterSubtitleDataSouce: FileURLSubtitleDataSouce {
public var infos: [any SubtitleInfo]
public init()
public func searchSubtitle(fileURL: URL?) async throws
}
```
### AssrtSubtitleDataSouce
Assrt.net subtitle search:
```swift
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:
```swift
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
```swift
// Set default subtitle data sources
KSOptions.subtitleDataSouces = [
DirectorySubtitleDataSouce(),
PlistCacheSubtitleDataSouce.singleton
]
// Add online search
KSOptions.subtitleDataSouces.append(
OpenSubtitleDataSouce(apiKey: "your-key")
)
```
## UIKit Integration
### With VideoPlayerView
```swift
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
```swift
// 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
```swift
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
```swift
func addSubtitle(url: URL) {
let info = URLSubtitleInfo(url: url)
coordinator.subtitleModel.addSubtitle(info: info)
}
```
### Searching Online Subtitles
```swift
func searchSubtitles(title: String) {
coordinator.subtitleModel.searchSubtitle(
query: title,
languages: ["en", "es"]
)
}
```
## TextPosition
Subtitle text positioning:
```swift
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
```swift
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
```swift
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
}
}
}
```