From 660ae1326fdb1201f1f83b024abfcc8f9ab09184 Mon Sep 17 00:00:00 2001 From: mapleafgo Date: Fri, 8 Nov 2024 15:37:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E4=B8=89=E4=B8=AA=E7=82=B9=E4=BD=8D?= =?UTF-8?q?=E6=89=80=E6=9C=89=E5=8A=9F=E8=83=BD=E5=B7=B2=E8=B0=83=E9=80=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game.md | 53 ++++++++++++++++++++++++++++ internal/middleware/bgm.go | 3 +- internal/middleware/log.go | 15 ++++++++ internal/routes/play.go | 24 +++++++++++++ internal/routes/play/default.go | 19 +++++++++++ internal/routes/play/only_video.go | 17 +++++++++ internal/routes/wait.go | 46 ++++++++++++++++++++++--- internal/server.go | 24 ++++++------- pkg/{audio => utils}/link_audio.go | 10 ++++-- pkg/utils/link_video.go | 55 ++++++++++++++++++++++++++++++ pkg/video/paly.go | 46 +++++++++++++++++++++++++ puml/游戏.puml | 12 +++---- readme.md | 34 ++++++++++++++++++ 13 files changed, 331 insertions(+), 27 deletions(-) create mode 100644 internal/middleware/log.go create mode 100644 internal/routes/play.go create mode 100644 internal/routes/play/default.go create mode 100644 internal/routes/play/only_video.go rename pkg/{audio => utils}/link_audio.go (88%) create mode 100644 pkg/utils/link_video.go diff --git a/game.md b/game.md index e69de29..d7c1ece 100644 --- a/game.md +++ b/game.md @@ -0,0 +1,53 @@ +# 特殊游戏节点报文 + +## 入口 + +入口处不需要任何额外的处理 + +```json lines +{} +``` + +## 召唤神女 + +```json lines +{ + // 等待30s + "wait": 30 +} +``` + +## 镇水神力 + +```json lines +{ + // 播放文件地址,支持 file:// 本地文件地址、 http(s):// 远程文件地址 + "video": "", +} +``` + +## 神女除妖 + +除妖处不需要任何额外的处理,待游戏自然完成即可 + +```json lines +{} +``` + +## 神女授书 + +待定 + +```json lines +{ +} +``` + +## 青云龙台 + +待定 + +```json lines +{ +} +``` diff --git a/internal/middleware/bgm.go b/internal/middleware/bgm.go index 62da888..3678a05 100644 --- a/internal/middleware/bgm.go +++ b/internal/middleware/bgm.go @@ -4,6 +4,7 @@ import ( "game-driver/internal/schema" "game-driver/leaf" "game-driver/pkg/audio" + "game-driver/pkg/utils" "github.com/gopxl/beep/v2/speaker" "log" "sync" @@ -13,7 +14,7 @@ func PlayBgm() leaf.HandlerFunc { return func(c *leaf.Context) { pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey) - bgm := audio.LinkAudio(pm.BGM) + bgm := utils.LinkAudio(pm.BGM) if bgm != nil { // 等待组 var wait sync.WaitGroup diff --git a/internal/middleware/log.go b/internal/middleware/log.go new file mode 100644 index 0000000..5f1f002 --- /dev/null +++ b/internal/middleware/log.go @@ -0,0 +1,15 @@ +package middleware + +import ( + "game-driver/leaf" + "log" +) + +func RunLog() leaf.HandlerFunc { + return func(c *leaf.Context) { + log.Printf("收到消息, topic: %s, payload: %s", c.Topic, c.Payload) + defer log.Println("执行结束") + + c.Next() + } +} diff --git a/internal/routes/play.go b/internal/routes/play.go new file mode 100644 index 0000000..bee846d --- /dev/null +++ b/internal/routes/play.go @@ -0,0 +1,24 @@ +package routes + +import ( + "game-driver/internal/routes/play" + "game-driver/leaf" +) + +func PlayRouter(location string, point int) leaf.HandlerFunc { + switch location { + case "wushan": + return switchPoint(point) + default: + return play.Default + } +} + +func switchPoint(point int) leaf.HandlerFunc { + switch point { + case 3: + return play.OnlyVideo + default: + return play.Default + } +} diff --git a/internal/routes/play/default.go b/internal/routes/play/default.go new file mode 100644 index 0000000..523015f --- /dev/null +++ b/internal/routes/play/default.go @@ -0,0 +1,19 @@ +package play + +import ( + "game-driver/internal/middleware" + "game-driver/internal/schema" + "game-driver/leaf" + "time" +) + +func Default(c *leaf.Context) { + payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) + + if w, ok := payload.Game["wait"]; ok { + select { + case <-c.Done(): + case <-time.After(time.Duration(w.(int)) * time.Second): + } + } +} diff --git a/internal/routes/play/only_video.go b/internal/routes/play/only_video.go new file mode 100644 index 0000000..e1bc6f5 --- /dev/null +++ b/internal/routes/play/only_video.go @@ -0,0 +1,17 @@ +package play + +import ( + "game-driver/internal/middleware" + "game-driver/internal/schema" + "game-driver/leaf" + "game-driver/pkg/utils" + "game-driver/pkg/video" +) + +func OnlyVideo(c *leaf.Context) { + payload := leaf.Value[*schema.PlayModal](c, middleware.PayloadJSONKey) + + if url, ok := payload.Game["video"]; ok { + _ = video.Play(c, utils.LinkVideo(url.(string))) + } +} diff --git a/internal/routes/wait.go b/internal/routes/wait.go index dc9abc7..1d260ff 100644 --- a/internal/routes/wait.go +++ b/internal/routes/wait.go @@ -7,6 +7,8 @@ import ( "game-driver/pkg/audio" "game-driver/pkg/relay" "game-driver/pkg/tts" + "game-driver/pkg/utils" + "game-driver/pkg/video" "github.com/gopxl/beep/v2/speaker" "log" "sync" @@ -51,26 +53,32 @@ func WaitAction(c *leaf.Context) { switch item.Type { case schema.WaitAudio: // 执行音乐播放 + wait.Add(1) go func() { - wait.Add(1) defer wait.Done() audioAction(c, item, payload.TimeModel) }() case schema.WaitTTS: // 执行TTS播放 + wait.Add(1) go func() { - wait.Add(1) defer wait.Done() ttsAction(c, item, payload.TimeModel) }() case schema.WaitRelay: // 执行继电器供电 + wait.Add(1) go func() { - wait.Add(1) defer wait.Done() relayAction(c, item, payload.TimeModel) }() case schema.WaitVideo: + // 执行视频播放 + wait.Add(1) + go func() { + defer wait.Done() + videoAction(c, item, payload.TimeModel) + }() case schema.WaitWeb: default: log.Printf("不支持的类型: %d\n", item.Type) @@ -95,7 +103,7 @@ func audioAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeMod case <-timerAction(item.Start): { log.Println("开始执行后台任务") - data := audio.LinkAudio(item.Data) + data := utils.LinkAudio(item.Data) ctrl, closer := audio.PlayBgmMP3(data) defer closer() @@ -177,3 +185,33 @@ func relayAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeMod } } } + +func videoAction(c *leaf.Context, item schema.WaitItemModel, root schema.TimeModel) { + if item.Start != 0 && time.Unix(item.Start, 0).Before(time.Unix(root.Start, 0)) { + log.Println("开始时间小于根任务开始时间") + return + } + + if item.End != 0 { + cancel := leaf.WithDeadline(c, time.Unix(item.End, 0)) + defer cancel() + } + + select { + case <-c.Done(): + case <-timerAction(item.Start): + { + for { + err := video.Play(c, utils.LinkVideo(item.Data)) + if err != nil { + log.Panicln("视频播放异常: ", err) + } + select { + case <-c.Done(): + return + case <-time.After(time.Duration(item.Interval) * time.Second): + } + } + } + } +} diff --git a/internal/server.go b/internal/server.go index 98786ad..fa13752 100644 --- a/internal/server.go +++ b/internal/server.go @@ -28,6 +28,7 @@ func buildMqtt(c config.MqttConfig, r *leaf.Engine, subTopics ...string) autopah subscriptions := make([]paho.SubscribeOptions, 0) for _, topic := range subTopics { + log.Println("订阅主题: ", topic) subscriptions = append(subscriptions, paho.SubscribeOptions{Topic: topic, QoS: 0}) } @@ -75,15 +76,13 @@ func Run() { topicPrefix := fmt.Sprintf("server/%s/%v/", config.C.Location, config.C.Point) publishTopic := fmt.Sprintf("device/%s/%v/status", config.C.Location, config.C.Point) - log.Println("topicPrefix: ", topicPrefix) - ctx, cancel := context.WithCancel(context.Background()) defer cancel() router := leaf.Default(ctx) router.DefaultHandler(func(c *leaf.Context) { - log.Println("接收到未处理消息: " + string(c.Payload)) + log.Printf("未处理消息 topic: %s\n payload: %s\n", c.Topic, c.Payload) }) // 构建 MQTT 连接 @@ -114,6 +113,7 @@ func Run() { // 处理启动报文 router.RegisterHandler(topicPrefix+"play", + middleware.RunLog(), middleware.PayloadJSON[schema.PlayModal](), middleware.DeviceLock(device), middleware.EmergencyStop(common.GlobalStopper), @@ -122,22 +122,20 @@ func Run() { middleware.TimeoutOver(config.C.Game.MaxTimeout), middleware.TickerAction(), middleware.PlayBgm(), - func(c *leaf.Context) { - log.Println("接收到启动消息: ", string(c.Payload)) - select { - case <-c.Done(): - log.Println("执行结束") - } - }, + routes.PlayRouter(config.C.Location, config.C.Point), ) // 处理待机线程报文 router.RegisterHandler(topicPrefix+"wait", + middleware.RunLog(), middleware.PayloadJSON[schema.WaitModel](), middleware.EmergencyStop(common.GlobalBgStopper), routes.WaitAction, ) // 处理指令 - router.RegisterHandler(topicPrefix+"command", routes.Command(device)) + router.RegisterHandler(topicPrefix+"command", + middleware.RunLog(), + routes.Command(device), + ) // 启动完成发送一次设备状态 device.PublishStatus() @@ -150,8 +148,8 @@ func Run() { ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := cm.Disconnect(ctx); err != nil { - fmt.Printf("断开连接异常: %s\n", err) + if e := cm.Disconnect(ctx); e != nil { + fmt.Printf("断开连接异常: %s\n", e) } fmt.Println("关闭完成") diff --git a/pkg/audio/link_audio.go b/pkg/utils/link_audio.go similarity index 88% rename from pkg/audio/link_audio.go rename to pkg/utils/link_audio.go index 3f48930..bcb1179 100644 --- a/pkg/audio/link_audio.go +++ b/pkg/utils/link_audio.go @@ -1,4 +1,4 @@ -package audio +package utils import ( "bytes" @@ -47,15 +47,19 @@ func get(u string) io.ReadCloser { return resp.Body } +// LinkAudio 链接音频,解析链接,直接提取为数据流 func LinkAudio(link string) (bgm io.ReadCloser) { + if link == "" { + return nil + } u, err := url.Parse(link) if err != nil { log.Println("音频 URL 解析错误: ", err) } else { if u.Scheme == "file" { - bgm = open(u.String()) + bgm = open(link) } else if u.Scheme == "http" || u.Scheme == "https" { - bgm = get(u.String()) + bgm = get(link) } else { log.Printf("不支持的音频文件协议: %v\n", u.String()) return diff --git a/pkg/utils/link_video.go b/pkg/utils/link_video.go new file mode 100644 index 0000000..80034f1 --- /dev/null +++ b/pkg/utils/link_video.go @@ -0,0 +1,55 @@ +package utils + +import ( + "io" + "log" + "net/http" + "net/url" + "os" + "path" + "strings" +) + +// LinkVideo 链接视频,解析链接,网络文件会下载到临时目录并返回本地路径 +func LinkVideo(link string) (local string) { + if link == "" { + return + } + u, err := url.Parse(link) + if err != nil { + log.Println("音频 URL 解析错误: ", err) + } else { + if u.Scheme == "file" { + local, _ = strings.CutPrefix(link, "file://") + } else if u.Scheme == "http" || u.Scheme == "https" { + p, _ := url.PathUnescape(u.EscapedPath()) + tmpLocal := path.Join(os.TempDir(), path.Base(p)) + err = Download(link, tmpLocal) + if err != nil { + log.Println("音频文件下载失败: ", err) + return + } + local = tmpLocal + } else { + log.Printf("不支持的视频链接协议: %v\n", u.String()) + return + } + } + return +} + +// Download 下载文件 +func Download(link string, local string) (err error) { + resp, err := http.Get(link) + if err != nil { + return + } + defer resp.Body.Close() + f, err := os.OpenFile(local, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + return +} diff --git a/pkg/video/paly.go b/pkg/video/paly.go index 2c9ffe7..ccf3e4f 100644 --- a/pkg/video/paly.go +++ b/pkg/video/paly.go @@ -1 +1,47 @@ package video + +import ( + "bufio" + "context" + "io" + "log" + "os/exec" + "sync" +) + +func Play(ctx context.Context, file string) error { + if file == "" { + log.Println("video file is empty") + return nil + } + cmd := exec.CommandContext(ctx, "ffplay", "-autoexit", "-fs", file) + pipe, err := cmd.StderrPipe() + if err != nil { + return err + } + + var wait sync.WaitGroup + defer wait.Wait() + + a := make(chan struct{}) + defer close(a) + go func() { + wait.Add(1) + defer wait.Done() + for { + select { + case <-a: + default: + line, _, err := bufio.NewReader(pipe).ReadLine() + if err != nil { + if err == io.EOF { + return + } + break + } + log.Println(string(line)) + } + } + }() + return cmd.Run() +} diff --git a/puml/游戏.puml b/puml/游戏.puml index 4ef55b4..4325618 100644 --- a/puml/游戏.puml +++ b/puml/游戏.puml @@ -1,37 +1,37 @@ @startmindmap 游戏 + 游戏 -++ 入口 +++ 入口(0) +++ 待机 ++++ 背景音乐 +++ Game ++++ 播放欢迎语音 ++ 一阶段门头(无) -++ 召唤神女 +++ 召唤神女(1) +++ Game ++++ 等待结束 ++ 神女低语(无) +++ 按下按钮 +++ 播放语音 +++ 发送结果数据 -++ 镇水神力 +++ 镇水神力(2) +++ 待机 ++++ 投影仪待机 +++ Game ++++ 播放法阵视频 -- 二阶段门头(无) --- 神女除妖 +-- 神女除妖(3) --- Game ---- 控制设备启动 ---- 接收游戏结果 ---- 发送结果数据 -- 流光寻踪(无) --- 神女授书 +-- 神女授书(4) --- 待机 --- Game ---- 控制设备吐卡 ---- 播放取卡提示语音 ---- 异常状态处理 --- 青云龙台 +-- 青云龙台(5) --- 待机 ---- 网页默认页面 --- Game diff --git a/readme.md b/readme.md index 1b0eaae..dadd6af 100644 --- a/readme.md +++ b/readme.md @@ -149,3 +149,37 @@ Payload: ``` > 同一个类型的待机任务只能有一个,当有新的任务到达时会覆盖之前的任务 + + +## 说明 + +1. linux 下播放音频 + ```bash + sudo apt install libasound2-dev alsa-utils + ``` +2. linux 下播放视频 + ```bash + sudo apt install ffmpeg + ``` + 驱动安装 + ```bash + libdirectfb-dev + ``` +3. 当前用户加入播放音频与视频的组中 + ```bash + sudo usermod -aG audio,video $USER + ``` + +### 关闭屏幕帧缓冲 +```bash +# 关闭帧缓冲设备 +echo 1 | sudo tee /sys/class/graphics/fb0/blank + +# 重新打开帧缓冲设备 +echo 0 | sudo tee /sys/class/graphics/fb0/blank +``` + +### 播放视频 +```bash +ffplay -autoexit -fs -i video.mp4 +``` \ No newline at end of file