From 1cbc087ddc6da7401ccfb9e26100cb363690bb9d Mon Sep 17 00:00:00 2001 From: Shugo Kawamura Date: Mon, 4 Dec 2023 18:36:22 -0500 Subject: [PATCH] =?UTF-8?q?=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8?= =?UTF-8?q?=E3=81=AE=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.go | 127 +++++++++++++ scraper/cmd/main.go | 28 --- server/main.go | 251 ------------------------- {scraper => server/scraper}/scraper.go | 0 server/vmix_utility.go | 160 ++++++++++++++++ 5 files changed, 287 insertions(+), 279 deletions(-) create mode 100644 main.go delete mode 100644 scraper/cmd/main.go delete mode 100644 server/main.go rename {scraper => server/scraper}/scraper.go (100%) create mode 100644 server/vmix_utility.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..4c42748 --- /dev/null +++ b/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net/http" + "os/exec" + "runtime" + + "embed" + + vmixutility "github.com/FlowingSPDG/vmix-utility/server/core" + "github.com/gin-gonic/gin" +) + +// Static files +// +//go:embed static/* +var staticFS embed.FS + +//go:embed vMixMultiview/* +var multiviewFS embed.FS + +func main() { + log.Println("STARTING...") + + // Parse flags + vmixAddr := "" + hostPort := 0 + flag.StringVar(&vmixAddr, "vmix", "http://localhost:8088", "vMix API Address") + flag.IntVar(&hostPort, "host", 8080, "Server listen port") + flag.Parse() + + // Init utility instance + util, err := vmixutility.NewUtilityClient(hostPort, vmixAddr) + if err != nil { + panic(err) + } + + // Init Gin router + gin.SetMode(gin.ReleaseMode) + r := gin.Default() + + // Cache files + index, err := staticFS.ReadFile("static/index.html") + if err != nil { + panic(err) + } + + favicon, err := staticFS.ReadFile("static/favicon.ico") + if err != nil { + panic(err) + } + // serve static files + r.GET("/", func(c *gin.Context) { + c.Writer.WriteString(string(index)) + }) + r.GET("/favicon.ico", func(c *gin.Context) { + c.Data(http.StatusOK, "image/x-icon", favicon) + }) + r.GET("/css/*file", func(c *gin.Context) { + file := c.Param("file") + b, err := staticFS.ReadFile("static/css" + file) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.Data(http.StatusOK, "text/css", b) + }) + r.GET("/js/*file", func(c *gin.Context) { + file := c.Param("file") + b, err := staticFS.ReadFile("static/js" + file) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.Data(http.StatusOK, "text/css", b) + }) + r.GET("/img/*file", func(c *gin.Context) { + file := c.Param("file") + b, err := staticFS.ReadFile("static/img" + file) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.Data(http.StatusOK, "text/css", b) + }) + r.GET("/fonts/*file", func(c *gin.Context) { + file := c.Param("file") + b, err := staticFS.ReadFile("static/fonts" + file) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.Data(http.StatusOK, "text/css", b) + }) + r.GET("/multiviewer/*file", func(c *gin.Context) { + file := c.Param("file") + b, err := multiviewFS.ReadFile("vMixMultiview" + file) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + c.Data(http.StatusOK, "", b) + }) + + api := r.Group("/api") + { + api.GET("/vmix", util.GetvMixURLHandler) + api.GET("/shortcuts", util.GetvMixShortcuts) + api.GET("/inputs", util.GetInputsHandler) + api.POST("/refresh", util.RefreshInputHandler) + api.POST("/multiple", util.DoMultipleFunctionsHandler) + } + + // Windowsの場合、自動的にブラウザを開く + if runtime.GOOS == "windows" { + url := fmt.Sprintf("http://localhost:%d/", hostPort) + if err := exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", url).Start(); err != nil { + log.Println("Failed to open link. ignoring...") + log.Printf("ERR : %v\n", err) + } + } + + log.Panicf("Failed to listen port %s : %v\n", hostPort, r.Run(fmt.Sprintf(":%d", hostPort))) +} diff --git a/scraper/cmd/main.go b/scraper/cmd/main.go deleted file mode 100644 index b941423..0000000 --- a/scraper/cmd/main.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - - "github.com/FlowingSPDG/vmix-utility/scraper" -) - -var ( - helpVer int -) - -func main() { - flag.IntVar(&helpVer, "helpver", 25, "vMix help version") - flag.Parse() - - shortcuts, err := scraper.GetShortcuts(helpVer) - if err != nil { - log.Fatalln(err) - } - - fmt.Printf("Got %d Functions\n", len(shortcuts)) - for i, f := range shortcuts { - fmt.Printf("Shortcut[%d] : %s(%s) . Queries:%v\n", i, f.Name, f.Description, f.Parameters) - } -} diff --git a/server/main.go b/server/main.go deleted file mode 100644 index 64463a7..0000000 --- a/server/main.go +++ /dev/null @@ -1,251 +0,0 @@ -package main - -import ( - "embed" - "flag" - "fmt" - "log" - "net/http" - "os/exec" - "runtime" - "strings" - "sync" - - "github.com/gin-gonic/gin" - - vmixgo "github.com/FlowingSPDG/vmix-go" - "github.com/FlowingSPDG/vmix-utility/scraper" -) - -// vMix variables -var ( - hostaddr *string // API Listen host - vmixaddr *string // Target vMix host address - shortcuts []scraper.Shortcut // vMix functions slice. TODO! - vmix *vmixgo.Vmix -) - -// Static files -//go:embed static/* -var staticFS embed.FS - -//go:embed vMixMultiview/* -var multiviewFS embed.FS - -// GetvMixURLHandler returns vMix API Endpoint. -func GetvMixURLHandler(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "url": *vmixaddr, - }) -} - -// GetvMixURLHandler returns vMix API Endpoint. -func GetvMixShortcuts(c *gin.Context) { - if shortcuts == nil { - s, err := scraper.GetShortcuts(25) - if err != nil { - c.AbortWithError(http.StatusInternalServerError, err) - return - } - shortcuts = s - } - - c.JSON(http.StatusOK, shortcuts) -} - -// RefreshInputHandler returns vMix API Endpoint. -func RefreshInputHandler(c *gin.Context) { - var err error - vmix, err = vmix.Refresh() - if err != nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - "err": err.Error(), - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "inputs": vmix.Inputs.Input, - }) -} - -// GetInputsHandler returns available vmix inputs for [GET] /api/inputs as JSON. -func GetInputsHandler(c *gin.Context) { - if vmix == nil { - c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ - "error": "vmix instance not loaded", - }) - return - } - if vmix.Inputs.Input == nil { - c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ - "error": "Input not loaded", - }) - return - } - c.JSON(http.StatusOK, gin.H{ - "inputs": vmix.Inputs.Input, - }) - return -} - -// DoMultipleFunctionsRequest Request JSON for DoMultipleFunctionsHandler -type DoMultipleFunctionsRequest struct { - Function string `json:"function"` // function name. e.g. "Fade" . - Queries []struct { - Key string `json:"key"` // Key. - Value string `json:"value"` // Value. - } `json:"queries"` // Key-Value queries. - Num int `json:"num"` -} - -// Validate form -func (r *DoMultipleFunctionsRequest) Validate() error { - if strings.TrimSpace(r.Function) == "" { - return fmt.Errorf("Function empty") - } - for _, v := range r.Queries { - if v.Key == "" || v.Value == "" { - return fmt.Errorf("Invalid queries") - } - } - if r.Num <= 0 { - return fmt.Errorf("Invalid Number length") - } - return nil -} - -// DoMultipleFunctionsHandler Sends multiple functions to vMix. -func DoMultipleFunctionsHandler(c *gin.Context) { - req := DoMultipleFunctionsRequest{} - c.BindJSON(&req) - if err := req.Validate(); err != nil { - c.AbortWithError(http.StatusBadRequest, err) - return - } - params := make(map[string]string) - for _, v := range req.Queries { - params[v.Key] = v.Value - } - - wg := &sync.WaitGroup{} - numerrors := 0 - for i := 0; i < req.Num; i++ { - wg.Add(1) - go func() { - if err := vmix.SendFunction(req.Function, params); err != nil { - numerrors++ - log.Printf("Error sending function %s with %v queries. ERR : %v\n", req.Function, params, err) - } - wg.Done() - }() - } - wg.Wait() - if numerrors == 0 { - c.String(http.StatusOK, "Done with no errors") - } else { - c.String(http.StatusAccepted, fmt.Sprintf("Done with %d errors", numerrors)) - } -} - -func init() { - vmixaddr = flag.String("vmix", "http://localhost:8088", "vMix API Address") - hostaddr = flag.String("host", ":8080", "Server listen port") - flag.Parse() -} - -func main() { - log.Println("STARTING...") - - // Init vMix - var err error - vmix, err = vmixgo.NewVmix(*vmixaddr) - if err != nil { - panic(err) - } - - // Init Gin router - gin.SetMode(gin.ReleaseMode) - r := gin.Default() - - // Cache files - index, err := staticFS.ReadFile("static/index.html") - if err != nil { - panic(err) - } - - favicon, err := staticFS.ReadFile("static/favicon.ico") - if err != nil { - panic(err) - } - // serve static files - r.GET("/", func(c *gin.Context) { - c.Writer.WriteString(string(index)) - }) - r.GET("/favicon.ico", func(c *gin.Context) { - c.Data(http.StatusOK, "image/x-icon", favicon) - }) - r.GET("/css/*file", func(c *gin.Context) { - file := c.Param("file") - b, err := staticFS.ReadFile("static/css" + file) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - c.Data(http.StatusOK, "text/css", b) - }) - r.GET("/js/*file", func(c *gin.Context) { - file := c.Param("file") - b, err := staticFS.ReadFile("static/js" + file) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - c.Data(http.StatusOK, "text/css", b) - }) - r.GET("/img/*file", func(c *gin.Context) { - file := c.Param("file") - b, err := staticFS.ReadFile("static/img" + file) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - c.Data(http.StatusOK, "text/css", b) - }) - r.GET("/fonts/*file", func(c *gin.Context) { - file := c.Param("file") - b, err := staticFS.ReadFile("static/fonts" + file) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - c.Data(http.StatusOK, "text/css", b) - }) - r.GET("/multiviewer/*file", func(c *gin.Context) { - file := c.Param("file") - b, err := multiviewFS.ReadFile("vMixMultiview" + file) - if err != nil { - c.AbortWithError(http.StatusNotFound, err) - return - } - c.Data(http.StatusOK, "", b) - }) - - api := r.Group("/api") - { - api.GET("/vmix", GetvMixURLHandler) - api.GET("/shortcuts", GetvMixShortcuts) - api.GET("/inputs", GetInputsHandler) - api.POST("/refresh", RefreshInputHandler) - api.POST("/multiple", DoMultipleFunctionsHandler) - } - - url := fmt.Sprintf("http://localhost%s/", *hostaddr) - if runtime.GOOS == "windows" { - if err := exec.Command("rundll32.exe", "url.dll,FileProtocolHandler", url).Start(); err != nil { - log.Println("Failed to open link. ignoring...") - log.Printf("ERR : %v\n", err) - } - } - - log.Panicf("Failed to listen port %s : %v\n", *hostaddr, r.Run(*hostaddr)) -} diff --git a/scraper/scraper.go b/server/scraper/scraper.go similarity index 100% rename from scraper/scraper.go rename to server/scraper/scraper.go diff --git a/server/vmix_utility.go b/server/vmix_utility.go new file mode 100644 index 0000000..f7d1d54 --- /dev/null +++ b/server/vmix_utility.go @@ -0,0 +1,160 @@ +package vmixutility + +import ( + "fmt" + "log" + "net/http" + "strings" + "sync" + + "github.com/gin-gonic/gin" + + vmixgo "github.com/FlowingSPDG/vmix-go" + "github.com/FlowingSPDG/vmix-utility/server/scraper" +) + +type utilityClient struct { + hostPort int // API Listen port to listen + vmixAddr string // Target vMix host address + vmix *vmixgo.Vmix // vMix instance. Never be nil but could be disconnected. + shortcuts []scraper.Shortcut // vMix Shortcuts. Neber be nil but could be empty. +} + +type UtilityClient interface { + GetvMixURLHandler(c *gin.Context) + GetvMixShortcuts(c *gin.Context) + RefreshInputHandler(c *gin.Context) + GetInputsHandler(c *gin.Context) + DoMultipleFunctionsHandler(c *gin.Context) +} + +func NewUtilityClient(hostPort int, vmixAddr string) (UtilityClient, error) { + vmix, err := vmixgo.NewVmix(vmixAddr) + if err != nil { + return nil, err + } + return &utilityClient{ + hostPort: hostPort, + vmixAddr: vmixAddr, + vmix: vmix, + shortcuts: []scraper.Shortcut{}, + }, nil +} + +// GetvMixURLHandler returns vMix API Endpoint. +func (u *utilityClient) GetvMixURLHandler(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "url": u.vmixAddr, + }) +} + +// GetvMixURLHandler returns vMix API Endpoint. +func (u *utilityClient) GetvMixShortcuts(c *gin.Context) { + if u.shortcuts == nil { + s, err := scraper.GetShortcuts(26) // TODO: 実際のバージョンを使用する + if err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + u.shortcuts = s + } + + c.JSON(http.StatusOK, u.shortcuts) +} + +// RefreshInputHandler returns vMix API Endpoint. +func (u *utilityClient) RefreshInputHandler(c *gin.Context) { + vmix, err := u.vmix.Refresh() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "err": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "inputs": vmix.Inputs.Input, + }) +} + +// GetInputsHandler returns available vmix inputs for [GET] /api/inputs as JSON. +func (u *utilityClient) GetInputsHandler(c *gin.Context) { + if u.vmix.Inputs.Input == nil { + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{ + "error": "Input not loaded", + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "inputs": u.vmix.Inputs.Input, + }) + return +} + +// DoMultipleFunctionsRequest Request JSON for DoMultipleFunctionsHandler +type DoMultipleFunctionsRequest struct { + Function string `json:"function"` // function name. e.g. "Fade" . + Queries []struct { + Key string `json:"key"` // Key. + Value string `json:"value"` // Value. + } `json:"queries"` // Key-Value queries. + Num int `json:"num"` +} + +// Validate form +func (r *DoMultipleFunctionsRequest) Validate() error { + if strings.TrimSpace(r.Function) == "" { + return fmt.Errorf("Function empty") + } + for _, v := range r.Queries { + if v.Key == "" || v.Value == "" { + return fmt.Errorf("Invalid queries") + } + } + if r.Num <= 0 { + return fmt.Errorf("Invalid Number length") + } + return nil +} + +// DoMultipleFunctionsHandler Sends multiple functions to vMix. +func (u *utilityClient) DoMultipleFunctionsHandler(c *gin.Context) { + req := DoMultipleFunctionsRequest{} + if err := c.BindJSON(&req); err != nil { + c.AbortWithError(http.StatusInternalServerError, err) + return + } + if err := req.Validate(); err != nil { + c.AbortWithError(http.StatusBadRequest, err) + return + } + + // パラメタをmap化 + params := make(map[string]string) + for _, v := range req.Queries { + params[v.Key] = v.Value + } + + // 同時実行のためのgoroutineを準備する + // TODO: errgroupを使う + wg := &sync.WaitGroup{} + numerrors := 0 + for i := 0; i < req.Num; i++ { + wg.Add(1) + go func() { + if err := u.vmix.SendFunction(req.Function, params); err != nil { + numerrors++ + log.Printf("Error sending function %s with %v queries. ERR : %v\n", req.Function, params, err) + } + wg.Done() + }() + } + // goroutine合流 + wg.Wait() + + // 結果を返す + if numerrors == 0 { + c.String(http.StatusOK, "Done with no errors") + } else { + c.String(http.StatusAccepted, fmt.Sprintf("Done with %d errors", numerrors)) + } +}