fix: cannot parse input config
mod: set gin release mode fix: input item start and end args not work fix: cannot auto play next video
This commit is contained in:
parent
104195b977
commit
6093f5d36b
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
video
|
||||
config.json
|
||||
logs
|
||||
dist
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
1
main.go
1
main.go
@ -47,7 +47,6 @@ func input() {
|
||||
GlobalStreamer.Prev()
|
||||
case "quit":
|
||||
GlobalStreamer.Close()
|
||||
os.Exit(0)
|
||||
case "current":
|
||||
fmt.Println(GlobalStreamer.GetCurrentVideoPath())
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user