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