Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
53 changes: 45 additions & 8 deletions vhs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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++
Expand Down