From e21b1c797955a231f2bcf71818e0259fbb6aeba1 Mon Sep 17 00:00:00 2001
From: Runt <qingingrunt2010@qq.com>
Date: Fri, 27 Jun 2025 15:57:25 +0000
Subject: [PATCH] 相机权限

---
 LiveProject/views/VideoRendererView.swift       |   17 
 LiveProject/shape/IconMic.swift                 |   63 ++++
 LiveProject.xcodeproj/project.pbxproj           |  259 +------------------
 LiveProject/activity/stream/LiveActivity.swift  |   76 +++--
 LiveProject/enum/StreamType.swift               |   14 
 LiveProject/Info.plist                          |    8 
 LiveProject/tool/MetalRenderer.swift            |   55 +++
 LiveProject/data/DeviceInfo.swift               |    3 
 LiveProject/tool/CameraHelper.swift             |   12 
 /dev/null                                       |   15 -
 LiveProject/shader/Shaders.metal                |   63 ++++
 LiveProject/controller/CameraCapture.swift      |   47 +++
 LiveProject/activity/stream/LiveViewModel.swift |   54 ++++
 LiveProject/shape/IconCamera.swift              |   60 ++++
 LiveProject/LiveProjectApp.swift                |    2 
 15 files changed, 439 insertions(+), 309 deletions(-)

diff --git a/LiveProject.xcodeproj/project.pbxproj b/LiveProject.xcodeproj/project.pbxproj
index f659319..e28389d 100644
--- a/LiveProject.xcodeproj/project.pbxproj
+++ b/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 */;
 }
diff --git a/LiveProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LiveProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
deleted file mode 100644
index 5ac2f71..0000000
--- a/LiveProject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "originHash" : "50578bb08258a739890c5d3d0a495ba4213b27b4dbbbc844c23e93aa2d80550b",
-  "pins" : [
-    {
-      "identity" : "wrappinghstack",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/dkk/WrappingHStack",
-      "state" : {
-        "revision" : "425d9488ba55f58f0b34498c64c054c77fc2a44b",
-        "version" : "2.2.11"
-      }
-    }
-  ],
-  "version" : 3
-}
diff --git a/LiveProject/Info.plist b/LiveProject/Info.plist
new file mode 100644
index 0000000..e36c0f5
--- /dev/null
+++ b/LiveProject/Info.plist
@@ -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>
diff --git a/LiveProject/LiveProjectApp.swift b/LiveProject/LiveProjectApp.swift
index 78e5b7f..a5bcad8 100644
--- a/LiveProject/LiveProjectApp.swift
+++ b/LiveProject/LiveProjectApp.swift
@@ -11,7 +11,7 @@
 struct LiveProjectApp: App {
     var body: some Scene {
         WindowGroup {
-            ContentView()
+            LiveActivity()
         }
     }
 }
diff --git a/LiveProject/activity/stream/LiveActivity.swift b/LiveProject/activity/stream/LiveActivity.swift
index 8f16308..ef284a2 100644
--- a/LiveProject/activity/stream/LiveActivity.swift
+++ b/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: "+"){
                     
                 }
diff --git a/LiveProject/activity/stream/LiveViewModel.swift b/LiveProject/activity/stream/LiveViewModel.swift
index e69de29..347d761 100644
--- a/LiveProject/activity/stream/LiveViewModel.swift
+++ b/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)
+        }
+    }
+}
diff --git a/LiveProject/controller/CameraCapture.swift b/LiveProject/controller/CameraCapture.swift
index e69de29..455b88a 100644
--- a/LiveProject/controller/CameraCapture.swift
+++ b/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)
+    }
+}
diff --git a/LiveProject/data/DeviceInfo.swift b/LiveProject/data/DeviceInfo.swift
index e3313f4..86d4ecf 100644
--- a/LiveProject/data/DeviceInfo.swift
+++ b/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)
diff --git a/LiveProject/enum/StreamType.swift b/LiveProject/enum/StreamType.swift
index c2c1254..c6ea454 100644
--- a/LiveProject/enum/StreamType.swift
+++ b/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
 }
diff --git a/LiveProject/shader/Shaders.metal b/LiveProject/shader/Shaders.metal
index e69de29..5151b97 100644
--- a/LiveProject/shader/Shaders.metal
+++ b/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));
+}
diff --git a/LiveProject/shape/IconCamera.swift b/LiveProject/shape/IconCamera.swift
index e69de29..2bd8d01 100644
--- a/LiveProject/shape/IconCamera.swift
+++ b/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)
+    }
+}
diff --git a/LiveProject/shape/IconMic.swift b/LiveProject/shape/IconMic.swift
index e69de29..91aec35 100644
--- a/LiveProject/shape/IconMic.swift
+++ b/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)
+    }
+}
diff --git a/LiveProject/tool/CameraHelper.swift b/LiveProject/tool/CameraHelper.swift
index e69de29..299f2ce 100644
--- a/LiveProject/tool/CameraHelper.swift
+++ b/LiveProject/tool/CameraHelper.swift
@@ -0,0 +1,12 @@
+//
+//  CameraHelper.swift
+//  LiveProject
+//
+//  Created by 倪路朋 on 6/27/25.
+//
+import UIKit
+import AVFoundation
+
+class CameraHelper{
+    
+}
diff --git a/LiveProject/tool/MetalRenderer.swift b/LiveProject/tool/MetalRenderer.swift
index 791dfc8..a2aa4ba 100644
--- a/LiveProject/tool/MetalRenderer.swift
+++ b/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("绘制画面")
     }
 }
diff --git a/LiveProject/views/VideoRendererView.swift b/LiveProject/views/VideoRendererView.swift
index f3b0341..67fdc70 100644
--- a/LiveProject/views/VideoRendererView.swift
+++ b/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) {}

--
Gitblit v1.9.1