New file |
| | |
| | | // |
| | | // MiniWindow.swift |
| | | // LiveProject |
| | | // 小窗 |
| | | // Created by 倪路朋 on 7/1/25. |
| | | // |
| | | import SwiftUI |
| | | |
| | | class MiniWindowData:ObservableObject,Identifiable{ |
| | | |
| | | var id:Int; |
| | | var streamType:StreamType |
| | | var remark:String? = nil |
| | | var listener:MiniWindowListener? = nil |
| | | |
| | | var audioDelay:Int64 = 0; |
| | | var videoDelay:Int64 = 0; |
| | | var volume:Double = 1.0; |
| | | |
| | | var hasAlpha = false; |
| | | |
| | | |
| | | init(streamType: StreamType) { |
| | | self.streamType = streamType |
| | | self.id = UUID().uuidString.hashValue |
| | | } |
| | | |
| | | @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 mainPosition:CGSize = CGSize(width: 0, height: 0) |
| | | @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; |
| | | @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 { |
| | | var onTopClick : () -> Void = {} |
| | | var onCloseClick : () -> Void = {} |
| | | var onRotateLeftClick : () -> Void = {} |
| | | var onRotateRightClick : () -> Void = {} |
| | | var onCameraRotateClick : () -> Void = {} |
| | | var onHomeClick : () -> Void = {} |
| | | var onImageClick : () -> Void = {} |
| | | var onMicClick : () -> Void = {} |
| | | var onSpeakerClick : () -> Void = {} |
| | | var onPaintClick : () -> Void = {} |
| | | var onMoreClick : () -> Void = {} |
| | | } |
| | | |
| | | private struct MiniWindowActionKey: EnvironmentKey { |
| | | static let defaultValue = MiniWindowActions() |
| | | } |
| | | |
| | | extension EnvironmentValues { |
| | | var miniWindowActions: MiniWindowActions { |
| | | get { self[MiniWindowActionKey.self] } |
| | | set { self[MiniWindowActionKey.self] = newValue } |
| | | } |
| | | } |
| | | struct MiniWindow: View{ |
| | | |
| | | @ObservedObject var miniData:MiniWindowData |
| | | @State var lastLocation = CGSize(width: 0, height: 0) |
| | | @Environment(\.miniWindowActions) var actions |
| | | |
| | | var body: some View { |
| | | ZStack(){ |
| | | VideoRendererView(pixelBuffer: $miniData.pixelBuffer).background(Color.black) |
| | | |
| | | //名称 |
| | | Text(self.miniData.name) |
| | | .font(Font.system(size: 16)) |
| | | .foregroundColor(Color.white) |
| | | .frame(width: .infinity, height: 40) |
| | | .frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .top) |
| | | if(self.miniData.streamType != .TEXT && self.miniData.streamType != .PICTURE |
| | | && self.miniData.hasVideo){ |
| | | //fps |
| | | Text("\(self.miniData.frameFPS)") |
| | | .font(Font.system(size: 16)) |
| | | .foregroundColor(Color.green) |
| | | .frame(width: .infinity, height: 40) |
| | | .padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 40)) |
| | | .frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topTrailing) |
| | | } |
| | | //序号 |
| | | HStack{ |
| | | Text("\(self.miniData.frameFPS)") |
| | | .font(Font.system(size: 16)) |
| | | .foregroundColor(Color.white) |
| | | .padding(EdgeInsets(top: 2, leading: 10, bottom: 2, trailing: 10)) |
| | | }.background(Color.gray) |
| | | .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)) |
| | | 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)) |
| | | Spacer() |
| | | IconButton(info: Icons.ROTATE_RIGHT,action:actions.onRotateRightClick).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)) |
| | | } |
| | | |
| | | BottomBtns().background(Color.gray.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) |
| | | .onChanged{ val in |
| | | let x = self.miniData.position.width |
| | | let y = self.miniData.position.height |
| | | 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) ") |
| | | lastLocation = val.translation; |
| | | }.onEnded{ val in |
| | | let x = self.miniData.position.width |
| | | let y = self.miniData.position.height |
| | | 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) ") |
| | | lastLocation = CGSize(width: 0, height: 0); |
| | | print(" onEnded \(val)") |
| | | }) |
| | | } |
| | | |
| | | /** |
| | | 小窗底部按钮 |
| | | */ |
| | | func BottomBtns() -> some View{ |
| | | HStack{ |
| | | if(self.miniData.streamType == .CAMERA){ |
| | | IconButton(info: Icons.ROTATE,action:actions.onCameraRotateClick) |
| | | Spacer() |
| | | } |
| | | if(self.miniData.streamType != .TEXT && self.miniData.streamType != .PICTURE |
| | | && self.miniData.hasVideo){ |
| | | IconButton(info: Icons.HOME,action:actions.onHomeClick) |
| | | Spacer() |
| | | } |
| | | if(self.miniData.hasVideo){ |
| | | IconButton(info: self.miniData.videoState == .IN_LIVE ? Icons.IMAGE : Icons.IMAGE_MUTE){ |
| | | if(self.miniData.videoState == .IN_LIVE){ |
| | | self.miniData.videoState = .OUT_LIVE |
| | | }else{ |
| | | self.miniData.videoState = .IN_LIVE |
| | | } |
| | | actions.onImageClick() |
| | | } |
| | | Spacer() |
| | | } |
| | | if(self.miniData.hasAudio){ |
| | | IconButton(info: self.miniData.audioState == .IN_LIVE ? Icons.MIC : Icons.MIC_MUTE){ |
| | | if(self.miniData.audioState == .IN_LIVE){ |
| | | self.miniData.audioState = .OUT_LIVE |
| | | }else{ |
| | | self.miniData.audioState = .IN_LIVE |
| | | } |
| | | actions.onMicClick() |
| | | } |
| | | Spacer() |
| | | if(self.miniData.streamType != .MICROPHONE && self.miniData.streamType != .UAC && self.miniData.streamType != .SYSTEM ){ |
| | | IconButton(info: self.miniData.speakerState == .UN_MUTE ? Icons.SPEAKER : Icons.SPEAKER_MUTE){ |
| | | if(self.miniData.speakerState == .MUTE){ |
| | | self.miniData.speakerState = .UN_MUTE |
| | | }else{ |
| | | self.miniData.speakerState = .MUTE |
| | | } |
| | | actions.onSpeakerClick() |
| | | } |
| | | Spacer() |
| | | } |
| | | } |
| | | if(self.miniData.streamType == .TEXT){ |
| | | IconButton(info: Icons.PAINT,action:actions.onPaintClick) |
| | | Spacer() |
| | | } |
| | | if(self.miniData.streamType != .TEXT && self.miniData.streamType != .PICTURE){ |
| | | IconButton(info: Icons.MORE,action:actions.onMoreClick) |
| | | } |
| | | }.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{ |
| | | Button(action:{ |
| | | print("IconButton ") |
| | | action() |
| | | }){ |
| | | ZStack{ |
| | | |
| | | Image(systemName: info.name) |
| | | .resizable() |
| | | .frame(width: info.size.width, height: info.size.height) |
| | | .aspectRatio(contentMode: .fit) |
| | | .foregroundColor(Color.white) |
| | | }.frame(width: 30,height: 30) |
| | | }.buttonStyle(TextBtnStyle()) |
| | | //.allowsHitTesting(allow) // ✅ 完全禁用点击(不可交互,不触发,不高亮) |
| | | //.disabled(disabled) // ✅ 禁用点击事件,但按钮可能变灰(系统默认行为) |
| | | } |
| | | |
| | | |
| | | } |
| | | |
| | | extension View { |
| | | |
| | | func onTopClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onTopClick = action |
| | | } |
| | | } |
| | | |
| | | func onCloseClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onCloseClick = action |
| | | } |
| | | } |
| | | func onRotateLeftClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onRotateLeftClick = action |
| | | } |
| | | } |
| | | func onRotateRightClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onRotateRightClick = action |
| | | } |
| | | } |
| | | func onCameraRotateClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onCameraRotateClick = action |
| | | } |
| | | } |
| | | func onHomeClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onHomeClick = action |
| | | } |
| | | } |
| | | func onImageClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onImageClick = action |
| | | } |
| | | } |
| | | func onMicClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onMicClick = action |
| | | } |
| | | } |
| | | func onSpeakerClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onSpeakerClick = action |
| | | } |
| | | } |
| | | func onPaintClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onPaintClick = action |
| | | } |
| | | } |
| | | func onMoreClick(_ action: @escaping () -> Void) -> some View { |
| | | self.transformEnvironment(\.miniWindowActions) { value in |
| | | value.onMoreClick = action |
| | | } |
| | | } |
| | | |
| | | } |
| | | struct MiniWindow_BottomBtns_Previews: PreviewProvider{ |
| | | @EnvironmentObject var miniData:MiniWindowData |
| | | static var previews: some View { |
| | | var miniData = MiniWindowData(streamType: StreamType.CAMERA) |
| | | var miniDataBinding = Binding<MiniWindowData> { |
| | | return miniData |
| | | } set: { MiniWindowData, Transaction in |
| | | |
| | | } |
| | | |
| | | MiniWindow(miniData:miniData).environmentObject(miniData); |
| | | } |
| | | |
| | | } |
| | | |
| | | |