mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-15 17:02:58 +01:00
feat: KSPlayer as an option for iOS + other improvements (#1266)
This commit is contained in:
committed by
GitHub
parent
d1795c9df8
commit
74d86b5d12
490
docs/ks-player/SubtitleSupport.md
Normal file
490
docs/ks-player/SubtitleSupport.md
Normal file
@@ -0,0 +1,490 @@
|
||||
# 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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user