From 3b7521a47ae731f0bf0a922822e4417493489539 Mon Sep 17 00:00:00 2001
From: Runt <qingingrunt2010@qq.com>
Date: Wed, 09 Jul 2025 10:42:02 +0000
Subject: [PATCH] 小窗布局,小窗移动

---
 LiveProject/enum/Icons.swift                       |   15 +
 LiveProject/activity/stream/LiveViewModel.swift    |    1 
 LiveProject/enum/MuteState.swift                   |   11 +
 LiveProject/listener/MiniWindowListener.swift      |   32 ++++
 LiveProject/enum/LiveState.swift                   |   13 +
 LiveProject/activity/stream/LiveActivity.swift     |   22 ++
 LiveProject/enum/StreamType.swift                  |    1 
 LiveProject/activity/stream/views/MiniWindow.swift |  301 +++++++++++++++++++++++++++++++++++++++++++
 LiveProject/shape/RoundedCorner.swift              |   22 +++
 9 files changed, 410 insertions(+), 8 deletions(-)

diff --git a/LiveProject/activity/stream/LiveActivity.swift b/LiveProject/activity/stream/LiveActivity.swift
index 57e7890..045e563 100644
--- a/LiveProject/activity/stream/LiveActivity.swift
+++ b/LiveProject/activity/stream/LiveActivity.swift
@@ -25,6 +25,7 @@
                                   DeviceInfo(name: "话筒", type: .MICROPHONE,deviceId: UUID().uuidString,icon: Icons.MIC),
                                   DeviceInfo(name: "系统", type: .SYSTEM,deviceId : UUID().uuidString,icon: Icons.PORTRAIT)]
 
+    @State private var miniWindows:Array<MiniWindowData> = []
     
     @StateObject private var mViewModel = LiveViewModel()
     
@@ -32,10 +33,14 @@
         ZStack{
             Color.clear
                 .ignoresSafeArea() // 填满全屏
-            VStack{
+            ZStack{
                 VideoRendererView(pixelBuffer: $mViewModel.pixelBuffer).background(Color.black).frame(width: mainSize.width,height:mainSize.height)
-                Spacer()
+                    .frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topTrailing)
+                ForEach(miniWindows, id: \.id) { miniWindow in
+                    NewMiniWindow(miniWindow: miniWindow)
+                }
             }.border(Color.blue)
+            
             VStack{
                 Spacer()
                 BottomBtns().frame(alignment: .bottom).border(Color.green)
@@ -156,8 +161,9 @@
                                 withAnimation{
                                     showDeviceDialog = false;
                                 }
+                                miniWindows.append(MiniWindowData(streamType: device.type))
                             }
-                            print("\(device.name) click")
+                            print("\(device.name) click \(self.miniWindows.count)")
                         }
                     }
                 }
@@ -221,6 +227,16 @@
             }
         }
     }
+    
+    func NewMiniWindow(miniWindow:MiniWindowData) -> some View{
+        MiniWindow(miniData: miniWindow)
+            .frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .topLeading)
+            .onCloseClick {
+                guard let index = miniWindows.firstIndex(where: { $0.id == miniWindow.id }) else { return }
+                miniWindows.remove(at: index)
+            }
+    }
+    
 }
 
 
diff --git a/LiveProject/activity/stream/LiveViewModel.swift b/LiveProject/activity/stream/LiveViewModel.swift
index ab408ed..bfc3671 100644
--- a/LiveProject/activity/stream/LiveViewModel.swift
+++ b/LiveProject/activity/stream/LiveViewModel.swift
@@ -62,6 +62,7 @@
             }
             break;
         default:
+            completion(true)
             break;
         }
     }
diff --git a/LiveProject/activity/stream/views/MiniWindow.swift b/LiveProject/activity/stream/views/MiniWindow.swift
new file mode 100644
index 0000000..357e6a0
--- /dev/null
+++ b/LiveProject/activity/stream/views/MiniWindow.swift
@@ -0,0 +1,301 @@
+//
+//  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);
+    }
+    
+}
+
+
diff --git a/LiveProject/enum/Icons.swift b/LiveProject/enum/Icons.swift
index c1b1d7b..56b2ca6 100644
--- a/LiveProject/enum/Icons.swift
+++ b/LiveProject/enum/Icons.swift
@@ -9,16 +9,21 @@
 struct Icons{
     static let CAMERA = IconInfo(name: "camera",size: CGSize(width: 25, height: 20))
     static let MIC = IconInfo(name: "mic",size: CGSize(width: 15, height: 23))
-    static let MIC_MUTE = IconInfo(name: "mic.slash",size: CGSize(width: 20, height: 23))
+    static let MIC_MUTE = IconInfo(name: "mic.slash",size: CGSize(width: 18, height: 20))
     static let PORTRAIT = IconInfo(name: "ipad",size: CGSize(width: 18, height: 23))
     static let LANDSCAPE = IconInfo(name: "ipad.landscape",size: CGSize(width: 25, height: 20))
     static let BACK = IconInfo(name: "arrow.left",size: CGSize(width: 25, height: 20))
+    static let CLOSE = IconInfo(name: "xmark",size: CGSize(width: 22, height: 22))
+    
     static let SPEAKER = IconInfo(name: "speaker",size: CGSize(width: 18, height: 23))
     static let SPEAKER_MUTE = IconInfo(name: "speaker.slash",size: CGSize(width: 18, height: 23))
-    static let IMAGE = IconInfo(name: "photo",size: CGSize(width: 25, height: 23))
-    static let IMAGE_MUTE = IconInfo(name: "photo.slash",size: CGSize(width: 25, height: 23))
-    static let ROTATE_LEFT = IconInfo(name: "rotate.left",size: CGSize(width: 25, height: 25))
-    static let ROTATE_RIGHT = IconInfo(name: "rotate.right",size: CGSize(width: 25, height: 25))
+    static let IMAGE = IconInfo(name: "video",size: CGSize(width: 28, height: 19))
+    static let IMAGE_MUTE = IconInfo(name: "video.slash",size: CGSize(width: 25, height: 20))
+    static let ROTATE_LEFT = IconInfo(name: "rotate.left",size: CGSize(width: 23, height: 25))
+    static let ROTATE_RIGHT = IconInfo(name: "rotate.right",size: CGSize(width: 23, height: 25))
     static let INFO = IconInfo(name: "info.circle",size: CGSize(width: 25, height: 25))
     static let PAINT = IconInfo(name: "paintpalette",size: CGSize(width: 25, height: 25))
+    static let HOME = IconInfo(name: "house",size: CGSize(width: 25, height: 23))
+    static let ROTATE = IconInfo(name: "camera.rotate",size: CGSize(width: 25, height: 20))
+    static let MORE = IconInfo(name: "ellipsis",size: CGSize(width: 25, height: 5))
 }
diff --git a/LiveProject/enum/LiveState.swift b/LiveProject/enum/LiveState.swift
new file mode 100644
index 0000000..dd2da1f
--- /dev/null
+++ b/LiveProject/enum/LiveState.swift
@@ -0,0 +1,13 @@
+//
+//  LiveState.swift
+//  LiveProject
+//
+//  Created by 倪路朋 on 7/5/25.
+//
+
+
+enum LiveState{
+    case
+    IN_LIVE,OUT_LIVE
+    //直播中,未直播
+}
diff --git a/LiveProject/enum/MuteState.swift b/LiveProject/enum/MuteState.swift
new file mode 100644
index 0000000..81cd0fd
--- /dev/null
+++ b/LiveProject/enum/MuteState.swift
@@ -0,0 +1,11 @@
+//
+//  MuteState.swift
+//  LiveProject
+//
+//  Created by 倪路朋 on 7/5/25.
+//
+
+
+enum MuteState {
+    case MUTE,UN_MUTE
+}
diff --git a/LiveProject/enum/StreamType.swift b/LiveProject/enum/StreamType.swift
index c6ea454..30efca5 100644
--- a/LiveProject/enum/StreamType.swift
+++ b/LiveProject/enum/StreamType.swift
@@ -8,6 +8,7 @@
 
 enum StreamType : Int{
     case
+    NONE = -1,
     //系统
     SYSTEM = 0,
     //本地类型
diff --git a/LiveProject/listener/MiniWindowListener.swift b/LiveProject/listener/MiniWindowListener.swift
new file mode 100644
index 0000000..7584d80
--- /dev/null
+++ b/LiveProject/listener/MiniWindowListener.swift
@@ -0,0 +1,32 @@
+//
+//  MiniWindowListener.swift
+//  LiveProject
+//
+//  Created by 倪路朋 on 7/5/25.
+//
+
+class MiniWindowListener{
+   /**
+    * 执行加载,连接成功
+    */
+   var onStarted : (() -> Void?)? = nil
+   /**
+    * 执行加载,连接失败
+    */
+   var onFailed : ((Int) -> Void?)? = nil
+   /**
+    * 中途执行出错断开
+    */
+   var onBroken : ((Int) -> Void?)? = nil
+
+   /**
+    * 画面变化
+    */
+   var onViewUpdate : (()->Void)? = nil
+
+   /**
+    * 停止
+    */
+   var onStop:(() -> Void?)? = nil;
+
+}
diff --git a/LiveProject/shape/RoundedCorner.swift b/LiveProject/shape/RoundedCorner.swift
new file mode 100644
index 0000000..471ad0e
--- /dev/null
+++ b/LiveProject/shape/RoundedCorner.swift
@@ -0,0 +1,22 @@
+//
+//  Untitled.swift
+//  LiveProject
+//
+//  Created by 倪路朋 on 7/5/25.
+//
+
+import SwiftUI
+
+struct RoundedCorner: Shape {
+    var radius: CGFloat = 10
+    var corners: UIRectCorner = .allCorners
+
+    func path(in rect: CGRect) -> Path {
+        let path = UIBezierPath(
+            roundedRect: rect,
+            byRoundingCorners: corners,
+            cornerRadii: CGSize(width: radius, height: radius)
+        )
+        return Path(path.cgPath)
+    }
+}

--
Gitblit v1.9.1