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