@scooper/meeting-sdk 使用指南
本文面向需要在自己的前端页面中接入视频会商或普通视频播放的第三方工程师。建议先按本文完成一条最小链路,再根据 API 参考、事件对照 和 后台监听桥接 扩展能力。
目录
- 1. 先判断是否适用
- 2. 选择引入方式
- 3. 准备依赖和静态资源
- 4. 会议模式最小示例
- 5. 播放模式最小示例
- 6. 核心配置与 token
- 7. 推荐生命周期
- 8. 会议、成员和视频操作
- 9. 事件与回调
- 10. Promise、同步方法与错误
- 11. 可选软手柄
- 12. 可选后台监听桥接
- 13. React/Vite 示例
- 14. 安全与部署检查
- 15. 故障排查
- 16. 相关文档
1. 先判断是否适用
SDK 是浏览器侧封装层,不是 token 签发服务,也不是业务后台。它把以下能力收敛到一个实例上:
| 能力 | 需要的底层对象 | 适用模式 |
|---|---|---|
| 调度登录、创建/加入会议、成员控制、会议通知 | scooper.dispatch,以及它的 meets / calls | meeting |
| 视频窗口、播放、关闭、分屏和轮巡 | VideoWebRtc(某些部署也提供 scooper.video) | meeting、player |
| SIP/Janus 软手柄注册、呼入、应答、挂断 | ScSoftHandler.createSoftHandler | 可选 |
| 由第三方后台转发 SSE/WebSocket 通知 | backendListener | 可选 |
mode: "meeting" 是默认模式,通常需要调度依赖和登录 token。mode: "player" 只初始化视频控制器,不会自动连接或登录 scooper.dispatch,适合已经有视频编号、只需要播放控制的页面。
SDK 不会替业务系统完成以下工作:
- 申请或刷新业务 token、判断当前用户是否有权操作某个会场。
- 替第三方后台实现业务审计、数据脱敏、跨域代理或权限校验。
- 保证后端一定推送某个事件;事件还受
dispatch-web版本、账号权限、网络和实际业务操作影响。
开始前至少确认:
- 页面使用 HTTPS(本地开发可使用受控的
localhost),浏览器允许 WebRTC、摄像头/麦克风和必要的自动播放权限。 - 页面可以访问实际部署的 dispatch、视频服务和可选 Janus/SIP 服务,跨域响应包含正确的 CORS 配置。
- 视频容器在调用
init()或initVideo()时已经存在,例如<div id="video-root"></div>。 - 已确定依赖脚本的部署路径、调度账号 token 来源、视频编号格式,以及会议操作所需的账号权限。
2. 选择引入方式
包根 ESM(bundler/Node 推荐)
通过 Vite、webpack、Rollup 或其他支持 package.json exports 的工具,从包根路径引入:
import ScooperMeetingSDK, {
EVENT,
SDKError,
loadDependencies
} from "@scooper/meeting-sdk";
const sdk = ScooperMeetingSDK.create({ mode: "player" });
也可以直接使用 ESM 具名的 create:
import { create, EVENT } from "@scooper/meeting-sdk";
const sdk = create({ mode: "player" });
console.log(EVENT.VIDEO_PLAY_SUCCESS);
包根的 import 条件指向 src/index.mjs,默认导出是 SDK 构造器,具名导出包括 EVENT、SDKError、VERSION、DISPATCH_EVENT_MAP、create 和 loadDependencies。业务代码应优先使用包根路径,不要把 UMD 文件路径当作 ESM 模块路径。
TypeScript 类型位于 types/index.d.ts,当前采用 export = 声明。使用默认导入时打开 esModuleInterop;也可以使用 TypeScript 的 CommonJS 写法:
import ScooperMeetingSDK = require("@scooper/meeting-sdk");
const sdk = ScooperMeetingSDK.create({ mode: "player" });
CommonJS
const ScooperMeetingSDK = require("@scooper/meeting-sdk");
const sdk = ScooperMeetingSDK.create({ mode: "player" });
const { EVENT, SDKError } = ScooperMeetingSDK;
经典浏览器脚本
不使用模块打包器时,先以经典 <script> 按顺序加载底层依赖,再加载 UMD 主文件。加载后使用全局 ScooperMeetingSDK:
<script src="/dispatch-web/static/js/lib/jquery-3.6.0.min.js"></script>
<script src="/dispatch-web/static/js/org/cometd.js"></script>
<script src="/dispatch-web/static/js/lib/jquery.cometd.js"></script>
<script src="/dispatch-web/static/js/scooper.util.js"></script>
<script src="/dispatch-web/static/js/scooper.sse.js"></script>
<script src="/dispatch-web/static/js/scooper.dispatch.js"></script>
<script src="/scooper-video-player/scooper.video.min.js"></script>
<script src="/sc-soft-handler/index.umd.js"></script>
<script src="/scooper-meeting-sdk/src/scooper-meeting-sdk.js"></script>
<script>
const sdk = ScooperMeetingSDK.create({ mode: "player" });
</script>
软手柄脚本只在启用 softHandler.enabled 时需要。只做播放器时可以省略调度和软手柄脚本,只加载视频脚本。
原生浏览器 ESM 的当前行为
src/index.mjs 当前会 namespace-import src/scooper-meeting-sdk.js,然后从 UMD 模块的 default 或浏览器全局 ScooperMeetingSDK 解析 SDK,再导出默认和具名成员。因此,在通过 HTTP(S) 提供、且入口文件旁边的 UMD 文件也能访问的页面中,下面这种相对路径加载是可行的:
<script type="module">
import ScooperMeetingSDK, { EVENT } from "/vendor/meeting-sdk/src/index.mjs";
const sdk = ScooperMeetingSDK.create({ mode: "player" });
sdk.on(EVENT.VIDEO_PLAY_SUCCESS, console.log);
</script>
这里的路径只是示例,需替换为实际静态资源路径;不要用 file:// 直接打开,并确认浏览器可以继续请求 ./scooper-meeting-sdk.js。部署页面仍建议通过 bundler 使用包根导入;不需要模块化时使用经典脚本更简单。
不要直接这样写:
import ScooperMeetingSDK, { EVENT } from "./src/scooper-meeting-sdk.js";
该 文件是 UMD,不提供可被静态分析的 ESM default/具名导出。Node 或 bundler 中直接从它导入,常见会出现 Named export 'EVENT' not found;浏览器原生 ESM 中直接请求它也可能出现 does not provide an export named 'default'。请改用包根导入、src/index.mjs 包装入口,或经典脚本的全局对象。
3. 准备依赖和静态资源
让 SDK 顺序加载脚本
loadDependencies() 是第三方页面的便捷入口,会串行加载脚本并在结束时检查全局对象。会议模式的默认调度链是 jquery、CometD、jquery CometD、util、SSE、dispatch,随后加载视频脚本;软手柄默认关闭:
await ScooperMeetingSDK.loadDependencies({
dispatchBaseUrl: "/dispatch-web/",
videoScript: "/scooper-video-player/scooper.video.min.js"
});
需要软手柄时显式打开:
await ScooperMeetingSDK.loadDependencies({
dispatchBaseUrl: "/dispatch-web/",
videoScript: "/scooper-video-player/scooper.video.min.js",
softHandler: true,
softHandlerScript: "/vendor/sc-soft-handler/index.umd.js"
});
播放器不需要 dispatch 时关闭调度组:
await ScooperMeetingSDK.loadDependencies({
dispatch: false,
videoScript: "/scooper-video-player/scooper.video.min.js"
});
dispatchBaseUrl、videoBaseUrl、softHandlerBaseUrl 和 baseUrl 只负责拼接相对脚本路径;也可以用 dispatchScripts、videoScript、softHandlerScript 和 extraScripts 完全指定路径。默认 ensureGlobals 为 true,加载后找不到 scooper.dispatch、VideoWebRtc 或软手柄工厂时会失败。脚本路径变更后如需重新尝试,可调用 ScooperMeetingSDK.clearDependencyCache()。
生产资源管理建议
生产环境建议由宿主工程统一管理并固定底层资源版本,确保加载顺序和缓存策略可控。使用 loadDependencies() 时,最终应能看到:
window.scooper.dispatch:会议模式必需。window.VideoWebRtc或可被 SDK 解析到的window.scooper.video:视频模式必需。window.ScSoftHandler.createSoftHandler:启用软手柄时必需。
脚本加载成功不等于已经登录或有权访问业务数据。仍需调用 sdk.init(),并验证实际 token、账号权限和后端连接。
4. 会议模式最小示例
下面示例展示一条“加载依赖 -> 创建 SDK -> 初始化/自动登录 -> 创建会议 -> 视频成员入会并播放”的最小链路。Sc-Token 只是示例 key,不代表具体系统必须使用这个 key。
页面先准备容器:
<div id="video-root"></div>
前端代码:
import ScooperMeetingSDK, { EVENT, SDKError } from "@scooper/meeting-sdk";
const getToken = () => sessionStorage.getItem("Sc-Token") || "";
await ScooperMeetingSDK.loadDependencies({
dispatchBaseUrl: "/dispatch-web/",
videoScript: "/scooper-video-player/scooper.video.min.js"
});
const sdk = ScooperMeetingSDK.create({
mode: "meeting",
dispatch: {
remoteBaseUrl: "https://example.com/dispatch-web/",
token: getToken
},
video: {
container: "#video-root",
options: {
windows: 4,
windowsNum: 4,
isScooperMeet: true,
showVideoName: true
}
},
callbacks: {
onMeetingStatus: (meet) => console.log("会议状态", meet),
onMeetingMember: (member) => console.log("成员变化", member),
onError: (error) => console.error(error.code, error.message)
}
});
const offError = sdk.on(EVENT.ERROR, (error) => {
console.error("SDK error", error.code, error.message);
});
try {
await sdk.init();
const result = await sdk.createMeeting({
name: "第三方视频会商",
businessId: "demo-business",
members: [
{ tel: "1001", level: "chairman" },
{ tel: "1002", level: "speak" }
],
mediaType: "video"
});
const data = result && result.data;
const meetId = data && (data.meetId || data.meet_id);
if (!meetId) throw new Error("createMeeting result does not contain meetId");
// meetId + tel 是异步的视频会商流程:入会 -> 等待成员通知 -> 播放。
const playback = await sdk.playVideo({
meetId,
tel: "1002",
index: 0,
mediaType: "video",
autoAnswer: true,
joinNotifyTimeout: 15000
});
console.log("视频会商已请求", playback.playResult);
} catch (error) {
if (error instanceof SDKError) {
console.error("SDK 请求失败", error.code, error.message, error.cause);
} else {
console.error("业务流程失败", error);
}
}
// 页面卸载或组件卸载时执行;不需要长期保留本实例。
async function dispose() {
offError();
await sdk.destroy();
}
createMeeting() 的底层返回受部署版本影响,示例只读取当前实现和现有示例中使用的 result.data.meetId / result.data.meet_id。生产代码应根据实际后端响应检查会议 ID,不要把任意响应字段硬编码成通用协议。
playVideo({ meetId, tel }) 会委托 playMeetingVideo():默认执行视频入会,等待匹配的 meeting:member 入会通知,再调用视频控制器。成功值是包含 joinResponse、memberNotify、video 和 playResult 的结果对象;等待超时会以 JOIN_NOTIFY_TIMEOUT reject。已经在会中的成员不会重复入会,已经播放的画面也不会被重复打断。
如果页面要先音频入会,再决定是否上屏,也可以拆开调用:
await sdk.joinMeeting({
meetId,
tel: "1003",
mediaType: "audio",
autoAnswer: true
});
await sdk.playVideo({
meetId,
tel: "1003",
index: 1,
mediaType: "video"
});
5. 播放模式最小示例
播放模式只关心 视频控制器,不需要调度会议登录:
<div id="video-root"></div>
import ScooperMeetingSDK, { SDKError } from "@scooper/meeting-sdk";
await ScooperMeetingSDK.loadDependencies({
dispatch: false,
videoScript: "/scooper-video-player/scooper.video.min.js"
});
const sdk = ScooperMeetingSDK.create({
mode: "player",
video: {
container: "#video-root",
options: {
windows: 4,
windowsNum: 4,
isVideoweb: true
}
}
});
await sdk.init();
try {
// 没有 meetId + tel 时是普通视频控制,直接调用底层 controller.play。
// 这个重载是同步的,返回底层播放结果,不承诺固定对象结构。
const result = sdk.playVideo({
index: 0,
video: "34020000001320000001",
id: "camera-1",
name: "门口摄像头"
});
console.log("已调用播放", result);
} catch (error) {
if (error instanceof SDKError) {
console.error(error.code, error.message);
} else {
console.error(error);
}
}
// 这些也是同步视频控制方法。
sdk.closeVideo(0, { isSave: false });
sdk.closeAllVideos(false);
这里的 sdk.playVideo({ video, ... }) 与上一节的 sdk.playVideo({ meetId, tel }) 不是同一条链路:前者不入会、不等待会议通知、直接返回视频控制器的同步结果;后者会返回 Promise,并由 SDK 处理入会、软手柄自应答和通知等待。即使播放器调用最终使用 await,也不能把普通视频重载当成会议工作流。
视频服务需要鉴权时,应按实际 scooper.video 部署约定提供运行时 token。会议模式下 SDK 会从 dispatch.token getter 读取 token;播放器模式没有 dispatch,不能假设它会自动拥有调度 token。
6. 核心配置与 token
配置是可分组的浅层对象,通常只需要覆盖下面这些字段:
| 配置 | 常用字段 | 说明 |
|---|---|---|
| 顶层 | mode、autoLogin、requestTimeout | 默认 meeting、自动登录、请求等待 10000 ms。 |
meeting | autoPlayMemberVideo、autoCloseMemberVideo | 成员入会自动上屏、离会自动关窗,默认都为 true。 |
dispatch | enabled、instance、type、remoteBaseUrl、token、scAuth、skipInitialize | 会议/呼叫能力及调度初始化方式。可注入已有 instance。 |
video | container、autoInit、options、syncToken、syncTokenKeys | 视频容器、视频引擎选项和 token 同步策略。 |
softHandler | enabled、configs、registerInfo、自动应答开 关 | 仅在需要软手柄时配置。 |
backendListener | enabled、transport、url、token、query | 可选 SSE/WebSocket 通知桥接。 |
callbacks | onEvent、具名 onMeetingMember 等 | 初始化时注册的回调。 |
token getter
dispatch.token 和 login(token?) 都支持字符串或函数。优先传 getter,避免在页面初始化前固定一份可能过期的 token:
const getToken = () => sessionStorage.getItem("Sc-Token") || "";
const sdk = ScooperMeetingSDK.create({
autoLogin: true,
dispatch: {
token: getToken
}
});
await sdk.init();
默认 autoLogin: true 时,init() 只有在 dispatch.token 有值时才会调用调度登录。如果 token 在登录页异步取得,可以关闭自动登录,先初始化依赖,再显式登录:
const sdk = ScooperMeetingSDK.create({
autoLogin: false,
dispatch: { token: getToken }
});
await sdk.init();
await sdk.login();
也可以把本次 token 直接传给 login(token)。缺少 token 会 reject TOKEN_REQUIRED;token 过期或后台拒绝会 reject LOGIN_FAILED,并触发 sdk:error / onError。
视频 token 同步
视频控制器默认从 sessionStorage 读取鉴权信息。SDK 默认只同步到 sc-auth、meetWebToken、videoWebToken,不会写入宿主常用的裸 token key:
const sdk = ScooperMeetingSDK.create({
video: {
container: "#video-root",
syncToken: true,
syncTokenKeys: ["sc-auth", "meetWebToken", "videoWebToken"]
}
});
如果宿主不希望 SDK 触碰 sessionStorage,设置 video.syncToken: false。自定义 key 前先确认视频引擎的读取约定,避免把业务登录态覆盖掉。
7. 推荐生命周期
把 SDK 实例当作一个有明确边界的会话对象管理,推荐顺序如下:
加载依赖/确认静态资源
-> 创建 sdk 实例
-> 注册 on()/once()/onAny() 或 callbacks
-> init()(会议模式按配置自动 login,或随后显式 login)
-> createMeeting()/joinMeeting()/playVideo() 或普通 player 播放
-> 会议、成员、视频、软手柄控制
-> 页面/组件卸载时 destroy()
实现时注意:
init()会按配置初始化 dispatch、视频、软手柄和后台监听;同一个实例已初始化时会直接返回该实例。- 会议模式的会议/呼叫方法依赖 dispatch。播放器模式只初始化视频控制器。
createMeeting()/joinMeeting()成功后,SDK 会记录currentMeet;省略meetId的成员查询和全员禁言等方法会使用它。destroy()会关闭视频、软手柄和后台监听,并清理本地会议缓存。默认还会清空on()/onAny()注册的监听;传{ keepListeners: true }可以保留。配置中的callbacks仍挂在实例配置上。- 默认不会销毁全局或注入的 dispatch;只有传
{ destroyDispatch: true }或配置dispatch.destroyOnSdkDestroy: true才会销毁它。多个业务页面共用 dispatch 时尤其要谨慎。
React 等组件框架中,实例通常放在组件生命周期之外或 useRef 中,组件卸载时执行 await sdk.destroy()。如果随后复用同一实例重新 init(),并且没有设置 keepListeners,需要重新注册 on() 监听。
8. 会议、成员和视频操作
会议操作
常见的任务路径如下,完整参数请查 API 参考:
const meeting = await sdk.createMeeting({
name: "第三方视频会商",
businessId: "case-001",
members: [
{ tel: "1001", level: "chairman" },
{ tel: "1002", level: "speak" }
],
mediaType: "video"
});
const meetingData = meeting && meeting.data;
const meetId = meetingData && (meetingData.meetId || meetingData.meet_id);
if (!meetId) throw new Error("createMeeting result does not contain meetId");
await sdk.editMeeting(meetId, { name: "更新后的会商名称" });
const detail = await sdk.getMeeting(meetId);
const ownMeetings = await sdk.listMeetings({ mine: true });
await sdk.joinMeeting({
meetId,
tel: "1003",
mediaType: "audio",
level: "audience",
autoAnswer: true
});
await sdk.joinMembers({
meetId,
members: [{ tel: "1004", level: "speak" }]
});
await sdk.kickMember(meetId, "1004");
await sdk.lockMeeting(meetId);
await sdk.startRecord(meetId);
await sdk.stopRecord(meetId);
await sdk.unlockMeeting(meetId);
await sdk.endMeeting(meetId, { destroy: false });
endMeeting(meetId, { destroy: true }) 调用销毁会场的路径;省略或传 false 是结束会议路径。waitResponse: false 可用于不需要等待 dispatch 统一响应的调用,但方法会更早完成,不能当作后台操作已经成功。
成员和角色
const currentMeetId = sdk.getCurrentMeetId();
const members = await sdk.getMeetingMembers(meetId);
const cachedMembers = sdk.getMeetingMembersSync(meetId);
const member = await sdk.getMeetingMember(meetId, "1002");
const inMeeting = sdk.isMemberInMeeting(meetId, "1002");
await sdk.changeMemberToSpeaker(meetId, "1002");
await sdk.changeMemberToAudience(meetId, "1003");
await sdk.changeMemberToChairman(meetId, "1001");
await sdk.changeMemberToController(meetId, "1001");
const muted = await sdk.muteAllMembers(meetId);
const unmuted = await sdk.unmuteAllMembers(meetId);
console.log(muted.total, muted.succeeded, muted.failed);
getMeetingMembers() 是异步读取,会优先请求底层缓存接口;getMeetingMembersSync()、isMemberInMeeting() 是同步读取本地缓存。全员禁言/解除禁言会收集逐成员失败项,返回 { total, succeeded, failed },不会因单个成员失败而中断其他成员。
混屏和分屏控制主要由后台通知驱动,没有统一的前端开关 API。使用 meeting:mixScreen、meeting:splitScreen 等事件接收变化,用 sdk.getMeetingScreenState(meetId) 读取 SDK 当前缓存的 isMixScreen、shareTel、divType 等字段。
视频操作
// 普通视频:同步调用,未指定 index 时自动找空闲窗口。
const playResult = sdk.playVideo({
video: "34020000001320000001",
id: "camera-1",
name: "门口摄像头",
index: 0
});
sdk.playVideos([
{ video: "camera-2", id: "camera-2", name: "大厅" },
{ video: "camera-3", id: "camera-3", name: "走廊", index: 2 }
]);
const index = sdk.isPlaying("camera-1");
const windows = sdk.getPlayingWindows();
sdk.setVideoWindows(4);
sdk.startVideoPoll({ interval: 10000 });
sdk.stopVideoPoll();
sdk.closeVideo(index, { isSave: false });
sdk.closeAllVideos(false);
上述普通视频方法与视频引擎的返回值保持一致,SDK 不为底层播放结果包装固定结构。playVideo 未指定 index 时会自动选择空闲窗口,必要时按 1 -> 4 -> 6 -> 9 -> 16 扩充分屏;需要固定窗口时传数字 index。
会议视频优先使用 sdk.playVideo({ meetId, tel, ... }) 或 sdk.playMeetingVideo({...}),这样能复用 SDK 的入会、成员通知和软手柄应答逻辑。
9. 事件与回调
有两种互补方式。初始化配置适合固定的业务回调;运行时监听适合按页面/组件动态绑定:
const sdk = ScooperMeetingSDK.create({
callbacks: {
onEvent(event, payload) {
console.log("全 部事件", event, payload);
},
onMeetingMember(member) {
console.log("成员变化", member);
},
onVideoPlayError(error) {
console.error("视频播放失败", error);
},
onDispatchReAuth(info) {
console.warn("需要重新鉴权", info);
},
onError(error) {
console.error("SDKError", error.code, error.message);
}
}
});
const offMember = sdk.on(EVENT.MEETING_MEMBER, (member) => {
console.log("动态成员监听", member);
});
const offOnce = sdk.once(EVENT.READY, () => {
console.log("SDK 已就绪");
});
const offAny = sdk.onAny((event, payload) => {
console.debug(event, payload);
});
// 页面或组件不再关心时取消动态监听。
offMember();
offOnce();
offAny();
最常用的事件包括:
| 场景 | 事件常量/事件名 | 用途 |
|---|---|---|
| SDK 生命周期 | EVENT.READY、EVENT.ERROR、EVENT.DESTROYED | 初始化完成、统一错误、销毁完成。 |
| 调度 | EVENT.DISPATCH_LOGIN、EVENT.DISPATCH_REAUTH、EVENT.DISPATCH_DISCONNECT | 登录、鉴权失效、连接断开。 |
| 会议 | EVENT.MEETING_STATUS、EVENT.MEETING_MEMBER、EVENT.MEETING_HANDS_UP | 会场状态、成员入离会/角色、举手。 |
| 视频 | EVENT.VIDEO_INIT、EVENT.VIDEO_PLAY_SUCCESS、EVENT.VIDEO_PLAY_ERROR、EVENT.VIDEO_CLOSE | 视频控制器状态和播放结果。 |
| 软手柄 | EVENT.SOFT_HANDLER_INCOMING_CALL、EVENT.SOFT_HANDLER_ACCEPTED、EVENT.SOFT_HANDLER_HANGUP | 呼入、接通、挂断。 |
| 后台桥接 | EVENT.BACKEND_LISTENER_CONNECTED、EVENT.BACKEND_LISTENER_MESSAGE、EVENT.BACKEND_LISTENER_ERROR、EVENT.BACKEND_LISTENER_DISCONNECTED | SSE/WebSocket 连接和原始 envelope。 |
并非所有透传事件都有 EVENT 常量。例如视频控制器的 video:click、video:remoteStream、video:message,以及软手柄的 softHandler:ringing、softHandler:answerResult 需要使用文档中的裸字符串监听。完整映射见 事件对照。
监听器回调抛错时,SDK 会优先调用 callbacks.onCallbackError;没有配置时输出到控制台。业务回调中应自行处理展示和重试,不要在回调中泄露 token 或完整鉴权响应。
10. Promise、同步方法与错误
Promise 方法
会议、呼叫、软手柄、后台监听和 init()/destroy() 等方法返回 Promise。依赖缺失、参数错误、后台业务失败和超时都会以 reject 交付:
try {
await sdk.makeVideoCall({ tel: "1002", businessId: "case-001" });
await sdk.joinMeeting({ meetId, tel: "1002", mediaType: "video" });
} catch (error) {
console.error(error.code, error.message);
}
多数等待 dispatch methodResponse 的调用默认串行化,因为旧协议响应没有 requestId。waitResponse: false 会发送请求但不等待统一响应;业务需要确认成功时不要关闭等待。
同步方法
普通视频重载的 playVideo({ video, ... })、playVideos()、initVideo()、closeVideo()、closeAllVideos()、setVideoWindows()、startVideoPoll()、stopVideoPoll()、requestMeeting()、requestMeetingMembers()、requestMeetingMember()、getMeetingScreenState()、isPlaying()、isMemberInMeeting() 等仍可能同步 throw:
try {
sdk.initVideo("#video-root");
sdk.playVideo({ video: "camera-1", id: "camera-1" });
} catch (error) {
console.error(error.code, error.message);
}
注意:同名 playVideo 的会议重载(同时传 meetId 和 tel,且没有 join: false)会进入异步 playMeetingVideo,应使用 await/.catch()。如果显式传 join: false,它会执行普通视频挂载路径,仍需按同步方法处理。
SDKError
统一错误构造器是 ScooperMeetingSDK.SDKError,ESM 也可具名导入 SDKError。错误至少包含 code、message 和可能存在的 cause;SDK 同时派发 sdk:error 和 callbacks.onError:
| code | 常见原因 |
|---|---|
DEPENDENCY_LOAD_FAILED | 脚本 404、网络/CSP 或脚本执行失败。 |
DISPATCH_NOT_FOUND | 未加载 scooper.dispatch,或会议模式未注入 dispatch.instance。 |
VIDEO_CTOR_NOT_FOUND | 未加载 VideoWebRtc。 |
VIDEO_CONTAINER_NOT_FOUND | 视频容器选择器无对应 DOM 元素。 |
TOKEN_REQUIRED / LOGIN_FAILED | token 为空、过期或被调度后台拒绝。 |
REQUEST_TIMEOUT / REQUEST_FAILED | 等待后台响应超时或业务返回失败。 |
JOIN_NOTIFY_TIMEOUT | 视频入会后未在等待窗口内收到匹配的成员通知。 |
SOFT_HANDLER_FACTORY_NOT_FOUND | 启用 软手柄但未加载工厂。 |
完整方法列表和返回策略见 API 参考。不要只检查“Promise 没有抛错”来判断视频已经出画面;同时监听 VIDEO_PLAY_SUCCESS / VIDEO_PLAY_ERROR,并结合底层服务日志验证。
11. 可选软手柄
软手柄默认关闭。启用时,依赖加载和 SDK 配置都要打开;configs.localVideo / remoteVideo 是 DOM 元素而不是选择器字符串:
<video id="local-video" autoplay muted></video>
<video id="remote-video" autoplay></video>
await ScooperMeetingSDK.loadDependencies({
dispatchBaseUrl: "/dispatch-web/",
videoScript: "/scooper-video-player/scooper.video.min.js",
softHandler: true,
softHandlerScript: "/vendor/sc-soft-handler/index.umd.js"
});
const sdk = ScooperMeetingSDK.create({
mode: "meeting",
dispatch: { token: () => sessionStorage.getItem("Sc-Token") || "" },
softHandler: {
enabled: true,
configs: {
janusUrl: "wss://example.com/janus",
autoRegister: true,
videoEnable: true,
localVideo: document.querySelector("#local-video"),
remoteVideo: document.querySelector("#remote-video")
},
registerInfo: {
username: "<sip-user>",
displayName: "第三方页面",
sipServerIp: "<sip-server-ip>",
sipServerPort: 5060
},
autoAnswerSelfCall: true,
autoAnswerDispatchCall: true,
dispatchAnswerTimeout: 5000
}
});
await sdk.init();
await sdk.softCall("1002", { audio: true, video: true });
await sdk.softHangup();
autoAnswerSelfCall 和 autoAnswerDispatchCall 默认开启。若业务需要自行决定何时应答,分别设为 false,再监听 softHandler:incomingCall 并调用 sdk.softAnswer({ audio: true, video: true })。软手柄使用摄像头/麦克风时必须处理浏览器媒体权限和 HTTPS。
12. 可选后台监听桥接
默认情况下,scooper.dispatch.js 在浏览器中直连 dispatch-web 并转发事件。以下情况可增加后台桥接:
- 页面不能直连 dispatch-web,存在跨域、隔离网段或统一网关限制。
- 希望由第三方后台统一做鉴权、日志、过滤、脱敏或多端转发。
- 需要 SSE/WebSocket 作为页面唯一的通知出口。
前端配置示例:
const sdk = ScooperMeetingSDK.create({
mode: "meeting",
dispatch: {
// 仍需要会议/呼叫 API 时保留 dispatch;只接收桥接通知时按实际架构关闭。
token: () => sessionStorage.getItem("Sc-Token") || ""
},
backendListener: {
enabled: true,
transport: "sse",
url: "/scooper-events",
token: () => sessionStorage.getItem("Sc-Token") || ""
},
callbacks: {
onBackendListenerMessage(message) {
console.log("后台 envelope", message.event, message.channel);
},
onMeetingStatus(meet) {
console.log("会场状态", meet);
}
}
});
await sdk.init();
支持 sse、websocket/ws 和自定义 transport。原始桥接消息包含 event、payload、可选的 channel、accountId、meta、receivedAt 和 raw;SDK 会先触发 backend:message,再按 message.event 触发对应业务事件。
前端 backendListener 只负责连接和事件转换,不能替代服务端订阅 dispatch-web。Node 服务端可从 @scooper/meeting-sdk/server 使用 createScooperBackendListener、SSE/WebSocket handler 和 transport adapter;账号过滤、token 校验、重连和生产 CORS 应在服务端完成。完整链路和消息格式见 后台监听桥接。
不要同时让浏览器直连 dispatch 和后台桥接同一批事件,否则可能收到两次。跨域/隔离场景通常选择桥接;内网直连场景通常选择默认 dispatch 监听。
13. React/Vite 示例
可运行示例使用 examples/react-vite-antd 目录,默认开发端口是 5174。线上可直接打开 SDK 在线调试台:
cd scooper-meeting-sdk/examples/react-vite-antd
npm install --registry=https://registry.npmjs.org/
cp .env.example .env.local
# 编辑 .env.local,设置可访问的 VITE_BACKEND_ORIGIN
npm run dev
浏览器访问 http://127.0.0.1:5174/。构建检查:
npm run build
这个示例会加载真实的 scooper.dispatch、CometD/SSE、VideoWebRtc 和 ScSoftHandler,没有 mock 回退;VITE_BACKEND_ORIGIN 未配置时只会回落到示例默认地址。构建成功只说明前端产物可生成,不代表真实登录、联系人查询、邀请、入会或播放已通过。真实业务验证还需要可访问的后端、合法测试账号/权限、短期 token 和实际媒体服务。
Vite 会把所有 VITE_ 前缀变量内联进浏览器产物。VITE_DEMO_TOKEN 只能用于测试环境的短期令牌,不能放生产长效 token、密码、私钥或其他服务端密钥。
最小静态页面示例位于 SDK 源码包的 examples/basic.html;不使用 bundler 时可按本页的“经典浏览器脚本”顺序加载。
14. 安全与部署检查
- 只使用 HTTPS 和受控的跨域策略;WebRTC、SSE、WebSocket、视频脚本和页面协议不要混用不安全来源。
- 前端只保存短期、可撤销、权限受限的测试 token。不要把密码、长期 token、SIP 密钥或后台服务凭据写入源码、
.env、日志、截图或VITE_变量。 - token getter 只在需要时读取最新值;不要在
onEvent、onError或网络调试日志中打印完整 token。生产环境可通过后台桥接隐藏 dispatch 凭据,并在后台执行账号/会场授权。 - SDK 默认把视频 token 同步到
sc-auth、meetWebToken、videoWebToken。若宿主已有不同的会话模型,审查video.syncToken/syncTokenKeys,必要时关闭同步,避免覆盖宿主登录态。 - 固定底层脚本版本和路径,部署后检查 jquery -> CometD -> util/SSE -> dispatch -> video -> soft-handler 的顺序和响应内容类型;不要把 HTML 错误页当成 JS 资源。
- 后台桥接按账号过滤 channel,SSE/WebSocket 连接建立前校验登录态;不要在生产环境照搬允许任意来源的示例配置。
- 页面卸载和路由切换时调用
destroy(),释放视频、软手柄、事件和桥接连接;多个页面共用 dispatch 时不要随意传destroyDispatch: true。 - 业务上仍需验证“登录成功、成员有权入会、视频服务已出流”三个独立条件。脚本加载、
sdk:ready或createMeeting()返回并不能单独证明完整业务链路成功。
15. 故障排查
| 现象 | 优先检查 | 处理建议 |
|---|---|---|
Named export 'EVENT' not found | 是否从 src/scooper-meeting-sdk.js 直接具名导入。 | 改用 import ... from "@scooper/meeting-sdk",或导入 src/index.mjs 包装入口。 |
does not provide an export named 'default' | 是否把 UMD 文件写成浏览器原生 ESM 的默认导入。 | UMD 用经典 <script> + ScooperMeetingSDK 全局;需要 ESM 时使用包根或 src/index.mjs。 |
DISPATCH_NOT_FOUND / MEETS_NOT_FOUND | 调度脚本顺序、全局名、mode 和 dispatch.instance。 | 先确认 window.scooper.dispatch,再调用 init();播放器请设 mode: "player" 并关闭 dispatch 加载。 |
VIDEO_CTOR_NOT_FOUND | VideoWebRtc 脚本路径、脚本响应和全局名。 | 确认视频脚本在 dispatch 之后加载并暴露 VideoWebRtc 或 scooper.video。 |
VIDEO_CONTAINER_NOT_FOUND | init() 时容器是否已挂载、选择器是否正确。 | 在 DOM/组件挂载后创建或初始化 SDK;也可传 Element。 |
TOKEN_REQUIRED / LOGIN_FAILED | getter 是否返回空字符串、token 是否过期、remoteBaseUrl 是否正确。 | 先在不打印敏感值的前提下确认 token 存在,再通过 login() 重新登录并处理 DISPATCH_REAUTH。 |
JOIN_NOTIFY_TIMEOUT | dispatch 长连接、会议 ID/号码、成员通知是否到达。 | 检查 meeting:member payload,按网络情况增加 joinNotifyTimeout;只有明确不等待通知时才使用 waitJoinNotify: false。 |
依赖脚本 404 或 DEPENDENCY_LOAD_FAILED | loadDependencies() 拼出的最终 URL、CSP、CORS 和 Content-Type。 | 直接打开资源 URL 检查响应,或改用宿主统一静态资源加载。 |
| 视频窗口空白或播放失败 | HTTPS、媒体权限、视频 token 同步、视频编号格式、服务端流状态。 | 监听 video:playError,检查 syncToken 和底层视频服务日志;不要只看 sdk:ready。 |
| 收到重复会议/成员事件 | 是否同时启用浏览器 dispatch 和 backendListener。 | 同一批通知只保留一种订阅链路;检查 backend:message 与 onEvent 日志。 |
| React/Vite 页面能构建但无法登录 | 后端地址、真实脚本、测试账号、网络和权限。 | 将构建结果与真实后端验证分开;该 demo 没有 mock,需具备实际运行环境。 |
16. 相关文档
- SDK API 参考:方法签名、配置字段、返回/错误策略。
- 事件与后台 channel 对照:SDK 事件、具名 callback、底层事件和透传事件。
- 后台监听与通知桥接:第三方服务端订阅、SSE/WebSocket 和消息 envelope。
- 经典 HTML 和 React/Vite 示例位于 SDK 源码包的
examples/目录;本站不重复维护示例源码。 - SDK 在线调试台:验证登录、运行时 profile 和 SDK 调用。
- 包 README 与类 型声明请以发布包中的
README.md、types/index.d.ts为准。