2
0

index.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import { useState, useEffect } from "react"
  2. import { Card, Button, Input, Form, message, Space, Typography, Divider, Alert } from "antd"
  3. import {
  4. InfoCircleOutlined,
  5. PlayCircleOutlined,
  6. StopOutlined,
  7. ReloadOutlined,
  8. EyeOutlined,
  9. EyeInvisibleOutlined,
  10. } from "@ant-design/icons"
  11. import { useNavigate } from "react-router-dom"
  12. import { genRandomUserId } from "@/common"
  13. import styles from "./index.module.scss"
  14. // 导入SDK核心模块
  15. import { SttSdk } from "../../../packages/stt-sdk-core/src"
  16. import type {
  17. ISttManagerAdapter,
  18. IRtmManagerAdapter,
  19. IRtcManagerAdapter,
  20. } from "../../../packages/stt-sdk-core/src/types"
  21. // 导入字幕显示组件
  22. import CaptionDisplay from "./components/caption-display"
  23. const { Title, Text } = Typography
  24. const SdkTestPage = () => {
  25. const nav = useNavigate()
  26. const [messageApi, contextHolder] = message.useMessage()
  27. const [form] = Form.useForm()
  28. const [sdk, setSdk] = useState<SttSdk | null>(null)
  29. const [sttManager, setSttManager] = useState<ISttManagerAdapter | null>(null)
  30. const [rtmManager, setRtmManager] = useState<IRtmManagerAdapter | null>(null)
  31. const [rtcManager, setRtcManager] = useState<IRtcManagerAdapter | null>(null)
  32. const [isSdkInitialized, setIsSdkInitialized] = useState(false)
  33. const [isSttManagerInitialized, setIsSttManagerInitialized] = useState(false)
  34. const [isRtcManagerJoined, setIsRtcManagerJoined] = useState(false)
  35. const [isTranscriptionActive, setIsTranscriptionActive] = useState(false)
  36. const [transcriptionStatus, setTranscriptionStatus] = useState<string>("idle")
  37. const [testResults, setTestResults] = useState<string[]>([])
  38. const [sttData, setSttData] = useState<any>({})
  39. const [captionVisible, setCaptionVisible] = useState(true)
  40. const [captionLanguages, setCaptionLanguages] = useState<string[]>(["live"])
  41. // 添加测试日志
  42. const addTestLog = (log: string) => {
  43. setTestResults((prev) => [...prev, `${new Date().toLocaleTimeString()}: ${log}`])
  44. }
  45. // 监听STT数据变化
  46. const onSttDataChanged = (data: any) => {
  47. console.log("[SDK Test] sttDataChanged:", data)
  48. setSttData(data)
  49. if (data.status === "start") {
  50. setIsTranscriptionActive(true)
  51. setTranscriptionStatus("active")
  52. addTestLog("📢 转录状态更新: 转录已开始")
  53. } else if (data.status === "end") {
  54. setIsTranscriptionActive(false)
  55. setTranscriptionStatus("stopped")
  56. addTestLog("📢 转录状态更新: 转录已停止")
  57. }
  58. // 如果有转录内容,记录到日志
  59. if (data.transcribe1 || data.transcribe2) {
  60. addTestLog(`📝 转录结果: ${data.transcribe1 || data.transcribe2}`)
  61. }
  62. // 如果有翻译内容,记录到日志
  63. if (data.translate1List?.length) {
  64. data.translate1List.forEach((text: string, index: number) => {
  65. if (text) {
  66. addTestLog(`🌐 翻译${index + 1}: ${text}`)
  67. }
  68. })
  69. }
  70. if (data.translate2List?.length) {
  71. data.translate2List.forEach((text: string, index: number) => {
  72. if (text) {
  73. addTestLog(`🌐 翻译${index + 2}: ${text}`)
  74. }
  75. })
  76. }
  77. }
  78. // 初始化SDK
  79. const initializeSdk = async (values: {
  80. appId: string
  81. certificate: string
  82. channel: string
  83. userName: string
  84. }) => {
  85. try {
  86. addTestLog("开始初始化SDK...")
  87. const newSdk = new SttSdk()
  88. await newSdk.initialize({
  89. appId: values.appId,
  90. certificate: values.certificate,
  91. logLevel: "info",
  92. })
  93. setSdk(newSdk)
  94. setIsSdkInitialized(true)
  95. addTestLog("✅ SDK初始化成功")
  96. // 创建管理器 - 先创建RTM管理器,然后创建STT管理器并传入RTM管理器
  97. const rtmManager = newSdk.createRtmManager()
  98. const sttManager = newSdk.createSttManager(rtmManager)
  99. const rtcManager = newSdk.createRtcManager()
  100. // 监听RTM管理器的事件
  101. rtmManager.on("sttDataChanged", onSttDataChanged)
  102. setSttManager(sttManager)
  103. setRtmManager(rtmManager)
  104. setRtcManager(rtcManager)
  105. addTestLog("✅ STT、RTM和RTC管理器创建成功")
  106. messageApi.success("SDK初始化成功")
  107. } catch (error) {
  108. addTestLog(`❌ SDK初始化失败: ${error}`)
  109. messageApi.error(`SDK初始化失败: ${error}`)
  110. }
  111. }
  112. // 初始化STT管理器(会自动加入RTM频道)
  113. const initializeSttManager = async () => {
  114. if (!sttManager || !form) return
  115. try {
  116. const values = form.getFieldsValue()
  117. addTestLog("开始初始化STT管理器...")
  118. await sttManager.init({
  119. userId: genRandomUserId(),
  120. channel: values.channel,
  121. userName: values.userName,
  122. })
  123. setIsSttManagerInitialized(true)
  124. addTestLog("✅ STT管理器初始化成功(已自动加入RTM频道)")
  125. messageApi.success("STT管理器初始化成功")
  126. } catch (error) {
  127. addTestLog(`❌ STT管理器初始化失败: ${error}`)
  128. messageApi.error(`STT管理器初始化失败: ${error}`)
  129. }
  130. }
  131. // 开始转录
  132. const startTranscription = async () => {
  133. if (!sttManager) return
  134. try {
  135. addTestLog("开始语音转录...")
  136. setTranscriptionStatus("starting")
  137. await sttManager.startTranscription({
  138. // languages: [{ source: "en-US", target: ["zh-CN"] }],
  139. languages: [{ source: "zh-CN" }],
  140. })
  141. setIsTranscriptionActive(true)
  142. setTranscriptionStatus("active")
  143. addTestLog("✅ 语音转录已开始")
  144. messageApi.success("语音转录已开始")
  145. } catch (error) {
  146. addTestLog(`❌ 语音转录启动失败: ${error}`)
  147. setTranscriptionStatus("error")
  148. messageApi.error(`语音转录启动失败: ${error}`)
  149. }
  150. }
  151. // 停止转录
  152. const stopTranscription = async () => {
  153. if (!sttManager) return
  154. try {
  155. addTestLog("停止语音转录...")
  156. setTranscriptionStatus("stopping")
  157. await sttManager.stopTranscription()
  158. setIsTranscriptionActive(false)
  159. setTranscriptionStatus("stopped")
  160. addTestLog("✅ 语音转录已停止")
  161. messageApi.success("语音转录已停止")
  162. } catch (error) {
  163. addTestLog(`❌ 语音转录停止失败: ${error}`)
  164. setTranscriptionStatus("error")
  165. messageApi.error(`语音转录停止失败: ${error}`)
  166. }
  167. }
  168. // 查询转录状态
  169. const queryTranscription = async () => {
  170. if (!sttManager) return
  171. try {
  172. addTestLog("查询转录状态...")
  173. const result = await sttManager.queryTranscription()
  174. addTestLog(`📊 转录状态查询结果: ${JSON.stringify(result)}`)
  175. messageApi.info("转录状态查询完成,查看日志了解详情")
  176. } catch (error) {
  177. addTestLog(`❌ 转录状态查询失败: ${error}`)
  178. messageApi.error(`转录状态查询失败: ${error}`)
  179. }
  180. }
  181. // 加入RTC频道
  182. const joinRtcChannel = async () => {
  183. if (!rtcManager) return
  184. try {
  185. const values = form.getFieldsValue()
  186. addTestLog("开始加入RTC频道...")
  187. await rtcManager.join({
  188. channel: values.channel,
  189. userId: genRandomUserId(),
  190. })
  191. setIsRtcManagerJoined(true)
  192. addTestLog("✅ RTC频道加入成功")
  193. messageApi.success("RTC频道加入成功")
  194. } catch (error) {
  195. addTestLog(`❌ RTC频道加入失败: ${error}`)
  196. messageApi.error(`RTC频道加入失败: ${error}`)
  197. }
  198. }
  199. // 创建音视频轨道
  200. const createRtcTracks = async () => {
  201. if (!rtcManager) return
  202. try {
  203. addTestLog("开始创建音视频轨道...")
  204. await rtcManager.createTracks()
  205. addTestLog("✅ 音视频轨道创建成功")
  206. messageApi.success("音视频轨道创建成功")
  207. } catch (error) {
  208. addTestLog(`❌ 音视频轨道创建失败: ${error}`)
  209. messageApi.error(`音视频轨道创建失败: ${error}`)
  210. }
  211. }
  212. // 发布音视频流
  213. const publishRtcStream = async () => {
  214. if (!rtcManager) return
  215. try {
  216. addTestLog("开始发布音视频流...")
  217. await rtcManager.publish()
  218. addTestLog("✅ 音视频流发布成功")
  219. messageApi.success("音视频流发布成功")
  220. } catch (error) {
  221. addTestLog(`❌ 音视频流发布失败: ${error}`)
  222. messageApi.error(`音视频流发布失败: ${error}`)
  223. }
  224. }
  225. // 清理资源
  226. const cleanup = async () => {
  227. try {
  228. addTestLog("开始清理资源...")
  229. if (isTranscriptionActive && sttManager) {
  230. await stopTranscription()
  231. }
  232. if (sttManager) {
  233. await sttManager.destroy()
  234. setSttManager(null)
  235. setIsSttManagerInitialized(false)
  236. }
  237. if (rtmManager) {
  238. rtmManager.off("sttDataChanged", onSttDataChanged)
  239. await rtmManager.destroy()
  240. setRtmManager(null)
  241. }
  242. if (sdk) {
  243. await sdk.destroy()
  244. setSdk(null)
  245. setIsSdkInitialized(false)
  246. }
  247. addTestLog("✅ 资源清理完成")
  248. setTranscriptionStatus("idle")
  249. messageApi.success("资源清理完成")
  250. } catch (error) {
  251. addTestLog(`❌ 资源清理失败: ${error}`)
  252. messageApi.error(`资源清理失败: ${error}`)
  253. }
  254. }
  255. // 切换字幕显示
  256. const toggleCaptionVisibility = () => {
  257. setCaptionVisible(!captionVisible)
  258. addTestLog(`📺 字幕显示: ${!captionVisible ? "开启" : "关闭"}`)
  259. }
  260. // 切换语言显示
  261. const toggleLanguage = (language: string) => {
  262. setCaptionLanguages((prev) => {
  263. if (prev.includes(language)) {
  264. return prev.filter((lang) => lang !== language)
  265. } else {
  266. return [...prev, language]
  267. }
  268. })
  269. addTestLog(`🌐 语言显示: ${captionLanguages.includes(language) ? "关闭" : "开启"} ${language}`)
  270. }
  271. // 返回主应用
  272. const goToMainApp = () => {
  273. nav("/home")
  274. }
  275. // 组件卸载时清理资源
  276. useEffect(() => {
  277. return () => {
  278. if (sdk) {
  279. cleanup()
  280. }
  281. }
  282. }, [sdk])
  283. return (
  284. <div className={styles.sdkTestPage}>
  285. {contextHolder}
  286. <div className={styles.header}>
  287. <Title level={2}>🎯 SDK 功能测试页面</Title>
  288. <Text type="secondary">测试 STT SDK 核心功能的集成和兼容性</Text>
  289. </div>
  290. <div className={styles.content}>
  291. <div className={styles.leftPanel}>
  292. <Card title="🔧 SDK 配置" className={styles.configCard}>
  293. <Form
  294. form={form}
  295. layout="vertical"
  296. initialValues={{
  297. appId: import.meta.env.VITE_AGORA_APP_ID || "",
  298. certificate: import.meta.env.VITE_AGORA_APP_CERTIFICATE || "",
  299. channel: `test-channel-${Date.now()}`,
  300. userName: `TestUser-${Date.now()}`,
  301. }}
  302. >
  303. <Form.Item
  304. label="App ID"
  305. name="appId"
  306. rules={[{ required: true, message: "请输入 App ID" }]}
  307. >
  308. <Input placeholder="请输入 Agora App ID" />
  309. </Form.Item>
  310. <Form.Item
  311. label="Certificate"
  312. name="certificate"
  313. rules={[{ required: true, message: "请输入 Certificate" }]}
  314. >
  315. <Input placeholder="请输入 Agora Certificate" />
  316. </Form.Item>
  317. <Form.Item
  318. label="频道名称"
  319. name="channel"
  320. rules={[{ required: true, message: "请输入频道名称" }]}
  321. >
  322. <Input placeholder="请输入频道名称" />
  323. </Form.Item>
  324. <Form.Item
  325. label="用户名称"
  326. name="userName"
  327. rules={[{ required: true, message: "请输入用户名称" }]}
  328. >
  329. <Input placeholder="请输入用户名称" />
  330. </Form.Item>
  331. <Form.Item>
  332. <Space>
  333. <Button
  334. type="primary"
  335. onClick={() => initializeSdk(form.getFieldsValue())}
  336. disabled={isSdkInitialized}
  337. >
  338. {isSdkInitialized ? "✅ 已初始化" : "初始化 SDK"}
  339. </Button>
  340. <Button onClick={cleanup} danger disabled={!isSdkInitialized}>
  341. 清理资源
  342. </Button>
  343. </Space>
  344. </Form.Item>
  345. </Form>
  346. </Card>
  347. <Card title="🚀 功能测试" className={styles.testCard}>
  348. <Space direction="vertical" style={{ width: "100%" }}>
  349. <Button
  350. onClick={initializeSttManager}
  351. disabled={!isSdkInitialized || isSttManagerInitialized}
  352. block
  353. >
  354. {isSttManagerInitialized ? "✅ STT管理器已初始化" : "初始化STT管理器"}
  355. </Button>
  356. <Divider />
  357. <Button
  358. type="primary"
  359. icon={<PlayCircleOutlined />}
  360. onClick={startTranscription}
  361. disabled={!isSttManagerInitialized || isTranscriptionActive}
  362. block
  363. >
  364. {isTranscriptionActive ? "转录进行中..." : "开始转录"}
  365. </Button>
  366. <Button
  367. danger
  368. icon={<StopOutlined />}
  369. onClick={stopTranscription}
  370. disabled={!isTranscriptionActive}
  371. block
  372. >
  373. 停止转录
  374. </Button>
  375. <Button
  376. icon={<ReloadOutlined />}
  377. onClick={queryTranscription}
  378. disabled={!isTranscriptionActive}
  379. block
  380. >
  381. 查询状态
  382. </Button>
  383. <Divider />
  384. <Button
  385. onClick={joinRtcChannel}
  386. disabled={!isSdkInitialized || isRtcManagerJoined}
  387. block
  388. >
  389. {isRtcManagerJoined ? "✅ RTC频道已加入" : "加入RTC频道"}
  390. </Button>
  391. <Button onClick={createRtcTracks} disabled={!isRtcManagerJoined} block>
  392. 创建音视频轨道
  393. </Button>
  394. <Button
  395. type="primary"
  396. onClick={publishRtcStream}
  397. disabled={!isRtcManagerJoined}
  398. block
  399. >
  400. 发布音视频流
  401. </Button>
  402. </Space>
  403. <Divider />
  404. <Alert
  405. message="转录状态"
  406. description={
  407. <div>
  408. <div>
  409. {transcriptionStatus === "idle"
  410. ? "等待开始转录"
  411. : transcriptionStatus === "starting"
  412. ? "正在启动转录..."
  413. : transcriptionStatus === "active"
  414. ? "转录进行中"
  415. : transcriptionStatus === "stopping"
  416. ? "正在停止转录..."
  417. : transcriptionStatus === "stopped"
  418. ? "转录已停止"
  419. : "状态异常"}
  420. </div>
  421. {sttData.status && (
  422. <div style={{ marginTop: 8, fontSize: "12px", color: "#666" }}>
  423. <div>任务ID: {sttData.taskId || "未设置"}</div>
  424. <div>
  425. 开始时间:{" "}
  426. {sttData.startTime
  427. ? new Date(sttData.startTime).toLocaleTimeString()
  428. : "未开始"}
  429. </div>
  430. <div>
  431. 持续时间: {sttData.duration ? `${sttData.duration / 1000}秒` : "未设置"}
  432. </div>
  433. </div>
  434. )}
  435. </div>
  436. }
  437. type={
  438. transcriptionStatus === "active"
  439. ? "success"
  440. : transcriptionStatus === "error"
  441. ? "error"
  442. : "info"
  443. }
  444. showIcon
  445. />
  446. </Card>
  447. </div>
  448. <div className={styles.rightPanel}>
  449. <Card title="📋 测试日志" className={styles.logCard}>
  450. <div className={styles.logContainer}>
  451. {testResults.length === 0 ? (
  452. <Text type="secondary">暂无测试日志,请开始测试...</Text>
  453. ) : (
  454. testResults.map((log, index) => (
  455. <div key={index} className={styles.logEntry}>
  456. {log}
  457. </div>
  458. ))
  459. )}
  460. </div>
  461. {testResults.length > 0 && (
  462. <Button onClick={() => setTestResults([])} size="small" style={{ marginTop: 16 }}>
  463. 清空日志
  464. </Button>
  465. )}
  466. </Card>
  467. <Card title="💡 使用说明" className={styles.infoCard}>
  468. <Space direction="vertical" style={{ width: "100%" }}>
  469. <Text type="secondary">
  470. <InfoCircleOutlined /> 测试步骤:
  471. </Text>
  472. <ol>
  473. <li>填写 App ID 和 Certificate</li>
  474. <li>点击"初始化 SDK"</li>
  475. <li>初始化 STT 管理器和加入 RTM 频道</li>
  476. <li>开始/停止转录测试</li>
  477. <li>加入 RTC 频道并测试音频传输</li>
  478. <li>查看测试日志了解详细结果</li>
  479. </ol>
  480. <Button type="link" onClick={goToMainApp}>
  481. 返回主应用
  482. </Button>
  483. </Space>
  484. </Card>
  485. <Card
  486. title={
  487. <Space>
  488. <span>📺 实时转录显示</span>
  489. <Button
  490. type="text"
  491. size="small"
  492. icon={captionVisible ? <EyeOutlined /> : <EyeInvisibleOutlined />}
  493. onClick={toggleCaptionVisibility}
  494. >
  495. {captionVisible ? "隐藏" : "显示"}
  496. </Button>
  497. </Space>
  498. }
  499. className={styles.captionCard}
  500. extra={
  501. <Space>
  502. <Button
  503. size="small"
  504. type={captionLanguages.includes("live") ? "primary" : "default"}
  505. onClick={() => toggleLanguage("live")}
  506. >
  507. 原文
  508. </Button>
  509. <Button
  510. size="small"
  511. type={captionLanguages.includes("translate1") ? "primary" : "default"}
  512. onClick={() => toggleLanguage("translate1")}
  513. >
  514. 翻译1
  515. </Button>
  516. <Button
  517. size="small"
  518. type={captionLanguages.includes("translate2") ? "primary" : "default"}
  519. onClick={() => toggleLanguage("translate2")}
  520. >
  521. 翻译2
  522. </Button>
  523. </Space>
  524. }
  525. >
  526. <CaptionDisplay
  527. visible={captionVisible}
  528. captionLanguages={captionLanguages}
  529. sttData={sttData}
  530. />
  531. </Card>
  532. </div>
  533. </div>
  534. </div>
  535. )
  536. }
  537. export default SdkTestPage