1
Runt
2025-06-27 bf9e4680eb466bcb7c9cb1bef567252bb1f2bb7d
1
2 files added
12 files modified
593 ■■■■ changed files
LiveProject.xcodeproj/project.pbxproj 28 ●●●●● patch | view | raw | blame | history
LiveProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 15 ●●●●● patch | view | raw | blame | history
LiveProject.xcodeproj/xcuserdata/nilupeng.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist 6 ●●●●● patch | view | raw | blame | history
LiveProject/activity/stream/LiveActivity.swift 128 ●●●●● patch | view | raw | blame | history
LiveProject/data/DeviceInfo.swift 21 ●●●●● patch | view | raw | blame | history
LiveProject/enum/StreamType.swift 23 ●●●●● patch | view | raw | blame | history
LiveProject/shape/IconBack.swift 30 ●●●●● patch | view | raw | blame | history
LiveProject/styles/TextBtnStyle.swift 2 ●●●●● patch | view | raw | blame | history
LiveProject/tool/MetalRenderer.swift 78 ●●●● patch | view | raw | blame | history
LiveProject/views/FlowLayout.swift 193 ●●●●● patch | view | raw | blame | history
LiveProject/views/LButton.swift 1 ●●●● patch | view | raw | blame | history
LiveProject/views/MButton.swift 1 ●●●● patch | view | raw | blame | history
LiveProject/views/TitleBarView.swift 9 ●●●●● patch | view | raw | blame | history
LiveProject/views/VideoRendererView.swift 58 ●●●● patch | view | raw | blame | history
LiveProject.xcodeproj/project.pbxproj
@@ -6,6 +6,10 @@
    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;
@@ -42,6 +46,7 @@
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
                A1615DB82E0D9A6E00D73CE3 /* WrappingHStack in Frameworks */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
@@ -100,6 +105,7 @@
            );
            name = LiveProject;
            packageProductDependencies = (
                A1615DB72E0D9A6E00D73CE3 /* WrappingHStack */,
            );
            productName = LiveProject;
            productReference = A1615D292E0C27F500D73CE3 /* LiveProject.app */;
@@ -177,6 +183,9 @@
            );
            mainGroup = A1615D202E0C27F500D73CE3;
            minimizedProjectReferenceProxies = 1;
            packageReferences = (
                A1615DB62E0D996500D73CE3 /* XCRemoteSwiftPackageReference "WrappingHStack" */,
            );
            preferredProjectObjectVersion = 77;
            productRefGroup = A1615D2A2E0C27F500D73CE3 /* Products */;
            projectDirPath = "";
@@ -532,6 +541,25 @@
            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 */;
}
LiveProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
New file
@@ -0,0 +1,15 @@
{
  "originHash" : "50578bb08258a739890c5d3d0a495ba4213b27b4dbbbc844c23e93aa2d80550b",
  "pins" : [
    {
      "identity" : "wrappinghstack",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/dkk/WrappingHStack",
      "state" : {
        "revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b",
        "version" : "2.2.11"
      }
    }
  ],
  "version" : 3
}
LiveProject.xcodeproj/xcuserdata/nilupeng.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Bucket
   uuid = "B6E33E15-95B1-4036-89E0-5C9B0FA33668"
   type = "1"
   version = "2.0">
</Bucket>
LiveProject/activity/stream/LiveActivity.swift
@@ -7,21 +7,121 @@
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()){
                    
@@ -35,14 +135,17 @@
            }
            HStack{
                LButton(text: "设备"){
                    print("Click 设备 button")
                    withAnimation{
                        showDeviceDialog.toggle()
                    }
                }
                LButton(text: "RTMP"){
                    
                }
                LButton(text: "文件"){
                /*flLButton(text: "文件"){
                    
                }
                }*/
                LButton(text: "文本"){
                    
                }
@@ -56,9 +159,10 @@
    }
}
struct LiveActivity_Previews: PreviewProvider{
struct LiveActivity_BottomBtns_Previews: PreviewProvider{
    static var previews: some View {
        Bottombtns();
        LiveActivity();
    }
    
}
LiveProject/data/DeviceInfo.swift
@@ -0,0 +1,21 @@
//
//  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
    }
}
LiveProject/enum/StreamType.swift
@@ -0,0 +1,23 @@
//
//  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)
}
LiveProject/shape/IconBack.swift
@@ -5,4 +5,32 @@
//  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)
    }
}
LiveProject/styles/TextBtnStyle.swift
@@ -11,5 +11,7 @@
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)
    }
}
LiveProject/tool/MetalRenderer.swift
@@ -4,75 +4,35 @@
//  渲染工具
//  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) {}
}
LiveProject/views/FlowLayout.swift
@@ -4,4 +4,197 @@
//
//  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()
    }
}
LiveProject/views/LButton.swift
@@ -21,7 +21,6 @@
    }
    
    var body: some View {
        //登录按钮
        Button(action: {
            if(valid == .INVALID){
                return
LiveProject/views/MButton.swift
@@ -21,7 +21,6 @@
    }
    
    var body: some View {
        //登录按钮
        Button(action: {
            if(valid == .INVALID){
                return
LiveProject/views/TitleBarView.swift
@@ -12,9 +12,9 @@
    @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 {
        
@@ -24,11 +24,10 @@
                    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)
LiveProject/views/VideoRendererView.swift
@@ -7,57 +7,19 @@
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) {}
}