## 1. Project Overview & Quickstart (netless-io/web)

# web

Open-source repository netless-io/web

### Repository Details
- **Repository:** [netless-io/web](https://github.com/netless-io/web)
- **Primary Language:** Code

*Note: High-volume repository documentation is actively indexed and synchronized by YakaAI.*

## 2. Official Technical Reference & Guides (netless-io/docs)

## File: README.md

# docs

本项目为 netless 白板官方文档站，由 CI 自动部署在[文档站](https://developer.netless.link)。

目前使用 docusaurus，满足以下需求：

1. 顶部大分类：iOS，Android，JavaScript 以及其他内容
2. 不同分类，独立导航，自定义顺序
3. 当前文章目录TOC
4. 相对地址跳转检查
5. 语法块切换

未满足以下需求：

1. 搜索
2. 多语言
3. 术语超链接，降低输入相对地址的复杂程度，全站同一术语，自动使用同一超链接
4. 自动生成导航目录，而不是手动编写（可能会与自定义排列顺序冲突）

---

## File: docs/server-v4/overview/introduction.md

---
id: server-introduction
title: 简要说明
sidebar_label: 简要说明
---

## 支持业务

sdk 客户端，目前只有对白板进行内容操作的 API，没有对白板房间进行管理的 API。
开发者需要通过 sdk token 与白板

sdk 服务器进行交互，执行创建房间，获取房间一系列信息，以及房间管理操作。 

## 业务服务器和 White 的关系



## 结合 RTC 服务
RTC 供应商一般也会有房间（room）或频道（channel）的概念。White 的一块白板内部称之为 room，room 的 uuid 属性全局唯一，用户可以把 RTC 的房间或频道和 White 的 room 一一对应起来。

---

## File: docs/server-v4/api/dynamic-conversion.md

---
id: server-dynamic-conversion
title: 文档转网页（动态文档转换）
---

动态文档转换是指将 pptx 格式的文件，转换成网页的服务。

## 文档转网页不支持的功能列表
目前已知无法支持的 pptx 功能如下：
1. 不支持 wps 文件，且 wps 转换为 pptx 后的文件不保证能成功解析
2. 不支持渐变色渲染
3. 不支持文字行末标点超出文本框（会导致自动换行）
4. 不支持艺术字
5. 不支持柱状图之类的图表
6. 不支持通过 SmartArt 功能创建的图形
7. 不支持页面切换时的过渡特效
8. 不支持文字动画及行动画（即文本框中每一行有一个动画）
9. 不支持 emf 或者 wmf 格式的图片
10. 不支持大多数动画特效比如溶解，棋盘等，目前只有淡入淡出特效
11. 如果转换结果字体缺失可以使用 sdk 中的自定义字体功能或者联系我们
12. 不支持文字的纵向排版
13. 不支持视频的裁剪功能
14. 不支持隐藏页的转换

文档转网页正在进行研发迭代，以上列表我们会持续更新

>相对于静态文档转换，动态文档转换，保留了 ppt 文件中的顺序动画，提供切换控制功能。
>动态`ppt`基于`Microsoft Office`规范，不能保证WPS文件能够正确解析。

## 准备工作

### 1. 根据 [配置云存储](/docs/blog/add-driver) 文章，在 [console](https://console.herewhite.com) 中配置云存储

### 2. 在管理控制台上开启动态文档服务

1. 进入 [console](https://console.herewhite.com)，点击左侧列表中的 <svg viewBox="64 64 896 896" class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"></svg> ，进入应用管理页面。

2. 找到 `文档转图片` 进行开通，更新 QPS ，结束操作。


**点击展开：console 中操作示意图**

* 动态文档转换初始状态


* 动态文档转换管理页面


* 关闭动态文档转换服务





## 转换 API

动态文档转换功能由“发起转换任务”和“查询转换任务”两个 API 组成

### 1. 发起转换


`POST /services/conversion/tasks`

>在服务端可以使用 sdk token。客户端封装类要求使用 roomToken，避免 sdk token 泄露。

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken 或 token | string | {{roomtoken}} 或 {{token}}|

* body参数

字段 | 类型 | 描述 |
--  | -- | -- |
sourceUrl | stirng | 需要进行转换的文件的地址 |
serviceType | string | 服务类型，动态文档转换固定为 "dynamic_conversion" |
preview | boolean | (**可选**)是否需要生成预览图，默认为 false |

> 注意：只有动态文档转换支持预览图功能，即 serviceType == "dynamic_conversion" 时，同时生成预览图需要消耗较长时间，请谨慎选择

* body

```json
{
    //请确保该文件可下载
    "sourceUrl": "https://xxxx.xxx.xxx.com/xxxx.pptx",
    "serviceType": "dynamic_conversion",
    "preview": false
}
```

* response

> 在发起转换任务前请确保您已经在 [console](https://console.herewhite.com) 上开启了`文档转网页`服务并配置 QPS 上限大于 0，否则该接口将会报 `Service not enable`、`Task waiting line is full`。

```JSON
{
    "code": 200,
    "msg": {
        "succeed": true,
        "reason": "",
        "taskUUID": "xxx6a660a6274c898b1689902734cxxx"
    }
}
```
task UUID 长度为 32 位，是转换任务的唯一标识。后续请求中需要以该 task 作为查询。

### 2. 查询转换进度

`GET /services/conversion/tasks/{{taskUUID}}/progress?serviceType=dynamic_conversion`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken 或 token | string | {{roomtoken}} 或 {{token}}|

* response

```JSON
{
    "code": 200,
    "msg": {
        "task": {
            "convertStatus": "Finished",
            "currentStep": "Extracting", // 当前转换任务步骤，只有 serviceType == dynamic_conversion 时才有该字段
            "reason": "",
            "totalPageSize": 3, // 文档总页数
            "convertedPageSize": 3, // 文档已转换完成页数
            "convertedPercentage": 100, // 文档转换进度百分比
            "convertedFileList": [  // 文档转换结果列表
                {
                    "width": 960,
                    "height": 720,
                    "conversionFileUrl": "dynamicConvert/{{taskUUID}}/slide/slide1.xml",
                    "preview": "https://xxx.xxx/xxx.png"    // 该字段仅在参数 preview == true 时出现
                },
                {
                    "width": 960,
                    "height": 720,
                    "conversionFileUrl": "dynamicConvert/{{taskUUID}}/slide/slide2.xml",
                    "preview": "https://xxx.xxx/xxx.png"
                },
                {
                    "width": 960,
                    "height": 720,
                    "conversionFileUrl": "dynamicConvert/{{taskUUID}}/slide/slide3.xml",
                    "preview": "https://xxx.xxx/xxx.png"
                }
            ],
            "prefix": "pptx://xxxx.xxx.xxx.com/" // 文档转换结果前缀
        }
    }
}
```

> 1. 动态转换任务将会返回每一页的宽高，该宽高单位是 px
> 2. 用户使用返回结果中的 "prefix" 仅在转换结果为 "Finished" 时存在
> 3. 转换任务需要用户轮询结果，时间间隔建议为 3 秒以上

`convertStatus` 存在以下几种情况：
- Waiting: 由于 QPS 到达上限等原因任务在等待中
- Converting: 任务正在执行中
- NotFound: 根据 taskUUID 未找到对应任务信息
- Finished: 任务执行完成且正常
- Fail: 任务执行失败，失败时，会有提示 reason

由于动态转换步骤较多，我们添加了当前步骤字段，用于表示任务进度，`currentStep` 存在以下几种取值
-  Extracting: 资源提取
-  Packaging: 资源打包
-  GeneratingPreview: 生成预览图
-  MediaTranscode: 媒体转码

## 数据拼接

获取转换结果后，需要自行进行拼接转换为 sdk 可用的场景数据（scenes）。

该部分可以交由客户端自行拼接，或者在服务器端拼好，以 JSON 格式发送给客户端。

### 1. 交由客户端自行拼接

将转换结果的 `json`，`taskId`，还有 `prefix` 转给客户端，进行拼接。（推荐方式，因为客户端仍然需要转换成 sdk 支持的 scene 类进行传入）



```javascript
// info 为转换结果返回的 response
const count = info.totalPageSize;
const scenes: {name: string, ppt: PptDescription}[] = [];
const ppts = info.convertedFileList;
for (let i = 0; i < ppts; ++ i) {
    const url = `${prefix}${ppts[i].conversionFileUrl}`;
    slideURLs[i] = url;
    scenes[i] = {
        // 请使用字符串
        name: `${i + 1}`,
        ppt: {src: url, width: info.width, height: info.height},
    };
}
// scenes 即为白板支持的数据格式
```

```Objective-C
// response 为转换结果返回的 response
NSInteger count = [response[@"totalPageSize"] integerValue];
NSArray *ppts = response[@"convertedFileList"];
NSMutableArray<WhiteScene *> *scenes = [NSMutableArray arrayWithCapacity:count];

for (int i = 0; i < ppts; i++) {
    NSDictionary *dict = ppts[i];
    WhitePptPage *pptPage = [[WhitePptPage alloc] init];
    pptPage.src = [NSString stringWithFormat:@"%@%@", prefixUrl ? : @"", dict[@"conversionFileUrl"]];
    pptPage.width = [response[@"width"] doubleValue];
    pptPage.height = [response[@"height"] doubleValue];
    WhiteScene *scene = [[WhiteScene alloc] initWithName:[NSString stringWithFormat:@"%d", i+1] ppt:pptPage];
    [scenes addObject:scene];
}
// scenes 数组，即为白板支持的数据格式。iOS 端接收到 JSON 后，可以主动转换为WhiteScenes
```

```Java
// json 即为查询结果 API 返回的 json
Integer count = json.get("totalPageSize").getAsInt();
JsonArray ppts = json.get("convertedFileList").getAsJsonArray();
Scene[] scenes = new Scene[count];

for (int i = 0; i < ppts.size(); i++) {
    PptPage pptPage = new PptPage(String.valueOf(i+1), json.get("width").getAsDouble(), json.get("height").getAsDouble());
    JsonObject object = array.get(i).getAsJsonObject();
    pptPage.setSrc(prefix + object.getAsJsonObject("conversionFileUrl").getAsString());
    sliderURLs[i] = pptPage.getSrc();
    scenes[i] = new Scene(String.valueOf(i+1), pptPage);
}
// scenes 数组，即为白板支持的数据格式。Android 端接收到 JSON 后，需要主动转换
```


### 2. 转换成 JSON 发给客户端

```JSON
scenes: [
    {
        //name 为字符串即可
        "name" : "1",
        // height，width 为 info 中返回的宽高
        "height" : {info.heigh},
        "width" : {info.width},
        //数字索引值+1，第一页即为 slide1.xml,第二页为 slide2.xml
        "src" : {prefix}/{taskId}/slide/slide1.xml
    },
    {
        //name 为字符串即可
        "name" : "2",
        // height，width 为 info 中返回的宽高
        "height" : {info.heigh},
        "width" : {info.width},
        //数字索引值+1，第一页即为 slide1.xml,第二页为 slide2.xml
        "src" : {prefix}/{taskId}/slide/slide2.xml
    }
]
```

## SDK 封装类使用

基于使用便捷性考虑，SDK 内部已经封装了转换任务完整的请求流程。在测试时，可以使用该 API。

>由于转换任务计算的是 QPS，即以每日峰值计费，所以不推荐在生产环境上，使用`SDK`端API。

### 使用转换 API

>iOS Android 2.2.0 新增 API



```js
import {WhiteWebSdk} from "white-web-sdk";
const whiteWebSdk = new WhiteWebSdk();
// 服务端鉴权用
const pptConverter = whiteWebSdk.pptConverter("输入 roomToken");


// 1、调用 pptConverter 的成员方法 convert 开始转码
// 2、convert 的参数类型参考 PptConvertParams
type PptConvertParams = {
    readonly url: string; //静态文档的网络地址，请确保可以下载
    readonly kind: PptKind; //文档转换类型，静态为 PptKind.static(typescript) 或者 "static" (javascript)
    readonly onProgressUpdated?: (progress: number) => void;
    readonly checkProgressInterval?: number;  // 轮询间隔时间
    readonly checkProgressTimeout?: number; // 超时时间
};

// 请求转码，获得每一个页面的数据
res = await pptConverter.convert({
    // ppt 在云存储中地址，注意需要在控制台配置
    url: pptURL,
    kind: "dynamic", 
    // 转换进度监听
    onProgressUpdated: progress => {
        if (onProgress) {
            onProgress(PPTProgressPhase.Converting, progress);
        }
    },
});

// convert 返回的数据结构
export type Ppt = {
    // 服务器端，转换任务的 uuid
    readonly uuid: string;
    readonly kind: PptKind;
    readonly width: number;
    readonly height: number;
    // 预览页面地址
    readonly slideURLs: ReadonlyArray<string>;
    // ppt场景数据格式，使用该属性，即可直接插入新的场景页面
    readonly scenes: ReadonlyArray<SceneDefinition>;
};

blog
room.putScenes(`/${filename}`, res.scenes);
room.setScenePath(`/${filename}/${res.scenes[0].name}`);
```


```Objective-C
#import <WhiteSDK.h>
// 详情请见 sdk WhiteConverter.h WhiteConversionInfo.h
@implementation RoomCommandListController
- (void)convertStatic {
  WhiteConverter *converter = [[WhiteConverter alloc] initWithRoomToken:self.roomToken];
  [converter startConvertTask:@"文档地址" type:ConvertTypeDynamic progress:^(CGFloat progress, WhiteConversionInfo * _Nullable info) {
      NSLog(@"progress:%f", progress);
  } completionHandler:^(BOOL success, ConvertedFiles * _Nullable ppt, WhiteConversionInfo * _Nullable info, NSError * _Nullable error) {
      NSLog(@"success:%d ppt: %@ error:%@", success, [ppt yy_modelDescription], error);
      if (ppt) {
          // 场景相关内容，详情参考[场景管理](/docs/advance/scenes.md)文档
          [self.room putScenes:@"/dynamic" scenes:ppt.scenes index:0];
          //第一页
          [self.room setScenePath:@"/dynamic/1"];
      }
  }];
}
@end
```


```Java
Converter c = new Converter(this.roomToken);
c.startConvertTask("文档地址", Converter.ConvertType.Dynamic, new ConverterCallbacks(){
    @Override
    public void onFailure(ConvertException e) {
        logAction("ppt fail");
    }

    @Override
    public void onFinish(ConvertedFiles ppt, ConversionInfo convertInfo) {
        if (ppt.getScenes() != null) {
            // 场景相关内容，详情参考[场景管理](/docs/advance/scenes.md)文档
            room.putScenes("dynamic", ppt.getScenes(), 0); 
            // 第一页
            room.setScenePath("dynamic/1");
        }
    }

    @Override
    public void onProgress(Double progress, ConversionInfo convertInfo) {
        logAction(String.valueOf(progress));
    }
});
```



### 进度顺序切换



```javascript
room.pptNextStep(); // 下一页（下一步）
room.pptPreviousStep() // 上一页（上一步）
```

```Objective-C
[room pptNextStep]; // 下一页（下一步）
[room pptPreviousStep]; // 上一页（上一步）
```

```Java
room.pptNextStep(); // 下一页（下一步）
room.pptPreviousStep(); // 上一页（上一步）
```


## 效果展示

<video style="width: 100%" loop="loop" autoplay="autoplay" id="video">
  
</video>

---

## File: docs/server-v4/api/dynamic-conversion-zip.md

---
id: server-dynamic-conversion-zip
title: 动态文档转换资源包
---

为了提升用户体验，在动态文档转换完成后，我们提供了渲染文档的资源包。
用户预先下载这些资源包并解压到指定本地目录后，渲染文档时可以直接读取本地资源文件，省去资源的网络下载时间。

> 注意：2020-05-25 之前已经转换完成的文档没有资源包供下载，只有在该日期之后新发起的转换任务才有资源包，用户需要判断资源包是否存在

## 资源包下载地址

在转换任务完成后，通过链接
```
https://convertcdn.netless.link/publicFiles.zip  // 公共资源包
https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}.zip   // 任务资源包
```
下载转换任务的资源包，其中，公共资源包是所有动态文档渲染所必须的一些文件，建议下载后保存到本地且不要删除。
任务资源包对应每一个动态转换任务。

> 目前动态文档转换功能没有支持用户自定义存储，因此下载域名固定为 https://convertcdn.netless.link，在支持自定义存储后，这个域名应该是用户自定义的存储域名

## 资源包解压后的目录结构

publicFiles.zip
```
/publicFiles
    /icon
        xxx.png
        ...
    /xml
        xxx.xml
        ...
    /pic
        xxx.png
        ...
    /info.json
```

{{taskUUID}}.zip
```
/{{taskUUID}}
    /layout
        xxx.xml
        ...
    /resource
        xxx.xml
        ...
    /slide
        xxx.xml
        ...
    ...
    /info.json
```

## 获取资源包后的操作

用户需要将资源包解压到本地，资源目录可以自行指定，公共资源包和任务资源包解压后的资源目录关系如下
例如：
用户指定 /root 为资源保存的根目录，那么用户的文件目录结构应该为：

> 其中 /root/convertcdn.netless.link 和 /root/convertcdn.netless.link/dynamicConvert 两个文件夹需要用户创建
> 在支持自定义存储后，convertcdn.netless.link 这个文件夹也应该修改为用户指定存储的域名

```shell
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/info.json
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/layout/layout1.xml
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/master/master1.xml
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/pptxml/ppt.xml
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/resources/resource1.json
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/resources/ppt/media/audio1.mp4
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/slide/slide1.xml
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/slideRef/slideRef1.xml
    /root/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/theme/theme1.xml
    /root/convertcdn.netless.link/publicFiles/icon/audio.png
    /root/convertcdn.netless.link/publicFiles/icon/pause.png
    /root/convertcdn.netless.link/publicFiles/icon/play.png
    /root/convertcdn.netless.link/publicFiles/pic/notSupport.png
    /root/convertcdn.netless.link/publicFiles/xml/PresetShapeDefinitions.xml
```
该例中公共资源存放在 /root/convertcdn.netless.link 路径下，任务资源存放在 /root/convertcdn.netless.link/dynamicConvert 路径下

文件路径如图：

---

## File: docs/server-v4/api/list-dynamic-resource.md

---
id: list-dynamic-resource
title: 列出动态转换的资源
---

该 api 用于下载所有 pptx 内的资源到本地，保证用户在运行文档转网页的结果时不会因为资源加载失败导致无法运行。
该 api 目前仅针对于 native 端用户使用，web 端用户不需要调用。
调用该 api 之前请先参考 [文档转网页](/docs/server/api/server-dynamic-conversion)

## 服务端 API

### 列出动态转换的资源

`GET /services/conversion/tasks/{{taskUUID}}/resources?serviceType=dynamic_conversion`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken 或 token | string | {{roomtoken}} 或 {{token}}|

* response

```JSON
{
    "code": 200,
    "msg": {
        "resources": [
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/info.json",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/layout/layout1.xml",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/master/master1.xml",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/pptxml/ppt.xml",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/resources/resource1.json",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/resources/ppt/media/audio1.mp4",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/slide/slide1.xml",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/slideRef/slideRef1.xml",
            "https://convertcdn.netless.link/dynamicConvert/{{taskUUID}}/theme/theme1.xml",
            "https://convertcdn.netless.link/publicFiles/icon/audio.png",
            "https://convertcdn.netless.link/publicFiles/icon/pause.png",
            "https://convertcdn.netless.link/publicFiles/icon/play.png",
            "https://convertcdn.netless.link/publicFiles/pic/notSupport.png",
            "https://convertcdn.netless.link/publicFiles/xml/PresetShapeDefinitions.xml"
        ]
    }
}
```

> 该 api 目前只支持 serviceType = dynamic_conversion，传入其他类型会被拒绝

## 获取资源列表后的操作

用户需要将 api 返回的所有资源下载到本地，且将返回的 url 根据 "/" 分隔为文件夹形式进行保存并压缩为 zip，协议头省略，资源保存的根目录可以自行指定。
例如：
用户调用了上述 api 且返回值为实例中的内容，之后指定 /root/{{taskUUID}} 为资源保存的根目录，那么用户的文件目录结构为：

* {{taskUUID}} 为用户传入的路径中的 param，建议使用 {{taskUUID}} 作为资源保存的根目录可以避免不同的文档资源冲突

```shell
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/info.json
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/layout/layout1.xml
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/master/master1.xml
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/pptxml/ppt.xml
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/resources/resource1.json
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/resources/ppt/media/audio1.mp4
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/slide/slide1.xml
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/slideRef/slideRef1.xml
    /root/{{taskUUID}}/convertcdn.netless.link/dynamicConvert/{{taskUUID}}/theme/theme1.xml
    /root/{{taskUUID}}/convertcdn.netless.link/publicFiles/icon/audio.png
    /root/{{taskUUID}}/convertcdn.netless.link/publicFiles/icon/pause.png
    /root/{{taskUUID}}/convertcdn.netless.link/publicFiles/icon/play.png
    /root/{{taskUUID}}/convertcdn.netless.link/publicFiles/pic/notSupport.png
    /root/{{taskUUID}}/convertcdn.netless.link/publicFiles/xml/PresetShapeDefinitions.xml
```

之后将 /root/{{taskUUID}}/convertcdn.netless.link 文件夹压缩为 zip 格式，这样用户在 /root/{{taskUUID}} 目录下就有了一个 convertcdn.netless.link.zip 文件，zip 包内解压后，内部应该是先有 convertcdn.netless.link 文件夹，然后依次根据网络地址依次有 dynamicConvert， publicFiles文件夹：

```shell
/convertcdn.netless.link/dynamicConvert
/convertcdn.netless.link/publicFiles
```

---

## File: docs/server-v4/api/postman-config.md

---
id: server-postman-config
title: Postman 配置文件
---

## v2.0版本 Postman 配置文件

如你也像我们一样使用 Postman 调试 API，恭喜这里有份 Postman 文件。可以导入（import）进你的 Postman 来协作调试。
[download: White Open API.postman_collection.json](https://sdk.herewhite.com/postman/White%20Open%20API%202.0.postman_collection.json)
*请替换其中的 token 为注册后得到的 token。*

---

## File: docs/server-v4/api/request.md

---
id: server-request
title: 请求规范
---

## API 请求域名

```plain
https://cloudcapiv4.herewhite.com
```

## 全局使用参数

阅读 [接入准备](/docs/blog/begin-netless/)，注册账号，获取该 token。

* {{token}} 参数

sdk 服务器，通过 URL 参数中的 token 进行用户验证。

* {{roomtoken}} 参数

用户在调用 [获取白板 roomtoken](/docs/server/api/server-whiteboard-base#获取特定白板-room-token) 后得到的参数，用于调用部分房间 api

## 请求格式

对于 POST 和 PUT 请求，请求的主体必须是 JSON 格式，而且 HTTP header 的 Content-Type 需要设置为 `application/json`

## 响应格式

对于所有的请求，响应格式都是一个 JSON 对象。

一个请求总会包含 code 字段，200 表示成功，成功的响应附带一个 msg 字段表示具体业务的返回内容：

```json
{
    "code": 200,
    "msg": {
        "room": {
            "id": 10987,
            "name": "111",
            "limit": 0,
            "teamId": 1,
            "adminId": 1,
            "uuid": "5d10677345324c0cb3febd3291e2a607",
            "updatedAt": "2018-08-14T11:19:04.895Z",
            "createdAt": "2018-08-14T11:19:04.895Z"
        },
        "roomToken": "xx"
    }
}
```

当一个请求失败时响应的主体仍然是一个 JSON 对象，但是总是会包含 `code` 和 `msg`这两个字段，你可以用它们来进行调试，举个例子：

```json
{
  "code": 112,
  "msg": "require uuid"
}
```

---

## File: docs/server-v4/api/static-conversion.md

---
id: server-static-conversion
title: 文档转图片（静态文档转换）
---


静态文档转换是指将 PPT、PPTX、Word、PDF 等格式文件，转换成图片的服务。主要是帮助客户在白板中插入演示资料来辅助在线授课或者远程会议。该功能由 SDK 服务器提供，需要与服务器进行交互。

在最新版本中，我们将这部分交互，封装在了 SDK 中，开发者只需要在后台开启服务，配置存储地址，即可在项目中，通过 `Converter` 类（不同平台，名称略有不同）进行转换。

## 文档转图片注意事项

1. 页数理想数值在 50 页以内，超过 100 页的文档有一定可能性转换超时。
2. PPT、PPTX、Word、PDF 以 PDF 转换结果最为准确。
3. 文档内引用的图片分辨率越高转换速度越慢。
4. 图片素材格式以 png 和 jpg 最为推荐。
5. 如果发现转码过程中样式表达过于不准确，请导出 pdf 后进行重新转换。
6. 本功能实现基于 [libreoffice](https://www.libreoffice.org/)，由于 libreoffice 历史悠久、代码复杂，遇到转换 bug 我们难以自行处理。所以客户使用前先做充分的与测试，如果不符合预期请使用三方转换服务。

## 准备工作

### 1. 根据 [配置云存储](/docs/blog/add-driver) 文章，在 [console](https://console.herewhite.com) 中配置云存储

### 2. 在管理控制台上开启静态文档服务

1. 进入 [console](https://console.herewhite.com)，点击左侧列表中的 <svg viewBox="64 64 896 896" class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"></svg> ，进入应用管理页面。

2. 找到 `文档转图片` 进行开通，更新 QPS ，结束操作。


**点击展开：console 中操作示意图**

* 静态文档转换初始状态


* 静态文档转换管理页面


* 关闭静态文档转换服务




## 转换 API

静态文档转换功能由“发起转换任务”和“查询转换任务”两个 API 组成

### 1. 发起转换

`POST /services/conversion/tasks`

>在服务端可以使用 sdk token。客户端封装类要求使用 roomToken，避免 sdk token 泄露。

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken 或 token | string | {{roomtoken}} 或 {{token}}|

* body参数

字段 | 类型 | 描述 |
--  | -- | -- |
sourceUrl | stirng | 需要进行转换的文件的地址 |
serviceType | string | 服务类型，静态文档转换固定为 "static_conversion" |
scale | number | (**可选**)图片缩放比例，取值范围在 0.1 到 3 之间，默认为 1.2 |
outputFormat | string | (**可选**)输出图片格式，默认为 png，可选参数为 png/jpg/jpeg |

> 在发起转换任务前请确保您已经在 console 上开启了“文档转图片”服务并配置 QPS 上限大于 0，否则该接口将会报"Service not enable"、"Task waiting line is full"等异常

> 注意：只有静态文档转换支持缩放功能，即 serviceType == "static_conversion" 时，不传为默认值，该值会改变生成的图片大小

> 注意：只有静态文档转换支持自定义输出格式，即 serviceType == "static_conversion" 时，不传为 png，jpg 与 jpeg 输出格式都为 jpeg

* body 例子

```json
{
    "sourceUrl": "https://xxxx.xxx.xxx.com/xxxx.pptx",
    "serviceType": "static_conversion",
    "scale": 1.2,
    "outputFormat": "jpg"
}
```

* response 例子

```JSON
{
    "code": 200,
    "msg": {
        "succeed": true,
        "reason": "",
        "taskUUID": "xxx6a660a6274c898b1689902734cxxx"
    }
}
```
task UUID 长度为 32 位，是转换任务的唯一标识

### 2. 查询转换进度

`GET /services/conversion/tasks/{{taskUUID}}/progress?serviceType=static_conversion`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken 或 token | string | {{roomtoken}} 或 {{token}}|

* response 例子

```JSON
{
    "code": 200,
    "msg": {
        "task": {
            "convertStatus": "Finished",
            "reason": "",
            "totalPageSize": 3, // 文档总页数
            "convertedPageSize": 3, // 文档已转换完成页数
            "convertedPercentage": 100, // 文档转换进度百分比
            "convertedFileList": [  // 文档转换结果列表
                {
                    "width": 1652,
                    "height": 2338,
                    "conversionFileUrl": "staticConvert/{{taskUUID}}/1.png"
                },
                {
                    "width": 1652,
                    "height": 2338,
                    "conversionFileUrl": "staticConvert/{{taskUUID}}/2.png"
                },
                {
                    "width": 1652,
                    "height": 2338,
                    "conversionFileUrl": "staticConvert/{{taskUUID}}/3.png"
                }
            ],
            "prefix": "https://xxxx.xxx.xxx.com/" // 文档转换结果前缀
        }
    }
}
```

> 1. 静态转换任务将会返回每一页的宽高，该宽高单位是 px，但由于数字可能会过大导致在白板中渲染时超出视野，用户可以仅仅使用比例，自定义合适的宽度或高度
> 2. 用户使用返回结果中的 "prefix" 仅在转换结果为 "Finished" 时有效
> 3. 转换任务需要用户轮询结果，时间间隔建议为 3 秒以上

`convertStatus` 存在以下几种情况：
- Waiting: 由于 QPS 到达上限等原因任务在等待中
- Converting: 任务正在执行中
- NotFound: 根据 taskUUID 未找到对应任务信息
- Finished: 任务执行完成且正常
- Fail: 任务执行失败，失败时，会有提示 reason

## 数据拼接

获取转换结果后，需要自行进行拼接转换为 sdk 可用的场景数据（scenes）。
该部分可以交由客户端自行拼接，或者在服务器端拼好，以 JSON 格式发送给客户端。

## SDK 封装类使用

基于使用便捷性考虑，SDK 内部已经封装了转换任务完整的请求流程。在测试时，可以使用该 API。

>由于转换任务计算的是 QPS，即以每日峰值计费，所以不推荐在生产环境上，使用`SDK`端API。

>iOS Android 2.2.0 新增 API



```js
import {WhiteWebSdk} from "white-web-sdk";
const whiteWebSdk = new WhiteWebSdk();
// 服务端鉴权用
const pptConverter = whiteWebSdk.pptConverter("输入 roomToken");


// 1、调用 pptConverter 的成员方法 convert 开始转码
// 2、convert 的参数类型参考 PptConvertParams
type PptConvertParams = {
    readonly url: string; //静态文档的网络地址，请确保可以下载
    readonly kind: PptKind; //文档转换类型，静态为 PptKind.static(typescript) 或者 "static" (javascript)
    readonly onProgressUpdated?: (progress: number) => void;
    readonly checkProgressInterval?: number;  // 轮询间隔时间
    readonly checkProgressTimeout?: number; // 超时时间
};

// 请求转码，获得每一个页面的数据
res = await pptConverter.convert({
    // ppt 在云存储中地址，注意需要在控制台配置
    url: pptURL,
    kind: "static",
    // 转换进度监听
    onProgressUpdated: progress => {
        if (onProgress) {
            onProgress(PPTProgressPhase.Converting, progress);
        }
    },
});

// convert 返回的数据结构
export type Ppt = {
    // 服务器端，转换任务的 uuid
    readonly uuid: string;
    readonly kind: PptKind;
    readonly width: number;
    readonly height: number;
    // 预览页面地址
    readonly slideURLs: ReadonlyArray<string>;
    // ppt场景数据格式，使用该属性，即可直接插入新的场景页面
    readonly scenes: ReadonlyArray<SceneDefinition>;
};

blog
room.putScenes(`/${filename}`, res.scenes);
room.setScenePath(`/${filename}/${res.scenes[0].name}`);
```


```Objective-C
#import <WhiteSDK.h>
// 详情请见 sdk WhiteConverter.h WhiteConversionInfo.h
@implementation RoomCommandListController
- (void)convertStatic {
  WhiteConverter *converter = [[WhiteConverter alloc] initWithRoomToken:self.roomToken];
  [converter startConvertTask:@"文档地址" type:ConvertTypeStatic progress:^(CGFloat progress, WhiteConversionInfo * _Nullable info) {
      NSLog(@"progress:%f", progress);
  } completionHandler:^(BOOL success, ConvertedFiles * _Nullable ppt, WhiteConversionInfo * _Nullable info, NSError * _Nullable error) {
      NSLog(@"success:%d ppt: %@ error:%@", success, [ppt yy_modelDescription], error);
      if (ppt) {
          // 场景相关内容，详情参考[场景管理](/docs/advance/scenes.md)文档
          [self.room putScenes:@"/static" scenes:ppt.scenes index:0];
          //第一页
          [self.room setScenePath:@"/static/1"];
      }
  }];
}
@end
```


```Java
Converter c = new Converter(this.roomToken);
c.startConvertTask("文档地址", Converter.ConvertType.Static, new ConverterCallbacks(){
    @Override
    public void onFailure(ConvertException e) {
        logAction("ppt fail");
    }

    @Override
    public void onFinish(ConvertedFiles ppt, ConversionInfo convertInfo) {
        if (ppt.getScenes() != null) {
            // 场景相关内容，详情参考[场景管理](/docs/advance/scenes.md)文档
            room.putScenes("static", ppt.getScenes(), 0);
            // 第一页
            room.setScenePath("static/1");
        }
    }

    @Override
    public void onProgress(Double progress, ConversionInfo convertInfo) {
        logAction(String.valueOf(progress));
    }
});
```



## 调用示意图

---

## File: docs/server-v4/api/whiteboard-base.md

---
id: server-whiteboard-base
title: 白板基础 API
---

## 主要 API

### 创建白板

`POST /room`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* body参数

字段 | 类型 | 描述 |
--  | -- | -- |
name | string | 白板名称|
limit | number | 设置为 0 是不限制，推荐使用设置为 0：房间不限制，从业务上去限制。 | 
mode | string | **v2版本参数**；房间类型：`persistent`,`historied` |

* 房间类型：

房间有两种模式：持久化房间、可回放房间。房间模式必须在创建时指定，一旦确定，将不可修改。不同模式，特征如下：

|    模式    | 可持久化 | 可回放 |                            描述                            |
| :--------: | :------: | :----: | :--------------------------------------------------------: |
| 持久化房间（默认）- `persistent` |    ✓     |   ✘   |        即便房间将永久存在，除非调用 API 手动删除。         |
| 可回放房间 - `historied` |    ✓     |   ✓    | 同「持久化房间」。并且房间所有内容将被自动录制，以供回放。 |

* body 例子

```json
{
    "name":"白板名称",
    "limit":0,
    "mode": "persistent"
}
```

* response

```JSON
{
    "code": 200,
    "msg": {
        "room": {
            "id": 650,
            "name": "console-room",
            "limit": 0,
            "teamId": 1,
            "adminId": 1,
            "mode": "persistent",
            "template": "meeting",
            "region": "cn",
            "uuid": "此处为房间 uuid",
            "updatedAt": "2019-01-15T09:12:05.974Z",
            "createdAt": "2019-01-15T09:12:05.974Z"
        },
        "hare": "{\"uuid\":\"uuid\",\"teamId\":\"1\",\"mode\":\"persistent\",\"region\":\"cn\",\"isBan\":false,\"beginTimestamp\":1547543526200,\"endTimestamp\":1547543526200,\"endFrameId\":0,\"usersMaxCount\":100,\"survivalDuration\":30000,\"chunkFramesCount\":700,\"snapshotIdInterval\":80}",
        "roomToken": "此处为房间 roomToken",
        "code": 201
    }
}
```

### 获取特定白板 room Token

`GET /room/{{uuid}}/roomtoken`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* query 参数

字段 | 类型 | 描述 |
--  | -- | -- |
uuid | string | 白板唯一标识符 |

该请求的 response 中，在 `msg` 字段中，可以获取到需要的 `roomToken` 字段。


**点击查看：旧版获取 roomtoken 接口**
`POST /room/join?uuid={{uuid}}`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* query 参数

字段 | 类型 | 描述 |
--  | -- | -- |
uuid | string | 白板唯一标识符 |

该请求的 response 中，在 `msg` 字段中，可以获取到需要的 `roomToken` 字段。

新旧版本接口同时有效且实现一致，仅 url 不同


## 白板信息

### 获取白板列表

`GET /room?offset={{offset}}&limit={{limit}}`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* query 参数

字段 | 类型 | 描述 |
--  | -- | -- |
offset | number | 第几块白板开始查找(从1开始计数) |
limit | number | 每次获取白板的个数 |


### 获取特定白板详细信息

`GET /room/id?uuid={{uuid}}`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* query 参数

字段 | 类型 | 描述 |
--  | -- | -- |
uuid | string | 白板唯一标识符 |

* response

```JSON
{
    "code": 200,
    "msg": {
        "id": 11600,
        "teamId": 1,
        "adminId": 1,
        "uuid": "此处为uuid",
        "name": "未命名",
        "limit": 0,
        "current": 0,
        "enable": true,
        "playable": false,
        "videoready": false,
        "mode": null,
        "region": "cn",
        "template": null,
        "createdAt": "2018-08-20T14:57:13.000Z",
        "updatedAt": "2018-08-26T05:56:36.000Z"
    }
}
```

### 获取特定白板页数

`GET /room/scenes/count?roomuuid={{uuid}}`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* query 参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomuuid | string | 白板唯一标识符 |

## 白板管理

### 禁用和恢复白板

`POST /banRoom`

使用该 API后，会将所有用户从房间中踢出，并无法在客户端 sdk 中执行加入房间操作。被禁用的白板，可以进行回放操作。

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
token | string | {{token}}|

* body 参数

字段 | 类型 | 描述 |
--  | -- | -- |
ban | boolean | true为禁用；false为恢复 |
uuid | string | 白板唯一标识符 |

---

## File: docs/server-v4/api/whiteboard-cover.md

---
id: server-whiteboard-cover
title: 获取封面 API
---

获取封面是将白板场景转换为图片的服务。
白板场景在转换为图片后可以用于实现缩略图、房间封面等功能。

关于场景的相关介绍可以参考:  [场景管理](/docs/javascript/guides/js-scenes)

## 准备工作

### 1. 根据 [配置云存储](/docs/blog/add-driver) 文章，在 [console](https://console.herewhite.com) 中配置云存储

### 2. 在管理控制台上开启获取封面服务

1. 进入 [console](https://console.herewhite.com)，点击左侧列表中的 <svg viewBox="64 64 896 896" class="" data-icon="appstore" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"></svg> ，进入应用管理页面。

2. 找到 `获取封面` 进行开通


**点击展开：console 中操作示意图**

* 获取封面服务初始状态


* 获取封面服务管理页面


* 关闭获取封面服务




## 特定场景封面

`POST /handle/rooms/single-snapshot`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken | string | {{roomtoken}}|

* body 参数

字段 | 类型 | 描述 |
--  | -- | -- |
width | number | 封面宽(px) |
height | number | 封面高(px) |
uuid | string | 白板唯一标识符 |
scenePath（可选） | string | 需要读取封面的场景路径（如果不传则返回当前场景的封面图片） |

**注意：该接口只能使用 room token**

* body 例子

```json
{
  "width": 240,
  "height": 180,
  "scenePath": "/init",
  "uuid": "xxxxxx4e96db4e6cb1ce492157xxxxxx"
}
```

* response

```JSON
{
     "code": 200,
    "msg": {
        "image": {
            "url": "url",   // 封面图片 url
            "key": "key", // 封面图片存放在存储服务中的 key
            "bucket": "bucket", // 封面图片存放在存储服务中的 bucket
            "region": "region"  // 封面图片存放在存储服务中的 region
        }
    }
}
```

## 范围内封面

`POST /handle/rooms/snapshots`

* header参数

字段 | 类型 | 描述 |
--  | -- | -- |
roomToken | string | {{roomtoken}}|

* body 参数

字段 | 类型 | 描述 |
--  | -- | -- |
width | number | 封面宽(px) |
height | number | 封面高(px) |
uuid | string | 白板唯一标识符 |
page （可选）| number | 返回值进行分页号（默认为 1 ）|
size（可选） | number | 返回列表每一页返回的场景截图数量（默认为5，最大为10）|
scenePath | string | 场景组路径 |

* body 例子

```json
{
    "width": 400,
    "height": 400,
    "uuid": "xxxxxx4e96db4e6cb1ce492157xxxxxx",
    "scenePath": "/",
    "page":1,
    "size": 1
}
```

关于分页的说明如下:

用户一次请求的场景截图数量是有限制的，后台会对指定场景组下的场景列表进行分页，返回用户输入 page 和 size 对应的数据，每一页最多返回 10 条数据。

关于场景路径的说明下:

假设用户有场景列表
* /physics/quantum-mechanics/first-chapter
* /physics/newtonian-mechanics
* /english

那么在用户传入 scenePath = "/physics" 后，该接口只会返回

* /physics/newtonian-mechanics
* /physics/relativity-theory

这两条截图数据，同样，在用户传入 scenePath = "/" 后，该接口只会返回 

* /english 

的截图数据

* response

```JSON
{
    "code": 200,
    "msg": [
        {
            "url": "url1",
            "key": "key1",
            "bucket": "bucket",
            "region": "region"
        },
        {
            "url": "url2",
            "key": "key2",
            "bucket": "bucket",
            "region": "region"
        }
    ]
}
```

**注意：该接口只能输入 room token**