Runt
2025-06-27 e21b1c797955a231f2bcf71818e0259fbb6aeba1
相机权限

相机,话筒图标

横竖屏操作
1 files deleted
1 files added
13 files modified
748 ■■■■■ changed files
LiveProject.xcodeproj/project.pbxproj 259 ●●●●● patch | view | raw | blame | history
LiveProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 15 ●●●●● patch | view | raw | blame | history
LiveProject/Info.plist 8 ●●●●● patch | view | raw | blame | history
LiveProject/LiveProjectApp.swift 2 ●●● patch | view | raw | blame | history
LiveProject/activity/stream/LiveActivity.swift 76 ●●●●● patch | view | raw | blame | history
LiveProject/activity/stream/LiveViewModel.swift 54 ●●●●● patch | view | raw | blame | history
LiveProject/controller/CameraCapture.swift 47 ●●●●● patch | view | raw | blame | history
LiveProject/data/DeviceInfo.swift 3 ●●●● patch | view | raw | blame | history
LiveProject/enum/StreamType.swift 14 ●●●● patch | view | raw | blame | history
LiveProject/shader/Shaders.metal 63 ●●●●● patch | view | raw | blame | history
LiveProject/shape/IconCamera.swift 60 ●●●●● patch | view | raw | blame | history
LiveProject/shape/IconMic.swift 63 ●●●●● patch | view | raw | blame | history
LiveProject/tool/CameraHelper.swift 12 ●●●●● patch | view | raw | blame | history
LiveProject/tool/MetalRenderer.swift 55 ●●●● patch | view | raw | blame | history
LiveProject/views/VideoRendererView.swift 17 ●●●● patch | view | raw | blame | history
LiveProject.xcodeproj/project.pbxproj
@@ -6,36 +6,26 @@
    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;
            containerPortal = A1615D212E0C27F500D73CE3 /* Project object */;
            proxyType = 1;
            remoteGlobalIDString = A1615D282E0C27F500D73CE3;
            remoteInfo = LiveProject;
        };
        A1615D412E0C27F700D73CE3 /* PBXContainerItemProxy */ = {
            isa = PBXContainerItemProxy;
            containerPortal = A1615D212E0C27F500D73CE3 /* Project object */;
            proxyType = 1;
            remoteGlobalIDString = A1615D282E0C27F500D73CE3;
            remoteInfo = LiveProject;
        };
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
        A1615D292E0C27F500D73CE3 /* LiveProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveProject.app; sourceTree = BUILT_PRODUCTS_DIR; };
        A1615D362E0C27F700D73CE3 /* LiveProjectTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LiveProjectTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
        A1615D402E0C27F700D73CE3 /* LiveProjectUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LiveProjectUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
        A1615DC22E0E7F8F00D73CE3 /* Exceptions for "LiveProject" folder in "LiveProject" target */ = {
            isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
            membershipExceptions = (
                Info.plist,
            );
            target = A1615D282E0C27F500D73CE3 /* LiveProject */;
        };
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
        A1615D2B2E0C27F500D73CE3 /* LiveProject */ = {
            isa = PBXFileSystemSynchronizedRootGroup;
            exceptions = (
                A1615DC22E0E7F8F00D73CE3 /* Exceptions for "LiveProject" folder in "LiveProject" target */,
            );
            path = LiveProject;
            sourceTree = "<group>";
        };
@@ -43,21 +33,6 @@
/* Begin PBXFrameworksBuildPhase section */
        A1615D262E0C27F500D73CE3 /* Frameworks */ = {
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
                A1615DB82E0D9A6E00D73CE3 /* WrappingHStack in Frameworks */,
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        A1615D332E0C27F700D73CE3 /* Frameworks */ = {
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        A1615D3D2E0C27F700D73CE3 /* Frameworks */ = {
            isa = PBXFrameworksBuildPhase;
            buildActionMask = 2147483647;
            files = (
@@ -79,8 +54,6 @@
            isa = PBXGroup;
            children = (
                A1615D292E0C27F500D73CE3 /* LiveProject.app */,
                A1615D362E0C27F700D73CE3 /* LiveProjectTests.xctest */,
                A1615D402E0C27F700D73CE3 /* LiveProjectUITests.xctest */,
            );
            name = Products;
            sourceTree = "<group>";
@@ -105,51 +78,10 @@
            );
            name = LiveProject;
            packageProductDependencies = (
                A1615DB72E0D9A6E00D73CE3 /* WrappingHStack */,
            );
            productName = LiveProject;
            productReference = A1615D292E0C27F500D73CE3 /* LiveProject.app */;
            productType = "com.apple.product-type.application";
        };
        A1615D352E0C27F700D73CE3 /* LiveProjectTests */ = {
            isa = PBXNativeTarget;
            buildConfigurationList = A1615D4D2E0C27F700D73CE3 /* Build configuration list for PBXNativeTarget "LiveProjectTests" */;
            buildPhases = (
                A1615D322E0C27F700D73CE3 /* Sources */,
                A1615D332E0C27F700D73CE3 /* Frameworks */,
                A1615D342E0C27F700D73CE3 /* Resources */,
            );
            buildRules = (
            );
            dependencies = (
                A1615D382E0C27F700D73CE3 /* PBXTargetDependency */,
            );
            name = LiveProjectTests;
            packageProductDependencies = (
            );
            productName = LiveProjectTests;
            productReference = A1615D362E0C27F700D73CE3 /* LiveProjectTests.xctest */;
            productType = "com.apple.product-type.bundle.unit-test";
        };
        A1615D3F2E0C27F700D73CE3 /* LiveProjectUITests */ = {
            isa = PBXNativeTarget;
            buildConfigurationList = A1615D502E0C27F700D73CE3 /* Build configuration list for PBXNativeTarget "LiveProjectUITests" */;
            buildPhases = (
                A1615D3C2E0C27F700D73CE3 /* Sources */,
                A1615D3D2E0C27F700D73CE3 /* Frameworks */,
                A1615D3E2E0C27F700D73CE3 /* Resources */,
            );
            buildRules = (
            );
            dependencies = (
                A1615D422E0C27F700D73CE3 /* PBXTargetDependency */,
            );
            name = LiveProjectUITests;
            packageProductDependencies = (
            );
            productName = LiveProjectUITests;
            productReference = A1615D402E0C27F700D73CE3 /* LiveProjectUITests.xctest */;
            productType = "com.apple.product-type.bundle.ui-testing";
        };
/* End PBXNativeTarget section */
@@ -164,14 +96,6 @@
                    A1615D282E0C27F500D73CE3 = {
                        CreatedOnToolsVersion = 16.4;
                    };
                    A1615D352E0C27F700D73CE3 = {
                        CreatedOnToolsVersion = 16.4;
                        TestTargetID = A1615D282E0C27F500D73CE3;
                    };
                    A1615D3F2E0C27F700D73CE3 = {
                        CreatedOnToolsVersion = 16.4;
                        TestTargetID = A1615D282E0C27F500D73CE3;
                    };
                };
            };
            buildConfigurationList = A1615D242E0C27F500D73CE3 /* Build configuration list for PBXProject "LiveProject" */;
@@ -184,7 +108,6 @@
            mainGroup = A1615D202E0C27F500D73CE3;
            minimizedProjectReferenceProxies = 1;
            packageReferences = (
                A1615DB62E0D996500D73CE3 /* XCRemoteSwiftPackageReference "WrappingHStack" */,
            );
            preferredProjectObjectVersion = 77;
            productRefGroup = A1615D2A2E0C27F500D73CE3 /* Products */;
@@ -192,28 +115,12 @@
            projectRoot = "";
            targets = (
                A1615D282E0C27F500D73CE3 /* LiveProject */,
                A1615D352E0C27F700D73CE3 /* LiveProjectTests */,
                A1615D3F2E0C27F700D73CE3 /* LiveProjectUITests */,
            );
        };
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
        A1615D272E0C27F500D73CE3 /* Resources */ = {
            isa = PBXResourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        A1615D342E0C27F700D73CE3 /* Resources */ = {
            isa = PBXResourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        A1615D3E2E0C27F700D73CE3 /* Resources */ = {
            isa = PBXResourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
@@ -230,34 +137,7 @@
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        A1615D322E0C27F700D73CE3 /* Sources */ = {
            isa = PBXSourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
        A1615D3C2E0C27F700D73CE3 /* Sources */ = {
            isa = PBXSourcesBuildPhase;
            buildActionMask = 2147483647;
            files = (
            );
            runOnlyForDeploymentPostprocessing = 0;
        };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
        A1615D382E0C27F700D73CE3 /* PBXTargetDependency */ = {
            isa = PBXTargetDependency;
            target = A1615D282E0C27F500D73CE3 /* LiveProject */;
            targetProxy = A1615D372E0C27F700D73CE3 /* PBXContainerItemProxy */;
        };
        A1615D422E0C27F700D73CE3 /* PBXTargetDependency */ = {
            isa = PBXTargetDependency;
            target = A1615D282E0C27F500D73CE3 /* LiveProject */;
            targetProxy = A1615D412E0C27F700D73CE3 /* PBXContainerItemProxy */;
        };
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
        A1615D482E0C27F700D73CE3 /* Debug */ = {
@@ -386,8 +266,12 @@
                ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
                CODE_SIGN_STYLE = Automatic;
                CURRENT_PROJECT_VERSION = 1;
                DEVELOPMENT_TEAM = 847H24T2J9;
                ENABLE_PREVIEWS = YES;
                GENERATE_INFOPLIST_FILE = YES;
                INFOPLIST_FILE = LiveProject/Info.plist;
                INFOPLIST_KEY_NSCameraUsageDescription = "相机权限";
                INFOPLIST_KEY_NSMicrophoneUsageDescription = "话筒权限";
                INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
                INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
                INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -413,8 +297,12 @@
                ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
                CODE_SIGN_STYLE = Automatic;
                CURRENT_PROJECT_VERSION = 1;
                DEVELOPMENT_TEAM = 847H24T2J9;
                ENABLE_PREVIEWS = YES;
                GENERATE_INFOPLIST_FILE = YES;
                INFOPLIST_FILE = LiveProject/Info.plist;
                INFOPLIST_KEY_NSCameraUsageDescription = "相机权限";
                INFOPLIST_KEY_NSMicrophoneUsageDescription = "话筒权限";
                INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
                INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
                INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -430,74 +318,6 @@
                SWIFT_EMIT_LOC_STRINGS = YES;
                SWIFT_VERSION = 5.0;
                TARGETED_DEVICE_FAMILY = "1,2";
            };
            name = Release;
        };
        A1615D4E2E0C27F700D73CE3 /* Debug */ = {
            isa = XCBuildConfiguration;
            buildSettings = {
                BUNDLE_LOADER = "$(TEST_HOST)";
                CODE_SIGN_STYLE = Automatic;
                CURRENT_PROJECT_VERSION = 1;
                GENERATE_INFOPLIST_FILE = YES;
                IPHONEOS_DEPLOYMENT_TARGET = 18.5;
                MARKETING_VERSION = 1.0;
                PRODUCT_BUNDLE_IDENTIFIER = com.hefan.LiveProjectTests;
                PRODUCT_NAME = "$(TARGET_NAME)";
                SWIFT_EMIT_LOC_STRINGS = NO;
                SWIFT_VERSION = 5.0;
                TARGETED_DEVICE_FAMILY = "1,2";
                TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LiveProject.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LiveProject";
            };
            name = Debug;
        };
        A1615D4F2E0C27F700D73CE3 /* Release */ = {
            isa = XCBuildConfiguration;
            buildSettings = {
                BUNDLE_LOADER = "$(TEST_HOST)";
                CODE_SIGN_STYLE = Automatic;
                CURRENT_PROJECT_VERSION = 1;
                GENERATE_INFOPLIST_FILE = YES;
                IPHONEOS_DEPLOYMENT_TARGET = 18.5;
                MARKETING_VERSION = 1.0;
                PRODUCT_BUNDLE_IDENTIFIER = com.hefan.LiveProjectTests;
                PRODUCT_NAME = "$(TARGET_NAME)";
                SWIFT_EMIT_LOC_STRINGS = NO;
                SWIFT_VERSION = 5.0;
                TARGETED_DEVICE_FAMILY = "1,2";
                TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LiveProject.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/LiveProject";
            };
            name = Release;
        };
        A1615D512E0C27F700D73CE3 /* Debug */ = {
            isa = XCBuildConfiguration;
            buildSettings = {
                CODE_SIGN_STYLE = Automatic;
                CURRENT_PROJECT_VERSION = 1;
                GENERATE_INFOPLIST_FILE = YES;
                MARKETING_VERSION = 1.0;
                PRODUCT_BUNDLE_IDENTIFIER = com.hefan.LiveProjectUITests;
                PRODUCT_NAME = "$(TARGET_NAME)";
                SWIFT_EMIT_LOC_STRINGS = NO;
                SWIFT_VERSION = 5.0;
                TARGETED_DEVICE_FAMILY = "1,2";
                TEST_TARGET_NAME = LiveProject;
            };
            name = Debug;
        };
        A1615D522E0C27F700D73CE3 /* Release */ = {
            isa = XCBuildConfiguration;
            buildSettings = {
                CODE_SIGN_STYLE = Automatic;
                CURRENT_PROJECT_VERSION = 1;
                GENERATE_INFOPLIST_FILE = YES;
                MARKETING_VERSION = 1.0;
                PRODUCT_BUNDLE_IDENTIFIER = com.hefan.LiveProjectUITests;
                PRODUCT_NAME = "$(TARGET_NAME)";
                SWIFT_EMIT_LOC_STRINGS = NO;
                SWIFT_VERSION = 5.0;
                TARGETED_DEVICE_FAMILY = "1,2";
                TEST_TARGET_NAME = LiveProject;
            };
            name = Release;
        };
@@ -522,44 +342,7 @@
            defaultConfigurationIsVisible = 0;
            defaultConfigurationName = Release;
        };
        A1615D4D2E0C27F700D73CE3 /* Build configuration list for PBXNativeTarget "LiveProjectTests" */ = {
            isa = XCConfigurationList;
            buildConfigurations = (
                A1615D4E2E0C27F700D73CE3 /* Debug */,
                A1615D4F2E0C27F700D73CE3 /* Release */,
            );
            defaultConfigurationIsVisible = 0;
            defaultConfigurationName = Release;
        };
        A1615D502E0C27F700D73CE3 /* Build configuration list for PBXNativeTarget "LiveProjectUITests" */ = {
            isa = XCConfigurationList;
            buildConfigurations = (
                A1615D512E0C27F700D73CE3 /* Debug */,
                A1615D522E0C27F700D73CE3 /* Release */,
            );
            defaultConfigurationIsVisible = 0;
            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
File was deleted
LiveProject/Info.plist
New file
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>NSCameraUsageDescription</key>
    <string>App 需要访问相机用于视频显示</string>
</dict>
</plist>
LiveProject/LiveProjectApp.swift
@@ -11,7 +11,7 @@
struct LiveProjectApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
            LiveActivity()
        }
    }
}
LiveProject/activity/stream/LiveActivity.swift
@@ -5,9 +5,10 @@
//  Created by 倪路朋 on 6/25/25.
//
import Foundation
import UIKit
import AVFoundation
import SwiftUI
import WrappingHStack
import MetalKit
struct LiveActivity: View {
    @State private var pixelBuffer: CVPixelBuffer?
@@ -16,18 +17,24 @@
    @State private var showDeviceDialog = false
    
    @State private var streamRate = Float(9/16.0);
    @State private var fpsState = 30;
    @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 : "系统")]
    @State private var displaySize : CGSize = .zero;
    @State private var devices = [DeviceInfo(name: "相机", type: .CAMERA, deviceId: UUID().uuidString,icon: IconCamera()),
                                  DeviceInfo(name: "话筒", type: .MICROPHONE,deviceId: UUID().uuidString,icon: IconMic()),
                                  DeviceInfo(name: "系统", type: .SYSTEM,deviceId : UUID().uuidString,icon: IconPortrait())]
    private let mViewModel = LiveViewModel()
    
    var body: some View {
        ZStack{
            Color.clear
                .ignoresSafeArea() // 填满全屏
            VStack{
                VideoRendererView(renderer: MetalRenderer()).background(Color.black).frame(width: mainSize.width,height:mainSize.height)
                VideoRendererView(renderer:mViewModel.renderer).background(Color.black).frame(width: mainSize.width,height:mainSize.height)
                Spacer()
            }.border(Color.blue)
            VStack{
@@ -42,10 +49,14 @@
                GeometryReader { geometry in
                    Color.clear
                        .onAppear {
                            updateWindowSize(width: Int(geometry.size.width), height: Int(geometry.size.height))
                            displaySize = geometry.size;
                            updateWindowSize()
                            print("displaySize:\(displaySize)")
                        }
                        .onChange(of: geometry.size) { newSize in
                            updateWindowSize(width: Int(newSize.width), height: Int(newSize.height))
                            displaySize = newSize;
                            updateWindowSize()
                            print("displaySize:\(displaySize)")
                        }
                })
            .border(Color.red)
@@ -57,35 +68,35 @@
            }
    }
    
    func updateWindowSize(width:Int,height:Int){
        var rate : Float = Float(width)/Float(height);
    func updateWindowSize(){
        var rate : Float = Float(displaySize.width)/Float(displaySize.height);
        if(rate != streamRate) {
            var mainWidth = 0;
            var mainHeight = 0;
            var mainWidth = 0.0;
            var mainHeight = 0.0;
            if(rate < streamRate){
                mainWidth = width;
                mainWidth = displaySize.width;
                if(9.0/16 == streamRate){
                    mainHeight = (mainWidth / 9 * 16);
                    mainHeight = (mainWidth / 9.0 * 16);
                }else{
                    mainHeight = (mainWidth / 16 * 9);
                    mainHeight = (mainWidth / 16.0 * 9);
                }
            }else{
                mainHeight = height;
                mainHeight = displaySize.height;
                if(9.0 / 16 == streamRate){
                    mainWidth = (mainHeight / 16 * 9);
                    mainWidth = (mainHeight / 16.0 * 9);
                }else{
                    mainWidth = (mainHeight / 9 * 16);
                    mainWidth = (mainHeight / 9.0 * 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);
            if(mainSize.width != displaySize.width || mainSize.height != displaySize.height){
                mainSize = .init(width: displaySize.width, height: displaySize.height);
            }
        }
        //Log.w(TAG , "onSizeChanged: ${mainWindowSize.value}" , )
        print("updateWindow:\(mainSize)")
    }
    
    func DialogDevices() -> some View{
@@ -102,12 +113,16 @@
                VStack(spacing: 20) {
                    Spacer().frame(height:40)
                    FlowLayout(devices){ device in
                        MButton(text:device.name){
                        MButton(icon: device.icon,text: device.name){
                            mViewModel.newWindowAction(device: device){ status in
                                withAnimation{
                                    showDeviceDialog = false;
                                }
                            }
                            print("\(device.name) click")
                        }
                    }
                    .padding()
                    .animation(.default, value: devices)
                }
                .frame(maxWidth: .infinity)
                .padding()
@@ -116,6 +131,7 @@
                .transition(.move(edge: .bottom))
            }
            .zIndex(1)
            .animation(.default, value: devices)
        }
    }
    
@@ -123,12 +139,16 @@
        VStack{
        
            HStack(){
                MButton(icon: IconPortrait()){
                //横竖屏控制
                MButton(icon:streamRate == (9/16.0) ? IconPortrait() : IconLandscape() ){
                    streamRate = streamRate == (9/16.0) ? (16/9.0) : (9/16.0)
                    updateWindowSize()
                }
                MButton(text: "30帧"){
                // fps 控制
                MButton(text: "\(fpsState)帧"){
                    fpsState = fpsState == 30 ? 60 : 30;
                }
                //添加推流地址
                MButton(valid: .INVALID,text: "+"){
                    
                }
LiveProject/activity/stream/LiveViewModel.swift
@@ -0,0 +1,54 @@
//
//  LiveViewModel.swift
//  LiveProject
//
//  Created by 倪路朋 on 6/27/25.
//
import UIKit
import AVFoundation
class LiveViewModel{
    lazy var camera = CameraCapture()
    lazy var renderer = MetalRenderer()
    func newWindowAction(device:DeviceInfo,completion: @escaping (Bool) -> Void = {b in}){
        switch device.type{
        case StreamType.CAMERA:
            requestCameraPermission(mediaType: .video){ staus in
                if(staus){
                    self.camera.onFrame = { buffer in
                        self.renderer.updateFrame(pixelBuffer: buffer)
                        print("画面更新")
                    }
                    self.camera.start()
                    print("启动相机")
                }else{
                }
                completion(staus)
            }
            break;
        default:
            break;
        }
    }
    func requestCameraPermission(mediaType: AVMediaType,completion: @escaping (Bool) -> Void) {
        let status = AVCaptureDevice.authorizationStatus(for: mediaType)
        switch status {
        case .authorized:
            completion(true)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { granted in
                DispatchQueue.main.async {
                    completion(granted)
                }
            }
        default:
            // denied / restricted
            completion(false)
        }
    }
}
LiveProject/controller/CameraCapture.swift
@@ -0,0 +1,47 @@
//
//  CameraCapture.swift
//  LiveProject
//
//  Created by 倪路朋 on 6/27/25.
//
import AVFoundation
class CameraCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    private let session = AVCaptureSession()
    var onFrame: ((CVPixelBuffer) -> Void)?
    func start() {
        guard let device = AVCaptureDevice.default(for: .video),
              let input = try? AVCaptureDeviceInput(device: device) else {
            print("❌ 相机设备无法创建")
            return
        }
        session.beginConfiguration()
        session.sessionPreset = .high
        if session.canAddInput(input) {
            session.addInput(input)
        }
        let output = AVCaptureVideoDataOutput()
        output.videoSettings = [
            kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
        ]
        output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "camera.queue"))
        if session.canAddOutput(output) {
            session.addOutput(output)
        }
        session.commitConfiguration()
        session.startRunning()
    }
    func captureOutput(_ output: AVCaptureOutput,
                       didOutput sampleBuffer: CMSampleBuffer,
                       from connection: AVCaptureConnection) {
        guard let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return }
        onFrame?(buffer)
    }
}
LiveProject/data/DeviceInfo.swift
@@ -4,12 +4,13 @@
//
//  Created by 倪路朋 on 6/26/25.
//
import SwiftUI
struct DeviceInfo : Hashable {
    let name:String
    let type:StreamType
    let deviceId:String;
    var icon : (any View)? = nil;
    
    func hash(into hasher: inout Hasher){
        hasher.combine(deviceId)
LiveProject/enum/StreamType.swift
@@ -6,18 +6,18 @@
//
enum StreamType{
enum StreamType : Int{
    case
    //系统
    SYSTEM(val:Int = 0),
    SYSTEM = 0,
    //本地类型
    FILE(val:Int = 1),
    FILE = 1,
    //图像
    CAMERA(val:Int = 100),VIDEO(val:Int = 102),PICTURE(val:Int = 104),TEXT(val:Int = 106),
    CAMERA = 100,VIDEO = 102,PICTURE = 104,TEXT = 106,
    //音频
    MICROPHONE(val:Int = 101),AUDIO(val:Int = 103),
    MICROPHONE = 101,AUDIO = 103,
    //外置设备
    UVC(val:Int = 301),UAC(val:Int = 302),
    UVC = 301,UAC = 302,
    //远程
    RTMP(val:Int = 200)
    RTMP = 200
}
LiveProject/shader/Shaders.metal
@@ -0,0 +1,63 @@
//
//  Shaders.metal
//  LiveProject
//
//  Created by 倪路朋 on 6/27/25.
//
#include <metal_stdlib>
using namespace metal;
struct VertexOut {
    float4 position [[position]];
    float2 texCoord;
};
struct Uniforms {
    float2 textureSize;
    float2 drawableSize;
};
vertex VertexOut vertex_main(uint vertexID [[vertex_id]],
                             constant Uniforms& uniforms [[buffer(0)]]) {
    float4 positions[4] = {
        float4(-1.0, -1.0, 0, 1),
        float4( 1.0, -1.0, 0, 1),
        float4(-1.0,  1.0, 0, 1),
        float4( 1.0,  1.0, 0, 1)
    };
    float2 texCoords[4] = {
        float2(0.0, 1.0),
        float2(1.0, 1.0),
        float2(0.0, 0.0),
        float2(1.0, 0.0)
    };
    float texAspect = uniforms.textureSize.x / uniforms.textureSize.y;
    float viewAspect = uniforms.drawableSize.x / uniforms.drawableSize.y;
    float scaleX = 1.0;
    float scaleY = 1.0;
    if (texAspect > viewAspect) {
        scaleY = viewAspect / texAspect;
    } else {
        scaleX = texAspect / viewAspect;
    }
    VertexOut out;
    float4 pos = positions[vertexID];
    pos.x *= scaleX;
    pos.y *= scaleY;
    out.position = pos;
    out.texCoord = texCoords[vertexID];
    return out;
}
fragment float4 fragment_main(VertexOut in [[stage_in]],
                              texture2d<half> tex [[texture(0)]]) {
    constexpr sampler s(filter::linear);
    return float4(tex.sample(s, in.texCoord));
}
LiveProject/shape/IconCamera.swift
@@ -0,0 +1,60 @@
//
//  IconCamera.swift
//  LiveProject
//
//  Created by 倪路朋 on 6/27/25.
//
import SwiftUI
struct IconCameraShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        // 比例参数
        let cornerRadius = rect.height * 0.1
        let bodyInset = rect.height * 0.1
        let lensDiameter = rect.height * 0.3
        let flashSize = CGSize(width: rect.width * 0.08, height: rect.height * 0.08)
        // 相机机身(主圆角矩形)
        let bodyRect = rect.insetBy(dx: 0, dy: bodyInset)
        path.addRoundedRect(in: bodyRect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius))
        // 镜头(中间大圆)
        let lensOrigin = CGPoint(
            x: rect.midX - lensDiameter / 2,
            y: rect.midY - lensDiameter / 2
        )
        let lensRect = CGRect(origin: lensOrigin, size: CGSize(width: lensDiameter, height: lensDiameter))
        path.addEllipse(in: lensRect)
        // 闪光灯(机身内的小圆点)
        let flashOrigin = CGPoint(
            x: bodyRect.maxX - flashSize.width * 1.5,
            y: bodyRect.minY + flashSize.height * 1.5
        )
        let flashRect = CGRect(origin: flashOrigin, size: flashSize)
        path.addEllipse(in: flashRect)
        return path
    }
}
struct IconCamera: View {
    var color: Color = .white
    var size: CGSize = CGSize(width: 20, height:20)
    var lineWidth: CGFloat = 2.0
    var body: some View {
        IconCameraShape()
            .stroke(color, lineWidth: lineWidth)
            .frame(width: size.width, height: size.height)
    }
}
struct IconCamera_Previews : PreviewProvider{
    static var previews: some View {
        IconCamera(color: .black).background(.red)
    }
}
LiveProject/shape/IconMic.swift
@@ -0,0 +1,63 @@
//
//  IconMic.swift
//  LiveProject
//
//  Created by 倪路朋 on 6/27/25.
//
import SwiftUI
struct IconMicShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        // 计算容器高度(麦克风主体 + 支架 + 底座 + 一点留白)
        let micHeight = rect.height * 0.5
        let stemHeight = rect.height * 0.1
        let baseHeight = rect.height * 0.02
        let containerHeight = micHeight + stemHeight + baseHeight
        // 计算顶部偏移量,确保垂直居中
        let topOffset = (rect.height - containerHeight) / 2
        // 主体:麦克风(居中)
        let micWidth = rect.width * 0.3
        let micRect = CGRect(
            x: rect.midX - micWidth / 2,
            y: topOffset,
            width: micWidth,
            height: micHeight
        )
        path.addRoundedRect(in: micRect, cornerSize: CGSize(width: micWidth / 2, height: micWidth / 2))
        // 支架
        let stemTop = CGPoint(x: rect.midX, y: micRect.maxY)
        let stemBottom = CGPoint(x: rect.midX, y: stemTop.y + stemHeight)
        path.move(to: stemTop)
        path.addLine(to: stemBottom)
        // 底座
        let baseWidth = rect.width * 0.3
        path.move(to: CGPoint(x: rect.midX - baseWidth / 2, y: stemBottom.y))
        path.addLine(to: CGPoint(x: rect.midX + baseWidth / 2, y: stemBottom.y))
        return path
    }
}
struct IconMic: View {
    var color: Color = .white
    var size: CGSize = CGSize(width: 20, height:30)
    var lineWidth: CGFloat = 2.0
    var body: some View {
        IconMicShape()
            .stroke(color, lineWidth: lineWidth)
            .frame(width: size.width, height: size.height)
    }
}
struct IconMic_Previews : PreviewProvider{
    static var previews: some View {
        IconMic(color: .black).background(.red)
    }
}
LiveProject/tool/CameraHelper.swift
@@ -0,0 +1,12 @@
//
//  CameraHelper.swift
//  LiveProject
//
//  Created by 倪路朋 on 6/27/25.
//
import UIKit
import AVFoundation
class CameraHelper{
}
LiveProject/tool/MetalRenderer.swift
@@ -4,35 +4,68 @@
//  渲染工具
//  Created by 倪路朋 on 6/26/25.
//
import CoreVideo
import Metal
import MetalKit
class MetalRenderer: NSObject, MTKViewDelegate {
    var device: MTLDevice!
    var commandQueue: MTLCommandQueue!
    private var device: MTLDevice!
    private var commandQueue: MTLCommandQueue!
    private var textureCache: CVMetalTextureCache!
    private var currentPixelBuffer: CVPixelBuffer?
    func setup(view: MTKView) {
        self.device = view.device
        self.commandQueue = device.makeCommandQueue()
        // 初始化 texture、pipeline 等
        CVMetalTextureCacheCreate(nil, nil, device, nil, &textureCache)
    }
    // ✅ 必须实现的方法 1:窗口大小改变时调用
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
        // 可以留空或更新视图缩放、渲染区域等
    func updateFrame(pixelBuffer: CVPixelBuffer) {
        self.currentPixelBuffer = pixelBuffer
    }
    // ✅ 必须实现的方法 2:每一帧绘制时调用
    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
    func draw(in view: MTKView) {
        guard let drawable = view.currentDrawable,
              let descriptor = view.currentRenderPassDescriptor else { return }
              let descriptor = view.currentRenderPassDescriptor,
              let pixelBuffer = currentPixelBuffer else { return }
        var textureRef: CVMetalTexture?
        let width = CVPixelBufferGetWidth(pixelBuffer)
        let height = CVPixelBufferGetHeight(pixelBuffer)
        let status = CVMetalTextureCacheCreateTextureFromImage(
            nil, textureCache, pixelBuffer, nil,
            .bgra8Unorm, width, height, 0, &textureRef)
        guard status == kCVReturnSuccess,
              let cvTexture = textureRef,
              let texture = CVMetalTextureGetTexture(cvTexture) else { return }
        let commandBuffer = commandQueue.makeCommandBuffer()!
        let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor)!
        // 渲染逻辑,如绘制纹理
        encoder.setFragmentTexture(texture, index: 0)
        encoder.endEncoding()
        // 简单拷贝(不做 shader 处理)
        let blitEncoder = commandBuffer.makeBlitCommandEncoder()!
        let dstTexture = drawable.texture
        if dstTexture.width != texture.width || dstTexture.height != texture.height {
            print("❌ 尺寸不一致,无法 blit:src = \(texture.width)x\(texture.height), dst = \(dstTexture.width)x\(dstTexture.height)")
            return
        }
        blitEncoder.copy(from: texture,
                         sourceSlice: 0,
                         sourceLevel: 0,
                         sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0),
                         sourceSize: MTLSize(width: width, height: height, depth: 1),
                         to: drawable.texture,
                         destinationSlice: 0,
                         destinationLevel: 0,
                         destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0))
        blitEncoder.endEncoding()
        commandBuffer.present(drawable)
        commandBuffer.commit()
        print("绘制画面")
    }
}
LiveProject/views/VideoRendererView.swift
@@ -11,14 +11,15 @@
    let renderer: MetalRenderer  // 自定义 Metal 渲染器,支持传入 RGBA/YUV 数据帧
    func makeUIView(context: Context) -> MTKView {
        let mtkView = MTKView()
        mtkView.device = MTLCreateSystemDefaultDevice()
        mtkView.framebufferOnly = false
        mtkView.enableSetNeedsDisplay = false
        mtkView.isPaused = true
        mtkView.delegate = renderer
        renderer.setup(view: mtkView)
        return mtkView
        let view = MTKView()
        view.device = MTLCreateSystemDefaultDevice()
        view.colorPixelFormat = .bgra8Unorm
        view.clearColor = MTLClearColor(red: 0.2, green: 0.5, blue: 0.7, alpha: 1.0)
        view.delegate = renderer
        view.isPaused = false
        view.enableSetNeedsDisplay = false
        renderer.setup(view: view)
        return view
    }
    func updateUIView(_ uiView: MTKView, context: Context) {}