# 生命周期 客户端,实时会话与录音器的创建,释放与状态查询。 来源:https://open.shengzhiai.com/sdk/v2/guides/lifecycle/ 客户端线程安全,一个应用创建一个实例即可。客户端,实时会话与录音器都有显式的释放方法,全部可以重复调用,异常路径同样会关闭连接,取消定时器。 ## 创建,使用与释放 Java: ```java try (YuguClient client = YuguClient.builder() .apiKey(appKey, secretKey) .build()) { // 创建 EvalResult r = client.evaluate(audio, config); // 使用 StreamSession s = client.streamEvaluate(config, listener); s.sendAudio(pcm); s.end(); s.result().get(60, TimeUnit.SECONDS); } // 释放:close() 自动调用 ``` 安卓: ```kotlin class EvalActivity : Activity() { private lateinit var client: YuguClient private lateinit var recorder: Recorder private var session: StreamSession? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 创建 client = YuguClient.builder().auth(Auth.token(tokenFromYourServer)).build() recorder = Recorder(this) recorder.setListener(object : Recorder.Listener { override fun onLevel(level: Int) { runOnUiThread { volumeBar.progress = level } } override fun onError(error: YuguException) { session?.cancel() } }) } // 使用:每道题一个会话 fun startQuestion(refText: String) { val s = client.streamEvaluate(EvaluateConfig(CoreType.SENTENCE, refText, Language.ZH_CN), listener) session = s recorder.start(s) } fun finishQuestion() { recorder.stop() session?.end() } override fun onDestroy() { // 释放 recorder.release() session?.cancel() client.close() super.onDestroy() } } ``` iOS: ```swift final class ReadingViewController: UIViewController { private let client = YuguClient(options: YuguClientOptions( auth: .appKey("", secretKey: ""))) private let recorder = YuguRecorder() private let recorderHandlers = YuguRecorderHandlers() private var session: YuguStreamSession? func startReading(text: String) throws { let handlers = YuguStreamHandlers() handlers.onResult = { [weak self] result in self?.show(result) } handlers.onError = { [weak self] error in self?.show(error) } let session = try client.streamEvaluate( config: EvaluateConfig(coreType: .sentence, referenceText: text, language: "zh-CN"), listener: handlers) self.session = session recorderHandlers.onFrame = { frame in try? session.sendAudio(frame) } recorder.setListener(recorderHandlers) try session.start() try recorder.start() } func finishReading() { recorder.stop() try? session?.end() } deinit { session?.cancel() recorder.release() client.close() } } ``` 网页: ```js import { YuguClient, YuguRecorder } from '@shengzhiai/yugu-web-sdk'; // 创建 const client = new YuguClient({ token: () => fetch('/api/yugu-token').then((r) => r.text()) }); const recorder = new YuguRecorder({ listener: { onStateChanged: (_old, state) => console.log('录音器', state), onLevel: (level) => meter.value = level, onError: (e) => console.warn('录音出错', e.code, e.message), }, }); // 使用 async function readOnce(referenceText) { const session = client.streamEvaluate( { coreType: 'sentence', referenceText, language: 'zh-CN' }, { onStateChanged: (_old, state) => (statusLabel.textContent = state), onReconnecting: (attempt, delayMs) => console.log('重连中', attempt, delayMs), onResult: (result) => console.log('总分', result.overall), onError: (error) => console.warn('评测失败', error.code, error.message), }, ); await recorder.start({ session }); await waitForUserToFinish(); await recorder.stop(); session.end(); return session.waitForResult(); } // 释放 window.addEventListener('pagehide', () => { recorder.release(); client.close(); }); ``` 小程序: ```js const { YuguClient, RecorderState } = require('@shengzhiai/yugu-miniprogram-sdk'); Page({ data: { state: 'IDLE', overall: null }, onLoad() { this.client = new YuguClient({ auth: { token: getApp().globalData.token } }); this.recorder = this.client.createRecorder({ sampleRate: 16000, numberOfChannels: 1, format: 'PCM' }); this.recorder.setListener({ onError: (err) => wx.showToast({ title: '录音失败 ' + err.code, icon: 'none' }), }); }, start() { this.session = this.client.streamEvaluate( { coreType: 'sentence', referenceText: '今天天气很好', language: 'zh-CN' }, { onStateChanged: (oldState, newState) => this.setData({ state: newState }), onReconnecting: (attempt, delayMs) => console.log('重连', attempt, delayMs), onResult: (result) => this.setData({ overall: result.overall }), onError: (err) => wx.showToast({ title: err.message, icon: 'none' }), onClosed: () => console.log('会话结束', this.session.getState()), }, ); this.recorder.pipeTo(this.session); this.recorder.start(); }, stop() { if (this.recorder.getState() === RecorderState.RECORDING) this.recorder.stop(); }, onUnload() { if (this.session) this.session.close(); this.recorder.release(); this.client.close(); }, }); ``` ## 释放规则 | 对象 | 释放 | 释放后 | |---|---|---| | 客户端 | `close()` | 未结束的实时会话依次进入 `CANCELLED` 与 `CLOSED`,进行中的请求以 90004 结束,之后的调用以 90004 失败。小程序同时释放 `createRecorder` 创建的录音器 | | 实时会话 | `cancel()` 或 `close()` | 进入 `CANCELLED` 与 `CLOSED`,不再回调终评与错误,连接关闭回调照常触发 | | 录音器 | `release()` | 停止录音,释放麦克风,注销监听器 | Java 客户端实现 `AutoCloseable`,可以用 try-with-resources 管理,SDK 线程都是守护线程,不阻止 JVM 退出。示例里安卓在 `onDestroy` 释放,iOS 在 `deinit` 里释放,网页在 `pagehide` 事件里释放,小程序在页面 `onUnload` 里释放。 ## 状态查询 | 对象 | 查询 | |---|---| | 客户端 | 网页与小程序 `isClosed()`,Java `getOpenSessionCount()` 返回尚未关闭的会话数 | | 实时会话 | `getState()` 或 `state`,`isActive()` 在终态之前为 true | | 录音器 | 安卓,iOS,网页与小程序的录音器状态都是 `IDLE`,`RECORDING`,`PAUSED`,`STOPPED`,`RELEASED`,iOS 用 `recorder.state` 查询,其余用 `getState()` | ## 声通平替层 安卓平替: ```java // 创建:进入评测页面时 SkEgnManager manager = SkEgnManager.getInstance(context); EngineSetting es = EngineSetting.getInstance(context).setOnInitEngineListener(new OnInitEngineListener() { @Override public void onStartInitEngine() { } @Override public void onInitEngineSuccess() { /* 可以开始评测 */ } @Override public void onInitEngineFailed(String reason) { /* appKey 为空,地址不合法等 */ } }); manager.initEngine(appKey, secretKey, userId, es); // 使用:每道题一次 RecordSetting rs = new RecordSetting(CoreType.CN_SENT_EVAL, "今天天气很好"); rs.setDuration(15000); manager.startRecord(rs, new OnRecorderListener() { @Override public void onStart() { } @Override public void onStartRecordFail(String reason) { } @Override public void onPause() { } @Override public void onTick(long millisUntilFinished, double percentUntilFinished) { } @Override public void onRecordEnd() { } @Override public void onRecording(int vadStatus, int soundIntensity) { } @Override public void onScore(String json) { // 结果 JSON 或错误 JSON,见“结果 JSON”与“错误 JSON”两节 } }); manager.stopRecord(); // 释放:离开页面时,可以重复调用 manager.clearActivityListener(); manager.recycle(); ``` iOS 平替: ```objc #import @interface ReadingViewController () @end @implementation ReadingViewController - (void)setUpEngine { KYStartEngineConfig *config = [[KYStartEngineConfig alloc] init]; config.appKey = @"优谷雅言 appKey"; config.secretKey = @"优谷雅言 secretKey"; config.vadEnable = YES; [KYTestEngine sharedInstance].delegate = self; [[KYTestEngine sharedInstance] initEngine:KY_CloudEngine startEngineConfig:config finishBlock:^(BOOL isSuccess, NSString *str) { NSLog(@"init %d %@", isSuccess, str); }]; } - (void)startReading { KYTestConfig *test = [[KYTestConfig alloc] init]; test.coreType = KYTestType_Sentence_Cn; test.refText = @"今天天气很好"; test.duration = 10000; NSString *tokenId = [[KYTestEngine sharedInstance] startEngineWithTestConfig:test result:^(NSString *testResult) { NSData *data = [testResult dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL]; if (json[@"errId"]) { NSLog(@"errId %@ %@", json[@"errId"], json[@"error"]); } else { NSLog(@"overall %@", json[@"result"][@"overall"]); } } finishBlock:^(BOOL isSuccess, NSString *str) { NSLog(@"start %d %@", isSuccess, str); }]; NSLog(@"tokenId %@", tokenId); } - (void)stopReading { [[KYTestEngine sharedInstance] stopEngine]; // 结束录音并评测,结果回到 result block } - (void)leavePage { [[KYTestEngine sharedInstance] cancelEngine]; // 放弃本次评测,不再回调 } - (void)shutDown { [[KYTestEngine sharedInstance] deleteEngine]; // 释放引擎,可重复调用,之后需要重新 initEngine } @end ``` 平替层沿用声通的引擎单例与回调写法,引擎初始化一次,每道题调用一次开始与结束,页面退出时销毁引擎。