//
|
// 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:16,height:9)
|
//视图比例(相对于 main 尺寸)
|
@Published var viewRate = 0.5
|
//位置
|
@Published var position:CGSize = CGSize(width: 0, height: 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 {
|
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)
|
@State var lastZoom = 0.0
|
@Environment(\.miniWindowActions) var actions
|
|
var body: some View {
|
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)
|
.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,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:{
|
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:{
|
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: 0))
|
}
|
|
BottomBtns().background(Color.black.opacity(0.4)).frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .bottom)
|
|
}.background(Color.blue).frame(width:width,height:height)
|
.offset(self.miniData.position)
|
.gesture(SimultaneousGesture(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)")
|
},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{
|
var w = miniData.mainSize.width * l / 2
|
self.miniData.position = CGSize(width: self.miniData.position.width - w , height: self.miniData.position.height - w)
|
}
|
}
|
lastZoom = val.magnification;
|
}.onEnded{ val in
|
var l = val.magnification - lastZoom;
|
miniData.viewRate += l;
|
if(miniData.viewRate < 0.4){
|
miniData.viewRate = 0.4
|
}else{
|
var w = miniData.mainSize.width * l / 2
|
self.miniData.position = CGSize(width: self.miniData.position.width - w , height: self.miniData.position.height - w)
|
}
|
lastZoom = 0;
|
}))
|
.onAppear{
|
|
print("onAppear body width = \(width) , height = \(height)")
|
}
|
}
|
|
/**
|
小窗底部按钮
|
*/
|
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 && 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
|
}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 = {},width:CGFloat = 20,height:CGFloat = 20) -> 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: width,height: height)
|
.contentShape(Rectangle()) // 明确指定可点击区域
|
}.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> {
|
miniData.hasAudio = false;
|
return miniData
|
} set: { MiniWindowData, Transaction in
|
|
}
|
|
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);
|
}
|
|
}
|