| 1 | "use client"; |
| 2 | |
| 3 | import { create } from "zustand"; |
| 4 | import { persist } from "zustand/middleware"; |
| 5 | |
| 6 | type NullableStream = MediaStream | null; |
| 7 | |
| 8 | interface OverlayPosition { |
| 9 | overlayX: number; // px relative to viewport left |
| 10 | overlayY: number; // px relative to viewport top |
| 11 | } |
| 12 | |
| 13 | interface PresentationRecordingState extends OverlayPosition { |
| 14 | // UI flags |
| 15 | wantsToRecord: boolean; |
| 16 | isRecording: boolean; |
| 17 | isStarting: boolean; |
| 18 | isStopping: boolean; |
| 19 | // device toggles |
| 20 | webcamEnabled: boolean; |
| 21 | micEnabled: boolean; |
| 22 | |
| 23 | // device selection |
| 24 | selectedVideoDeviceId: string | null; |
| 25 | selectedAudioDeviceId: string | null; |
| 26 | |
| 27 | // media + recorder |
| 28 | camStream: NullableStream; |
| 29 | micStream: NullableStream; |
| 30 | composedStream: NullableStream; // canvas video + audio tracks |
| 31 | blobUrl: string | null; |
| 32 | |
| 33 | // mutators |
| 34 | setWantsToRecord: (b: boolean) => void; |
| 35 | setWebcamEnabled: (v: boolean) => void; |
| 36 | setMicEnabled: (v: boolean) => void; |
| 37 | setSelectedVideoDevice: (id: string | null) => void; |
| 38 | setSelectedAudioDevice: (id: string | null) => void; |
| 39 | setOverlayPosition: (x: number, y: number) => void; |
| 40 | |
| 41 | setCamStream: (s: NullableStream) => void; |
| 42 | setMicStream: (s: NullableStream) => void; |
| 43 | setComposedStream: (s: NullableStream) => void; |
| 44 | setBlobUrl: (url: string | null) => void; |
| 45 | setIsStarting: (v: boolean) => void; |
| 46 | setIsStopping: (v: boolean) => void; |
| 47 | beginRecording: () => void; |
| 48 | endRecording: () => void; |
| 49 | reset: () => void; |
| 50 | } |
| 51 | |
| 52 | export const usePresentationRecordingState = |
| 53 | create<PresentationRecordingState>()( |
| 54 | persist( |
| 55 | (set, get) => ({ |
| 56 | // defaults |
| 57 | wantsToRecord: false, |
| 58 | isRecording: false, |
| 59 | isStarting: false, |
| 60 | isStopping: false, |
| 61 | webcamEnabled: true, |
| 62 | micEnabled: true, |
| 63 | selectedVideoDeviceId: null, |
| 64 | selectedAudioDeviceId: null, |
| 65 | overlayX: 0, |
| 66 | overlayY: 0, |
| 67 | overlayWidth: 240, |
| 68 | camStream: null, |
| 69 | micStream: null, |
| 70 | composedStream: null, |
| 71 | blobUrl: null, |
| 72 | |
| 73 | setWantsToRecord: (b) => |
| 74 | set((state) => { |
| 75 | if (!b) { |
| 76 | state.reset(); |
| 77 | } |
| 78 | |
| 79 | return { wantsToRecord: b }; |
| 80 | }), |
| 81 | setWebcamEnabled: (v) => set({ webcamEnabled: v }), |
| 82 | setMicEnabled: (v) => set({ micEnabled: v }), |
| 83 | setSelectedVideoDevice: (id) => set({ selectedVideoDeviceId: id }), |
| 84 | setSelectedAudioDevice: (id) => set({ selectedAudioDeviceId: id }), |
| 85 | setOverlayPosition: (x, y) => set({ overlayX: x, overlayY: y }), |
| 86 | setCamStream: (s) => set({ camStream: s }), |
| 87 | setMicStream: (s) => set({ micStream: s }), |
| 88 | setComposedStream: (s) => set({ composedStream: s }), |
| 89 | setBlobUrl: (url) => set({ blobUrl: url }), |
| 90 | setIsStarting: (v) => set({ isStarting: v }), |
| 91 | setIsStopping: (v) => set({ isStopping: v }), |
| 92 | |
| 93 | beginRecording: () => set({ isRecording: true }), |
| 94 | endRecording: () => set({ isRecording: false }), |
| 95 | |
| 96 | reset: () => { |
| 97 | const { camStream, micStream, composedStream, blobUrl } = get(); |
| 98 | camStream?.getTracks().forEach((t) => t.stop()); |
| 99 | micStream?.getTracks().forEach((t) => t.stop()); |
| 100 | composedStream?.getTracks().forEach((t) => t.stop()); |
| 101 | if (blobUrl) URL.revokeObjectURL(blobUrl); |
| 102 | set({ |
| 103 | wantsToRecord: false, |
| 104 | isRecording: false, |
| 105 | isStarting: false, |
| 106 | isStopping: false, |
| 107 | camStream: null, |
| 108 | micStream: null, |
| 109 | composedStream: null, |
| 110 | blobUrl: null, |
| 111 | }); |
| 112 | }, |
| 113 | }), |
| 114 | { |
| 115 | name: "presentation-recording-state-storage", |
| 116 | partialize: (state) => ({ |
| 117 | webcamEnabled: state.webcamEnabled, |
| 118 | micEnabled: state.micEnabled, |
| 119 | selectedVideoDeviceId: state.selectedVideoDeviceId, |
| 120 | selectedAudioDeviceId: state.selectedAudioDeviceId, |
| 121 | overlayX: state.overlayX, |
| 122 | overlayY: state.overlayY, |
| 123 | }), |
| 124 | }, |
| 125 | ), |
| 126 | ); |
| 127 |