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