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

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
        }
    }
}