diff --git a/.gitignore b/.gitignore index 08bdb1b..f56b303 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ video config.json logs +dist diff --git a/config/config.go b/config/config.go index bc5c5c5..f742af1 100644 --- a/config/config.go +++ b/config/config.go @@ -8,7 +8,6 @@ import ( "log" "os" "path/filepath" - "reflect" "strings" ) @@ -63,18 +62,7 @@ var GlobalConfig Config func init() { GlobalConfig = Config{} err := readConfig("config.json") - for i, item := range GlobalConfig.InputItems { - if item.ItemType == "file" { - GlobalConfig.VideoList = append(GlobalConfig.VideoList, item) - } else if item.ItemType == "dir" { - videos, err := getAllVideos(item.Path) - if err != nil { - log.Fatalf("input[%v] walk error: %v", i, err) - } - GlobalConfig.VideoList = append(GlobalConfig.VideoList, videos...) - } - } - if len(GlobalConfig.VideoList) == 0 { + if len(GlobalConfig.Input) == 0 { log.Fatal("No input video found") } if err != nil { @@ -127,31 +115,66 @@ func validateConfig() error { func validateInputConfig() error { if GlobalConfig.Input == nil { return errors.New("video_path is nil") - } else { - for i, item := range GlobalConfig.Input { - typeOf := reflect.TypeOf(item) - var inputItem InputItem - if typeOf.Kind() == reflect.String { - inputItem = InputItem{Path: item.(string)} - } - if inputItem.Path == "" { - return fmt.Errorf("video_path[%v] is empty", i) - } - stat, err := os.Stat(inputItem.Path) - if err != nil { - return fmt.Errorf("video_path[%v] stat failed: %v", i, err) - } - if stat.IsDir() { - inputItem.ItemType = "dir" - } else { - inputItem.ItemType = "file" - if !utils.IsSupportedVideo(inputItem.Path) { - return fmt.Errorf("video_path[%v] is not supported", i) - } - } - GlobalConfig.InputItems = append(GlobalConfig.InputItems, inputItem) - } } + + GlobalConfig.InputItems = make([]InputItem, 0, len(GlobalConfig.Input)) + GlobalConfig.VideoList = []InputItem{} + + for i, item := range GlobalConfig.Input { + var inputItem InputItem + + switch v := item.(type) { + case string: + inputItem = InputItem{Path: v} + case map[string]any: + data, err := json.Marshal(v) + if err != nil { + return fmt.Errorf("failed to marshal input item[%d]: %v", i, err) + } + if err := json.Unmarshal(data, &inputItem); err != nil { + return fmt.Errorf("failed to unmarshal input item[%d]: %v", i, err) + } + // more efficient, but coupled + // if path, ok := v["path"].(string); ok { + // inputItem.Path = path + // } + // if start, ok := v["start"].(string); ok { + // inputItem.Start = start + // } + // if end, ok := v["end"].(string); ok { + // inputItem.End = end + // } + default: + return fmt.Errorf("invalid input type for item[%d]: %T", i, item) + } + + if inputItem.Path == "" { + return fmt.Errorf("video_path[%d] is empty", i) + } + + stat, err := os.Stat(inputItem.Path) + if err != nil { + return fmt.Errorf("video_path[%d] stat failed: %v", i, err) + } + + if stat.IsDir() { + inputItem.ItemType = "dir" + videos, err := getAllVideos(inputItem.Path) + if err != nil { + return fmt.Errorf("video_path[%d] get videos error: %v", i, err) + } + GlobalConfig.VideoList = append(GlobalConfig.VideoList, videos...) + } else { + inputItem.ItemType = "file" + if !utils.IsSupportedVideo(inputItem.Path) { + return fmt.Errorf("video_path[%d] is not supported", i) + } + GlobalConfig.VideoList = append(GlobalConfig.VideoList, inputItem) + } + + GlobalConfig.InputItems = append(GlobalConfig.InputItems, inputItem) + } + return nil } diff --git a/main.go b/main.go index 5d4a6d0..6819764 100644 --- a/main.go +++ b/main.go @@ -47,7 +47,6 @@ func input() { GlobalStreamer.Prev() case "quit": GlobalStreamer.Close() - os.Exit(0) case "current": fmt.Println(GlobalStreamer.GetCurrentVideoPath()) } diff --git a/server/server.go b/server/server.go index e2b27a8..6708529 100644 --- a/server/server.go +++ b/server/server.go @@ -52,6 +52,7 @@ func NewServer(addr string, dealInputFunc InputFunc) { } func (s *Server) Run() { + gin.SetMode(gin.ReleaseMode) router := gin.New() tpl, err := template.ParseFS(staticFiles, "static/*") if err != nil { diff --git a/server/static/index.html b/server/static/index.html index bbe0540..3c28ea8 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -377,6 +377,7 @@ ws.onopen = function () { console.log("Connected to WebSocket"); + setStoredToken(document.getElementById("token-input").value); document.getElementById("token-screen").style.display = "none"; document.querySelector(".container-fluid").style.display = "flex"; document.getElementById("status").textContent = @@ -387,7 +388,7 @@ ws.onmessage = function (evt) { let obj = JSON.parse(evt.data); messagesArea.value = obj.output; - messagesArea.scrollTop = messagesArea.scrollHeight; + // messagesArea.scrollTop = messagesArea.scrollHeight; document.querySelector("#current-video>span").innerHTML = obj.currentVideoPath; const listContainer = document.querySelector( @@ -400,6 +401,7 @@ }; ws.onerror = function () { + localStorage.removeItem("streaming_token"); document.getElementById("token-error").style.display = "block"; }; @@ -412,8 +414,22 @@ }; } + function getStoredToken() { + return localStorage.getItem("streaming_token"); + } + + function setStoredToken(token) { + localStorage.setItem("streaming_token", token); + } + function validateToken() { - connectWebSocket(); + const tokenInput = document.getElementById("token-input"); + const token = tokenInput.value || getStoredToken(); + + if (token) { + tokenInput.value = token; + connectWebSocket(); + } } const messagesArea = document.getElementById("messages"); diff --git a/streamer/streamer.go b/streamer/streamer.go index 2033bf7..6ba3a5b 100644 --- a/streamer/streamer.go +++ b/streamer/streamer.go @@ -8,6 +8,7 @@ import ( "io" "live-streamer/config" "log" + "os" "os/exec" "strings" "sync" @@ -47,11 +48,11 @@ func NewStreamer(videoList []config.InputItem) *Streamer { func (s *Streamer) start() { s.playStateMu.Lock() s.playState.ctx, s.playState.cancel = context.WithCancel(context.Background()) + cancel := s.playState.cancel currentVideo := s.videoList[s.playState.currentVideoIndex] videoPath := currentVideo.Path s.playState.cmd = exec.CommandContext(s.playState.ctx, "ffmpeg", s.buildFFmpegArgs(currentVideo)...) cmd := s.playState.cmd - ctx := s.playState.ctx s.playStateMu.Unlock() s.writeOutput(fmt.Sprintln("start stream: ", videoPath)) @@ -71,7 +72,9 @@ func (s *Streamer) start() { go s.log(reader) - <-ctx.Done() + _ = cmd.Wait() + cancel() + s.writeOutput(fmt.Sprintf("stop stream: %s\n", videoPath)) s.playStateMu.Lock() @@ -102,33 +105,24 @@ func (s *Streamer) Stream() { func (s *Streamer) Stop() { s.playStateMu.Lock() cancel := s.playState.cancel - cmd := s.playState.cmd s.playState.cancel = nil + cmd := s.playState.cmd s.playState.cmd = nil + ctx := s.playState.ctx s.playStateMu.Unlock() if cancel == nil || cmd == nil { return } - stopped := make(chan error, 1) - go func() { - if cmd.Process != nil { - stopped <- cmd.Wait() - } else { - stopped <- nil - } - }() - cancel() if cmd.Process != nil { select { - case <-stopped: + case <-ctx.Done(): case <-time.After(3 * time.Second): _ = cmd.Process.Kill() } - close(stopped) } } @@ -285,6 +279,7 @@ func (s *Streamer) GetOutput() string { func (s *Streamer) Close() { s.Stop() + os.Exit(0) } func (s *Streamer) buildFFmpegArgs(videoItem config.InputItem) []string { @@ -295,13 +290,12 @@ func (s *Streamer) buildFFmpegArgs(videoItem config.InputItem) []string { args = append(args, "-ss", videoItem.Start) } - args = append(args, "-i", videoPath) - if videoItem.End != "" { args = append(args, "-to", videoItem.End) } args = append(args, + "-i", videoPath, "-c:v", config.GlobalConfig.Play.VideoCodec, "-preset", config.GlobalConfig.Play.Preset, "-crf", fmt.Sprintf("%d", config.GlobalConfig.Play.CRF), @@ -313,7 +307,6 @@ func (s *Streamer) buildFFmpegArgs(videoItem config.InputItem) []string { "-b:a", config.GlobalConfig.Play.AudioBitrate, "-ar", fmt.Sprintf("%d", config.GlobalConfig.Play.AudioSampleRate), "-f", config.GlobalConfig.Play.OutputFormat, - "-stats", "-loglevel", "info", ) if config.GlobalConfig.Play.CustomArgs != "" { @@ -323,7 +316,7 @@ func (s *Streamer) buildFFmpegArgs(videoItem config.InputItem) []string { args = append(args, fmt.Sprintf("%s/%s", config.GlobalConfig.Output.RTMPServer, config.GlobalConfig.Output.StreamKey)) - // logger.GlobalLogger.Println("ffmpeg args: ", args) + log.Println("ffmpeg args: ", args) return args } diff --git a/websocket/websocket.go b/websocket/websocket.go index 9c10469..1ab65eb 100644 --- a/websocket/websocket.go +++ b/websocket/websocket.go @@ -2,7 +2,6 @@ package websocket import ( "live-streamer/streamer" - "os" ) type RequestType string @@ -32,6 +31,5 @@ func RequestHandler(reqType RequestType) { streamer.GlobalStreamer.Prev() case TypeQuit: streamer.GlobalStreamer.Close() - os.Exit(0) } }