113 lines
3.6 KiB
Swift
113 lines
3.6 KiB
Swift
// Full-screen video with an auto-hiding control overlay. The VLCKit drawable is
|
|
// a plain UIView bridged via UIViewRepresentable; all transport goes through
|
|
// VLCPlayerModel.
|
|
|
|
import SwiftUI
|
|
import MobileVLCKit
|
|
import LilithDesignTokens
|
|
|
|
struct PlayerScreen: View {
|
|
let title: String
|
|
let url: URL
|
|
let networkCachingMs: Int
|
|
|
|
@StateObject private var model = VLCPlayerModel()
|
|
@State private var controlsVisible = true
|
|
@State private var scrubValue: Double = 0
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.black.ignoresSafeArea()
|
|
VLCVideoView(player: model.player)
|
|
.ignoresSafeArea()
|
|
|
|
if model.buffering {
|
|
ProgressView().tint(.white).scaleEffect(1.4)
|
|
}
|
|
|
|
if controlsVisible {
|
|
controls
|
|
.transition(.opacity)
|
|
}
|
|
}
|
|
.contentShape(Rectangle())
|
|
.onTapGesture {
|
|
withAnimation { controlsVisible.toggle() }
|
|
}
|
|
.navigationTitle(title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.onAppear { model.start(url: url, networkCachingMs: networkCachingMs) }
|
|
.onDisappear { model.teardown() }
|
|
}
|
|
|
|
private var controls: some View {
|
|
VStack {
|
|
Spacer()
|
|
HStack(spacing: 40) {
|
|
Button { model.skip(seconds: -10) } label: {
|
|
Image(systemName: "gobackward.10").font(.title)
|
|
}
|
|
Button { model.togglePlay() } label: {
|
|
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
|
|
.font(.system(size: 48))
|
|
}
|
|
.accessibilityIdentifier("playPauseButton")
|
|
Button { model.skip(seconds: 30) } label: {
|
|
Image(systemName: "goforward.30").font(.title)
|
|
}
|
|
}
|
|
.foregroundStyle(.white)
|
|
.padding(.bottom, 12)
|
|
|
|
HStack(spacing: 10) {
|
|
Text(model.elapsed)
|
|
.font(.caption.monospacedDigit())
|
|
.accessibilityIdentifier("elapsed") // UI test asserts this advances
|
|
Slider(
|
|
value: $scrubValue,
|
|
in: 0...1,
|
|
onEditingChanged: { editing in
|
|
if editing {
|
|
model.beginScrub()
|
|
} else {
|
|
model.commitScrub(to: scrubValue)
|
|
}
|
|
}
|
|
)
|
|
.tint(AppColors.primary)
|
|
Text(model.remaining)
|
|
.font(.caption.monospacedDigit())
|
|
}
|
|
.foregroundStyle(.white)
|
|
.padding(.horizontal)
|
|
.padding(.bottom, 24)
|
|
}
|
|
.background(
|
|
LinearGradient(
|
|
colors: [.clear, .black.opacity(0.6)],
|
|
startPoint: .center,
|
|
endPoint: .bottom
|
|
)
|
|
.ignoresSafeArea()
|
|
)
|
|
// Keep the slider in sync with playback unless the user is dragging it.
|
|
.onReceive(model.$position) { p in
|
|
scrubValue = p
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Hosts VLCKit's video output. The drawable must be set on a live UIView, so we
|
|
/// hand the player's drawable to the view we create here.
|
|
struct VLCVideoView: UIViewRepresentable {
|
|
let player: VLCMediaPlayer
|
|
|
|
func makeUIView(context: Context) -> UIView {
|
|
let view = UIView()
|
|
view.backgroundColor = .black
|
|
player.drawable = view
|
|
return view
|
|
}
|
|
|
|
func updateUIView(_ uiView: UIView, context: Context) {}
|
|
}
|