190 lines
5.7 KiB
Swift
190 lines
5.7 KiB
Swift
import SwiftUI
|
||
import AVKit
|
||
|
||
/// VideoPlayer widget — 视频播放器控件
|
||
/// 使用 AVKit 渲染视频,通过 SwiftBricks 接口控制
|
||
///
|
||
/// JSON Schema 示例:
|
||
/// ```json
|
||
/// {
|
||
/// "widgettype": "VideoPlayer",
|
||
/// "options": {
|
||
/// "url": "https://example.com/video.mp4",
|
||
/// "autoplay": true,
|
||
/// "controls": true
|
||
/// }
|
||
/// }
|
||
/// ```
|
||
///
|
||
/// 支持的事件:
|
||
/// - play: 播放
|
||
/// - pause: 暂停
|
||
/// - toggle: 切换播放/暂停
|
||
/// - seek: 跳转到指定时间 (参数: time)
|
||
/// - setVolume: 设置音量 (参数: volume)
|
||
|
||
struct VideoPlayerControl: View {
|
||
let schema: ControlSchema
|
||
@ObservedObject var engine: BricksEngine
|
||
@State private var player: AVPlayer?
|
||
@State private var isPlaying: Bool = false
|
||
|
||
var body: some View {
|
||
Group {
|
||
#if os(macOS)
|
||
macOSPlayerView
|
||
#else
|
||
iOSPlayerView
|
||
#endif
|
||
}
|
||
.onAppear {
|
||
setupPlayer()
|
||
setupEventListeners()
|
||
}
|
||
.onDisappear {
|
||
player?.pause()
|
||
}
|
||
}
|
||
|
||
// MARK: - Platform-specific views
|
||
|
||
#if os(macOS)
|
||
private var macOSPlayerView: some View {
|
||
VideoPlayerView(player: player)
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
|
||
private struct VideoPlayerView: NSViewRepresentable {
|
||
let player: AVPlayer?
|
||
|
||
func makeNSView(context: Context) -> NSView {
|
||
let view = AVPlayerView()
|
||
view.player = player
|
||
view.controlsStyle = .default
|
||
return view
|
||
}
|
||
|
||
func updateNSView(_ nsView: NSView, context: Context) {
|
||
if let playerView = nsView as? AVPlayerView {
|
||
playerView.player = player
|
||
}
|
||
}
|
||
}
|
||
#else
|
||
private var iOSPlayerView: some View {
|
||
VideoPlayer(player: player)
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
#endif
|
||
|
||
// MARK: - Setup
|
||
|
||
private func setupPlayer() {
|
||
// 优先从 Store 获取外部注入的共享 AVPlayer(如 PlayerBridge)
|
||
let widgetId = schema.effectiveId
|
||
if let sharedPlayer = engine.store.getValue(id: "\(widgetId).player") as? AVPlayer {
|
||
player = sharedPlayer
|
||
isPlaying = sharedPlayer.rate > 0
|
||
return
|
||
}
|
||
|
||
// Fallback: 用 schema options 中的 url 创建自己的 player
|
||
guard let urlString = schema.options.url else { return }
|
||
|
||
if let url = URL(string: urlString) {
|
||
player = AVPlayer(url: url)
|
||
|
||
if schema.options.autoplay == true {
|
||
player?.play()
|
||
isPlaying = true
|
||
}
|
||
}
|
||
}
|
||
|
||
private func setupEventListeners() {
|
||
let widgetId = schema.effectiveId
|
||
|
||
// 监听播放事件
|
||
engine.eventBus.on("\(widgetId).play") { [weak engine] data in
|
||
self.player?.play()
|
||
self.isPlaying = true
|
||
await engine?.eventBus.dispatch("\(widgetId).playing", data: EventData())
|
||
}
|
||
|
||
// 监听暂停事件
|
||
engine.eventBus.on("\(widgetId).pause") { [weak engine] data in
|
||
self.player?.pause()
|
||
self.isPlaying = false
|
||
await engine?.eventBus.dispatch("\(widgetId).paused", data: EventData())
|
||
}
|
||
|
||
// 监听切换事件
|
||
engine.eventBus.on("\(widgetId).toggle") { [weak engine] data in
|
||
if self.isPlaying {
|
||
self.player?.pause()
|
||
self.isPlaying = false
|
||
await engine?.eventBus.dispatch("\(widgetId).paused", data: EventData())
|
||
} else {
|
||
self.player?.play()
|
||
self.isPlaying = true
|
||
await engine?.eventBus.dispatch("\(widgetId).playing", data: EventData())
|
||
}
|
||
}
|
||
|
||
// 监听跳转事件
|
||
engine.eventBus.on("\(widgetId).seek") { data in
|
||
if let timeStr = data["time"] as? String, let time = Double(timeStr) {
|
||
self.player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
|
||
} else if let time = data["time"] as? Double {
|
||
self.player?.seek(to: CMTime(seconds: time, preferredTimescale: 600))
|
||
}
|
||
}
|
||
|
||
// 监听音量事件
|
||
engine.eventBus.on("\(widgetId).setVolume") { data in
|
||
if let volStr = data["volume"] as? String, let vol = Float(volStr) {
|
||
self.player?.volume = vol
|
||
} else if let vol = data["volume"] as? Float {
|
||
self.player?.volume = vol
|
||
}
|
||
}
|
||
|
||
// 监听设置视频源事件
|
||
engine.eventBus.on("\(widgetId).setURL") { data in
|
||
if let urlString = data["url"] as? String, let url = URL(string: urlString) {
|
||
let wasPlaying = self.isPlaying
|
||
self.player?.pause()
|
||
self.player = AVPlayer(url: url)
|
||
if wasPlaying {
|
||
self.player?.play()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Helpers
|
||
|
||
private func resolveWidth(_ opts: ControlOptions) -> CGFloat? {
|
||
if let w = opts.width {
|
||
if w == "100%" { return nil }
|
||
if let px = parsePixels(w) { return px }
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func resolveHeight(_ opts: ControlOptions) -> CGFloat? {
|
||
if let h = opts.height {
|
||
if h == "100%" { return nil }
|
||
if let px = parsePixels(h) { return px }
|
||
}
|
||
return nil
|
||
}
|
||
|
||
private func parsePixels(_ str: String) -> CGFloat? {
|
||
let cleaned = str.replacingOccurrences(of: "px", with: "")
|
||
return CGFloat(Double(cleaned) ?? 0)
|
||
}
|
||
}
|
||
|
||
|