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