Skip to content

Commit 35bdf2a

Browse files
Tried implementation of full screen recording
1 parent e4c8e0d commit 35bdf2a

File tree

5 files changed

+174
-0
lines changed

5 files changed

+174
-0
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ require (
3838
github.com/vcaesar/screenshot v0.11.1 // indirect
3939
github.com/vcaesar/tt v0.20.1 // indirect
4040
github.com/yusufpapurcu/wmi v1.2.4 // indirect
41+
golang.design/x/hotkey v0.4.1 // indirect
4142
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
4243
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
4344
golang.org/x/image v0.29.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
8989
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
9090
golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
9191
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
92+
golang.design/x/hotkey v0.4.1 h1:zLP/2Pztl4WjyxURdW84GoZ5LUrr6hr69CzJFJ5U1go=
93+
golang.design/x/hotkey v0.4.1/go.mod h1:M8SGcwFYHnKRa83FpTFQoZvPO5vVT+kWPztFqTQKmXA=
9294
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
9395
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
9496
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=

main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func extractFFmpegExe(zipPath, destDir string) error {
5757
type Config struct {
5858
SaveLocation string `json:"save_location"`
5959
RecordFunc bool `json:"record_func_enabled"`
60+
RecordingOpts RecordingOptions `json:"recording_options"`
6061
}
6162

6263
func setConfig(key string, value any) {
@@ -110,13 +111,15 @@ func initConfig() {
110111
func initDownloads() {
111112
dwnPath := filepath.Join(appdataDir, "bin")
112113
if _, err := os.Stat(filepath.Join(dwnPath, "ffmpeg.exe")); err == nil {
114+
mergeRecordingDefaults()
113115
return
114116
}
115117
if !config.RecordFunc {
116118
return
117119
}
118120
cmd := exec.Command("ffmpeg", "-version")
119121
if err := cmd.Run(); err == nil {
122+
mergeRecordingDefaults()
120123
return
121124
}
122125
fmt.Println("Captr requires ffmpeg to record videos. However, the screenshotting functionality is not affected.")
@@ -158,6 +161,7 @@ func initDownloads() {
158161
}
159162
extractFFmpegExe(filepath.Join(os.TempDir(), "ffmpeg_captr.zip"), dwnPath)
160163
fmt.Printf("FFMPEG has been downloaded to %s", dwnPath)
164+
mergeRecordingDefaults()
161165
} else {
162166
setConfig("record_func_enabled", false)
163167
}
@@ -209,6 +213,8 @@ ________/\\\\\\\\\__________________________________________________________
209213
}
210214

211215
switch i {
216+
case 0:
217+
RecordDisplay()
212218
case 2:
213219
Screenshot_Window()
214220
case 3:

recscreen.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strconv"
10+
"time"
11+
12+
"github.com/go-vgo/robotgo"
13+
"github.com/manifoldco/promptui"
14+
"golang.design/x/hotkey"
15+
)
16+
17+
type RecordingOptions struct {
18+
FPS int `json:"fps"`
19+
CaptureMouse bool `json:"capture_mouse"`
20+
AudioDevice string `json:"audio_device"`
21+
Width int `json:"width"`
22+
Height int `json:"height"`
23+
}
24+
25+
var defaultOpts = map[string]any{
26+
"fps": 30,
27+
"capture_mouse": true,
28+
"audio_device": "",
29+
}
30+
31+
func mergeRecordingDefaults() {
32+
data, err := os.ReadFile(configFilePath)
33+
if err != nil {
34+
initConfig()
35+
return
36+
}
37+
38+
var config map[string]any
39+
if err := json.Unmarshal(data, &config); err != nil {
40+
fmt.Println("Error reading config:", err)
41+
return
42+
}
43+
44+
recRaw, ok := config["recording_options"]
45+
if !ok {
46+
config["recording_options"] = defaultOpts
47+
} else {
48+
recMap, ok := recRaw.(map[string]any)
49+
if !ok {
50+
fmt.Println("Error reading recording options")
51+
return
52+
}
53+
54+
changed := false
55+
for k, v := range defaultOpts {
56+
if _, exists := recMap[k]; !exists {
57+
recMap[k] = v
58+
changed = true
59+
}
60+
}
61+
62+
if changed {
63+
config["recording_options"] = recMap
64+
} else {
65+
return
66+
}
67+
}
68+
69+
out, _ := json.MarshalIndent(config, "", " ")
70+
os.WriteFile(configFilePath, out, 0644)
71+
}
72+
73+
func ternary(cond bool, a, b any) any {
74+
if cond {
75+
return a
76+
}
77+
return b
78+
}
79+
80+
func RecordDisplay() {
81+
active_displays := robotgo.DisplaysNum()
82+
displays := []string{"Display 1 (Primary)"}
83+
for i := 2; i < active_displays; i++ {
84+
displays = append(displays, fmt.Sprintf("Display %d", i))
85+
}
86+
prompt := promptui.Select{
87+
Label: "Select Display",
88+
Items: displays,
89+
}
90+
91+
display, _, err := prompt.Run()
92+
if err != nil {
93+
fmt.Print("Some error occurred")
94+
return
95+
}
96+
97+
x, y, w, h := robotgo.GetDisplayBounds(display)
98+
filename := filepath.Join(config.SaveLocation, fmt.Sprintf("Recording_Disp%d_%dx%d_%s.mp4", display+1, w, h, time.Now().Format("20060102_150405")))
99+
args := []string{
100+
"-f", "gdigrab",
101+
"-framerate", fmt.Sprintf("%d", config.RecordingOpts.FPS),
102+
"-offset_x", strconv.Itoa(x),
103+
"-offset_y", strconv.Itoa(y),
104+
"-video_size", fmt.Sprintf("%dx%d", w, h),
105+
"-draw_mouse", ternary(config.RecordingOpts.CaptureMouse, "1", "0").(string),
106+
"-i", "desktop",
107+
"-c:v", "libx264",
108+
"-preset", "ultrafast",
109+
"-y", filename,
110+
}
111+
112+
cmd := exec.Command(getFfmpegPath(), args...)
113+
stdin, _ := cmd.StdinPipe()
114+
var start time.Time
115+
if err, start = cmd.Start(), time.Now(); err != nil {
116+
fmt.Println("Error starting ffmpeg:", err)
117+
return
118+
}
119+
120+
fmt.Println("Recording started. Press ctrl+shift+3 to stop")
121+
ticker := time.NewTicker(time.Second)
122+
go func() {
123+
defer ticker.Stop()
124+
for range ticker.C {
125+
select {
126+
case <-ticker.C:
127+
fmt.Printf("\rRecording time elapsed: %02d:%02d", int(time.Since(start).Minutes()), int(time.Since(start).Seconds())%60)
128+
default:
129+
}
130+
}
131+
fmt.Println()
132+
}()
133+
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.Key3)
134+
err = hk.Register()
135+
if err != nil {
136+
fmt.Println("Error registering hotkey:", err)
137+
return
138+
}
139+
defer hk.Unregister()
140+
keyChan := hk.Keydown()
141+
for range keyChan {
142+
fmt.Println("\nStopping recording...")
143+
stdin.Write([]byte("q"))
144+
stdin.Close()
145+
ticker.Stop()
146+
break
147+
}
148+
149+
err = cmd.Wait()
150+
if err != nil {
151+
fmt.Println("Error waiting for ffmpeg to exit:", err)
152+
}
153+
fmt.Printf("\nRecording stopped. Recording saved at %s", filename)
154+
}

utils.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"fmt"
66
"image"
77
"image/png"
8+
"os/exec"
9+
"path/filepath"
810
"syscall"
911
"unsafe"
1012

@@ -415,3 +417,12 @@ func ActivateWindowAndGetBounds(hwnd uintptr) (WindowBounds, error) {
415417

416418
return bounds, nil
417419
}
420+
421+
func getFfmpegPath() string {
422+
cmd := exec.Command("ffmpeg", "-version")
423+
if err := cmd.Run(); err == nil {
424+
return "ffmpeg"
425+
}
426+
427+
return filepath.Join(appdataDir, "bin", "ffmpeg.exe")
428+
}

0 commit comments

Comments
 (0)