返回 presentation-ai
presentation-recording-state.ts
根目录 / src / states / presentation-recording-state.ts
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
127 lines TYPESCRIPT