diff --git a/tty.go b/tty.go index c8af5dea..c6409974 100644 --- a/tty.go +++ b/tty.go @@ -28,7 +28,7 @@ func buildTtyCmd(port int, shell Shell) *exec.Cmd { args := []string{ //nolint:prealloc fmt.Sprintf("--port=%d", port), "--interface", "127.0.0.1", - "-t", "rendererType=canvas", + "-t", "rendererType=dom", "-t", "disableResizeOverlay=true", "-t", "enableSixel=true", "-t", "customGlyphs=true", diff --git a/vhs.go b/vhs.go index 235e7b42..61d40c23 100644 --- a/vhs.go +++ b/vhs.go @@ -27,6 +27,8 @@ type VHS struct { browser *rod.Browser TextCanvas *rod.Element CursorCanvas *rod.Element + Screen *rod.Element // fallback for DOM renderer (no canvas) + useDOMFallback bool mutex *sync.Mutex started bool recording bool @@ -138,7 +140,14 @@ func (vhs *VHS) Start() error { path, _ := launcher.LookPath() enableNoSandbox := os.Getenv("VHS_NO_SANDBOX") != "" - u, err := launcher.New().Leakless(false).Bin(path).NoSandbox(enableNoSandbox).Launch() + u, err := launcher.New(). + Leakless(false). + Bin(path). + NoSandbox(enableNoSandbox). + Set("use-gl", "angle"). + Set("use-angle", "swiftshader"). + Set("enable-webgl"). + Launch() if err != nil { return fmt.Errorf("could not launch browser: %w", err) } @@ -174,8 +183,24 @@ func (vhs *VHS) Setup() { vhs.Page = vhs.Page.MustSetViewport(width, height, 0, false) // Find xterm.js canvases for the text and cursor layer for recording. - vhs.TextCanvas, _ = vhs.Page.Element("canvas.xterm-text-layer") - vhs.CursorCanvas, _ = vhs.Page.Element("canvas.xterm-cursor-layer") + // Newer versions of xterm.js (bundled with ttyd 1.7.7+) may use a DOM + // renderer instead of canvas when WebGL is unavailable (e.g. headless + // Chrome, RDP sessions). In that case, fall back to screenshotting + // the .xterm-screen element. + // + // Use a temporary timeout page for the Element() lookup only. + // Do not store elements obtained from Timeout() -- their context expires, + // causing all subsequent Screenshot()/CanvasToImage() calls to fail. + probe, _ := vhs.Page.Timeout(5 * time.Second).Element("canvas.xterm-text-layer") + if probe != nil { + vhs.TextCanvas, _ = vhs.Page.Element("canvas.xterm-text-layer") + vhs.CursorCanvas, _ = vhs.Page.Element("canvas.xterm-cursor-layer") + } else { + vhs.Screen, _ = vhs.Page.Element(".xterm-screen") + if vhs.Screen != nil { + vhs.useDOMFallback = true + } + } // Apply options to the terminal // By this point the setting commands have been executed, so the `opts` struct is up to date. @@ -351,11 +376,23 @@ func (vhs *VHS) Record(ctx context.Context) <-chan error { continue } - cursor, cursorErr := vhs.CursorCanvas.CanvasToImage("image/png", quality) - text, textErr := vhs.TextCanvas.CanvasToImage("image/png", quality) - if textErr != nil || cursorErr != nil { - ch <- fmt.Errorf("error: %v, %v", textErr, cursorErr) - continue + var text, cursor []byte + if vhs.useDOMFallback { + screenPng, screenErr := vhs.Screen.Screenshot(proto.PageCaptureScreenshotFormatPng, quality) + if screenErr != nil { + ch <- fmt.Errorf("error capturing screen: %v", screenErr) + continue + } + text = screenPng + cursor = screenPng + } else { + var cursorErr, textErr error + cursor, cursorErr = vhs.CursorCanvas.CanvasToImage("image/png", quality) + text, textErr = vhs.TextCanvas.CanvasToImage("image/png", quality) + if textErr != nil || cursorErr != nil { + ch <- fmt.Errorf("error: %v, %v", textErr, cursorErr) + continue + } } counter++