图片与视频
更新时间: 2024/07/24 13:59:48
视频载入
互动白板既提供了工具栏,支持用户将视频上传至云信nos
服务器,并加载到白板中。也支持直接通过url
方式添加视频
上传图片和音视频文件需要关闭点播的防盗链,开启防盗链将导致文件无法共享给其他白板成员。
工具栏配置
jsitems: [
//其他工具
{
tool: 'docUpload',
hint: '上传资源',
supportPptToH5: true,
supportDocToPic: true
supportUploadMedia: true,
supportTransMedia: true
}
]
资源上传按钮(docUpload)是工具栏默认的工具。设置supportUploadMedia
为true
时,将支持上传mp3, mp4, aac
等格式的音视频文件。支持supportTransMedia
为true
时,将支持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
未传入,则默认为调用该函数时的页面。
jsdrawPlugin.addVideo({
url: string,
sourceType: string, //视频文件的格式, 如: 'mp4'等
title?: string
pageIndex?: number,
boardName?: string,
})
图片载入
互动白板既提供了工具栏,支持用户将图片上传至云信nos
服务器,并加载到白板中。也支持直接通过url
方式添加图片。
工具栏配置
jsitems: [
//其他工具
{
tool: 'image'
}
]
SDK接口
调用下面接口可以上传图片。其中url可以是图片URL地址,亦可以是base64编码地址。如果没有设置boardName
,则默认为当前文档。如果pageIndex
未传入,则默认为调用该函数时的页面。
jsdrawPlugin.addImage({
url: string,
pageIndex?: number,
boardName?: string,
})
图片导出
互动白板可以将当前白板页的内容导出为图片。用户可以通过配置工具栏开启该功能。若用户想要自定义工具栏,也可以调用互动白板的接口。
工具栏配置
jsitems: [
//其他工具
{
tool: 'exportImage'
}
]
SDK接口
调用下面接口可以导出图片
jsdrawPlugin.exportAsImage()
客户端适配
客户端导出图片,与上传图片、音视频需要添加下面示例代码。源码请参考仓库:https://github.com/netease-im/whiteboard
-
在
Info.plist
中设置相册请求描述信息,支持app请求相册权限 -
设置SDK的
wkDelegate
,接收webView的相关回调,根据回调信息判断是否是IMG标签
,保存图片
objective-c// 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];
}
}
监听WebEngineView
的 DownloadRequested
信号,并调用 accept()
接收该信号。
javascript 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)
}
}
java//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();
}
});
java//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});
}
安卓用户推荐在设置webview中,无需手势交互,即可用自动播放音视频。设置方法如下:
java//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端示例
jsWhiteBoardSDK.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}`
}
})
}
客户端示例
js// 登录白板时配置资源上传时,设置防盗链参数
{
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
}
}