diff --git a/.gitignore b/.gitignore index 69f77654..9b58eca8 100644 --- a/.gitignore +++ b/.gitignore @@ -188,3 +188,12 @@ infra/tests/* # mise-en-place .mise.toml + +# uv +.venv/ +env/ +venv/ + +# sample capture artifacts +samples/virtual-inputs/feed_capture*.mpegts +samples/virtual-inputs/feed_capture*.ivf diff --git a/images/chromium-headful/Dockerfile b/images/chromium-headful/Dockerfile index 974c733c..ebec06e1 100644 --- a/images/chromium-headful/Dockerfile +++ b/images/chromium-headful/Dockerfile @@ -224,12 +224,13 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap apt-get update; \ apt-get --no-install-recommends -y install \ wget ca-certificates python2 supervisor xclip xdotool unclutter \ - pulseaudio dbus-x11 xserver-xorg-video-dummy \ + pulseaudio dbus-x11 xserver-xorg-video-dummy rtkit upower \ libcairo2 libxcb1 libxrandr2 libxv1 libopus0 libvpx7 \ x11-xserver-utils \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \ - gstreamer1.0-pulseaudio gstreamer1.0-omx; \ + gstreamer1.0-pulseaudio gstreamer1.0-omx \ + v4l2loopback-dkms v4l2loopback-utils; \ # # install libxcvt0 (not available in debian:bullseye) ARCH=$(dpkg --print-architecture); \ @@ -249,7 +250,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap /home/$USERNAME/.local/share/xorg; \ chmod 1777 /var/log/neko; \ chown $USERNAME /var/log/neko/ /tmp/runtime-$USERNAME; \ - chown -R $USERNAME:$USERNAME /home/$USERNAME; + chown -R $USERNAME:$USERNAME /home/$USERNAME; \ + chmod 777 /etc/pulse; # install chromium and sqlite3 for debugging the cookies file RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-apt-cache \ @@ -287,6 +289,10 @@ ENV WITHDOCKER=true COPY images/chromium-headful/xorg.conf /etc/neko/xorg.conf COPY images/chromium-headful/neko.yaml /etc/neko/neko.yaml +COPY images/chromium-headful/default.pa /etc/pulse/default.pa +COPY images/chromium-headful/daemon.conf /etc/pulse/daemon.conf +COPY images/chromium-headful/dbus-pulseaudio.conf /etc/dbus-1/system.d/pulseaudio.conf +COPY images/chromium-headful/dbus-mpris.conf /etc/dbus-1/system.d/mpris.conf COPY --from=neko /usr/bin/neko /usr/bin/neko COPY --from=client /src/dist/ /var/www COPY --from=xorg-deps /usr/local/lib/xorg/modules/drivers/dummy_drv.so /usr/lib/xorg/modules/drivers/dummy_drv.so @@ -306,6 +312,11 @@ COPY --from=server-builder /out/chromium-launcher /usr/local/bin/chromium-launch # Copy the Playwright executor runtime COPY server/runtime/playwright-executor.ts /usr/local/lib/playwright-executor.ts -RUN useradd -m -s /bin/bash kernel +RUN useradd -m -s /bin/bash kernel && \ + usermod -aG audio,video,pulse,pulse-access kernel + +# Environment variables for audio +ENV XDG_RUNTIME_DIR=/tmp/runtime-kernel +ENV PULSE_SERVER=unix:/tmp/runtime-kernel/pulse/native ENTRYPOINT [ "/wrapper.sh" ] diff --git a/images/chromium-headful/client/public/browserconfig.xml b/images/chromium-headful/client/public/browserconfig.xml index ededce1f..0fd3ece4 100644 --- a/images/chromium-headful/client/public/browserconfig.xml +++ b/images/chromium-headful/client/public/browserconfig.xml @@ -3,7 +3,7 @@ - #19bd9c + #7B42F6 diff --git a/images/chromium-headful/client/public/index.html b/images/chromium-headful/client/public/index.html index b9546790..9ffe2c34 100644 --- a/images/chromium-headful/client/public/index.html +++ b/images/chromium-headful/client/public/index.html @@ -9,9 +9,9 @@ - - - + + + diff --git a/images/chromium-headful/client/public/kernel.svg b/images/chromium-headful/client/public/kernel.svg new file mode 100644 index 00000000..b9a86da4 --- /dev/null +++ b/images/chromium-headful/client/public/kernel.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/chromium-headful/client/public/site.webmanifest b/images/chromium-headful/client/public/site.webmanifest index 03c2b31f..4d1b7924 100644 --- a/images/chromium-headful/client/public/site.webmanifest +++ b/images/chromium-headful/client/public/site.webmanifest @@ -13,7 +13,7 @@ "type": "image/png" } ], - "theme_color": "#19bd9c", - "background_color": "#19bd9c", + "theme_color": "#7B42F6", + "background_color": "#7B42F6", "display": "standalone" } diff --git a/images/chromium-headful/client/src/assets/styles/_variables.scss b/images/chromium-headful/client/src/assets/styles/_variables.scss index f74ac679..de85520b 100644 --- a/images/chromium-headful/client/src/assets/styles/_variables.scss +++ b/images/chromium-headful/client/src/assets/styles/_variables.scss @@ -21,7 +21,7 @@ $background-modifier-accent: hsla(0, 0%, 100%, 0.06); $elevation-low: 0 1px 0 rgba(4, 4, 5, 0.2), 0 1.5px 0 rgba(6, 6, 7, 0.05), 0 2px 0 rgba(4, 4, 5, 0.05); $elevation-high: 0 8px 16px rgba(0, 0, 0, 0.24); -$style-primary: #19bd9c; +$style-primary: #7B42F6; $style-error: #d32f2f; $menu-height: 40px; diff --git a/images/chromium-headful/client/src/components/connect.vue b/images/chromium-headful/client/src/components/connect.vue index c850c642..2d175244 100644 --- a/images/chromium-headful/client/src/components/connect.vue +++ b/images/chromium-headful/client/src/components/connect.vue @@ -11,8 +11,7 @@
-
-
+
@@ -103,40 +102,25 @@ .loader { width: 90px; height: 90px; - position: relative; margin: 0 auto; + display: flex; + justify-content: center; + align-items: center; - .bounce1, - .bounce2 { + .spinning-logo { width: 100%; height: 100%; - border-radius: 50%; - background-color: $style-primary; - opacity: 0.6; - position: absolute; - top: 0; - left: 0; - - -webkit-animation: bounce 2s infinite ease-in-out; - animation: bounce 2s infinite ease-in-out; - } - - .bounce2 { - -webkit-animation-delay: -1s; - animation-delay: -1s; + animation: spin 2s linear infinite; } } } - @keyframes bounce { - 0%, - 100% { - transform: scale(0); - -webkit-transform: scale(0); + @keyframes spin { + from { + transform: rotate(0deg); } - 50% { - transform: scale(1); - -webkit-transform: scale(1); + to { + transform: rotate(360deg); } } } diff --git a/images/chromium-headful/client/src/components/video.vue b/images/chromium-headful/client/src/components/video.vue index 4c4fe0f8..69b5bb27 100644 --- a/images/chromium-headful/client/src/components/video.vue +++ b/images/chromium-headful/client/src/components/video.vue @@ -1,6 +1,6 @@ + + + + + +
Loading virtual feed…
+ + + +`, fit, sourceJSON, formatJSON, fit) +} diff --git a/server/cmd/api/api/virtual_feed_page_test.go b/server/cmd/api/api/virtual_feed_page_test.go new file mode 100644 index 00000000..98bd04ed --- /dev/null +++ b/server/cmd/api/api/virtual_feed_page_test.go @@ -0,0 +1,183 @@ +package api + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/coder/websocket" + "github.com/stretchr/testify/require" + + oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/onkernel/kernel-images/server/lib/recorder" + "github.com/onkernel/kernel-images/server/lib/virtualinputs" +) + +func readFeedBody(t *testing.T, resp oapi.GetVirtualInputFeedResponseObject) string { + t.Helper() + out, ok := resp.(oapi.GetVirtualInputFeed200TexthtmlResponse) + require.True(t, ok, "unexpected response type %T", resp) + data, err := io.ReadAll(out.Body) + require.NoError(t, err) + return string(data) +} + +func TestGetVirtualInputFeed_AutoDetectsSources(t *testing.T) { + t.Parallel() + + ctx := context.Background() + mgr := recorder.NewFFmpegManager() + + t.Run("socket ingests default to websocket feed and mpegts format", func(t *testing.T) { + svc, vimgr := newTestApiService(t, mgr) + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: string(virtualinputs.SourceTypeSocket), Path: "/tmp/video.pipe"}, + } + + resp, err := svc.GetVirtualInputFeed(ctx, oapi.GetVirtualInputFeedRequestObject{Params: oapi.GetVirtualInputFeedParams{}}) + require.NoError(t, err) + body := readFeedBody(t, resp) + + require.Contains(t, body, "/input/devices/virtual/feed/socket") + require.Contains(t, body, `const defaultFormat = "mpegts";`) + }) + + t.Run("webrtc ingests default to ivf websocket preview", func(t *testing.T) { + svc, vimgr := newTestApiService(t, mgr) + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: string(virtualinputs.SourceTypeWebRTC), Path: "/tmp/webrtc.pipe"}, + } + + resp, err := svc.GetVirtualInputFeed(ctx, oapi.GetVirtualInputFeedRequestObject{Params: oapi.GetVirtualInputFeedParams{}}) + require.NoError(t, err) + body := readFeedBody(t, resp) + + require.Contains(t, body, "/input/devices/virtual/feed/socket") + require.Contains(t, body, `const defaultFormat = "ivf";`) + }) + + t.Run("falls back to configured video url when no ingest", func(t *testing.T) { + svc, vimgr := newTestApiService(t, mgr) + vimgr.status.Ingest = nil + vimgr.status.Video = &virtualinputs.MediaSource{URL: "https://example.com/feed.m3u8"} + + resp, err := svc.GetVirtualInputFeed(ctx, oapi.GetVirtualInputFeedRequestObject{Params: oapi.GetVirtualInputFeedParams{}}) + require.NoError(t, err) + body := readFeedBody(t, resp) + + require.Contains(t, body, `const defaultSource = "https://example.com/feed.m3u8";`) + }) + + t.Run("source query param overrides detection", func(t *testing.T) { + svc, vimgr := newTestApiService(t, mgr) + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: string(virtualinputs.SourceTypeSocket), Path: "/tmp/video.pipe"}, + } + override := "https://override.local/video" + + resp, err := svc.GetVirtualInputFeed(ctx, oapi.GetVirtualInputFeedRequestObject{ + Params: oapi.GetVirtualInputFeedParams{Source: &override}, + }) + require.NoError(t, err) + body := readFeedBody(t, resp) + + require.Contains(t, body, `const defaultSource = "https://override.local/video";`) + }) +} + +func TestVirtualFeedPageUsesWebsocketURLs(t *testing.T) { + t.Parallel() + + svc, _ := newTestApiService(t, recorder.NewFFmpegManager()) + resp, err := svc.GetVirtualInputFeed(context.Background(), oapi.GetVirtualInputFeedRequestObject{ + Params: oapi.GetVirtualInputFeedParams{}, + }) + require.NoError(t, err) + body := readFeedBody(t, resp) + + require.Contains(t, body, "function toWebSocketURL") + require.Contains(t, body, "const wsURL = toWebSocketURL") + require.Contains(t, body, "new JSMpeg.Player(wsURL") + require.Contains(t, body, "new WebSocket(toWebSocketURL") +} + +func TestVirtualFeedSocketBroadcasts(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + svc, _ := newTestApiService(t, recorder.NewFFmpegManager()) + svc.virtualFeed.setFormat("ivf") + + server := httptest.NewServer(http.HandlerFunc(svc.HandleVirtualInputFeedSocket)) + defer server.Close() + + wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + conn, _, err := websocket.Dial(ctx, wsURL, nil) + require.NoError(t, err) + defer conn.Close(websocket.StatusNormalClosure, "") + + msgType, msg, err := conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, websocket.MessageText, msgType) + require.Equal(t, "ivf", string(msg)) + + payload := []byte{0x01, 0x02, 0x03, 0x04} + svc.virtualFeed.broadcastWithFormat("ivf", payload) + + msgType, msg, err = conn.Read(ctx) + require.NoError(t, err) + require.Equal(t, websocket.MessageBinary, msgType) + require.Equal(t, payload, msg) +} + +func TestGetVirtualInputFeedSocketInfo(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("returns 409 when no ingest is active", func(t *testing.T) { + svc, vimgr := newTestApiService(t, recorder.NewFFmpegManager()) + vimgr.status.Ingest = nil + + resp, err := svc.GetVirtualInputFeedSocketInfo(ctx, oapi.GetVirtualInputFeedSocketInfoRequestObject{}) + require.NoError(t, err) + _, ok := resp.(oapi.GetVirtualInputFeedSocketInfo409JSONResponse) + require.True(t, ok, "expected 409 when no ingest") + }) + + t.Run("reports websocket URL and mpegts for socket ingest", func(t *testing.T) { + svc, vimgr := newTestApiService(t, recorder.NewFFmpegManager()) + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: string(virtualinputs.SourceTypeSocket)}, + } + + resp, err := svc.GetVirtualInputFeedSocketInfo(ctx, oapi.GetVirtualInputFeedSocketInfoRequestObject{}) + require.NoError(t, err) + out, ok := resp.(oapi.GetVirtualInputFeedSocketInfo200JSONResponse) + require.True(t, ok, "expected 200 response") + require.Equal(t, "/input/devices/virtual/feed/socket", out.Url) + require.NotNil(t, out.Format) + require.Equal(t, "mpegts", *out.Format) + }) + + t.Run("reports ivf for webrtc ingest", func(t *testing.T) { + svc, vimgr := newTestApiService(t, recorder.NewFFmpegManager()) + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: string(virtualinputs.SourceTypeWebRTC), Format: "ivf"}, + } + + resp, err := svc.GetVirtualInputFeedSocketInfo(ctx, oapi.GetVirtualInputFeedSocketInfoRequestObject{}) + require.NoError(t, err) + out, ok := resp.(oapi.GetVirtualInputFeedSocketInfo200JSONResponse) + require.True(t, ok, "expected 200 response") + require.Equal(t, "/input/devices/virtual/feed/socket", out.Url) + require.NotNil(t, out.Format) + require.Equal(t, "ivf", *out.Format) + }) +} diff --git a/server/cmd/api/api/virtual_feed_socket.go b/server/cmd/api/api/virtual_feed_socket.go new file mode 100644 index 00000000..e9d2fd7f --- /dev/null +++ b/server/cmd/api/api/virtual_feed_socket.go @@ -0,0 +1,30 @@ +package api + +import ( + "net/http" + + "github.com/coder/websocket" + + "github.com/onkernel/kernel-images/server/lib/logger" +) + +// HandleVirtualInputFeedSocket broadcasts the live virtual video feed chunks to websocket listeners. +func (s *ApiService) HandleVirtualInputFeedSocket(w http.ResponseWriter, r *http.Request) { + conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionNoContextTakeover, + }) + if err != nil { + logger.FromContext(r.Context()).Error("failed to accept virtual feed websocket", "err", err) + return + } + defer s.virtualFeed.remove(conn) + + s.virtualFeed.add(conn) + + // Consume incoming frames until the client disconnects. + for { + if _, _, err := conn.Read(r.Context()); err != nil { + return + } + } +} diff --git a/server/cmd/api/api/virtual_inputs.go b/server/cmd/api/api/virtual_inputs.go new file mode 100644 index 00000000..8495b76e --- /dev/null +++ b/server/cmd/api/api/virtual_inputs.go @@ -0,0 +1,432 @@ +package api + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/onkernel/kernel-images/server/lib/chromiumflags" + "github.com/onkernel/kernel-images/server/lib/logger" + oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/onkernel/kernel-images/server/lib/virtualinputs" +) + +func (s *ApiService) ConfigureVirtualInputs(ctx context.Context, req oapi.ConfigureVirtualInputsRequestObject) (oapi.ConfigureVirtualInputsResponseObject, error) { + log := logger.FromContext(ctx) + + if req.Body == nil { + return oapi.ConfigureVirtualInputs400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "request body required"}, + }, nil + } + + cfg, startPaused := fromVirtualInputsRequest(*req.Body) + status, err := s.virtualInputs.Configure(ctx, cfg, startPaused) + if err != nil { + if isVirtualInputBadRequest(err) { + return oapi.ConfigureVirtualInputs400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}, + }, nil + } + log.Error("failed to configure virtual inputs", "err", err) + return oapi.ConfigureVirtualInputs500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to configure virtual inputs"}, + }, nil + } + + if err := s.applyChromiumCaptureFlags(ctx, status); err != nil { + log.Error("failed to apply chromium capture flags", "err", err) + return oapi.ConfigureVirtualInputs500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to apply chromium flags"}, + }, nil + } + s.updateVirtualInputIngest(status) + + return oapi.ConfigureVirtualInputs200JSONResponse(toVirtualInputsStatus(status)), nil +} + +func (s *ApiService) PauseVirtualInputs(ctx context.Context, _ oapi.PauseVirtualInputsRequestObject) (oapi.PauseVirtualInputsResponseObject, error) { + log := logger.FromContext(ctx) + status, err := s.virtualInputs.Pause(ctx) + if err != nil { + if isVirtualInputBadRequest(err) { + return oapi.PauseVirtualInputs400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}, + }, nil + } + log.Error("failed to pause virtual inputs", "err", err) + return oapi.PauseVirtualInputs500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to pause virtual inputs"}, + }, nil + } + return oapi.PauseVirtualInputs200JSONResponse(toVirtualInputsStatus(status)), nil +} + +func (s *ApiService) ResumeVirtualInputs(ctx context.Context, _ oapi.ResumeVirtualInputsRequestObject) (oapi.ResumeVirtualInputsResponseObject, error) { + log := logger.FromContext(ctx) + status, err := s.virtualInputs.Resume(ctx) + if err != nil { + if isVirtualInputBadRequest(err) { + return oapi.ResumeVirtualInputs400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: err.Error()}, + }, nil + } + log.Error("failed to resume virtual inputs", "err", err) + return oapi.ResumeVirtualInputs500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to resume virtual inputs"}, + }, nil + } + s.updateVirtualInputIngest(status) + return oapi.ResumeVirtualInputs200JSONResponse(toVirtualInputsStatus(status)), nil +} + +func (s *ApiService) StopVirtualInputs(ctx context.Context, _ oapi.StopVirtualInputsRequestObject) (oapi.StopVirtualInputsResponseObject, error) { + log := logger.FromContext(ctx) + status, err := s.virtualInputs.Stop(ctx) + if err != nil { + log.Error("failed to stop virtual inputs", "err", err) + return oapi.StopVirtualInputs500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to stop virtual inputs"}, + }, nil + } + if err := s.applyChromiumCaptureFlags(ctx, status); err != nil { + log.Error("failed to clear chromium capture flags", "err", err) + return oapi.StopVirtualInputs500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to clear chromium flags"}, + }, nil + } + s.updateVirtualInputIngest(status) + return oapi.StopVirtualInputs200JSONResponse(toVirtualInputsStatus(status)), nil +} + +func (s *ApiService) GetVirtualInputsStatus(ctx context.Context, _ oapi.GetVirtualInputsStatusRequestObject) (oapi.GetVirtualInputsStatusResponseObject, error) { + status := s.virtualInputs.Status(ctx) + return oapi.GetVirtualInputsStatus200JSONResponse(toVirtualInputsStatus(status)), nil +} + +func (s *ApiService) GetVirtualInputFeedSocketInfo(ctx context.Context, _ oapi.GetVirtualInputFeedSocketInfoRequestObject) (oapi.GetVirtualInputFeedSocketInfoResponseObject, error) { + status := s.virtualInputs.Status(ctx) + if status.Ingest == nil || status.Ingest.Video == nil { + return oapi.GetVirtualInputFeedSocketInfo409JSONResponse{ + ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "virtual video ingest not active"}, + }, nil + } + + format := status.Ingest.Video.Format + switch { + case format != "": + case status.Ingest.Video.Protocol == string(virtualinputs.SourceTypeWebRTC): + format = "ivf" + case status.Ingest.Video.Protocol == string(virtualinputs.SourceTypeSocket): + format = "mpegts" + } + + resp := oapi.VirtualFeedSocketInfo{ + Url: "/input/devices/virtual/feed/socket", + } + if format != "" { + resp.Format = &format + } + + return oapi.GetVirtualInputFeedSocketInfo200JSONResponse(resp), nil +} + +func fromVirtualInputsRequest(body oapi.VirtualInputsRequest) (virtualinputs.Config, bool) { + cfg := virtualinputs.Config{} + if body.Video != nil { + cfg.Video = &virtualinputs.MediaSource{ + Type: virtualinputs.SourceType(body.Video.Type), + } + if body.Video.Url != nil { + cfg.Video.URL = *body.Video.Url + } + if body.Video.Format != nil { + cfg.Video.Format = *body.Video.Format + } + if body.Video.Width != nil { + cfg.Width = int(*body.Video.Width) + } + if body.Video.Height != nil { + cfg.Height = int(*body.Video.Height) + } + if body.Video.FrameRate != nil { + cfg.FrameRate = int(*body.Video.FrameRate) + } + } + if body.Audio != nil { + cfg.Audio = &virtualinputs.MediaSource{ + Type: virtualinputs.SourceType(body.Audio.Type), + } + if body.Audio.Url != nil { + cfg.Audio.URL = *body.Audio.Url + } + if body.Audio.Format != nil { + cfg.Audio.Format = *body.Audio.Format + } + // Handle audio destination parameter (default: microphone) + if body.Audio.Destination != nil { + switch *body.Audio.Destination { + case oapi.Speaker: + cfg.Audio.Destination = virtualinputs.AudioDestinationSpeaker + default: + cfg.Audio.Destination = virtualinputs.AudioDestinationMicrophone + } + } else { + cfg.Audio.Destination = virtualinputs.AudioDestinationMicrophone + } + } + + startPaused := false + if body.StartPaused != nil { + startPaused = *body.StartPaused + } + + return cfg, startPaused +} + +func toVirtualInputsStatus(status virtualinputs.Status) oapi.VirtualInputsStatus { + resp := oapi.VirtualInputsStatus{ + State: oapi.VirtualInputsStatusState(status.State), + VideoDevice: status.VideoDevice, + AudioSink: status.AudioSink, + MicrophoneSource: status.MicrophoneSource, + } + if status.Mode != "" { + resp.Mode = oapi.VirtualInputsStatusMode(status.Mode) + } else { + resp.Mode = oapi.Device + } + if status.LastError != "" { + resp.LastError = &status.LastError + } + if status.StartedAt != nil { + resp.StartedAt = status.StartedAt + } + if status.VideoFile != "" { + resp.VideoFile = &status.VideoFile + } + if status.AudioFile != "" { + resp.AudioFile = &status.AudioFile + } + if status.Video != nil { + url := status.Video.URL + resp.Video = &oapi.VirtualInputVideo{ + Type: oapi.VirtualInputType(status.Video.Type), + Url: &url, + Format: func() *string { + if status.Video.Format == "" { + return nil + } + return &status.Video.Format + }(), + } + if status.Width > 0 { + resp.Video.Width = &status.Width + } + if status.Height > 0 { + resp.Video.Height = &status.Height + } + if status.FrameRate > 0 { + resp.Video.FrameRate = &status.FrameRate + } + } + if status.Audio != nil { + url := status.Audio.URL + resp.Audio = &oapi.VirtualInputAudio{ + Type: oapi.VirtualInputType(status.Audio.Type), + Url: &url, + Format: func() *string { + if status.Audio.Format == "" { + return nil + } + return &status.Audio.Format + }(), + } + if status.Audio.Destination != "" { + dest := oapi.VirtualInputAudioDestination(status.Audio.Destination) + resp.Audio.Destination = &dest + } + } + if status.Ingest != nil { + resp.Ingest = &oapi.VirtualInputsIngest{} + if status.Ingest.Audio != nil { + audioURL := status.Ingest.Audio.Path + if status.Ingest.Audio.Protocol == string(virtualinputs.SourceTypeSocket) { + audioURL = "/input/devices/virtual/socket/audio" + } else if status.Ingest.Audio.Protocol == string(virtualinputs.SourceTypeWebRTC) { + audioURL = "/input/devices/virtual/webrtc/offer" + } + resp.Ingest.Audio = &oapi.VirtualInputIngestEndpoint{} + if status.Ingest.Audio.Protocol != "" { + resp.Ingest.Audio.Protocol = &status.Ingest.Audio.Protocol + } + if status.Ingest.Audio.Format != "" { + resp.Ingest.Audio.Format = &status.Ingest.Audio.Format + } + if status.Ingest.Audio.Destination != "" { + dest := oapi.VirtualInputAudioDestination(status.Ingest.Audio.Destination) + resp.Ingest.Audio.Destination = &dest + } + resp.Ingest.Audio.Url = &audioURL + } + if status.Ingest.Video != nil { + videoURL := status.Ingest.Video.Path + if status.Ingest.Video.Protocol == string(virtualinputs.SourceTypeSocket) { + videoURL = "/input/devices/virtual/socket/video" + } else if status.Ingest.Video.Protocol == string(virtualinputs.SourceTypeWebRTC) { + videoURL = "/input/devices/virtual/webrtc/offer" + } + resp.Ingest.Video = &oapi.VirtualInputIngestEndpoint{} + if status.Ingest.Video.Protocol != "" { + resp.Ingest.Video.Protocol = &status.Ingest.Video.Protocol + } + if status.Ingest.Video.Format != "" { + resp.Ingest.Video.Format = &status.Ingest.Video.Format + } + resp.Ingest.Video.Url = &videoURL + } + } + return resp +} + +func (s *ApiService) applyChromiumCaptureFlags(ctx context.Context, status virtualinputs.Status) error { + const ( + flagFakeDevice = "--use-fake-device-for-media-stream" + flagAutoAccept = "--auto-accept-camera-and-microphone-capture" + videoPrefix = "--use-file-for-fake-video-capture=" + audioPrefix = "--use-file-for-fake-audio-capture=" + flagsPath = "/chromium/flags" + ) + + useVideoFile := status.Mode == "virtual-file" && status.VideoFile != "" + useAudioFile := status.Mode == "virtual-file" && status.AudioFile != "" + existing, err := chromiumflags.ReadOptionalFlagFile(flagsPath) + if err != nil { + return fmt.Errorf("read flags: %w", err) + } + + filtered := filterTokens(existing, []string{flagFakeDevice}, []string{videoPrefix, audioPrefix}) + required := []string{flagAutoAccept} + if useVideoFile || useAudioFile { + required = append(required, flagFakeDevice) + } + if useVideoFile { + required = append(required, videoPrefix+status.VideoFile) + } + if useAudioFile { + required = append(required, audioPrefix+status.AudioFile) + } + + merged := chromiumflags.MergeFlags(filtered, required) + if slicesEqual(existing, merged) { + return nil + } + + if err := chromiumflags.WriteFlagFile(flagsPath, merged); err != nil { + return fmt.Errorf("write flags: %w", err) + } + + return s.restartChromiumAndWait(ctx, "virtual inputs configure") +} + +func filterTokens(tokens, exact []string, prefixes []string) []string { + out := make([]string, 0, len(tokens)) + for _, t := range tokens { + skip := false + for _, ex := range exact { + if t == ex { + skip = true + break + } + } + if skip { + continue + } + for _, p := range prefixes { + if strings.HasPrefix(t, p) { + skip = true + break + } + } + if skip { + continue + } + out = append(out, t) + } + return out +} + +func slicesEqual(a, b []string) bool { + return reflect.DeepEqual(a, b) +} + +func isVirtualInputBadRequest(err error) bool { + return errors.Is(err, virtualinputs.ErrMissingSources) || + errors.Is(err, virtualinputs.ErrVideoURLRequired) || + errors.Is(err, virtualinputs.ErrAudioURLRequired) || + errors.Is(err, virtualinputs.ErrVideoTypeRequired) || + errors.Is(err, virtualinputs.ErrAudioTypeRequired) || + errors.Is(err, virtualinputs.ErrUnsupportedVideo) || + errors.Is(err, virtualinputs.ErrUnsupportedAudio) || + errors.Is(err, virtualinputs.ErrPauseWithoutSession) || + errors.Is(err, virtualinputs.ErrNoConfigToPause) || + errors.Is(err, virtualinputs.ErrNoConfigToResume) +} + +func (s *ApiService) updateVirtualInputIngest(status virtualinputs.Status) { + if s.virtualInputsWebRTC == nil { + return + } + + if status.Ingest == nil { + s.virtualInputsWebRTC.Clear() + s.virtualInputsWebRTC.SetSinks(nil, nil) + if s.virtualFeed != nil { + s.virtualFeed.clear() + } + return + } + + videoPath := "" + videoFmt := "" + audioPath := "" + audioFmt := "" + var audioDest virtualinputs.AudioDestination + if status.Ingest.Video != nil { + videoPath = status.Ingest.Video.Path + videoFmt = status.Ingest.Video.Format + } + if status.Ingest.Audio != nil { + audioPath = status.Ingest.Audio.Path + audioFmt = status.Ingest.Audio.Format + audioDest = status.Ingest.Audio.Destination + } + s.virtualInputsWebRTC.Configure(videoPath, videoFmt, audioPath, audioFmt, audioDest) + + if s.virtualFeed == nil { + return + } + + format := "" + if status.Ingest.Video == nil { + s.virtualInputsWebRTC.SetSinks(nil, nil) + s.virtualFeed.clear() + return + } + format = status.Ingest.Video.Format + if format == "" { + if status.Ingest.Video.Protocol == string(virtualinputs.SourceTypeWebRTC) { + format = "ivf" + } else if status.Ingest.Video.Protocol == string(virtualinputs.SourceTypeSocket) { + format = "mpegts" + } + } + + videoSink := s.virtualFeed.writer(format) + if format != "" { + s.virtualFeed.setFormat(format) + } + s.virtualInputsWebRTC.SetSinks(videoSink, nil) +} diff --git a/server/cmd/api/api/virtual_inputs_socket.go b/server/cmd/api/api/virtual_inputs_socket.go new file mode 100644 index 00000000..c6484ca9 --- /dev/null +++ b/server/cmd/api/api/virtual_inputs_socket.go @@ -0,0 +1,228 @@ +package api + +import ( + "io" + "log/slog" + "net/http" + "os/exec" + + "github.com/coder/websocket" + + "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/virtualinputs" +) + +const ( + socketChunkSize = 64 * 1024 + // Lift the default websocket read cap so clients can deliver larger payloads + // while still encouraging chunked writes. + socketReadLimit = 64 * 1024 * 1024 +) + +// HandleVirtualInputAudioSocket upgrades the connection and streams binary chunks to PulseAudio. +func (s *ApiService) HandleVirtualInputAudioSocket(w http.ResponseWriter, r *http.Request) { + s.handleVirtualInputSocket(w, r, "audio") +} + +// HandleVirtualInputVideoSocket upgrades the connection and streams binary chunks to the virtual feed broadcaster. +func (s *ApiService) HandleVirtualInputVideoSocket(w http.ResponseWriter, r *http.Request) { + s.handleVirtualInputSocket(w, r, "video") +} + +func (s *ApiService) handleVirtualInputSocket(w http.ResponseWriter, r *http.Request, kind string) { + log := logger.FromContext(r.Context()) + log.Info("virtual input socket connection", "kind", kind) + status := s.virtualInputs.Status(r.Context()) + var endpoint *virtualinputs.IngestEndpoint + switch kind { + case "audio": + if status.Ingest != nil { + endpoint = status.Ingest.Audio + } + case "video": + if status.Ingest != nil { + endpoint = status.Ingest.Video + } + } + + if endpoint == nil || endpoint.Protocol != "socket" { + http.Error(w, "socket ingest not configured", http.StatusConflict) + return + } + + s.socketMu.Lock() + active := (kind == "audio" && s.audioSocketActive) || (kind == "video" && s.videoSocketActive) + if active { + s.socketMu.Unlock() + http.Error(w, "socket ingest already connected", http.StatusConflict) + return + } + if kind == "audio" { + s.audioSocketActive = true + } else { + s.videoSocketActive = true + } + s.socketMu.Unlock() + defer func() { + s.socketMu.Lock() + if kind == "audio" { + s.audioSocketActive = false + } else { + s.videoSocketActive = false + } + s.socketMu.Unlock() + }() + + conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionNoContextTakeover, + }) + if err != nil { + log.Error("failed to accept websocket for virtual input", "err", err, "kind", kind) + return + } + conn.SetReadLimit(socketReadLimit) + defer conn.Close(websocket.StatusNormalClosure, "done") + + // send a tiny hint to the client about the expected format + if endpoint.Format != "" { + _ = conn.Write(r.Context(), websocket.MessageText, []byte(endpoint.Format)) + } + + format := endpoint.Format + if format == "" { + if kind == "video" { + format = "mpegts" + } else { + format = "mp3" + } + } + + if kind == "video" { + // Video goes directly to the broadcaster - no pipe/FFmpeg needed + s.handleVideoSocketIngest(r, conn, log, format) + } else { + // Audio goes to PulseAudio via ffmpeg + // Destination determines whether it goes to virtual mic (audio_input) or speaker (audio_output) + destination := endpoint.Destination + if destination == "" { + destination = virtualinputs.AudioDestinationMicrophone // default to virtual mic + } + s.handleAudioSocketIngest(r, conn, log, format, destination) + } +} + +// handleVideoSocketIngest streams video chunks directly to the virtual feed broadcaster. +// The broadcaster fans out to all connected websocket clients watching the feed page. +func (s *ApiService) handleVideoSocketIngest(r *http.Request, conn *websocket.Conn, log *slog.Logger, format string) { + if s.virtualFeed == nil { + log.Error("virtual feed broadcaster not available") + _ = conn.Close(websocket.StatusInternalError, "broadcaster unavailable") + return + } + + // Set the format on the broadcaster so clients know what to expect + s.virtualFeed.setFormat(format) + + buf := make([]byte, socketChunkSize) + for { + msgType, reader, err := conn.Reader(r.Context()) + if err != nil { + log.Info("virtual input video socket closed", "err", err) + return + } + if msgType != websocket.MessageBinary { + _, _ = io.Copy(io.Discard, reader) + continue + } + + // Read and broadcast directly - no pipe intermediary + written, chunks := broadcastFromReader(reader, s.virtualFeed, format, buf) + log.Info("received video websocket chunk", "len", written, "chunks", chunks) + } +} + +// handleAudioSocketIngest streams audio chunks to PulseAudio via ffmpeg. +// This creates a long-running ffmpeg process that decodes the incoming audio format +// and outputs to either the virtual microphone sink (audio_input) or speaker (audio_output). +func (s *ApiService) handleAudioSocketIngest(r *http.Request, conn *websocket.Conn, log *slog.Logger, format string, destination virtualinputs.AudioDestination) { + // Determine the PulseAudio sink based on destination + sink := "audio_input" // default: virtual microphone + if destination == virtualinputs.AudioDestinationSpeaker { + sink = "audio_output" + } + + // Start ffmpeg to decode incoming audio and pipe to PulseAudio + // Use -device to specify the PulseAudio sink; the output "pulse" is a placeholder. + args := []string{ + "-hide_banner", "-loglevel", "warning", + "-f", format, + "-i", "pipe:0", + "-ac", "2", + "-ar", "48000", + "-f", "pulse", + "-device", sink, + "pulse", + } + log.Info("starting audio socket ingest", "format", format, "destination", destination, "sink", sink) + + cmd := exec.CommandContext(r.Context(), "ffmpeg", args...) + stdin, err := cmd.StdinPipe() + if err != nil { + log.Error("failed to create ffmpeg stdin pipe", "err", err) + _ = conn.Close(websocket.StatusInternalError, "ffmpeg setup failed") + return + } + + if err := cmd.Start(); err != nil { + log.Error("failed to start ffmpeg for audio ingest", "err", err) + _ = conn.Close(websocket.StatusInternalError, "ffmpeg start failed") + return + } + + // Ensure ffmpeg is cleaned up when we're done + defer func() { + stdin.Close() + _ = cmd.Wait() + }() + + buf := make([]byte, socketChunkSize) + for { + msgType, reader, err := conn.Reader(r.Context()) + if err != nil { + log.Info("virtual input audio socket closed", "err", err) + return + } + if msgType != websocket.MessageBinary { + _, _ = io.Copy(io.Discard, reader) + continue + } + + // Write audio data to ffmpeg's stdin + written, err := io.CopyBuffer(stdin, reader, buf) + if err != nil { + log.Error("failed writing audio to ffmpeg", "err", err) + return + } + log.Debug("received audio websocket chunk", "len", written) + } +} + +// broadcastFromReader reads from src and broadcasts chunks to the virtual feed. +func broadcastFromReader(src io.Reader, broadcaster *virtualFeedBroadcaster, format string, buf []byte) (int64, int) { + var ( + totalWritten int64 + chunks int + ) + for { + n, readErr := src.Read(buf) + if n > 0 { + broadcaster.broadcastWithFormat(format, buf[:n]) + totalWritten += int64(n) + chunks++ + } + + if readErr != nil { + return totalWritten, chunks + } + } +} diff --git a/server/cmd/api/api/virtual_inputs_socket_test.go b/server/cmd/api/api/virtual_inputs_socket_test.go new file mode 100644 index 00000000..0798a53c --- /dev/null +++ b/server/cmd/api/api/virtual_inputs_socket_test.go @@ -0,0 +1,125 @@ +package api + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/coder/websocket" + "github.com/stretchr/testify/require" + + "github.com/onkernel/kernel-images/server/lib/recorder" + "github.com/onkernel/kernel-images/server/lib/virtualinputs" +) + +func TestVirtualInputVideoSocketMirrorsFeed(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + dir := t.TempDir() + pipePath := filepath.Join(dir, "video.pipe") + require.NoError(t, syscall.Mkfifo(pipePath, 0o666)) + + readerReady := make(chan struct{}) + readerErr := make(chan error, 1) + go func() { + f, err := openPipeReader(pipePath) + if err != nil { + readerErr <- err + close(readerReady) + return + } + close(readerReady) + defer f.Close() + buf := make([]byte, 1024) + for { + select { + case <-ctx.Done(): + return + default: + } + if _, err := f.Read(buf); err != nil { + if errors.Is(err, io.EOF) { + time.Sleep(20 * time.Millisecond) + continue + } + readerErr <- err + return + } + } + }() + select { + case <-readerReady: + case <-time.After(2 * time.Second): + t.Fatal("pipe reader did not open in time") + } + select { + case err := <-readerErr: + require.NoError(t, err, "pipe reader failed") + default: + } + + svc, vimgr := newTestApiService(t, recorder.NewFFmpegManager()) + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{ + Protocol: string(virtualinputs.SourceTypeSocket), + Format: "mpegts", + Path: pipePath, + }, + } + + mux := http.NewServeMux() + mux.HandleFunc("/input/devices/virtual/socket/video", svc.HandleVirtualInputVideoSocket) + mux.HandleFunc("/input/devices/virtual/feed/socket", svc.HandleVirtualInputFeedSocket) + server := httptest.NewServer(mux) + defer server.Close() + + feedURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/input/devices/virtual/feed/socket" + feedConn, _, err := websocket.Dial(ctx, feedURL, nil) + require.NoError(t, err) + defer feedConn.Close(websocket.StatusNormalClosure, "") + + ingestURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/input/devices/virtual/socket/video" + ingestConn, _, err := websocket.Dial(ctx, ingestURL, nil) + require.NoError(t, err) + defer ingestConn.Close(websocket.StatusNormalClosure, "") + + payload := bytes.Repeat([]byte{0x01, 0x02, 0x03, 0x04}, 8*1024) + require.NoError(t, ingestConn.Write(ctx, websocket.MessageBinary, payload)) + + msgType, msg, err := feedConn.Read(ctx) + require.NoError(t, err) + require.Equal(t, websocket.MessageBinary, msgType) + require.Equal(t, payload, msg) +} + +// openPipeReader opens the read end of a FIFO without blocking forever. +func openPipeReader(path string) (*os.File, error) { + deadline := time.Now().Add(2 * time.Second) + for { + f, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK, 0) + if err == nil { + _ = syscall.SetNonblock(int(f.Fd()), false) + return f, nil + } + if errors.Is(err, syscall.ENXIO) || errors.Is(err, syscall.EAGAIN) { + if time.Now().After(deadline) { + return nil, err + } + time.Sleep(50 * time.Millisecond) + continue + } + return nil, err + } +} diff --git a/server/cmd/api/api/virtual_inputs_webrtc.go b/server/cmd/api/api/virtual_inputs_webrtc.go new file mode 100644 index 00000000..ee15f955 --- /dev/null +++ b/server/cmd/api/api/virtual_inputs_webrtc.go @@ -0,0 +1,48 @@ +package api + +import ( + "context" + + "github.com/onkernel/kernel-images/server/lib/logger" + oapi "github.com/onkernel/kernel-images/server/lib/oapi" +) + +// NegotiateVirtualInputsWebrtc handles SDP offer/answer exchange for realtime ingest. +func (s *ApiService) NegotiateVirtualInputsWebrtc(ctx context.Context, req oapi.NegotiateVirtualInputsWebrtcRequestObject) (oapi.NegotiateVirtualInputsWebrtcResponseObject, error) { + log := logger.FromContext(ctx) + if req.Body == nil || req.Body.Sdp == "" { + return oapi.NegotiateVirtualInputsWebrtc400JSONResponse{ + BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{Message: "sdp is required"}, + }, nil + } + + status := s.virtualInputs.Status(ctx) + if status.Ingest == nil || ((status.Ingest.Video == nil || status.Ingest.Video.Protocol != "webrtc") && + (status.Ingest.Audio == nil || status.Ingest.Audio.Protocol != "webrtc")) { + return oapi.NegotiateVirtualInputsWebrtc409JSONResponse{ + ConflictErrorJSONResponse: oapi.ConflictErrorJSONResponse{Message: "virtual input not configured for webrtc ingest"}, + }, nil + } + + if s.virtualFeed != nil && s.virtualInputsWebRTC != nil { + format := "" + if status.Ingest.Video != nil { + format = status.Ingest.Video.Format + if format == "" && status.Ingest.Video.Protocol == "webrtc" { + format = "ivf" + } + } + s.virtualFeed.setFormat(format) + s.virtualInputsWebRTC.SetSinks(s.virtualFeed.writer(format), nil) + } + + answer, err := s.virtualInputsWebRTC.HandleOffer(ctx, req.Body.Sdp) + if err != nil { + log.Error("failed to negotiate virtual input webrtc", "err", err) + return oapi.NegotiateVirtualInputsWebrtc500JSONResponse{ + InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{Message: "failed to negotiate webrtc ingest"}, + }, nil + } + + return oapi.NegotiateVirtualInputsWebrtc200JSONResponse(oapi.VirtualInputWebRTCAnswer{Sdp: &answer}), nil +} diff --git a/server/cmd/api/api/virtual_inputs_webrtc_test.go b/server/cmd/api/api/virtual_inputs_webrtc_test.go new file mode 100644 index 00000000..b8907943 --- /dev/null +++ b/server/cmd/api/api/virtual_inputs_webrtc_test.go @@ -0,0 +1,95 @@ +package api + +import ( + "context" + "path/filepath" + "syscall" + "testing" + + "github.com/pion/webrtc/v4" + "github.com/stretchr/testify/require" + + oapi "github.com/onkernel/kernel-images/server/lib/oapi" + "github.com/onkernel/kernel-images/server/lib/virtualinputs" +) + +func TestNegotiateVirtualInputsWebrtc(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + t.Run("missing offer sdp returns 400", func(t *testing.T) { + mgr := recorderManagerWithDefault(t) + svc, _ := newTestApiService(t, mgr) + + resp, err := svc.NegotiateVirtualInputsWebrtc(ctx, oapi.NegotiateVirtualInputsWebrtcRequestObject{}) + require.NoError(t, err) + _, ok := resp.(oapi.NegotiateVirtualInputsWebrtc400JSONResponse) + require.True(t, ok) + }) + + t.Run("ingest not configured returns 409", func(t *testing.T) { + mgr := recorderManagerWithDefault(t) + svc, _ := newTestApiService(t, mgr) + + body := oapi.VirtualInputWebRTCOffer{Sdp: "offer"} + resp, err := svc.NegotiateVirtualInputsWebrtc(ctx, oapi.NegotiateVirtualInputsWebrtcRequestObject{Body: &body}) + require.NoError(t, err) + _, ok := resp.(oapi.NegotiateVirtualInputsWebrtc409JSONResponse) + require.True(t, ok) + }) + + t.Run("invalid sdp returns 500 when ingest configured", func(t *testing.T) { + mgr := recorderManagerWithDefault(t) + svc, vimgr := newTestApiService(t, mgr) + + pipeDir := t.TempDir() + videoPipe := filepath.Join(pipeDir, "video.pipe") + require.NoError(t, syscall.Mkfifo(videoPipe, 0o666)) + + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: "webrtc", Path: videoPipe, Format: "ivf"}, + } + svc.virtualInputsWebRTC.Configure(videoPipe, "ivf", "", "", "") + + body := oapi.VirtualInputWebRTCOffer{Sdp: "bad-sdp"} + resp, err := svc.NegotiateVirtualInputsWebrtc(ctx, oapi.NegotiateVirtualInputsWebrtcRequestObject{Body: &body}) + require.NoError(t, err) + _, ok := resp.(oapi.NegotiateVirtualInputsWebrtc500JSONResponse) + require.True(t, ok) + }) + + t.Run("successful negotiation returns answer", func(t *testing.T) { + mgr := recorderManagerWithDefault(t) + svc, vimgr := newTestApiService(t, mgr) + + pipeDir := t.TempDir() + videoPipe := filepath.Join(pipeDir, "video.pipe") + require.NoError(t, syscall.Mkfifo(videoPipe, 0o666)) + + vimgr.status.Ingest = &virtualinputs.IngestStatus{ + Video: &virtualinputs.IngestEndpoint{Protocol: "webrtc", Path: videoPipe, Format: "ivf"}, + } + svc.virtualInputsWebRTC.Configure(videoPipe, "ivf", "", "", "") + + pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() + + _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo) + require.NoError(t, err) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + require.NoError(t, pc.SetLocalDescription(offer)) + <-webrtc.GatheringCompletePromise(pc) + + body := oapi.VirtualInputWebRTCOffer{Sdp: pc.LocalDescription().SDP} + resp, err := svc.NegotiateVirtualInputsWebrtc(ctx, oapi.NegotiateVirtualInputsWebrtcRequestObject{Body: &body}) + require.NoError(t, err) + out, ok := resp.(oapi.NegotiateVirtualInputsWebrtc200JSONResponse) + require.True(t, ok) + require.NotNil(t, out.Sdp) + require.NotEmpty(t, *out.Sdp) + }) +} diff --git a/server/cmd/api/main.go b/server/cmd/api/main.go index e25f5496..b17eb7e2 100644 --- a/server/cmd/api/main.go +++ b/server/cmd/api/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/tls" "encoding/json" "fmt" "log/slog" @@ -27,6 +28,8 @@ import ( oapi "github.com/onkernel/kernel-images/server/lib/oapi" "github.com/onkernel/kernel-images/server/lib/recorder" "github.com/onkernel/kernel-images/server/lib/scaletozero" + "github.com/onkernel/kernel-images/server/lib/stream" + "github.com/onkernel/kernel-images/server/lib/virtualinputs" ) func main() { @@ -72,6 +75,31 @@ func main() { os.Exit(1) } + streamDefaults := stream.Params{ + DisplayNum: &config.DisplayNum, + FrameRate: &config.FrameRate, + Mode: stream.ModeInternal, + } + + var tlsConfig *tls.Config + if config.RTMPSCertPath != "" && config.RTMPSKeyPath != "" { + cert, err := tls.LoadX509KeyPair(config.RTMPSCertPath, config.RTMPSKeyPath) + if err != nil { + slogger.Error("failed to load RTMPS certificate", "err", err) + } else { + tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}} + } + } + if tlsConfig == nil && config.RTMPSListenAddr != "" { + cfg, err := stream.SelfSignedTLSConfig() + if err != nil { + slogger.Error("failed to generate self-signed RTMPS certificate", "err", err) + } else { + tlsConfig = cfg + } + } + rtmpServer := stream.NewRTMPServer(config.RTMPListenAddr, config.RTMPSListenAddr, tlsConfig, slogger) + // DevTools WebSocket upstream manager: tail Chromium supervisord log const chromiumLogPath = "/var/log/supervisord/chromium" upstreamMgr := devtoolsproxy.NewUpstreamManager(chromiumLogPath, slogger) @@ -88,12 +116,29 @@ func main() { os.Exit(1) } + virtualInputsMgr := virtualinputs.NewManager( + config.PathToFFmpeg, + config.VirtualVideoDevice, + config.VirtualAudioSink, + config.VirtualMicrophoneSource, + config.VirtualInputWidth, + config.VirtualInputHeight, + config.VirtualInputFrameRate, + stz, + ) + apiService, err := api.New( recorder.NewFFmpegManager(), recorder.NewFFmpegRecorderFactory(config.PathToFFmpeg, defaultParams, stz), upstreamMgr, stz, nekoAuthClient, + virtualInputsMgr, + stream.NewStreamManager(), + stream.NewFFmpegStreamerFactory(config.PathToFFmpeg, streamDefaults, stz), + rtmpServer, + streamDefaults, + config.PathToFFmpeg, ) if err != nil { slogger.Error("failed to create api service", "err", err) @@ -101,6 +146,12 @@ func main() { } strictHandler := oapi.NewStrictHandler(apiService, nil) + // WebSocket chunk ingest endpoints for virtual inputs are mounted directly because the strict + // OpenAPI handler does not expose the ResponseWriter needed for upgrades. + r.Get("/input/devices/virtual/socket/audio", apiService.HandleVirtualInputAudioSocket) + r.Get("/input/devices/virtual/socket/video", apiService.HandleVirtualInputVideoSocket) + r.Get("/input/devices/virtual/feed/socket", apiService.HandleVirtualInputFeedSocket) + r.Get("/stream/socket/{id}", apiService.HandleStreamSocket) oapi.HandlerFromMux(strictHandler, r) // endpoints to expose the spec diff --git a/server/cmd/config/config.go b/server/cmd/config/config.go index 7b063d3b..b6d8ef40 100644 --- a/server/cmd/config/config.go +++ b/server/cmd/config/config.go @@ -20,6 +20,20 @@ type Config struct { // Absolute or relative path to the ffmpeg binary. If empty the code falls back to "ffmpeg" on $PATH. PathToFFmpeg string `envconfig:"FFMPEG_PATH" default:"ffmpeg"` + // RTMP/RTMPS internal server configuration + RTMPListenAddr string `envconfig:"RTMP_LISTEN_ADDR" default:":1935"` + RTMPSListenAddr string `envconfig:"RTMPS_LISTEN_ADDR" default:":1936"` + RTMPSCertPath string `envconfig:"RTMPS_CERT_PATH" default:""` + RTMPSKeyPath string `envconfig:"RTMPS_KEY_PATH" default:""` + + // Virtual input defaults + VirtualVideoDevice string `envconfig:"VIRTUAL_INPUT_VIDEO_DEVICE" default:"/dev/video20"` + VirtualAudioSink string `envconfig:"VIRTUAL_INPUT_AUDIO_SINK" default:"audio_input"` + VirtualMicrophoneSource string `envconfig:"VIRTUAL_INPUT_MICROPHONE_SOURCE" default:"microphone"` + VirtualInputWidth int `envconfig:"VIRTUAL_INPUT_WIDTH" default:"1280"` + VirtualInputHeight int `envconfig:"VIRTUAL_INPUT_HEIGHT" default:"720"` + VirtualInputFrameRate int `envconfig:"VIRTUAL_INPUT_FRAME_RATE" default:"30"` + // DevTools proxy configuration LogCDPMessages bool `envconfig:"LOG_CDP_MESSAGES" default:"false"` } @@ -53,6 +67,27 @@ func validate(config *Config) error { if config.PathToFFmpeg == "" { return fmt.Errorf("FFMPEG_PATH is required") } + if config.RTMPListenAddr == "" { + return fmt.Errorf("RTMP_LISTEN_ADDR is required") + } + if (config.RTMPSCertPath == "") != (config.RTMPSKeyPath == "") { + return fmt.Errorf("RTMPS_CERT_PATH and RTMPS_KEY_PATH must both be set or both be empty") + } + if config.VirtualVideoDevice == "" { + return fmt.Errorf("VIRTUAL_INPUT_VIDEO_DEVICE is required") + } + if config.VirtualAudioSink == "" { + return fmt.Errorf("VIRTUAL_INPUT_AUDIO_SINK is required") + } + if config.VirtualMicrophoneSource == "" { + return fmt.Errorf("VIRTUAL_INPUT_MICROPHONE_SOURCE is required") + } + if config.VirtualInputWidth <= 0 || config.VirtualInputHeight <= 0 { + return fmt.Errorf("VIRTUAL_INPUT_WIDTH and VIRTUAL_INPUT_HEIGHT must be greater than 0") + } + if config.VirtualInputFrameRate <= 0 || config.VirtualInputFrameRate > 60 { + return fmt.Errorf("VIRTUAL_INPUT_FRAME_RATE must be between 1 and 60") + } return nil } diff --git a/server/cmd/config/config_test.go b/server/cmd/config/config_test.go index 5b2ce1d2..19807272 100644 --- a/server/cmd/config/config_test.go +++ b/server/cmd/config/config_test.go @@ -17,31 +17,84 @@ func TestLoad(t *testing.T) { name: "defaults (no env set)", env: map[string]string{}, wantCfg: &Config{ - Port: 10001, - FrameRate: 10, - DisplayNum: 1, - MaxSizeInMB: 500, - OutputDir: ".", - PathToFFmpeg: "ffmpeg", + Port: 10001, + FrameRate: 10, + DisplayNum: 1, + MaxSizeInMB: 500, + OutputDir: ".", + PathToFFmpeg: "ffmpeg", + RTMPListenAddr: ":1935", + RTMPSListenAddr: ":1936", + RTMPSCertPath: "", + RTMPSKeyPath: "", + VirtualVideoDevice: "/dev/video20", + VirtualAudioSink: "audio_input", + VirtualMicrophoneSource: "microphone", + VirtualInputWidth: 1280, + VirtualInputHeight: 720, + VirtualInputFrameRate: 30, }, }, { name: "custom valid env", env: map[string]string{ - "PORT": "12345", - "FRAME_RATE": "20", - "DISPLAY_NUM": "2", - "MAX_SIZE_MB": "250", - "OUTPUT_DIR": "/tmp", - "FFMPEG_PATH": "/usr/local/bin/ffmpeg", + "PORT": "12345", + "FRAME_RATE": "20", + "DISPLAY_NUM": "2", + "MAX_SIZE_MB": "250", + "OUTPUT_DIR": "/tmp", + "FFMPEG_PATH": "/usr/local/bin/ffmpeg", + "RTMP_LISTEN_ADDR": "0.0.0.0:1935", + "RTMPS_LISTEN_ADDR": "0.0.0.0:1936", + "RTMPS_CERT_PATH": "/cert.pem", + "RTMPS_KEY_PATH": "/key.pem", }, wantCfg: &Config{ - Port: 12345, - FrameRate: 20, - DisplayNum: 2, - MaxSizeInMB: 250, - OutputDir: "/tmp", - PathToFFmpeg: "/usr/local/bin/ffmpeg", + Port: 12345, + FrameRate: 20, + DisplayNum: 2, + MaxSizeInMB: 250, + OutputDir: "/tmp", + PathToFFmpeg: "/usr/local/bin/ffmpeg", + RTMPListenAddr: "0.0.0.0:1935", + RTMPSListenAddr: "0.0.0.0:1936", + RTMPSCertPath: "/cert.pem", + RTMPSKeyPath: "/key.pem", + VirtualVideoDevice: "/dev/video20", + VirtualAudioSink: "audio_input", + VirtualMicrophoneSource: "microphone", + VirtualInputWidth: 1280, + VirtualInputHeight: 720, + VirtualInputFrameRate: 30, + }, + }, + { + name: "custom virtual input env", + env: map[string]string{ + "VIRTUAL_INPUT_VIDEO_DEVICE": "/dev/video42", + "VIRTUAL_INPUT_AUDIO_SINK": "custom_sink", + "VIRTUAL_INPUT_MICROPHONE_SOURCE": "custom_mic", + "VIRTUAL_INPUT_WIDTH": "800", + "VIRTUAL_INPUT_HEIGHT": "600", + "VIRTUAL_INPUT_FRAME_RATE": "25", + }, + wantCfg: &Config{ + Port: 10001, + FrameRate: 10, + DisplayNum: 1, + MaxSizeInMB: 500, + OutputDir: ".", + PathToFFmpeg: "ffmpeg", + RTMPListenAddr: ":1935", + RTMPSListenAddr: ":1936", + RTMPSCertPath: "", + RTMPSKeyPath: "", + VirtualVideoDevice: "/dev/video42", + VirtualAudioSink: "custom_sink", + VirtualMicrophoneSource: "custom_mic", + VirtualInputWidth: 800, + VirtualInputHeight: 600, + VirtualInputFrameRate: 25, }, }, { @@ -79,6 +132,20 @@ func TestLoad(t *testing.T) { }, wantErr: true, }, + { + name: "rtmp listen required", + env: map[string]string{ + "RTMP_LISTEN_ADDR": "", + }, + wantErr: true, + }, + { + name: "rtmps cert and key must both be set", + env: map[string]string{ + "RTMPS_CERT_PATH": "/cert", + }, + wantErr: true, + }, } for idx := range testCases { diff --git a/server/go.mod b/server/go.mod index 9b16ea6c..c5963f87 100644 --- a/server/go.mod +++ b/server/go.mod @@ -12,8 +12,11 @@ require ( github.com/google/uuid v1.6.0 github.com/kelseyhightower/envconfig v1.4.0 github.com/m1k1o/neko/server v0.0.0-20251008185748-46e2fc7d3866 + github.com/notedit/rtmp v0.0.2 github.com/nrednav/cuid2 v1.1.0 github.com/oapi-codegen/runtime v1.1.2 + github.com/pion/rtp v1.8.26 + github.com/pion/webrtc/v4 v4.1.7 github.com/samber/lo v1.52.0 github.com/stretchr/testify v1.11.1 golang.org/x/sync v0.17.0 @@ -35,9 +38,25 @@ require ( github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.0.8 // indirect + github.com/pion/ice/v4 v4.0.13 // indirect + github.com/pion/interceptor v0.1.42 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.1.0 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.16 // indirect + github.com/pion/sctp v1.8.41 // indirect + github.com/pion/sdp/v3 v3.0.16 // indirect + github.com/pion/srtp/v3 v3.0.9 // indirect + github.com/pion/stun/v3 v3.0.1 // indirect + github.com/pion/transport/v3 v3.1.1 // indirect + github.com/pion/turn/v4 v4.1.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.40.0 // indirect + golang.org/x/net v0.42.0 // indirect golang.org/x/sys v0.34.0 // indirect golang.org/x/text v0.27.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/server/go.sum b/server/go.sum index 95301225..599d9717 100644 --- a/server/go.sum +++ b/server/go.sum @@ -50,6 +50,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/notedit/rtmp v0.0.2 h1:5+to4yezKATiJgnrcETu9LbV5G/QsWkOV9Ts2M/p33w= +github.com/notedit/rtmp v0.0.2/go.mod h1:vzuE21rowz+lT1NGsWbreIvYulgBpCGnQyeTyFblUHc= github.com/nrednav/cuid2 v1.1.0 h1:Y2P9Fo1Iz7lKuwcn+fS0mbxkNvEqoNLUtm0+moHCnYc= github.com/nrednav/cuid2 v1.1.0/go.mod h1:jBjkJAI+QLM4EUGvtwGDHC1cP1QQrRNfLo/A7qJFDhA= github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= @@ -62,6 +64,38 @@ github.com/onkernel/neko/server v0.0.0-20251008185748-46e2fc7d3866 h1:Cix/sgZLCs github.com/onkernel/neko/server v0.0.0-20251008185748-46e2fc7d3866/go.mod h1:0+zactiySvtKwfe5JFjyNrSuQLA+EEPZl5bcfcZf1RM= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.0.8 h1:ZrPUrvPVDaTJDM8Vu1veatzXebLlsIWeT7Vaate/zwM= +github.com/pion/dtls/v3 v3.0.8/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os= +github.com/pion/ice/v4 v4.0.13 h1:1cdmd80gmLdnVTM2bXzw2CBebvXvkGNEaWi/CuDK9WQ= +github.com/pion/ice/v4 v4.0.13/go.mod h1:Xo5f5DBbEjQac+6pR7i83AGuwoGxnxwXkOOvHFVnfnM= +github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ= +github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= +github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo= +github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo= +github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc= +github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM= +github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs= +github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY= +github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo= +github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo= +github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY= +github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8= +github.com/pion/stun/v3 v3.0.1 h1:jx1uUq6BdPihF0yF33Jj2mh+C9p0atY94IkdnW174kA= +github.com/pion/stun/v3 v3.0.1/go.mod h1:RHnvlKFg+qHgoKIqtQWMOJF52wsImCAf/Jh5GjX+4Tw= +github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= +github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= +github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA= +github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A= +github.com/pion/webrtc/v4 v4.1.7 h1:sl3vFuVHa1u/7DcFbud7e1zk3sG3RjBS5GI2ckltROg= +github.com/pion/webrtc/v4 v4.1.7/go.mod h1:y3mRk8wpmOVkTTEGYB/eXAg0DPEfTEdC/Y021zRiOiM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -79,8 +113,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/server/lib/oapi/oapi.go b/server/lib/oapi/oapi.go index 4b08cf14..46c4010a 100644 --- a/server/lib/oapi/oapi.go +++ b/server/lib/oapi/oapi.go @@ -1,6 +1,6 @@ // Package oapi provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.5.1 DO NOT EDIT. package oapi import ( @@ -74,8 +74,8 @@ const ( // Defines values for ProcessStatusState. const ( - Exited ProcessStatusState = "exited" - Running ProcessStatusState = "running" + ProcessStatusStateExited ProcessStatusState = "exited" + ProcessStatusStateRunning ProcessStatusState = "running" ) // Defines values for ProcessStreamEventEvent. @@ -89,6 +89,49 @@ const ( Stdout ProcessStreamEventStream = "stdout" ) +// Defines values for StartStreamRequestMode. +const ( + StartStreamRequestModeInternal StartStreamRequestMode = "internal" + StartStreamRequestModeRemote StartStreamRequestMode = "remote" + StartStreamRequestModeSocket StartStreamRequestMode = "socket" + StartStreamRequestModeWebrtc StartStreamRequestMode = "webrtc" +) + +// Defines values for StreamInfoMode. +const ( + StreamInfoModeInternal StreamInfoMode = "internal" + StreamInfoModeRemote StreamInfoMode = "remote" + StreamInfoModeSocket StreamInfoMode = "socket" + StreamInfoModeWebrtc StreamInfoMode = "webrtc" +) + +// Defines values for VirtualInputAudioDestination. +const ( + Microphone VirtualInputAudioDestination = "microphone" + Speaker VirtualInputAudioDestination = "speaker" +) + +// Defines values for VirtualInputType. +const ( + File VirtualInputType = "file" + Socket VirtualInputType = "socket" + Stream VirtualInputType = "stream" + Webrtc VirtualInputType = "webrtc" +) + +// Defines values for VirtualInputsStatusMode. +const ( + Device VirtualInputsStatusMode = "device" + VirtualFile VirtualInputsStatusMode = "virtual-file" +) + +// Defines values for VirtualInputsStatusState. +const ( + VirtualInputsStatusStateIdle VirtualInputsStatusState = "idle" + VirtualInputsStatusStatePaused VirtualInputsStatusState = "paused" + VirtualInputsStatusStateRunning VirtualInputsStatusState = "running" +) + // Defines values for LogsStreamParamsSource. const ( Path LogsStreamParamsSource = "path" @@ -533,6 +576,30 @@ type StartRecordingRequest struct { MaxFileSizeInMB *int `json:"maxFileSizeInMB,omitempty"` } +// StartStreamRequest defines model for StartStreamRequest. +type StartStreamRequest struct { + // Framerate Streaming framerate in fps (overrides server default) + Framerate *int `json:"framerate,omitempty"` + + // Id Optional identifier for the streaming session. Alphanumeric or hyphen. + Id *string `json:"id,omitempty"` + + // Mode Where to send the stream output. "internal" starts a local RTMP(S) server and streams to it. + // "remote" pushes the stream to the provided RTMP/RTMPS target_url. + // "webrtc" exposes a WebRTC offer/answer endpoint for browser-friendly playback. + // "socket" broadcasts MPEG-TS chunks over a websocket endpoint. + Mode *StartStreamRequestMode `json:"mode,omitempty"` + + // TargetUrl RTMP or RTMPS URL to push the stream to when mode is "remote". + TargetUrl *string `json:"target_url,omitempty"` +} + +// StartStreamRequestMode Where to send the stream output. "internal" starts a local RTMP(S) server and streams to it. +// "remote" pushes the stream to the provided RTMP/RTMPS target_url. +// "webrtc" exposes a WebRTC offer/answer endpoint for browser-friendly playback. +// "socket" broadcasts MPEG-TS chunks over a websocket endpoint. +type StartStreamRequestMode string + // StopRecordingRequest defines model for StopRecordingRequest. type StopRecordingRequest struct { // ForceStop Immediately stop without graceful shutdown. This may result in a corrupted video file. @@ -542,6 +609,60 @@ type StopRecordingRequest struct { Id *string `json:"id,omitempty"` } +// StopStreamRequest defines model for StopStreamRequest. +type StopStreamRequest struct { + // Id Identifier of the stream to stop. Alphanumeric or hyphen. + Id *string `json:"id,omitempty"` +} + +// StreamInfo defines model for StreamInfo. +type StreamInfo struct { + // Id Stream identifier + Id string `json:"id"` + + // IngestUrl URL ffmpeg is publishing to + IngestUrl string `json:"ingest_url"` + + // IsStreaming Whether the ffmpeg streaming process is currently running + IsStreaming bool `json:"is_streaming"` + + // Mode Whether the stream is using the internal RTMP server, remote endpoint, WebRTC, or websocket broadcast. + Mode StreamInfoMode `json:"mode"` + + // PlaybackUrl RTMP playback URL if available (internal streams only) + PlaybackUrl *string `json:"playback_url"` + + // SecurePlaybackUrl RTMPS playback URL when TLS is enabled for the internal server + SecurePlaybackUrl *string `json:"secure_playback_url"` + + // StartedAt Timestamp when streaming started + StartedAt time.Time `json:"started_at"` + + // WebrtcOfferUrl HTTP endpoint to post SDP offers to when mode is "webrtc" + WebrtcOfferUrl *string `json:"webrtc_offer_url"` + + // WebsocketUrl Websocket endpoint that streams MPEG-TS chunks when mode is "socket" + WebsocketUrl *string `json:"websocket_url"` +} + +// StreamInfoMode Whether the stream is using the internal RTMP server, remote endpoint, WebRTC, or websocket broadcast. +type StreamInfoMode string + +// StreamWebRTCAnswer defines model for StreamWebRTCAnswer. +type StreamWebRTCAnswer struct { + // Sdp SDP answer to set as the remote description on the viewer + Sdp *string `json:"sdp,omitempty"` +} + +// StreamWebRTCOffer defines model for StreamWebRTCOffer. +type StreamWebRTCOffer struct { + // Id Stream identifier (defaults to "default" if omitted) + Id *string `json:"id,omitempty"` + + // Sdp SDP offer from the viewer + Sdp string `json:"sdp"` +} + // TypeTextRequest defines model for TypeTextRequest. type TypeTextRequest struct { // Delay Delay in milliseconds between keystrokes @@ -551,6 +672,149 @@ type TypeTextRequest struct { Text string `json:"text"` } +// VirtualFeedSocketInfo Endpoint information for the websocket preview mirror of the virtual video feed. +type VirtualFeedSocketInfo struct { + // Format Container/codec expected over the websocket (e.g. mpegts or ivf). + Format *string `json:"format,omitempty"` + + // Url Websocket URL that mirrors the configured virtual video feed. + Url string `json:"url"` +} + +// VirtualInputAudio defines model for VirtualInputAudio. +type VirtualInputAudio struct { + // Destination Where to route the virtual input audio (both for configured sources and ingest endpoints): + // - "microphone": Route to the virtual microphone input (PulseAudio audio_input sink). + // This is the default. Applications reading from the virtual mic will receive this audio. + // - "speaker": Route directly to the container's audio output (PulseAudio audio_output sink). + // Use this for monitoring/playback purposes. + Destination *VirtualInputAudioDestination `json:"destination,omitempty"` + + // Format Optional format hint for socket/WebRTC feeds. Socket audio accepts mp3 chunks; WebRTC ingest expects Ogg/Opus. + Format *string `json:"format,omitempty"` + + // Type Type of media source being injected. + Type VirtualInputType `json:"type"` + + // Url Input URL (supports file URLs, HLS, RTMP(S), DASH, etc). + Url *string `json:"url,omitempty"` +} + +// VirtualInputAudioDestination Where to route the virtual input audio (both for configured sources and ingest endpoints): +// - "microphone": Route to the virtual microphone input (PulseAudio audio_input sink). +// This is the default. Applications reading from the virtual mic will receive this audio. +// - "speaker": Route directly to the container's audio output (PulseAudio audio_output sink). +// Use this for monitoring/playback purposes. +type VirtualInputAudioDestination string + +// VirtualInputIngestEndpoint defines model for VirtualInputIngestEndpoint. +type VirtualInputIngestEndpoint struct { + // Destination Where audio ingest will be routed. Only relevant for audio endpoints; ignored for video. + Destination *VirtualInputAudioDestination `json:"destination,omitempty"` + + // Format Expected format/codec for the ingest stream. + Format *string `json:"format,omitempty"` + + // Protocol Protocol used to ingest media (socket or webrtc). + Protocol *string `json:"protocol,omitempty"` + + // Url URL to push media to (ws:// or HTTP offer endpoint). + Url *string `json:"url,omitempty"` +} + +// VirtualInputType Type of media source being injected. +type VirtualInputType string + +// VirtualInputVideo defines model for VirtualInputVideo. +type VirtualInputVideo struct { + // Format Optional format hint for socket/WebRTC feeds. Socket video should use MPEG-TS chunks; WebRTC ingest expects IVF (VP8/VP9). + Format *string `json:"format,omitempty"` + + // FrameRate Frame rate for the virtual webcam output. + FrameRate *int `json:"frame_rate,omitempty"` + + // Height Target height for the virtual webcam output. + Height *int `json:"height,omitempty"` + + // Type Type of media source being injected. + Type VirtualInputType `json:"type"` + + // Url Input URL (supports file URLs, HLS, RTMP(S), DASH, etc). + Url *string `json:"url,omitempty"` + + // Width Target width for the virtual webcam output. + Width *int `json:"width,omitempty"` +} + +// VirtualInputWebRTCAnswer defines model for VirtualInputWebRTCAnswer. +type VirtualInputWebRTCAnswer struct { + // Sdp SDP answer to set on the publisher. + Sdp *string `json:"sdp,omitempty"` +} + +// VirtualInputWebRTCOffer defines model for VirtualInputWebRTCOffer. +type VirtualInputWebRTCOffer struct { + // Sdp SDP offer from the publisher. + Sdp string `json:"sdp"` +} + +// VirtualInputsIngest Network endpoints that accept realtime media for the configured virtual inputs. +type VirtualInputsIngest struct { + Audio *VirtualInputIngestEndpoint `json:"audio,omitempty"` + Video *VirtualInputIngestEndpoint `json:"video,omitempty"` +} + +// VirtualInputsRequest Configure virtual webcam and microphone inputs. +type VirtualInputsRequest struct { + Audio *VirtualInputAudio `json:"audio,omitempty"` + + // StartPaused Start with silence/black frames instead of live media until resumed. + StartPaused *bool `json:"start_paused,omitempty"` + Video *VirtualInputVideo `json:"video,omitempty"` +} + +// VirtualInputsStatus defines model for VirtualInputsStatus. +type VirtualInputsStatus struct { + Audio *VirtualInputAudio `json:"audio,omitempty"` + + // AudioFile Path to the WAV file/pipe used when virtual-file mode is active. + AudioFile *string `json:"audio_file,omitempty"` + + // AudioSink PulseAudio sink receiving injected audio. + AudioSink string `json:"audio_sink"` + + // Ingest Network endpoints that accept realtime media for the configured virtual inputs. + Ingest *VirtualInputsIngest `json:"ingest,omitempty"` + + // LastError Last observed error (if any). + LastError *string `json:"last_error,omitempty"` + + // MicrophoneSource PulseAudio source clients should use as a microphone. + MicrophoneSource string `json:"microphone_source"` + + // Mode Output mode in use (v4l2 device vs virtual capture files). + Mode VirtualInputsStatusMode `json:"mode"` + + // StartedAt Timestamp when the current pipelines started. + StartedAt *time.Time `json:"started_at,omitempty"` + + // State Current state of the virtual input pipelines. + State VirtualInputsStatusState `json:"state"` + Video *VirtualInputVideo `json:"video,omitempty"` + + // VideoDevice Video4Linux device path used for the virtual webcam. + VideoDevice string `json:"video_device"` + + // VideoFile Path to the Y4M file/pipe used when virtual-file mode is active. + VideoFile *string `json:"video_file,omitempty"` +} + +// VirtualInputsStatusMode Output mode in use (v4l2 device vs virtual capture files). +type VirtualInputsStatusMode string + +// VirtualInputsStatusState Current state of the virtual input pipelines. +type VirtualInputsStatusState string + // BadRequestError defines model for BadRequestError. type BadRequestError = Error @@ -630,6 +894,12 @@ type WriteFileParams struct { Mode *string `form:"mode,omitempty" json:"mode,omitempty"` } +// GetVirtualInputFeedParams defines parameters for GetVirtualInputFeed. +type GetVirtualInputFeedParams struct { + Fit *string `form:"fit,omitempty" json:"fit,omitempty"` + Source *string `form:"source,omitempty" json:"source,omitempty"` +} + // LogsStreamParams defines parameters for LogsStream. type LogsStreamParams struct { Source LogsStreamParamsSource `form:"source" json:"source"` @@ -708,6 +978,12 @@ type UploadZipMultipartRequestBody UploadZipMultipartBody // StartFsWatchJSONRequestBody defines body for StartFsWatch for application/json ContentType. type StartFsWatchJSONRequestBody = StartFsWatchRequest +// ConfigureVirtualInputsJSONRequestBody defines body for ConfigureVirtualInputs for application/json ContentType. +type ConfigureVirtualInputsJSONRequestBody = VirtualInputsRequest + +// NegotiateVirtualInputsWebrtcJSONRequestBody defines body for NegotiateVirtualInputsWebrtc for application/json ContentType. +type NegotiateVirtualInputsWebrtcJSONRequestBody = VirtualInputWebRTCOffer + // ExecutePlaywrightCodeJSONRequestBody defines body for ExecutePlaywrightCode for application/json ContentType. type ExecutePlaywrightCodeJSONRequestBody = ExecutePlaywrightRequest @@ -732,6 +1008,15 @@ type StartRecordingJSONRequestBody = StartRecordingRequest // StopRecordingJSONRequestBody defines body for StopRecording for application/json ContentType. type StopRecordingJSONRequestBody = StopRecordingRequest +// StartStreamJSONRequestBody defines body for StartStream for application/json ContentType. +type StartStreamJSONRequestBody = StartStreamRequest + +// StopStreamJSONRequestBody defines body for StopStream for application/json ContentType. +type StopStreamJSONRequestBody = StopStreamRequest + +// StreamWebrtcOfferJSONRequestBody defines body for StreamWebrtcOffer for application/json ContentType. +type StreamWebrtcOfferJSONRequestBody = StreamWebRTCOffer + // RequestEditorFn is the function signature for the RequestEditor callback function type RequestEditorFn func(ctx context.Context, req *http.Request) error @@ -915,6 +1200,34 @@ type ClientInterface interface { // WriteFileWithBody request with any body WriteFileWithBody(ctx context.Context, params *WriteFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + // ConfigureVirtualInputsWithBody request with any body + ConfigureVirtualInputsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ConfigureVirtualInputs(ctx context.Context, body ConfigureVirtualInputsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetVirtualInputFeed request + GetVirtualInputFeed(ctx context.Context, params *GetVirtualInputFeedParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetVirtualInputFeedSocketInfo request + GetVirtualInputFeedSocketInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // PauseVirtualInputs request + PauseVirtualInputs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ResumeVirtualInputs request + ResumeVirtualInputs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetVirtualInputsStatus request + GetVirtualInputsStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StopVirtualInputs request + StopVirtualInputs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // NegotiateVirtualInputsWebrtcWithBody request with any body + NegotiateVirtualInputsWebrtcWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + NegotiateVirtualInputsWebrtc(ctx context.Context, body NegotiateVirtualInputsWebrtcJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // LogsStream request LogsStream(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -969,6 +1282,24 @@ type ClientInterface interface { StopRecordingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) StopRecording(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ListStreams request + ListStreams(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StartStreamWithBody request with any body + StartStreamWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + StartStream(ctx context.Context, body StartStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StopStreamWithBody request with any body + StopStreamWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + StopStream(ctx context.Context, body StopStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // StreamWebrtcOfferWithBody request with any body + StreamWebrtcOfferWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + StreamWebrtcOffer(ctx context.Context, body StreamWebrtcOfferJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) } func (c *Client) PatchChromiumFlagsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { @@ -1475,6 +1806,126 @@ func (c *Client) WriteFileWithBody(ctx context.Context, params *WriteFileParams, return c.Client.Do(req) } +func (c *Client) ConfigureVirtualInputsWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewConfigureVirtualInputsRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ConfigureVirtualInputs(ctx context.Context, body ConfigureVirtualInputsJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewConfigureVirtualInputsRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetVirtualInputFeed(ctx context.Context, params *GetVirtualInputFeedParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetVirtualInputFeedRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetVirtualInputFeedSocketInfo(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetVirtualInputFeedSocketInfoRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) PauseVirtualInputs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPauseVirtualInputsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ResumeVirtualInputs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewResumeVirtualInputsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) GetVirtualInputsStatus(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetVirtualInputsStatusRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StopVirtualInputs(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStopVirtualInputsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) NegotiateVirtualInputsWebrtcWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewNegotiateVirtualInputsWebrtcRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) NegotiateVirtualInputsWebrtc(ctx context.Context, body NegotiateVirtualInputsWebrtcJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewNegotiateVirtualInputsWebrtcRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) LogsStream(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewLogsStreamRequest(c.Server, params) if err != nil { @@ -1727,48 +2178,132 @@ func (c *Client) StopRecording(ctx context.Context, body StopRecordingJSONReques return c.Client.Do(req) } -// NewPatchChromiumFlagsRequest calls the generic PatchChromiumFlags builder with application/json body -func NewPatchChromiumFlagsRequest(server string, body PatchChromiumFlagsJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) +func (c *Client) ListStreams(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListStreamsRequest(c.Server) if err != nil { return nil, err } - bodyReader = bytes.NewReader(buf) - return NewPatchChromiumFlagsRequestWithBody(server, "application/json", bodyReader) + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) } -// NewPatchChromiumFlagsRequestWithBody generates requests for PatchChromiumFlags with any type of body -func NewPatchChromiumFlagsRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { - var err error - - serverURL, err := url.Parse(server) +func (c *Client) StartStreamWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartStreamRequestWithBody(c.Server, contentType, body) if err != nil { return nil, err } - - operationPath := fmt.Sprintf("/chromium/flags") - if operationPath[0] == '/' { - operationPath = "." + operationPath + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err } + return c.Client.Do(req) +} - queryURL, err := serverURL.Parse(operationPath) +func (c *Client) StartStream(ctx context.Context, body StartStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStartStreamRequest(c.Server, body) if err != nil { return nil, err } - - req, err := http.NewRequest("PATCH", queryURL.String(), body) - if err != nil { + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { return nil, err } - - req.Header.Add("Content-Type", contentType) - - return req, nil + return c.Client.Do(req) } -// NewUploadExtensionsAndRestartRequestWithBody generates requests for UploadExtensionsAndRestart with any type of body -func NewUploadExtensionsAndRestartRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +func (c *Client) StopStreamWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStopStreamRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StopStream(ctx context.Context, body StopStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStopStreamRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StreamWebrtcOfferWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStreamWebrtcOfferRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) StreamWebrtcOffer(ctx context.Context, body StreamWebrtcOfferJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewStreamWebrtcOfferRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewPatchChromiumFlagsRequest calls the generic PatchChromiumFlags builder with application/json body +func NewPatchChromiumFlagsRequest(server string, body PatchChromiumFlagsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPatchChromiumFlagsRequestWithBody(server, "application/json", bodyReader) +} + +// NewPatchChromiumFlagsRequestWithBody generates requests for PatchChromiumFlags with any type of body +func NewPatchChromiumFlagsRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/chromium/flags") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PATCH", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewUploadExtensionsAndRestartRequestWithBody generates requests for UploadExtensionsAndRestart with any type of body +func NewUploadExtensionsAndRestartRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -2765,8 +3300,19 @@ func NewWriteFileRequestWithBody(server string, params *WriteFileParams, content return req, nil } -// NewLogsStreamRequest generates requests for LogsStream -func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Request, error) { +// NewConfigureVirtualInputsRequest calls the generic ConfigureVirtualInputs builder with application/json body +func NewConfigureVirtualInputsRequest(server string, body ConfigureVirtualInputsJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewConfigureVirtualInputsRequestWithBody(server, "application/json", bodyReader) +} + +// NewConfigureVirtualInputsRequestWithBody generates requests for ConfigureVirtualInputs with any type of body +func NewConfigureVirtualInputsRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -2774,7 +3320,7 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques return nil, err } - operationPath := fmt.Sprintf("/logs/stream") + operationPath := fmt.Sprintf("/input/devices/virtual/configure") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2784,40 +3330,41 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques return nil, err } - if params != nil { - queryValues := queryURL.Query() + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, params.Source); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } + req.Header.Add("Content-Type", contentType) - if params.Follow != nil { + return req, nil +} - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "follow", runtime.ParamLocationQuery, *params.Follow); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } +// NewGetVirtualInputFeedRequest generates requests for GetVirtualInputFeed +func NewGetVirtualInputFeedRequest(server string, params *GetVirtualInputFeedParams) (*http.Request, error) { + var err error - } + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } - if params.Path != nil { + operationPath := fmt.Sprintf("/input/devices/virtual/feed") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, *params.Path); err != nil { + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Fit != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "fit", runtime.ParamLocationQuery, *params.Fit); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -2831,9 +3378,9 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques } - if params.SupervisorProcess != nil { + if params.Source != nil { - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "supervisor_process", runtime.ParamLocationQuery, *params.SupervisorProcess); err != nil { + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, *params.Source); err != nil { return nil, err } else if parsed, err := url.ParseQuery(queryFrag); err != nil { return nil, err @@ -2858,19 +3405,8 @@ func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Reques return req, nil } -// NewExecutePlaywrightCodeRequest calls the generic ExecutePlaywrightCode builder with application/json body -func NewExecutePlaywrightCodeRequest(server string, body ExecutePlaywrightCodeJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewExecutePlaywrightCodeRequestWithBody(server, "application/json", bodyReader) -} - -// NewExecutePlaywrightCodeRequestWithBody generates requests for ExecutePlaywrightCode with any type of body -func NewExecutePlaywrightCodeRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +// NewGetVirtualInputFeedSocketInfoRequest generates requests for GetVirtualInputFeedSocketInfo +func NewGetVirtualInputFeedSocketInfoRequest(server string) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -2878,7 +3414,7 @@ func NewExecutePlaywrightCodeRequestWithBody(server string, contentType string, return nil, err } - operationPath := fmt.Sprintf("/playwright/execute") + operationPath := fmt.Sprintf("/input/devices/virtual/feed/socket/info") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2888,29 +3424,16 @@ func NewExecutePlaywrightCodeRequestWithBody(server string, contentType string, return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewProcessExecRequest calls the generic ProcessExec builder with application/json body -func NewProcessExecRequest(server string, body ProcessExecJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewProcessExecRequestWithBody(server, "application/json", bodyReader) -} - -// NewProcessExecRequestWithBody generates requests for ProcessExec with any type of body -func NewProcessExecRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +// NewPauseVirtualInputsRequest generates requests for PauseVirtualInputs +func NewPauseVirtualInputsRequest(server string) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -2918,7 +3441,7 @@ func NewProcessExecRequestWithBody(server string, contentType string, body io.Re return nil, err } - operationPath := fmt.Sprintf("/process/exec") + operationPath := fmt.Sprintf("/input/devices/virtual/pause") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2928,29 +3451,16 @@ func NewProcessExecRequestWithBody(server string, contentType string, body io.Re return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("POST", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewProcessSpawnRequest calls the generic ProcessSpawn builder with application/json body -func NewProcessSpawnRequest(server string, body ProcessSpawnJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewProcessSpawnRequestWithBody(server, "application/json", bodyReader) -} - -// NewProcessSpawnRequestWithBody generates requests for ProcessSpawn with any type of body -func NewProcessSpawnRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +// NewResumeVirtualInputsRequest generates requests for ResumeVirtualInputs +func NewResumeVirtualInputsRequest(server string) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -2958,7 +3468,7 @@ func NewProcessSpawnRequestWithBody(server string, contentType string, body io.R return nil, err } - operationPath := fmt.Sprintf("/process/spawn") + operationPath := fmt.Sprintf("/input/devices/virtual/resume") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -2968,44 +3478,24 @@ func NewProcessSpawnRequestWithBody(server string, contentType string, body io.R return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("POST", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewProcessKillRequest calls the generic ProcessKill builder with application/json body -func NewProcessKillRequest(server string, processId openapi_types.UUID, body ProcessKillJSONRequestBody) (*http.Request, error) { - var bodyReader io.Reader - buf, err := json.Marshal(body) - if err != nil { - return nil, err - } - bodyReader = bytes.NewReader(buf) - return NewProcessKillRequestWithBody(server, processId, "application/json", bodyReader) -} - -// NewProcessKillRequestWithBody generates requests for ProcessKill with any type of body -func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { +// NewGetVirtualInputsStatusRequest generates requests for GetVirtualInputsStatus +func NewGetVirtualInputsStatusRequest(server string) (*http.Request, error) { var err error - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) - if err != nil { - return nil, err - } - serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/process/%s/kill", pathParam0) + operationPath := fmt.Sprintf("/input/devices/virtual/status") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3015,33 +3505,24 @@ func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, return nil, err } - req, err := http.NewRequest("POST", queryURL.String(), body) + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err } - req.Header.Add("Content-Type", contentType) - return req, nil } -// NewProcessStatusRequest generates requests for ProcessStatus -func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http.Request, error) { +// NewStopVirtualInputsRequest generates requests for StopVirtualInputs +func NewStopVirtualInputsRequest(server string) (*http.Request, error) { var err error - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) - if err != nil { - return nil, err - } - serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/process/%s/status", pathParam0) + operationPath := fmt.Sprintf("/input/devices/virtual/stop") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3051,7 +3532,7 @@ func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("POST", queryURL.String(), nil) if err != nil { return nil, err } @@ -3059,34 +3540,27 @@ func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http return req, nil } -// NewProcessStdinRequest calls the generic ProcessStdin builder with application/json body -func NewProcessStdinRequest(server string, processId openapi_types.UUID, body ProcessStdinJSONRequestBody) (*http.Request, error) { +// NewNegotiateVirtualInputsWebrtcRequest calls the generic NegotiateVirtualInputsWebrtc builder with application/json body +func NewNegotiateVirtualInputsWebrtcRequest(server string, body NegotiateVirtualInputsWebrtcJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewProcessStdinRequestWithBody(server, processId, "application/json", bodyReader) + return NewNegotiateVirtualInputsWebrtcRequestWithBody(server, "application/json", bodyReader) } -// NewProcessStdinRequestWithBody generates requests for ProcessStdin with any type of body -func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { +// NewNegotiateVirtualInputsWebrtcRequestWithBody generates requests for NegotiateVirtualInputsWebrtc with any type of body +func NewNegotiateVirtualInputsWebrtcRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) - if err != nil { - return nil, err - } - serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/process/%s/stdin", pathParam0) + operationPath := fmt.Sprintf("/input/devices/virtual/webrtc/offer") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3106,23 +3580,16 @@ func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID, return req, nil } -// NewProcessStdoutStreamRequest generates requests for ProcessStdoutStream -func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) (*http.Request, error) { +// NewLogsStreamRequest generates requests for LogsStream +func NewLogsStreamRequest(server string, params *LogsStreamParams) (*http.Request, error) { var err error - var pathParam0 string - - pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + serverURL, err := url.Parse(server) if err != nil { return nil, err } - serverURL, err := url.Parse(server) - if err != nil { - return nil, err - } - - operationPath := fmt.Sprintf("/process/%s/stdout/stream", pathParam0) + operationPath := fmt.Sprintf("/logs/stream") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3132,6 +3599,72 @@ func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) return nil, err } + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "source", runtime.ParamLocationQuery, params.Source); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + if params.Follow != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "follow", runtime.ParamLocationQuery, *params.Follow); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.Path != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "path", runtime.ParamLocationQuery, *params.Path); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + if params.SupervisorProcess != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "supervisor_process", runtime.ParamLocationQuery, *params.SupervisorProcess); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + req, err := http.NewRequest("GET", queryURL.String(), nil) if err != nil { return nil, err @@ -3140,19 +3673,19 @@ func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) return req, nil } -// NewDeleteRecordingRequest calls the generic DeleteRecording builder with application/json body -func NewDeleteRecordingRequest(server string, body DeleteRecordingJSONRequestBody) (*http.Request, error) { +// NewExecutePlaywrightCodeRequest calls the generic ExecutePlaywrightCode builder with application/json body +func NewExecutePlaywrightCodeRequest(server string, body ExecutePlaywrightCodeJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewDeleteRecordingRequestWithBody(server, "application/json", bodyReader) + return NewExecutePlaywrightCodeRequestWithBody(server, "application/json", bodyReader) } -// NewDeleteRecordingRequestWithBody generates requests for DeleteRecording with any type of body -func NewDeleteRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +// NewExecutePlaywrightCodeRequestWithBody generates requests for ExecutePlaywrightCode with any type of body +func NewExecutePlaywrightCodeRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -3160,7 +3693,7 @@ func NewDeleteRecordingRequestWithBody(server string, contentType string, body i return nil, err } - operationPath := fmt.Sprintf("/recording/delete") + operationPath := fmt.Sprintf("/playwright/execute") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3180,8 +3713,19 @@ func NewDeleteRecordingRequestWithBody(server string, contentType string, body i return req, nil } -// NewDownloadRecordingRequest generates requests for DownloadRecording -func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) (*http.Request, error) { +// NewProcessExecRequest calls the generic ProcessExec builder with application/json body +func NewProcessExecRequest(server string, body ProcessExecJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewProcessExecRequestWithBody(server, "application/json", bodyReader) +} + +// NewProcessExecRequestWithBody generates requests for ProcessExec with any type of body +func NewProcessExecRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -3189,7 +3733,7 @@ func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) return nil, err } - operationPath := fmt.Sprintf("/recording/download") + operationPath := fmt.Sprintf("/process/exec") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3199,38 +3743,29 @@ func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) return nil, err } - if params != nil { - queryValues := queryURL.Query() - - if params.Id != nil { - - if queryFrag, err := runtime.StyleParamWithLocation("form", true, "id", runtime.ParamLocationQuery, *params.Id); err != nil { - return nil, err - } else if parsed, err := url.ParseQuery(queryFrag); err != nil { - return nil, err - } else { - for k, v := range parsed { - for _, v2 := range v { - queryValues.Add(k, v2) - } - } - } + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } - } + req.Header.Add("Content-Type", contentType) - queryURL.RawQuery = queryValues.Encode() - } + return req, nil +} - req, err := http.NewRequest("GET", queryURL.String(), nil) +// NewProcessSpawnRequest calls the generic ProcessSpawn builder with application/json body +func NewProcessSpawnRequest(server string, body ProcessSpawnJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) if err != nil { return nil, err } - - return req, nil + bodyReader = bytes.NewReader(buf) + return NewProcessSpawnRequestWithBody(server, "application/json", bodyReader) } -// NewListRecordersRequest generates requests for ListRecorders -func NewListRecordersRequest(server string) (*http.Request, error) { +// NewProcessSpawnRequestWithBody generates requests for ProcessSpawn with any type of body +func NewProcessSpawnRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { var err error serverURL, err := url.Parse(server) @@ -3238,7 +3773,7 @@ func NewListRecordersRequest(server string) (*http.Request, error) { return nil, err } - operationPath := fmt.Sprintf("/recording/list") + operationPath := fmt.Sprintf("/process/spawn") if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3248,35 +3783,44 @@ func NewListRecordersRequest(server string) (*http.Request, error) { return nil, err } - req, err := http.NewRequest("GET", queryURL.String(), nil) + req, err := http.NewRequest("POST", queryURL.String(), body) if err != nil { return nil, err } + req.Header.Add("Content-Type", contentType) + return req, nil } -// NewStartRecordingRequest calls the generic StartRecording builder with application/json body -func NewStartRecordingRequest(server string, body StartRecordingJSONRequestBody) (*http.Request, error) { +// NewProcessKillRequest calls the generic ProcessKill builder with application/json body +func NewProcessKillRequest(server string, processId openapi_types.UUID, body ProcessKillJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewStartRecordingRequestWithBody(server, "application/json", bodyReader) + return NewProcessKillRequestWithBody(server, processId, "application/json", bodyReader) } -// NewStartRecordingRequestWithBody generates requests for StartRecording with any type of body -func NewStartRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +// NewProcessKillRequestWithBody generates requests for ProcessKill with any type of body +func NewProcessKillRequestWithBody(server string, processId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { var err error + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + if err != nil { + return nil, err + } + serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/recording/start") + operationPath := fmt.Sprintf("/process/%s/kill", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3296,27 +3840,68 @@ func NewStartRecordingRequestWithBody(server string, contentType string, body io return req, nil } -// NewStopRecordingRequest calls the generic StopRecording builder with application/json body -func NewStopRecordingRequest(server string, body StopRecordingJSONRequestBody) (*http.Request, error) { +// NewProcessStatusRequest generates requests for ProcessStatus +func NewProcessStatusRequest(server string, processId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/process/%s/status", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewProcessStdinRequest calls the generic ProcessStdin builder with application/json body +func NewProcessStdinRequest(server string, processId openapi_types.UUID, body ProcessStdinJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewStopRecordingRequestWithBody(server, "application/json", bodyReader) + return NewProcessStdinRequestWithBody(server, processId, "application/json", bodyReader) } -// NewStopRecordingRequestWithBody generates requests for StopRecording with any type of body -func NewStopRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { +// NewProcessStdinRequestWithBody generates requests for ProcessStdin with any type of body +func NewProcessStdinRequestWithBody(server string, processId openapi_types.UUID, contentType string, body io.Reader) (*http.Request, error) { var err error + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + if err != nil { + return nil, err + } + serverURL, err := url.Parse(server) if err != nil { return nil, err } - operationPath := fmt.Sprintf("/recording/stop") + operationPath := fmt.Sprintf("/process/%s/stdin", pathParam0) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -3336,29 +3921,406 @@ func NewStopRecordingRequestWithBody(server string, contentType string, body io. return req, nil } -func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { - for _, r := range c.RequestEditors { - if err := r(ctx, req); err != nil { - return err - } +// NewProcessStdoutStreamRequest generates requests for ProcessStdoutStream +func NewProcessStdoutStreamRequest(server string, processId openapi_types.UUID) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "process_id", runtime.ParamLocationPath, processId) + if err != nil { + return nil, err } - for _, r := range additionalEditors { - if err := r(ctx, req); err != nil { - return err - } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err } - return nil -} -// ClientWithResponses builds on ClientInterface to offer response payloads -type ClientWithResponses struct { - ClientInterface -} + operationPath := fmt.Sprintf("/process/%s/stdout/stream", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } -// NewClientWithResponses creates a new ClientWithResponses, which wraps -// Client with return type handling -func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { - client, err := NewClient(server, opts...) + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewDeleteRecordingRequest calls the generic DeleteRecording builder with application/json body +func NewDeleteRecordingRequest(server string, body DeleteRecordingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewDeleteRecordingRequestWithBody(server, "application/json", bodyReader) +} + +// NewDeleteRecordingRequestWithBody generates requests for DeleteRecording with any type of body +func NewDeleteRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/delete") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewDownloadRecordingRequest generates requests for DownloadRecording +func NewDownloadRecordingRequest(server string, params *DownloadRecordingParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/download") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if params.Id != nil { + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "id", runtime.ParamLocationQuery, *params.Id); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewListRecordersRequest generates requests for ListRecorders +func NewListRecordersRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/list") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewStartRecordingRequest calls the generic StartRecording builder with application/json body +func NewStartRecordingRequest(server string, body StartRecordingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStartRecordingRequestWithBody(server, "application/json", bodyReader) +} + +// NewStartRecordingRequestWithBody generates requests for StartRecording with any type of body +func NewStartRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/start") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewStopRecordingRequest calls the generic StopRecording builder with application/json body +func NewStopRecordingRequest(server string, body StopRecordingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStopRecordingRequestWithBody(server, "application/json", bodyReader) +} + +// NewStopRecordingRequestWithBody generates requests for StopRecording with any type of body +func NewStopRecordingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/recording/stop") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewListStreamsRequest generates requests for ListStreams +func NewListStreamsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/stream/list") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewStartStreamRequest calls the generic StartStream builder with application/json body +func NewStartStreamRequest(server string, body StartStreamJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStartStreamRequestWithBody(server, "application/json", bodyReader) +} + +// NewStartStreamRequestWithBody generates requests for StartStream with any type of body +func NewStartStreamRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/stream/start") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewStopStreamRequest calls the generic StopStream builder with application/json body +func NewStopStreamRequest(server string, body StopStreamJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStopStreamRequestWithBody(server, "application/json", bodyReader) +} + +// NewStopStreamRequestWithBody generates requests for StopStream with any type of body +func NewStopStreamRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/stream/stop") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewStreamWebrtcOfferRequest calls the generic StreamWebrtcOffer builder with application/json body +func NewStreamWebrtcOfferRequest(server string, body StreamWebrtcOfferJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewStreamWebrtcOfferRequestWithBody(server, "application/json", bodyReader) +} + +// NewStreamWebrtcOfferRequestWithBody generates requests for StreamWebrtcOffer with any type of body +func NewStreamWebrtcOfferRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/stream/webrtc/offer") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) if err != nil { return nil, err } @@ -3489,6 +4451,34 @@ type ClientWithResponsesInterface interface { // WriteFileWithBodyWithResponse request with any body WriteFileWithBodyWithResponse(ctx context.Context, params *WriteFileParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*WriteFileResponse, error) + // ConfigureVirtualInputsWithBodyWithResponse request with any body + ConfigureVirtualInputsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ConfigureVirtualInputsResponse, error) + + ConfigureVirtualInputsWithResponse(ctx context.Context, body ConfigureVirtualInputsJSONRequestBody, reqEditors ...RequestEditorFn) (*ConfigureVirtualInputsResponse, error) + + // GetVirtualInputFeedWithResponse request + GetVirtualInputFeedWithResponse(ctx context.Context, params *GetVirtualInputFeedParams, reqEditors ...RequestEditorFn) (*GetVirtualInputFeedResponse, error) + + // GetVirtualInputFeedSocketInfoWithResponse request + GetVirtualInputFeedSocketInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVirtualInputFeedSocketInfoResponse, error) + + // PauseVirtualInputsWithResponse request + PauseVirtualInputsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PauseVirtualInputsResponse, error) + + // ResumeVirtualInputsWithResponse request + ResumeVirtualInputsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ResumeVirtualInputsResponse, error) + + // GetVirtualInputsStatusWithResponse request + GetVirtualInputsStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVirtualInputsStatusResponse, error) + + // StopVirtualInputsWithResponse request + StopVirtualInputsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*StopVirtualInputsResponse, error) + + // NegotiateVirtualInputsWebrtcWithBodyWithResponse request with any body + NegotiateVirtualInputsWebrtcWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NegotiateVirtualInputsWebrtcResponse, error) + + NegotiateVirtualInputsWebrtcWithResponse(ctx context.Context, body NegotiateVirtualInputsWebrtcJSONRequestBody, reqEditors ...RequestEditorFn) (*NegotiateVirtualInputsWebrtcResponse, error) + // LogsStreamWithResponse request LogsStreamWithResponse(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*LogsStreamResponse, error) @@ -3542,7 +4532,25 @@ type ClientWithResponsesInterface interface { // StopRecordingWithBodyWithResponse request with any body StopRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) - StopRecordingWithResponse(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) + StopRecordingWithResponse(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) + + // ListStreamsWithResponse request + ListStreamsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListStreamsResponse, error) + + // StartStreamWithBodyWithResponse request with any body + StartStreamWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartStreamResponse, error) + + StartStreamWithResponse(ctx context.Context, body StartStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*StartStreamResponse, error) + + // StopStreamWithBodyWithResponse request with any body + StopStreamWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopStreamResponse, error) + + StopStreamWithResponse(ctx context.Context, body StopStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*StopStreamResponse, error) + + // StreamWebrtcOfferWithBodyWithResponse request with any body + StreamWebrtcOfferWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StreamWebrtcOfferResponse, error) + + StreamWebrtcOfferWithResponse(ctx context.Context, body StreamWebrtcOfferJSONRequestBody, reqEditors ...RequestEditorFn) (*StreamWebrtcOfferResponse, error) } type PatchChromiumFlagsResponse struct { @@ -4166,6 +5174,195 @@ func (r WriteFileResponse) StatusCode() int { return 0 } +type ConfigureVirtualInputsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualInputsStatus + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ConfigureVirtualInputsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ConfigureVirtualInputsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetVirtualInputFeedResponse struct { + Body []byte + HTTPResponse *http.Response + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r GetVirtualInputFeedResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetVirtualInputFeedResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetVirtualInputFeedSocketInfoResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualFeedSocketInfo + JSON409 *ConflictError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r GetVirtualInputFeedSocketInfoResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetVirtualInputFeedSocketInfoResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type PauseVirtualInputsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualInputsStatus + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r PauseVirtualInputsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PauseVirtualInputsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ResumeVirtualInputsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualInputsStatus + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ResumeVirtualInputsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ResumeVirtualInputsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetVirtualInputsStatusResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualInputsStatus + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r GetVirtualInputsStatusResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetVirtualInputsStatusResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type StopVirtualInputsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualInputsStatus + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r StopVirtualInputsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r StopVirtualInputsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type NegotiateVirtualInputsWebrtcResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *VirtualInputWebRTCAnswer + JSON400 *BadRequestError + JSON409 *ConflictError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r NegotiateVirtualInputsWebrtcResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r NegotiateVirtualInputsWebrtcResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type LogsStreamResponse struct { Body []byte HTTPResponse *http.Response @@ -4214,13 +5411,112 @@ func (r ExecutePlaywrightCodeResponse) StatusCode() int { type ProcessExecResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *ProcessExecResult + JSON200 *ProcessExecResult + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ProcessExecResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ProcessExecResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ProcessSpawnResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ProcessSpawnResult + JSON400 *BadRequestError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ProcessSpawnResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ProcessSpawnResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ProcessKillResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *OkResponse + JSON400 *BadRequestError + JSON404 *NotFoundError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ProcessKillResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ProcessKillResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ProcessStatusResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ProcessStatus + JSON400 *BadRequestError + JSON404 *NotFoundError + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ProcessStatusResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ProcessStatusResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ProcessStdinResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ProcessStdinResult JSON400 *BadRequestError + JSON404 *NotFoundError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ProcessExecResponse) Status() string { +func (r ProcessStdinResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4228,23 +5524,23 @@ func (r ProcessExecResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ProcessExecResponse) StatusCode() int { +func (r ProcessStdinResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type ProcessSpawnResponse struct { +type ProcessStdoutStreamResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *ProcessSpawnResult JSON400 *BadRequestError + JSON404 *NotFoundError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ProcessSpawnResponse) Status() string { +func (r ProcessStdoutStreamResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4252,24 +5548,23 @@ func (r ProcessSpawnResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ProcessSpawnResponse) StatusCode() int { +func (r ProcessStdoutStreamResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type ProcessKillResponse struct { +type DeleteRecordingResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *OkResponse JSON400 *BadRequestError JSON404 *NotFoundError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ProcessKillResponse) Status() string { +func (r DeleteRecordingResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4277,24 +5572,23 @@ func (r ProcessKillResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ProcessKillResponse) StatusCode() int { +func (r DeleteRecordingResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type ProcessStatusResponse struct { +type DownloadRecordingResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *ProcessStatus JSON400 *BadRequestError JSON404 *NotFoundError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ProcessStatusResponse) Status() string { +func (r DownloadRecordingResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4302,24 +5596,22 @@ func (r ProcessStatusResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ProcessStatusResponse) StatusCode() int { +func (r DownloadRecordingResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type ProcessStdinResponse struct { +type ListRecordersResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *ProcessStdinResult - JSON400 *BadRequestError - JSON404 *NotFoundError + JSON200 *[]RecorderInfo JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ProcessStdinResponse) Status() string { +func (r ListRecordersResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4327,23 +5619,23 @@ func (r ProcessStdinResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ProcessStdinResponse) StatusCode() int { +func (r ListRecordersResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type ProcessStdoutStreamResponse struct { +type StartRecordingResponse struct { Body []byte HTTPResponse *http.Response JSON400 *BadRequestError - JSON404 *NotFoundError + JSON409 *ConflictError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ProcessStdoutStreamResponse) Status() string { +func (r StartRecordingResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4351,23 +5643,22 @@ func (r ProcessStdoutStreamResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ProcessStdoutStreamResponse) StatusCode() int { +func (r StartRecordingResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type DeleteRecordingResponse struct { +type StopRecordingResponse struct { Body []byte HTTPResponse *http.Response JSON400 *BadRequestError - JSON404 *NotFoundError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r DeleteRecordingResponse) Status() string { +func (r StopRecordingResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4375,23 +5666,22 @@ func (r DeleteRecordingResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r DeleteRecordingResponse) StatusCode() int { +func (r StopRecordingResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type DownloadRecordingResponse struct { +type ListStreamsResponse struct { Body []byte HTTPResponse *http.Response - JSON400 *BadRequestError - JSON404 *NotFoundError + JSON200 *[]StreamInfo JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r DownloadRecordingResponse) Status() string { +func (r ListStreamsResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4399,22 +5689,24 @@ func (r DownloadRecordingResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r DownloadRecordingResponse) StatusCode() int { +func (r ListStreamsResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type ListRecordersResponse struct { +type StartStreamResponse struct { Body []byte HTTPResponse *http.Response - JSON200 *[]RecorderInfo + JSON201 *StreamInfo + JSON400 *BadRequestError + JSON409 *ConflictError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r ListRecordersResponse) Status() string { +func (r StartStreamResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4422,23 +5714,23 @@ func (r ListRecordersResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r ListRecordersResponse) StatusCode() int { +func (r StartStreamResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type StartRecordingResponse struct { +type StopStreamResponse struct { Body []byte HTTPResponse *http.Response JSON400 *BadRequestError - JSON409 *ConflictError + JSON404 *NotFoundError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r StartRecordingResponse) Status() string { +func (r StopStreamResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4446,22 +5738,25 @@ func (r StartRecordingResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r StartRecordingResponse) StatusCode() int { +func (r StopStreamResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } return 0 } -type StopRecordingResponse struct { +type StreamWebrtcOfferResponse struct { Body []byte HTTPResponse *http.Response + JSON200 *StreamWebRTCAnswer JSON400 *BadRequestError + JSON404 *NotFoundError + JSON409 *ConflictError JSON500 *InternalError } // Status returns HTTPResponse.Status -func (r StopRecordingResponse) Status() string { +func (r StreamWebrtcOfferResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -4469,7 +5764,7 @@ func (r StopRecordingResponse) Status() string { } // StatusCode returns HTTPResponse.StatusCode -func (r StopRecordingResponse) StatusCode() int { +func (r StreamWebrtcOfferResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -4838,6 +6133,94 @@ func (c *ClientWithResponses) WriteFileWithBodyWithResponse(ctx context.Context, return ParseWriteFileResponse(rsp) } +// ConfigureVirtualInputsWithBodyWithResponse request with arbitrary body returning *ConfigureVirtualInputsResponse +func (c *ClientWithResponses) ConfigureVirtualInputsWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ConfigureVirtualInputsResponse, error) { + rsp, err := c.ConfigureVirtualInputsWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseConfigureVirtualInputsResponse(rsp) +} + +func (c *ClientWithResponses) ConfigureVirtualInputsWithResponse(ctx context.Context, body ConfigureVirtualInputsJSONRequestBody, reqEditors ...RequestEditorFn) (*ConfigureVirtualInputsResponse, error) { + rsp, err := c.ConfigureVirtualInputs(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseConfigureVirtualInputsResponse(rsp) +} + +// GetVirtualInputFeedWithResponse request returning *GetVirtualInputFeedResponse +func (c *ClientWithResponses) GetVirtualInputFeedWithResponse(ctx context.Context, params *GetVirtualInputFeedParams, reqEditors ...RequestEditorFn) (*GetVirtualInputFeedResponse, error) { + rsp, err := c.GetVirtualInputFeed(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetVirtualInputFeedResponse(rsp) +} + +// GetVirtualInputFeedSocketInfoWithResponse request returning *GetVirtualInputFeedSocketInfoResponse +func (c *ClientWithResponses) GetVirtualInputFeedSocketInfoWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVirtualInputFeedSocketInfoResponse, error) { + rsp, err := c.GetVirtualInputFeedSocketInfo(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetVirtualInputFeedSocketInfoResponse(rsp) +} + +// PauseVirtualInputsWithResponse request returning *PauseVirtualInputsResponse +func (c *ClientWithResponses) PauseVirtualInputsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*PauseVirtualInputsResponse, error) { + rsp, err := c.PauseVirtualInputs(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParsePauseVirtualInputsResponse(rsp) +} + +// ResumeVirtualInputsWithResponse request returning *ResumeVirtualInputsResponse +func (c *ClientWithResponses) ResumeVirtualInputsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ResumeVirtualInputsResponse, error) { + rsp, err := c.ResumeVirtualInputs(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseResumeVirtualInputsResponse(rsp) +} + +// GetVirtualInputsStatusWithResponse request returning *GetVirtualInputsStatusResponse +func (c *ClientWithResponses) GetVirtualInputsStatusWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetVirtualInputsStatusResponse, error) { + rsp, err := c.GetVirtualInputsStatus(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetVirtualInputsStatusResponse(rsp) +} + +// StopVirtualInputsWithResponse request returning *StopVirtualInputsResponse +func (c *ClientWithResponses) StopVirtualInputsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*StopVirtualInputsResponse, error) { + rsp, err := c.StopVirtualInputs(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopVirtualInputsResponse(rsp) +} + +// NegotiateVirtualInputsWebrtcWithBodyWithResponse request with arbitrary body returning *NegotiateVirtualInputsWebrtcResponse +func (c *ClientWithResponses) NegotiateVirtualInputsWebrtcWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*NegotiateVirtualInputsWebrtcResponse, error) { + rsp, err := c.NegotiateVirtualInputsWebrtcWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseNegotiateVirtualInputsWebrtcResponse(rsp) +} + +func (c *ClientWithResponses) NegotiateVirtualInputsWebrtcWithResponse(ctx context.Context, body NegotiateVirtualInputsWebrtcJSONRequestBody, reqEditors ...RequestEditorFn) (*NegotiateVirtualInputsWebrtcResponse, error) { + rsp, err := c.NegotiateVirtualInputsWebrtc(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseNegotiateVirtualInputsWebrtcResponse(rsp) +} + // LogsStreamWithResponse request returning *LogsStreamResponse func (c *ClientWithResponses) LogsStreamWithResponse(ctx context.Context, params *LogsStreamParams, reqEditors ...RequestEditorFn) (*LogsStreamResponse, error) { rsp, err := c.LogsStream(ctx, params, reqEditors...) @@ -4881,153 +6264,451 @@ func (c *ClientWithResponses) ProcessExecWithResponse(ctx context.Context, body return ParseProcessExecResponse(rsp) } -// ProcessSpawnWithBodyWithResponse request with arbitrary body returning *ProcessSpawnResponse -func (c *ClientWithResponses) ProcessSpawnWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessSpawnResponse, error) { - rsp, err := c.ProcessSpawnWithBody(ctx, contentType, body, reqEditors...) +// ProcessSpawnWithBodyWithResponse request with arbitrary body returning *ProcessSpawnResponse +func (c *ClientWithResponses) ProcessSpawnWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessSpawnResponse, error) { + rsp, err := c.ProcessSpawnWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessSpawnResponse(rsp) +} + +func (c *ClientWithResponses) ProcessSpawnWithResponse(ctx context.Context, body ProcessSpawnJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessSpawnResponse, error) { + rsp, err := c.ProcessSpawn(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessSpawnResponse(rsp) +} + +// ProcessKillWithBodyWithResponse request with arbitrary body returning *ProcessKillResponse +func (c *ClientWithResponses) ProcessKillWithBodyWithResponse(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessKillResponse, error) { + rsp, err := c.ProcessKillWithBody(ctx, processId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessKillResponse(rsp) +} + +func (c *ClientWithResponses) ProcessKillWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessKillJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessKillResponse, error) { + rsp, err := c.ProcessKill(ctx, processId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessKillResponse(rsp) +} + +// ProcessStatusWithResponse request returning *ProcessStatusResponse +func (c *ClientWithResponses) ProcessStatusWithResponse(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*ProcessStatusResponse, error) { + rsp, err := c.ProcessStatus(ctx, processId, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessStatusResponse(rsp) +} + +// ProcessStdinWithBodyWithResponse request with arbitrary body returning *ProcessStdinResponse +func (c *ClientWithResponses) ProcessStdinWithBodyWithResponse(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessStdinResponse, error) { + rsp, err := c.ProcessStdinWithBody(ctx, processId, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessStdinResponse(rsp) +} + +func (c *ClientWithResponses) ProcessStdinWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessStdinJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessStdinResponse, error) { + rsp, err := c.ProcessStdin(ctx, processId, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessStdinResponse(rsp) +} + +// ProcessStdoutStreamWithResponse request returning *ProcessStdoutStreamResponse +func (c *ClientWithResponses) ProcessStdoutStreamWithResponse(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*ProcessStdoutStreamResponse, error) { + rsp, err := c.ProcessStdoutStream(ctx, processId, reqEditors...) + if err != nil { + return nil, err + } + return ParseProcessStdoutStreamResponse(rsp) +} + +// DeleteRecordingWithBodyWithResponse request with arbitrary body returning *DeleteRecordingResponse +func (c *ClientWithResponses) DeleteRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DeleteRecordingResponse, error) { + rsp, err := c.DeleteRecordingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteRecordingResponse(rsp) +} + +func (c *ClientWithResponses) DeleteRecordingWithResponse(ctx context.Context, body DeleteRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*DeleteRecordingResponse, error) { + rsp, err := c.DeleteRecording(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseDeleteRecordingResponse(rsp) +} + +// DownloadRecordingWithResponse request returning *DownloadRecordingResponse +func (c *ClientWithResponses) DownloadRecordingWithResponse(ctx context.Context, params *DownloadRecordingParams, reqEditors ...RequestEditorFn) (*DownloadRecordingResponse, error) { + rsp, err := c.DownloadRecording(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseDownloadRecordingResponse(rsp) +} + +// ListRecordersWithResponse request returning *ListRecordersResponse +func (c *ClientWithResponses) ListRecordersWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListRecordersResponse, error) { + rsp, err := c.ListRecorders(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListRecordersResponse(rsp) +} + +// StartRecordingWithBodyWithResponse request with arbitrary body returning *StartRecordingResponse +func (c *ClientWithResponses) StartRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { + rsp, err := c.StartRecordingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartRecordingResponse(rsp) +} + +func (c *ClientWithResponses) StartRecordingWithResponse(ctx context.Context, body StartRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { + rsp, err := c.StartRecording(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStartRecordingResponse(rsp) +} + +// StopRecordingWithBodyWithResponse request with arbitrary body returning *StopRecordingResponse +func (c *ClientWithResponses) StopRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { + rsp, err := c.StopRecordingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseStopRecordingResponse(rsp) +} + +func (c *ClientWithResponses) StopRecordingWithResponse(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { + rsp, err := c.StopRecording(ctx, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessSpawnResponse(rsp) + return ParseStopRecordingResponse(rsp) } -func (c *ClientWithResponses) ProcessSpawnWithResponse(ctx context.Context, body ProcessSpawnJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessSpawnResponse, error) { - rsp, err := c.ProcessSpawn(ctx, body, reqEditors...) +// ListStreamsWithResponse request returning *ListStreamsResponse +func (c *ClientWithResponses) ListStreamsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListStreamsResponse, error) { + rsp, err := c.ListStreams(ctx, reqEditors...) if err != nil { return nil, err } - return ParseProcessSpawnResponse(rsp) + return ParseListStreamsResponse(rsp) } -// ProcessKillWithBodyWithResponse request with arbitrary body returning *ProcessKillResponse -func (c *ClientWithResponses) ProcessKillWithBodyWithResponse(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessKillResponse, error) { - rsp, err := c.ProcessKillWithBody(ctx, processId, contentType, body, reqEditors...) +// StartStreamWithBodyWithResponse request with arbitrary body returning *StartStreamResponse +func (c *ClientWithResponses) StartStreamWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartStreamResponse, error) { + rsp, err := c.StartStreamWithBody(ctx, contentType, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessKillResponse(rsp) + return ParseStartStreamResponse(rsp) } -func (c *ClientWithResponses) ProcessKillWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessKillJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessKillResponse, error) { - rsp, err := c.ProcessKill(ctx, processId, body, reqEditors...) +func (c *ClientWithResponses) StartStreamWithResponse(ctx context.Context, body StartStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*StartStreamResponse, error) { + rsp, err := c.StartStream(ctx, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessKillResponse(rsp) + return ParseStartStreamResponse(rsp) } -// ProcessStatusWithResponse request returning *ProcessStatusResponse -func (c *ClientWithResponses) ProcessStatusWithResponse(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*ProcessStatusResponse, error) { - rsp, err := c.ProcessStatus(ctx, processId, reqEditors...) +// StopStreamWithBodyWithResponse request with arbitrary body returning *StopStreamResponse +func (c *ClientWithResponses) StopStreamWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopStreamResponse, error) { + rsp, err := c.StopStreamWithBody(ctx, contentType, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessStatusResponse(rsp) + return ParseStopStreamResponse(rsp) } -// ProcessStdinWithBodyWithResponse request with arbitrary body returning *ProcessStdinResponse -func (c *ClientWithResponses) ProcessStdinWithBodyWithResponse(ctx context.Context, processId openapi_types.UUID, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*ProcessStdinResponse, error) { - rsp, err := c.ProcessStdinWithBody(ctx, processId, contentType, body, reqEditors...) +func (c *ClientWithResponses) StopStreamWithResponse(ctx context.Context, body StopStreamJSONRequestBody, reqEditors ...RequestEditorFn) (*StopStreamResponse, error) { + rsp, err := c.StopStream(ctx, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessStdinResponse(rsp) + return ParseStopStreamResponse(rsp) } -func (c *ClientWithResponses) ProcessStdinWithResponse(ctx context.Context, processId openapi_types.UUID, body ProcessStdinJSONRequestBody, reqEditors ...RequestEditorFn) (*ProcessStdinResponse, error) { - rsp, err := c.ProcessStdin(ctx, processId, body, reqEditors...) +// StreamWebrtcOfferWithBodyWithResponse request with arbitrary body returning *StreamWebrtcOfferResponse +func (c *ClientWithResponses) StreamWebrtcOfferWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StreamWebrtcOfferResponse, error) { + rsp, err := c.StreamWebrtcOfferWithBody(ctx, contentType, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessStdinResponse(rsp) + return ParseStreamWebrtcOfferResponse(rsp) } -// ProcessStdoutStreamWithResponse request returning *ProcessStdoutStreamResponse -func (c *ClientWithResponses) ProcessStdoutStreamWithResponse(ctx context.Context, processId openapi_types.UUID, reqEditors ...RequestEditorFn) (*ProcessStdoutStreamResponse, error) { - rsp, err := c.ProcessStdoutStream(ctx, processId, reqEditors...) +func (c *ClientWithResponses) StreamWebrtcOfferWithResponse(ctx context.Context, body StreamWebrtcOfferJSONRequestBody, reqEditors ...RequestEditorFn) (*StreamWebrtcOfferResponse, error) { + rsp, err := c.StreamWebrtcOffer(ctx, body, reqEditors...) if err != nil { return nil, err } - return ParseProcessStdoutStreamResponse(rsp) + return ParseStreamWebrtcOfferResponse(rsp) } -// DeleteRecordingWithBodyWithResponse request with arbitrary body returning *DeleteRecordingResponse -func (c *ClientWithResponses) DeleteRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*DeleteRecordingResponse, error) { - rsp, err := c.DeleteRecordingWithBody(ctx, contentType, body, reqEditors...) +// ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call +func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseDeleteRecordingResponse(rsp) + + response := &PatchChromiumFlagsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -func (c *ClientWithResponses) DeleteRecordingWithResponse(ctx context.Context, body DeleteRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*DeleteRecordingResponse, error) { - rsp, err := c.DeleteRecording(ctx, body, reqEditors...) +// ParseUploadExtensionsAndRestartResponse parses an HTTP response from a UploadExtensionsAndRestartWithResponse call +func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensionsAndRestartResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseDeleteRecordingResponse(rsp) + + response := &UploadExtensionsAndRestartResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -// DownloadRecordingWithResponse request returning *DownloadRecordingResponse -func (c *ClientWithResponses) DownloadRecordingWithResponse(ctx context.Context, params *DownloadRecordingParams, reqEditors ...RequestEditorFn) (*DownloadRecordingResponse, error) { - rsp, err := c.DownloadRecording(ctx, params, reqEditors...) +// ParseClickMouseResponse parses an HTTP response from a ClickMouseWithResponse call +func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseDownloadRecordingResponse(rsp) + + response := &ClickMouseResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -// ListRecordersWithResponse request returning *ListRecordersResponse -func (c *ClientWithResponses) ListRecordersWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListRecordersResponse, error) { - rsp, err := c.ListRecorders(ctx, reqEditors...) +// ParseSetCursorResponse parses an HTTP response from a SetCursorWithResponse call +func ParseSetCursorResponse(rsp *http.Response) (*SetCursorResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseListRecordersResponse(rsp) + + response := &SetCursorResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest OkResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -// StartRecordingWithBodyWithResponse request with arbitrary body returning *StartRecordingResponse -func (c *ClientWithResponses) StartRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { - rsp, err := c.StartRecordingWithBody(ctx, contentType, body, reqEditors...) +// ParseDragMouseResponse parses an HTTP response from a DragMouseWithResponse call +func ParseDragMouseResponse(rsp *http.Response) (*DragMouseResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseStartRecordingResponse(rsp) + + response := &DragMouseResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -func (c *ClientWithResponses) StartRecordingWithResponse(ctx context.Context, body StartRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StartRecordingResponse, error) { - rsp, err := c.StartRecording(ctx, body, reqEditors...) +// ParseMoveMouseResponse parses an HTTP response from a MoveMouseWithResponse call +func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseStartRecordingResponse(rsp) + + response := &MoveMouseResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil } -// StopRecordingWithBodyWithResponse request with arbitrary body returning *StopRecordingResponse -func (c *ClientWithResponses) StopRecordingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { - rsp, err := c.StopRecordingWithBody(ctx, contentType, body, reqEditors...) +// ParsePressKeyResponse parses an HTTP response from a PressKeyWithResponse call +func ParsePressKeyResponse(rsp *http.Response) (*PressKeyResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - return ParseStopRecordingResponse(rsp) -} -func (c *ClientWithResponses) StopRecordingWithResponse(ctx context.Context, body StopRecordingJSONRequestBody, reqEditors ...RequestEditorFn) (*StopRecordingResponse, error) { - rsp, err := c.StopRecording(ctx, body, reqEditors...) - if err != nil { - return nil, err + response := &PressKeyResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + } - return ParseStopRecordingResponse(rsp) + + return response, nil } -// ParsePatchChromiumFlagsResponse parses an HTTP response from a PatchChromiumFlagsWithResponse call -func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsResponse, error) { +// ParseTakeScreenshotResponse parses an HTTP response from a TakeScreenshotWithResponse call +func ParseTakeScreenshotResponse(rsp *http.Response) (*TakeScreenshotResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &PatchChromiumFlagsResponse{ + response := &TakeScreenshotResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5052,15 +6733,15 @@ func ParsePatchChromiumFlagsResponse(rsp *http.Response) (*PatchChromiumFlagsRes return response, nil } -// ParseUploadExtensionsAndRestartResponse parses an HTTP response from a UploadExtensionsAndRestartWithResponse call -func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensionsAndRestartResponse, error) { +// ParseScrollResponse parses an HTTP response from a ScrollWithResponse call +func ParseScrollResponse(rsp *http.Response) (*ScrollResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UploadExtensionsAndRestartResponse{ + response := &ScrollResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5085,15 +6766,15 @@ func ParseUploadExtensionsAndRestartResponse(rsp *http.Response) (*UploadExtensi return response, nil } -// ParseClickMouseResponse parses an HTTP response from a ClickMouseWithResponse call -func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { +// ParseTypeTextResponse parses an HTTP response from a TypeTextWithResponse call +func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ClickMouseResponse{ + response := &TypeTextResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5118,22 +6799,22 @@ func ParseClickMouseResponse(rsp *http.Response) (*ClickMouseResponse, error) { return response, nil } -// ParseSetCursorResponse parses an HTTP response from a SetCursorWithResponse call -func ParseSetCursorResponse(rsp *http.Response) (*SetCursorResponse, error) { +// ParsePatchDisplayResponse parses an HTTP response from a PatchDisplayWithResponse call +func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &SetCursorResponse{ + response := &PatchDisplayResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest OkResponse + var dest DisplayConfig if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } @@ -5146,6 +6827,13 @@ func ParseSetCursorResponse(rsp *http.Response) (*SetCursorResponse, error) { } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5158,15 +6846,15 @@ func ParseSetCursorResponse(rsp *http.Response) (*SetCursorResponse, error) { return response, nil } -// ParseDragMouseResponse parses an HTTP response from a DragMouseWithResponse call -func ParseDragMouseResponse(rsp *http.Response) (*DragMouseResponse, error) { +// ParseCreateDirectoryResponse parses an HTTP response from a CreateDirectoryWithResponse call +func ParseCreateDirectoryResponse(rsp *http.Response) (*CreateDirectoryResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DragMouseResponse{ + response := &CreateDirectoryResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5191,15 +6879,15 @@ func ParseDragMouseResponse(rsp *http.Response) (*DragMouseResponse, error) { return response, nil } -// ParseMoveMouseResponse parses an HTTP response from a MoveMouseWithResponse call -func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { +// ParseDeleteDirectoryResponse parses an HTTP response from a DeleteDirectoryWithResponse call +func ParseDeleteDirectoryResponse(rsp *http.Response) (*DeleteDirectoryResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &MoveMouseResponse{ + response := &DeleteDirectoryResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5212,6 +6900,13 @@ func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5224,15 +6919,15 @@ func ParseMoveMouseResponse(rsp *http.Response) (*MoveMouseResponse, error) { return response, nil } -// ParsePressKeyResponse parses an HTTP response from a PressKeyWithResponse call -func ParsePressKeyResponse(rsp *http.Response) (*PressKeyResponse, error) { +// ParseDeleteFileResponse parses an HTTP response from a DeleteFileWithResponse call +func ParseDeleteFileResponse(rsp *http.Response) (*DeleteFileResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &PressKeyResponse{ + response := &DeleteFileResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5245,6 +6940,13 @@ func ParsePressKeyResponse(rsp *http.Response) (*PressKeyResponse, error) { } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5257,15 +6959,15 @@ func ParsePressKeyResponse(rsp *http.Response) (*PressKeyResponse, error) { return response, nil } -// ParseTakeScreenshotResponse parses an HTTP response from a TakeScreenshotWithResponse call -func ParseTakeScreenshotResponse(rsp *http.Response) (*TakeScreenshotResponse, error) { +// ParseDownloadDirZipResponse parses an HTTP response from a DownloadDirZipWithResponse call +func ParseDownloadDirZipResponse(rsp *http.Response) (*DownloadDirZipResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &TakeScreenshotResponse{ + response := &DownloadDirZipResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5278,6 +6980,13 @@ func ParseTakeScreenshotResponse(rsp *http.Response) (*TakeScreenshotResponse, e } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5290,20 +6999,27 @@ func ParseTakeScreenshotResponse(rsp *http.Response) (*TakeScreenshotResponse, e return response, nil } -// ParseScrollResponse parses an HTTP response from a ScrollWithResponse call -func ParseScrollResponse(rsp *http.Response) (*ScrollResponse, error) { +// ParseFileInfoResponse parses an HTTP response from a FileInfoWithResponse call +func ParseFileInfoResponse(rsp *http.Response) (*FileInfoResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ScrollResponse{ + response := &FileInfoResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest FileInfo + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5311,6 +7027,13 @@ func ParseScrollResponse(rsp *http.Response) (*ScrollResponse, error) { } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5323,20 +7046,27 @@ func ParseScrollResponse(rsp *http.Response) (*ScrollResponse, error) { return response, nil } -// ParseTypeTextResponse parses an HTTP response from a TypeTextWithResponse call -func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { +// ParseListFilesResponse parses an HTTP response from a ListFilesWithResponse call +func ParseListFilesResponse(rsp *http.Response) (*ListFilesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &TypeTextResponse{ + response := &ListFilesResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ListFiles + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5344,6 +7074,13 @@ func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5356,27 +7093,20 @@ func ParseTypeTextResponse(rsp *http.Response) (*TypeTextResponse, error) { return response, nil } -// ParsePatchDisplayResponse parses an HTTP response from a PatchDisplayWithResponse call -func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error) { +// ParseMovePathResponse parses an HTTP response from a MovePathWithResponse call +func ParseMovePathResponse(rsp *http.Response) (*MovePathResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &PatchDisplayResponse{ + response := &MovePathResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest DisplayConfig - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5384,12 +7114,12 @@ func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest ConflictError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON404 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -5403,15 +7133,15 @@ func ParsePatchDisplayResponse(rsp *http.Response) (*PatchDisplayResponse, error return response, nil } -// ParseCreateDirectoryResponse parses an HTTP response from a CreateDirectoryWithResponse call -func ParseCreateDirectoryResponse(rsp *http.Response) (*CreateDirectoryResponse, error) { +// ParseReadFileResponse parses an HTTP response from a ReadFileWithResponse call +func ParseReadFileResponse(rsp *http.Response) (*ReadFileResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &CreateDirectoryResponse{ + response := &ReadFileResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5424,6 +7154,13 @@ func ParseCreateDirectoryResponse(rsp *http.Response) (*CreateDirectoryResponse, } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5436,15 +7173,15 @@ func ParseCreateDirectoryResponse(rsp *http.Response) (*CreateDirectoryResponse, return response, nil } -// ParseDeleteDirectoryResponse parses an HTTP response from a DeleteDirectoryWithResponse call -func ParseDeleteDirectoryResponse(rsp *http.Response) (*DeleteDirectoryResponse, error) { +// ParseSetFilePermissionsResponse parses an HTTP response from a SetFilePermissionsWithResponse call +func ParseSetFilePermissionsResponse(rsp *http.Response) (*SetFilePermissionsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteDirectoryResponse{ + response := &SetFilePermissionsResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5476,15 +7213,15 @@ func ParseDeleteDirectoryResponse(rsp *http.Response) (*DeleteDirectoryResponse, return response, nil } -// ParseDeleteFileResponse parses an HTTP response from a DeleteFileWithResponse call -func ParseDeleteFileResponse(rsp *http.Response) (*DeleteFileResponse, error) { +// ParseUploadFilesResponse parses an HTTP response from a UploadFilesWithResponse call +func ParseUploadFilesResponse(rsp *http.Response) (*UploadFilesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteFileResponse{ + response := &UploadFilesResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5516,15 +7253,15 @@ func ParseDeleteFileResponse(rsp *http.Response) (*DeleteFileResponse, error) { return response, nil } -// ParseDownloadDirZipResponse parses an HTTP response from a DownloadDirZipWithResponse call -func ParseDownloadDirZipResponse(rsp *http.Response) (*DownloadDirZipResponse, error) { +// ParseUploadZipResponse parses an HTTP response from a UploadZipWithResponse call +func ParseUploadZipResponse(rsp *http.Response) (*UploadZipResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DownloadDirZipResponse{ + response := &UploadZipResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5556,26 +7293,29 @@ func ParseDownloadDirZipResponse(rsp *http.Response) (*DownloadDirZipResponse, e return response, nil } -// ParseFileInfoResponse parses an HTTP response from a FileInfoWithResponse call -func ParseFileInfoResponse(rsp *http.Response) (*FileInfoResponse, error) { +// ParseStartFsWatchResponse parses an HTTP response from a StartFsWatchWithResponse call +func ParseStartFsWatchResponse(rsp *http.Response) (*StartFsWatchResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &FileInfoResponse{ + response := &StartFsWatchResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest FileInfo + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest struct { + // WatchId Unique identifier for the directory watch + WatchId *string `json:"watch_id,omitempty"` + } if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON201 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError @@ -5603,27 +7343,20 @@ func ParseFileInfoResponse(rsp *http.Response) (*FileInfoResponse, error) { return response, nil } -// ParseListFilesResponse parses an HTTP response from a ListFilesWithResponse call -func ParseListFilesResponse(rsp *http.Response) (*ListFilesResponse, error) { +// ParseStopFsWatchResponse parses an HTTP response from a StopFsWatchWithResponse call +func ParseStopFsWatchResponse(rsp *http.Response) (*StopFsWatchResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListFilesResponse{ + response := &StopFsWatchResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest ListFiles - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5650,15 +7383,15 @@ func ParseListFilesResponse(rsp *http.Response) (*ListFilesResponse, error) { return response, nil } -// ParseMovePathResponse parses an HTTP response from a MovePathWithResponse call -func ParseMovePathResponse(rsp *http.Response) (*MovePathResponse, error) { +// ParseStreamFsEventsResponse parses an HTTP response from a StreamFsEventsWithResponse call +func ParseStreamFsEventsResponse(rsp *http.Response) (*StreamFsEventsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &MovePathResponse{ + response := &StreamFsEventsResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5690,15 +7423,15 @@ func ParseMovePathResponse(rsp *http.Response) (*MovePathResponse, error) { return response, nil } -// ParseReadFileResponse parses an HTTP response from a ReadFileWithResponse call -func ParseReadFileResponse(rsp *http.Response) (*ReadFileResponse, error) { +// ParseWriteFileResponse parses an HTTP response from a WriteFileWithResponse call +func ParseWriteFileResponse(rsp *http.Response) (*WriteFileResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ReadFileResponse{ + response := &WriteFileResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -5730,34 +7463,60 @@ func ParseReadFileResponse(rsp *http.Response) (*ReadFileResponse, error) { return response, nil } -// ParseSetFilePermissionsResponse parses an HTTP response from a SetFilePermissionsWithResponse call -func ParseSetFilePermissionsResponse(rsp *http.Response) (*SetFilePermissionsResponse, error) { +// ParseConfigureVirtualInputsResponse parses an HTTP response from a ConfigureVirtualInputsWithResponse call +func ParseConfigureVirtualInputsResponse(rsp *http.Response) (*ConfigureVirtualInputsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &SetFilePermissionsResponse{ + response := &ConfigureVirtualInputsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualInputsStatus + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseGetVirtualInputFeedResponse parses an HTTP response from a GetVirtualInputFeedWithResponse call +func ParseGetVirtualInputFeedResponse(rsp *http.Response) (*GetVirtualInputFeedResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetVirtualInputFeedResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest BadRequestError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5770,33 +7529,33 @@ func ParseSetFilePermissionsResponse(rsp *http.Response) (*SetFilePermissionsRes return response, nil } -// ParseUploadFilesResponse parses an HTTP response from a UploadFilesWithResponse call -func ParseUploadFilesResponse(rsp *http.Response) (*UploadFilesResponse, error) { +// ParseGetVirtualInputFeedSocketInfoResponse parses an HTTP response from a GetVirtualInputFeedSocketInfoWithResponse call +func ParseGetVirtualInputFeedSocketInfoResponse(rsp *http.Response) (*GetVirtualInputFeedSocketInfoResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UploadFilesResponse{ + response := &GetVirtualInputFeedSocketInfoResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest BadRequestError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualFeedSocketInfo if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON409 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -5810,33 +7569,33 @@ func ParseUploadFilesResponse(rsp *http.Response) (*UploadFilesResponse, error) return response, nil } -// ParseUploadZipResponse parses an HTTP response from a UploadZipWithResponse call -func ParseUploadZipResponse(rsp *http.Response) (*UploadZipResponse, error) { +// ParsePauseVirtualInputsResponse parses an HTTP response from a PauseVirtualInputsWithResponse call +func ParsePauseVirtualInputsResponse(rsp *http.Response) (*PauseVirtualInputsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &UploadZipResponse{ + response := &PauseVirtualInputsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest BadRequestError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualInputsStatus if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON400 = &dest + response.JSON200 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON400 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -5850,29 +7609,26 @@ func ParseUploadZipResponse(rsp *http.Response) (*UploadZipResponse, error) { return response, nil } -// ParseStartFsWatchResponse parses an HTTP response from a StartFsWatchWithResponse call -func ParseStartFsWatchResponse(rsp *http.Response) (*StartFsWatchResponse, error) { +// ParseResumeVirtualInputsResponse parses an HTTP response from a ResumeVirtualInputsWithResponse call +func ParseResumeVirtualInputsResponse(rsp *http.Response) (*ResumeVirtualInputsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StartFsWatchResponse{ + response := &ResumeVirtualInputsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: - var dest struct { - // WatchId Unique identifier for the directory watch - WatchId *string `json:"watch_id,omitempty"` - } + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualInputsStatus if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON201 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError @@ -5881,13 +7637,6 @@ func ParseStartFsWatchResponse(rsp *http.Response) (*StartFsWatchResponse, error } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -5900,33 +7649,26 @@ func ParseStartFsWatchResponse(rsp *http.Response) (*StartFsWatchResponse, error return response, nil } -// ParseStopFsWatchResponse parses an HTTP response from a StopFsWatchWithResponse call -func ParseStopFsWatchResponse(rsp *http.Response) (*StopFsWatchResponse, error) { +// ParseGetVirtualInputsStatusResponse parses an HTTP response from a GetVirtualInputsStatusWithResponse call +func ParseGetVirtualInputsStatusResponse(rsp *http.Response) (*GetVirtualInputsStatusResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StopFsWatchResponse{ + response := &GetVirtualInputsStatusResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest BadRequestError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualInputsStatus if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -5940,33 +7682,26 @@ func ParseStopFsWatchResponse(rsp *http.Response) (*StopFsWatchResponse, error) return response, nil } -// ParseStreamFsEventsResponse parses an HTTP response from a StreamFsEventsWithResponse call -func ParseStreamFsEventsResponse(rsp *http.Response) (*StreamFsEventsResponse, error) { +// ParseStopVirtualInputsResponse parses an HTTP response from a StopVirtualInputsWithResponse call +func ParseStopVirtualInputsResponse(rsp *http.Response) (*StopVirtualInputsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StreamFsEventsResponse{ + response := &StopVirtualInputsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest BadRequestError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualInputsStatus if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -5980,20 +7715,27 @@ func ParseStreamFsEventsResponse(rsp *http.Response) (*StreamFsEventsResponse, e return response, nil } -// ParseWriteFileResponse parses an HTTP response from a WriteFileWithResponse call -func ParseWriteFileResponse(rsp *http.Response) (*WriteFileResponse, error) { +// ParseNegotiateVirtualInputsWebrtcResponse parses an HTTP response from a NegotiateVirtualInputsWebrtcWithResponse call +func ParseNegotiateVirtualInputsWebrtcResponse(rsp *http.Response) (*NegotiateVirtualInputsWebrtcResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &WriteFileResponse{ + response := &NegotiateVirtualInputsWebrtcResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest VirtualInputWebRTCAnswer + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6001,12 +7743,12 @@ func ParseWriteFileResponse(rsp *http.Response) (*WriteFileResponse, error) { } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON409 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -6278,12 +8020,165 @@ func ParseProcessStdinResponse(rsp *http.Response) (*ProcessStdinResponse, error } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseProcessStdoutStreamResponse parses an HTTP response from a ProcessStdoutStreamWithResponse call +func ParseProcessStdoutStreamResponse(rsp *http.Response) (*ProcessStdoutStreamResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ProcessStdoutStreamResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseDeleteRecordingResponse parses an HTTP response from a DeleteRecordingWithResponse call +func ParseDeleteRecordingResponse(rsp *http.Response) (*DeleteRecordingResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteRecordingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseDownloadRecordingResponse parses an HTTP response from a DownloadRecordingWithResponse call +func ParseDownloadRecordingResponse(rsp *http.Response) (*DownloadRecordingResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DownloadRecordingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + +// ParseListRecordersResponse parses an HTTP response from a ListRecordersWithResponse call +func ParseListRecordersResponse(rsp *http.Response) (*ListRecordersResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListRecordersResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []RecorderInfo if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -6297,15 +8192,15 @@ func ParseProcessStdinResponse(rsp *http.Response) (*ProcessStdinResponse, error return response, nil } -// ParseProcessStdoutStreamResponse parses an HTTP response from a ProcessStdoutStreamWithResponse call -func ParseProcessStdoutStreamResponse(rsp *http.Response) (*ProcessStdoutStreamResponse, error) { +// ParseStartRecordingResponse parses an HTTP response from a StartRecordingWithResponse call +func ParseStartRecordingResponse(rsp *http.Response) (*StartRecordingResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ProcessStdoutStreamResponse{ + response := &StartRecordingResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -6318,12 +8213,12 @@ func ParseProcessStdoutStreamResponse(rsp *http.Response) (*ProcessStdoutStreamR } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON409 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -6337,15 +8232,15 @@ func ParseProcessStdoutStreamResponse(rsp *http.Response) (*ProcessStdoutStreamR return response, nil } -// ParseDeleteRecordingResponse parses an HTTP response from a DeleteRecordingWithResponse call -func ParseDeleteRecordingResponse(rsp *http.Response) (*DeleteRecordingResponse, error) { +// ParseStopRecordingResponse parses an HTTP response from a StopRecordingWithResponse call +func ParseStopRecordingResponse(rsp *http.Response) (*StopRecordingResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DeleteRecordingResponse{ + response := &StopRecordingResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -6358,13 +8253,6 @@ func ParseDeleteRecordingResponse(rsp *http.Response) (*DeleteRecordingResponse, } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON404 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6377,33 +8265,26 @@ func ParseDeleteRecordingResponse(rsp *http.Response) (*DeleteRecordingResponse, return response, nil } -// ParseDownloadRecordingResponse parses an HTTP response from a DownloadRecordingWithResponse call -func ParseDownloadRecordingResponse(rsp *http.Response) (*DownloadRecordingResponse, error) { +// ParseListStreamsResponse parses an HTTP response from a ListStreamsWithResponse call +func ParseListStreamsResponse(rsp *http.Response) (*ListStreamsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &DownloadRecordingResponse{ + response := &ListStreamsResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: - var dest BadRequestError - if err := json.Unmarshal(bodyBytes, &dest); err != nil { - return nil, err - } - response.JSON400 = &dest - - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: - var dest NotFoundError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []StreamInfo if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON404 = &dest + response.JSON200 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -6417,26 +8298,40 @@ func ParseDownloadRecordingResponse(rsp *http.Response) (*DownloadRecordingRespo return response, nil } -// ParseListRecordersResponse parses an HTTP response from a ListRecordersWithResponse call -func ParseListRecordersResponse(rsp *http.Response) (*ListRecordersResponse, error) { +// ParseStartStreamResponse parses an HTTP response from a StartStreamWithResponse call +func ParseStartStreamResponse(rsp *http.Response) (*StartStreamResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &ListRecordersResponse{ + response := &StartStreamResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: - var dest []RecorderInfo + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest StreamInfo if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON200 = &dest + response.JSON201 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest BadRequestError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -6450,15 +8345,15 @@ func ParseListRecordersResponse(rsp *http.Response) (*ListRecordersResponse, err return response, nil } -// ParseStartRecordingResponse parses an HTTP response from a StartRecordingWithResponse call -func ParseStartRecordingResponse(rsp *http.Response) (*StartRecordingResponse, error) { +// ParseStopStreamResponse parses an HTTP response from a StopStreamWithResponse call +func ParseStopStreamResponse(rsp *http.Response) (*StopStreamResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StartRecordingResponse{ + response := &StopStreamResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -6471,12 +8366,12 @@ func ParseStartRecordingResponse(rsp *http.Response) (*StartRecordingResponse, e } response.JSON400 = &dest - case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: - var dest ConflictError + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError if err := json.Unmarshal(bodyBytes, &dest); err != nil { return nil, err } - response.JSON409 = &dest + response.JSON404 = &dest case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError @@ -6490,20 +8385,27 @@ func ParseStartRecordingResponse(rsp *http.Response) (*StartRecordingResponse, e return response, nil } -// ParseStopRecordingResponse parses an HTTP response from a StopRecordingWithResponse call -func ParseStopRecordingResponse(rsp *http.Response) (*StopRecordingResponse, error) { +// ParseStreamWebrtcOfferResponse parses an HTTP response from a StreamWebrtcOfferWithResponse call +func ParseStreamWebrtcOfferResponse(rsp *http.Response) (*StreamWebrtcOfferResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &StopRecordingResponse{ + response := &StreamWebrtcOfferResponse{ Body: bodyBytes, HTTPResponse: rsp, } switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest StreamWebRTCAnswer + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: var dest BadRequestError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6511,6 +8413,20 @@ func ParseStopRecordingResponse(rsp *http.Response) (*StopRecordingResponse, err } response.JSON400 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFoundError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest ConflictError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: var dest InternalError if err := json.Unmarshal(bodyBytes, &dest); err != nil { @@ -6603,6 +8519,30 @@ type ServerInterface interface { // Write or create a file // (PUT /fs/write_file) WriteFile(w http.ResponseWriter, r *http.Request, params WriteFileParams) + // Configure virtual video and audio inputs + // (POST /input/devices/virtual/configure) + ConfigureVirtualInputs(w http.ResponseWriter, r *http.Request) + // Render a fullscreen HTML page with the virtual video feed + // (GET /input/devices/virtual/feed) + GetVirtualInputFeed(w http.ResponseWriter, r *http.Request, params GetVirtualInputFeedParams) + // Discover the websocket URL for the virtual video feed mirror + // (GET /input/devices/virtual/feed/socket/info) + GetVirtualInputFeedSocketInfo(w http.ResponseWriter, r *http.Request) + // Pause virtual inputs with silence and black frames + // (POST /input/devices/virtual/pause) + PauseVirtualInputs(w http.ResponseWriter, r *http.Request) + // Resume previously configured virtual inputs + // (POST /input/devices/virtual/resume) + ResumeVirtualInputs(w http.ResponseWriter, r *http.Request) + // Get the current virtual input status + // (GET /input/devices/virtual/status) + GetVirtualInputsStatus(w http.ResponseWriter, r *http.Request) + // Stop virtual input pipelines and release resources + // (POST /input/devices/virtual/stop) + StopVirtualInputs(w http.ResponseWriter, r *http.Request) + // Negotiate a WebRTC ingest session for virtual inputs + // (POST /input/devices/virtual/webrtc/offer) + NegotiateVirtualInputsWebrtc(w http.ResponseWriter, r *http.Request) // Stream logs over SSE // (GET /logs/stream) LogsStream(w http.ResponseWriter, r *http.Request, params LogsStreamParams) @@ -6642,6 +8582,18 @@ type ServerInterface interface { // Stop the recording // (POST /recording/stop) StopRecording(w http.ResponseWriter, r *http.Request) + // List active streams + // (GET /stream/list) + ListStreams(w http.ResponseWriter, r *http.Request) + // Start live streaming to an internal RTMP(S) server or a remote RTMP(S) endpoint. + // (POST /stream/start) + StartStream(w http.ResponseWriter, r *http.Request) + // Stop a live stream + // (POST /stream/stop) + StopStream(w http.ResponseWriter, r *http.Request) + // Exchange SDP for a WebRTC livestream + // (POST /stream/webrtc/offer) + StreamWebrtcOffer(w http.ResponseWriter, r *http.Request) } // Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. @@ -6804,6 +8756,54 @@ func (_ Unimplemented) WriteFile(w http.ResponseWriter, r *http.Request, params w.WriteHeader(http.StatusNotImplemented) } +// Configure virtual video and audio inputs +// (POST /input/devices/virtual/configure) +func (_ Unimplemented) ConfigureVirtualInputs(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Render a fullscreen HTML page with the virtual video feed +// (GET /input/devices/virtual/feed) +func (_ Unimplemented) GetVirtualInputFeed(w http.ResponseWriter, r *http.Request, params GetVirtualInputFeedParams) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Discover the websocket URL for the virtual video feed mirror +// (GET /input/devices/virtual/feed/socket/info) +func (_ Unimplemented) GetVirtualInputFeedSocketInfo(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Pause virtual inputs with silence and black frames +// (POST /input/devices/virtual/pause) +func (_ Unimplemented) PauseVirtualInputs(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Resume previously configured virtual inputs +// (POST /input/devices/virtual/resume) +func (_ Unimplemented) ResumeVirtualInputs(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get the current virtual input status +// (GET /input/devices/virtual/status) +func (_ Unimplemented) GetVirtualInputsStatus(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Stop virtual input pipelines and release resources +// (POST /input/devices/virtual/stop) +func (_ Unimplemented) StopVirtualInputs(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Negotiate a WebRTC ingest session for virtual inputs +// (POST /input/devices/virtual/webrtc/offer) +func (_ Unimplemented) NegotiateVirtualInputsWebrtc(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Stream logs over SSE // (GET /logs/stream) func (_ Unimplemented) LogsStream(w http.ResponseWriter, r *http.Request, params LogsStreamParams) { @@ -6882,6 +8882,30 @@ func (_ Unimplemented) StopRecording(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotImplemented) } +// List active streams +// (GET /stream/list) +func (_ Unimplemented) ListStreams(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Start live streaming to an internal RTMP(S) server or a remote RTMP(S) endpoint. +// (POST /stream/start) +func (_ Unimplemented) StartStream(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Stop a live stream +// (POST /stream/stop) +func (_ Unimplemented) StopStream(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Exchange SDP for a WebRTC livestream +// (POST /stream/webrtc/offer) +func (_ Unimplemented) StreamWebrtcOffer(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // ServerInterfaceWrapper converts contexts to parameters. type ServerInterfaceWrapper struct { Handler ServerInterface @@ -7323,17 +9347,150 @@ func (siw *ServerInterfaceWrapper) StreamFsEvents(w http.ResponseWriter, r *http var err error - // ------------- Path parameter "watch_id" ------------- - var watchId string + // ------------- Path parameter "watch_id" ------------- + var watchId string + + err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StreamFsEvents(w, r, watchId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// WriteFile operation middleware +func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params WriteFileParams + + // ------------- Required query parameter "path" ------------- + + if paramValue := r.URL.Query().Get("path"); paramValue != "" { + + } else { + siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) + return + } + + err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) + return + } + + // ------------- Optional query parameter "mode" ------------- + + err = runtime.BindQueryParameter("form", true, false, "mode", r.URL.Query(), ¶ms.Mode) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "mode", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.WriteFile(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// ConfigureVirtualInputs operation middleware +func (siw *ServerInterfaceWrapper) ConfigureVirtualInputs(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ConfigureVirtualInputs(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetVirtualInputFeed operation middleware +func (siw *ServerInterfaceWrapper) GetVirtualInputFeed(w http.ResponseWriter, r *http.Request) { + + var err error + + // Parameter object where we will unmarshal all parameters from the context + var params GetVirtualInputFeedParams + + // ------------- Optional query parameter "fit" ------------- + + err = runtime.BindQueryParameter("form", true, false, "fit", r.URL.Query(), ¶ms.Fit) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "fit", Err: err}) + return + } + + // ------------- Optional query parameter "source" ------------- + + err = runtime.BindQueryParameter("form", true, false, "source", r.URL.Query(), ¶ms.Source) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "source", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetVirtualInputFeed(w, r, params) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// GetVirtualInputFeedSocketInfo operation middleware +func (siw *ServerInterfaceWrapper) GetVirtualInputFeedSocketInfo(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetVirtualInputFeedSocketInfo(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PauseVirtualInputs operation middleware +func (siw *ServerInterfaceWrapper) PauseVirtualInputs(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PauseVirtualInputs(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} - err = runtime.BindStyledParameterWithOptions("simple", "watch_id", chi.URLParam(r, "watch_id"), &watchId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "watch_id", Err: err}) - return - } +// ResumeVirtualInputs operation middleware +func (siw *ServerInterfaceWrapper) ResumeVirtualInputs(w http.ResponseWriter, r *http.Request) { handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.StreamFsEvents(w, r, watchId) + siw.Handler.ResumeVirtualInputs(w, r) })) for _, middleware := range siw.HandlerMiddlewares { @@ -7343,39 +9500,39 @@ func (siw *ServerInterfaceWrapper) StreamFsEvents(w http.ResponseWriter, r *http handler.ServeHTTP(w, r) } -// WriteFile operation middleware -func (siw *ServerInterfaceWrapper) WriteFile(w http.ResponseWriter, r *http.Request) { +// GetVirtualInputsStatus operation middleware +func (siw *ServerInterfaceWrapper) GetVirtualInputsStatus(w http.ResponseWriter, r *http.Request) { - var err error + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetVirtualInputsStatus(w, r) + })) - // Parameter object where we will unmarshal all parameters from the context - var params WriteFileParams + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } - // ------------- Required query parameter "path" ------------- + handler.ServeHTTP(w, r) +} - if paramValue := r.URL.Query().Get("path"); paramValue != "" { +// StopVirtualInputs operation middleware +func (siw *ServerInterfaceWrapper) StopVirtualInputs(w http.ResponseWriter, r *http.Request) { - } else { - siw.ErrorHandlerFunc(w, r, &RequiredParamError{ParamName: "path"}) - return - } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StopVirtualInputs(w, r) + })) - err = runtime.BindQueryParameter("form", true, true, "path", r.URL.Query(), ¶ms.Path) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "path", Err: err}) - return + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) } - // ------------- Optional query parameter "mode" ------------- + handler.ServeHTTP(w, r) +} - err = runtime.BindQueryParameter("form", true, false, "mode", r.URL.Query(), ¶ms.Mode) - if err != nil { - siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "mode", Err: err}) - return - } +// NegotiateVirtualInputsWebrtc operation middleware +func (siw *ServerInterfaceWrapper) NegotiateVirtualInputsWebrtc(w http.ResponseWriter, r *http.Request) { handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.WriteFile(w, r, params) + siw.Handler.NegotiateVirtualInputsWebrtc(w, r) })) for _, middleware := range siw.HandlerMiddlewares { @@ -7668,6 +9825,62 @@ func (siw *ServerInterfaceWrapper) StopRecording(w http.ResponseWriter, r *http. handler.ServeHTTP(w, r) } +// ListStreams operation middleware +func (siw *ServerInterfaceWrapper) ListStreams(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListStreams(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// StartStream operation middleware +func (siw *ServerInterfaceWrapper) StartStream(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StartStream(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// StopStream operation middleware +func (siw *ServerInterfaceWrapper) StopStream(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StopStream(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// StreamWebrtcOffer operation middleware +func (siw *ServerInterfaceWrapper) StreamWebrtcOffer(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StreamWebrtcOffer(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + type UnescapedCookieParamError struct { ParamName string Err error @@ -7859,6 +10072,30 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Put(options.BaseURL+"/fs/write_file", wrapper.WriteFile) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/input/devices/virtual/configure", wrapper.ConfigureVirtualInputs) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/input/devices/virtual/feed", wrapper.GetVirtualInputFeed) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/input/devices/virtual/feed/socket/info", wrapper.GetVirtualInputFeedSocketInfo) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/input/devices/virtual/pause", wrapper.PauseVirtualInputs) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/input/devices/virtual/resume", wrapper.ResumeVirtualInputs) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/input/devices/virtual/status", wrapper.GetVirtualInputsStatus) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/input/devices/virtual/stop", wrapper.StopVirtualInputs) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/input/devices/virtual/webrtc/offer", wrapper.NegotiateVirtualInputsWebrtc) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/logs/stream", wrapper.LogsStream) }) @@ -7898,272 +10135,561 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/recording/stop", wrapper.StopRecording) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/stream/list", wrapper.ListStreams) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/stream/start", wrapper.StartStream) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/stream/stop", wrapper.StopStream) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/stream/webrtc/offer", wrapper.StreamWebrtcOffer) + }) + + return r +} + +type BadRequestErrorJSONResponse Error + +type ConflictErrorJSONResponse Error + +type InternalErrorJSONResponse Error + +type NotFoundErrorJSONResponse Error + +type PatchChromiumFlagsRequestObject struct { + Body *PatchChromiumFlagsJSONRequestBody +} + +type PatchChromiumFlagsResponseObject interface { + VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error +} + +type PatchChromiumFlags200Response struct { +} + +func (response PatchChromiumFlags200Response) VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type PatchChromiumFlags400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response PatchChromiumFlags400JSONResponse) VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type PatchChromiumFlags500JSONResponse struct{ InternalErrorJSONResponse } + +func (response PatchChromiumFlags500JSONResponse) VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type UploadExtensionsAndRestartRequestObject struct { + Body *multipart.Reader +} + +type UploadExtensionsAndRestartResponseObject interface { + VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error +} + +type UploadExtensionsAndRestart201Response struct { +} + +func (response UploadExtensionsAndRestart201Response) VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error { + w.WriteHeader(201) + return nil +} + +type UploadExtensionsAndRestart400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response UploadExtensionsAndRestart400JSONResponse) VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UploadExtensionsAndRestart500JSONResponse struct{ InternalErrorJSONResponse } + +func (response UploadExtensionsAndRestart500JSONResponse) VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ClickMouseRequestObject struct { + Body *ClickMouseJSONRequestBody +} + +type ClickMouseResponseObject interface { + VisitClickMouseResponse(w http.ResponseWriter) error +} + +type ClickMouse200Response struct { +} + +func (response ClickMouse200Response) VisitClickMouseResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type ClickMouse400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response ClickMouse400JSONResponse) VisitClickMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type ClickMouse500JSONResponse struct{ InternalErrorJSONResponse } + +func (response ClickMouse500JSONResponse) VisitClickMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursorRequestObject struct { + Body *SetCursorJSONRequestBody +} + +type SetCursorResponseObject interface { + VisitSetCursorResponse(w http.ResponseWriter) error +} + +type SetCursor200JSONResponse OkResponse + +func (response SetCursor200JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursor400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response SetCursor400JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type SetCursor500JSONResponse struct{ InternalErrorJSONResponse } + +func (response SetCursor500JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) - return r + return json.NewEncoder(w).Encode(response) } -type BadRequestErrorJSONResponse Error +type DragMouseRequestObject struct { + Body *DragMouseJSONRequestBody +} -type ConflictErrorJSONResponse Error +type DragMouseResponseObject interface { + VisitDragMouseResponse(w http.ResponseWriter) error +} -type InternalErrorJSONResponse Error +type DragMouse200Response struct { +} -type NotFoundErrorJSONResponse Error +func (response DragMouse200Response) VisitDragMouseResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} -type PatchChromiumFlagsRequestObject struct { - Body *PatchChromiumFlagsJSONRequestBody +type DragMouse400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response DragMouse400JSONResponse) VisitDragMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) } -type PatchChromiumFlagsResponseObject interface { - VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error +type DragMouse500JSONResponse struct{ InternalErrorJSONResponse } + +func (response DragMouse500JSONResponse) VisitDragMouseResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) } -type PatchChromiumFlags200Response struct { +type MoveMouseRequestObject struct { + Body *MoveMouseJSONRequestBody } -func (response PatchChromiumFlags200Response) VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error { +type MoveMouseResponseObject interface { + VisitMoveMouseResponse(w http.ResponseWriter) error +} + +type MoveMouse200Response struct { +} + +func (response MoveMouse200Response) VisitMoveMouseResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type PatchChromiumFlags400JSONResponse struct{ BadRequestErrorJSONResponse } +type MoveMouse400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response PatchChromiumFlags400JSONResponse) VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error { +func (response MoveMouse400JSONResponse) VisitMoveMouseResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type PatchChromiumFlags500JSONResponse struct{ InternalErrorJSONResponse } +type MoveMouse500JSONResponse struct{ InternalErrorJSONResponse } -func (response PatchChromiumFlags500JSONResponse) VisitPatchChromiumFlagsResponse(w http.ResponseWriter) error { +func (response MoveMouse500JSONResponse) VisitMoveMouseResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type UploadExtensionsAndRestartRequestObject struct { - Body *multipart.Reader +type PressKeyRequestObject struct { + Body *PressKeyJSONRequestBody } -type UploadExtensionsAndRestartResponseObject interface { - VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error +type PressKeyResponseObject interface { + VisitPressKeyResponse(w http.ResponseWriter) error } -type UploadExtensionsAndRestart201Response struct { +type PressKey200Response struct { } -func (response UploadExtensionsAndRestart201Response) VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error { - w.WriteHeader(201) +func (response PressKey200Response) VisitPressKeyResponse(w http.ResponseWriter) error { + w.WriteHeader(200) return nil } -type UploadExtensionsAndRestart400JSONResponse struct{ BadRequestErrorJSONResponse } +type PressKey400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response UploadExtensionsAndRestart400JSONResponse) VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error { +func (response PressKey400JSONResponse) VisitPressKeyResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type UploadExtensionsAndRestart500JSONResponse struct{ InternalErrorJSONResponse } +type PressKey500JSONResponse struct{ InternalErrorJSONResponse } -func (response UploadExtensionsAndRestart500JSONResponse) VisitUploadExtensionsAndRestartResponse(w http.ResponseWriter) error { +func (response PressKey500JSONResponse) VisitPressKeyResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type ClickMouseRequestObject struct { - Body *ClickMouseJSONRequestBody +type TakeScreenshotRequestObject struct { + Body *TakeScreenshotJSONRequestBody } -type ClickMouseResponseObject interface { - VisitClickMouseResponse(w http.ResponseWriter) error +type TakeScreenshotResponseObject interface { + VisitTakeScreenshotResponse(w http.ResponseWriter) error } -type ClickMouse200Response struct { +type TakeScreenshot200ImagepngResponse struct { + Body io.Reader + ContentLength int64 } -func (response ClickMouse200Response) VisitClickMouseResponse(w http.ResponseWriter) error { +func (response TakeScreenshot200ImagepngResponse) VisitTakeScreenshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "image/png") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err +} + +type TakeScreenshot400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response TakeScreenshot400JSONResponse) VisitTakeScreenshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type TakeScreenshot500JSONResponse struct{ InternalErrorJSONResponse } + +func (response TakeScreenshot500JSONResponse) VisitTakeScreenshotResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ScrollRequestObject struct { + Body *ScrollJSONRequestBody +} + +type ScrollResponseObject interface { + VisitScrollResponse(w http.ResponseWriter) error +} + +type Scroll200Response struct { +} + +func (response Scroll200Response) VisitScrollResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type ClickMouse400JSONResponse struct{ BadRequestErrorJSONResponse } +type Scroll400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response ClickMouse400JSONResponse) VisitClickMouseResponse(w http.ResponseWriter) error { +func (response Scroll400JSONResponse) VisitScrollResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type ClickMouse500JSONResponse struct{ InternalErrorJSONResponse } +type Scroll500JSONResponse struct{ InternalErrorJSONResponse } -func (response ClickMouse500JSONResponse) VisitClickMouseResponse(w http.ResponseWriter) error { +func (response Scroll500JSONResponse) VisitScrollResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type SetCursorRequestObject struct { - Body *SetCursorJSONRequestBody +type TypeTextRequestObject struct { + Body *TypeTextJSONRequestBody } -type SetCursorResponseObject interface { - VisitSetCursorResponse(w http.ResponseWriter) error +type TypeTextResponseObject interface { + VisitTypeTextResponse(w http.ResponseWriter) error } -type SetCursor200JSONResponse OkResponse +type TypeText200Response struct { +} -func (response SetCursor200JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { +func (response TypeText200Response) VisitTypeTextResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type TypeText400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response TypeText400JSONResponse) VisitTypeTextResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type TypeText500JSONResponse struct{ InternalErrorJSONResponse } + +func (response TypeText500JSONResponse) VisitTypeTextResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type PatchDisplayRequestObject struct { + Body *PatchDisplayJSONRequestBody +} + +type PatchDisplayResponseObject interface { + VisitPatchDisplayResponse(w http.ResponseWriter) error +} + +type PatchDisplay200JSONResponse DisplayConfig + +func (response PatchDisplay200JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type SetCursor400JSONResponse struct{ BadRequestErrorJSONResponse } +type PatchDisplay400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response SetCursor400JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { +func (response PatchDisplay400JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type SetCursor500JSONResponse struct{ InternalErrorJSONResponse } +type PatchDisplay409JSONResponse struct{ ConflictErrorJSONResponse } -func (response SetCursor500JSONResponse) VisitSetCursorResponse(w http.ResponseWriter) error { +func (response PatchDisplay409JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type PatchDisplay500JSONResponse struct{ InternalErrorJSONResponse } + +func (response PatchDisplay500JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type DragMouseRequestObject struct { - Body *DragMouseJSONRequestBody +type CreateDirectoryRequestObject struct { + Body *CreateDirectoryJSONRequestBody } -type DragMouseResponseObject interface { - VisitDragMouseResponse(w http.ResponseWriter) error +type CreateDirectoryResponseObject interface { + VisitCreateDirectoryResponse(w http.ResponseWriter) error } -type DragMouse200Response struct { +type CreateDirectory201Response struct { } -func (response DragMouse200Response) VisitDragMouseResponse(w http.ResponseWriter) error { - w.WriteHeader(200) +func (response CreateDirectory201Response) VisitCreateDirectoryResponse(w http.ResponseWriter) error { + w.WriteHeader(201) return nil } -type DragMouse400JSONResponse struct{ BadRequestErrorJSONResponse } +type CreateDirectory400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response DragMouse400JSONResponse) VisitDragMouseResponse(w http.ResponseWriter) error { +func (response CreateDirectory400JSONResponse) VisitCreateDirectoryResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type DragMouse500JSONResponse struct{ InternalErrorJSONResponse } +type CreateDirectory500JSONResponse struct{ InternalErrorJSONResponse } -func (response DragMouse500JSONResponse) VisitDragMouseResponse(w http.ResponseWriter) error { +func (response CreateDirectory500JSONResponse) VisitCreateDirectoryResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type MoveMouseRequestObject struct { - Body *MoveMouseJSONRequestBody +type DeleteDirectoryRequestObject struct { + Body *DeleteDirectoryJSONRequestBody } -type MoveMouseResponseObject interface { - VisitMoveMouseResponse(w http.ResponseWriter) error +type DeleteDirectoryResponseObject interface { + VisitDeleteDirectoryResponse(w http.ResponseWriter) error } -type MoveMouse200Response struct { +type DeleteDirectory200Response struct { } -func (response MoveMouse200Response) VisitMoveMouseResponse(w http.ResponseWriter) error { +func (response DeleteDirectory200Response) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type MoveMouse400JSONResponse struct{ BadRequestErrorJSONResponse } +type DeleteDirectory400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response MoveMouse400JSONResponse) VisitMoveMouseResponse(w http.ResponseWriter) error { +func (response DeleteDirectory400JSONResponse) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type MoveMouse500JSONResponse struct{ InternalErrorJSONResponse } +type DeleteDirectory404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response DeleteDirectory404JSONResponse) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteDirectory500JSONResponse struct{ InternalErrorJSONResponse } -func (response MoveMouse500JSONResponse) VisitMoveMouseResponse(w http.ResponseWriter) error { +func (response DeleteDirectory500JSONResponse) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type PressKeyRequestObject struct { - Body *PressKeyJSONRequestBody +type DeleteFileRequestObject struct { + Body *DeleteFileJSONRequestBody } -type PressKeyResponseObject interface { - VisitPressKeyResponse(w http.ResponseWriter) error +type DeleteFileResponseObject interface { + VisitDeleteFileResponse(w http.ResponseWriter) error } -type PressKey200Response struct { +type DeleteFile200Response struct { } -func (response PressKey200Response) VisitPressKeyResponse(w http.ResponseWriter) error { +func (response DeleteFile200Response) VisitDeleteFileResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type PressKey400JSONResponse struct{ BadRequestErrorJSONResponse } +type DeleteFile400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response PressKey400JSONResponse) VisitPressKeyResponse(w http.ResponseWriter) error { +func (response DeleteFile400JSONResponse) VisitDeleteFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type PressKey500JSONResponse struct{ InternalErrorJSONResponse } +type DeleteFile404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response PressKey500JSONResponse) VisitPressKeyResponse(w http.ResponseWriter) error { +func (response DeleteFile404JSONResponse) VisitDeleteFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type DeleteFile500JSONResponse struct{ InternalErrorJSONResponse } + +func (response DeleteFile500JSONResponse) VisitDeleteFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type TakeScreenshotRequestObject struct { - Body *TakeScreenshotJSONRequestBody +type DownloadDirZipRequestObject struct { + Params DownloadDirZipParams } -type TakeScreenshotResponseObject interface { - VisitTakeScreenshotResponse(w http.ResponseWriter) error +type DownloadDirZipResponseObject interface { + VisitDownloadDirZipResponse(w http.ResponseWriter) error } -type TakeScreenshot200ImagepngResponse struct { +type DownloadDirZip200ApplicationzipResponse struct { Body io.Reader ContentLength int64 } -func (response TakeScreenshot200ImagepngResponse) VisitTakeScreenshotResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "image/png") +func (response DownloadDirZip200ApplicationzipResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/zip") if response.ContentLength != 0 { w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) } @@ -8176,834 +10702,824 @@ func (response TakeScreenshot200ImagepngResponse) VisitTakeScreenshotResponse(w return err } -type TakeScreenshot400JSONResponse struct{ BadRequestErrorJSONResponse } +type DownloadDirZip400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response TakeScreenshot400JSONResponse) VisitTakeScreenshotResponse(w http.ResponseWriter) error { +func (response DownloadDirZip400JSONResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type TakeScreenshot500JSONResponse struct{ InternalErrorJSONResponse } +type DownloadDirZip404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response TakeScreenshot500JSONResponse) VisitTakeScreenshotResponse(w http.ResponseWriter) error { +func (response DownloadDirZip404JSONResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type ScrollRequestObject struct { - Body *ScrollJSONRequestBody -} +type DownloadDirZip500JSONResponse struct{ InternalErrorJSONResponse } -type ScrollResponseObject interface { - VisitScrollResponse(w http.ResponseWriter) error +func (response DownloadDirZip500JSONResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) } -type Scroll200Response struct { +type FileInfoRequestObject struct { + Params FileInfoParams } -func (response Scroll200Response) VisitScrollResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil +type FileInfoResponseObject interface { + VisitFileInfoResponse(w http.ResponseWriter) error } -type Scroll400JSONResponse struct{ BadRequestErrorJSONResponse } +type FileInfo200JSONResponse FileInfo -func (response Scroll400JSONResponse) VisitScrollResponse(w http.ResponseWriter) error { +func (response FileInfo200JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type Scroll500JSONResponse struct{ InternalErrorJSONResponse } +type FileInfo400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response Scroll500JSONResponse) VisitScrollResponse(w http.ResponseWriter) error { +func (response FileInfo400JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type TypeTextRequestObject struct { - Body *TypeTextJSONRequestBody -} - -type TypeTextResponseObject interface { - VisitTypeTextResponse(w http.ResponseWriter) error -} - -type TypeText200Response struct { -} - -func (response TypeText200Response) VisitTypeTextResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil -} - -type TypeText400JSONResponse struct{ BadRequestErrorJSONResponse } +type FileInfo404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response TypeText400JSONResponse) VisitTypeTextResponse(w http.ResponseWriter) error { +func (response FileInfo404JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type TypeText500JSONResponse struct{ InternalErrorJSONResponse } +type FileInfo500JSONResponse struct{ InternalErrorJSONResponse } -func (response TypeText500JSONResponse) VisitTypeTextResponse(w http.ResponseWriter) error { +func (response FileInfo500JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type PatchDisplayRequestObject struct { - Body *PatchDisplayJSONRequestBody +type ListFilesRequestObject struct { + Params ListFilesParams } -type PatchDisplayResponseObject interface { - VisitPatchDisplayResponse(w http.ResponseWriter) error +type ListFilesResponseObject interface { + VisitListFilesResponse(w http.ResponseWriter) error } -type PatchDisplay200JSONResponse DisplayConfig +type ListFiles200JSONResponse ListFiles -func (response PatchDisplay200JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { +func (response ListFiles200JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type PatchDisplay400JSONResponse struct{ BadRequestErrorJSONResponse } +type ListFiles400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response PatchDisplay400JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { +func (response ListFiles400JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type PatchDisplay409JSONResponse struct{ ConflictErrorJSONResponse } +type ListFiles404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response PatchDisplay409JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { +func (response ListFiles404JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(409) + w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type PatchDisplay500JSONResponse struct{ InternalErrorJSONResponse } +type ListFiles500JSONResponse struct{ InternalErrorJSONResponse } -func (response PatchDisplay500JSONResponse) VisitPatchDisplayResponse(w http.ResponseWriter) error { +func (response ListFiles500JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type CreateDirectoryRequestObject struct { - Body *CreateDirectoryJSONRequestBody +type MovePathRequestObject struct { + Body *MovePathJSONRequestBody } -type CreateDirectoryResponseObject interface { - VisitCreateDirectoryResponse(w http.ResponseWriter) error +type MovePathResponseObject interface { + VisitMovePathResponse(w http.ResponseWriter) error } -type CreateDirectory201Response struct { +type MovePath200Response struct { } -func (response CreateDirectory201Response) VisitCreateDirectoryResponse(w http.ResponseWriter) error { - w.WriteHeader(201) +func (response MovePath200Response) VisitMovePathResponse(w http.ResponseWriter) error { + w.WriteHeader(200) return nil } -type CreateDirectory400JSONResponse struct{ BadRequestErrorJSONResponse } +type MovePath400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response CreateDirectory400JSONResponse) VisitCreateDirectoryResponse(w http.ResponseWriter) error { +func (response MovePath400JSONResponse) VisitMovePathResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type CreateDirectory500JSONResponse struct{ InternalErrorJSONResponse } +type MovePath404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response CreateDirectory500JSONResponse) VisitCreateDirectoryResponse(w http.ResponseWriter) error { +func (response MovePath404JSONResponse) VisitMovePathResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type MovePath500JSONResponse struct{ InternalErrorJSONResponse } + +func (response MovePath500JSONResponse) VisitMovePathResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type DeleteDirectoryRequestObject struct { - Body *DeleteDirectoryJSONRequestBody +type ReadFileRequestObject struct { + Params ReadFileParams } -type DeleteDirectoryResponseObject interface { - VisitDeleteDirectoryResponse(w http.ResponseWriter) error +type ReadFileResponseObject interface { + VisitReadFileResponse(w http.ResponseWriter) error } -type DeleteDirectory200Response struct { +type ReadFile200ApplicationoctetStreamResponse struct { + Body io.Reader + ContentLength int64 } -func (response DeleteDirectory200Response) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { +func (response ReadFile200ApplicationoctetStreamResponse) VisitReadFileResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/octet-stream") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } w.WriteHeader(200) - return nil + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err } -type DeleteDirectory400JSONResponse struct{ BadRequestErrorJSONResponse } +type ReadFile400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response DeleteDirectory400JSONResponse) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { +func (response ReadFile400JSONResponse) VisitReadFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type DeleteDirectory404JSONResponse struct{ NotFoundErrorJSONResponse } +type ReadFile404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response DeleteDirectory404JSONResponse) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { +func (response ReadFile404JSONResponse) VisitReadFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type DeleteDirectory500JSONResponse struct{ InternalErrorJSONResponse } +type ReadFile500JSONResponse struct{ InternalErrorJSONResponse } -func (response DeleteDirectory500JSONResponse) VisitDeleteDirectoryResponse(w http.ResponseWriter) error { +func (response ReadFile500JSONResponse) VisitReadFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type DeleteFileRequestObject struct { - Body *DeleteFileJSONRequestBody +type SetFilePermissionsRequestObject struct { + Body *SetFilePermissionsJSONRequestBody } -type DeleteFileResponseObject interface { - VisitDeleteFileResponse(w http.ResponseWriter) error +type SetFilePermissionsResponseObject interface { + VisitSetFilePermissionsResponse(w http.ResponseWriter) error } -type DeleteFile200Response struct { +type SetFilePermissions200Response struct { } -func (response DeleteFile200Response) VisitDeleteFileResponse(w http.ResponseWriter) error { +func (response SetFilePermissions200Response) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { w.WriteHeader(200) return nil } -type DeleteFile400JSONResponse struct{ BadRequestErrorJSONResponse } +type SetFilePermissions400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response DeleteFile400JSONResponse) VisitDeleteFileResponse(w http.ResponseWriter) error { +func (response SetFilePermissions400JSONResponse) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type DeleteFile404JSONResponse struct{ NotFoundErrorJSONResponse } +type SetFilePermissions404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response DeleteFile404JSONResponse) VisitDeleteFileResponse(w http.ResponseWriter) error { +func (response SetFilePermissions404JSONResponse) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type DeleteFile500JSONResponse struct{ InternalErrorJSONResponse } +type SetFilePermissions500JSONResponse struct{ InternalErrorJSONResponse } -func (response DeleteFile500JSONResponse) VisitDeleteFileResponse(w http.ResponseWriter) error { +func (response SetFilePermissions500JSONResponse) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type DownloadDirZipRequestObject struct { - Params DownloadDirZipParams +type UploadFilesRequestObject struct { + Body *multipart.Reader } -type DownloadDirZipResponseObject interface { - VisitDownloadDirZipResponse(w http.ResponseWriter) error +type UploadFilesResponseObject interface { + VisitUploadFilesResponse(w http.ResponseWriter) error } -type DownloadDirZip200ApplicationzipResponse struct { - Body io.Reader - ContentLength int64 +type UploadFiles201Response struct { } - -func (response DownloadDirZip200ApplicationzipResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/zip") - if response.ContentLength != 0 { - w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) - } - w.WriteHeader(200) - - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - _, err := io.Copy(w, response.Body) - return err + +func (response UploadFiles201Response) VisitUploadFilesResponse(w http.ResponseWriter) error { + w.WriteHeader(201) + return nil } -type DownloadDirZip400JSONResponse struct{ BadRequestErrorJSONResponse } +type UploadFiles400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response DownloadDirZip400JSONResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { +func (response UploadFiles400JSONResponse) VisitUploadFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type DownloadDirZip404JSONResponse struct{ NotFoundErrorJSONResponse } +type UploadFiles404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response DownloadDirZip404JSONResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { +func (response UploadFiles404JSONResponse) VisitUploadFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type DownloadDirZip500JSONResponse struct{ InternalErrorJSONResponse } +type UploadFiles500JSONResponse struct{ InternalErrorJSONResponse } -func (response DownloadDirZip500JSONResponse) VisitDownloadDirZipResponse(w http.ResponseWriter) error { +func (response UploadFiles500JSONResponse) VisitUploadFilesResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type FileInfoRequestObject struct { - Params FileInfoParams +type UploadZipRequestObject struct { + Body *multipart.Reader } -type FileInfoResponseObject interface { - VisitFileInfoResponse(w http.ResponseWriter) error +type UploadZipResponseObject interface { + VisitUploadZipResponse(w http.ResponseWriter) error } -type FileInfo200JSONResponse FileInfo - -func (response FileInfo200JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) +type UploadZip201Response struct { +} - return json.NewEncoder(w).Encode(response) +func (response UploadZip201Response) VisitUploadZipResponse(w http.ResponseWriter) error { + w.WriteHeader(201) + return nil } -type FileInfo400JSONResponse struct{ BadRequestErrorJSONResponse } +type UploadZip400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response FileInfo400JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { +func (response UploadZip400JSONResponse) VisitUploadZipResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type FileInfo404JSONResponse struct{ NotFoundErrorJSONResponse } +type UploadZip404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response FileInfo404JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { +func (response UploadZip404JSONResponse) VisitUploadZipResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type FileInfo500JSONResponse struct{ InternalErrorJSONResponse } +type UploadZip500JSONResponse struct{ InternalErrorJSONResponse } -func (response FileInfo500JSONResponse) VisitFileInfoResponse(w http.ResponseWriter) error { +func (response UploadZip500JSONResponse) VisitUploadZipResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type ListFilesRequestObject struct { - Params ListFilesParams +type StartFsWatchRequestObject struct { + Body *StartFsWatchJSONRequestBody } -type ListFilesResponseObject interface { - VisitListFilesResponse(w http.ResponseWriter) error +type StartFsWatchResponseObject interface { + VisitStartFsWatchResponse(w http.ResponseWriter) error } -type ListFiles200JSONResponse ListFiles +type StartFsWatch201JSONResponse struct { + // WatchId Unique identifier for the directory watch + WatchId *string `json:"watch_id,omitempty"` +} -func (response ListFiles200JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { +func (response StartFsWatch201JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) + w.WriteHeader(201) return json.NewEncoder(w).Encode(response) } -type ListFiles400JSONResponse struct{ BadRequestErrorJSONResponse } +type StartFsWatch400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response ListFiles400JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { +func (response StartFsWatch400JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type ListFiles404JSONResponse struct{ NotFoundErrorJSONResponse } +type StartFsWatch404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response ListFiles404JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { +func (response StartFsWatch404JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type ListFiles500JSONResponse struct{ InternalErrorJSONResponse } +type StartFsWatch500JSONResponse struct{ InternalErrorJSONResponse } -func (response ListFiles500JSONResponse) VisitListFilesResponse(w http.ResponseWriter) error { +func (response StartFsWatch500JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type MovePathRequestObject struct { - Body *MovePathJSONRequestBody +type StopFsWatchRequestObject struct { + WatchId string `json:"watch_id"` } -type MovePathResponseObject interface { - VisitMovePathResponse(w http.ResponseWriter) error +type StopFsWatchResponseObject interface { + VisitStopFsWatchResponse(w http.ResponseWriter) error } -type MovePath200Response struct { +type StopFsWatch204Response struct { } -func (response MovePath200Response) VisitMovePathResponse(w http.ResponseWriter) error { - w.WriteHeader(200) +func (response StopFsWatch204Response) VisitStopFsWatchResponse(w http.ResponseWriter) error { + w.WriteHeader(204) return nil } -type MovePath400JSONResponse struct{ BadRequestErrorJSONResponse } +type StopFsWatch400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response MovePath400JSONResponse) VisitMovePathResponse(w http.ResponseWriter) error { +func (response StopFsWatch400JSONResponse) VisitStopFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type MovePath404JSONResponse struct{ NotFoundErrorJSONResponse } +type StopFsWatch404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response MovePath404JSONResponse) VisitMovePathResponse(w http.ResponseWriter) error { +func (response StopFsWatch404JSONResponse) VisitStopFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type MovePath500JSONResponse struct{ InternalErrorJSONResponse } +type StopFsWatch500JSONResponse struct{ InternalErrorJSONResponse } -func (response MovePath500JSONResponse) VisitMovePathResponse(w http.ResponseWriter) error { +func (response StopFsWatch500JSONResponse) VisitStopFsWatchResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type ReadFileRequestObject struct { - Params ReadFileParams +type StreamFsEventsRequestObject struct { + WatchId string `json:"watch_id"` } -type ReadFileResponseObject interface { - VisitReadFileResponse(w http.ResponseWriter) error +type StreamFsEventsResponseObject interface { + VisitStreamFsEventsResponse(w http.ResponseWriter) error } -type ReadFile200ApplicationoctetStreamResponse struct { +type StreamFsEvents200ResponseHeaders struct { + XSSEContentType string +} + +type StreamFsEvents200TexteventStreamResponse struct { Body io.Reader + Headers StreamFsEvents200ResponseHeaders ContentLength int64 } -func (response ReadFile200ApplicationoctetStreamResponse) VisitReadFileResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/octet-stream") +func (response StreamFsEvents200TexteventStreamResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/event-stream") if response.ContentLength != 0 { w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) } + w.Header().Set("X-SSE-Content-Type", fmt.Sprint(response.Headers.XSSEContentType)) w.WriteHeader(200) if closer, ok := response.Body.(io.ReadCloser); ok { defer closer.Close() } - _, err := io.Copy(w, response.Body) - return err + flusher, ok := w.(http.Flusher) + if !ok { + // If w doesn't support flushing, might as well use io.Copy + _, err := io.Copy(w, response.Body) + return err + } + + // Use a buffer for efficient copying and flushing + buf := make([]byte, 4096) // text/event-stream are usually very small messages + for { + n, err := response.Body.Read(buf) + if n > 0 { + if _, werr := w.Write(buf[:n]); werr != nil { + return werr + } + flusher.Flush() // Flush after each write + } + if err != nil { + if err == io.EOF { + return nil // End of file, no error + } + return err + } + } } -type ReadFile400JSONResponse struct{ BadRequestErrorJSONResponse } +type StreamFsEvents400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response ReadFile400JSONResponse) VisitReadFileResponse(w http.ResponseWriter) error { +func (response StreamFsEvents400JSONResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type ReadFile404JSONResponse struct{ NotFoundErrorJSONResponse } +type StreamFsEvents404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response ReadFile404JSONResponse) VisitReadFileResponse(w http.ResponseWriter) error { +func (response StreamFsEvents404JSONResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type ReadFile500JSONResponse struct{ InternalErrorJSONResponse } +type StreamFsEvents500JSONResponse struct{ InternalErrorJSONResponse } -func (response ReadFile500JSONResponse) VisitReadFileResponse(w http.ResponseWriter) error { +func (response StreamFsEvents500JSONResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type SetFilePermissionsRequestObject struct { - Body *SetFilePermissionsJSONRequestBody +type WriteFileRequestObject struct { + Params WriteFileParams + Body io.Reader } -type SetFilePermissionsResponseObject interface { - VisitSetFilePermissionsResponse(w http.ResponseWriter) error +type WriteFileResponseObject interface { + VisitWriteFileResponse(w http.ResponseWriter) error } -type SetFilePermissions200Response struct { +type WriteFile201Response struct { } -func (response SetFilePermissions200Response) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { - w.WriteHeader(200) +func (response WriteFile201Response) VisitWriteFileResponse(w http.ResponseWriter) error { + w.WriteHeader(201) return nil } -type SetFilePermissions400JSONResponse struct{ BadRequestErrorJSONResponse } +type WriteFile400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response SetFilePermissions400JSONResponse) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { +func (response WriteFile400JSONResponse) VisitWriteFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type SetFilePermissions404JSONResponse struct{ NotFoundErrorJSONResponse } +type WriteFile404JSONResponse struct{ NotFoundErrorJSONResponse } -func (response SetFilePermissions404JSONResponse) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { +func (response WriteFile404JSONResponse) VisitWriteFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(404) return json.NewEncoder(w).Encode(response) } -type SetFilePermissions500JSONResponse struct{ InternalErrorJSONResponse } +type WriteFile500JSONResponse struct{ InternalErrorJSONResponse } -func (response SetFilePermissions500JSONResponse) VisitSetFilePermissionsResponse(w http.ResponseWriter) error { +func (response WriteFile500JSONResponse) VisitWriteFileResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type UploadFilesRequestObject struct { - Body *multipart.Reader -} - -type UploadFilesResponseObject interface { - VisitUploadFilesResponse(w http.ResponseWriter) error -} - -type UploadFiles201Response struct { +type ConfigureVirtualInputsRequestObject struct { + Body *ConfigureVirtualInputsJSONRequestBody } -func (response UploadFiles201Response) VisitUploadFilesResponse(w http.ResponseWriter) error { - w.WriteHeader(201) - return nil +type ConfigureVirtualInputsResponseObject interface { + VisitConfigureVirtualInputsResponse(w http.ResponseWriter) error } -type UploadFiles400JSONResponse struct{ BadRequestErrorJSONResponse } +type ConfigureVirtualInputs200JSONResponse VirtualInputsStatus -func (response UploadFiles400JSONResponse) VisitUploadFilesResponse(w http.ResponseWriter) error { +func (response ConfigureVirtualInputs200JSONResponse) VisitConfigureVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type UploadFiles404JSONResponse struct{ NotFoundErrorJSONResponse } +type ConfigureVirtualInputs400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response UploadFiles404JSONResponse) VisitUploadFilesResponse(w http.ResponseWriter) error { +func (response ConfigureVirtualInputs400JSONResponse) VisitConfigureVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) + w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type UploadFiles500JSONResponse struct{ InternalErrorJSONResponse } +type ConfigureVirtualInputs500JSONResponse struct{ InternalErrorJSONResponse } -func (response UploadFiles500JSONResponse) VisitUploadFilesResponse(w http.ResponseWriter) error { +func (response ConfigureVirtualInputs500JSONResponse) VisitConfigureVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type UploadZipRequestObject struct { - Body *multipart.Reader +type GetVirtualInputFeedRequestObject struct { + Params GetVirtualInputFeedParams } -type UploadZipResponseObject interface { - VisitUploadZipResponse(w http.ResponseWriter) error +type GetVirtualInputFeedResponseObject interface { + VisitGetVirtualInputFeedResponse(w http.ResponseWriter) error } -type UploadZip201Response struct { +type GetVirtualInputFeed200TexthtmlResponse struct { + Body io.Reader + ContentLength int64 } -func (response UploadZip201Response) VisitUploadZipResponse(w http.ResponseWriter) error { - w.WriteHeader(201) - return nil +func (response GetVirtualInputFeed200TexthtmlResponse) VisitGetVirtualInputFeedResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "text/html") + if response.ContentLength != 0 { + w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) + } + w.WriteHeader(200) + + if closer, ok := response.Body.(io.ReadCloser); ok { + defer closer.Close() + } + _, err := io.Copy(w, response.Body) + return err } -type UploadZip400JSONResponse struct{ BadRequestErrorJSONResponse } +type GetVirtualInputFeed500JSONResponse struct{ InternalErrorJSONResponse } -func (response UploadZip400JSONResponse) VisitUploadZipResponse(w http.ResponseWriter) error { +func (response GetVirtualInputFeed500JSONResponse) VisitGetVirtualInputFeedResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type UploadZip404JSONResponse struct{ NotFoundErrorJSONResponse } - -func (response UploadZip404JSONResponse) VisitUploadZipResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) +type GetVirtualInputFeedSocketInfoRequestObject struct { +} - return json.NewEncoder(w).Encode(response) +type GetVirtualInputFeedSocketInfoResponseObject interface { + VisitGetVirtualInputFeedSocketInfoResponse(w http.ResponseWriter) error } -type UploadZip500JSONResponse struct{ InternalErrorJSONResponse } +type GetVirtualInputFeedSocketInfo200JSONResponse VirtualFeedSocketInfo -func (response UploadZip500JSONResponse) VisitUploadZipResponse(w http.ResponseWriter) error { +func (response GetVirtualInputFeedSocketInfo200JSONResponse) VisitGetVirtualInputFeedSocketInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type StartFsWatchRequestObject struct { - Body *StartFsWatchJSONRequestBody -} - -type StartFsWatchResponseObject interface { - VisitStartFsWatchResponse(w http.ResponseWriter) error -} - -type StartFsWatch201JSONResponse struct { - // WatchId Unique identifier for the directory watch - WatchId *string `json:"watch_id,omitempty"` -} +type GetVirtualInputFeedSocketInfo409JSONResponse struct{ ConflictErrorJSONResponse } -func (response StartFsWatch201JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { +func (response GetVirtualInputFeedSocketInfo409JSONResponse) VisitGetVirtualInputFeedSocketInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(201) + w.WriteHeader(409) return json.NewEncoder(w).Encode(response) } -type StartFsWatch400JSONResponse struct{ BadRequestErrorJSONResponse } +type GetVirtualInputFeedSocketInfo500JSONResponse struct{ InternalErrorJSONResponse } -func (response StartFsWatch400JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { +func (response GetVirtualInputFeedSocketInfo500JSONResponse) VisitGetVirtualInputFeedSocketInfoResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type StartFsWatch404JSONResponse struct{ NotFoundErrorJSONResponse } +type PauseVirtualInputsRequestObject struct { +} -func (response StartFsWatch404JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { +type PauseVirtualInputsResponseObject interface { + VisitPauseVirtualInputsResponse(w http.ResponseWriter) error +} + +type PauseVirtualInputs200JSONResponse VirtualInputsStatus + +func (response PauseVirtualInputs200JSONResponse) VisitPauseVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) + w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type StartFsWatch500JSONResponse struct{ InternalErrorJSONResponse } +type PauseVirtualInputs400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response StartFsWatch500JSONResponse) VisitStartFsWatchResponse(w http.ResponseWriter) error { +func (response PauseVirtualInputs400JSONResponse) VisitPauseVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(500) + w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type StopFsWatchRequestObject struct { - WatchId string `json:"watch_id"` -} +type PauseVirtualInputs500JSONResponse struct{ InternalErrorJSONResponse } -type StopFsWatchResponseObject interface { - VisitStopFsWatchResponse(w http.ResponseWriter) error +func (response PauseVirtualInputs500JSONResponse) VisitPauseVirtualInputsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) } -type StopFsWatch204Response struct { +type ResumeVirtualInputsRequestObject struct { } -func (response StopFsWatch204Response) VisitStopFsWatchResponse(w http.ResponseWriter) error { - w.WriteHeader(204) - return nil +type ResumeVirtualInputsResponseObject interface { + VisitResumeVirtualInputsResponse(w http.ResponseWriter) error } -type StopFsWatch400JSONResponse struct{ BadRequestErrorJSONResponse } +type ResumeVirtualInputs200JSONResponse VirtualInputsStatus -func (response StopFsWatch400JSONResponse) VisitStopFsWatchResponse(w http.ResponseWriter) error { +func (response ResumeVirtualInputs200JSONResponse) VisitResumeVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type StopFsWatch404JSONResponse struct{ NotFoundErrorJSONResponse } +type ResumeVirtualInputs400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response StopFsWatch404JSONResponse) VisitStopFsWatchResponse(w http.ResponseWriter) error { +func (response ResumeVirtualInputs400JSONResponse) VisitResumeVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) + w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type StopFsWatch500JSONResponse struct{ InternalErrorJSONResponse } +type ResumeVirtualInputs500JSONResponse struct{ InternalErrorJSONResponse } -func (response StopFsWatch500JSONResponse) VisitStopFsWatchResponse(w http.ResponseWriter) error { +func (response ResumeVirtualInputs500JSONResponse) VisitResumeVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type StreamFsEventsRequestObject struct { - WatchId string `json:"watch_id"` -} - -type StreamFsEventsResponseObject interface { - VisitStreamFsEventsResponse(w http.ResponseWriter) error +type GetVirtualInputsStatusRequestObject struct { } -type StreamFsEvents200ResponseHeaders struct { - XSSEContentType string +type GetVirtualInputsStatusResponseObject interface { + VisitGetVirtualInputsStatusResponse(w http.ResponseWriter) error } -type StreamFsEvents200TexteventStreamResponse struct { - Body io.Reader - Headers StreamFsEvents200ResponseHeaders - ContentLength int64 -} +type GetVirtualInputsStatus200JSONResponse VirtualInputsStatus -func (response StreamFsEvents200TexteventStreamResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { - w.Header().Set("Content-Type", "text/event-stream") - if response.ContentLength != 0 { - w.Header().Set("Content-Length", fmt.Sprint(response.ContentLength)) - } - w.Header().Set("X-SSE-Content-Type", fmt.Sprint(response.Headers.XSSEContentType)) +func (response GetVirtualInputsStatus200JSONResponse) VisitGetVirtualInputsStatusResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - if closer, ok := response.Body.(io.ReadCloser); ok { - defer closer.Close() - } - flusher, ok := w.(http.Flusher) - if !ok { - // If w doesn't support flushing, might as well use io.Copy - _, err := io.Copy(w, response.Body) - return err - } - - // Use a buffer for efficient copying and flushing - buf := make([]byte, 4096) // text/event-stream are usually very small messages - for { - n, err := response.Body.Read(buf) - if n > 0 { - if _, werr := w.Write(buf[:n]); werr != nil { - return werr - } - flusher.Flush() // Flush after each write - } - if err != nil { - if err == io.EOF { - return nil // End of file, no error - } - return err - } - } + return json.NewEncoder(w).Encode(response) } -type StreamFsEvents400JSONResponse struct{ BadRequestErrorJSONResponse } +type GetVirtualInputsStatus500JSONResponse struct{ InternalErrorJSONResponse } -func (response StreamFsEvents400JSONResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { +func (response GetVirtualInputsStatus500JSONResponse) VisitGetVirtualInputsStatusResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type StreamFsEvents404JSONResponse struct{ NotFoundErrorJSONResponse } +type StopVirtualInputsRequestObject struct { +} -func (response StreamFsEvents404JSONResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { +type StopVirtualInputsResponseObject interface { + VisitStopVirtualInputsResponse(w http.ResponseWriter) error +} + +type StopVirtualInputs200JSONResponse VirtualInputsStatus + +func (response StopVirtualInputs200JSONResponse) VisitStopVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) + w.WriteHeader(200) return json.NewEncoder(w).Encode(response) } -type StreamFsEvents500JSONResponse struct{ InternalErrorJSONResponse } +type StopVirtualInputs500JSONResponse struct{ InternalErrorJSONResponse } -func (response StreamFsEvents500JSONResponse) VisitStreamFsEventsResponse(w http.ResponseWriter) error { +func (response StopVirtualInputs500JSONResponse) VisitStopVirtualInputsResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) return json.NewEncoder(w).Encode(response) } -type WriteFileRequestObject struct { - Params WriteFileParams - Body io.Reader +type NegotiateVirtualInputsWebrtcRequestObject struct { + Body *NegotiateVirtualInputsWebrtcJSONRequestBody } -type WriteFileResponseObject interface { - VisitWriteFileResponse(w http.ResponseWriter) error +type NegotiateVirtualInputsWebrtcResponseObject interface { + VisitNegotiateVirtualInputsWebrtcResponse(w http.ResponseWriter) error } -type WriteFile201Response struct { -} +type NegotiateVirtualInputsWebrtc200JSONResponse VirtualInputWebRTCAnswer -func (response WriteFile201Response) VisitWriteFileResponse(w http.ResponseWriter) error { - w.WriteHeader(201) - return nil +func (response NegotiateVirtualInputsWebrtc200JSONResponse) VisitNegotiateVirtualInputsWebrtcResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) } -type WriteFile400JSONResponse struct{ BadRequestErrorJSONResponse } +type NegotiateVirtualInputsWebrtc400JSONResponse struct{ BadRequestErrorJSONResponse } -func (response WriteFile400JSONResponse) VisitWriteFileResponse(w http.ResponseWriter) error { +func (response NegotiateVirtualInputsWebrtc400JSONResponse) VisitNegotiateVirtualInputsWebrtcResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(400) return json.NewEncoder(w).Encode(response) } -type WriteFile404JSONResponse struct{ NotFoundErrorJSONResponse } +type NegotiateVirtualInputsWebrtc409JSONResponse struct{ ConflictErrorJSONResponse } -func (response WriteFile404JSONResponse) VisitWriteFileResponse(w http.ResponseWriter) error { +func (response NegotiateVirtualInputsWebrtc409JSONResponse) VisitNegotiateVirtualInputsWebrtcResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(404) + w.WriteHeader(409) return json.NewEncoder(w).Encode(response) } -type WriteFile500JSONResponse struct{ InternalErrorJSONResponse } +type NegotiateVirtualInputsWebrtc500JSONResponse struct{ InternalErrorJSONResponse } -func (response WriteFile500JSONResponse) VisitWriteFileResponse(w http.ResponseWriter) error { +func (response NegotiateVirtualInputsWebrtc500JSONResponse) VisitNegotiateVirtualInputsWebrtcResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) @@ -9573,34 +12089,199 @@ func (response StartRecording500JSONResponse) VisitStartRecordingResponse(w http return json.NewEncoder(w).Encode(response) } -type StopRecordingRequestObject struct { - Body *StopRecordingJSONRequestBody +type StopRecordingRequestObject struct { + Body *StopRecordingJSONRequestBody +} + +type StopRecordingResponseObject interface { + VisitStopRecordingResponse(w http.ResponseWriter) error +} + +type StopRecording200Response struct { +} + +func (response StopRecording200Response) VisitStopRecordingResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type StopRecording400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response StopRecording400JSONResponse) VisitStopRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type StopRecording500JSONResponse struct{ InternalErrorJSONResponse } + +func (response StopRecording500JSONResponse) VisitStopRecordingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type ListStreamsRequestObject struct { +} + +type ListStreamsResponseObject interface { + VisitListStreamsResponse(w http.ResponseWriter) error +} + +type ListStreams200JSONResponse []StreamInfo + +func (response ListStreams200JSONResponse) VisitListStreamsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListStreams500JSONResponse struct{ InternalErrorJSONResponse } + +func (response ListStreams500JSONResponse) VisitListStreamsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type StartStreamRequestObject struct { + Body *StartStreamJSONRequestBody +} + +type StartStreamResponseObject interface { + VisitStartStreamResponse(w http.ResponseWriter) error +} + +type StartStream201JSONResponse StreamInfo + +func (response StartStream201JSONResponse) VisitStartStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type StartStream400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response StartStream400JSONResponse) VisitStartStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type StartStream409JSONResponse struct{ ConflictErrorJSONResponse } + +func (response StartStream409JSONResponse) VisitStartStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + +type StartStream500JSONResponse struct{ InternalErrorJSONResponse } + +func (response StartStream500JSONResponse) VisitStartStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type StopStreamRequestObject struct { + Body *StopStreamJSONRequestBody +} + +type StopStreamResponseObject interface { + VisitStopStreamResponse(w http.ResponseWriter) error +} + +type StopStream200Response struct { +} + +func (response StopStream200Response) VisitStopStreamResponse(w http.ResponseWriter) error { + w.WriteHeader(200) + return nil +} + +type StopStream400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response StopStream400JSONResponse) VisitStopStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type StopStream404JSONResponse struct{ NotFoundErrorJSONResponse } + +func (response StopStream404JSONResponse) VisitStopStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type StopStream500JSONResponse struct{ InternalErrorJSONResponse } + +func (response StopStream500JSONResponse) VisitStopStreamResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type StreamWebrtcOfferRequestObject struct { + Body *StreamWebrtcOfferJSONRequestBody +} + +type StreamWebrtcOfferResponseObject interface { + VisitStreamWebrtcOfferResponse(w http.ResponseWriter) error +} + +type StreamWebrtcOffer200JSONResponse StreamWebRTCAnswer + +func (response StreamWebrtcOffer200JSONResponse) VisitStreamWebrtcOfferResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type StreamWebrtcOffer400JSONResponse struct{ BadRequestErrorJSONResponse } + +func (response StreamWebrtcOffer400JSONResponse) VisitStreamWebrtcOfferResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) } -type StopRecordingResponseObject interface { - VisitStopRecordingResponse(w http.ResponseWriter) error -} +type StreamWebrtcOffer404JSONResponse struct{ NotFoundErrorJSONResponse } -type StopRecording200Response struct { -} +func (response StreamWebrtcOffer404JSONResponse) VisitStreamWebrtcOfferResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(404) -func (response StopRecording200Response) VisitStopRecordingResponse(w http.ResponseWriter) error { - w.WriteHeader(200) - return nil + return json.NewEncoder(w).Encode(response) } -type StopRecording400JSONResponse struct{ BadRequestErrorJSONResponse } +type StreamWebrtcOffer409JSONResponse struct{ ConflictErrorJSONResponse } -func (response StopRecording400JSONResponse) VisitStopRecordingResponse(w http.ResponseWriter) error { +func (response StreamWebrtcOffer409JSONResponse) VisitStreamWebrtcOfferResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") - w.WriteHeader(400) + w.WriteHeader(409) return json.NewEncoder(w).Encode(response) } -type StopRecording500JSONResponse struct{ InternalErrorJSONResponse } +type StreamWebrtcOffer500JSONResponse struct{ InternalErrorJSONResponse } -func (response StopRecording500JSONResponse) VisitStopRecordingResponse(w http.ResponseWriter) error { +func (response StreamWebrtcOffer500JSONResponse) VisitStreamWebrtcOfferResponse(w http.ResponseWriter) error { w.Header().Set("Content-Type", "application/json") w.WriteHeader(500) @@ -9687,6 +12368,30 @@ type StrictServerInterface interface { // Write or create a file // (PUT /fs/write_file) WriteFile(ctx context.Context, request WriteFileRequestObject) (WriteFileResponseObject, error) + // Configure virtual video and audio inputs + // (POST /input/devices/virtual/configure) + ConfigureVirtualInputs(ctx context.Context, request ConfigureVirtualInputsRequestObject) (ConfigureVirtualInputsResponseObject, error) + // Render a fullscreen HTML page with the virtual video feed + // (GET /input/devices/virtual/feed) + GetVirtualInputFeed(ctx context.Context, request GetVirtualInputFeedRequestObject) (GetVirtualInputFeedResponseObject, error) + // Discover the websocket URL for the virtual video feed mirror + // (GET /input/devices/virtual/feed/socket/info) + GetVirtualInputFeedSocketInfo(ctx context.Context, request GetVirtualInputFeedSocketInfoRequestObject) (GetVirtualInputFeedSocketInfoResponseObject, error) + // Pause virtual inputs with silence and black frames + // (POST /input/devices/virtual/pause) + PauseVirtualInputs(ctx context.Context, request PauseVirtualInputsRequestObject) (PauseVirtualInputsResponseObject, error) + // Resume previously configured virtual inputs + // (POST /input/devices/virtual/resume) + ResumeVirtualInputs(ctx context.Context, request ResumeVirtualInputsRequestObject) (ResumeVirtualInputsResponseObject, error) + // Get the current virtual input status + // (GET /input/devices/virtual/status) + GetVirtualInputsStatus(ctx context.Context, request GetVirtualInputsStatusRequestObject) (GetVirtualInputsStatusResponseObject, error) + // Stop virtual input pipelines and release resources + // (POST /input/devices/virtual/stop) + StopVirtualInputs(ctx context.Context, request StopVirtualInputsRequestObject) (StopVirtualInputsResponseObject, error) + // Negotiate a WebRTC ingest session for virtual inputs + // (POST /input/devices/virtual/webrtc/offer) + NegotiateVirtualInputsWebrtc(ctx context.Context, request NegotiateVirtualInputsWebrtcRequestObject) (NegotiateVirtualInputsWebrtcResponseObject, error) // Stream logs over SSE // (GET /logs/stream) LogsStream(ctx context.Context, request LogsStreamRequestObject) (LogsStreamResponseObject, error) @@ -9726,6 +12431,18 @@ type StrictServerInterface interface { // Stop the recording // (POST /recording/stop) StopRecording(ctx context.Context, request StopRecordingRequestObject) (StopRecordingResponseObject, error) + // List active streams + // (GET /stream/list) + ListStreams(ctx context.Context, request ListStreamsRequestObject) (ListStreamsResponseObject, error) + // Start live streaming to an internal RTMP(S) server or a remote RTMP(S) endpoint. + // (POST /stream/start) + StartStream(ctx context.Context, request StartStreamRequestObject) (StartStreamResponseObject, error) + // Stop a live stream + // (POST /stream/stop) + StopStream(ctx context.Context, request StopStreamRequestObject) (StopStreamResponseObject, error) + // Exchange SDP for a WebRTC livestream + // (POST /stream/webrtc/offer) + StreamWebrtcOffer(ctx context.Context, request StreamWebrtcOfferRequestObject) (StreamWebrtcOfferResponseObject, error) } type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc @@ -10530,6 +13247,214 @@ func (sh *strictHandler) WriteFile(w http.ResponseWriter, r *http.Request, param } } +// ConfigureVirtualInputs operation middleware +func (sh *strictHandler) ConfigureVirtualInputs(w http.ResponseWriter, r *http.Request) { + var request ConfigureVirtualInputsRequestObject + + var body ConfigureVirtualInputsJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ConfigureVirtualInputs(ctx, request.(ConfigureVirtualInputsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ConfigureVirtualInputs") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ConfigureVirtualInputsResponseObject); ok { + if err := validResponse.VisitConfigureVirtualInputsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetVirtualInputFeed operation middleware +func (sh *strictHandler) GetVirtualInputFeed(w http.ResponseWriter, r *http.Request, params GetVirtualInputFeedParams) { + var request GetVirtualInputFeedRequestObject + + request.Params = params + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetVirtualInputFeed(ctx, request.(GetVirtualInputFeedRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetVirtualInputFeed") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetVirtualInputFeedResponseObject); ok { + if err := validResponse.VisitGetVirtualInputFeedResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetVirtualInputFeedSocketInfo operation middleware +func (sh *strictHandler) GetVirtualInputFeedSocketInfo(w http.ResponseWriter, r *http.Request) { + var request GetVirtualInputFeedSocketInfoRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetVirtualInputFeedSocketInfo(ctx, request.(GetVirtualInputFeedSocketInfoRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetVirtualInputFeedSocketInfo") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetVirtualInputFeedSocketInfoResponseObject); ok { + if err := validResponse.VisitGetVirtualInputFeedSocketInfoResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PauseVirtualInputs operation middleware +func (sh *strictHandler) PauseVirtualInputs(w http.ResponseWriter, r *http.Request) { + var request PauseVirtualInputsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PauseVirtualInputs(ctx, request.(PauseVirtualInputsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PauseVirtualInputs") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PauseVirtualInputsResponseObject); ok { + if err := validResponse.VisitPauseVirtualInputsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// ResumeVirtualInputs operation middleware +func (sh *strictHandler) ResumeVirtualInputs(w http.ResponseWriter, r *http.Request) { + var request ResumeVirtualInputsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ResumeVirtualInputs(ctx, request.(ResumeVirtualInputsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ResumeVirtualInputs") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ResumeVirtualInputsResponseObject); ok { + if err := validResponse.VisitResumeVirtualInputsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetVirtualInputsStatus operation middleware +func (sh *strictHandler) GetVirtualInputsStatus(w http.ResponseWriter, r *http.Request) { + var request GetVirtualInputsStatusRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetVirtualInputsStatus(ctx, request.(GetVirtualInputsStatusRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetVirtualInputsStatus") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetVirtualInputsStatusResponseObject); ok { + if err := validResponse.VisitGetVirtualInputsStatusResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// StopVirtualInputs operation middleware +func (sh *strictHandler) StopVirtualInputs(w http.ResponseWriter, r *http.Request) { + var request StopVirtualInputsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StopVirtualInputs(ctx, request.(StopVirtualInputsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StopVirtualInputs") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StopVirtualInputsResponseObject); ok { + if err := validResponse.VisitStopVirtualInputsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// NegotiateVirtualInputsWebrtc operation middleware +func (sh *strictHandler) NegotiateVirtualInputsWebrtc(w http.ResponseWriter, r *http.Request) { + var request NegotiateVirtualInputsWebrtcRequestObject + + var body NegotiateVirtualInputsWebrtcJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.NegotiateVirtualInputsWebrtc(ctx, request.(NegotiateVirtualInputsWebrtcRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "NegotiateVirtualInputsWebrtc") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(NegotiateVirtualInputsWebrtcResponseObject); ok { + if err := validResponse.VisitNegotiateVirtualInputsWebrtcResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // LogsStream operation middleware func (sh *strictHandler) LogsStream(w http.ResponseWriter, r *http.Request, params LogsStreamParams) { var request LogsStreamRequestObject @@ -10910,126 +13835,287 @@ func (sh *strictHandler) StopRecording(w http.ResponseWriter, r *http.Request) { } } +// ListStreams operation middleware +func (sh *strictHandler) ListStreams(w http.ResponseWriter, r *http.Request) { + var request ListStreamsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListStreams(ctx, request.(ListStreamsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListStreams") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListStreamsResponseObject); ok { + if err := validResponse.VisitListStreamsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// StartStream operation middleware +func (sh *strictHandler) StartStream(w http.ResponseWriter, r *http.Request) { + var request StartStreamRequestObject + + var body StartStreamJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StartStream(ctx, request.(StartStreamRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StartStream") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StartStreamResponseObject); ok { + if err := validResponse.VisitStartStreamResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// StopStream operation middleware +func (sh *strictHandler) StopStream(w http.ResponseWriter, r *http.Request) { + var request StopStreamRequestObject + + var body StopStreamJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StopStream(ctx, request.(StopStreamRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StopStream") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StopStreamResponseObject); ok { + if err := validResponse.VisitStopStreamResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// StreamWebrtcOffer operation middleware +func (sh *strictHandler) StreamWebrtcOffer(w http.ResponseWriter, r *http.Request) { + var request StreamWebrtcOfferRequestObject + + var body StreamWebrtcOfferJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.StreamWebrtcOffer(ctx, request.(StreamWebrtcOfferRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "StreamWebrtcOffer") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(StreamWebrtcOfferResponseObject); ok { + if err := validResponse.VisitStreamWebrtcOfferResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/server/lib/stream/errors.go b/server/lib/stream/errors.go new file mode 100644 index 00000000..689aa6e3 --- /dev/null +++ b/server/lib/stream/errors.go @@ -0,0 +1,9 @@ +package stream + +import "errors" + +var ( + ErrInvalidParams = errors.New("invalid stream parameters") + ErrStreamInProgress = errors.New("stream already in progress") + ErrStreamStartFailed = errors.New("stream failed to start") +) diff --git a/server/lib/stream/ffmpeg.go b/server/lib/stream/ffmpeg.go new file mode 100644 index 00000000..cf51d6f8 --- /dev/null +++ b/server/lib/stream/ffmpeg.go @@ -0,0 +1,410 @@ +package stream + +import ( + "context" + "errors" + "fmt" + "math" + "os" + "os/exec" + "runtime" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/scaletozero" +) + +const ( + // exitCodeInitValue mirrors recorder to represent an unset exit code. + exitCodeInitValue = math.MinInt +) + +var runCommand = func(ctx context.Context, name string, args ...string) ([]byte, error) { + cmd := exec.CommandContext(ctx, name, args...) + cmd.Env = os.Environ() + return cmd.Output() +} + +// FFmpegStreamer streams the display to an RTMP(S) endpoint. +type FFmpegStreamer struct { + mu sync.Mutex + + id string + binaryPath string + params Params + + cmd *exec.Cmd + exited chan struct{} + ffmpegErr error + exitCode int + startedAt time.Time + stz *scaletozero.Oncer +} + +// NewFFmpegStreamerFactory builds an FFmpeg-backed streamer factory. +func NewFFmpegStreamerFactory(pathToFFmpeg string, defaults Params, ctrl scaletozero.Controller) FFmpegStreamerFactory { + return func(id string, overrides Params) (Streamer, error) { + merged := mergeParams(defaults, overrides) + if err := validateParams(merged); err != nil { + return nil, err + } + + return &FFmpegStreamer{ + id: id, + binaryPath: pathToFFmpeg, + params: merged, + stz: scaletozero.NewOncer(ctrl), + exitCode: exitCodeInitValue, + }, nil + } +} + +func mergeParams(defaults Params, overrides Params) Params { + out := Params{ + FrameRate: defaults.FrameRate, + DisplayNum: defaults.DisplayNum, + Mode: defaults.Mode, + IngestURL: defaults.IngestURL, + PlaybackURL: defaults.PlaybackURL, + SecurePlaybackURL: defaults.SecurePlaybackURL, + } + if overrides.FrameRate != nil { + out.FrameRate = overrides.FrameRate + } + if overrides.DisplayNum != nil { + out.DisplayNum = overrides.DisplayNum + } + if overrides.Mode != "" { + out.Mode = overrides.Mode + } + if overrides.IngestURL != "" { + out.IngestURL = overrides.IngestURL + } + if overrides.PlaybackURL != nil { + out.PlaybackURL = overrides.PlaybackURL + } + if overrides.SecurePlaybackURL != nil { + out.SecurePlaybackURL = overrides.SecurePlaybackURL + } + if out.Mode == "" { + out.Mode = ModeInternal + } + return out +} + +func validateParams(p Params) error { + if p.IngestURL == "" { + return fmt.Errorf("ingest URL is required") + } + if p.FrameRate == nil { + return fmt.Errorf("frame rate is required") + } + if p.DisplayNum == nil { + return fmt.Errorf("display number is required") + } + return nil +} + +func (fs *FFmpegStreamer) ID() string { + return fs.id +} + +// Start begins streaming to the configured ingest URL. +func (fs *FFmpegStreamer) Start(ctx context.Context) error { + log := logger.FromContext(ctx) + + fs.mu.Lock() + if fs.cmd != nil { + fs.mu.Unlock() + return fmt.Errorf("stream already in progress") + } + + if err := fs.stz.Disable(ctx); err != nil { + fs.mu.Unlock() + return fmt.Errorf("failed to disable scale-to-zero: %w", err) + } + + fs.ffmpegErr = nil + fs.exitCode = exitCodeInitValue + fs.startedAt = time.Now() + fs.exited = make(chan struct{}) + + args, err := ffmpegStreamArgs(ctx, fs.params) + if err != nil { + _ = fs.stz.Enable(context.WithoutCancel(ctx)) + fs.mu.Unlock() + return err + } + log.Info(fmt.Sprintf("%s %s", fs.binaryPath, strings.Join(args, " "))) + + cmd := exec.Command(fs.binaryPath, args...) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + fs.cmd = cmd + fs.mu.Unlock() + + if err := cmd.Start(); err != nil { + _ = fs.stz.Enable(context.WithoutCancel(ctx)) + fs.mu.Lock() + fs.ffmpegErr = err + fs.cmd = nil + close(fs.exited) + fs.mu.Unlock() + return fmt.Errorf("failed to start ffmpeg process: %w", err) + } + + go fs.waitForCommand(ctx) + + // Detect immediate startup failures. + if err := waitForChan(ctx, 250*time.Millisecond, fs.exited); err == nil { + fs.mu.Lock() + defer fs.mu.Unlock() + if fs.ffmpegErr != nil { + return fmt.Errorf("failed to start ffmpeg process: %w", fs.ffmpegErr) + } + return fmt.Errorf("ffmpeg process exited immediately with code %d", fs.exitCode) + } + + return nil +} + +// Stop attempts a graceful shutdown of the ffmpeg process, escalating to SIGKILL on timeout. +func (fs *FFmpegStreamer) Stop(ctx context.Context) error { + defer fs.stz.Enable(context.WithoutCancel(ctx)) + + fs.mu.Lock() + cmd := fs.cmd + exited := fs.exited + fs.mu.Unlock() + + if cmd == nil { + return nil + } + + // Request graceful stop. + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) + if err := waitForChan(ctx, 5*time.Second, exited); err == nil { + return nil + } + + // Force kill. + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + _ = waitForChan(ctx, 2*time.Second, exited) + return nil +} + +func (fs *FFmpegStreamer) IsStreaming(ctx context.Context) bool { + fs.mu.Lock() + defer fs.mu.Unlock() + + return fs.cmd != nil && fs.cmd.ProcessState == nil +} + +func (fs *FFmpegStreamer) Metadata() Metadata { + fs.mu.Lock() + defer fs.mu.Unlock() + + return Metadata{ + ID: fs.id, + Mode: fs.params.Mode, + IngestURL: fs.params.IngestURL, + PlaybackURL: fs.params.PlaybackURL, + SecurePlaybackURL: fs.params.SecurePlaybackURL, + StartedAt: fs.startedAt, + } +} + +func (fs *FFmpegStreamer) waitForCommand(ctx context.Context) { + defer fs.stz.Enable(context.WithoutCancel(ctx)) + + err := fs.cmd.Wait() + + fs.mu.Lock() + defer fs.mu.Unlock() + fs.ffmpegErr = err + if fs.cmd.ProcessState != nil { + fs.exitCode = fs.cmd.ProcessState.ExitCode() + } + fs.cmd = nil + close(fs.exited) + + if err != nil { + logger.FromContext(ctx).Info("ffmpeg stream exited with error", "err", err, "exitCode", fs.exitCode) + } else { + logger.FromContext(ctx).Info("ffmpeg stream exited", "exitCode", fs.exitCode) + } +} + +// ffmpegStreamArgs builds input/output arguments for live streaming. +func ffmpegStreamArgs(ctx context.Context, params Params) ([]string, error) { + videoInput, err := screenCaptureArgs(params) + if err != nil { + return nil, err + } + + args := append([]string{}, videoInput...) + + hasAudio := false + if runtime.GOOS == "linux" { + audioInput, err := audioCaptureArgs(ctx) + if err != nil { + return nil, fmt.Errorf("detect PulseAudio sink: %w", err) + } + args = append(args, audioInput...) + hasAudio = true + } + + args = append(args, + "-c:v", "libx264", + "-preset", "veryfast", + "-tune", "zerolatency", + "-pix_fmt", "yuv420p", + "-g", strconv.Itoa(*params.FrameRate*2), + ) + + if hasAudio { + args = append(args, + "-map", "0:v:0", + "-map", "1:a:0", + "-c:a", "aac", + "-b:a", "128k", + "-ar", "44100", + "-ac", "2", + ) + } + + args = append(args, + "-use_wallclock_as_timestamps", "1", + "-fflags", "nobuffer", + "-f", "flv", + params.IngestURL, + ) + + return args, nil +} + +func screenCaptureArgs(params Params) ([]string, error) { + switch runtime.GOOS { + case "darwin": + return []string{ + "-f", "avfoundation", + "-framerate", strconv.Itoa(*params.FrameRate), + "-pixel_format", "nv12", + "-i", fmt.Sprintf("%d:none", *params.DisplayNum), + }, nil + case "linux": + return []string{ + "-f", "x11grab", + "-framerate", strconv.Itoa(*params.FrameRate), + "-i", fmt.Sprintf(":%d", *params.DisplayNum), + }, nil + default: + return nil, fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } +} + +func audioCaptureArgs(ctx context.Context) ([]string, error) { + mon, err := pulseAudioMonitorSource(ctx) + if err != nil { + return nil, err + } + + return []string{ + "-f", "pulse", + "-thread_queue_size", "512", + "-i", mon, + }, nil +} + +func pulseAudioMonitorSource(ctx context.Context) (string, error) { + ctx, cancel := context.WithTimeout(ctx, 2*time.Second) + defer cancel() + + defaultSink, defaultErr := pulseDefaultSink(ctx) + if defaultSink != "" { + return defaultSink + ".monitor", nil + } + + sinks, listErr := listPulseSinks(ctx) + if listErr != nil { + if defaultErr != nil { + return "", fmt.Errorf("failed to detect pulseaudio default sink (%v) and list sinks (%w)", defaultErr, listErr) + } + return "", fmt.Errorf("failed to list pulseaudio sinks: %w", listErr) + } + + sink := pickPreferredSink(sinks) + if sink == "" { + if defaultErr != nil { + return "", fmt.Errorf("no pulseaudio sinks detected (default sink error: %v)", defaultErr) + } + return "", errors.New("no pulseaudio sinks detected") + } + + return sink + ".monitor", nil +} + +func pulseDefaultSink(ctx context.Context) (string, error) { + out, err := runCommand(ctx, "pactl", "info") + if err != nil { + return "", err + } + + for _, line := range strings.Split(string(out), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "Default Sink:") { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + if sink := strings.TrimSpace(parts[1]); sink != "" { + return sink, nil + } + } + } + } + return "", nil +} + +func listPulseSinks(ctx context.Context) ([]string, error) { + out, err := runCommand(ctx, "pactl", "list", "short", "sinks") + if err != nil { + return nil, err + } + lines := strings.Split(string(out), "\n") + sinks := make([]string, 0, len(lines)) + for _, line := range lines { + fields := strings.Fields(strings.TrimSpace(line)) + if len(fields) >= 2 && fields[1] != "" { + sinks = append(sinks, fields[1]) + } + } + return sinks, nil +} + +func pickPreferredSink(sinks []string) string { + if len(sinks) == 0 { + return "" + } + for _, sink := range sinks { + if sink == "audio_output" { + return sink + } + } + return sinks[0] +} + +// waitForChan returns nil if and only if the channel is closed within the timeout. +func waitForChan(ctx context.Context, timeout time.Duration, c <-chan struct{}) error { + select { + case <-c: + return nil + case <-time.After(timeout): + return errors.New("process did not exit within timeout") + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/server/lib/stream/ffmpeg_test.go b/server/lib/stream/ffmpeg_test.go new file mode 100644 index 00000000..810a6b9c --- /dev/null +++ b/server/lib/stream/ffmpeg_test.go @@ -0,0 +1,75 @@ +package stream + +import ( + "context" + "errors" + "fmt" + "runtime" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPulseAudioMonitorSourceUsesDefaultSink(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("audio capture detection is only relevant on linux") + } + stubRunCommand(t, func(_ context.Context, name string, args ...string) ([]byte, error) { + require.Equal(t, "pactl", name) + require.Equal(t, []string{"info"}, args) + return []byte("Default Sink: monitor_sink\n"), nil + }) + + mon, err := pulseAudioMonitorSource(context.Background()) + require.NoError(t, err) + require.Equal(t, "monitor_sink.monitor", mon) +} + +func TestPulseAudioMonitorSourceFallsBackToAudioOutput(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("audio capture detection is only relevant on linux") + } + callCount := 0 + stubRunCommand(t, func(_ context.Context, name string, args ...string) ([]byte, error) { + callCount++ + switch fmt.Sprint(name, " ", args) { + case "pactl [info]": + return []byte("Server String: pulse\n"), nil + case "pactl [list short sinks]": + return []byte("0\taudio_output\tmodule-null-sink.c\ts16le 2ch 44100Hz\n1\tsecond_sink\tmodule-null-sink.c\ts16le 2ch 44100Hz\n"), nil + default: + return nil, fmt.Errorf("unexpected command: %s %v", name, args) + } + }) + + mon, err := pulseAudioMonitorSource(context.Background()) + require.NoError(t, err) + require.Equal(t, "audio_output.monitor", mon) + require.Equal(t, 2, callCount) +} + +func TestPulseAudioMonitorSourceErrorsWithoutSinks(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("audio capture detection is only relevant on linux") + } + stubRunCommand(t, func(_ context.Context, name string, args ...string) ([]byte, error) { + switch fmt.Sprint(name, " ", args) { + case "pactl [info]": + return nil, errors.New("pactl unavailable") + case "pactl [list short sinks]": + return []byte(""), nil + default: + return nil, fmt.Errorf("unexpected command: %s %v", name, args) + } + }) + + _, err := pulseAudioMonitorSource(context.Background()) + require.Error(t, err) +} + +func stubRunCommand(t *testing.T, fn func(ctx context.Context, name string, args ...string) ([]byte, error)) { + t.Helper() + original := runCommand + runCommand = fn + t.Cleanup(func() { runCommand = original }) +} diff --git a/server/lib/stream/manager.go b/server/lib/stream/manager.go new file mode 100644 index 00000000..72cdd4cc --- /dev/null +++ b/server/lib/stream/manager.go @@ -0,0 +1,86 @@ +package stream + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/onkernel/kernel-images/server/lib/logger" +) + +type StreamManager struct { + mu sync.Mutex + streams map[string]Streamer +} + +func NewStreamManager() *StreamManager { + return &StreamManager{ + streams: make(map[string]Streamer), + } +} + +func (sm *StreamManager) GetStream(id string) (Streamer, bool) { + sm.mu.Lock() + defer sm.mu.Unlock() + + stream, ok := sm.streams[id] + return stream, ok +} + +func (sm *StreamManager) ListStreams(ctx context.Context) []Streamer { + sm.mu.Lock() + defer sm.mu.Unlock() + + streams := make([]Streamer, 0, len(sm.streams)) + for _, stream := range sm.streams { + streams = append(streams, stream) + } + return streams +} + +func (sm *StreamManager) RegisterStream(ctx context.Context, streamer Streamer) error { + log := logger.FromContext(ctx) + + sm.mu.Lock() + defer sm.mu.Unlock() + + if _, exists := sm.streams[streamer.ID()]; exists { + return fmt.Errorf("stream with id '%s' already exists", streamer.ID()) + } + + sm.streams[streamer.ID()] = streamer + log.Info("registered new stream", "id", streamer.ID()) + return nil +} + +func (sm *StreamManager) DeregisterStream(ctx context.Context, streamer Streamer) error { + sm.mu.Lock() + defer sm.mu.Unlock() + + delete(sm.streams, streamer.ID()) + return nil +} + +func (sm *StreamManager) StopAll(ctx context.Context) error { + log := logger.FromContext(ctx) + + sm.mu.Lock() + defer sm.mu.Unlock() + + var errs []error + for id, streamer := range sm.streams { + if streamer.IsStreaming(ctx) { + if err := streamer.Stop(ctx); err != nil { + errs = append(errs, fmt.Errorf("failed to stop stream '%s': %w", id, err)) + log.Error("failed to stop stream during shutdown", "id", id, "err", err) + } + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} diff --git a/server/lib/stream/rtmp_server.go b/server/lib/stream/rtmp_server.go new file mode 100644 index 00000000..0ed2d39d --- /dev/null +++ b/server/lib/stream/rtmp_server.go @@ -0,0 +1,310 @@ +package stream + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + "log/slog" + "math/big" + "net" + "strings" + "sync" + "time" + + "github.com/notedit/rtmp/format/rtmp" + "github.com/notedit/rtmp/pubsub" +) + +// RTMPServer implements an internal RTMP/RTMPS server backed by pubsub. +type RTMPServer struct { + mu sync.Mutex + rtmpAddr string + rtmpsAddr string + tlsConfig *tls.Config + streams map[string]*pubsub.PubSub + listener net.Listener + tlsListener net.Listener + server *rtmp.Server + running bool + logger *slog.Logger + wg sync.WaitGroup +} + +func NewRTMPServer(rtmpAddr, rtmpsAddr string, tlsConfig *tls.Config, logger *slog.Logger) *RTMPServer { + return &RTMPServer{ + rtmpAddr: rtmpAddr, + rtmpsAddr: rtmpsAddr, + tlsConfig: tlsConfig, + streams: make(map[string]*pubsub.PubSub), + logger: logger, + } +} + +func (s *RTMPServer) Start(ctx context.Context) error { + s.mu.Lock() + if s.running { + s.mu.Unlock() + return nil + } + if s.streams == nil { + s.streams = make(map[string]*pubsub.PubSub) + } + + rtmpListener, err := net.Listen("tcp", s.rtmpAddr) + if err != nil { + s.mu.Unlock() + return fmt.Errorf("failed to start rtmp listener: %w", err) + } + + srv := rtmp.NewServer() + srv.HandleConn = func(c *rtmp.Conn, nc net.Conn) { + s.handleConn(ctx, c, nc) + } + srv.LogEvent = func(c *rtmp.Conn, nc net.Conn, e int) { + if s.logger == nil { + return + } + s.logger.Debug("rtmp event", slog.String("event", rtmp.EventString[e]), slog.String("remote_addr", nc.RemoteAddr().String())) + } + + s.listener = rtmpListener + s.server = srv + s.running = true + s.mu.Unlock() + + if s.logger != nil { + s.logger.Info("rtmp listener started", slog.String("addr", rtmpListener.Addr().String())) + } + s.wg.Add(1) + go s.acceptLoop(ctx, rtmpListener, false) + + if s.tlsConfig != nil && s.rtmpsAddr != "" { + tlsListener, err := tls.Listen("tcp", s.rtmpsAddr, s.tlsConfig) + if err != nil { + _ = rtmpListener.Close() + return fmt.Errorf("failed to start rtmps listener: %w", err) + } + s.mu.Lock() + s.tlsListener = tlsListener + s.mu.Unlock() + + if s.logger != nil { + s.logger.Info("rtmps listener started", slog.String("addr", tlsListener.Addr().String())) + } + s.wg.Add(1) + go s.acceptLoop(ctx, tlsListener, true) + } + + return nil +} + +func (s *RTMPServer) acceptLoop(ctx context.Context, ln net.Listener, secure bool) { + defer s.wg.Done() + for { + conn, err := ln.Accept() + if err != nil { + select { + case <-ctx.Done(): + return + default: + } + if errors.Is(err, net.ErrClosed) { + return + } + if s.logger != nil { + s.logger.Warn("rtmp accept error", slog.String("err", err.Error()), slog.Bool("secure", secure)) + } + continue + } + go s.server.HandleNetConn(conn) + } +} + +func (s *RTMPServer) Close(ctx context.Context) error { + s.mu.Lock() + if !s.running { + s.mu.Unlock() + return nil + } + s.running = false + rtmpListener := s.listener + tlsListener := s.tlsListener + s.mu.Unlock() + + if rtmpListener != nil { + _ = rtmpListener.Close() + } + if tlsListener != nil { + _ = tlsListener.Close() + } + + done := make(chan struct{}) + go func() { + defer close(done) + s.wg.Wait() + }() + + select { + case <-done: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} + +func (s *RTMPServer) EnsureStream(path string) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.streams[path]; !ok { + s.streams[path] = &pubsub.PubSub{} + } +} + +func (s *RTMPServer) IngestURL(streamPath string) string { + port := s.listenPort(s.rtmpAddr, "1935") + hostPort := net.JoinHostPort("127.0.0.1", port) + return fmt.Sprintf("rtmp://%s/%s", hostPort, strings.TrimPrefix(streamPath, "/")) +} + +func (s *RTMPServer) PlaybackURLs(host string, streamPath string) (rtmpURL *string, rtmpsURL *string) { + cleanHost := stripPort(host) + if cleanHost == "" { + cleanHost = "127.0.0.1" + } + streamPath = strings.TrimPrefix(streamPath, "/") + + if s.rtmpAddr != "" { + port := s.listenPort(s.rtmpAddr, "1935") + hostPort := net.JoinHostPort(cleanHost, port) + url := fmt.Sprintf("rtmp://%s/%s", hostPort, streamPath) + rtmpURL = &url + } + + s.mu.Lock() + tlsListener := s.tlsListener + s.mu.Unlock() + if tlsListener != nil && s.rtmpsAddr != "" { + port := s.listenPort(s.rtmpsAddr, "1936") + hostPort := net.JoinHostPort(cleanHost, port) + url := fmt.Sprintf("rtmps://%s/%s", hostPort, streamPath) + rtmpsURL = &url + } + + return rtmpURL, rtmpsURL +} + +func (s *RTMPServer) handleConn(ctx context.Context, c *rtmp.Conn, nc net.Conn) { + if err := c.Prepare(rtmp.StageGotPublishOrPlayCommand, 0); err != nil { + if s.logger != nil { + s.logger.Error("rtmp handshake failed", slog.String("err", err.Error())) + } + _ = nc.Close() + return + } + + path := strings.TrimPrefix(c.URL.Path, "/") + if path == "" { + path = "live/default" + } + + ps := s.ensurePubSub(path) + if c.Publishing { + if s.logger != nil { + s.logger.Info("rtmp publisher connected", slog.String("path", path)) + } + go func() { + ps.SetPub(c) + _ = nc.Close() + if s.logger != nil { + s.logger.Info("rtmp publisher disconnected", slog.String("path", path)) + } + }() + } else { + if s.logger != nil { + s.logger.Info("rtmp subscriber connected", slog.String("path", path)) + } + done := make(chan bool, 1) + go func() { + <-c.CloseNotify() + close(done) + if s.logger != nil { + s.logger.Info("rtmp subscriber disconnected", slog.String("path", path)) + } + }() + go ps.AddSub(done, c) + } +} + +func (s *RTMPServer) ensurePubSub(path string) *pubsub.PubSub { + s.mu.Lock() + defer s.mu.Unlock() + + ps, ok := s.streams[path] + if !ok { + ps = &pubsub.PubSub{} + s.streams[path] = ps + } + return ps +} + +func (s *RTMPServer) listenPort(addr string, defaultPort string) string { + _, port, err := net.SplitHostPort(addr) + if err == nil && port != "" { + return port + } + if strings.HasPrefix(addr, ":") { + return strings.TrimPrefix(addr, ":") + } + return defaultPort +} + +func stripPort(hostport string) string { + host, _, err := net.SplitHostPort(hostport) + if err != nil { + return hostport + } + return host +} + +// SelfSignedTLSConfig generates a minimal self-signed TLS config for RTMPS. +func SelfSignedTLSConfig() (*tls.Config, error) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, fmt.Errorf("generate key: %w", err) + } + + serial, err := rand.Int(rand.Reader, big.NewInt(1<<62)) + if err != nil { + return nil, fmt.Errorf("serial: %w", err) + } + + template := x509.Certificate{ + SerialNumber: serial, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{"localhost"}, + } + + der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return nil, fmt.Errorf("create cert: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return nil, fmt.Errorf("tls key pair: %w", err) + } + + return &tls.Config{Certificates: []tls.Certificate{cert}}, nil +} diff --git a/server/lib/stream/socket_streamer.go b/server/lib/stream/socket_streamer.go new file mode 100644 index 00000000..4bc98c6f --- /dev/null +++ b/server/lib/stream/socket_streamer.go @@ -0,0 +1,264 @@ +package stream + +import ( + "context" + "fmt" + "io" + "os" + "os/exec" + "strconv" + "sync" + "syscall" + "time" + + "github.com/coder/websocket" + + "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/scaletozero" +) + +// SocketStreamer captures the display/audio and broadcasts MPEG-TS chunks over WebSocket. +type SocketStreamer struct { + mu sync.Mutex + + id string + params Params + ffmpegPath string + + cmd *exec.Cmd + exited chan struct{} + startedAt time.Time + stz *scaletozero.Oncer + + pr *io.PipeReader + pw *io.PipeWriter + clients map[WebSocketConn]struct{} + + introBuf []byte +} + +const introMirrorLimit = 512 * 1024 + +func NewSocketStreamer(id string, params Params, ffmpegPath string, ctrl scaletozero.Controller) (*SocketStreamer, error) { + if params.FrameRate == nil || params.DisplayNum == nil { + return nil, ErrInvalidParams + } + return &SocketStreamer{ + id: id, + params: params, + ffmpegPath: ffmpegPath, + stz: scaletozero.NewOncer(ctrl), + clients: make(map[WebSocketConn]struct{}), + }, nil +} + +func (s *SocketStreamer) ID() string { + return s.id +} + +func (s *SocketStreamer) Start(ctx context.Context) error { + log := logger.FromContext(ctx) + runCtx := context.WithoutCancel(ctx) + + keyInt := *s.params.FrameRate * 2 + + s.mu.Lock() + if s.cmd != nil { + s.mu.Unlock() + return ErrStreamInProgress + } + if err := s.stz.Disable(runCtx); err != nil { + s.mu.Unlock() + return err + } + + videoInput, err := screenCaptureArgs(s.params) + if err != nil { + s.mu.Unlock() + _ = s.stz.Enable(context.Background()) + return err + } + audioInput, err := audioCaptureArgs(runCtx) + if err != nil { + s.mu.Unlock() + _ = s.stz.Enable(context.Background()) + return err + } + + args := append([]string{"-hide_banner", "-loglevel", "warning", "-nostdin"}, videoInput...) + args = append(args, audioInput...) + args = append(args, + "-c:v", "libx264", + "-preset", "veryfast", + "-tune", "zerolatency", + "-pix_fmt", "yuv420p", + "-g", strconv.Itoa(keyInt), + "-x264-params", fmt.Sprintf("keyint=%d:min-keyint=%d:scenecut=0:repeat-headers=1", keyInt, keyInt), + "-map", "0:v:0", + "-map", "1:a:0", + "-c:a", "aac", + "-b:a", "128k", + "-ar", "44100", + "-ac", "2", + "-use_wallclock_as_timestamps", "1", + "-fflags", "nobuffer", + "-mpegts_flags", "resend_headers", + "-muxdelay", "0", + "-muxpreload", "0", + "-f", "mpegts", + "pipe:1", + ) + + pr, pw := io.Pipe() + cmd := exec.CommandContext(runCtx, s.ffmpegPath, args...) + cmd.Stdout = pw + cmd.Stderr = os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + exited := make(chan struct{}) + s.cmd = cmd + s.exited = exited + s.startedAt = time.Now() + s.pr = pr + s.pw = pw + s.introBuf = nil + s.mu.Unlock() + + if err := cmd.Start(); err != nil { + s.mu.Lock() + s.cmd = nil + s.exited = nil + s.mu.Unlock() + _ = s.stz.Enable(context.Background()) + return err + } + + go s.broadcastLoop(pr) + + go func() { + _ = cmd.Wait() + close(exited) + s.mu.Lock() + defer s.mu.Unlock() + s.cmd = nil + _ = s.stz.Enable(context.Background()) + }() + + // Detect immediate failures. + select { + case <-time.After(300 * time.Millisecond): + log.Info("socket stream started", "id", s.id) + return nil + case <-exited: + return ErrStreamStartFailed + } +} + +func (s *SocketStreamer) Stop(ctx context.Context) error { + s.mu.Lock() + cmd := s.cmd + exited := s.exited + pw := s.pw + s.cmd = nil + s.exited = nil + s.pw = nil + s.pr = nil + s.mu.Unlock() + + if cmd != nil { + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) + select { + case <-exited: + case <-time.After(2 * time.Second): + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + } + if pw != nil { + _ = pw.Close() + } + + s.mu.Lock() + for c := range s.clients { + _ = c.Close(int(websocket.StatusNormalClosure), "stream stopped") + } + s.clients = make(map[WebSocketConn]struct{}) + s.introBuf = nil + s.mu.Unlock() + + return s.stz.Enable(context.WithoutCancel(ctx)) +} + +func (s *SocketStreamer) IsStreaming(ctx context.Context) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.cmd != nil +} + +func (s *SocketStreamer) Metadata() Metadata { + s.mu.Lock() + defer s.mu.Unlock() + socketURL := "" + // the HTTP handler fills the actual host, so we expose a relative path here + url := "/stream/socket/" + s.id + socketURL = url + return Metadata{ + ID: s.id, + Mode: ModeSocket, + IngestURL: "", + PlaybackURL: nil, + StartedAt: s.startedAt, + WebsocketURL: &socketURL, + } +} + +func (s *SocketStreamer) RegisterClient(conn WebSocketConn) error { + s.mu.Lock() + if s.clients == nil { + s.clients = make(map[WebSocketConn]struct{}) + } + s.clients[conn] = struct{}{} + intro := append([]byte(nil), s.introBuf...) + s.mu.Unlock() + + if len(intro) > 0 { + _ = conn.Write(context.Background(), int(websocket.MessageBinary), intro) + } + + return nil +} + +func (s *SocketStreamer) broadcastLoop(r io.Reader) { + buf := make([]byte, 32*1024) + for { + n, err := r.Read(buf) + if n > 0 { + chunk := buf[:n] + s.writeChunk(chunk) + } + if err != nil { + return + } + } +} + +func (s *SocketStreamer) writeChunk(chunk []byte) { + s.mu.Lock() + defer s.mu.Unlock() + + if len(s.introBuf) < introMirrorLimit { + remaining := introMirrorLimit - len(s.introBuf) + if remaining > len(chunk) { + remaining = len(chunk) + } + if remaining > 0 { + s.introBuf = append(s.introBuf, chunk[:remaining]...) + } + } + + for c := range s.clients { + if err := c.Write(context.Background(), int(websocket.MessageBinary), chunk); err != nil { + _ = c.Close(int(websocket.StatusInternalError), "write failed") + delete(s.clients, c) + } + } +} diff --git a/server/lib/stream/socket_streamer_test.go b/server/lib/stream/socket_streamer_test.go new file mode 100644 index 00000000..70b3d471 --- /dev/null +++ b/server/lib/stream/socket_streamer_test.go @@ -0,0 +1,58 @@ +package stream + +import ( + "context" + "testing" + "time" + + "github.com/coder/websocket" + "github.com/onkernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/require" +) + +func TestSocketStreamerMetadataAndRegisterClient(t *testing.T) { + t.Parallel() + + fr := 30 + display := 0 + streamer, err := NewSocketStreamer("sock", Params{FrameRate: &fr, DisplayNum: &display, Mode: ModeSocket}, "ffmpeg", scaletozero.NewNoopController()) + require.NoError(t, err) + + meta := streamer.Metadata() + require.Equal(t, ModeSocket, meta.Mode) + require.NotNil(t, meta.WebsocketURL) + require.Equal(t, "/stream/socket/sock", *meta.WebsocketURL) + + conn := &mockWebSocketConn{} + require.NoError(t, streamer.RegisterClient(conn)) + require.Len(t, conn.writes, 1) + require.Equal(t, websocket.MessageText, conn.writes[0].messageType) + require.Equal(t, "mpegts", string(conn.writes[0].data)) +} + +type wsWrite struct { + messageType websocket.MessageType + data []byte +} + +type mockWebSocketConn struct { + writes []wsWrite +} + +func (m *mockWebSocketConn) Read(ctx context.Context) (int, []byte, error) { + select { + case <-ctx.Done(): + return 0, nil, ctx.Err() + case <-time.After(10 * time.Millisecond): + return 0, nil, ctx.Err() + } +} + +func (m *mockWebSocketConn) Write(ctx context.Context, messageType int, data []byte) error { + m.writes = append(m.writes, wsWrite{messageType: websocket.MessageType(messageType), data: data}) + return nil +} + +func (m *mockWebSocketConn) Close(status int, reason string) error { + return nil +} diff --git a/server/lib/stream/stream.go b/server/lib/stream/stream.go new file mode 100644 index 00000000..4cadcd4e --- /dev/null +++ b/server/lib/stream/stream.go @@ -0,0 +1,86 @@ +package stream + +import ( + "context" + "time" +) + +type Mode string + +const ( + ModeInternal Mode = "internal" + ModeRemote Mode = "remote" + ModeWebRTC Mode = "webrtc" + ModeSocket Mode = "socket" +) + +// Params holds stream creation settings. +type Params struct { + FrameRate *int + DisplayNum *int + IngestURL string + Mode Mode + PlaybackURL *string + SecurePlaybackURL *string +} + +// Metadata describes a running stream. +type Metadata struct { + ID string + Mode Mode + IngestURL string + PlaybackURL *string + SecurePlaybackURL *string + StartedAt time.Time + WebsocketURL *string + WebRTCOfferURL *string +} + +// Streamer defines the interface for a streaming session. +type Streamer interface { + ID() string + Start(ctx context.Context) error + Stop(ctx context.Context) error + IsStreaming(ctx context.Context) bool + Metadata() Metadata +} + +// WebSocketEndpoint allows clients to register for chunked playback. +type WebSocketEndpoint interface { + Streamer + RegisterClient(conn WebSocketConn) error +} + +// WebSocketConn mirrors the subset of websocket.Conn needed by streamers to remain decoupled. +type WebSocketConn interface { + Read(ctx context.Context) (messageType int, p []byte, err error) + Write(ctx context.Context, messageType int, data []byte) error + Close(status int, reason string) error +} + +// WebRTCNegotiator supports SDP offer/answer exchange for live streaming. +type WebRTCNegotiator interface { + Streamer + HandleOffer(ctx context.Context, offer string) (string, error) +} + +// Manager defines the interface for tracking streaming sessions. +type Manager interface { + GetStream(id string) (Streamer, bool) + ListStreams(ctx context.Context) []Streamer + RegisterStream(ctx context.Context, streamer Streamer) error + DeregisterStream(ctx context.Context, streamer Streamer) error + StopAll(ctx context.Context) error +} + +// FFmpegStreamerFactory returns a Streamer configured with the provided id and params. +type FFmpegStreamerFactory func(id string, params Params) (Streamer, error) + +// InternalServer represents the internal RTMP(S) server used for live streaming. +type InternalServer interface { + Start(ctx context.Context) error + EnsureStream(path string) + IngestURL(streamPath string) string + PlaybackURLs(host string, streamPath string) (rtmpURL *string, rtmpsURL *string) + Close(ctx context.Context) error +} diff --git a/server/lib/stream/webrtc_streamer.go b/server/lib/stream/webrtc_streamer.go new file mode 100644 index 00000000..697aa552 --- /dev/null +++ b/server/lib/stream/webrtc_streamer.go @@ -0,0 +1,335 @@ +package stream + +import ( + "context" + "fmt" + "net" + "os" + "os/exec" + "strconv" + "sync" + "syscall" + "time" + + "github.com/pion/rtp" + "github.com/pion/webrtc/v4" + + "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/scaletozero" +) + +// WebRTCStreamer captures the display/audio and publishes them to WebRTC peers. +type WebRTCStreamer struct { + mu sync.Mutex + + id string + params Params + ffmpegPath string + + videoTrack *webrtc.TrackLocalStaticRTP + audioTrack *webrtc.TrackLocalStaticRTP + + videoConn *net.UDPConn + audioConn *net.UDPConn + + cmd *exec.Cmd + exited chan struct{} + stz *scaletozero.Oncer + startedAt time.Time + + cancel context.CancelFunc + peers map[*webrtc.PeerConnection]struct{} +} + +// NewWebRTCStreamer constructs a WebRTC streamer for the given params. +func NewWebRTCStreamer(id string, params Params, ffmpegPath string, ctrl scaletozero.Controller) (*WebRTCStreamer, error) { + if params.FrameRate == nil || params.DisplayNum == nil { + return nil, ErrInvalidParams + } + return &WebRTCStreamer{ + id: id, + params: params, + ffmpegPath: ffmpegPath, + stz: scaletozero.NewOncer(ctrl), + peers: make(map[*webrtc.PeerConnection]struct{}), + }, nil +} + +func (w *WebRTCStreamer) ID() string { return w.id } + +func (w *WebRTCStreamer) Start(ctx context.Context) error { + log := logger.FromContext(ctx) + runCtx := context.WithoutCancel(ctx) + + w.mu.Lock() + if w.cmd != nil { + w.mu.Unlock() + return ErrStreamInProgress + } + if err := w.stz.Disable(runCtx); err != nil { + w.mu.Unlock() + return err + } + + videoTrack, err := webrtc.NewTrackLocalStaticRTP( + webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeVP8, ClockRate: 90000}, + "video", "display", + ) + if err != nil { + w.mu.Unlock() + _ = w.stz.Enable(context.Background()) + return err + } + audioTrack, err := webrtc.NewTrackLocalStaticRTP( + webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2}, + "audio", "display", + ) + if err != nil { + w.mu.Unlock() + _ = w.stz.Enable(context.Background()) + return err + } + + videoConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + w.mu.Unlock() + _ = w.stz.Enable(context.Background()) + return err + } + audioConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + videoConn.Close() + w.mu.Unlock() + _ = w.stz.Enable(context.Background()) + return err + } + + videoPort := videoConn.LocalAddr().(*net.UDPAddr).Port + audioPort := audioConn.LocalAddr().(*net.UDPAddr).Port + + ctx, cancel := context.WithCancel(runCtx) + + videoInput, err := screenCaptureArgs(w.params) + if err != nil { + cancel() + w.mu.Unlock() + _ = w.stz.Enable(context.Background()) + return err + } + audioInput, err := audioCaptureArgs(ctx) + if err != nil { + cancel() + w.mu.Unlock() + _ = w.stz.Enable(context.Background()) + return err + } + + args := append([]string{"-hide_banner", "-loglevel", "warning", "-nostdin"}, videoInput...) + args = append(args, audioInput...) + args = append(args, + "-map", "0:v:0", + "-map", "1:a:0", + "-c:v", "libvpx", + "-b:v", "2M", + "-g", strconv.Itoa(*w.params.FrameRate*2), + "-pix_fmt", "yuv420p", + "-c:a", "libopus", + "-b:a", "128k", + "-ar", "48000", + "-ac", "2", + "-f", "tee", + fmt.Sprintf("[select=v:f=rtp:payload_type=96]rtp://127.0.0.1:%d|[select=a:f=rtp:payload_type=111]rtp://127.0.0.1:%d", videoPort, audioPort), + ) + + cmd := exec.CommandContext(ctx, w.ffmpegPath, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + + exited := make(chan struct{}) + w.cmd = cmd + w.exited = exited + w.videoTrack = videoTrack + w.audioTrack = audioTrack + w.videoConn = videoConn + w.audioConn = audioConn + w.cancel = cancel + w.startedAt = time.Now() + w.mu.Unlock() + + if err := cmd.Start(); err != nil { + w.mu.Lock() + w.cmd = nil + w.exited = nil + w.videoConn.Close() + w.audioConn.Close() + w.mu.Unlock() + cancel() + _ = w.stz.Enable(context.Background()) + return err + } + + go w.forwardRTP(ctx, videoConn, videoTrack) + go w.forwardRTP(ctx, audioConn, audioTrack) + + go func() { + _ = cmd.Wait() + close(exited) + cancel() + w.mu.Lock() + defer w.mu.Unlock() + for pc := range w.peers { + _ = pc.Close() + } + w.peers = make(map[*webrtc.PeerConnection]struct{}) + w.cmd = nil + _ = w.stz.Enable(context.Background()) + }() + + select { + case <-time.After(300 * time.Millisecond): + log.Info("webrtc stream started", "id", w.id, "video_port", videoPort, "audio_port", audioPort) + return nil + case <-exited: + return ErrStreamStartFailed + } +} + +func (w *WebRTCStreamer) Stop(ctx context.Context) error { + w.mu.Lock() + cmd := w.cmd + exited := w.exited + videoConn := w.videoConn + audioConn := w.audioConn + cancel := w.cancel + w.cmd = nil + w.exited = nil + w.videoConn = nil + w.audioConn = nil + w.cancel = nil + peers := w.peers + w.peers = make(map[*webrtc.PeerConnection]struct{}) + w.mu.Unlock() + + if cancel != nil { + cancel() + } + for pc := range peers { + _ = pc.Close() + } + if cmd != nil { + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) + select { + case <-exited: + case <-time.After(2 * time.Second): + _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } + } + if videoConn != nil { + _ = videoConn.Close() + } + if audioConn != nil { + _ = audioConn.Close() + } + return w.stz.Enable(context.WithoutCancel(ctx)) +} + +func (w *WebRTCStreamer) IsStreaming(ctx context.Context) bool { + w.mu.Lock() + defer w.mu.Unlock() + return w.cmd != nil +} + +func (w *WebRTCStreamer) Metadata() Metadata { + w.mu.Lock() + defer w.mu.Unlock() + offerURL := "/stream/webrtc/offer" + return Metadata{ + ID: w.id, + Mode: ModeWebRTC, + IngestURL: "", + StartedAt: w.startedAt, + WebRTCOfferURL: &offerURL, + } +} + +// HandleOffer attaches a new peer to the running stream and returns the SDP answer. +func (w *WebRTCStreamer) HandleOffer(ctx context.Context, offer string) (string, error) { + w.mu.Lock() + if w.videoTrack == nil && w.audioTrack == nil { + w.mu.Unlock() + return "", fmt.Errorf("webrtc stream not ready") + } + pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + w.mu.Unlock() + return "", err + } + if w.videoTrack != nil { + if _, err := pc.AddTrack(w.videoTrack); err != nil { + w.mu.Unlock() + return "", err + } + } + if w.audioTrack != nil { + if _, err := pc.AddTrack(w.audioTrack); err != nil { + w.mu.Unlock() + return "", err + } + } + w.peers[pc] = struct{}{} + w.mu.Unlock() + + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + if state == webrtc.PeerConnectionStateFailed || state == webrtc.PeerConnectionStateClosed || state == webrtc.PeerConnectionStateDisconnected { + w.mu.Lock() + delete(w.peers, pc) + w.mu.Unlock() + _ = pc.Close() + } + }) + + if err := pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: offer}); err != nil { + return "", err + } + answer, err := pc.CreateAnswer(nil) + if err != nil { + return "", err + } + gatherComplete := webrtc.GatheringCompletePromise(pc) + if err := pc.SetLocalDescription(answer); err != nil { + return "", err + } + select { + case <-gatherComplete: + case <-ctx.Done(): + return "", ctx.Err() + } + local := pc.LocalDescription() + if local == nil { + return "", fmt.Errorf("local description missing") + } + return local.SDP, nil +} + +func (w *WebRTCStreamer) forwardRTP(ctx context.Context, conn *net.UDPConn, track *webrtc.TrackLocalStaticRTP) { + buf := make([]byte, 1500) + for { + select { + case <-ctx.Done(): + return + default: + } + n, _, err := conn.ReadFrom(buf) + if err != nil { + return + } + var pkt rtp.Packet + if err := pkt.Unmarshal(buf[:n]); err != nil { + continue + } + if err := track.WriteRTP(&pkt); err != nil { + return + } + } +} diff --git a/server/lib/stream/webrtc_streamer_test.go b/server/lib/stream/webrtc_streamer_test.go new file mode 100644 index 00000000..bfe09148 --- /dev/null +++ b/server/lib/stream/webrtc_streamer_test.go @@ -0,0 +1,26 @@ +package stream + +import ( + "context" + "testing" + + "github.com/onkernel/kernel-images/server/lib/scaletozero" + "github.com/stretchr/testify/require" +) + +func TestWebRTCStreamerMetadataAndHandleOfferGuard(t *testing.T) { + t.Parallel() + + fr := 30 + display := 0 + streamer, err := NewWebRTCStreamer("webrtc", Params{FrameRate: &fr, DisplayNum: &display, Mode: ModeWebRTC}, "ffmpeg", scaletozero.NewNoopController()) + require.NoError(t, err) + + meta := streamer.Metadata() + require.Equal(t, ModeWebRTC, meta.Mode) + require.NotNil(t, meta.WebRTCOfferURL) + require.Equal(t, "/stream/webrtc/offer", *meta.WebRTCOfferURL) + + _, err = streamer.HandleOffer(context.Background(), "bad") + require.ErrorContains(t, err, "webrtc stream not ready") +} diff --git a/server/lib/virtualinputs/manager.go b/server/lib/virtualinputs/manager.go new file mode 100644 index 00000000..a5bf35f5 --- /dev/null +++ b/server/lib/virtualinputs/manager.go @@ -0,0 +1,1169 @@ +package virtualinputs + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + "time" + + "github.com/onkernel/kernel-images/server/lib/logger" + "github.com/onkernel/kernel-images/server/lib/scaletozero" +) + +const ( + defaultWidth = 1280 + defaultHeight = 720 + defaultFrameRate = 30 + + modeDevice = "device" + modeVirtualFile = "virtual-file" + + defaultVideoFile = "/tmp/virtual-inputs/video.y4m" + defaultAudioFile = "/tmp/virtual-inputs/audio.wav" + defaultVideoPipe = "/tmp/virtual-inputs/ingest-video.pipe" + defaultAudioPipe = "/tmp/virtual-inputs/ingest-audio.pipe" + + stateIdle = "idle" + stateRunning = "running" + statePaused = "paused" +) + +var ( + ErrMissingSources = errors.New("either video or audio source must be provided") + ErrVideoURLRequired = errors.New("video URL must be provided when video source is set") + ErrAudioURLRequired = errors.New("audio URL must be provided when audio source is set") + ErrVideoTypeRequired = errors.New("video source type is required") + ErrAudioTypeRequired = errors.New("audio source type is required") + ErrUnsupportedVideo = errors.New("unsupported video format for realtime ingest") + ErrUnsupportedAudio = errors.New("unsupported audio format for realtime ingest") + + ErrPauseWithoutSession = errors.New("no active virtual input session to pause") + ErrNoConfigToPause = errors.New("no previous configuration to pause") + ErrNoConfigToResume = errors.New("no virtual input configuration to resume") +) + +// SourceType enumerates supported input types. +type SourceType string + +const ( + SourceTypeStream SourceType = "stream" + SourceTypeFile SourceType = "file" + SourceTypeSocket SourceType = "socket" + SourceTypeWebRTC SourceType = "webrtc" +) + +// AudioDestination specifies where to route virtual input audio. +type AudioDestination string + +const ( + // AudioDestinationMicrophone routes audio to the virtual microphone input (default). + // Applications reading from the virtual mic will receive this audio. + AudioDestinationMicrophone AudioDestination = "microphone" + // AudioDestinationSpeaker routes audio directly to the container's audio output. + // Use this for monitoring/playback purposes. + AudioDestinationSpeaker AudioDestination = "speaker" +) + +// MediaSource represents a single audio or video input definition. +type MediaSource struct { + Type SourceType + URL string + // Format hints the expected container/codec when the source is a socket or WebRTC feed + // (e.g. "wav" for audio sockets, "mpegts" for video sockets, "ivf"/"ogg" for WebRTC). + Format string + // Destination specifies where to route audio (only applicable for audio sources). + // "microphone" routes to virtual mic input (default), "speaker" routes to audio output. + Destination AudioDestination +} + +// Config describes the desired virtual input pipeline. +type Config struct { + Video *MediaSource + Audio *MediaSource + Width int + Height int + FrameRate int +} + +// Status reports the current pipeline state. +type Status struct { + State string + VideoDevice string + AudioSink string + MicrophoneSource string + Video *MediaSource + Audio *MediaSource + Width int + Height int + FrameRate int + StartedAt *time.Time + LastError string + Mode string + VideoFile string + AudioFile string + Ingest *IngestStatus +} + +// IngestEndpoint describes how callers can push realtime media into the pipelines. +type IngestEndpoint struct { + Protocol string + Format string + Path string + Destination AudioDestination // Only applicable for audio endpoints +} + +// IngestStatus surfaces the ingest endpoints for audio/video when socket or WebRTC sources are active. +type IngestStatus struct { + Video *IngestEndpoint + Audio *IngestEndpoint +} + +// Manager coordinates FFmpeg pipelines that feed virtual camera and microphone devices. +type Manager struct { + mu sync.Mutex + + ffmpegPath string + videoDevice string + audioSink string + microphoneSource string + + defaultWidth int + defaultHeight int + defaultFrameRate int + + cmd *exec.Cmd + exited chan struct{} + processGroupID int + lastError string + lastCfg *Config + state string + mode string + startedAt *time.Time + videoFile string + audioFile string + audioPipe string + videoPipe string + ingest *IngestStatus + videoKeepalive *os.File + audioKeepalive *os.File + + stz *scaletozero.Oncer + scaleDisabled bool + execCommand func(name string, arg ...string) *exec.Cmd +} + +// NewManager builds a Manager with sensible defaults and optional overrides. +func NewManager(ffmpegPath, videoDevice, audioSink, microphoneSource string, width, height, frameRate int, stz scaletozero.Controller) *Manager { + if ffmpegPath == "" { + ffmpegPath = "ffmpeg" + } + if videoDevice == "" { + videoDevice = "/dev/video20" + } + if audioSink == "" { + audioSink = "audio_input" + } + if microphoneSource == "" { + microphoneSource = "microphone" + } + if width <= 0 { + width = defaultWidth + } + if height <= 0 { + height = defaultHeight + } + if frameRate <= 0 { + frameRate = defaultFrameRate + } + + return &Manager{ + ffmpegPath: ffmpegPath, + videoDevice: videoDevice, + audioSink: audioSink, + microphoneSource: microphoneSource, + defaultWidth: width, + defaultHeight: height, + defaultFrameRate: frameRate, + state: stateIdle, + mode: modeDevice, + stz: scaletozero.NewOncer(stz), + execCommand: exec.Command, + } +} + +// Configure starts (or restarts) the pipeline with the provided sources. +// When startPaused is true, silence/black frames are pushed instead of the real inputs. +func (m *Manager) Configure(ctx context.Context, cfg Config, startPaused bool) (Status, error) { + log := logger.FromContext(ctx) + + m.mu.Lock() + defer m.mu.Unlock() + + normalized, err := m.normalizeConfig(cfg) + if err != nil { + return m.statusLocked(), err + } + + if err := m.stopLocked(ctx); err != nil { + return m.statusLocked(), err + } + + useVirtualFileMode := false + if usesRealtimeSource(normalized) { + useVirtualFileMode = true + } + // Avoid accidentally routing injected audio into the playback sink; force the + // dedicated virtual input sink when the configured sink is the output sink. + if m.audioSink == "audio_output" { + log.Warn("forcing virtual input audio sink to avoid leaking into output sink") + m.audioSink = "audio_input" + } + if normalized.Video != nil && normalized.Video.Type != SourceTypeSocket && normalized.Video.Type != SourceTypeWebRTC { + if ok, err := m.ensureVideoDevice(ctx); err != nil || !ok { + useVirtualFileMode = true + log.Warn("v4l2loopback unavailable, using virtual capture files instead", "err", err) + } + } + if err := m.ensurePulseDevices(ctx); err != nil { + return m.statusLocked(), err + } + + m.setDefaultPulseDevices(ctx) + + m.killAllFFmpeg() + + if useVirtualFileMode { + m.mode = modeVirtualFile + m.audioFile = "" + m.videoFile = "" + // Only set video/audio files for non-realtime sources (file/stream). + // WebSocket and WebRTC sources use the virtual feed page instead of a Y4M file, + // so we skip setting videoFile to avoid --use-file-for-fake-video-capture. + if normalized.Video != nil && normalized.Video.Type != SourceTypeSocket && normalized.Video.Type != SourceTypeWebRTC { + m.videoFile = defaultVideoFile + } + if normalized.Audio != nil && normalized.Audio.Type != SourceTypeSocket && normalized.Audio.Type != SourceTypeWebRTC { + m.audioFile = defaultAudioFile + } + captureDir := filepath.Dir(defaultVideoFile) + if err := os.MkdirAll(captureDir, 0o755); err != nil { + return m.statusLocked(), fmt.Errorf("prepare virtual capture dir: %w", err) + } + if m.videoFile != "" { + _ = os.Remove(m.videoFile) + } + if m.audioFile != "" { + _ = os.Remove(m.audioFile) + } + } else { + m.mode = modeDevice + m.videoFile = "" + m.audioFile = "" + } + + m.ingest = buildIngestStatus(normalized) + // For realtime sources, we don't need pipes - data goes directly to broadcaster/pulse + // Only create pipes for non-realtime sources that FFmpeg will read from + m.videoPipe = "" + m.audioPipe = "" + + args, err := m.buildFFmpegArgs(normalized, startPaused) + if err != nil { + return m.statusLocked(), err + } + + // Only start FFmpeg if we have args (non-realtime sources or paused mode) + if args != nil { + if err := m.startFFmpegLocked(ctx, args); err != nil { + return m.statusLocked(), err + } + } + + log.Info("virtual inputs started", "state", func() string { + if startPaused { + return statePaused + } + return stateRunning + }(), "video_device", m.videoDevice, "audio_sink", m.audioSink, "mode", m.mode, "video_file", m.videoFile, "audio_file", m.audioFile, "ffmpeg", args != nil) + + m.lastCfg = &normalized + if startPaused { + m.state = statePaused + } else { + m.state = stateRunning + } + now := time.Now() + m.startedAt = &now + m.lastError = "" + return m.statusLocked(), nil +} + +// Pause replaces active inputs with silence/black while keeping devices alive. +// For realtime sources, this stops the realtime ingest but keeps the configuration. +func (m *Manager) Pause(ctx context.Context) (Status, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.state == stateIdle && m.lastCfg == nil { + return m.statusLocked(), ErrPauseWithoutSession + } + if m.state == statePaused { + return m.statusLocked(), nil + } + if m.lastCfg == nil { + return m.statusLocked(), ErrNoConfigToPause + } + + if err := m.stopLocked(ctx); err != nil { + return m.statusLocked(), err + } + m.killAllFFmpeg() + m.setDefaultPulseDevices(ctx) + + // For paused mode, we send black frames/silence via FFmpeg + args, err := m.buildFFmpegArgs(*m.lastCfg, true) + if err != nil { + return m.statusLocked(), err + } + + // Paused mode always has FFmpeg args (for black frames) + if args != nil { + if err := m.startFFmpegLocked(ctx, args); err != nil { + return m.statusLocked(), err + } + } + + now := time.Now() + m.startedAt = &now + m.state = statePaused + m.lastError = "" + return m.statusLocked(), nil +} + +// Resume restarts the last configuration with live inputs. +// For realtime sources (socket/webrtc), this just sets the state to running +// since the actual media comes from websocket/webrtc connections, not FFmpeg. +func (m *Manager) Resume(ctx context.Context) (Status, error) { + m.mu.Lock() + defer m.mu.Unlock() + if m.lastCfg == nil { + return m.statusLocked(), ErrNoConfigToResume + } + if m.state == stateRunning { + return m.statusLocked(), nil + } + + if err := m.stopLocked(ctx); err != nil { + return m.statusLocked(), err + } + m.killAllFFmpeg() + m.setDefaultPulseDevices(ctx) + + args, err := m.buildFFmpegArgs(*m.lastCfg, false) + if err != nil { + return m.statusLocked(), err + } + + // Only start FFmpeg if we have args (non-realtime sources) + if args != nil { + if err := m.startFFmpegLocked(ctx, args); err != nil { + return m.statusLocked(), err + } + } + + now := time.Now() + m.startedAt = &now + m.state = stateRunning + m.lastError = "" + return m.statusLocked(), nil +} + +// Stop terminates any running pipeline and clears state. +func (m *Manager) Stop(ctx context.Context) (Status, error) { + m.mu.Lock() + defer m.mu.Unlock() + if err := m.stopLocked(ctx); err != nil { + return m.statusLocked(), err + } + if err := m.ensureNoFFmpeg(); err != nil { + m.lastError = err.Error() + return m.statusLocked(), err + } + m.state = stateIdle + m.startedAt = nil + m.lastError = "" + m.lastCfg = nil + m.mode = modeDevice + m.videoFile = "" + m.audioFile = "" + m.videoPipe = "" + m.audioPipe = "" + m.ingest = nil + return m.statusLocked(), nil +} + +// Status returns the current status snapshot. +func (m *Manager) Status(_ context.Context) Status { + m.mu.Lock() + defer m.mu.Unlock() + return m.statusLocked() +} + +func (m *Manager) statusLocked() Status { + status := Status{ + State: m.state, + VideoDevice: m.videoDevice, + AudioSink: m.audioSink, + MicrophoneSource: m.microphoneSource, + LastError: m.lastError, + Width: m.defaultWidth, + Height: m.defaultHeight, + FrameRate: m.defaultFrameRate, + StartedAt: m.startedAt, + Mode: m.mode, + VideoFile: m.videoFile, + AudioFile: m.audioFile, + Ingest: m.ingest, + } + if m.lastCfg != nil { + status.Width = m.lastCfg.Width + status.Height = m.lastCfg.Height + status.FrameRate = m.lastCfg.FrameRate + status.Video = cloneSource(m.lastCfg.Video) + status.Audio = cloneSource(m.lastCfg.Audio) + } + return status +} + +func (m *Manager) normalizeConfig(cfg Config) (Config, error) { + if cfg.Video == nil && cfg.Audio == nil { + return Config{}, ErrMissingSources + } + + if cfg.Video != nil { + cfg.Video.Type = normalizeSourceType(cfg.Video.Type) + } + if cfg.Audio != nil { + cfg.Audio.Type = normalizeSourceType(cfg.Audio.Type) + } + + if cfg.Video != nil && cfg.Video.Type == "" { + return Config{}, ErrVideoTypeRequired + } + if cfg.Audio != nil && cfg.Audio.Type == "" { + return Config{}, ErrAudioTypeRequired + } + if cfg.Video != nil && cfg.Video.URL == "" && cfg.Video.Type != SourceTypeSocket && cfg.Video.Type != SourceTypeWebRTC { + return Config{}, ErrVideoURLRequired + } + if cfg.Audio != nil && cfg.Audio.URL == "" && cfg.Audio.Type != SourceTypeSocket && cfg.Audio.Type != SourceTypeWebRTC { + return Config{}, ErrAudioURLRequired + } + + out := cfg + out.Video = normalizeSource(cfg.Video, true) + out.Audio = normalizeSource(cfg.Audio, false) + if err := validateRealtimeFormat(out.Video, true); err != nil { + return Config{}, err + } + if err := validateRealtimeFormat(out.Audio, false); err != nil { + return Config{}, err + } + if out.Width <= 0 { + out.Width = m.defaultWidth + } + if out.Height <= 0 { + out.Height = m.defaultHeight + } + if out.FrameRate <= 0 { + out.FrameRate = m.defaultFrameRate + } + return out, nil +} + +func (m *Manager) stopLocked(ctx context.Context) error { + m.closePipeKeepalivesLocked() + if m.cmd == nil { + if m.processGroupID > 0 { + m.killAllFFmpeg() + if !processGroupAlive(m.processGroupID) { + m.processGroupID = 0 + } + } + return nil + } + defer m.enableScaleToZero(ctx) + + pid := m.cmd.Process.Pid + if !processAlive(pid) { + m.cmd = nil + m.exited = nil + m.state = stateIdle + if !processGroupAlive(m.processGroupID) { + m.processGroupID = 0 + } + return nil + } + + pgid, _ := syscall.Getpgid(m.cmd.Process.Pid) + if pgid > 0 { + m.processGroupID = pgid + } + killProcessGroupOrPID(pgid, pid, syscall.SIGTERM) + + waitCtx, cancel := context.WithTimeout(context.Background(), 7*time.Second) + defer cancel() + waitDone := make(chan struct{}) + go func() { + if m.exited != nil { + <-m.exited + } + close(waitDone) + }() + + select { + case <-waitDone: + case <-waitCtx.Done(): + killProcessGroupOrPID(pgid, pid, syscall.SIGKILL) + select { + case <-waitDone: + case <-time.After(2 * time.Second): + } + } + + if processAlive(pid) || processGroupAlive(m.processGroupID) { + m.killAllFFmpeg() + } + + m.cmd = nil + m.exited = nil + m.state = stateIdle + if !processGroupAlive(m.processGroupID) { + m.processGroupID = 0 + } + return nil +} + +func processAlive(pid int) bool { + if pid <= 0 { + return false + } + return syscall.Kill(pid, 0) == nil +} + +func processGroupAlive(pgid int) bool { + if pgid <= 0 { + return false + } + return syscall.Kill(-pgid, 0) == nil +} + +func killProcessGroupOrPID(pgid int, pid int, sig syscall.Signal) { + if pgid > 0 { + _ = syscall.Kill(-pgid, sig) + return + } + if pid > 0 { + _ = syscall.Kill(pid, sig) + } +} + +func (m *Manager) killAllFFmpeg() { + if m.processGroupID > 0 { + killProcessGroupOrPID(m.processGroupID, 0, syscall.SIGTERM) + } + m.killVirtualFFmpegProcesses() + time.Sleep(150 * time.Millisecond) + if m.processGroupID > 0 { + killProcessGroupOrPID(m.processGroupID, 0, syscall.SIGKILL) + } + m.killVirtualFFmpegProcesses() +} + +func (m *Manager) ensureNoFFmpeg() error { + deadline := time.Now().Add(2 * time.Second) + for { + m.killAllFFmpeg() + if !m.ownedFFmpegRunning() { + m.processGroupID = 0 + return nil + } + if time.Now().After(deadline) { + return errors.New("virtual input ffmpeg processes still running after stop") + } + time.Sleep(150 * time.Millisecond) + } +} + +func (m *Manager) ownedFFmpegRunning() bool { + if processGroupAlive(m.processGroupID) { + return true + } + procs, err := m.virtualFFmpegProcesses() + return err == nil && len(procs) > 0 +} + +type ffmpegProcess struct { + pid int + cmdline string +} + +func (m *Manager) virtualFFmpegProcesses() ([]ffmpegProcess, error) { + cmd := m.execCommand("pgrep", "-a", "ffmpeg") + var buf bytes.Buffer + cmd.Stdout = &buf + cmd.Stderr = io.Discard + err := cmd.Run() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { + return nil, nil + } + return nil, err + } + + out := []ffmpegProcess{} + for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + parts := strings.SplitN(line, " ", 2) + if len(parts) == 0 { + continue + } + pid, err := strconv.Atoi(parts[0]) + if err != nil { + continue + } + cmdline := "" + if len(parts) > 1 { + cmdline = parts[1] + } + if m.isVirtualFFmpegCommand(cmdline) { + out = append(out, ffmpegProcess{pid: pid, cmdline: cmdline}) + } + } + return out, nil +} + +func (m *Manager) killVirtualFFmpegProcesses() { + procs, err := m.virtualFFmpegProcesses() + if err != nil || len(procs) == 0 { + return + } + for _, proc := range procs { + pgid, _ := syscall.Getpgid(proc.pid) + killProcessGroupOrPID(pgid, proc.pid, syscall.SIGTERM) + } + time.Sleep(100 * time.Millisecond) + for _, proc := range procs { + pgid, _ := syscall.Getpgid(proc.pid) + killProcessGroupOrPID(pgid, proc.pid, syscall.SIGKILL) + } +} + +func (m *Manager) isVirtualFFmpegCommand(cmdline string) bool { + markers := []string{ + m.videoDevice, + m.audioSink, + m.microphoneSource, + defaultVideoFile, + defaultAudioFile, + "/tmp/virtual-inputs", + } + for _, marker := range markers { + if marker != "" && strings.Contains(cmdline, marker) { + return true + } + } + return false +} + +func (m *Manager) ensureVideoDevice(ctx context.Context) (bool, error) { + if _, err := os.Stat(m.videoDevice); err == nil { + return true, nil + } + + videoNr, err := parseVideoNumber(m.videoDevice) + if err != nil { + return false, fmt.Errorf("invalid video device path: %w", err) + } + + args := []string{ + "v4l2loopback", + fmt.Sprintf("video_nr=%d", videoNr), + "card_label=Virtual Camera", + "exclusive_caps=1", + } + cmd := m.execCommand("modprobe", args...) + var buf bytes.Buffer + cmd.Stdout = io.MultiWriter(os.Stdout, &buf) + cmd.Stderr = io.MultiWriter(os.Stderr, &buf) + if err := cmd.Run(); err != nil { + return false, fmt.Errorf("failed to load v4l2loopback: %w: %s", err, strings.TrimSpace(buf.String())) + } + + waitCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + for { + if _, err := os.Stat(m.videoDevice); err == nil { + _ = os.Chmod(m.videoDevice, 0o666) + return true, nil + } + select { + case <-waitCtx.Done(): + return false, errors.New("v4l2loopback device did not appear after modprobe") + case <-time.After(200 * time.Millisecond): + } + } +} + +func (m *Manager) ensurePulseDevices(ctx context.Context) error { + pulseEnv := append(os.Environ(), fmt.Sprintf("PULSE_SERVER=%s", os.Getenv("PULSE_SERVER"))) + + check := func(kind, name string) error { + listCmd := m.execCommand("pactl", "list", "short", kind) + listCmd.Env = pulseEnv + out, err := listCmd.Output() + if err != nil { + return fmt.Errorf("failed to query pulseaudio %s: %w", kind, err) + } + if !strings.Contains(string(out), name) { + return fmt.Errorf("pulseaudio %s %s not found", kind[:len(kind)-1], name) + } + return nil + } + + for _, checkCase := range []struct { + kind string + name string + }{ + {"sinks", m.audioSink}, + {"sources", m.microphoneSource}, + } { + if err := check(checkCase.kind, checkCase.name); err != nil { + return err + } + } + return nil +} + +func (m *Manager) setDefaultPulseDevices(ctx context.Context) { + log := logger.FromContext(ctx) + pulseEnv := append(os.Environ(), fmt.Sprintf("PULSE_SERVER=%s", os.Getenv("PULSE_SERVER"))) + + setDefault := func(kind, name string) error { + if name == "" { + return nil + } + cmd := m.execCommand("pactl", "set-default-"+kind, name) + cmd.Env = pulseEnv + return cmd.Run() + } + + if err := setDefault("source", m.microphoneSource); err != nil { + log.Warn("failed to set default pulseaudio source", "err", err, "source", m.microphoneSource) + } +} + +func (m *Manager) buildFFmpegArgs(cfg Config, paused bool) ([]string, error) { + var ( + args []string + videoIdx = -1 + audioIdx = -1 + ) + + // For realtime sources (socket/webrtc), video goes directly to the feed broadcaster + // and audio goes directly to PulseAudio. FFmpeg is not needed for these cases. + // Only build FFmpeg args for non-realtime video or non-realtime audio. + realtimeVideo := cfg.Video != nil && (cfg.Video.Type == SourceTypeSocket || cfg.Video.Type == SourceTypeWebRTC) + realtimeAudio := cfg.Audio != nil && (cfg.Audio.Type == SourceTypeSocket || cfg.Audio.Type == SourceTypeWebRTC) + + // Skip video in FFmpeg if it's a realtime source (handled by broadcaster) + needsFFmpegVideo := cfg.Video != nil && !realtimeVideo + // Skip audio in FFmpeg if it's a realtime source (will be handled by direct pulse writer) + needsFFmpegAudio := cfg.Audio != nil && !realtimeAudio + + // If neither video nor audio need FFmpeg processing, return empty args + if !needsFFmpegVideo && !needsFFmpegAudio && !paused { + return nil, nil + } + + args = append(args, "-hide_banner", "-loglevel", "warning", "-nostdin", "-fflags", "+genpts", "-threads", "2", "-y") + + // Build inputs and track indexes for mapping. + if paused { + if cfg.Video != nil { + videoIdx = 0 + args = append(args, + "-f", "lavfi", "-re", "-i", fmt.Sprintf("color=size=%dx%d:rate=%d:color=black", cfg.Width, cfg.Height, cfg.FrameRate), + ) + } + if cfg.Audio != nil { + if videoIdx == -1 { + audioIdx = 0 + } else { + audioIdx = 1 + } + args = append(args, "-f", "lavfi", "-i", "anullsrc=channel_layout=stereo:sample_rate=48000") + } + } else { + shared := sourcesShared(cfg) + if needsFFmpegVideo { + videoIdx = 0 + args = append(args, buildInputArgs(cfg.Video)...) + } + if needsFFmpegAudio { + if shared && needsFFmpegVideo { + audioIdx = videoIdx + } else { + if videoIdx == -1 { + audioIdx = 0 + } else { + audioIdx = 1 + } + args = append(args, buildInputArgs(cfg.Audio)...) + } + } + } + + if needsFFmpegVideo && videoIdx == -1 && !paused { + return nil, errors.New("video mapping requested without input") + } + if needsFFmpegAudio && audioIdx == -1 && !paused { + return nil, errors.New("audio mapping requested without input") + } + + // Only add video output if we have video input for FFmpeg + if (needsFFmpegVideo || paused) && videoIdx >= 0 { + args = append(args, + "-map", fmt.Sprintf("%d:v:0", videoIdx), + "-vf", fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease,pad=%d:%d:(ow-iw)/2:(oh-ih)/2", cfg.Width, cfg.Height, cfg.Width, cfg.Height), + "-pix_fmt", "yuv420p", + "-r", strconv.Itoa(cfg.FrameRate), + ) + if m.mode == modeVirtualFile && m.videoFile != "" { + args = append(args, + "-f", "yuv4mpegpipe", + m.videoFile, + ) + } else { + args = append(args, + "-f", "v4l2", + m.videoDevice, + ) + } + } + + // Only add audio output if we have audio input for FFmpeg + if (needsFFmpegAudio || paused) && audioIdx >= 0 { + // Only route audio into Pulse when using a v4l2loopback device; in virtual-file + // mode Chromium consumes the WAV via --use-file-for-fake-audio-capture, so + // sending audio to a sink risks leaking it to the output path. + routeToPulse := m.mode != modeVirtualFile + if routeToPulse { + // Determine the audio sink based on destination setting + sink := m.audioSink + if cfg.Audio != nil && cfg.Audio.Destination == AudioDestinationSpeaker { + sink = "audio_output" + } + args = append(args, + "-map", fmt.Sprintf("%d:a:0", audioIdx), + "-ac", "2", + "-ar", "48000", + "-f", "pulse", + "-device", sink, + "pulse", + ) + } + if m.audioFile != "" { + args = append(args, + "-map", fmt.Sprintf("%d:a:0", audioIdx), + "-ac", "2", + "-ar", "48000", + "-f", "wav", + m.audioFile, + ) + } + } + + return args, nil +} + +func buildInputArgs(src *MediaSource) []string { + var parts []string + if src == nil { + return parts + } + if src.Type == SourceTypeStream { + parts = append(parts, "-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "2") + } + parts = append(parts, "-thread_queue_size", "64") + if src.Type == SourceTypeFile { + parts = append(parts, "-re") + } + if src.Type == SourceTypeSocket || src.Type == SourceTypeWebRTC { + if src.Format != "" { + parts = append(parts, "-f", src.Format) + } + } + parts = append(parts, "-i", src.URL) + return parts +} + +func sourcesShared(cfg Config) bool { + if cfg.Video == nil || cfg.Audio == nil { + return false + } + return cfg.Video.URL == cfg.Audio.URL && cfg.Video.Type == cfg.Audio.Type +} + +func usesRealtimeSource(cfg Config) bool { + return (cfg.Video != nil && (cfg.Video.Type == SourceTypeSocket || cfg.Video.Type == SourceTypeWebRTC)) || + (cfg.Audio != nil && (cfg.Audio.Type == SourceTypeSocket || cfg.Audio.Type == SourceTypeWebRTC)) +} + +func needsVideoPipe(cfg Config) bool { + return cfg.Video != nil && (cfg.Video.Type == SourceTypeSocket || cfg.Video.Type == SourceTypeWebRTC) +} + +func needsAudioPipe(cfg Config) bool { + return cfg.Audio != nil && (cfg.Audio.Type == SourceTypeSocket || cfg.Audio.Type == SourceTypeWebRTC) +} + +func buildIngestStatus(cfg Config) *IngestStatus { + var status IngestStatus + if cfg.Video != nil && (cfg.Video.Type == SourceTypeSocket || cfg.Video.Type == SourceTypeWebRTC) { + status.Video = &IngestEndpoint{ + Protocol: string(cfg.Video.Type), + Format: cfg.Video.Format, + Path: cfg.Video.URL, + } + } + if cfg.Audio != nil && (cfg.Audio.Type == SourceTypeSocket || cfg.Audio.Type == SourceTypeWebRTC) { + dest := cfg.Audio.Destination + if dest == "" { + dest = AudioDestinationMicrophone // default + } + status.Audio = &IngestEndpoint{ + Protocol: string(cfg.Audio.Type), + Format: cfg.Audio.Format, + Path: cfg.Audio.URL, + Destination: dest, + } + } + if status.Audio == nil && status.Video == nil { + return nil + } + return &status +} + +func normalizeSourceType(t SourceType) SourceType { + switch strings.TrimSpace(strings.ToLower(string(t))) { + case string(SourceTypeStream): + return SourceTypeStream + case string(SourceTypeFile): + return SourceTypeFile + case string(SourceTypeSocket): + return SourceTypeSocket + case string(SourceTypeWebRTC): + return SourceTypeWebRTC + default: + return t + } +} + +func normalizeSource(src *MediaSource, isVideo bool) *MediaSource { + if src == nil { + return nil + } + out := *src + if out.Type == SourceTypeSocket { + if out.URL == "" { + if isVideo { + out.URL = defaultVideoPipe + } else { + out.URL = defaultAudioPipe + } + } + if out.Format == "" { + if isVideo { + out.Format = "mpegts" + } else { + out.Format = "mp3" + } + } + } + if out.Type == SourceTypeWebRTC { + if out.URL == "" { + if isVideo { + out.URL = defaultVideoPipe + } else { + out.URL = defaultAudioPipe + } + } + if out.Format == "" { + if isVideo { + out.Format = "ivf" + } else { + out.Format = "ogg" + } + } + } + return &out +} + +func validateRealtimeFormat(src *MediaSource, isVideo bool) error { + if src == nil { + return nil + } + switch src.Type { + case SourceTypeSocket: + if isVideo && src.Format != "" && src.Format != "mpegts" { + return fmt.Errorf("%w: expected mpegts for socket video, got %s", ErrUnsupportedVideo, src.Format) + } + if !isVideo && src.Format != "" && src.Format != "mp3" { + return fmt.Errorf("%w: expected mp3 for socket audio, got %s", ErrUnsupportedAudio, src.Format) + } + case SourceTypeWebRTC: + if isVideo && src.Format != "" && src.Format != "ivf" { + return fmt.Errorf("%w: expected ivf for webrtc video, got %s", ErrUnsupportedVideo, src.Format) + } + if !isVideo && src.Format != "" && src.Format != "ogg" { + return fmt.Errorf("%w: expected ogg for webrtc audio, got %s", ErrUnsupportedAudio, src.Format) + } + } + return nil +} + +func preparePipe(path string) error { + if path == "" { + return nil + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + _ = os.Remove(path) + if err := syscall.Mkfifo(path, 0o666); err != nil { + return fmt.Errorf("create fifo %s: %w", path, err) + } + return nil +} + +func (m *Manager) closePipeKeepalivesLocked() { + if m.videoKeepalive != nil { + _ = m.videoKeepalive.Close() + m.videoKeepalive = nil + } + if m.audioKeepalive != nil { + _ = m.audioKeepalive.Close() + m.audioKeepalive = nil + } +} + +func (m *Manager) openPipeKeepalivesLocked(ctx context.Context, cfg Config, paused bool) { + log := logger.FromContext(ctx) + m.closePipeKeepalivesLocked() + + if paused { + return + } + + if needsVideoPipe(cfg) && m.videoPipe != "" { + writer, err := OpenPipeReadWriter(m.videoPipe, DefaultPipeOpenTimeout) + if err != nil { + log.Warn("failed to open keepalive for virtual video pipe", "err", err, "path", m.videoPipe) + } else { + m.videoKeepalive = writer + } + } + if needsAudioPipe(cfg) && m.audioPipe != "" { + writer, err := OpenPipeReadWriter(m.audioPipe, DefaultPipeOpenTimeout) + if err != nil { + log.Warn("failed to open keepalive for virtual audio pipe", "err", err, "path", m.audioPipe) + } else { + m.audioKeepalive = writer + } + } +} + +func (m *Manager) startFFmpegLocked(ctx context.Context, args []string) error { + if err := m.stz.Disable(ctx); err != nil { + return fmt.Errorf("failed to disable scale-to-zero: %w", err) + } + m.scaleDisabled = true + + cmd := m.execCommand(m.ffmpegPath, args...) + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + var buf bytes.Buffer + cmd.Stdout = io.MultiWriter(os.Stdout, &buf) + cmd.Stderr = io.MultiWriter(os.Stderr, &buf) + env := os.Environ() + if m.audioSink != "" { + env = append(env, fmt.Sprintf("PULSE_SINK=%s", m.audioSink)) + } + cmd.Env = env + + if err := cmd.Start(); err != nil { + m.enableScaleToZero(ctx) + return fmt.Errorf("failed to start ffmpeg: %w", err) + } + + m.processGroupID = cmd.Process.Pid + exited := make(chan struct{}) + m.cmd = cmd + m.exited = exited + + go func() { + err := cmd.Wait() + m.mu.Lock() + defer m.mu.Unlock() + if err != nil && m.state != stateIdle { + m.lastError = fmt.Sprintf("ffmpeg exited: %v: %s", err, strings.TrimSpace(buf.String())) + m.state = stateIdle + m.startedAt = nil + } + m.cmd = nil + m.processGroupID = 0 + m.closePipeKeepalivesLocked() + close(exited) + m.enableScaleToZero(context.Background()) + }() + + select { + case <-time.After(300 * time.Millisecond): + return nil + case <-exited: + m.enableScaleToZero(ctx) + return fmt.Errorf("ffmpeg exited immediately: %s", strings.TrimSpace(buf.String())) + } +} + +func (m *Manager) enableScaleToZero(ctx context.Context) { + if m.scaleDisabled { + _ = m.stz.Enable(context.WithoutCancel(ctx)) + m.scaleDisabled = false + } +} + +func parseVideoNumber(path string) (int, error) { + base := filepath.Base(path) + num := strings.TrimPrefix(base, "video") + return strconv.Atoi(num) +} + +func cloneSource(src *MediaSource) *MediaSource { + if src == nil { + return nil + } + copy := *src + return © +} diff --git a/server/lib/virtualinputs/manager_test.go b/server/lib/virtualinputs/manager_test.go new file mode 100644 index 00000000..1591f376 --- /dev/null +++ b/server/lib/virtualinputs/manager_test.go @@ -0,0 +1,127 @@ +package virtualinputs + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNormalizeSourceDefaults(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + src *MediaSource + isVideo bool + wantURL string + wantFmt string + }{ + { + name: "socket video uses default pipe and mpegts", + src: &MediaSource{Type: SourceTypeSocket}, + isVideo: true, + wantURL: defaultVideoPipe, + wantFmt: "mpegts", + }, + { + name: "socket audio uses default pipe and mp3", + src: &MediaSource{Type: SourceTypeSocket}, + isVideo: false, + wantURL: defaultAudioPipe, + wantFmt: "mp3", + }, + { + name: "webrtc video uses default pipe and ivf", + src: &MediaSource{Type: SourceTypeWebRTC}, + isVideo: true, + wantURL: defaultVideoPipe, + wantFmt: "ivf", + }, + { + name: "webrtc audio uses default pipe and ogg", + src: &MediaSource{Type: SourceTypeWebRTC}, + isVideo: false, + wantURL: defaultAudioPipe, + wantFmt: "ogg", + }, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := normalizeSource(tt.src, tt.isVideo) + require.NotNil(t, got) + require.Equal(t, tt.wantURL, got.URL) + require.Equal(t, tt.wantFmt, got.Format) + }) + } +} + +func TestBuildIngestStatus(t *testing.T) { + t.Parallel() + + cfg := Config{ + Video: &MediaSource{Type: SourceTypeSocket, URL: "/tmp/vid.pipe", Format: "mpegts"}, + Audio: &MediaSource{Type: SourceTypeWebRTC, URL: "/tmp/aud.pipe", Format: "ogg"}, + } + status := buildIngestStatus(cfg) + require.NotNil(t, status) + require.NotNil(t, status.Video) + require.NotNil(t, status.Audio) + require.Equal(t, string(SourceTypeSocket), status.Video.Protocol) + require.Equal(t, "mpegts", status.Video.Format) + require.Equal(t, "/tmp/vid.pipe", status.Video.Path) + require.Equal(t, string(SourceTypeWebRTC), status.Audio.Protocol) + require.Equal(t, "ogg", status.Audio.Format) + require.Equal(t, "/tmp/aud.pipe", status.Audio.Path) + + require.Nil(t, buildIngestStatus(Config{})) +} + +func TestBuildInputArgsIncludesFormatForRealtimeSources(t *testing.T) { + t.Parallel() + + videoArgs := buildInputArgs(&MediaSource{Type: SourceTypeSocket, URL: "/tmp/video.pipe", Format: "mpegts"}) + require.Equal(t, []string{"-thread_queue_size", "64", "-f", "mpegts", "-i", "/tmp/video.pipe"}, videoArgs) + + audioArgs := buildInputArgs(&MediaSource{Type: SourceTypeWebRTC, URL: "/tmp/audio.pipe", Format: "ogg"}) + require.Equal(t, []string{"-thread_queue_size", "64", "-f", "ogg", "-i", "/tmp/audio.pipe"}, audioArgs) +} + +func TestNormalizeConfigAcceptsRealtimeSourcesWithoutURLs(t *testing.T) { + t.Parallel() + + mgr := NewManager("", "", "", "", 0, 0, 0, nil) + cfg, err := mgr.normalizeConfig(Config{ + Video: &MediaSource{Type: SourceTypeSocket}, + Audio: &MediaSource{Type: SourceTypeWebRTC}, + }) + require.NoError(t, err) + require.Equal(t, SourceTypeSocket, cfg.Video.Type) + require.Equal(t, defaultVideoPipe, cfg.Video.URL) + require.Equal(t, "mpegts", cfg.Video.Format) + require.Equal(t, SourceTypeWebRTC, cfg.Audio.Type) + require.Equal(t, defaultAudioPipe, cfg.Audio.URL) + require.Equal(t, "ogg", cfg.Audio.Format) +} + +func TestNormalizeConfigValidatesTypesAndNormalizes(t *testing.T) { + t.Parallel() + + mgr := NewManager("", "", "", "", 0, 0, 0, nil) + + _, err := mgr.normalizeConfig(Config{Video: &MediaSource{}}) + require.ErrorIs(t, err, ErrVideoTypeRequired) + + _, err = mgr.normalizeConfig(Config{Audio: &MediaSource{}}) + require.ErrorIs(t, err, ErrAudioTypeRequired) + + cfg, err := mgr.normalizeConfig(Config{ + Video: &MediaSource{Type: "WebRTC"}, + Audio: &MediaSource{Type: "SoCkEt"}, + }) + require.NoError(t, err) + require.Equal(t, SourceTypeWebRTC, cfg.Video.Type) + require.Equal(t, SourceTypeSocket, cfg.Audio.Type) +} diff --git a/server/lib/virtualinputs/pipes.go b/server/lib/virtualinputs/pipes.go new file mode 100644 index 00000000..6f2df817 --- /dev/null +++ b/server/lib/virtualinputs/pipes.go @@ -0,0 +1,69 @@ +package virtualinputs + +import ( + "errors" + "fmt" + "os" + "syscall" + "time" +) + +const ( + pipeOpenRetryInterval = 150 * time.Millisecond + DefaultPipeOpenTimeout = 2 * time.Second +) + +// OpenPipeReadWriter opens a FIFO in read/write mode to keep both ends alive without blocking. +func OpenPipeReadWriter(path string, timeout time.Duration) (*os.File, error) { + if path == "" { + return nil, fmt.Errorf("pipe path required") + } + if timeout <= 0 { + timeout = DefaultPipeOpenTimeout + } + deadline := time.Now().Add(timeout) + for { + f, err := os.OpenFile(path, os.O_RDWR|syscall.O_NONBLOCK, 0) + if err == nil { + return f, nil + } + if errors.Is(err, os.ErrNotExist) { + if time.Now().After(deadline) { + return nil, fmt.Errorf("open pipe %s: %w", path, err) + } + time.Sleep(pipeOpenRetryInterval) + continue + } + return nil, fmt.Errorf("open pipe %s: %w", path, err) + } +} + +// OpenPipeWriter opens a FIFO for writing without blocking indefinitely when no reader is present. +// It retries until a reader appears or the timeout elapses. +func OpenPipeWriter(path string, timeout time.Duration) (*os.File, error) { + if path == "" { + return nil, fmt.Errorf("pipe path required") + } + if timeout <= 0 { + timeout = DefaultPipeOpenTimeout + } + deadline := time.Now().Add(timeout) + for { + f, err := os.OpenFile(path, os.O_WRONLY|syscall.O_NONBLOCK, 0) + if err == nil { + if err := syscall.SetNonblock(int(f.Fd()), false); err != nil { + _ = f.Close() + return nil, fmt.Errorf("open pipe %s: %w", path, err) + } + return f, nil + } + if errors.Is(err, syscall.ENXIO) || errors.Is(err, os.ErrNotExist) { + if time.Now().After(deadline) { + return nil, fmt.Errorf("open pipe %s: %w", path, err) + } + time.Sleep(pipeOpenRetryInterval) + continue + } + return nil, fmt.Errorf("open pipe %s: %w", path, err) + } +} diff --git a/server/lib/virtualinputs/pipes_test.go b/server/lib/virtualinputs/pipes_test.go new file mode 100644 index 00000000..d81de9e5 --- /dev/null +++ b/server/lib/virtualinputs/pipes_test.go @@ -0,0 +1,54 @@ +package virtualinputs + +import ( + "os" + "path/filepath" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestOpenPipeWriterSucceedsWithReader(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + pipe := filepath.Join(dir, "video.pipe") + require.NoError(t, syscall.Mkfifo(pipe, 0o666)) + + reader, err := os.OpenFile(pipe, os.O_RDONLY|syscall.O_NONBLOCK, 0) + require.NoError(t, err) + defer reader.Close() + + writer, err := OpenPipeWriter(pipe, time.Second) + require.NoError(t, err) + require.NotNil(t, writer) + require.NoError(t, writer.Close()) +} + +func TestOpenPipeWriterTimesOutWithoutReader(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + pipe := filepath.Join(dir, "audio.pipe") + require.NoError(t, syscall.Mkfifo(pipe, 0o666)) + + start := time.Now() + _, err := OpenPipeWriter(pipe, 200*time.Millisecond) + require.Error(t, err) + require.GreaterOrEqual(t, time.Since(start), 180*time.Millisecond) +} + +func TestOpenPipeReadWriterDoesNotBlockWithoutPeer(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + pipe := filepath.Join(dir, "keepalive.pipe") + require.NoError(t, syscall.Mkfifo(pipe, 0o666)) + + f, err := OpenPipeReadWriter(pipe, time.Second) + require.NoError(t, err) + require.NotNil(t, f) + require.NoError(t, f.Close()) +} diff --git a/server/lib/virtualinputs/webrtc.go b/server/lib/virtualinputs/webrtc.go new file mode 100644 index 00000000..fcd634c6 --- /dev/null +++ b/server/lib/virtualinputs/webrtc.go @@ -0,0 +1,286 @@ +package virtualinputs + +import ( + "context" + "errors" + "fmt" + "io" + "os/exec" + "sync" + + "github.com/pion/webrtc/v4" + "github.com/pion/webrtc/v4/pkg/media/ivfwriter" + "github.com/pion/webrtc/v4/pkg/media/oggwriter" +) + +// WebRTCIngestor handles inbound WebRTC offers and writes received media to sinks. +// Video goes to the broadcaster (for the virtual feed page). +// Audio goes to PulseAudio via ffmpeg. +type WebRTCIngestor struct { + mu sync.Mutex + config *webrtcIngestConfig + + pc *webrtc.PeerConnection + cancel context.CancelFunc + + videoSink io.Writer + audioSink io.Writer +} + +type webrtcIngestConfig struct { + videoPath string + videoFormat string + audioPath string + audioFormat string + audioDestination AudioDestination +} + +func NewWebRTCIngestor() *WebRTCIngestor { + return &WebRTCIngestor{} +} + +// Configure sets the target formats for subsequent WebRTC offers. +// Note: paths are kept for API compatibility but video goes directly to sink, +// and audio goes to PulseAudio via ffmpeg. +// audioDestination specifies where to route audio: "microphone" (default) or "speaker". +func (w *WebRTCIngestor) Configure(videoPath, videoFormat, audioPath, audioFormat string, audioDestination AudioDestination) { + w.mu.Lock() + defer w.mu.Unlock() + if audioDestination == "" { + audioDestination = AudioDestinationMicrophone // default + } + w.config = &webrtcIngestConfig{ + videoPath: videoPath, + videoFormat: videoFormat, + audioPath: audioPath, + audioFormat: audioFormat, + audioDestination: audioDestination, + } +} + +// Clear tears down any active connection and removes the configured targets. +func (w *WebRTCIngestor) Clear() { + w.mu.Lock() + defer w.mu.Unlock() + if w.cancel != nil { + w.cancel() + w.cancel = nil + } + if w.pc != nil { + _ = w.pc.Close() + w.pc = nil + } + w.config = nil +} + +// SetSinks sets the writers for incoming media. +// Video sink is required for the virtual feed broadcaster. +// Audio sink is optional (audio goes to PulseAudio via ffmpeg). +func (w *WebRTCIngestor) SetSinks(video io.Writer, audio io.Writer) { + w.mu.Lock() + defer w.mu.Unlock() + w.videoSink = video + w.audioSink = audio +} + +// HandleOffer negotiates a PeerConnection and starts forwarding tracks. +func (w *WebRTCIngestor) HandleOffer(ctx context.Context, offerSDP string) (string, error) { + runCtx := context.WithoutCancel(ctx) + + w.mu.Lock() + cfg := w.config + if cfg == nil { + w.mu.Unlock() + return "", fmt.Errorf("webrtc ingest not configured") + } + // We need either video or audio configured + hasVideo := cfg.videoPath != "" || cfg.videoFormat != "" + hasAudio := cfg.audioPath != "" || cfg.audioFormat != "" + if !hasVideo && !hasAudio { + w.mu.Unlock() + return "", fmt.Errorf("webrtc ingest not configured for video or audio") + } + if w.cancel != nil { + w.cancel() + w.cancel = nil + } + if w.pc != nil { + _ = w.pc.Close() + w.pc = nil + } + + pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + if err != nil { + w.mu.Unlock() + return "", fmt.Errorf("failed to create peerconnection: %w", err) + } + ctx, cancel := context.WithCancel(runCtx) + w.pc = pc + w.cancel = cancel + videoSink := w.videoSink + w.mu.Unlock() + + pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + if state == webrtc.PeerConnectionStateFailed || + state == webrtc.PeerConnectionStateClosed { + cancel() + } + }) + + pc.OnTrack(func(track *webrtc.TrackRemote, _ *webrtc.RTPReceiver) { + switch track.Kind() { + case webrtc.RTPCodecTypeVideo: + if !hasVideo { + return + } + _ = w.forwardVideo(ctx, cfg, track, videoSink) + case webrtc.RTPCodecTypeAudio: + if !hasAudio { + return + } + _ = w.forwardAudio(ctx, cfg, track) + default: + } + }) + + if err := pc.SetRemoteDescription(webrtc.SessionDescription{SDP: offerSDP, Type: webrtc.SDPTypeOffer}); err != nil { + cancel() + return "", fmt.Errorf("set remote description: %w", err) + } + + answer, err := pc.CreateAnswer(nil) + if err != nil { + cancel() + return "", fmt.Errorf("create answer: %w", err) + } + gatherComplete := webrtc.GatheringCompletePromise(pc) + if err := pc.SetLocalDescription(answer); err != nil { + cancel() + return "", fmt.Errorf("set local description: %w", err) + } + <-gatherComplete + + local := pc.LocalDescription() + if local == nil { + cancel() + return "", fmt.Errorf("local description missing") + } + return local.SDP, nil +} + +// forwardVideo writes incoming video RTP packets as IVF to the video sink (broadcaster). +func (w *WebRTCIngestor) forwardVideo(ctx context.Context, cfg *webrtcIngestConfig, track *webrtc.TrackRemote, videoSink io.Writer) error { + if cfg.videoFormat != "" && cfg.videoFormat != "ivf" { + return fmt.Errorf("unsupported video format %s", cfg.videoFormat) + } + if track.Codec().MimeType != webrtc.MimeTypeVP8 && track.Codec().MimeType != webrtc.MimeTypeVP9 { + return fmt.Errorf("unsupported video codec %s", track.Codec().MimeType) + } + + if videoSink == nil { + return fmt.Errorf("video sink not configured") + } + + writer, err := ivfwriter.NewWith(videoSink) + if err != nil { + return fmt.Errorf("create ivf writer: %w", err) + } + defer writer.Close() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + pkt, _, err := track.ReadRTP() + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, context.Canceled) { + return nil + } + return err + } + if err := writer.WriteRTP(pkt); err != nil { + return err + } + } +} + +// forwardAudio pipes incoming audio RTP packets through ffmpeg to PulseAudio. +// The destination determines whether audio goes to virtual mic (audio_input) or speaker (audio_output). +func (w *WebRTCIngestor) forwardAudio(ctx context.Context, cfg *webrtcIngestConfig, track *webrtc.TrackRemote) error { + if cfg.audioFormat != "" && cfg.audioFormat != "ogg" { + return fmt.Errorf("unsupported audio format %s", cfg.audioFormat) + } + if track.Codec().MimeType != webrtc.MimeTypeOpus { + return fmt.Errorf("unsupported audio codec %s", track.Codec().MimeType) + } + + // Determine the PulseAudio sink based on destination + sink := "audio_input" // default: virtual microphone + if cfg.audioDestination == AudioDestinationSpeaker { + sink = "audio_output" + } + + // Create a pipe to connect oggwriter output to ffmpeg input + pr, pw := io.Pipe() + + // Start ffmpeg to decode OGG/Opus and output to PulseAudio + // Use -device to specify the PulseAudio sink; the output "pulse" is a placeholder. + args := []string{ + "-hide_banner", "-loglevel", "warning", + "-f", "ogg", + "-i", "pipe:0", + "-ac", "2", + "-ar", "48000", + "-f", "pulse", + "-device", sink, + "pulse", + } + + cmd := exec.CommandContext(ctx, "ffmpeg", args...) + cmd.Stdin = pr + + if err := cmd.Start(); err != nil { + pw.Close() + pr.Close() + return fmt.Errorf("failed to start ffmpeg for audio: %w", err) + } + + // Clean up ffmpeg when done + go func() { + _ = cmd.Wait() + }() + + // Create OGG writer that writes to the pipe + writer, err := oggwriter.NewWith(pw, track.Codec().ClockRate, track.Codec().Channels) + if err != nil { + pw.Close() + pr.Close() + return fmt.Errorf("create ogg writer: %w", err) + } + + defer func() { + writer.Close() + pw.Close() + pr.Close() + }() + + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + pkt, _, err := track.ReadRTP() + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, context.Canceled) { + return nil + } + return err + } + if err := writer.WriteRTP(pkt); err != nil { + return err + } + } +} diff --git a/server/lib/virtualinputs/webrtc_test.go b/server/lib/virtualinputs/webrtc_test.go new file mode 100644 index 00000000..2e482d80 --- /dev/null +++ b/server/lib/virtualinputs/webrtc_test.go @@ -0,0 +1,58 @@ +package virtualinputs + +import ( + "context" + "path/filepath" + "syscall" + "testing" + + "github.com/pion/webrtc/v4" + "github.com/stretchr/testify/require" +) + +func TestWebRTCIngestorHandleOfferRequiresConfig(t *testing.T) { + t.Parallel() + + ing := NewWebRTCIngestor() + _, err := ing.HandleOffer(context.Background(), "dummy") + require.ErrorContains(t, err, "webrtc ingest not configured") +} + +func TestWebRTCIngestorHandleOfferRequiresPaths(t *testing.T) { + t.Parallel() + + ing := NewWebRTCIngestor() + ing.Configure("", "", "", "", "") + _, err := ing.HandleOffer(context.Background(), "dummy") + require.ErrorContains(t, err, "webrtc ingest not configured for video or audio") +} + +func TestWebRTCIngestorHandleOfferNegotiates(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + videoPipe := filepath.Join(dir, "video.pipe") + require.NoError(t, syscall.Mkfifo(videoPipe, 0o666)) + + ing := NewWebRTCIngestor() + ing.Configure(videoPipe, "ivf", "", "", "") + t.Cleanup(func() { ing.Clear() }) + + pc, err := webrtc.NewPeerConnection(webrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() + + _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo) + require.NoError(t, err) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + require.NoError(t, pc.SetLocalDescription(offer)) + <-webrtc.GatheringCompletePromise(pc) + + answer, err := ing.HandleOffer(context.Background(), pc.LocalDescription().SDP) + require.NoError(t, err) + require.NotEmpty(t, answer) + + require.NoError(t, pc.SetRemoteDescription(webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: answer})) +} diff --git a/server/openapi.yaml b/server/openapi.yaml index 9b23c130..0535e134 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -23,6 +23,91 @@ paths: $ref: "#/components/responses/ConflictError" "500": $ref: "#/components/responses/InternalError" + /stream/start: + post: + summary: Start live streaming to an internal RTMP(S) server or a remote RTMP(S) endpoint. + description: | + Start streaming the display to either an internally hosted RTMP(S) server (default) or a remote RTMP/RTMPS URL. + When using the internal server the response includes playback URLs that can be used by viewers. + operationId: startStream + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StartStreamRequest" + responses: + "201": + description: Stream started + content: + application/json: + schema: + $ref: "#/components/schemas/StreamInfo" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" + /stream/stop: + post: + summary: Stop a live stream + operationId: stopStream + requestBody: + required: false + content: + application/json: + schema: + $ref: "#/components/schemas/StopStreamRequest" + responses: + "200": + description: Stream stopped + "400": + $ref: "#/components/responses/BadRequestError" + "404": + $ref: "#/components/responses/NotFoundError" + "500": + $ref: "#/components/responses/InternalError" + /stream/list: + get: + summary: List active streams + operationId: listStreams + responses: + "200": + description: List of active streams + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/StreamInfo" + "500": + $ref: "#/components/responses/InternalError" + /stream/webrtc/offer: + post: + summary: Exchange SDP for a WebRTC livestream + operationId: streamWebrtcOffer + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/StreamWebRTCOffer" + responses: + "200": + description: SDP answer created for the provided offer. + content: + application/json: + schema: + $ref: "#/components/schemas/StreamWebRTCAnswer" + "400": + $ref: "#/components/responses/BadRequestError" + "404": + $ref: "#/components/responses/NotFoundError" + "409": + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" /process/exec: post: summary: Execute a command synchronously @@ -434,6 +519,155 @@ paths: $ref: "#/components/responses/BadRequestError" "500": $ref: "#/components/responses/InternalError" + /input/devices/virtual/configure: + post: + summary: Configure virtual video and audio inputs + description: Start or restart the virtual webcam and microphone pipelines using the provided sources. + operationId: configureVirtualInputs + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputsRequest" + responses: + "200": + description: Virtual inputs configured + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputsStatus" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/pause: + post: + summary: Pause virtual inputs with silence and black frames + operationId: pauseVirtualInputs + responses: + "200": + description: Virtual inputs paused + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputsStatus" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/resume: + post: + summary: Resume previously configured virtual inputs + operationId: resumeVirtualInputs + responses: + "200": + description: Virtual inputs resumed + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputsStatus" + "400": + $ref: "#/components/responses/BadRequestError" + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/stop: + post: + summary: Stop virtual input pipelines and release resources + operationId: stopVirtualInputs + responses: + "200": + description: Virtual inputs stopped + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputsStatus" + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/status: + get: + summary: Get the current virtual input status + operationId: getVirtualInputsStatus + responses: + "200": + description: Status + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputsStatus" + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/feed: + get: + summary: Render a fullscreen HTML page with the virtual video feed + description: | + Serves a minimal HTML page that plays the configured virtual input video. + The source can be overridden via query parameters or auto-detected from the current virtual input status. + operationId: getVirtualInputFeed + parameters: + - in: query + name: fit + required: false + schema: + type: string + description: Object-fit mode for the video element. + default: cover + - in: query + name: source + required: false + schema: + type: string + description: Explicit video source to render (HTTP/HLS/WebSocket/WebRTC helper). + responses: + "200": + description: Fullscreen HTML feed page + content: + text/html: + schema: + type: string + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/feed/socket/info: + get: + summary: Discover the websocket URL for the virtual video feed mirror + description: | + Returns the websocket endpoint and expected format for the live virtual video feed preview. + This helper does not upgrade the connection; use the returned URL to open the websocket stream. + operationId: getVirtualInputFeedSocketInfo + responses: + "200": + description: Websocket feed info + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualFeedSocketInfo" + "409": + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" + /input/devices/virtual/webrtc/offer: + post: + summary: Negotiate a WebRTC ingest session for virtual inputs + operationId: negotiateVirtualInputsWebrtc + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputWebRTCOffer" + responses: + "200": + description: Answer SDP ready for the caller to set as remote description. + content: + application/json: + schema: + $ref: "#/components/schemas/VirtualInputWebRTCAnswer" + "400": + $ref: "#/components/responses/BadRequestError" + "409": + $ref: "#/components/responses/ConflictError" + "500": + $ref: "#/components/responses/InternalError" /logs/stream: get: summary: Stream logs over SSE @@ -978,6 +1212,92 @@ paths: $ref: "#/components/responses/InternalError" components: schemas: + StartStreamRequest: + type: object + properties: + id: + type: string + description: Optional identifier for the streaming session. Alphanumeric or hyphen. + pattern: "^[a-zA-Z0-9-]+$" + mode: + type: string + description: | + Where to send the stream output. "internal" starts a local RTMP(S) server and streams to it. + "remote" pushes the stream to the provided RTMP/RTMPS target_url. + "webrtc" exposes a WebRTC offer/answer endpoint for browser-friendly playback. + "socket" broadcasts MPEG-TS chunks over a websocket endpoint. + enum: [internal, remote, webrtc, socket] + default: internal + target_url: + type: string + description: RTMP or RTMPS URL to push the stream to when mode is "remote". + pattern: "^rtmps?://.+" + framerate: + type: integer + description: Streaming framerate in fps (overrides server default) + minimum: 1 + maximum: 20 + additionalProperties: false + StopStreamRequest: + type: object + properties: + id: + type: string + description: Identifier of the stream to stop. Alphanumeric or hyphen. + pattern: "^[a-zA-Z0-9-]+$" + additionalProperties: false + StreamInfo: + type: object + required: [id, mode, ingest_url, is_streaming, started_at] + properties: + id: + type: string + description: Stream identifier + mode: + type: string + enum: [internal, remote, webrtc, socket] + description: Whether the stream is using the internal RTMP server, remote endpoint, WebRTC, or websocket broadcast. + ingest_url: + type: string + description: URL ffmpeg is publishing to + playback_url: + type: [string, "null"] + description: RTMP playback URL if available (internal streams only) + secure_playback_url: + type: [string, "null"] + description: RTMPS playback URL when TLS is enabled for the internal server + websocket_url: + type: [string, "null"] + description: Websocket endpoint that streams MPEG-TS chunks when mode is "socket" + webrtc_offer_url: + type: [string, "null"] + description: HTTP endpoint to post SDP offers to when mode is "webrtc" + started_at: + type: string + format: date-time + description: Timestamp when streaming started + is_streaming: + type: boolean + description: Whether the ffmpeg streaming process is currently running + additionalProperties: false + StreamWebRTCOffer: + type: object + required: [sdp] + properties: + id: + type: string + description: Stream identifier (defaults to "default" if omitted) + sdp: + type: string + description: SDP offer from the viewer + additionalProperties: false + StreamWebRTCAnswer: + type: object + properties: + sdp: + type: string + description: SDP answer to set as the remote description on the viewer + additionalProperties: false StartRecordingRequest: type: object properties: @@ -1542,6 +1862,175 @@ components: description: Indicates success. default: true additionalProperties: false + VirtualInputType: + type: string + enum: [stream, file, socket, webrtc] + description: Type of media source being injected. + VirtualInputAudioDestination: + type: string + enum: [microphone, speaker] + default: microphone + description: | + Where to route the virtual input audio (both for configured sources and ingest endpoints): + - "microphone": Route to the virtual microphone input (PulseAudio audio_input sink). + This is the default. Applications reading from the virtual mic will receive this audio. + - "speaker": Route directly to the container's audio output (PulseAudio audio_output sink). + Use this for monitoring/playback purposes. + VirtualInputAudio: + type: object + required: [type] + properties: + type: + $ref: "#/components/schemas/VirtualInputType" + url: + type: string + description: Input URL (supports file URLs, HLS, RTMP(S), DASH, etc). + format: + type: string + description: | + Optional format hint for socket/WebRTC feeds. Socket audio accepts mp3 chunks; WebRTC ingest expects Ogg/Opus. + destination: + $ref: "#/components/schemas/VirtualInputAudioDestination" + additionalProperties: false + VirtualInputVideo: + type: object + required: [type] + properties: + type: + $ref: "#/components/schemas/VirtualInputType" + url: + type: string + description: Input URL (supports file URLs, HLS, RTMP(S), DASH, etc). + format: + type: string + description: | + Optional format hint for socket/WebRTC feeds. Socket video should use MPEG-TS chunks; WebRTC ingest expects IVF (VP8/VP9). + width: + type: integer + minimum: 1 + description: Target width for the virtual webcam output. + height: + type: integer + minimum: 1 + description: Target height for the virtual webcam output. + frame_rate: + type: integer + minimum: 1 + maximum: 60 + description: Frame rate for the virtual webcam output. + additionalProperties: false + VirtualInputsRequest: + type: object + description: Configure virtual webcam and microphone inputs. + properties: + video: + $ref: "#/components/schemas/VirtualInputVideo" + audio: + $ref: "#/components/schemas/VirtualInputAudio" + start_paused: + type: boolean + description: Start with silence/black frames instead of live media until resumed. + additionalProperties: false + VirtualInputsStatus: + type: object + required: + - state + - video_device + - audio_sink + - microphone_source + - mode + properties: + state: + type: string + enum: [idle, running, paused] + description: Current state of the virtual input pipelines. + mode: + type: string + enum: [device, virtual-file] + description: Output mode in use (v4l2 device vs virtual capture files). + video_device: + type: string + description: Video4Linux device path used for the virtual webcam. + audio_sink: + type: string + description: PulseAudio sink receiving injected audio. + microphone_source: + type: string + description: PulseAudio source clients should use as a microphone. + video_file: + type: string + description: Path to the Y4M file/pipe used when virtual-file mode is active. + audio_file: + type: string + description: Path to the WAV file/pipe used when virtual-file mode is active. + video: + $ref: "#/components/schemas/VirtualInputVideo" + audio: + $ref: "#/components/schemas/VirtualInputAudio" + started_at: + type: string + format: date-time + description: Timestamp when the current pipelines started. + last_error: + type: string + description: Last observed error (if any). + ingest: + $ref: "#/components/schemas/VirtualInputsIngest" + additionalProperties: false + VirtualInputsIngest: + type: object + description: Network endpoints that accept realtime media for the configured virtual inputs. + properties: + video: + $ref: "#/components/schemas/VirtualInputIngestEndpoint" + audio: + $ref: "#/components/schemas/VirtualInputIngestEndpoint" + additionalProperties: false + VirtualInputIngestEndpoint: + type: object + properties: + protocol: + type: string + description: Protocol used to ingest media (socket or webrtc). + format: + type: string + description: Expected format/codec for the ingest stream. + destination: + allOf: + - $ref: "#/components/schemas/VirtualInputAudioDestination" + description: | + Where audio ingest will be routed. Only relevant for audio endpoints; ignored for video. + url: + type: string + description: URL to push media to (ws:// or HTTP offer endpoint). + additionalProperties: false + VirtualInputWebRTCOffer: + type: object + required: [sdp] + properties: + sdp: + type: string + description: SDP offer from the publisher. + additionalProperties: false + VirtualInputWebRTCAnswer: + type: object + properties: + sdp: + type: string + description: SDP answer to set on the publisher. + additionalProperties: false + VirtualFeedSocketInfo: + type: object + description: Endpoint information for the websocket preview mirror of the virtual video feed. + required: [url] + properties: + url: + type: string + description: Websocket URL that mirrors the configured virtual video feed. + format: + type: string + description: Container/codec expected over the websocket (e.g. mpegts or ivf). + additionalProperties: false ExecutePlaywrightRequest: type: object description: Request to execute Playwright code