mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
feat: KSPlayer as an option for iOS + other improvements (#1266)
This commit is contained in:
committed by
GitHub
parent
d1795c9df8
commit
74d86b5d12
69435
docs/jellyfin-openapi-stable.json
Normal file
69435
docs/jellyfin-openapi-stable.json
Normal file
File diff suppressed because it is too large
Load Diff
157
docs/ks-player/GettingStarted.md
Normal file
157
docs/ks-player/GettingStarted.md
Normal 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
349
docs/ks-player/KSOptions.md
Normal 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)
|
||||
```
|
||||
|
||||
442
docs/ks-player/KSPlayerLayer.md
Normal file
442
docs/ks-player/KSPlayerLayer.md
Normal 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()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
426
docs/ks-player/SwiftUIUsage.md
Normal file
426
docs/ks-player/SwiftUIUsage.md
Normal 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")!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
473
docs/ks-player/TrackManagement.md
Normal file
473
docs/ks-player/TrackManagement.md
Normal 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) {}
|
||||
}
|
||||
```
|
||||
|
||||
543
docs/ks-player/TypesAndProtocols.md
Normal file
543
docs/ks-player/TypesAndProtocols.md
Normal 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.
|
||||
}
|
||||
```
|
||||
|
||||
337
docs/ks-player/UIKitUsage.md
Normal file
337
docs/ks-player/UIKitUsage.md
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user