Runt
2025-07-27 7b3ecfffc59d2d980d9f7628365b64c20fe015be
LiveProject/activity/stream/views/MiniWindow.swift
@@ -26,22 +26,36 @@
    }
    
    @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 {
@@ -72,11 +86,25 @@
    
    @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)
@@ -104,44 +132,72 @@
                .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;
                }))
    }
    
    /**
@@ -179,7 +235,7 @@
                    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
@@ -201,8 +257,8 @@
        }.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()
@@ -214,7 +270,8 @@
                    .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) // ✅ 禁用点击事件,但按钮可能变灰(系统默认行为)
@@ -286,14 +343,35 @@
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);
    }
    
}