Skip to content

Commit 7c5f21c

Browse files
feat: Add streaming to YouTube and Twitch
This feature adds the ability to stream the screen to YouTube and Twitch. It also includes several improvements and fixes to the existing recording functionality. Key changes: - Implement streaming to YouTube and Twitch with hotkey support to stop. - Add a "Twitch (Test Stream)" option to allow users to test their connection. - Improve ffmpeg arguments for better streaming quality and performance. - Add a default audio device to the recording options. - Update the README with the new features and usage instructions. - Bump the version to v2.0.0.
1 parent d406b6a commit 7c5f21c

File tree

5 files changed

+136
-34
lines changed

5 files changed

+136
-34
lines changed

main.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ var (
2828
RecordingOpts: RecordingOptions{
2929
FPS: 30,
3030
CaptureMouse: true,
31+
AudioDevice: "Stereo Mix (Realtek(R) Audio)",
3132
},
3233
HotkeyConfig: hotkeyConfig{
3334
Modkeys: []string{"ctrl", "shift"},
@@ -81,7 +82,7 @@ type streamConfig struct {
8182
type Config struct {
8283
SaveLocation string `json:"save_location"`
8384
RecordFunc bool `json:"record_func_enabled"`
84-
RecordingOpts RecordingOptions `json:"recording_options,omitempty"`
85+
RecordingOpts RecordingOptions `json:"recording_options"`
8586
HotkeyConfig hotkeyConfig `json:"hotkey_config"`
8687
StreamConfig streamConfig `json:"stream_config"`
8788
}
@@ -257,7 +258,6 @@ func init() {
257258
}
258259
initConfig()
259260
configMode, reset, hotkeyConfigMode := flag.Bool("config", false, "Configure Captr"), flag.Bool("reset", false, "Reset Captr and delete appdata"), flag.Bool("hotkey", false, "Register a hotkey for stopping recording")
260-
flag.Parse()
261261
if *configMode {
262262
cmd := exec.Command("notepad.exe", configFilePath)
263263
if err := cmd.Start(); err != nil {
@@ -315,7 +315,7 @@ ________/\\\\\\\\\__________________________________________________________
315315
____\////\\\\\\\\\_\//\\\\\\\\/\\_\/\\\____________\//\\\\\___\/\\\_________
316316
_______\/////////___\////////\//__\///______________\/////____\///__________
317317
318-
v1.0.2
318+
v2.0.0
319319
320320
`)
321321
fmt.Println("Open config file by passing the --config flag")

readme.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
(Windows OS Only)
55

66
> **Status:** Release
7-
> Currently supports full-screen, window-specific screenshots and full-screen recording. Window specific recording is still in development.
7+
> Currently supports full-screen, window-specific screenshots and full-screen recording. Supports streaming to youtube and twitch. Window specific recording is partially implemented.
88
99
## Features
1010

1111
- 📸 Capture full-screen screenshots
1212
- 🖼️ Capture specific window screenshots
1313
- 🎥 Screen recording
14+
- 🎥 Window specific recording (partially implemented)
15+
- 🎥 Stream display to twitch or youtube
1416

1517
## Installation
1618
**It's a portable executable file. Doesn't need any installation.**<br><br>
@@ -19,7 +21,8 @@
1921
## Usage
2022
Download the exe from the releases or [build yourself](#build-yourself).<br>
2123
Run the exe to use it.<br>
22-
For recording functionaility, you must have `ffmpeg` installed and added to path. If you don't have it, the app has the prebuilt prompt to download ffmpeg to the `%appdata%\captr\bin` folder in appdata to use.
24+
For recording and streaming functionaility, you must have `ffmpeg` installed and added to path. If you don't have it, the app has the prebuilt prompt to download ffmpeg to the `%appdata%\captr\bin` folder in appdata to use.<br>
25+
**For audio, please manually configure the config file or enable "Stereo Mix" device in windows settings.**
2326

2427
### Flags
2528
- `--config`: Opens the config file in the notepad for manual edits.
@@ -30,7 +33,8 @@ For recording functionaility, you must have `ffmpeg` installed and added to path
3033
- [x] Full-Screen Screenshots
3134
- [x] Window-Specific Screenshots
3235
- [x] Screen Recording
33-
- [ ] Window Recording [DELAYED]
36+
- [x] Stream to Youtube and Twitch
37+
- [x] Window Recording [Partial]
3438

3539
## Build Yourself
3640
### Prerequisites

recwindow.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"fmt"
5-
"os"
65
"os/exec"
76
"path/filepath"
87
"strconv"
@@ -35,8 +34,6 @@ func RecordWindow() {
3534
}
3635

3736
cmd := exec.Command(getFfmpegPath(), args...)
38-
cmd.Stderr = os.Stderr
39-
cmd.Stdout = os.Stdout
4037
stdin, _ := cmd.StdinPipe()
4138
var start time.Time
4239
var err error

streamdisplay.go

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import (
55
"os"
66
"os/exec"
77
"strconv"
8+
"strings"
9+
"time"
810

911
"github.com/AlecAivazis/survey/v2"
12+
"github.com/go-toast/toast"
1013
"github.com/go-vgo/robotgo"
14+
"golang.design/x/hotkey"
1115
)
1216

1317
const (
14-
YOUTUBE_RTMP = "rtmp://a.rtmp.youtube.com/live2"
18+
YOUTUBE_RTMP = "rtmp://x.rtmp.youtube.com/live2"
1519
TWITCH_RTMP = "rtmp://ingest.global-contribute.live-video.net/app"
1620
)
1721

@@ -35,13 +39,31 @@ func StreamDisp() {
3539
var service string
3640
err = survey.AskOne(&survey.Select{
3741
Message: "Select Service",
38-
Options: []string{"Youtube", "Twitch"},
42+
Options: []string{"Youtube", "Twitch", "Twitch (Test Stream)"},
3943
}, &service, survey.WithValidator(survey.Required))
4044
if err != nil {
4145
fmt.Println("Some error occurred")
4246
return
4347
}
4448

49+
var modkeys []hotkey.Modifier
50+
pressedKeys := map[string]bool{}
51+
for _, key := range config.HotkeyConfig.Modkeys {
52+
pressedKeys[key] = true
53+
}
54+
for _, mod := range []string{"ctrl", "alt", "shift"} {
55+
if pressedKeys[mod] {
56+
switch mod {
57+
case "ctrl":
58+
modkeys = append(modkeys, hotkey.ModCtrl)
59+
case "alt":
60+
modkeys = append(modkeys, hotkey.ModAlt)
61+
case "shift":
62+
modkeys = append(modkeys, hotkey.ModShift)
63+
}
64+
}
65+
}
66+
4567
switch service {
4668
case "Youtube":
4769
key := config.StreamConfig.YoutubeStreamKey
@@ -59,35 +81,74 @@ func StreamDisp() {
5981
setConfig("stream_config", stream_config)
6082
}
6183
args := []string{
62-
"-filter_complex", fmt.Sprintf("ddagrab=output_idx=%d:framerate=%d,hwdownload,format=bgra", display, config.RecordingOpts.FPS),
84+
"-filter_complex", fmt.Sprintf("ddagrab=output_idx=%d:framerate=%d:draw_mouse=%d,hwdownload,format=bgra", display, config.RecordingOpts.FPS, ternary(config.RecordingOpts.CaptureMouse, 1, 0)),
6385
"-f", "dshow",
64-
"-i", "audio=\"Stereo Mix (Realtek(R) Audio)\"",
86+
"-i", fmt.Sprintf("audio=%s", config.RecordingOpts.AudioDevice),
6587
"-c:v", "libx264",
6688
"-preset", "veryfast",
6789
"-b:v", "3000k",
6890
"-c:a", "aac",
6991
"-f", "flv",
7092
fmt.Sprintf("%s/%s", YOUTUBE_RTMP, key),
7193
}
94+
fmt.Println("./ffmpeg", strings.Join(args, " "))
7295
cmd := exec.Command(getFfmpegPath(), args...)
73-
cmd.Stdout = os.Stdout
74-
cmd.Stderr = os.Stderr
75-
if err := cmd.Start(); err != nil {
96+
stdin, _ := cmd.StdinPipe()
97+
var start time.Time
98+
if err, start = cmd.Start(), time.Now(); err != nil {
7699
fmt.Println("Cannot start ffmpeg")
77100
os.Exit(0)
78101
}
102+
fmt.Printf("Streaming started on youtube. Press %s to stop\n", strings.Join(append(config.HotkeyConfig.Modkeys, config.HotkeyConfig.Finalkey), "+"))
103+
tickStop := make(chan struct{})
104+
ticker := time.NewTicker(time.Second)
105+
defer ticker.Stop()
106+
i := 0
107+
last := []string{"🔴", "⚫"}
108+
go func() {
109+
for {
110+
select {
111+
case <-ticker.C:
112+
fmt.Printf("\r%s Streaming time elapsed: %02d:%02d", last[i], int(time.Since(start).Minutes()), int(time.Since(start).Seconds())%60)
113+
i = (i + 1) % 2
114+
case <-tickStop:
115+
}
116+
}
117+
}()
79118
defer func() {
80119
if r := recover(); r != nil {
81120
fmt.Println("\nUnexpected error:", r)
82121
}
83122
exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
84123
}()
85-
err = cmd.Wait()
124+
go func() {
125+
err = cmd.Wait()
126+
if err != nil {
127+
fmt.Println("Error waiting for ffmpeg to exit:", err)
128+
}
129+
}()
130+
hk := hotkey.New(modkeys, keys[config.HotkeyConfig.Finalkey])
131+
err = hk.Register()
86132
if err != nil {
87-
fmt.Println("Error waiting for ffmpeg to exit:", err)
133+
fmt.Println("Error registering hotkey:", err)
134+
return
135+
}
136+
defer hk.Unregister()
137+
keyChan := hk.Keydown()
138+
for range keyChan {
139+
tickStop <- struct{}{}
140+
stdin.Write([]byte("q"))
141+
stdin.Close()
142+
fmt.Println("\nStopping streaming...")
143+
notif := toast.Notification{
144+
AppID: "Captr",
145+
Title: "Streaming stopped",
146+
}
147+
notif.Push()
148+
break
88149
}
89150
fmt.Println("\nStreaming stopped")
90-
case "Twitch":
151+
case "Twitch", "Twitch (Test Stream)":
91152
key := config.StreamConfig.TwitchStreamKey
92153
if key == "" {
93154
fmt.Println("Stream key not set")
@@ -103,31 +164,71 @@ func StreamDisp() {
103164
setConfig("stream_config", stream_config)
104165
}
105166
args := []string{
106-
"-filter_complex", fmt.Sprintf("ddagrab=output_idx=%d:framerate=%d,hwdownload,format=bgra", display, config.RecordingOpts.FPS),
107-
"-draw_mouse", ternary(config.RecordingOpts.CaptureMouse, "1", "0"),
167+
"-filter_complex", fmt.Sprintf("ddagrab=output_idx=%d:framerate=%d:draw_mouse=%d,hwdownload,format=bgra,format=yuv420p", display, config.RecordingOpts.FPS, ternary(config.RecordingOpts.CaptureMouse, 1, 0)),
168+
"-f", "dshow",
169+
"-i", fmt.Sprintf("audio=%s", config.RecordingOpts.AudioDevice),
108170
"-c:v", "libx264",
109-
"-preset", "ultrafast",
110-
"-c:a", "aac",
171+
"-preset", "veryfast",
111172
"-b:v", "3000k",
173+
"-c:a", "aac",
174+
"-pix_fmt", "yuv420p",
112175
"-f", "flv",
113-
fmt.Sprintf("%s/%s", TWITCH_RTMP, key),
176+
fmt.Sprintf("%s/%s", TWITCH_RTMP, ternary(service == "Twitch (Test Stream)", fmt.Sprintf("%s?bandwidthtest=true", key), key)),
114177
}
115178
cmd := exec.Command(getFfmpegPath(), args...)
116-
cmd.Stdout = os.Stdout
117-
cmd.Stderr = os.Stderr
118-
if err := cmd.Start(); err != nil {
179+
stdin, _ := cmd.StdinPipe()
180+
var start time.Time
181+
if err, start = cmd.Start(), time.Now(); err != nil {
119182
fmt.Println("Cannot start ffmpeg")
120183
os.Exit(0)
121184
}
185+
fmt.Printf("Streaming started on twitch. Press %s to stop\n", strings.Join(append(config.HotkeyConfig.Modkeys, config.HotkeyConfig.Finalkey), "+"))
186+
tickStop := make(chan struct{})
187+
ticker := time.NewTicker(time.Second)
188+
defer ticker.Stop()
189+
i := 0
190+
last := []string{"🔴", "⚫"}
191+
go func() {
192+
for {
193+
select {
194+
case <-ticker.C:
195+
fmt.Printf("\r%s Streaming time elapsed: %02d:%02d", last[i], int(time.Since(start).Minutes()), int(time.Since(start).Seconds())%60)
196+
i = (i + 1) % 2
197+
case <-tickStop:
198+
}
199+
}
200+
}()
122201
defer func() {
123202
if r := recover(); r != nil {
124203
fmt.Println("\nUnexpected error:", r)
125204
}
126205
exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid)).Run()
127206
}()
128-
err = cmd.Wait()
207+
go func() {
208+
err = cmd.Wait()
209+
if err != nil {
210+
fmt.Println("Error waiting for ffmpeg to exit:", err)
211+
}
212+
}()
213+
hk := hotkey.New(modkeys, keys[config.HotkeyConfig.Finalkey])
214+
err = hk.Register()
129215
if err != nil {
130-
fmt.Println("Error waiting for ffmpeg to exit:", err)
216+
fmt.Println("Error registering hotkey:", err)
217+
return
218+
}
219+
defer hk.Unregister()
220+
keyChan := hk.Keydown()
221+
for range keyChan {
222+
tickStop <- struct{}{}
223+
stdin.Write([]byte("q"))
224+
stdin.Close()
225+
fmt.Println("\nStopping streaming...")
226+
notif := toast.Notification{
227+
AppID: "Captr",
228+
Title: "Streaming stopped",
229+
}
230+
notif.Push()
231+
break
131232
}
132233
fmt.Println("\nStreaming stopped")
133234
}

utils.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ func chooseWindow() w32.HWND {
239239
}, &result, survey.WithValidator(survey.Required))
240240

241241
if err != nil {
242-
fmt.Printf("Prompt failed %v\n", err)
243-
return w32.HWND(0)
242+
fmt.Printf("Prompt failed")
243+
os.Exit(0)
244244
}
245245

246246
return windows[result]
@@ -446,11 +446,11 @@ func getFfmpegPath() string {
446446
return filepath.Join(appdataDir, "bin", "ffmpeg.exe")
447447
}
448448

449-
func ternary[T any](cond bool, a, b T) T {
449+
func ternary[T any](cond bool, trueval, falseval T) T {
450450
if cond {
451-
return a
451+
return trueval
452452
}
453-
return b
453+
return falseval
454454
}
455455

456456
func RegisterHotkey() ([]string, string) {

0 commit comments

Comments
 (0)