对接声网SDK问题汇总及相关场景实现

问题

官方文档

https://docs.agora.io/cn/Interactive%20Broadcast/start_live_audio_electron?platform=Electron

license用量地址

https://console.shengwang.cn/license/usage

错误

agora_node_ext.node is not a valid Win32 application

查看Electron所有版本

https://npm.taobao.org/mirrors/electron/

解决办法

先删除项目下的node_moudle

清空缓存

1
npm cache clean --force

项目根目录添加.npmrc文件

保证下载的Electron为32位版本

1
2
arch=ia32
registry=https://registry.npm.taobao.org

或者

Windows 平台请先运行 npm install -D --arch=ia32 electron 安装 32 位的 Electron,然后运行 npm install。否则会收到报错:Not a valid win32 application

如果项目根目录下已有 node_modules 文件夹,建议先删除该文件夹,再运行 npm install,以免出现报错。

package.json中添加agora_electronconfig的部分

1
2
3
4
5
6
7
8
9
10
11
12
{
"agora_electron": {
"electron_version": "10.2.0",
"platform": "win32",
"prebuilt": true,
"arch": "ia32"
},
"devDependencies": {
"electron": "10.2.0",
"agora-electron-sdk": "latest",
}
}

其中:

agora_electron :保证预编译版本和electron的版本一致

main.js中添加

1
app.allowRendererProcessReuse = false;

缺一不可

安装

1
npm install

推送摄像头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// Code that will run only after the
// entire view has been rendered
if(global.rtcEngine) {
global.rtcEngine.release()
global.rtcEngine = null
}

if(!APPID) {
alert('Please provide APPID in App.vue')
return
}

const consoleContainer = document.querySelector('#console')

let rtcEngine = new AgoraRtcEngine()
rtcEngine.initialize(APPID)

// listen to events
rtcEngine.on('joinedChannel', (channel, uid, elapsed) => {
consoleContainer.innerHTML = `join channel success ${channel} ${uid} ${elapsed}`
let localVideoContainer = document.querySelector('#local')
//setup render area for local user
rtcEngine.setupLocalVideo(localVideoContainer)
})
rtcEngine.on('error', (err, msg) => {
consoleContainer.innerHTML = `error: code ${err} - ${msg}`
})
rtcEngine.on('userJoined', (uid) => {
//setup render area for joined user
let remoteVideoContainer = document.querySelector('#remote')
rtcEngine.setupViewContentMode(uid, 1);
rtcEngine.subscribe(uid, remoteVideoContainer)
})

// set channel profile, 0: video call, 1: live broadcasting
rtcEngine.setChannelProfile(1)
rtcEngine.setClientRole(1)

// enable video, call disableVideo() is you don't need video at all
rtcEngine.enableVideo()

const logpath = path.join(os.homedir(), 'agorasdk.log')
// set where log file should be put for problem diagnostic
rtcEngine.setLogFile(logpath)

// join channel to rock!
rtcEngine.joinChannel(null, "demoChannel", null, Math.floor(new Date().getTime() / 1000))

global.rtcEngine = rtcEngine

共享窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
const AgoraRtcEngine = require('agora-electron-sdk').default

const os = require('os')
const path = require('path')
const consoleContainer = document.getElementById('agora-vs-screen-share-window-console')
const sdkLogPath = path.resolve(os.homedir(), "./agoramainsdk.log")
const localVideoContainer = document.getElementById('vs-screen-share-window-local-video')
const localScreenContainer = document.getElementById('vs-screen-share-window-local-screen')
const remoteVideoContainer = document.getElementById('vs-screen-share-window-remote-video')
const APPID = global.AGORA_APPID || ""
const channel = "demoChannel"

if(!APPID) {
alert(`AGORA_APPID not found in environment variables`)
return
}

if(global.rtcEngine) {
// if rtc engine exists already, you must call release to free it first
global.rtcEngine.release()
}

let rtcEngine = new AgoraRtcEngine()
rtcEngine.initialize(APPID)

// listen to events
rtcEngine.on('joinedChannel', (channel, uid, elapsed) => {
consoleContainer.innerHTML = `joined channel ${channel} with uid ${uid}, elapsed ${elapsed}ms`
//setup render area for local user
rtcEngine.setupLocalVideo(localVideoContainer)

//find a display
let windows = rtcEngine.getScreenWindowsInfo()

if(windows.length === 0) {
return alert('no window found')
}

//start video source
rtcEngine.videoSourceInitialize(APPID)
rtcEngine.videoSourceSetChannelProfile(1);
rtcEngine.videoSourceJoin(null, channel, "", 1)
rtcEngine.on('videosourcejoinedsuccess', () => {

// start screenshare
rtcEngine.videoSourceSetVideoProfile(43, false);
rtcEngine.videoSourceStartScreenCaptureByWindow(windows[0].windowId, {
x: 0, y: 0, width: 0, height: 0
}, {
width: 0,
height: 0,
bitrate: 0,
frameRate: 5,
captureMouseCursor: true,
windowFocus: false,
})
//setup dom where to display screenshare preview
rtcEngine.setupLocalVideoSource(localScreenContainer)
rtcEngine.startScreenCapturePreview()
})
})
rtcEngine.on('error', (err, msg) => {
consoleContainer.innerHTML = `error: code ${err} - ${msg}`
})
rtcEngine.on('userJoined', (uid) => {
//setup render area for joined user
rtcEngine.setupViewContentMode(uid, 1);
rtcEngine.subscribe(uid, remoteVideoContainer)
})

// set channel profile, 0: video call, 1: live broadcasting
rtcEngine.setChannelProfile(1)
rtcEngine.setClientRole(1)

// enable video, call disableVideo() is you don't need video at all
rtcEngine.enableVideo()
//不开启本地摄像头
rtcEngine.enableLocalVideo(false)
//不共享本地摄像头
rtcEngine.muteLocalVideoStream(true)

// set where log file should be put for problem diagnostic
rtcEngine.setLogFile(sdkLogPath)

// join channel to rock!
rtcEngine.joinChannel(null, channel, null, Math.floor(new Date().getTime() / 1000))

global.rtcEngine = rtcEngine

获取所有的窗口

1
2
3
4
5
6
7
8
9
getScreenWindowsInfo(rtcEngine) {//声网获取窗口
return new Promise((resolve, reject) => {
if (rtcEngine) {
rtcEngine.getScreenWindowsInfo(list => {
resolve(list);
});
}
})
},

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let win_arr = await this.getScreenWindowsInfo(rtcEngine);
logger.log("win_arr", win_arr);
for (const win of win_arr) {
if (win.name === "工具条") {
exclude_win_arr.push(win.windowId);
}

if (win.name === "互动消息") {
exclude_win_arr.push(win.windowId);
}

if (win.name === "互动消息-小窗口") {
exclude_win_arr.push(win.windowId);
}
}

共享屏幕

获取屏幕

1
2
3
4
5
6
7
8
9
getDisplays(rtcEngine) {//声网获取屏幕
return new Promise((resolve, reject) => {
if (rtcEngine) {
rtcEngine.getScreenDisplaysInfo(list => {
resolve(list);
});
}
})
},

单路推流

如果是不推送摄像头建议用该方法,更省CPU

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
let that = this;

const APPID = global.AGORA_APPID || "";
const mychannel = this.sectionid;
console.info("声网 APPID", APPID);
console.info("声网 token", global.AGORA_TOKEN);
console.info("声网 频道", mychannel);
console.info("声网 用户ID", this.userInfo.userid);

if (global.rtcEngine) {
global.rtcEngine.release();
}

let rtcEngine = new AgoraRtcEngine();
rtcEngine.initialize(APPID);
rtcEngine.on("joinedChannel", (channel, uid, elapsed) => {
that.record_screen_audio();
console.info(
`joined channel ${channel} with uid ${uid}, elapsed ${elapsed}ms`
);
//find a display
let displays = rtcEngine.getScreenDisplaysInfo();

if (displays.length === 0) {
return alert("no display found");
}
rtcEngine.startScreenCaptureByScreen(
displays[0].displayId,
{
x: 0,
y: 0,
width: 0,
height: 0,
},
{
width: 1280,
height: 720,
bitrate: 1000,
frameRate: 15,
captureMouseCursor: true,
windowFocus: false
});
rtcEngine.setVideoEncoderConfiguration({
bitrate:0,
width:1280,
height:720,
frameRate:24
});
});
rtcEngine.on("error", (err, msg) => {
console.info(`error: code ${err} - ${msg}`);
if (err === 109) {
//token失效重试
}
});

rtcEngine.on("userJoined", (uid) => {
console.info("用户推送流:uid:", uid);
let user2 = that.getuserByid(uid);
let username = "";
if (user2) {
username = user2.username;
}
that.shangtai_userlist.push({
uid: uid,
username: username,
video: 1,
audio: 1,
});
that.$nextTick(() => {
const remoteVideoContainer = document.getElementById(
"stu_list_div_video" + uid
);

if (remoteVideoContainer) {
await rtcEngine.setupRemoteVideo(userid, null);
await this.$nextTick();
await rtcEngine.subscribe(userid, remoteVideoContainer);
await rtcEngine.setupViewContentMode(userid, 0, this.mychannel);
}
});
});

rtcEngine.on("removeStream", (uid) => {
console.info("用户关闭流:uid:" + uid);
});

// set channel profile, 0: video call, 1: live broadcasting
rtcEngine.setChannelProfile(1);
rtcEngine.setClientRole(1);
rtcEngine.enableVideo();
rtcEngine.enableAudio()
rtcEngine.adjustRecordingSignalVolume(100);
rtcEngine.adjustAudioMixingPublishVolume(100);
// rtcEngine.enableLocalAudio(true);
rtcEngine.muteLocalAudioStream(!this.audio_open)
if (this.audio_deviceid) {
rtcEngine.setAudioRecordingDevice(this.audio_deviceid);
}
const sdkLogPath = path.join(
app.getPath("downloads"),
"agoramainsdk.log"
);

rtcEngine.setLogFile(sdkLogPath);

// join channel to rock!
rtcEngine.joinChannel(
global.AGORA_TOKEN,
mychannel,
null,
this.userInfo.userid
);
global.rtcEngine = rtcEngine;

双路推流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
{  
async get_token(ist1) {
//ist1 双实例时使用
let cloudcode = localStorage.getItem("cloudcode")
const mychannel = cloudcode + "_" + this.sectionid;
this.mychannel = mychannel;
let uid = 0;
if (ist1) {
uid = this.userInfo.userid
} else {
uid = this.userInfo.userid + 10000000;
}
let form_token = {
uid: uid,
userid: this.userInfo.userid,
channelName: mychannel,
license: uselicense ? 1 : 0,
cloudcode: cloudcode
};
try {
let res = await api_agora_token_rtc_token(form_token);
let mdata = res.data;
if (mdata.code === 0) {
let token = "";
if (mdata.obj && mdata.obj.token) {
token = mdata.obj.token;
if (uselicense) {
localStorage.setItem("cert", mdata.obj.cert);
localStorage.setItem("credential", mdata.obj.credential);
}
} else {
token = mdata.obj;
}

if (ist1) {
global.token_t1 = token;
} else {
global.token_t2 = token;
}
localStorage.setItem("token", token);
console.info("获取到的token:", token);
} else {
this.exitWithMsg("获取授权失败,请重试!");
}
} catch (e) {
this.exitWithMsg("获取授权失败,请重试!");
}
},
async begin_zhibo() {//开始直播
let msg = new WsMsg(this.sectionid, this.userInfo.userid);
msg.zhibo_mode();
console.info("直播模式:" + JSON.stringify(msg));
ipcRenderer.send("ws_sendmsg", msg);
console.info("获取授权和Token");
remote.getGlobal("sharedObject").classtype = 0;
this.token_try_num -= 1;
await this.get_token(true)
await this.get_token(false)
await this.shengwang();
},
getDisplays(rtcEngine) {//声网获取屏幕
return new Promise((resolve, reject) => {
if (rtcEngine) {
rtcEngine.getScreenDisplaysInfo(list => {
resolve(list);
});
}
})
},
getScreenWindowsInfo(rtcEngine) {//声网获取窗口
return new Promise((resolve, reject) => {
if (rtcEngine) {
rtcEngine.getScreenWindowsInfo(list => {
resolve(list);
});
}
})
},

async videoSourceJoinChannel(
token,
channelId,
userid
) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Join Channel Timeout'));
}, 5000);
const rtcEngine = this.getRtcEngine();

rtcEngine.once('videosourcejoinedsuccess', (uid) => {
clearTimeout(timer);
console.log(`videoSourceJoinChannelSuccess`);
resolve(uid);
});
rtcEngine.once('videoSourceLeaveChannel', () => {
console.log(`videoSourceLeaveChannel`);
});
try {
rtcEngine.videoSourceJoin(
token,
channelId,
"",
userid
);
} catch (err) {
clearTimeout(timer);
reject(err);
}
})
},
getRtcEngine() {
if (global.rtcEngine) {
return global.rtcEngine;
} else {
const APPID = global.AGORA_APPID || "";
let cloudcode = localStorage.getItem("cloudcode")
const mychannel = cloudcode + "_" + this.sectionid;
logger.log("声网 APPID", APPID);
logger.log("声网 token", global.token_t1);
logger.log("声网 频道", mychannel);
logger.log("声网 用户ID", this.userInfo.userid);
let temp_path = remote.getGlobal("sharedObject").temp_path;
let mypath = "";
mypath = path.join(
temp_path,
"agora"
);
try {
if (!fs.existsSync(mypath)) {
fs.mkdirSync(mypath, {recursive: true});
}
} catch (e) {
}
logger.log("mypath", mypath);
let cert = "";
if (uselicense) {
cert = localStorage.getItem("cert")
let credential = localStorage.getItem("credential")
logger.log("声网 cert", cert);
logger.log("声网 credential", credential);

let filepath = path.join(mypath, "deviceId");

if (credential) {
try {
fs.writeFileSync(filepath, credential)
} catch (err) {
logger.error(err)
}
}
}
let rtcEngine = new AgoraRtcEngine();

rtcEngine.initialize(APPID);
global.rtcEngine = rtcEngine;
if (uselicense) {
rtcEngine.verifyCertificate(cert, mypath);
}
const sdkLogPath = path.join(
temp_path,
"agora",
"live_class.log"
);
rtcEngine.setLogFile(sdkLogPath);

rtcEngine.setParameters("{\"che.video.mutigpu_exclude_window\":true}");
return rtcEngine;
}
},
async shengwang() {//调用声网方法
console.info("启动声网推流");
let push_width = 1920;
let push_height = 1080;
const APPID = global.AGORA_APPID || "";
let cloudcode = localStorage.getItem("cloudcode")
const mychannel = cloudcode + "_" + this.sectionid;
logger.log("声网 APPID", APPID);
logger.log("声网 token", global.token_t1);
logger.log("声网 频道", mychannel);
logger.log("声网 用户ID", this.userInfo.userid);
if (global.rtcEngine) {
global.rtcEngine.release();
}

let rtcEngine = this.getRtcEngine();
//直播未开始直接关闭会崩溃
if (uselicense) {
rtcEngine.on("certificateRequired", () => {
logger.log("certificateRequired")
})
rtcEngine.on("licenseRequest", () => {
logger.log("licenseRequest")
})
rtcEngine.on("licenseValidated", () => {
logger.log("licenseValidated:", "licenseValidated")
})
rtcEngine.on("licenseError", (result) => {
logger.log("licenseError", result)
this.exitWithMsg("直播授权失败,请以管理员身份运行程序!");
})
}


rtcEngine.on("error", (err, msg) => {
logger.log(`error: code ${err} - ${msg}`);
if (err === 109) {
//授权失败
this.exitWithMsg("授权失败,请重试!");
}
});

rtcEngine.on("joinedChannel", async (channel, uid, elapsed) => {
logger.log(
`joined channel ${channel} with uid ${uid}, elapsed ${elapsed}ms`
);

rtcEngine.enableLocalVideo(true);
await this.screenShare(mychannel);
});

// 用户进入课堂
rtcEngine.on("userJoined", async (userid) => {
if (this.usercamera_big) {
win.unmaximize()
this.usercamera_big = false;
}
await this.$nextTick();
const remoteVideoContainer = document.getElementById(
"stu_list_div_video" + userid
);
if (remoteVideoContainer) {
await rtcEngine.setupRemoteVideo(userid, null);
await this.$nextTick();
await rtcEngine.subscribe(userid, remoteVideoContainer);
await rtcEngine.setupViewContentMode(userid, 0, this.mychannel);
}
});

rtcEngine.on("removeStream", (uid) => {
logger.log("用户关闭流:uid:" + uid);
});

// set channel profile, 0: video call, 1: live broadcasting
rtcEngine.setChannelProfile(1);
rtcEngine.setClientRole(1);
rtcEngine.setVideoEncoderConfiguration({
bitrate: 0,
width: push_width,
height: push_height,
frameRate: 15,
});
rtcEngine.enableVideo();
rtcEngine.enableLocalVideo(true);
// rtcEngine.setParameters("{\"che.hardware_encoding\":1}");
rtcEngine.enableAudio()
rtcEngine.adjustRecordingSignalVolume(400);
rtcEngine.adjustAudioMixingPublishVolume(400);

rtcEngine.adjustPlaybackSignalVolume(200);
// rtcEngine.enableLocalAudio(true);
rtcEngine.enableLoopbackRecording(true, null);
let audio_open = remote.getGlobal("sharedObject").audio_open;
rtcEngine.muteLocalAudioStream(!audio_open);
console.info("是否开启音频:" + audio_open);
if (this.audio_deviceid) {
console.info("设置的麦克风ID:", this.audio_deviceid);
rtcEngine.setAudioRecordingDevice(this.audio_deviceid);
}

console.info("this.userInfo.userid", this.userInfo.userid);
let user2 = this.userInfo.userid + 10000000;
console.info("user2", user2);
rtcEngine.joinChannel(
global.token_t2,
mychannel,
null,
user2
);
},
async screenShare(mychannel) {
let push_width = 1920;
let push_height = 1080;
const APPID = global.AGORA_APPID || "";
let rtcEngine = this.getRtcEngine();
rtcEngine.videoSourceInitialize(APPID);
// rtcEngine.videoSourceSetLogFile(config.nativeSDKVideoSourceLogPath);
// rtcEngine.videoSourceSetAddonLogFile(config.videoSourceAddonLogPath);
// rtcEngine.videoSourceEnableAudio();
console.info("global.token_t1", global.token_t1);
await this.videoSourceJoinChannel(global.token_t1, mychannel, this.userInfo.userid)

//find a display
let displays = await this.getDisplays(rtcEngine);
// let displays = rtcEngine.getRealScreenDisplaysInfo();
console.info("displays", displays);

// let displays1 = rtcEngine.getRealScreenDisplaysInfo();
// console.info("displays1", displays1);

if (displays && displays.length === 0) {
return alert("no display found");
}

let displayId = displays[0].displayId;
for (let i = 0; i < displays.length; i++) {
let item = displays[i];
if (item.isMain) {
displayId = item.displayId;
}
}
console.info("displayId", displayId);

let exclude_win_arr = remote.getGlobal('sharedObject').exclude_win_arr;

let win_arr = await this.getScreenWindowsInfo(rtcEngine);
logger.log("win_arr", win_arr);
for (const win of win_arr) {
if (win.name === "工具条") {
exclude_win_arr.push(win.windowId);
}

if (win.name === "互动消息") {
exclude_win_arr.push(win.windowId);
}

if (win.name === "互动消息-小窗口") {
exclude_win_arr.push(win.windowId);
}
}

logger.log("exclude_win_arr", exclude_win_arr);
let result = rtcEngine.videoSourceStartScreenCaptureByDisplayId(
displayId,
{
x: 0,
y: 0,
width: 0,
height: 0,
},
{
width: push_width,
height: push_height,
bitrate: 0,
frameRate: 15,
captureMouseCursor: true,
windowFocus: false,
excludeWindowList: exclude_win_arr,
excludeWindowCount: exclude_win_arr.length
});
if (result === 0) {
console.info("开始录制");
this.start_record_screen_audio();
console.info("开始定时检查录制状态");
this.check_record_status();
} else {
this.exitWithMsg();
}
},
}

注意

rtcEngine.enableLocalVideo(false);rtcEngine.muteLocalVideoStream(true);不能同时调用会导致网页上闪屏

二次渲染报错

1
2
3
4
5
6
7
8
9
const remoteVideoContainer = document.getElementById(
"stu_list_div_video" + userid
);
if (remoteVideoContainer) {
await rtcEngine.setupRemoteVideo(userid, null);
await this.$nextTick();
await rtcEngine.subscribe(userid, remoteVideoContainer);
await rtcEngine.setupViewContentMode(userid, 0, mychannel);
}

主要是调用

1
await rtcEngine.setupRemoteVideo(userid, null);

setupViewContentMode

setupViewContentMode要在subscribe之后调用

1
setupViewContentMode(uid, mode, channelId)

设置视窗内容显示模式。

Parameters

  • uid: number | “local” | “videosource”

    用户 ID,表示设置的是哪个用户的流。设置远端用户的流时,请确保你已先调用 subscribe 方法订阅该远端用户流。

  • mode: 0 | 1

    视窗内容显示模式:

    • 0:优先保证视窗被填满。视频尺寸等比缩放,直至整个视窗被视频填满。如果视频长宽与显示窗口不同,多出的视频将被截掉。
    • 1: 优先保证视频内容全部显示。视频尺寸等比缩放,直至视频窗口的一边与视窗边框对齐。如果视频长宽与显示窗口不同,视窗上未被填满的区域将被涂黑。
  • channelId: string | undefined

设置音频设备

获取所有的音频设备

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#getaudiorecordingdevices

获取麦克风设备

1
2
3
4
let rtcEngine = new AgoraRtcEngine();
rtcEngine.initialize(APPID);
let audioDevices = rtcEngine.getAudioRecordingDevices();
console.info("audioDevices", audioDevices);

设置音频设备

1
rtcEngine.setAudioRecordingDevice(audio_deviceId);

注意

获取到的设备ID和HTML5获取到的设备ID的值是不一样的

H5获取的

1
2
3
4
5
6
{
deviceId: "9785d8e59125fe95568772bc8e04308b0f5a33a49f5baadd28cc22f337d995db",
groupId: "f5465fe43e22924d51430be263f4a5efbdc627a4fe42034ef6693e7c743a3fc9",
kind: "audioinput",
label: "Internal Microphone (Cirrus Logic CS8409 (AB 51))"
}

声网获取的

1
2
3
4
{
deviceid: "{0.0.1.00000000}.{8d2744e3-fe09-49ec-b9ae-3eb7693a091a}",
devicename: "Internal Microphone (Cirrus Logic CS8409 (AB 51))(default)"
}

麦克风设备测试

音频设备测试

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#startaudiorecordingdevicetest

设置音频设备

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#setaudiorecordingdevice

获取当前的音频设备

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#getcurrentaudiorecordingdevice

获取麦克风的最大音量

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#getaudiorecordingvolume

获取音频设备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
getAllAudio() {
const APPID = global.AGORA_APPID || "";
if (global.rtcEngine) {
global.rtcEngine.release();
}
let rtcEngine = new AgoraRtcEngine();
global.rtcEngine = rtcEngine;
rtcEngine.initialize(APPID);
let audioDevices = rtcEngine.getAudioRecordingDevices();
this.audio_devices = audioDevices;
this.result.audio.deviceid = audioDevices[0].deviceId;
console.info("audioDevices", audioDevices);
this.showAudioTest();
},

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
showAudioTest() {
let audio_deviceId = this.result.audio.deviceid;
let rtcEngine = global.rtcEngine;
let canvas = this.$refs["audio_canvas"];
let width = canvas.width,
height = canvas.height,
g = canvas.getContext("2d");

let num = 18;
let space = 8;
let barwidth = (width - (num - 1) * space) / num;
rtcEngine.setAudioRecordingDevice(audio_deviceId);
rtcEngine.startAudioRecordingDeviceTest(100);
rtcEngine.on("groupAudioVolumeIndication", function (speakers, speakerNumber, totalVolume) {
g.clearRect(0, 0, width, height);
let shounum = parseInt(totalVolume * num / 255 + "");
for (let i = 0; i < num; i++) {
let x = space * i + i * barwidth;
let colorstr = "#DDDDDD";
if (i < shounum) {
colorstr = "#960200"
}
fillRoundRect(g, x, 0, barwidth, height, barwidth / 2, colorstr);
}
})
},

退出前结束测试

1
2
let rtcEngine = global.rtcEngine;
rtcEngine.stopAudioRecordingDeviceTest()

其他测试

设备通话回路测试

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#startechotestwithinterval

网络质量测试

https://docs.agora.io/cn/Interactive%20Broadcast/API%20Reference/electron/classes/agorartcengine.html#startlastmileprobetest

监听声音

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rtcEngine.enableAudioVolumeIndication(200, 3, false);

rtcEngine.on("groupAudioVolumeIndication", (speakers, speakerNumber, totalVolume) => {
for (let i = 0; i < speakers.length; i++) {
let speaker = speakers[i];
for (let j = 0; j < this.shangtai_userlist.length; j++) {
let user = this.shangtai_userlist[j];
if (speaker.uid === user.userid) {
user.volume = speaker.volume;
}
}

if (speaker.uid === 0) {
this.teacherVolume = speaker.volume;
}
}
});

uid为0的是自己

volume值范围(0-255)

服务端录制

开始录制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"cname":"{{AccessChannel}}",
"uid":"{{RecordingUID}}",
"clientRequest":{
"token":"{{token}}",
"recordingConfig":{
"maxIdleTime":120,
"streamTypes":2,
"audioProfile":1,
"channelType":1,
"videoStreamType":0,
"transcodingConfig":{
"width":360,
"height":640,
"fps":30,
"bitrate":600,
"mixedVideoLayout":1,
"maxResolutionUid":"1"
}
},
"snapshotConfig": {
"captureInterval": 5,
"fileType": [
"jpg"
]
},
"recordingFileConfig": {
"avFileType": [
"hls",
"mp4"
]
},
"storageConfig":{
"vendor":"{{Vendor}}",
"region":"{{Region}}",
"bucket":"{{Bucket}}",
"accessKey":"{{AccessKey}}",
"secretKey":"{{SecretKey}}"
}
}
}

OSS获取封面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 填写视频文件的完整路径。
String objectName = dto.getVideoUrl();
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 使用精确时间模式截取视频1s处的内容,输出为JPG格式的图片,宽度为800,高度为600。
//如果要第一帧 应该用t_1 第一毫秒就可以 但很多视频前面几帧是纯黑的 所以我用第一秒
String style = "video/snapshot,t_1000,f_jpg,w_800,h_600";
// 指定过期时间为一年
Date expiration = new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365 );
GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest(bucketName, objectName, HttpMethod.GET);
req.setExpiration(expiration);
req.setProcess(style);
URL signedUrl = ossClient.generatePresignedUrl(req);
//这里注意,最终的结果是拼接在原视频地址后面的,拼接方式是?x-oss-process=
//这一部分在query当中,我根据等号切割拿到最后一部分再在前面手动拼接上形成最终图片url
String query = signedUrl.getQuery();
String[] split = query.split("=");
shareDetailDO.setImgUrl(dto.getVideoUrl()+"?x-oss-process="+split[split.length-1]);
// 关闭OSSClient。
ossClient.shutdown();

阿里云的说明地址:https://help.aliyun.com/document_detail/64555.html?spm=a2c4g.11174283.6.1472.55457da2vrSZqX

使用fast模式截取视频3s处的内容,输出为JPG格式的图片,宽度为1280,高度为800。

处理后的URL为:

1
https://xhlivetest.oss-cn-hangzhou.aliyuncs.com/0ba655c2f84cd780626e13838c965168_AAAAAA_10706_0.mp4?x-oss-process=video/snapshot,t_3000,f_jpg,w_1280,h_800,m_fast

获取时长

引入阿里云osspom

1
2
3
4
5
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>

阿里云工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
/**
* @author ful
*/
@Component
@Slf4j
public class AliOSSUtil {

// endpoint 访问OSS的域名
@Value("${oss.agora.endpoint}")
public String endpoint;
// accessKeyId和accessKeySecret OSS的访问密钥
@Value("${oss.agora.id}")
public String accessKeyId;
@Value("${oss.agora.secret}")
public String accessKeySecret;
// Bucket 用来管理所存储Object的存储空间
@Value("${oss.agora.bucket}")
public String bucketName;
@Value("${oss.agora.region}")
public String region;
@Value("${oss.agora.vendor}")
public String vendor;
@Value("${oss.agora.cdn}")
public String cdn;

/**
* 文件直传
*
* @param objectKey 上传路径
* @param inputStream 上传流
* @throws RuntimeException
*/
public void fileUpload(String objectKey, InputStream inputStream) throws RuntimeException {
Map map = getCommon(objectKey);
OSS ossClient = null;
try {
ossClient = (OSS) map.get(0);
AliOssPublicEntity model = (AliOssPublicEntity) map.get(1);
if (ossClient.doesObjectExist(model.getBucketName(), model.getObjectKey())) {
log.error("此文件重名,请更改文件名重试!");
throw new RuntimeException("此文件重名,请更改文件名重试!");
}
PutObjectRequest putObjectRequest = new PutObjectRequest(model.getBucketName(), model.getObjectKey(), inputStream);
PutObjectResult putObjectResult = ossClient.putObject(putObjectRequest);
String eTag = putObjectResult.getETag();
if (StringUtils.isBlank(eTag)) {
log.error("文件直传失败!");
throw new RuntimeException("文件直传失败");
}
} catch (Exception e) {
log.error("文件直传失败,exp={}", e);
throw new RuntimeException("文件直传失败:" + e.getMessage());
} finally {
ossClient.shutdown();
try {
inputStream.close();
} catch (IOException e) {
log.error("关闭文件流异常={}", e);
}
}
}

/**
* OSS获取下载签名URL
*
* @param objectKey 文件对象key
* @return 签名URL
*/
public String getOssObjectDownAuthUrl(String objectKey) throws RuntimeException {
Map map = getCommon(objectKey);
OSS ossClient = null;
try {
ossClient = (OSS) map.get(0);
AliOssPublicEntity model = (AliOssPublicEntity) map.get(1);
GeneratePresignedUrlRequest req =
new GeneratePresignedUrlRequest(model.getBucketName(), model.getObjectKey(), HttpMethod.GET);
//这里设置签名在30小时后过期
Date expiration = new Date(new Date().getTime() + 30L * 60L * 60L * 1000);// 生成URL
// Date expireDate = new Date(System.currentTimeMillis() + 30L * 60L * 60L * 1000L);
req.setExpiration(expiration);
URL url = ossClient.generatePresignedUrl(req);
String urlStr = url.toString();
return urlStr;
} catch (Exception e) {
log.error("getOssObjectDownAuthUrl 获取下载签名URL失败,exp={}", e);
throw new RuntimeException("获取下载签名URL失败");
} finally {
ossClient.shutdown();
}
}

/**
* OSS获取下载签名URL
*
* @param objectKey 文件对象key
* @param expireTime 当前时间加多少毫秒后过期,过期时间(毫秒)
* @return 签名URL
*/
public String getOssObjectDownAuthUrl(String objectKey, long expireTime) throws RuntimeException {
Map map = getCommon(objectKey);
OSS ossClient = null;
try {
ossClient = (OSS) map.get(0);
AliOssPublicEntity model = (AliOssPublicEntity) map.get(1);
GeneratePresignedUrlRequest req =
new GeneratePresignedUrlRequest(model.getBucketName(), model.getObjectKey(), HttpMethod.GET);
//这里设置签名在半个小时后过期
Date expireDate = new Date(System.currentTimeMillis() + expireTime);
req.setExpiration(expireDate);
URL url = ossClient.generatePresignedUrl(req);
String urlStr = url.toString();
return urlStr;
} catch (Exception e) {
log.error("getOssObjectDownAuthUrl long获取下载签名URL失败,exp={}", e);
throw new RuntimeException("获取下载签名URL失败");
} finally {
ossClient.shutdown();
}
}

/**
* OSS获取上传签名URL
*
* @param objectKey 文件对象key
* @return 签名URL
*/
public String getOssObjectUploadAuthUrl(String objectKey) throws RuntimeException {
Map map = getCommon(objectKey);
OSS ossClient = null;
try {
ossClient = (OSS) map.get(0);
AliOssPublicEntity model = (AliOssPublicEntity) map.get(1);
if (ossClient.doesObjectExist(model.getBucketName(), model.getObjectKey())) {
throw new RuntimeException("此文件重名,请更改文件名重试!");
}
Date expirationTime = new Date(System.currentTimeMillis() + 30L * 60L * 1000L);
GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(model.getBucketName(), model.getObjectKey(), HttpMethod.PUT);
request.setExpiration(expirationTime);
//必须要!!!!!!,而且前端上传时,也需要在header里面设置,content-type为"application/octet-stream"
request.setContentType("application/octet-stream");
URL url = ossClient.generatePresignedUrl(request);
String urlstr = url.toString();
return urlstr;
} catch (Exception e) {
log.error("getOssObjectDownAuthUrl long获取上传签名URL失败,exp={}", e);
throw new RuntimeException("获取上传签名URL失败" + e.getMessage());
} finally {
ossClient.shutdown();
}
}

/**
* 删除存储对象
*
* @param objectKey 文件对象key
* @return 签名URL
*/
public void deleteObject(String objectKey) throws RuntimeException {
Map map = getCommon(objectKey);
OSS ossClient = null;
try {
ossClient = (OSS) map.get(0);
AliOssPublicEntity model = (AliOssPublicEntity) map.get(1);
// 指定对象所在的存储桶
ossClient.deleteObject(model.getBucketName(), model.getObjectKey());
} catch (RuntimeException clientException) {
log.error("deleteObject 删除存储对象失败,exp={}", clientException);
throw new RuntimeException("删除存储对象失败");
} finally {
ossClient.shutdown();
}
}

/**
* 绝对路径更换为相对路径
*
* @param url 绝对路径
* @return 相对路径
*/
public String getRelativePath(String url) {
url = url.substring(url.indexOf(".com") + 5, url.indexOf("?"));
return url;
}

/**
* client公共参数
*
* @param objectKey
* @return
*/
private Map getCommon(String objectKey) {
AliOssPublicEntity entity = AliOssPublicEntity.build(objectKey, endpoint, accessKeyId, accessKeySecret, bucketName);
OSS ossClient = new OSSClientBuilder().build(entity.getEndpoint(), entity.getAccessKeyId(), entity.getAccessKeySecret());
Map map = new HashMap();
map.put(0, ossClient);
map.put(1, entity);
return map;
}
}

阿里云配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Data
public class AliOssPublicEntity {

private String endpoint;

private String accessKeyId;

private String accessKeySecret;

private String bucketName;

private String objectKey;

public static AliOssPublicEntity build(String objectKey,String endpoint,String accessKeyId,
String accessKeySecret,String bucketName) {
AliOssPublicEntity entity = new AliOssPublicEntity();
entity.setEndpoint(endpoint);
entity.setAccessKeyId(accessKeyId);
entity.setAccessKeySecret(accessKeySecret);
entity.setBucketName(bucketName);
entity.setObjectKey(objectKey);
return entity;
}
}

阿里云根据资源路径获取资源时长代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public int getVideoDuration(String videoUrl) {
if (StringUtils.isNotEmpty(videoUrl) && "m3u8".equals(videoUrl.substring(videoUrl.length() - 4))) {
try {
videoUrl = aliOSSUtil.getOssObjectDownAuthUrl(videoUrl);
log.info("getVideoDuration:aliOSSUtil.videoUrl ={}", videoUrl);
HttpRequest httpRequest = HttpRequest.get(videoUrl)
.timeout(30000);
log.info("getRequest httpRequest:{}", httpRequest);
HttpResponse res = httpRequest.execute();
String result = res.body();
String pattern = "\\d+[.]\\d+";
List<String> matchStrs = new ArrayList<>();
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(result);
while (m.find()) { //此处find()每次被调用后,会偏移到下一个匹配
matchStrs.add(m.group());//获取当前匹配的值
}
Double durationDouble = 0.0;
for (int i = 0; i < matchStrs.size(); i++) {
durationDouble += Double.parseDouble(matchStrs.get(i));
}
log.info("LiveDetailController.getVideoDuration->duration=", durationDouble.intValue());
return durationDouble.intValue();
} catch (Exception e) {
log.error("getVideoDuration 异常={}", e);
}
}
return 0;
}

这里需要注意:
HttpRequest httpRequest = HttpRequest.get(videoUrl).timeout(30000);

请求是一定不要加 .header("Content-Type", "application/json") 头,不然会验证签名失败