-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a6ebcf7
commit fe7d416
Showing
11 changed files
with
364 additions
and
1,144 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
node_modules | ||
package-lock.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 not shown.
Oops, something went wrong.