Skip to content

Commit 1e3db9c

Browse files
committed
fix(camera): clarify toolbar semantics and delay first frame
1 parent c35c745 commit 1e3db9c

8 files changed

Lines changed: 38 additions & 33 deletions

File tree

app/(tabs)/main.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
888888
return false;
889889
}, [audio.isConnected, audio.isRecording, audio.sendMessage]);
890890

891-
const handleToggleScreen = useCallback(async (next: boolean) => {
891+
const handleToggleCamera = useCallback(async (next: boolean) => {
892892
if (!next) {
893893
// 停止摄像头
894894
cameraStream.stopStreaming();
@@ -1352,7 +1352,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
13521352
- 详见:docs/strategy/cross-platform-components.md
13531353
13541354
功能包括:
1355-
- 麦克风/屏幕共享切换
1355+
- 麦克风/摄像头切换
13561356
- Agent 设置面板
13571357
- Settings 面板
13581358
- 设置菜单(Live2D设置、API密钥、角色管理等)
@@ -1364,7 +1364,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
13641364
right={isMobile ? 12 : 24}
13651365
top={isMobile ? screenHeight * 0.05 : 24}
13661366
micEnabled={toolbarMicEnabled}
1367-
screenEnabled={cameraStream.isStreaming}
1367+
cameraEnabled={cameraStream.isStreaming}
13681368
goodbyeMode={toolbarGoodbyeMode}
13691369
openPanel={toolbarOpenPanel}
13701370
onOpenPanelChange={setToolbarOpenPanel}
@@ -1373,7 +1373,7 @@ const MainUIScreen: React.FC<MainUIScreenProps> = () => {
13731373
agent={agent}
13741374
onAgentChange={handleToolbarAgentChange}
13751375
onToggleMic={handleToggleMic}
1376-
onToggleScreen={handleToggleScreen}
1376+
onToggleCamera={handleToggleCamera}
13771377
onGoodbye={handleGoodbye}
13781378
onReturn={handleReturn}
13791379
onSettingsMenuClick={handleSettingsMenuClick}

assets/icons/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
请从主项目的 `static/icons/` 目录复制以下图标文件到此目录:
88

99
- `mic_icon_off.png` - 麦克风图标
10-
- `screen_icon_off.png` - 屏幕分享图标
10+
- `screen_icon_off.png` - 摄像头按钮当前复用的历史图标资源
1111
- `Agent_off.png` - Agent 工具图标
1212
- `set_off.png` - 设置图标
1313
- `rest_off.png` - 离开/返回图标

docs/features/realtime-vision-guide.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export interface CameraStreamConfig {
172172
**关键实现**
173173

174174
- `setCameraRef(ref)` — 绑定 CameraView ref
175-
- `start()`启动 `setInterval` 帧捕获循环,立即捕获第一帧
175+
- `start()`启动帧捕获循环,首帧延迟约 500ms 以提升 CameraView 启动稳定性
176176
- `stop()` — 清除 interval,状态回到 `idle`
177177
- `pause()` / `resume()` — 后台暂停/恢复
178178
- `captureAndSend()` — 内部方法:
@@ -256,7 +256,7 @@ const cameraStream = useCameraStream({
256256
**(c) 替换 toolbarScreenEnabled 状态 (line 220)**
257257

258258
- 删除 `const [toolbarScreenEnabled, setToolbarScreenEnabled] = useState(false);`
259-
- 把 toolbar 的 `screenEnabled` prop 改为 `screenEnabled={cameraStream.isStreaming}`
259+
- 把 toolbar 的 `cameraEnabled` prop 接到 `cameraStream.isStreaming`
260260

261261
**(d) 重写 handleToggleScreen (line 772-775)**
262262

@@ -303,7 +303,7 @@ iOS `NSCameraUsageDescription` 也做同样更新。
303303
| `services/imageCompression.ts` | 直接复用 `compress()` 方法,无需改动 |
304304
| `services/AudioService.ts` | 通过 hook 层的 `sendMessage` 消费 |
305305
| `hooks/useCamera.ts` | 单张拍照 hook,保持独立 |
306-
| `packages/project-neko-components/` | toolbar 已有 `screenEnabled` / `onToggleScreen` props |
306+
| `packages/project-neko-components/` | toolbar 已统一为 `cameraEnabled` / `onToggleCamera` props |
307307
| 后端所有文件 | 协议和限流完全兼容 |
308308

309309
#### 4.2.5 实现顺序
@@ -385,7 +385,7 @@ React 渲染 CameraStreamOverlay + CameraView
385385
386386
onCameraReady 回调触发 → setCameraRef + start()
387387
388-
setInterval 启动 (每 1.5s) + 立即捕获第一帧
388+
定时启动 (每 1.5s) + 首帧延迟约 500ms
389389
390390
takePictureAsync() → ImageCompressionService.compress()
391391

packages/project-neko-components/src/Live2DRightToolbar/Live2DRightToolbar.native.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function Live2DRightToolbar({
4444
top = 24,
4545
isMobile,
4646
micEnabled,
47-
screenEnabled,
47+
cameraEnabled,
4848
goodbyeMode,
4949
openPanel,
5050
onOpenPanelChange,
@@ -53,7 +53,7 @@ export function Live2DRightToolbar({
5353
agent,
5454
onAgentChange,
5555
onToggleMic,
56-
onToggleScreen,
56+
onToggleCamera,
5757
onGoodbye,
5858
onReturn,
5959
onSettingsMenuClick,
@@ -68,19 +68,19 @@ export function Live2DRightToolbar({
6868
// 使用共享的按钮配置(RN 使用本地 require() 资源)
6969
const buttons = useToolbarButtons<number>({
7070
micEnabled,
71-
screenEnabled,
71+
cameraEnabled,
7272
openPanel,
7373
goodbyeMode,
7474
isMobile,
7575
onToggleMic,
76-
onToggleScreen,
76+
onToggleCamera,
7777
onGoodbye,
7878
togglePanel,
7979
t,
8080
// RN: 使用本地打包的图标资源(确保这些文件存在于 assets/icons/)
8181
icons: {
8282
mic: require('../../../../assets/icons/mic_icon_off.png'),
83-
screen: require('../../../../assets/icons/screen_icon_off.png'),
83+
camera: require('../../../../assets/icons/screen_icon_off.png'),
8484
agent: require('../../../../assets/icons/Agent_off.png'),
8585
settings: require('../../../../assets/icons/set_off.png'),
8686
goodbye: require('../../../../assets/icons/rest_off.png'),

packages/project-neko-components/src/Live2DRightToolbar/Live2DRightToolbar.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function Live2DRightToolbar({
2525
top,
2626
isMobile,
2727
micEnabled,
28-
screenEnabled,
28+
cameraEnabled,
2929
goodbyeMode,
3030
openPanel,
3131
onOpenPanelChange,
@@ -34,7 +34,7 @@ export function Live2DRightToolbar({
3434
agent,
3535
onAgentChange,
3636
onToggleMic,
37-
onToggleScreen,
37+
onToggleCamera,
3838
onGoodbye,
3939
onReturn,
4040
onSettingsMenuClick,
@@ -51,12 +51,12 @@ export function Live2DRightToolbar({
5151
// 使用共享的按钮配置
5252
const buttons = useToolbarButtons<string>({
5353
micEnabled,
54-
screenEnabled,
54+
cameraEnabled,
5555
openPanel,
5656
goodbyeMode,
5757
isMobile,
5858
onToggleMic,
59-
onToggleScreen,
59+
onToggleCamera,
6060
onGoodbye,
6161
togglePanel,
6262
t,

packages/project-neko-components/src/Live2DRightToolbar/hooks.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,32 +87,32 @@ export function usePanelToggle(
8787
*/
8888
export function useToolbarButtons<TIcon = ToolbarIcon>({
8989
micEnabled,
90-
screenEnabled,
90+
cameraEnabled,
9191
openPanel,
9292
goodbyeMode,
9393
isMobile,
9494
onToggleMic,
95-
onToggleScreen,
95+
onToggleCamera,
9696
onGoodbye,
9797
togglePanel,
9898
t,
9999
iconBasePath = '/static/icons', // Web 使用
100100
icons, // RN 使用 require() 资源
101101
}: {
102102
micEnabled: boolean;
103-
screenEnabled: boolean;
103+
cameraEnabled: boolean;
104104
openPanel: Live2DRightToolbarPanel;
105105
goodbyeMode: boolean;
106106
isMobile?: boolean;
107107
onToggleMic: (next: boolean) => void;
108-
onToggleScreen: (next: boolean) => void;
108+
onToggleCamera: (next: boolean) => void;
109109
onGoodbye: () => void;
110110
togglePanel: (panel: Exclude<Live2DRightToolbarPanel, null>) => void;
111111
t?: TFunction;
112112
iconBasePath?: string;
113113
icons?: {
114114
mic: TIcon;
115-
screen: TIcon;
115+
camera: TIcon;
116116
agent: TIcon;
117117
settings: TIcon;
118118
goodbye: TIcon;
@@ -130,12 +130,12 @@ export function useToolbarButtons<TIcon = ToolbarIcon>({
130130
icon: (icons?.mic ?? `${iconBasePath}/mic_icon_off.png`) as TIcon,
131131
},
132132
{
133-
id: 'screen' as const,
133+
id: 'camera' as const,
134134
title: tOrDefault(t, 'buttons.cameraShare', '摄像头'),
135135
hidden: false,
136-
active: screenEnabled,
137-
onClick: () => onToggleScreen(!screenEnabled),
138-
icon: (icons?.screen ?? `${iconBasePath}/screen_icon_off.png`) as TIcon,
136+
active: cameraEnabled,
137+
onClick: () => onToggleCamera(!cameraEnabled),
138+
icon: (icons?.camera ?? `${iconBasePath}/screen_icon_off.png`) as TIcon,
139139
},
140140
{
141141
id: 'agent' as const,
@@ -165,7 +165,7 @@ export function useToolbarButtons<TIcon = ToolbarIcon>({
165165
hasPanel: false,
166166
},
167167
].filter((b) => !b.hidden),
168-
[goodbyeMode, isMobile, micEnabled, onGoodbye, onToggleMic, onToggleScreen, openPanel, screenEnabled, t, togglePanel, iconBasePath, icons]
168+
[cameraEnabled, goodbyeMode, isMobile, micEnabled, onGoodbye, onToggleCamera, onToggleMic, openPanel, t, togglePanel, iconBasePath, icons]
169169
);
170170
}
171171

packages/project-neko-components/src/Live2DRightToolbar/types.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* 此文件包含 Web 和 RN 版本共享的所有类型定义
55
*/
66

7-
export type Live2DRightToolbarButtonId = "mic" | "screen" | "agent" | "settings" | "goodbye" | "return";
7+
export type Live2DRightToolbarButtonId = "mic" | "camera" | "agent" | "settings" | "goodbye" | "return";
88

99
export type Live2DRightToolbarPanel = "agent" | "settings" | null;
1010

@@ -47,7 +47,7 @@ export interface Live2DRightToolbarProps {
4747
isMobile?: boolean;
4848

4949
micEnabled: boolean;
50-
screenEnabled: boolean;
50+
cameraEnabled: boolean;
5151
goodbyeMode: boolean;
5252

5353
openPanel: Live2DRightToolbarPanel;
@@ -60,7 +60,7 @@ export interface Live2DRightToolbarProps {
6060
onAgentChange: (id: Live2DAgentToggleId, next: boolean) => void;
6161

6262
onToggleMic: (next: boolean) => void;
63-
onToggleScreen: (next: boolean) => void;
63+
onToggleCamera: (next: boolean) => void;
6464
onGoodbye: () => void;
6565
onReturn: () => void;
6666

services/CameraStreamService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export interface CameraStreamConfig {
1717
const yieldToMain = (ms: number = 0): Promise<void> =>
1818
new Promise(resolve => setTimeout(resolve, ms));
1919

20+
/**
21+
* CameraView 刚 ready 时给原生预览一点稳定时间,避免首帧拍照过早失败。
22+
*/
23+
const INITIAL_CAPTURE_DELAY_MS = 500;
24+
2025
/**
2126
* 摄像头流式服务
2227
* 使用 aggressive yielding 策略避免阻塞 JS 线程
@@ -64,7 +69,7 @@ export class CameraStreamService {
6469

6570
console.log('📹 启动摄像头流');
6671
this.setStatus('streaming');
67-
this.scheduleNextCapture(0);
72+
this.scheduleNextCapture(INITIAL_CAPTURE_DELAY_MS);
6873
}
6974

7075
private scheduleNextCapture(delayMs: number = this.frameInterval) {
@@ -103,7 +108,7 @@ export class CameraStreamService {
103108
if (this.status !== 'paused') return;
104109
console.log('📹 恢复摄像头流');
105110
this.setStatus('streaming');
106-
this.scheduleNextCapture(0);
111+
this.scheduleNextCapture(INITIAL_CAPTURE_DELAY_MS);
107112
}
108113

109114
/**

0 commit comments

Comments
 (0)