@@ -53,12 +53,20 @@ type Manager struct {
5353 camsHandledLock sync.Mutex
5454
5555 lightLock sync.Mutex
56+
57+ liveStateNotifier LiveStateNotifier
58+ liveNotified map [uint ]bool
59+ liveNotifiedLock sync.Mutex
5660}
5761
5862type CamService interface {
5963 For (address string , cameraType model.CameraType ) (camera.Cam , error )
6064}
6165
66+ // LiveStateNotifier is called whenever a stream transitions into or out of the live state,
67+ // so that other parts of the application (e.g. the websocket hub) can react to it.
68+ type LiveStateNotifier func (streamID uint , live bool )
69+
6270// New returns a new instance of Manager with the given Options
6371func New (dao dao.DaoWrapper , opts ... Option ) * Manager {
6472 m := Manager {
@@ -69,6 +77,7 @@ func New(dao dao.DaoWrapper, opts ...Option) *Manager {
6977 })).With ("service" , "runner_manager" ),
7078 streamStartLock : sync.Mutex {},
7179 camsHandled : make (map [uint ]bool ),
80+ liveNotified : make (map [uint ]bool ),
7281 }
7382 m .applyOpts (opts )
7483 return & m
@@ -157,6 +166,14 @@ func WithSubtitleClient(client pb.SubtitleGeneratorClient, auth string) Option {
157166 }
158167}
159168
169+ // WithLiveStateNotifier registers a callback invoked when a stream starts or stops being live,
170+ // e.g. to notify viewers via websocket.
171+ func WithLiveStateNotifier (notifier LiveStateNotifier ) Option {
172+ return func (m * Manager ) {
173+ m .liveStateNotifier = notifier
174+ }
175+ }
176+
160177func (m * Manager ) applyOpts (opts []Option ) {
161178 for _ , opt := range opts {
162179 opt (m )
@@ -274,6 +291,8 @@ func (m *Manager) streamStarted(ctx context.Context, req *protobuf.StreamStartNo
274291 }
275292 m .streamStartLock .Unlock ()
276293
294+ m .notifyLiveState (stream .ID , true )
295+
277296 err = m .handleCamera (ctx , stream )
278297 if err != nil {
279298 log .Error ("failed to handle camera" , "stream" , stream .ID , "err" , err )
@@ -287,6 +306,28 @@ func (m *Manager) streamStarted(ctx context.Context, req *protobuf.StreamStartNo
287306 return nil
288307}
289308
309+ // notifyLiveState invokes the configured LiveStateNotifier, making sure viewers are only notified
310+ // once per stream when going live, even though streamStarted is called once per stream version.
311+ func (m * Manager ) notifyLiveState (streamID uint , live bool ) {
312+ if m .liveStateNotifier == nil {
313+ return
314+ }
315+ if live {
316+ m .liveNotifiedLock .Lock ()
317+ alreadyNotified := m .liveNotified [streamID ]
318+ m .liveNotified [streamID ] = true
319+ m .liveNotifiedLock .Unlock ()
320+ if alreadyNotified {
321+ return
322+ }
323+ } else {
324+ m .liveNotifiedLock .Lock ()
325+ delete (m .liveNotified , streamID )
326+ m .liveNotifiedLock .Unlock ()
327+ }
328+ m .liveStateNotifier (streamID , live )
329+ }
330+
290331var errNotNoLectureSource = fmt .Errorf ("no source configured for this lecture hall ip" )
291332
292333func (m * Manager ) requestStreamVersion (ctx context.Context , s model.Stream , client protobuf.RunnerServiceClient , lh model.LectureHall , version protobuf.StreamVersion ) (* protobuf.StreamResponse , error ) {
@@ -598,10 +639,12 @@ func (m *Manager) handleLightsOff(stream model.Stream) (err error) {
598639
599640func (m * Manager ) streamEnded (ctx context.Context , notification * protobuf.StreamEndNotification ) error {
600641 m .logger .Debug ("streamEnd" , "payload" , notification )
601- err := m .dao .StreamsDao .SetStreamNotLiveById (uint (notification .GetStream ().GetId ()))
642+ streamID := uint (notification .GetStream ().GetId ())
643+ err := m .dao .StreamsDao .SetStreamNotLiveById (streamID )
602644 if err != nil {
603645 return err
604646 }
647+ m .notifyLiveState (streamID , false )
605648
606649 stream , err := m .dao .StreamsDao .GetStreamByID (ctx , strconv .FormatUint (notification .GetStream ().GetId (), 10 ))
607650 if err != nil {
0 commit comments