From f417e16319af3d82d600c534072fc682e8a9b47d Mon Sep 17 00:00:00 2001 From: Nuwan Date: Sun, 7 Dec 2025 13:37:46 +0530 Subject: [PATCH] add recording functionality --- .../client/JKSessionRecordingModal.js | 87 ++++++++++++++++--- .../src/components/client/JKSessionScreen.js | 83 +++++++++++++++++- jam-ui/src/context/GlobalContext.js | 12 ++- jam-ui/src/context/JamClientContext.js | 6 +- jam-ui/src/helpers/utils.js | 10 +++ jam-ui/src/hooks/useRecordingHelpers.js | 6 +- 6 files changed, 186 insertions(+), 18 deletions(-) diff --git a/jam-ui/src/components/client/JKSessionRecordingModal.js b/jam-ui/src/components/client/JKSessionRecordingModal.js index 6b94b08ca..82f32454f 100644 --- a/jam-ui/src/components/client/JKSessionRecordingModal.js +++ b/jam-ui/src/components/client/JKSessionRecordingModal.js @@ -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 ( @@ -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 }) => { - + +
+ +
+
@@ -282,6 +346,7 @@ const JKSessionRecordingModal = ({ isOpen, toggle }) => { diff --git a/jam-ui/src/components/client/JKSessionScreen.js b/jam-ui/src/components/client/JKSessionScreen.js index e01b53cfd..4d418d13b 100644 --- a/jam-ui/src/components/client/JKSessionScreen.js +++ b/jam-ui/src/components/client/JKSessionScreen.js @@ -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 ( {!isConnected &&
Connecting to backend...
} @@ -712,6 +788,7 @@ const JKSessionScreen = () => { setShowRecordingModal(!showRecordingModal)} + onSubmit={handleRecordingSubmit} />
) diff --git a/jam-ui/src/context/GlobalContext.js b/jam-ui/src/context/GlobalContext.js index 28b028817..77f7674a3 100644 --- a/jam-ui/src/context/GlobalContext.js +++ b/jam-ui/src/context/GlobalContext.js @@ -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 ( {children} ); }; -export const useGlobalContext = () => React.useContext(GlobalContext); \ No newline at end of file +export const useGlobalContext = () => React.useContext(GlobalContext); diff --git a/jam-ui/src/context/JamClientContext.js b/jam-ui/src/context/JamClientContext.js index 05f06c459..f0e61b538 100644 --- a/jam-ui/src/context/JamClientContext.js +++ b/jam-ui/src/context/JamClientContext.js @@ -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 ( diff --git a/jam-ui/src/helpers/utils.js b/jam-ui/src/helpers/utils.js index 3b632681b..486aef2a7 100644 --- a/jam-ui/src/helpers/utils.js +++ b/jam-ui/src/helpers/utils.js @@ -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; }; \ No newline at end of file diff --git a/jam-ui/src/hooks/useRecordingHelpers.js b/jam-ui/src/hooks/useRecordingHelpers.js index bd02fde8f..9e66ba1c9 100644 --- a/jam-ui/src/hooks/useRecordingHelpers.js +++ b/jam-ui/src/hooks/useRecordingHelpers.js @@ -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