"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;