mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-10-26 06:50:24 +08:00
fix: server restart
This commit is contained in:
@@ -16,6 +16,6 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
// ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*100))
|
// ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Second*100))
|
||||||
go m7s.Run(ctx, "config1.yaml")
|
m7s.AddRootTaskWithContext(ctx, m7s.NewServer("config2.yaml"))
|
||||||
m7s.NewServer().Run(ctx, "config2.yaml")
|
m7s.Run(ctx, "config1.yaml")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ func main() {
|
|||||||
flag.BoolVar(&multi, "multi", false, "debug")
|
flag.BoolVar(&multi, "multi", false, "debug")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if multi {
|
if multi {
|
||||||
go m7s.Run(ctx, "config1.yaml")
|
m7s.AddRootTaskWithContext(ctx, m7s.NewServer("config2.yaml"))
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
m7s.NewServer().Run(ctx, "config2.yaml")
|
m7s.Run(ctx, "config1.yaml")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ func main() {
|
|||||||
flag.BoolVar(&multi, "multi", false, "debug")
|
flag.BoolVar(&multi, "multi", false, "debug")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if multi {
|
if multi {
|
||||||
go m7s.Run(ctx, "config1.yaml")
|
m7s.AddRootTaskWithContext(ctx, m7s.NewServer("config2.yaml"))
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
m7s.NewServer().Run(ctx, "config2.yaml")
|
m7s.Run(ctx, "config1.yaml")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ func main() {
|
|||||||
flag.BoolVar(&multi, "multi", false, "debug")
|
flag.BoolVar(&multi, "multi", false, "debug")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if multi {
|
if multi {
|
||||||
go m7s.Run(ctx, "config1.yaml")
|
m7s.AddRootTaskWithContext(ctx, m7s.NewServer("config2.yaml"))
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
m7s.NewServer().Run(ctx, "config2.yaml")
|
m7s.Run(ctx, "config1.yaml")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ var (
|
|||||||
ErrPublishTimeout = errors.New("publish timeout")
|
ErrPublishTimeout = errors.New("publish timeout")
|
||||||
ErrPublishIdleTimeout = errors.New("publish idle timeout")
|
ErrPublishIdleTimeout = errors.New("publish idle timeout")
|
||||||
ErrPublishDelayCloseTimeout = errors.New("publish delay close timeout")
|
ErrPublishDelayCloseTimeout = errors.New("publish delay close timeout")
|
||||||
ErrPublishWaitCloseTimeout = errors.New("publish wait close timeout")
|
|
||||||
ErrPushRemoteURLExist = errors.New("push remote url exist")
|
ErrPushRemoteURLExist = errors.New("push remote url exist")
|
||||||
ErrSubscribeTimeout = errors.New("subscribe timeout")
|
ErrSubscribeTimeout = errors.New("subscribe timeout")
|
||||||
ErrRestart = errors.New("restart")
|
ErrRestart = errors.New("restart")
|
||||||
|
|||||||
@@ -67,3 +67,8 @@ func initFatalLog() *os.File {
|
|||||||
}
|
}
|
||||||
return logFile
|
return logFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Exist(filename string) bool {
|
||||||
|
_, err := os.Stat(filename)
|
||||||
|
return err == nil || os.IsExist(err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RootTask MarcoLongTask
|
|
||||||
var idG atomic.Uint32
|
var idG atomic.Uint32
|
||||||
|
|
||||||
func GetNextTaskID() uint32 {
|
func GetNextTaskID() uint32 {
|
||||||
return idG.Add(1)
|
return idG.Add(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var RootTask MarcoLongTask
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootTask.initTask(context.Background(), &RootTask)
|
RootTask.initTask(context.Background(), &RootTask)
|
||||||
|
RootTask.Description = map[string]any{
|
||||||
|
"title": "RootTask",
|
||||||
|
}
|
||||||
RootTask.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
|
RootTask.Logger = slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,6 +162,10 @@ func (mt *MarcoTask) AddChan(channel any, callback any) *ChannelTask {
|
|||||||
func (mt *MarcoTask) run() {
|
func (mt *MarcoTask) run() {
|
||||||
cases := []reflect.SelectCase{{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(mt.addSub)}}
|
cases := []reflect.SelectCase{{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(mt.addSub)}}
|
||||||
defer func() {
|
defer func() {
|
||||||
|
err := recover()
|
||||||
|
if err != nil {
|
||||||
|
mt.Stop(err.(error))
|
||||||
|
}
|
||||||
stopReason := mt.StopReason()
|
stopReason := mt.StopReason()
|
||||||
for _, task := range mt.children {
|
for _, task := range mt.children {
|
||||||
task.Stop(stopReason)
|
task.Stop(stopReason)
|
||||||
|
|||||||
@@ -122,9 +122,12 @@ func (task *Task) StopReason() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (task *Task) Stop(err error) {
|
func (task *Task) Stop(err error) {
|
||||||
|
if err == nil {
|
||||||
|
panic("task stop with nil error")
|
||||||
|
}
|
||||||
if task.CancelCauseFunc != nil {
|
if task.CancelCauseFunc != nil {
|
||||||
if task.Logger != nil {
|
if task.Logger != nil {
|
||||||
task.Debug("task stop", "reason", err.Error(), "elapsed", time.Since(task.StartTime), "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
|
task.Debug("task stop", "reason", err, "elapsed", time.Since(task.StartTime), "taskId", task.ID, "taskType", task.GetTaskType(), "ownerType", task.GetOwnerType())
|
||||||
}
|
}
|
||||||
task.CancelCauseFunc(err)
|
task.CancelCauseFunc(err)
|
||||||
}
|
}
|
||||||
|
|||||||
266
plugin/flv/api.go
Normal file
266
plugin/flv/api.go
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
package plugin_flv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"m7s.live/m7s/v5/pkg/util"
|
||||||
|
flv "m7s.live/m7s/v5/plugin/flv/pkg"
|
||||||
|
rtmp "m7s.live/m7s/v5/plugin/rtmp/pkg"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (plugin *FLVPlugin) Download(w http.ResponseWriter, r *http.Request) {
|
||||||
|
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/download/"), ".flv")
|
||||||
|
singleFile := filepath.Join(plugin.Path, streamPath+".flv")
|
||||||
|
query := r.URL.Query()
|
||||||
|
rangeStr := strings.Split(query.Get("range"), "-")
|
||||||
|
s, err := strconv.Atoi(rangeStr[0])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startTime := time.UnixMilli(int64(s))
|
||||||
|
e, err := strconv.Atoi(rangeStr[1])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
endTime := time.UnixMilli(int64(e))
|
||||||
|
timeRange := endTime.Sub(startTime)
|
||||||
|
plugin.Info("download", "stream", streamPath, "start", startTime, "end", endTime)
|
||||||
|
dir := filepath.Join(plugin.Path, streamPath)
|
||||||
|
if util.Exist(singleFile) {
|
||||||
|
|
||||||
|
} else if util.Exist(dir) {
|
||||||
|
var fileList []fs.FileInfo
|
||||||
|
var found bool
|
||||||
|
var startOffsetTime time.Duration
|
||||||
|
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if info.IsDir() || !strings.HasSuffix(info.Name(), ".flv") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
modTime := info.ModTime()
|
||||||
|
//tmp, _ := strconv.Atoi(strings.TrimSuffix(info.Name(), ".flv"))
|
||||||
|
//fileStartTime := time.Unix(tmp, 10)
|
||||||
|
if !found {
|
||||||
|
if modTime.After(startTime) {
|
||||||
|
found = true
|
||||||
|
//fmt.Println(path, modTime, startTime, found)
|
||||||
|
} else {
|
||||||
|
fileList = []fs.FileInfo{info}
|
||||||
|
startOffsetTime = startTime.Sub(modTime)
|
||||||
|
//fmt.Println(path, modTime, startTime, found)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if modTime.After(endTime) {
|
||||||
|
return fs.ErrInvalid
|
||||||
|
}
|
||||||
|
fileList = append(fileList, info)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if !found {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "video/x-flv")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment")
|
||||||
|
var writer io.Writer = w
|
||||||
|
flvHead := make([]byte, 9+4)
|
||||||
|
tagHead := make(util.Buffer, 11)
|
||||||
|
var contentLength uint64
|
||||||
|
|
||||||
|
var amf *rtmp.AMF
|
||||||
|
var metaData rtmp.EcmaArray
|
||||||
|
var filepositions []uint64
|
||||||
|
var times []float64
|
||||||
|
for pass := 0; pass < 2; pass++ {
|
||||||
|
offsetTime := startOffsetTime
|
||||||
|
var offsetTimestamp, lastTimestamp uint32
|
||||||
|
var init, seqAudioWritten, seqVideoWritten bool
|
||||||
|
if pass == 1 {
|
||||||
|
metaData["keyframes"] = map[string]any{
|
||||||
|
"filepositions": filepositions,
|
||||||
|
"times": times,
|
||||||
|
}
|
||||||
|
amf.Marshals("onMetaData", metaData)
|
||||||
|
offsetDelta := amf.Len() + 15
|
||||||
|
offset := offsetDelta + len(flvHead)
|
||||||
|
contentLength += uint64(offset)
|
||||||
|
metaData["duration"] = timeRange.Seconds()
|
||||||
|
metaData["filesize"] = contentLength
|
||||||
|
for i := range filepositions {
|
||||||
|
filepositions[i] += uint64(offset)
|
||||||
|
}
|
||||||
|
metaData["keyframes"] = map[string]any{
|
||||||
|
"filepositions": filepositions,
|
||||||
|
"times": times,
|
||||||
|
}
|
||||||
|
amf.Reset()
|
||||||
|
amf.Marshals("onMetaData", metaData)
|
||||||
|
plugin.Info("start download", "metaData", metaData)
|
||||||
|
w.Header().Set("Content-Length", strconv.FormatInt(int64(contentLength), 10))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
if offsetTime == 0 {
|
||||||
|
init = true
|
||||||
|
} else {
|
||||||
|
offsetTimestamp = -uint32(offsetTime.Milliseconds())
|
||||||
|
}
|
||||||
|
for i, info := range fileList {
|
||||||
|
if r.Context().Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(dir, info.Name())
|
||||||
|
plugin.Debug("read", "file", filePath)
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
if i == 0 {
|
||||||
|
_, err = io.ReadFull(reader, flvHead)
|
||||||
|
if pass == 1 {
|
||||||
|
// 第一次写入头
|
||||||
|
_, err = writer.Write(flvHead)
|
||||||
|
tagHead[0] = flv.FLV_TAG_TYPE_SCRIPT
|
||||||
|
l := amf.Len()
|
||||||
|
tagHead[1] = byte(l >> 16)
|
||||||
|
tagHead[2] = byte(l >> 8)
|
||||||
|
tagHead[3] = byte(l)
|
||||||
|
flv.PutFlvTimestamp(tagHead, 0)
|
||||||
|
writer.Write(tagHead)
|
||||||
|
writer.Write(amf.Buffer)
|
||||||
|
l += 11
|
||||||
|
binary.BigEndian.PutUint32(tagHead[:4], uint32(l))
|
||||||
|
writer.Write(tagHead[:4])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 后面的头跳过
|
||||||
|
_, err = reader.Discard(13)
|
||||||
|
if !init {
|
||||||
|
offsetTime = 0
|
||||||
|
offsetTimestamp = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for err == nil {
|
||||||
|
_, err = io.ReadFull(reader, tagHead)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tmp := tagHead
|
||||||
|
t := tmp.ReadByte()
|
||||||
|
dataLen := tmp.ReadUint24()
|
||||||
|
lastTimestamp = tmp.ReadUint24() | uint32(tmp.ReadByte())<<24
|
||||||
|
//fmt.Println(lastTimestamp, tagHead)
|
||||||
|
if init {
|
||||||
|
if t == flv.FLV_TAG_TYPE_SCRIPT {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
} else {
|
||||||
|
lastTimestamp += offsetTimestamp
|
||||||
|
if lastTimestamp >= uint32(timeRange.Milliseconds()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if pass == 0 {
|
||||||
|
data := make([]byte, dataLen+4)
|
||||||
|
_, err = io.ReadFull(reader, data)
|
||||||
|
frameType := (data[0] >> 4) & 0b0111
|
||||||
|
idr := frameType == 1 || frameType == 4
|
||||||
|
if idr {
|
||||||
|
filepositions = append(filepositions, contentLength)
|
||||||
|
times = append(times, float64(lastTimestamp)/1000)
|
||||||
|
}
|
||||||
|
contentLength += uint64(11 + dataLen + 4)
|
||||||
|
} else {
|
||||||
|
//fmt.Println("write", lastTimestamp)
|
||||||
|
flv.PutFlvTimestamp(tagHead, lastTimestamp)
|
||||||
|
_, err = writer.Write(tagHead)
|
||||||
|
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case flv.FLV_TAG_TYPE_SCRIPT:
|
||||||
|
if pass == 0 {
|
||||||
|
data := make([]byte, dataLen+4)
|
||||||
|
_, err = io.ReadFull(reader, data)
|
||||||
|
amf = &rtmp.AMF{
|
||||||
|
Buffer: util.Buffer(data[1+2+len("onMetaData") : len(data)-4]),
|
||||||
|
}
|
||||||
|
var obj any
|
||||||
|
obj, err = amf.Unmarshal()
|
||||||
|
metaData = obj.(map[string]any)
|
||||||
|
} else {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
}
|
||||||
|
case flv.FLV_TAG_TYPE_AUDIO:
|
||||||
|
if !seqAudioWritten {
|
||||||
|
if pass == 0 {
|
||||||
|
contentLength += uint64(11 + dataLen + 4)
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
} else {
|
||||||
|
flv.PutFlvTimestamp(tagHead, 0)
|
||||||
|
_, err = writer.Write(tagHead)
|
||||||
|
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||||
|
}
|
||||||
|
seqAudioWritten = true
|
||||||
|
} else {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
}
|
||||||
|
case flv.FLV_TAG_TYPE_VIDEO:
|
||||||
|
if !seqVideoWritten {
|
||||||
|
if pass == 0 {
|
||||||
|
contentLength += uint64(11 + dataLen + 4)
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
} else {
|
||||||
|
flv.PutFlvTimestamp(tagHead, 0)
|
||||||
|
_, err = writer.Write(tagHead)
|
||||||
|
_, err = io.CopyN(writer, reader, int64(dataLen+4))
|
||||||
|
}
|
||||||
|
seqVideoWritten = true
|
||||||
|
} else {
|
||||||
|
if lastTimestamp >= uint32(offsetTime.Milliseconds()) {
|
||||||
|
data := make([]byte, dataLen+4)
|
||||||
|
_, err = io.ReadFull(reader, data)
|
||||||
|
frameType := (data[0] >> 4) & 0b0111
|
||||||
|
idr := frameType == 1 || frameType == 4
|
||||||
|
if idr {
|
||||||
|
init = true
|
||||||
|
plugin.Debug("init", "lastTimestamp", lastTimestamp)
|
||||||
|
if pass == 0 {
|
||||||
|
filepositions = append(filepositions, contentLength)
|
||||||
|
times = append(times, float64(lastTimestamp)/1000)
|
||||||
|
contentLength += uint64(11 + dataLen + 4)
|
||||||
|
} else {
|
||||||
|
flv.PutFlvTimestamp(tagHead, 0)
|
||||||
|
_, err = writer.Write(tagHead)
|
||||||
|
_, err = writer.Write(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offsetTimestamp = lastTimestamp
|
||||||
|
err = file.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.Info("end download")
|
||||||
|
} else {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,21 +17,22 @@ import (
|
|||||||
|
|
||||||
type FLVPlugin struct {
|
type FLVPlugin struct {
|
||||||
m7s.Plugin
|
m7s.Plugin
|
||||||
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig m7s.DefaultYaml = `publish:
|
const defaultConfig m7s.DefaultYaml = `publish:
|
||||||
speed: 1`
|
speed: 1`
|
||||||
|
|
||||||
func (p *FLVPlugin) OnInit() error {
|
func (plugin *FLVPlugin) OnInit() error {
|
||||||
for streamPath, url := range p.GetCommonConf().PullOnStart {
|
for streamPath, url := range plugin.GetCommonConf().PullOnStart {
|
||||||
p.Pull(streamPath, url)
|
plugin.Pull(streamPath, url)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, PullFLV, RecordFlv)
|
var _ = m7s.InstallPlugin[FLVPlugin](defaultConfig, PullFLV, RecordFlv)
|
||||||
|
|
||||||
func (p *FLVPlugin) WriteFlvHeader(sub *m7s.Subscriber) (flv net.Buffers) {
|
func (plugin *FLVPlugin) WriteFlvHeader(sub *m7s.Subscriber) (flv net.Buffers) {
|
||||||
at, vt := &sub.Publisher.AudioTrack, &sub.Publisher.VideoTrack
|
at, vt := &sub.Publisher.AudioTrack, &sub.Publisher.VideoTrack
|
||||||
hasAudio, hasVideo := at.AVTrack != nil && sub.SubAudio, vt.AVTrack != nil && sub.SubVideo
|
hasAudio, hasVideo := at.AVTrack != nil && sub.SubAudio, vt.AVTrack != nil && sub.SubVideo
|
||||||
var amf rtmp.AMF
|
var amf rtmp.AMF
|
||||||
@@ -70,13 +73,33 @@ func (p *FLVPlugin) WriteFlvHeader(sub *m7s.Subscriber) (flv net.Buffers) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (plugin *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".flv")
|
streamPath := strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/"), ".flv")
|
||||||
if r.URL.RawQuery != "" {
|
if r.URL.RawQuery != "" {
|
||||||
streamPath += "?" + r.URL.RawQuery
|
streamPath += "?" + r.URL.RawQuery
|
||||||
}
|
}
|
||||||
|
query := r.URL.Query()
|
||||||
sub, err := p.Subscribe(r.Context(), streamPath)
|
startTimeStr := query.Get("start")
|
||||||
|
speedStr := query.Get("speed")
|
||||||
|
speed, err := strconv.ParseFloat(speedStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
speed = 1
|
||||||
|
}
|
||||||
|
s, err := strconv.Atoi(startTimeStr)
|
||||||
|
if err == nil {
|
||||||
|
startTime := time.UnixMilli(int64(s))
|
||||||
|
var vod Vod
|
||||||
|
vod.Context = r.Context()
|
||||||
|
vod.Logger = plugin.Logger.With("streamPath", streamPath)
|
||||||
|
if err = vod.Init(startTime, filepath.Join(plugin.Path, streamPath)); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vod.SetSpeed(speed)
|
||||||
|
err = vod.Run()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sub, err := plugin.Subscribe(r.Context(), streamPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
@@ -84,7 +107,7 @@ func (p *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "video/x-flv")
|
w.Header().Set("Content-Type", "video/x-flv")
|
||||||
w.Header().Set("Transfer-Encoding", "identity")
|
w.Header().Set("Transfer-Encoding", "identity")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
wto := p.GetCommonConf().WriteTimeout
|
wto := plugin.GetCommonConf().WriteTimeout
|
||||||
var gotFlvTag func(net.Buffers) error
|
var gotFlvTag func(net.Buffers) error
|
||||||
var b [15]byte
|
var b [15]byte
|
||||||
|
|
||||||
@@ -103,7 +126,7 @@ func (p *FLVPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
}
|
}
|
||||||
flv := p.WriteFlvHeader(sub)
|
flv := plugin.WriteFlvHeader(sub)
|
||||||
copy(b[:4], flv[3])
|
copy(b[:4], flv[3])
|
||||||
gotFlvTag(flv[:3])
|
gotFlvTag(flv[:3])
|
||||||
rtmpData2FlvTag := func(t byte, data *rtmp.RTMPData) error {
|
rtmpData2FlvTag := func(t byte, data *rtmp.RTMPData) error {
|
||||||
|
|||||||
@@ -52,10 +52,17 @@ func (w *FlvWriter) WriteTag(t byte, ts, dataSize uint32, payload ...[]byte) (er
|
|||||||
// return append(append(append(flv, b), avcc...), util.PutBE(b.Malloc(4), dataSize+11))
|
// return append(append(append(flv, b), avcc...), util.PutBE(b.Malloc(4), dataSize+11))
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
func PutFlvTimestamp(header []byte, timestamp uint32) {
|
||||||
|
header[4] = byte(timestamp >> 16)
|
||||||
|
header[5] = byte(timestamp >> 8)
|
||||||
|
header[6] = byte(timestamp)
|
||||||
|
header[7] = byte(timestamp >> 24)
|
||||||
|
}
|
||||||
|
|
||||||
func WriteFLVTagHead(t uint8, ts, dataSize uint32, b []byte) {
|
func WriteFLVTagHead(t uint8, ts, dataSize uint32, b []byte) {
|
||||||
b[0] = t
|
b[0] = t
|
||||||
b[1], b[2], b[3] = byte(dataSize>>16), byte(dataSize>>8), byte(dataSize)
|
b[1], b[2], b[3] = byte(dataSize>>16), byte(dataSize>>8), byte(dataSize)
|
||||||
b[4], b[5], b[6], b[7] = byte(ts>>16), byte(ts>>8), byte(ts), byte(ts>>24)
|
PutFlvTimestamp(b, ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
//func WriteFLVTag(w io.Writer, t byte, timestamp uint32, payload ...[]byte) (n int64, err error) {
|
//func WriteFLVTag(w io.Writer, t byte, timestamp uint32, payload ...[]byte) (n int64, err error) {
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var writeMetaTagQueueTask util.MarcoLongTask
|
type WriteFlvMetaTagQueueTask struct {
|
||||||
|
util.MarcoLongTask
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeMetaTagQueueTask WriteFlvMetaTagQueueTask
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
util.RootTask.AddTask(&writeMetaTagQueueTask)
|
m7s.AddRootTask(&writeMetaTagQueueTask)
|
||||||
}
|
}
|
||||||
|
|
||||||
type writeMetaTagTask struct {
|
type writeMetaTagTask struct {
|
||||||
|
|||||||
@@ -1,11 +1,170 @@
|
|||||||
package flv
|
package flv
|
||||||
|
|
||||||
import "m7s.live/m7s/v5/pkg/util"
|
import (
|
||||||
|
"bufio"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"m7s.live/m7s/v5/pkg/util"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type Vod struct {
|
type Vod struct {
|
||||||
util.Task
|
util.Task
|
||||||
|
io.WriteCloser
|
||||||
|
Dir string
|
||||||
|
lastTimestamp uint32
|
||||||
|
speed float64
|
||||||
|
singleFile bool
|
||||||
|
offsetTime time.Duration
|
||||||
|
offsetTimestamp uint32
|
||||||
|
fileList []fs.FileInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vod) Start() error {
|
func (v *Vod) SetSpeed(speed float64) {
|
||||||
return nil
|
v.speed = speed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vod) speedControl() {
|
||||||
|
targetTime := time.Duration(float64(time.Since(v.StartTime)) * v.speed)
|
||||||
|
sleepTime := time.Duration(v.lastTimestamp)*time.Millisecond - targetTime
|
||||||
|
//fmt.Println("sleepTime", sleepTime, time.Since(start).Milliseconds(), lastTimestamp)
|
||||||
|
if sleepTime > 0 {
|
||||||
|
time.Sleep(sleepTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vod) Init(startTime time.Time, dir string) (err error) {
|
||||||
|
v.Dir = dir
|
||||||
|
singleFile := filepath.Join(dir, ".flv")
|
||||||
|
if util.Exist(singleFile) {
|
||||||
|
v.singleFile = true
|
||||||
|
} else if util.Exist(dir) {
|
||||||
|
var found bool
|
||||||
|
err = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
|
||||||
|
if info.IsDir() || !strings.HasSuffix(info.Name(), ".flv") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
modTime := info.ModTime()
|
||||||
|
//tmp, _ := strconv.Atoi(strings.TrimSuffix(info.Name(), ".flv"))
|
||||||
|
//fileStartTime := time.Unix(tmp, 10)
|
||||||
|
if !found {
|
||||||
|
if modTime.After(startTime) {
|
||||||
|
found = true
|
||||||
|
//fmt.Println(path, modTime, startTime, found)
|
||||||
|
} else {
|
||||||
|
v.fileList = []fs.FileInfo{info}
|
||||||
|
v.offsetTime = startTime.Sub(modTime)
|
||||||
|
//fmt.Println(path, modTime, startTime, found)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.fileList = append(v.fileList, info)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if !found {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vod) Run() (err error) {
|
||||||
|
flvHead := make([]byte, 9+4)
|
||||||
|
tagHead := make(util.Buffer, 11)
|
||||||
|
var file *os.File
|
||||||
|
var init, seqAudioWritten, seqVideoWritten bool
|
||||||
|
if v.offsetTime == 0 {
|
||||||
|
init = true
|
||||||
|
} else {
|
||||||
|
v.offsetTimestamp = -uint32(v.offsetTime.Milliseconds())
|
||||||
|
}
|
||||||
|
for i, info := range v.fileList {
|
||||||
|
if v.IsStopped() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
filePath := filepath.Join(v.Dir, info.Name())
|
||||||
|
v.Debug("read", "file", filePath)
|
||||||
|
file, err = os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reader := bufio.NewReader(file)
|
||||||
|
if i == 0 {
|
||||||
|
// 第一次写入头
|
||||||
|
_, err = io.ReadFull(reader, flvHead)
|
||||||
|
_, err = v.Write(flvHead)
|
||||||
|
} else {
|
||||||
|
// 后面的头跳过
|
||||||
|
_, err = reader.Discard(13)
|
||||||
|
if !init {
|
||||||
|
v.offsetTime = 0
|
||||||
|
v.offsetTimestamp = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for err == nil {
|
||||||
|
_, err = io.ReadFull(reader, tagHead)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tmp := tagHead
|
||||||
|
t := tmp.ReadByte()
|
||||||
|
dataLen := tmp.ReadUint24()
|
||||||
|
v.lastTimestamp = tmp.ReadUint24() | uint32(tmp.ReadByte())<<24
|
||||||
|
//fmt.Println(lastTimestamp, tagHead)
|
||||||
|
if init {
|
||||||
|
if t == FLV_TAG_TYPE_SCRIPT {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
} else {
|
||||||
|
v.lastTimestamp += v.offsetTimestamp
|
||||||
|
PutFlvTimestamp(tagHead, v.lastTimestamp)
|
||||||
|
_, err = v.Write(tagHead)
|
||||||
|
_, err = io.CopyN(v, reader, int64(dataLen+4))
|
||||||
|
v.speedControl()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case FLV_TAG_TYPE_SCRIPT:
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
case FLV_TAG_TYPE_AUDIO:
|
||||||
|
if !seqAudioWritten {
|
||||||
|
PutFlvTimestamp(tagHead, 0)
|
||||||
|
_, err = v.Write(tagHead)
|
||||||
|
_, err = io.CopyN(v, reader, int64(dataLen+4))
|
||||||
|
seqAudioWritten = true
|
||||||
|
} else {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
}
|
||||||
|
case FLV_TAG_TYPE_VIDEO:
|
||||||
|
if !seqVideoWritten {
|
||||||
|
PutFlvTimestamp(tagHead, 0)
|
||||||
|
_, err = v.Write(tagHead)
|
||||||
|
_, err = io.CopyN(v, reader, int64(dataLen+4))
|
||||||
|
seqVideoWritten = true
|
||||||
|
} else {
|
||||||
|
if v.lastTimestamp >= uint32(v.offsetTime.Milliseconds()) {
|
||||||
|
data := make([]byte, dataLen+4)
|
||||||
|
_, err = io.ReadFull(reader, data)
|
||||||
|
frameType := (data[0] >> 4) & 0b0111
|
||||||
|
idr := frameType == 1 || frameType == 4
|
||||||
|
if idr {
|
||||||
|
init = true
|
||||||
|
//fmt.Println("init", lastTimestamp)
|
||||||
|
PutFlvTimestamp(tagHead, 0)
|
||||||
|
_, err = v.Write(tagHead)
|
||||||
|
_, err = v.Write(data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err = reader.Discard(int(dataLen) + 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.offsetTimestamp = v.lastTimestamp
|
||||||
|
err = file.Close()
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
30
server.go
30
server.go
@@ -91,7 +91,21 @@ func NewServer(conf any) (s *Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Run(ctx context.Context, conf any) error {
|
func Run(ctx context.Context, conf any) error {
|
||||||
return util.RootTask.AddTaskWithContext(ctx, NewServer(conf)).WaitStopped()
|
for {
|
||||||
|
if err := util.RootTask.AddTaskWithContext(ctx, NewServer(conf)).WaitStopped(); err != ErrRestart {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddRootTask[T util.ITask](task T) T {
|
||||||
|
util.RootTask.AddTask(task)
|
||||||
|
return task
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddRootTaskWithContext[T util.ITask](ctx context.Context, task T) T {
|
||||||
|
util.RootTask.AddTaskWithContext(ctx, task)
|
||||||
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
type rawconfig = map[string]map[string]any
|
type rawconfig = map[string]map[string]any
|
||||||
@@ -259,15 +273,11 @@ func (s *Server) Dispose() {
|
|||||||
_ = s.tcplis.Close()
|
_ = s.tcplis.Close()
|
||||||
_ = s.grpcClientConn.Close()
|
_ = s.grpcClientConn.Close()
|
||||||
s.config.HTTP.StopListen()
|
s.config.HTTP.StopListen()
|
||||||
if err := s.StopReason(); err == ErrRestart {
|
if s.DB != nil {
|
||||||
var server Server
|
db, err := s.DB.DB()
|
||||||
server.ID = s.ID
|
if err == nil {
|
||||||
server.Meta = s.Meta
|
err = db.Close()
|
||||||
server.DB = s.DB
|
}
|
||||||
*s = server
|
|
||||||
util.RootTask.AddTask(s)
|
|
||||||
} else {
|
|
||||||
s.Info("server stopped", "err", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package test
|
package test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"m7s.live/m7s/v5"
|
"m7s.live/m7s/v5"
|
||||||
"m7s.live/m7s/v5/pkg"
|
"m7s.live/m7s/v5/pkg"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,8 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestRestart(b *testing.T) {
|
func TestRestart(b *testing.T) {
|
||||||
ctx := context.TODO()
|
conf := map[string]map[string]any{"global": {"loglevel": "debug"}}
|
||||||
var server = m7s.NewServer()
|
var server *m7s.Server
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Second * 2)
|
time.Sleep(time.Second * 2)
|
||||||
server.Stop(pkg.ErrRestart)
|
server.Stop(pkg.ErrRestart)
|
||||||
@@ -22,7 +21,13 @@ func TestRestart(b *testing.T) {
|
|||||||
server.Stop(pkg.ErrStopFromAPI)
|
server.Stop(pkg.ErrStopFromAPI)
|
||||||
b.Log("server stop3")
|
b.Log("server stop3")
|
||||||
}()
|
}()
|
||||||
if err := server.Run(ctx, map[string]map[string]any{"global": {"loglevel": "debug"}}); err != pkg.ErrStopFromAPI {
|
for {
|
||||||
b.Error("server.Run should return ErrStopFromAPI", err)
|
server = m7s.NewServer(conf)
|
||||||
|
if err := m7s.AddRootTask(server).WaitStopped(); err != pkg.ErrRestart {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
//if err := util.RootTask.AddTask(server).WaitStopped(); err != pkg.ErrStopFromAPI {
|
||||||
|
// b.Error("server.Run should return ErrStopFromAPI", err)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user