Kaynağa Gözat

[RN] Refactor video muting

Simplify the code by using a bitfied instead of a couple of boolean flags. This
allows us to mute the video from multiple places and only make the unmute
effective once they have all unmuted.

Alas, this cannot be applied to the web without a massive refactor, because it
uses the track muted state as the source of truth instead of the media state.
j8
Saúl Ibarra Corretgé 7 yıl önce
ebeveyn
işleme
d600504d85

+ 0
- 14
react/features/base/conference/actionTypes.js Dosyayı Görüntüle

@@ -75,20 +75,6 @@ export const LOCK_STATE_CHANGED = Symbol('LOCK_STATE_CHANGED');
75 75
  */
76 76
 export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
77 77
 
78
-/**
79
- * The type of (redux) action which signals that video will be muted because the
80
- * audio-only mode was enabled/disabled.
81
- *
82
- * {
83
- *     type: _SET_AUDIO_ONLY_VIDEO_MUTED,
84
- *     muted: boolean
85
- * }
86
- *
87
- * @protected
88
- */
89
-export const _SET_AUDIO_ONLY_VIDEO_MUTED
90
-    = Symbol('_SET_AUDIO_ONLY_VIDEO_MUTED');
91
-
92 78
 /**
93 79
  * The type of (redux) action to set whether or not the displayed large video is
94 80
  * in high-definition.

+ 16
- 44
react/features/base/conference/actions.js Dosyayı Görüntüle

@@ -1,5 +1,5 @@
1 1
 import { JitsiConferenceEvents } from '../lib-jitsi-meet';
2
-import { setVideoMuted } from '../media';
2
+import { setAudioMuted, setVideoMuted } from '../media';
3 3
 import {
4 4
     dominantSpeakerChanged,
5 5
     getLocalParticipant,
@@ -19,7 +19,6 @@ import {
19 19
     CONFERENCE_WILL_LEAVE,
20 20
     LOCK_STATE_CHANGED,
21 21
     SET_AUDIO_ONLY,
22
-    _SET_AUDIO_ONLY_VIDEO_MUTED,
23 22
     SET_LARGE_VIDEO_HD_STATUS,
24 23
     SET_LASTN,
25 24
     SET_PASSWORD,
@@ -61,6 +60,19 @@ function _addConferenceListeners(conference, dispatch) {
61 60
         JitsiConferenceEvents.LOCK_STATE_CHANGED,
62 61
         (...args) => dispatch(lockStateChanged(conference, ...args)));
63 62
 
63
+    // Dispatches into features/base/media follow:
64
+
65
+    // FIXME: This is needed because when Jicofo tells us to start muted
66
+    // lib-jitsi-meet does the actual muting. Perhaps this should be refactored
67
+    // so applications are hinted to start muted, but lib-jitsi-meet doesn't
68
+    // take action.
69
+    conference.on(
70
+        JitsiConferenceEvents.STARTED_MUTED,
71
+        () => {
72
+            dispatch(setAudioMuted(Boolean(conference.startAudioMuted)));
73
+            dispatch(setVideoMuted(Boolean(conference.startVideoMuted)));
74
+        });
75
+
64 76
     // Dispatches into features/base/tracks follow:
65 77
 
66 78
     conference.on(
@@ -296,58 +308,18 @@ export function lockStateChanged(conference, locked) {
296 308
  *
297 309
  * @param {boolean} audioOnly - True if the conference should be audio only;
298 310
  * false, otherwise.
299
- * @private
300 311
  * @returns {{
301 312
  *     type: SET_AUDIO_ONLY,
302 313
  *     audioOnly: boolean
303 314
  * }}
304 315
  */
305
-function _setAudioOnly(audioOnly) {
316
+export function setAudioOnly(audioOnly) {
306 317
     return {
307 318
         type: SET_AUDIO_ONLY,
308 319
         audioOnly
309 320
     };
310 321
 }
311 322
 
312
-/**
313
- * Signals that the app should mute video because it's now in audio-only mode,
314
- * or unmute it because it no longer is. If video was already muted, nothing
315
- * will happen; otherwise, it will be muted. When audio-only mode is disabled,
316
- * the previous state will be restored.
317
- *
318
- * @param {boolean} muted - True if video should be muted; false, otherwise.
319
- * @protected
320
- * @returns {Function}
321
- */
322
-export function _setAudioOnlyVideoMuted(muted: boolean) {
323
-    return (dispatch, getState) => {
324
-        if (muted) {
325
-            const { video } = getState()['features/base/media'];
326
-
327
-            if (video.muted) {
328
-                // Video is already muted, do nothing.
329
-                return;
330
-            }
331
-        } else {
332
-            const { audioOnlyVideoMuted }
333
-                = getState()['features/base/conference'];
334
-
335
-            if (!audioOnlyVideoMuted) {
336
-                // We didn't mute video, do nothing.
337
-                return;
338
-            }
339
-        }
340
-
341
-        // Remember that local video was muted due to the audio-only mode
342
-        // vs user's choice.
343
-        dispatch({
344
-            type: _SET_AUDIO_ONLY_VIDEO_MUTED,
345
-            muted
346
-        });
347
-        dispatch(setVideoMuted(muted));
348
-    };
349
-}
350
-
351 323
 /**
352 324
  * Action to set whether or not the currently displayed large video is in
353 325
  * high-definition.
@@ -488,6 +460,6 @@ export function toggleAudioOnly() {
488 460
     return (dispatch: Dispatch<*>, getState: Function) => {
489 461
         const { audioOnly } = getState()['features/base/conference'];
490 462
 
491
-        return dispatch(_setAudioOnly(!audioOnly));
463
+        return dispatch(setAudioOnly(!audioOnly));
492 464
     };
493 465
 }

+ 40
- 6
react/features/base/conference/middleware.js Dosyayı Görüntüle

@@ -2,6 +2,7 @@
2 2
 import UIEvents from '../../../../service/UI/UIEvents';
3 3
 
4 4
 import { CONNECTION_ESTABLISHED } from '../connection';
5
+import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../media';
5 6
 import {
6 7
     getLocalParticipant,
7 8
     getParticipantById,
@@ -12,10 +13,16 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
12 13
 
13 14
 import {
14 15
     createConference,
15
-    _setAudioOnlyVideoMuted,
16
+    setAudioOnly,
16 17
     setLastN
17 18
 } from './actions';
18
-import { CONFERENCE_JOINED, SET_AUDIO_ONLY, SET_LASTN } from './actionTypes';
19
+import {
20
+    CONFERENCE_FAILED,
21
+    CONFERENCE_JOINED,
22
+    CONFERENCE_LEFT,
23
+    SET_AUDIO_ONLY,
24
+    SET_LASTN
25
+} from './actionTypes';
19 26
 import {
20 27
     _addLocalTracksToConference,
21 28
     _handleParticipantError,
@@ -36,6 +43,10 @@ MiddlewareRegistry.register(store => next => action => {
36 43
     case CONFERENCE_JOINED:
37 44
         return _conferenceJoined(store, next, action);
38 45
 
46
+    case CONFERENCE_FAILED:
47
+    case CONFERENCE_LEFT:
48
+        return _conferenceFailedOrLeft(store, next, action);
49
+
39 50
     case PIN_PARTICIPANT:
40 51
         return _pinParticipant(store, next, action);
41 52
 
@@ -79,6 +90,29 @@ function _connectionEstablished(store, next, action) {
79 90
     return result;
80 91
 }
81 92
 
93
+/**
94
+ * Does extra sync up on properties that may need to be updated, after
95
+ * the conference failed or was left.
96
+ *
97
+ * @param {Store} store - The Redux store in which the specified action is being
98
+ * dispatched.
99
+ * @param {Dispatch} next - The Redux dispatch function to dispatch the
100
+ * specified action to the specified store.
101
+ * @param {Action} action - The Redux action {@link CONFERENCE_FAILED} or
102
+ * {@link CONFERENCE_LEFT} which is being dispatched in the specified store.
103
+ * @private
104
+ * @returns {Object} The new state that is the result of the reduction of the
105
+ * specified action.
106
+ */
107
+function _conferenceFailedOrLeft(store, next, action) {
108
+    const result = next(action);
109
+    const { audioOnly } = store.getState()['features/base/conference'];
110
+
111
+    audioOnly && store.dispatch(setAudioOnly(false));
112
+
113
+    return result;
114
+}
115
+
82 116
 /**
83 117
  * Does extra sync up on properties that may need to be updated, after
84 118
  * the conference was joined.
@@ -171,17 +205,17 @@ function _pinParticipant(store, next, action) {
171 205
  * @returns {Object} The new state that is the result of the reduction of the
172 206
  * specified action.
173 207
  */
174
-function _setAudioOnly(store, next, action) {
208
+function _setAudioOnly({ dispatch }, next, action) {
175 209
     const result = next(action);
176 210
 
177 211
     const { audioOnly } = action;
178 212
 
179 213
     // Set lastN to 0 in case audio-only is desired; leave it as undefined,
180 214
     // otherwise, and the default lastN value will be chosen automatically.
181
-    store.dispatch(setLastN(audioOnly ? 0 : undefined));
215
+    dispatch(setLastN(audioOnly ? 0 : undefined));
182 216
 
183
-    // Mute local video
184
-    store.dispatch(_setAudioOnlyVideoMuted(audioOnly));
217
+    // Mute the local video.
218
+    dispatch(setVideoMuted(audioOnly, VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY));
185 219
 
186 220
     if (typeof APP !== 'undefined') {
187 221
         // TODO This should be a temporary solution that lasts only until

+ 0
- 23
react/features/base/conference/reducer.js Dosyayı Görüntüle

@@ -11,7 +11,6 @@ import {
11 11
     CONFERENCE_WILL_LEAVE,
12 12
     LOCK_STATE_CHANGED,
13 13
     SET_AUDIO_ONLY,
14
-    _SET_AUDIO_ONLY_VIDEO_MUTED,
15 14
     SET_LARGE_VIDEO_HD_STATUS,
16 15
     SET_PASSWORD,
17 16
     SET_ROOM
@@ -45,9 +44,6 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
45 44
     case SET_AUDIO_ONLY:
46 45
         return _setAudioOnly(state, action);
47 46
 
48
-    case _SET_AUDIO_ONLY_VIDEO_MUTED:
49
-        return _setAudioOnlyVideoMuted(state, action);
50
-
51 47
     case SET_LARGE_VIDEO_HD_STATUS:
52 48
         return _setLargeVideoHDStatus(state, action);
53 49
 
@@ -82,8 +78,6 @@ function _conferenceFailed(state, { conference, error }) {
82 78
             : undefined;
83 79
 
84 80
     return assign(state, {
85
-        audioOnly: undefined,
86
-        audioOnlyVideoMuted: undefined,
87 81
         conference: undefined,
88 82
         joining: undefined,
89 83
         leaving: undefined,
@@ -161,8 +155,6 @@ function _conferenceLeft(state, { conference }) {
161 155
     }
162 156
 
163 157
     return assign(state, {
164
-        audioOnly: undefined,
165
-        audioOnlyVideoMuted: undefined,
166 158
         conference: undefined,
167 159
         joining: undefined,
168 160
         leaving: undefined,
@@ -250,21 +242,6 @@ function _setAudioOnly(state, action) {
250 242
     return set(state, 'audioOnly', action.audioOnly);
251 243
 }
252 244
 
253
-/**
254
- * Reduces a specific Redux action _SET_AUDIO_ONLY_VIDEO_MUTED of the feature
255
- * base/conference.
256
- *
257
- * @param {Object} state - The Redux state of the feature base/conference.
258
- * @param {Action} action - The Redux action SET_AUDIO_ONLY_VIDEO_MUTED to
259
- * reduce.
260
- * @private
261
- * @returns {Object} The new state of the feature base/conference after the
262
- * reduction of the specified action.
263
- */
264
-function _setAudioOnlyVideoMuted(state, action) {
265
-    return set(state, 'audioOnlyVideoMuted', action.muted);
266
-}
267
-
268 245
 /**
269 246
  * Reduces a specific Redux action SET_LARGE_VIDEO_HD_STATUS of the feature
270 247
  * base/conference.

+ 20
- 10
react/features/base/media/actions.js Dosyayı Görüntüle

@@ -10,7 +10,7 @@ import {
10 10
     SET_VIDEO_MUTED,
11 11
     TOGGLE_CAMERA_FACING_MODE
12 12
 } from './actionTypes';
13
-import { CAMERA_FACING_MODE } from './constants';
13
+import { CAMERA_FACING_MODE, VIDEO_MUTISM_AUTHORITY } from './constants';
14 14
 
15 15
 /**
16 16
  * Action to adjust the availability of the local audio.
@@ -84,15 +84,23 @@ export function setVideoAvailable(available: boolean) {
84 84
  *
85 85
  * @param {boolean} muted - True if the local video is to be muted or false if
86 86
  * the local video is to be unmuted.
87
- * @returns {{
88
- *     type: SET_VIDEO_MUTED,
89
- *     muted: boolean
90
- * }}
87
+ * @param {number} authority - The {@link VIDEO_MUTISM_AUTHORITY} which is
88
+ * muting/unmuting the local video.
89
+ * @returns {Function}
91 90
  */
92
-export function setVideoMuted(muted: boolean) {
93
-    return {
94
-        type: SET_VIDEO_MUTED,
95
-        muted
91
+export function setVideoMuted(
92
+        muted: boolean,
93
+        authority: number = VIDEO_MUTISM_AUTHORITY.USER) {
94
+    return (dispatch: Dispatch<*>, getState: Function) => {
95
+        const oldValue = getState()['features/base/media'].video.muted;
96
+
97
+        // eslint-disable-next-line no-bitwise
98
+        const newValue = muted ? oldValue | authority : oldValue & ~authority;
99
+
100
+        return dispatch({
101
+            type: SET_VIDEO_MUTED,
102
+            muted: newValue
103
+        });
96 104
     };
97 105
 }
98 106
 
@@ -135,6 +143,8 @@ export function toggleVideoMuted() {
135 143
     return (dispatch: Dispatch<*>, getState: Function) => {
136 144
         const muted = getState()['features/base/media'].video.muted;
137 145
 
138
-        return dispatch(setVideoMuted(!muted));
146
+        // XXX The following directly invokes the action creator in order to
147
+        // silence Flow.
148
+        return setVideoMuted(!muted)(dispatch, getState);
139 149
     };
140 150
 }

+ 18
- 2
react/features/base/media/constants.js Dosyayı Görüntüle

@@ -9,7 +9,7 @@ export const CAMERA_FACING_MODE = {
9 9
 };
10 10
 
11 11
 /**
12
- * The set of media types for a track.
12
+ * The set of media types.
13 13
  *
14 14
  * @enum {string}
15 15
  */
@@ -18,8 +18,24 @@ export const MEDIA_TYPE = {
18 18
     VIDEO: 'video'
19 19
 };
20 20
 
21
+/* eslint-disable no-bitwise */
22
+
23
+/**
24
+ * The types of authorities which may mute/unmute the local video.
25
+ *
26
+ * @enum {number}
27
+ */
28
+export const VIDEO_MUTISM_AUTHORITY = {
29
+    AUDIO_ONLY: 1 << 0,
30
+    BACKGROUND: 1 << 1,
31
+    USER: 1 << 2
32
+};
33
+
34
+/* eslint-enable no-bitwise */
35
+
21 36
 /**
22
- * The set of video types for a video track.
37
+ * The types of video tracks.
38
+ *
23 39
  * @enum {string}
24 40
  */
25 41
 export const VIDEO_TYPE = {

+ 3
- 2
react/features/base/media/middleware.js Dosyayı Görüntüle

@@ -80,7 +80,8 @@ function _setRoom({ dispatch, getState }, next, action) {
80 80
     (audio.muted !== audioMuted) && dispatch(setAudioMuted(audioMuted));
81 81
     (video.facingMode !== CAMERA_FACING_MODE.USER)
82 82
         && dispatch(setCameraFacingMode(CAMERA_FACING_MODE.USER));
83
-    (video.muted !== videoMuted) && dispatch(setVideoMuted(videoMuted));
83
+    (Boolean(video.muted) !== videoMuted)
84
+        && dispatch(setVideoMuted(videoMuted));
84 85
 
85 86
     return next(action);
86 87
 }
@@ -95,7 +96,7 @@ function _setRoom({ dispatch, getState }, next, action) {
95 96
  */
96 97
 function _syncTrackMutedState({ dispatch, getState }, track) {
97 98
     const state = getState()['features/base/media'];
98
-    const muted = state[track.mediaType].muted;
99
+    const muted = Boolean(state[track.mediaType].muted);
99 100
 
100 101
     // XXX If muted state of track when it was added is different from our media
101 102
     // muted state, we need to mute track and explicitly modify 'muted' property

+ 1
- 1
react/features/base/media/reducer.js Dosyayı Görüntüle

@@ -73,7 +73,7 @@ function _audio(state = AUDIO_INITIAL_MEDIA_STATE, action) {
73 73
 const VIDEO_INITIAL_MEDIA_STATE = {
74 74
     available: true,
75 75
     facingMode: CAMERA_FACING_MODE.USER,
76
-    muted: false
76
+    muted: 0
77 77
 };
78 78
 
79 79
 /**

+ 5
- 0
react/features/base/tracks/actions.js Dosyayı Görüntüle

@@ -358,12 +358,17 @@ function _getLocalTracksToChange(currentTracks, newTracks) {
358 358
  */
359 359
 export function setTrackMuted(track, muted) {
360 360
     return dispatch => {
361
+        muted = Boolean(muted); // eslint-disable-line no-param-reassign
362
+
361 363
         if (track.isMuted() === muted) {
362 364
             return Promise.resolve();
363 365
         }
364 366
 
365 367
         const f = muted ? 'mute' : 'unmute';
366 368
 
369
+        // FIXME: This operation disregards the authority. It is not a problem
370
+        // (on mobile) at the moment, but it will be once we start not creating
371
+        // tracks early. Refactor this then.
367 372
         return track[f]().catch(error => {
368 373
             console.error(`set track ${f} failed`, error);
369 374
 

+ 7
- 1
react/features/base/tracks/middleware.js Dosyayı Görüntüle

@@ -123,9 +123,15 @@ MiddlewareRegistry.register(store => next => action => {
123 123
             } else {
124 124
                 APP.UI.setAudioMuted(participantID, isMuted);
125 125
             }
126
+
127
+            // XXX This function synchronizes track states with media states.
128
+            // This is not required in React, because media is the source of
129
+            // truth, synchronization should always happen in the media -> track
130
+            // direction. The old web, however, does the opposite, hence the
131
+            // need for this.
132
+            return _trackUpdated(store, next, action);
126 133
         }
127 134
 
128
-        return _trackUpdated(store, next, action);
129 135
     }
130 136
 
131 137
     return next(action);

+ 11
- 3
react/features/mobile/audio-mode/middleware.js Dosyayı Görüntüle

@@ -39,12 +39,20 @@ MiddlewareRegistry.register(store => next => action => {
39 39
             break;
40 40
         }
41 41
 
42
-        case SET_AUDIO_ONLY:
43
-            mode
44
-                = action.audioOnly
42
+        case SET_AUDIO_ONLY: {
43
+            const { conference } = store.getState()['features/base/conference'];
44
+
45
+            if (conference) {
46
+                mode
47
+                    = action.audioOnly
45 48
                     ? AudioMode.AUDIO_CALL
46 49
                     : AudioMode.VIDEO_CALL;
50
+            } else {
51
+                mode = null;
52
+            }
53
+
47 54
             break;
55
+        }
48 56
 
49 57
         default:
50 58
             mode = null;

+ 0
- 14
react/features/mobile/background/actionTypes.js Dosyayı Görüntüle

@@ -10,20 +10,6 @@
10 10
  */
11 11
 export const _SET_APP_STATE_LISTENER = Symbol('_SET_APP_STATE_LISTENER');
12 12
 
13
-/**
14
- * The type of redux action which signals that video will be muted because the
15
- * app is going to the background.
16
- *
17
- * {
18
- *     type: _SET_BACKGROUND_VIDEO_MUTED,
19
- *     muted: boolean
20
- * }
21
- *
22
- * @protected
23
- */
24
-export const _SET_BACKGROUND_VIDEO_MUTED
25
-    = Symbol('_SET_BACKGROUND_VIDEO_MUTED');
26
-
27 13
 /**
28 14
  * The type of redux action which signals that the app state has changed (in
29 15
  * terms of execution mode). The app state can be one of 'active', 'inactive',

+ 3
- 31
react/features/mobile/background/actions.js Dosyayı Görüntüle

@@ -1,9 +1,8 @@
1 1
 import { setLastN } from '../../base/conference';
2
-import { setVideoMuted } from '../../base/media';
2
+import { setVideoMuted, VIDEO_MUTISM_AUTHORITY } from '../../base/media';
3 3
 
4 4
 import {
5 5
     _SET_APP_STATE_LISTENER,
6
-    _SET_BACKGROUND_VIDEO_MUTED,
7 6
     APP_STATE_CHANGED
8 7
 } from './actionTypes';
9 8
 
@@ -42,35 +41,8 @@ export function _setBackgroundVideoMuted(muted: boolean) {
42 41
         // for last N will be chosen automatically.
43 42
         const { audioOnly } = getState()['features/base/conference'];
44 43
 
45
-        if (audioOnly) {
46
-            return;
47
-        }
48
-
49
-        dispatch(setLastN(muted ? 0 : undefined));
50
-
51
-        if (muted) {
52
-            const { video } = getState()['features/base/media'];
53
-
54
-            if (video.muted) {
55
-                // Video is already muted, do nothing.
56
-                return;
57
-            }
58
-        } else {
59
-            const { videoMuted } = getState()['features/background'];
60
-
61
-            if (!videoMuted) {
62
-                // We didn't mute video, do nothing.
63
-                return;
64
-            }
65
-        }
66
-
67
-        // Remember that local video was muted due to the app going to the
68
-        // background vs user's choice.
69
-        dispatch({
70
-            type: _SET_BACKGROUND_VIDEO_MUTED,
71
-            muted
72
-        });
73
-        dispatch(setVideoMuted(muted));
44
+        !audioOnly && dispatch(setLastN(muted ? 0 : undefined));
45
+        dispatch(setVideoMuted(muted, VIDEO_MUTISM_AUTHORITY.BACKGROUND));
74 46
     };
75 47
 }
76 48
 

+ 0
- 7
react/features/mobile/background/reducer.js Dosyayı Görüntüle

@@ -2,7 +2,6 @@ import { ReducerRegistry } from '../../base/redux';
2 2
 
3 3
 import {
4 4
     _SET_APP_STATE_LISTENER,
5
-    _SET_BACKGROUND_VIDEO_MUTED,
6 5
     APP_STATE_CHANGED
7 6
 } from './actionTypes';
8 7
 
@@ -14,12 +13,6 @@ ReducerRegistry.register('features/background', (state = {}, action) => {
14 13
             appStateListener: action.listener
15 14
         };
16 15
 
17
-    case _SET_BACKGROUND_VIDEO_MUTED:
18
-        return {
19
-            ...state,
20
-            videoMuted: action.muted
21
-        };
22
-
23 16
     case APP_STATE_CHANGED:
24 17
         return {
25 18
             ...state,

+ 5
- 2
react/features/mobile/full-screen/middleware.js Dosyayı Görüntüle

@@ -61,10 +61,13 @@ MiddlewareRegistry.register(store => next => action => {
61 61
         break;
62 62
     }
63 63
 
64
-    case SET_AUDIO_ONLY:
65
-        fullScreen = !action.audioOnly;
64
+    case SET_AUDIO_ONLY: {
65
+        const { conference } = store.getState()['features/base/conference'];
66
+
67
+        fullScreen = conference ? !action.audioOnly : false;
66 68
         break;
67 69
     }
70
+    }
68 71
 
69 72
     if (fullScreen !== null) {
70 73
         _setFullScreen(fullScreen)

+ 5
- 2
react/features/mobile/proximity/middleware.js Dosyayı Görüntüle

@@ -31,10 +31,13 @@ MiddlewareRegistry.register(store => next => action => {
31 31
         _setProximityEnabled(false);
32 32
         break;
33 33
 
34
-    case SET_AUDIO_ONLY:
35
-        _setProximityEnabled(action.audioOnly);
34
+    case SET_AUDIO_ONLY: {
35
+        const { conference } = store.getState()['features/base/conference'];
36
+
37
+        conference && _setProximityEnabled(action.audioOnly);
36 38
         break;
37 39
     }
40
+    }
38 41
 
39 42
     return next(action);
40 43
 });

Loading…
İptal
Kaydet