12 changed files with 346 additions and 267 deletions
@ -1,124 +1,84 @@ |
|||||||
package record |
package record |
||||||
|
|
||||||
import ( |
import ( |
||||||
"context" |
|
||||||
"strings" |
|
||||||
"time" |
"time" |
||||||
|
|
||||||
"github.com/bluenviron/mediacommon/pkg/formats/fmp4" |
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/asyncwriter" |
|
||||||
"github.com/bluenviron/mediamtx/internal/conf" |
"github.com/bluenviron/mediamtx/internal/conf" |
||||||
"github.com/bluenviron/mediamtx/internal/logger" |
"github.com/bluenviron/mediamtx/internal/logger" |
||||||
"github.com/bluenviron/mediamtx/internal/stream" |
"github.com/bluenviron/mediamtx/internal/stream" |
||||||
) |
) |
||||||
|
|
||||||
// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete
|
// Agent is a record agent.
|
||||||
type OnSegmentFunc = func(string) |
|
||||||
|
|
||||||
type sample struct { |
|
||||||
*fmp4.PartSample |
|
||||||
dts time.Duration |
|
||||||
} |
|
||||||
|
|
||||||
// Agent saves streams on disk.
|
|
||||||
type Agent struct { |
type Agent struct { |
||||||
path string |
WriteQueueSize int |
||||||
partDuration time.Duration |
RecordPath string |
||||||
segmentDuration time.Duration |
Format conf.RecordFormat |
||||||
stream *stream.Stream |
PartDuration time.Duration |
||||||
onSegmentCreate OnSegmentFunc |
SegmentDuration time.Duration |
||||||
onSegmentComplete OnSegmentFunc |
PathName string |
||||||
parent logger.Writer |
Stream *stream.Stream |
||||||
|
OnSegmentCreate OnSegmentFunc |
||||||
ctx context.Context |
OnSegmentComplete OnSegmentFunc |
||||||
ctxCancel func() |
Parent logger.Writer |
||||||
writer *asyncwriter.Writer |
|
||||||
format recFormat |
restartPause time.Duration |
||||||
|
|
||||||
done chan struct{} |
currentInstance *agentInstance |
||||||
|
|
||||||
|
terminate chan struct{} |
||||||
|
done chan struct{} |
||||||
} |
} |
||||||
|
|
||||||
// NewAgent allocates an Agent.
|
// Initialize initializes Agent.
|
||||||
func NewAgent( |
func (w *Agent) Initialize() { |
||||||
writeQueueSize int, |
if w.restartPause == 0 { |
||||||
path string, |
w.restartPause = 2 * time.Second |
||||||
format conf.RecordFormat, |
|
||||||
partDuration time.Duration, |
|
||||||
segmentDuration time.Duration, |
|
||||||
pathName string, |
|
||||||
stream *stream.Stream, |
|
||||||
onSegmentCreate OnSegmentFunc, |
|
||||||
onSegmentComplete OnSegmentFunc, |
|
||||||
parent logger.Writer, |
|
||||||
) *Agent { |
|
||||||
path = strings.ReplaceAll(path, "%path", pathName) |
|
||||||
|
|
||||||
switch format { |
|
||||||
case conf.RecordFormatMPEGTS: |
|
||||||
path += ".ts" |
|
||||||
|
|
||||||
default: |
|
||||||
path += ".mp4" |
|
||||||
} |
} |
||||||
|
|
||||||
ctx, ctxCancel := context.WithCancel(context.Background()) |
w.terminate = make(chan struct{}) |
||||||
|
w.done = make(chan struct{}) |
||||||
a := &Agent{ |
|
||||||
path: path, |
|
||||||
partDuration: partDuration, |
|
||||||
segmentDuration: segmentDuration, |
|
||||||
stream: stream, |
|
||||||
onSegmentCreate: onSegmentCreate, |
|
||||||
onSegmentComplete: onSegmentComplete, |
|
||||||
parent: parent, |
|
||||||
ctx: ctx, |
|
||||||
ctxCancel: ctxCancel, |
|
||||||
done: make(chan struct{}), |
|
||||||
} |
|
||||||
|
|
||||||
a.writer = asyncwriter.New(writeQueueSize, a) |
|
||||||
|
|
||||||
switch format { |
w.currentInstance = &agentInstance{ |
||||||
case conf.RecordFormatMPEGTS: |
wrapper: w, |
||||||
a.format = newRecFormatMPEGTS(a) |
|
||||||
|
|
||||||
default: |
|
||||||
a.format = newRecFormatFMP4(a) |
|
||||||
} |
} |
||||||
|
w.currentInstance.initialize() |
||||||
|
|
||||||
go a.run() |
go w.run() |
||||||
|
|
||||||
return a |
|
||||||
} |
|
||||||
|
|
||||||
// Close closes the Agent.
|
|
||||||
func (a *Agent) Close() { |
|
||||||
a.Log(logger.Info, "recording stopped") |
|
||||||
|
|
||||||
a.ctxCancel() |
|
||||||
<-a.done |
|
||||||
} |
} |
||||||
|
|
||||||
// Log is the main logging function.
|
// Log is the main logging function.
|
||||||
func (a *Agent) Log(level logger.Level, format string, args ...interface{}) { |
func (w *Agent) Log(level logger.Level, format string, args ...interface{}) { |
||||||
a.parent.Log(level, "[record] "+format, args...) |
w.Parent.Log(level, "[record] "+format, args...) |
||||||
} |
} |
||||||
|
|
||||||
func (a *Agent) run() { |
// Close closes the agent.
|
||||||
defer close(a.done) |
func (w *Agent) Close() { |
||||||
|
w.Log(logger.Info, "recording stopped") |
||||||
a.writer.Start() |
close(w.terminate) |
||||||
|
<-w.done |
||||||
select { |
} |
||||||
case err := <-a.writer.Error(): |
|
||||||
a.Log(logger.Error, err.Error()) |
|
||||||
a.stream.RemoveReader(a.writer) |
|
||||||
|
|
||||||
case <-a.ctx.Done(): |
func (w *Agent) run() { |
||||||
a.stream.RemoveReader(a.writer) |
defer close(w.done) |
||||||
a.writer.Stop() |
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-w.currentInstance.done: |
||||||
|
w.currentInstance.close() |
||||||
|
case <-w.terminate: |
||||||
|
w.currentInstance.close() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
case <-time.After(w.restartPause): |
||||||
|
case <-w.terminate: |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
w.currentInstance = &agentInstance{ |
||||||
|
wrapper: w, |
||||||
|
} |
||||||
|
w.currentInstance.initialize() |
||||||
} |
} |
||||||
|
|
||||||
a.format.close() |
|
||||||
} |
} |
||||||
|
|||||||
@ -0,0 +1,87 @@ |
|||||||
|
package record |
||||||
|
|
||||||
|
import ( |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/bluenviron/mediacommon/pkg/formats/fmp4" |
||||||
|
|
||||||
|
"github.com/bluenviron/mediamtx/internal/asyncwriter" |
||||||
|
"github.com/bluenviron/mediamtx/internal/conf" |
||||||
|
"github.com/bluenviron/mediamtx/internal/logger" |
||||||
|
) |
||||||
|
|
||||||
|
// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete
|
||||||
|
type OnSegmentFunc = func(string) |
||||||
|
|
||||||
|
type sample struct { |
||||||
|
*fmp4.PartSample |
||||||
|
dts time.Duration |
||||||
|
} |
||||||
|
|
||||||
|
type agentInstance struct { |
||||||
|
wrapper *Agent |
||||||
|
|
||||||
|
resolvedPath string |
||||||
|
writer *asyncwriter.Writer |
||||||
|
format recFormat |
||||||
|
|
||||||
|
terminate chan struct{} |
||||||
|
done chan struct{} |
||||||
|
} |
||||||
|
|
||||||
|
func (a *agentInstance) initialize() { |
||||||
|
a.resolvedPath = strings.ReplaceAll(a.wrapper.RecordPath, "%path", a.wrapper.PathName) |
||||||
|
|
||||||
|
switch a.wrapper.Format { |
||||||
|
case conf.RecordFormatMPEGTS: |
||||||
|
a.resolvedPath += ".ts" |
||||||
|
|
||||||
|
default: |
||||||
|
a.resolvedPath += ".mp4" |
||||||
|
} |
||||||
|
|
||||||
|
a.terminate = make(chan struct{}) |
||||||
|
a.done = make(chan struct{}) |
||||||
|
|
||||||
|
a.writer = asyncwriter.New(a.wrapper.WriteQueueSize, a.wrapper) |
||||||
|
|
||||||
|
switch a.wrapper.Format { |
||||||
|
case conf.RecordFormatMPEGTS: |
||||||
|
a.format = &recFormatMPEGTS{ |
||||||
|
a: a, |
||||||
|
} |
||||||
|
a.format.initialize() |
||||||
|
|
||||||
|
default: |
||||||
|
a.format = &recFormatFMP4{ |
||||||
|
a: a, |
||||||
|
} |
||||||
|
a.format.initialize() |
||||||
|
} |
||||||
|
|
||||||
|
go a.run() |
||||||
|
} |
||||||
|
|
||||||
|
func (a *agentInstance) close() { |
||||||
|
close(a.terminate) |
||||||
|
<-a.done |
||||||
|
} |
||||||
|
|
||||||
|
func (a *agentInstance) run() { |
||||||
|
defer close(a.done) |
||||||
|
|
||||||
|
a.writer.Start() |
||||||
|
|
||||||
|
select { |
||||||
|
case err := <-a.writer.Error(): |
||||||
|
a.wrapper.Log(logger.Error, err.Error()) |
||||||
|
a.wrapper.Stream.RemoveReader(a.writer) |
||||||
|
|
||||||
|
case <-a.terminate: |
||||||
|
a.wrapper.Stream.RemoveReader(a.writer) |
||||||
|
a.writer.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
a.format.close() |
||||||
|
} |
||||||
@ -1,5 +1,6 @@ |
|||||||
package record |
package record |
||||||
|
|
||||||
type recFormat interface { |
type recFormat interface { |
||||||
|
initialize() |
||||||
close() |
close() |
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue