2 files added
12 files modified
| | |
| | | objectVersion = 77; |
| | | objects = { |
| | | |
| | | /* Begin PBXBuildFile section */ |
| | | A1615DB82E0D9A6E00D73CE3 /* WrappingHStack in Frameworks */ = {isa = PBXBuildFile; productRef = A1615DB72E0D9A6E00D73CE3 /* WrappingHStack */; }; |
| | | /* End PBXBuildFile section */ |
| | | |
| | | /* Begin PBXContainerItemProxy section */ |
| | | A1615D372E0C27F700D73CE3 /* PBXContainerItemProxy */ = { |
| | | isa = PBXContainerItemProxy; |
| | |
| | | isa = PBXFrameworksBuildPhase; |
| | | buildActionMask = 2147483647; |
| | | files = ( |
| | | A1615DB82E0D9A6E00D73CE3 /* WrappingHStack in Frameworks */, |
| | | ); |
| | | runOnlyForDeploymentPostprocessing = 0; |
| | | }; |
| | |
| | | ); |
| | | name = LiveProject; |
| | | packageProductDependencies = ( |
| | | A1615DB72E0D9A6E00D73CE3 /* WrappingHStack */, |
| | | ); |
| | | productName = LiveProject; |
| | | productReference = A1615D292E0C27F500D73CE3 /* LiveProject.app */; |
| | |
| | | ); |
| | | mainGroup = A1615D202E0C27F500D73CE3; |
| | | minimizedProjectReferenceProxies = 1; |
| | | packageReferences = ( |
| | | A1615DB62E0D996500D73CE3 /* XCRemoteSwiftPackageReference "WrappingHStack" */, |
| | | ); |
| | | preferredProjectObjectVersion = 77; |
| | | productRefGroup = A1615D2A2E0C27F500D73CE3 /* Products */; |
| | | projectDirPath = ""; |
| | |
| | | defaultConfigurationName = Release; |
| | | }; |
| | | /* End XCConfigurationList section */ |
| | | |
| | | /* Begin XCRemoteSwiftPackageReference section */ |
| | | A1615DB62E0D996500D73CE3 /* XCRemoteSwiftPackageReference "WrappingHStack" */ = { |
| | | isa = XCRemoteSwiftPackageReference; |
| | | repositoryURL = "https://github.com/dkk/WrappingHStack"; |
| | | requirement = { |
| | | kind = upToNextMajorVersion; |
| | | minimumVersion = 2.2.11; |
| | | }; |
| | | }; |
| | | /* End XCRemoteSwiftPackageReference section */ |
| | | |
| | | /* Begin XCSwiftPackageProductDependency section */ |
| | | A1615DB72E0D9A6E00D73CE3 /* WrappingHStack */ = { |
| | | isa = XCSwiftPackageProductDependency; |
| | | package = A1615DB62E0D996500D73CE3 /* XCRemoteSwiftPackageReference "WrappingHStack" */; |
| | | productName = WrappingHStack; |
| | | }; |
| | | /* End XCSwiftPackageProductDependency section */ |
| | | }; |
| | | rootObject = A1615D212E0C27F500D73CE3 /* Project object */; |
| | | } |
New file |
| | |
| | | { |
| | | "originHash" : "50578bb08258a739890c5d3d0a495ba4213b27b4dbbbc844c23e93aa2d80550b", |
| | | "pins" : [ |
| | | { |
| | | "identity" : "wrappinghstack", |
| | | "kind" : "remoteSourceControl", |
| | | "location" : "https://github.com/dkk/WrappingHStack", |
| | | "state" : { |
| | | "revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b", |
| | | "version" : "2.2.11" |
| | | } |
| | | } |
| | | ], |
| | | "version" : 3 |
| | | } |
New file |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <Bucket |
| | | uuid = "B6E33E15-95B1-4036-89E0-5C9B0FA33668" |
| | | type = "1" |
| | | version = "2.0"> |
| | | </Bucket> |
| | |
| | | |
| | | import Foundation |
| | | import SwiftUI |
| | | import WrappingHStack |
| | | |
| | | struct LiveActivity: View { |
| | | @State private var pixelBuffer: CVPixelBuffer? |
| | | |
| | | |
| | | @State private var showDeviceDialog = false |
| | | |
| | | @State private var streamRate = Float(9/16.0); |
| | | @State private var mainSize: CGSize = .init(width: 100, height: 100) |
| | | |
| | | @State private var devices = [DeviceInfo(name: "相机", type: .CAMERA(), deviceId: "相机"), |
| | | DeviceInfo(name: "话筒", type: .MICROPHONE(),deviceId: "话筒"), |
| | | DeviceInfo(name: "系统", type: .SYSTEM(),deviceId : "系统")] |
| | | |
| | | var body: some View { |
| | | NavigationView{ |
| | | ZStack{ |
| | | ZStack{ |
| | | Color.clear |
| | | .ignoresSafeArea() // 填满全屏 |
| | | VStack{ |
| | | VideoRendererView(renderer: MetalRenderer()).background(Color.black).frame(width: mainSize.width,height:mainSize.height) |
| | | Spacer() |
| | | }.border(Color.blue) |
| | | VStack{ |
| | | Spacer() |
| | | BottomBtns().frame(alignment: .bottom).border(Color.green) |
| | | } |
| | | if showDeviceDialog { |
| | | DialogDevices() |
| | | } |
| | | }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) |
| | | .background( |
| | | GeometryReader { geometry in |
| | | Color.clear |
| | | .onAppear { |
| | | updateWindowSize(width: Int(geometry.size.width), height: Int(geometry.size.height)) |
| | | } |
| | | .onChange(of: geometry.size) { newSize in |
| | | updateWindowSize(width: Int(newSize.width), height: Int(newSize.height)) |
| | | } |
| | | }) |
| | | .border(Color.red) |
| | | .onDisappear { |
| | | print("onDisappear 视图消失了!") |
| | | |
| | | }.onAppear { |
| | | print("onAppear 视图出现了!") |
| | | } |
| | | } |
| | | |
| | | func updateWindowSize(width:Int,height:Int){ |
| | | var rate : Float = Float(width)/Float(height); |
| | | if(rate != streamRate) { |
| | | var mainWidth = 0; |
| | | var mainHeight = 0; |
| | | if(rate < streamRate){ |
| | | mainWidth = width; |
| | | if(9.0/16 == streamRate){ |
| | | mainHeight = (mainWidth / 9 * 16); |
| | | }else{ |
| | | mainHeight = (mainWidth / 16 * 9); |
| | | } |
| | | }else{ |
| | | mainHeight = height; |
| | | if(9.0 / 16 == streamRate){ |
| | | mainWidth = (mainHeight / 16 * 9); |
| | | }else{ |
| | | mainWidth = (mainHeight / 9 * 16); |
| | | } |
| | | } |
| | | if(mainSize.width != CGFloat(mainWidth) || mainSize.height != CGFloat(mainHeight)){ |
| | | mainSize = .init(width: mainWidth, height: mainHeight); |
| | | } |
| | | }else{ |
| | | if(mainSize.width != CGFloat(width) || mainSize.height != CGFloat(height)){ |
| | | mainSize = .init(width: width, height: height); |
| | | } |
| | | } |
| | | //Log.w(TAG , "onSizeChanged: ${mainWindowSize.value}" , ) |
| | | } |
| | | } |
| | | |
| | | struct Bottombtns:View { |
| | | var body: some View { |
| | | |
| | | func DialogDevices() -> some View{ |
| | | ZStack{ |
| | | Color.black.opacity(0.4) |
| | | .edgesIgnoringSafeArea(.all) |
| | | .onTapGesture { |
| | | withAnimation { |
| | | showDeviceDialog = false |
| | | } |
| | | } |
| | | VStack { |
| | | Spacer() |
| | | VStack(spacing: 20) { |
| | | Spacer().frame(height:40) |
| | | FlowLayout(devices){ device in |
| | | MButton(text:device.name){ |
| | | |
| | | } |
| | | } |
| | | .padding() |
| | | .animation(.default, value: devices) |
| | | } |
| | | .frame(maxWidth: .infinity) |
| | | .padding() |
| | | .background(Color.white) |
| | | .cornerRadius(20) |
| | | .transition(.move(edge: .bottom)) |
| | | } |
| | | .zIndex(1) |
| | | } |
| | | } |
| | | |
| | | func BottomBtns() -> some View{ |
| | | VStack{ |
| | | |
| | | |
| | | HStack(){ |
| | | MButton(icon: IconPortrait()){ |
| | | |
| | |
| | | } |
| | | HStack{ |
| | | LButton(text: "设备"){ |
| | | |
| | | print("Click 设备 button") |
| | | withAnimation{ |
| | | showDeviceDialog.toggle() |
| | | } |
| | | } |
| | | LButton(text: "RTMP"){ |
| | | |
| | | } |
| | | LButton(text: "文件"){ |
| | | /*flLButton(text: "文件"){ |
| | | |
| | | } |
| | | }*/ |
| | | LButton(text: "文本"){ |
| | | |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | struct LiveActivity_Previews: PreviewProvider{ |
| | | |
| | | struct LiveActivity_BottomBtns_Previews: PreviewProvider{ |
| | | static var previews: some View { |
| | | Bottombtns(); |
| | | LiveActivity(); |
| | | } |
| | | |
| | | } |
| | |
| | | // |
| | | // DeviceInfo.swift |
| | | // LiveProject |
| | | // |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | |
| | | |
| | | struct DeviceInfo : Hashable { |
| | | let name:String |
| | | let type:StreamType |
| | | let deviceId:String; |
| | | |
| | | func hash(into hasher: inout Hasher){ |
| | | hasher.combine(deviceId) |
| | | } |
| | | |
| | | static func == (lhs: DeviceInfo, rhs: DeviceInfo) -> Bool { |
| | | lhs.deviceId == rhs.deviceId |
| | | } |
| | | } |
| | |
| | | // |
| | | // StreamType.swift |
| | | // LiveProject |
| | | // |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | |
| | | |
| | | enum StreamType{ |
| | | case |
| | | //系统 |
| | | SYSTEM(val:Int = 0), |
| | | //本地类型 |
| | | FILE(val:Int = 1), |
| | | //图像 |
| | | CAMERA(val:Int = 100),VIDEO(val:Int = 102),PICTURE(val:Int = 104),TEXT(val:Int = 106), |
| | | //音频 |
| | | MICROPHONE(val:Int = 101),AUDIO(val:Int = 103), |
| | | //外置设备 |
| | | UVC(val:Int = 301),UAC(val:Int = 302), |
| | | //远程 |
| | | RTMP(val:Int = 200) |
| | | } |
| | |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | |
| | | import Foundation |
| | | import SwiftUI |
| | | |
| | | struct IconBack: Shape { |
| | | |
| | | func path(in rect: CGRect) -> Path { |
| | | var path = Path() |
| | | |
| | | let arrowHeadLength = rect.width * 0.4 |
| | | let shaftY = rect.midY |
| | | let headHeight = rect.height * 0.5 |
| | | |
| | | // 箭头头部(三角形) |
| | | path.move(to: CGPoint(x: arrowHeadLength, y: rect.minY)) |
| | | path.addLine(to: CGPoint(x: 0, y: shaftY)) |
| | | path.addLine(to: CGPoint(x: arrowHeadLength, y: rect.maxY)) |
| | | |
| | | // 箭身(横线) |
| | | path.move(to: CGPoint(x: 0, y: shaftY)) |
| | | path.addLine(to: CGPoint(x: rect.maxX, y: shaftY)) |
| | | return path |
| | | } |
| | | } |
| | | |
| | | struct IconBack_Previews : PreviewProvider{ |
| | | static var previews: some View { |
| | | IconBack() |
| | | .stroke(Color.primary, lineWidth: 3).frame(width: 30,height: 25) |
| | | } |
| | | } |
| | |
| | | struct TextBtnStyle: ButtonStyle { |
| | | func makeBody(configuration: Configuration) -> some View { |
| | | configuration.label |
| | | .scaleEffect(configuration.isPressed ? 0.95 : 1.0) |
| | | .opacity(configuration.isPressed ? 0.8 : 1.0) |
| | | } |
| | | } |
| | |
| | | // 渲染工具 |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | import CoreVideo |
| | | import Metal |
| | | import MetalKit |
| | | |
| | | class Renderer: NSObject, MTKViewDelegate { |
| | | static var shared: Renderer? |
| | | class MetalRenderer: NSObject, MTKViewDelegate { |
| | | var device: MTLDevice! |
| | | var commandQueue: MTLCommandQueue! |
| | | |
| | | private var device: MTLDevice! |
| | | private var commandQueue: MTLCommandQueue! |
| | | private var pipelineState: MTLRenderPipelineState! |
| | | private var texture: MTLTexture? |
| | | private var textureDescriptor: MTLTextureDescriptor! |
| | | private var currentData: [UInt8]? |
| | | private var textureWidth = 0 |
| | | private var textureHeight = 0 |
| | | |
| | | weak var mtkView: MTKView? { |
| | | didSet { |
| | | guard let view = mtkView else { return } |
| | | device = view.device |
| | | commandQueue = device.makeCommandQueue() |
| | | |
| | | let library = device.makeDefaultLibrary() |
| | | let pipelineDesc = MTLRenderPipelineDescriptor() |
| | | pipelineDesc.vertexFunction = library?.makeFunction(name: "vertexShader") |
| | | pipelineDesc.fragmentFunction = library?.makeFunction(name: "fragmentShader") |
| | | pipelineDesc.colorAttachments[0].pixelFormat = view.colorPixelFormat |
| | | |
| | | pipelineState = try? device.makeRenderPipelineState(descriptor: pipelineDesc) |
| | | |
| | | Renderer.shared = self |
| | | } |
| | | func setup(view: MTKView) { |
| | | self.device = view.device |
| | | self.commandQueue = device.makeCommandQueue() |
| | | // 初始化 texture、pipeline 等 |
| | | } |
| | | |
| | | func updateFrame(data: [UInt8], width: Int, height: Int) { |
| | | currentData = data |
| | | textureWidth = width |
| | | textureHeight = height |
| | | // ✅ 必须实现的方法 1:窗口大小改变时调用 |
| | | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { |
| | | // 可以留空或更新视图缩放、渲染区域等 |
| | | } |
| | | |
| | | // ✅ 必须实现的方法 2:每一帧绘制时调用 |
| | | func draw(in view: MTKView) { |
| | | guard let drawable = view.currentDrawable, |
| | | let descriptor = view.currentRenderPassDescriptor, |
| | | let data = currentData else { return } |
| | | let descriptor = view.currentRenderPassDescriptor else { return } |
| | | |
| | | if texture == nil || texture?.width != textureWidth || texture?.height != textureHeight { |
| | | textureDescriptor = MTLTextureDescriptor.texture2DDescriptor( |
| | | pixelFormat: .rgba8Unorm, |
| | | width: textureWidth, |
| | | height: textureHeight, |
| | | mipmapped: false |
| | | ) |
| | | textureDescriptor.usage = [.shaderRead, .shaderWrite] |
| | | texture = device.makeTexture(descriptor: textureDescriptor) |
| | | } |
| | | |
| | | let region = MTLRegionMake2D(0, 0, textureWidth, textureHeight) |
| | | data.withUnsafeBytes { ptr in |
| | | texture?.replace(region: region, mipmapLevel: 0, withBytes: ptr.baseAddress!, bytesPerRow: textureWidth * 4) |
| | | } |
| | | |
| | | let commandBuffer = commandQueue.makeCommandBuffer() |
| | | let encoder = commandBuffer?.makeRenderCommandEncoder(descriptor: descriptor) |
| | | encoder?.setRenderPipelineState(pipelineState) |
| | | encoder?.setFragmentTexture(texture, index: 0) |
| | | encoder?.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) |
| | | encoder?.endEncoding() |
| | | commandBuffer?.present(drawable) |
| | | commandBuffer?.commit() |
| | | let commandBuffer = commandQueue.makeCommandBuffer()! |
| | | let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)! |
| | | // 渲染逻辑,如绘制纹理 |
| | | encoder.endEncoding() |
| | | commandBuffer.present(drawable) |
| | | commandBuffer.commit() |
| | | } |
| | | |
| | | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} |
| | | } |
| | |
| | | // |
| | | // Created by 倪路朋 on 6/26/25. |
| | | // |
| | | import SwiftUI |
| | | import SwiftUI |
| | | |
| | | /// 完全自定义的自动换行布局容器 |
| | | struct FlowLayout<Data: RandomAccessCollection, Content: View>: View where Data.Element: Hashable { |
| | | // MARK: - 属性 |
| | | |
| | | /// 要显示的数据集合 |
| | | let data: Data |
| | | /// 水平间距 |
| | | let horizontalSpacing: CGFloat |
| | | /// 垂直间距 |
| | | let verticalSpacing: CGFloat |
| | | /// 对齐方式 |
| | | let alignment: HorizontalAlignment |
| | | /// 内容构建闭包 |
| | | let content: (Data.Element) -> Content |
| | | /// 总高度状态 |
| | | @State private var totalHeight: CGFloat = 0 |
| | | |
| | | // MARK: - 初始化 |
| | | |
| | | /// 初始化FlowLayout |
| | | /// - Parameters: |
| | | /// - data: 要显示的数据集合 |
| | | /// - horizontalSpacing: 水平间距,默认为8 |
| | | /// - verticalSpacing: 垂直间距,默认为8 |
| | | /// - alignment: 对齐方式,默认为.leading |
| | | /// - content: 内容构建闭包 |
| | | init( |
| | | _ data: Data, |
| | | horizontalSpacing: CGFloat = 8, |
| | | verticalSpacing: CGFloat = 8, |
| | | alignment: HorizontalAlignment = .leading, |
| | | @ViewBuilder content: @escaping (Data.Element) -> Content |
| | | ) { |
| | | self.data = data |
| | | self.horizontalSpacing = horizontalSpacing |
| | | self.verticalSpacing = verticalSpacing |
| | | self.alignment = alignment |
| | | self.content = content |
| | | } |
| | | |
| | | // MARK: - 主体视图 |
| | | |
| | | var body: some View { |
| | | GeometryReader { geometry in |
| | | self.contentView(in: geometry) |
| | | .background( |
| | | HeightReader(height: $totalHeight) |
| | | ) |
| | | } |
| | | .frame(height: totalHeight) |
| | | } |
| | | |
| | | // MARK: - 私有方法 |
| | | |
| | | /// 构建内容视图 |
| | | private func contentView(in geometry: GeometryProxy) -> some View { |
| | | var width: CGFloat = 0 |
| | | var height: CGFloat = 0 |
| | | var lastHeight: CGFloat = 0 |
| | | |
| | | return ZStack(alignment: Alignment(horizontal: alignment, vertical: .top)) { |
| | | ForEach(data.map { $0 }, id: \.self) { item in |
| | | content(item) |
| | | .padding(.trailing, horizontalSpacing) |
| | | .padding(.bottom, verticalSpacing) |
| | | .alignmentGuide(.leading) { dimensions in |
| | | // 检查是否需要换行 |
| | | if abs(width - dimensions.width) > geometry.size.width { |
| | | width = 0 |
| | | height += lastHeight + verticalSpacing |
| | | } |
| | | |
| | | let result = width |
| | | |
| | | // 更新宽度计算 |
| | | if item == data.last { |
| | | width = 0 // 重置为0,最后一项 |
| | | } else { |
| | | width -= dimensions.width + horizontalSpacing |
| | | } |
| | | |
| | | // 记录当前行高度 |
| | | lastHeight = dimensions.height |
| | | return result |
| | | } |
| | | .alignmentGuide(.top) { dimensions in |
| | | let result = height |
| | | |
| | | // 如果是最后一项,更新总高度 |
| | | if item == data.last { |
| | | height += lastHeight + verticalSpacing |
| | | } |
| | | |
| | | return result |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - 高度读取器 |
| | | |
| | | /// 用于读取视图高度的辅助视图 |
| | | private struct HeightReader: View { |
| | | @Binding var height: CGFloat |
| | | |
| | | var body: some View { |
| | | GeometryReader { geometry in |
| | | Color.clear |
| | | .preference( |
| | | key: HeightPreferenceKey.self, |
| | | value: geometry.size.height |
| | | ) |
| | | } |
| | | .onPreferenceChange(HeightPreferenceKey.self) { newHeight in |
| | | DispatchQueue.main.async { |
| | | self.height = newHeight |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // MARK: - 高度偏好键 |
| | | |
| | | /// 用于传递高度值的PreferenceKey |
| | | private struct HeightPreferenceKey: PreferenceKey { |
| | | static var defaultValue: CGFloat = 0 |
| | | |
| | | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { |
| | | value = nextValue() |
| | | } |
| | | } |
| | | |
| | | // MARK: - 使用示例 |
| | | |
| | | struct FlowLayoutExample: View { |
| | | let tags = [ |
| | | "Swift", "SwiftUI", "UIKit", "Combine", "Core Data", |
| | | "Xcode", "Interface Builder", "Core Animation", "ARKit", |
| | | "Metal", "Core ML", "Vision", "MapKit", "CloudKit" |
| | | ] |
| | | |
| | | @State private var newTag = "" |
| | | @State private var customTags = ["自定义标签1", "自定义标签2"] |
| | | |
| | | var body: some View { |
| | | VStack { |
| | | FlowLayout(customTags + tags, horizontalSpacing: 10, verticalSpacing: 10) { tag in |
| | | MButton(text:tag){ |
| | | |
| | | } |
| | | } |
| | | .padding() |
| | | .animation(.default, value: customTags) |
| | | } |
| | | } |
| | | |
| | | private func addTag() { |
| | | withAnimation { |
| | | customTags.append(newTag) |
| | | newTag = "" |
| | | } |
| | | } |
| | | } |
| | | |
| | | /// 标签视图 |
| | | struct TagView: View { |
| | | let text: String |
| | | |
| | | var body: some View { |
| | | Text(text) |
| | | .padding(.horizontal, 12) |
| | | .padding(.vertical, 8) |
| | | .background(Color.blue.opacity(0.2)) |
| | | .foregroundColor(.blue) |
| | | .cornerRadius(10) |
| | | .overlay( |
| | | RoundedRectangle(cornerRadius: 10) |
| | | .stroke(Color.blue, lineWidth: 1) |
| | | ) |
| | | .fixedSize(horizontal: true, vertical: false) // 确保文本不被截断 |
| | | } |
| | | } |
| | | |
| | | // MARK: - 预览 |
| | | |
| | | struct FlowLayout_Previews: PreviewProvider { |
| | | static var previews: some View { |
| | | FlowLayoutExample() |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | var body: some View { |
| | | //登录按钮 |
| | | Button(action: { |
| | | if(valid == .INVALID){ |
| | | return |
| | |
| | | } |
| | | |
| | | var body: some View { |
| | | //登录按钮 |
| | | Button(action: { |
| | | if(valid == .INVALID){ |
| | | return |
| | |
| | | @Environment(\.presentationMode) var presentationMode |
| | | |
| | | var title = "" |
| | | var imgLeft = "IconBack"; |
| | | var iconBack = IconBack(); |
| | | var imgRight = ""; |
| | | var titleColor = "ColorText" |
| | | var titleColor = Color.colorText |
| | | |
| | | var body: some View { |
| | | |
| | |
| | | print("Click back button") |
| | | self.presentationMode.wrappedValue.dismiss() |
| | | }) { |
| | | Image(imgLeft) |
| | | .frame(width: 28,height: 28) |
| | | iconBack.stroke(Color.primary, lineWidth: 2.5).frame(width: 18,height: 14) |
| | | } |
| | | Spacer() |
| | | Text(title).foregroundColor(Color.init(titleColor)) |
| | | Text(title).foregroundColor(titleColor) |
| | | .bold().font(Font.system(size: 16)) |
| | | Spacer() |
| | | Image(imgRight).frame(width: 28,height: 28) |
| | |
| | | import SwiftUI |
| | | import MetalKit |
| | | |
| | | struct MetalVideoRenderView: UIViewRepresentable { |
| | | // 统一接口,接收各种视频源的帧数据 |
| | | var frameData: VideoFrameData? |
| | | |
| | | func makeCoordinator() -> Coordinator { |
| | | Coordinator(self) |
| | | } |
| | | |
| | | struct VideoRendererView: UIViewRepresentable { |
| | | let renderer: MetalRenderer // 自定义 Metal 渲染器,支持传入 RGBA/YUV 数据帧 |
| | | |
| | | func makeUIView(context: Context) -> MTKView { |
| | | let mtkView = MTKView() |
| | | mtkView.device = context.coordinator.device |
| | | mtkView.delegate = context.coordinator |
| | | mtkView.device = MTLCreateSystemDefaultDevice() |
| | | mtkView.framebufferOnly = false |
| | | mtkView.colorPixelFormat = .bgra8Unorm |
| | | mtkView.autoResizeDrawable = true |
| | | mtkView.enableSetNeedsDisplay = false |
| | | mtkView.isPaused = true |
| | | mtkView.delegate = renderer |
| | | renderer.setup(view: mtkView) |
| | | return mtkView |
| | | } |
| | | |
| | | func updateUIView(_ uiView: MTKView, context: Context) { |
| | | // 更新帧数据 |
| | | context.coordinator.updateFrame(frameData) |
| | | } |
| | | |
| | | class Coordinator: NSObject, MTKViewDelegate { |
| | | var parent: MetalVideoRenderView |
| | | var device: MTLDevice |
| | | var commandQueue: MTLCommandQueue |
| | | var pipelineState: MTLRenderPipelineState |
| | | var textureCache: CVMetalTextureCache? |
| | | |
| | | // YUV转RGB的着色器 |
| | | var yuvConversionPipeline: MTLComputePipelineState? |
| | | |
| | | init(_ parent: MetalVideoRenderView) { |
| | | self.parent = parent |
| | | self.device = MTLCreateSystemDefaultDevice()! |
| | | self.commandQueue = device.makeCommandQueue()! |
| | | |
| | | // 初始化Metal资源 |
| | | // (这里需要设置渲染管线和纹理缓存) |
| | | } |
| | | |
| | | func updateFrame(_ frameData: VideoFrameData?) { |
| | | // 根据不同的帧数据格式(YUV/RGBA)进行处理 |
| | | // 将数据转换为Metal纹理 |
| | | } |
| | | |
| | | func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {} |
| | | |
| | | func draw(in view: MTKView) { |
| | | // 执行Metal绘制命令 |
| | | } |
| | | } |
| | | |
| | | func updateUIView(_ uiView: MTKView, context: Context) {} |
| | | } |