├── .gitignore ├── README.md ├── assets └── slide-lifecycle.svg ├── docs ├── asset-manifest.json ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static │ ├── css │ ├── main.abd63719.css │ └── main.abd63719.css.map │ └── js │ ├── 787.abff3aea.chunk.js │ ├── 787.abff3aea.chunk.js.map │ ├── main.8734c09d.js │ ├── main.8734c09d.js.LICENSE.txt │ └── main.8734c09d.js.map ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── RtcAudioPlayer.js ├── index.css ├── index.js ├── reportWebVitals.js └── setupTests.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .idea 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netless-slide demo 2 | 3 | 这个项目是 `@netless/slide` 库的前端示例, 需要已经完成文档转换并且获取了 `taskId` 和 `prefixUrl` 才能进一步使用 `@netless/slide` 在浏览器中展示 ppt。 4 | 5 | [在线演示](https://netless-io.github.io/netless-slide-demo/) 6 | 7 | ## 基本使用 8 | 9 | ### 初始化 `Slide` 对象 10 | 11 | 要初始化 `Slide` 对象, 至少要指定三项配置 12 | 13 | | key | type | description | 14 | | ---- | ---- | --- | 15 | | anchor | HTMLElement | 作为 `Slide` 渲染出的 `canvas` 元素的挂载点 | 16 | | interactive | boolean |ppt 是否可交互, 不可交互的 ppt 无法响应用户的事件| 17 | | mode | "local" | "interactive" |local: 单机模式, Slide 对象不会触发任意同步事件。
interactive: 互动模式, 所有客户端都可以交互| 18 | 19 | ```javascript 20 | import { Slide } from "@netless/slide"; 21 | 22 | const slide = new Slide({ 23 | anchor: someDivElement, 24 | interactive: true, 25 | mode: "local", 26 | }); 27 | ``` 28 | 29 | ### 设置转换资源 30 | 31 | `Slide` 对象创建之后, 下一步需要设置转换后的资源。`taskId` 为一串 hash 字符串代表一次转换任务 id, `prefixUrl` 为一段 url 地址, 指向转换后的资源根路径。这两个参数都可以从 [转码服务的进度查询 api](https://developer.netless.link/server-zh/home/server-projector) 中获取. 32 | 33 | `注意`: 你需要保证访问 prefixUrl 路径里的资源不会跨域。 34 | 35 | ```javascript 36 | 37 | slide.setResource("06415a307f2011ec8bdc15d18ec9acc7", "https://convertcdn.netless.group/dynamicConvert"); 38 | 39 | ``` 40 | 41 | ### 渲染 ppt 页面 42 | 43 | 设置好转换资源后,就可以调用 `renderSlide` 渲染页面了, 传入参数是 ppt 页码, 页码从 1 开始。你也可以调用 `renderSlide` 跳转到任意页码。 44 | 你需要确保传入的页码在原始 ppt 页数范围内, 访问 `slide.slideCount` 可以获取总页数 45 | 46 | ```javascript 47 | // 渲染第一页 48 | slide.renderSlide(1); 49 | 50 | // 渲染最后一页 51 | slide.slideCount(slide.slideCount); 52 | ``` 53 | 54 | ## Slide 生命周期与事件触发时机 55 | 56 | ![lifecycle](./assets/slide-lifecycle.svg) 57 | 58 | ## 可选配置项 59 | 60 | ### `Slide` 配置 61 | 62 | 初始化 `Slide` 还有一些可选的配置项,说明如下 63 | 64 | ```javascript 65 | const slide = new Slide({ 66 | anchor: someDivElement, 67 | interactive: true, 68 | mode: "local", 69 | // 以下为可选配置 70 | resize: false, 71 | enableGlobalClick: false, 72 | timestamp: Date.now, 73 | renderOptions: { 74 | 75 | } 76 | }); 77 | ``` 78 | 79 | | key | type | description | 80 | |-------------------|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 81 | | resize | boolean | **默认值:** false
设置是否根据窗口大小自动调整分辨率。
默认情况下 ppt 的 css 尺寸会随着 anchor 元素的大小而变化, 但是 canvas 元素的渲染分辨率不会变化。将此值设置为 true, 会使 canvas 的分辨率也跟随缩放比例缩放,这样能获得更好的性能,但是当 anchor 的 css 尺寸太小的情况下,也会导致画面模糊。
除非遇到性能问题,一般不建议设置为 true 。 | 82 | | enableGlobalClick | boolean | **默认值:** false
用于控制是否可以通过点击 ppt 画面执行下一步功能。
建议移动端开启,移动端受限于屏幕尺寸,交互 UI 较小,如果开启此功能会比较方便执行下一步。 | 83 | | timestamp | () => number | **默认值:** Date.now
此函数用于获取当前时间, 在同步及互动场景下,ppt 内部需要知道当前时间,这个时间对于参与同步(互动)的多个客户端应该是一致的,这个时间越精确,画面同步也越精确。
建议通过后端服务,为多个客户端下发相同的时间。 | 84 | | rtcAudio | RtcAudioClazz | **默认值:** null
用于 rtc 混音, 具体用法见下文 | 85 | | logger | ILogger | **默认值:** null
用于接受日志, 具体用法见下文 | 86 | | useLocalCache | boolean | **默认值:** true
是否启用本地缓存,启用后会将 ppt 远程资源缓存在 indexDB 中 | 87 | | renderOptions | ISlideRenderOptions 对象 | 见下表 | 88 | | urlInterrupter | (url: string) => Promise<string> | **默认值:** url
根据公开地址返回可访问地址(用于私有存储服务) | 89 | 90 | #### urlInterrupter 举例 91 | ```ts 92 | const urlInterrupter = async (url: string) => { 93 | // 根据不同的云存储服务会有不同的实现, 一般都是在查询参数添加签名 94 | const { ak, expire } = await getSTSToken() // 客户服务端实现 95 | return `${url}?expire=${expire}&ak=${ak}` 96 | }; 97 | ``` 98 | 99 | 100 | ### ISlideRenderOptions 配置 101 | 102 | | key | type | description | 103 | | ---- | ---- | --- | 104 | | minFPS | number | **默认值:** 30
设置最小 fps, 应用会尽量保证实际 fps 高于此值, 此值越小, cpu 开销越小。 | 105 | | maxFPS | number | **默认值:** 40
设置最大 fps, 应用会保证实际 fps 低于此值, 此值越小, cpu 开销越小。 | 106 | | resolution | number | **默认值:** pc 浏览器为 window.devicePixelRatio; 移动端浏览器为 1 。
设置渲染分辨倍率, 原始 ppt 有自己的像素尺寸,当在 2k 或者 4k 屏幕下,如果按原始 ppt 分辨率显示,画面会比较模糊。可以调整此值,使画面更清晰,同时性能开销也变高。
建议保持默认值就行,或者固定为 1。 | 107 | | autoResolution | boolean | **默认值:** false, 控制是否根据运行时实际 fps 自动缩放渲染分辨率, 使得运行时 fps 保持在 minFPS 和 masFPS 之间 | 108 | | autoFPS | boolean | **默认值:** false, 控制开启动态 fps, 开启后, 会根据 cpu 效率动态提升和降低 fps | 109 | | maxResolutionLevel | **默认值:** [0, 4]的整数 pc端为4, 手机端为2。 GPU性能不够的机型建议下降此值. | 110 | | transactionBgColor | string | number | **默认值:** 0x000000, 设置切页动画的背景色, 接受 css 颜色字符串或者 16进制颜色值("#ffffff",0xffffff) | 111 | 112 | 113 | maxResolutionLevel 取值解释: 114 | 0. 640*360 115 | 1. 960*540 116 | 2. 1280*720 117 | 3. 1920*1080 118 | 4. 3200*1800 119 | 120 | 121 | ### 互动模式 122 | 123 | 互动模式下, 各个客户端都可以自由操作 ppt. 与同步模式一样, `@netless/slide` 库通过事件将各个客户端的操作通知给 `@netless/slide` 的调用方, 调用方负责将这些事件传递给所有客户端(包括自己). 与同步模式不同的是, 互动模式下, 发送事件的客户端也同时需要处理接收事件. 124 | 125 | 要使用互动模式需要将上述的 `mode` 参数设置为 `"interactive"`. 126 | 127 | ```javascript 128 | // client A 129 | slideA.on(SLIDE_EVENTS.syncDispatch, (event) => { 130 | // event 为可序列化的 js 对象, 你无需关心 event 具体信息 131 | // 需要将序列化后的 event 广播给所有参与互动的客户端(包括 slideA 自己) 132 | socket.boardcast("slide-sync", JSON.stringify(event)); 133 | }); 134 | // 与同步模式不同, 互动模式下, slideA 自己也需要监听来自 socket 135 | // 的事件, 并将事件派发给 slideA 对象. 136 | socket.on("slide-sync", msg => { 137 | const event = JSON.parse(msg); 138 | slideA.emit(SLIDE_EVENTS.syncReceive, event); 139 | }); 140 | 141 | // client B 执行与 clientA 一样的逻辑, 监听 SLIDE_EVENTS.syncDispatch 事件并广播出去 142 | // 同时自己处理来自 socket 的事件 143 | slideB.on(SLIDE_EVENTS.syncDispatch, (event) => { 144 | socket.boardcast("slide-sync", JSON.stringify(event)); 145 | }); 146 | socket.on("slide-sync", msg => { 147 | const event = JSON.parse(msg); 148 | slideB.emit(SLIDE_EVENTS.syncReceive, event); 149 | }); 150 | ``` 151 | 152 | #### 互动模式下的事件模型 153 | 154 | 互动模式下, 对 slide 对象的任何操作, 都仅以事件的形式通过 `SLIDE_EVENTS.syncDispatch` 事件将要执行的操作派发出去, 155 | 直到收到 `SLIDE_EVENTS.syncReceive` 事件才会实际执行操作. 156 | 157 | 下图描述了互动模式下的同步事件流转流程, 其中每一页 PPT 初始状态为页码 1 动画 1. 以所有事件都要经过服务器后才执行的事件模型运行, 可以保证最终每个客户端的状态都是一致的. 158 | 159 | ```mermaid 160 | sequenceDiagram 161 | 客户端 A->>Socket 服务: 渲染下一页(发消息) 162 | note left of 客户端 A: 页码: 1 动画 1 163 | 客户端 B->>Socket 服务: 执行下一步动画(发消息) 164 | note right of 客户端 B: 页码: 1 动画 1 165 | Note over Socket 服务: 服务器以自己收到的顺序派发事件 166 | par Socket 服务 to 客户端 A 167 | Socket 服务->>客户端 A: 1. 渲染下一页(收消息) 168 | note left of 客户端 A: 页码: 2 动画 1 169 | Socket 服务->>客户端 A: 2. 执行下一步(收消息) 170 | note left of 客户端 A: 页码: 2 动画 2 171 | and Socket 服务 to 客户端 B 172 | Socket 服务->>客户端 B: 1. 渲染下一页(收消息) 173 | note right of 客户端 B: 页码: 2 动画 1 174 | Socket 服务->>客户端 B: 2. 执行下一步(收消息) 175 | note right of 客户端 B: 页码: 2 动画 2 176 | end 177 | ``` 178 | 179 | 如果消息不经过服务端排序及序号添加, 则互动模式下会出现最终状态不一致的问题, 见下图描述: 180 | 181 | ```mermaid 182 | sequenceDiagram 183 | note left of 客户端 A: (初始状态)页码: 1 动画 1 184 | 客户端 A->>客户端 A: 执行渲染下一页 185 | note left of 客户端 A: 页码: 2 动画 1 186 | 客户端 A->>Socket 服务: 渲染下一页(发消息) 187 | note right of 客户端 B: (初始状态)页码: 1 动画 1 188 | 客户端 B->>客户端 B: 执行下一步动画 189 | note right of 客户端 B: 页码: 1 动画 2 190 | 客户端 B->>Socket 服务: 下一步动画(发消息) 191 | Socket 服务-->>客户端 A: 下一步动画(收消息) 192 | Socket 服务-->>客户端 B: 渲染下一页(收消息) 193 | 客户端 A->>客户端 A: 执行下一步动画 194 | note left of 客户端 A: 页码: 2 动画 2 195 | 客户端 B->>客户端 B: 执行渲染下一页 196 | note right of 客户端 B: 页码: 2 动画 1 197 | ``` 198 | 199 | ### 整体同步 200 | 201 | 在某些情况下, 需要一种机制将客户端 A 的状态一次性整体同步给客户端 B, 而不是通过一条一条事件完成同步。例如: 客户端 B 断线后重新连接至 socket 房间, 此时需要将客户端 A 的当前状态一次性同步给 B. 202 | 203 | 为此 `@netless/slide` 提供了获取和设置应用整体状态的机制. 204 | 205 | ```javascript 206 | // 访问 slideState 可以获取 slide 状态快照 207 | const snapshot = slideA.slideState; 208 | 209 | // 将 slideB 的状态同步到 slideA 当前状态 210 | slideB.setSlideState(snapshot); 211 | 212 | ``` 213 | 214 | 在同步模式下, 被同步的客户端 B 可以在断线重连后询问客户端 A 的当前状态, 客户端 A 收到询问后可以使用上述 API 获取状态快照. 215 | 但是在互动模式下, 这种询问的机制就不适用了, 互动模式下所有客户端应该共享同一个状态, 要做到这种效果, 可以在某处(一般是 socket 房间信息上)记录这个状态快照, `@netless/slide` 会在状态变更后通知给你,此时可以将最新的状态记录下来. 216 | 217 | ```javascript 218 | slideA.on(SLIDE_EVENTS.stateChange, snapshot => { 219 | socket.room.slideState = snapshot; 220 | }); 221 | 222 | // 客户端 B 重新连接后, 获取房间信息上的状态并设置 223 | socket.on("connect", () => { 224 | slideB.setSlideState(socket.room.slideState); 225 | }); 226 | ``` 227 | 228 | ### 竞态处理 229 | 230 | 在互动模式下, 由于每个客户端都可以独立的与 ppt 交互,因此存在竞态条件。例如, 客户端 A 执行翻到下一页(记为事件 A),与此同时客户端 B 执行切换到下一个动画(记为事件 B). 这两个事件执行的顺序会影响最终的状态(假设执行事件之前, 处于 ppt 第一页的第一个动画): 231 | 232 | **A-B:** 先翻页, 再播放下一个动画, 最终状态为第二页的第一个动画 233 | **B-A:** 先播放下一个动画, 再执行下一页, 最终状态为第二页第 0 个动画 234 | 235 | 这两个事件都会传递到 socket 服务器。socket 服务器是否是按事件产生的真实时间来下发这两个事件并不重要, 重要的是两个客户端接收事件的顺序必须一致(A-B或者B-A),如此才能保证两个客户端最终状态一致。因此, 你需要保证参与互动的每个客户端收到的事件顺序是一致的。 236 | 237 | ## rtc 混音 238 | 239 | **注意: `@netless/slide@0.2.9` 版本才开始支持。** 240 | 241 | ppt 里设计的音频和视频, 默认是用浏览器的 api 来播放, 如果有 rtc 混音需要可以提供自定义的播放器类来替换掉内置播放器。自定义的播放器需要实现下面的 `RtcAudioClazz` 接口。 242 | ```typescript 243 | export interface RtcAudio { 244 | /** 245 | * 开始播放音频. 246 | */ 247 | play(): void; 248 | 249 | /** 250 | * 暂停音频播放, 且音频当前播放时间不变 251 | */ 252 | pause(): void; 253 | 254 | /** 255 | * 当音频对象不再使用时候被调用 256 | */ 257 | destroy(): void; 258 | 259 | /** 260 | * 获取音频当前播放时间, 单位为:秒 261 | */ 262 | get currentTime(): number; 263 | /** 264 | * 设置音频当前播放时间, 单位为:秒。需注意, 无论音频是否正在播放, 都需要确保能设置成功。 265 | * 如果音频暂停状态下, 设置此值, 那么需保证, 下次恢复播放是从此值位置开始播放。 266 | */ 267 | set currentTime(time: number); 268 | 269 | /** 270 | * 返回音频是否暂停状态 271 | */ 272 | get isPaused(): boolean; 273 | 274 | /** 275 | * 返回音频时长 276 | */ 277 | get duration(): number; 278 | 279 | /** 280 | * 当音频加载完成时触发, 例如: 音频 meta 数据加载完成, 这时候知道了音频实际时长, 就需要触发此事件, 需要保证此事件触发时, 281 | * 能通过 duration 属性获取到更新后的音频时长 282 | * @param event 283 | * @param listener 284 | */ 285 | on(event: "load", listener: () => void): this; 286 | 287 | /** 288 | * 当音频暂停时候触发 289 | * @param event 290 | * @param listener 291 | */ 292 | on(event: "pause", listener: () => void): this; 293 | 294 | /** 295 | * 当音频开始播放时候触发 296 | * @param event 297 | * @param listener 298 | */ 299 | on(event: "play", listener: () => void): this; 300 | 301 | /** 302 | * 移除参数指定事件的所有监听器 303 | */ 304 | removeAllListeners(event: string): void; 305 | } 306 | 307 | export interface RtcAudioClazz { 308 | /** 309 | * 创建 rtc 播放器, url 为音频地址 310 | * @param url 311 | */ 312 | new(url: string): RtcAudio; 313 | } 314 | ``` 315 | 316 | js 实现的自定义播放器示例代码可以[参考](./src/RtcAudioPlayer.js)。将自定义的播放器类传递给 `Slide` 的构造函数, 即可替换默认的音频播放器。 317 | ```typescript 318 | import { Slide } from "@netless/slide"; 319 | 320 | const slide = new Slide({ 321 | anchor: someDivElement, 322 | interactive: true, 323 | mode: "local", 324 | rtcAudio: RtcAudioPlayer, 325 | }); 326 | ``` 327 | 328 | 对于 mp3 文件, `@netless/slide` 直接调用自定义的播放器播放音频。 329 | 330 | 对于 mp4 文件, 转码服务已经将 mp4 的音频单独提取出一个 mp3 文件, `@netless/slide` 将 mp4 静音, 同时用提供的自定义播放器播放对应的 mp3。 331 | 332 | ## 错误处理与日志 333 | 334 | `@netless/slide@0.3.3` 版本开始, 会捕获当前页面的所有错误, 并通过 `SLIDE_EVENTS.renderError` 事件通知出来, 335 | 你可以在此事件的回调函数里跳转到下一页. 336 | 337 | ### 错误类型说明 338 | 339 | `@netless/slide` 导出有 `ErrorType` 枚举类型, 指示了 `SLIDE_EVENTS.renderError` 事件对应的错误类型,说明如下 340 | 341 | | 名称 | 触发时机 | 恢复手段 | 342 | | ---- | ---- | --- | 343 | | ResourceError | 在 ppt 依赖的远程资源(json,png)不可用时触发, 触发后当前页无法交互 | 重新渲染当前页或者跳转下一页 | 344 | | RuntimeError | 未知的异常, 触发后当前页无法交互 | 跳转下一页 | 345 | | RuntimeWarn | 未知的警告, 在动画过程中出现,触发后动画当前帧表现异常,不影响下一帧和页面交互 | 无需特殊处理 | 346 | |CanvasCrash| 由于内存不足,或者 canvas 被意外的移除(没有调用 slide.destroy() 的情况下移除 canvas 元素为意外移除), 触发后 canvas 元素白屏 | 刷新网页(或者销毁 slide 对象然后重新创建) | 347 | 348 | ```typescript 349 | import { SLIDE_EVENTS, ErrorType } from "@netless/slide"; 350 | 351 | // SlideError 继承自 Error, 除了 message, stack 等属性外 352 | // 还添加了 errorType 及 errorMsg 属性, 353 | interface SlideError extends Error { 354 | errorType: ErrorType; 355 | errorMsg: string; 356 | } 357 | 358 | slide.on(SLIDE_EVENTS.renderError, ({error, index}: {error: SlideError, index: number}) => { 359 | console.log(`第 ${index} 页出错`); 360 | if (err.errorType === ErrorType.ResourceError) { 361 | // 跳转到下一页, 可以根据具体需求选择如何恢复, 例如弹窗确认后再做跳转动作 362 | slide.renderSlide(index + 1); 363 | } else if (err.errorType === ErrorType.CanvasCrash) { 364 | // 需要刷新页面 365 | } else if (err.errorType === ErrorType.RuntimeError) { 366 | // 跳转到下一页 367 | slide.renderSlide(index + 1); 368 | } else if (err.errorType === ErrorType.RuntimeWarn) { 369 | // 无需特殊处理, 可以记录日志 370 | } 371 | }); 372 | ``` 373 | 374 | #### 通过 message 事件处理错误 375 | 376 | `@netless/slide@0.7.1` 版本开始, 还支持通过 window 的 message 事件处理错误. 主要应用于 Android 和 iOS 设备处理错误, native 代码 377 | 可以直接在 webview 上监听 window 的 message 事件,获取错误信息,然后通过往 window 上派发 message 事件来恢复 ppt 的画面. 378 | 379 | **监听错误** 380 | ```typescript 381 | window.addEventListener("message", evt => { 382 | if (evt.data.type === "@slide/_error_") { 383 | const { errorType, errorMsg, slideId, slideIndex } = evt.data; 384 | // errorType 与上述错误类型对应,不同之处只是这里 errorType 是字符串值 385 | // errorType 可能的类型有 386 | // "RESOURCE_ERROR" 对应上述 ResourceError 387 | // "RUNTIME_ERROR" 对应上述 RuntimeError 388 | // "RUNTIME_WARN" 对应上述 RuntimeWarn 389 | // "CANVAS_CRASH" 对应上述 CanvasCrash 390 | 391 | // slideId 指示 slide 对象的唯一 id, 发送恢复消息时候要用到这个 id 392 | // slideIndex 指示报错的页码 393 | } 394 | }); 395 | ``` 396 | 397 | **恢复错误** 398 | 399 | 恢复错误需要向 window 派发 message 事件, 不同的恢复方式代码如下: 400 | 401 | 1. 跳转到其他页, RESOURCE_ERROR 和 RUNTIME_ERROR 可以用这种方式恢复 402 | ```typescript 403 | window.postMessage({ 404 | type: "@slide/_recover_", 405 | recoverBy: "renderOtherPage", 406 | slideId: "${slideId}", // 使用错误消息里告知的 slideId 407 | payload: { 408 | slideIndex: "${slideIndex}", // 指定要跳转到哪一页, 如果想要跳转到下一页可以使用错误消息里告知的报错页码 + 1 409 | } 410 | }, "*"); 411 | ``` 412 | 413 | 2. 重新渲染当前页, RESOURCE_ERROR 可以用这种方式恢复 414 | ```typescript 415 | window.postMessage({ 416 | type: "@slide/_recover_", 417 | recoverBy: "reloadCurrentPage", 418 | slideId: "${slideId}", // 使用错误消息里告知的 slideId 419 | }, "*"); 420 | ``` 421 | 422 | ### 日志 423 | 424 | #### 逐条获取日志 425 | 426 | `@netless/slide@0.3.3` 版本开始, `ISlideConfig` 中添加了可选的 logger 属性, 需要传入一个符合如下接口的对象 427 | 428 | ```typescript 429 | interface ILogger { 430 | info?(msg: string): void; 431 | error?(msg: string): void; 432 | warn?(msg: string): void; 433 | } 434 | ``` 435 | 436 | 如此便可以接收 ppt 运行日志. 437 | 438 | ```typescript 439 | import { Slide } from "@netless/slide"; 440 | 441 | const slide = new Slide({ 442 | anchor: someDivElement, 443 | interactive: true, 444 | mode: "local", 445 | logger: { 446 | info(msg: string) { 447 | console.log(msg); 448 | } 449 | } 450 | }); 451 | ``` 452 | 453 | #### 通过 postMessage 获取日志 454 | 455 | `@netless/slide@0.7.1` 版本开始, 除了上述逐条获取日志的方式外, 还可以通过 postMessage 的方式以文本的形式获取日志. 456 | 457 | 1. 在 window 上派发事件, 通知 `@netless/slide` 开始发送日志 458 | ```typescript 459 | window.postMessage({ 460 | type: "@slide/_request_log_", 461 | sessionId: "${sessionId}", // session 标识 462 | }, "*"); 463 | ``` 464 | 2. 通过监听 message 事件, 分块收取日志文本 465 | ```typescript 466 | window.addEventListener("message", (evt) => { 467 | if (evt.data.type === "@slide/_report_log_") { 468 | console.log(evt.data.index); // 日志当前分块索引 469 | console.log(evt.data.log); // 日志文本 470 | console.log(evt.data.total); // 总分块数量 471 | console.log(evt.data.sessionId); // "@slide/_request_log_" 事件中的 sessionId 472 | if (evt.data.index === evt.data.total) { 473 | // 则日志收取完毕 474 | } 475 | } 476 | }); 477 | ``` 478 | 479 | ## webgl 上限文限制 480 | 481 | 浏览器一般会限制 webgl 上下文数量在 8 到 16 个之间, 对于 `@netless/slide` 来说, 一个活动的 Slide 实例占用两个上下文, 一个负责 2D 渲染一个负责 3D 渲染. 482 | 如果你创建的 Slide 超过了浏览器限制, 那么前面创建的 Slide 将丢失 webgl 上下文, 导致渲染异常. 483 | 484 | **活动的 Slide 实例** 即指没有调用过 `slideInstance.frozen()` 方法的实例, 它的 webgl 绘制环境可以正常工作. 如果你想冻结当前 Slide 实例,将 webgl 上下文留给新创建的 Slide 对象, 就可以 485 | 调用 `slideInstance.frozen()`, 这个方法会将当前 ppt 画面截图, 并且保存 ppt 状态, 然后销毁 canvas 元素, 并用截到的图片替代 canvas 元素。冻结之后, Slide 对象的任意切页、上(下)一步 等操作都将失效. 486 | 487 | 调用 `slideInstance.release()` 可以将 Slide 对象从冻结状态恢复. 488 | 489 | 你需要自己控制活动的 Slide 实例的数量 在 4 到 8 之间, 一般建议 pc 上控制在 8 以下, 移动端控制在 4 以下. 490 | 491 | 关于控制活动 ppt 的建议: 492 | 1. 通过 [Page_Visibility_API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API) 监听页面可见性, 在页面不可见的时候, 将 Slide 对象冻结,并在页面恢复的时候将 Slide 解冻. 493 | 2. 如果同一个页面内有多个活动 PPT, 可以设置一个固定长度的活动 PPT 队列, 将 当前获取焦点的 Slide 激活并推入队列, 将被挤出队列的 Slide 冻结. 494 | 495 | ## 本地缓存管理 496 | 497 | `@netless/slide` 使用 indexDB 缓存网络资源及临时生成的纹理. `@netless/slide` 不负责缓存数据的清理, 你需要在适当的时候来清理这部分数据. 498 | 499 | 和清理缓存相关的两个 api, 实例方法 clearSlideCache 和静态方法 clearLocalCache, 前者清理当前 ppt 缓存, 后者清理所有缓存. 需要注意, clearSlideCache 需要在调用 slide.destroy 之前调用, 否则不能完成清理工作. 500 | 501 | ```typescript 502 | /** 503 | * 销毁当前 Slide 实例的本地缓存, 需要在 destroy 之前调用。 504 | */ 505 | clearSlideCache(): void; 506 | /** 507 | * 销毁历史所有本地缓存 508 | */ 509 | static clearLocalCache(): void; 510 | ``` 511 | 512 | ### 资源代理 513 | 514 | **注意: `@netless/slide@0.4.0` 版本才开始支持。** 515 | 516 | 从 `@netless/slide@0.4.0` 开始, 你可以在创建 Slide 对象时提供一个 loaderDelegate 对象, 从而代理 Slide 内部所有远程资源, 进而可以实现 517 | 资源重定向, 资源鉴权等需求. 518 | 519 | loaderDelegate 属性需符合 `ILoaderDelegate` 接口, 需要注意对于媒体资源, 并不能直接返回资源内容, 只能同步的返回重定向后的资源地址. 520 | 521 | ```typescript 522 | export interface ILoaderDelegate { 523 | /** 524 | * 加载 json 资源, 需返回 json 文本 525 | * @param url 原始资源地址 526 | */ 527 | loadJson(url: string): Promise; 528 | /** 529 | * 加载图片资源, 需返回 Blob 对象 530 | * @param url 原始资源地址 531 | */ 532 | loadImage(url: string): Promise; 533 | /** 534 | * 媒体文件重定向, mp3 和 mp4 资源会调用这个代理函数, 需返回重定向后的 url 535 | * @param url 原始资源地址 536 | */ 537 | redirectMedia(url: string): string; 538 | } 539 | ``` 540 | 541 | 一个什么也不干的 loaderDelegate 如下所示, 但是你可以对传入的 url 进行加工: 542 | 543 | ```typescript 544 | import { Slide, ILoaderDelegate } from "@netless/slide" 545 | 546 | const delegate: ILoaderDelegate = { 547 | async loadJson(url: string): Promise { 548 | const res = await fetch(url); 549 | return await res.text(); 550 | }, 551 | async loadImage(url: string): Promise { 552 | const res = await fetch(url) 553 | return await res.blob(); 554 | }, 555 | redirectMedia(url: string): string { 556 | return url; 557 | } 558 | } 559 | 560 | const slide = new Slide({ 561 | /// ... 其他初始化配置 562 | loaderDelegate: delegate, 563 | }) 564 | ``` 565 | 566 | ## 全局事件 567 | 568 | ### 页面渲染 569 | 570 | 页面渲染过程指从页面依赖的资源加载知道页面显示完成. 571 | 572 | ```javascript 573 | window.addEventListener("message", evt => { 574 | if (evt.data.type === "@slide/_render_start_") { 575 | console.log(evt.data.taskId); // ppt 转码任务的 taskId 576 | console.log(`第 ${evt.data.index} 页开始渲染`); 577 | } else if (evt.data.type === "@slide/_render_end_") { 578 | console.log(evt.data.taskId); // ppt 转码任务的 taskId 579 | console.log(`第 ${evt.data.index} 页开始结束`); 580 | } 581 | }); 582 | ``` 583 | 584 | ### 离线缓存 585 | 586 | 离线缓存会将 json、png 格式的资源和运行时生成的 svg 等资源缓存到 indexDB. 587 | 588 | ```javascript 589 | // 发起离线缓存 590 | // 缓存指定页码 591 | window.postMessage({ 592 | type: "@slide/_preload_slide_", 593 | taskId: "", // 转码任务的 taskId, 594 | prefix: "", // 转码任务返回的资源前缀 595 | pages: [1,2,3,4,5], // 缓存第 1 2 3 4 5 页 596 | sessionId: '12345', // 随机传, 用于区分不同的缓存 597 | }, "*"); 598 | 599 | 600 | window.postMessage({ 601 | type: "@slide/_preload_slide_", 602 | taskId: "", 603 | prefix: "", 604 | pages: [1,2,3,5,8,13], // 缓存 1 2 3 5 8 13 页, 可以跳页缓存 605 | sessionId: "32345", 606 | }, "*"); 607 | 608 | // 不传 pages 缓存所有页 609 | window.postMessage({ 610 | type: "@slide/_preload_slide_", 611 | taskId: "", 612 | prefix: "", 613 | sessionId: "all", 614 | }, "*"); 615 | 616 | // 监听缓存进度 617 | window.addEventListener("message", evt => { 618 | if (evt.data.type === "@slide/_preload_slide_progress_") { 619 | const { sessionId, taskId, progress } = evt.data; 620 | console.log(sessionId, taskId, progress); 621 | } else if (evt.data.type === "@slide/_preload_slide_error_") { 622 | // 缓存出错 转码任务的 taskId 623 | const { sessionId, taskId, error } = evt.data; 624 | console.log(sessionId, taskId, error); 625 | } 626 | }); 627 | ``` 628 | 629 | ## 更多用法 630 | 631 | ### 已转换PPT添加自定义`link` 632 | 633 | 要使用完整功能需2步 634 | 1. 在ppt中添加自定义的`link` 635 | 636 | `@netless/slide@1.4.32`以上版本支持 `addlink` 模式, 可以在已转换的 PPT 中添加自定义的 `link` 事件. 并通过监听 `SLIDE_EVENTS.useraddLink` 时间获取用户点击的元素 id. 637 | ```javascript 638 | import { Slide, SLIDE_EVENTS } from "@netless/slide"; 639 | 640 | const slide = new Slide({ 641 | anchor: someDivElement, 642 | interactive: true, 643 | mode: "addLink", 644 | logger: { 645 | info(msg: string) { 646 | console.log(msg); 647 | } 648 | } 649 | }); 650 | 651 | slide.current.on(SLIDE_EVENTS.useraddLink, (taskId: string, pageIndex: number, shapeId: string) => { 652 | console.log("useraddLink", taskId, pageIndex, shapeId); 653 | }); 654 | ``` 655 | 可以通过`SLIDE_EVENTS.useraddLink`事件获取用户点击的元素的`shapeId`, 通过弹框或其他方式让用户输入使用其他模式渲染时候需要跳转的`link`, 后续传入`slide`内使用 656 | 注意: 这个方法无法覆盖原有的`link`, 只能给无点击事件的元素添加. `addLink`只作为添加自定义`link`的模式, 不应该在多人房间内使用. 657 | 658 | 2. 将准备好的 `link` 传入 `slide` 内 659 | 同样需要 `@netless/slide@1.4.32`以上版本, 在创建 `slide` 实例时传入 `customLinks`和`navigatorDelegate`即可 660 | ```typescript 661 | import { Slide, CustomLink } from "@netless/slide"; 662 | const customLinks: CustomLink[] = [ 663 | { 664 | "pageIndex": 1, 665 | "shapeId": "slide-3", 666 | "link": "random11=1" 667 | } 668 | ]; 669 | 670 | const slide = new Slide({ 671 | anchor: someDivElement, 672 | interactive: true, 673 | mode: "interactive", // 按需选择 674 | // 自定义的 link 信息 675 | customLinks: customLinks, 676 | logger: { 677 | info(msg: string) { 678 | console.log(msg); 679 | } 680 | }, 681 | // 自定义的 link 跳转逻辑 682 | navigatorDelegate: { 683 | openUrl(url: string) { 684 | console.log("open url", url); 685 | } 686 | } 687 | }); 688 | ``` 689 | 点击元素后会触发`navigatorDelegate.openUrl`函数, 并传入`url`, 可以根据`url`参数自定义要完成的动作 690 | 691 | -------------------------------------------------------------------------------- /assets/slide-lifecycle.svg: -------------------------------------------------------------------------------- 1 |
new Slide()
new Slide()
setResource()
setResource()
renderSlide(1)
renderSlide(1)
renderStart
renderStart
renderEnd
renderEnd
No
No
yes
yes
是否渲染
成功
是否渲染成功...
renderError
renderError
nextStep()
nextStep()
slideChange
slideChange
yes
yes
是否存在
下一步动画
是否存在 2 | 下一步动画
继续下一步
继续下一步
执行 ppt 动画
执行 ppt 动画
mainSeqStepStart
mainSeqStepStart
mainSeqStepEnd
mainSeqStepEnd
No
No
跳转下一页
这一步是自动的
无需调用
跳转下一页这一步是自动的无需调用...
animateStart
animateStart
仅主序列动画触发
仅主序列动画触发
任意动画都会触发
任意动画都会触发
animateEnd
animateEnd
仅主序列动画触发
仅主序列动画触发
任意动画都会触发
任意动画都会触发
初始化
初始化
slide 方法
slide 方法
slide 事件
slide 事件
Viewer does not support full SVG 1.1
-------------------------------------------------------------------------------- /docs/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.abd63719.css", 4 | "main.js": "/static/js/main.8734c09d.js", 5 | "static/js/787.abff3aea.chunk.js": "/static/js/787.abff3aea.chunk.js", 6 | "index.html": "/index.html", 7 | "main.abd63719.css.map": "/static/css/main.abd63719.css.map", 8 | "main.8734c09d.js.map": "/static/js/main.8734c09d.js.map", 9 | "787.abff3aea.chunk.js.map": "/static/js/787.abff3aea.chunk.js.map" 10 | }, 11 | "entrypoints": [ 12 | "static/css/main.abd63719.css", 13 | "static/js/main.8734c09d.js" 14 | ] 15 | } -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-slide-demo/0a0a87f470e9b305ba3b5d52deb4b0c94f0bc0f8/docs/favicon.ico -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | React App 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /docs/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-slide-demo/0a0a87f470e9b305ba3b5d52deb4b0c94f0bc0f8/docs/logo192.png -------------------------------------------------------------------------------- /docs/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-slide-demo/0a0a87f470e9b305ba3b5d52deb4b0c94f0bc0f8/docs/logo512.png -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/static/css/main.abd63719.css: -------------------------------------------------------------------------------- 1 | body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{background-color:#999;background-image:linear-gradient(45deg,#00000036 25%,transparent 0,transparent 75%,#00000036 0,#00000036),linear-gradient(45deg,#00000036 25%,transparent 0,transparent 75%,#00000036 0,#00000036);background-position:0 0,15px 15px;background-size:30px 30px;height:100vh;width:100vw}.header{align-items:center;background:#fff;display:flex;height:40px;justify-content:space-between;padding:0 14px}.header>div>label{font-size:14px;margin:0 6px 0 12px}.header>div>label:first-child{margin-left:0}.anchor{height:100%;position:relative}.arrow{background:#4caf50;color:#fff;cursor:pointer;padding:6px 4px 6px 2px;position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);z-index:9}.content{align-items:center;display:flex;height:calc(100% - 40px);justify-content:space-around;width:100%} 2 | /*# sourceMappingURL=main.abd63719.css.map*/ -------------------------------------------------------------------------------- /docs/static/css/main.abd63719.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.abd63719.css","mappings":"AAAA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,mIAEY,CAHZ,QAMF,CAEA,KACE,uEAEF,CCZA,KACE,qBAAsB,CACtB,kMACiG,CAEjG,iCAAmC,CADnC,yBAA0B,CAG1B,YAAa,CADb,WAEF,CAEA,QAIE,kBAAmB,CAFnB,eAAgB,CAChB,YAAa,CAFb,WAAY,CAIZ,6BAA8B,CAC9B,cACF,CAEA,kBAEE,cAAe,CADf,mBAEF,CAEA,8BACE,aACF,CAEA,QAEE,WAAY,CADZ,iBAEF,CAEA,OAIE,kBAAmB,CACnB,UAAW,CAGX,cAAe,CALf,uBAAwB,CAFxB,iBAAkB,CAKlB,OAAQ,CACR,kCAA2B,CAA3B,0BAA2B,CAL3B,SAOF,CAEA,SAKE,kBAAmB,CAJnB,YAAa,CAEb,wBAAyB,CACzB,4BAA6B,CAF7B,UAIF","sources":["index.css","App.css"],"sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n",".App {\n background-color: #999;\n background-image: linear-gradient(45deg, #00000036 25%, transparent 25%, transparent 75%, #00000036 75%, #00000036),\n linear-gradient(45deg, #00000036 25%, transparent 25%, transparent 75%, #00000036 75%, #00000036);\n background-size: 30px 30px;\n background-position: 0 0, 15px 15px;\n width: 100vw;\n height: 100vh;\n}\n\n.header {\n height: 40px;\n background: #fff;\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 0 14px;\n}\n\n.header > div > label {\n margin: 0 6px 0 12px;\n font-size: 14px;\n}\n\n.header > div > label:first-child {\n margin-left: 0;\n}\n\n.anchor {\n position: relative;\n height: 100%;\n}\n\n.arrow {\n position: absolute;\n z-index: 9;\n padding: 6px 4px 6px 2px;\n background: #4caf50;\n color: #fff;\n top: 50%;\n transform: translateY(-50%);\n cursor: pointer;\n}\n\n.content {\n display: flex;\n width: 100%;\n height: calc(100% - 40px);\n justify-content: space-around;\n align-items: center;\n}\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/787.abff3aea.chunk.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunknetless_slide_demo=self.webpackChunknetless_slide_demo||[]).push([[787],{787:function(e,t,n){n.r(t),n.d(t,{getCLS:function(){return y},getFCP:function(){return g},getFID:function(){return C},getLCP:function(){return P},getTTFB:function(){return D}});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},f=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},s=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},v=-1,d=function(){return"hidden"===document.visibilityState?0:1/0},p=function(){f((function(e){var t=e.timeStamp;v=t}),!0)},l=function(){return v<0&&(v=d(),p(),s((function(){setTimeout((function(){v=d(),p()}),0)}))),{get firstHiddenTime(){return v}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(f&&f.disconnect(),e.startTime-1&&e(t)},r=u("CLS",0),a=0,o=[],v=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},d=c("layout-shift",v);d&&(n=m(i,r,t),f((function(){d.takeRecords().map(v),n(!0)})),s((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),v=u("FID"),d=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]); 2 | //# sourceMappingURL=787.abff3aea.chunk.js.map -------------------------------------------------------------------------------- /docs/static/js/787.abff3aea.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/js/787.abff3aea.chunk.js","mappings":"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","sources":["../node_modules/web-vitals/dist/web-vitals.js"],"sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},u=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},c=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),u((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},d=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=c(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),u((function(){s=0,l=-1,f=r(\"CLS\",0),n=c(i,f,t)})))},T={passive:!0,capture:!0},y=new Date,g=function(i,r){e||(e=r,t=i,n=new Date,w(removeEventListener),E())},E=function(){if(t>=0&&t1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){g(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,T),removeEventListener(\"pointercancel\",i,T)};addEventListener(\"pointerup\",n,T),addEventListener(\"pointercancel\",i,T)}(t,e):g(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,S,T)}))},L=function(n,f){var s,m=v(),d=r(\"FID\"),p=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"load\",(function(){return setTimeout(t,0)}))};export{h as getCLS,d as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","u","persisted","c","f","s","m","timeStamp","v","setTimeout","firstHiddenTime","d","disconnect","startTime","push","window","performance","getEntriesByName","requestAnimationFrame","p","l","h","hadRecentInput","length","takeRecords","T","passive","capture","y","g","w","E","entryType","target","cancelable","processingStart","forEach","S","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"sourceRoot":""} -------------------------------------------------------------------------------- /docs/static/js/main.8734c09d.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /* 8 | object-assign 9 | (c) Sindre Sorhus 10 | @license MIT 11 | */ 12 | 13 | /*! 14 | * @pixi/constants - v6.4.2 15 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 16 | * 17 | * @pixi/constants is licensed under the MIT License. 18 | * http://www.opensource.org/licenses/mit-license 19 | */ 20 | 21 | /*! 22 | * Spatial Plugin - Adds support for stereo and 3D audio where Web Audio is supported. 23 | * 24 | * howler.js v2.2.3 25 | * howlerjs.com 26 | * 27 | * (c) 2013-2020, James Simpson of GoldFire Studios 28 | * goldfirestudios.com 29 | * 30 | * MIT License 31 | */ 32 | 33 | /*! 34 | * howler.js v2.2.3 35 | * howlerjs.com 36 | * 37 | * (c) 2013-2020, James Simpson of GoldFire Studios 38 | * goldfirestudios.com 39 | * 40 | * MIT License 41 | */ 42 | 43 | /*! 44 | * @pixi/core - v6.4.2 45 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 46 | * 47 | * @pixi/core is licensed under the MIT License. 48 | * http://www.opensource.org/licenses/mit-license 49 | */ 50 | 51 | /*! 52 | * @pixi/polyfill - v6.4.2 53 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 54 | * 55 | * @pixi/polyfill is licensed under the MIT License. 56 | * http://www.opensource.org/licenses/mit-license 57 | */ 58 | 59 | /*! 60 | * @pixi/runner - v6.4.2 61 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 62 | * 63 | * @pixi/runner is licensed under the MIT License. 64 | * http://www.opensource.org/licenses/mit-license 65 | */ 66 | 67 | /*! 68 | * @pixi/ticker - v6.4.2 69 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 70 | * 71 | * @pixi/ticker is licensed under the MIT License. 72 | * http://www.opensource.org/licenses/mit-license 73 | */ 74 | 75 | /*! 76 | * @pixi/utils - v6.4.2 77 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 78 | * 79 | * @pixi/utils is licensed under the MIT License. 80 | * http://www.opensource.org/licenses/mit-license 81 | */ 82 | 83 | /*! 84 | * @pixi/canvas-display - v6.4.2 85 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 86 | * 87 | * @pixi/canvas-display is licensed under the MIT License. 88 | * http://www.opensource.org/licenses/mit-license 89 | */ 90 | 91 | /*! 92 | * @pixi/canvas-extract - v6.4.2 93 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 94 | * 95 | * @pixi/canvas-extract is licensed under the MIT License. 96 | * http://www.opensource.org/licenses/mit-license 97 | */ 98 | 99 | /*! 100 | * @pixi/canvas-particle-container - v6.4.2 101 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 102 | * 103 | * @pixi/canvas-particle-container is licensed under the MIT License. 104 | * http://www.opensource.org/licenses/mit-license 105 | */ 106 | 107 | /*! 108 | * @pixi/canvas-sprite - v6.4.2 109 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 110 | * 111 | * @pixi/canvas-sprite is licensed under the MIT License. 112 | * http://www.opensource.org/licenses/mit-license 113 | */ 114 | 115 | /*! 116 | * @pixi/canvas-sprite-tiling - v6.4.2 117 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 118 | * 119 | * @pixi/canvas-sprite-tiling is licensed under the MIT License. 120 | * http://www.opensource.org/licenses/mit-license 121 | */ 122 | 123 | /*! 124 | * @pixi/canvas-text - v6.4.2 125 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 126 | * 127 | * @pixi/canvas-text is licensed under the MIT License. 128 | * http://www.opensource.org/licenses/mit-license 129 | */ 130 | 131 | /*! 132 | * @pixi/compressed-textures - v6.4.2 133 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 134 | * 135 | * @pixi/compressed-textures is licensed under the MIT License. 136 | * http://www.opensource.org/licenses/mit-license 137 | */ 138 | 139 | /*! 140 | * @pixi/constants - v6.4.2 141 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 142 | * 143 | * @pixi/constants is licensed under the MIT License. 144 | * http://www.opensource.org/licenses/mit-license 145 | */ 146 | 147 | /*! 148 | * @pixi/core - v6.4.2 149 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 150 | * 151 | * @pixi/core is licensed under the MIT License. 152 | * http://www.opensource.org/licenses/mit-license 153 | */ 154 | 155 | /*! 156 | * @pixi/display - v6.4.2 157 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 158 | * 159 | * @pixi/display is licensed under the MIT License. 160 | * http://www.opensource.org/licenses/mit-license 161 | */ 162 | 163 | /*! 164 | * @pixi/extract - v6.4.2 165 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 166 | * 167 | * @pixi/extract is licensed under the MIT License. 168 | * http://www.opensource.org/licenses/mit-license 169 | */ 170 | 171 | /*! 172 | * @pixi/filter-displacement - v6.4.2 173 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 174 | * 175 | * @pixi/filter-displacement is licensed under the MIT License. 176 | * http://www.opensource.org/licenses/mit-license 177 | */ 178 | 179 | /*! 180 | * @pixi/math - v6.4.2 181 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 182 | * 183 | * @pixi/math is licensed under the MIT License. 184 | * http://www.opensource.org/licenses/mit-license 185 | */ 186 | 187 | /*! 188 | * @pixi/mixin-get-child-by-name - v6.4.2 189 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 190 | * 191 | * @pixi/mixin-get-child-by-name is licensed under the MIT License. 192 | * http://www.opensource.org/licenses/mit-license 193 | */ 194 | 195 | /*! 196 | * @pixi/mixin-get-global-position - v6.4.2 197 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 198 | * 199 | * @pixi/mixin-get-global-position is licensed under the MIT License. 200 | * http://www.opensource.org/licenses/mit-license 201 | */ 202 | 203 | /*! 204 | * @pixi/prepare - v6.4.2 205 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 206 | * 207 | * @pixi/prepare is licensed under the MIT License. 208 | * http://www.opensource.org/licenses/mit-license 209 | */ 210 | 211 | /*! 212 | * @pixi/runner - v6.4.2 213 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 214 | * 215 | * @pixi/runner is licensed under the MIT License. 216 | * http://www.opensource.org/licenses/mit-license 217 | */ 218 | 219 | /*! 220 | * @pixi/settings - v6.4.2 221 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 222 | * 223 | * @pixi/settings is licensed under the MIT License. 224 | * http://www.opensource.org/licenses/mit-license 225 | */ 226 | 227 | /*! 228 | * @pixi/ticker - v6.4.2 229 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 230 | * 231 | * @pixi/ticker is licensed under the MIT License. 232 | * http://www.opensource.org/licenses/mit-license 233 | */ 234 | 235 | /*! 236 | * pixi.js - v6.4.2 237 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 238 | * 239 | * pixi.js is licensed under the MIT License. 240 | * http://www.opensource.org/licenses/mit-license 241 | */ 242 | 243 | /*! 244 | * pixi.js-legacy - v6.4.2 245 | * Compiled Thu, 02 Jun 2022 15:39:26 UTC 246 | * 247 | * pixi.js-legacy is licensed under the MIT License. 248 | * http://www.opensource.org/licenses/mit-license 249 | */ 250 | 251 | /*! ***************************************************************************** 252 | Copyright (c) Microsoft Corporation. All rights reserved. 253 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 254 | this file except in compliance with the License. You may obtain a copy of the 255 | License at http://www.apache.org/licenses/LICENSE-2.0 256 | 257 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 258 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 259 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 260 | MERCHANTABLITY OR NON-INFRINGEMENT. 261 | 262 | See the Apache Version 2.0 License for specific language governing permissions 263 | and limitations under the License. 264 | ***************************************************************************** */ 265 | 266 | /*! ***************************************************************************** 267 | Copyright (c) Microsoft Corporation. All rights reserved. 268 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 269 | this file except in compliance with the License. You may obtain a copy of the 270 | License at http://www.apache.org/licenses/LICENSE-2.0 271 | 272 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 273 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 274 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 275 | MERCHANTABLITY OR NON-INFRINGEMENT. 276 | 277 | See the Apache Version 2.0 License for specific language governing permissions 278 | and limitations under the License. 279 | ***************************************************************************** */ 280 | 281 | /*! https://mths.be/punycode v1.4.1 by @mathias */ 282 | 283 | /** 284 | * @license 285 | * Copyright 2010-2021 Three.js Authors 286 | * SPDX-License-Identifier: MIT 287 | */ 288 | 289 | /** @license React v0.20.2 290 | * scheduler.production.min.js 291 | * 292 | * Copyright (c) Facebook, Inc. and its affiliates. 293 | * 294 | * This source code is licensed under the MIT license found in the 295 | * LICENSE file in the root directory of this source tree. 296 | */ 297 | 298 | /** @license React v17.0.2 299 | * react-dom.production.min.js 300 | * 301 | * Copyright (c) Facebook, Inc. and its affiliates. 302 | * 303 | * This source code is licensed under the MIT license found in the 304 | * LICENSE file in the root directory of this source tree. 305 | */ 306 | 307 | /** @license React v17.0.2 308 | * react-jsx-runtime.production.min.js 309 | * 310 | * Copyright (c) Facebook, Inc. and its affiliates. 311 | * 312 | * This source code is licensed under the MIT license found in the 313 | * LICENSE file in the root directory of this source tree. 314 | */ 315 | 316 | /** @license React v17.0.2 317 | * react.production.min.js 318 | * 319 | * Copyright (c) Facebook, Inc. and its affiliates. 320 | * 321 | * This source code is licensed under the MIT license found in the 322 | * LICENSE file in the root directory of this source tree. 323 | */ 324 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netless-slide-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@netless/slide": "^0.9.2", 7 | "@testing-library/jest-dom": "^5.16.1", 8 | "@testing-library/react": "^12.1.2", 9 | "@testing-library/user-event": "^13.5.0", 10 | "dat.gui": "^0.7.7", 11 | "eventemitter3": "^4.0.7", 12 | "qs": "^6.11.0", 13 | "react": "^17.0.2", 14 | "react-dom": "^17.0.2", 15 | "react-scripts": "5.0.0", 16 | "web-vitals": "^2.1.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-slide-demo/0a0a87f470e9b305ba3b5d52deb4b0c94f0bc0f8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-slide-demo/0a0a87f470e9b305ba3b5d52deb4b0c94f0bc0f8/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netless-io/netless-slide-demo/0a0a87f470e9b305ba3b5d52deb4b0c94f0bc0f8/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | background-color: #999; 3 | background-image: linear-gradient(45deg, #00000036 25%, transparent 25%, transparent 75%, #00000036 75%, #00000036), 4 | linear-gradient(45deg, #00000036 25%, transparent 25%, transparent 75%, #00000036 75%, #00000036); 5 | background-size: 30px 30px; 6 | background-position: 0 0, 15px 15px; 7 | width: 100vw; 8 | height: 100vh; 9 | } 10 | 11 | .header { 12 | height: 40px; 13 | background: #fff; 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | padding: 0 14px; 18 | } 19 | 20 | .header > div > label { 21 | margin: 0 6px 0 12px; 22 | font-size: 14px; 23 | } 24 | 25 | .header > div > label:first-child { 26 | margin-left: 0; 27 | } 28 | 29 | .anchor { 30 | position: relative; 31 | height: 100%; 32 | } 33 | 34 | .arrow { 35 | position: absolute; 36 | z-index: 9; 37 | padding: 6px 4px 6px 2px; 38 | background: #4caf50; 39 | color: #fff; 40 | top: 50%; 41 | transform: translateY(-50%); 42 | cursor: pointer; 43 | } 44 | 45 | .content { 46 | display: flex; 47 | width: 100%; 48 | height: calc(100% - 40px); 49 | justify-content: space-around; 50 | align-items: center; 51 | } 52 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef, useState } from "react"; 2 | import { parse } from "qs"; 3 | import { Slide, SLIDE_EVENTS } from "@netless/slide"; 4 | import './App.css'; 5 | import {RtcAudioPlayer} from "./RtcAudioPlayer"; 6 | 7 | function App() { 8 | 9 | const query = parse(window.location.search.replaceAll(/^\?/g, "")); 10 | 11 | const { minFPS, maxFPS, resolution, maxResolutionLevel, pptMode, task_id, prefix, forceCanvas } = query; 12 | 13 | const [useRtc, setUseRtc] = useState(false); 14 | const [mode, setMode] = useState(pptMode ? pptMode : "local"); 15 | const [taskId, setTaskId] = useState(task_id || "06415a307f2011ec8bdc15d18ec9acc7"); 16 | const [prefixUrl, setPrefixUrl] = useState(prefix || "https://convertcdn.netless.group/dynamicConvert"); 17 | 18 | const anchorA = useRef(null); 19 | const slideA = useRef(null); 20 | const slideB = useRef(null); 21 | const anchorB = useRef(null); 22 | 23 | const updateTaskId = useCallback((event) => { 24 | setTaskId(event.target.value); 25 | localStorage.setItem("slide-taskId", event.target.value); 26 | }, []); 27 | 28 | const updatePrefix = useCallback((event) => { 29 | setPrefixUrl(event.target.value); 30 | localStorage.setItem("slide-prefix", event.target.value); 31 | }, []); 32 | 33 | const updateMode = useCallback((event) => { 34 | setMode(event.target.value); 35 | localStorage.setItem("slide-mode", event.target.value); 36 | }, []); 37 | 38 | useEffect(() => { 39 | const mode = localStorage.getItem("slide-mode"); 40 | const taskId = localStorage.getItem("slide-taskId"); 41 | const prefix = localStorage.getItem("slide-prefix"); 42 | if (mode) { 43 | setMode(mode); 44 | } 45 | if (taskId) { 46 | setTaskId(taskId); 47 | } 48 | if (prefix) { 49 | setPrefixUrl(prefix); 50 | } 51 | }, []); 52 | 53 | useEffect(() => { 54 | console.log("[t]", taskId); 55 | if (anchorA.current) { 56 | slideA.current = new Slide({ 57 | anchor: anchorA.current, 58 | interactive: true, 59 | mode: mode, 60 | controller: true, 61 | rtcAudio: useRtc ? RtcAudioPlayer : undefined, 62 | logger: { 63 | info(msg) { 64 | console.log(msg); 65 | }, 66 | warn(msg) { 67 | console.warn(msg); 68 | }, 69 | error(msg) { 70 | console.error(msg); 71 | } 72 | }, 73 | renderOptions: { 74 | minFPS: minFPS ? parseInt(minFPS, 10) : undefined, 75 | maxFPS: maxFPS ? parseInt(maxFPS, 10) : undefined, 76 | resolution: resolution ? parseInt(resolution, 10) : undefined, 77 | maxResolutionLevel: maxResolutionLevel ? parseInt(maxResolutionLevel, 10) : undefined, 78 | forceCanvas: forceCanvas ? (forceCanvas === "true") : undefined, 79 | } 80 | }); 81 | slideA.current.on(SLIDE_EVENTS.stateChange, (s) => { 82 | console.log(s); 83 | }); 84 | slideA.current.on(SLIDE_EVENTS.renderError, (err, index) => { 85 | console.log(err, index); 86 | }); 87 | if (mode === "sync") { 88 | slideA.current.on(SLIDE_EVENTS.syncDispatch, (e) => { 89 | if (slideB.current) { 90 | slideB.current.emit(SLIDE_EVENTS.syncReceive, e); 91 | } 92 | }); 93 | } else if (mode === "interactive") { 94 | slideA.current.on(SLIDE_EVENTS.syncDispatch, (e) => { 95 | slideA.current.emit(SLIDE_EVENTS.syncReceive, e); 96 | if (slideB.current) { 97 | slideB.current.emit(SLIDE_EVENTS.syncReceive, e); 98 | } 99 | }); 100 | } 101 | slideA.current.setResource(taskId, prefixUrl); 102 | slideA.current.renderSlide(1); 103 | } 104 | return () => { 105 | slideA.current?.destroy(); 106 | }; 107 | }, [taskId, prefixUrl, mode, useRtc]); 108 | 109 | useEffect(() => { 110 | if (mode === "sync" || mode === "interactive") { 111 | slideB.current = new Slide({ 112 | anchor: anchorB.current, 113 | interactive: mode !== "sync", 114 | mode: mode, 115 | controller: true, 116 | rtcAudio: useRtc ? RtcAudioPlayer : undefined, 117 | renderOptions: { 118 | minFPS: minFPS ? parseInt(minFPS, 10) : undefined, 119 | maxFPS: maxFPS ? parseInt(maxFPS, 10) : undefined, 120 | resolution: resolution ? parseInt(resolution, 10) : undefined, 121 | maxResolutionLevel: maxResolutionLevel ? parseInt(maxResolutionLevel, 10) : undefined, 122 | } 123 | }); 124 | if (mode === "interactive") { 125 | slideB.current.on(SLIDE_EVENTS.syncDispatch, (e) => { 126 | slideB.current.emit(SLIDE_EVENTS.syncReceive, e); 127 | if (slideA.current) { 128 | slideA.current.emit(SLIDE_EVENTS.syncReceive, e); 129 | } 130 | }); 131 | } 132 | slideB.current.setResource(taskId, prefixUrl); 133 | slideB.current.renderSlide(1); 134 | } 135 | return () => { 136 | slideB.current?.destroy(); 137 | }; 138 | }, [mode, taskId, prefixUrl, useRtc]); 139 | 140 | return ( 141 |
142 |
143 |
144 | 145 | 146 | 147 | 148 |
149 |
150 | 启用 rtc 混音 151 | setUseRtc(!useRtc)}/> 152 | {" "} 153 | 158 |
159 |
160 |
161 |
162 |
slideA.current?.prevStep()} className={"arrow"} style={{left: "0"}}>{"<"}
163 |
slideA.current?.nextStep()} className={"arrow"} style={{right: "0"}}>{">"}
164 |
165 |
166 |
slideB.current?.prevStep()} className={"arrow"} style={{left: "0"}}>{"<"}
167 |
slideB.current?.nextStep()} className={"arrow"} style={{right: "0"}}>{">"}
168 |
169 |
170 |
171 | ); 172 | } 173 | 174 | export default App; 175 | -------------------------------------------------------------------------------- /src/RtcAudioPlayer.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "eventemitter3"; 2 | 3 | /** 4 | * 提供自定义的音频播放类, 以实现 rtc 混音效果。 5 | * 必须实现了标准的 EventEmitter 相关接口, 因为 `@netless/slide` 库需要监听 'load'、'play'、'pause'事件。 6 | * 以上三个事件必须实现。 7 | */ 8 | export class RtcAudioPlayer extends EventEmitter { 9 | constructor(src) { 10 | super(); 11 | this.audio = new Audio(src); 12 | this.audio.addEventListener("loadeddata", () => { 13 | // 在音频时长数据获取后, 触发 'load' 事件 14 | this.emit("load"); 15 | }); 16 | this.audio.addEventListener("play", () => { 17 | this.emit("play"); 18 | }); 19 | this.audio.addEventListener("pause", () => { 20 | this.emit("pause"); 21 | }); 22 | this.audio.load(); 23 | } 24 | 25 | play() { 26 | console.log("使用 rtc 播放器"); 27 | this.audio.play(); 28 | } 29 | 30 | pause() { 31 | this.audio.pause(); 32 | } 33 | 34 | destroy() { 35 | // 36 | } 37 | 38 | get currentTime() { 39 | return this.audio.currentTime; 40 | } 41 | set currentTime(t) { 42 | this.audio.currentTime = t; 43 | } 44 | 45 | get isPaused() { 46 | return this.audio.paused; 47 | } 48 | 49 | get duration() { 50 | return this.audio.duration; 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | window.__nativeTags = { 8 | platform: "ios" 9 | } 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | 18 | // If you want to start measuring performance in your app, pass a function 19 | // to log results (for example: reportWebVitals(console.log)) 20 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 21 | reportWebVitals(); 22 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------