From 2638c02fb000639375af5912287139b074e3dbc9 Mon Sep 17 00:00:00 2001 From: zztkm Date: Wed, 25 Sep 2024 12:34:07 +0900 Subject: [PATCH] =?UTF-8?q?SwiftUI=20view=20=E3=81=AE=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sora/Video.swift | 152 +++++++++++++++++++++++++++++++++++++++++++ Sora/VideoView.swift | 11 ++++ 2 files changed, 163 insertions(+) create mode 100644 Sora/Video.swift diff --git a/Sora/Video.swift b/Sora/Video.swift new file mode 100644 index 00000000..07287154 --- /dev/null +++ b/Sora/Video.swift @@ -0,0 +1,152 @@ +import Foundation +import SwiftUI +import UIKit + +/** + ストリームの映像を描画する SwiftUI ビューです。 + */ +public struct Video: View where Background: View { + private var stream: MediaStream? + private var background: Background + + @ObservedObject private var controller: VideoController + + /** + ビューを初期化します。 + + - parameter stream: 描画される映像ストリーム。 nil の場合は何も描画されません + */ + public init(_ stream: MediaStream?) where Background == EmptyView { + self.init(stream, background: EmptyView()) + } + + /** + ビューを初期化します。 + + - parameter stream: 描画される映像ストリーム nil の場合は何も描画されません + - paramater background: 映像のクリア時に表示する背景ビュー + */ + public init(_ stream: MediaStream?, background: Background) { + self.stream = stream + self.background = background + controller = VideoController(stream: stream) + } + + /// :nodoc: + public var body: some View { + ZStack { + background + .opacity(controller.isCleared ? 1 : 0) + RepresentedVideoView(controller) + .opacity(controller.isCleared ? 0 : 1) + } + } + + /** + デバッグモードを有効にします。 + 有効にすると、映像の上部に解像度とフレームレートを表示します。 + */ + public func debugMode(_ flag: Bool) -> Video { + controller.videoView.debugMode = flag + return self + } + + /// 映像ソース停止時の処理を指定します。 + public func connectionMode(_ mode: VideoViewConnectionMode) -> Video { + controller.videoView.connectionMode = mode + return self + } + + /// 映像のアスペクト比を指定します。 + public func videoAspect(_ contentMode: ContentMode) -> Video { + var uiContentMode: UIView.ContentMode + switch contentMode { + case .fill: + uiContentMode = .scaleAspectFill + case .fit: + uiContentMode = .scaleAspectFit + } + controller.videoView.contentMode = uiContentMode + return self + } + + /// 映像のクリア時に表示する背景ビューを指定します。 + /// TODO(zztkm): 以下の警告が出るので Swift 6 対応までに修正する + /// ジェネリックパラメータ 'Background' は、同じ名前を持つ外部スコープのジェネリックパラメータを隠します。これは Swift 6 言語モードのエラーです。 + public func videoBackground(_ background: Background) -> Video where Background: View { + var new = Video(stream, background: background) + new.controller = controller + return new + } + + /** + 映像の描画を停止します。 + */ + public func videoStop(_ flag: Bool) -> Video { + if flag { + controller.videoView.stop() + } else if !controller.videoView.isRendering { + controller.videoView.start() + } + return self + } + + /** + 画面を背景ビューに切り替えます。 + このメソッドは描画停止時のみ有効です。 + */ + public func videoClear(_ flag: Bool) -> Video { + if flag { + controller.videoView.clear() + controller.isCleared = true + } + return self + } + + /// 映像のサイズの変更時に実行されるブロックを指定します。 + public func videoOnChange(perform: @escaping (CGSize) -> Void) -> Video { + controller.videoView.handlers.onChange = perform + return self + } + + /// 映像フレームの描画時に実行されるブロックを指定します。 + public func videoOnRender(perform: @escaping (VideoFrame?) -> Void) -> Video { + controller.videoView.handlers.onRender = perform + return self + } +} + +/* + VideoView (UIKit) を SwiftUI view に統合するためのラッパーです。 + */ +private struct RepresentedVideoView: UIViewRepresentable { + typealias UIViewType = VideoView + + @ObservedObject private var controller: VideoController + + public init(_ controller: VideoController) { + self.controller = controller + } + + public func makeUIView(context: Context) -> VideoView { + controller.videoView + } + + public func updateUIView(_ uiView: VideoView, context: Context) { + controller.stream?.videoRenderer = uiView + } +} + +class VideoController: ObservableObject { + var stream: MediaStream? + + // init() で VideoView を生成すると次のエラーが出るので、生成のタイミングを遅らせておく + // Failed to bind EAGLDrawable: to GL_RENDERBUFFER 1 + lazy var videoView = VideoView() + + @Published var isCleared: Bool = false + + init(stream: MediaStream?) { + self.stream = stream + } +} diff --git a/Sora/VideoView.swift b/Sora/VideoView.swift index 4359c2bb..c3362b36 100644 --- a/Sora/VideoView.swift +++ b/Sora/VideoView.swift @@ -15,6 +15,13 @@ public enum VideoViewConnectionMode { case manual } +// Video (SwiftUI 用) で使う +/// :nodoc: +public struct VideoViewHandlers { + public var onChange: ((CGSize) -> Void)? + public var onRender: ((VideoFrame?) -> Void)? +} + /** VideoRenderer プロトコルのデフォルト実装となる UIView です。 @@ -59,6 +66,8 @@ public class VideoView: UIView { self.addSubview(view) return view }() + + public var handlers = VideoViewHandlers() // MARK: - インスタンスの生成 @@ -206,11 +215,13 @@ extension VideoView: VideoRenderer { /// :nodoc: public func onChange(size: CGSize) { contentView.onVideoFrameSizeUpdated(size) + handlers.onChange?(size) } /// :nodoc: public func render(videoFrame: VideoFrame?) { if isRendering { + handlers.onRender?(videoFrame) contentView.render(videoFrame: videoFrame) } }