Skip to content

Commit d406b6a

Browse files
feat: Add streaming and window recording
This commit tries implementing the ability to stream a display and record a specific window. The main changes are: - Added a new "Stream a display" option to the main menu, which utilizes ddagrab for efficient screen capture. - Tried implementing window recording functionality. - Added a new stream_config to the config file to store stream keys. - Improved FFmpeg argument handling in recscreen.go. - Added cleanup for downloaded FFmpeg files. - Made config file handling more robust.
1 parent b32d011 commit d406b6a

File tree

5 files changed

+291
-14
lines changed

5 files changed

+291
-14
lines changed

main.go

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ var (
2828
RecordingOpts: RecordingOptions{
2929
FPS: 30,
3030
CaptureMouse: true,
31-
AudioDevice: "",
3231
},
3332
HotkeyConfig: hotkeyConfig{
3433
Modkeys: []string{"ctrl", "shift"},
@@ -61,7 +60,6 @@ func extractFFmpegExe(zipPath, destDir string) error {
6160
defer outFile.Close()
6261

6362
_, err = io.Copy(outFile, rc)
64-
os.Remove(zipPath)
6563
return err
6664
}
6765
}
@@ -75,11 +73,17 @@ type hotkeyConfig struct {
7573
Note string `json:"note"`
7674
}
7775

76+
type streamConfig struct {
77+
YoutubeStreamKey string `json:"ytstreamkey"`
78+
TwitchStreamKey string `json:"twitchstreamkey"`
79+
}
80+
7881
type Config struct {
7982
SaveLocation string `json:"save_location"`
8083
RecordFunc bool `json:"record_func_enabled"`
8184
RecordingOpts RecordingOptions `json:"recording_options,omitempty"`
8285
HotkeyConfig hotkeyConfig `json:"hotkey_config"`
86+
StreamConfig streamConfig `json:"stream_config"`
8387
}
8488

8589
type RecordingOptions struct {
@@ -111,7 +115,7 @@ func initConfig() {
111115
}
112116
var loadedConfig Config
113117
if err := json.Unmarshal(data, &loadedConfig); err != nil {
114-
panic(err)
118+
os.WriteFile(configFilePath, []byte{'{', '}'}, 0644)
115119
}
116120

117121
config = mergeConfig(defaultConfig, loadedConfig)
@@ -186,6 +190,7 @@ func initDownloads() {
186190
tasks.Add("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-win64-gpl-7.1.zip", filepath.Join(dwnPath, "ffmpeg_captr.zip"), progressbar.WithBarSpinner(51))
187191
tasks.Add("https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/checksums.sha256", filepath.Join(dwnPath, "checksums.sha256"), progressbar.WithBarSpinner(51))
188192
tasks.Wait()
193+
tasks.Close()
189194
fmt.Println("Checking sha256 hash of the downloaded file.")
190195
file, err := os.ReadFile(filepath.Join(dwnPath, "checksums.sha256"))
191196
if err != nil {
@@ -200,20 +205,22 @@ func initDownloads() {
200205
shaHash := strings.Split(line, " ")[0]
201206
f, err := os.Open(filepath.Join(dwnPath, "ffmpeg_captr.zip"))
202207
if err != nil {
208+
f.Close()
203209
fmt.Println(err)
204210
fmt.Println("Cannot match checksum file of the download. Aborting install...")
205211
os.Remove(filepath.Join(dwnPath, "checksums.sha256"))
206212
os.Remove(filepath.Join(dwnPath, "ffmpeg_captr.zip"))
207213
os.Exit(1)
208214
}
209-
defer f.Close()
210215
h := sha256.New()
211216
if _, err := io.Copy(h, f); err != nil {
217+
f.Close()
212218
fmt.Println("Cannot generate sha256 for the download. Aborting install...")
213219
os.Remove(filepath.Join(dwnPath, "checksums.sha256"))
214220
os.Remove(filepath.Join(dwnPath, "ffmpeg_captr.zip"))
215221
os.Exit(1)
216222
}
223+
f.Close()
217224
if shaHash != fmt.Sprintf("%x", h.Sum(nil)) {
218225
fmt.Println("SHA256 hash unmatched for the downloaded file. Install aborted.")
219226
fmt.Printf("Expected hash: %s\nHash got: %x", shaHash, h.Sum(nil))
@@ -223,7 +230,19 @@ func initDownloads() {
223230
}
224231
}
225232
}
226-
extractFFmpegExe(filepath.Join(dwnPath, "ffmpeg_captr.zip"), dwnPath)
233+
err = extractFFmpegExe(filepath.Join(dwnPath, "ffmpeg_captr.zip"), dwnPath)
234+
if err != nil {
235+
fmt.Println(err)
236+
return
237+
}
238+
err = os.Remove(filepath.Join(dwnPath, "checksums.sha256"))
239+
if err != nil {
240+
fmt.Println(err)
241+
}
242+
err = os.Remove(filepath.Join(dwnPath, "ffmpeg_captr.zip"))
243+
if err != nil {
244+
fmt.Println(err)
245+
}
227246
fmt.Printf("FFMPEG has been downloaded to %s", dwnPath)
228247
} else {
229248
setConfig("record_func_enabled", false)
@@ -300,7 +319,7 @@ v1.0.2
300319
301320
`)
302321
fmt.Println("Open config file by passing the --config flag")
303-
capture_ops := []string{"Record full screen", "Record specific window", "Screenshot specific window", "Screenshot full screen"}
322+
capture_ops := []string{"Record full screen", "Record specific window", "Screenshot specific window", "Screenshot full screen", "Stream a display"}
304323
var i int
305324
err := survey.AskOne(&survey.Select{
306325
Message: "Select Action",
@@ -315,9 +334,13 @@ v1.0.2
315334
switch i {
316335
case 0:
317336
RecordDisplay()
337+
case 1:
338+
RecordWindow()
318339
case 2:
319340
Screenshot_Window()
320341
case 3:
321342
Screenshot_Display()
343+
case 4:
344+
StreamDisp()
322345
}
323346
}

recscreen.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,27 +68,24 @@ func RecordDisplay() {
6868
Options: displays,
6969
}, &display, survey.WithValidator(survey.Required))
7070
if err != nil {
71-
fmt.Print("Some error occurred")
71+
fmt.Print("Action Aborted")
7272
return
7373
}
7474

75-
x, y, w, h := robotgo.GetDisplayBounds(display)
75+
_, _, w, h := robotgo.GetDisplayBounds(display)
7676
filename := filepath.Join(config.SaveLocation, fmt.Sprintf("Recording_Disp%d_%dx%d_%s.mp4", display+1, w, h, time.Now().Format("20060102_150405")))
7777
args := []string{
78-
"-f", "gdigrab",
79-
"-framerate", fmt.Sprintf("%d", config.RecordingOpts.FPS),
80-
"-offset_x", strconv.Itoa(x),
81-
"-offset_y", strconv.Itoa(y),
78+
"-filter_complex", fmt.Sprintf("ddagrab=output_idx=%d:framerate=%d,hwdownload,format=bgra", display, config.RecordingOpts.FPS),
8279
"-video_size", fmt.Sprintf("%dx%d", w, h),
8380
"-draw_mouse", ternary(config.RecordingOpts.CaptureMouse, "1", "0"),
84-
"-i", "desktop",
8581
"-c:v", "libx264",
8682
"-preset", "ultrafast",
8783
"-profile:v", "main",
8884
"-level", "4.0",
8985
"-pix_fmt", "yuv420p",
9086
"-c:a", "aac",
9187
"-movflags", "+faststart",
88+
"-f", "mp4",
9289
"-y", filename,
9390
}
9491

recwindow.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"strconv"
9+
10+
"strings"
11+
"time"
12+
13+
"github.com/go-toast/toast"
14+
"golang.design/x/hotkey"
15+
)
16+
17+
func RecordWindow() {
18+
hwnd := chooseWindow()
19+
ActivateWindowAndGetBounds(uintptr(hwnd))
20+
filename := filepath.Join(config.SaveLocation, fmt.Sprintf("Recording_%s.mp4", time.Now().Format("20060102_150405")))
21+
args := []string{
22+
"-f", "gdigrab",
23+
"-framerate", fmt.Sprintf("%d", config.RecordingOpts.FPS),
24+
"-draw_mouse", ternary(config.RecordingOpts.CaptureMouse, "1", "0"),
25+
"-show_region", "1",
26+
"-i", fmt.Sprintf("hwnd=%d", uintptr(hwnd)),
27+
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
28+
"-c:v", "libx264",
29+
"-preset", "ultrafast",
30+
"-profile:v", "main",
31+
"-level", "4.0",
32+
"-pix_fmt", "yuv420p",
33+
"-movflags", "+faststart",
34+
"-y", filename,
35+
}
36+
37+
cmd := exec.Command(getFfmpegPath(), args...)
38+
cmd.Stderr = os.Stderr
39+
cmd.Stdout = os.Stdout
40+
stdin, _ := cmd.StdinPipe()
41+
var start time.Time
42+
var err error
43+
if err, start = cmd.Start(), time.Now(); err != nil {
44+
fmt.Println("Error starting ffmpeg:", err)
45+
return
46+
}
47+
48+
defer func() {
49+
if r := recover(); r != nil {
50+
fmt.Println("\nUnexpected error:", r)
51+
}
52+
exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
53+
}()
54+
55+
var modkeys []hotkey.Modifier
56+
pressedKeys := map[string]bool{}
57+
for _, key := range config.HotkeyConfig.Modkeys {
58+
pressedKeys[key] = true
59+
}
60+
for _, mod := range []string{"ctrl", "alt", "shift"} {
61+
if pressedKeys[mod] {
62+
switch mod {
63+
case "ctrl":
64+
modkeys = append(modkeys, hotkey.ModCtrl)
65+
case "alt":
66+
modkeys = append(modkeys, hotkey.ModAlt)
67+
case "shift":
68+
modkeys = append(modkeys, hotkey.ModShift)
69+
}
70+
}
71+
}
72+
fmt.Printf("Recording started. Press %s to stop\n", strings.Join(append(config.HotkeyConfig.Modkeys, config.HotkeyConfig.Finalkey), "+"))
73+
tickStop := make(chan struct{})
74+
ticker := time.NewTicker(time.Second)
75+
defer ticker.Stop()
76+
i := 0
77+
last := []string{"🔴", "⚫"}
78+
go func() {
79+
for {
80+
select {
81+
case <-ticker.C:
82+
fmt.Printf("\r%s Recording time elapsed: %02d:%02d", last[i], int(time.Since(start).Minutes()), int(time.Since(start).Seconds())%60)
83+
i = (i + 1) % 2
84+
case <-tickStop:
85+
}
86+
}
87+
}()
88+
go func() {
89+
err = cmd.Wait()
90+
if err != nil {
91+
fmt.Println("Error waiting for ffmpeg to exit:", err)
92+
}
93+
}()
94+
hk := hotkey.New(modkeys, keys[config.HotkeyConfig.Finalkey])
95+
err = hk.Register()
96+
if err != nil {
97+
fmt.Println("Error registering hotkey:", err)
98+
return
99+
}
100+
defer hk.Unregister()
101+
keyChan := hk.Keydown()
102+
for range keyChan {
103+
tickStop <- struct{}{}
104+
stdin.Write([]byte("q"))
105+
stdin.Close()
106+
fmt.Println("\nStopping recording...")
107+
notification := toast.Notification{
108+
AppID: "Captr",
109+
Title: "Recording Stopped",
110+
Message: fmt.Sprintf("Recording saved at %s", filename),
111+
Icon: filename,
112+
ActivationArguments: filename,
113+
Audio: toast.IM,
114+
Actions: []toast.Action{
115+
{Type: "protocol", Label: "Open", Arguments: filename},
116+
},
117+
}
118+
notification.Push()
119+
break
120+
}
121+
122+
fmt.Printf("\nRecording stopped. Recording saved at %s", filename)
123+
}

0 commit comments

Comments
 (0)