//
|
// 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);
|
}
|
|
}
|