"Fossies" - the Fresh Open Source Software Archive 
Member "jitsi-meet-7316/react/features/recording/components/Recording/LocalRecordingManager.web.ts" (5 Jun 2023, 10523 Bytes) of package /linux/misc/jitsi-meet-7316.tar.gz:
As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style:
standard) with prefixed line numbers and
code folding option.
Alternatively you can here
view or
download the uninterpreted source code file.
1 import i18next from 'i18next';
2 import { v4 as uuidV4 } from 'uuid';
3 import fixWebmDuration from 'webm-duration-fix';
4
5 import { IStore } from '../../../app/types';
6 import { getRoomName } from '../../../base/conference/functions';
7 import { MEDIA_TYPE } from '../../../base/media/constants';
8 import { getLocalTrack, getTrackState } from '../../../base/tracks/functions';
9 import { inIframe } from '../../../base/util/iframeUtils';
10 import { stopLocalVideoRecording } from '../../actions.any';
11
12 interface ISelfRecording {
13 on: boolean;
14 withVideo: boolean;
15 }
16
17 interface ILocalRecordingManager {
18 addAudioTrackToLocalRecording: (track: MediaStreamTrack) => void;
19 audioContext: AudioContext | undefined;
20 audioDestination: MediaStreamAudioDestinationNode | undefined;
21 getFilename: () => string;
22 initializeAudioMixer: () => void;
23 isRecordingLocally: () => boolean;
24 mediaType: string;
25 mixAudioStream: (stream: MediaStream) => void;
26 recorder: MediaRecorder | undefined;
27 recordingData: Blob[];
28 roomName: string;
29 saveRecording: (recordingData: Blob[], filename: string) => void;
30 selfRecording: ISelfRecording;
31 startLocalRecording: (store: IStore, onlySelf: boolean) => void;
32 stopLocalRecording: () => void;
33 stream: MediaStream | undefined;
34 totalSize: number;
35 }
36
37 const getMimeType = (): string => {
38 const possibleTypes = [
39 'video/mp4;codecs=h264',
40 'video/webm;codecs=h264',
41 'video/webm;codecs=vp9',
42 'video/webm;codecs=vp8'
43 ];
44
45 for (const type of possibleTypes) {
46 if (MediaRecorder.isTypeSupported(type)) {
47 return type;
48 }
49 }
50 throw new Error('No MIME Type supported by MediaRecorder');
51 };
52
53 const VIDEO_BIT_RATE = 2500000; // 2.5Mbps in bits
54 const MAX_SIZE = 1073741824; // 1GB in bytes
55
56 // Lazily initialize.
57 let preferredMediaType: string;
58
59 const LocalRecordingManager: ILocalRecordingManager = {
60 recordingData: [],
61 recorder: undefined,
62 stream: undefined,
63 audioContext: undefined,
64 audioDestination: undefined,
65 roomName: '',
66 totalSize: MAX_SIZE,
67 selfRecording: {
68 on: false,
69 withVideo: false
70 },
71
72 get mediaType() {
73 if (!preferredMediaType) {
74 preferredMediaType = getMimeType();
75 }
76
77 return preferredMediaType;
78 },
79
80 /**
81 * Initializes audio context used for mixing audio tracks.
82 *
83 * @returns {void}
84 */
85 initializeAudioMixer() {
86 this.audioContext = new AudioContext();
87 this.audioDestination = this.audioContext.createMediaStreamDestination();
88 },
89
90 /**
91 * Mixes multiple audio tracks to the destination media stream.
92 *
93 * @param {MediaStream} stream - The stream to mix.
94 * @returns {void}
95 * */
96 mixAudioStream(stream) {
97 if (stream.getAudioTracks().length > 0 && this.audioDestination) {
98 this.audioContext?.createMediaStreamSource(stream).connect(this.audioDestination);
99 }
100 },
101
102 /**
103 * Adds audio track to the recording stream.
104 *
105 * @param {MediaStreamTrack} track - The track to be added.
106 * @returns {void}
107 */
108 addAudioTrackToLocalRecording(track) {
109 if (this.selfRecording.on) {
110 return;
111 }
112 if (track) {
113 const stream = new MediaStream([ track ]);
114
115 this.mixAudioStream(stream);
116 }
117 },
118
119 /**
120 * Returns a filename based ono the Jitsi room name in the URL and timestamp.
121 *
122 * @returns {string}
123 * */
124 getFilename() {
125 const now = new Date();
126 const timestamp = now.toISOString();
127
128 return `${this.roomName}_${timestamp}`;
129 },
130
131 /**
132 * Saves local recording to file.
133 *
134 * @param {Array} recordingData - The recording data.
135 * @param {string} filename - The name of the file.
136 * @returns {void}
137 * */
138 async saveRecording(recordingData, filename) {
139 // @ts-ignore
140 const blob = await fixWebmDuration(new Blob(recordingData, { type: this.mediaType }));
141 const url = URL.createObjectURL(blob);
142 const a = document.createElement('a');
143
144 const extension = this.mediaType.slice(this.mediaType.indexOf('/') + 1, this.mediaType.indexOf(';'));
145
146 a.style.display = 'none';
147 a.href = url;
148 a.download = `${filename}.${extension}`;
149 a.click();
150 },
151
152 /**
153 * Stops local recording.
154 *
155 * @returns {void}
156 * */
157 stopLocalRecording() {
158 if (this.recorder) {
159 this.recorder.stop();
160 this.recorder = undefined;
161 this.audioContext = undefined;
162 this.audioDestination = undefined;
163 this.totalSize = MAX_SIZE;
164 setTimeout(() => this.saveRecording(this.recordingData, this.getFilename()), 1000);
165 }
166 },
167
168 /**
169 * Starts a local recording.
170 *
171 * @param {IStore} store - The redux store.
172 * @param {boolean} onlySelf - Whether to record only self streams.
173 * @returns {void}
174 */
175 async startLocalRecording(store, onlySelf) {
176 const { dispatch, getState } = store;
177
178 // @ts-ignore
179 const supportsCaptureHandle = Boolean(navigator.mediaDevices.setCaptureHandleConfig) && !inIframe();
180 const tabId = uuidV4();
181
182 this.selfRecording.on = onlySelf;
183 this.recordingData = [];
184 this.roomName = getRoomName(getState()) ?? '';
185 let gdmStream: MediaStream = new MediaStream();
186 const tracks = getTrackState(getState());
187
188 if (onlySelf) {
189 let audioTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
190 let videoTrack: MediaStreamTrack | undefined = getLocalTrack(tracks, MEDIA_TYPE.VIDEO)?.jitsiTrack?.track;
191
192 if (!audioTrack) {
193 APP.conference.muteAudio(false);
194 setTimeout(() => APP.conference.muteAudio(true), 100);
195 await new Promise(resolve => {
196 setTimeout(resolve, 100);
197 });
198 }
199 if (videoTrack && videoTrack.readyState !== 'live') {
200 videoTrack = undefined;
201 }
202 audioTrack = getLocalTrack(getTrackState(getState()), MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
203 if (!audioTrack && !videoTrack) {
204 throw new Error('NoLocalStreams');
205 }
206 this.selfRecording.withVideo = Boolean(videoTrack);
207 const localTracks = [];
208
209 audioTrack && localTracks.push(audioTrack);
210 videoTrack && localTracks.push(videoTrack);
211 this.stream = new MediaStream(localTracks);
212 } else {
213 if (supportsCaptureHandle) {
214 // @ts-ignore
215 navigator.mediaDevices.setCaptureHandleConfig({
216 handle: `JitsiMeet-${tabId}`,
217 permittedOrigins: [ '*' ]
218 });
219 }
220 const localAudioTrack = getLocalTrack(tracks, MEDIA_TYPE.AUDIO)?.jitsiTrack?.track;
221
222 // Starting chrome 107, the recorder does not record any data if the audio stream has no tracks
223 // To fix this we create a track for the local user(muted track)
224 if (!localAudioTrack) {
225 APP.conference.muteAudio(false);
226 setTimeout(() => APP.conference.muteAudio(true), 100);
227 await new Promise(resolve => {
228 setTimeout(resolve, 100);
229 });
230 }
231
232 // handle no mic permission
233 if (!getLocalTrack(getTrackState(getState()), MEDIA_TYPE.AUDIO)?.jitsiTrack?.track) {
234 throw new Error('NoMicTrack');
235 }
236
237 const currentTitle = document.title;
238
239 document.title = i18next.t('localRecording.selectTabTitle');
240
241 // @ts-ignore
242 gdmStream = await navigator.mediaDevices.getDisplayMedia({
243 video: { displaySurface: 'browser',
244 frameRate: 30 },
245 audio: false, // @ts-ignore
246 preferCurrentTab: true
247 });
248 document.title = currentTitle;
249
250 const isBrowser = gdmStream.getVideoTracks()[0].getSettings().displaySurface === 'browser';
251
252 if (!isBrowser || (supportsCaptureHandle // @ts-ignore
253 && gdmStream.getVideoTracks()[0].getCaptureHandle()?.handle !== `JitsiMeet-${tabId}`)) {
254 gdmStream.getTracks().forEach((track: MediaStreamTrack) => track.stop());
255 throw new Error('WrongSurfaceSelected');
256 }
257
258 this.initializeAudioMixer();
259
260 const allTracks = getTrackState(getState());
261
262 allTracks.forEach((track: any) => {
263 if (track.mediaType === MEDIA_TYPE.AUDIO) {
264 const audioTrack = track?.jitsiTrack?.track;
265
266 this.addAudioTrackToLocalRecording(audioTrack);
267 }
268 });
269 this.stream = new MediaStream([
270 ...this.audioDestination?.stream.getAudioTracks() || [],
271 gdmStream.getVideoTracks()[0]
272 ]);
273 }
274
275 this.recorder = new MediaRecorder(this.stream, {
276 mimeType: this.mediaType,
277 videoBitsPerSecond: VIDEO_BIT_RATE
278 });
279 this.recorder.addEventListener('dataavailable', e => {
280 if (e.data && e.data.size > 0) {
281 this.recordingData.push(e.data);
282 this.totalSize -= e.data.size;
283 if (this.totalSize <= 0) {
284 dispatch(stopLocalVideoRecording());
285 }
286 }
287 });
288
289 if (!onlySelf) {
290 this.recorder.addEventListener('stop', () => {
291 this.stream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
292 gdmStream?.getTracks().forEach((track: MediaStreamTrack) => track.stop());
293 });
294
295 gdmStream?.addEventListener('inactive', () => {
296 dispatch(stopLocalVideoRecording());
297 });
298
299 this.stream.addEventListener('inactive', () => {
300 dispatch(stopLocalVideoRecording());
301 });
302 }
303
304 this.recorder.start(5000);
305 },
306
307 /**
308 * Whether or not we're currently recording locally.
309 *
310 * @returns {boolean}
311 */
312 isRecordingLocally() {
313 return Boolean(this.recorder);
314 }
315
316 };
317
318 export default LocalRecordingManager;