• 正文
    • 1、獲取MQTT oaunth token:
    • 2、獲取mqttAccount信息:
    • 測(cè)試MQTT數(shù)據(jù)流
    • 代碼概述
    • 詳細(xì)解析
    • 總結(jié)
  • 相關(guān)推薦
申請(qǐng)入駐 產(chǎn)業(yè)圖譜

基于MQTT的Web獲取火柴人實(shí)時(shí)數(shù)據(jù)并播放

4小時(shí)前
68
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點(diǎn)資訊討論

大家好,我是谷動(dòng)谷力的大樹(shù)。

今天我們來(lái)探討一下獲取火柴人(骨架數(shù)據(jù))的實(shí)時(shí)數(shù)據(jù)并播放的方法。由于筆者水平有限,難免會(huì)有出入之處,請(qǐng)大家批評(píng)指正。

獲取火柴人(骨架數(shù)據(jù))的實(shí)時(shí)數(shù)據(jù)并播放的通常涉及以下步驟:

    1. 獲取MQTT oaunth token;
    2. 獲取mqttAccount相關(guān)信息;
    3. 獲取streamtoken;
    4. 獲取組ID;
    5. 建立MQTT連接;
    6. 訂閱骨架數(shù)據(jù)流:
    7. 接收骨架數(shù)據(jù);
    8. 解析骨架數(shù)據(jù);
    9. 渲染骨架數(shù)據(jù);
    10. 保持連接活躍.

下面我們展開(kāi)講解,每個(gè)步聚。

1、獲取MQTT oaunth token:

依oauth接口文檔

調(diào)用接口

https://oauth.altumview.com/v1.0/token

方法:

post請(qǐng)求頭:

字段 類(lèi)型 描述
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)求體:

字段 類(lèi)型 描述
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)求攝像頭的讀寫(xiě)權(quán)限
    • client_secret: ********** //聯(lián)系我們獲取

響應(yīng)

如何返回200,恭喜你,第一步成功了

請(qǐng)求成功(200)

字段 類(lèi)型 描述
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)盡心思,寫(xiě)接口要獲取的東西,有了它才進(jìn)行下一步
如果請(qǐng)求失敗呢,請(qǐng)依如下說(shuō)明,排除

請(qǐng)求失?。?xx)

名稱(chēng) 類(lèi)型 描述
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用戶(hù)名、密碼和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)求頭

字段 類(lèi)型 描述
Authorization必需 String Bearer access token

如果返回碼為200,恭喜你又成功了一步

請(qǐng)求成功(200)

字段 類(lèi)型 描述
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用戶(hù)名、密碼和WSS URL, 可以訂閱的主題等信息

拿到上面信息,我們來(lái)測(cè)試一下MQTT數(shù)據(jù)流

測(cè)試MQTT數(shù)據(jù)流

首先建立MQTT連接

要用上面接口獲取的MQTT用戶(hù)名、密碼和WSS URL

我測(cè)試用的MQTTX windows客戶(hù)端(可聯(lián)系我們獲?。?,其他平臺(tái)測(cè)試的客戶(hù)端(需要的朋友聯(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)畫(huà)了。這個(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)畫(huà)。下面是對(duì)代碼的詳細(xì)解析:

代碼概述

HTML結(jié)構(gòu):頁(yè)面包含一個(gè)<canvas>元素,用于繪制動(dòng)畫(huà)。

JavaScript邏輯:

    • 引入了jQuery、MQTT WebSocket客戶(hù)端庫(kù)和Polyfill庫(kù)。
    • 獲取URL參數(shù),如客戶(hù)名稱(chēng)和應(yīng)用類(lèi)型。
    • 動(dòng)態(tài)設(shè)置畫(huà)布尺寸以適應(yīng)不同屏幕。
    • 使用Ajax請(qǐng)求獲取訪問(wèn)令牌和MQTT賬戶(hù)信息。
    • 連接到MQTT服務(wù)器,并訂閱特定主題以接收動(dòng)畫(huà)數(shù)據(jù)。
    • 接收到數(shù)據(jù)后,解析并在畫(huà)布上繪制火柴人動(dòng)畫(huà)。
    • 還包含了一些輔助函數(shù),如繪制線條、解析數(shù)據(jù)等。

詳細(xì)解析

1、HTML頭部:

  • 引入了必要的JavaScript庫(kù)。
  • 設(shè)置了頁(yè)面標(biāo)題。

2、HTML主體:

一個(gè)<div>容器包裹了一個(gè)<canvas>元素,用于顯示動(dòng)畫(huà)。

3、JavaScript代碼:

    • drawSkeleton:繪制火柴人的骨架。
    • Point:表示點(diǎn)的類(lèi)。
    • drawLine:繪制線條。
    • parseStringInt32和parseStringFloat:解析二進(jìn)制數(shù)據(jù)。
    • 解析接收到的數(shù)據(jù),提取出火柴人的位置信息。
    • 在畫(huà)布上繪制火柴人動(dòng)畫(huà)。
    • 使用獲取的用戶(hù)名和密碼連接到MQTT服務(wù)器。
    • 訂閱特定主題以接收火柴人動(dòng)畫(huà)數(shù)據(jù)。
    • 定期發(fā)布消息以保持連接。
    • 參數(shù)獲取:從URL中獲取客戶(hù)名稱(chēng)和應(yīng)用類(lèi)型。
    • 畫(huà)布尺寸設(shè)置:根據(jù)窗口大小動(dòng)態(tài)調(diào)整畫(huà)布尺寸。
    • 獲取憑證:通過(guò)Ajax請(qǐng)求獲取OAuth令牌和MQTT賬戶(hù)信息。
    • 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)畫(huà)的展示。它涵蓋了從前端界面設(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ù)支持獲取幫助。

相關(guān)推薦

登錄即可解鎖
  • 海量技術(shù)文章
  • 設(shè)計(jì)資源下載
  • 產(chǎn)業(yè)鏈客戶(hù)資源
  • 寫(xiě)文章/發(fā)需求
立即登錄