1
1
import Browser from 'webextension-polyfill' ;
2
+ import { nanoid } from 'nanoid' ;
2
3
import type { eventWithTime } from '@rrweb/types' ;
3
4
import Channel from '~/utils/channel' ;
4
5
import {
5
- type LocalData ,
6
+ EventName ,
6
7
LocalDataKey ,
8
+ MessageName ,
7
9
RecorderStatus ,
8
- type Settings ,
9
- type SyncData ,
10
+ ServiceName ,
10
11
SyncDataKey ,
11
12
} from '~/types' ;
12
- import { pauseRecording , resumeRecording } from '~/utils/recording' ;
13
-
14
- const channel = new Channel ( ) ;
13
+ import type {
14
+ LocalData ,
15
+ RecordStartedMessage ,
16
+ RecordStoppedMessage ,
17
+ Session ,
18
+ Settings ,
19
+ SyncData ,
20
+ } from '~/types' ;
21
+ import { isFirefox } from '~/utils' ;
22
+ import { addSession } from '~/utils/storage' ;
15
23
16
24
void ( async ( ) => {
17
25
// assign default value to settings of this extension
@@ -28,105 +36,215 @@ void (async () => {
28
36
settings,
29
37
} as SyncData ) ;
30
38
31
- // When tab is changed during the recording process, pause recording in the old tab and start a new one in the new tab.
32
- Browser . tabs . onActivated . addListener ( ( activeInfo ) => {
33
- Browser . storage . local
34
- . get ( LocalDataKey . recorderStatus )
35
- . then ( async ( data ) => {
36
- const localData = data as LocalData ;
37
- if ( ! localData || ! localData [ LocalDataKey . recorderStatus ] ) return ;
38
- let statusData = localData [ LocalDataKey . recorderStatus ] ;
39
- let { status } = statusData ;
40
- let bufferedEvents : eventWithTime [ ] | undefined ;
41
-
42
- if ( status === RecorderStatus . RECORDING ) {
43
- const result = await pauseRecording (
44
- channel ,
45
- RecorderStatus . PausedSwitch ,
46
- statusData ,
47
- ) . catch ( async ( ) => {
48
- /**
49
- * This error happen when the old tab is closed.
50
- * In this case, the recording process would be stopped through Browser.tabs.onRemoved API.
51
- * So we just read the new status here.
52
- */
53
- const localData = ( await Browser . storage . local . get (
54
- LocalDataKey . recorderStatus ,
55
- ) ) as LocalData ;
56
- return {
57
- status : localData [ LocalDataKey . recorderStatus ] ,
58
- bufferedEvents,
59
- } ;
60
- } ) ;
61
- if ( ! result ) return ;
62
- statusData = result . status ;
63
- status = statusData . status ;
64
- bufferedEvents = result . bufferedEvents ;
65
- }
66
- if ( status === RecorderStatus . PausedSwitch )
67
- await resumeRecording (
68
- channel ,
69
- activeInfo . tabId ,
70
- statusData ,
71
- bufferedEvents ,
72
- ) ;
73
- } )
39
+ const events : eventWithTime [ ] = [ ] ;
40
+ const channel = new Channel ( ) ;
41
+ let recorderStatus : LocalData [ LocalDataKey . recorderStatus ] = {
42
+ status : RecorderStatus . IDLE ,
43
+ activeTabId : - 1 ,
44
+ } ;
45
+ // Reset recorder status when the extension is reloaded.
46
+ await Browser . storage . local . set ( {
47
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
48
+ } ) ;
49
+
50
+ channel . on ( EventName . StartButtonClicked , async ( ) => {
51
+ if ( recorderStatus . status !== RecorderStatus . IDLE ) return ;
52
+ recorderStatus = {
53
+ status : RecorderStatus . IDLE ,
54
+ activeTabId : - 1 ,
55
+ } ;
56
+ await Browser . storage . local . set ( {
57
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
58
+ } ) ;
59
+
60
+ events . length = 0 ; // clear events before recording
61
+ const tabId = await channel . getCurrentTabId ( ) ;
62
+ if ( tabId === - 1 ) return ;
63
+
64
+ const res = ( await channel
65
+ . requestToTab ( tabId , ServiceName . StartRecord , { } )
66
+ . catch ( async ( error : Error ) => {
67
+ recorderStatus . errorMessage = error . message ;
68
+ await Browser . storage . local . set ( {
69
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
70
+ } ) ;
71
+ } ) ) as RecordStartedMessage ;
72
+ if ( ! res ) return ;
73
+ Object . assign ( recorderStatus , {
74
+ status : RecorderStatus . RECORDING ,
75
+ activeTabId : tabId ,
76
+ startTimestamp : res . startTimestamp ,
77
+ } ) ;
78
+ await Browser . storage . local . set ( {
79
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
80
+ } ) ;
81
+ } ) ;
82
+
83
+ channel . on ( EventName . StopButtonClicked , async ( ) => {
84
+ if ( recorderStatus . status === RecorderStatus . IDLE ) return ;
85
+
86
+ if ( recorderStatus . status === RecorderStatus . RECORDING )
87
+ ( await channel
88
+ . requestToTab ( recorderStatus . activeTabId , ServiceName . StopRecord , { } )
89
+ . catch ( ( ) => ( {
90
+ message : MessageName . RecordStopped ,
91
+ endTimestamp : Date . now ( ) ,
92
+ } ) ) ) as RecordStoppedMessage ;
93
+ recorderStatus = {
94
+ status : RecorderStatus . IDLE ,
95
+ activeTabId : - 1 ,
96
+ } ;
97
+ await Browser . storage . local . set ( {
98
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
99
+ } ) ;
100
+ const title =
101
+ ( await Browser . tabs
102
+ . query ( { active : true , currentWindow : true } )
103
+ . then ( ( tabs ) => tabs [ 0 ] ?. title )
104
+ . catch ( ( ) => {
105
+ // ignore error
106
+ } ) ) ?? 'new session' ;
107
+ const newSession = generateSession ( title ) ;
108
+ await addSession ( newSession , events ) . catch ( ( e ) => {
109
+ recorderStatus . errorMessage = ( e as { message : string } ) . message ;
110
+ void Browser . storage . local . set ( {
111
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
112
+ } ) ;
113
+ } ) ;
114
+ channel . emit ( EventName . SessionUpdated , {
115
+ session : newSession ,
116
+ } ) ;
117
+ events . length = 0 ;
118
+ } ) ;
119
+
120
+ async function pauseRecording ( newStatus : RecorderStatus ) {
121
+ if (
122
+ recorderStatus . status !== RecorderStatus . RECORDING ||
123
+ recorderStatus . activeTabId === - 1
124
+ )
125
+ return ;
126
+
127
+ const stopResponse = ( await channel
128
+ . requestToTab ( recorderStatus . activeTabId , ServiceName . StopRecord , { } )
74
129
. catch ( ( ) => {
75
- // the extension can't access to the tab
130
+ // ignore error
131
+ } ) ) as RecordStoppedMessage | undefined ;
132
+ Object . assign ( recorderStatus , {
133
+ status : newStatus ,
134
+ activeTabId : - 1 ,
135
+ pausedTimestamp : stopResponse ?. endTimestamp ,
136
+ } ) ;
137
+ await Browser . storage . local . set ( {
138
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
139
+ } ) ;
140
+ }
141
+ channel . on ( EventName . PauseButtonClicked , async ( ) => {
142
+ if ( recorderStatus . status !== RecorderStatus . RECORDING ) return ;
143
+ await pauseRecording ( RecorderStatus . PAUSED ) ;
144
+ } ) ;
145
+
146
+ async function resumeRecording ( newTabId : number ) {
147
+ if (
148
+ ! [ RecorderStatus . PAUSED , RecorderStatus . PausedSwitch ] . includes (
149
+ recorderStatus . status ,
150
+ )
151
+ )
152
+ return ;
153
+ const { startTimestamp, pausedTimestamp } = recorderStatus ;
154
+ // On Firefox, the new tab is not communicable immediately after it is created.
155
+ if ( isFirefox ( ) ) await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
156
+ const pausedTime = pausedTimestamp ? Date . now ( ) - pausedTimestamp : 0 ;
157
+ // Decrease the time spent in the pause state and make them look like a continuous recording.
158
+ events . forEach ( ( event ) => {
159
+ event . timestamp += pausedTime ;
160
+ } ) ;
161
+ const startResponse = ( await channel
162
+ . requestToTab ( newTabId , ServiceName . StartRecord , { } )
163
+ . catch ( ( e : { message : string } ) => {
164
+ recorderStatus . errorMessage = e . message ;
165
+ void Browser . storage . local . set ( {
166
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
167
+ } ) ;
168
+ } ) ) as RecordStartedMessage | undefined ;
169
+ if ( ! startResponse ) {
170
+ // Restore the events data when the recording fails to start.
171
+ events . forEach ( ( event ) => {
172
+ event . timestamp -= pausedTime ;
76
173
} ) ;
174
+ return ;
175
+ }
176
+ recorderStatus = {
177
+ status : RecorderStatus . RECORDING ,
178
+ activeTabId : newTabId ,
179
+ startTimestamp : ( startTimestamp || Date . now ( ) ) + pausedTime ,
180
+ } ;
181
+ await Browser . storage . local . set ( {
182
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
183
+ } ) ;
184
+ }
185
+ channel . on ( EventName . ResumeButtonClicked , async ( ) => {
186
+ if ( recorderStatus . status !== RecorderStatus . PAUSED ) return ;
187
+ recorderStatus . errorMessage = undefined ;
188
+ await Browser . storage . local . set ( {
189
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
190
+ } ) ;
191
+ const tabId = await channel . getCurrentTabId ( ) ;
192
+ await resumeRecording ( tabId ) ;
193
+ } ) ;
194
+
195
+ channel . on ( EventName . ContentScriptEmitEvent , ( data ) => {
196
+ events . push ( data as eventWithTime ) ;
197
+ } ) ;
198
+
199
+ // When tab is changed during the recording process, pause recording in the old tab and start a new one in the new tab.
200
+ Browser . tabs . onActivated . addListener ( ( activeInfo ) => {
201
+ void ( async ( ) => {
202
+ if (
203
+ recorderStatus . status !== RecorderStatus . RECORDING &&
204
+ recorderStatus . status !== RecorderStatus . PausedSwitch
205
+ )
206
+ return ;
207
+ if ( activeInfo . tabId === recorderStatus . activeTabId ) return ;
208
+ if ( recorderStatus . status === RecorderStatus . RECORDING )
209
+ await pauseRecording ( RecorderStatus . PausedSwitch ) ;
210
+ if ( recorderStatus . status === RecorderStatus . PausedSwitch )
211
+ await resumeRecording ( activeInfo . tabId ) ;
212
+ } ) ( ) ;
213
+ return ;
77
214
} ) ;
78
215
79
216
// If the recording can't start on an invalid tab, resume it when the tab content is updated.
80
217
Browser . tabs . onUpdated . addListener ( function ( tabId , info ) {
81
218
if ( info . status !== 'complete' ) return ;
82
- Browser . storage . local
83
- . get ( LocalDataKey . recorderStatus )
84
- . then ( async ( data ) => {
85
- const localData = data as LocalData ;
86
- if ( ! localData || ! localData [ LocalDataKey . recorderStatus ] ) return ;
87
- const { status, activeTabId } = localData [ LocalDataKey . recorderStatus ] ;
88
- if ( status !== RecorderStatus . PausedSwitch || activeTabId === tabId )
89
- return ;
90
- await resumeRecording (
91
- channel ,
92
- tabId ,
93
- localData [ LocalDataKey . recorderStatus ] ,
94
- ) ;
95
- } )
96
- . catch ( ( ) => {
97
- // the extension can't access to the tab
98
- } ) ;
219
+ if (
220
+ recorderStatus . status !== RecorderStatus . PausedSwitch ||
221
+ recorderStatus . activeTabId === tabId
222
+ )
223
+ return ;
224
+ void resumeRecording ( tabId ) ;
99
225
} ) ;
100
226
101
227
/**
102
- * When the current tab is closed, the recording events will be lost because this event is fired after it is closed.
103
- * This event listener is just used to make sure the recording status is updated.
228
+ * When the current tab is closed, and there's no other tab to resume recording, make sure the recording status is updated to SwitchPaused.
104
229
*/
105
230
Browser . tabs . onRemoved . addListener ( ( tabId ) => {
106
- Browser . storage . local
107
- . get ( LocalDataKey . recorderStatus )
108
- . then ( async ( data ) => {
109
- const localData = data as LocalData ;
110
- if ( ! localData || ! localData [ LocalDataKey . recorderStatus ] ) return ;
111
- const { status, activeTabId, startTimestamp } =
112
- localData [ LocalDataKey . recorderStatus ] ;
113
- if ( activeTabId !== tabId || status !== RecorderStatus . RECORDING )
114
- return ;
115
-
116
- // Update the recording status to make it resumable after users switch to other tabs.
117
- const statusData : LocalData [ LocalDataKey . recorderStatus ] = {
118
- status : RecorderStatus . PausedSwitch ,
119
- activeTabId,
120
- startTimestamp,
121
- pausedTimestamp : Date . now ( ) ,
122
- } ;
123
- await Browser . storage . local . set ( {
124
- [ LocalDataKey . recorderStatus ] : statusData ,
125
- } ) ;
126
- } )
127
- . catch ( ( err ) => {
128
- console . error ( err ) ;
231
+ void ( async ( ) => {
232
+ if (
233
+ recorderStatus . activeTabId !== tabId ||
234
+ recorderStatus . status !== RecorderStatus . RECORDING
235
+ )
236
+ return ;
237
+ // Update the recording status to make it resumable after users switch to other tabs.
238
+ Object . assign ( recorderStatus , {
239
+ status : RecorderStatus . PausedSwitch ,
240
+ activeTabId : - 1 ,
241
+ pausedTimestamp : Date . now ( ) ,
242
+ } ) ;
243
+
244
+ await Browser . storage . local . set ( {
245
+ [ LocalDataKey . recorderStatus ] : recorderStatus ,
129
246
} ) ;
247
+ } ) ( ) ;
130
248
} ) ;
131
249
} ) ( ) ;
132
250
@@ -160,3 +278,15 @@ function setDefaultSettings(
160
278
}
161
279
}
162
280
}
281
+
282
+ function generateSession ( title : string ) {
283
+ const newSession : Session = {
284
+ id : nanoid ( ) ,
285
+ name : title ,
286
+ tags : [ ] ,
287
+ createTimestamp : Date . now ( ) ,
288
+ modifyTimestamp : Date . now ( ) ,
289
+ recorderVersion : Browser . runtime . getManifest ( ) . version_name || 'unknown' ,
290
+ } ;
291
+ return newSession ;
292
+ }
0 commit comments