概述
此篇文章主要描述 监控客户端(video_client) 开发部分;
功能点
- 开启或关闭实时视频监控功能
- 根据输入的开始结束时间下载视频文件到指定文件夹
使用技术
- 语言:go
- 软件或框架:fyne(go语言的GUI框架)
- 应用层协议:HTTP
- 传输层协议:UDP,TCP
系统设计
此服务数据流概览
知识准备
- fyne: 用go语言写的一个GUI框架,支持跨平台;
实现部分
UI效果图
代码部分
main.go
package main
import (
"fmt"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"github.com/flopp/go-findfont"
"github.com/goki/freetype/truetype"
"os"
"time"
"video_client/gui_service"
"video_client/udp_client"
)
//
// @Description: main函数的defer操作,负责 udp服务关闭 及 通知udp服务端 停止发送视频帧 的信号
// @param MqttMessageChannel:
//
func MainDefer(MqttMessageChannel chan []byte) {
defer udp_client.UDPClose()
defer func() {
fmt.Println("程序退出前,发送停止直播信号")
// 连接udp服务,接收视频帧数据,并播放
MqttMessageChannel <- []byte("clientStop")
time.Sleep(1 * time.Second)
}()
}
//
// @Description: 初始化,功能为 让fyne框架支持中文显示
//
func init() {
fontPath, err := findfont.Find("Songti.ttc")
if err != nil {
panic(err)
}
fmt.Printf("Found 'arial.ttf' in '%s'\n", fontPath)
// load the font with the freetype library
// 原作者使用的ioutil.ReadFile已经弃用
fontData, err := os.ReadFile(fontPath)
if err != nil {
panic(err)
}
_, err = truetype.Parse(fontData)
if err != nil {
panic(err)
}
_ = os.Setenv("FYNE_FONT", fontPath)
}
//
// @Description: UDP相关配置运行
// @param MqttMessageChannel:
// @param img:
//
func SetUp(MqttMessageChannel chan []byte, img *canvas.Image) {
//udp客户端启动并接收数据
udp_client.UDPClientSetUp()
go udp_client.UDPSend(MqttMessageChannel)
go udp_client.UDPReceive(img)
}
//
// @Description:
//
func main() {
//GUI初始化
myApp := app.New()
myWindow := myApp.NewWindow("Video Client")
//第一行 视频播放窗口
img, one := gui_service.FirstLineButton()
//第二行 直播/停止直播 按钮
MqttMessageChannel := make(chan []byte, 20)
SetUp(MqttMessageChannel, img)
defer MainDefer(MqttMessageChannel)
two := gui_service.SecondLineButton(MqttMessageChannel)
//第三行 输入框 和 筛选按钮 放在一个子 水平布局中
startTimeEntry, endTimeEntry, three := gui_service.ThirdLineButton()
//第四行,功能是 选择文件夹及下载按钮
four := gui_service.FourthLineButton(myWindow, startTimeEntry, endTimeEntry)
//将四行放到一个window中
all := container.NewVBox(one, two, three, four)
myWindow.SetContent(all)
//fyne gui框架运行及展示
myWindow.ShowAndRun()
}
gui.go
//
// All rights reserved
// create time '2023/2/14 16:48'
//
// Usage:
//
package gui_service
import (
"encoding/json"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/asmcos/requests"
"video_client/utils/constant"
"video_client/utils/gtime"
"video_client/utils/requests_handler"
)
//
// @Description: 第一行 视频播放窗口
// @return *canvas.Image:
// @return *fyne.Container:
//
func FirstLineButton() (*canvas.Image, *fyne.Container) {
img := canvas.NewImageFromResource(theme.FyneLogo())
img.FillMode = canvas.ImageFillOriginal
//first := container.NewGridWithColumns(1, img)
first := container.New(
layout.NewGridWrapLayout(fyne.NewSize(500, 500)), img)
return img, first
}
//
// @Description: 第二行 直播/停止直播按钮及操作
// @param MqttMessageChannel:
// @param img:
// @return *fyne.Container:
//
func SecondLineButton(MqttMessageChannel chan []byte) *fyne.Container {
//第二行 监控指标按钮
playButton := widget.NewButton("开始直播", func() {
fmt.Println("点击直播按钮了")
// 连接udp服务,接收视频帧数据,并播放
MqttMessageChannel <- []byte("clientPlay")
})
playButton.Icon = theme.ConfirmIcon()
stopButton := widget.NewButton("停止直播", func() {
fmt.Println("点击停止直播按钮了")
// 连接udp服务,接收视频帧数据,并播放
MqttMessageChannel <- []byte("clientStop")
})
stopButton.Icon = theme.CancelIcon()
return container.NewGridWithColumns(2, playButton, stopButton)
}
//
// @Description: 第三行 输入框 和 筛选按钮及相关操作函数
// @return *widget.Entry:
// @return *widget.Entry:
// @return *fyne.Container:
//
func ThirdLineButton() (*widget.Entry, *widget.Entry, *fyne.Container) {
startTimeEntry := widget.NewEntry()
startTimeEntry.SetPlaceHolder("输入开始时间")
startTimeEntry.SetText(gtime.GetCurrentTime())
endTimeEntry := widget.NewEntry()
endTimeEntry.SetPlaceHolder("输入结束时间")
three := container.NewGridWithColumns(2, startTimeEntry, endTimeEntry)
return startTimeEntry, endTimeEntry, three
}
//
// @Description: 第四行 选择文件夹及下载按钮 和对应函数操作
// @param myWindow:
// @param startTimeEntry:
// @param endTimeEntry:
// @return *fyne.Container:
//
func FourthLineButton(myWindow fyne.Window, startTimeEntry *widget.Entry, endTimeEntry *widget.Entry) *fyne.Container {
var DownloadDir string
HistoryVideoDownloadFunc := func() {
defer RecoverAllPanic(myWindow)
//根据开始/结束时间 查看时间范围内的 文件名列表
resp, err := requests.PostJson(fmt.Sprintf("%v/video/query", constant.VideoServer), map[string]interface{}{"start_time": startTimeEntry.Text, "end_time": endTimeEntry.Text})
if err != nil {
dialog.ShowInformation("request error", fmt.Sprintf("%v", err), myWindow)
return
}
respData, err := requests_handler.ResponseCheck(resp)
if err != nil {
dialog.ShowInformation("response check error", fmt.Sprintf("%v", err), myWindow)
return
}
if respData["result"] != nil {
files := respData["result"].([]interface{})
// 开始下载
for _, file := range files {
x, err := json.Marshal(map[string]interface{}{"FileName": file})
if err != nil {
dialog.ShowInformation("json marshal error", fmt.Sprintf("%v", err), myWindow)
return
}
//下载文件
resp, err := requests.Get(fmt.Sprintf("%v/video/download?FileName=%v", constant.VideoServer, file), x)
if err != nil {
dialog.ShowInformation("download status error", fmt.Sprintf("%v", err), myWindow)
return
}
err = resp.SaveFile(fmt.Sprintf("%v/%v", DownloadDir, file))
if err != nil {
dialog.ShowInformation("download error", fmt.Sprintf("%v", err), myWindow)
return
}
}
}
dialog.ShowInformation("下载成功", fmt.Sprintf("请在 %v 文件夹中查看下载的文件", DownloadDir), myWindow)
}
filterButton := widget.NewButton("历史视频下载", HistoryVideoDownloadFunc)
filterButton.Icon = theme.ConfirmIcon()
DirButton := widget.NewButton("选择下载文件夹", func() {
dialog.ShowFolderOpen(func(closer fyne.ListableURI, err error) {
if err != nil {
dialog.ShowInformation("open dir error", fmt.Sprintf("%v", err), myWindow)
return
}
if closer != nil {
DownloadDir = closer.Path()
dialog.ShowInformation("下载文件夹选择成功", fmt.Sprintf("%v", DownloadDir), myWindow)
}
}, myWindow)
})
filterButton.Icon = theme.ConfirmIcon()
return container.NewGridWithColumns(2, DirButton, filterButton)
}
udp.go
//
// All rights reserved
// create time '2022/12/9 16:56'
//
// Usage:
//
package udp_client
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"net"
"video_client/utils/constant"
)
var UDPListen *net.UDPConn
//
// @Description: 启动udp客户端
//
func UDPClientSetUp() {
var err error
UDPListen, err = net.DialUDP("udp", nil, &net.UDPAddr{
IP: constant.UdpServerIp,
Port: constant.UdpServerPort,
})
if err != nil {
panic(fmt.Sprintf("监听UDP服务失败,请修改,err:", err))
}
fmt.Println(fmt.Sprintf("连接UDP服务成功,端口为:%v", constant.UdpServerPort))
return
}
//
// @Description: 收尾性工作;关闭udp client
//
func UDPClose() {
err := UDPListen.Close()
if err != nil {
panic(fmt.Sprintf("关闭UDPListen失败,err:%v", err))
}
}
//
// @Description: 接收UDP服务端的图片数据,并展示给gui(直播使用)
// @param Img:
//
func UDPReceive(Img *canvas.Image) {
for {
//定义一个长度为10万的切片
sliceData := make([]byte, 100000, 100000)
//将读取的数据存到数组中
n, err := UDPListen.Read(sliceData)
if err != nil {
fmt.Println("读取UDP数据失败,err:", err)
continue
}
if n == 0 {
continue
}
//将读取的视频帧数据 以图片方式展示出来
res := &fyne.StaticResource{
StaticName: "test",
StaticContent: sliceData,
}
Img.Resource = res
Img.Refresh()
}
}
//
// @Description: 接收channel中的message 信号数据,并发送数据给服务端
// @param MqttMessageChannel:
//
func UDPSend(MqttMessageChannel <-chan []byte) {
for {
message, ok := <-MqttMessageChannel
if !ok {
fmt.Println("发送数据到图片处理channel关闭")
break
}
b, err := UDPListen.Write(message)
if err != nil {
panic(fmt.Sprintf("发送UDP数据失败 err:%v", err))
}
fmt.Println("发送数据 %v", b)
}
}