package main import ( "embed" "errors" "fmt" "log" "net" "net/http" _ "embed" "flag" "math/rand" "github.com/gin-gonic/gin" "github.com/olahol/melody" ) type PlayState = int const ( NotPlaying PlayState = iota PlayingFallback PlayingVideo ) type State struct { VideoProcess ffmpegProcess PlayState PlayState CurrentFile string Config Websocket *melody.Melody Users map[net.Addr]WebsocketUserInfo } type WebsocketUserInfo struct { Username string Color string } func (w WebsocketUserInfo) ToWebsocketUser() WebsocketUser { return WebsocketUser{ Username: w.Username, Color: w.Color, } } type Config struct { StreamCacheDir string FallbackFile string MediaFolder string IgnoreHiddenFiles bool PublicPort int AdminApiPort int } func (s *State) getRandomMediaFile() (foundFile string, err error) { var files []string files, err = listFilesRecursively(s.MediaFolder, s.IgnoreHiddenFiles) if err != nil { return } foundFile = files[rand.Intn(len(files))] return } func newState(c Config) (state State, err error) { state = State{ Config: c, PlayState: NotPlaying, Users: make(map[net.Addr]WebsocketUserInfo), } return } //go:embed html/* var embedHTML embed.FS var ErrMediaFolderNotSpecified = errors.New("Please specify media folder to use") func parseFlags() (config Config, err error) { flag.StringVar(&config.StreamCacheDir, "cache", "stream_cache", "location to dump hls/m3u8 livestream files") flag.StringVar(&config.FallbackFile, "fallback", "family_guy_wheel.mp4", "Fallback video that is played when main stream is down") flag.StringVar(&config.MediaFolder, "media", "", "Folder to play random media from") flag.IntVar(&config.PublicPort, "public_port", 3000, "Port to run public website on") flag.IntVar(&config.AdminApiPort, "admin_api_port", 3001, "Port to run admin api on") flag.BoolVar(&config.IgnoreHiddenFiles, "hidden_files", true, "Ignores hidden files in random media picking") flag.Parse() if config.MediaFolder == "" { err = ErrMediaFolderNotSpecified } return } func main() { config, err := parseFlags() if err != nil { log.Fatal(err) } if err = RecreateDir(config.StreamCacheDir); err != nil { log.Fatal(err) } state, err := newState(config) if err != nil { log.Fatal(err) } state.setupWebsocket() if err = state.playFallback(); err != nil { log.Fatal(err) } public := gin.Default() public.GET("/", func(c *gin.Context) { ready, err := state.IsLivestreamReady() if err != nil { c.String(http.StatusInternalServerError, "Something went wrong") return } if ready { c.FileFromFS("html/stream.html", http.FS(embedHTML)) } else { c.FileFromFS("html/loading.html", http.FS(embedHTML)) } }) public.Static("/static", "static") public.Static("/media", config.StreamCacheDir) public.Any("/ws", func(c *gin.Context) { state.Websocket.HandleRequest(c.Writer, c.Request) }) publicAPI := public.Group("/api") publicAPI.GET("now_playing", state.NowPlayingAPI) publicAPI.GET("is_livestream_ready", state.IsLivestreamReadyAPI) private := gin.Default() // TODO: autoplay feature that keeps playing random things after current one ends privateAPI := private.Group("/api") privateAPI.GET("play", state.PlayAPI) privateAPI.GET("random", state.PlayRandomAPI) privateAPI.GET("stop", state.StopPlayingAPI) privateAPI.GET("now_playing", state.NowPlayingAPI) fmt.Println("Starting public server on port", fmt.Sprint(config.PublicPort)) go public.Run(fmt.Sprintf(":%d", config.PublicPort)) fmt.Println("Starting private server on port", fmt.Sprint(config.AdminApiPort)) log.Fatal(private.Run(fmt.Sprintf(":%d", config.AdminApiPort))) }