H265Session2.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. function H265Session() {
  2. let rtpTimeStamp = 0;
  3. let size1M = 1048576; //1024 * 1024
  4. let inputBuffer = new Uint8Array(size1M);
  5. let spsSegment = null;
  6. let ppsSegment = null;
  7. let SPSParser = null;
  8. let width = 0;
  9. let height = 0;
  10. let inputLength = 0;
  11. let initalSegmentFlag = true; //用于确定是否是initSegment
  12. let initalMediaFrameFlag = true;
  13. let frameRate = null; //根据SDP或者SPS设置
  14. let preSample = null; //上一个Sample
  15. let inputSegBufferSub = null;
  16. //MSE使用的数据以及相关配置,顺序codecInfo -> initSegmentData -> mediaSample -> frameData
  17. //时间戳用于绘制人脸框
  18. let decodedData = {
  19. frameData: null, //视频数据
  20. timeStamp: null, //时间戳
  21. initSegmentData: null, //MP4配置,用于initsegment
  22. mediaSample: null, //使用duration控制每一帧的播放时间
  23. codecInfo: "", //MSE init时传入,用于创建mediasource
  24. };
  25. let FRAMETYPE = {
  26. 1: 'P',
  27. 5: 'I',
  28. 6: 'SEI',
  29. 7: 'I'
  30. };
  31. let frameType = '';
  32. let decodeMode = 'canvas';
  33. let outputSize = 0;
  34. let curSize = 0;
  35. const PREFIX = new Uint8Array(['0x00', '0x00', '0x00', '0x01']);
  36. let firstIframe = false;
  37. let SEIInfo = {
  38. ivs: null,
  39. timestamp:null,
  40. };
  41. let preWidth = null,
  42. preHeight = null;
  43. let resetTimeCount = 0;
  44. let lastTimeStamp = 0;
  45. //const RESETTIME = 162000000;
  46. const RESETTIME = 4320000;
  47. let SEIBuffer = null;
  48. let lastTime =0;
  49. let decoder = null;
  50. function constructor() {
  51. }
  52. constructor.prototype = {
  53. init() {
  54. SPSParser = new H265SPSParser();
  55. decoder = new H265Decoder();
  56. this.resolutionChangedCallback = ()=>{};
  57. },
  58. remuxRTPData(rtspInterleaved, rtpHeader, rtpPayload) {
  59. //console.log(rtspInterleaved)
  60. //console.log(rtpHeader)
  61. let PaddingSize = 0;
  62. let extensionHeaderLen = 0; //如果RtpHeader.X=1,则在RTP报头后跟有一个扩展报头
  63. let PAYLOAD = null;
  64. //console.log(rtpHeader)
  65. //console.log(rtspInterleaved, rtpHeader, rtpPayload.subarray(0,5))
  66. let RtpHeader = {
  67. V: rtpHeader[0] >>> 6,
  68. P: rtpHeader[0] & 0x20,
  69. X: rtpHeader[0] & 0x10,
  70. CC: rtpHeader[0] & 0x0F,
  71. M: (rtpHeader[1] & 0x80) === 0x80,
  72. PT: rtpHeader[1] & 127,
  73. SN: (rtpHeader[2] << 8) + rtpHeader[3],
  74. timeStamp: (rtpHeader[4] << 24) + (rtpHeader[5] << 16) + (rtpHeader[6] << 8) + rtpHeader[7],
  75. SSRC: (rtpHeader[8] << 24) + (rtpHeader[9] << 16) + (rtpHeader[10] << 8) + rtpHeader[11],
  76. };
  77. var HEADER = rtpHeader,
  78. timeData = {
  79. 'timestamp': null,
  80. 'timezone': null,
  81. };
  82. if (rtspInterleaved[0] !== 0x24) {
  83. console.log("H265Session::it is not valid interleave header (RTSP over TCP)");
  84. return;
  85. } else if ((rtpHeader[0] & 0x0F) === 0x0F) {
  86. console.log("H265Session::There is additional CSRC which is not handled in this version");
  87. return;
  88. } else if ((rtpHeader[0] & 0x20) === 0x20) {
  89. PaddingSize = rtpPayload[rtpPayload.length - 1];
  90. console.log("H265Session::PaddingSize - " + PaddingSize);
  91. }
  92. //Extension bit check in RTPHeader
  93. if ((rtpHeader[0] & 0x10) === 0x10) {
  94. extensionHeaderLen = (((rtpPayload[2] << 8) | rtpPayload[3]) * 4) + 4;
  95. //Playback check
  96. if (rtpPayload[0] === 0xAB && rtpPayload[1] === 0xAD) {
  97. var startHeader = 4,
  98. NTPmsw = new Uint8Array(new ArrayBuffer(4)),
  99. NTPlsw = new Uint8Array(new ArrayBuffer(4)),
  100. gmt = new Uint8Array(new ArrayBuffer(2)),
  101. fsynctime = {
  102. 'seconds': null,
  103. 'useconds': null,
  104. },
  105. microseconds = null;
  106. NTPmsw.set(rtpPayload.subarray(startHeader, startHeader + 4), 0);
  107. startHeader += 4;
  108. NTPlsw.set(rtpPayload.subarray(startHeader, startHeader + 4), 0);
  109. startHeader += 6;
  110. gmt.set(rtpPayload.subarray(startHeader, startHeader + 2), 0);
  111. microseconds = (this.ntohl(NTPlsw) / 0xffffffff) * 1000;
  112. fsynctime.seconds = ((this.ntohl(NTPmsw) - 0x83AA7E80) >>> 0);
  113. fsynctime.useconds = microseconds;
  114. gmt = (((gmt[0] << 8) | gmt[1]) << 16) >> 16;
  115. timeData = {
  116. timestamp: fsynctime.seconds,
  117. timestamp_usec: fsynctime.useconds,
  118. timezone: gmt,
  119. };
  120. }
  121. }
  122. //console.log('extensionHeaderLen: '+ extensionHeaderLen)
  123. PAYLOAD = rtpPayload.subarray(extensionHeaderLen, rtpPayload.length - PaddingSize);
  124. rtpTimeStamp = RtpHeader.timeStamp;
  125. //console.log(rtpTimeStamp, rtpHeader[4], rtpHeader[5], rtpHeader[6] , rtpHeader[7], PAYLOAD[0] & 0x1f)
  126. /* 载荷结构(https://blog.csdn.net/davebobo/article/details/52994596)
  127. +---------------+
  128. |0|1|2|3|4|5|6|7|
  129. +-+-+-+-+-+-+-+-+
  130. |F|NRI| Type |
  131. +---------------+
  132. Type = 1-23 单个NAL单元包
  133. Type = 24,25, 26, 27聚合包
  134. Type = 28,29, 分片单元
  135. */
  136. //console.log(rtspInterleaved,rtpHeader, PAYLOAD[0]);
  137. let nalType = (PAYLOAD[0] >> 1) & 0x3f;
  138. //console.log(PAYLOAD[0] + ' nalType: ' + nalType);
  139. //console.log('rtpPayload.length: ' + rtpPayload.length)
  140. //console.log(nalType, PAYLOAD[0])
  141. //console.log('nalType: ' + nalType, RtpHeader.M, rtpTimeStamp)
  142. //console.log('nalType: ', nalType, ' timestamp: ', rtpTimeStamp);
  143. switch (nalType) {
  144. default:
  145. inputBuffer = setBuffer(inputBuffer, PREFIX);
  146. inputBuffer = setBuffer(inputBuffer, PAYLOAD);
  147. break;
  148. // Fragmentation unit(FU)
  149. case 49:
  150. var startBit = ((PAYLOAD[2] & 0x80) === 0x80);
  151. var endBit = ((PAYLOAD[2] & 0x40) === 0x40);
  152. var fuType = PAYLOAD[2] & 0x3f;
  153. var payloadStartIndex = 3;
  154. if (startBit === true && endBit === false) {
  155. var newNalHeader = new Uint8Array(2);
  156. newNalHeader[0] = (PAYLOAD[0] & 0x81) | (fuType << 1);
  157. newNalHeader[1] = PAYLOAD[1];
  158. inputBuffer = setBuffer(inputBuffer, PREFIX);
  159. inputBuffer = setBuffer(inputBuffer, newNalHeader);
  160. inputBuffer = setBuffer(inputBuffer,
  161. PAYLOAD.subarray(payloadStartIndex, PAYLOAD.length));
  162. } else {
  163. inputBuffer = setBuffer(inputBuffer,
  164. PAYLOAD.subarray(payloadStartIndex, PAYLOAD.length));
  165. }
  166. break;
  167. //SPS
  168. case 33:
  169. SPSParser.parse(PAYLOAD);
  170. var resolution = SPSParser.getSizeInfo();
  171. curSize = resolution.width * resolution.height;
  172. inputBuffer = setBuffer(inputBuffer, PREFIX);
  173. inputBuffer = setBuffer(inputBuffer, PAYLOAD);
  174. width = resolution.width;
  175. height = resolution.height;
  176. break;
  177. } //end of switch(nalType)
  178. //console.log('ppsSegment: ', ppsSegment)
  179. //console.log('nalType: ' + nalType);
  180. //console.log('M: ' + RtpHeader.M + ' ' + (rtpHeader[1] & 0x80))
  181. //console.log(RtpHeader)
  182. let inputBufferSub = inputBuffer.subarray(0, inputLength);
  183. if (inputBufferSub[4] === 0x40) {
  184. frameType = 'I';
  185. } else {
  186. frameType = 'P';
  187. }
  188. //console.log('RtpHeader.M: ', RtpHeader.M)
  189. //check marker bit
  190. if (RtpHeader.M) {
  191. // if (!firstIframe) {
  192. // inputLength = 0;
  193. // return;
  194. // }
  195. let inputBufferSub = inputBuffer.subarray(0, inputLength);
  196. //只根据视频帧计算resetTimeCount
  197. if(frameType !== 'SEI') {
  198. // rtp时间戳周期为RESETTIME,如果单向递增,设为0
  199. if(lastTimeStamp - rtpTimeStamp > (RESETTIME / 2)) { //判断lastTimeStamp远大于rtpTimeStamp,防止后一帧比前一帧先到的情况
  200. //console.log(lastTimeStamp - rtpTimeStamp)
  201. console.warn('时间戳重置', lastTimeStamp, rtpTimeStamp, frameType, new Date())
  202. resetTimeCount ++;
  203. }
  204. rtpTimeStamp = rtpTimeStamp + RESETTIME * resetTimeCount;
  205. } else {
  206. //同一帧的SEI比视频发送慢时
  207. if (rtpTimeStamp - lastTimeStamp > (RESETTIME / 2)) {
  208. console.warn('SEI翻转', rtpTimeStamp , lastTimeStamp);
  209. rtpTimeStamp = rtpTimeStamp + RESETTIME * (resetTimeCount - 1);
  210. } else {
  211. rtpTimeStamp = rtpTimeStamp + RESETTIME * resetTimeCount;
  212. }
  213. //console.log('SEI', rtpTimeStamp, 'video', lastTimeStamp, lastTimeStamp+ RESETTIME * resetTimeCount - rtpTimeStamp)
  214. // //同一帧的SEI比视频发送快时
  215. // if(rtpTimeStamp - lastTimeStamp === 3600) {
  216. // if(rtpTimeStamp === 0 ) {
  217. // rtpTimeStamp = rtpTimeStamp + RESETTIME * (resetTimeCount + 1);
  218. // } else {
  219. // rtpTimeStamp = rtpTimeStamp + RESETTIME * resetTimeCount;
  220. // }
  221. // }
  222. }
  223. if(frameType === 'SEI') {
  224. //SEI被分片(nal === 28)时,分片包发送完后marker为1,不会和视频帧一起
  225. SEIBuffer = inputBuffer.subarray(4, inputLength);
  226. //console.log(SEIBuffer)
  227. inputBufferSub = new Uint8Array();
  228. }
  229. if(SEIBuffer) {
  230. let SEI = SEIParse(SEIBuffer);
  231. if (SEI) {
  232. SEIInfo.ivs = SEI;
  233. SEIInfo.timestamp = rtpTimeStamp;
  234. //console.log('GET SEI', rtpTimeStamp)
  235. //SEI信息
  236. decodedData.SEIInfo = SEIInfo;
  237. if (!(rtpTimeStamp - lastTime)) {
  238. console.log('上个SET包时间和本次相等');
  239. console.log('lastTime: ', lastTime, ' rtpTimeStamp: ', rtpTimeStamp, RtpHeader.timeStamp, rtpHeader[4], rtpHeader[5], rtpHeader[6], rtpHeader[7])
  240. }
  241. if ((rtpTimeStamp - lastTime) !== 3600) {
  242. //console.log('SEI 时间差:', (rtpTimeStamp - lastTime), rtpTimeStamp, lastTime)
  243. }
  244. lastTime = rtpTimeStamp;
  245. SEIInfo = {
  246. ivs: null,
  247. timestamp: 0,
  248. };
  249. }
  250. SEIBuffer = null;
  251. }
  252. if(decodeMode === 'canvas' && (frameType !== 'SEI')) {
  253. if (outputSize !== curSize) {
  254. outputSize = curSize;
  255. decoder.setOutputSize(curSize);
  256. }
  257. decodedData.frameData = decoder.decode(inputBufferSub);
  258. decodedData.decodeMode = 'canvas';
  259. decodedData.timeStamp = rtpTimeStamp;
  260. decodedData.width = width;
  261. decodedData.height = height;
  262. this.handleDecodedData(decodedData)
  263. } else {
  264. if (!initalSegmentFlag) {
  265. decodedData.initSegmentData = null;
  266. decodedData.codecInfo = null;
  267. } else {
  268. initalSegmentFlag = false;
  269. const info = {
  270. id: 1,
  271. width: width,
  272. height: height,
  273. type: "video",
  274. profileIdc: SPSParser.getSpsValue("profile_idc"),
  275. profileCompatibility: 0,
  276. levelIdc: SPSParser.getSpsValue("level_idc"),
  277. sps: [spsSegment],
  278. pps: [ppsSegment],
  279. timescale: 1e3,
  280. fps: frameRate
  281. };
  282. decodedData.initSegmentData = info;
  283. //decodedData.codecInfo = SPSParser.getCodecInfo();
  284. //console.log(info.pps)
  285. }
  286. if (frameType === 'I') {
  287. //console.log('ppsSegment: ', ppsSegment)
  288. let h264parameterLength = spsSegment.length + ppsSegment.length + 8;
  289. inputSegBufferSub = inputBufferSub.subarray(h264parameterLength, inputBufferSub.length);
  290. } else {
  291. inputSegBufferSub = inputBufferSub.subarray(0, inputBufferSub.length);
  292. }
  293. let segSize = inputSegBufferSub.length - 4;
  294. //mp4 box头
  295. inputSegBufferSub[0] = (segSize & 0xFF000000) >>> 24;
  296. inputSegBufferSub[1] = (segSize & 0xFF0000) >>> 16;
  297. inputSegBufferSub[2] = (segSize & 0xFF00) >>> 8;
  298. inputSegBufferSub[3] = (segSize & 0xFF);
  299. decodedData.frameData = new Uint8Array(inputSegBufferSub);
  300. let sample = {
  301. duration: Math.round((1 / frameRate) * 1000),
  302. size: inputSegBufferSub.length,
  303. frame_time_stamp: null,
  304. frameDuration: null,
  305. };
  306. sample.frame_time_stamp = rtpTimeStamp; //Todo:暂时为null,通过帧率控制duration
  307. if (initalMediaFrameFlag) {
  308. sample.frameDuration = 0;
  309. initalMediaFrameFlag = false;
  310. } else {
  311. if(frameRate) {
  312. sample.frameDuration = Math.round(1000 / frameRate);
  313. }else {
  314. sample.frameDuration = (sample.frame_time_stamp - preSample.frame_time_stamp) / 90; // 时钟频率90000,timescale=1000
  315. }
  316. }
  317. preSample = sample;
  318. decodedData.mediaSample = sample;
  319. decodedData.timeStamp = rtpTimeStamp;
  320. this.handleDecodedData(decodedData);
  321. }
  322. inputLength = 0;
  323. decodedData.SEIInfo = null;
  324. inputSegBufferSub = null;
  325. if(frameType !== 'SEI') {
  326. lastTimeStamp = RtpHeader.timeStamp;
  327. }
  328. frameType = '';
  329. }
  330. //console.log('xxxxxxxxxxxxxxxxxxxxxxxxx')
  331. },
  332. set rtpSessionCallback(func) {
  333. this.handleDecodedData = func;
  334. },
  335. setFrameRate(fps) {
  336. frameRate = fps;
  337. //console.log('frameRate: ', frameRate)
  338. },
  339. setResolutionChangedCallback(callback) {
  340. this.resolutionChangedCallback = callback;
  341. }
  342. }
  343. return new constructor();
  344. function setBuffer(buffer1, buffer2) {
  345. let bufferTemp = buffer1;
  346. if ((inputLength + buffer2.length) > buffer1.length) {
  347. bufferTemp = new Uint8Array(buffer1.length + size1M);
  348. }
  349. bufferTemp.set(buffer2, inputLength);
  350. inputLength += buffer2.length;
  351. return bufferTemp;
  352. }
  353. }
  354. /**
  355. * 去除SPS中的Emulation字节
  356. * @param data SPS源数据
  357. * @returns {Array} 去除后Emulation字节后的SPS
  358. */
  359. function removeH264or5EmulationBytes(data) {
  360. let toSize = 0;
  361. let i = 0;
  362. let to = [];
  363. let dataLength = data.length;
  364. while (i < dataLength) {
  365. if (i + 2 < dataLength && data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 3) {
  366. to[toSize] = to[toSize + 1] = 0;
  367. toSize += 2;
  368. i += 3;
  369. } else {
  370. to[toSize] = data[i];
  371. toSize += 1;
  372. i += 1;
  373. }
  374. }
  375. return to;
  376. }
  377. /**
  378. * 解析SEI信息
  379. * @param data
  380. * @return {Array}
  381. */
  382. function SEIParse(data) {
  383. //console.log(data)
  384. if((data[0] & 0x1f) !== 6) {
  385. //非SEI
  386. return null;
  387. }
  388. if(data[1] !== 0x55 || data[2] !== 0x4C ||data[3] !== 0x53 ||data[4] !== 0x40) {
  389. //ULS@开头
  390. console.warn('unknown SEI type');
  391. return null;
  392. }
  393. let dataLength = data.length;
  394. let type = (data[5] << 8) + data[6];
  395. let checkSum = (data[7] << 8) + data[8];
  396. // if(dataLength !== (checkSum + 1 + 4 + 4)) {
  397. // console.log('SEI check fail!');
  398. // return null;
  399. // }
  400. let result;
  401. data = data.subarray(9);
  402. switch(type) {
  403. case 0:
  404. result = parseRect(data);
  405. break;
  406. case 1:
  407. //console.log(parseBody(data))
  408. result = parseBody(data);
  409. break;
  410. default:
  411. result = null;
  412. break;
  413. }
  414. return result;
  415. }
  416. function parseRect(data) {
  417. //console.log(data)
  418. let dataLength = data.length;
  419. let contents = [];
  420. while(dataLength > 0) {
  421. //console.log('dataLength: ', dataLength)
  422. let x0 = ((data[4] & 0x7f) << 8) + data[5],
  423. y0 = ((data[6] & 0x7f) << 8) + data[7],
  424. width = ((data[8] & 0x7f) << 8) + data[9] - x0,
  425. height = ((data[10] & 0x7f) << 8) + data[11] - y0;
  426. let content = {
  427. type: 'rect',
  428. id: (data[2] << 8) + data[3],
  429. rect: [x0, y0, width, height],
  430. state: data[1] & 0x01,
  431. quality: (data[1] & 0x02) >> 1,
  432. };
  433. Array.prototype.push.apply(contents, [content]);
  434. data = data.subarray(12);
  435. dataLength = data.length;
  436. }
  437. return contents;
  438. }
  439. function parseBody(data) {
  440. let dataLength = data.length;
  441. let contents = [];
  442. while(dataLength > 0) {
  443. //console.log('dataLength: ', dataLength)
  444. let x0 = ((data[4] & 0x7f) << 8) + data[5],
  445. y0 = ((data[6] & 0x7f) << 8) + data[7],
  446. width = ((data[8] & 0x7f) << 8) + data[9] - x0,
  447. height = ((data[10] & 0x7f) << 8) + data[11] - y0,
  448. boxConfidence = ((data[12] & 0x7f) << 8) + data[13];
  449. let points = [];
  450. for(let i = 0; i < 17; i++) {
  451. let point = {
  452. x: ((data[16 + i * 8] & 0x7f) << 8) + data[17 + i * 8],
  453. y: ((data[18 + i * 8] & 0x7f) << 8) + data[19 + i * 8],
  454. confidence: ((data[20 + i * 8] & 0x7f) << 8) + data[21 + i * 8],
  455. // x: Math.random() * 8191,
  456. // y: Math.random() * 8191,
  457. // confidence: 1,
  458. };
  459. points.push(point);
  460. }
  461. let content = {
  462. type: 'coco-pose',
  463. id: (data[2] << 8) + data[3],
  464. handsUp: data[1] & 0x04,
  465. boundingBox: [x0, y0, width, height],
  466. boxConfidence,
  467. points: parseBodyToTree(points),
  468. state: data[1] & 0x01,
  469. };
  470. Array.prototype.push.apply(contents, [content]);
  471. data = data.subarray(152);
  472. dataLength = data.length;
  473. }
  474. return contents;
  475. }
  476. /**
  477. * 将智能帧中的人体姿态点转化为树结构(双亲表示法)
  478. * @param points
  479. */
  480. function parseBodyToTree(points) {
  481. let newPoints = [];
  482. newPoints[0] = {...points[0], parent: -1, pointColor:'#FF0002', lineColor: '#FF0002'};
  483. newPoints[1] = {...points[1], parent: 0, pointColor:'#FF0002', lineColor: '#FF0002'};
  484. newPoints[2] = {...points[2], parent: 0, pointColor:'#FF0002', lineColor: '#FF0002'};
  485. newPoints[3] = {...points[5], parent: 17, pointColor:'#D9E34F', lineColor: '#FF0002'};
  486. newPoints[4] = {...points[6], parent: 17, pointColor:'#D9E34F', lineColor: '#FF0002'};
  487. newPoints[5] = {...points[3], parent: 1, pointColor:'#00FF00', lineColor: '#FF0002'};
  488. newPoints[6] = {...points[4], parent: 2, pointColor:'#FFAC00', lineColor: '#FF0002'};
  489. newPoints[7] = {...points[7], parent: 3, pointColor:'#00FF45', lineColor: '#'};
  490. newPoints[8] = {...points[11], parent: 17, pointColor:'#EEC446', lineColor: '#'};
  491. newPoints[9] = {...points[8], parent: 4, pointColor:'#43D3AF', lineColor: '#'};
  492. newPoints[10] = {...points[12], parent: 17, pointColor:'#7A93E8', lineColor: '#'};
  493. newPoints[11] = {...points[9], parent: 7, pointColor:'#EFB842', lineColor: '#'};
  494. newPoints[12] = {...points[13], parent: 8, pointColor:'#E56C00', lineColor: '#'};
  495. newPoints[13] = {...points[10], parent: 9, pointColor:'#47CD43', lineColor: '#0096FF'};
  496. newPoints[14] = {...points[14], parent: 10, pointColor:'#3868D2', lineColor: '#00FF51'};
  497. newPoints[15] = {...points[15], parent: 12, pointColor:'#DF4D01', lineColor: '#5100FF'};
  498. newPoints[16] = {...points[16], parent: 14, pointColor:'#1E48D4', lineColor: '#00FFA0'};
  499. newPoints[17] = {x: (points[5].x + points[6].x) / 2,y: (points[5].y + points[6].y) / 2, parent: -1, pointColor:'#D9E34F', lineColor: '#00FFA0'};
  500. return newPoints;
  501. }