add recording functionality

This commit is contained in:
Nuwan 2025-12-07 13:37:46 +05:30
parent 0fab4532c6
commit f417e16319
6 changed files with 186 additions and 18 deletions

View File

@ -16,6 +16,7 @@ import {
import { useJamServerContext } from '../../context/JamServerContext';
import useRecordingHelpers from '../../hooks/useRecordingHelpers';
import { AUDIO_STORE_TYPE_MIX_AND_STEMS, AUDIO_STORE_TYPE_MIX_ONLY, RECORD_TYPE_AUDIO, RECORD_TYPE_BOTH } from '../../helpers/globals.js';
import { set } from 'lodash';
const JKSessionRecordingModal = ({ isOpen, toggle }) => {
const { jamClient } = useJamServerContext();
@ -46,6 +47,26 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
{ key: 'video-window', text: 'Record session video window' }
];
//On mount get the audio store type from backend and set the radio button accordingly using jamClient.GetAudioRecordingPreference()
useEffect(() => {
const fetchAudioStoreType = async () => {
try {
if (jamClient) {
const pref = await jamClient.GetAudioRecordingPreference();
if (AUDIO_STORE_TYPE_MIX_AND_STEMS['backendValues'].includes(pref)) {
setAudioStoreType(AUDIO_STORE_TYPE_MIX_AND_STEMS['key']);
} else if (AUDIO_STORE_TYPE_MIX_ONLY['backendValues'].includes(pref)) {
setAudioStoreType(AUDIO_STORE_TYPE_MIX_ONLY['key']);
}
}
} catch (error) {
console.error('Error fetching audio store type preference:', error);
}
};
fetchAudioStoreType();
}, [jamClient]);
// Load available video sources when modal opens
useEffect(() => {
if (isOpen && jamClient) {
@ -76,7 +97,7 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
const handleStartStopRecording = async () => {
setIsLoading(true);
setError(null);
alert('Processing recording request. Please wait...');
try {
if (isRecording) {
// Stop recording
@ -112,13 +133,24 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
}
const recordSettings = {
recordingType: recordVideo ? 'JK.RECORD_TYPE_BOTH' : 'JK.RECORD_TYPE_AUDIO',
recordingType: recordVideo ? RECORD_TYPE_BOTH : RECORD_TYPE_AUDIO,
recordVideo: recordVideo,
recordChat: includeChat,
videoType: recordVideoType
};
await recordingHelpers.startRecording(recordSettings);
const success = await recordingHelpers.startRecording(recordSettings);
if (!success) {
setError('Failed to start recording. Please try again.');
setIsLoading(false);
return;
}
setIsLoading(false);
setRecordingName('');
setAudioFormat('');
alert('Recording started successfully.');
handleCancel();
}
} catch (error) {
console.error('Recording operation failed:', error);
@ -161,6 +193,29 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
}
});
const onRecordingTypeChange = async (e) => {
const selectedType = e.target.value;
setRecordingType(selectedType);
if (selectedType === RECORD_TYPE_BOTH) {
const obsAvailable = await jamClient.IsOBSAvailable();
if (!obsAvailable) {
alert("OBS Studio is required for video recording. Please install OBS Studio and try again.");
setRecordingType(RECORD_TYPE_AUDIO);
}
}
};
const onAudioStoreTypeChange = (e) => {
const value = e.target.value;
if (value === AUDIO_STORE_TYPE_MIX_AND_STEMS['key']) {
jamClient.SetAudioRecordingPreference(AUDIO_STORE_TYPE_MIX_AND_STEMS['backendValues'][0]);
} else if (value === AUDIO_STORE_TYPE_MIX_ONLY['key']) {
jamClient.SetAudioRecordingPreference(AUDIO_STORE_TYPE_MIX_ONLY['backendValues'][0]);
}
setAudioStoreType(value);
}
return (
<Modal isOpen={isOpen} toggle={handleCancel} modalClassName="theme-modal" contentClassName="border" size="md">
<ModalHeader toggle={handleCancel} className="bg-light d-flex flex-between-center border-bottom-0">
@ -181,7 +236,7 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
name="recordingType"
value="audio-only"
checked={recordingType === 'audio-only'}
onChange={(e) => setRecordingType(e.target.value)}
onChange={onRecordingTypeChange}
disabled={isRecording || isStarting || isStopping}
/>
Audio only
@ -194,7 +249,7 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
name="recordingType"
value="audio-video"
checked={recordingType === 'audio-video'}
onChange={(e) => setRecordingType(e.target.value)}
onChange={onRecordingTypeChange}
disabled={isRecording || isStarting || isStopping}
/>
Audio and video
@ -214,7 +269,7 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
name="audioStoreType"
value={AUDIO_STORE_TYPE_MIX_AND_STEMS['key']}
checked={AUDIO_STORE_TYPE_MIX_AND_STEMS['key'] === audioStoreType}
onChange={(e) => setAudioStoreType(e.target.value)}
onChange={onAudioStoreTypeChange}
disabled={isRecording || isStarting || isStopping}
/>
Session mix & individual parts (streams)
@ -270,11 +325,20 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
<Row className='mt-2'>
<Col xs="12" md="4" lg="3"><label htmlFor="voiceChat">Voice Chat</label></Col>
<Col>
<Input
type="check"
name='voiceChat'
value=""
/>
<FormGroup tag="fieldset">
<div style={{ }}>
<Label >
<Input
type="checkbox"
checked={includeChat}
onChange={(e) => setIncludeChat(e.target.checked)}
disabled={isRecording || isStarting || isStopping}
name="includeChat"
/>
Include voice chat in recording
</Label>
</div>
</FormGroup>
</Col>
</Row>
@ -282,6 +346,7 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => {
<Col xs="12" md="4" lg="3"><label htmlFor="videoFormat">Video Format</label></Col>
<Col>
<Input
disabled={recordingType === RECORD_TYPE_AUDIO || isRecording || isStarting || isStopping}
type='select'
name=""
>

View File

@ -12,13 +12,16 @@ import useMixerStore from '../../hooks/useMixerStore.js';
import { useCurrentSessionContext } from '../../context/CurrentSessionContext.js';
import { useJamServerContext } from '../../context/JamServerContext.js';
import { useGlobalContext } from '../../context/GlobalContext.js';
import { useJamKazamApp } from '../../context/JamKazamAppContext.js';
import { useMixersContext } from '../../context/MixersContext.js';
import { useAuth } from '../../context/UserAuth';
import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends } from '../../helpers/rest';
import { dkeys } from '../../helpers/utils.js';
import { CLIENT_ROLE } from '../../helpers/globals';
import { getSessionHistory, getSession, joinSession as joinSessionRest, updateSessionSettings, getFriends, startRecording, stopRecording } from '../../helpers/rest';
import { CLIENT_ROLE, RECORD_TYPE_AUDIO, RECORD_TYPE_BOTH } from '../../helpers/globals';
import { MessageType } from '../../helpers/MessageFactory.js';
import { Alert, Col, Row, Button, Card, CardBody, Modal, ModalHeader, ModalBody, ModalFooter, CardHeader, Badge } from 'reactstrap';
@ -53,7 +56,8 @@ const JKSessionScreen = () => {
server,
registerMessageCallback } = useJamServerContext();
const { currentSession, setCurrentSession, currentSessionIdRef, setCurrentSessionId, inSession } = useCurrentSessionContext();
const { getCurrentRecordingState, reset: resetRecordingState } = useRecordingHelpers();
const { globalObject } = useGlobalContext();
const { getCurrentRecordingState, reset: resetRecordingState, currentlyRecording } = useRecordingHelpers();
const { SessionPageEnter } = useSessionUtils();
// Use the session model hook
@ -457,6 +461,78 @@ const JKSessionScreen = () => {
}
};
const handleRecordingSubmit = async (settings) => {
settings.volume = getCurrentRecordingState().inputVolumeLevel;
try {
localStorage.setItem("recordSettings", JSON.stringify(settings));
} catch (e) {
logger.info("error while saving recordSettings to localStorage");
logger.log(e.stack);
}
const params = {
recordingType: settings.recordingType,
name: settings.recordingName,
audioFormat: settings.audioFormat,
audioStoreType: settings.audioStoreType,
includeChat: settings.includeChat,
volume: settings.volume,
};
if (params.recordingType === RECORD_TYPE_BOTH) {
params['videoFormat'] = settings.videoFormat;
params['audioDelay'] = settings.audioDelay;
const obsAvailable = await jamClient.IsOBSAvailable();
if (!obsAvailable) {
toast.warning("OBS Studio is not available. Please ensure OBS Studio is installed and running to record video.");
return;
}
if (!globalObject.JK.videoIsOngoing) {
toast.warning("To make a video recording in JamKazam you must have an ongoing video. You can start a video by clicking the Video button on session tool bar.");
return;
}
}
//this.startStopRecording(params);
//TODO: handle startStopRecording
doStartRecording(params);
}
function groupTracksToClient(recording) {
// group N tracks to the same client Id
let groupedTracks = {};
let recordingTracks = recording["recorded_tracks"];
for (let i = 0; i < recordingTracks.length; i++) {
let clientId = recordingTracks[i].client_id;
let tracksForClient = groupedTracks[clientId];
if (!tracksForClient) {
tracksForClient = [];
groupedTracks[clientId] = tracksForClient;
}
tracksForClient.push(recordingTracks[i]);
}
return dkeys(groupedTracks);
}
const doStartRecording = (params) => {
startRecording({ music_session_id: currentSession.id, recordVideo: params.recordVideo }).then(async (recording) => {
const currentRecordingId = recording.id;
console.debug("Recording started with ID: ", currentRecordingId);
const groupedTracks = groupTracksToClient(recording);
try {
await jamClient.StartMediaRecording(currentRecordingId, groupedTracks, params);
} catch (error) {
console.error("Error starting media recording:", error);
}
}).catch((error) => {
console.error("Error starting recording:", error);
});
}
return (
<Card>
{!isConnected && <div className='d-flex align-items-center'>Connecting to backend...</div>}
@ -712,6 +788,7 @@ const JKSessionScreen = () => {
<JKSessionRecordingModal
isOpen={showRecordingModal}
toggle={() => setShowRecordingModal(!showRecordingModal)}
onSubmit={handleRecordingSubmit}
/>
</Card>
)

View File

@ -23,14 +23,24 @@ export const GlobalProvider = ({ children }) => {
wigetID: ""
});
const [globalObject, setGlobalObject] = useState({
JK: window.JK || {},
//ExternalVideoActions: window.ExternalVideoActions || null,
//RecordingActions: window.RecordingActions || null,
});
const [videoEnabled, setVideoEnabled] = useState(false);
return (
<GlobalContext.Provider value={{
trackVolumeObject,
setTrackVolumeObject,
globalObject,
setGlobalObject,
}}>
{children}
</GlobalContext.Provider>
);
};
export const useGlobalContext = () => React.useContext(GlobalContext);
export const useGlobalContext = () => React.useContext(GlobalContext);

View File

@ -5,6 +5,7 @@ import { FakeJamClientProxy } from '../fakeJamClientProxy';
import { FakeJamClientRecordings } from '../fakeJamClientRecordings';
import { FakeJamClientMessages } from '../fakeJamClientMessages';
import { useJamKazamApp } from './JamKazamAppContext';
import { useGlobalContext } from './GlobalContext';
const JamClientContext = createContext(null);
@ -17,6 +18,7 @@ export const JamClientProvider = ({ children }) => {
//get the app instance from JamKazamAppContext
const app = useJamKazamApp();
const { globalObject } = useGlobalContext();
const proxyRef = useRef(null);
@ -29,13 +31,13 @@ export const JamClientProvider = ({ children }) => {
// proxyRef.current.SetFakeRecordingImpl(fakeJamClientRecordings);
// } else {
// if (!proxyRef.current) {
// const proxy = new JamClientProxy(app, console);
// const proxy = new JamClientProxy(app, console, globalObject);
// proxyRef.current = proxy.init();
// }
// }
if (!proxyRef.current) {
const proxy = new JamClientProxy(app, console);
const proxy = new JamClientProxy(app, console, globalObject);
proxyRef.current = proxy.init();
}
return (

View File

@ -540,4 +540,14 @@ export const SKIPPED_NETWORK_TEST = -1;
export const getAvatarUrl = (photo_url) => {
return photo_url ? photo_url : "/assets/shared/avatar_generic.png";
};
export const dkeys = (d) => {
var keys = []
for (var i in d) {
if (d.hasOwnProperty(i)) {
keys.push(i);
}
}
return keys;
};

View File

@ -2,6 +2,8 @@ import { useState, useCallback, useRef, useEffect, useContext } from 'react';
import { useCurrentSessionContext } from '../context/CurrentSessionContext';
import { useJamKazamApp } from '../context/JamKazamAppContext';
import { startRecording as startRecordingRest, stopRecording as stopRecordingRest, getRecordingPromise } from '../helpers/rest';
import { useGlobalContext } from '../context/GlobalContext';
import { RECORD_TYPE_BOTH } from '../helpers/globals'
const useRecordingHelpers = (jamClient) => {
const app = useJamKazamApp();
@ -18,6 +20,8 @@ const useRecordingHelpers = (jamClient) => {
const [waitingOnServerStop, setWaitingOnServerStop] = useState(false);
const [waitingOnClientStop, setWaitingOnClientStop] = useState(false);
const { globalObject, setGlobalObject } = useGlobalContext();
const waitingOnStopTimer = useRef(null);
const sessionId = currentSession?.id;
@ -75,7 +79,7 @@ const useRecordingHelpers = (jamClient) => {
// Start recording
const startRecording = useCallback(async (recordSettings) => {
const recordVideo = recordSettings.recordingType === 'JK.RECORD_TYPE_BOTH';
const recordVideo = recordSettings.recordingType === RECORD_TYPE_BOTH;
// Trigger startingRecording event
// In React, we might use a callback or context to notify parent components