├── docs
├── favicon.ico
├── logo192.png
├── logo512.png
├── robots.txt
├── asset-manifest.json
├── manifest.json
├── index.html
└── static
│ ├── css
│ ├── main.abd63719.css
│ └── main.abd63719.css.map
│ └── js
│ ├── 787.abff3aea.chunk.js
│ ├── 787.abff3aea.chunk.js.map
│ └── main.8734c09d.js.LICENSE.txt
├── public
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── manifest.json
└── index.html
├── src
├── setupTests.js
├── index.css
├── reportWebVitals.js
├── index.js
├── App.css
├── RtcAudioPlayer.js
└── App.js
├── .gitignore
├── package.json
├── README.md
└── assets
└── slide-lifecycle.svg
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netless-io/netless-slide-demo/HEAD/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netless-io/netless-slide-demo/HEAD/docs/logo192.png
--------------------------------------------------------------------------------
/docs/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netless-io/netless-slide-demo/HEAD/docs/logo512.png
--------------------------------------------------------------------------------
/docs/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netless-io/netless-slide-demo/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netless-io/netless-slide-demo/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/netless-io/netless-slide-demo/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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*/
--------------------------------------------------------------------------------
/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/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | 
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 |
--------------------------------------------------------------------------------