Skip to content

dmsghttp dmsgweb streaming media

Moses Narrow edited this page May 3, 2024 · 6 revisions

VLC network stream over DMSGHTTP

This guide requires skywire and golang installed

Generate two keypairs and save them to a file

skywire cli config gen-keys > dmsgvlc.key
skywire cli config gen-keys > dmsgweb.key

Save the following to a file called main.go

package main

import (
	"fmt"
	"context"
	"log"
  "net/http"
	"net/http/httputil"
	"net/url"
	"strings"
	"time"
	"sync"
	"net"

	"github.com/gin-gonic/gin"
	"github.com/spf13/cobra"
	cc "github.com/ivanpirog/coloredcobra"
	"github.com/skycoin/skywire-utilities/pkg/logging"
	"github.com/skycoin/skywire-utilities/pkg/cipher"
	"github.com/skycoin/skywire-utilities/pkg/cmdutil"
	"github.com/skycoin/dmsg/pkg/disc"
	dmsg "github.com/skycoin/dmsg/pkg/dmsg"
	"github.com/skycoin/skywire-utilities/pkg/skyenv"

)

func main() {
	Execute()
}

var (
	startTime = time.Now()
	runTime   time.Duration
	sk       cipher.SecKey
	pk			cipher.PubKey
	dmsgDisc string
	dmsgPort uint
	wl       string
	wlkeys   []cipher.PubKey
  webPort uint
  vlcPort uint
  vlcPath string
)

func init() {
  rootCmd.Flags().UintVarP(&vlcPort, "vport", "v", 8079, "vlc port to connect to")
  rootCmd.Flags().StringVarP(&vlcPath, "vpath", "x", "/music", "vlc path configured")
  rootCmd.Flags().UintVarP(&webPort, "port", "p", 8081, "port to serve")
	rootCmd.Flags().UintVarP(&dmsgPort, "dport", "d", 80, "dmsg port to serve")
	rootCmd.Flags().StringVarP(&wl, "wl", "w", "", "whitelisted keys for dmsg authenticated routes")
	rootCmd.Flags().StringVarP(&dmsgDisc, "dmsg-disc", "D", skyenv.DmsgDiscAddr, "dmsg discovery url")
	pk, _ = sk.PubKey()
	rootCmd.Flags().VarP(&sk, "sk", "s", "a random key is generated if unspecified\n\r")
	rootCmd.CompletionOptions.DisableDefaultCmd = true
	var helpflag bool
	rootCmd.SetUsageTemplate(help)
	rootCmd.PersistentFlags().BoolVarP(&helpflag, "help", "h", false, "help for "+rootCmd.Use)
	rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
	rootCmd.PersistentFlags().MarkHidden("help") //nolint
}

var rootCmd = &cobra.Command{
	Use:   "dmsgvlc",
	Short: "dmsg vlc",
	Long: "dmsg streaming media with vlc",
	Run: func(_ *cobra.Command, _ []string) {
		Server()
	},
}

func Execute() {
	cc.Init(&cc.Config{
		RootCmd:       rootCmd,
		Headings:      cc.HiBlue + cc.Bold,
		Commands:      cc.HiBlue + cc.Bold,
		CmdShortDescr: cc.HiBlue,
		Example:       cc.HiBlue + cc.Italic,
		ExecName:      cc.HiBlue + cc.Bold,
		Flags:         cc.HiBlue + cc.Bold,
		FlagsDescr:      cc.HiBlue,
		NoExtraNewlines: true,
		NoBottomNewline: true,
	})
	if err := rootCmd.Execute(); err != nil {
		log.Fatal("Failed to execute command: ", err)
	}
}

func Server() {
	wg := new(sync.WaitGroup)
	wg.Add(1)

	log := logging.MustGetLogger("dmsgvlc")

	ctx, cancel := cmdutil.SignalContext(context.Background(), log)
	defer cancel()
	pk, err := sk.PubKey()
	if err != nil {
		pk, sk = cipher.GenerateKeyPair()
	}
	if wl != "" {
		wlk := strings.Split(wl, ",")
		for _, key := range wlk {
			var pk1 cipher.PubKey
			err := pk1.Set(key)
			if err == nil {
				wlkeys = append(wlkeys, pk1)
			}
		}
	}
	if len(wlkeys) > 0 {
		if len(wlkeys) == 1 {
			log.Info(fmt.Sprintf("%d key whitelisted", len(wlkeys)))
		} else {
			log.Info(fmt.Sprintf("%d keys whitelisted", len(wlkeys)))
		}
	}

	dmsgclient := dmsg.NewClient(pk, sk, disc.NewHTTP(dmsgDisc, &http.Client{}, log), dmsg.DefaultConfig())
	defer func() {
		if err := dmsgclient.Close(); err != nil {
			log.WithError(err).Error()
		}
	}()

	go dmsgclient.Serve(context.Background())

	select {
	case <-ctx.Done():
		log.WithError(ctx.Err()).Warn()
		return

	case <-dmsgclient.Ready():
	}

	lis, err := dmsgclient.Listen(uint16(dmsgPort))
	if err != nil {
		log.WithError(err).Fatal()
	}
	go func() {
		<-ctx.Done()
		if err := lis.Close(); err != nil {
			log.WithError(err).Error()
		}
	}()

	r1 := gin.New()
	// Disable Gin's default logger middleware
	r1.Use(gin.Recovery())
	r1.Use(loggingMiddleware())
	r1.GET("/", func(c *gin.Context) {
  c.Writer.Header().Set("Server", "")
		c.Writer.WriteHeader(http.StatusOK)
//		l := "<!doctype html><html lang=en><head><title>Example Website</title></head><body style='background-color:black;color:white;'>\n<style type='text/css'>\npre {\n  font-family:Courier New;\n  font-size:10pt;\n}\n.af_line {\n  color: gray;\n  text-decoration: none;\n}\n.column {\n  float: left;\n  width: 30%;\n  padding: 10px;\n}\n.row:after {\n  content: '';\n  display: table;\n  clear: both;\n}\n</style>\n<pre>"
		l := "<!DOCTYPE html><html><head><meta name='viewport' content='initial-scale=1'></head><body style='background-color:black;color:white;'><audio controls><source src='"+vlcPath+"' type='audio/mpeg'><source src='"+vlcPath+"' type='audio/ogg'><source src='"+vlcPath+"' type='audio/wav'>Your browser does not support the audio element.</audio></body></html>"
//		l += "</body></html>"
		c.Writer.Write([]byte(l))
		return
	})

  r1.GET(vlcPath, func(c *gin.Context) {
		targetURL, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%v/", vlcPort))
		proxy := httputil.NewSingleHostReverseProxy(targetURL)
		proxy.ServeHTTP(c.Writer, c.Request)
	})
	// only whitelisted public keys can access authRoute(s)
	authRoute := r1.Group("/")
	if len(wlkeys) > 0 {
		authRoute.Use(whitelistAuth(wlkeys))
	}

	authRoute.GET("/auth", func(c *gin.Context) {
		//override the behavior of `public fallback` for this endpoint when no keys are whitelisted
		if len(wlkeys) == 0 {
			c.Writer.WriteHeader(http.StatusNotFound)
			return
		}
		c.Writer.WriteHeader(http.StatusOK)
		l := "<!doctype html><html lang=en><head><title>Example Website</title></head><body style='background-color:black;color:white;'>\n<style type='text/css'>\npre {\n  font-family:Courier New;\n  font-size:10pt;\n}\n.af_line {\n  color: gray;\n  text-decoration: none;\n}\n.column {\n  float: left;\n  width: 30%;\n  padding: 10px;\n}\n.row:after {\n  content: '';\n  display: table;\n  clear: both;\n}\n</style>\n<pre>"
		l += "<p>Hello World!</p>"
		l += "</body></html>"
		c.Writer.Write([]byte(l))
	})

r1.GET("/health", func(c *gin.Context) {
			runTime = time.Since(startTime)
	c.JSON(http.StatusOK, gin.H{
			"frontend_start_time": startTime,
			"frontend_run_time":   runTime.String(),
			"dmsg_address": fmt.Sprintf("%s:%d", pk.String(), dmsgPort),
		})
})

	// Start the server using the custom Gin handler
	serve := &http.Server{
		Handler:           &GinHandler{Router: r1},
		ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 10 * time.Second,
	}

	// Start serving
	go func() {
		log.WithField("dmsg_addr", lis.Addr().String()).Info("Serving...")
		if err := serve.Serve(lis); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Serve: %v", err)
		}
		wg.Done()
	}()


	go func() {
		fmt.Printf("listening on http://127.0.0.1:%d using gin router\n", webPort)
		r1.Run(fmt.Sprintf(":%d", webPort))
		wg.Done()
	}()

	wg.Wait()
}


	func whitelistAuth(whitelistedPKs []cipher.PubKey) gin.HandlerFunc {
		return func(c *gin.Context) {
			// Get the remote PK.
			remotePK, _, err := net.SplitHostPort(c.Request.RemoteAddr)
			if err != nil {
				c.Writer.WriteHeader(http.StatusInternalServerError)
				c.Writer.Write([]byte("500 Internal Server Error"))
				c.AbortWithStatus(http.StatusInternalServerError)
				return
			}
			// Check if the remote PK is whitelisted.
			whitelisted := false
			if len(whitelistedPKs) == 0 {
				whitelisted = true
			} else {
				for _, whitelistedPK := range whitelistedPKs {
					if remotePK == whitelistedPK.String() {
						whitelisted = true
						break
					}
				}
			}
			if whitelisted {
				c.Next()
			} else {
				// Otherwise, return a 401 Unauthorized error.
				c.Writer.WriteHeader(http.StatusUnauthorized)
				c.Writer.Write([]byte("401 Unauthorized"))
				c.AbortWithStatus(http.StatusUnauthorized)
				return
			}
		}
	}


type GinHandler struct {
Router *gin.Engine
}

func (h *GinHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.Router.ServeHTTP(w, r)
}


func loggingMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		c.Next()
		latency := time.Since(start)
		if latency > time.Minute {
			latency = latency.Truncate(time.Second)
		}
		statusCode := c.Writer.Status()
		method := c.Request.Method
		path := c.Request.URL.Path

		// Get the background color based on the status code
		statusCodeBackgroundColor := getBackgroundColor(statusCode)

		// Get the method color
		methodColor := getMethodColor(method)

		fmt.Printf("[EXAMPLE] %s |%s %3d %s| %13v | %15s | %72s |%s %-7s %s %s\n",
			time.Now().Format("2006/01/02 - 15:04:05"),
			statusCodeBackgroundColor,
			statusCode,
			resetColor(),
			latency,
			c.ClientIP(),
			c.Request.RemoteAddr,
			methodColor,
			method,
			resetColor(),
			path,
		)
	}
}
func getBackgroundColor(statusCode int) string {
	switch {
	case statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices:
		return green
	case statusCode >= http.StatusMultipleChoices && statusCode < http.StatusBadRequest:
		return white
	case statusCode >= http.StatusBadRequest && statusCode < http.StatusInternalServerError:
		return yellow
	default:
		return red
	}
}

func getMethodColor(method string) string {
	switch method {
	case http.MethodGet:
		return blue
	case http.MethodPost:
		return cyan
	case http.MethodPut:
		return yellow
	case http.MethodDelete:
		return red
	case http.MethodPatch:
		return green
	case http.MethodHead:
		return magenta
	case http.MethodOptions:
		return white
	default:
		return reset
	}
}

func resetColor() string {
	return reset
}

type consoleColorModeValue int
var consoleColorMode = autoColor
const (
	autoColor consoleColorModeValue = iota
	disableColor
	forceColor
)

const (
	green   = "\033[97;42m"
	white   = "\033[90;47m"
	yellow  = "\033[90;43m"
	red     = "\033[97;41m"
	blue    = "\033[97;44m"
	magenta = "\033[97;45m"
	cyan    = "\033[97;46m"
	reset   = "\033[0m"
)

var (
	err error
)
const help = "Usage:\r\n" +
	"  {{.UseLine}}{{if .HasAvailableSubCommands}}{{end}} {{if gt (len .Aliases) 0}}\r\n\r\n" +
	"{{.NameAndAliases}}{{end}}{{if .HasAvailableSubCommands}}\r\n\r\n" +
	"Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand)}}\r\n  " +
	"{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\r\n\r\n" +
	"Flags:\r\n" +
	"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\r\n\r\n" +
	"Global Flags:\r\n" +
	"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\r\n\r\n"

Sync the deps

$ go mod init ; go mod tidy ; go mod vendor

Run the application

go run main.go -s $(tail -n1 dmsgvlc.key)

Start a VLC network audio stream

Media > Stream (or CTRL+S)

image

Add media to stream. It's recommended to create a playlist and then select the playlist in this dialog

image

Click stream after adding media or playlist

image

Click Next

image

Select HTTP from the dropdown menu, then click Add

image

Use port 8079 and the path /music ; click Next

image

Click Next

image

Finally, click Stream

image

Access the stream with dmsgweb

NOTE: this does not work using the resolving proxy of dmsgweb configured as socks5 proxy in your web browser and entering the dmsg address in the address bar - which is the standard configuration of dmsgweb

Instead, you must use dmsgweb to resolve the dmsg address of the dmsgvlc application to a local port; in this example, port 8082

skywire dmsg web -t $(head -n1 dmsgvlc.key) $(tail -n1 dmsgweb.key) -p 8082

Access the port that dmsgweb is serving locally in a web browser: http://127.0.0.1:8082

You should see an audio widget. Click it to start the audio stream.

image

The same will work on any two machines, or when dmsgweb is not run on the same machine as the dmsgvlc program. Simply manually copy the public key - the first line of dmsgvlc.key - instead of copying the whole file to another machine. Provide the public key as an argument for the -t flag of skywire dmsg web

As long as any two clients are able to access the dmsg network, they can connect to each other.

Access the stream with vlc

It's also possible to simply open the network stream in VLC, instead of accessing it in a web browser

Media > Open Network Stream (or CTRL+N)

image

enter http://127.0.0.1:8082/music and click Play

image

Known issues

Sometimes you may get the error failed to connect to http server

Two things may have happen in this instance. Either dmsgvlc has become disconnected from dmsg, or dmsgweb has encountered an error.

In this instance, first try restarting dmsgweband attempt again to access the interface. If that does not work, try restarting dmsgvlc.

Clone this wiki locally