Skip to content

Commit

Permalink
Add recording support back, now supports all browsers
Browse files Browse the repository at this point in the history
  • Loading branch information
havfo committed Mar 18, 2024
1 parent 56665b1 commit 7c0d8ab
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 98 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@feathersjs/rest-client": "^5.0.11",
"@mui/icons-material": "^5.14.16",
"@mui/material": "^5.14.16",
"@observertc/client-monitor-js": "^3.5.0",
"@reduxjs/toolkit": "^1.9.7",
"awaitqueue": "^3.0.2",
"bowser": "^2.11.0",
Expand All @@ -25,6 +26,7 @@
"hark": "^1.2.3",
"marked": "^9.1.4",
"mediasoup-client": "^3.7.3",
"native-file-system-adapter": "^3.0.1",
"notistack": "^3.0.1",
"ortc-p2p": "havfo/ortc-p2p#0.1.1",
"random-string": "^0.2.0",
Expand Down
8 changes: 3 additions & 5 deletions src/components/controlbuttons/RecordButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,20 @@ import {
stopRecordingLabel
} from '../translated/translatedComponents';
import { permissions } from '../../utils/roles';
import { recordingActions } from '../../store/slices/recordingSlice';
import { startRecording, stopRecording } from '../../store/actions/recordingActions';

const RecordButton = (
props
: ControlButtonProps): JSX.Element => {
const dispatch = useAppDispatch();
const hasRecordingPermission = usePermissionSelector(permissions.LOCAL_RECORD_ROOM);
const recording = useAppSelector((state) => state.recording.recording);
const recording = useAppSelector((state) => state.room.recording);

return (
<ControlButton
toolTip={recording ? stopRecordingLabel() : startRecordingLabel() }
onClick={() => {
recording ?
dispatch(recordingActions.stop()) :
dispatch(recordingActions.start());
recording ? dispatch(stopRecording()) : dispatch(startRecording());
}}
disabled={!hasRecordingPermission}
{ ...props }
Expand Down
10 changes: 4 additions & 6 deletions src/components/menuitems/Recording.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import StopIcon from '@mui/icons-material/Stop';
import { MenuItemProps } from '../floatingmenu/FloatingMenu';
import { permissions } from '../../utils/roles';
import MoreActions from '../moreactions/MoreActions';
import { recordingActions } from '../../store/slices/recordingSlice';
import { startRecording, stopRecording } from '../../store/actions/recordingActions';

const Recording = ({
onClick
}: MenuItemProps): JSX.Element => {
const dispatch = useAppDispatch();
const hasRecordingPermission = usePermissionSelector(permissions.LOCAL_RECORD_ROOM);
const canRecord = useAppSelector((state) => state.me.canRecord);
const recording = useAppSelector((state) => state.recording.recording);
const recording = useAppSelector((state) => state.room.recording);
const recordTip = recording ? stopRecordingLabel() : startRecordingLabel();

return (
Expand All @@ -30,9 +30,7 @@ const Recording = ({
onClick={() => {
onClick();

recording ?
dispatch(recordingActions.stop()) :
dispatch(recordingActions.start());
recording ? dispatch(stopRecording()) : dispatch(startRecording());
}}
>
{ recording ? <StopIcon /> : <RecordIcon /> }
Expand All @@ -43,4 +41,4 @@ const Recording = ({
);
};

export default Recording;
export default Recording;
23 changes: 22 additions & 1 deletion src/services/mediaService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { Logger } from 'edumeet-common';
import { safePromise } from '../utils/safePromise';
import { ProducerSource } from '../utils/types';
import { MediaSender } from '../utils/mediaSender';
import type { ClientMonitor } from '@observertc/client-monitor-js';

const logger = new Logger('MediaService');

Expand Down Expand Up @@ -133,6 +134,18 @@ export class MediaService extends EventEmitter {
public resolveTransportsReady!: () => void;
public transportsReady!: ReturnType<typeof safePromise>;

public monitor: Promise<ClientMonitor> = (async () => {
const { createClientMonitor } = await import('@observertc/client-monitor-js');

const monitor = createClientMonitor({ collectingPeriodInMs: 5000 });

monitor.on('stats-collected', (stats) => {
logger.debug('stats-collected [stats:%o]', stats);
});

return monitor;
})();

constructor({ signalingService }: { signalingService: SignalingService }) {
super();

Expand Down Expand Up @@ -644,7 +657,7 @@ export class MediaService extends EventEmitter {

get localCapabilities(): LocalCapabilities {
return {
canRecord: Boolean(MediaRecorder && window.showSaveFilePicker),
canRecord: Boolean(MediaRecorder),
canTranscribe: Boolean(window.webkitSpeechRecognition),
};
}
Expand Down Expand Up @@ -712,6 +725,10 @@ export class MediaService extends EventEmitter {
const MediaSoup = await import('mediasoup-client');

this.mediasoup = new MediaSoup.Device();

const monitor = await this.monitor;

monitor.collectors.addMediasoupDevice(this.mediasoup);
}

if (!this.mediasoup.loaded) await this.mediasoup.load({ routerRtpCapabilities });
Expand Down Expand Up @@ -842,6 +859,10 @@ export class MediaService extends EventEmitter {
transport = p2pDevice.createSendTransport({ iceServers: this.iceServers });
}

const monitor = await this.monitor;

monitor.collectors.addRTCPeerConnection(transport.handler.pc);

transport.on('icecandidate', (candidate) => {
this.signalingService.notify('candidate', {
peerId,
Expand Down
98 changes: 51 additions & 47 deletions src/store/actions/recordingActions.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Logger } from 'edumeet-common';
import { AppThunk } from '../store';
import { recordingActions } from '../slices/recordingSlice';
import { roomActions } from '../slices/roomSlice';

const logger = new Logger('RecordingActions');

Expand All @@ -27,29 +27,6 @@ let audioContext: AudioContext;
let audioDestination: MediaStreamAudioDestinationNode;
let mimeType: string;

const stopRecorder = async () => {
logger.debug('stopRecorder()');

try {
recorder?.stop();
screenStream?.getTracks().forEach((track) => track.stop());
recorderStream?.getTracks().forEach((track) => track.stop());
audioContext?.close();
audioDestination?.disconnect();

// Give some time for last recording chunks to come through
setTimeout(async () => {
try {
await writableStream?.close();
} catch (error) {
logger.error('stopRecorder() [error:%o]', error);
}
}, RECORDING_SLICE_SIZE);
} catch (error) {
logger.error('stopRecorder() [error:%o]', error);
}
};

export const startRecording = (): AppThunk<Promise<void>> => async (
dispatch,
getState,
Expand All @@ -59,19 +36,15 @@ export const startRecording = (): AppThunk<Promise<void>> => async (

logger.debug('recordingActions.start [mimeType:%s]', mimeType);

if (!MediaRecorder || !window.showSaveFilePicker)
return logger.error('Recording is not supported');
if (!MediaRecorder) return logger.error('Recording is not supported');

const { showSaveFilePicker } = await import('native-file-system-adapter');

const roomName = new URL(getState().signaling.url).searchParams.get('roomId');

const opts:SaveFilePickerOptions = {
const opts: SaveFilePickerOptions = {
suggestedName: `${roomName}.mp4`,
types: [
{
description: 'LocalRecording',
accept: { 'video/mp4': [ '.mp4' ] },
},
],
types: [ { description: 'LocalRecording', accept: { 'video/mp4': [ '.mp4' ] } } ],
};

const saveFileHandle = await showSaveFilePicker(opts);
Expand All @@ -95,32 +68,46 @@ export const startRecording = (): AppThunk<Promise<void>> => async (
).connect(audioDestination);
}

const audioConsumers = mediaService.getConsumers()
.filter((consumer) => consumer.appData.source === 'mic');
mediaService.mediaSenders['mic'].on('started', () => {
if (mediaService.mediaSenders['mic'].track) {
audioContext.createMediaStreamSource(
new MediaStream([ mediaService.mediaSenders['mic'].track ])
).connect(audioDestination);
}
});

for (const consumer of audioConsumers) {
if (consumer.track) {
mediaService.mediaSenders['screenaudio'].on('started', () => {
if (mediaService.mediaSenders['screenaudio'].track) {
audioContext.createMediaStreamSource(
new MediaStream([ consumer.track ])
new MediaStream([ mediaService.mediaSenders['screenaudio'].track ])
).connect(audioDestination);
}
}
});

mediaService.on('consumerCreated', (consumer) => {
if (consumer.appData.source === 'mic' && consumer.track) {
if (consumer.kind === 'audio') {
logger.debug('consumer.transportclose event');

audioContext.createMediaStreamSource(
new MediaStream([ consumer.track ])
).connect(audioDestination);
}
});

const [ mixedAudioTrack ] = audioDestination.stream.getTracks();
const audioConsumers = mediaService.getConsumers().filter((consumer) => consumer.kind === 'audio');

for (const consumer of audioConsumers) {
logger.debug('audioConsumer [consumer:%o]', consumer);

audioContext.createMediaStreamSource(
new MediaStream([ consumer.track ])
).connect(audioDestination);
}

screenStream = await navigator.mediaDevices.getDisplayMedia(
RECORDING_CONSTRAINTS
);
screenStream = await navigator.mediaDevices.getDisplayMedia(RECORDING_CONSTRAINTS);

const [ screenVideotrack ] = screenStream.getVideoTracks();
const [ mixedAudioTrack ] = audioDestination.stream.getTracks();

screenVideotrack.addEventListener('ended', () => {
logger.debug('screenVideotrack ended event');
Expand Down Expand Up @@ -151,7 +138,7 @@ export const startRecording = (): AppThunk<Promise<void>> => async (

recorder.start(RECORDING_SLICE_SIZE);

dispatch(recordingActions.start());
dispatch(roomActions.updateRoom({ recording: true }));
} catch (error) {
logger.error('recordingActions.start [error:%o]', error);
}
Expand All @@ -160,7 +147,24 @@ export const startRecording = (): AppThunk<Promise<void>> => async (
export const stopRecording = (): AppThunk<Promise<void>> => async (dispatch) => {
logger.debug('stopRecording()');

await stopRecorder();
try {
recorder?.stop();
screenStream?.getTracks().forEach((track) => track.stop());
recorderStream?.getTracks().forEach((track) => track.stop());
audioContext?.close();
audioDestination?.disconnect();

// Give some time for last recording chunks to come through
setTimeout(async () => {
try {
await writableStream?.close();
} catch (error) {
logger.error('stopRecorder() [error:%o]', error);
}
}, RECORDING_SLICE_SIZE);
} catch (error) {
logger.error('stopRecorder() [error:%o]', error);
}

dispatch(recordingActions.stop());
dispatch(roomActions.updateRoom({ recording: false }));
};
33 changes: 0 additions & 33 deletions src/store/slices/recordingSlice.tsx

This file was deleted.

4 changes: 3 additions & 1 deletion src/store/slices/roomSlice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface RoomState {
joinInProgress?: boolean;
updateBreakoutInProgress?: boolean;
transitBreakoutRoomInProgress?: boolean;
recording?: boolean;
lockInProgress?: boolean;
localeInProgress?: boolean;
muteAllInProgress?: boolean;
Expand Down Expand Up @@ -44,6 +45,7 @@ const initialState: RoomState = {
backgroundImage: edumeetConfig.theme.backgroundImage,
roomMode: 'P2P',
state: 'new',
recording: false,
breakoutsEnabled: true,
chatEnabled: true,
filesharingEnabled: true,
Expand Down Expand Up @@ -79,4 +81,4 @@ const roomSlice = createSlice({
});

export const roomActions = roomSlice.actions;
export default roomSlice;
export default roomSlice;
2 changes: 0 additions & 2 deletions src/store/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import edumeetConfig from '../utils/edumeetConfig';
import { createContext } from 'react';
import { DeviceService } from '../services/deviceService';
import { FileService } from '../services/fileService';
import recordingSlice from './slices/recordingSlice';
import roomSessionsSlice from './slices/roomSessionsSlice';
import { Application, feathers } from '@feathersjs/feathers/lib';
import rest from '@feathersjs/rest-client';
Expand Down Expand Up @@ -121,7 +120,6 @@ const reducer = combineReducers({
settings: settingsSlice.reducer,
signaling: signalingSlice.reducer,
ui: uiSlice.reducer,
recording: recordingSlice.reducer,
});

const pReducer = persistReducer<RootState>(persistConfig, reducer);
Expand Down
Loading

0 comments on commit 7c0d8ab

Please sign in to comment.