Procházet zdrojové kódy

feat(android/ios): start/stop recording events for native (#15598)

Added native android and ios events for start and stop recording.
factor2
Calinteodor před 4 měsíci
rodič
revize
ef138fb5aa
Žádný účet není propojen s e-mailovou adresou tvůrce revize

+ 3
- 1
android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastAction.java Zobrazit soubor

@@ -80,7 +80,9 @@ public class BroadcastAction {
80 80
         SET_CLOSED_CAPTIONS_ENABLED("org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED"),
81 81
         TOGGLE_CAMERA("org.jitsi.meet.TOGGLE_CAMERA"),
82 82
         SHOW_NOTIFICATION("org.jitsi.meet.SHOW_NOTIFICATION"),
83
-        HIDE_NOTIFICATION("org.jitsi.meet.HIDE_NOTIFICATION");
83
+        HIDE_NOTIFICATION("org.jitsi.meet.HIDE_NOTIFICATION"),
84
+        START_RECORDING("org.jitsi.meet.START_RECORDING"),
85
+        STOP_RECORDING("org.jitsi.meet.STOP_RECORDING");
84 86
 
85 87
         private final String action;
86 88
 

+ 58
- 5
android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastIntentHelper.java Zobrazit soubor

@@ -1,16 +1,13 @@
1 1
 package org.jitsi.meet.sdk;
2 2
 
3 3
 import android.content.Intent;
4
-
5
-import org.jitsi.meet.sdk.log.JitsiMeetLogger;
6
-
7
-import java.util.Arrays;
8
-import java.util.List;
4
+import android.os.Bundle;
9 5
 
10 6
 public class BroadcastIntentHelper {
11 7
     public static Intent buildSetAudioMutedIntent(boolean muted) {
12 8
         Intent intent = new Intent(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
13 9
         intent.putExtra("muted", muted);
10
+
14 11
         return intent;
15 12
     }
16 13
 
@@ -22,18 +19,21 @@ public class BroadcastIntentHelper {
22 19
         Intent intent = new Intent(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
23 20
         intent.putExtra("to", to);
24 21
         intent.putExtra("message", message);
22
+
25 23
         return intent;
26 24
     }
27 25
 
28 26
     public static Intent buildToggleScreenShareIntent(boolean enabled) {
29 27
         Intent intent = new Intent(BroadcastAction.Type.TOGGLE_SCREEN_SHARE.getAction());
30 28
         intent.putExtra("enabled", enabled);
29
+
31 30
         return intent;
32 31
     }
33 32
 
34 33
     public static Intent buildOpenChatIntent(String participantId) {
35 34
         Intent intent = new Intent(BroadcastAction.Type.OPEN_CHAT.getAction());
36 35
         intent.putExtra("to", participantId);
36
+
37 37
         return intent;
38 38
     }
39 39
 
@@ -45,24 +45,28 @@ public class BroadcastIntentHelper {
45 45
         Intent intent = new Intent(BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
46 46
         intent.putExtra("to", participantId);
47 47
         intent.putExtra("message", message);
48
+
48 49
         return intent;
49 50
     }
50 51
 
51 52
     public static Intent buildSetVideoMutedIntent(boolean muted) {
52 53
         Intent intent = new Intent(BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
53 54
         intent.putExtra("muted", muted);
55
+
54 56
         return intent;
55 57
     }
56 58
 
57 59
     public static Intent buildSetClosedCaptionsEnabledIntent(boolean enabled) {
58 60
         Intent intent = new Intent(BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
59 61
         intent.putExtra("enabled", enabled);
62
+
60 63
         return intent;
61 64
     }
62 65
 
63 66
     public static Intent buildRetrieveParticipantsInfo(String requestId) {
64 67
         Intent intent = new Intent(BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction());
65 68
         intent.putExtra("requestId", requestId);
69
+
66 70
         return intent;
67 71
     }
68 72
 
@@ -78,12 +82,61 @@ public class BroadcastIntentHelper {
78 82
         intent.putExtra("timeout", timeout);
79 83
         intent.putExtra("title", title);
80 84
         intent.putExtra("uid", uid);
85
+
81 86
         return intent;
82 87
     }
83 88
 
84 89
     public static Intent buildHideNotificationIntent(String uid) {
85 90
         Intent intent = new Intent(BroadcastAction.Type.HIDE_NOTIFICATION.getAction());
86 91
         intent.putExtra("uid", uid);
92
+
93
+        return intent;
94
+    }
95
+
96
+    public enum RecordingMode {
97
+        FILE("file"),
98
+        STREAM("stream");
99
+
100
+        private final String mode;
101
+
102
+        RecordingMode(String mode) {
103
+            this.mode = mode;
104
+        }
105
+
106
+        public String getMode() {
107
+            return mode;
108
+        }
109
+    }
110
+
111
+    public static Intent buildStartRecordingIntent(
112
+        RecordingMode mode,
113
+        String dropboxToken,
114
+        boolean shouldShare,
115
+        String rtmpStreamKey,
116
+        String rtmpBroadcastID,
117
+        String youtubeStreamKey,
118
+        String youtubeBroadcastID,
119
+        Bundle extraMetadata,
120
+        boolean transcription) {
121
+        Intent intent = new Intent(BroadcastAction.Type.START_RECORDING.getAction());
122
+        intent.putExtra("mode", mode.getMode());
123
+        intent.putExtra("dropboxToken", dropboxToken);
124
+        intent.putExtra("shouldShare", shouldShare);
125
+        intent.putExtra("rtmpStreamKey", rtmpStreamKey);
126
+        intent.putExtra("rtmpBroadcastID", rtmpBroadcastID);
127
+        intent.putExtra("youtubeStreamKey", youtubeStreamKey);
128
+        intent.putExtra("youtubeBroadcastID", youtubeBroadcastID);
129
+        intent.putExtra("extraMetadata", extraMetadata);
130
+        intent.putExtra("transcription", transcription);
131
+
132
+        return intent;
133
+    }
134
+
135
+    public static Intent buildStopRecordingIntent(RecordingMode mode, boolean transcription) {
136
+        Intent intent = new Intent(BroadcastAction.Type.STOP_RECORDING.getAction());
137
+        intent.putExtra("mode", mode.getMode());
138
+        intent.putExtra("transcription", transcription);
139
+
87 140
         return intent;
88 141
     }
89 142
 }

+ 2
- 0
android/sdk/src/main/java/org/jitsi/meet/sdk/ExternalAPIModule.java Zobrazit soubor

@@ -99,6 +99,8 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
99 99
         constants.put("TOGGLE_CAMERA", BroadcastAction.Type.TOGGLE_CAMERA.getAction());
100 100
         constants.put("SHOW_NOTIFICATION", BroadcastAction.Type.SHOW_NOTIFICATION.getAction());
101 101
         constants.put("HIDE_NOTIFICATION", BroadcastAction.Type.HIDE_NOTIFICATION.getAction());
102
+        constants.put("START_RECORDING", BroadcastAction.Type.START_RECORDING.getAction());
103
+        constants.put("STOP_RECORDING", BroadcastAction.Type.STOP_RECORDING.getAction());
102 104
 
103 105
         return constants;
104 106
     }

+ 7
- 0
ios/sdk/src/ExternalAPI.h Zobrazit soubor

@@ -18,6 +18,11 @@
18 18
 
19 19
 static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
20 20
 
21
+typedef NS_ENUM(NSInteger, RecordingMode) {
22
+    RecordingModeFile,
23
+    RecordingModeStream
24
+};
25
+
21 26
 @interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
22 27
 
23 28
 - (void)sendHangUp;
@@ -33,5 +38,7 @@ static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
33 38
 - (void)toggleCamera;
34 39
 - (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid;
35 40
 - (void)hideNotification:(NSString*)uid;
41
+- (void)startRecording:(RecordingMode)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription;
42
+- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription;
36 43
 
37 44
 @end

+ 47
- 4
ios/sdk/src/ExternalAPI.m Zobrazit soubor

@@ -30,6 +30,8 @@ static NSString * const setClosedCaptionsEnabledAction = @"org.jitsi.meet.SET_CL
30 30
 static NSString * const toggleCameraAction = @"org.jitsi.meet.TOGGLE_CAMERA";
31 31
 static NSString * const showNotificationAction = @"org.jitsi.meet.SHOW_NOTIFICATION";
32 32
 static NSString * const hideNotificationAction = @"org.jitsi.meet.HIDE_NOTIFICATION";
33
+static NSString * const startRecordingAction = @"org.jitsi.meet.START_RECORDING";
34
+static NSString * const stopRecordingAction = @"org.jitsi.meet.STOP_RECORDING";
33 35
 
34 36
 @implementation ExternalAPI
35 37
 
@@ -56,7 +58,9 @@ RCT_EXPORT_MODULE();
56 58
         @"SET_CLOSED_CAPTIONS_ENABLED": setClosedCaptionsEnabledAction,
57 59
         @"TOGGLE_CAMERA": toggleCameraAction,
58 60
         @"SHOW_NOTIFICATION": showNotificationAction,
59
-        @"HIDE_NOTIFICATION": hideNotificationAction
61
+        @"HIDE_NOTIFICATION": hideNotificationAction,
62
+        @"START_RECORDING": startRecordingAction,
63
+        @"STOP_RECORDING": stopRecordingAction
60 64
     };
61 65
 };
62 66
 
@@ -84,7 +88,9 @@ RCT_EXPORT_MODULE();
84 88
               setClosedCaptionsEnabledAction,
85 89
               toggleCameraAction,
86 90
               showNotificationAction,
87
-              hideNotificationAction
91
+              hideNotificationAction,
92
+              startRecordingAction,
93
+              stopRecordingAction
88 94
     ];
89 95
 }
90 96
 
@@ -186,7 +192,7 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
186 192
     [self sendEventWithName:toggleCameraAction body:nil];
187 193
 }
188 194
 
189
-- (void)showNotification:(NSString *)appearance :(NSString *)description :(NSString *)timeout :(NSString *)title :(NSString *)uid {
195
+- (void)showNotification:(NSString*)appearance :(NSString*)description :(NSString*)timeout :(NSString*)title :(NSString*)uid {
190 196
     NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
191 197
     data[@"appearance"] = appearance;
192 198
     data[@"description"] = description;
@@ -197,11 +203,48 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
197 203
     [self sendEventWithName:showNotificationAction body:data];
198 204
 }
199 205
 
200
-- (void)hideNotification:(NSString *)uid {
206
+- (void)hideNotification:(NSString*)uid {
201 207
     NSMutableDictionary *data = [[NSMutableDictionary alloc] init];
202 208
     data[@"uid"] = uid;
203 209
     
204 210
     [self sendEventWithName:hideNotificationAction body:data];
205 211
 }
206 212
 
213
+static inline NSString *RecordingModeToString(RecordingMode mode) {
214
+    switch (mode) {
215
+        case RecordingModeFile:
216
+            return @"file";
217
+        case RecordingModeStream:
218
+            return @"stream";
219
+        default:
220
+            return nil;
221
+    }
222
+}
223
+
224
+- (void)startRecording:(RecordingMode)mode :(NSString*)dropboxToken :(BOOL)shouldShare :(NSString*)rtmpStreamKey :(NSString*)rtmpBroadcastID :(NSString*)youtubeStreamKey :(NSString*)youtubeBroadcastID :(NSDictionary*)extraMetadata :(BOOL)transcription {
225
+    NSString *modeString = RecordingModeToString(mode);
226
+    NSDictionary *data = @{
227
+        @"mode": modeString,
228
+        @"dropboxToken": dropboxToken,
229
+        @"shouldShare": @(shouldShare),
230
+        @"rtmpStreamKey": rtmpStreamKey,
231
+        @"rtmpBroadcastID": rtmpBroadcastID,
232
+        @"youtubeStreamKey": youtubeStreamKey,
233
+        @"youtubeBroadcastID": youtubeBroadcastID,
234
+        @"extraMetadata": extraMetadata,
235
+        @"transcription": @(transcription)
236
+    };
237
+    
238
+    [self sendEventWithName:startRecordingAction body:data];
239
+}
240
+
241
+- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
242
+    NSString *modeString = RecordingModeToString(mode);
243
+    NSDictionary *data = @{
244
+        @"mode": modeString,
245
+        @"transcription": @(transcription)
246
+    };
247
+    
248
+    [self sendEventWithName:stopRecordingAction body:data];
249
+}
207 250
 @end

+ 4
- 0
ios/sdk/src/JitsiMeetView.h Zobrazit soubor

@@ -21,6 +21,8 @@
21 21
 #import "JitsiMeetConferenceOptions.h"
22 22
 #import "JitsiMeetViewDelegate.h"
23 23
 
24
+typedef NS_ENUM(NSInteger, RecordingMode);
25
+
24 26
 @interface JitsiMeetView : UIView
25 27
 
26 28
 @property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@@ -49,5 +51,7 @@
49 51
 - (void)toggleCamera;
50 52
 - (void)showNotification:(NSString * _Nonnull)appearance :(NSString * _Nullable)description :(NSString * _Nullable)timeout :(NSString * _Nullable)title :(NSString * _Nullable)uid;
51 53
 - (void)hideNotification:(NSString * _Nullable)uid;
54
+- (void)startRecording:(RecordingMode)mode :(NSString * _Nullable)dropboxToken :(BOOL)shouldShare :(NSString * _Nullable)rtmpStreamKey :(NSString * _Nullable)rtmpBroadcastID :(NSString * _Nullable)youtubeStreamKey :(NSString * _Nullable)youtubeBroadcastID :(NSString * _Nullable)extraMetadata :(BOOL)transcription;
55
+- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription;
52 56
 
53 57
 @end

+ 12
- 0
ios/sdk/src/JitsiMeetView.m Zobrazit soubor

@@ -142,15 +142,27 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
142 142
     ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
143 143
     [externalAPI toggleCamera];
144 144
 }
145
+
145 146
 - (void)showNotification:(NSString *)appearance :(NSString *)description :(NSString *)timeout :(NSString *)title :(NSString *)uid {
146 147
     ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
147 148
     [externalAPI showNotification:appearance :description :timeout :title :uid];
148 149
 }
150
+
149 151
 -(void)hideNotification:(NSString *)uid {
150 152
     ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
151 153
     [externalAPI hideNotification:uid];
152 154
 }
153 155
 
156
+- (void)startRecording:(RecordingMode)mode :(NSString *)dropboxToken :(BOOL)shouldShare :(NSString *)rtmpStreamKey :(NSString *)rtmpBroadcastID :(NSString *)youtubeStreamKey :(NSString *)youtubeBroadcastID :(NSString *)extraMetadata :(BOOL)transcription {
157
+    ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
158
+    [externalAPI startRecording:mode :dropboxToken :shouldShare :rtmpStreamKey :rtmpBroadcastID :youtubeStreamKey :youtubeBroadcastID :extraMetadata :transcription];
159
+}
160
+
161
+- (void)stopRecording:(RecordingMode)mode :(BOOL)transcription {
162
+    ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
163
+    [externalAPI stopRecording:mode :transcription];
164
+}       
165
+
154 166
 #pragma mark Private methods
155 167
 
156 168
 - (void)registerObservers {

+ 130
- 3
react/features/mobile/external-api/middleware.ts Zobrazit soubor

@@ -4,7 +4,7 @@ import { debounce } from 'lodash-es';
4 4
 import { NativeEventEmitter, NativeModules } from 'react-native';
5 5
 import { AnyAction } from 'redux';
6 6
 
7
-// @ts-expect-error
7
+// @ts-ignore
8 8
 import { ENDPOINT_TEXT_MESSAGE_NAME } from '../../../../modules/API/constants';
9 9
 import { appNavigate } from '../../app/actions.native';
10 10
 import { IStore } from '../../app/types';
@@ -32,8 +32,7 @@ import {
32 32
     JITSI_CONNECTION_URL_KEY
33 33
 } from '../../base/connection/constants';
34 34
 import { getURLWithoutParams } from '../../base/connection/utils';
35
-import {
36
-    JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
35
+import { JitsiConferenceEvents, JitsiRecordingConstants } from '../../base/lib-jitsi-meet';
37 36
 import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
38 37
 import { toggleCameraFacingMode } from '../../base/media/actions';
39 38
 import { MEDIA_TYPE, VIDEO_TYPE } from '../../base/media/constants';
@@ -51,8 +50,11 @@ import { getLocalTracks, isLocalTrackMuted } from '../../base/tracks/functions.n
51 50
 import { ITrack } from '../../base/tracks/types';
52 51
 import { CLOSE_CHAT, OPEN_CHAT } from '../../chat/actionTypes';
53 52
 import { closeChat, openChat, sendMessage, setPrivateMessageRecipient } from '../../chat/actions.native';
53
+import { isEnabled as isDropboxEnabled } from '../../dropbox/functions.native';
54 54
 import { hideNotification, showNotification } from '../../notifications/actions';
55 55
 import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../../notifications/constants';
56
+import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../recording/constants';
57
+import { getActiveSession } from '../../recording/functions';
56 58
 import { setRequestingSubtitles } from '../../subtitles/actions.any';
57 59
 import { CUSTOM_BUTTON_PRESSED } from '../../toolbox/actionTypes';
58 60
 import { muteLocal } from '../../video-menu/actions.native';
@@ -450,6 +452,129 @@ function _registerForNativeEvents(store: IStore) {
450 452
     eventEmitter.addListener(ExternalAPI.HIDE_NOTIFICATION, ({ uid }: any) => {
451 453
         dispatch(hideNotification(uid));
452 454
     });
455
+
456
+    eventEmitter.addListener(ExternalAPI.START_RECORDING, (
457
+            {
458
+                mode,
459
+                dropboxToken,
460
+                shouldShare,
461
+                rtmpStreamKey,
462
+                rtmpBroadcastID,
463
+                youtubeStreamKey,
464
+                youtubeBroadcastID,
465
+                extraMetadata = {},
466
+                transcription
467
+            }: any) => {
468
+        const state = store.getState();
469
+        const conference = getCurrentConference(state);
470
+
471
+        if (!conference) {
472
+            logger.error('Conference is not defined');
473
+
474
+            return;
475
+        }
476
+
477
+        if (dropboxToken && !isDropboxEnabled(state)) {
478
+            logger.error('Failed starting recording: dropbox is not enabled on this deployment');
479
+
480
+            return;
481
+        }
482
+
483
+        if (mode === JitsiRecordingConstants.mode.STREAM && !(youtubeStreamKey || rtmpStreamKey)) {
484
+            logger.error('Failed starting recording: missing youtube or RTMP stream key');
485
+
486
+            return;
487
+        }
488
+
489
+        let recordingConfig;
490
+
491
+        if (mode === JitsiRecordingConstants.mode.FILE) {
492
+            const { recordingService } = state['features/base/config'];
493
+
494
+            if (!recordingService?.enabled && !dropboxToken) {
495
+                logger.error('Failed starting recording: the recording service is not enabled');
496
+
497
+                return;
498
+            }
499
+
500
+            if (dropboxToken) {
501
+                recordingConfig = {
502
+                    mode: JitsiRecordingConstants.mode.FILE,
503
+                    appData: JSON.stringify({
504
+                        'file_recording_metadata': {
505
+                            ...extraMetadata,
506
+                            'upload_credentials': {
507
+                                'service_name': RECORDING_TYPES.DROPBOX,
508
+                                'token': dropboxToken
509
+                            }
510
+                        }
511
+                    })
512
+                };
513
+            } else {
514
+                recordingConfig = {
515
+                    mode: JitsiRecordingConstants.mode.FILE,
516
+                    appData: JSON.stringify({
517
+                        'file_recording_metadata': {
518
+                            ...extraMetadata,
519
+                            'share': shouldShare
520
+                        }
521
+                    })
522
+                };
523
+            }
524
+        } else if (mode === JitsiRecordingConstants.mode.STREAM) {
525
+            recordingConfig = {
526
+                broadcastId: youtubeBroadcastID || rtmpBroadcastID,
527
+                mode: JitsiRecordingConstants.mode.STREAM,
528
+                streamId: youtubeStreamKey || rtmpStreamKey
529
+            };
530
+        }
531
+
532
+        // Start audio / video recording, if requested.
533
+        if (typeof recordingConfig !== 'undefined') {
534
+            conference.startRecording(recordingConfig);
535
+        }
536
+
537
+        if (transcription) {
538
+            store.dispatch(setRequestingSubtitles(true, false, null));
539
+            conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
540
+                isTranscribingEnabled: true
541
+            });
542
+        }
543
+    });
544
+
545
+    eventEmitter.addListener(ExternalAPI.STOP_RECORDING, ({ mode, transcription }: any) => {
546
+        const state = store.getState();
547
+        const conference = getCurrentConference(state);
548
+
549
+        if (!conference) {
550
+            logger.error('Conference is not defined');
551
+
552
+            return;
553
+        }
554
+
555
+        if (transcription) {
556
+            store.dispatch(setRequestingSubtitles(false, false, null));
557
+            conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
558
+                isTranscribingEnabled: false
559
+            });
560
+        }
561
+
562
+        if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
563
+            logger.error('Invalid recording mode provided!');
564
+
565
+            return;
566
+        }
567
+
568
+        const activeSession = getActiveSession(state, mode);
569
+
570
+        if (!activeSession?.id) {
571
+            logger.error('No recording or streaming session found');
572
+
573
+            return;
574
+        }
575
+
576
+        conference.stopRecording(activeSession.id);
577
+    });
453 578
 }
454 579
 
455 580
 /**
@@ -472,6 +597,8 @@ function _unregisterForNativeEvents() {
472 597
     eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_CAMERA);
473 598
     eventEmitter.removeAllListeners(ExternalAPI.SHOW_NOTIFICATION);
474 599
     eventEmitter.removeAllListeners(ExternalAPI.HIDE_NOTIFICATION);
600
+    eventEmitter.removeAllListeners(ExternalAPI.START_RECORDING);
601
+    eventEmitter.removeAllListeners(ExternalAPI.STOP_RECORDING);
475 602
 }
476 603
 
477 604
 /**

Načítá se…
Zrušit
Uložit