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