Skip to content

Commit

Permalink
added go version
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffmikels committed Mar 23, 2022
1 parent a6ebcf7 commit fe7d416
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 1,144 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
package-lock.json
3 changes: 3 additions & 0 deletions go-version/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

env GOOS=windows GOARCH=amd64 go build -o vmix-snapshot-proxy.exe
16 changes: 16 additions & 0 deletions go-version/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module org.jeffmikels/vmix-snapshot-proxy

go 1.17

require github.com/gofiber/fiber/v2 v2.29.0

require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/klauspost/compress v1.15.0 // indirect
github.com/reiver/go-oi v1.0.0 // indirect
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.34.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
)
30 changes: 30 additions & 0 deletions go-version/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/gofiber/fiber/v2 v2.29.0 h1:wopU1kXxdD9XxvQqYd1vSWMGu2PiZN0yy+DojygTRRA=
github.com/gofiber/fiber/v2 v2.29.0/go.mod h1:1Ega6O199a3Y7yDGuM9FyXDPYQfv+7/y48wl6WCwUF4=
github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/reiver/go-oi v1.0.0 h1:nvECWD7LF+vOs8leNGV/ww+F2iZKf3EYjYZ527turzM=
github.com/reiver/go-oi v1.0.0/go.mod h1:RrDBct90BAhoDTxB1fenZwfykqeGvhI6LsNfStJoEkI=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e h1:quuzZLi72kkJjl+f5AQ93FMcadG19WkS7MO6TXFOSas=
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e/go.mod h1:+5vNVvEWwEIx86DB9Ke/+a5wBI464eDRo3eF0LcfpWg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4=
github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
13 changes: 13 additions & 0 deletions go-version/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import "github.com/gofiber/fiber/v2"

func tmain() {
app := fiber.New()

app.Get("/", func(c *fiber.Ctx) error {
return c.SendString("Hello, World!")
})

app.Listen(":3000")
}
6 changes: 6 additions & 0 deletions go-version/vmix-snapshot-proxy.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

@REM vmix-snapshot-proxy.exe -h
@REM vmix-snapshot-proxy.exe -d default
@REM vmix-snapshot-proxy.exe -d vmixStoragePath -p vmixWebAPIPort

vmix-snapshot-proxy.exe
Binary file added go-version/vmix-snapshot-proxy.exe
Binary file not shown.
294 changes: 294 additions & 0 deletions go-version/vmix-snapshot-proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
package main

import (
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"strconv"
"strings"
"time"

"encoding/xml"

"github.com/gofiber/fiber/v2"
)

var myIp string
var proxyPort int = 8098
var vmixIP string = "localhost"
var vmixPort int = 8088
var vmixUrl string
var vmixPath string = fmt.Sprintf("%s\\Documents\\vMixStorage", os.Getenv("USERPROFILE"))

/* Example vMix Input XML
<inputs>
<input key="26cae087-b7b6-4d45-98e4-de03ab4feb6b" number="1" type="Xaml" title="NewsHD.xaml" state="Paused" position="0" duration="0" muted="True" loop="False" selectedIndex="0">
NewsHD.xaml
<text index="0" name="Headline">Hello</text>
<text index="1" name="Description">Hello</text>
</input>
<input key="55cbe357-a801-4d54-8ff2-08ee68766fae" number="2" type="VirtualSet" title="LateNightNews" state="Paused" position="0" duration="0" muted="True" loop="False" selectedIndex="0">
LateNightNews
<overlay index="0" key="2fe8ff9d-e400-4504-85ab-df7c17a1edd4"/>
<overlay index="1" key="20e4ee9a-05cc-4f58-bb69-cd179e1c1958"/>
<overlay index="2" key="94b88db0-c5cd-49d8-98a2-27d83d4bf3fe"/>
</input>
</inputs>
*/

// XML structs for later decomposition
type Vmix struct {
XMLName xml.Name `xml:"vmix"`
Inputs Inputs `xml:"inputs"`
}

type Inputs struct {
XMLName xml.Name `xml:"inputs"`
Inputs []Input `xml:"input"`
}

//Input{name: "Camera 1", number: 1}
type Input struct {
XMLName xml.Name `xml:"input"`
Name string `xml:"title,attr"`
Number string `xml:"number,attr"`
}

// declare this as a global variable
var vmix Vmix

func main() {
myIp = GetOutboundIP().String()

// flag parser settings
pathPtr := flag.String("d", "default", "path to the vMix Storage Directory")
portPtr := flag.Int("p", vmixPort, "port as set in the vMix web API settings")

flag.Parse()
if *pathPtr != "default" {
vmixPath = *pathPtr
}
vmixUrl = fmt.Sprintf("http://%s:%d/api", vmixIP, *portPtr)

// start the http server
app := fiber.New()

// on the root route, refresh inputs and return them
app.Get("/", func(c *fiber.Ctx) error {
GetInputs()
json := "["
var jsonStrings []string
for i := 0; i < len(vmix.Inputs.Inputs); i++ {
input := vmix.Inputs.Inputs[i]
jsonStrings = append(jsonStrings, fmt.Sprintf(`{"name":"%s", "number":%s}`, input.Name, input.Number))
}
json += strings.Join(jsonStrings, ",") + "]"
return c.SendString(json)
})

// on the regen route, re-request all snapshots
app.Get("/regen", func(c *fiber.Ctx) error {
RequestSnapshots(-1)
return c.SendString("snapshots are regenerating")
})

// request regeneration of one input
app.Get("/regen/:input", func(c *fiber.Ctx) error {
input, err := strconv.Atoi(c.Params("input"))
if err != nil {
return c.SendString("request was invalid")
}
RequestSnapshots(input)
return c.SendString("snapshot " + c.Params("input") + " is regenerating")
})

// app.Use("/:input.jpg", func(c *fiber.Ctx) error {
// // Set some security headers:
// // c.Set("X-XSS-Protection", "1; mode=block")
// // c.Set("X-Content-Type-Options", "nosniff")
// // c.Set("X-Download-Options", "noopen")
// // c.Set("Strict-Transport-Security", "max-age=5184000")
// // c.Set("X-Frame-Options", "SAMEORIGIN")
// // c.Set("X-DNS-Prefetch-Control", "off")
// input, err := strconv.Atoi(c.Params("input"))
// if err != nil {
// return c.SendString("request was invalid")
// }
// RequestSnapshots(input)

// // Go to next middleware:
// return c.Next()
// })

// do the static route
app.Static("/", vmixPath)

// start the interval to refresh snapshots
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
lastInput := 0
tickerCounter := 0
go func() {
for range ticker.C {
// ask for a snapshot
if len(vmix.Inputs.Inputs) > 0 {
lastInput = (lastInput + 1) % len(vmix.Inputs.Inputs)
RequestSnapshots(lastInput)
}

// maybe ask for all inputs again
tickerCounter = (tickerCounter + 1) % 20 // counts to 10 seconds
if tickerCounter == 0 {
GetInputs()
PrintStatus()
}
}
}()

// listen to the telnet socket
// setup the telnet connection to vmix too
var conn net.Conn
go func() {
var telnetError error
buffer := make([]byte, 1024)
accum := []byte{}
for {
if conn == nil {
conn, telnetError = net.Dial("tcp", "localhost:8099")
if telnetError == nil {
fmt.Println("No connection to vMix Telent API: (localhost:8099)")
continue
}
conn.Write([]byte("SUBSCRIBE TALLY\r\n"))
} else {
conn.SetReadDeadline(time.Now().Add(time.Millisecond * 10))
count, err := conn.Read(buffer)
if err != nil {
conn = nil
accum = []byte{}
} else {
accum = append(accum, buffer[0:count]...)
str := string(accum)
lines := strings.Split(str, "\r\n")
for i, line := range lines {
fields := strings.Split(line, " ")
// will be SUBSCRIBE OK TALLY
// or TALLY OK 0121...
if len(fields) > 0 && fields[0] == "TALLY" {
RequestSnapshots(0)
}
// if it is the last element of the lines slice, make it the new accumulator
if i == len(lines)-1 {
accum = []byte(line)
}
}

}
}
time.Sleep(time.Second)
}
}()

// start the server
app.Listen(fmt.Sprintf("%s:%d", myIp, proxyPort))

if conn != nil {
conn.Close()
}

}

func PrintStatus() {
fmt.Println("=====================================================================================")
fmt.Println("|- SETTINGS ---------------------------------------------------------------------------")
fmt.Printf("| vMix Storage Path: %s\n", vmixPath)
fmt.Printf("| vMix Web API URL: %s\n", vmixUrl)
fmt.Println("|- AVAILABLE COMMANDS -----------------------------------------------------------------")
fmt.Printf("| Running vMix Snapshot Proxy at port %d\n", proxyPort)
fmt.Printf("| Get a list of all inputs: http://%s:%d/\n", myIp, proxyPort)
fmt.Printf("| Force regen one input (0 means program): http://%s:%d/regen/#\n", myIp, proxyPort)
fmt.Printf("| Force regen all inputs: http://%s:%d/regen\n", myIp, proxyPort)
fmt.Printf("| Get input snapshot: http://%s:%d/#.jpg\n", myIp, proxyPort)
fmt.Println("|")
fmt.Println("| Getting an input snapshot sends the most recent snapshot, and queues the generation of a new one.")
fmt.Println("| If there are no snapshots for that input yet, it will wait a bit before trying again.")
fmt.Println("| Snapshots take about 1 second to process")
fmt.Println("=====================================================================================")
}

func DoRequest(url string) []byte {
resp, err := http.Get(url)
if err != nil {
print(`ERROR attempting to reach vMix: ` + url)
return nil
}
defer resp.Body.Close()

// get response body
bodyBytes, _ := io.ReadAll(resp.Body)
return bodyBytes
}

// will request the XML from vMix and parse the inputs saving the results
// to the global `inputs` variable
func GetInputs() {
url := vmixUrl + "?XML"
fmt.Println(url)
bodyBytes := DoRequest(url)
if bodyBytes == nil {
fmt.Println("ERROR: vMix failed to retriev inputs... Is vMix running?")
return
}
fmt.Println(string(bodyBytes))

// clear out the old vmix data
vmix = Vmix{}
err := xml.Unmarshal(bodyBytes, &vmix)
if err != nil {
print(err)
}

for i := 0; i < len(vmix.Inputs.Inputs); i++ {
fmt.Println("Input Name: " + vmix.Inputs.Inputs[i].Name)
fmt.Println("Input Number: " + vmix.Inputs.Inputs[i].Number)
}
}

// this will tell vMix to generate a snapshot of the specified input
// if `inputNumber` is -1, it will request a snapshot for all inputs
// if `inputNumber` is 0, it will generate a snapshot for the program
// remember that vMix inputs are 1-indexed
func RequestSnapshots(inputNumber int) {
if inputNumber == -1 {
for i := 0; i <= len(vmix.Inputs.Inputs); i++ {
go RequestSnapshots(i)
}
} else {
var url string
if inputNumber == 0 {
url = vmixUrl + "?Function=Snapshot&Value=0.jpg"
} else {
url = fmt.Sprintf("%s?Function=SnapshotInput&Input=%d&Value=%d.jpg", vmixUrl, inputNumber, inputNumber)
}
fmt.Println(url)
bytes := DoRequest(url)
fmt.Println(string(bytes))
}
}

// Get preferred outbound ip of this machine
func GetOutboundIP() net.IP {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
log.Fatal(err)
}
defer conn.Close()

localAddr := conn.LocalAddr().(*net.UDPAddr)

return localAddr.IP
}
Binary file added go-version/vmix-snapshot-proxy.zip
Binary file not shown.
Loading

0 comments on commit fe7d416

Please sign in to comment.