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