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) {}