diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml old mode 100644 new mode 100755 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml old mode 100644 new mode 100755 diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/.goreleaser.yaml b/.goreleaser.yaml old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 60f45fd..728bdc6 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ - 🎯 支持视频片段截取推流(指定开始和结束时间) - 🔄 支持手动切换当前推流视频 -## 示例配置 +## 配置 + +### 示例 除了 input 和 output 部分,其余都是可选的 @@ -35,7 +37,7 @@ "-crf": 23, "-maxrate": "1000k", "-bufsize": "2000k", - "-vf": "1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2", + "-vf": "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2", "-r": 30, "-c:a": "aac", "-b:a": "128k", @@ -55,3 +57,18 @@ } } ``` + +### play 参数占位符 + +`{{filename}}`: 视频文件名(不包含后缀) +`{{filepath}}`: 视频路径 + +示例: + +```json +{ + "play": { + "-vf": "drawtext=text='{{filename}}':x=5:y=5:fontsize=24:fontcolor=white:borderw=2:bordercolor=black" + } +} +``` diff --git a/config/config.go b/config/config.go old mode 100644 new mode 100755 index 21e4994..f2d01a5 --- a/config/config.go +++ b/config/config.go @@ -24,7 +24,8 @@ type InputItem struct { } type LogConfig struct { - PlayState bool `json:"play_state"` + Level string `json:"level"` + PlayState bool `json:"play_state"` } type ServerConfig struct { @@ -42,10 +43,10 @@ type Config struct { Server ServerConfig `json:"server"` } -var GlobalConfig Config +var GlobalConfig *Config func init() { - GlobalConfig = Config{} + GlobalConfig = &Config{} err := readConfig("config.json") if len(GlobalConfig.Input) == 0 { log.Fatal("No input video found") @@ -91,9 +92,16 @@ func validateConfig() error { if err := validateServerConfig(); err != nil { return err } + validateLogConfig() return nil } +func validateLogConfig() { + if GlobalConfig.Log.Level == "" { + GlobalConfig.Log.Level = "info" + } +} + func validateInputConfig() error { if GlobalConfig.Input == nil { return errors.New("video_path is nil") diff --git a/constant/constant.go b/constant/constant.go old mode 100644 new mode 100755 diff --git a/constant/version.go b/constant/version.go old mode 100644 new mode 100755 diff --git a/docker-compose.yaml b/docker-compose.yaml old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 index 56b6819..bbbc72e --- a/go.mod +++ b/go.mod @@ -30,6 +30,8 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect diff --git a/go.sum b/go.sum old mode 100644 new mode 100755 index e8b1f09..b7c9d5b --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -74,6 +76,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= @@ -81,6 +87,7 @@ golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..84f8708 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,41 @@ +package logger + +import ( + c "live-streamer/config" + "os" + "strings" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + GlobalLogger *zap.Logger + config *c.Config +) + +func init() { + config = c.GlobalConfig + + var logLevel zapcore.Level + switch strings.ToLower(config.Log.Level) { + case "info": + logLevel = zap.InfoLevel + case "error": + logLevel = zap.ErrorLevel + case "warn": + logLevel = zap.WarnLevel + case "debug": + logLevel = zap.DebugLevel + case "panic": + logLevel = zap.PanicLevel + } + + encoderConfig := zap.NewProductionEncoderConfig() + encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + + consoleWriter := zapcore.AddSync(os.Stdout) + consoleCore := zapcore.NewCore(zapcore.NewConsoleEncoder(encoderConfig), consoleWriter, logLevel) + GlobalLogger = zap.New(consoleCore) +} diff --git a/main.go b/main.go old mode 100644 new mode 100755 index d461b53..ec91297 --- a/main.go +++ b/main.go @@ -4,35 +4,51 @@ import ( "bufio" "fmt" - "live-streamer/config" + c "live-streamer/config" "live-streamer/constant" + "live-streamer/logger" "live-streamer/server" "live-streamer/streamer" "live-streamer/utils" "live-streamer/websocket" - "log" + "os" "github.com/fsnotify/fsnotify" + "go.uber.org/zap" ) -var GlobalStreamer *streamer.Streamer +var ( + GlobalStreamer *streamer.Streamer + log *zap.Logger + config *c.Config +) + +func init() { + config = c.GlobalConfig + log = logger.GlobalLogger +} func main() { fmt.Println("Version: " + constant.Version) - server.NewServer(config.GlobalConfig.Server.Addr, websocket.RequestHandler) - server.GlobalServer.Run() + if !utils.HasFFMPEG() { log.Fatal("ffmpeg not found") } - GlobalStreamer = streamer.NewStreamer(config.GlobalConfig.VideoList) + + server.NewServer(config.Server.Addr, websocket.RequestHandler) + server.GlobalServer.Run() + + GlobalStreamer = streamer.NewStreamer(config.VideoList) + go startWatcher() - go input() + go inputHandler() + GlobalStreamer.Start() select {} } -func input() { +func inputHandler() { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { line := scanner.Text() @@ -56,20 +72,20 @@ func input() { func startWatcher() { watcher, err := fsnotify.NewWatcher() if err != nil { - log.Fatalf("failed to create watcher: %v", err) + log.Fatal("failed to create watcher", zap.Error(err)) } defer watcher.Close() - for _, item := range config.GlobalConfig.InputItems { + for _, item := range config.InputItems { if item.ItemType == "dir" { err = watcher.Add(item.Path) if err != nil { - log.Fatalf("failed to add dir to watcher: %v", err) + log.Fatal("failed to add dir to watcher", zap.Error(err)) } - log.Println("watching dir:", item.Path) + log.Info("watching dir", zap.String("path", item.Path)) } } if err != nil { - log.Fatalf("failed to start watcher: %v", err) + log.Fatal("failed to start watcher", zap.Error(err)) } for { @@ -80,19 +96,19 @@ func startWatcher() { } if event.Op&fsnotify.Create == fsnotify.Create { if utils.IsSupportedVideo(event.Name) { - log.Println("new video added:", event.Name) + log.Info("new video added", zap.String("path", event.Name)) GlobalStreamer.Add(event.Name) } } if event.Op&fsnotify.Remove == fsnotify.Remove { - log.Println("video removed:", event.Name) + log.Info("video removed", zap.String("path", event.Name)) GlobalStreamer.Remove(event.Name) } case err, ok := <-watcher.Errors: if !ok { return } - log.Println("watcher error:", err) + log.Error("watcher error", zap.Error(err)) } } } diff --git a/server/server.go b/server/server.go old mode 100644 new mode 100755 index 08e8a49..5dc2c17 --- a/server/server.go +++ b/server/server.go @@ -3,10 +3,10 @@ package server import ( "embed" "html/template" - "live-streamer/config" + c "live-streamer/config" + "live-streamer/logger" "live-streamer/streamer" mywebsocket "live-streamer/websocket" - "log" "net/http" "sync" "time" @@ -14,6 +14,7 @@ import ( "github.com/gin-gonic/gin" uuid "github.com/gofrs/uuid/v5" "github.com/gorilla/websocket" + "go.uber.org/zap" ) //go:embed static @@ -41,7 +42,17 @@ type Client struct { hasSentSize int } -var GlobalServer *Server +var ( + GlobalServer *Server + + config *c.Config + log *zap.Logger +) + +func init() { + config = c.GlobalConfig + log = logger.GlobalLogger +} func NewServer(addr string, dealInputFunc InputFunc) { GlobalServer = &Server{ @@ -56,7 +67,7 @@ func (s *Server) Run() { router := gin.New() tpl, err := template.ParseFS(staticFiles, "static/*") if err != nil { - log.Fatalf("Error parsing templates: %v", err) + log.Fatal("parsing templates error", zap.Error(err)) } router.SetHTMLTemplate(tpl) @@ -69,7 +80,7 @@ func (s *Server) Run() { go func() { if err := router.Run(s.addr); err != nil { - log.Fatalf("Error starting server: %v", err) + log.Fatal("starting server error", zap.Error(err)) } }() } @@ -86,7 +97,7 @@ func (s *Server) handleWebSocket(c *gin.Context) { id, err := uuid.NewV7() if err != nil { - log.Printf("generating uuid error: %v", err) + log.Error("generating uuid error", zap.Error(err)) return } client := &Client{id: id.String(), conn: ws, hasSentSize: 0} @@ -102,7 +113,7 @@ func (s *Server) handleWebSocket(c *gin.Context) { delete(s.clients, client.id) s.mu.Unlock() if r := recover(); r != nil { - log.Printf("webSocket handler panic: %v", r) + log.Panic("webSocket handler panic", zap.Any("recover", r)) } }() @@ -126,7 +137,7 @@ func (s *Server) handleWebSocket(c *gin.Context) { client.mu.Unlock() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { - log.Printf("websocket error: %v", err) + log.Error("websocket error", zap.Error(err)) } break } @@ -136,8 +147,8 @@ func (s *Server) handleWebSocket(c *gin.Context) { func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - if config.GlobalConfig.Server.Token == "" || - c.Query("token") == config.GlobalConfig.Server.Token { + if config.Server.Token == "" || + c.Query("token") == config.Server.Token { c.Next() } else { c.AbortWithStatus(http.StatusUnauthorized) @@ -150,7 +161,7 @@ func (s *Server) Broadcast(obj mywebsocket.Date) { for _, client := range s.clients { obj.Timestamp = time.Now().UnixMilli() if err := client.conn.WriteJSON(obj); err != nil { - log.Printf("websocket writing message error: %v", err) + log.Error("websocket writing message error", zap.Error(err)) } } s.mu.Unlock() @@ -161,7 +172,7 @@ func (s *Server) Single(userID string, obj mywebsocket.Date) { if client, ok := s.clients[userID]; ok { obj.Timestamp = time.Now().UnixMilli() if err := client.conn.WriteJSON(obj); err != nil { - log.Printf("websocket writing message error: %v", err) + log.Error("websocket writing message error", zap.Error(err)) } } s.mu.Unlock() diff --git a/server/static/index.html b/server/static/index.html old mode 100644 new mode 100755 diff --git a/streamer/helper.go b/streamer/helper.go old mode 100644 new mode 100755 index b43424d..eebf509 --- a/streamer/helper.go +++ b/streamer/helper.go @@ -2,11 +2,14 @@ package streamer import ( "fmt" - "live-streamer/config" - "log" + c "live-streamer/config" + "path/filepath" + "strings" + + "go.uber.org/zap" ) -func buildFFmpegArgs(videoItem config.InputItem) []string { +func buildFFmpegArgs(videoItem c.InputItem) []string { videoPath := videoItem.Path args := []string{"-re"} @@ -22,14 +25,21 @@ func buildFFmpegArgs(videoItem config.InputItem) []string { "-i", videoPath, ) - for k, v := range config.GlobalConfig.Play { + for k, v := range config.Play { args = append(args, k) - args = append(args, fmt.Sprint(v)) + if str, ok := v.(string); ok { + filename := strings.TrimSuffix(filepath.Base(videoPath), filepath.Ext(videoPath)) + str = strings.ReplaceAll(str, "{{filepath}}", videoPath) + str = strings.ReplaceAll(str, "{{filename}}", filename) + args = append(args, str) + } else { + args = append(args, fmt.Sprint(v)) + } } - args = append(args, fmt.Sprintf("%s/%s", config.GlobalConfig.Output.RTMPServer, config.GlobalConfig.Output.StreamKey)) + args = append(args, fmt.Sprintf("%s/%s", config.Output.RTMPServer, config.Output.StreamKey)) - log.Println("ffmpeg args: ", args) + log.Debug("build ffmpeg", zap.Strings("args", args)) return args } diff --git a/streamer/message.go b/streamer/message.go old mode 100644 new mode 100755 index c81c53b..b5fc7c9 --- a/streamer/message.go +++ b/streamer/message.go @@ -1,6 +1,8 @@ package streamer -import "live-streamer/config" +import ( + c "live-streamer/config" +) type Message interface { messageType() string @@ -20,7 +22,7 @@ type GetCurrentVideoMessage struct { Response chan string } type GetVideoListMessage struct { - Response chan []config.InputItem + Response chan []c.InputItem } type GetVideoListPathMessage struct { Response chan []string diff --git a/streamer/streamer.go b/streamer/streamer.go old mode 100644 new mode 100755 index 51fce14..5ec57a0 --- a/streamer/streamer.go +++ b/streamer/streamer.go @@ -5,13 +5,16 @@ import ( "context" "fmt" "io" - "live-streamer/config" - "log" + c "live-streamer/config" + "live-streamer/logger" + "os" "os/exec" "strings" "sync" "time" + + "go.uber.org/zap" ) type Streamer struct { @@ -22,11 +25,12 @@ type Streamer struct { outputQueue chan string outputReq chan chan string // address output concurrency security issue - wg sync.WaitGroup // wait all handlers(except closehandler) to finish before closure + wg sync.WaitGroup // wait all handlers(except closehandler) to finish before closure + close chan any } type streamerState struct { - videoList []config.InputItem + videoList []c.InputItem currentVideoIndex int manualControl bool cmd *exec.Cmd @@ -37,7 +41,17 @@ type streamerState struct { var GlobalStreamer *Streamer -func NewStreamer(videoList []config.InputItem) *Streamer { +var ( + config *c.Config + log *zap.Logger +) + +func init() { + config = c.GlobalConfig + log = logger.GlobalLogger +} + +func NewStreamer(videoList []c.InputItem) *Streamer { s := &Streamer{ mailbox: make(chan Message, 100), state: &streamerState{ @@ -46,6 +60,7 @@ func NewStreamer(videoList []config.InputItem) *Streamer { output: strings.Builder{}, outputQueue: make(chan string, 100), outputReq: make(chan chan string), + close: make(chan any), } GlobalStreamer = s go s.actorLoop() @@ -54,13 +69,18 @@ func NewStreamer(videoList []config.InputItem) *Streamer { } func (s *Streamer) actorLoop() { - for msg := range s.mailbox { - if msg.messageType() != CloseMessage.messageType(CloseMessage{}) { - s.wg.Add(1) - s.handleMessage(msg) - s.wg.Done() - } else { - s.handleMessage(msg) + for { + select { + case <-s.close: + return + case msg := <-s.mailbox: + if _, ok := msg.(CloseMessage); !ok { + s.wg.Add(1) + s.handleMessage(msg) + s.wg.Done() + } else { + s.handleMessage(msg) + } } } } @@ -105,43 +125,46 @@ func (s *Streamer) handleStart() { s.state.cmd = exec.CommandContext(s.state.ctx, "ffmpeg", buildFFmpegArgs(currentVideo)...) s.state.waitDone = make(chan any) - s.writeOutput(fmt.Sprintln("start stream: ", videoPath)) - pipe, err := s.state.cmd.StderrPipe() // ffmpeg send all messages to stderr if err != nil { - log.Printf("failed to get pipe: %v", err) + log.Error("failed to get pipe", zap.Error(err)) return } reader := bufio.NewReader(pipe) + log.Info("start stream", zap.String("path", videoPath)) + s.writeOutput(fmt.Sprintln("start stream: ", videoPath)) if err := s.state.cmd.Start(); err != nil { s.writeOutput(fmt.Sprintf("starting ffmpeg error: %v\n", err)) return } - go s.log(reader) - go func() { + log.Debug("wait stream end", zap.String("path", videoPath)) _ = s.state.cmd.Wait() + log.Debug("process stop", zap.String("path", videoPath)) s.state.cancel() + log.Debug("context cancel", zap.String("path", videoPath)) s.writeOutput(fmt.Sprintf("stop stream: %s\n", videoPath)) if !s.state.manualControl { - log.Println("ready to stream next video") + log.Debug("video end", zap.String("path", videoPath)) s.state.currentVideoIndex++ if s.state.currentVideoIndex >= len(s.state.videoList) { s.state.currentVideoIndex = 0 } s.mailbox <- StartMessage{} } else { - log.Println("manually control") + log.Debug("manually end", zap.String("path", videoPath)) s.state.manualControl = false } close(s.state.waitDone) }() + + go s.log(reader) } func (s *Streamer) handleStop() { @@ -149,18 +172,20 @@ func (s *Streamer) handleStop() { return } - log.Println("wait context to be cancelled") + videoPath := s.state.videoList[s.state.currentVideoIndex].Path + + log.Debug("wait context to be cancelled", zap.String("path", videoPath)) s.state.cancel() - log.Println("context has been cancelled") + log.Debug("context has been cancelled", zap.String("path", videoPath)) if s.state.cmd.Process != nil { - log.Println("wait ffmpeg process stop") + log.Debug("wait ffmpeg process stop", zap.String("path", videoPath)) select { case <-s.state.waitDone: case <-time.After(3 * time.Second): _ = s.state.cmd.Process.Kill() } - log.Println("ffmpeg process has stopped") + log.Debug("ffmpeg process has stopped", zap.String("path", videoPath)) } s.state.cancel = nil @@ -168,7 +193,7 @@ func (s *Streamer) handleStop() { } func (s *Streamer) handleAdd(path string) { - s.state.videoList = append(s.state.videoList, config.InputItem{Path: path}) + s.state.videoList = append(s.state.videoList, c.InputItem{Path: path}) } func (s *Streamer) handleRemove(path string) { @@ -235,7 +260,7 @@ func (s *Streamer) handleGetCurrentVideo(response chan string) { response <- s.state.videoList[s.state.currentVideoIndex].Path } -func (s *Streamer) handleGetVideoList(response chan []config.InputItem) { +func (s *Streamer) handleGetVideoList(response chan []c.InputItem) { response <- s.state.videoList } @@ -252,6 +277,8 @@ func (s *Streamer) handleGetCurrentIndex(response chan int) { } func (s *Streamer) handleClose() { + close(s.close) + s.handleStop() s.wg.Wait() os.Exit(0) } @@ -287,8 +314,8 @@ func (s *Streamer) GetCurrentVideoPath() string { return <-response } -func (s *Streamer) GetVideoList() []config.InputItem { - response := make(chan []config.InputItem) +func (s *Streamer) GetVideoList() []c.InputItem { + response := make(chan []c.InputItem) s.mailbox <- GetVideoListMessage{Response: response} return <-response } @@ -306,7 +333,6 @@ func (s *Streamer) GetCurrentIndex() int { } func (s *Streamer) Close() { - s.mailbox <- StopMessage{} s.mailbox <- CloseMessage{} } @@ -335,13 +361,14 @@ func (s *Streamer) log(reader *bufio.Reader) { case <-s.state.ctx.Done(): return default: - if !config.GlobalConfig.Log.PlayState { + if !config.Log.PlayState { return } buf := make([]byte, 1024) for { n, err := reader.Read(buf) if n > 0 { + log.Debug("ffmpeg output", zap.String("msg", strings.TrimSpace(string(buf[:n])))) s.writeOutput(string(buf[:n])) } if err != nil { diff --git a/utils/has_ffmpeg.go b/utils/has_ffmpeg.go old mode 100644 new mode 100755 diff --git a/utils/is_supported_video.go b/utils/is_supported_video.go old mode 100644 new mode 100755 diff --git a/websocket/websocket.go b/websocket/websocket.go old mode 100644 new mode 100755