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