-
Notifications
You must be signed in to change notification settings - Fork 110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RSDK-8982, RSDK-9167, RSDK-8979] - Add SetStreamOptions to stream server #4530
base: main
Are you sure you want to change the base?
Changes from 21 commits
7634a56
08ee02e
1d4d5b3
47193fe
686dbb6
d0d0c5c
3bd8e98
c95ea9d
eef2ab1
9f77dd9
95315d4
cd74a13
2c222eb
50a6fbf
56b8f2a
ef3dff0
5a52a36
f5ef994
fb9eac5
c41c2df
6a5292c
37174cf
424c3b1
2956cde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,6 +43,9 @@ type StreamState struct { | |
streamSource streamSource | ||
// streamSourceSub is only non nil if streamSource == streamSourcePassthrough | ||
streamSourceSub rtppassthrough.Subscription | ||
// isResized indicates whether the stream has been resized by the stream server. | ||
// When set to true, it signals that the passthrough stream should not be restarted. | ||
isResized bool | ||
} | ||
|
||
// New returns a new *StreamState. | ||
|
@@ -61,6 +64,7 @@ func New( | |
robot: r, | ||
msgChan: make(chan msg), | ||
tickChan: make(chan struct{}), | ||
isResized: false, | ||
logger: logger, | ||
} | ||
|
||
|
@@ -90,6 +94,24 @@ func (state *StreamState) Decrement() error { | |
return state.send(msgTypeDecrement) | ||
} | ||
|
||
// Resize notifies that the gostream source has been resized. This will stop and prevent | ||
// the use of the passthrough stream if it is supported. | ||
func (state *StreamState) Resize() error { | ||
if err := state.closedCtx.Err(); err != nil { | ||
return multierr.Combine(ErrClosed, err) | ||
} | ||
return state.send(msgTypeResize) | ||
} | ||
|
||
// Reset notifies that the gostream source has been reset to the original resolution. | ||
// This will restart the passthrough stream if it is supported. | ||
func (state *StreamState) Reset() error { | ||
if err := state.closedCtx.Err(); err != nil { | ||
return multierr.Combine(ErrClosed, err) | ||
} | ||
return state.send(msgTypeReset) | ||
} | ||
|
||
// Close closes the StreamState. | ||
func (state *StreamState) Close() error { | ||
state.logger.Info("Closing streamState") | ||
|
@@ -129,6 +151,8 @@ const ( | |
msgTypeUnknown msgType = iota | ||
msgTypeIncrement | ||
msgTypeDecrement | ||
msgTypeResize | ||
msgTypeReset | ||
) | ||
|
||
func (mt msgType) String() string { | ||
|
@@ -137,6 +161,10 @@ func (mt msgType) String() string { | |
return "Increment" | ||
case msgTypeDecrement: | ||
return "Decrement" | ||
case msgTypeResize: | ||
return "Resize" | ||
case msgTypeReset: | ||
return "Reset" | ||
case msgTypeUnknown: | ||
fallthrough | ||
default: | ||
|
@@ -185,6 +213,14 @@ func (state *StreamState) sourceEventHandler() { | |
if state.activeClients == 0 { | ||
state.tick() | ||
} | ||
case msgTypeResize: | ||
state.logger.Debug("resize event received") | ||
state.isResized = true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we ever go back to not resized? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for bringing this up -- the Reset path has been implemented to handle getting out of resized mode. |
||
state.tick() | ||
case msgTypeReset: | ||
state.logger.Debug("reset event received") | ||
state.isResized = false | ||
state.tick() | ||
case msgTypeUnknown: | ||
fallthrough | ||
default: | ||
|
@@ -247,6 +283,11 @@ func (state *StreamState) tick() { | |
// stop stream if there are no active clients | ||
// noop if there is no stream source | ||
state.stopInputStream() | ||
// If streamSource is unknown and resized is true, we do not want to attempt passthrough. | ||
case state.streamSource == streamSourceUnknown && state.isResized: | ||
state.logger.Debug("in a resized state, using gostream") | ||
state.Stream.Start() | ||
state.streamSource = streamSourceGoStream | ||
case state.streamSource == streamSourceUnknown: // && state.activeClients > 0 | ||
// this is the first subscription, attempt passthrough | ||
state.logger.Info("attempting to subscribe to rtp_passthrough") | ||
|
@@ -257,6 +298,13 @@ func (state *StreamState) tick() { | |
state.Stream.Start() | ||
state.streamSource = streamSourceGoStream | ||
} | ||
// If we are currently using passthrough, and the stream state changes to resized | ||
// we need to stop the passthrough stream and restart it through gostream. | ||
case state.streamSource == streamSourcePassthrough && state.isResized: | ||
state.logger.Info("stream resized, stopping passthrough stream") | ||
state.stopInputStream() | ||
state.Stream.Start() | ||
state.streamSource = streamSourceGoStream | ||
case state.streamSource == streamSourcePassthrough && state.streamSourceSub.Terminated.Err() != nil: | ||
// restart stream if there we were using passthrough but the sub is terminated | ||
state.logger.Info("previous subscription terminated attempting to subscribe to rtp_passthrough") | ||
|
@@ -271,7 +319,7 @@ func (state *StreamState) tick() { | |
case state.streamSource == streamSourcePassthrough: | ||
// no op if we are using passthrough & are healthy | ||
state.logger.Debug("still healthy and using h264 passthrough") | ||
case state.streamSource == streamSourceGoStream: | ||
case state.streamSource == streamSourceGoStream && !state.isResized: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does a user do to hit this state after they've selected a resolution in the UI? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implemented the |
||
// Try to upgrade to passthrough if we are using gostream. We leave logs these as debugs as | ||
// we expect some components to not implement rtp passthrough. | ||
state.logger.Debugw("currently using gostream, trying upgrade to rtp_passthrough") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -762,4 +762,164 @@ func TestStreamState(t *testing.T) { | |
test.That(tb, stopCount.Load(), test.ShouldEqual, 3) | ||
}) | ||
}) | ||
|
||
t.Run("when in rtppassthrough mode and a resize occurs test downgrade path to gostream", func(t *testing.T) { | ||
var startCount atomic.Int64 | ||
Comment on lines
+766
to
+767
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A lot of your tests are time based and waiting for assertions, can you run with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
var stopCount atomic.Int64 | ||
writeRTPCalledCtx, writeRTPCalledFunc := context.WithCancel(ctx) | ||
streamMock := &mockStream{ | ||
name: camName, | ||
t: t, | ||
startFunc: func() { | ||
startCount.Add(1) | ||
}, | ||
stopFunc: func() { | ||
stopCount.Add(1) | ||
}, | ||
writeRTPFunc: func(pkt *rtp.Packet) error { | ||
// Test that WriteRTP is eventually called when SubscribeRTP is called | ||
writeRTPCalledFunc() | ||
return nil | ||
}, | ||
} | ||
|
||
var subscribeRTPCount atomic.Int64 | ||
var unsubscribeCount atomic.Int64 | ||
type subAndCancel struct { | ||
sub rtppassthrough.Subscription | ||
cancelFn context.CancelFunc | ||
wg *sync.WaitGroup | ||
} | ||
|
||
var subsAndCancelByIDMu sync.Mutex | ||
subsAndCancelByID := map[rtppassthrough.SubscriptionID]subAndCancel{} | ||
|
||
subscribeRTPFunc := func( | ||
ctx context.Context, | ||
bufferSize int, | ||
packetsCB rtppassthrough.PacketCallback, | ||
) (rtppassthrough.Subscription, error) { | ||
subsAndCancelByIDMu.Lock() | ||
defer subsAndCancelByIDMu.Unlock() | ||
defer subscribeRTPCount.Add(1) | ||
terminatedCtx, terminatedFn := context.WithCancel(context.Background()) | ||
id := uuid.New() | ||
sub := rtppassthrough.Subscription{ID: id, Terminated: terminatedCtx} | ||
subsAndCancelByID[id] = subAndCancel{sub: sub, cancelFn: terminatedFn, wg: &sync.WaitGroup{}} | ||
subsAndCancelByID[id].wg.Add(1) | ||
utils.ManagedGo(func() { | ||
for terminatedCtx.Err() == nil { | ||
packetsCB([]*rtp.Packet{{}}) | ||
time.Sleep(time.Millisecond * 50) | ||
} | ||
}, subsAndCancelByID[id].wg.Done) | ||
return sub, nil | ||
} | ||
|
||
unsubscribeFunc := func(ctx context.Context, id rtppassthrough.SubscriptionID) error { | ||
subsAndCancelByIDMu.Lock() | ||
defer subsAndCancelByIDMu.Unlock() | ||
defer unsubscribeCount.Add(1) | ||
subAndCancel, ok := subsAndCancelByID[id] | ||
if !ok { | ||
test.That(t, fmt.Sprintf("Unsubscribe called with unknown id: %s", id.String()), test.ShouldBeFalse) | ||
} | ||
subAndCancel.cancelFn() | ||
return nil | ||
} | ||
|
||
mockRTPPassthroughSource := &mockRTPPassthroughSource{ | ||
subscribeRTPFunc: subscribeRTPFunc, | ||
unsubscribeFunc: unsubscribeFunc, | ||
} | ||
|
||
robot := mockRobot(mockRTPPassthroughSource) | ||
s := state.New(streamMock, robot, logger) | ||
defer func() { | ||
utils.UncheckedError(s.Close()) | ||
}() | ||
|
||
test.That(t, subscribeRTPCount.Load(), test.ShouldEqual, 0) | ||
test.That(t, unsubscribeCount.Load(), test.ShouldEqual, 0) | ||
test.That(t, writeRTPCalledCtx.Err(), test.ShouldBeNil) | ||
|
||
t.Run("Increment should call SubscribeRTP", func(t *testing.T) { | ||
test.That(t, s.Increment(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, subscribeRTPCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 0) | ||
}) | ||
// WriteRTP is called | ||
<-writeRTPCalledCtx.Done() | ||
}) | ||
|
||
t.Run("Resize should stop rtp_passthrough and start gostream", func(t *testing.T) { | ||
test.That(t, s.Resize(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 0) | ||
}) | ||
}) | ||
|
||
t.Run("Decrement should call Stop as gostream is the data source", func(t *testing.T) { | ||
test.That(t, s.Decrement(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 1) | ||
}) | ||
}) | ||
|
||
t.Run("Increment should call Start as gostream is the data source", func(t *testing.T) { | ||
test.That(t, s.Increment(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, subscribeRTPCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 2) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 1) | ||
}) | ||
}) | ||
|
||
t.Run("Decrement should call Stop as gostream is the data source", func(t *testing.T) { | ||
test.That(t, s.Decrement(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, subscribeRTPCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 2) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 2) | ||
}) | ||
}) | ||
|
||
t.Run("Increment should call Start as gostream is the data source", func(t *testing.T) { | ||
test.That(t, s.Increment(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, subscribeRTPCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 3) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 2) | ||
}) | ||
}) | ||
|
||
t.Run("Reset should call Stop as gostream is the current data source and then "+ | ||
"Subscribe as rtp_passthrough is the new data source", func(t *testing.T) { | ||
test.That(t, s.Reset(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, subscribeRTPCount.Load(), test.ShouldEqual, 2) | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 1) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 3) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 3) | ||
}) | ||
}) | ||
|
||
t.Run("Decrement should call unsubscribe as rtp_passthrough is the data source", func(t *testing.T) { | ||
test.That(t, s.Decrement(), test.ShouldBeNil) | ||
testutils.WaitForAssertion(t, func(tb testing.TB) { | ||
test.That(tb, subscribeRTPCount.Load(), test.ShouldEqual, 2) | ||
test.That(tb, unsubscribeCount.Load(), test.ShouldEqual, 2) | ||
test.That(tb, startCount.Load(), test.ShouldEqual, 3) | ||
test.That(tb, stopCount.Load(), test.ShouldEqual, 3) | ||
}) | ||
}) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: put this above where it's used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, moved.