技术研究之声网直播

标签: 无 分类: 未分类 创建时间:2023-06-29 02:17:06 更新时间:2024-11-15 10:49:44

前言

  • 极速直播
  • 互动直播
参考文章:
1. 有了WebRTC,直播可以这样玩!
2.推流、拉流与播流 本文主要介绍视频直播的推流、拉流和播流,及其实现步骤,提供了 阿里云 播放器,实现直播内容的播放。

1.直播逻辑

经过我长时间的查阅官方文档,我发现了在每一个实例包括大疆的直播中,都包含了APPID、TOken和Channel三部分,但是我并没有发现有单独的创建Channel隧道的地方。后来我想,是不是说,这个Channel是随建随取的呢?也就是说,只要是直播端和观察端,输入了相同的Channel隧道,那就算是建立了一个Channel呢?

教程 Tutorial:快速跑通声网 RTC Web Demo 这篇文章中,提到 “Channel:字符串,要音视频双方保证输入的字符串相同 ” 这样也证实了我的说法,下面就是需要实践了,我记得我无意中获取到了一个在线的例子,可是后来还是不知道放到哪里去了,后来还是找到了:基于声网 Web SDK 实现视频通话场景 这里也有一个在线的实例,输入两个相同的Channel名字,就可以实现实时直播。

实现直播的基本逻辑如下:

  • 1.调用 createClient 方法创建 AgoraRTCClient 对象。

  • 2.调用 setClientRole 方法,将角色 role 设为 “audience”(观众), 将延时等级 level 设为 1(低延时)。

  • 3.调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。

  • 4.当一个远端用户加入频道并发布音视频轨道时:

监听 client.on(“user-published”) 事件。当 SDK 触发该事件时,在这个事件回调函数的参数中你可以获取远端用户 AgoraRTCRemoteUser 对象 。

调用 subscribe 方法订阅远端用户 AgoraRTCRemoteUser 对象,获取远端用户的远端音频轨RemoteAudioTrack 和远端视频轨道 RemoteVideoTrack 对象。

调用 play 方法播放远端音视频轨道。

参考文章:
1.基于 Agora SDK 实现 Web 端的多人视频互动
2.旁路推流 RESTful API
3.声网 CDN 旁路推流 API 经验谈
4.融合 CDN 直播
5.实现视频直播 (Web SDK 4.x) 调用 setClientRole 方法,将角色 role 设为 “audience”(观众), 将延时等级 level 设为 1(低延时)。调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。
6.频道基础 对于 Stream Channel,虽然本质上也是随用随取的,但在使用前你需要明确的调用 createStreamChannel 方法创建一个 RTMStreamChannel 对象实例,然后才能调用 join 方法加入频道。对于 Message Channel,在使用前,你无需提前定义或创建频道。在你首次向 Message Channel 发送消息或首次订阅 Message Channel 时,RTM 会为你自动创建该频道。

2.Token鉴权

Token申请总共分为 9 个步骤:

1.客户端根据需要,向 app 服务端申请 Token

2.App 服务端生成并返回 Token

3.客户端以 UID、频道名以及获取到的 Token 加入频道

4.声网平台读取该 Token 中包含的信息,并进行校验。调用 joinChannel 方法,使用 Token、用户 ID 和频道名加入频道。用户 ID 和频道名必须和用于生成 Token 的用户 ID 和频道名一致。

5.客户端收到加入频道成功回调,并获取用户 UID

6.Token 最大有效期为 24 小时。当即将过期时,客户端会收到 Token 即将过期的回调

7.此时,如果客户端需要继续进行音视频互动,需要申请新的 Token

8.App 服务端生成并返回 Token

9.客户端更新 Token

问题

(1) AgoraRTCError CAN_NOT_GET_GATEWAY_SERVER: flag: 4096, message: AgoraRTCError CAN_NOT_GET_GATEWAY_SERVER: dynamic key or token timeout

这个多半是token的问题,token过期了会提示这个错误。

3.Java生成token

到官方给的代码仓库中,找到java部分代码,然后把 main/java/io/agora/media 文件夹拷贝到自己的项目中。然后编写下面的代码,通过 RtcTokenBuilder2 生成token。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RequestMapping("/token")
public HttpResultResponse token(@RequestParam(value = "uid")Integer uid,
@RequestParam(value = "channelName")String channelName,
@RequestParam(value = "role",defaultValue = "2")Integer role){
String result="";
Object agoraToken = RedisOpsUtils.get("agoraToken");
if(agoraToken!=null){
String agoraTokenStr=String.valueOf(agoraToken);
result=agoraTokenStr;
}else {
RtcTokenBuilder2 tokenBuilder2=new RtcTokenBuilder2();
// 设置角色
Role roleBuild=Role.ROLE_PUBLISHER;
if(role==2){
roleBuild=Role.ROLE_SUBSCRIBER;
}
// 生成token
result =tokenBuilder2.buildTokenWithUid(appId, appCertificate, channelName, uid, roleBuild
, tokenExpirationInSeconds, privilegeExpirationInSeconds);
RedisOpsUtils.setWithExpire("agoraToken",result,tokenExpirationInSeconds);
}

return HttpResultResponse.success(result);
}
参考文章:
1.RtcTokenBuilder2Sample 官网生成的token示例。
2.使用Java构建Agora令牌服务器

4.Web集成

(1)安装sdk

1
pnpm add agora-rtc-sdk-ng

(2)初始化直播能力,监听直播频道

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
let agoraPara={
appid:"xxx",
channel:"xxx",
token:"xxx"
}
let agoraClient = AgoraRTC.createClient({ mode: 'live', codec: 'vp8' })
agoraClient.setClientRole('audience', { level: 2 })
console.log(agoraClient.connectionState)
if (agoraClient.connectionState === 'DISCONNECTED') {
agoraClient.join(agoraPara.appid, agoraPara.channel, agoraPara.token)
}
// Subscribe when a remote user publishes a stream
agoraClient.on('user-joined', async (user) => {
console.log('user[' + user.uid + '] join')
})
agoraClient.on('user-published', async (user, mediaType) => {
console.log("订阅")
await agoraClient.subscribe(user, mediaType)
if (mediaType === 'video') {
console.log('subscribe success')
// Get `RemoteVideoTrack` in the `user` object.
const remoteVideoTrack = user.videoTrack
// Dynamically create a container in the form of a DIV element for playing the remote video track.
remoteVideoTrack.play(document.getElementById('drone-1'))
}
})
agoraClient.on('user-unpublished', async (user) => {
console.log('unpublish live:', user)
})
agoraClient.on('exception', async (e) => {
console.log(e)
})

(3) 开启
这个地方其实使用直播发布就可以搞定了,因为测试的时候使用了大疆无人机的直播,这个暂时还没有时间进行摘录。

(4) 退出直播

1
agoraClinet.leave()
参考文章:
【1】. 用声网 Cloud Player 在直播推流应用中推流视频
【2】.AgoraIO / API-Examples-Web
【3】.AgoraRTCClient 这是 web sdk 的api说明,可以这里查询相关的信息。
【4】.Interface IAgoraRTCClient An interface providing the local client with basic functions for a voice or video call, such as joining a channel, publishing tracks, or subscribing to tracks.
【5】.setClientRole 设置声网的角色,这里的角色说明是 macos 的,但是和其他客户端的概念是相同的。

5.视频截屏

云端截图不是截了一张图,而是截了很多张图,只有调用截图之后,然后直接关闭截图,这样才能停止截图。云端录制生成的截图文件的命名规则为 __uid_s___uid_e_video.jpg
(1)启动云端录制
在项目管理,配置里面,打开云端录制功能。

(2)配置Restful API
在用户管理里面,找到 Restful API,然后创建一个新的 restappid 和restappscret

(3)请求资源id
通过

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
/**
* 获取资源id
*/
private String getResourceId(Integer uid,String cname){
String resourceId="";
/**
* 请求参数
*/
JSONObject bodyJSON=new JSONObject();
bodyJSON.put("cname",cname); // 问题来源
bodyJSON.put("uid",String.valueOf(uid)); // 问题类型

JSONObject clientRequest=new JSONObject();
clientRequest.put("resourceExpiredHour",24); // 云端录制 RESTful API 的调用时效。从成功开启云端录制并获得 sid (录制 ID)后开始计算。单位为小时。
clientRequest.put("scene",0); // 推送对象
bodyJSON.put("clientRequest",clientRequest);

// 打印请求信息
log.info("resourceid send:"+bodyJSON.toJSONString());

// 拼接url
String functionUrl="https://api.sd-rtn.com/v1/apps/"+appId+"/cloud_recording/acquire";
log.info(functionUrl); // 输出测试url

HttpRequest httpRequest= HttpRequest.post(functionUrl);
String Authorization=getResultApiAuthorization();
HttpResponse response= httpRequest
.header("Authorization",Authorization)
.header("Content-Type","application/json")
.body(bodyJSON.toJSONString()) // 请求体内容
.execute();

Integer resultStatus=response.getStatus();
String resultBody=response.body();
// 打印输出结果
log.info("resourceid result:"+resultBody);
if(resultStatus==200){
JSONObject resultObj=JSONObject.parseObject(resultBody);
resourceId=resultObj.getString("resourceId");
}
return resourceId;
}

(4)调用接口进行截图

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
/**
* 开始截图
* @param uid 云端录制服务在 RTC 频道内使用的 UID
* @param cname 待录制的频道名
* @return
*/
@RequestMapping("/screenshot/start")
private HttpResultResponse screenshotStart(@RequestParam(value = "uid")Integer uid,
@RequestParam(value = "cname")String cname){

/**
* 请求参数
*/
JSONObject bodyJSON=new JSONObject();
bodyJSON.put("cname",cname); // 问题来源
bodyJSON.put("uid",String.valueOf(uid)); // 问题类型

JSONObject clientRequest=new JSONObject();
String token=getToken(uid,cname,2);
clientRequest.put("token",token); // 云端录制 频道的Token


/**
* 用于设置媒体流的订阅、转码、输出音视频的属性。
*/
JSONObject recordingConfig=new JSONObject();
recordingConfig.put("channelType",1);
recordingConfig.put("subscribeUidGroup",0);
clientRequest.put("recordingConfig",recordingConfig);

JSONObject transcodeOptions=new JSONObject();
JSONObject transConfig=new JSONObject();
transConfig.put("transMode","audioMix");
transcodeOptions.put("transConfig",transConfig);

/**
* 设置截图周期和截图文件格式
*/
JSONObject snapshotConfig=new JSONObject();
snapshotConfig.put("captureInterval",5);
JSONArray fileType=new JSONArray();
fileType.add("jpg");
snapshotConfig.put("fileType",fileType);
clientRequest.put("snapshotConfig",snapshotConfig);

/**
* 设置三方云存储
*/
JSONObject storageConfig=new JSONObject();
storageConfig.put("vendor",2); // 阿里云存储
storageConfig.put("region",0); // 存储区域
storageConfig.put("bucket",bucket);
storageConfig.put("accessKey",accessKey);
storageConfig.put("secretKey",secretKey);
JSONArray fileNamePrefix=new JSONArray();
fileNamePrefix.add("snapshot");
storageConfig.put("fileNamePrefix",fileNamePrefix);
clientRequest.put("storageConfig",storageConfig);

// 参数加入请求内容中
bodyJSON.put("clientRequest",clientRequest);

// 打印请求信息
String bodyStr=bodyJSON.toJSONString();
log.info("resourceid send:"+bodyStr);

// 拼接url
String resourceid=getResourceId(uid,cname);
String functionUrl="https://api.sd-rtn.com/v1/apps/"+appId+"/cloud_recording/resourceid/"+resourceid+"/mode" +
"/individual/start";
log.info(functionUrl); // 输出测试url

HttpRequest httpRequest= HttpRequest.post(functionUrl);
String Authorization=getResultApiAuthorization();
HttpResponse response= httpRequest
.header("Authorization",Authorization)
.header("Content-Type","application/json;charset=utf-8 ")
.body(bodyStr) // 请求体内容
.execute();

Integer resultStatus=response.getStatus();
String resultStr=response.body();
// 打印输出围栏创建结果
log.info("resourceid result:"+resultStr);
if(resultStatus==200){
JSONObject resultObj=JSONObject.parseObject(resultStr);
String result_cname=resultObj.getString("cname"); // 频道录制名称
String result_uid=resultObj.getString("uid"); // 字符串内容为云端录制服务在 RTC 频道内使用的 UID,用于标识该录制服务。
String result_resourceId=resultObj.getString("resourceId"); // 云端录制使用的 Resource ID。
String result_sid=resultObj.getString("sid");
// 保存录制资源
Object screenshot=RedisOpsUtils.get("screenshot");
JSONObject screenObj=new JSONObject();
if(screenshot!=null){
screenObj=(JSONObject)screenshot;
}
screenObj.put(result_sid,resultObj);
RedisOpsUtils.setWithExpire("screenshot",screenObj,20*60);

// 调用成功之后,直接停止截图
return screenshotStop(result_uid,result_cname,result_resourceId,result_sid);
}
return HttpResultResponse.error();
}

(5)停止截图

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
/**
* 屏幕录制
* @return
*/
@RequestMapping("/screenshot/stop")
private HttpResultResponse screenshotStop( @RequestParam(value = "uid")String uid,
@RequestParam(value = "cname")String cname,
@RequestParam(value = "resourceId")String resourceid,
@RequestParam(value = "sid")String sid){

/**
* 请求参数
*/
JSONObject bodyJSON=new JSONObject();
bodyJSON.put("cname",cname); // 问题来源
bodyJSON.put("uid",uid); // 问题类型

JSONObject clientRequest=new JSONObject();
clientRequest.put("async_stop",false);

// 参数加入请求内容中
bodyJSON.put("clientRequest",clientRequest);

// 打印请求信息
log.info("screenshotStart send:"+bodyJSON.toJSONString());

// 拼接url
String functionUrl="https://api.sd-rtn.com/v1/apps/"+appId+"/cloud_recording/resourceid/"+resourceid+"/sid/"
+sid+"/mode" + "/individual/stop";
log.info(functionUrl); // 输出测试url

HttpRequest httpRequest= HttpRequest.post(functionUrl);
String Authorization=getResultApiAuthorization();
HttpResponse response= httpRequest
.header("Authorization",Authorization)
.header("Content-Type","application/json;charset=utf-8 ")
.body(bodyJSON.toJSONString()) // 请求体内容
.execute();

Integer resultStatus=response.getStatus();
String resultStr=response.body();
log.info("resourceid result:"+resultStr);
if(resultStatus==200){
// JSONObject resultObj=JSONObject.parseObject(resultStr);
// String result_sid=resultObj.getString("sid");
// // 保存结果
// Object screenshot=RedisOpsUtils.get("screenshot");
// JSONObject screenObj=new JSONObject();
// if(screenshot!=null){
// screenObj=(JSONObject)screenshot;
// }
// screenObj.put(result_sid,resultObj);
// RedisOpsUtils.setWithExpire("screenshot",screenObj,20*60);
HttpResultResponse.success("停止截图");
}

return HttpResultResponse.error();
}
参考文章:
1.视频截图 这是官方文档,这里面有云端截图的文件说明,还有一些注意事项。
2.JAVA 对接 声网(agora) 屏幕截图功能实现直播监控 除了截图还有停止截图功能。
3.开通服务
4.截图管理 这是早期的开发文档
5.开始云端录制 这个是新的文档的位置,还说了一些必须传入的参数,还有返回值的内容。

问题

(1) response detail error:2,errMsg:post method api body check failed!
【解决方法】
尝试查看那个uid是不是字符串类型的

参考文章:
1.response detail error:2,errMsg:post method api body check failed! Agora REST API 这里说的是 Authorization key 的问题。
2.Common errors Invalid parameter. Possible reasons:可能的原因。

(2) INVALID_OPERATION
非法操作,通常是因为在当前状态不能进行该操作。确认操作的先后顺序,比如发布前请确认已经加入频道。

参考文章:
【1】.Agora Web API Reference
小额赞助
本人提供免费与付费咨询服务,感谢您的支持!赞助请发邮件通知,方便公布您的善意!
**光 3.01 元
Sun 3.00 元
bibichuan 3.00 元
微信公众号
广告位
诚心邀请广大金主爸爸洽谈合作
每日一省
isNaN 和 Number.isNaN 函数的区别?

1.函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。

2.函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

每日二省
为什么0.1+0.2 ! == 0.3,如何让其相等?

一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON属性,而它的值就是2-52,只要判断0.1+0.2-0.3是否小于Number.EPSILON,如果小于,就可以判断为0.1+0.2 ===0.3。

每日三省
== 操作符的强制类型转换规则?

1.首先会判断两者类型是否**相同,**相同的话就比较两者的大小。

2.类型不相同的话,就会进行类型转换。

3.会先判断是否在对比 null 和 undefined,是的话就会返回 true。

4.判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number。

5.判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断。

6.判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断。

每日英语
Happiness is time precipitation, smile is the lonely sad.
幸福是年华的沉淀,微笑是寂寞的悲伤。