环信HarmonyOS IM SDK 正式版已经发布,该版本全面覆盖即时通讯(IM)的核心功能,为用户提供了完整的IM全功能体验,同时支持从Android APK到 NEXT 的数据迁移,更好地满足企业在不同业务场景下的适配需求。
环信鸿蒙IM SDK 除文本消息外,还支持发送附件类型消息,包括语音、图片、视频和文件消息。本文为大家介绍从系统获取系统图片,视频,语音文件,使用sdk进行发送与下载。
一、发送附件消息
1、发送图片消息
① 权限添加
在对应模块的module.json5配置中,添加requestPermissions节点和对应权限的配置:
javascript
{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:app_name",
"usedScene": {
"abilities": [
"EntryAbility"
]
}
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:app_name",
"usedScene": {
"abilities": [
"EntryAbility"
]
}
}
② 权限检测
javascript
//检查权限
export async function checkAppPermission(context:common.UIAbilityContext): Promise<boolean> {
try {
const READ_MEDIA_PERMISSION: Permissions = 'ohos.permission.READ_MEDIA' //媒体读取权限
const WRITE_MEDIA_PERMISSION: Permissions = 'ohos.permission.WRITE_MEDIA' //媒体写入权限
let permissionList: Permissions[] = []; //需要申请选项列表
let readPermission = await checkPermissions(READ_MEDIA_PERMISSION)//检查是否有媒体读取权限
!readPermission && permissionList.push(READ_MEDIA_PERMISSION)
let writePermission = await checkPermissions(WRITE_MEDIA_PERMISSION)//检查是否有媒体写入权限
!writePermission && permissionList.push(READ_MEDIA_PERMISSION)
if (permissionList.length) {
//申请权限
let res: boolean = await applyPermission(context, permissionList)
if (!res) {//用户未同意授权
AlertDialog.show({
title: "提示",
message: "无权限读写用户外部存储中的媒体文件信息,请前往系统设置开启",
alignment: DialogAlignment.Center,
secondaryButton: {
value: '关闭',
action: () => {
}
}
})
}
return res
}
return true
}
catch (e) {
return Promise.reject(e)
}
}
检查用户权限:
javascript
//检查用户权限
//@params permissions:权限名称数组
export async function checkPermissions(permissions: Permissions): Promise<boolean> {
try {
let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions);
return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}
catch (e) {
return Promise.reject(e)
}
}
从相册中选择图片:
javascript
//从相册选择
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length) {
//复制图片到缓存目录(缓存目录才有读写权限)
let filePath = PhotoSelectResult.photoUris[0]
if (filePath) {
//这里获取到的filePath 就是图片的路径
}
}
发送方调用 createImageSendMessage 方法传入图片的本地资源标志符 URI、设置是否发送原图以及接收方的用户 ID (群聊或聊天室分别为群组 ID 或聊天室 ID)创建图片消息,然后调用 sendMessage 方法发送该消息。SDK 会将图片上传至环信服务器,服务器自动生成图片缩略图。
javascript
这里imageFilePathOrUri 为上面获取到的filePath
// `imageFilePathOrUri` 为图片本地路径或者Uri。
let message = ChatMessage.createImageSendMessage(toChatUsername, imageFilePathOrUri);
// 会话类型,包含 `Chat`、`GroupChat` 和 `ChatRoom`,表示单聊、群聊或聊天室,默认为单聊。
message.setChatType(ChatType.GroupChat);
// 发送消息
ChatClient.getInstance().chatManager()?.sendMessage(message);
2、发送视频消息
① 缩略图获取
javascript
import picker from '@ohos.file.picker';
import {checkAppPermission} from '../../utils/permissions/CheckAccess'
import { common } from '@kit.AbilityKit';
import camerapicker from '@ohos.multimedia.cameraPicker';
import camera from '@ohos.multimedia.camera';
import { BusinessError } from '@ohos.base';
import { ChatMessage, } from 'easemob'
import SendMessages from '../../presenter/SendMessage'
//全局公共样式
@Styles
function fillScreen() {
.width('100%')
.backgroundColor(Color.White)
.borderRadius(20)
.padding(10)
}
@CustomDialog
@Component
export struct AttachmentMessageDialog {
@Prop conversationId: ResourceStr
@Prop imgMsg: ChatMessage|undefined = undefined
changeMessage = (imgMsg: ChatMessage|undefined)=> {}
private controller: CustomDialogController
private context = getContext(this) as common.UIAbilityContext; //UIAbilityContext
build() {
Column() {
Row(){
Image($r('app.media.ease_chat_takepic_pressed'))
.width(20)
.height(20)
Text("相机")
.fontColor('#33b1ff')
}
.width('100%')
.padding({top:30,left:20,bottom:20})
.onClick(async ()=>{this.camera()})
Row(){}.width('100%').backgroundColor("#e7e9eb").height(1)
Row(){
Image($r('app.media.ease_chat_image_pressed'))
.width(20)
.height(20)
Text("相册")
.fontColor('#33b1ff')
}
.width('100%')
.padding({top:20,left:20,bottom:20})
.onClick(async () =>{this.photo()})
Row(){}.width('100%').backgroundColor("#e7e9eb").height(1)
Row(){
Image($r('app.media.em_chat_video_pressed'))
.width(20)
.height(20)
Text("视频")
.fontColor('#33b1ff')
}
.width('100%')
.padding({top:20,left:20,bottom:20})
.onClick(async ()=>{ this.video()})
Row(){}.width('100%').backgroundColor("#e7e9eb").height(1)
Row(){
Image($r('app.media.em_chat_card_normal'))
.width(20)
.height(20)
Text("名片")
.fontColor('#33b1ff')
}
.width('100%')
.padding({top:20,left:20,bottom:20})
.onClick( ()=>{
})
Row(){}.width('100%').backgroundColor("#e7e9eb").height(7)
Row(){
Text("取消")
.fontColor('#33b1ff')
}
.padding({top:20,left:20,bottom:20})
}
.width('100%')
}
private async photo(){
//检查是否有读写外部媒体权限
let res: boolean = await checkAppPermission(this.context)
//无权限返回
if (!res) return
//从相册选择
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length) {
//复制图片到缓存目录(缓存目录才有读写权限)
let filePath = PhotoSelectResult.photoUris[0]
if (filePath) {
SendMessages.sendImageMessage(this.conversationId+"", filePath).then((value)=>{
this.changeMessage(value)
})
}
}
})
}
private async camera(){
//检查是否有读写外部媒体权限
let res: boolean = await checkAppPermission(this.context)
//无权限返回
if (!res) return
try {
let pickerProfile: camerapicker.PickerProfile = {
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
};
let pickerResult: camerapicker.PickerResult = await camerapicker.pick(this.context,
[camerapicker.PickerMediaType.PHOTO, camerapicker.PickerMediaType.PHOTO], pickerProfile);
if(pickerResult?.resultUri){
}
} catch (error) {
let err = error as BusinessError;
console.error(`the pick call failed. error code: ${err.code}`);
}
}
private async video(){
console.log(this.conversationId+"------")
//检查是否有读写外部媒体权限
let res: boolean = await checkAppPermission(this.context)
//无权限返回
if (!res) return
//从相册选择
let PhotoSelectOptions = new picker.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE;
PhotoSelectOptions.maxSelectNumber = 1;
let photoPicker = new picker.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions).then(async (PhotoSelectResult) => {
if (PhotoSelectResult.photoUris.length) {
//复制图片到缓存目录(缓存目录才有读写权限)
let filePath = PhotoSelectResult.photoUris[0]
if (filePath) {
SendMessages.sendVideoMessage(this.conversationId+"", filePath,this.context).then((value)=>{
this.changeMessage(value)
})
console.log("PhotoSelectOptions",filePath)
}
}
})
}
}
②发送方调用 createVideoSendMessage 方法传入接收方的用户 ID(群聊或聊天室分别为群组 ID 或聊天室 ID)、视频文件的本地路径、视频时长以及缩略图的本地存储路径,然后调用 sendMessage 方法发送消息。SDK 会将视频文件上传至消息服务器。若需要视频缩略图,你需自行获取视频首帧的路径,将该路径传入 createVideoSendMessage 方法。
上面获取到视频首诊
javascript
// 在应用层获取视频首帧
let thumbPath = this.getThumbPath(videoPath);
let message = ChatMessage.createVideoSendMessage(toChatUsername, videoPath, videoLength, thumbPath);
if (!message) {
return;
}
// 会话类型,包含 `Chat`、`GroupChat` 和 `ChatRoom`,表示单聊、群聊或聊天室,默认为单聊。
message.setChatType(ChatType.GroupChat);
// 发送消息
ChatClient.getInstance().chatManager()?.sendMessage(message);
3、发送语音消息
①音频录制
javascript
// 音频录制
import { media } from '@kit.MediaKit';
import { BusinessError, systemDateTime } from '@kit.BasicServicesKit';
import fs from '@ohos.file.fs';
class AudioRecorder {
private avRecorder: media.AVRecorder | undefined = undefined;
public maxAmplitude: number = 0;
public audioPath = ""
private avProfile: media.AVRecorderProfile = {
audioBitrate: 100000, // 音频比特率
audioChannels: 2, // 音频声道数
audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
audioSampleRate: 48000, // 音频采样率
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
};
private avConfig: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
profile: this.avProfile,
url: 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件
};
// 注册audioRecorder回调函数
setAudioRecorderCallback(): void {
if (this.avRecorder !== undefined) {
// 状态机变化回调函数
this.avRecorder.on('stateChange', (state: media.AVRecorderState, _: media.StateChangeReason) => {
console.log(`AudioRecorder current state is ${state}`);
})
// 错误上报回调函数
this.avRecorder.on('error', (err: BusinessError) => {
console.error(`AudioRecorder failed, code is ${err.code}, message is ${err.message}`);
})
}
}
// 开始录制对应的流程
async startRecordingProcess(): Promise<void> {
if (this.avRecorder !== undefined) {
await this.avRecorder.release();
this.avRecorder = undefined;
}
// 1.创建录制实例
this.avRecorder = await media.createAVRecorder();
this.setAudioRecorderCallback();
// 2.获取录制文件fd赋予avConfig里的url;参考FilePicker文档
const context = getContext(this);
const path = context.filesDir;
let currentTimeMillis: number = new Date().getTime();
const filepath = path + '/' + currentTimeMillis + '.wav';
this.audioPath = filepath
const file = fs.openSync(filepath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
const fdNumber = file.fd;
this.avConfig.url = 'fd://' + fdNumber;
// 3.配置录制参数完成准备工作
await this.avRecorder.prepare(this.avConfig);
// 4.开始录制
await this.avRecorder.start();
}
// 暂停录制对应的流程
async pauseRecordingProcess(): Promise<void> {
if (this.avRecorder !== undefined && this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换
await this.avRecorder.pause();
}
}
// 恢复录制对应的流程
async resumeRecordingProcess(): Promise<void> {
if (this.avRecorder !== undefined && this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换
await this.avRecorder.resume();
}
}
// 停止录制对应的流程
async stopRecordingProcess(): Promise<void> {
if (this.avRecorder !== undefined) {
// 1. 停止录制
if (this.avRecorder.state === 'started'
|| this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换
await this.avRecorder.stop();
}
// 2.重置
await this.avRecorder.reset();
// 3.释放录制实例
await this.avRecorder.release();
this.avRecorder = undefined;
}
}
}
let audioRecorder = new AudioRecorder()
export default audioRecorder as AudioRecorder
②发送方调用 createVoiceSendMessage 方法传入接收方的用户 ID(群聊或聊天室分别为群组 ID 或聊天室 ID)、语音文件的路径和语音时长创建语音消息,然后调用 sendMessage 方法发送消息。SDK 会将语音文件上传至环信服务器
javascript
// `filePathOrUri` 为语音文件的本地路径或者文件的 URI,`duration` 为语音时长(单位为秒)。
let message = ChatMessage.createVoiceSendMessage(to, filePathOrUri, duration);
if (!message) {
return;
}
// 设置会话类型,即`ChatMessage` 类的 `ChatType` 属性,包含 `Chat`、`GroupChat` 和 `ChatRoom`,表示单聊、群聊或聊天室,默认为单聊。
message.setChatType(ChatType.GroupChat);
// 发送消息
ChatClient.getInstance().chatManager()?.sendMessage(message);
二、接收附件消息
1、接收语音文件
接收方收到 onMessageReceived 回调,调用 getRemoteUrl 或 getLocalPath 方法获取语音文件的服务器地址或本地路径,从而获取语音文件。
javascript
let voiceBody = message.getBody() as VoiceMessageBody;
// 获取语音文件在服务器的地址。
let voiceRemoteUrl = voiceBody.getRemoteUrl();
// 本地语音文件的本地路径。
let voiceLocalPath = voiceBody.getLocalPath();
2、接收图片消息
接收方收到图片消息,自动下载图片缩略图。
SDK 默认自动下载缩略图,即 ChatOptions.setAutoDownloadThumbnail 设置为 true。若设置为手动下载缩略图,即 ChatOptions.setAutoDownloadThumbnail 设置为 false ,需调用 ChatClient.getInstance().chatManager()?.downloadThumbnail(message) 下载。
接收方收到 onMessageReceived 回调,调用 downloadAttachment 下载原图。
javascript
let msgListener: ChatMessageListener = {
onMessageReceived: (messages: ChatMessage[]): void => {
messages.forEach( message => {
if (message.getType() === ContentType.IMAGE) {
let callback: ChatCallback = {
onSuccess: (): void => {
// 附件下载成功
},
onError: (code: number, error: string): void => {
// 附件下载失败
},
onProgress: (progress: number): void => {
// 附件下载进度
}
}
message.setMessageStatusCallback(callback);
// 下载附件
ChatClient.getInstance().chatManager()?.downloadAttachment(message);
}
})
}
}
3、接收视频消息
接收方收到视频消息时,自动下载视频缩略图。
SDK 默认自动下载缩略图,即 ChatOptions.setAutoDownloadThumbnail 设置为 true 。若设置为手动下载缩略图,即 ChatOptions.setAutoDownloadThumbnail 设置为 false,需调用 ChatClient.getInstance().chatManager()?.downloadThumbnail(message) 下载。
接收方收到 onMessageReceived 回调,可以调用 ChatClient.getInstance().chatManager()?.downloadAttachment(message) 方法下载视频原文件
javascript
let msgListener: ChatMessageListener = {
onMessageReceived: (messages: ChatMessage[]): void => {
messages.forEach( message => {
if (message.getType() === ContentType.VIDEO) {
let callback: ChatCallback = {
onSuccess: (): void => {
// 附件下载成功
},
onError: (code: number, error: string): void => {
// 附件下载失败
},
onProgress: (progress: number): void => {
// 附件下载进度
}
}
message.setMessageStatusCallback(callback);
// 下载附件
ChatClient.getInstance().chatManager()?.downloadAttachment(message);
}
})
}
}
获取视频缩略图和视频原文件。
javascript
let body = message.getBody() as VideoMessageBody;
// 从服务器端获取视频文件。
let imgRemoteUrl = body.getRemoteUrl();
// 从服务器获取视频缩略图文件。
let thumbnailUrl = body.getThumbnailRemoteUrl();
// 从本地获取视频文件文件。
let localPath = body.getLocalPath();
// 从本地获取视频缩略图文件。
let localThumbPath = body.getThumbnailLocalPath();