detect prolonged video unavailability
This commit is contained in:
@@ -10,15 +10,43 @@ import (
|
||||
"mxl-player/internal/source"
|
||||
)
|
||||
|
||||
const DefaultVideoReadTimeout = 200 * time.Millisecond
|
||||
const (
|
||||
DefaultVideoReadTimeout = 200 * time.Millisecond
|
||||
DefaultVideoUnavailableAfter = 2 * time.Second
|
||||
DefaultTemporaryRetryDelay = 10 * time.Millisecond
|
||||
)
|
||||
|
||||
type VideoFactory struct {
|
||||
ReadTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
UnavailableAfter time.Duration
|
||||
}
|
||||
|
||||
type videoReader struct {
|
||||
source *source.Source
|
||||
timeout time.Duration
|
||||
source localVideoSource
|
||||
readTimeout time.Duration
|
||||
unavailableAfter time.Duration
|
||||
retryDelay time.Duration
|
||||
now func() time.Time
|
||||
wait temporaryWaitFunc
|
||||
}
|
||||
|
||||
type localVideoSource interface {
|
||||
ReadOnceCtx(context.Context, time.Duration) (source.Frame, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
type temporaryWaitFunc func(context.Context, time.Duration) error
|
||||
|
||||
func waitForTemporaryRetry(ctx context.Context, delay time.Duration) error {
|
||||
timer := time.NewTimer(delay)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case <-timer.C:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
var _ playback.VideoReaderFactory = VideoFactory{}
|
||||
@@ -56,34 +84,73 @@ func (f VideoFactory) OpenVideo(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := f.ReadTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultVideoReadTimeout
|
||||
readTimeout := f.ReadTimeout
|
||||
if readTimeout <= 0 {
|
||||
readTimeout = DefaultVideoReadTimeout
|
||||
}
|
||||
|
||||
unavailableAfter := f.UnavailableAfter
|
||||
if unavailableAfter <= 0 {
|
||||
unavailableAfter = DefaultVideoUnavailableAfter
|
||||
}
|
||||
|
||||
return &videoReader{
|
||||
source: src,
|
||||
timeout: timeout,
|
||||
source: src,
|
||||
readTimeout: readTimeout,
|
||||
unavailableAfter: unavailableAfter,
|
||||
retryDelay: DefaultTemporaryRetryDelay,
|
||||
now: time.Now,
|
||||
wait: waitForTemporaryRetry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *videoReader) ReadVideo(
|
||||
ctx context.Context,
|
||||
) (playback.VideoFrame, error) {
|
||||
frame, err := r.source.NextCtx(ctx, r.timeout)
|
||||
if err != nil {
|
||||
return playback.VideoFrame{}, err
|
||||
}
|
||||
var unavailableSince time.Time
|
||||
|
||||
return playback.VideoFrame{
|
||||
Index: frame.Index,
|
||||
Width: frame.Width,
|
||||
Height: frame.Height,
|
||||
Stride: frame.Stride,
|
||||
Size: frame.Size,
|
||||
Invalid: frame.Invalid,
|
||||
Payload: frame.Payload,
|
||||
}, nil
|
||||
for {
|
||||
frame, err := r.source.ReadOnceCtx(ctx, r.readTimeout)
|
||||
if err == nil {
|
||||
return playback.VideoFrame{
|
||||
Index: frame.Index,
|
||||
Width: frame.Width,
|
||||
Height: frame.Height,
|
||||
Stride: frame.Stride,
|
||||
Size: frame.Size,
|
||||
Invalid: frame.Invalid,
|
||||
Payload: frame.Payload,
|
||||
}, nil
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return playback.VideoFrame{}, ctx.Err()
|
||||
}
|
||||
if source.KindOf(err) != source.ErrorKindTemporary {
|
||||
return playback.VideoFrame{}, err
|
||||
}
|
||||
|
||||
now := r.now()
|
||||
if unavailableSince.IsZero() {
|
||||
unavailableSince = now
|
||||
} else if now.Sub(unavailableSince) >= r.unavailableAfter {
|
||||
return playback.VideoFrame{}, &source.SourceError{
|
||||
Op: "read local MXL video",
|
||||
Kind: source.ErrorKindUnavailable,
|
||||
Err: fmt.Errorf(
|
||||
"no video data for %s: %w",
|
||||
r.unavailableAfter,
|
||||
err,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.wait(ctx, r.retryDelay); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return playback.VideoFrame{}, ctx.Err()
|
||||
}
|
||||
return playback.VideoFrame{}, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *videoReader) Close() error {
|
||||
|
||||
Reference in New Issue
Block a user