feat: KSPlayer as an option for iOS + other improvements (#1266)

This commit is contained in:
Fredrik Burmester
2026-01-03 13:05:50 +01:00
committed by GitHub
parent d1795c9df8
commit 74d86b5d12
191 changed files with 88479 additions and 2316 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
# Getting Started with KSPlayer
KSPlayer is a powerful media playback framework for iOS, tvOS, macOS, xrOS, and visionOS. It supports both AVPlayer and FFmpeg-based playback with AppKit/UIKit/SwiftUI support.
## Requirements
- iOS 13+
- macOS 10.15+
- tvOS 13+
- xrOS 1+
## Troubleshooting
### Missing Metal Toolchain (CocoaPods builds)
If your build fails compiling `Shaders.metal` with:
`cannot execute tool 'metal' due to missing Metal Toolchain`
Install the component:
```bash
xcodebuild -downloadComponent MetalToolchain
```
Then verify:
```bash
xcrun --find metal
xcrun metal -v
```
## Installation
### Swift Package Manager
Add KSPlayer to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/kingslay/KSPlayer.git", .branch("main"))
]
```
Or in Xcode: File → Add Packages → Enter the repository URL.
### CocoaPods
Add to your `Podfile`:
```ruby
target 'YourApp' do
use_frameworks!
pod 'KSPlayer', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main'
pod 'DisplayCriteria', :git => 'https://github.com/kingslay/KSPlayer.git', :branch => 'main'
pod 'FFmpegKit', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main'
pod 'Libass', :git => 'https://github.com/kingslay/FFmpegKit.git', :branch => 'main'
end
```
Then run:
```bash
pod install
```
## Initial Setup
### Configure Player Type
KSPlayer supports two player backends:
- `KSAVPlayer` - Uses AVPlayer (default first player)
- `KSMEPlayer` - Uses FFmpeg for decoding
Configure the player type before creating any player views:
```swift
import KSPlayer
// Use KSMEPlayer as the secondary/fallback player
KSOptions.secondPlayerType = KSMEPlayer.self
// Or set KSMEPlayer as the primary player
KSOptions.firstPlayerType = KSMEPlayer.self
```
### Player Type Selection Strategy
The player uses `firstPlayerType` initially. If playback fails, it automatically switches to `secondPlayerType`.
```swift
// Default configuration
KSOptions.firstPlayerType = KSAVPlayer.self // Uses AVPlayer first
KSOptions.secondPlayerType = KSMEPlayer.self // Falls back to FFmpeg
```
## Quick Start
### UIKit
```swift
import KSPlayer
class VideoViewController: UIViewController {
private var playerView: IOSVideoPlayerView!
override func viewDidLoad() {
super.viewDidLoad()
KSOptions.secondPlayerType = KSMEPlayer.self
playerView = IOSVideoPlayerView()
view.addSubview(playerView)
playerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
playerView.topAnchor.constraint(equalTo: view.topAnchor),
playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
let url = URL(string: "https://example.com/video.mp4")!
playerView.set(url: url, options: KSOptions())
}
}
```
### SwiftUI (iOS 16+)
```swift
import KSPlayer
import SwiftUI
struct VideoPlayerScreen: View {
let url: URL
var body: some View {
KSVideoPlayerView(url: url, options: KSOptions())
}
}
```
## Key Imports
```swift
import KSPlayer
import AVFoundation // For AVMediaType, etc.
```
## Next Steps
- [UIKit Usage](UIKitUsage.md) - Detailed UIKit integration
- [SwiftUI Usage](SwiftUIUsage.md) - SwiftUI views and modifiers
- [KSOptions](KSOptions.md) - Configuration options
- [Types and Protocols](TypesAndProtocols.md) - Core types reference

349
docs/ks-player/KSOptions.md Normal file
View File

@@ -0,0 +1,349 @@
# KSOptions
`KSOptions` is the configuration class for KSPlayer. It contains both instance properties (per-player settings) and static properties (global defaults).
## Creating Options
```swift
let options = KSOptions()
// Configure instance properties
options.isLoopPlay = true
options.startPlayTime = 30.0 // Start at 30 seconds
// Use with player
playerView.set(url: url, options: options)
```
## Instance Properties
### Buffering
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `preferredForwardBufferDuration` | `TimeInterval` | `3.0` | Minimum buffer duration before playback starts |
| `maxBufferDuration` | `TimeInterval` | `30.0` | Maximum buffer duration |
| `isSecondOpen` | `Bool` | `false` | Enable fast open (instant playback) |
### Seeking
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `isAccurateSeek` | `Bool` | `false` | Enable frame-accurate seeking |
| `seekFlags` | `Int32` | `1` | FFmpeg seek flags (AVSEEK_FLAG_BACKWARD) |
| `isSeekedAutoPlay` | `Bool` | `true` | Auto-play after seeking |
### Playback
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `isLoopPlay` | `Bool` | `false` | Loop playback (for short videos) |
| `startPlayTime` | `TimeInterval` | `0` | Initial playback position (seconds) |
| `startPlayRate` | `Float` | `1.0` | Initial playback rate |
| `registerRemoteControll` | `Bool` | `true` | Enable system remote control |
### Video
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `display` | `DisplayEnum` | `.plane` | Display mode (`.plane`, `.vr`, `.vrBox`) |
| `videoDelay` | `Double` | `0.0` | Video delay in seconds |
| `autoDeInterlace` | `Bool` | `false` | Auto-detect interlacing |
| `autoRotate` | `Bool` | `true` | Auto-rotate based on metadata |
| `destinationDynamicRange` | `DynamicRange?` | `nil` | Target HDR mode |
| `videoAdaptable` | `Bool` | `true` | Enable adaptive bitrate |
| `videoFilters` | `[String]` | `[]` | FFmpeg video filters |
| `syncDecodeVideo` | `Bool` | `false` | Synchronous video decoding |
| `hardwareDecode` | `Bool` | `true` | Use hardware decoding |
| `asynchronousDecompression` | `Bool` | `false` | Async hardware decompression |
| `videoDisable` | `Bool` | `false` | Disable video track |
### Audio
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `audioFilters` | `[String]` | `[]` | FFmpeg audio filters |
| `syncDecodeAudio` | `Bool` | `false` | Synchronous audio decoding |
### Subtitles
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `autoSelectEmbedSubtitle` | `Bool` | `true` | Auto-select embedded subtitles |
| `isSeekImageSubtitle` | `Bool` | `false` | Seek for image subtitles |
### Picture-in-Picture
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `canStartPictureInPictureAutomaticallyFromInline` | `Bool` | `true` | Auto-start PiP when app backgrounds |
### Window (macOS)
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `automaticWindowResize` | `Bool` | `true` | Auto-resize window to video aspect ratio |
### Network/HTTP
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `referer` | `String?` | `nil` | HTTP referer header |
| `userAgent` | `String?` | `"KSPlayer"` | HTTP user agent |
| `cache` | `Bool` | `false` | Enable FFmpeg HTTP caching |
| `outputURL` | `URL?` | `nil` | URL to record/save stream |
### FFmpeg Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `avOptions` | `[String: Any]` | `[:]` | AVURLAsset options |
| `formatContextOptions` | `[String: Any]` | See below | FFmpeg format context options |
| `decoderOptions` | `[String: Any]` | See below | FFmpeg decoder options |
| `probesize` | `Int64?` | `nil` | FFmpeg probe size |
| `maxAnalyzeDuration` | `Int64?` | `nil` | Max analyze duration |
| `lowres` | `UInt8` | `0` | Low resolution decoding |
| `nobuffer` | `Bool` | `false` | Disable buffering |
| `codecLowDelay` | `Bool` | `false` | Low delay codec mode |
#### Default formatContextOptions
```swift
[
"user_agent": "KSPlayer",
"scan_all_pmts": 1,
"reconnect": 1,
"reconnect_streamed": 1
]
```
#### Default decoderOptions
```swift
[
"threads": "auto",
"refcounted_frames": "1"
]
```
### Read-Only Timing Properties
| Property | Type | Description |
|----------|------|-------------|
| `formatName` | `String` | Detected format name |
| `prepareTime` | `Double` | Time when prepare started |
| `dnsStartTime` | `Double` | DNS lookup start time |
| `tcpStartTime` | `Double` | TCP connection start time |
| `tcpConnectedTime` | `Double` | TCP connected time |
| `openTime` | `Double` | File open time |
| `findTime` | `Double` | Stream find time |
| `readyTime` | `Double` | Ready to play time |
| `readAudioTime` | `Double` | First audio read time |
| `readVideoTime` | `Double` | First video read time |
| `decodeAudioTime` | `Double` | First audio decode time |
| `decodeVideoTime` | `Double` | First video decode time |
## Static Properties (Global Defaults)
### Player Types
```swift
// Primary player type (default: AVPlayer)
KSOptions.firstPlayerType: MediaPlayerProtocol.Type = KSAVPlayer.self
// Fallback player type (default: FFmpeg)
KSOptions.secondPlayerType: MediaPlayerProtocol.Type? = KSMEPlayer.self
```
### Buffering Defaults
```swift
KSOptions.preferredForwardBufferDuration: TimeInterval = 3.0
KSOptions.maxBufferDuration: TimeInterval = 30.0
KSOptions.isSecondOpen: Bool = false
```
### Playback Defaults
```swift
KSOptions.isAccurateSeek: Bool = false
KSOptions.isLoopPlay: Bool = false
KSOptions.isAutoPlay: Bool = true
KSOptions.isSeekedAutoPlay: Bool = true
```
### Decoding
```swift
KSOptions.hardwareDecode: Bool = true
KSOptions.asynchronousDecompression: Bool = false
KSOptions.canStartPictureInPictureAutomaticallyFromInline: Bool = true
```
### UI Options
```swift
// Top bar visibility: .always, .horizantalOnly, .none
KSOptions.topBarShowInCase: KSPlayerTopBarShowCase = .always
// Auto-hide controls delay
KSOptions.animateDelayTimeInterval: TimeInterval = 5.0
// Gesture controls
KSOptions.enableBrightnessGestures: Bool = true
KSOptions.enableVolumeGestures: Bool = true
KSOptions.enablePlaytimeGestures: Bool = true
// Background playback
KSOptions.canBackgroundPlay: Bool = false
```
### PiP
```swift
KSOptions.isPipPopViewController: Bool = false
```
### Logging
```swift
// Log levels: .panic, .fatal, .error, .warning, .info, .verbose, .debug, .trace
KSOptions.logLevel: LogLevel = .warning
KSOptions.logger: LogHandler = OSLog(lable: "KSPlayer")
```
### System
```swift
KSOptions.useSystemHTTPProxy: Bool = true
KSOptions.preferredFrame: Bool = true
```
### Subtitle Data Sources
```swift
KSOptions.subtitleDataSouces: [SubtitleDataSouce] = [DirectorySubtitleDataSouce()]
```
## Methods
### HTTP Headers
```swift
let options = KSOptions()
options.appendHeader(["Referer": "https://example.com"])
options.appendHeader(["Authorization": "Bearer token123"])
```
### Cookies
```swift
let cookies = [HTTPCookie(properties: [
.name: "session",
.value: "abc123",
.domain: "example.com",
.path: "/"
])!]
options.setCookie(cookies)
```
## Overridable Methods
Subclass `KSOptions` to customize behavior:
### Buffering Algorithm
```swift
class CustomOptions: KSOptions {
override func playable(capacitys: [CapacityProtocol], isFirst: Bool, isSeek: Bool) -> LoadingState {
// Custom buffering logic
super.playable(capacitys: capacitys, isFirst: isFirst, isSeek: isSeek)
}
}
```
### Adaptive Bitrate
```swift
override func adaptable(state: VideoAdaptationState?) -> (Int64, Int64)? {
// Return (currentBitrate, targetBitrate) or nil
super.adaptable(state: state)
}
```
### Track Selection
```swift
// Select preferred video track
override func wantedVideo(tracks: [MediaPlayerTrack]) -> Int? {
// Return index of preferred track or nil for auto
return tracks.firstIndex { $0.bitRate > 5_000_000 }
}
// Select preferred audio track
override func wantedAudio(tracks: [MediaPlayerTrack]) -> Int? {
// Return index of preferred track or nil for auto
return tracks.firstIndex { $0.languageCode == "en" }
}
```
### Display Layer
```swift
override func isUseDisplayLayer() -> Bool {
// Return true to use AVSampleBufferDisplayLayer (supports HDR10+)
// Return false for other display modes
display == .plane
}
```
### Track Processing
```swift
override func process(assetTrack: some MediaPlayerTrack) {
super.process(assetTrack: assetTrack)
// Custom processing before decoder creation
}
```
### Live Playback Rate
```swift
override func liveAdaptivePlaybackRate(loadingState: LoadingState) -> Float? {
// Return adjusted playback rate for live streams
// Return nil to keep current rate
if loadingState.loadedTime > preferredForwardBufferDuration + 5 {
return 1.2 // Speed up if too far behind
}
return nil
}
```
## Example: Custom Options
```swift
class StreamingOptions: KSOptions {
override init() {
super.init()
// Low latency settings
preferredForwardBufferDuration = 1.0
isSecondOpen = true
nobuffer = true
codecLowDelay = true
// Custom headers
appendHeader(["X-Custom-Header": "value"])
}
override func wantedAudio(tracks: [MediaPlayerTrack]) -> Int? {
// Prefer English audio
return tracks.firstIndex { $0.languageCode == "en" }
}
}
// Usage
let options = StreamingOptions()
playerView.set(url: streamURL, options: options)
```

View File

@@ -0,0 +1,442 @@
# KSPlayerLayer
`KSPlayerLayer` is the core playback controller that manages the media player instance and provides a high-level API for playback control.
## Overview
`KSPlayerLayer` wraps `MediaPlayerProtocol` implementations (`KSAVPlayer` or `KSMEPlayer`) and handles:
- Player lifecycle management
- Playback state transitions
- Remote control integration
- Picture-in-Picture support
- Background/foreground handling
## Creating a KSPlayerLayer
### Basic Initialization
```swift
let url = URL(string: "https://example.com/video.mp4")!
let options = KSOptions()
let playerLayer = KSPlayerLayer(
url: url,
isAutoPlay: true, // Default: KSOptions.isAutoPlay
options: options,
delegate: self
)
```
### Constructor Parameters
```swift
public init(
url: URL,
isAutoPlay: Bool = KSOptions.isAutoPlay,
options: KSOptions,
delegate: KSPlayerLayerDelegate? = nil
)
```
## Properties
### Core Properties
| Property | Type | Description |
|----------|------|-------------|
| `url` | `URL` | Current media URL (read-only after init) |
| `options` | `KSOptions` | Player configuration (read-only) |
| `player` | `MediaPlayerProtocol` | Underlying player instance |
| `state` | `KSPlayerState` | Current playback state (read-only) |
| `delegate` | `KSPlayerLayerDelegate?` | Event delegate |
### Published Properties (for Combine/SwiftUI)
```swift
@Published public var bufferingProgress: Int = 0 // 0-100
@Published public var loopCount: Int = 0 // Loop iteration count
@Published public var isPipActive: Bool = false // Picture-in-Picture state
```
## Playback Control Methods
### play()
Start or resume playback:
```swift
playerLayer.play()
```
### pause()
Pause playback:
```swift
playerLayer.pause()
```
### stop()
Stop playback and reset player state:
```swift
playerLayer.stop()
```
### seek(time:autoPlay:completion:)
Seek to a specific time:
```swift
playerLayer.seek(time: 30.0, autoPlay: true) { finished in
if finished {
print("Seek completed")
}
}
```
Parameters:
- `time: TimeInterval` - Target time in seconds
- `autoPlay: Bool` - Whether to auto-play after seeking
- `completion: @escaping ((Bool) -> Void)` - Called when seek completes
### prepareToPlay()
Prepare the player (called automatically when `isAutoPlay` is true):
```swift
playerLayer.prepareToPlay()
```
## URL Management
### set(url:options:)
Change the video URL:
```swift
let newURL = URL(string: "https://example.com/another-video.mp4")!
playerLayer.set(url: newURL, options: KSOptions())
```
### set(urls:options:)
Set a playlist of URLs:
```swift
let urls = [
URL(string: "https://example.com/video1.mp4")!,
URL(string: "https://example.com/video2.mp4")!,
URL(string: "https://example.com/video3.mp4")!
]
playerLayer.set(urls: urls, options: KSOptions())
```
The player automatically advances to the next URL when playback finishes.
## Accessing the Player
### Player Properties
Access underlying player properties through `playerLayer.player`:
```swift
// Duration
let duration = playerLayer.player.duration
// Current time
let currentTime = playerLayer.player.currentPlaybackTime
// Playing state
let isPlaying = playerLayer.player.isPlaying
// Seekable
let canSeek = playerLayer.player.seekable
// Natural size
let videoSize = playerLayer.player.naturalSize
// File size (estimated)
let fileSize = playerLayer.player.fileSize
```
### Player Control
```swift
// Volume (0.0 to 1.0)
playerLayer.player.playbackVolume = 0.5
// Mute
playerLayer.player.isMuted = true
// Playback rate
playerLayer.player.playbackRate = 1.5
// Content mode
playerLayer.player.contentMode = .scaleAspectFit
```
### Tracks
```swift
// Get audio tracks
let audioTracks = playerLayer.player.tracks(mediaType: .audio)
// Get video tracks
let videoTracks = playerLayer.player.tracks(mediaType: .video)
// Select a track
if let englishTrack = audioTracks.first(where: { $0.languageCode == "en" }) {
playerLayer.player.select(track: englishTrack)
}
```
### External Playback (AirPlay)
```swift
// Enable AirPlay
playerLayer.player.allowsExternalPlayback = true
// Check if actively using AirPlay
let isAirPlaying = playerLayer.player.isExternalPlaybackActive
// Auto-switch to external when screen connected
playerLayer.player.usesExternalPlaybackWhileExternalScreenIsActive = true
```
### Picture-in-Picture
```swift
// Available on tvOS 14.0+, iOS 14.0+
if #available(tvOS 14.0, iOS 14.0, *) {
// Toggle PiP
playerLayer.isPipActive.toggle()
// Or access controller directly
playerLayer.player.pipController?.start(view: playerLayer)
playerLayer.player.pipController?.stop(restoreUserInterface: true)
}
```
### Dynamic Info
```swift
if let dynamicInfo = playerLayer.player.dynamicInfo {
print("FPS: \(dynamicInfo.displayFPS)")
print("A/V Sync: \(dynamicInfo.audioVideoSyncDiff)")
print("Dropped frames: \(dynamicInfo.droppedVideoFrameCount)")
print("Audio bitrate: \(dynamicInfo.audioBitrate)")
print("Video bitrate: \(dynamicInfo.videoBitrate)")
// Metadata
if let title = dynamicInfo.metadata["title"] {
print("Title: \(title)")
}
}
```
### Chapters
```swift
let chapters = playerLayer.player.chapters
for chapter in chapters {
print("\(chapter.title): \(chapter.start) - \(chapter.end)")
}
```
### Thumbnails
```swift
Task {
if let thumbnail = await playerLayer.player.thumbnailImageAtCurrentTime() {
let image = UIImage(cgImage: thumbnail)
// Use thumbnail
}
}
```
## KSPlayerLayerDelegate
Implement the delegate to receive events:
```swift
extension MyViewController: KSPlayerLayerDelegate {
func player(layer: KSPlayerLayer, state: KSPlayerState) {
switch state {
case .initialized:
print("Player initialized")
case .preparing:
print("Preparing...")
case .readyToPlay:
print("Ready - Duration: \(layer.player.duration)")
case .buffering:
print("Buffering...")
case .bufferFinished:
print("Playing")
case .paused:
print("Paused")
case .playedToTheEnd:
print("Finished")
case .error:
print("Error occurred")
}
}
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
let progress = totalTime > 0 ? currentTime / totalTime : 0
print("Progress: \(Int(progress * 100))%")
}
func player(layer: KSPlayerLayer, finish error: Error?) {
if let error = error {
print("Playback error: \(error.localizedDescription)")
} else {
print("Playback completed successfully")
}
}
func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval) {
// bufferedCount: 0 = initial load
// consumeTime: time spent buffering
print("Buffer #\(bufferedCount), took \(consumeTime)s")
}
}
```
## Player View Integration
The player's view can be added to your view hierarchy:
```swift
if let playerView = playerLayer.player.view {
containerView.addSubview(playerView)
playerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
playerView.topAnchor.constraint(equalTo: containerView.topAnchor),
playerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
playerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
}
```
## Remote Control
Remote control is automatically registered when `options.registerRemoteControll` is `true` (default).
### Supported Commands
- Play/Pause
- Stop
- Next/Previous track (for playlists)
- Skip forward/backward (15 seconds)
- Change playback position
- Change playback rate
- Change repeat mode
- Language/audio track selection
### Customizing Remote Control
```swift
// Disable auto-registration
options.registerRemoteControll = false
// Manually register later
playerLayer.registerRemoteControllEvent()
```
### Now Playing Info
```swift
import MediaPlayer
// Set custom Now Playing info
MPNowPlayingInfoCenter.default().nowPlayingInfo = [
MPMediaItemPropertyTitle: "Video Title",
MPMediaItemPropertyArtist: "Artist Name",
MPMediaItemPropertyPlaybackDuration: playerLayer.player.duration
]
```
## Background/Foreground Handling
KSPlayerLayer automatically handles app lifecycle:
- **Background**: Pauses video (unless `KSOptions.canBackgroundPlay` is `true`)
- **Foreground**: Resumes display
```swift
// Enable background playback
KSOptions.canBackgroundPlay = true
```
## Player Type Switching
The player automatically switches from `firstPlayerType` to `secondPlayerType` on failure:
```swift
// Configure player types before creating KSPlayerLayer
KSOptions.firstPlayerType = KSAVPlayer.self
KSOptions.secondPlayerType = KSMEPlayer.self
```
## Complete Example
```swift
class VideoPlayerController: UIViewController, KSPlayerLayerDelegate {
private var playerLayer: KSPlayerLayer!
private var containerView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
containerView = UIView()
view.addSubview(containerView)
containerView.frame = view.bounds
let url = URL(string: "https://example.com/video.mp4")!
let options = KSOptions()
options.isLoopPlay = true
playerLayer = KSPlayerLayer(
url: url,
options: options,
delegate: self
)
if let playerView = playerLayer.player.view {
containerView.addSubview(playerView)
playerView.frame = containerView.bounds
playerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
}
}
// MARK: - KSPlayerLayerDelegate
func player(layer: KSPlayerLayer, state: KSPlayerState) {
print("State: \(state)")
}
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {
// Update progress UI
}
func player(layer: KSPlayerLayer, finish error: Error?) {
if let error = error {
showError(error)
}
}
func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval) {
if bufferedCount == 0 {
print("Initial load took \(consumeTime)s")
}
}
deinit {
playerLayer.stop()
}
}
```

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

View File

@@ -0,0 +1,426 @@
# SwiftUI Usage
KSPlayer provides full SwiftUI support with `KSVideoPlayer` (a UIViewRepresentable) and `KSVideoPlayerView` (a complete player view with controls).
**Minimum Requirements:** iOS 16.0, macOS 13.0, tvOS 16.0
## KSVideoPlayerView
`KSVideoPlayerView` is a complete video player with built-in controls, subtitle display, and settings.
### Basic Usage
```swift
import KSPlayer
import SwiftUI
struct VideoScreen: View {
let url = URL(string: "https://example.com/video.mp4")!
var body: some View {
KSVideoPlayerView(url: url, options: KSOptions())
}
}
```
### With Custom Title
```swift
KSVideoPlayerView(
url: url,
options: KSOptions(),
title: "My Video Title"
)
```
### With Coordinator and Subtitle Data Source
```swift
struct VideoScreen: View {
@StateObject private var coordinator = KSVideoPlayer.Coordinator()
let url: URL
let subtitleDataSource: SubtitleDataSouce?
var body: some View {
KSVideoPlayerView(
coordinator: coordinator,
url: url,
options: KSOptions(),
title: "Video Title",
subtitleDataSouce: subtitleDataSource
)
}
}
```
## KSVideoPlayer
`KSVideoPlayer` is the lower-level UIViewRepresentable that provides the video rendering surface. Use this when you want full control over the UI.
### Basic Usage
```swift
import KSPlayer
import SwiftUI
struct CustomPlayerView: View {
@StateObject private var coordinator = KSVideoPlayer.Coordinator()
let url: URL
let options: KSOptions
var body: some View {
KSVideoPlayer(coordinator: coordinator, url: url, options: options)
.onStateChanged { layer, state in
print("State changed: \(state)")
}
.onPlay { currentTime, totalTime in
print("Playing: \(currentTime)/\(totalTime)")
}
.onFinish { layer, error in
if let error = error {
print("Error: \(error)")
}
}
}
}
```
### Initializer
```swift
public struct KSVideoPlayer {
public init(
coordinator: Coordinator,
url: URL,
options: KSOptions
)
}
```
## KSVideoPlayer.Coordinator
The Coordinator manages player state and provides bindings for SwiftUI views.
### Creating a Coordinator
```swift
@StateObject private var coordinator = KSVideoPlayer.Coordinator()
```
### Published Properties
```swift
@MainActor
public final class Coordinator: ObservableObject {
// Playback state (read-only computed property)
public var state: KSPlayerState { get }
// Mute control
@Published public var isMuted: Bool = false
// Volume (0.0 to 1.0)
@Published public var playbackVolume: Float = 1.0
// Content mode toggle
@Published public var isScaleAspectFill: Bool = false
// Playback rate (1.0 = normal)
@Published public var playbackRate: Float = 1.0
// Controls visibility
@Published public var isMaskShow: Bool = true
// Subtitle model
public var subtitleModel: SubtitleModel
// Time model for progress display
public var timemodel: ControllerTimeModel
// The underlying player layer
public var playerLayer: KSPlayerLayer?
}
```
### Coordinator Methods
```swift
// Skip forward/backward by seconds
public func skip(interval: Int)
// Seek to specific time
public func seek(time: TimeInterval)
// Show/hide controls with optional auto-hide
public func mask(show: Bool, autoHide: Bool = true)
// Reset player state (called automatically on view dismissal)
public func resetPlayer()
```
### Using Coordinator for Playback Control
```swift
struct PlayerView: View {
@StateObject private var coordinator = KSVideoPlayer.Coordinator()
let url: URL
var body: some View {
VStack {
KSVideoPlayer(coordinator: coordinator, url: url, options: KSOptions())
HStack {
Button("Play") {
coordinator.playerLayer?.play()
}
Button("Pause") {
coordinator.playerLayer?.pause()
}
Button("-15s") {
coordinator.skip(interval: -15)
}
Button("+15s") {
coordinator.skip(interval: 15)
}
}
Slider(value: $coordinator.playbackVolume, in: 0...1)
Toggle("Mute", isOn: $coordinator.isMuted)
}
}
}
```
## View Modifiers
### onStateChanged
Called when playback state changes:
```swift
KSVideoPlayer(coordinator: coordinator, url: url, options: options)
.onStateChanged { layer, state in
switch state {
case .initialized: break
case .preparing: break
case .readyToPlay:
// Access metadata
if let title = layer.player.dynamicInfo?.metadata["title"] {
print("Title: \(title)")
}
case .buffering: break
case .bufferFinished: break
case .paused: break
case .playedToTheEnd: break
case .error: break
}
}
```
### onPlay
Called periodically during playback with current and total time:
```swift
.onPlay { currentTime, totalTime in
let progress = currentTime / totalTime
print("Progress: \(Int(progress * 100))%")
}
```
### onFinish
Called when playback ends (naturally or with error):
```swift
.onFinish { layer, error in
if let error = error {
print("Playback failed: \(error.localizedDescription)")
} else {
print("Playback completed")
}
}
```
### onBufferChanged
Called when buffering status changes:
```swift
.onBufferChanged { bufferedCount, consumeTime in
// bufferedCount: 0 = initial loading
print("Buffer count: \(bufferedCount), time: \(consumeTime)")
}
```
### onSwipe (iOS only)
Called on swipe gestures:
```swift
#if canImport(UIKit)
.onSwipe { direction in
switch direction {
case .up: print("Swipe up")
case .down: print("Swipe down")
case .left: print("Swipe left")
case .right: print("Swipe right")
default: break
}
}
#endif
```
## ControllerTimeModel
Used for displaying playback time:
```swift
public class ControllerTimeModel: ObservableObject {
@Published public var currentTime: Int = 0
@Published public var totalTime: Int = 1
}
```
Usage:
```swift
struct TimeDisplay: View {
@ObservedObject var timeModel: ControllerTimeModel
var body: some View {
Text("\(timeModel.currentTime) / \(timeModel.totalTime)")
}
}
// In your player view:
TimeDisplay(timeModel: coordinator.timemodel)
```
## Subtitle Integration
Access subtitles through the coordinator:
```swift
struct SubtitlePicker: View {
@ObservedObject var subtitleModel: SubtitleModel
var body: some View {
Picker("Subtitle", selection: $subtitleModel.selectedSubtitleInfo) {
Text("Off").tag(nil as (any SubtitleInfo)?)
ForEach(subtitleModel.subtitleInfos, id: \.subtitleID) { info in
Text(info.name).tag(info as (any SubtitleInfo)?)
}
}
}
}
// Usage:
SubtitlePicker(subtitleModel: coordinator.subtitleModel)
```
## Complete Example
```swift
import KSPlayer
import SwiftUI
@available(iOS 16.0, *)
struct FullPlayerView: View {
@StateObject private var coordinator = KSVideoPlayer.Coordinator()
@State private var url: URL
@State private var title: String
@Environment(\.dismiss) private var dismiss
init(url: URL, title: String) {
_url = State(initialValue: url)
_title = State(initialValue: title)
}
var body: some View {
ZStack {
KSVideoPlayer(coordinator: coordinator, url: url, options: KSOptions())
.onStateChanged { layer, state in
if state == .readyToPlay {
if let movieTitle = layer.player.dynamicInfo?.metadata["title"] {
title = movieTitle
}
}
}
.onFinish { _, error in
if error != nil {
dismiss()
}
}
.ignoresSafeArea()
.onTapGesture {
coordinator.isMaskShow.toggle()
}
// Custom controls overlay
if coordinator.isMaskShow {
VStack {
HStack {
Button("Back") { dismiss() }
Spacer()
Text(title)
}
.padding()
Spacer()
HStack(spacing: 40) {
Button(action: { coordinator.skip(interval: -15) }) {
Image(systemName: "gobackward.15")
}
Button(action: {
if coordinator.state.isPlaying {
coordinator.playerLayer?.pause()
} else {
coordinator.playerLayer?.play()
}
}) {
Image(systemName: coordinator.state.isPlaying ? "pause.fill" : "play.fill")
}
Button(action: { coordinator.skip(interval: 15) }) {
Image(systemName: "goforward.15")
}
}
.font(.largeTitle)
Spacer()
}
.foregroundColor(.white)
}
}
.preferredColorScheme(.dark)
}
}
```
## URL Change Handling
The player automatically detects URL changes:
```swift
struct DynamicPlayerView: View {
@StateObject private var coordinator = KSVideoPlayer.Coordinator()
@State private var currentURL: URL
var body: some View {
VStack {
KSVideoPlayer(coordinator: coordinator, url: currentURL, options: KSOptions())
Button("Load Next Video") {
currentURL = URL(string: "https://example.com/next-video.mp4")!
}
}
}
}
```

View File

@@ -0,0 +1,473 @@
# Track Management
KSPlayer provides APIs for managing audio, video, and subtitle tracks within media files.
## Overview
Tracks represent individual streams within a media container (video tracks, audio tracks, subtitle tracks). You can:
- Query available tracks
- Get track metadata
- Select/enable specific tracks
## Getting Tracks
### From MediaPlayerProtocol
```swift
// Get audio tracks
let audioTracks = player.tracks(mediaType: .audio)
// Get video tracks
let videoTracks = player.tracks(mediaType: .video)
// Get subtitle tracks
let subtitleTracks = player.tracks(mediaType: .subtitle)
```
### From KSPlayerLayer
```swift
if let player = playerLayer.player {
let audioTracks = player.tracks(mediaType: .audio)
// ...
}
```
### From VideoPlayerView
```swift
if let player = playerView.playerLayer?.player {
let tracks = player.tracks(mediaType: .audio)
// ...
}
```
### From SwiftUI Coordinator
```swift
let audioTracks = coordinator.playerLayer?.player.tracks(mediaType: .audio) ?? []
```
## MediaPlayerTrack Protocol
All tracks conform to `MediaPlayerTrack`:
```swift
public protocol MediaPlayerTrack: AnyObject, CustomStringConvertible {
var trackID: Int32 { get }
var name: String { get }
var languageCode: String? { get }
var mediaType: AVFoundation.AVMediaType { get }
var nominalFrameRate: Float { get set }
var bitRate: Int64 { get }
var bitDepth: Int32 { get }
var isEnabled: Bool { get set }
var isImageSubtitle: Bool { get }
var rotation: Int16 { get }
var dovi: DOVIDecoderConfigurationRecord? { get }
var fieldOrder: FFmpegFieldOrder { get }
var formatDescription: CMFormatDescription? { get }
}
```
## Track Properties
### Basic Properties
| Property | Type | Description |
|----------|------|-------------|
| `trackID` | `Int32` | Unique track identifier |
| `name` | `String` | Track name (often empty) |
| `languageCode` | `String?` | ISO 639-1/639-2 language code |
| `mediaType` | `AVMediaType` | `.audio`, `.video`, or `.subtitle` |
| `isEnabled` | `Bool` | Whether track is currently active |
### Audio Properties
| Property | Type | Description |
|----------|------|-------------|
| `bitRate` | `Int64` | Audio bitrate in bps |
| `audioStreamBasicDescription` | `AudioStreamBasicDescription?` | Core Audio format info |
### Video Properties
| Property | Type | Description |
|----------|------|-------------|
| `naturalSize` | `CGSize` | Video dimensions |
| `nominalFrameRate` | `Float` | Frame rate |
| `bitRate` | `Int64` | Video bitrate in bps |
| `bitDepth` | `Int32` | Color depth (8, 10, 12) |
| `rotation` | `Int16` | Rotation in degrees |
| `fieldOrder` | `FFmpegFieldOrder` | Interlacing type |
| `dynamicRange` | `DynamicRange?` | SDR/HDR/Dolby Vision |
| `dovi` | `DOVIDecoderConfigurationRecord?` | Dolby Vision config |
### Color Properties
| Property | Type | Description |
|----------|------|-------------|
| `colorPrimaries` | `String?` | Color primaries (e.g., "ITU_R_709_2") |
| `transferFunction` | `String?` | Transfer function |
| `yCbCrMatrix` | `String?` | YCbCr matrix |
| `colorSpace` | `CGColorSpace?` | Computed color space |
### Subtitle Properties
| Property | Type | Description |
|----------|------|-------------|
| `isImageSubtitle` | `Bool` | True for bitmap subtitles (SUP, VobSub) |
### Computed Properties
```swift
extension MediaPlayerTrack {
// Localized language name
var language: String? {
languageCode.flatMap { Locale.current.localizedString(forLanguageCode: $0) }
}
// FourCC codec type
var codecType: FourCharCode
// Video format subtype
var mediaSubType: CMFormatDescription.MediaSubType
}
```
## Selecting Tracks
### Select a Track
```swift
// Find English audio track
if let englishTrack = audioTracks.first(where: { $0.languageCode == "en" }) {
player.select(track: englishTrack)
}
```
### Select Track by Index
```swift
let audioTracks = player.tracks(mediaType: .audio)
if audioTracks.count > 1 {
player.select(track: audioTracks[1])
}
```
### Check Currently Selected Track
```swift
let currentAudio = audioTracks.first(where: { $0.isEnabled })
print("Current audio: \(currentAudio?.name ?? "none")")
```
## Track Selection Examples
### Audio Track Selection
```swift
func selectAudioTrack(languageCode: String) {
let audioTracks = player.tracks(mediaType: .audio)
if let track = audioTracks.first(where: { $0.languageCode == languageCode }) {
player.select(track: track)
print("Selected: \(track.language ?? track.name)")
}
}
// Usage
selectAudioTrack(languageCode: "en") // English
selectAudioTrack(languageCode: "es") // Spanish
selectAudioTrack(languageCode: "ja") // Japanese
```
### Video Track Selection (Multi-angle/quality)
```swift
func selectVideoTrack(preferredBitrate: Int64) {
let videoTracks = player.tracks(mediaType: .video)
// Find closest bitrate
let sorted = videoTracks.sorted {
abs($0.bitRate - preferredBitrate) < abs($1.bitRate - preferredBitrate)
}
if let track = sorted.first {
player.select(track: track)
print("Selected video: \(track.naturalSize.width)x\(track.naturalSize.height)")
}
}
```
### HDR Track Selection
```swift
func selectHDRTrack() {
let videoTracks = player.tracks(mediaType: .video)
// Prefer Dolby Vision, then HDR10, then SDR
let preferredOrder: [DynamicRange] = [.dolbyVision, .hdr10, .hlg, .sdr]
for range in preferredOrder {
if let track = videoTracks.first(where: { $0.dynamicRange == range }) {
player.select(track: track)
print("Selected: \(range)")
return
}
}
}
```
## UIKit Track Selection UI
### Using UIAlertController
```swift
func showAudioTrackPicker() {
guard let player = playerLayer?.player else { return }
let audioTracks = player.tracks(mediaType: .audio)
guard !audioTracks.isEmpty else { return }
let alert = UIAlertController(
title: "Select Audio Track",
message: nil,
preferredStyle: .actionSheet
)
for track in audioTracks {
let title = track.language ?? track.name
let action = UIAlertAction(title: title, style: .default) { _ in
player.select(track: track)
}
if track.isEnabled {
action.setValue(true, forKey: "checked")
}
alert.addAction(action)
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
```
### Track Info Display
```swift
func displayTrackInfo() {
guard let player = playerLayer?.player else { return }
// Video info
if let videoTrack = player.tracks(mediaType: .video).first(where: { $0.isEnabled }) {
print("Video: \(videoTrack.naturalSize.width)x\(videoTrack.naturalSize.height)")
print("FPS: \(videoTrack.nominalFrameRate)")
print("Bitrate: \(videoTrack.bitRate / 1000) kbps")
print("HDR: \(videoTrack.dynamicRange?.description ?? "SDR")")
}
// Audio info
if let audioTrack = player.tracks(mediaType: .audio).first(where: { $0.isEnabled }) {
print("Audio: \(audioTrack.language ?? "Unknown")")
print("Bitrate: \(audioTrack.bitRate / 1000) kbps")
}
}
```
## SwiftUI Track Selection
### Audio Track Picker
```swift
struct AudioTrackPicker: View {
let player: MediaPlayerProtocol?
var audioTracks: [MediaPlayerTrack] {
player?.tracks(mediaType: .audio) ?? []
}
var body: some View {
Menu {
ForEach(audioTracks, id: \.trackID) { track in
Button {
player?.select(track: track)
} label: {
HStack {
Text(track.language ?? track.name)
if track.isEnabled {
Image(systemName: "checkmark")
}
}
}
}
} label: {
Image(systemName: "waveform.circle.fill")
}
}
}
```
### Video Track Picker
```swift
struct VideoTrackPicker: View {
let player: MediaPlayerProtocol?
var videoTracks: [MediaPlayerTrack] {
player?.tracks(mediaType: .video) ?? []
}
var body: some View {
Picker("Video", selection: Binding(
get: { videoTracks.first(where: { $0.isEnabled })?.trackID },
set: { newValue in
if let track = videoTracks.first(where: { $0.trackID == newValue }) {
player?.select(track: track)
}
}
)) {
ForEach(videoTracks, id: \.trackID) { track in
Text("\(Int(track.naturalSize.width))x\(Int(track.naturalSize.height))")
.tag(track.trackID as Int32?)
}
}
}
}
```
## Automatic Track Selection
Configure `KSOptions` for automatic track selection:
```swift
class CustomOptions: KSOptions {
// Prefer English audio
override func wantedAudio(tracks: [MediaPlayerTrack]) -> Int? {
if let index = tracks.firstIndex(where: { $0.languageCode == "en" }) {
return index
}
return nil // Use default selection
}
// Prefer highest quality video
override func wantedVideo(tracks: [MediaPlayerTrack]) -> Int? {
if let index = tracks.enumerated().max(by: { $0.element.bitRate < $1.element.bitRate })?.offset {
return index
}
return nil
}
}
```
## Track Events
### Detecting Track Changes
```swift
func player(layer: KSPlayerLayer, state: KSPlayerState) {
if state == .readyToPlay {
let player = layer.player
// Log available tracks
print("Audio tracks: \(player.tracks(mediaType: .audio).count)")
print("Video tracks: \(player.tracks(mediaType: .video).count)")
print("Subtitle tracks: \(player.tracks(mediaType: .subtitle).count)")
// Get current selections
let currentAudio = player.tracks(mediaType: .audio).first(where: { $0.isEnabled })
let currentVideo = player.tracks(mediaType: .video).first(where: { $0.isEnabled })
print("Current audio: \(currentAudio?.language ?? "unknown")")
print("Current video: \(currentVideo?.naturalSize ?? .zero)")
}
}
```
## Complete Example
```swift
class TrackSelectionController: UIViewController, KSPlayerLayerDelegate {
private var playerLayer: KSPlayerLayer!
private var audioButton: UIButton!
private var videoInfoLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
let url = URL(string: "https://example.com/multi-track-video.mkv")!
playerLayer = KSPlayerLayer(url: url, options: KSOptions(), delegate: self)
}
func player(layer: KSPlayerLayer, state: KSPlayerState) {
if state == .readyToPlay {
updateTrackUI()
}
}
private func updateTrackUI() {
guard let player = playerLayer?.player else { return }
// Update video info
if let video = player.tracks(mediaType: .video).first(where: { $0.isEnabled }) {
videoInfoLabel.text = """
\(Int(video.naturalSize.width))x\(Int(video.naturalSize.height)) @ \(Int(video.nominalFrameRate))fps
\(video.dynamicRange?.description ?? "SDR")
"""
}
// Update audio button
let audioCount = player.tracks(mediaType: .audio).count
audioButton.isHidden = audioCount < 2
if let audio = player.tracks(mediaType: .audio).first(where: { $0.isEnabled }) {
audioButton.setTitle(audio.language ?? "Audio", for: .normal)
}
}
@objc private func audioButtonTapped() {
guard let player = playerLayer?.player else { return }
let tracks = player.tracks(mediaType: .audio)
let alert = UIAlertController(title: "Audio Track", message: nil, preferredStyle: .actionSheet)
for track in tracks {
let title = [track.language, track.name]
.compactMap { $0 }
.joined(separator: " - ")
let action = UIAlertAction(title: title.isEmpty ? "Track \(track.trackID)" : title, style: .default) { [weak self] _ in
player.select(track: track)
self?.updateTrackUI()
}
if track.isEnabled {
action.setValue(true, forKey: "checked")
alert.preferredAction = action
}
alert.addAction(action)
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
present(alert, animated: true)
}
private func setupUI() {
audioButton = UIButton(type: .system)
audioButton.addTarget(self, action: #selector(audioButtonTapped), for: .touchUpInside)
view.addSubview(audioButton)
videoInfoLabel = UILabel()
videoInfoLabel.numberOfLines = 0
view.addSubview(videoInfoLabel)
}
// Delegate methods...
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval) {}
func player(layer: KSPlayerLayer, finish error: Error?) {}
func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval) {}
}
```

View File

@@ -0,0 +1,543 @@
# Types and Protocols
This reference documents all core types, protocols, and enums in KSPlayer.
## Protocols
### MediaPlayerProtocol
The main protocol that player implementations (`KSAVPlayer`, `KSMEPlayer`) must conform to.
```swift
public protocol MediaPlayerProtocol: MediaPlayback {
var delegate: MediaPlayerDelegate? { get set }
var view: UIView? { get }
var playableTime: TimeInterval { get }
var isReadyToPlay: Bool { get }
var playbackState: MediaPlaybackState { get }
var loadState: MediaLoadState { get }
var isPlaying: Bool { get }
var seekable: Bool { get }
var isMuted: Bool { get set }
var allowsExternalPlayback: Bool { get set }
var usesExternalPlaybackWhileExternalScreenIsActive: Bool { get set }
var isExternalPlaybackActive: Bool { get }
var playbackRate: Float { get set }
var playbackVolume: Float { get set }
var contentMode: UIViewContentMode { get set }
var subtitleDataSouce: SubtitleDataSouce? { get }
var dynamicInfo: DynamicInfo? { get }
@available(macOS 12.0, iOS 15.0, tvOS 15.0, *)
var playbackCoordinator: AVPlaybackCoordinator { get }
@available(tvOS 14.0, *)
var pipController: KSPictureInPictureController? { get }
init(url: URL, options: KSOptions)
func replace(url: URL, options: KSOptions)
func play()
func pause()
func enterBackground()
func enterForeground()
func thumbnailImageAtCurrentTime() async -> CGImage?
func tracks(mediaType: AVFoundation.AVMediaType) -> [MediaPlayerTrack]
func select(track: some MediaPlayerTrack)
}
```
### MediaPlayback
Base protocol for playback functionality:
```swift
public protocol MediaPlayback: AnyObject {
var duration: TimeInterval { get }
var fileSize: Double { get }
var naturalSize: CGSize { get }
var chapters: [Chapter] { get }
var currentPlaybackTime: TimeInterval { get }
func prepareToPlay()
func shutdown()
func seek(time: TimeInterval, completion: @escaping ((Bool) -> Void))
}
```
### MediaPlayerDelegate
Delegate for receiving player events:
```swift
@MainActor
public protocol MediaPlayerDelegate: AnyObject {
func readyToPlay(player: some MediaPlayerProtocol)
func changeLoadState(player: some MediaPlayerProtocol)
func changeBuffering(player: some MediaPlayerProtocol, progress: Int)
func playBack(player: some MediaPlayerProtocol, loopCount: Int)
func finish(player: some MediaPlayerProtocol, error: Error?)
}
```
### MediaPlayerTrack
Protocol for audio/video/subtitle track information:
```swift
public protocol MediaPlayerTrack: AnyObject, CustomStringConvertible {
var trackID: Int32 { get }
var name: String { get }
var languageCode: String? { get }
var mediaType: AVFoundation.AVMediaType { get }
var nominalFrameRate: Float { get set }
var bitRate: Int64 { get }
var bitDepth: Int32 { get }
var isEnabled: Bool { get set }
var isImageSubtitle: Bool { get }
var rotation: Int16 { get }
var dovi: DOVIDecoderConfigurationRecord? { get }
var fieldOrder: FFmpegFieldOrder { get }
var formatDescription: CMFormatDescription? { get }
}
```
#### Extension Properties
```swift
extension MediaPlayerTrack {
var language: String? // Localized language name
var codecType: FourCharCode
var dynamicRange: DynamicRange?
var colorSpace: CGColorSpace?
var mediaSubType: CMFormatDescription.MediaSubType
var audioStreamBasicDescription: AudioStreamBasicDescription?
var naturalSize: CGSize
var colorPrimaries: String?
var transferFunction: String?
var yCbCrMatrix: String?
}
```
### KSPlayerLayerDelegate
Delegate for `KSPlayerLayer` events:
```swift
@MainActor
public protocol KSPlayerLayerDelegate: AnyObject {
func player(layer: KSPlayerLayer, state: KSPlayerState)
func player(layer: KSPlayerLayer, currentTime: TimeInterval, totalTime: TimeInterval)
func player(layer: KSPlayerLayer, finish error: Error?)
func player(layer: KSPlayerLayer, bufferedCount: Int, consumeTime: TimeInterval)
}
```
### PlayerControllerDelegate
Delegate for `PlayerView` events:
```swift
public protocol PlayerControllerDelegate: AnyObject {
func playerController(state: KSPlayerState)
func playerController(currentTime: TimeInterval, totalTime: TimeInterval)
func playerController(finish error: Error?)
func playerController(maskShow: Bool)
func playerController(action: PlayerButtonType)
func playerController(bufferedCount: Int, consumeTime: TimeInterval)
func playerController(seek: TimeInterval)
}
```
### CapacityProtocol
Buffer capacity information:
```swift
public protocol CapacityProtocol {
var fps: Float { get }
var packetCount: Int { get }
var frameCount: Int { get }
var frameMaxCount: Int { get }
var isEndOfFile: Bool { get }
var mediaType: AVFoundation.AVMediaType { get }
}
```
## Enums
### KSPlayerState
Player state enumeration:
```swift
public enum KSPlayerState: CustomStringConvertible {
case initialized // Player created
case preparing // Loading media
case readyToPlay // Ready to start playback
case buffering // Buffering data
case bufferFinished // Buffer sufficient, playing
case paused // Playback paused
case playedToTheEnd // Reached end of media
case error // Error occurred
public var isPlaying: Bool // true for .buffering or .bufferFinished
}
```
### MediaPlaybackState
Low-level playback state:
```swift
public enum MediaPlaybackState: Int {
case idle
case playing
case paused
case seeking
case finished
case stopped
}
```
### MediaLoadState
Loading state:
```swift
public enum MediaLoadState: Int {
case idle
case loading
case playable
}
```
### DynamicRange
HDR/SDR content range:
```swift
public enum DynamicRange: Int32 {
case sdr = 0
case hdr10 = 2
case hlg = 3
case dolbyVision = 5
static var availableHDRModes: [DynamicRange] // Device-supported modes
}
```
### DisplayEnum
Video display mode:
```swift
@MainActor
public enum DisplayEnum {
case plane // Normal 2D display
case vr // VR mode (spherical)
case vrBox // VR Box mode (side-by-side)
}
```
### FFmpegFieldOrder
Video interlacing:
```swift
public enum FFmpegFieldOrder: UInt8 {
case unknown = 0
case progressive
case tt // Top coded first, top displayed first
case bb // Bottom coded first, bottom displayed first
case tb // Top coded first, bottom displayed first
case bt // Bottom coded first, top displayed first
}
```
### VideoInterlacingType
Detected interlacing type:
```swift
public enum VideoInterlacingType: String {
case tff // Top field first
case bff // Bottom field first
case progressive // Progressive scan
case undetermined
}
```
### ClockProcessType
Internal clock synchronization:
```swift
public enum ClockProcessType {
case remain
case next
case dropNextFrame
case dropNextPacket
case dropGOPPacket
case flush
case seek
}
```
### PlayerButtonType
UI button types:
```swift
public enum PlayerButtonType: Int {
case play = 101
case pause
case back
case srt // Subtitles
case landscape // Fullscreen
case replay
case lock
case rate // Playback speed
case definition // Quality
case pictureInPicture
case audioSwitch
case videoSwitch
}
```
### KSPlayerTopBarShowCase
Top bar visibility:
```swift
public enum KSPlayerTopBarShowCase {
case always // Always show
case horizantalOnly // Only in landscape
case none // Never show
}
```
### KSPanDirection
Gesture direction:
```swift
public enum KSPanDirection {
case horizontal
case vertical
}
```
### TimeType
Time formatting:
```swift
public enum TimeType {
case min // MM:SS
case hour // H:MM:SS
case minOrHour // MM:SS or H:MM:SS based on duration
case millisecond // HH:MM:SS.ms
}
```
### LogLevel
Logging levels:
```swift
public enum LogLevel: Int32 {
case panic = 0
case fatal = 8
case error = 16
case warning = 24
case info = 32
case verbose = 40
case debug = 48
case trace = 56
}
```
## Structs
### Chapter
Video chapter information:
```swift
public struct Chapter {
public let start: TimeInterval
public let end: TimeInterval
public let title: String
}
```
### LoadingState
Buffer loading state:
```swift
public struct LoadingState {
public let loadedTime: TimeInterval
public let progress: TimeInterval
public let packetCount: Int
public let frameCount: Int
public let isEndOfFile: Bool
public let isPlayable: Bool
public let isFirst: Bool
public let isSeek: Bool
}
```
### VideoAdaptationState
Adaptive bitrate state:
```swift
public struct VideoAdaptationState {
public struct BitRateState {
let bitRate: Int64
let time: TimeInterval
}
public let bitRates: [Int64]
public let duration: TimeInterval
public internal(set) var fps: Float
public internal(set) var bitRateStates: [BitRateState]
public internal(set) var currentPlaybackTime: TimeInterval
public internal(set) var isPlayable: Bool
public internal(set) var loadedCount: Int
}
```
### DOVIDecoderConfigurationRecord
Dolby Vision configuration:
```swift
public struct DOVIDecoderConfigurationRecord {
public let dv_version_major: UInt8
public let dv_version_minor: UInt8
public let dv_profile: UInt8
public let dv_level: UInt8
public let rpu_present_flag: UInt8
public let el_present_flag: UInt8
public let bl_present_flag: UInt8
public let dv_bl_signal_compatibility_id: UInt8
}
```
### KSClock
Internal clock for A/V sync:
```swift
public struct KSClock {
public private(set) var lastMediaTime: CFTimeInterval
public internal(set) var position: Int64
public internal(set) var time: CMTime
func getTime() -> TimeInterval
}
```
### 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
public var edgeInsets: EdgeInsets { get }
}
```
## Classes
### DynamicInfo
Runtime playback information:
```swift
public class DynamicInfo: ObservableObject {
public var metadata: [String: String] // Media metadata
public var bytesRead: Int64 // Bytes transferred
public var audioBitrate: Int // Current audio bitrate
public var videoBitrate: Int // Current video bitrate
@Published
public var displayFPS: Double = 0.0 // Current display FPS
public var audioVideoSyncDiff: Double = 0.0 // A/V sync difference
public var droppedVideoFrameCount: UInt32 // Dropped frames
public var droppedVideoPacketCount: UInt32 // Dropped packets
}
```
## Error Handling
### KSPlayerErrorCode
```swift
public enum KSPlayerErrorCode: Int {
case unknown
case formatCreate
case formatOpenInput
case formatOutputCreate
case formatWriteHeader
case formatFindStreamInfo
case readFrame
case codecContextCreate
case codecContextSetParam
case codecContextFindDecoder
case codesContextOpen
case codecVideoSendPacket
case codecAudioSendPacket
case codecVideoReceiveFrame
case codecAudioReceiveFrame
case auidoSwrInit
case codecSubtitleSendPacket
case videoTracksUnplayable
case subtitleUnEncoding
case subtitleUnParse
case subtitleFormatUnSupport
case subtitleParamsEmpty
}
```
### Error Domain
```swift
public let KSPlayerErrorDomain = "KSPlayerErrorDomain"
```
## Extensions
### TimeInterval Formatting
```swift
extension TimeInterval {
func toString(for type: TimeType) -> String
}
// Example:
let time: TimeInterval = 3661 // 1 hour, 1 minute, 1 second
time.toString(for: .min) // "61:01"
time.toString(for: .hour) // "1:01:01"
time.toString(for: .minOrHour) // "1:01:01"
```
### Int Formatting
```swift
extension Int {
func toString(for type: TimeType) -> String
}
extension FixedWidthInteger {
var kmFormatted: String // "1.5K", "2.3M", etc.
}
```

View File

@@ -0,0 +1,337 @@
# UIKit Usage
This guide covers using KSPlayer with UIKit in iOS applications.
## IOSVideoPlayerView
`IOSVideoPlayerView` is the main UIKit video player view for iOS. It extends `VideoPlayerView` with iOS-specific features like fullscreen, gestures, and AirPlay.
### Basic Setup
```swift
import KSPlayer
class VideoViewController: UIViewController {
private var playerView: IOSVideoPlayerView!
override func viewDidLoad() {
super.viewDidLoad()
playerView = IOSVideoPlayerView()
view.addSubview(playerView)
playerView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
playerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
}
```
### Setting Video URL
#### Simple URL
```swift
let url = URL(string: "https://example.com/video.mp4")!
playerView.set(url: url, options: KSOptions())
```
#### With KSPlayerResource
```swift
let resource = KSPlayerResource(
url: URL(string: "https://example.com/video.mp4")!,
options: KSOptions(),
name: "Video Title",
cover: URL(string: "https://example.com/cover.jpg"),
subtitleURLs: [URL(string: "https://example.com/subtitle.srt")!]
)
playerView.set(resource: resource)
```
#### Multiple Definitions (Quality Options)
```swift
let hdDefinition = KSPlayerResourceDefinition(
url: URL(string: "https://example.com/video_hd.mp4")!,
definition: "1080p",
options: KSOptions()
)
let sdDefinition = KSPlayerResourceDefinition(
url: URL(string: "https://example.com/video_sd.mp4")!,
definition: "480p",
options: KSOptions()
)
let resource = KSPlayerResource(
name: "Video Title",
definitions: [hdDefinition, sdDefinition],
cover: URL(string: "https://example.com/cover.jpg")
)
playerView.set(resource: resource, definitionIndex: 0)
```
### KSPlayerResource
```swift
public class KSPlayerResource {
public let name: String
public let definitions: [KSPlayerResourceDefinition]
public let cover: URL?
public let subtitleDataSouce: SubtitleDataSouce?
public var nowPlayingInfo: KSNowPlayableMetadata?
// Convenience initializer for single URL
public convenience init(
url: URL,
options: KSOptions = KSOptions(),
name: String = "",
cover: URL? = nil,
subtitleURLs: [URL]? = nil
)
// Full initializer
public init(
name: String,
definitions: [KSPlayerResourceDefinition],
cover: URL? = nil,
subtitleDataSouce: SubtitleDataSouce? = nil
)
}
```
### KSPlayerResourceDefinition
```swift
public struct KSPlayerResourceDefinition {
public let url: URL
public let definition: String
public let options: KSOptions
public init(url: URL, definition: String, options: KSOptions = KSOptions())
}
```
### PlayerControllerDelegate
Implement `PlayerControllerDelegate` to receive playback events:
```swift
class VideoViewController: UIViewController, PlayerControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
playerView.delegate = self
}
func playerController(state: KSPlayerState) {
switch state {
case .initialized:
print("Player initialized")
case .preparing:
print("Preparing to play")
case .readyToPlay:
print("Ready to play")
case .buffering:
print("Buffering...")
case .bufferFinished:
print("Buffer finished, playing")
case .paused:
print("Paused")
case .playedToTheEnd:
print("Playback completed")
case .error:
print("Error occurred")
}
}
func playerController(currentTime: TimeInterval, totalTime: TimeInterval) {
// Called periodically during playback
print("Progress: \(currentTime)/\(totalTime)")
}
func playerController(finish error: Error?) {
if let error = error {
print("Playback error: \(error)")
} else {
print("Playback finished")
}
}
func playerController(maskShow: Bool) {
// Controls visibility changed
}
func playerController(action: PlayerButtonType) {
// Button pressed
}
func playerController(bufferedCount: Int, consumeTime: TimeInterval) {
// Buffer status update (bufferedCount: 0 = first load)
}
func playerController(seek: TimeInterval) {
// Seek completed
}
}
```
### Playback Control
```swift
// Play
playerView.play()
// Pause
playerView.pause()
// Seek to time
playerView.seek(time: 30.0) { finished in
print("Seek completed: \(finished)")
}
// Reset player
playerView.resetPlayer()
```
### Time Callbacks
```swift
// Listen to time changes
playerView.playTimeDidChange = { currentTime, totalTime in
print("Current: \(currentTime), Total: \(totalTime)")
}
// Back button handler
playerView.backBlock = { [weak self] in
self?.navigationController?.popViewController(animated: true)
}
```
### Fullscreen Control
```swift
// Check if in fullscreen
let isFullscreen = playerView.landscapeButton.isSelected
// Toggle fullscreen
playerView.updateUI(isFullScreen: true) // Enter fullscreen
playerView.updateUI(isFullScreen: false) // Exit fullscreen
```
### Customizing IOSVideoPlayerView
Subclass to customize behavior:
```swift
class CustomVideoPlayerView: IOSVideoPlayerView {
override func customizeUIComponents() {
super.customizeUIComponents()
// Hide playback rate button
toolBar.playbackRateButton.isHidden = true
}
override func onButtonPressed(type: PlayerButtonType, button: UIButton) {
if type == .landscape {
// Custom landscape button behavior
} else {
super.onButtonPressed(type: type, button: button)
}
}
override func updateUI(isLandscape: Bool) {
super.updateUI(isLandscape: isLandscape)
// Additional UI updates for orientation
}
}
```
## VideoPlayerView (Base Class)
`VideoPlayerView` is the base class with playback controls. `IOSVideoPlayerView` extends it for iOS.
### Key Properties
```swift
public var playerLayer: KSPlayerLayer?
public weak var delegate: PlayerControllerDelegate?
public let toolBar: PlayerToolBar
public let srtControl: SubtitleModel
public var playTimeDidChange: ((TimeInterval, TimeInterval) -> Void)?
public var backBlock: (() -> Void)?
```
### Accessing Player Layer
```swift
// Get the underlying player
if let player = playerView.playerLayer?.player {
// Access player properties
let duration = player.duration
let currentTime = player.currentPlaybackTime
let isPlaying = player.isPlaying
}
```
## IOSVideoPlayerView Properties
```swift
// UI Components
public var backButton: UIButton
public var maskImageView: UIImageView // Cover image
public var airplayStatusView: UIView // AirPlay status indicator
public var routeButton: AVRoutePickerView // AirPlay route picker
public var landscapeButton: UIControl // Fullscreen toggle
public var volumeViewSlider: UXSlider // Volume control
// State
public var isMaskShow: Bool // Controls visibility
```
## PlayerButtonType
Button types for `onButtonPressed`:
```swift
public enum PlayerButtonType: Int {
case play = 101
case pause
case back
case srt // Subtitle selection
case landscape // Fullscreen toggle
case replay
case lock // Lock controls
case rate // Playback rate
case definition // Quality selection
case pictureInPicture
case audioSwitch // Audio track
case videoSwitch // Video track
}
```
## Document Picker Integration
`IOSVideoPlayerView` supports opening files via document picker:
```swift
extension IOSVideoPlayerView: UIDocumentPickerDelegate {
public func documentPicker(_ controller: UIDocumentPickerViewController,
didPickDocumentsAt urls: [URL]) {
if let url = urls.first {
if url.isMovie || url.isAudio {
set(url: url, options: KSOptions())
} else {
// Assume subtitle file
srtControl.selectedSubtitleInfo = URLSubtitleInfo(url: url)
}
}
}
}
```