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

10 KiB

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

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

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)

@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:

playerLayer.play()

pause()

Pause playback:

playerLayer.pause()

stop()

Stop playback and reset player state:

playerLayer.stop()

seek(time:autoPlay:completion:)

Seek to a specific time:

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):

playerLayer.prepareToPlay()

URL Management

set(url:options:)

Change the video URL:

let newURL = URL(string: "https://example.com/another-video.mp4")!
playerLayer.set(url: newURL, options: KSOptions())

set(urls:options:)

Set a playlist of URLs:

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:

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

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

// 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)

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

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

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

let chapters = playerLayer.player.chapters
for chapter in chapters {
    print("\(chapter.title): \(chapter.start) - \(chapter.end)")
}

Thumbnails

Task {
    if let thumbnail = await playerLayer.player.thumbnailImageAtCurrentTime() {
        let image = UIImage(cgImage: thumbnail)
        // Use thumbnail
    }
}

KSPlayerLayerDelegate

Implement the delegate to receive events:

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:

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

// Disable auto-registration
options.registerRemoteControll = false

// Manually register later
playerLayer.registerRemoteControllEvent()

Now Playing Info

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
// Enable background playback
KSOptions.canBackgroundPlay = true

Player Type Switching

The player automatically switches from firstPlayerType to secondPlayerType on failure:

// Configure player types before creating KSPlayerLayer
KSOptions.firstPlayerType = KSAVPlayer.self
KSOptions.secondPlayerType = KSMEPlayer.self

Complete Example

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