大家好,我是谷動(dòng)谷力的大樹。
今天我們來(lái)探討一下獲取火柴人(骨架數(shù)據(jù))的實(shí)時(shí)數(shù)據(jù)并播放的方法。由于筆者水平有限,難免會(huì)有出入之處,請(qǐng)大家批評(píng)指正。
獲取火柴人(骨架數(shù)據(jù))的實(shí)時(shí)數(shù)據(jù)并播放的通常涉及以下步驟:
-
- 獲取MQTT oaunth token;
- 獲取mqttAccount相關(guān)信息;
- 獲取streamtoken;
- 獲取組ID;
- 建立MQTT連接;
- 訂閱骨架數(shù)據(jù)流:
- 接收骨架數(shù)據(jù);
- 解析骨架數(shù)據(jù);
- 渲染骨架數(shù)據(jù);
- 保持連接活躍.
下面我們展開講解,每個(gè)步聚。
1、獲取MQTT oaunth token:
依oauth接口文檔
調(diào)用接口
https://oauth.altumview.com/v1.0/token
方法:
post請(qǐng)求頭:
字段 | 類型 | 描述 |
---|---|---|
Content-Type | String | application/x-www-form-urlencoded **Ensure that the post request content is urlencoded |
示例:content-type:application/x-www-form-urlencoded
請(qǐng)求體:
字段 | 類型 | 描述 |
---|---|---|
client_id | String | Your application client id |
grant_type | String | access token grant type. authorization_code, refresh_token, or client_credentials |
client_secret可選 | String | Your application's client secret. Required if grant_type is client_credentials or refresh_token. NOTE: if using the authorization code flow, refresh token will only be returned if client secret is provided. However, refrain from storing the client_secret in public apps where the code can be exposed. |
scope可選 | String | Scopes of this request. Default: [user:read]. Options: camera:write person:write alert:write user:write group:write invitation:write room:write camera:read person:read alert:read user:read group:read invitation:read room:read person_info:write |
redirect_uri可選 | String | Required for grant_type of authorization_code. Redirect uri value, previously used when receiving authorization code |
code可選 | String | The authorization code. Required for grant_type of authorization_code. |
code_verifier可選 | String | Required for grant_type of authorization_code. Code verifier for the proivded challenge from GET login endpoint. |
refresh_token可選 | String | Required for grant_type of refresh_token. This is the refresh token retreived from the previous request. NOTE: if using the authorization code flow, refresh token will only be returned if client secret is provided. |
state可選 | String | Application state, which will return the same value in the same field during return |
device_desctiption可選 | String | Desciption of the device making the request |
必填項(xiàng):
-
- client_id: 聯(lián)系我們獲取
- grant_type:client_credentials
非必填項(xiàng)我們也填上,因?yàn)槲覀兊膅rant_type是 client_credentials,所以我們client_secret也需要填上,稍后用到
-
- scope:camera:write camera:read //請(qǐng)求攝像頭的讀寫權(quán)限
- client_secret: ********** //聯(lián)系我們獲取
響應(yīng)
如何返回200,恭喜你,第一步成功了
請(qǐng)求成功(200)
字段 | 類型 | 描述 |
---|---|---|
status_code | Number | HTTP response code. |
success | Boolean | The status of the operation. |
message | String | The message of the operation. |
token_type | String | token type; default is "bearer" |
access_token | String | authrization code for obtain access token. |
refresh_token可選 | String | Refresh token is not included if using the client credential grant, or if client_secret is not provided in the authorization code flow |
data | Object | The data of the operation. |
??is_group_owner | String | Is this user a group owner |
expires_in | Number | the token expiration time in seconds |
state | Number | The state from the request |
Success-Response 200:
HTTP/1.1 200 OK
{
"status_code": 200,
"message": "The request has succeeded.",
"success": true,
"token_type": "bearer",
"access_token": "12346e3babcd21c1bef3f2f12342d64087a3abcd",
"refresh_token": "cfab8df1234380abcd378123412aabcdd2c41234",
"expires_in": 3600,
"state": "",
"data": {
"is_group_owner": true,
"email": "de@sunsili.com",
"user_id": 123
}
}
上面有一個(gè)重要東西,就是
access_token
也就是我們費(fèi)盡心思,寫接口要獲取的東西,有了它才進(jìn)行下一步
如果請(qǐng)求失敗呢,請(qǐng)依如下說(shuō)明,排除
請(qǐng)求失?。?xx)
名稱 | 類型 | 描述 |
---|---|---|
InvalidRequestFieldError | The parameter is provided in invalid format. | |
error_code | Number | The error response code. |
message | String | The message of the operation. |
success | Boolean | The status of the operation. |
status_code | Number | HTTP response code. |
FeatureNotSupportedError | This feature is not supported on this Camera. |
示例:
HTTP/1.1 400 Bad Request
{
"status_code": 400,
"error_code": 6,
"message": "Invalid neccessary fields'",
"success": false
}
2、獲取mqttAccount信息:
獲取MQTT用戶名、密碼和WSS URL。注意,MQTT會(huì)話有時(shí)間限制,需要定期更新。
依接口文檔
調(diào)用接口:https://api.altumview.com/v1.0/mqttAccount
示例如下:
此接口需要權(quán)限: camera:write camera:read,上一個(gè)步一定申請(qǐng)這個(gè)權(quán)限。
請(qǐng)求頭
字段 | 類型 | 描述 |
---|---|---|
Authorization必需 | String | Bearer access token |
如果返回碼為200,恭喜你又成功了一步
請(qǐng)求成功(200)
字段 | 類型 | 描述 |
---|---|---|
status_code必需 | Number | HTTP response code. |
success必需 | Boolean | The status of the operation. |
message必需 | String | The message of the operation. |
data必需 | Object | The data of the operation. |
??wss_url必需 | String | The WSS URL. |
??mqtt_account必需 | Object | The MQTT account object result. |
????username必需 | String | The MQTT username connect to MQTT server |
????passcode必需 | String | The MQTT passcode connect to MQTT server |
????expires_at必需 | Number | The MQTT account expires epoch time in second |
????legacy_subscribe_topics必需 | String[] | The legacy MQTT subscribe topics that are allowed for the account, this will be removed in the future |
????subscribe_topics必需 | String[] | The MQTT subscribe topics that are allowed for the account, not in use yet |
????legacy_publish_topics必需 | String[] | The legacy MQTT publish topics that are allowed for the account, this will be removed in the future |
????publish_topics必需 | String[] | The MQTT publish topics that are allowed for the account, not in use yet |
請(qǐng)求成功返回示例Success-Response 200:
HTTP/1.1 200 OK
{
"data":{
"mqtt_account":{
"username": "someusername",
"passcode": "somepasscode",
"expires_at": 1594432207,
"legacy_subscribe_topics": [
"mobileClient/43A726FEE257AAAA/#",
"mobileClient/200776FFFF70E05F/#",
"mobileClient/2B9FBBBBDD6DAB73/#",
],
"subscribe_topics": [mobileClient/160/#],
"legacy_publish_topics": [
"mobile/43A726FEE2576342/#",
"mobile/200776214270E05F/#",
},
"publish_topics": ["mobile/160/#"]
},
"wss_url": "wss://beijing.altumview.com:8084/mqtt"
},
"message":"The request has succeeded.",
"success":true,
"status_code":200
}
此接口返回?cái)?shù)據(jù)會(huì)告訴你:
MQTT用戶名、密碼和WSS URL, 可以訂閱的主題等信息
拿到上面信息,我們來(lái)測(cè)試一下MQTT數(shù)據(jù)流
測(cè)試MQTT數(shù)據(jù)流
首先建立MQTT連接
要用上面接口獲取的MQTT用戶名、密碼和WSS URL
我測(cè)試用的MQTTX windows客戶端(可聯(lián)系我們獲取),其他平臺(tái)測(cè)試的客戶端(需要的朋友聯(lián)系我們獲取幫助)
然后訂閱主題
使用流令牌訂閱MQTT主題,以接收骨架數(shù)據(jù)。主題格式為
mobileClient/${groupId}/camera/${serialNumber}/skeleton/${streamToken}
火柴人檢測(cè)到有人時(shí),才推送火柴人數(shù)據(jù)到MQTT
有了上面的基礎(chǔ),我們已經(jīng)可以創(chuàng)建MQTT連接并獲取到火柴人數(shù)據(jù)了,下面我們要做就是解析火柴人數(shù)據(jù),并渲染生成火柴人動(dòng)畫了。這個(gè)用代碼說(shuō)話吧!
<html>
<title>Skeleton Stream Demo</title>
<script src="https://docs.altumview.com/resources/js_libs/jquery.min.js"></script>
<script src="https://docs.altumview.com/resources/js_libs/mqttws31.min.js" type="text/javascript"></script>
<script src="https://docs.altumview.com/resources/js_libs/polyfill.min.js"></script>
<script type="text/javascript" language="javascript">
/*****************************************************************************************
* You must replace parameters with your own. Refer to the FAQ for more detail on how to configure them:
* https://docs.altumview.com/FAQ.pdf
* For demo, these settings are configured to an AltumView account on the Canadian server.
* If you do not see any skeleton rendering, the sensor is no longer available.
*
* Last updated: March 18, 2022 by Andrew A.
******************************************************************************************/
const oauthUrl = "https://oauth.ailecare.cn/v1.0";
const apiUrl = "https://api.ailecare.cn/v1.0";
const mqttUrl = "beijing.altumview.com.cn";
const clientId = "HkJMDXEe6G1tJ66s";
const clientSecret = "zFAl2CSkB6hGdzcIwfMMRbFErh8ValC7CS9ISsbnYZyH6xZdXbltoKrVAD7lQ4Xm";
const serialNumber = "23E94A5DACD323EE"; // Use the mobile app to get the serial number
const streamToken = "701406606"; // Call GET '/cameras/:id/streamtoken' endpoint to get Stream Token
const groupId = 72; // Call GET '/info' endpoint to get Group ID
const getCredentials = () => {
$.ajax({
"type": "POST",
"url": `${oauthUrl}/token`,
"headers": {
"Content-Type": "application/x-www-form-urlencoded"
},
"data": {
"client_id": clientId,
"client_secret": clientSecret,
"grant_type": "client_credentials",
"scope": "camera:write camera:read",
},
"success": function(response) {
token = response.access_token;
console.log("token", token)
var url = `${apiUrl}/mqttAccount`;
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.setRequestHeader("Authorization", "Bearer " + token);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
console.log(xhr.responseText)
const response = JSON.parse(xhr.responseText);
username = response.data.mqtt_account.username;
password = response.data.mqtt_account.passcode;
const canvasWidth = 960;
const canvasHeight = 540;
const onFailure = () => {
const reconnectTimeout = 2000;
console.log("Connect failed. Trying to reconnect after 2 sec");
setTimeout(MQTTConnect, reconnectTimeout);
}
const onMessageArrived = (message) => {
const byteList = message.payloadBytes
const frameNum = parseStringInt32(byteList, 0)
const numPeople = parseStringInt32(byteList, 4)
const people = []
for (let i = 0; i < numPeople; i++) {
const pos = 8 + 152 * i;
const personId = parseStringInt32(byteList, pos);
const person = {};
for (let j = 0; j < 18; j++) {
const x = parseStringFloat(byteList, pos + 8 + j * 4);
const y = parseStringFloat(byteList, pos + 80 + j * 4);
if (x && y) person[j] = new Point(x, y);
}
person.name = personId;
people.push(person);
}
const canvas = document.getElementById('canvas');
if (canvas && people) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
people.forEach(person => {
drawSkeleton(ctx, 4, person);
})
}
}
const drawSkeleton = (ctx, lineWidth, points) => {
ctx.lineWidth = lineWidth;
ctx.lineCap = 'round';
let minX = 1;
let minY = 1;
pointPairs.forEach(pair => {
const startPoint = points[pair.start];
const endPoint = points[pair.end];
if (startPoint !== undefined && endPoint !== undefined) {
if (endPoint.x < minX) minX = endPoint.x;
if (endPoint.y < minY) minY = endPoint.y;
ctx.strokeStyle = pair.color;
drawLine(ctx, startPoint.x * canvasWidth, startPoint.y * canvasHeight, endPoint.x * canvasWidth, endPoint.y * canvasHeight);
}
})
}
function Point(x, y) {
this.x = x;
this.y = y;
}
const drawLine = (ctx, x0, y0, x1, y1) => {
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
}
const pointPairs = [
{ start: 0, end: 1, color: 'pink' },
{ start: 1, end: 2, color: 'orange' },
{ start: 2, end: 3, color: 'yellow' },
{ start: 3, end: 4, color: 'lightYellow' },
{ start: 1, end: 5, color: 'darkSalmon' },
{ start: 5, end: 6, color: 'salmon' },
{ start: 6, end: 7, color: 'lightSalmon' },
{ start: 1, end: 8, color: 'darkTurquoise' },
{ start: 8, end: 9, color: 'turquoise' },
{ start: 9, end: 10, color: 'paleTurquoise' },
{ start: 1, end: 11, color: 'darkRed' },
{ start: 11, end: 12, color: 'red' },
{ start: 12, end: 13, color: 'orange' },
{ start: 0, end: 14, color: 'purple' },
{ start: 14, end: 16, color: 'purple' },
{ start: 0, end: 15, color: 'violet' },
{ start: 15, end: 17, color: 'violet' }
]
const parseStringInt32 = (stringData, startIndex) => {
const t = stringData.slice(startIndex, startIndex + 4);
return new DataView(t.buffer).getInt32(0, true);
}
const parseStringFloat = (stringData, startIndex) => {
const t = stringData.slice(startIndex, startIndex + 4);
return new DataView(t.buffer).getFloat32(0, true);
}
const onConnect = () => {
console.log('connect success');
var soptions = {
qos: 0
};
// Next, subscribe to this topic with the aforementioned stream token appended
const subscribeTopic = `mobileClient/${groupId}/camera/${serialNumber}/skeleton/${streamToken}`;
mqtt.subscribe(subscribeTopic, soptions);
console.log(`subscribe to ${subscribeTopic}`);
// Finally, publish the same stream token as a message to the camera in order to start streaming. You must publish this message every 45 seconds to keep streaming going.
const publishTopic = `mobile/${groupId}/camera/${serialNumber}/token/mobileStreamToken`;
message = new Paho.MQTT.Message(streamToken);
message.destinationName = publishTopic;
message.qos = 2;
message.retained = false;
mqtt.send(message);
console.log("Connected");
const reconnectTimeout = 44000;
setTimeout(MQTTConnect, reconnectTimeout);
}
const MQTTConnect = async (id) => {
const port = 8084;
console.log(`connecting to ${mqttUrl}:${port}`);
mqtt = new Paho.MQTT.Client(mqttUrl, port, username);
const options = {
timeout: 3,
onSuccess: onConnect,
onFailure: onFailure,
useSSL: true,
userName: username,
password: password
};
mqtt.onMessageArrived = onMessageArrived;
mqtt.connect(options);
}
MQTTConnect(1);
}
};
xhr.send();
},
"error": function(errorThrown) {
alert(JSON.stringify(errorThrown.error()));
}
});
}
</script>
<body>
<p>This is a demo of the Skeleton Streaming</p>
<canvas id="canvas" width="960" height="540" style="background-color: black; transform: scaleX(-1)"></canvas>
<script>
getCredentials();
</script>
</body>
</html>
這段代碼是一個(gè)HTML頁(yè)面,用于展示一個(gè)基于MQTT協(xié)議的在線直播演示,具體是展示火柴人動(dòng)畫。下面是對(duì)代碼的詳細(xì)解析:
代碼概述
HTML結(jié)構(gòu):頁(yè)面包含一個(gè)<canvas>元素,用于繪制動(dòng)畫。
JavaScript邏輯:
-
- 引入了jQuery、MQTT WebSocket客戶端庫(kù)和Polyfill庫(kù)。
- 獲取URL參數(shù),如客戶名稱和應(yīng)用類型。
- 動(dòng)態(tài)設(shè)置畫布尺寸以適應(yīng)不同屏幕。
- 使用Ajax請(qǐng)求獲取訪問(wèn)令牌和MQTT賬戶信息。
- 連接到MQTT服務(wù)器,并訂閱特定主題以接收動(dòng)畫數(shù)據(jù)。
- 接收到數(shù)據(jù)后,解析并在畫布上繪制火柴人動(dòng)畫。
- 還包含了一些輔助函數(shù),如繪制線條、解析數(shù)據(jù)等。
詳細(xì)解析
1、HTML頭部:
- 引入了必要的JavaScript庫(kù)。
- 設(shè)置了頁(yè)面標(biāo)題。
2、HTML主體:
一個(gè)<div>容器包裹了一個(gè)<canvas>元素,用于顯示動(dòng)畫。
3、JavaScript代碼:
-
- drawSkeleton:繪制火柴人的骨架。
- Point:表示點(diǎn)的類。
- drawLine:繪制線條。
- parseStringInt32和parseStringFloat:解析二進(jìn)制數(shù)據(jù)。
- 解析接收到的數(shù)據(jù),提取出火柴人的位置信息。
- 在畫布上繪制火柴人動(dòng)畫。
- 使用獲取的用戶名和密碼連接到MQTT服務(wù)器。
- 訂閱特定主題以接收火柴人動(dòng)畫數(shù)據(jù)。
- 定期發(fā)布消息以保持連接。
- 參數(shù)獲?。簭腢RL中獲取客戶名稱和應(yīng)用類型。
- 畫布尺寸設(shè)置:根據(jù)窗口大小動(dòng)態(tài)調(diào)整畫布尺寸。
- 獲取憑證:通過(guò)Ajax請(qǐng)求獲取OAuth令牌和MQTT賬戶信息。
- MQTT連接:
- 數(shù)據(jù)處理:
- 輔助函數(shù):
錯(cuò)誤處理
請(qǐng)注意,根據(jù)文檔說(shuō)明,您需要每15分鐘獲取一次流令牌以保持?jǐn)?shù)據(jù)流的活躍狀態(tài)。
總結(jié)
這段代碼是一個(gè)完整的在線直播演示頁(yè)面,展示了如何使用MQTT協(xié)議和Web技術(shù)(HTML、JavaScript)來(lái)實(shí)現(xiàn)實(shí)時(shí)動(dòng)畫的展示。它涵蓋了從前端界面設(shè)計(jì),后端數(shù)據(jù)處理的完整流程,包括網(wǎng)絡(luò)通信、數(shù)據(jù)解析和圖形繪制等關(guān)鍵技術(shù)。
這個(gè)過(guò)程需要您的應(yīng)用程序能夠處理網(wǎng)絡(luò)請(qǐng)求、WebSocket連接、二進(jìn)制數(shù)據(jù)處理和圖形渲染。您可能需要根據(jù)您應(yīng)用程序的具體技術(shù)棧選擇合適的庫(kù)和工具來(lái)實(shí)現(xiàn)上述功能。
如果您遇到任何問(wèn)題,可以參考我們API文檔或聯(lián)系技術(shù)支持獲取幫助。