图片与视频

更新时间: 2023/06/13 16:38:54

视频载入

互动白板既提供了工具栏,支持用户将视频上传至云信nos服务器,并加载到白板中。也支持直接通过url方式添加视频

上传图片和音视频文件需要关闭点播的防盗链,开启防盗链将导致文件无法共享给其他白板成员。

工具栏配置

items: [
    //其他工具
    {
        tool: 'docUpload',
        hint: '上传资源',
        supportPptToH5: true,
        supportDocToPic: true
        supportUploadMedia: true,
        supportTransMedia: true
    }
]

资源上传按钮(docUpload)是工具栏默认的工具。设置supportUploadMediatrue时,将支持上传mp3, mp4, aac等格式的音视频文件。支持supportTransMediatrue时,将支持aac, mp3, mov, mp4, wmv, flv, avi, mkv, mpeg等格式的音视频文件。supportTransMedia允许用户上传音视频后,通过云信服务器将音视频转码为兼容性更高的格式,从而更好的支持音视频在多端的兼容效果。

为了使用上传并转码功能,需要调用drawPlugin.setAppConfig({presetId: 234234})设置转码模板,具体请参见模板创建

下面是创建模板的示例代码。您在调用时,请将appkey, nonce, curtime, checksum替换为您应用的参数,这四个参数请参考点播的API概述。然后使用返回的presetId放入您的代码中。

curl --location --request POST 'https://vcloud.163.com/app/vod/preset/create' \
--header 'AppKey: 2a82b865c4bb70ea8335f7e387214645' \
--header 'nonce: 1c9dfb26-2575-4aa3-8b14-7d7b1e64a495' \
--header 'curtime: 1662024189' \
--header 'checksum: xxxxxxxxxxxxxxxxxxxxxxxxxxx' \
--header 'Content-Type: application/json' \
--data-raw '{
    "presetName":"preset",
    "sdMp4":1,
    "hdMp4":0,
    "shdMp4":0,
    "uhdMp4": 0,
    "sdFlv":0,
    "hdFlv":0,
    "shdFlv":0,
    "sdHls":0,
    "hdHls":0,
    "shdHls":0,
    "transConfig": [{
        "presetType": 1,
        "video": {
            "maxWidth": "auto",
            "maxHeight": "auto"
        } 
    }]
    }'

SDK接口

调用下面接口可以上传视频。如果没有设置boardName,则默认为当前文档。如果pageIndex未传入,则默认为调用该函数时的页面。

drawPlugin.addVideo({
    url: string,
    sourceType: string,     //视频文件的格式, 如: 'mp4'等
    title?: string
    pageIndex?: number,
    boardName?: string,
})

图片载入

互动白板既提供了工具栏,支持用户将图片上传至云信nos服务器,并加载到白板中。也支持直接通过url方式添加图片。

工具栏配置

items: [
    //其他工具
    {
        tool: 'image'
    }
]

SDK接口

调用下面接口可以上传图片。其中url可以是图片URL地址,亦可以是base64编码地址。如果没有设置boardName,则默认为当前文档。如果pageIndex未传入,则默认为调用该函数时的页面。

drawPlugin.addImage({
    url: string,
    pageIndex?: number,
    boardName?: string,
})

图片导出

互动白板可以将当前白板页的内容导出为图片。用户可以通过配置工具栏开启该功能。若用户想要自定义工具栏,也可以调用互动白板的接口。

工具栏配置

items: [
    //其他工具
    {
        tool: 'exportImage'
    }
]

SDK接口

调用下面接口可以导出图片

drawPlugin.exportAsImage()

客户端适配

客户端导出图片,与上传图片、音视频需要添加下面示例代码。源码请参考仓库:https://github.com/netease-im/whiteboard

Ios导出图片
  1. Info.plist中设置相册请求描述信息,支持app请求相册权限

  2. 设置SDK的wkDelegate,接收webView的相关回调,根据回调信息判断是否是IMG标签,保存图片

// https://github.com/netease-im/whiteboard/blob/master/ios/WhiteBoardWebDemo/WhiteBoardWebDemo/Controller/NTESWhiteBoardViewController.m#L274


- (void)onDecidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    if ([self needSaveImage:navigationAction]) {
        decisionHandler(WKNavigationActionPolicyCancel);
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }
}

- (void)onDecidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    decisionHandler(WKNavigationResponsePolicyAllow);
}


#pragma mark - Save Image

- (BOOL)needSaveImage:(WKNavigationAction *)navigationAction {
    NSString *requestString = navigationAction.request.URL.absoluteString;
    NSRange pngKeywordRange = [requestString rangeOfString:@"data:image/png;base64,"];
    NSRange jpegKeywordRange = [requestString rangeOfString:@"data:image/jpeg;base64,"];
    BOOL isValidImageString = (pngKeywordRange.location != NSNotFound) || (jpegKeywordRange.location != NSNotFound);
    if ((navigationAction.navigationType == WKNavigationTypeLinkActivated) && isValidImageString) {
        NSString *dataString = nil;
        if (pngKeywordRange.location != NSNotFound) {
            dataString = [requestString stringByReplacingCharactersInRange:pngKeywordRange withString:@""];
        } else {
            dataString = [requestString stringByReplacingCharactersInRange:jpegKeywordRange withString:@""];
        }
        
        NSData *imageData = [[NSData alloc] initWithBase64EncodedString:dataString options:NSDataBase64DecodingIgnoreUnknownCharacters];
        UIImage *image = [UIImage imageWithData:imageData];
        PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
        if (status == PHAuthorizationStatusNotDetermined) {
            [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
                if (status == PHAuthorizationStatusAuthorized) {
                    UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge void*)self);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [self.view makeToast:@"请开启相册权限" duration:2.0 position:CSToastPositionCenter];
                    });
                }
            }];
            
            return YES;
        }
        if (status == PHAuthorizationStatusAuthorized) {
            UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), (__bridge void*)self);
        } else {
            [self.view makeToast:@"请开启相册访问权限" duration:2.0 position:CSToastPositionCenter];
        }
        
        return YES;
    }
    
    return NO;
}

- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo: (void *)contextInfo {
    if (error != nil) {
        NSLog(@"Image Can not be saved");
        NSString *errMsg = [NSString stringWithFormat:@"图片保存失败%@", error.localizedDescription];
        [self.view makeToast:errMsg duration:2.0 position:CSToastPositionCenter];
    } else {
        NSLog(@"Successfully saved Image");
        [self.view makeToast:@"图片保存成功" duration:2.0 position:CSToastPositionCenter];
    }
}
Qt Quick导出图片

监听WebEngineViewDownloadRequested 信号,并调用 accept() 接收该信号。

    WebEngineView {
        id: webview
        anchors.fill: parent
        url: whiteboardUrl
        webChannel: channel

        property var downloads;
        profile.onDownloadRequested: {
            var arr = download.path.split('/');
            var name = arr[arr.length - 1];
            download.path = defaultDownloadPath + "/" + name;
            webview.downloads = download;
            download.accept();
        }

        profile.onDownloadFinished: {
            downloadFinished(download.path)
        }
    }
Android导出图片
//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardActivity.java
whiteboardWv.setDownloadListener((url, userAgent, contentDisposition, mimeType, contentLength) -> {
    String key = "base64,";
    int keyIndex = url.indexOf(key);
    String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
    String dataBase64Str = keyIndex < 0 ? url : url.substring(keyIndex + key.length());
    if (TextUtils.isEmpty(dataBase64Str)) {
        Log.e(TAG, "empty file");
        return;
    }
    byte[] dataOriginBytes = Base64.decode(dataBase64Str, Base64.DEFAULT);
    bgHandler.post(() -> Log.i(TAG, "dataOriginBytes=" + (dataOriginBytes == null ? "null" : HexDump.toHex(dataOriginBytes))));
    StringBuilder imgName = new StringBuilder();
    imgName.append(UUID.randomUUID().toString());
    if (!TextUtils.isEmpty(ext)) {
        imgName.append(".");
        imgName.append(ext);
    }
    File local = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath(), imgName.toString());
    try {
        if (!local.exists() && !local.createNewFile()) {
            Log.i(TAG, "path error, exist: " + local.exists());
            return;
        }
        FileOutputStream outputStream = new FileOutputStream(local, false);
        outputStream.write(dataOriginBytes);
        outputStream.close();
        Toast.makeText(this, "已下载到 " + local.getAbsolutePath(), Toast.LENGTH_LONG).show();
        Log.i(TAG, "download complete, path is " + local.getAbsolutePath());
    } catch (Throwable e) {
        Toast.makeText(this, "下载异常 " + local.getAbsolutePath(), Toast.LENGTH_SHORT).show();
        e.printStackTrace();
    }
});
Android选择文件
//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardJsInterface.java
/**
 * 1. 设置读取文件后的回调
 */
private ValueCallback<Uri[]> fileValueCallback;
public synchronized void setFileValueCallback(ValueCallback<Uri[]> callback) {
    fileValueCallback = callback;
}

//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardActivity.java
/**
 * 2. 监听webview中文件选择事件。Intent启动安卓系统的文件选择系统能力。同时设置文件读取后的回调文件路径
 */
private void initViews() {
    //...
    whiteboardWv.setWebChromeClient(new WebChromeClient() {
        @Override
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
            Log.i(TAG, "onShowFileChooser");
            try {
                jsInterface.setFileValueCallback(filePathCallback);
                Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
                intent.setType("*/*");
                intent.addCategory(Intent.CATEGORY_OPENABLE);
                startActivityForResult(intent, REQUEST_CODE_FILE_BROWSER);
                return true;
            } catch (Throwable e) {
                return false;
            }
        }
    });
    //...
}

//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardActivity.java
/**
 * 3. 监听activity result, 并调用onGetChosenFile处理结果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case REQUEST_CODE_FILE_BROWSER:
            onGetChosenFile(resultCode, data);
            break;
        default:
            break;
    }

}

//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardActivity.java
/**
 * 4. 获取Intent结果,传递文件Uri
 */
private void onGetChosenFile(int resultCode, Intent data) {
    if (resultCode != Activity.RESULT_OK || data == null) {
        jsInterface.transferFile(null);
        return;
    }
    Uri uri = data.getData();
    jsInterface.transferFile(uri);
}


//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardJsInterface.java
/**
 * 5. 回到jsInterface,使用transferFile激活回调函数
 */
public synchronized void transferFile(Uri uri) {
    if (fileValueCallback == null) {
        return;
    }

    fileValueCallback.onReceiveValue(uri == null ? null : new Uri[]{uri});
}
Android音视频自动播放

安卓用户推荐在设置webview中,无需手势交互,即可用自动播放音视频。设置方法如下:

//https://github.com/netease-im/whiteboard/blob/master/android/WhiteboardAndroidDemo/app/src/main/java/com/netease/whiteboardandroiddemo/whiteboard/WhiteboardActivity.java
/**
 * 允许白板中音视频在没有手势交互时自动播放
 */
if(android.os.Build.VERSION.SDK_INT >= 17) {
    whiteboardWv.getSettings().setMediaPlaybackRequiresUserGesture(false);
}

防盗链配置

为了防止您的资源被盗链而产生高额流量,网易云信建议您设置 CDN 资源的访问控制,保障您的资源不被盗播,您可以根据实际需要选择 Referer 防盗链、IP 防盗链、User-Agent 防盗链或 URL 鉴权。如果您开启 URL 鉴权防盗链,在使用白板SDK时,需要做一些额外的改动。点播-防盗链

具体来说,一是需要设置开启防盗链配置,这样每次上传资源时,会带上防盗链参数,即资源的桶名和对象名。二是需要配置防盗链鉴权函数。这样每次遇到有防盗链参数的资源时,都会通过该异步函数请求带有防盗链的URL地址。

Web端示例

WhiteBoardSDK.getInstance({
  getAntiLeechInfo: getAntiLeechInfo,
  drawPluginParams: {
    appConfig: {
      nosAntiLeech: true,
      nosAntiLeechExpire: 7200   //防盗链过期时间。应该和业务后台设置的过期时间保持一致。默认为2小时
    }
  }
})


function getAntiLeechInfo(prop, url) {
    const wsTime = Math.ceil((Date.now() / 1000))

    // 这里是一个示例函数。实际请求需要结合你的应用服务器的接口来实现
    return fetch('你的应用服务器地址', {
        body: {
            bucket: prop.bucket,
            object: prop.object,
            wsTime: wsTime
        }
    })
    .then(res => {
        return res.json()
    })
    .then(url => {
        return {
            url: `${url}?wsSecret=${res.data.wsSecret}&wsTime=${wsTime}`
        }
    })
  }

客户端示例

// 登录白板时配置资源上传时,设置防盗链参数
{
    action: 'jsJoinWB',
    param: {
        // 其它参数
        drawPluginParams: {
            appConfig: {
                nosAntiLeech: true,
                nosAntiLeechExpire: 7200   //防盗链过期时间。应该和业务后台设置的过期时间保持一致。默认为2小时
            }
        }
    }
}

// 收到 webGetAntiLeechInfo 后,返回 jsSendAntiLeechInfo。其中 url 为含有防盗链的 url 地址。seqId 为 webGetAntiLeechInfo 传入的 序列号
{
    action: 'jsSendAntiLeechInfo',
    param: {
        code: 200,
        seqId: param.seqId,                                    // webGetAntiLeechInfo 中的 seqId,代表这次请求的序列号
        url: urlWithAntiLeech
    }
}
此文档是否对你有帮助?
有帮助
去反馈
  • 视频载入
  • 工具栏配置
  • SDK接口
  • 图片载入
  • 工具栏配置
  • SDK接口
  • 图片导出
  • 工具栏配置
  • SDK接口
  • 客户端适配
  • 防盗链配置
  • Web端示例
  • 客户端示例