mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
13 KiB
13 KiB
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
// 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
if let player = playerLayer.player {
let audioTracks = player.tracks(mediaType: .audio)
// ...
}
From VideoPlayerView
if let player = playerView.playerLayer?.player {
let tracks = player.tracks(mediaType: .audio)
// ...
}
From SwiftUI Coordinator
let audioTracks = coordinator.playerLayer?.player.tracks(mediaType: .audio) ?? []
MediaPlayerTrack Protocol
All tracks conform to MediaPlayerTrack:
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
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
// Find English audio track
if let englishTrack = audioTracks.first(where: { $0.languageCode == "en" }) {
player.select(track: englishTrack)
}
Select Track by Index
let audioTracks = player.tracks(mediaType: .audio)
if audioTracks.count > 1 {
player.select(track: audioTracks[1])
}
Check Currently Selected Track
let currentAudio = audioTracks.first(where: { $0.isEnabled })
print("Current audio: \(currentAudio?.name ?? "none")")
Track Selection Examples
Audio Track Selection
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)
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
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
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
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
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
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:
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
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
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) {}
}