| | |
| | | } |
| | | |
| | | @Published var pixelBuffer: CVPixelBuffer? = nil |
| | | @Published var size:CGSize = CGSize(width:300,height:200) |
| | | @Published var position:CGSize = CGSize(width: 0, height: 0) |
| | | //流尺寸(图片和视频的分辨率) |
| | | @Published var size:CGSize = CGSize(width:16,height:9) |
| | | //视图比例(相对于 main 尺寸) |
| | | @Published var viewRate = 0.5 |
| | | //位置 |
| | | @Published var position:CGPoint = CGPoint(x: 0, y: 0) |
| | | //主选框位置 |
| | | @Published var mainPosition:CGSize = CGSize(width: 0, height: 0) |
| | | //main 尺寸 |
| | | @Published var mainSize:CGSize = CGSize(width:200,height:100) |
| | | //是否加入直播 |
| | | @Published var videoState:LiveState = .IN_LIVE |
| | | @Published var audioState:LiveState = .IN_LIVE |
| | | //是否静音 |
| | | @Published var speakerState:MuteState = .UN_MUTE |
| | | //包含视频和图片 |
| | | @Published var hasVideo = true; |
| | | //包含音频 |
| | | @Published var hasAudio = true; |
| | | //旋转角度 |
| | | @Published var rotate = 0; |
| | | //fps |
| | | @Published var frameFPS = 0; |
| | | // |
| | | @Published var name = " 名称"; |
| | | //相机焦距 |
| | | @Published var minZoom = 0; |
| | | @Published var maxZoom = 0; |
| | | @Published var zoom = 1.0 |
| | | |
| | | //文本颜色 |
| | | @Published var textColor = Color.white; |
| | | } |
| | | struct MiniWindowActions { |
| | |
| | | |
| | | @ObservedObject var miniData:MiniWindowData |
| | | @State var lastLocation = CGSize(width: 0, height: 0) |
| | | @State var lastZoom = 0.0 |
| | | @Environment(\.miniWindowActions) var actions |
| | | |
| | | var body: some View { |
| | | ZStack(){ |
| | | VideoRendererView(pixelBuffer: $miniData.pixelBuffer).background(Color.black) |
| | | var width = (miniData.mainSize.width * miniData.viewRate); |
| | | var height = (miniData.mainSize.width / (miniData.size.width / miniData.size.height) * miniData.viewRate); |
| | | if(miniData.rotate == 90 || miniData.rotate == 270){ |
| | | var temp = width; |
| | | width = height; |
| | | height = temp; |
| | | } |
| | | return ZStack(){ |
| | | let optionalIntBinding = Binding<Int?>( |
| | | get: { miniData.rotate }, |
| | | set: { newValue in |
| | | miniData.rotate = newValue ?? 0 |
| | | } |
| | | ) |
| | | VideoRendererView(pixelBuffer: $miniData.pixelBuffer,rotate:optionalIntBinding).background(Color.black) |
| | | |
| | | //名称 |
| | | Text(self.miniData.name) |
| | |
| | | .clipShape(RoundedCorner(radius: 20,corners: [.bottomRight])) |
| | | .frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topLeading) |
| | | //关闭 |
| | | IconButton(info: Icons.CLOSE,action:actions.onCloseClick).frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topTrailing) |
| | | .padding(EdgeInsets(top: 10, leading: 0, bottom: 0, trailing: 10)) |
| | | IconButton(info: Icons.CLOSE,action:actions.onCloseClick,width: 40,height: 40).frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topTrailing) |
| | | .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) |
| | | if(self.miniData.streamType != .TEXT && self.miniData.hasVideo){ |
| | | //旋转 |
| | | VStack{ |
| | | |
| | | Spacer() |
| | | IconButton(info: Icons.ROTATE_LEFT,action:actions.onRotateLeftClick).padding(EdgeInsets(top: 0, leading: 13, bottom: 0, trailing: 0)) |
| | | IconButton(info: Icons.ROTATE_LEFT,action:{ |
| | | if (self.miniData.rotate == 0) { |
| | | self.miniData.rotate = 270 |
| | | } else { |
| | | self.miniData.rotate -= 90 |
| | | } |
| | | actions.onRotateLeftClick() |
| | | },width: 40,height: 40).padding(EdgeInsets(top: 0, leading: 13, bottom: 0, trailing: 0)) |
| | | Spacer() |
| | | IconButton(info: Icons.ROTATE_RIGHT,action:actions.onRotateRightClick).padding(EdgeInsets(top: 0, leading: 13, bottom: 0, trailing: 11)) |
| | | IconButton(info: Icons.ROTATE_RIGHT,action:{ |
| | | self.miniData.rotate = (self.miniData.rotate + 90) % 360 |
| | | actions.onRotateRightClick() |
| | | },width: 40,height: 40).padding(EdgeInsets(top: 0, leading: 13, bottom: 0, trailing: 11)) |
| | | Spacer() |
| | | }.frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topTrailing) |
| | | .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 10)) |
| | | .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) |
| | | } |
| | | |
| | | BottomBtns().background(Color.gray.opacity(0.4)).frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .bottom) |
| | | BottomBtns().background(Color.black.opacity(0.4)).frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .bottom) |
| | | |
| | | }.background(Color.blue).frame(width: miniData.size.width,height:miniData.size.height) |
| | | .offset(self.miniData.position) |
| | | .gesture(DragGesture(minimumDistance: 0) |
| | | }.background(Color.blue).frame(width:width,height:height) |
| | | .position(self.miniData.position) |
| | | .gesture(SimultaneousGesture(DragGesture(minimumDistance: 0) |
| | | .onChanged{ val in |
| | | let x = self.miniData.position.width |
| | | let y = self.miniData.position.height |
| | | let x = self.miniData.position.x |
| | | let y = self.miniData.position.y |
| | | let newX = x - lastLocation.width + val.translation.width; |
| | | let newY = y - lastLocation.height + val.translation.height; |
| | | self.miniData.position = CGSize(width: newX, height: newY) |
| | | print(" onChanged \(index) \(self.miniData.position) ") |
| | | self.miniData.position = CGPoint(x: newX, y: newY) |
| | | //print(" onChanged \(index) \(self.miniData.position) ") |
| | | lastLocation = val.translation; |
| | | }.onEnded{ val in |
| | | let x = self.miniData.position.width |
| | | let y = self.miniData.position.height |
| | | let x = self.miniData.position.x |
| | | let y = self.miniData.position.y |
| | | let newX = x - lastLocation.width + val.translation.width; |
| | | let newY = y - lastLocation.height + val.translation.height; |
| | | self.miniData.position = CGSize(width: newX, height: newY) |
| | | print(" onChanged \(index) \(self.miniData.position) ") |
| | | self.miniData.position = CGPoint(x: newX, y: newY) |
| | | //print(" onChanged \(index) \(self.miniData.position) ") |
| | | lastLocation = CGSize(width: 0, height: 0); |
| | | print(" onEnded \(val)") |
| | | }) |
| | | //print(" onEnded \(val)") |
| | | },MagnifyGesture().onChanged{ val in |
| | | if(lastZoom > 0){ |
| | | var l = val.magnification - lastZoom; |
| | | miniData.viewRate += l; |
| | | if(miniData.viewRate < 0.4){ |
| | | miniData.viewRate = 0.4 |
| | | }else{ |
| | | } |
| | | } |
| | | lastZoom = val.magnification; |
| | | }.onEnded{ val in |
| | | var l = val.magnification - lastZoom; |
| | | miniData.viewRate += l; |
| | | if(miniData.viewRate < 0.4){ |
| | | miniData.viewRate = 0.4 |
| | | }else{ |
| | | } |
| | | lastZoom = 0; |
| | | })) |
| | | } |
| | | |
| | | /** |
| | |
| | | actions.onMicClick() |
| | | } |
| | | Spacer() |
| | | if(self.miniData.streamType != .MICROPHONE && self.miniData.streamType != .UAC && self.miniData.streamType != .SYSTEM ){ |
| | | if(self.miniData.streamType != .MICROPHONE && self.miniData.streamType != .UAC && self.miniData.streamType != .SYSTEM && self.miniData.streamType != .CAMERA){ |
| | | IconButton(info: self.miniData.speakerState == .UN_MUTE ? Icons.SPEAKER : Icons.SPEAKER_MUTE){ |
| | | if(self.miniData.speakerState == .MUTE){ |
| | | self.miniData.speakerState = .UN_MUTE |
| | |
| | | }.frame(maxWidth: .infinity) |
| | | .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15)) |
| | | } |
| | | |
| | | func IconButton(info:IconInfo,action:@escaping ()->Void = {}, disabled:Bool = true,allow:Bool = true) -> some View{ |
| | | |
| | | func IconButton(info:IconInfo,action:@escaping ()->Void = {},width:CGFloat = 20,height:CGFloat = 20, disabled:Bool = true,allow:Bool = true) -> some View{ |
| | | Button(action:{ |
| | | print("IconButton ") |
| | | action() |
| | |
| | | .frame(width: info.size.width, height: info.size.height) |
| | | .aspectRatio(contentMode: .fit) |
| | | .foregroundColor(Color.white) |
| | | }.frame(width: 30,height: 30) |
| | | }.frame(width: width,height: height) |
| | | .contentShape(Rectangle()) // 明确指定可点击区域 |
| | | }.buttonStyle(TextBtnStyle()) |
| | | //.allowsHitTesting(allow) // ✅ 完全禁用点击(不可交互,不触发,不高亮) |
| | | //.disabled(disabled) // ✅ 禁用点击事件,但按钮可能变灰(系统默认行为) |
| | |
| | | struct MiniWindow_BottomBtns_Previews: PreviewProvider{ |
| | | @EnvironmentObject var miniData:MiniWindowData |
| | | static var previews: some View { |
| | | var miniData = MiniWindowData(streamType: StreamType.CAMERA) |
| | | var miniData = MiniWindowData(streamType: StreamType.CAMERA); |
| | | var miniDataBinding = Binding<MiniWindowData> { |
| | | miniData.hasAudio = false; |
| | | return miniData |
| | | } set: { MiniWindowData, Transaction in |
| | | |
| | | } |
| | | |
| | | MiniWindow(miniData:miniData).environmentObject(miniData); |
| | | MiniWindow(miniData:miniData).frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading) |
| | | .background( |
| | | GeometryReader { geometry in |
| | | Color.clear |
| | | .onAppear { |
| | | miniData.mainSize = geometry.size; |
| | | |
| | | print("displaySize:\(miniData.mainSize)") |
| | | } |
| | | .onChange(of: geometry.size) { newSize in |
| | | miniData.mainSize = newSize; |
| | | print("displaySize:\(miniData.mainSize)") |
| | | } |
| | | }) |
| | | .border(Color.red) |
| | | .onDisappear { |
| | | print("onDisappear 视图消失了!") |
| | | |
| | | }.onAppear { |
| | | print("onAppear 视图出现了!") |
| | | }.environmentObject(miniData); |
| | | } |
| | | |
| | | } |