feat: log level config
feat: ffmpeg args placeholder
This commit is contained in:
parent
2afcac48dc
commit
dfdb6003ea
0
.github/workflows/docker.yml
vendored
Normal file → Executable file
0
.github/workflows/docker.yml
vendored
Normal file → Executable file
0
.github/workflows/release.yml
vendored
Normal file → Executable file
0
.github/workflows/release.yml
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
0
.goreleaser.yaml
Normal file → Executable file
0
.goreleaser.yaml
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
0
Dockerfile
Normal file → Executable file
21
README.md
Normal file → Executable file
21
README.md
Normal file → Executable file
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
14
config/config.go
Normal file → Executable file
14
config/config.go
Normal file → Executable file
@ -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")
|
||||
|
0
constant/constant.go
Normal file → Executable file
0
constant/constant.go
Normal file → Executable file
0
constant/version.go
Normal file → Executable file
0
constant/version.go
Normal file → Executable file
0
docker-compose.yaml
Normal file → Executable file
0
docker-compose.yaml
Normal file → Executable file
2
go.mod
Normal file → Executable file
2
go.mod
Normal file → Executable file
@ -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
|
||||
|
7
go.sum
Normal file → Executable file
7
go.sum
Normal file → Executable file
@ -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=
|
||||
|
41
logger/logger.go
Normal file
41
logger/logger.go
Normal file
@ -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)
|
||||
}
|
48
main.go
Normal file → Executable file
48
main.go
Normal file → Executable file
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
35
server/server.go
Normal file → Executable file
35
server/server.go
Normal file → Executable file
@ -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()
|
||||
|
0
server/static/index.html
Normal file → Executable file
0
server/static/index.html
Normal file → Executable file
24
streamer/helper.go
Normal file → Executable file
24
streamer/helper.go
Normal file → Executable file
@ -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
|
||||
}
|
||||
|
6
streamer/message.go
Normal file → Executable file
6
streamer/message.go
Normal file → Executable file
@ -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
|
||||
|
85
streamer/streamer.go
Normal file → Executable file
85
streamer/streamer.go
Normal file → Executable file
@ -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 {
|
||||
|
0
utils/has_ffmpeg.go
Normal file → Executable file
0
utils/has_ffmpeg.go
Normal file → Executable file
0
utils/is_supported_video.go
Normal file → Executable file
0
utils/is_supported_video.go
Normal file → Executable file
0
websocket/websocket.go
Normal file → Executable file
0
websocket/websocket.go
Normal file → Executable file
Loading…
Reference in New Issue
Block a user