├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── ascii-roulette.gif
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── app.go
├── avatar
├── README.md
├── contributors
│ ├── Sean-Der.png
│ ├── djbaskin.png
│ └── maxhawkins.png
└── main.go
├── camera
├── cam.go
├── cam_avfoundation.h
├── cam_avfoundation.mm
├── cam_callback_darwin.go
├── cam_darwin.go
└── cam_linux.go
├── capture.go
├── cmd
└── ascii_roulette
│ └── main.go
├── conn.go
├── go.mod
├── go.sum
├── install.sh
├── match.go
├── signal
├── Dockerfile
├── README.md
├── conn.go
├── go.mod
├── go.sum
├── group.go
├── main.go
└── server.go
├── term
├── ansi.go
├── input.go
├── input_bsd.go
├── input_linux.go
└── window.go
├── ui
├── ansi.go
├── events.go
├── reducer.go
├── renderer.go
└── state.go
├── videos
├── bindata.go
├── ivfreader.go
├── player.go
├── src
│ ├── globe.ivf
│ └── pion.ivf
└── videos.go
├── vpx
├── decoder.c
├── decoder.go
├── encoder.c
├── encoder.go
└── errors.go
└── yuv
└── encoding.go
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/ascii-roulette.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialup-inc/ascii/07c3fdeef4e5805838478594a0be1e4301f4aed5/.github/ascii-roulette.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ascii_roulette
2 | *.a
3 | *.o
4 | .DS_Store
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: go
3 | env:
4 | - GO111MODULE=on
5 | go:
6 | - 1.12.1
7 | git:
8 | depth: 1
9 |
10 | before_install:
11 | - sudo apt-get update
12 | - sudo apt-get install -y libvpx-dev
13 |
14 | install: true
15 |
16 | script:
17 | - go test -v -race ./...
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2019 Dialup, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | CC = g++
2 |
3 | CFLAGS = \
4 | -x objective-c++ \
5 | -Wall \
6 | -Wcast-align \
7 | -Wundef \
8 | -Wformat-security \
9 | -Wwrite-strings \
10 | -Wno-sign-compare \
11 | -Wno-conversion
12 |
13 | LDFLAGS = \
14 | -framework Foundation \
15 | -framework AVFoundation
16 |
17 | ifeq ($(shell uname -s), Darwin)
18 | libs = camera/libcam.a
19 | else
20 | libs =
21 | endif
22 |
23 | .PHONY: run
24 | run: ascii_roulette
25 | ./ascii_roulette
26 |
27 | ascii_roulette: $(libs) *.go
28 | go build -o $@ ./cmd/ascii_roulette
29 |
30 | camera/libcam.a: camera/cam_avfoundation.o
31 | $(AR) -cr $@ $<
32 |
33 | camera/cam_avfoundation.o: camera/cam_avfoundation.mm
34 | $(CC) $(CFLAGS) -o $@ -c $<
35 |
36 | clean:
37 | rm -f camera/libcam.a camera/cam_avfoundation.o ascii_roulette
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ASCII Roulette
2 |
3 | > 👾 Make friends on the command line!
4 |
5 | [](LICENSE)
6 | [](https://travis-ci.org/dialup-inc/ascii)
7 | [](https://goreportcard.com/report/github.com/dialup-inc/ascii)
8 | [](https://godoc.org/github.com/dialup-inc/ascii)
9 | [](https://github.com/pion/webrtc)
10 |
11 |
12 |
13 |
14 | Chat with a random person without ever leaving your terminal window.
15 |
16 | ## Installation
17 |
18 | **Download the fast way:**
19 |
20 | ```sh
21 | # Paste this command into your terminal to install
22 | bash <(curl -Ls dialup.com/ascii)
23 | ```
24 |
25 | **Or, build from from source:**
26 |
27 | First, install [libvpx](https://www.webmproject.org/code/). On OSX you can run `brew install libvpx`
28 |
29 | ```sh
30 | git clone https://github.com/dialup-inc/ascii.git
31 | cd ascii
32 | make
33 | ```
34 |
35 | ## Contributing
36 |
37 | Contributions and bug reports are welcome! Please check [the issues section](https://github.com/dialup-inc/ascii/issues) before submitting.
38 |
39 | Here are some starter issues if you'd like to help out:
40 |
41 | - [#13 - Windows Support](https://github.com/dialup-inc/ascii/issues/13)
42 | - [#36 - Rooms](https://github.com/dialup-inc/ascii/issues/34)
43 | - [#10 - Scrollback](https://github.com/dialup-inc/ascii/issues/10)
44 | - [#27 - Typing Indicator](https://github.com/dialup-inc/ascii/issues/27)
45 |
46 | #### Our Contributors
47 |
48 |
49 |
50 |
51 |
52 | ## About Dialup
53 |
54 | This app is a project from the Dialup company hackathon.
55 |
56 | [Dialup](https://dialup.com) is a WebRTC-based voice chat app that connects you to a random person with a similar interest.
57 |
58 | _We're hiring!_ Visit [our jobs page](https://dialup.com/jobs) to learn more. We 💖 Go and WebRTC.
59 |
60 | ## Copyright
61 |
62 | Code and documentation Copyright 2019 [Dialup, Inc.](https://dialup.com)
63 |
64 | Code released under the [MIT license](LICENSE).
65 |
66 | [webmaster@dialup.com](mailto:webmaster@dialup.com)
67 |
--------------------------------------------------------------------------------
/app.go:
--------------------------------------------------------------------------------
1 | package ascii
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "fmt"
8 | "image"
9 | "image/color"
10 | "log"
11 | "math"
12 | "os"
13 | "runtime/debug"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/dialup-inc/ascii/term"
19 | "github.com/dialup-inc/ascii/ui"
20 | "github.com/dialup-inc/ascii/videos"
21 | "github.com/dialup-inc/ascii/vpx"
22 | "github.com/pion/stun"
23 | "github.com/pion/webrtc/v2"
24 | )
25 |
26 | const defaultSTUNServer = "stun.l.google.com:19302"
27 |
28 | type App struct {
29 | STUNServer string
30 |
31 | decoder *vpx.Decoder
32 |
33 | signalerURL string
34 |
35 | cancelMu sync.Mutex
36 | quit context.CancelFunc
37 | skipIntro context.CancelFunc
38 | nextPartner context.CancelFunc
39 | startChat context.CancelFunc
40 |
41 | renderer *ui.Renderer
42 |
43 | conn *Conn
44 |
45 | capture *Capture
46 | }
47 |
48 | func (a *App) run(ctx context.Context) error {
49 | a.cancelMu.Lock()
50 | if a.quit != nil {
51 | a.cancelMu.Unlock()
52 | return errors.New("app can only be run once")
53 | }
54 | ctx, cancel := context.WithCancel(ctx)
55 | a.quit = cancel
56 | a.cancelMu.Unlock()
57 |
58 | if err := term.CaptureStdin(a.onKeypress); err != nil {
59 | return err
60 | }
61 |
62 | winSize, _ := term.GetWinSize()
63 | if winSize.Rows < 15 || winSize.Cols < 50 {
64 | ansi := term.ANSI{os.Stdout}
65 | ansi.ResizeWindow(15, 50)
66 | }
67 |
68 | go a.watchWinSize(ctx)
69 |
70 | var introCtx context.Context
71 | introCtx, skipIntro := context.WithCancel(ctx)
72 |
73 | a.cancelMu.Lock()
74 | a.skipIntro = skipIntro
75 | a.cancelMu.Unlock()
76 |
77 | // Play Dialup intro
78 | a.renderer.Dispatch(ui.SetPageEvent(ui.GlobePage))
79 |
80 | player, err := videos.NewPlayer(videos.Globe())
81 | if err != nil {
82 | log.Fatal(err)
83 | }
84 | player.OnFrame = func(img image.Image) {
85 | a.renderer.Dispatch(ui.FrameEvent(img))
86 | }
87 | player.Play(introCtx)
88 |
89 | // Play Pion intro
90 | a.renderer.Dispatch(ui.SetPageEvent(ui.PionPage))
91 |
92 | player, err = videos.NewPlayer(videos.Pion())
93 | if err != nil {
94 | log.Fatal(err)
95 | }
96 | player.OnFrame = func(img image.Image) {
97 | a.renderer.Dispatch(ui.FrameEvent(img))
98 | }
99 | player.Play(introCtx)
100 |
101 | // Show confirmation page
102 | a.renderer.Dispatch(ui.SetPageEvent(ui.ConfirmPage))
103 |
104 | startCtx, startChat := context.WithCancel(ctx)
105 |
106 | a.cancelMu.Lock()
107 | a.startChat = startChat
108 | a.cancelMu.Unlock()
109 |
110 | <-startCtx.Done()
111 |
112 | // give user a chance to quit
113 | if err := ctx.Err(); err != nil {
114 | return nil
115 | }
116 |
117 | a.renderer.Dispatch(ui.SetPageEvent(ui.ChatPage))
118 |
119 | // Start up camera
120 | for {
121 | if err := ctx.Err(); err != nil {
122 | return nil
123 | }
124 |
125 | err := a.capture.Start(0, 5)
126 | if err == nil {
127 | break
128 | }
129 | msg := fmt.Sprintf("camera error: %v", err)
130 | a.renderer.Dispatch(ui.LogEvent{
131 | Level: ui.LogLevelError,
132 | Text: msg,
133 | })
134 |
135 | select {
136 | case <-time.After(1500 * time.Millisecond):
137 | continue
138 | case <-ctx.Done():
139 | return nil
140 | }
141 | }
142 |
143 | // Attempt to find match
144 | var backoff float64
145 | for {
146 | if err := ctx.Err(); err != nil {
147 | break
148 | }
149 |
150 | var connCtx context.Context
151 | connCtx, nextPartner := context.WithCancel(ctx)
152 |
153 | a.cancelMu.Lock()
154 | a.nextPartner = nextPartner
155 | a.cancelMu.Unlock()
156 |
157 | endReason, err := a.connect(connCtx)
158 | // HACK(maxhawkins): these errors get returned when the context passed
159 | // into match is canceled, so we ignore them. There's probably a more elegant
160 | // way to close the websocket without all this error munging.
161 | if err != nil && strings.Contains(err.Error(), "use of closed network connection") {
162 | continue
163 | } else if err != nil && strings.Contains(err.Error(), "operation was canceled") {
164 | continue
165 | } else if err != nil {
166 | a.renderer.Dispatch(ui.LogEvent{
167 | Level: ui.LogLevelError,
168 | Text: err.Error(),
169 | })
170 |
171 | sec := math.Pow(2, backoff) - 1
172 | if backoff < 4 {
173 | backoff++
174 | }
175 |
176 | select {
177 | case <-time.After(time.Duration(sec) * time.Second):
178 | continue
179 | case <-ctx.Done():
180 | return nil
181 | }
182 | }
183 |
184 | a.renderer.Dispatch(ui.ConnEndedEvent{endReason})
185 | a.renderer.Dispatch(ui.FrameEvent(nil))
186 |
187 | time.Sleep(100 * time.Millisecond)
188 | }
189 |
190 | return nil
191 | }
192 |
193 | func (a *App) catchError(msg interface{}, stack []byte) {
194 | buf := bytes.NewBuffer(nil)
195 | ansi := term.ANSI{buf}
196 |
197 | ansi.CursorPosition(1, 1)
198 | ansi.Reset()
199 |
200 | ansi.Bold()
201 | ansi.Foreground(color.RGBA{0xFF, 0x00, 0x00, 0xFF})
202 | buf.WriteString("Oops! ASCII Roulette hit a snag.\n")
203 | ansi.Normal()
204 | ansi.ForegroundReset()
205 |
206 | buf.WriteString("\n")
207 | buf.WriteString("Please report this error at https://github.com/dialup-inc/ascii/issues/new/choose\n")
208 | buf.WriteString("\n")
209 | buf.WriteString("\n")
210 |
211 | buf.WriteString(fmt.Sprintf("[panic] %v\n", msg))
212 | buf.WriteString("\n")
213 | buf.Write(stack)
214 | buf.WriteString("\n")
215 |
216 | data := bytes.ReplaceAll(buf.Bytes(), []byte("\n"), []byte("\r\n"))
217 | os.Stderr.Write(data)
218 | }
219 |
220 | func (a *App) Run(ctx context.Context) error {
221 | // Show a nice error page if there's a panic somewhere in the code
222 | defer func() {
223 | if r := recover(); r != nil {
224 | a.catchError(r, debug.Stack())
225 | }
226 | }()
227 |
228 | err := a.run(ctx)
229 |
230 | // Clean up:
231 | a.renderer.Stop()
232 | if a.conn != nil && a.conn.IsConnected() {
233 | a.conn.SendBye()
234 | }
235 |
236 | return err
237 | }
238 |
239 | func (a *App) watchWinSize(ctx context.Context) error {
240 | checkWinSize := func() {
241 | winSize, err := term.GetWinSize()
242 | if err != nil {
243 | return
244 | }
245 | a.renderer.Dispatch(ui.ResizeEvent(winSize))
246 | }
247 |
248 | checkWinSize()
249 |
250 | tick := time.Tick(500 * time.Millisecond)
251 | for {
252 | select {
253 | case <-ctx.Done():
254 | return ctx.Err()
255 | case <-tick:
256 | checkWinSize()
257 | }
258 | }
259 | }
260 |
261 | func (a *App) sendMessage() {
262 | if a.conn == nil || !a.conn.IsConnected() {
263 | return
264 | }
265 |
266 | msg := a.renderer.GetState().Input
267 | msg = strings.TrimSpace(msg)
268 |
269 | // Don't send empty messages
270 | if len(msg) == 0 {
271 | return
272 | }
273 |
274 | if err := a.conn.SendMessage(msg); err != nil {
275 | a.renderer.Dispatch(ui.LogEvent{
276 | Level: ui.LogLevelError,
277 | Text: fmt.Sprintf("sending failed: %v", err),
278 | })
279 | } else {
280 | a.renderer.Dispatch(ui.SentMessageEvent(msg))
281 | }
282 | }
283 |
284 | func (a *App) checkConnection(ctx context.Context) error {
285 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
286 | defer cancel()
287 |
288 | c, err := stun.Dial("udp", a.STUNServer)
289 | if err != nil {
290 | return errors.New("connection error: check your firewall or network")
291 | }
292 | message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
293 |
294 | errChan := make(chan error)
295 |
296 | go c.Do(message, func(res stun.Event) {
297 | errChan <- res.Error
298 | })
299 |
300 | select {
301 | case err := <-errChan:
302 | if err != nil {
303 | return errors.New("binding request failed: firewall may be blocking connections")
304 | }
305 | return nil
306 |
307 | case <-ctx.Done():
308 | switch ctx.Err() {
309 | case context.Canceled:
310 | return err
311 | case context.DeadlineExceeded:
312 | return errors.New("binding request failed: firewall may be blocking connections")
313 | default:
314 | return nil
315 | }
316 | }
317 | }
318 |
319 | func (a *App) connect(ctx context.Context) (ui.EndConnReason, error) {
320 | ctx, cancel := context.WithCancel(ctx)
321 | defer cancel()
322 |
323 | frameTimeout := time.NewTimer(999 * time.Hour)
324 | frameTimeout.Stop()
325 |
326 | connectTimeout := time.NewTimer(999 * time.Hour)
327 | connectTimeout.Stop()
328 |
329 | ended := make(chan ui.EndConnReason)
330 |
331 | conn, err := NewConn(webrtc.Configuration{
332 | ICEServers: []webrtc.ICEServer{
333 | {URLs: []string{fmt.Sprintf("stun:%s", a.STUNServer)}},
334 | },
335 | })
336 | if err != nil {
337 | return ui.EndConnSetupError, err
338 | }
339 | a.conn = conn
340 |
341 | defer func() {
342 | // Turn off callbacks
343 | conn.OnBye = func() {}
344 | conn.OnMessage = func(string) {}
345 | conn.OnICEConnectionStateChange = func(webrtc.ICEConnectionState) {}
346 | conn.OnFrame = func([]byte) {}
347 | conn.OnPLI = func() {}
348 | conn.OnDataOpen = func() {}
349 |
350 | // Send Goodbye packet
351 | if conn.IsConnected() {
352 | conn.SendBye()
353 | }
354 | }()
355 |
356 | conn.OnBye = func() {
357 | a.renderer.Dispatch(ui.FrameEvent(nil))
358 | ended <- ui.EndConnGone
359 | }
360 | conn.OnMessage = func(s string) {
361 | a.renderer.Dispatch(ui.ReceivedChatEvent(s))
362 | }
363 | conn.OnICEConnectionStateChange = func(s webrtc.ICEConnectionState) {
364 | switch s {
365 | case webrtc.ICEConnectionStateConnected:
366 | a.capture.RequestKeyframe()
367 | connectTimeout.Stop()
368 | a.renderer.Dispatch(ui.ConnStartedEvent{})
369 |
370 | case webrtc.ICEConnectionStateDisconnected:
371 | a.renderer.Dispatch(ui.LogEvent{
372 | Level: ui.LogLevelInfo,
373 | Text: "Reconnecting...",
374 | })
375 |
376 | case webrtc.ICEConnectionStateFailed:
377 | ended <- ui.EndConnDisconnected
378 | }
379 | }
380 | conn.OnDataOpen = func() {
381 | a.renderer.Dispatch(ui.DataOpenedEvent{})
382 | }
383 |
384 | a.capture.SetTrack(conn.SendTrack)
385 |
386 | dec, err := vpx.NewDecoder(320, 240)
387 | if err != nil {
388 | return ui.EndConnSetupError, err
389 | }
390 | conn.OnFrame = func(frame []byte) {
391 | frameTimeout.Reset(5 * time.Second)
392 | connectTimeout.Stop()
393 |
394 | img, err := dec.Decode(frame)
395 | if err != nil {
396 | conn.SendPLI()
397 | return
398 | }
399 | a.renderer.Dispatch(ui.FrameEvent(img))
400 | }
401 | conn.OnPLI = func() {
402 | a.capture.RequestKeyframe()
403 | }
404 |
405 | a.renderer.Dispatch(ui.LogEvent{
406 | Level: ui.LogLevelInfo,
407 | Text: "Searching for match...",
408 | })
409 |
410 | err = Match(ctx, a.signalerURL, conn.pc)
411 | if err == errMatchFailed {
412 | return ui.EndConnMatchError, nil
413 | }
414 | if err != nil {
415 | return ui.EndConnMatchError, err
416 | }
417 |
418 | connectTimeout.Reset(10 * time.Second)
419 |
420 | a.renderer.Dispatch(ui.LogEvent{
421 | Level: ui.LogLevelInfo,
422 | Text: "Found match. Connecting...",
423 | })
424 |
425 | if err := a.checkConnection(ctx); err != nil {
426 | return ui.EndConnSetupError, err
427 | }
428 |
429 | var reason ui.EndConnReason
430 | select {
431 | case <-ctx.Done():
432 | reason = ui.EndConnNormal
433 | case <-connectTimeout.C:
434 | reason = ui.EndConnTimedOut
435 | case <-frameTimeout.C:
436 | reason = ui.EndConnDisconnected
437 | case r := <-ended:
438 | reason = r
439 | }
440 |
441 | return reason, nil
442 | }
443 |
444 | func (a *App) onKeypress(c rune) {
445 | switch c {
446 | case 3: // ctrl-c
447 |
448 | a.renderer.Dispatch(ui.LogEvent{
449 | Level: ui.LogLevelInfo,
450 | Text: "Quitting...",
451 | })
452 |
453 | a.cancelMu.Lock()
454 | if a.quit != nil {
455 | a.quit()
456 | }
457 | a.cancelMu.Unlock()
458 |
459 | case 4: // ctrl-d
460 | a.cancelMu.Lock()
461 | if a.nextPartner != nil {
462 | a.nextPartner()
463 | a.nextPartner = nil
464 | }
465 | a.cancelMu.Unlock()
466 |
467 | a.renderer.Dispatch(ui.SkipEvent{})
468 |
469 | case 20: // ctrl-t
470 | a.renderer.Dispatch(ui.ToggleHelpEvent{})
471 |
472 | case 127: // backspace
473 | a.renderer.Dispatch(ui.BackspaceEvent{})
474 |
475 | case '\n', '\r':
476 | a.cancelMu.Lock()
477 | if a.startChat != nil {
478 | a.startChat()
479 | a.startChat = nil
480 | }
481 | a.cancelMu.Unlock()
482 |
483 | a.sendMessage()
484 |
485 | case ' ':
486 | a.renderer.Dispatch(ui.KeypressEvent(c))
487 |
488 | a.cancelMu.Lock()
489 | if a.skipIntro != nil {
490 | a.skipIntro()
491 | a.skipIntro = nil
492 | }
493 | if a.startChat != nil {
494 | a.startChat()
495 | a.startChat = nil
496 | }
497 | a.cancelMu.Unlock()
498 |
499 | default:
500 | a.renderer.Dispatch(ui.KeypressEvent(c))
501 | }
502 | }
503 |
504 | func New(signalerURL string) (*App, error) {
505 | cap, err := NewCapture(320, 240)
506 | if err != nil {
507 | return nil, err
508 | }
509 |
510 | a := &App{
511 | signalerURL: signalerURL,
512 | STUNServer: defaultSTUNServer,
513 |
514 | renderer: ui.NewRenderer(),
515 | capture: cap,
516 | }
517 | a.renderer.Start()
518 |
519 | return a, nil
520 | }
521 |
--------------------------------------------------------------------------------
/avatar/README.md:
--------------------------------------------------------------------------------
1 | # ASCII Avatar Script
2 |
3 | This is a tiny script to make the ascii-fied avatars that appear on
4 | the contributor list.
5 |
6 | Just run:
7 |
8 | ```bash
9 | go run *.go
10 | ```
11 |
--------------------------------------------------------------------------------
/avatar/contributors/Sean-Der.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialup-inc/ascii/07c3fdeef4e5805838478594a0be1e4301f4aed5/avatar/contributors/Sean-Der.png
--------------------------------------------------------------------------------
/avatar/contributors/djbaskin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialup-inc/ascii/07c3fdeef4e5805838478594a0be1e4301f4aed5/avatar/contributors/djbaskin.png
--------------------------------------------------------------------------------
/avatar/contributors/maxhawkins.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialup-inc/ascii/07c3fdeef4e5805838478594a0be1e4301f4aed5/avatar/contributors/maxhawkins.png
--------------------------------------------------------------------------------
/avatar/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "image"
7 | "log"
8 | "net/http"
9 | "os"
10 | "time"
11 |
12 | "github.com/dialup-inc/ascii/term"
13 | "github.com/dialup-inc/ascii/ui"
14 |
15 | "image/color"
16 | _ "image/jpeg"
17 | _ "image/png"
18 | )
19 |
20 | type profile struct {
21 | AvatarURL string `json:"avatar_url"`
22 | }
23 |
24 | func fetchAvatar(username string) (image.Image, error) {
25 | resp, err := http.Get("https://api.github.com/users/" + username)
26 | if err != nil {
27 | return nil, err
28 | }
29 | defer resp.Body.Close()
30 |
31 | var prof profile
32 | if err := json.NewDecoder(resp.Body).Decode(&prof); err != nil {
33 | return nil, err
34 | }
35 |
36 | resp, err = http.Get(prof.AvatarURL)
37 | if err != nil {
38 | return nil, err
39 | }
40 | defer resp.Body.Close()
41 |
42 | img, _, err := image.Decode(resp.Body)
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | return img, nil
48 | }
49 |
50 | func main() {
51 | flag.Parse()
52 |
53 | username := flag.Arg(0)
54 |
55 | img, err := fetchAvatar(username)
56 | if err != nil {
57 | log.Fatal(err)
58 | }
59 |
60 | ansi := term.ANSI{os.Stdout}
61 |
62 | defer func() {
63 | ansi.ShowCursor()
64 | ansi.Reset()
65 | os.Stdout.Sync()
66 | }()
67 |
68 | ws, err := term.GetWinSize()
69 | if err != nil {
70 | log.Fatal(err)
71 | }
72 | aspect := float64(ws.Height) * float64(ws.Cols) / float64(ws.Rows) / float64(ws.Width)
73 |
74 | ansi.ResizeWindow(10, int(10*aspect))
75 |
76 | // Let the resize happen
77 | time.Sleep(500 * time.Millisecond)
78 |
79 | ws, err = term.GetWinSize()
80 | if err != nil {
81 | log.Fatal(err)
82 | }
83 |
84 | ansi.Background(color.RGBA{0, 0, 0, 255})
85 | ansi.CursorPosition(1, 1)
86 |
87 | imgANSI := ui.Image2ANSI(img, ws.Cols, ws.Rows, aspect, false)
88 | os.Stdout.Write(imgANSI)
89 |
90 | ansi.HideCursor()
91 |
92 | buf := make([]byte, 1)
93 | os.Stdin.Read(buf)
94 | }
95 |
--------------------------------------------------------------------------------
/camera/cam.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | import "image"
4 |
5 | type FrameCallback func(image.Image, error)
6 |
--------------------------------------------------------------------------------
/camera/cam_avfoundation.h:
--------------------------------------------------------------------------------
1 | #ifndef _CAM_H_
2 | #define _CAM_H_
3 |
4 | #ifdef __cplusplus
5 | extern "C" {
6 | #endif
7 |
8 | #define E_OK 0
9 | #define E_INIT_FAILED -1
10 | #define E_OPEN_CAMERA_FAILED -2
11 | #define E_CAMERA_NOT_FOUND -3
12 |
13 | typedef void (*FrameCallback)(void *userdata, void *buf, int len, int width,
14 | int height);
15 |
16 | typedef void *Camera;
17 | int cam_init(Camera *cam, FrameCallback callback, void *userdata);
18 | int cam_start(Camera cam, int cam_id, int width, int height);
19 | int cam_close(Camera cam);
20 |
21 | #ifdef __cplusplus
22 | }
23 | #endif
24 |
25 | #endif
26 |
--------------------------------------------------------------------------------
/camera/cam_avfoundation.mm:
--------------------------------------------------------------------------------
1 | #import "cam_avfoundation.h"
2 |
3 | #import
4 |
5 | // image_src is the source image, image_dst is the converted image
6 | void NV12_YUV420P(const unsigned char *image_src, unsigned char *image_dst,
7 | int image_width, int image_height) {
8 | unsigned char *p = image_dst;
9 | memcpy(p, image_src, image_width * image_height * 3 / 2);
10 | const unsigned char *pNV = image_src + image_width * image_height;
11 | unsigned char *pU = p + image_width * image_height;
12 | unsigned char *pV =
13 | p + image_width * image_height + ((image_width * image_height) >> 2);
14 | for (int i = 0; i < (image_width * image_height) / 2; i++) {
15 | if ((i % 2) == 0)
16 | *pU++ = *(pNV + i);
17 | else
18 | *pV++ = *(pNV + i);
19 | }
20 | }
21 |
22 | @interface FrameDelegate
23 | : NSObject {
24 | FrameCallback mCallback;
25 | void *mUserdata;
26 | }
27 |
28 | - (void)captureOutput:(AVCaptureOutput *)captureOutput
29 | didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
30 | fromConnection:(AVCaptureConnection *)connection;
31 |
32 | @end
33 |
34 | @implementation FrameDelegate
35 |
36 | - (id)initWithCallback:(FrameCallback)callback Userdata:(void *)userdata {
37 | [super init];
38 |
39 | self->mCallback = callback;
40 | self->mUserdata = userdata;
41 |
42 | return self;
43 | }
44 |
45 | - (void)captureOutput:(AVCaptureOutput *)captureOutput
46 | didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
47 | fromConnection:(AVCaptureConnection *)connection {
48 |
49 | if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 ||
50 | !CMSampleBufferIsValid(sampleBuffer) ||
51 | !CMSampleBufferDataIsReady(sampleBuffer)) {
52 | return;
53 | }
54 |
55 | CVImageBufferRef image_buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
56 | if (image_buffer == NULL) {
57 | return;
58 | }
59 |
60 | image_buffer = CVBufferRetain(image_buffer);
61 |
62 | CVReturn ret =
63 | CVPixelBufferLockBaseAddress(image_buffer, kCVPixelBufferLock_ReadOnly);
64 | if (ret != kCVReturnSuccess) {
65 | return;
66 | }
67 | static size_t const kYPlaneIndex = 0;
68 | static size_t const kUVPlaneIndex = 1;
69 | uint8_t *y_plane_address = static_cast(
70 | CVPixelBufferGetBaseAddressOfPlane(image_buffer, kYPlaneIndex));
71 | size_t y_plane_height =
72 | CVPixelBufferGetHeightOfPlane(image_buffer, kYPlaneIndex);
73 | size_t y_plane_width =
74 | CVPixelBufferGetWidthOfPlane(image_buffer, kYPlaneIndex);
75 | size_t y_plane_bytes_per_row =
76 | CVPixelBufferGetBytesPerRowOfPlane(image_buffer, kYPlaneIndex);
77 | size_t uv_plane_height =
78 | CVPixelBufferGetHeightOfPlane(image_buffer, kUVPlaneIndex);
79 | size_t uv_plane_bytes_per_row =
80 | CVPixelBufferGetBytesPerRowOfPlane(image_buffer, kUVPlaneIndex);
81 | size_t frame_size = y_plane_bytes_per_row * y_plane_height +
82 | uv_plane_bytes_per_row * uv_plane_height;
83 |
84 | // TODO(maxhawkins): is this slow?
85 | unsigned char *i420_image = (unsigned char *)malloc(frame_size);
86 | NV12_YUV420P(y_plane_address, i420_image, y_plane_width, y_plane_height);
87 |
88 | self->mCallback(self->mUserdata, i420_image, frame_size, y_plane_width,
89 | y_plane_height);
90 |
91 | free(i420_image);
92 |
93 | CVPixelBufferUnlockBaseAddress(image_buffer, 0);
94 | CVBufferRelease(image_buffer);
95 | }
96 |
97 | @end
98 |
99 | class Capture {
100 | public:
101 | Capture(FrameCallback callback, void *userdata);
102 | ~Capture();
103 |
104 | int start(int cam_id, int width, int height);
105 |
106 | private:
107 | AVCaptureSession *mSession;
108 | AVCaptureDeviceInput *mInput;
109 | AVCaptureVideoDataOutput *mOutput;
110 | AVCaptureDevice *mCamera;
111 | FrameDelegate *mDelegate;
112 |
113 | FrameCallback mCallback;
114 | void *mUserdata;
115 | };
116 |
117 | Capture::Capture(FrameCallback callback, void *userdata) {
118 | mCallback = callback;
119 | mUserdata = userdata;
120 | }
121 |
122 | int Capture::start(int cam_id, int width, int height) {
123 | NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
124 |
125 | NSArray *cameras = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]
126 | arrayByAddingObjectsFromArray:[AVCaptureDevice
127 | devicesWithMediaType:AVMediaTypeVideo]];
128 |
129 | if (cam_id < 0 || cam_id >= int(cameras.count)) {
130 | [pool drain];
131 | return E_CAMERA_NOT_FOUND;
132 | }
133 | mCamera = cameras[cam_id];
134 | if (!mCamera) {
135 | [pool drain];
136 | return E_CAMERA_NOT_FOUND;
137 | }
138 |
139 | NSError *error = nil;
140 | mInput = [[AVCaptureDeviceInput alloc] initWithDevice:mCamera error:&error];
141 | if (error) {
142 | NSLog(@"Camera init failed: %@", error.localizedDescription);
143 | [pool drain];
144 | return E_OPEN_CAMERA_FAILED;
145 | }
146 |
147 | mDelegate = [[FrameDelegate alloc] initWithCallback:mCallback
148 | Userdata:mUserdata];
149 |
150 | mOutput = [[AVCaptureVideoDataOutput alloc] init];
151 | dispatch_queue_t queue =
152 | dispatch_queue_create("captureQueue", DISPATCH_QUEUE_SERIAL);
153 | [mOutput setSampleBufferDelegate:mDelegate queue:queue];
154 | dispatch_release(queue);
155 |
156 | mOutput.videoSettings = @{
157 | (id)kCVPixelBufferWidthKey : @(1.0 * width),
158 | (id)kCVPixelBufferHeightKey : @(1.0 * height),
159 | (id)kCVPixelBufferPixelFormatTypeKey :
160 | @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
161 | };
162 | mOutput.alwaysDiscardsLateVideoFrames = YES;
163 |
164 | mSession = [[AVCaptureSession alloc] init];
165 | mSession.sessionPreset = AVCaptureSessionPresetMedium;
166 | [mSession addInput:mInput];
167 | [mSession addOutput:mOutput];
168 |
169 | [mSession startRunning];
170 |
171 | [pool drain];
172 | return E_OK;
173 | }
174 |
175 | extern "C" {
176 |
177 | // cam_init allocates a new Camera and sets its frame callback
178 | int cam_init(Camera *cam, FrameCallback callback, void *userdata) {
179 | if (!callback) {
180 | return E_INIT_FAILED;
181 | }
182 |
183 | Capture *capture = new Capture(callback, userdata);
184 | *cam = capture;
185 |
186 | return E_OK;
187 | };
188 |
189 | // cam_start makes a Camera begin reading frames from the device
190 | int cam_start(Camera cam, int cam_id, int width, int height) {
191 | Capture *capture = (Capture *)cam;
192 | return capture->start(cam_id, width, height);
193 | };
194 | }
195 |
--------------------------------------------------------------------------------
/camera/cam_callback_darwin.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | import (
4 | "sync"
5 | "unsafe"
6 | )
7 |
8 | /*
9 | extern void onFrame(void *userdata, void *buf, int len, int width, int height);
10 | */
11 | import "C"
12 |
13 | type frameCb func(frame []byte, width, height int)
14 |
15 | var mu sync.Mutex
16 | var nextID handleID
17 | var handles = make(map[handleID]frameCb)
18 |
19 | type handleID int
20 |
21 | //export onFrame
22 | func onFrame(userdata unsafe.Pointer, buf unsafe.Pointer, size, cWidth, cHeight C.int) {
23 | data := C.GoBytes(buf, size)
24 | width, height := int(cWidth), int(cHeight)
25 |
26 |
27 | handleNum := (*C.int)(userdata)
28 |
29 | cb := lookup(handleID(*handleNum))
30 | cb(data, width, height)
31 | }
32 |
33 | func register(fn frameCb) handleID {
34 | mu.Lock()
35 | defer mu.Unlock()
36 |
37 | nextID++
38 | for handles[nextID] != nil {
39 | nextID++
40 | }
41 | handles[nextID] = fn
42 |
43 | return nextID
44 | }
45 |
46 | func lookup(i handleID) frameCb {
47 | mu.Lock()
48 | defer mu.Unlock()
49 |
50 | return handles[i]
51 | }
52 |
53 | func unregister(i handleID) {
54 | mu.Lock()
55 | defer mu.Unlock()
56 |
57 | delete(handles, i)
58 | }
59 |
--------------------------------------------------------------------------------
/camera/cam_darwin.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | /*
4 | #cgo LDFLAGS: -L. -lcam -lc++ -framework Foundation -framework AVFoundation -framework CoreVideo -framework CoreMedia
5 |
6 | #include "cam_avfoundation.h"
7 |
8 | extern void onFrame(void *userdata, void *buf, int len, int width, int height);
9 | void onFrame_cgo(void *userdata, void *buf, int len, int width, int height) {
10 | onFrame(userdata, buf, len, width, height);
11 | }
12 | */
13 | import "C"
14 | import (
15 | "fmt"
16 | "unsafe"
17 |
18 | "github.com/dialup-inc/ascii/yuv"
19 | )
20 | type Camera struct {
21 | c C.Camera
22 |
23 | handleID handleID
24 | callback FrameCallback
25 | }
26 |
27 | type CamError int
28 |
29 | const (
30 | ErrCamOK CamError = 0
31 | ErrCamInitFailed CamError = -1
32 | ErrCamOpenFailed = -2
33 | ErrCamNotFound = -3
34 | )
35 |
36 | func (c CamError) Error() string {
37 | switch c {
38 | case ErrCamInitFailed:
39 | return "init failed"
40 | case ErrCamOpenFailed:
41 | return "open failed"
42 | case ErrCamNotFound:
43 | return "not found"
44 | default:
45 | return fmt.Sprintf("error %d", c)
46 | }
47 | }
48 |
49 | func (c *Camera) Start(camID, width, height int) error {
50 | if ret := C.cam_start(c.c, C.int(camID), C.int(width), C.int(height)); ret != 0 {
51 | return CamError(ret)
52 | }
53 | return nil
54 | }
55 |
56 | func (c *Camera) Close() error {
57 | // TODO
58 | return nil
59 | }
60 |
61 | func (c *Camera) onFrame(data []byte, width, height int) {
62 | c.callback(yuv.FromI420(data, width, height))
63 | }
64 |
65 | func New(cb FrameCallback) (*Camera, error) {
66 | cam := &Camera{}
67 |
68 | cam.callback = cb
69 | cam.handleID = register(func(data []byte, width, height int) {
70 | cam.onFrame(data, width, height)
71 | })
72 |
73 | if ret := C.cam_init(&cam.c, (C.FrameCallback)(unsafe.Pointer(C.onFrame_cgo)), unsafe.Pointer(&cam.handleID)); ret != 0 {
74 | return nil, fmt.Errorf("error %d", CamError(ret))
75 | }
76 | return cam, nil
77 | }
78 |
--------------------------------------------------------------------------------
/camera/cam_linux.go:
--------------------------------------------------------------------------------
1 | package camera
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "image/jpeg"
7 | "os"
8 | "strings"
9 |
10 | "github.com/blackjack/webcam"
11 | )
12 |
13 | const webcamReadTimeout = 5
14 |
15 | type Camera struct {
16 | callback FrameCallback
17 | }
18 |
19 | func (c *Camera) Start(camID, width, height int) error {
20 | cam, err := webcam.Open("/dev/video0")
21 | if err != nil {
22 | return err
23 | }
24 |
25 | var selectedFormat webcam.PixelFormat
26 | for v, k := range cam.GetSupportedFormats() {
27 | if strings.HasPrefix(k, "Motion-JPEG") {
28 | selectedFormat = v
29 | break
30 | }
31 | }
32 |
33 | if selectedFormat == 0 {
34 | return fmt.Errorf("Only Motion-JPEG supported")
35 | }
36 |
37 | if _, _, _, err = cam.SetImageFormat(selectedFormat, uint32(width), uint32(height)); err != nil {
38 | return err
39 | }
40 |
41 | if err = cam.StartStreaming(); err != nil {
42 | return err
43 | }
44 |
45 | go func() {
46 | for {
47 | err = cam.WaitForFrame(webcamReadTimeout)
48 | switch err.(type) {
49 | case nil:
50 | case *webcam.Timeout:
51 | fmt.Fprint(os.Stderr, err.Error())
52 | continue
53 | default:
54 | c.callback(nil, err)
55 | return
56 | }
57 |
58 | frame, err := cam.ReadFrame()
59 | if len(frame) != 0 {
60 | img, err := jpeg.Decode(bytes.NewReader(frame))
61 | c.callback(img, err)
62 | } else if err != nil {
63 | c.callback(nil, err)
64 | }
65 | }
66 | }()
67 | return nil
68 | }
69 |
70 | func (c *Camera) Close() error {
71 | // TODO
72 | return nil
73 | }
74 |
75 | func New(cb FrameCallback) (*Camera, error) {
76 | return &Camera{callback: cb}, nil
77 | }
78 |
--------------------------------------------------------------------------------
/capture.go:
--------------------------------------------------------------------------------
1 | package ascii
2 |
3 | import (
4 | "image"
5 | "sync"
6 | "sync/atomic"
7 |
8 | "github.com/dialup-inc/ascii/camera"
9 | "github.com/dialup-inc/ascii/vpx"
10 | "github.com/pion/webrtc/v2"
11 | "github.com/pion/webrtc/v2/pkg/media"
12 | )
13 |
14 | func NewCapture(width, height int) (*Capture, error) {
15 | cap := &Capture{
16 | vpxBuf: make([]byte, 5*1024*1024),
17 | width: width,
18 | height: height,
19 | }
20 |
21 | enc, err := vpx.NewEncoder(width, height)
22 | if err != nil {
23 | return nil, err
24 | }
25 | cap.enc = enc
26 |
27 | cam, err := camera.New(cap.onFrame)
28 | if err != nil {
29 | return nil, err
30 | }
31 | cap.cam = cam
32 |
33 | return cap, nil
34 | }
35 |
36 | type Capture struct {
37 | enc *vpx.Encoder
38 | cam *camera.Camera
39 |
40 | width int
41 | height int
42 |
43 | ptsMu sync.Mutex
44 | pts int
45 |
46 | vpxBuf []byte
47 |
48 | forceKeyframe uint32
49 | encodeLock uint32
50 |
51 | track *webrtc.Track
52 | }
53 |
54 | func (c *Capture) Start(camID int, frameRate float32) error {
55 | return c.cam.Start(camID, c.width, c.height)
56 | }
57 |
58 | func (c *Capture) Stop() error {
59 | // TODO
60 | return nil
61 | }
62 |
63 | func (c *Capture) RequestKeyframe() {
64 | atomic.StoreUint32(&c.forceKeyframe, 1)
65 | }
66 |
67 | func (c *Capture) SetTrack(track *webrtc.Track) {
68 | c.track = track
69 | }
70 |
71 | func (c *Capture) onFrame(img image.Image, err error) {
72 | if err != nil {
73 | return
74 | }
75 |
76 | if !atomic.CompareAndSwapUint32(&c.encodeLock, 0, 1) {
77 | return
78 | }
79 | defer atomic.StoreUint32(&c.encodeLock, 0)
80 |
81 | forceKeyframe := atomic.CompareAndSwapUint32(&c.forceKeyframe, 1, 0)
82 |
83 | n, err := c.enc.Encode(c.vpxBuf, img, c.pts, forceKeyframe)
84 | if err != nil {
85 | // fmt.Println("encode: ", err)
86 | return
87 | }
88 | c.pts++
89 |
90 | data := c.vpxBuf[:n]
91 | samp := media.Sample{Data: data, Samples: 1}
92 |
93 | if c.track == nil {
94 | return
95 | }
96 |
97 | if err := c.track.WriteSample(samp); err != nil {
98 | // fmt.Println("write sample: ", err)
99 | return
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/cmd/ascii_roulette/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "flag"
6 | "log"
7 |
8 | "github.com/dialup-inc/ascii"
9 | )
10 |
11 | func main() {
12 | var (
13 | signalerURL = flag.String("signaler-url", "wss://roulette.dialup.com/ws", "host and port of the signaler")
14 | )
15 | flag.Parse()
16 |
17 | ctx := context.Background()
18 |
19 | app, err := ascii.New(*signalerURL)
20 | if err != nil {
21 | log.Fatal(err)
22 | }
23 |
24 | if err := app.Run(ctx); err != nil {
25 | log.Fatal(err)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/conn.go:
--------------------------------------------------------------------------------
1 | package ascii
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "math/rand"
7 | "sync/atomic"
8 | "time"
9 |
10 | "github.com/pion/rtcp"
11 | "github.com/pion/rtp/codecs"
12 | "github.com/pion/webrtc/v2"
13 | "github.com/pion/webrtc/v2/pkg/media/samplebuilder"
14 | )
15 |
16 | // mode for frames width per timestamp from a 30 second capture
17 | const rtpAverageFrameWidth = 7
18 |
19 | type DCMessage struct {
20 | Event string
21 | Payload []byte
22 | }
23 |
24 | func NewConn(config webrtc.Configuration) (*Conn, error) {
25 | conn := &Conn{
26 | OnPLI: func() {},
27 | OnFrame: func([]byte) {},
28 | OnMessage: func(string) {},
29 | OnBye: func() {},
30 | OnDataOpen: func() {},
31 | OnICEConnectionStateChange: func(webrtc.ICEConnectionState) {},
32 | }
33 |
34 | m := webrtc.MediaEngine{}
35 | m.RegisterCodec(webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000))
36 | api := webrtc.NewAPI(webrtc.WithMediaEngine(m))
37 |
38 | pc, err := api.NewPeerConnection(config)
39 | if err != nil {
40 | return nil, err
41 | }
42 | conn.pc = pc
43 |
44 | pc.OnICEConnectionStateChange(conn.onICEConnectionStateChange)
45 | pc.OnTrack(conn.onTrack)
46 |
47 | if _, err = pc.AddTransceiver(webrtc.RTPCodecTypeVideo); err != nil {
48 | return nil, err
49 | }
50 |
51 | track, err := pc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "roulette")
52 | if err != nil {
53 | return nil, err
54 | }
55 | if _, err := pc.AddTrack(track); err != nil {
56 | return nil, err
57 | }
58 | conn.SendTrack = track
59 |
60 | dc, err := pc.CreateDataChannel("chat", nil)
61 | if err != nil {
62 | return nil, err
63 | }
64 | conn.dc = dc
65 |
66 | dc.OnMessage(conn.onMessage)
67 | dc.OnOpen(conn.onDataOpen)
68 |
69 | return conn, nil
70 | }
71 |
72 | type Conn struct {
73 | SendTrack *webrtc.Track
74 | OnPLI func()
75 | OnFrame func([]byte)
76 | OnMessage func(string)
77 | OnICEConnectionStateChange func(webrtc.ICEConnectionState)
78 | OnBye func()
79 | OnDataOpen func()
80 |
81 | pc *webrtc.PeerConnection
82 | recvTrack *webrtc.Track
83 | ssrc uint32
84 |
85 | dc *webrtc.DataChannel
86 |
87 | lastPLI time.Time
88 | }
89 |
90 | func (c *Conn) readRTCP(recv *webrtc.RTPReceiver) {
91 | for {
92 | rtcps, err := recv.ReadRTCP()
93 | if err == io.EOF {
94 | break
95 | }
96 | if err != nil {
97 | continue
98 | // fmt.Println(err)
99 | }
100 |
101 | for _, pkt := range rtcps {
102 | switch p := pkt.(type) {
103 | case *rtcp.PictureLossIndication:
104 | c.OnPLI()
105 | case *rtcp.Goodbye:
106 | for _, ssrc := range p.Sources {
107 | if ssrc == recv.Track().SSRC() {
108 | c.OnBye()
109 | break
110 | }
111 | }
112 | }
113 | }
114 | }
115 | }
116 |
117 | func (c *Conn) readRTP(track *webrtc.Track) {
118 | builder := samplebuilder.New(rtpAverageFrameWidth*5, &codecs.VP8Packet{})
119 |
120 | for {
121 | pkt, err := track.ReadRTP()
122 | if err == io.EOF {
123 | break
124 | }
125 | if err != nil {
126 | // fmt.Println(err)
127 | continue
128 | }
129 |
130 | builder.Push(pkt)
131 |
132 | for s := builder.Pop(); s != nil; s = builder.Pop() {
133 | c.OnFrame(s.Data)
134 | }
135 | }
136 | }
137 |
138 | func (c *Conn) onDataOpen() {
139 | c.OnDataOpen()
140 | }
141 |
142 | func (c *Conn) onMessage(msg webrtc.DataChannelMessage) {
143 | var dcm DCMessage
144 | if err := json.Unmarshal(msg.Data, &dcm); err != nil {
145 | // TODO
146 | }
147 | if dcm.Event == "chat" {
148 | c.OnMessage(string(dcm.Payload))
149 | }
150 | }
151 |
152 | func (c *Conn) onICEConnectionStateChange(s webrtc.ICEConnectionState) {
153 | c.OnICEConnectionStateChange(s)
154 | }
155 |
156 | func (c *Conn) SendMessage(m string) error {
157 | data, err := json.Marshal(DCMessage{
158 | Event: "chat",
159 | Payload: []byte(m),
160 | })
161 | if err != nil {
162 | return err
163 | }
164 | return c.dc.Send(data)
165 | }
166 |
167 | func (c *Conn) SendPLI() error {
168 | if time.Since(c.lastPLI) < 500*time.Millisecond {
169 | return nil
170 | }
171 | if c.recvTrack == nil {
172 | return nil
173 | }
174 |
175 | pli := &rtcp.PictureLossIndication{MediaSSRC: c.SendTrack.SSRC()}
176 | if err := c.pc.WriteRTCP([]rtcp.Packet{pli}); err != nil {
177 | return err
178 | }
179 |
180 | c.lastPLI = time.Now()
181 | return nil
182 | }
183 |
184 | func (c *Conn) SendBye() error {
185 | if c.SendTrack == nil {
186 | return nil
187 | }
188 |
189 | bye := &rtcp.Goodbye{Sources: []uint32{c.SendTrack.SSRC()}}
190 | if err := c.pc.WriteRTCP([]rtcp.Packet{bye}); err != nil {
191 | return err
192 | }
193 |
194 | return nil
195 | }
196 |
197 | func (c *Conn) IsConnected() bool {
198 | switch c.pc.ICEConnectionState() {
199 | case webrtc.ICEConnectionStateCompleted, webrtc.ICEConnectionStateConnected:
200 | return true
201 | default:
202 | return false
203 | }
204 | }
205 |
206 | func (c *Conn) onTrack(track *webrtc.Track, recv *webrtc.RTPReceiver) {
207 | if !atomic.CompareAndSwapUint32(&c.ssrc, 0, track.SSRC()) {
208 | return
209 | }
210 | c.recvTrack = track
211 |
212 | go c.readRTCP(recv)
213 | c.readRTP(track)
214 | }
215 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/dialup-inc/ascii
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/blackjack/webcam v0.0.0-20190407142958-6cd3de4f4861
7 | github.com/dialup-inc/ascii/signal v0.0.0-20190715163141-e9715ef66a09 // indirect
8 | github.com/golang/mock v1.3.1 // indirect
9 | github.com/golang/protobuf v1.3.2 // indirect
10 | github.com/gorilla/websocket v1.4.0
11 | github.com/kr/pretty v0.1.0 // indirect
12 | github.com/lucas-clemente/quic-go v0.11.2 // indirect
13 | github.com/marten-seemann/qtls v0.3.2 // indirect
14 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
15 | github.com/onsi/ginkgo v1.8.0 // indirect
16 | github.com/onsi/gomega v1.5.0 // indirect
17 | github.com/pion/dtls v1.4.0 // indirect
18 | github.com/pion/ice v0.5.1 // indirect
19 | github.com/pion/rtcp v1.2.1
20 | github.com/pion/rtp v1.1.3
21 | github.com/pion/stun v0.3.1
22 | github.com/pion/webrtc/v2 v2.0.24-0.20190715150138-632530bc69a7
23 | github.com/prometheus/client_golang v1.0.0 // indirect
24 | github.com/shuLhan/go-bindata v3.4.0+incompatible // indirect
25 | github.com/stretchr/objx v0.2.0 // indirect
26 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect
27 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7
28 | golang.org/x/text v0.3.2 // indirect
29 | golang.org/x/tools v0.0.0-20190716194459-b667c4c58e8b // indirect
30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
31 | gopkg.in/yaml.v2 v2.2.2 // indirect
32 | )
33 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
2 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
3 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
4 | github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
5 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
6 | github.com/blackjack/webcam v0.0.0-20190407142958-6cd3de4f4861/go.mod h1:G0X+rEqYPWSq0dG8OMf8M446MtKytzpPjgS3HbdOJZ4=
7 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
8 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11 | github.com/dialup-inc/ascii/signal v0.0.0-20190715163141-e9715ef66a09 h1:kmKnbk13MkcrEwJ4aEjPgItY8nx9k0R3oMRN8dCvSgY=
12 | github.com/dialup-inc/ascii/signal v0.0.0-20190715163141-e9715ef66a09/go.mod h1:PqJU9NJVIZYt4nxlU9hpa8unKlr/CkJb+TXcHuLSZrA=
13 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
14 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
15 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
16 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
17 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
18 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
19 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
20 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
21 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
22 | github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
23 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
24 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
25 | github.com/gortc/turn v0.7.1/go.mod h1:3FZ+LvCZKCKu6YYgwuYPqEi3FqCtdjfSFnFqVQNwfjk=
26 | github.com/gortc/turn v0.7.3 h1:CE72C79erbcsfa6L/QDhKztcl2kDq1UK20ImrJWDt/w=
27 | github.com/gortc/turn v0.7.3/go.mod h1:gvguwaGAFyv5/9KrcW9MkCgHALYD+e99mSM7pSCYYho=
28 | github.com/gortc/turn v0.8.0 h1:WWQi1jkoPmc2E7qgUMcZleveKikT9Ksi3QGIl8ZtY3Q=
29 | github.com/gortc/turn v0.8.0/go.mod h1:gvguwaGAFyv5/9KrcW9MkCgHALYD+e99mSM7pSCYYho=
30 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
31 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
32 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
33 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
34 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
35 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
36 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
37 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
38 | github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
39 | github.com/lucas-clemente/quic-go v0.11.2/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
40 | github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
41 | github.com/marten-seemann/qtls v0.3.2/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
42 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
44 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
45 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
46 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
47 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
48 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
49 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
50 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
51 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
52 | github.com/pion/datachannel v1.4.3 h1:tqS6YiqqAiFCxGGhvn1K7fHEzemK9Aov025dE/isGFo=
53 | github.com/pion/datachannel v1.4.3/go.mod h1:SpMJbuu8v+qbA94m6lWQwSdCf8JKQvgmdSHDNtcbe+w=
54 | github.com/pion/datachannel v1.4.4 h1:vVzvjCwEEgOF3KSS0jxNeF9z+DOrn8nIM6eD6qh9sIo=
55 | github.com/pion/datachannel v1.4.4/go.mod h1:SpMJbuu8v+qbA94m6lWQwSdCf8JKQvgmdSHDNtcbe+w=
56 | github.com/pion/dtls v1.3.5 h1:mBioifvh6JSE9pD4FtJh5WoizygoqkOJNJyS5Ns+y1U=
57 | github.com/pion/dtls v1.3.5/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk=
58 | github.com/pion/dtls v1.4.0 h1:s7TM+K8kwOg2jI+w5BLUk/+n51aGeI0Ri8Z2PFZU84o=
59 | github.com/pion/dtls v1.4.0/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk=
60 | github.com/pion/ice v0.4.0 h1:BdTXHTjzdsJHGi9yMFnj9ffgr+Kg2oHVv1qk4B0mQ8A=
61 | github.com/pion/ice v0.4.0/go.mod h1:/gw3aFmD/pBG8UM3TcEHs6HuaOEMSd/v1As3TodE7Ss=
62 | github.com/pion/ice v0.4.3/go.mod h1:/gw3aFmD/pBG8UM3TcEHs6HuaOEMSd/v1As3TodE7Ss=
63 | github.com/pion/ice v0.5.1 h1:4DaXfgr64yJ4hMl5t8BMcKzQVeP8Q5UnRsrCEaPhAPE=
64 | github.com/pion/ice v0.5.1/go.mod h1:dh9icbJ2IxF+OFx2cOPy9QAOQXLALPClLdAxPIzQqmU=
65 | github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk=
66 | github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
67 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
68 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
69 | github.com/pion/mdns v0.0.2 h1:T22Gg4dSuYVYsZ21oRFh9z7twzAm27+5PEKiABbjCvM=
70 | github.com/pion/mdns v0.0.2/go.mod h1:VrN3wefVgtfL8QgpEblPUC46ag1reLIfpqekCnKunLE=
71 | github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
72 | github.com/pion/rtcp v1.2.0 h1:rT2FptW5YHIern+4XlbGYnnsT26XGxurnkNLnzhtDXg=
73 | github.com/pion/rtcp v1.2.0/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
74 | github.com/pion/rtcp v1.2.1 h1:S3yG4KpYAiSmBVqKAfgRa5JdwBNj4zK3RLUa8JYdhak=
75 | github.com/pion/rtcp v1.2.1/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM=
76 | github.com/pion/rtp v1.1.2 h1:ERNugzYHW9F2ldpwoARbeFGKRoq1REe5Jxdjvm/rOx8=
77 | github.com/pion/rtp v1.1.2/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
78 | github.com/pion/rtp v1.1.3 h1:GTYSTsSLF5vH+UqShGYQEBdoYasWjTTC9UeYglnUO+o=
79 | github.com/pion/rtp v1.1.3/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
80 | github.com/pion/sctp v1.6.3 h1:SC4vKOjcddK8tXiTNj05a+0/GyPpCmuNfeBA/rzNFqs=
81 | github.com/pion/sctp v1.6.3/go.mod h1:cCqpLdYvgEUdl715+qbWtgT439CuQrAgy8BZTp0aEfA=
82 | github.com/pion/sctp v1.6.4 h1:edUNxTabSErLWOdeUUAxds8gwx5kGnFam4zL5DWpILk=
83 | github.com/pion/sctp v1.6.4/go.mod h1:cCqpLdYvgEUdl715+qbWtgT439CuQrAgy8BZTp0aEfA=
84 | github.com/pion/sdp/v2 v2.2.0 h1:JiixCEU8g6LbSsh1Bg5SOk0TPnJrn2HBOA1yJ+mRYhI=
85 | github.com/pion/sdp/v2 v2.2.0/go.mod h1:idSlWxhfWQDtTy9J05cgxpHBu/POwXN2VDRGYxT/EjU=
86 | github.com/pion/srtp v1.2.4 h1:wwGKC5ewuBukkZ+i+pZ8aO33+t6z2y/XRiYtyP0Xpv0=
87 | github.com/pion/srtp v1.2.4/go.mod h1:52qiP0g3FVMG/5NL6Ko8Vr2qirevKH+ukYbNS/4EX40=
88 | github.com/pion/srtp v1.2.6 h1:mHQuAMh0P67R7/j1F260u3O+fbRWLyjKLRPZYYvODFM=
89 | github.com/pion/srtp v1.2.6/go.mod h1:rd8imc5htjfs99XiEoOjLMEOcVjME63UHx9Ek9IGst0=
90 | github.com/pion/stun v0.3.0/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
91 | github.com/pion/stun v0.3.1 h1:d09JJzOmOS8ZzIp8NppCMgrxGZpJ4Ix8qirfNYyI3BA=
92 | github.com/pion/stun v0.3.1/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M=
93 | github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
94 | github.com/pion/transport v0.7.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
95 | github.com/pion/transport v0.8.0 h1:YHZnWBBrBuMqkuvMFUHeAETXS+LgfwW1IsVd2K2cyW8=
96 | github.com/pion/transport v0.8.0/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
97 | github.com/pion/transport v0.8.4/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
98 | github.com/pion/transport v0.8.5 h1:uo7g9BH6+Nm7DaJ1qX8+sv1o5rYREIM41kzrPVQYg14=
99 | github.com/pion/transport v0.8.5/go.mod h1:nAmRRnn+ArVtsoNuwktvAD+jrjSD7pA+H3iRmZwdUno=
100 | github.com/pion/turn v1.1.4/go.mod h1:2O2GFDGO6+hJ5gsyExDhoNHtVcacPB1NOyc81gkq0WA=
101 | github.com/pion/turn v1.3.1 h1:HyhVtxg/MYz5Fa/HTwpWNUZuV54JyLMO/q/i0IazDkw=
102 | github.com/pion/turn v1.3.1/go.mod h1:hWq4GRmlIgzbv2/6IneaETwooIIu/03uw5nJmsrAlyM=
103 | github.com/pion/turnc v0.0.6 h1:FHsmwYvdJ8mhT1/ZtWWer9L0unEb7AyRgrymfWy6mEY=
104 | github.com/pion/turnc v0.0.6/go.mod h1:4MSFv5i0v3MRkDLdo5eF9cD/xJtj1pxSphHNnxKL2W8=
105 | github.com/pion/webrtc/v2 v2.0.23 h1:v/tDKsP4zB6Sj+Wx861fLsaNmbwWbxacciHUhetH288=
106 | github.com/pion/webrtc/v2 v2.0.23/go.mod h1:AgremGibyNcHWIEkDbXt4ujKzKBO3tMuoYXybVRa8zo=
107 | github.com/pion/webrtc/v2 v2.0.24-0.20190715150138-632530bc69a7 h1:XERySyqxh3Jdamk1YR8pys9xp1xt7wdTQA427DFhlPw=
108 | github.com/pion/webrtc/v2 v2.0.24-0.20190715150138-632530bc69a7/go.mod h1:UfiEw3qQTlAcD9WkqE39o7bxGoVFHX+BnUrx7qFSMOU=
109 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
110 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
111 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
112 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
113 | github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
114 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
115 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
116 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
117 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
118 | github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
119 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
120 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
121 | github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
122 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
123 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
124 | github.com/rs/zerolog v1.14.3 h1:4EGfSkR2hJDB0s3oFfrlPqjU1e4WLncergLil3nEKW0=
125 | github.com/rs/zerolog v1.14.3/go.mod h1:3WXPzbXEEliJ+a6UFE4vhIxV8qR1EML6ngzP9ug4eYg=
126 | github.com/shuLhan/go-bindata v3.4.0+incompatible/go.mod h1:pkcPAATLBDD2+SpAPnX5vEM90F7fcwHCvvLCMXcmw3g=
127 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
128 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
129 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
130 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
131 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
132 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
133 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
134 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
135 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
136 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
137 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
138 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
139 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
140 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
141 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
142 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
143 | golang.org/x/net v0.0.0-20190403144856-b630fd6fe46b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
144 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
145 | golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
146 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
147 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
148 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
149 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
150 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
151 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
152 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
153 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
154 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
155 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
156 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
157 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
158 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
159 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
160 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
161 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
162 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
163 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
164 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
165 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
166 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
167 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
168 | golang.org/x/tools v0.0.0-20190716194459-b667c4c58e8b/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
169 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
170 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
171 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
172 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
173 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
174 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
175 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
176 |
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # ASCII Roulette v1.0.0
4 | #
5 | # presented by:
6 | #
7 | # ||` '||`
8 | # || '' ||
9 | # .|''|| || '''|. || '|| ||` '||''|, .|'', .|''|, '||),,(|,
10 | # || || || .|''|| || || || || || || || || || || ||
11 | # `|..||. .||. `|..||. .||. `|..'|. ||..|' .. `|..' `|..|' .|| ||.
12 | # ||
13 | # (we're hiring!) .||
14 | #
15 | #
16 | # ASCII Roulette is a command-line based video chat client that connects
17 | # you with a random person. It was written as a demonstration of the
18 | # Pion WebRTC library.
19 | #
20 | # [[ This project is completely open source! ]]
21 | # Github: https://github.com/dialup-inc/ascii
22 | # Website: https://dialup.com/ascii
23 | # Email: webmaster@dialup.com
24 |
25 | function fail_unsupported() {
26 | platform="${1:-"your platform"}"
27 | echo "ASCII Roulette isn't supported on $platform yet :-( Try Mac or Linux instead."
28 | echo ""
29 | echo "Contributions are welcome! https://github.com/dialup-inc/ascii"
30 | exit 1
31 | }
32 |
33 | function detect_platform() {
34 | case $(uname | tr '[:upper:]' '[:lower:]') in
35 | linux*)
36 | case $(uname -m) in
37 | x86_64)
38 | echo "linux64"
39 | ;;
40 | *)
41 | fail_unsupported "32-bit Linux"
42 | ;;
43 | esac
44 | ;;
45 | darwin*)
46 | case $(uname -m) in
47 | x86_64)
48 | echo "darwin64"
49 | ;;
50 | *)
51 | fail_unsupported "32-bit Macs"
52 | ;;
53 | esac
54 | ;;
55 | msys*)
56 | fail_unsupported "Windows"
57 | ;;
58 | *)
59 | fail_unsupported
60 | ;;
61 | esac
62 | }
63 |
64 | function getMD5() {
65 | file="$1"
66 | if ! [ -f "$file" ]; then
67 | return
68 | fi
69 |
70 | if [ -x "$(command -v md5sum)" ]; then
71 | md5sum | cut -d ' ' -f 1
72 | elif [ -x "$(command -v md5)" ]; then
73 | md5 -r $file | cut -d ' ' -f 1
74 | fi
75 | }
76 |
77 | function checkEtag() {
78 | url="$1"
79 | etag="$2"
80 |
81 | code="$(curl -s --head -o /dev/null -w "%{http_code}" -H "If-None-Match: $etag" "$url")"
82 |
83 | if [ "$code" != "304" ]; then
84 | echo "changed"
85 | fi
86 | }
87 |
88 | function main() {
89 | platform="$(detect_platform)"
90 | binaryURL="https://storage.googleapis.com/asciiroulette/ascii_roulette.$platform"
91 | md5="$(getMD5 /tmp/ascii_roulette)"
92 |
93 | if [ -n "$(checkEtag $binaryURL $md5)" ]; then
94 | printf "Downloading ASCII Roulette\033[5m...\033[0m\n"
95 | curl -fs "$binaryURL" > /tmp/ascii_roulette
96 | fi
97 |
98 | chmod +x /tmp/ascii_roulette
99 |
100 | clear
101 | /tmp/ascii_roulette
102 |
103 | if [ $? -eq 0 ]; then
104 | reset
105 | fi
106 | }
107 |
108 | echo < 1 || column > 1 {
20 | cmd = strconv.AppendInt(cmd, int64(row), 10)
21 | }
22 | if column > 1 {
23 | cmd = append(cmd, ';')
24 | cmd = strconv.AppendInt(cmd, int64(column), 10)
25 | }
26 | cmd = append(cmd, 'H')
27 |
28 | return a.Display.Write(cmd)
29 | }
30 |
31 | func (a *ANSI) Foreground(c color.Color) (int, error) {
32 | index := ANSIPalette.Index(c)
33 |
34 | var cmd []byte
35 | cmd = append(cmd, '\033', '[', '3', '8', ';', '5', ';')
36 | cmd = strconv.AppendInt(cmd, int64(index), 10)
37 | cmd = append(cmd, 'm')
38 |
39 | return a.Display.Write(cmd)
40 | }
41 |
42 | func (a *ANSI) ForegroundReset() (int, error) {
43 | return a.Display.Write([]byte{'\033', '[', '3', '9', 'm'})
44 | }
45 |
46 | func (a *ANSI) Background(c color.Color) (int, error) {
47 | index := ANSIPalette.Index(c)
48 |
49 | var cmd []byte
50 | cmd = append(cmd, '\033', '[', '4', '8', ';', '5', ';')
51 | cmd = strconv.AppendInt(cmd, int64(index), 10)
52 | cmd = append(cmd, 'm')
53 |
54 | return a.Display.Write(cmd)
55 | }
56 |
57 | func (a *ANSI) ResizeWindow(rows, cols int) (int, error) {
58 | var cmd []byte
59 | cmd = append(cmd, '\033', '[', '8', ';')
60 | cmd = strconv.AppendInt(cmd, int64(rows), 10)
61 | cmd = append(cmd, ';')
62 | cmd = strconv.AppendInt(cmd, int64(cols), 10)
63 | cmd = append(cmd, 't')
64 |
65 | return a.Display.Write(cmd)
66 | }
67 |
68 | func (a *ANSI) BackgroundReset() (int, error) {
69 | return a.Display.Write([]byte{'\033', '[', '4', '9', 'm'})
70 | }
71 |
72 | func (a *ANSI) Normal() (int, error) {
73 | return a.Display.Write([]byte{'\033', '[', '2', '2', 'm'})
74 | }
75 |
76 | func (a *ANSI) Bold() (int, error) {
77 | return a.Display.Write([]byte{'\033', '[', '1', 'm'})
78 | }
79 |
80 | func (a *ANSI) HideCursor() (int, error) {
81 | return a.Display.Write([]byte{'\033', '[', '?', '2', '5', 'l'})
82 | }
83 |
84 | func (a *ANSI) ShowCursor() (int, error) {
85 | return a.Display.Write([]byte{'\033', '[', '?', '2', '5', 'h'})
86 | }
87 |
88 | func (a *ANSI) Blink() (int, error) {
89 | return a.Display.Write([]byte{'\033', '[', '5', 'm'})
90 | }
91 |
92 | func (a *ANSI) BlinkOff() (int, error) {
93 | return a.Display.Write([]byte{'\033', '[', '2', '5', 'm'})
94 | }
95 |
96 | func (a *ANSI) Reset() (int, error) {
97 | return a.Display.Write([]byte{'\033', 'c'})
98 | }
99 |
100 | var ANSIPalette = color.Palette{
101 | color.RGBA{0x00, 0x00, 0x00, 0xFF},
102 | color.RGBA{0x80, 0x00, 0x00, 0xFF},
103 | color.RGBA{0x00, 0x80, 0x00, 0xFF},
104 | color.RGBA{0x80, 0x80, 0x00, 0xFF},
105 | color.RGBA{0x00, 0x00, 0x80, 0xFF},
106 | color.RGBA{0x80, 0x00, 0x80, 0xFF},
107 | color.RGBA{0x00, 0x80, 0x80, 0xFF},
108 | color.RGBA{0xc0, 0xc0, 0xc0, 0xFF},
109 | color.RGBA{0x80, 0x80, 0x80, 0xFF},
110 | color.RGBA{0xff, 0x00, 0x00, 0xFF},
111 | color.RGBA{0x00, 0xff, 0x00, 0xFF},
112 | color.RGBA{0xff, 0xff, 0x00, 0xFF},
113 | color.RGBA{0x00, 0x00, 0xff, 0xFF},
114 | color.RGBA{0xff, 0x00, 0xff, 0xFF},
115 | color.RGBA{0x00, 0xff, 0xff, 0xFF},
116 | color.RGBA{0xff, 0xff, 0xff, 0xFF},
117 | color.RGBA{0x00, 0x00, 0x00, 0xFF},
118 | color.RGBA{0x00, 0x00, 0x5f, 0xFF},
119 | color.RGBA{0x00, 0x00, 0x87, 0xFF},
120 | color.RGBA{0x00, 0x00, 0xaf, 0xFF},
121 | color.RGBA{0x00, 0x00, 0xd7, 0xFF},
122 | color.RGBA{0x00, 0x00, 0xff, 0xFF},
123 | color.RGBA{0x00, 0x5f, 0x00, 0xFF},
124 | color.RGBA{0x00, 0x5f, 0x5f, 0xFF},
125 | color.RGBA{0x00, 0x5f, 0x87, 0xFF},
126 | color.RGBA{0x00, 0x5f, 0xaf, 0xFF},
127 | color.RGBA{0x00, 0x5f, 0xd7, 0xFF},
128 | color.RGBA{0x00, 0x5f, 0xff, 0xFF},
129 | color.RGBA{0x00, 0x87, 0x00, 0xFF},
130 | color.RGBA{0x00, 0x87, 0x5f, 0xFF},
131 | color.RGBA{0x00, 0x87, 0x87, 0xFF},
132 | color.RGBA{0x00, 0x87, 0xaf, 0xFF},
133 | color.RGBA{0x00, 0x87, 0xd7, 0xFF},
134 | color.RGBA{0x00, 0x87, 0xff, 0xFF},
135 | color.RGBA{0x00, 0xaf, 0x00, 0xFF},
136 | color.RGBA{0x00, 0xaf, 0x5f, 0xFF},
137 | color.RGBA{0x00, 0xaf, 0x87, 0xFF},
138 | color.RGBA{0x00, 0xaf, 0xaf, 0xFF},
139 | color.RGBA{0x00, 0xaf, 0xd7, 0xFF},
140 | color.RGBA{0x00, 0xaf, 0xff, 0xFF},
141 | color.RGBA{0x00, 0xd7, 0x00, 0xFF},
142 | color.RGBA{0x00, 0xd7, 0x5f, 0xFF},
143 | color.RGBA{0x00, 0xd7, 0x87, 0xFF},
144 | color.RGBA{0x00, 0xd7, 0xaf, 0xFF},
145 | color.RGBA{0x00, 0xd7, 0xd7, 0xFF},
146 | color.RGBA{0x00, 0xd7, 0xff, 0xFF},
147 | color.RGBA{0x00, 0xff, 0x00, 0xFF},
148 | color.RGBA{0x00, 0xff, 0x5f, 0xFF},
149 | color.RGBA{0x00, 0xff, 0x87, 0xFF},
150 | color.RGBA{0x00, 0xff, 0xaf, 0xFF},
151 | color.RGBA{0x00, 0xff, 0xd7, 0xFF},
152 | color.RGBA{0x00, 0xff, 0xff, 0xFF},
153 | color.RGBA{0x5f, 0x00, 0x00, 0xFF},
154 | color.RGBA{0x5f, 0x00, 0x5f, 0xFF},
155 | color.RGBA{0x5f, 0x00, 0x87, 0xFF},
156 | color.RGBA{0x5f, 0x00, 0xaf, 0xFF},
157 | color.RGBA{0x5f, 0x00, 0xd7, 0xFF},
158 | color.RGBA{0x5f, 0x00, 0xff, 0xFF},
159 | color.RGBA{0x5f, 0x5f, 0x00, 0xFF},
160 | color.RGBA{0x5f, 0x5f, 0x5f, 0xFF},
161 | color.RGBA{0x5f, 0x5f, 0x87, 0xFF},
162 | color.RGBA{0x5f, 0x5f, 0xaf, 0xFF},
163 | color.RGBA{0x5f, 0x5f, 0xd7, 0xFF},
164 | color.RGBA{0x5f, 0x5f, 0xff, 0xFF},
165 | color.RGBA{0x5f, 0x87, 0x00, 0xFF},
166 | color.RGBA{0x5f, 0x87, 0x5f, 0xFF},
167 | color.RGBA{0x5f, 0x87, 0x87, 0xFF},
168 | color.RGBA{0x5f, 0x87, 0xaf, 0xFF},
169 | color.RGBA{0x5f, 0x87, 0xd7, 0xFF},
170 | color.RGBA{0x5f, 0x87, 0xff, 0xFF},
171 | color.RGBA{0x5f, 0xaf, 0x00, 0xFF},
172 | color.RGBA{0x5f, 0xaf, 0x5f, 0xFF},
173 | color.RGBA{0x5f, 0xaf, 0x87, 0xFF},
174 | color.RGBA{0x5f, 0xaf, 0xaf, 0xFF},
175 | color.RGBA{0x5f, 0xaf, 0xd7, 0xFF},
176 | color.RGBA{0x5f, 0xaf, 0xff, 0xFF},
177 | color.RGBA{0x5f, 0xd7, 0x00, 0xFF},
178 | color.RGBA{0x5f, 0xd7, 0x5f, 0xFF},
179 | color.RGBA{0x5f, 0xd7, 0x87, 0xFF},
180 | color.RGBA{0x5f, 0xd7, 0xaf, 0xFF},
181 | color.RGBA{0x5f, 0xd7, 0xd7, 0xFF},
182 | color.RGBA{0x5f, 0xd7, 0xff, 0xFF},
183 | color.RGBA{0x5f, 0xff, 0x00, 0xFF},
184 | color.RGBA{0x5f, 0xff, 0x5f, 0xFF},
185 | color.RGBA{0x5f, 0xff, 0x87, 0xFF},
186 | color.RGBA{0x5f, 0xff, 0xaf, 0xFF},
187 | color.RGBA{0x5f, 0xff, 0xd7, 0xFF},
188 | color.RGBA{0x5f, 0xff, 0xff, 0xFF},
189 | color.RGBA{0x87, 0x00, 0x00, 0xFF},
190 | color.RGBA{0x87, 0x00, 0x5f, 0xFF},
191 | color.RGBA{0x87, 0x00, 0x87, 0xFF},
192 | color.RGBA{0x87, 0x00, 0xaf, 0xFF},
193 | color.RGBA{0x87, 0x00, 0xd7, 0xFF},
194 | color.RGBA{0x87, 0x00, 0xff, 0xFF},
195 | color.RGBA{0x87, 0x5f, 0x00, 0xFF},
196 | color.RGBA{0x87, 0x5f, 0x5f, 0xFF},
197 | color.RGBA{0x87, 0x5f, 0x87, 0xFF},
198 | color.RGBA{0x87, 0x5f, 0xaf, 0xFF},
199 | color.RGBA{0x87, 0x5f, 0xd7, 0xFF},
200 | color.RGBA{0x87, 0x5f, 0xff, 0xFF},
201 | color.RGBA{0x87, 0x87, 0x00, 0xFF},
202 | color.RGBA{0x87, 0x87, 0x5f, 0xFF},
203 | color.RGBA{0x87, 0x87, 0x87, 0xFF},
204 | color.RGBA{0x87, 0x87, 0xaf, 0xFF},
205 | color.RGBA{0x87, 0x87, 0xd7, 0xFF},
206 | color.RGBA{0x87, 0x87, 0xff, 0xFF},
207 | color.RGBA{0x87, 0xaf, 0x00, 0xFF},
208 | color.RGBA{0x87, 0xaf, 0x5f, 0xFF},
209 | color.RGBA{0x87, 0xaf, 0x87, 0xFF},
210 | color.RGBA{0x87, 0xaf, 0xaf, 0xFF},
211 | color.RGBA{0x87, 0xaf, 0xd7, 0xFF},
212 | color.RGBA{0x87, 0xaf, 0xff, 0xFF},
213 | color.RGBA{0x87, 0xd7, 0x00, 0xFF},
214 | color.RGBA{0x87, 0xd7, 0x5f, 0xFF},
215 | color.RGBA{0x87, 0xd7, 0x87, 0xFF},
216 | color.RGBA{0x87, 0xd7, 0xaf, 0xFF},
217 | color.RGBA{0x87, 0xd7, 0xd7, 0xFF},
218 | color.RGBA{0x87, 0xd7, 0xff, 0xFF},
219 | color.RGBA{0x87, 0xff, 0x00, 0xFF},
220 | color.RGBA{0x87, 0xff, 0x5f, 0xFF},
221 | color.RGBA{0x87, 0xff, 0x87, 0xFF},
222 | color.RGBA{0x87, 0xff, 0xaf, 0xFF},
223 | color.RGBA{0x87, 0xff, 0xd7, 0xFF},
224 | color.RGBA{0x87, 0xff, 0xff, 0xFF},
225 | color.RGBA{0xaf, 0x00, 0x00, 0xFF},
226 | color.RGBA{0xaf, 0x00, 0x5f, 0xFF},
227 | color.RGBA{0xaf, 0x00, 0x87, 0xFF},
228 | color.RGBA{0xaf, 0x00, 0xaf, 0xFF},
229 | color.RGBA{0xaf, 0x00, 0xd7, 0xFF},
230 | color.RGBA{0xaf, 0x00, 0xff, 0xFF},
231 | color.RGBA{0xaf, 0x5f, 0x00, 0xFF},
232 | color.RGBA{0xaf, 0x5f, 0x5f, 0xFF},
233 | color.RGBA{0xaf, 0x5f, 0x87, 0xFF},
234 | color.RGBA{0xaf, 0x5f, 0xaf, 0xFF},
235 | color.RGBA{0xaf, 0x5f, 0xd7, 0xFF},
236 | color.RGBA{0xaf, 0x5f, 0xff, 0xFF},
237 | color.RGBA{0xaf, 0x87, 0x00, 0xFF},
238 | color.RGBA{0xaf, 0x87, 0x5f, 0xFF},
239 | color.RGBA{0xaf, 0x87, 0x87, 0xFF},
240 | color.RGBA{0xaf, 0x87, 0xaf, 0xFF},
241 | color.RGBA{0xaf, 0x87, 0xd7, 0xFF},
242 | color.RGBA{0xaf, 0x87, 0xff, 0xFF},
243 | color.RGBA{0xaf, 0xaf, 0x00, 0xFF},
244 | color.RGBA{0xaf, 0xaf, 0x5f, 0xFF},
245 | color.RGBA{0xaf, 0xaf, 0x87, 0xFF},
246 | color.RGBA{0xaf, 0xaf, 0xaf, 0xFF},
247 | color.RGBA{0xaf, 0xaf, 0xd7, 0xFF},
248 | color.RGBA{0xaf, 0xaf, 0xff, 0xFF},
249 | color.RGBA{0xaf, 0xd7, 0x00, 0xFF},
250 | color.RGBA{0xaf, 0xd7, 0x5f, 0xFF},
251 | color.RGBA{0xaf, 0xd7, 0x87, 0xFF},
252 | color.RGBA{0xaf, 0xd7, 0xaf, 0xFF},
253 | color.RGBA{0xaf, 0xd7, 0xd7, 0xFF},
254 | color.RGBA{0xaf, 0xd7, 0xff, 0xFF},
255 | color.RGBA{0xaf, 0xff, 0x00, 0xFF},
256 | color.RGBA{0xaf, 0xff, 0x5f, 0xFF},
257 | color.RGBA{0xaf, 0xff, 0x87, 0xFF},
258 | color.RGBA{0xaf, 0xff, 0xaf, 0xFF},
259 | color.RGBA{0xaf, 0xff, 0xd7, 0xFF},
260 | color.RGBA{0xaf, 0xff, 0xff, 0xFF},
261 | color.RGBA{0xd7, 0x00, 0x00, 0xFF},
262 | color.RGBA{0xd7, 0x00, 0x5f, 0xFF},
263 | color.RGBA{0xd7, 0x00, 0x87, 0xFF},
264 | color.RGBA{0xd7, 0x00, 0xaf, 0xFF},
265 | color.RGBA{0xd7, 0x00, 0xd7, 0xFF},
266 | color.RGBA{0xd7, 0x00, 0xff, 0xFF},
267 | color.RGBA{0xd7, 0x5f, 0x00, 0xFF},
268 | color.RGBA{0xd7, 0x5f, 0x5f, 0xFF},
269 | color.RGBA{0xd7, 0x5f, 0x87, 0xFF},
270 | color.RGBA{0xd7, 0x5f, 0xaf, 0xFF},
271 | color.RGBA{0xd7, 0x5f, 0xd7, 0xFF},
272 | color.RGBA{0xd7, 0x5f, 0xff, 0xFF},
273 | color.RGBA{0xd7, 0x87, 0x00, 0xFF},
274 | color.RGBA{0xd7, 0x87, 0x5f, 0xFF},
275 | color.RGBA{0xd7, 0x87, 0x87, 0xFF},
276 | color.RGBA{0xd7, 0x87, 0xaf, 0xFF},
277 | color.RGBA{0xd7, 0x87, 0xd7, 0xFF},
278 | color.RGBA{0xd7, 0x87, 0xff, 0xFF},
279 | color.RGBA{0xd7, 0xaf, 0x00, 0xFF},
280 | color.RGBA{0xd7, 0xaf, 0x5f, 0xFF},
281 | color.RGBA{0xd7, 0xaf, 0x87, 0xFF},
282 | color.RGBA{0xd7, 0xaf, 0xaf, 0xFF},
283 | color.RGBA{0xd7, 0xaf, 0xd7, 0xFF},
284 | color.RGBA{0xd7, 0xaf, 0xff, 0xFF},
285 | color.RGBA{0xd7, 0xd7, 0x00, 0xFF},
286 | color.RGBA{0xd7, 0xd7, 0x5f, 0xFF},
287 | color.RGBA{0xd7, 0xd7, 0x87, 0xFF},
288 | color.RGBA{0xd7, 0xd7, 0xaf, 0xFF},
289 | color.RGBA{0xd7, 0xd7, 0xd7, 0xFF},
290 | color.RGBA{0xd7, 0xd7, 0xff, 0xFF},
291 | color.RGBA{0xd7, 0xff, 0x00, 0xFF},
292 | color.RGBA{0xd7, 0xff, 0x5f, 0xFF},
293 | color.RGBA{0xd7, 0xff, 0x87, 0xFF},
294 | color.RGBA{0xd7, 0xff, 0xaf, 0xFF},
295 | color.RGBA{0xd7, 0xff, 0xd7, 0xFF},
296 | color.RGBA{0xd7, 0xff, 0xff, 0xFF},
297 | color.RGBA{0xff, 0x00, 0x00, 0xFF},
298 | color.RGBA{0xff, 0x00, 0x5f, 0xFF},
299 | color.RGBA{0xff, 0x00, 0x87, 0xFF},
300 | color.RGBA{0xff, 0x00, 0xaf, 0xFF},
301 | color.RGBA{0xff, 0x00, 0xd7, 0xFF},
302 | color.RGBA{0xff, 0x00, 0xff, 0xFF},
303 | color.RGBA{0xff, 0x5f, 0x00, 0xFF},
304 | color.RGBA{0xff, 0x5f, 0x5f, 0xFF},
305 | color.RGBA{0xff, 0x5f, 0x87, 0xFF},
306 | color.RGBA{0xff, 0x5f, 0xaf, 0xFF},
307 | color.RGBA{0xff, 0x5f, 0xd7, 0xFF},
308 | color.RGBA{0xff, 0x5f, 0xff, 0xFF},
309 | color.RGBA{0xff, 0x87, 0x00, 0xFF},
310 | color.RGBA{0xff, 0x87, 0x5f, 0xFF},
311 | color.RGBA{0xff, 0x87, 0x87, 0xFF},
312 | color.RGBA{0xff, 0x87, 0xaf, 0xFF},
313 | color.RGBA{0xff, 0x87, 0xd7, 0xFF},
314 | color.RGBA{0xff, 0x87, 0xff, 0xFF},
315 | color.RGBA{0xff, 0xaf, 0x00, 0xFF},
316 | color.RGBA{0xff, 0xaf, 0x5f, 0xFF},
317 | color.RGBA{0xff, 0xaf, 0x87, 0xFF},
318 | color.RGBA{0xff, 0xaf, 0xaf, 0xFF},
319 | color.RGBA{0xff, 0xaf, 0xd7, 0xFF},
320 | color.RGBA{0xff, 0xaf, 0xff, 0xFF},
321 | color.RGBA{0xff, 0xd7, 0x00, 0xFF},
322 | color.RGBA{0xff, 0xd7, 0x5f, 0xFF},
323 | color.RGBA{0xff, 0xd7, 0x87, 0xFF},
324 | color.RGBA{0xff, 0xd7, 0xaf, 0xFF},
325 | color.RGBA{0xff, 0xd7, 0xd7, 0xFF},
326 | color.RGBA{0xff, 0xd7, 0xff, 0xFF},
327 | color.RGBA{0xff, 0xff, 0x00, 0xFF},
328 | color.RGBA{0xff, 0xff, 0x5f, 0xFF},
329 | color.RGBA{0xff, 0xff, 0x87, 0xFF},
330 | color.RGBA{0xff, 0xff, 0xaf, 0xFF},
331 | color.RGBA{0xff, 0xff, 0xd7, 0xFF},
332 | color.RGBA{0xff, 0xff, 0xff, 0xFF},
333 | color.RGBA{0x08, 0x08, 0x08, 0xFF},
334 | color.RGBA{0x12, 0x12, 0x12, 0xFF},
335 | color.RGBA{0x1c, 0x1c, 0x1c, 0xFF},
336 | color.RGBA{0x26, 0x26, 0x26, 0xFF},
337 | color.RGBA{0x30, 0x30, 0x30, 0xFF},
338 | color.RGBA{0x3a, 0x3a, 0x3a, 0xFF},
339 | color.RGBA{0x44, 0x44, 0x44, 0xFF},
340 | color.RGBA{0x4e, 0x4e, 0x4e, 0xFF},
341 | color.RGBA{0x58, 0x58, 0x58, 0xFF},
342 | color.RGBA{0x62, 0x62, 0x62, 0xFF},
343 | color.RGBA{0x6c, 0x6c, 0x6c, 0xFF},
344 | color.RGBA{0x76, 0x76, 0x76, 0xFF},
345 | color.RGBA{0x80, 0x80, 0x80, 0xFF},
346 | color.RGBA{0x8a, 0x8a, 0x8a, 0xFF},
347 | color.RGBA{0x94, 0x94, 0x94, 0xFF},
348 | color.RGBA{0x9e, 0x9e, 0x9e, 0xFF},
349 | color.RGBA{0xa8, 0xa8, 0xa8, 0xFF},
350 | color.RGBA{0xb2, 0xb2, 0xb2, 0xFF},
351 | color.RGBA{0xbc, 0xbc, 0xbc, 0xFF},
352 | color.RGBA{0xc6, 0xc6, 0xc6, 0xFF},
353 | color.RGBA{0xd0, 0xd0, 0xd0, 0xFF},
354 | color.RGBA{0xda, 0xda, 0xda, 0xFF},
355 | color.RGBA{0xe4, 0xe4, 0xe4, 0xFF},
356 | color.RGBA{0xee, 0xee, 0xee, 0xFF},
357 | }
358 |
--------------------------------------------------------------------------------
/term/input.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import (
4 | "bufio"
5 | "io"
6 | "os"
7 |
8 | "golang.org/x/sys/unix"
9 | )
10 |
11 | func makeStdinRaw() error {
12 | fd := int(os.Stdin.Fd())
13 |
14 | termios, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
15 | if err != nil {
16 | return err
17 | }
18 |
19 | // This attempts to replicate the behaviour documented for cfmakeraw in
20 | // the termios(3) manpage.
21 | termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON
22 | termios.Oflag &^= unix.OPOST
23 | termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN
24 | termios.Cflag &^= unix.CSIZE | unix.PARENB
25 | termios.Cflag |= unix.CS8
26 | termios.Cc[unix.VMIN] = 1
27 | termios.Cc[unix.VTIME] = 0
28 | if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, termios); err != nil {
29 | return err
30 | }
31 |
32 | return nil
33 | }
34 |
35 | func CaptureStdin(onRune func(rune)) error {
36 | if err := makeStdinRaw(); err != nil {
37 | return err
38 | }
39 |
40 | go func() {
41 | reader := bufio.NewReader(os.Stdin)
42 | for {
43 | r, _, err := reader.ReadRune()
44 | if err == io.EOF {
45 | break
46 | }
47 | if err != nil {
48 | continue
49 | }
50 | onRune(r)
51 | }
52 | }()
53 |
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/term/input_bsd.go:
--------------------------------------------------------------------------------
1 | // +build darwin dragonfly freebsd netbsd openbsd
2 |
3 | package term
4 |
5 | import "golang.org/x/sys/unix"
6 |
7 | const ioctlReadTermios = unix.TIOCGETA
8 | const ioctlWriteTermios = unix.TIOCSETA
9 |
--------------------------------------------------------------------------------
/term/input_linux.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import "golang.org/x/sys/unix"
4 |
5 | const ioctlReadTermios = unix.TCGETS
6 | const ioctlWriteTermios = unix.TCSETS
7 |
--------------------------------------------------------------------------------
/term/window.go:
--------------------------------------------------------------------------------
1 | package term
2 |
3 | import (
4 | "os"
5 |
6 | "golang.org/x/sys/unix"
7 | )
8 |
9 | type WinSize struct {
10 | Rows int
11 | Cols int
12 | Width int
13 | Height int
14 | }
15 |
16 | func GetWinSize() (WinSize, error) {
17 | ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
18 | if err != nil {
19 | return WinSize{}, os.NewSyscallError("GetWinsize", err)
20 | }
21 | return WinSize{
22 | Rows: int(ws.Row),
23 | Cols: int(ws.Col),
24 | Width: int(ws.Xpixel),
25 | Height: int(ws.Ypixel),
26 | }, nil
27 | }
28 |
--------------------------------------------------------------------------------
/ui/ansi.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "bytes"
5 | "image"
6 | "image/color"
7 | "image/draw"
8 |
9 | "github.com/dialup-inc/ascii/term"
10 | "github.com/nfnt/resize"
11 | )
12 |
13 | var chars = []byte(" .,:;i1tfLCG08@")
14 |
15 | func Image2ANSI(img image.Image, cols, rows int, aspect float64, lightBackground bool) []byte {
16 | buf := bytes.NewBuffer(nil)
17 | a := term.ANSI{buf}
18 |
19 | // FIXME: Work around panic in resize when image is too small
20 | if rows < 2 || cols < 2 {
21 | return nil
22 | }
23 |
24 | colors := term.ANSIPalette
25 | canvasRect := image.Rect(0, 0, cols, rows)
26 | canvas := image.NewPaletted(canvasRect, colors)
27 |
28 | // If there's an image, resize to fit inside canvas dimensions...
29 | if img != nil {
30 | imgRect := img.Bounds()
31 | imgW, imgH := float64(imgRect.Dx())*aspect, float64(imgRect.Dy())
32 | fitW, fitH := float64(cols)/imgW, float64(rows)/imgH
33 |
34 | var scaleW, scaleH uint
35 | if fitW < fitH {
36 | scaleW = uint(imgW * fitW)
37 | scaleH = uint(imgH * fitW)
38 | } else {
39 | scaleW = uint(imgW * fitH)
40 | scaleH = uint(imgH * fitH)
41 | }
42 |
43 | scaled := resize.Resize(scaleW, scaleH, img, resize.Bilinear)
44 |
45 | offsetW, offsetH := (cols-int(scaleW))/2, (rows-int(scaleH))/2
46 | fitRect := image.Rect(
47 | offsetW,
48 | offsetH,
49 | offsetW+int(scaleW),
50 | offsetH+int(scaleH),
51 | )
52 | draw.Draw(canvas, fitRect, scaled, image.ZP, draw.Over)
53 | }
54 |
55 | // Draw a character and colored ANSI escape sequence for each pixel...
56 | currentColor := -1
57 | for _, p := range canvas.Pix {
58 | pxColor := colors[p]
59 |
60 | if int(p) != currentColor {
61 | a.Foreground(pxColor)
62 |
63 | currentColor = int(p)
64 | }
65 |
66 | k, _, _, _ := color.GrayModel.Convert(pxColor).RGBA()
67 | chr := int(k) * (len(chars) - 1) / 0xffff
68 |
69 | if lightBackground {
70 | chr = len(chars) - chr - 1
71 | }
72 |
73 | buf.WriteByte(chars[chr])
74 | }
75 |
76 | return buf.Bytes()
77 | }
78 |
--------------------------------------------------------------------------------
/ui/events.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "image"
5 |
6 | "github.com/dialup-inc/ascii/term"
7 | )
8 |
9 | // An Event represents a user action that cause changes in the UI state.
10 | //
11 | // They're processed by Renderer's Dispatch method.
12 | type Event interface{}
13 |
14 | // SentMessageEvent fires after a message has been sent to the current partner
15 | type SentMessageEvent string
16 |
17 | // FrameEvent is sent when the video decoder renders a new frame
18 | type FrameEvent image.Image
19 |
20 | // ReceivedChatEvent is fired when the user submits text in the chat input box.
21 | type ReceivedChatEvent string
22 |
23 | // KeypressEvent is fired when the user presses the keyboard.
24 | type KeypressEvent rune
25 |
26 | // BackspaceEvent is fired when the backspace button is pressed.
27 | type BackspaceEvent struct{}
28 |
29 | // ResizeEvent indicates that the terminal window's size has changed to the specified dimensions
30 | type ResizeEvent term.WinSize
31 |
32 | // ToggleHelpEvent toggles the help modal
33 | type ToggleHelpEvent struct{}
34 |
35 | // SkipEvent skips to the next match
36 | type SkipEvent struct{}
37 |
38 | // DataOpenedEvent fires when the text chat data channel opens and the user can begin typing
39 | type DataOpenedEvent struct{}
40 |
41 | // ConnStartedEvent fires when an ICE connection has been established and the call can begin
42 | type ConnStartedEvent struct{}
43 |
44 | // ConnEndedEvent fires when the connection with your partner has been lost
45 | type ConnEndedEvent struct {
46 | // The reason for the disconnection
47 | Reason EndConnReason
48 | }
49 |
50 | // SetPageEvent transitions to the specified page
51 | type SetPageEvent Page
52 |
53 | // LogLevel indicates the severity of a LogEvent message
54 | type LogLevel int
55 |
56 | const (
57 | // LogLevelInfo is for non-urgent, informational logs
58 | LogLevelInfo LogLevel = iota
59 | // LogLevelError is for logs that indicate problems
60 | LogLevelError
61 | )
62 |
63 | // A LogEvent prints a message to the console
64 | type LogEvent struct {
65 | Text string
66 | Level LogLevel
67 | }
68 |
--------------------------------------------------------------------------------
/ui/reducer.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "image"
5 | "regexp"
6 | "unicode"
7 | "unicode/utf8"
8 |
9 | "github.com/dialup-inc/ascii/term"
10 | )
11 |
12 | func StateReducer(s State, event Event) State {
13 | s.Image = imageReducer(s.Image, event)
14 | s.ChatActive = chatActiveReducer(s.ChatActive, event)
15 | s.Input = inputReducer(s.Input, s.ChatActive, event)
16 | s.Messages = messagesReducer(s.Messages, event)
17 | s.Page = pageReducer(s.Page, event)
18 | s.WinSize = winSizeReducer(s.WinSize, event)
19 | s.HelpOn = helpOnReducer(s.HelpOn, event)
20 |
21 | return s
22 | }
23 |
24 | func chatActiveReducer(s bool, event Event) bool {
25 | switch event.(type) {
26 | case DataOpenedEvent:
27 | return true
28 | case ConnEndedEvent:
29 | return false
30 | default:
31 | return s
32 | }
33 | }
34 |
35 | func helpOnReducer(s bool, event Event) bool {
36 | switch event.(type) {
37 | case ToggleHelpEvent:
38 | return !s
39 | case SkipEvent:
40 | return false
41 | case SentMessageEvent:
42 | return false
43 | default:
44 | return s
45 | }
46 | }
47 |
48 | func pageReducer(s Page, event Event) Page {
49 | switch e := event.(type) {
50 | case SetPageEvent:
51 | return Page(e)
52 | default:
53 | return s
54 | }
55 | }
56 |
57 | func winSizeReducer(s term.WinSize, event Event) term.WinSize {
58 | switch e := event.(type) {
59 | case ResizeEvent:
60 | return term.WinSize(e)
61 | default:
62 | return s
63 | }
64 | }
65 |
66 | var ansiRegex = regexp.MustCompile("[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))")
67 |
68 | func inputReducer(s string, chatActive bool, event Event) string {
69 | switch e := event.(type) {
70 | case ConnStartedEvent:
71 | return ""
72 |
73 | case KeypressEvent:
74 | if !chatActive {
75 | return s
76 | }
77 |
78 | s += string(e)
79 |
80 | // Strip ANSI escape codes
81 | s = ansiRegex.ReplaceAllString(s, "")
82 |
83 | // Strip unprintable characters
84 | var printable []rune
85 | for _, r := range s {
86 | if !unicode.IsPrint(r) {
87 | continue
88 | }
89 | printable = append(printable, r)
90 | }
91 | s = string(printable)
92 |
93 | return s
94 |
95 | case BackspaceEvent:
96 | if !chatActive {
97 | return s
98 | }
99 |
100 | if len(s) == 0 {
101 | return s
102 | }
103 | _, sz := utf8.DecodeLastRuneInString(s)
104 | return s[:len(s)-sz]
105 |
106 | case SentMessageEvent:
107 | return ""
108 |
109 | default:
110 | return s
111 | }
112 | }
113 |
114 | func imageReducer(s image.Image, event Event) image.Image {
115 | switch e := event.(type) {
116 | case FrameEvent:
117 | return image.Image(e)
118 |
119 | case SetPageEvent:
120 | return nil
121 |
122 | case SkipEvent:
123 | return nil
124 |
125 | default:
126 | return s
127 | }
128 | }
129 |
130 | func messagesReducer(s []Message, event Event) []Message {
131 | switch e := event.(type) {
132 | case SentMessageEvent:
133 | return append(s, Message{
134 | Type: MessageTypeOutgoing,
135 | User: "You",
136 | Text: string(e),
137 | })
138 |
139 | case ReceivedChatEvent:
140 | return append(s, Message{
141 | Type: MessageTypeIncoming,
142 | User: "Them",
143 | Text: string(e),
144 | })
145 |
146 | case ConnEndedEvent:
147 | var msg Message
148 |
149 | switch e.Reason {
150 |
151 | // Error handler will catch this
152 | case EndConnSetupError:
153 | return s
154 |
155 | // We ignore match errors
156 | case EndConnMatchError:
157 | return s
158 |
159 | case EndConnNormal:
160 | msg = Message{
161 | Type: MessageTypeInfo,
162 | Text: "Skipping...",
163 | }
164 |
165 | case EndConnTimedOut:
166 | msg = Message{
167 | Type: MessageTypeError,
168 | Text: "Connection timed out.",
169 | }
170 |
171 | case EndConnDisconnected:
172 | msg = Message{
173 | Type: MessageTypeError,
174 | Text: "Lost connection.",
175 | }
176 |
177 | case EndConnGone:
178 | msg = Message{
179 | Type: MessageTypeInfo,
180 | Text: "Your partner left the chat.",
181 | }
182 | }
183 |
184 | return append(s, msg)
185 |
186 | case ConnStartedEvent:
187 | return append(s, Message{
188 | Type: MessageTypeInfo,
189 | Text: "Connected",
190 | })
191 |
192 | case LogEvent:
193 | mtype := MessageTypeInfo
194 | if e.Level == LogLevelError {
195 | mtype = MessageTypeError
196 | }
197 |
198 | return append(s, Message{
199 | Type: mtype,
200 | Text: e.Text,
201 | })
202 |
203 | default:
204 | return s
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/ui/renderer.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "bytes"
5 | "image/color"
6 | "io"
7 | "math"
8 | "os"
9 | "reflect"
10 | "strings"
11 | "sync"
12 | "time"
13 | "unicode/utf8"
14 |
15 | "github.com/dialup-inc/ascii/term"
16 | )
17 |
18 | const (
19 | chatHeight = 5
20 | )
21 |
22 | func NewRenderer() *Renderer {
23 | return &Renderer{
24 | requestFrame: make(chan struct{}),
25 | }
26 | }
27 |
28 | type Renderer struct {
29 | requestFrame chan struct{}
30 |
31 | stateMu sync.Mutex
32 | state State
33 |
34 | start time.Time
35 | }
36 |
37 | func (r *Renderer) GetState() State {
38 | r.stateMu.Lock()
39 | defer r.stateMu.Unlock()
40 |
41 | return r.state
42 | }
43 |
44 | func (r *Renderer) Dispatch(e Event) {
45 | r.stateMu.Lock()
46 | newState := StateReducer(r.state, e)
47 | var changed bool
48 | if !reflect.DeepEqual(r.state, newState) {
49 | changed = true
50 | }
51 | r.state = newState
52 | r.stateMu.Unlock()
53 |
54 | if changed {
55 | r.RequestFrame()
56 | }
57 | }
58 |
59 | func (r *Renderer) RequestFrame() {
60 | select {
61 | case r.requestFrame <- struct{}{}:
62 | default:
63 | }
64 | }
65 |
66 | // pixels are rectangular, not square in the terminal. add a scale factor to account for this
67 | func getAspect(w term.WinSize) float64 {
68 | if w.Width == 0 || w.Height == 0 || w.Rows == 0 || w.Cols == 0 {
69 | return 2.0
70 | }
71 | return float64(w.Height) * float64(w.Cols) / float64(w.Rows) / float64(w.Width)
72 | }
73 |
74 | func (r *Renderer) drawVideo(buf *bytes.Buffer, s State, headHeight int) {
75 | a := term.ANSI{buf}
76 |
77 | vidW, vidH := s.WinSize.Cols, s.WinSize.Rows-chatHeight-headHeight
78 |
79 | a.CursorPosition(1+headHeight, 1)
80 | a.Background(color.Black)
81 | a.Bold()
82 |
83 | aspect := getAspect(s.WinSize)
84 | imgANSI := Image2ANSI(s.Image, vidW, vidH, aspect, false)
85 | buf.Write(imgANSI)
86 | }
87 |
88 | func (r *Renderer) drawHead(buf *bytes.Buffer, s State) {
89 | a := term.ANSI{buf}
90 |
91 | line1 := " dialup.com/ascii"
92 | line2 := " (we're hiring!)"
93 |
94 | a.CursorPosition(1, 1)
95 | a.Normal()
96 |
97 | a.Background(color.RGBA{0x12, 0x12, 0x12, 0xFF})
98 |
99 | a.Foreground(color.RGBA{0x00, 0xFF, 0xFF, 0xFF})
100 | buf.WriteString(line1)
101 |
102 | a.Foreground(color.RGBA{0x00, 0x44, 0x44, 0xFF})
103 | buf.WriteString(line2)
104 |
105 | remaining := s.WinSize.Cols - len(line1) - len(line2)
106 | if remaining > 0 {
107 | buf.WriteString(strings.Repeat(" ", remaining))
108 | }
109 | }
110 |
111 | func (r *Renderer) drawPrompt(buf *bytes.Buffer, s State) {
112 | a := term.ANSI{buf}
113 |
114 | prompt := " > "
115 | input := s.Input
116 |
117 | width := s.WinSize.Cols
118 | row := s.WinSize.Rows
119 |
120 | a.Background(color.RGBA{0x12, 0x12, 0x12, 0xFF})
121 | a.Bold()
122 |
123 | // Clear what's there
124 | a.CursorPosition(row, 1)
125 | buf.WriteString(strings.Repeat(" ", width))
126 |
127 | // Then write the line
128 | a.CursorPosition(row, 1)
129 |
130 | var lineLen int
131 |
132 | if !s.ChatActive {
133 | a.Foreground(color.RGBA{0x33, 0x33, 0x33, 0XFF})
134 | buf.WriteString(prompt)
135 | return
136 | }
137 |
138 | // Add prompt
139 | a.Foreground(color.White)
140 | buf.WriteString(prompt)
141 | lineLen += len(prompt)
142 |
143 | // Add input
144 | input = truncate(input, width-lineLen, "…")
145 | buf.WriteString(input)
146 | lineLen += len(input)
147 |
148 | // Add blinking cursor where you're supposed to type
149 | cursor := "_"
150 | if lineLen+len(cursor) < width {
151 | a.Foreground(color.White)
152 | a.Blink()
153 | buf.WriteString(cursor)
154 | a.BlinkOff()
155 | lineLen += len(cursor)
156 | }
157 |
158 | // add label
159 | label := " Send a message."
160 | label = truncate(label, width-lineLen, "")
161 | if input == "" {
162 | a.Foreground(color.RGBA{0x33, 0x33, 0x33, 0xFF})
163 | buf.WriteString(label)
164 | lineLen += len(label)
165 | }
166 | }
167 |
168 | func truncate(s string, n int, ellipsis string) string {
169 | if len(s) <= n {
170 | return s
171 | }
172 |
173 | maxLen := n - len(ellipsis)
174 | if len(s) > maxLen {
175 | s = s[:maxLen]
176 | }
177 |
178 | return s + ellipsis
179 | }
180 |
181 | func (r *Renderer) drawChat(buf *bytes.Buffer, s State) {
182 | a := term.ANSI{buf}
183 |
184 | width := s.WinSize.Cols
185 | chatTop := s.WinSize.Rows - chatHeight + 1
186 | logTop := chatTop + 1
187 |
188 | a.CursorPosition(chatTop, 1)
189 | // Draw background
190 | a.Normal()
191 |
192 | a.Background(color.RGBA{0x12, 0x12, 0x12, 0xFF})
193 | label := "ASCII Roulette"
194 | link := "hit ctrl-t for help"
195 | buf.WriteString(" ")
196 | a.Foreground(color.RGBA{0x00, 0xff, 0xff, 0xff})
197 | buf.WriteString(label)
198 | textLen := len(label) + len(link) + 2
199 | if width > textLen {
200 | buf.WriteString(strings.Repeat(" ", width-textLen))
201 | }
202 | a.Foreground(color.RGBA{0x00, 0x99, 0x99, 0xff})
203 | buf.WriteString(link)
204 | buf.WriteString(" ")
205 |
206 | a.Background(color.RGBA{0x22, 0x22, 0x22, 0xFF})
207 |
208 | drawChatLine := func(m Message) {
209 | switch m.Type {
210 | case MessageTypeIncoming:
211 | a.Foreground(color.RGBA{0xFF, 0, 0, 0xFF})
212 | buf.WriteString(" ")
213 | buf.WriteString(m.User)
214 | buf.WriteString(": ")
215 | a.Foreground(color.RGBA{0x99, 0x99, 0x99, 0xFF})
216 | buf.WriteString(m.Text)
217 |
218 | textLen := utf8.RuneCountInString(m.User) + utf8.RuneCountInString(m.Text) + 3
219 | if width > textLen {
220 | buf.WriteString(strings.Repeat(" ", width-textLen))
221 | }
222 |
223 | case MessageTypeOutgoing:
224 | a.Foreground(color.RGBA{0xFF, 0xFF, 0, 0xFF})
225 | buf.WriteString(" ")
226 | buf.WriteString(m.User)
227 | buf.WriteString(": ")
228 | a.Foreground(color.RGBA{0x99, 0x99, 0x99, 0xFF})
229 | buf.WriteString(m.Text)
230 |
231 | textLen := utf8.RuneCountInString(m.User) + utf8.RuneCountInString(m.Text) + 3
232 | if width > textLen {
233 | buf.WriteString(strings.Repeat(" ", width-textLen))
234 | }
235 |
236 | case MessageTypeInfo:
237 | a.Foreground(color.RGBA{0x99, 0x99, 0x99, 0xFF})
238 | buf.WriteString(" ")
239 | buf.WriteString(m.Text)
240 |
241 | textLen := utf8.RuneCountInString(m.Text) + 1
242 | if width > textLen {
243 | buf.WriteString(strings.Repeat(" ", width-textLen))
244 | }
245 |
246 | case MessageTypeError:
247 | a.Foreground(color.RGBA{0xAA, 0x00, 0x00, 0xFF})
248 | buf.WriteString(" ")
249 | buf.WriteString(m.Text)
250 |
251 | textLen := utf8.RuneCountInString(m.Text) + 1
252 | if width > textLen {
253 | buf.WriteString(strings.Repeat(" ", width-textLen))
254 | }
255 |
256 | }
257 | }
258 |
259 | msgs := s.Messages
260 | if len(s.Messages) > 3 {
261 | msgs = msgs[len(s.Messages)-3:]
262 | }
263 | for i, m := range msgs {
264 | a.CursorPosition(logTop+i, 0)
265 | drawChatLine(m)
266 | }
267 | // blank if there arent enough messages
268 | for i := len(msgs); i < 3; i++ {
269 | a.CursorPosition(logTop+i, 0)
270 | buf.WriteString(strings.Repeat(" ", width))
271 | }
272 |
273 | r.drawPrompt(buf, s)
274 | }
275 |
276 | func (r *Renderer) drawTitle(buf *bytes.Buffer, s State) {
277 | a := term.ANSI{buf}
278 |
279 | // Draw background
280 | a.Bold()
281 |
282 | var text string
283 | switch s.Page {
284 | case GlobePage:
285 | text = "Presented by dialup.com"
286 | case PionPage:
287 | text = "Powered by Pion"
288 | }
289 |
290 | a.Background(color.RGBA{0x00, 0x00, 0x00, 0xFF})
291 | buf.WriteString(strings.Repeat(" ", s.WinSize.Cols*chatHeight))
292 |
293 | a.Foreground(color.RGBA{0x00, 0xff, 0xff, 0xff})
294 | a.CursorPosition(s.WinSize.Rows-2, (s.WinSize.Cols-len(text))/2+1)
295 | buf.WriteString(text)
296 | }
297 |
298 | func (r *Renderer) drawBlank(buf *bytes.Buffer, s State) {
299 | a := term.ANSI{buf}
300 |
301 | a.Background(color.RGBA{0x00, 0x00, 0x00, 0xFF})
302 |
303 | a.CursorPosition(1, 1)
304 | buf.WriteString(strings.Repeat(" ", s.WinSize.Cols*s.WinSize.Rows))
305 | }
306 |
307 | func (r *Renderer) drawConfirm(buf *bytes.Buffer, s State) {
308 | a := term.ANSI{buf}
309 |
310 | // Blank background
311 | a.Background(color.RGBA{0x00, 0x00, 0x00, 0xFF})
312 | a.CursorPosition(1, 1)
313 | buf.WriteString(strings.Repeat(" ", s.WinSize.Cols*s.WinSize.Rows))
314 |
315 | // Draw title
316 | if s.WinSize.Rows > 6 {
317 | line := "ASCII Roulette"
318 | if s.WinSize.Cols > 25 {
319 | line = "Welcome to " + line
320 | }
321 |
322 | timeOffset := float64(time.Since(r.start)/time.Millisecond) / 2000.0
323 |
324 | a.Bold()
325 | a.CursorPosition(2, (s.WinSize.Cols-len(line))/2+1)
326 | for i, r := range line {
327 | t := float64(i)/float64(len(line)) + timeOffset
328 | a.Foreground(rainbow(t))
329 | buf.WriteRune(r)
330 | }
331 | }
332 |
333 | // Draw description
334 | descWidth := 40
335 | maxWidth := s.WinSize.Cols - 2
336 | if maxWidth < descWidth {
337 | descWidth = maxWidth
338 | }
339 |
340 | desc := "This program connects you in a video chat with a random person!\n🎥 Your webcam will activate\n🔉 There is no audio\nClicking, you agree to the TOS: dialup.com/terms"
341 | var descSections [][]string
342 | for _, line := range strings.Split(desc, "\n") {
343 | descSections = append(descSections, wordWrap(line, descWidth))
344 | }
345 |
346 | // Hide parts of the description if they're too long
347 | var totalLength int
348 | for i, lines := range descSections {
349 | if totalLength+len(lines) > s.WinSize.Rows-8 {
350 | descSections = descSections[:i]
351 | break
352 | }
353 |
354 | totalLength += len(lines)
355 | if i > 0 {
356 | totalLength++ // for newline
357 | }
358 | }
359 |
360 | a.Normal()
361 | a.Foreground(color.RGBA{0xAA, 0xAA, 0xAA, 0xFF})
362 |
363 | descOffset := 4
364 | for _, lines := range descSections {
365 | // Don't display if it'll clip the button
366 |
367 | for i, line := range lines {
368 | a.CursorPosition((s.WinSize.Rows-totalLength-8)/2+i+descOffset, (s.WinSize.Cols-len(line))/2+1)
369 | buf.WriteString(line)
370 | }
371 |
372 | descOffset += len(lines) + 1
373 | }
374 |
375 | // Draw button
376 | a.Bold()
377 | a.Background(color.RGBA{0x11, 0x11, 0x11, 0xFF})
378 | a.Foreground(color.White)
379 |
380 | line := " Press Enter to Start Camera "
381 | if s.WinSize.Cols <= 25 {
382 | line = " Press Enter "
383 | }
384 |
385 | a.CursorPosition(s.WinSize.Rows-3, (s.WinSize.Cols-len(line))/2+1)
386 | if len(line) > 0 {
387 | buf.WriteString(strings.Repeat(" ", len(line)))
388 | }
389 |
390 | a.CursorPosition(s.WinSize.Rows-2, (s.WinSize.Cols-len(line))/2+1)
391 | buf.WriteString(line)
392 |
393 | a.CursorPosition(s.WinSize.Rows-1, (s.WinSize.Cols-len(line))/2+1)
394 | if len(line) > 0 {
395 | buf.WriteString(strings.Repeat(" ", len(line)))
396 | }
397 | }
398 |
399 | func (r *Renderer) drawHelp(buf *bytes.Buffer, s State) {
400 | a := term.ANSI{buf}
401 |
402 | rows := []string{
403 | " ",
404 | " Skip ctrl-d ",
405 | " Help ctrl-t ",
406 | " Quit ctrl-c ",
407 | " ",
408 | }
409 |
410 | var boxWidth int
411 | for _, r := range rows {
412 | if len(r) > boxWidth {
413 | boxWidth = len(r)
414 | }
415 | }
416 |
417 | boxTop := (s.WinSize.Rows-len(rows)-1)/2 + 1
418 | boxLeft := (s.WinSize.Cols-boxWidth)/2 + 1
419 |
420 | a.CursorPosition(boxTop, boxLeft)
421 | a.Bold()
422 | a.Background(color.Black)
423 | a.Foreground(color.White)
424 | buf.WriteString(" Shortcuts ")
425 |
426 | a.Normal()
427 | a.Foreground(color.Black)
428 | a.Background(color.White)
429 | for i, line := range rows {
430 | a.CursorPosition(boxTop+i+1, boxLeft)
431 | buf.WriteString(line)
432 | }
433 | }
434 |
435 | func wordWrap(s string, lineLen int) []string {
436 | var lines []string
437 |
438 | var line string
439 | for _, word := range strings.Split(s, " ") {
440 | if len(line)+len(word)+1 > lineLen {
441 | lines = append(lines, line)
442 | line = ""
443 | }
444 | line += " " + word
445 | }
446 | if len(line) > 0 {
447 | lines = append(lines, line)
448 | }
449 |
450 | return lines
451 | }
452 |
453 | func rainbow(t float64) *color.RGBA {
454 | const freq = math.Pi
455 | r := math.Sin(freq*t)*127 + 128
456 | g := math.Sin(freq*t+2*math.Pi/3)*127 + 128
457 | b := math.Sin(freq*t+4*math.Pi/3)*127 + 128
458 |
459 | return &color.RGBA{uint8(r), uint8(g), uint8(b), 0xFF}
460 | }
461 |
462 | func (r *Renderer) draw() {
463 | buf := bytes.NewBuffer(nil)
464 |
465 | r.stateMu.Lock()
466 | s := r.state
467 | r.stateMu.Unlock()
468 |
469 | switch s.Page {
470 | case GlobePage:
471 | r.drawTitle(buf, s)
472 | r.drawVideo(buf, s, 0)
473 |
474 | case PionPage:
475 | r.drawTitle(buf, s)
476 | r.drawVideo(buf, s, 0)
477 |
478 | case ChatPage:
479 | r.drawHead(buf, s)
480 | r.drawChat(buf, s)
481 | r.drawVideo(buf, s, 1)
482 | if s.HelpOn {
483 | r.drawHelp(buf, s)
484 | }
485 |
486 | case ConfirmPage:
487 | r.drawConfirm(buf, s)
488 |
489 | default:
490 | r.drawBlank(buf, s)
491 | }
492 |
493 | io.Copy(os.Stdout, buf)
494 | }
495 |
496 | func (r *Renderer) loop() {
497 | r.start = time.Now()
498 |
499 | ticker := time.NewTicker(200 * time.Millisecond)
500 | for {
501 | r.draw()
502 |
503 | select {
504 | case <-r.requestFrame:
505 | case <-ticker.C:
506 | }
507 | }
508 | }
509 |
510 | func (r *Renderer) Start() {
511 | a := term.ANSI{os.Stdout}
512 | a.HideCursor()
513 |
514 | go r.loop()
515 | }
516 |
517 | func (r *Renderer) Stop() {
518 | r.stateMu.Lock()
519 | s := r.state
520 | r.stateMu.Unlock()
521 |
522 | buf := bytes.NewBuffer(nil)
523 | a := term.ANSI{buf}
524 |
525 | a.ShowCursor()
526 | a.Reset()
527 | a.BackgroundReset()
528 | a.ForegroundReset()
529 | a.Normal()
530 | a.CursorPosition(1, 1)
531 | buf.WriteString(strings.Repeat(" ", s.WinSize.Cols*s.WinSize.Rows))
532 | a.CursorPosition(1, 1)
533 |
534 | io.Copy(os.Stdout, buf)
535 | }
536 |
--------------------------------------------------------------------------------
/ui/state.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "image"
5 |
6 | "github.com/dialup-inc/ascii/term"
7 | )
8 |
9 | type Page string
10 |
11 | var (
12 | GlobePage Page = "globe"
13 | PionPage Page = "pion"
14 | ConfirmPage Page = "confirm"
15 | ChatPage Page = "chat"
16 | )
17 |
18 | type State struct {
19 | Page Page
20 |
21 | HelpOn bool
22 |
23 | Input string
24 | ChatActive bool
25 |
26 | Messages []Message
27 | Image image.Image
28 | WinSize term.WinSize
29 | }
30 |
31 | type MessageType int
32 |
33 | const (
34 | MessageTypeIncoming MessageType = iota
35 | MessageTypeOutgoing
36 | MessageTypeInfo
37 | MessageTypeError
38 | )
39 |
40 | type Message struct {
41 | Type MessageType
42 | User string
43 | Text string
44 | }
45 |
46 | type EndConnReason int
47 |
48 | const (
49 | // User closed connection
50 | EndConnNormal EndConnReason = iota
51 | // Error during connection setup
52 | EndConnSetupError
53 | // Error during matching
54 | EndConnMatchError
55 | // Connection timed out
56 | EndConnTimedOut
57 | // Lost connection with partner
58 | EndConnDisconnected
59 | // Partner left
60 | EndConnGone
61 | )
62 |
--------------------------------------------------------------------------------
/videos/ivfreader.go:
--------------------------------------------------------------------------------
1 | package videos
2 |
3 | import (
4 | "encoding/binary"
5 | "errors"
6 | "io"
7 | )
8 |
9 | type IVFReader struct {
10 | reader io.ReadSeeker
11 | Header IVFHeader
12 | }
13 |
14 | type IVFHeader struct {
15 | Signaure [4]byte
16 | Version uint16
17 | Size uint16
18 | Codec [4]byte
19 | Width uint16
20 | Height uint16
21 | FrameRate uint32
22 | FrameScale uint32
23 | FrameCount uint32
24 | _ uint32
25 | }
26 |
27 | type IVFFrameHeader struct {
28 | Size uint32
29 | PTS uint64
30 | }
31 |
32 | func NewIVFReader(r io.ReadSeeker) (*IVFReader, error) {
33 | var hdr IVFHeader
34 | if err := binary.Read(r, binary.LittleEndian, &hdr); err != nil {
35 | return nil, err
36 | }
37 |
38 | if string(hdr.Signaure[:]) != "DKIF" {
39 | return nil, errors.New("not a valid IVF file")
40 | }
41 | if hdr.Version != 0 {
42 | return nil, errors.New("unsupported IVF version")
43 | }
44 |
45 | return &IVFReader{
46 | reader: r,
47 | Header: hdr,
48 | }, nil
49 | }
50 |
51 | func (i *IVFReader) Codec() string {
52 | return string(i.Header.Codec[:])
53 | }
54 |
55 | func (i *IVFReader) ReadFrame() (data []byte, pts uint64, err error) {
56 | var hdr IVFFrameHeader
57 | if err := binary.Read(i.reader, binary.LittleEndian, &hdr); err != nil {
58 | return nil, 0, err
59 | }
60 |
61 | frame := make([]byte, hdr.Size)
62 | if _, err := io.ReadFull(i.reader, frame); err != nil {
63 | return nil, 0, err
64 | }
65 |
66 | return frame, hdr.PTS, nil
67 | }
68 |
69 | func (i *IVFReader) Rewind() error {
70 | _, err := i.reader.Seek(32, io.SeekStart)
71 | return err
72 | }
73 |
--------------------------------------------------------------------------------
/videos/player.go:
--------------------------------------------------------------------------------
1 | package videos
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "image"
7 | "io"
8 | "sync"
9 | "time"
10 |
11 | "github.com/dialup-inc/ascii/vpx"
12 | )
13 |
14 | type Player struct {
15 | reader *IVFReader
16 | decoder *vpx.Decoder
17 |
18 | OnFrame func(image.Image)
19 |
20 | ctxMu sync.Mutex
21 | ctx context.Context
22 | }
23 |
24 | func (p *Player) Play(ctx context.Context) error {
25 | p.ctxMu.Lock()
26 | if p.ctx != nil {
27 | p.ctxMu.Unlock()
28 | return nil
29 | }
30 | p.ctx = ctx
31 | p.ctxMu.Unlock()
32 |
33 | defer func() {
34 | p.ctxMu.Lock()
35 | p.ctx = ctx
36 | p.ctxMu.Unlock()
37 | }()
38 |
39 | if err := p.reader.Rewind(); err != nil {
40 | return err
41 | }
42 |
43 | lastFrame := time.Now()
44 |
45 | hdr := p.reader.Header
46 |
47 | period := time.Duration(float64(hdr.FrameScale) / float64(hdr.FrameRate) * float64(time.Second))
48 |
49 | for {
50 | frame, _, err := p.reader.ReadFrame()
51 | if err == io.EOF {
52 | return nil
53 | }
54 | if err != nil {
55 | return err
56 | }
57 |
58 | img, err := p.decoder.Decode(frame)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | waitTime := period - time.Since(lastFrame)
64 | select {
65 | case <-time.After(waitTime):
66 | case <-ctx.Done():
67 | return ctx.Err()
68 | }
69 |
70 | p.OnFrame(img)
71 |
72 | lastFrame = time.Now()
73 | }
74 | }
75 |
76 | func NewPlayer(r io.ReadSeeker) (*Player, error) {
77 | ivf, err := NewIVFReader(r)
78 | if err != nil {
79 | return nil, err
80 | }
81 | if c := ivf.Codec(); c != "VP80" {
82 | return nil, fmt.Errorf("unknown codec %q", c)
83 | }
84 |
85 | decoder, err := vpx.NewDecoder(int(ivf.Header.Width), int(ivf.Header.Height))
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | player := &Player{
91 | decoder: decoder,
92 | reader: ivf,
93 | OnFrame: func(image.Image) {},
94 | }
95 |
96 | return player, nil
97 | }
98 |
--------------------------------------------------------------------------------
/videos/src/globe.ivf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialup-inc/ascii/07c3fdeef4e5805838478594a0be1e4301f4aed5/videos/src/globe.ivf
--------------------------------------------------------------------------------
/videos/src/pion.ivf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dialup-inc/ascii/07c3fdeef4e5805838478594a0be1e4301f4aed5/videos/src/pion.ivf
--------------------------------------------------------------------------------
/videos/videos.go:
--------------------------------------------------------------------------------
1 | //go:generate go run github.com/shuLhan/go-bindata/cmd/go-bindata -pkg videos -nocompress src/...
2 | package videos
3 |
4 | import "bytes"
5 |
6 | func Globe() *bytes.Reader {
7 | return bytes.NewReader(MustAsset("src/globe.ivf"))
8 | }
9 |
10 | func Pion() *bytes.Reader {
11 | return bytes.NewReader(MustAsset("src/pion.ivf"))
12 | }
13 |
--------------------------------------------------------------------------------
/vpx/decoder.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | #define VPX_CODEC_DISABLE_COMPAT 1
7 | #include "vpx/vp8dx.h"
8 | #include "vpx/vpx_decoder.h"
9 |
10 | int vpx_init_dec(vpx_codec_ctx_t *ctx) {
11 | vpx_codec_iface_t *interface = vpx_codec_vp8_dx();
12 |
13 | // Initialize codec
14 | int flags = 0;
15 | vpx_codec_err_t err = vpx_codec_dec_init(ctx, interface, NULL, flags);
16 | if (err) {
17 | return err;
18 | }
19 |
20 | return 0;
21 | }
22 |
23 | int vpx_decode(vpx_codec_ctx_t *ctx, const char *frame, int frame_len,
24 | char *yv12_frame, int yv12_cap, int *yv12_len) {
25 | // Decode the frame
26 | vpx_codec_err_t err =
27 | vpx_codec_decode(ctx, (const unsigned char *)frame, frame_len, NULL, 0);
28 | if (err) {
29 | return err;
30 | }
31 |
32 | // Write decoded data to yv12_frame
33 | vpx_codec_iter_t iter = NULL;
34 | vpx_image_t *img;
35 |
36 | while ((img = vpx_codec_get_frame(ctx, &iter))) {
37 | for (int plane = 0; plane < 3; plane++) {
38 | unsigned char *buf = img->planes[plane];
39 | for (int y = 0; y < (plane ? (img->d_h + 1) >> 1 : img->d_h); y++) {
40 | if (*yv12_len > yv12_cap) {
41 | return VPX_CODEC_MEM_ERROR;
42 | }
43 |
44 | int len = (plane ? (img->d_w + 1) >> 1 : img->d_w);
45 | memcpy(yv12_frame + *yv12_len, buf, len);
46 | buf += img->stride[plane];
47 | *yv12_len += len;
48 | }
49 | }
50 | }
51 |
52 | return 0;
53 | }
54 |
55 | int vpx_cleanup_dec(vpx_codec_ctx_t *ctx) {
56 | vpx_codec_err_t err = vpx_codec_destroy(ctx);
57 | if (err) {
58 | return err;
59 | }
60 | return 0;
61 | }
62 |
--------------------------------------------------------------------------------
/vpx/decoder.go:
--------------------------------------------------------------------------------
1 | package vpx
2 |
3 | /*
4 | #cgo pkg-config: --static vpx
5 |
6 | #include "vpx/vpx_decoder.h"
7 |
8 | int vpx_init_dec(vpx_codec_ctx_t *ctx);
9 | int vpx_decode(vpx_codec_ctx_t *ctx, const char* frame, int frame_len, char* yv12_frame, int yv12_len, int *decoded_len);
10 | int vpx_cleanup_dec(vpx_codec_ctx_t *ctx);
11 | */
12 | import "C"
13 | import (
14 | "image"
15 | "sync"
16 | "unsafe"
17 |
18 | "github.com/dialup-inc/ascii/yuv"
19 | )
20 |
21 | type Decoder struct {
22 | mu sync.Mutex
23 |
24 | width int
25 | height int
26 | buf []byte
27 |
28 | ctx C.vpx_codec_ctx_t
29 | }
30 |
31 | func NewDecoder(width, height int) (*Decoder, error) {
32 | d := &Decoder{
33 | width: width,
34 | height: height,
35 |
36 | // PERF(maxhawkins): is this buffer too big?
37 | buf: make([]byte, width*height*4),
38 | }
39 | ret := C.vpx_init_dec(&d.ctx)
40 | if ret != 0 {
41 | return nil, VPXCodecErr(ret)
42 | }
43 | return d, nil
44 | }
45 |
46 | func (d *Decoder) Close() error {
47 | d.mu.Lock()
48 | defer d.mu.Unlock()
49 |
50 | ret := C.vpx_cleanup_dec(&d.ctx)
51 | if ret != 0 {
52 | return VPXCodecErr(ret)
53 | }
54 | return nil
55 | }
56 |
57 | func (d *Decoder) Decode(b []byte) (image.Image, error) {
58 | d.mu.Lock()
59 | defer d.mu.Unlock()
60 |
61 | if len(b) == 0 {
62 | return nil, nil
63 | }
64 |
65 | n, err := d.decode(d.buf, b)
66 | if err != nil {
67 | return nil, err
68 | }
69 | frame := d.buf[:n]
70 |
71 | return yuv.FromI420(frame, d.width, d.height)
72 | }
73 |
74 | func (d *Decoder) decode(out, in []byte) (n int, err error) {
75 | inP := (*C.char)(unsafe.Pointer(&in[0]))
76 | inL := C.int(len(in))
77 |
78 | outP := (*C.char)(unsafe.Pointer(&out[0]))
79 | outCap := C.int(cap(out))
80 | outL := (*C.int)(unsafe.Pointer(&n))
81 |
82 | ret := C.vpx_decode(&d.ctx, inP, inL, outP, outCap, outL)
83 | if ret != 0 {
84 | return n, VPXCodecErr(ret)
85 | }
86 |
87 | return n, nil
88 | }
89 |
--------------------------------------------------------------------------------
/vpx/encoder.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #define VPX_CODEC_DISABLE_COMPAT 1
6 | #include
7 | #include
8 |
9 | int vpx_init_enc(vpx_codec_ctx_t *codec, vpx_image_t **raw, int width,
10 | int height) {
11 | vpx_codec_iface_t *interface = vpx_codec_vp8_cx();
12 |
13 | vpx_image_t *img = vpx_img_alloc(NULL, VPX_IMG_FMT_I420, width, height, 1);
14 | if (!img) {
15 | return VPX_CODEC_MEM_ERROR;
16 | }
17 | *raw = img;
18 |
19 | vpx_codec_enc_cfg_t cfg;
20 |
21 | // Populate encoder configuration
22 | vpx_codec_err_t err = vpx_codec_enc_config_default(interface, &cfg, 0);
23 | if (err) {
24 | return err;
25 | }
26 |
27 | // Update the default configuration with our settings
28 | cfg.rc_target_bitrate =
29 | width * height * cfg.rc_target_bitrate / cfg.g_w / cfg.g_h;
30 | cfg.g_w = width;
31 | cfg.g_h = height;
32 | cfg.g_lag_in_frames = 0;
33 | cfg.g_pass = VPX_RC_ONE_PASS;
34 | cfg.rc_end_usage = VPX_CBR;
35 | cfg.kf_mode = VPX_KF_AUTO;
36 | cfg.kf_max_dist = 1000;
37 | cfg.g_error_resilient =
38 | VPX_ERROR_RESILIENT_DEFAULT | VPX_ERROR_RESILIENT_PARTITIONS;
39 |
40 | // Initialize codec
41 | err = vpx_codec_enc_init(codec, interface, &cfg, 0);
42 | if (err) {
43 | return err;
44 | }
45 |
46 | return 0;
47 | }
48 |
49 | int vpx_encode(vpx_codec_ctx_t *ctx, vpx_image_t *raw, const char *yv12_frame,
50 | int yv12_len, char *encoded, int encoded_cap, int *encoded_len,
51 | int pts, int force_key_frame) {
52 | *encoded_len = 0;
53 |
54 | int flags = 0;
55 | if (force_key_frame) {
56 | flags |= VPX_EFLAG_FORCE_KF;
57 | }
58 |
59 | // This does not work correctly (only the 1st plane is encoded), why?
60 | // raw->planes[0] = (unsigned char *)yv12_frame;
61 | memcpy(raw->planes[0], yv12_frame, yv12_len);
62 |
63 | vpx_codec_err_t err =
64 | vpx_codec_encode(ctx, raw, pts, 1, flags, VPX_DL_REALTIME);
65 | if (err) {
66 | return err;
67 | }
68 |
69 | vpx_codec_iter_t iter = NULL;
70 | const vpx_codec_cx_pkt_t *pkt;
71 | while ((pkt = vpx_codec_get_cx_data(ctx, &iter))) {
72 | if (pkt->kind == VPX_CODEC_CX_FRAME_PKT) {
73 | size_t frame_size = pkt->data.frame.sz;
74 | if (frame_size + *encoded_len > encoded_cap) {
75 | return VPX_CODEC_MEM_ERROR;
76 | }
77 |
78 | memcpy(encoded, pkt->data.frame.buf, pkt->data.frame.sz);
79 | *encoded_len += frame_size;
80 | encoded += frame_size;
81 |
82 | // TODO(maxhawkins): should we be concatenating packets instead of
83 | // returning early?
84 | return 0;
85 | }
86 | }
87 |
88 | return 0;
89 | }
90 |
91 | int vpx_cleanup_enc(vpx_codec_ctx_t *codec, vpx_image_t *raw) {
92 | vpx_img_free(raw);
93 |
94 | vpx_codec_err_t err = vpx_codec_destroy(codec);
95 | if (err) {
96 | return err;
97 | }
98 |
99 | return 0;
100 | }
101 |
--------------------------------------------------------------------------------
/vpx/encoder.go:
--------------------------------------------------------------------------------
1 | package vpx
2 |
3 | /*
4 | #cgo pkg-config: --static vpx
5 |
6 | #include "vpx/vpx_encoder.h"
7 |
8 | int vpx_init_enc(vpx_codec_ctx_t *ctx, vpx_image_t **raw, int width, int height);
9 | int vpx_encode(vpx_codec_ctx_t *ctx, vpx_image_t *raw, const char* yv12_frame, int yv12_len, char* encoded, int encoded_cap, int* size, int pts, int force_key_frame);
10 | int vpx_cleanup_enc(vpx_codec_ctx_t *ctx, vpx_image_t *raw);
11 | */
12 | import "C"
13 |
14 | import (
15 | "image"
16 | "sync"
17 | "unsafe"
18 |
19 | "github.com/dialup-inc/ascii/yuv"
20 | )
21 |
22 | type Encoder struct {
23 | mu sync.Mutex
24 |
25 | ctx C.vpx_codec_ctx_t
26 | img *C.vpx_image_t
27 | }
28 |
29 | func NewEncoder(width, height int) (*Encoder, error) {
30 | e := &Encoder{}
31 | ret := C.vpx_init_enc(&e.ctx, &e.img, C.int(width), C.int(height))
32 | if ret != 0 {
33 | return nil, VPXCodecErr(ret)
34 | }
35 | return e, nil
36 | }
37 |
38 | func (e *Encoder) Close() error {
39 | e.mu.Lock()
40 | defer e.mu.Unlock()
41 |
42 | ret := C.vpx_cleanup_enc(&e.ctx, e.img)
43 | if ret != 0 {
44 | return VPXCodecErr(ret)
45 | }
46 | return nil
47 | }
48 |
49 | func (e *Encoder) Encode(vpxFrame []byte, img image.Image, pts int, forceKeyframe bool) (n int, err error) {
50 | e.mu.Lock()
51 | defer e.mu.Unlock()
52 |
53 | i420, _, _ := yuv.ToI420(img)
54 | return e.encode(vpxFrame, i420, pts, forceKeyframe)
55 | }
56 |
57 | func (e *Encoder) encode(vpxFrame, yuvFrame []byte, pts int, forceKeyframe bool) (n int, err error) {
58 | inP := (*C.char)(unsafe.Pointer(&yuvFrame[0]))
59 | inL := C.int(len(yuvFrame))
60 |
61 | outP := (*C.char)(unsafe.Pointer(&vpxFrame[0]))
62 | outCap := C.int(cap(vpxFrame))
63 | outL := (*C.int)(unsafe.Pointer(&n))
64 |
65 | forceKeyframeB := C.int(0)
66 | if forceKeyframe {
67 | forceKeyframeB = C.int(1)
68 | }
69 |
70 | ret := C.vpx_encode(&e.ctx, e.img, inP, inL, outP, outCap, outL, C.int(pts), forceKeyframeB)
71 | if ret != 0 {
72 | return n, VPXCodecErr(ret)
73 | }
74 |
75 | return n, nil
76 | }
77 |
--------------------------------------------------------------------------------
/vpx/errors.go:
--------------------------------------------------------------------------------
1 | package vpx
2 |
3 | import "fmt"
4 |
5 | type VPXCodecErr int
6 |
7 | const (
8 | VPX_CODEC_OK VPXCodecErr = iota
9 | VPX_CODEC_ERROR
10 | VPX_CODEC_MEM_ERROR
11 | VPX_CODEC_ABI_MISMATCH
12 | VPX_CODEC_INCAPABLE
13 | VPX_CODEC_UNSUP_BITSTREAM
14 | VPX_CODEC_UNSUP_FEATURE
15 | VPX_CODEC_CORRUPT_FRAME
16 | VPX_CODEC_INVALID_PARAM
17 | VPX_CODEC_LIST_END
18 | )
19 |
20 | func (e VPXCodecErr) Error() string {
21 | switch e {
22 | case VPX_CODEC_OK:
23 | return "Success"
24 | case VPX_CODEC_ERROR:
25 | return "Unspecified internal error"
26 | case VPX_CODEC_MEM_ERROR:
27 | return "Memory allocation error"
28 | case VPX_CODEC_ABI_MISMATCH:
29 | return "ABI version mismatch"
30 | case VPX_CODEC_INCAPABLE:
31 | return "Codec does not implement requested capability"
32 | case VPX_CODEC_UNSUP_BITSTREAM:
33 | return "Bitstream not supported by this decoder"
34 | case VPX_CODEC_UNSUP_FEATURE:
35 | return "Bitstream required feature not supported by this decoder"
36 | case VPX_CODEC_CORRUPT_FRAME:
37 | return "Corrupt frame detected"
38 | case VPX_CODEC_INVALID_PARAM:
39 | return "Invalid parameter"
40 | case VPX_CODEC_LIST_END:
41 | return "End of iterated list"
42 | default:
43 | return fmt.Sprintf("codec error: %d", e)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/yuv/encoding.go:
--------------------------------------------------------------------------------
1 | package yuv
2 |
3 | import (
4 | "fmt"
5 | "image"
6 | "image/color"
7 | )
8 |
9 | // FromI420 decodes an i420-encoded YUV image into a Go Image.
10 | //
11 | // See https://www.fourcc.org/pixel-format/yuv-i420/
12 | func FromI420(frame []byte, width, height int) (*image.YCbCr, error) {
13 | yi := width * height
14 | cbi := yi + width*height/4
15 | cri := cbi + width*height/4
16 |
17 | if cri > len(frame) {
18 | return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), cri)
19 | }
20 |
21 | return &image.YCbCr{
22 | Y: frame[:yi],
23 | YStride: width,
24 | Cb: frame[yi:cbi],
25 | Cr: frame[cbi:cri],
26 | CStride: width / 2,
27 | SubsampleRatio: image.YCbCrSubsampleRatio420,
28 | Rect: image.Rect(0, 0, width, height),
29 | }, nil
30 | }
31 |
32 | // FromNV21 decodes an NV21-encoded, big-endian YUV image into a Go Image.
33 | //
34 | // See https://www.fourcc.org/pixel-format/yuv-nv21/
35 | func FromNV21(frame []byte, width, height int) (*image.YCbCr, error) {
36 | yi := width * height
37 | ci := yi + width*height/2
38 |
39 | if ci > len(frame) {
40 | return nil, fmt.Errorf("frame length (%d) less than expected (%d)", len(frame), ci)
41 | }
42 |
43 | var cb, cr []byte
44 | for i := yi; i < ci; i += 2 {
45 | cb = append(cb, frame[i])
46 | cr = append(cr, frame[i+1])
47 | }
48 |
49 | return &image.YCbCr{
50 | Y: frame[:yi],
51 | YStride: width,
52 | Cb: cb,
53 | Cr: cr,
54 | CStride: width / 2,
55 | SubsampleRatio: image.YCbCrSubsampleRatio420,
56 | Rect: image.Rect(0, 0, width, height),
57 | }, nil
58 | }
59 |
60 | func convertTo420(img image.Image) *image.YCbCr {
61 | bounds := img.Bounds()
62 | img420 := image.NewYCbCr(bounds, image.YCbCrSubsampleRatio420)
63 |
64 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
65 | for x := bounds.Min.X; x < bounds.Max.X; x++ {
66 | r, g, b, _ := img.At(x, y).RGBA()
67 | yy, cb, cr := color.RGBToYCbCr(uint8(r>>8), uint8(g>>8), uint8(b>>8))
68 |
69 | cy := img420.YOffset(x, y)
70 | ci := img420.COffset(x, y)
71 | img420.Y[cy] = yy
72 | img420.Cb[ci] = cb
73 | img420.Cr[ci] = cr
74 | }
75 | }
76 |
77 | return img420
78 | }
79 |
80 | // ToI420 converts a Go image into an I420-encoded YUV raw image slice
81 | //
82 | // See https://www.fourcc.org/pixel-format/yuv-i420/
83 | func ToI420(img image.Image) (frame []byte, width, height int) {
84 | bounds := img.Bounds()
85 |
86 | var img420 *image.YCbCr
87 | if y, ok := img.(*image.YCbCr); ok && y.SubsampleRatio == image.YCbCrSubsampleRatio420 {
88 | // If the image is already I420, just use it
89 | img420 = y
90 | } else {
91 | // Otherwise convert it to I420
92 | img420 = convertTo420(img)
93 | }
94 |
95 | frame = append(frame, img420.Y...)
96 | frame = append(frame, img420.Cb...)
97 | frame = append(frame, img420.Cr...)
98 |
99 | return frame, bounds.Dx(), bounds.Dy()
100 | }
101 |
--------------------------------------------------------------------------------