管理媒体采集与播放
媒体能力的核心不是一串字节,而是用户授权后产生、可随时撤销的设备句柄。
{
"apiVersion": "0.1",
"canvases": { "preview": { "mode": "pixels" } },
"capabilities": [
"host:capture.requestCamera",
"host:capture.requestMic",
"host:canvas.present",
"host:playback.open"
],
"events": ["capture.ended", "playback.ready", "playback.ended"],
"apiLimits": {}
}
读取、写入和停止方法都由已取得的媒体句柄授权,不出现在 capability 列表。
1. 在用户操作中申请
let cameraStream;
startButton.onclick = async () => {
cameraStream = await host.capture.requestCamera({
facing: 'user', maxWidth: 1280, maxHeight: 720,
});
if (cameraStream) startPreview(cameraStream);
};
用户取消返回 null。不要在定时器或页面启动时自动申请。
2. 读取与背压
async function readOneFrame(stream, afterSequence) {
const bytes = framePool.acquire(stream.frameBytes);
const frame = await host.capture.frame(stream.camera, bytes, afterSequence);
return { bytes: bytes.subarray(0, frame.written), frame };
}
复用缓冲;frame() 自己异步等待更新的 sequence,不需要另一个可读事件。不要并发读取同一句柄。
3. 预览或处理
将摄像头帧处理后交给 canvas.present():
const { bytes, frame } = await readOneFrame(cameraStream);
applyEffect(bytes, frame);
await host.canvas.present('preview', bytes, {
...frame,
format: 'rgba8',
});
句柄不穿过 asset/file token;只有已校验字节进入应用处理路径。
4. 播放 guest 产生的音频
const output = await host.playback.open({ channels: 1, latency: 'interactive' });
host.events.on('playback.ready', ({ writableFrames }) => {
host.playback.write(output.output, synth.renderBytes(writableFrames));
});
capture.readAudio() 与 playback.write() 可以组合,但回声消除、混音和重采样属于 guest 库。
5. 撤销与清理
host.events.on('capture.ended', ({ handle }) => detach(handle));
await host.capture.stop(cameraStream.camera);
await host.playback.stop(output.output, 'immediate');
事件送达后句柄已失效。只清理本地状态,不要对失效句柄循环重试。