forked from gravitational/teleport
/
playback.go
162 lines (146 loc) · 4.72 KB
/
playback.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package desktop
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/gorilla/websocket"
"github.com/sirupsen/logrus"
"github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/player"
"github.com/gravitational/teleport/lib/utils"
)
const (
minPlaybackSpeed = 0.25
maxPlaybackSpeed = 16
)
// playbackAction identifies a command sent from the
// browser to control playback
type playbackAction string
const (
// actionPlayPause toggles the playback state
// between playing and paused
actionPlayPause = playbackAction("play/pause")
// actionSpeed sets the playback speed
actionSpeed = playbackAction("speed")
// actionSeek moves to a different position in the recording
actionSeek = playbackAction("seek")
)
// actionMessage is a message passed from the playback client
// to the server over the websocket connection in order to
// control playback.
type actionMessage struct {
Action playbackAction `json:"action"`
PlaybackSpeed float64 `json:"speed,omitempty"`
Pos int64 `json:"pos"`
}
// ReceivePlaybackActions handles logic for receiving playbackAction messages
// over the websocket and updating the player state accordingly.
func ReceivePlaybackActions(
log logrus.FieldLogger,
ws *websocket.Conn,
player *player.Player) {
// playback always starts in a playing state
playing := true
for {
var action actionMessage
if err := ws.ReadJSON(&action); err != nil {
// Connection close errors are expected if the user closes the tab.
// Only log unexpected errors to avoid cluttering the logs.
if !utils.IsOKNetworkError(err) {
log.Warnf("websocket read error: %v", err)
}
return
}
switch action.Action {
case actionPlayPause:
if playing {
player.Pause()
} else {
player.Play()
}
playing = !playing
case actionSpeed:
action.PlaybackSpeed = max(action.PlaybackSpeed, minPlaybackSpeed)
action.PlaybackSpeed = min(action.PlaybackSpeed, maxPlaybackSpeed)
player.SetSpeed(action.PlaybackSpeed)
case actionSeek:
player.SetPos(time.Duration(action.Pos) * time.Millisecond)
default:
log.Warnf("invalid desktop playback action: %v", action.Action)
return
}
}
}
// PlayRecording feeds recorded events from a player
// over a websocket.
func PlayRecording(
ctx context.Context,
log logrus.FieldLogger,
ws *websocket.Conn,
player *player.Player) {
player.Play()
for {
select {
case <-ctx.Done():
return
case evt, ok := <-player.C():
if !ok {
if playerErr := player.Err(); playerErr != nil {
// Attempt to JSONify the error (escaping any quotes)
msg, err := json.Marshal(playerErr.Error())
if err != nil {
log.Warnf("failed to marshal player error message: %v", err)
msg = []byte(`"internal server error"`)
}
//lint:ignore QF1012 this write needs to happen in a single operation
bytes := []byte(fmt.Sprintf(`{"message":"error", "errorText":%s}`, string(msg)))
if err := ws.WriteMessage(websocket.BinaryMessage, bytes); err != nil {
log.Errorf("failed to write error message: %v", err)
}
return
}
if err := ws.WriteMessage(websocket.BinaryMessage, []byte(`{"message":"end"}`)); err != nil {
log.Errorf("failed to write end message: %v", err)
}
return
}
// some events are part of the stream but not currently
// needed during playback (session start/end, clipboard use, etc)
if _, ok := evt.(*events.DesktopRecording); !ok {
continue
}
msg, err := utils.FastMarshal(evt)
if err != nil {
log.Errorf("failed to marshal desktop event: %v", err)
ws.WriteMessage(websocket.BinaryMessage, []byte(`{"message":"error","errorText":"server error"}`))
return
}
if err := ws.WriteMessage(websocket.BinaryMessage, msg); err != nil {
// Connection close errors are expected if the user closes the tab.
// Only log unexpected errors to avoid cluttering the logs.
if !utils.IsOKNetworkError(err) {
log.Warnf("websocket write error: %v", err)
}
return
}
}
}
}