styleupime)注意事项 当前 SDK 只支持学生端接入,暂不支持老师端、助教端接入。 进课时 userType请使用listener。
| 组件 | 版本要求 |
|---|---|
| HarmonyOS | 5.0+(API 12+) |
| DevEco Studio | 5.0+ |
| 语言 | ArkTS |
build-profile.json5 中 compatibleSdkVersion ≥ 5.0.0(12)(Demo 为 5.0.0(12))module.json5 中 deviceTypes 包含 phone / tablet(或 default).ohpmrc 中添加伯索的 ohpm 仓库(styleupime 发布在 /repos/upime,需放在第一位;SDK 依赖的 @ohos/flutter_* 等包同样优先从 /repos/upime 拉取):entry/oh-package.json5 中添加 SDK 依赖:{
"dependencies": {
"styleupime": "1.68.100"
}
}entry/src/main/module.json5 中声明课堂所需权限:{
"module": {
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{
"name": "ohos.permission.CAMERA",
"reason": "$string:permission_camera_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:permission_microphone_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
}
]
}
}string.json 中补充权限用途说明。EntryAbility 中接入 SDK 生命周期,并注册 IResourceProvider(见下一节)。IResourceProvider 不能为 null,后续启动实时课堂需要使用 StyleUpime 对象。import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { StyleUpime, StyleUpimeAbilityHelper } from 'styleupime';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
StyleUpimeAbilityHelper.onAbilityCreate(this);
StyleUpime.getInstance().setResourceProvider(this.resProvider);
}
onDestroy(): void {
StyleUpimeAbilityHelper.onAbilityDestroy(this);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
StyleUpimeAbilityHelper.onWindowStageCreate(this, windowStage);
windowStage.loadContent('pages/Index', (err) => {});
}
onWindowStageDestroy(): void {
StyleUpimeAbilityHelper.onWindowStageDestroy(this);
}
}pages/PlasoLiveClassPage,并在 main_pages.json 中登记。entry/src/main/resources/base/profile/main_pages.json:{
"src": [
"pages/Index",
"pages/PlasoLiveClassPage"
]
}entry/src/main/ets/pages/PlasoLiveClassPage.ets:import { PlasoLiveClassView, LiveClassBackPressBridge } from 'styleupime';
@Entry
@Component
struct PlasoLiveClassPage {
build() {
Stack() {
PlasoLiveClassView()
}
.width('100%')
.height('100%')
}
onBackPress(): boolean {
return LiveClassBackPressBridge.handleBackPress();
}
}pages/PlasoLiveClassPage。IResourceProvider 是定制化资料中心接口,APP 提供该接口的实现,并传入 SDK。实现后通过 StyleUpime.getInstance().setResourceProvider 注册。import {
IResourceProvider,
IResourceCallback,
SignCallback,
IUrlCallback,
StyleUpime
} from 'styleupime';
class AppResourceProvider implements IResourceProvider {
supportResourceCenter(): boolean {
// 是否支持定制化资料中心,返回 true 代表支持,false 代表不支持
return false;
}
showResourceCenter(_checkAi: boolean): void {
// 显示 APP 定制化资料中心,在此处实现跳转到定制化资料中心页面
}
dismissResourceCenter(): void {
// 关闭 APP 定制化资料中心
}
getExtFileName(info: Object | null, callback: IResourceCallback): void {
// APP 根据传入的文件标识内容返回其可以访问的完整 URL
callback.onFilePath(null);
}
getPreParseFileName(info: Object | null, suffix: string | null, callback: IResourceCallback): void {
// APP 根据传入的预解析文件标识内容返回其可以访问的完整 URL
callback.onFilePath(null);
}
signQuery(queryMap: Map<string, Object> | null, cb: SignCallback): boolean {
// 请求 SDK Client 进行参数签名;窗口模式下插入文件,需要从服务器获取签名
if (queryMap) {
queryMap.set('appId', Config.appId); // Config.appId 填写从伯索获取的 appId,这个参数是必需的
queryMap.set('validTime', 120);
queryMap.set('validBegin', Math.floor(Date.now() / 1000));
const obj: Record<string, string | number> = {};
queryMap.forEach((v: Object, k: string) => {
obj[k] = (typeof v === 'string' || typeof v === 'number') ? v : `${v}`;
});
// 根据 queryMap、appKey 生成 query
SignHelper.sign(obj, Config.appKey).then((signed: string) => {
cb.onSignCompleted(signed);
}).catch(() => {
cb.onSignCompleted(null);
});
} else {
cb.onSignCompleted(null);
}
return true;
}
getWebviewUrl(_type: number, callback: IUrlCallback): void {
// 请求 SDK Client 是否有教材包的链接需要在课堂内打开
callback.onGetUrl('https://www.plaso.cn');
}
}
StyleUpime.getInstance().setResourceProvider(new AppResourceProvider());signQuery 方法中需要生成 query,生成方法参考如下形式:import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { util } from '@kit.ArkTS';
class SignHelper {
static async sign(params: Record<string, string | number>, signKey: string): Promise<string> {
const keys = Object.keys(params).sort();
const sorted = keys.map((key: string) => `${key}=${params[key]}`).join('&');
params['signature'] = await SignHelper.encrypt(sorted, signKey);
return SignHelper.buildQuery(params);
}
private static buildQuery(params: Record<string, string | number>): string {
return Object.keys(params)
.map((key: string) => `${key}=${encodeURIComponent(params[key].toString())}`)
.join('&');
}
private static async encrypt(encryptText: string, signKey: string): Promise<string> {
const macAlg = cryptoFramework.createMac('SHA1');
const keyBlob: cryptoFramework.DataBlob = {
data: new util.TextEncoder().encodeInto(signKey)
};
const symKey = await cryptoFramework.createSymKeyGenerator('HMAC').convertKey(keyBlob);
await macAlg.init(symKey);
await macAlg.update({ data: new util.TextEncoder().encodeInto(encryptText) });
const macResult = await macAlg.doFinal();
let hex = '';
for (let i = 0; i < macResult.data.length; i++) {
const part = macResult.data[i].toString(16);
hex += part.length === 1 ? `0${part}` : part;
}
return hex.toUpperCase().replace(/^0+/, '');
}
}import { ClassConfig, UpimeLogConfig, UpimeWaterMarkConfig } from 'styleupime';
const config = new ClassConfig();
const logConfig = new UpimeLogConfig();
logConfig.logDir = `${context.filesDir}/logs`;
logConfig.logLevel = UpimeLogConfig.DEBUG; // DEBUG: 0 INFO: 1 WARN: 2 ERROR: 3
config.logConfig = logConfig;
const waterMarkConfig = new UpimeWaterMarkConfig();
waterMarkConfig.text = 'waterMark';
waterMarkConfig.size = 14;
waterMarkConfig.opacity = 0.5;
config.waterMarkConfig = waterMarkConfig;| 参数名 | 参数类型 | 是否必须 | 默认值 | 参数说明 |
|---|---|---|---|---|
| logDir | string | 否 | {filesDir}/logs | 日志目录 |
| logLevel | number | 否 | INFO(1) | 日志级别 DEBUG: 0 INFO: 1 WARN: 2 ERROR: 3 |
| 参数名 | 参数类型 | 是否必须 | 默认值 | 参数说明 |
|---|---|---|---|---|
| text | string | 否 | - | 水印 |
| size | number | 否 | - | 字号 |
| opacity | number | 否 | - | 透明度 0~1 |
StyleUpime.getInstance().updateProhibitedWords(['违禁词1', '违禁词2']);async function getQuery(): Promise<string> {
const params: Record<string, string | number> = {
appId: Config.appId, // Config.appId 填写从伯索获取的 appId
appType: 'liveclassSDK',
beginTime: Math.floor(Date.now() / 1000),
endTime: endTime,
mediaType: meetingType,
meetingId: meetingId,
meetingType: useMeetingMode ? 'meeting' : 'public',
loginName: userName,
userName: userName,
userType: userType,
validTime: 1080000,
onlineMode: onlineMode,
d_sharpness: sharpness,
d_dimension: resolution,
vendorType: rtcType,
enableNewClassExam: enableNewClassExam,
d_enableObjectEraser: editElementMode
};
if (videoStream !== -1) {
params['videoStream'] = videoStream;
}
// SignHelper.sign 方法参考上一节,Config.appKey 填写从伯索获取的 appKey
return SignHelper.sign(params, Config.appKey);
}ClassConfig 对象。ClassConfig 创建完毕后,通过 StyleUpime 调用 launchLiveClass 即可启动实时课堂。async function launchLiveClass(context: common.UIAbilityContext): Promise<void> {
const query = await getQuery();
const config = new ClassConfig();
config.classURL = query;
config.query = query;
config.host = 'https://www.plaso.cn';
config.userName = userName;
config.taskId = meetingId;
config.openFileMode = UpimeConfig.OPEN_FILE_MODE_WINDOW;
config.toolboxItems = UpimeConfig.ToolBoxItem.ALL;
config.enableInteractPpt = enablePptInteract;
config.teachToolTypes = UpimeConfig.TeacherToolType.All;
config.supportUndo = undoSupport;
config.useNewSmallBoard = useNewSmallBoard;
config.supportSelect = supportSelect;
config.endRemindTime = remindTime;
config.redPacketLimit = redPacketLimit;
config.residentCamera = isResidentCamera;
config.auxiliaryCamera = isAuxiliaryCamera;
config.supportHighlighter = supportHighlighter;
config.forbiddenScreenShot = forbiddenScreenShot;
config.enableVote = enableVote;
config.enableSaveBoard = enableSaveBoard;
config.enableAns = true;
config.enableStarShow = enableStarShow;
config.hideRecordingIndicator = hideRecordingIndicator;
config.showStudentClassElapsedTime = showStudentClassElapsedTime;
const upime = StyleUpime.getInstance();
upime.setLiveClassExitHandler((event: NativeLiveClassExitEvent) => {
// 退出实时课堂后回调
});
upime.launchLiveClass(config, context);
}| 参数名 | 参数类型 | 是否必要 | 默认值 | 参数说明 |
|---|---|---|---|---|
| classURL | string | 是 | - | 实时课堂配置参数 query。如果 query 里面有中文字符串,此中文字符串需要编码后放到 query 里传入 |
| query | string | 是 | - | 实时课堂配置参数 query,与 classURL 使用同一份签名结果 |
| host | string | 是 | - | 服务地址,使用线上环境即可 |
| userName | string | 否 | - | 当前用户名称 |
| taskId | string | 否 | - | 会议 / 课堂 ID |
| classMember | User[] | 否 | - | 课堂成员列表 |
| allowLocalPPT | boolean | 否 | true | 是否允许在实时课堂中插入本地 PPT |
| enableSendMessage | boolean | 否 | true | 消息界面是否显示消息输入框 |
| hideOtherClient | boolean | 否 | false | 是否显示学生不在班级的所有学生和助教 |
| endRemindTime | number | 否 | 0 | 实时课堂添加双减的提示时间,到达这个时间之后,会提示用户(秒) |
| redPacketLimit | number | 否 | 0 | 红包雨个数限制 |
| defaultPermission | number | 否 | 0 | 会议模式默认权限 |
| supportSelect | boolean | 否 | true | 是否支持工具栏选择箭头 |
| openFileMode | number | 否 | 0 | 文件打开模式 |
| toolboxItems | number | 否 | UpimeConfig.ToolBoxItem.ALL | 百宝箱工具条目配置(工具请参考 UpimeConfig.ToolBoxItem) |
| enableInteractPpt | boolean | 否 | false | 是否开启 PPT 交互 |
| teachToolTypes | number | 否 | 0 | 教具类型支持的类型(类型请参考 UpimeConfig.TeacherToolType) |
| supportUndo | boolean | 否 | false | 是否支持撤销操作 |
| useNewSmallBoard | boolean | 否 | false | 是否使用新版小黑板 |
| supportHighlighter | boolean | 否 | false | 是否使用荧光笔 |
| forbiddenScreenShot | boolean | 否 | false | 是否防止课堂截屏 |
| enableVote | boolean | 否 | false | 是否支持投票工具 |
| enableSaveBoard | boolean | 否 | false | 是否支持保存板书功能 |
| chatMsgPopup | number | 否 | 0 | 学生进入课堂时消息弹框提醒设置。0: 本地记录; 1: 远端控制开启; 2: 远端控制关闭。默认 0,仅对学生生效,老师不受此配置控制 |
| enableAns | boolean | 否 | true | 是否开启降噪功能 |
| enableStarShow | boolean | 否 | false | 是否支持显示积分排行榜 |
| recordingPreparationTip | string | 否 | - | 服务端录制课堂延时提示语,只有使用服务端录制课堂才需要添加这个提示语 |
| hideRecordingIndicator | boolean | 否 | false | 是否隐藏「录制中」状态字样 |
| showStudentClassElapsedTime | boolean | 否 | false | 学生端是否显示已开课时间文案 |
| logConfig | UpimeLogConfig | 否 | - | 日志配置 |
| waterMarkConfig | UpimeWaterMarkConfig | 否 | - | 水印配置 |
insertObject 直接插入,视 info 类型决定是否触发回调签名(getExtFileName)insertObject 插入后,由 SDK 回调 getPreParseFileName,按页 / 资源后缀返回可访问直链getExtFileName(非预解析使用)getExtFileName(info: Object | null, callback: IResourceCallback): voidinfo 计算资源直链(含签名),成功回调 callback.onFilePath(realUrl),失败回调 callback.onFilePath(null)getPreParseFileName(预解析使用)getPreParseFileName(info: Object | null, suffix: string | null, callback: IResourceCallback): voidinfo)与页面后缀(suffix)计算资源直链(含签名),成功回调 callback.onFilePath(realUrl),失败回调 callback.onFilePath(null)import { PlasoUpimeBoard, UpimeObject } from 'styleupime';
PlasoUpimeBoard.getInstance().insertObject(
UpimeObject.TYPE_PDF,
info,
title
);UpimeObject 并调用 insertObject 将文件插入课堂:UpimeObject {
info; // 只可传入 string 或 List 两种类型
type; // 可传入 TYPE_PPT,TYPE_DOC,TYPE_XLS,TYPE_PDF,TYPE_AUDIO,TYPE_VIDEO
title; // 文件显示标题
}info 传入 string 类型TYPE_PPT、TYPE_DOC、TYPE_XLS、TYPE_PDF、TYPE_AUDIO、TYPE_VIDEOinfo 必须是「可直接访问的 HTTP 地址」(无需额外签名)insertObject 后,直接将文件插入白板,不会触发额外回调info 传入 List 类型TYPE_AUDIO、TYPE_VIDEOinfo 为 List 类型(实现方自定义结构)insertObject 插入后会回调 getExtFileName。实现方需在 getExtFileName 中完成签名并返回完整可访问的 HTTP 地址UpimeObject 并调用 insertObject 将文件插入课堂:UpimeObject {
info; // 只可传入 List 类型
type; // 可传入 TYPE_I_SPRING_PPT,TYPE_DOC,TYPE_XLS,TYPE_PDF
title; // 文件显示标题
}insertObject 后,会回调 getPreParseFileName;实现方需在该函数中完成签名并返回完整可访问的 HTTP 地址info:预解析文件的唯一标识。常见形态为 List,包含键 "id",例如 ["parsed", {"id":"abcdef123456"}](推荐)。实现方应优先从 List 中读取 id,据此计算文件地址suffix:资源相对路径 / 页面后缀,例如 "/1.jpg"、"/index.html"、"/ispring.html"、"/info.json"callback:成功 callback.onFilePath(realUrl),失败 callback.onFilePath(null)suffix 在参与拼接与请求时通常需要进行 URL 编码;若包含图片处理等查询参数,请整体进行编码后透传到服务端签名。appId 和 appKey 值。签名请通过在服务器中完成,Demo 中只是样例。signQuery(queryMap: Map<string, Object> | null, cb: SignCallback): boolean {
if (queryMap) {
queryMap.set('appId', xxx); // 自己机构的 appId
queryMap.set('validTime', 120);
queryMap.set('validBegin', Math.floor(Date.now() / 1000));
// TODO get it from server
// this is only for test
SignHelper.sign(obj, xxx).then((signed: string) => { // 自己机构的 appKey
cb.onSignCompleted(signed);
});
} else {
cb.onSignCompleted(null);
}
return true;
}pages/PlasoLiveClassPage,并写入 main_pages.json,否则无法进入实时课堂。EntryAbility 中必须调用 StyleUpimeAbilityHelper 的生命周期方法。git clone https://github.com/PlasoOpenPlatform/PlasoSdkHosSample.git,或下载 ZIP 并解压到本地)entry/oh-package.json5 中 SDK 版本为 1.68.100{
"dependencies": {
"styleupime": "1.68.100"
}
}entry/src/main/ets/config/Config.ets,填入伯索分配的 appId 和 key 后即可体验export class Config {
static appId: string = '<your-appId>';
static appKey: string = '<your-app-key>';
}