diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/Sources/SwiftBricks/Controls/VideoPlayerControl.swift b/Sources/SwiftBricks/Controls/VideoPlayerControl.swift new file mode 100644 index 0000000..78c1b9c --- /dev/null +++ b/Sources/SwiftBricks/Controls/VideoPlayerControl.swift @@ -0,0 +1,195 @@ +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( + width: resolveWidth(schema.options), + height: resolveHeight(schema.options) + ) + } + + 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( + width: resolveWidth(schema.options), + height: resolveHeight(schema.options) + ) + } + #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) + } +} + + diff --git a/Sources/SwiftBricks/Core/Schema.swift b/Sources/SwiftBricks/Core/Schema.swift index 92620e4..34ffc7e 100644 --- a/Sources/SwiftBricks/Core/Schema.swift +++ b/Sources/SwiftBricks/Core/Schema.swift @@ -129,6 +129,10 @@ public struct ControlOptions: Codable, Sendable { public var src: String? public var alt: String? + // VideoPlayer + public var autoplay: Bool? + public var controls: Bool? + // Misc public var fontsize: String? public var cfontsize: Double? diff --git a/Sources/SwiftBricks/Renderer/ControlRenderer.swift b/Sources/SwiftBricks/Renderer/ControlRenderer.swift index 058ebfd..71f28ce 100644 --- a/Sources/SwiftBricks/Renderer/ControlRenderer.swift +++ b/Sources/SwiftBricks/Renderer/ControlRenderer.swift @@ -108,6 +108,10 @@ public struct ControlRenderer: View { // Image case "Image": ImageControl(schema: schema, engine: engine) + + // VideoPlayer + case "VideoPlayer": + VideoPlayerControl(schema: schema, engine: engine) // urlwidget(直接加载远程内容) case "urlwidget":