171 lines
3.7 KiB
Go
171 lines
3.7 KiB
Go
|
|
// Package video implements logic to stream video from a config.
|
||
|
|
package video
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bufio"
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"os/exec"
|
||
|
|
"sync"
|
||
|
|
|
||
|
|
"github.com/chathaway-codes/home-sensors/v2/internal/pipespy"
|
||
|
|
"github.com/chathaway-codes/home-sensors/v2/internal/watcher/config"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
"github.com/pion/webrtc/v3"
|
||
|
|
"github.com/pion/webrtc/v3/pkg/media/ivfreader"
|
||
|
|
"github.com/rs/zerolog/log"
|
||
|
|
)
|
||
|
|
|
||
|
|
var Default = &Mod{}
|
||
|
|
|
||
|
|
type Video struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
h264Cmd, ivfCmd *exec.Cmd
|
||
|
|
ivfListeners map[string]chan<- []byte
|
||
|
|
ivfCodecReady chan struct{}
|
||
|
|
ivfCodec string
|
||
|
|
cancelFunc func()
|
||
|
|
ctx context.Context
|
||
|
|
}
|
||
|
|
|
||
|
|
func New(cfg *config.Config) (*Video, error) {
|
||
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||
|
|
// Setup commands
|
||
|
|
h264cmd := exec.CommandContext(ctx, cfg.H264Cmd.Binary, cfg.H264Cmd.Arguments...)
|
||
|
|
ivfCmd := exec.CommandContext(ctx, cfg.IVFCmd.Binary, cfg.IVFCmd.Arguments...)
|
||
|
|
|
||
|
|
return &Video{
|
||
|
|
h264Cmd: h264cmd,
|
||
|
|
ivfCmd: ivfCmd,
|
||
|
|
ivfListeners: make(map[string]chan<- []byte),
|
||
|
|
ivfCodecReady: make(chan struct{}),
|
||
|
|
cancelFunc: cancelFunc,
|
||
|
|
ctx: ctx,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run launches the commands and begins creating data. It will block until Done is called.
|
||
|
|
func (v *Video) Run() {
|
||
|
|
pipe := pipespy.New()
|
||
|
|
|
||
|
|
pipe.Add(pipespy.NewCmd(v.h264Cmd))
|
||
|
|
ivfSnoop := pipe.Add(pipespy.NewCmd(v.ivfCmd)).Snoop()
|
||
|
|
|
||
|
|
log.Info().Str("cmd", v.h264Cmd.String()).Msg("h264 command")
|
||
|
|
log.Info().Str("cmd", v.ivfCmd.String()).Msg("ivf command")
|
||
|
|
|
||
|
|
// Log stderr if it appears
|
||
|
|
go logToStdErr("h264", &v.h264Cmd.Stderr)
|
||
|
|
go logToStdErr("ivf", &v.ivfCmd.Stderr)
|
||
|
|
|
||
|
|
cleanUp := pipe.Start()
|
||
|
|
defer func() {
|
||
|
|
errs := cleanUp()
|
||
|
|
for _, err := range errs {
|
||
|
|
log.Err(err).Send()
|
||
|
|
}
|
||
|
|
}()
|
||
|
|
|
||
|
|
go func(r io.Reader) {
|
||
|
|
ivf, header, err := ivfreader.NewWith(r)
|
||
|
|
if err != nil {
|
||
|
|
log.Error().Err(err).Msg("failed to create ivfreader")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine video codec
|
||
|
|
var trackCodec string
|
||
|
|
switch header.FourCC {
|
||
|
|
case "AV01":
|
||
|
|
trackCodec = webrtc.MimeTypeAV1
|
||
|
|
case "VP90":
|
||
|
|
trackCodec = webrtc.MimeTypeVP9
|
||
|
|
case "VP80":
|
||
|
|
trackCodec = webrtc.MimeTypeVP8
|
||
|
|
default:
|
||
|
|
log.Error().Err(err).Str("fourcc", header.FourCC).Msg("unable to handle FourCC")
|
||
|
|
}
|
||
|
|
|
||
|
|
v.mu.Lock()
|
||
|
|
v.ivfCodec = trackCodec
|
||
|
|
close(v.ivfCodecReady)
|
||
|
|
v.mu.Unlock()
|
||
|
|
|
||
|
|
log.Info().Msgf("starting to stream IVF with codec %q", trackCodec)
|
||
|
|
|
||
|
|
for {
|
||
|
|
select {
|
||
|
|
case <-v.ctx.Done():
|
||
|
|
// Exit cleanly
|
||
|
|
return
|
||
|
|
default:
|
||
|
|
// do nothing
|
||
|
|
}
|
||
|
|
frame, _, ivfErr := ivf.ParseNextFrame()
|
||
|
|
if errors.Is(ivfErr, io.EOF) {
|
||
|
|
log.Debug().Msg("all video frames parsed and sent")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if ivfErr != nil {
|
||
|
|
log.Error().Err(err).Msg("failed to parse frame")
|
||
|
|
}
|
||
|
|
|
||
|
|
v.mu.Lock()
|
||
|
|
for _, lis := range v.ivfListeners {
|
||
|
|
lis <- frame
|
||
|
|
}
|
||
|
|
v.mu.Unlock()
|
||
|
|
}
|
||
|
|
}(ivfSnoop)
|
||
|
|
log.Info().Msg("video streams started")
|
||
|
|
|
||
|
|
<-v.ctx.Done()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Join will connect to a running stream; note, it will block
|
||
|
|
// until the ivf codec is decided. Make sure to call Run first.
|
||
|
|
func (v *Video) Join() (<-chan []byte, string, func()) {
|
||
|
|
<-v.ivfCodecReady
|
||
|
|
v.mu.Lock()
|
||
|
|
defer v.mu.Unlock()
|
||
|
|
|
||
|
|
myID := uuid.New().String()
|
||
|
|
ch := make(chan []byte)
|
||
|
|
v.ivfListeners[myID] = ch
|
||
|
|
|
||
|
|
return ch, v.ivfCodec, func() {
|
||
|
|
v.mu.Lock()
|
||
|
|
defer v.mu.Unlock()
|
||
|
|
|
||
|
|
delete(v.ivfListeners, myID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func logToStdErr(name string, w *io.Writer) {
|
||
|
|
pipeReader, pipeWriter := io.Pipe()
|
||
|
|
*w = pipeWriter
|
||
|
|
|
||
|
|
lineReader := bufio.NewScanner(pipeReader)
|
||
|
|
|
||
|
|
for lineReader.Scan() {
|
||
|
|
log.Info().Str("video", name).Str("stderr", lineReader.Text()).Send()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Done stops the processing.
|
||
|
|
func (v *Video) Done() {
|
||
|
|
v.cancelFunc()
|
||
|
|
}
|
||
|
|
|
||
|
|
type Mod struct{}
|
||
|
|
|
||
|
|
func (m *Mod) Get() (*Video, error) {
|
||
|
|
cfg, err := config.Default.Get()
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return New(cfg)
|
||
|
|
}
|