Files
mediamtx/internal/servers/hls/muxer_instance.go
T

191 lines
3.9 KiB
Go

package hls
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/bluenviron/gohlslib/v2"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/hls"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/gin-gonic/gin"
)
const (
sessionCookieName = "hlsSession"
sessionQueryParamName = "session"
sessionCloseAfter = 30 * time.Second
sessionCleanupPeriod = sessionCloseAfter / 3
)
// this prevents directory traversal.
// functionally it's useless since there's already conf.IsValidPathName, but it's needed by CodeQL.
func absolutePathInside(base string, candidate string) (string, error) {
baseAbs, err := filepath.Abs(filepath.Clean(base))
if err != nil {
return "", err
}
candidateAbs, err := filepath.Abs(filepath.Clean(candidate))
if err != nil {
return "", err
}
if !strings.HasPrefix(candidateAbs, baseAbs) {
return "", fmt.Errorf("path escapes base directory")
}
return candidateAbs, nil
}
type instanceParent interface {
logger.Writer
closeInstance(*muxerInstance, error)
}
type muxerInstance struct {
variant conf.HLSVariant
segmentCount int
segmentDuration conf.Duration
partDuration conf.Duration
segmentMaxSize conf.StringSize
directory string
pathName string
bytesSent *atomic.Uint64
wg *sync.WaitGroup
stream *stream.Stream
server logger.Writer
parent instanceParent
ctx context.Context
ctxCancel func()
hmuxer *gohlslib.Muxer
reader *stream.Reader
}
func (mi *muxerInstance) initialize() error {
mi.Log(logger.Debug, "instance created")
var muxerDirectory string
if mi.directory != "" {
var err error
muxerDirectory, err = absolutePathInside(mi.directory, filepath.Join(mi.directory, mi.pathName))
if err != nil {
return err
}
err = os.MkdirAll(muxerDirectory, 0o755)
if err != nil {
return err
}
}
mi.hmuxer = &gohlslib.Muxer{
Variant: gohlslib.MuxerVariant(mi.variant),
SegmentCount: mi.segmentCount,
SegmentMinDuration: time.Duration(mi.segmentDuration),
PartMinDuration: time.Duration(mi.partDuration),
SegmentMaxSize: uint64(mi.segmentMaxSize),
Directory: muxerDirectory,
OnEncodeError: func(err error) {
mi.Log(logger.Warn, err.Error())
},
}
mi.reader = &stream.Reader{
SkipOutboundBytes: true,
Parent: mi,
}
err := hls.FromStream(
mi.stream.OrigDesc,
mi.stream.OutDescCopy(),
mi.reader,
mi.hmuxer)
if err != nil {
return err
}
err = mi.hmuxer.Start()
if err != nil {
return err
}
mi.Log(logger.Info, "is converting into HLS, %s",
defs.FormatsInfo(mi.reader.Formats()))
mi.stream.AddReader(mi.reader)
mi.ctx, mi.ctxCancel = context.WithCancel(context.Background())
mi.wg.Add(1)
go mi.run()
return nil
}
// Log implements logger.Writer.
func (mi *muxerInstance) Log(level logger.Level, format string, args ...any) {
mi.parent.Log(level, format, args...)
}
func (mi *muxerInstance) close() {
mi.ctxCancel()
}
func (mi *muxerInstance) run() {
defer mi.wg.Done()
err := mi.runInner()
mi.ctxCancel()
mi.stream.RemoveReader(mi.reader)
mi.hmuxer.Close()
if mi.hmuxer.Directory != "" {
os.Remove(mi.hmuxer.Directory)
}
mi.Log(logger.Debug, "instance destroyed: %v", err)
mi.parent.closeInstance(mi, err)
}
func (mi *muxerInstance) runInner() error {
for {
select {
case <-mi.ctx.Done():
return fmt.Errorf("terminated")
case err := <-mi.reader.Error():
return err
}
}
}
func (mi *muxerInstance) handleRequest(ctx *gin.Context, isCDN bool) {
w := ctx.Writer
if !isCDN {
w = &responseWriterNoCache{ResponseWriter: w}
}
w = &responseWriterCounter{
ResponseWriter: w,
bytesSent: mi.bytesSent,
}
mi.hmuxer.Handle(w, ctx.Request)
}