fix(capture): derive video ids from pipelines and guard empty list (#711)

when NEKO_CAPTURE_VIDEO_PIPELINES is set without capture.video.ids the
VideoIDs list stayed empty, so capture.Video().IDs() returned [] and the
first websocket signal request crashed the whole server with
"index out of range [0] with length 0" in signalRequest.

now we fill VideoIDs from the configured pipeline keys (sorted by id)
whenever no ids are set explicitly, and signalRequest returns a plain
error instead of panicking when there is no video stream at all.

added tests for the config loader, the capture manager and the signal
handler so this path stays covered.
This commit is contained in:
Abdellatif Anaflous
2026-10-04 18:11:50 +01:00
committed by GitHub
parent bdd0428a60
commit a2cb38dd10
5 changed files with 245 additions and 0 deletions
+15
View File
@@ -3,6 +3,7 @@ package config
import (
"os"
"reflect"
"sort"
"strings"
"github.com/go-viper/mapstructure/v2"
@@ -423,6 +424,20 @@ func (s *Capture) Set() {
log.Warn().Msg("you are setting both single video pipeline and multiple video pipelines, ignoring single video pipeline")
}
// If no video ids were configured explicitly, derive them from the configured
// pipelines so the capture manager always has at least one stream to select.
// Without this, NEKO_CAPTURE_VIDEO_PIPELINES alone left VideoIDs empty and the
// first WebRTC signal request panicked on videos[0].
if len(s.VideoIDs) == 0 && len(s.VideoPipelines) > 0 {
ids := make([]string, 0, len(s.VideoPipelines))
for id := range s.VideoPipelines {
ids = append(ids, id)
}
sort.Strings(ids)
s.VideoIDs = ids
log.Warn().Strs("video_ids", ids).Msg("no capture.video.ids configured, using all video pipelines sorted by id")
}
s.VideoShowPointer = viper.GetBool("capture.video.show_pointer")
if viper.IsSet("capture.video.show_pointer") {
for k, p := range s.VideoPipelines {
@@ -0,0 +1,62 @@
package config
import (
"reflect"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// loadCaptureFromEnvWithIDs mirrors loadCaptureFromEnv but also allows setting
// capture.video.ids directly, so we can check that explicit ids are preserved.
func loadCaptureFromEnvWithIDs(t *testing.T, pipelines string, ids []string) *Capture {
t.Helper()
viper.Reset()
viper.SetEnvPrefix("NEKO")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
t.Setenv("NEKO_CAPTURE_VIDEO_PIPELINES", pipelines)
c := &Capture{}
cmd := &cobra.Command{}
if err := c.Init(cmd); err != nil {
t.Fatal(err)
}
if ids != nil {
viper.Set("capture.video.ids", ids)
}
c.Set()
return c
}
// Regression test for the panic in signalRequest: when video pipelines are
// configured without an explicit capture.video.ids list, VideoIDs must be
// derived from the pipeline keys so the capture manager always has a default
// stream to hand out.
func TestVideoIDsDerivedFromPipelines(t *testing.T) {
const pipeline = "ximagesrc display-name={display} show-pointer=false ! appsink name=appsink"
t.Run("multiple pipelines without ids populate sorted ids", func(t *testing.T) {
c := loadCaptureFromEnvWithIDs(t, `{"high":"`+pipeline+`","medium":"`+pipeline+`","low":"`+pipeline+`"}`, nil)
want := []string{"high", "low", "medium"}
if !reflect.DeepEqual(c.VideoIDs, want) {
t.Fatalf("expected video ids %v derived from pipeline keys, got %v (an empty list makes signalRequest panic)", want, c.VideoIDs)
}
})
t.Run("explicit ids are preserved", func(t *testing.T) {
c := loadCaptureFromEnvWithIDs(t, `{"high":"`+pipeline+`","low":"`+pipeline+`"}`, []string{"low", "high"})
want := []string{"low", "high"}
if !reflect.DeepEqual(c.VideoIDs, want) {
t.Fatalf("expected explicit video ids %v to be preserved, got %v", want, c.VideoIDs)
}
})
}