mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
491 lines
12 KiB
Markdown
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
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|