Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1eaab14793 | |||
| c9f237d9f1 | |||
| a7fcf12740 | |||
| e6a5e12d68 | |||
| 078f866234 | |||
| 9220bb1e7b | |||
| df15876afb | |||
| d329d493e3 |
@@ -243,7 +243,7 @@ func main() {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case args.VideoFlowId != "" && args.AudioFlowId != "":
|
case args.VideoFlowId != "" && args.AudioFlowId != "":
|
||||||
syncSrc, err = source.OpenSync(args.Domain, args.VideoFlowId, args.AudioFlowId)
|
syncSrc, err = source.OpenSameDomainSync(args.Domain, args.VideoFlowId, args.AudioFlowId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("sync source: %v", err)
|
log.Fatalf("sync source: %v", err)
|
||||||
}
|
}
|
||||||
@@ -378,7 +378,7 @@ func main() {
|
|||||||
// Try once. Return error if fails — caller loops back to select
|
// Try once. Return error if fails — caller loops back to select
|
||||||
// and can pick up new reconnect params or a new grant.
|
// and can pick up new reconnect params or a new grant.
|
||||||
if params.video != "" && params.audio != "" {
|
if params.video != "" && params.audio != "" {
|
||||||
s, e := source.OpenSync(params.domain, params.video, params.audio)
|
s, e := source.OpenSameDomainSync(params.domain, params.video, params.audio)
|
||||||
if e == nil {
|
if e == nil {
|
||||||
if r != nil {
|
if r != nil {
|
||||||
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
|
newSize := vk.DeviceSize(s.Stride()) * vk.DeviceSize(s.Height())
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func (p RetryPolicy) canRetry(failedAttempts int) bool {
|
||||||
|
return p.MaxAttempts == 0 || failedAttempts < p.MaxAttempts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p RetryPolicy) retryDelay(failedAttempts int) time.Duration {
|
||||||
|
delay := p.InitialDelay
|
||||||
|
|
||||||
|
for attempt := 1; attempt < failedAttempts; attempt++ {
|
||||||
|
if delay >= p.MaxDelay/2 {
|
||||||
|
return p.MaxDelay
|
||||||
|
}
|
||||||
|
delay *= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if delay > p.MaxDelay {
|
||||||
|
return p.MaxDelay
|
||||||
|
}
|
||||||
|
return delay
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFiniteAttempts(t *testing.T) {
|
||||||
|
rp := RetryPolicy{
|
||||||
|
MaxAttempts: 3,
|
||||||
|
InitialDelay: 500 * time.Millisecond,
|
||||||
|
MaxDelay: 10 * time.Second,
|
||||||
|
}
|
||||||
|
if !rp.canRetry(1) {
|
||||||
|
t.Fatal("MaxAttempts=3, failed=1, but can't retry")
|
||||||
|
}
|
||||||
|
if !rp.canRetry(2) {
|
||||||
|
t.Fatal("MaxAttempts=3, failed=2, but can't retry")
|
||||||
|
}
|
||||||
|
if rp.canRetry(3) {
|
||||||
|
t.Fatal("MaxAttempts=3, failed=3, but can retry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOneAllowedAttempt(t *testing.T) {
|
||||||
|
rp := RetryPolicy{
|
||||||
|
MaxAttempts: 1,
|
||||||
|
InitialDelay: 500 * time.Millisecond,
|
||||||
|
MaxDelay: 10 * time.Second,
|
||||||
|
}
|
||||||
|
if rp.canRetry(1) {
|
||||||
|
t.Fatal("MaxAttempts=1, failed=1, but can retry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnlimitedAttempts(t *testing.T) {
|
||||||
|
rp := RetryPolicy{
|
||||||
|
MaxAttempts: 0,
|
||||||
|
InitialDelay: 500 * time.Millisecond,
|
||||||
|
MaxDelay: 10 * time.Second,
|
||||||
|
}
|
||||||
|
for failedAttempts := 1; failedAttempts <= 10; failedAttempts++ {
|
||||||
|
if !rp.canRetry(failedAttempts) {
|
||||||
|
t.Fatalf(
|
||||||
|
"canRetry(%d) = false for unlimited policy",
|
||||||
|
failedAttempts,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackoff(t *testing.T) {
|
||||||
|
rp := RetryPolicy{
|
||||||
|
MaxAttempts: 0,
|
||||||
|
InitialDelay: 500 * time.Millisecond,
|
||||||
|
MaxDelay: 10 * time.Second,
|
||||||
|
}
|
||||||
|
want := []time.Duration{
|
||||||
|
500 * time.Millisecond,
|
||||||
|
1 * time.Second,
|
||||||
|
2 * time.Second,
|
||||||
|
4 * time.Second,
|
||||||
|
8 * time.Second,
|
||||||
|
10 * time.Second,
|
||||||
|
10 * time.Second,
|
||||||
|
}
|
||||||
|
for i, wantDelay := range want {
|
||||||
|
failedAttempts := i + 1
|
||||||
|
got := rp.retryDelay(failedAttempts)
|
||||||
|
if got != wantDelay {
|
||||||
|
t.Errorf(
|
||||||
|
"retryDelay(%d) = %s, want %s",
|
||||||
|
failedAttempts,
|
||||||
|
got,
|
||||||
|
wantDelay,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryDelayLargeFailureCount(t *testing.T) {
|
||||||
|
policy := RetryPolicy{
|
||||||
|
MaxAttempts: 0,
|
||||||
|
InitialDelay: 500 * time.Millisecond,
|
||||||
|
MaxDelay: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := policy.retryDelay(1_000_000); got != policy.MaxDelay {
|
||||||
|
t.Fatalf("retryDelay() = %s, want cap %s", got, policy.MaxDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package playback
|
||||||
|
|
||||||
|
type State uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
StateIdle State = iota
|
||||||
|
StateConnecting
|
||||||
|
StatePlaying
|
||||||
|
StateReconnecting
|
||||||
|
StateFailed
|
||||||
|
StateStopping
|
||||||
|
)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorKind uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
ErrorKindUnknown ErrorKind = iota
|
||||||
|
ErrorKindTemporary // timeout or temporarily early/late data i.e. wait or resync
|
||||||
|
ErrorKindUnavailable // producer/flow disappeared
|
||||||
|
ErrorKindInvalidConfig // wrong media type, invalid rate, etc.
|
||||||
|
)
|
||||||
|
|
||||||
|
type SourceError struct {
|
||||||
|
Op string
|
||||||
|
Kind ErrorKind
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SourceError) Error() string {
|
||||||
|
if e.Op == "" {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s: %v", e.Op, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SourceError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
// KindOf returns the source error category contained in err.
|
||||||
|
// It returns ErrorKindUnknown when err has no SourceError in its chain.
|
||||||
|
func KindOf(err error) ErrorKind {
|
||||||
|
var sourceErr *SourceError
|
||||||
|
if errors.As(err, &sourceErr) {
|
||||||
|
return sourceErr.Kind
|
||||||
|
}
|
||||||
|
return ErrorKindUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapError(op string, kind ErrorKind, err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &SourceError{
|
||||||
|
Op: op,
|
||||||
|
Kind: kind,
|
||||||
|
Err: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package source
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKindOfThroughWrapping(t *testing.T) {
|
||||||
|
base := errors.New("producer disappeared")
|
||||||
|
wrapped := wrapError("read video", ErrorKindUnavailable, base)
|
||||||
|
outer := fmt.Errorf("worker failed: %w", wrapped)
|
||||||
|
|
||||||
|
if got := KindOf(outer); got != ErrorKindUnavailable {
|
||||||
|
t.Fatalf("KindOf() = %v, want %v", got, ErrorKindUnavailable)
|
||||||
|
}
|
||||||
|
if !errors.Is(outer, base) {
|
||||||
|
t.Fatal("wrapped error does not preserve its cause")
|
||||||
|
}
|
||||||
|
if wrapError("nil", 0, nil) != nil {
|
||||||
|
t.Fatal("nil error is not wrapped as nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKindOfUnknown(t *testing.T) {
|
||||||
|
if got := KindOf(errors.New("ordinary error")); got != ErrorKindUnknown {
|
||||||
|
t.Fatalf("KindOf() = %v, want %v", got, ErrorKindUnknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceErrorWithOperation(t *testing.T) {
|
||||||
|
err := &SourceError{
|
||||||
|
Op: "read video",
|
||||||
|
Kind: ErrorKindTemporary,
|
||||||
|
Err: errors.New("timeout"),
|
||||||
|
}
|
||||||
|
|
||||||
|
const want = "read video: timeout"
|
||||||
|
if got := err.Error(); got != want {
|
||||||
|
t.Fatalf("Error() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSourceErrorWithoutOperation(t *testing.T) {
|
||||||
|
err := &SourceError{
|
||||||
|
Kind: ErrorKindTemporary,
|
||||||
|
Err: errors.New("timeout"),
|
||||||
|
}
|
||||||
|
|
||||||
|
const want = "timeout"
|
||||||
|
if got := err.Error(); got != want {
|
||||||
|
t.Fatalf("Error() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
-34
@@ -46,42 +46,59 @@ type Source struct {
|
|||||||
func Open(domain, flowID string) (*Source, error) {
|
func Open(domain, flowID string) (*Source, error) {
|
||||||
inst, err := mxl.NewInstance(domain, "")
|
inst, err := mxl.NewInstance(domain, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NewInstance: %w", err)
|
return nil, wrapError("new MXL instance", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
r, err := inst.NewReader(flowID)
|
r, err := inst.NewReader(flowID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("NewReader: %w", err)
|
return nil, wrapError("open video reader", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
info, err := r.Info()
|
info, err := r.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("Info: %w", err)
|
return nil, wrapError("get video info", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
def, err := inst.FlowDef(flowID)
|
def, err := inst.FlowDef(flowID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("FlowDef: %w", err)
|
return nil, wrapError("read video flow definition", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
var fd flowDef
|
var fd flowDef
|
||||||
if err := json.Unmarshal([]byte(def), &fd); err != nil {
|
if err := json.Unmarshal([]byte(def), &fd); err != nil {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("parse flow def: %w", err)
|
return nil, wrapError("parse video flow definition JSON", ErrorKindInvalidConfig, err)
|
||||||
}
|
}
|
||||||
if fd.FrameWidth == 0 || fd.FrameHeight == 0 {
|
if fd.FrameWidth == 0 || fd.FrameHeight == 0 {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("flow has no video dimensions (not a video flow?)")
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("flow has no video dimensions"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
rate := info.Config.Common.GrainRate
|
rate := info.Config.Common.GrainRate
|
||||||
idx := mxl.CurrentIndex(rate)
|
idx := mxl.CurrentIndex(rate)
|
||||||
if idx == mxl.UndefinedIndex {
|
if idx == mxl.UndefinedIndex {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("invalid grain rate: %d/%d", rate.Num, rate.Den)
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
fmt.Errorf("invalid grain rate: %d/%d", rate.Num, rate.Den),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if len(info.Config.Discrete.SliceSizes) == 0 {
|
||||||
|
r.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("video flow has no slice sizes"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return &Source{
|
return &Source{
|
||||||
inst: inst,
|
inst: inst,
|
||||||
@@ -101,10 +118,6 @@ func (s *Source) Close() error {
|
|||||||
return s.inst.Close()
|
return s.inst.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Source) Next(timeout time.Duration) (Frame, error) {
|
|
||||||
return s.NextCtx(context.Background(), timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Source) NextCtx(ctx context.Context, timeout time.Duration) (Frame, error) {
|
func (s *Source) NextCtx(ctx context.Context, timeout time.Duration) (Frame, error) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -165,36 +178,64 @@ type AudioFrame struct {
|
|||||||
func OpenAudio(domain, flowID string) (*AudioSource, error) {
|
func OpenAudio(domain, flowID string) (*AudioSource, error) {
|
||||||
inst, err := mxl.NewInstance(domain, "")
|
inst, err := mxl.NewInstance(domain, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NewInstance: %w", err)
|
return nil, wrapError("new MXL instance", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
r, err := inst.NewReader(flowID)
|
r, err := inst.NewReader(flowID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("NewReader: %w", err)
|
return nil, wrapError("open audio reader", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
info, err := r.Info()
|
info, err := r.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("Info: %w", err)
|
return nil, wrapError("get audio info", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
if info.Config.Common.Format.IsDiscrete() {
|
if info.Config.Common.Format.IsDiscrete() {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("flow is discrete (not audio)")
|
return nil, wrapError(
|
||||||
|
"validate audio flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("audio flow is discrete"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
channels := uint64(info.Config.Continuous.ChannelCount)
|
||||||
|
if channels == 0 {
|
||||||
|
r.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate audio flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("audio flow has no channels"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rate := info.Config.Common.GrainRate
|
||||||
|
if rate.Num <= 0 || rate.Den <= 0 {
|
||||||
|
r.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate audio flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
fmt.Errorf("invalid audio rate: %d/%d", rate.Num, rate.Den),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
idx := info.Runtime.HeadIndex
|
idx := info.Runtime.HeadIndex
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
r.Close()
|
r.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("flow has no head yet (no producer?)")
|
return nil, wrapError(
|
||||||
|
"open audio reader",
|
||||||
|
ErrorKindUnavailable,
|
||||||
|
errors.New("audio flow has no producer data"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return &AudioSource{
|
return &AudioSource{
|
||||||
inst: inst,
|
inst: inst,
|
||||||
r: r,
|
r: r,
|
||||||
info: info,
|
info: info,
|
||||||
rate: info.Config.Common.GrainRate,
|
rate: rate,
|
||||||
chans: uint64(info.Config.Continuous.ChannelCount),
|
chans: channels,
|
||||||
idx: idx,
|
idx: idx,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -264,22 +305,25 @@ type SyncSource struct {
|
|||||||
width, height, stride uint32
|
width, height, stride uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
// OpenSameDomainSync opens a native MXL synchronization group.
|
||||||
|
// Both feeds must belong to the supplied domain because go-mxl sync groups
|
||||||
|
// cannot contain readers from different MXL instances.
|
||||||
|
func OpenSameDomainSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
||||||
inst, err := mxl.NewInstance(domain, "")
|
inst, err := mxl.NewInstance(domain, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NewInstance: %w", err)
|
return nil, wrapError("new MXL instance", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vr, err := inst.NewReader(videoFlow)
|
vr, err := inst.NewReader(videoFlow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("NewReader(video): %w", err)
|
return nil, wrapError("open video reader", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
ar, err := inst.NewReader(audioFlow)
|
ar, err := inst.NewReader(audioFlow)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("NewReader(audio): %w", err)
|
return nil, wrapError("open audio reader", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
vInfo, err := vr.Info()
|
vInfo, err := vr.Info()
|
||||||
@@ -287,26 +331,56 @@ func OpenSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
|||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("Info(video): %w", err)
|
return nil, wrapError("get video info", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
if !vInfo.Config.Common.Format.IsDiscrete() {
|
if !vInfo.Config.Common.Format.IsDiscrete() {
|
||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("video flow is not discrete")
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("video flow is continuous"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
aInfo, err := ar.Info()
|
aInfo, err := ar.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("Info(audio): %w", err)
|
return nil, wrapError("get audio info", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
if aInfo.Config.Common.Format.IsDiscrete() {
|
if aInfo.Config.Common.Format.IsDiscrete() {
|
||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("audio flow is not continuous")
|
return nil, wrapError(
|
||||||
|
"validate audio flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("audio flow is discrete"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
channels := uint64(aInfo.Config.Continuous.ChannelCount)
|
||||||
|
if channels == 0 {
|
||||||
|
ar.Close()
|
||||||
|
vr.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate audio flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("audio flow has no channels"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
aRate := aInfo.Config.Common.GrainRate
|
||||||
|
if aRate.Num <= 0 || aRate.Den <= 0 {
|
||||||
|
ar.Close()
|
||||||
|
vr.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate audio flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
fmt.Errorf("invalid audio rate: %d/%d", aRate.Num, aRate.Den),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
def, err := inst.FlowDef(videoFlow)
|
def, err := inst.FlowDef(videoFlow)
|
||||||
@@ -314,14 +388,38 @@ func OpenSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
|||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("FlowDef: %w", err)
|
return nil, wrapError("read video flow definition", ErrorKindUnavailable, err)
|
||||||
}
|
}
|
||||||
var fd flowDef
|
var fd flowDef
|
||||||
if err := json.Unmarshal([]byte(def), &fd); err != nil {
|
if err := json.Unmarshal([]byte(def), &fd); err != nil {
|
||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("parse flow def: %w", err)
|
return nil, wrapError("parse video flow definition JSON", ErrorKindInvalidConfig, err)
|
||||||
|
}
|
||||||
|
if fd.FrameWidth <= 0 || fd.FrameHeight <= 0 {
|
||||||
|
ar.Close()
|
||||||
|
vr.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
fmt.Errorf(
|
||||||
|
"invalid video dimensions: %dx%d",
|
||||||
|
fd.FrameWidth,
|
||||||
|
fd.FrameHeight,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if len(vInfo.Config.Discrete.SliceSizes) == 0 {
|
||||||
|
ar.Close()
|
||||||
|
vr.Close()
|
||||||
|
inst.Close()
|
||||||
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
errors.New("video flow has no slice sizes"),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
vRate := vInfo.Config.Common.GrainRate
|
vRate := vInfo.Config.Common.GrainRate
|
||||||
@@ -330,7 +428,11 @@ func OpenSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
|||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("invalid video rate %d/%d", vRate.Num, vRate.Den)
|
return nil, wrapError(
|
||||||
|
"validate video flow",
|
||||||
|
ErrorKindInvalidConfig,
|
||||||
|
fmt.Errorf("invalid grain rate: %d/%d", vRate.Num, vRate.Den),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
group, err := inst.NewSyncGroup()
|
group, err := inst.NewSyncGroup()
|
||||||
@@ -338,21 +440,33 @@ func OpenSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
|||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("NewSyncGroup: %w", err)
|
return nil, wrapError(
|
||||||
|
"create native sync group",
|
||||||
|
ErrorKindUnavailable,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if err := group.AddReader(vr); err != nil {
|
if err := group.AddReader(vr); err != nil {
|
||||||
group.Close()
|
group.Close()
|
||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("AddReader(video): %w", err)
|
return nil, wrapError(
|
||||||
|
"add video reader to native sync group",
|
||||||
|
ErrorKindUnavailable,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if err := group.AddReader(ar); err != nil {
|
if err := group.AddReader(ar); err != nil {
|
||||||
group.Close()
|
group.Close()
|
||||||
ar.Close()
|
ar.Close()
|
||||||
vr.Close()
|
vr.Close()
|
||||||
inst.Close()
|
inst.Close()
|
||||||
return nil, fmt.Errorf("AddReader(audio): %w", err)
|
return nil, wrapError(
|
||||||
|
"add audio reader to native sync group",
|
||||||
|
ErrorKindUnavailable,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &SyncSource{
|
return &SyncSource{
|
||||||
@@ -361,8 +475,8 @@ func OpenSync(domain, videoFlow, audioFlow string) (*SyncSource, error) {
|
|||||||
ar: ar,
|
ar: ar,
|
||||||
group: group,
|
group: group,
|
||||||
rate: vRate,
|
rate: vRate,
|
||||||
aRate: aInfo.Config.Common.GrainRate,
|
aRate: aRate,
|
||||||
chans: uint64(aInfo.Config.Continuous.ChannelCount),
|
chans: channels,
|
||||||
idx: idx,
|
idx: idx,
|
||||||
width: uint32(fd.FrameWidth),
|
width: uint32(fd.FrameWidth),
|
||||||
height: uint32(fd.FrameHeight),
|
height: uint32(fd.FrameHeight),
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "mxl-player/internal/imgui"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
imgui.New()
|
|
||||||
}
|
|
||||||
Executable
BIN
Binary file not shown.
Reference in New Issue
Block a user