Toggle navigation
Toggle navigation
此项目
正在载入...
Sign in
李勇
/
McuClient
转到一个项目
Toggle navigation
项目
群组
代码片段
帮助
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
李勇
8 years ago
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
e2502d918235a9acde0acbf6c38a5d8c1ae9446e
e2502d91
1 parent
169b80b8
master
...
20170922-1
20171019-1
20171120-1w
dev
letv-20170426
ly20170622
ly20170622-2
ly20170622-3
ly20170627-2
ly20170706-1
ly20170708-2
ly20170708-3
ly20170710-1
ly20170717-1
ly20170719-1
ly20170723-1
ly20170724-2
ly20170726-1
ly20170731-1
ly20170731-2
ly20170801-2
ly20170802-1
ly20170818-1
ly20170818-2
ly20170820-1
ly20170821-1
ly20170824-1
ly20170829-1
ly20170925-1
ly20170926-1
ly20170927-1
ly20170929-1
ly20171011-1
ly20171013-1
ly20171013-2
ly20171016-1
ly20171021-1
ly20171023-1
ly20171024-1w
ly20171025-1w
ly20171026-1w
ly20171027-1w
ly20171030-1
ly20171030-2w
ly20171107-1
ly20171110-1w
ly20171113-1w
ly20171204-1w
ly20171208-1w
ly20171211-1w
ly20171211-2w
ly20171214-1w
webRtc-dev
v2.38.13.20171216
v2.38.11.20171214
v2.38.3.201712011
v2.38.1.201712011
v2.36.11.20171204
v2.36.8.20171206
v2.36.4.20171201
v2.35.11.20171130
v2.34.16.20171128
v2.34.5.20171127
v2.33.6.20171123
v2.32.1.20171123
v2.31.12.20171122
v2.31.10.20171122
v2.30.5.20171117
v2.30.2.20171117
v2.29.5.20171114
v2.28.1.20171110
v2.27.11.20171109
v2.26.9.20171107
v2.26.6.20171103
v2.26.2.20171102
v2.25.7.20171031
v2.25.6.20171031
v2.25.0.20171030
v2.24.2.20171030
v2.23.0.20171030
v2.22.7.20171026
v2.20.5.20171023
v2.20.0.20171021
v2.19.8.20171020
v2.18.14.20171020
v2.18.10.20171019
v2.17.11.20171014
v2.16.8.20171012
v2.16.5.20171012
v2.15.5.20171001
v2.15.3.20170929
v2.14.5.20170927
v2.13.5.20170927
v2.12.14.20170927
v2.12.8.20170926
v2.12.6.20170925
v2.11.13.20170925
v2.10.7.20170921
v2.10.6.20170921
v2.10.5.20170920
v2.10.4.20170920
v2.9.3.20170919
v2.8.17.20170918
v2.8.8.20170917
v2.8.2.20170916
v2.6.2.20170915
v2.5.12.20170915
v2.5.6.20170914
v2.5.5.20170914
v2.4.4.20170908
v2.4.2.20170908
v2.4.0.20170907
v2.3.6.20170907
v2.2.16.20170905
v2.1.22.20170904
v1.84.0.20170912
v1.83.2.20170831
v1.82.11.20170829
v1.81.19.20170828
v1.80.2.20170824
v1.79.6.20170822
v1.79.5.20170821
v1.79.4.20170821
v1.79.3.20170821
v1.78.4.20170820
v1.77.4.20170819
v1.76.2.20170818
v1.75.0.20170815
v1.74.0.20170814
v1.73.2.20170814
v1.73.1.20170814
v1.71.0.20170813
v1.70.5.20170812
v1.68.2.20170812
v1.66.1.20170809
v1.65.25.20170808
v1.65.24.20170806
v1.63.1.20170731
v1.62.3.20170731
v1.61.0.20170729
v1.60.0.20170729
v1.59.0.20170729
v1.58.0.20170729
v1.57.0.20170727
v1.56.1.20170727
v1.56.0.20170727
v1.52.1.20170726
v1.51.0.20170724
v1.50.7.20170724
v1.49.1.20170724
v1.48.2.20170723
v1.46.1.20170722
v1.45.1.20170717
v1.43.1.20170711
v1.42.1.20170708
v1.41.0.20170708
v1.40.0.20170706
v1.39.2.20170706
v1.39.1.20170705
v1.38.4.20170629
v1.37.5.20170627
v1.37.2.20170622
v1.36.7.20170620
v1.36.4.20170620
v1.36.1.20170619
v1.35.4.20170619
v1.34.2.20170615
v1.33.2.20170615
v1.32.1.20170614
v1.31.11.20170613
v1.30.20.20170607
v1.30.7.20170606
v1.30.6.20170606
v1.30.5.20170605
v1.30.3.20170602
v1.29.8.20170601
v1.28.0.201705031
v1.27.16.201705027
v1.27.14.201705027
v1.27.10.201705026
v1.25.2.201705025
v1.23.5.201705023
v1.23.4.201705018
修复录制回放多路音视频,在seek拖动播放的时候,显示数量不正确的问题
隐藏空白字符变更
内嵌
并排对比
正在显示
4 个修改的文件
包含
194 行增加
和
479 行删除
dist/McuClient.js
src/EngineEntrance.js
src/RecordPlayBackParse.js
src/apes/RecordApe.js
dist/McuClient.js
查看文件 @
e2502d9
此 diff 太大无法显示。
src/EngineEntrance.js
查看文件 @
e2502d9
...
...
@@ -28,7 +28,7 @@ import MediaModule from 'apes/MediaModule';
import
UTF8
from
'utf-8'
;
let
loger
=
Loger
.
getLoger
(
'McuClient'
);
let
_sdkInfo
=
{
"version"
:
"v1.2
2.0.201705017
"
,
"author"
:
"www.3mang.com"
};
let
_sdkInfo
=
{
"version"
:
"v1.2
3.0.201705018
"
,
"author"
:
"www.3mang.com"
};
//APE
let
_sass
;
...
...
src/RecordPlayBackParse.js
查看文件 @
e2502d9
...
...
@@ -7,6 +7,7 @@ import pdu from 'pdus/index';
import
PduType
from
'pdus/PduType'
;
import
PduConsts
from
'pdus/PduConsts'
;
import
ApeConsts
from
'apes/ApeConsts'
;
import
ArrayBufferUtil
from
'libs/ArrayBufferUtil'
;
import
Base64
from
'base64-js'
;
import
GlobalConfig
from
'GlobalConfig'
;
...
...
@@ -32,7 +33,7 @@ class RecordPlayBackParse extends Emiter {
this
.
_recordPlaybackMaxTime
=
0
;
//录制回放的总时间
this
.
_isReady
=
false
;
//录制回放是否已经准备完成
this
.
_apes
=
{};
this
.
mediaChannleList
=
{};
this
.
_conferApeMssages
=
{};
//会议数据
this
.
_chatApeMssages
=
{};
//聊天数据
this
.
_videoApeMssages
=
{};
//视频数据
...
...
@@ -118,6 +119,7 @@ class RecordPlayBackParse extends Emiter {
//解析和储存,录制回放EverSocket底层消息处理 data-数据;timestamp-数据对应的时间戳
_parseSaveSocketMsgReceivedHandler
(
data
,
timestamp
)
{
loger
.
log
(
'解析和储存,录制回放EverSocket底层消息处理 '
);
let
pduMsg
=
pdu
.
decode_pdu
(
data
);
let
pduType
=
pduMsg
.
get
(
"type"
);
let
pduData
=
pduMsg
.
get
(
"data"
);
...
...
@@ -170,9 +172,11 @@ class RecordPlayBackParse extends Emiter {
break
;
case
ApeConsts
.
VIDEO_SESSION_ID
:
this
.
saveParseData
(
data
,
timestamp
,
this
.
_videoApeMssages
);
this
.
_pduRegAdapterHandler
(
pduMsg
.
data
,
timestamp
,
data
,
ApeConsts
.
VIDEO_SESSION_ID
)
break
;
case
ApeConsts
.
AUDIO_SESSION_ID
:
this
.
saveParseData
(
data
,
timestamp
,
this
.
_audioApeMssages
);
this
.
_pduRegAdapterHandler
(
pduMsg
.
data
,
timestamp
,
data
,
ApeConsts
.
AUDIO_SESSION_ID
)
break
;
default
:
break
;
...
...
@@ -308,6 +312,7 @@ class RecordPlayBackParse extends Emiter {
}
GlobalConfig
.
recordPlaybackMaxTime
=
this
.
_recordPlaybackMaxTime
;
console
.
log
(
'MediaChannleList'
,
this
.
mediaChannleList
);
loger
.
log
(
"录制回放数据解析完成,录制回放的总时间长为->"
,
this
.
_recordPlaybackMaxTime
);
this
.
_emit
(
RecordPlayBackParse
.
CLASS_JOIN_RECORD_PLAYBACK_SUCCESS
,
{
"recordPlaybackMaxTime"
:
this
.
_recordPlaybackMaxTime
});
}
...
...
@@ -383,15 +388,19 @@ class RecordPlayBackParse extends Emiter {
GlobalConfig
.
activeDocCurPage
=
1
;
this
.
_emit
(
RecordPlayBackParse
.
RECORD_PLAYBACK_CLEAR_DATA
);
//各个ape模块查找关键帧数据
this
.
_searchKeyfram
();
this
.
_search
Seek
Keyfram
();
}
_searchKeyfram
()
{
//拖动进度条后根据seek时间点查找
_searchSeekKeyfram
()
{
//查找关键帧,找到关键帧后再继续播放
this
.
_searchApeMessageKeyfram
(
this
.
_conferApeMssages
,
ApeConsts
.
CONFERENCE_SESSION_ID
);
this
.
_searchApeMessageKeyfram
(
this
.
_docApeMssages
,
ApeConsts
.
DOCSHARING_SESSION_ID
);
this
.
_searchApeMessageKeyfram
(
this
.
_videoApeMssages
,
ApeConsts
.
VIDEO_SESSION_ID
);
this
.
_searchApeMessageKeyfram
(
this
.
_audioApeMssages
,
ApeConsts
.
AUDIO_SESSION_ID
);
//this._searchApeMessageKeyfram(this._videoApeMssages, ApeConsts.VIDEO_SESSION_ID);
//this._searchApeMessageKeyfram(this._audioApeMssages, ApeConsts.AUDIO_SESSION_ID);
this
.
_searchMediaApeMessageKeyfram
(
this
.
mediaChannleList
);
//聊天模块、白板标注模块的比较特殊,消息是累计的,默认最多30条
this
.
_searchChatHistoryMessageKeyfram
(
this
.
_chatApeMssages
,
ApeConsts
.
CHAT_SESSION_ID
);
...
...
@@ -425,7 +434,35 @@ class RecordPlayBackParse extends Emiter {
}
loger
.
log
(
"SEEK->APE"
,
ApeConsts
(
_apeId
),
this
.
_recordPlaybackTimestamp
,
"没有查找到相连的数据"
);
}
_searchMediaApeMessageKeyfram
(
_apeMessages
){
loger
.
log
(
"_searchMediaApeMessageKeyfram->SEEK->APE"
)
if
(
_apeMessages
)
{
for
(
let
k
in
_apeMessages
)
{
let
channelInfos
=
_apeMessages
[
k
];
let
messageItem
;
let
keyFrameSeekTime
=
0
;
for
(
let
i
=
this
.
_recordPlaybackTimestamp
;
i
>
0
;
i
--
)
{
messageItem
=
channelInfos
[
i
];
if
(
messageItem
)
{
keyFrameSeekTime
=
(
this
.
_recordPlaybackTimestamp
-
i
);
loger
.
log
(
"_searchMediaApeMessageKeyfram->SEEK->APE->messageItem"
,
messageItem
,
'keyFrameSeekTime->'
,
keyFrameSeekTime
)
this
.
_everSocketMsgReceivedHandler
(
messageItem
.
byteData
,
keyFrameSeekTime
);
/*for (let k = 0; k < messageItem.length; k++) {
this._everSocketMsgReceivedHandler(messageItem[k].byteData, keyFrameSeekTime);
}
if (_apeId == ApeConsts.AUDIO_SESSION_ID || _apeId == ApeConsts.VIDEO_SESSION_ID) {
this._emit(MessageTypes.RECORD_PLAYBACK_UPDATE, {
"status": SEEK,
"keyFrameSeekTime": keyFrameSeekTime
});
}
return;*/
}
}
}
}
}
//查找聊天模块ape关键帧数据,聊天模块比较特殊,消息是累积的,当前时间戳之前的都需要显示
_searchChatHistoryMessageKeyfram
(
_apeMessages
)
{
//最多30条数据
...
...
@@ -476,6 +513,156 @@ class RecordPlayBackParse extends Emiter {
}
}
}
// 数据同步处理 regBuffer已经解开的数据,timestamp 时间戳,data原始数据 ApeConsts.VIDEO_SESSION_ID
_pduRegAdapterHandler
(
regBuffer
,
timestamp
,
data
,
sessionId
)
{
console
.
log
(
'RCAdapterPdu--->'
)
let
regPdu
;
let
regItems
;
let
regItemSize
;
try
{
regPdu
=
pdu
[
'RCAdapterPdu'
].
decode
(
regBuffer
);
regItems
=
regPdu
.
item
;
regItemSize
=
regItems
.
length
;
}
catch
(
err
){
console
.
warn
(
'RCAdapterPdu->unpack-error'
,
err
)
return
;
}
//onsole.log('RCAdapterPdu',regPdu)
//loger.log(this._session_name + '数据同步消息');
//loger.log(this._session_name + '数据同步消息.同步条数', regItemSize,"seekTime->",seekTime);
for
(
var
i
=
0
;
i
<
regItemSize
;
++
i
)
{
let
regItem
=
regItems
[
i
];
let
regItemType
=
regItem
.
type
;
let
regItemData
=
regItem
.
itemData
;
//根据数据包中的type处理数据是否同步
if
(
pdu
.
RCPDU_REG_UPDATE_OBJ
!==
regItemType
)
{
if
(
pdu
.
RCPDU_REG_RESPONSE_OBJ
==
regItemType
)
{
let
regResponsePdu
=
pdu
[
'RCRegistryResponseObjPdu'
].
decode
(
regItemData
);
console
.
log
(
'regResponsePdu'
,
regResponsePdu
)
//this.regResponsePduHandler(regResponsePdu);
}
// 只处理两种类型
continue
;
}
//具体的数据包
let
regUpdatedItem
=
pdu
[
'RCRegistryUpdateObjPdu'
].
decode
(
regItemData
);
let
sub_type
=
regUpdatedItem
.
subType
;
let
object_id
=
regUpdatedItem
.
objId
;
let
user_data
=
regUpdatedItem
.
userData
;
//console.log('RCRegistryUpdateObjPdu',regUpdatedItem)
switch
(
sub_type
)
{
case
pdu
.
RCPDU_REG_ROSTER_INSERT_PDU
:
//let rosterInsertData = pdu['RCRegstryRosterInsertItemPdu'].decode(user_data);
// loger.log('RCPDU_REG_ROSTER_INSERT_PDU---->');
let
rosterInsertData
=
pdu
[
'RCRegistryRosterInsertItemPdu'
].
decode
(
user_data
);
// console.log('RCRegistryRosterInsertItemPdu',rosterInsertData)
let
rosterInsertItems
=
rosterInsertData
.
items
;
let
rosterInsertItemsLen
=
rosterInsertItems
.
length
;
for
(
let
i
=
0
;
i
<
rosterInsertItemsLen
;
++
i
)
{
let
record
=
rosterInsertItems
[
i
];
let
recordId
=
record
.
item_id
;
let
recordData
=
pdu
[
'RCNodeInfoRecordPdu'
].
decode
(
record
.
item_data
);
//this.rosterInsertHandler(recordId, recordData);
//console.log('RCNodeInfoRecordPdu',recordData)
}
break
;
case
pdu
.
RCPDU_REG_ROSTER_DELETE_PDU
:
let
rosterDelData
=
pdu
[
'RCRegistryRosterDeleteItemPdu'
].
decode
(
user_data
);
// console.log('RCRegistryRosterDeleteItemPdu',rosterDelData)
// this.rosterDelHandler(rosterDelData.nodeId);
break
;
case
pdu
.
RCPDU_REG_ROSTER_UPDATE_PDU
:
let
rosterUpdateData
=
pdu
[
'RCRegistryRosterUpdateItemPdu'
].
decode
(
user_data
);
let
rosterUpdateItems
=
rosterUpdateData
.
items
;
let
rosterUpdateItemsLen
=
rosterUpdateItems
.
length
;
//console.log('RCRegistryRosterUpdateItemPdu',rosterUpdateData)
for
(
let
i
=
0
;
i
<
rosterUpdateItemsLen
;
++
i
)
{
let
node
=
rosterUpdateItems
[
i
];
let
nodeId
=
node
.
nodeId
;
let
nodeData
=
pdu
[
'RCNodeInfoRecordPdu'
].
decode
(
node
.
nodeData
);
// console.log('RCNodeInfoRecordPdu',nodeData)
//this.rosterUpdateHandler(nodeId, nodeData);
}
break
;
case
pdu
.
RCPDU_REG_TABLE_INSERT_PDU
:
let
tableInsertData
=
pdu
[
'RCRegistryTableInsertItemPdu'
].
decode
(
user_data
);
let
tableInsertItems
=
tableInsertData
.
items
;
let
tableInsertItemsLen
=
tableInsertItems
.
length
;
//console.log('RCRegistryTableInsertItemPdu',tableInsertData)
for
(
let
i
=
0
;
i
<
tableInsertItemsLen
;
++
i
)
{
let
insertItem
=
tableInsertItems
[
i
];
//loger.log("insertItem",insertItem);
//this.tableInsertHandler(insertItem.owner, insertItem.itemIdx, insertItem.itemData);
}
//文档数据数组内部自己处理数组
//this.tableInsertApeHandler(tableInsertItems);
break
;
case
pdu
.
RCPDU_REG_TABLE_DELETE_PDU
:
let
tableDeleteData
=
pdu
[
'RCRegistryTableDeleteItemPdu'
].
decode
(
user_data
);
//console.log("tableDeleteData",object_id,tableDeleteData);
// console.log('RCRegistryTableDeleteItemPdu',tableDeleteData)
//this.tableDeleteHandler(object_id, tableDeleteData);
break
;
case
pdu
.
RCPDU_REG_TABLE_UPDATE_PDU
:
let
tableUpdateData
=
pdu
[
'RCRegistryTableUpdateItemPdu'
].
decode
(
user_data
);
let
tableUpdateItems
=
tableUpdateData
.
items
;
let
tableUpdateItemsLen
=
tableUpdateItems
.
length
;
//loger.log("RCRegistryTableUpdateItemPdu " + tableUpdateItemsLen);
//loger.log(tableUpdateData);
//console.log('RCRegistryTableUpdateItemPdu',tableUpdateData);
for
(
let
i
=
0
;
i
<
tableUpdateItemsLen
;
++
i
)
{
let
tableItem
=
tableUpdateItems
[
i
];
// this.tableUpdateHandler(tableItem.owner, tableItem.itemIdx, tableItem.itemData,seekTime);
if
(
sessionId
==
ApeConsts
.
VIDEO_SESSION_ID
){
try
{
let
videoChannelInfo
=
pdu
[
'RCVideoChannelInfoPdu'
].
decode
(
tableItem
.
itemData
);
loger
.
log
(
'RCVideoChannelInfoPdu->timestamp'
,
timestamp
,
videoChannelInfo
);
//储存音视频模块的数据
if
(
!
this
.
mediaChannleList
[
videoChannelInfo
.
channelId
]){
this
.
mediaChannleList
[
videoChannelInfo
.
channelId
]
=
{};
}
this
.
mediaChannleList
[
videoChannelInfo
.
channelId
][
timestamp
]
=
{
parseData
:
videoChannelInfo
,
byteData
:
data
,
timestamp
:
timestamp
};
}
catch
(
err
)
{
loger
.
log
(
"RCVideoChannelInfoPdu->unPackPdu error,itemIdx="
+
tableItem
.
itemIdx
+
" err:"
+
err
.
message
);
}
}
else
if
(
sessionId
==
ApeConsts
.
AUDIO_SESSION_ID
){
try
{
let
audioChannelInfo
=
pdu
[
'RCAudioChannelInfoPdu'
].
decode
(
tableItem
.
itemData
);
loger
.
log
(
'RCAudioChannelInfoPdu->timestamp'
,
timestamp
,
audioChannelInfo
);
//储存音视频模块的数据
if
(
!
this
.
mediaChannleList
[
audioChannelInfo
.
channelId
]){
this
.
mediaChannleList
[
audioChannelInfo
.
channelId
]
=
{};
}
this
.
mediaChannleList
[
audioChannelInfo
.
channelId
][
timestamp
]
=
{
parseData
:
audioChannelInfo
,
byteData
:
data
,
timestamp
:
timestamp
};
}
catch
(
err
)
{
loger
.
log
(
"RCAudioChannelInfoPdu->unPackPdu error,itemIdx="
+
tableItem
.
itemIdx
+
" err:"
+
err
.
message
);
}
}
}
break
;
case
pdu
.
RCPDU_REG_QUEUE_UPDATE_PDU
:
case
pdu
.
RCPDU_REG_QUEUE_DELETE_PDU
:
case
pdu
.
RCPDU_REG_QUEUE_INSERT_PDU
:
loger
.
warn
(
'REG QUEUE ARE IGNORED'
);
break
;
}
}
}
}
...
...
src/apes/RecordApe.js
已删除
100644 → 0
查看文件 @
169b80b
// //////////////////////////////////////////////////////////////////////////////
//视频模块
// //////////////////////////////////////////////////////////////////////////////
import
Ape
from
'./Ape'
;
import
ApeConsts
from
'./ApeConsts'
;
import
pdu
from
'pdus'
;
import
Loger
from
'Loger'
;
import
MessageTypes
from
'MessageTypes'
;
import
GlobalConfig
from
'GlobalConfig'
;
import
EngineUtils
from
'EngineUtils'
;
import
MediaModule
from
"./MediaModule"
;
let
loger
=
Loger
.
getLoger
(
'RecordApe'
);
class
RecordApe
extends
Ape
{
constructor
()
{
super
(
ApeConsts
.
VIDEO_SESSION_ID
,
ApeConsts
.
VIDEO_SESSION_NAME
,
ApeConsts
.
VIDEO_SESSION_TAG
);
this
.
mediaModule
=
new
MediaModule
();
this
.
mediaModule
.
MEDIA_OBJ_TABLE_ID
=
ApeConsts
.
VIDEO_OBJ_TABLE_ID
;
this
.
mediaModule
.
mediaChannels
=
{};
this
.
mediaModule
.
mediaType
=
ApeConsts
.
MEDIA_TYPE_VIDEO
;
// Ape Models
this
.
registerKey
(
this
.
_session_id
,
this
.
_session_name
,
this
.
_session_tag
,
new
ArrayBuffer
);
this
.
registerObj
(
pdu
.
RCPDU_REG_REGISTER_TABLE
,
ApeConsts
.
VIDEO_OBJ_TABLE_ID
,
ApeConsts
.
VIDEO_OBJ_TABLE_NAME
,
ApeConsts
.
VIDEO_OBJ_TABLE_TAG
,
0
,
new
ArrayBuffer
);
// videoApe 监听视频控制消息,用户之间的消息传递
this
.
on
(
pdu
.
RCPDU_SEND_VIDEO_DATA_REQUEST
,
this
.
receiveVideoCommandHandler
.
bind
(
this
));
}
//ape加入成功
onJoinChannelHandlerSuccess
(){
//这个设置很重要,因为只有Sass流程完成之后,APE才能取得GlobalConfig中的数据
this
.
mediaModule
.
maxMediaChannel
=
GlobalConfig
.
maxVideoChannels
;
}
/////////////发送数据操作////////////////////////////////////////////
//获取播流地址
getPlayVideoPath
(
_param
)
{
loger
.
log
(
'getPlayVideoPath'
);
return
this
.
mediaModule
.
getMediaPlayPath
(
_param
);
}
//获取推流地址
getPublishVideoPath
(
_param
)
{
loger
.
log
(
'获取推流地址->'
);
if
(
!
this
.
mcu
.
connected
){
loger
.
warn
(
GlobalConfig
.
getCurrentStatus
());
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"已经断开连接"
};
}
//监课比较特殊,不占用课堂内的音视频路数,额外创建
if
(
GlobalConfig
.
userRole
==
ApeConsts
.
invisible
){
let
result
=
this
.
mediaModule
.
getMediaPublishPathForInVisible
(
_param
);
//this._emit( MessageTypes.VIDEO_GET_PUBLISH_PATH,result);
return
result
;
}
//非监课的身份,需要判断是否可以继续推流
//需要判断当前已经使用的流路数
let
openChannel
=
0
;
let
allChannels
=
MediaModule
.
allMediaChannelsList
;
for
(
let
i
in
allChannels
){
let
channel
=
allChannels
[
i
];
if
(
channel
&&
channel
.
status
==
ApeConsts
.
CHANNEL_STATUS_OPENING
){
openChannel
++
;
}
}
//如果已经开启的数量大于等于最大允许开启的数量,不允许再推流
if
(
openChannel
>=
GlobalConfig
.
maxMediaChannels
){
loger
.
warn
(
'不能再打开设备->当前开启的设备数量->'
,
openChannel
);
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"不能再打开设备,当前开启的设备数量"
};
}
let
result
=
this
.
mediaModule
.
getMediaPublishPath
(
_param
);
//this._emit( MessageTypes.VIDEO_GET_PUBLISH_PATH,result);
return
result
;
}
//获取当前所有频道信息
getAllChannelInfo
(
_param
){
loger
.
log
(
'获取当前所有频道信息->'
);
return
this
.
mediaModule
.
getAllMediaChannelInfo
();
}
//推流
publishVideo
(
_param
)
{
if
(
!
this
.
mcu
.
connected
){
loger
.
warn
(
GlobalConfig
.
getCurrentStatus
());
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_FAILED, "data":"已经断开连接!","mediaId":0});
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"已经断开连接"
};
}
if
(
_param
==
null
||
_param
.
publishUrl
==
null
)
{
loger
.
warn
(
'推流->参数错误'
,
_param
);
//this._emit(MessageTypes.MCU_ERROR, MessageTypes.ERR_APE_INTERFACE_PARAM_WRONG);
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_FAILED, "data":"参数错误!","mediaId":0});
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"参数错误"
};
}
//根据推流的地址获取对应的频道信息
let
needPublishChannelInfo
=
this
.
mediaModule
.
getNeedPublishMediaChannel
(
_param
.
publishUrl
);
if
(
needPublishChannelInfo
==
null
){
loger
.
warn
(
'推流->推流数据已经无效'
,
_param
);
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_FAILED, "data":"推流数据已经无效!","mediaId":0});
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"推流数据已经无效"
};
}
//判断当前是否还有空闲的channle
let
freeChannel
=
this
.
mediaModule
.
getFreeMediaChannel
();
if
(
freeChannel
==
0
)
{
loger
.
warn
(
"推流->不能再打开更多的设备 "
);
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_FAILED, "data":"不能再打开更多的设备!","mediaId":0});
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"不能再打开更多的设备"
,
"mediaChannels"
:
this
.
mediaModule
.
mediaChannels
};
}
//判断当前的频道是否已经占用
if
(
this
.
mediaModule
.
checkChannelIsOpening
(
needPublishChannelInfo
.
channelId
)){
if
(
needPublishChannelInfo
.
nodeId
==
GlobalConfig
.
nodeId
){
loger
.
warn
(
needPublishChannelInfo
.
channelId
,
"已经推送过消息,不需要再次推送"
);
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_SUCCESS, "data":"已经推送过消息,不需要再次推送!","mediaId":needPublishChannelInfo.channelId});
return
{
"code"
:
ApeConsts
.
RETURN_SUCCESS
,
"data"
:
"已经推送过消息,不需要再次推送!"
,
"mediaId"
:
needPublishChannelInfo
.
channelId
};
}
else
{
loger
.
warn
(
needPublishChannelInfo
.
channelId
,
"频道已经被占用"
);
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_FAILED, "data":"频道已经被占用!","mediaId":0});
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"频道已经被占用!"
,
"mediaChannels"
:
this
.
mediaModule
.
mediaChannels
};
}
}
let
channelInfo
=
this
.
mediaModule
.
getDefaultChannelInfo
();
channelInfo
.
owner
=
GlobalConfig
.
nodeId
;
channelInfo
.
status
=
ApeConsts
.
CHANNEL_STATUS_OPENING
;
channelInfo
.
channelId
=
needPublishChannelInfo
.
channelId
;
channelInfo
.
streamId
=
needPublishChannelInfo
.
streamId
;
//按规则拼接的流名称
channelInfo
.
timestamp
=
needPublishChannelInfo
.
timestamp
;
//时间戳
channelInfo
.
mediaType
=
ApeConsts
.
MEDIA_TYPE_VIDEO
;
this
.
sendTableUpdateHandler
(
channelInfo
);
//this._emit( MessageTypes.VIDEO_PUBLISH_RESULT,{"code": ApeConsts.RETURN_SUCCESS, "data":"推流成功!","mediaId":needPublishChannelInfo.channelId});
return
{
"code"
:
ApeConsts
.
RETURN_SUCCESS
,
"data"
:
"推流成功!"
,
"mediaId"
:
needPublishChannelInfo
.
channelId
};
}
//停止推流,
stopPublishVideo
(
_param
)
{
loger
.
log
(
'停止推流->'
,
_param
);
if
(
!
this
.
mcu
.
connected
){
loger
.
warn
(
GlobalConfig
.
getCurrentStatus
());
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"已经断开连接"
};
}
//默认为自己的nodeId,_param如果为空,那么默认就是当前自己的nodeId,否则用_param
let
nodeId
=
GlobalConfig
.
nodeId
;
if
(
_param
&&
parseInt
(
_param
.
nodeId
)
>
0
){
nodeId
=
parseInt
(
_param
.
nodeId
);
}
//默认为0,如果releaseChannelId 存在就释放releaseChannelId通道
let
releaseChannelId
=
0
;
if
(
_param
&&
parseInt
(
_param
.
mediaId
)
>
0
){
releaseChannelId
=
parseInt
(
_param
.
mediaId
);
}
//释放channelId 的占用
if
(
releaseChannelId
>
0
){
//第一种情况,释放nodeId占用的指定mediaId (channelId)
this
.
_releaseChannelForNodeId
(
nodeId
,
releaseChannelId
);
}
else
{
//第二种情况,释放nodeId占用的所有channelId
this
.
_releaseNodeIdAllChannel
(
nodeId
);
}
}
//释放nodeId占用的指定的channelId频道
_releaseChannelForNodeId
(
nodeId
,
channelId
){
loger
.
log
(
nodeId
,
"_releaseChannelForNodeId-->channelId"
,
channelId
);
let
channelInfo
=
this
.
mediaModule
.
mediaChannels
[
channelId
];
if
(
channelInfo
&&
channelInfo
.
status
==
ApeConsts
.
CHANNEL_STATUS_OPENING
){
if
(
channelInfo
.
fromNodeId
==
nodeId
){
let
channelInfo
=
this
.
mediaModule
.
getDefaultChannelInfo
();
channelInfo
.
status
=
ApeConsts
.
CHANNEL_STATUS_RELEASED
;
channelInfo
.
channelId
=
channelId
;
this
.
sendTableUpdateHandler
(
channelInfo
);
}
else
{
loger
.
warn
(
channelId
,
"不属于nodeId"
,
nodeId
,
"不能释放"
,
channelInfo
);
}
}
else
{
loger
.
warn
(
nodeId
,
"要释放的channel不存在或者已经释放-->channelId"
,
channelInfo
);
}
}
//释放nodeId占用的所有频道
_releaseNodeIdAllChannel
(
nodeId
){
loger
.
log
(
nodeId
,
"_releaseNodeIdAllChannel"
,
this
.
mcu
.
connected
);
if
(
!
this
.
mcu
.
connected
){
clearTimeout
(
this
.
releaseTimeId
);
loger
.
warn
(
GlobalConfig
.
getCurrentStatus
());
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"已经断开连接"
};
}
let
openingChannel
=
this
.
mediaModule
.
getOpeningMediaChannel
(
nodeId
);
if
(
openingChannel
==
0
)
{
loger
.
warn
(
nodeId
,
"没有占用channel不需要处理"
);
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"没有占用channel不需要处理"
};
}
let
channelInfo
=
this
.
mediaModule
.
getDefaultChannelInfo
();
channelInfo
.
status
=
ApeConsts
.
CHANNEL_STATUS_RELEASED
;
channelInfo
.
channelId
=
openingChannel
;
this
.
sendTableUpdateHandler
(
channelInfo
);
//递归检查,800毫秒之后执行
this
.
releaseTimeId
=
setTimeout
(
function
(){
loger
.
warn
(
nodeId
,
"检查频道是否占用"
);
this
.
_releaseNodeIdAllChannel
(
nodeId
);
}.
bind
(
this
),
800
);
}
sendVideoBroadcastMsg
(
_param
)
{
if
(
!
this
.
mcu
.
connected
){
loger
.
warn
(
GlobalConfig
.
getCurrentStatus
());
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"已经断开连接"
};
}
if
(
this
.
_classInfo
===
null
||
EngineUtils
.
isEmptyObject
(
this
.
_classInfo
))
{
loger
.
log
(
'不能发送Video消息.McuClient还未初始化数据!'
);
if
(
GlobalConfig
.
getCurrentStatus
().
code
==
0
||
GlobalConfig
.
getCurrentStatus
().
code
==
1
)
{
//this._emit(MessageTypes.MCU_ERROR, MessageTypes.ERR_APE_SEND_FAILED_NO_JOIN);
return
{
"code"
:
1
,
"data"
:
"不能发送Video消息.McuClient还未初始化数据"
};
}
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"不能发送Video消息.McuClient还未初始化数据"
};
}
if
(
_param
==
null
)
{
loger
.
warn
(
'sendVideoCommandMsg失败,参数错误'
,
_param
);
//this._emit(MessageTypes.MCU_ERROR, MessageTypes.ERR_APE_INTERFACE_PARAM_WRONG);
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"sendVideoCommandMsg失败,参数错误"
};
}
// to, message
loger
.
log
(
'发送Video消息.'
,
_param
);
if
(
_param
.
actionType
!=
null
&&
_param
.
actionType
==
ApeConsts
.
MEDIA_ACTION_OPEN_CAMERA
)
{
//判断当前开启的视频数量是否已经是最大值,如果已经是最大值,不能再开启
let
freeChannel
=
this
.
mediaModule
.
getFreeMediaChannel
();
if
(
freeChannel
==
0
)
{
loger
.
warn
(
'sendVideoCommandMsg,不能再打开更多的设备'
,
_param
);
return
{
"code"
:
ApeConsts
.
RETURN_FAILED
,
"data"
:
"不能再打开更多的设备"
,
"mediaChannels"
:
this
.
mediaModule
.
mediaChannels
};
}
}
let
videoSendPdu
=
new
pdu
[
'RCVideoSendDataRequestPdu'
];
videoSendPdu
.
type
=
pdu
.
RCPDU_SEND_VIDEO_DATA_REQUEST
;
videoSendPdu
.
isPublic
=
true
;
videoSendPdu
.
fromNodeId
=
GlobalConfig
.
nodeId
;
//发起人
videoSendPdu
.
toNodeId
=
parseInt
(
_param
.
toNodeId
)
||
0
;
//接收者,0就是所有人
videoSendPdu
.
actionType
=
parseInt
(
_param
.
actionType
)
||
ApeConsts
.
MEDIA_ACTION_DEFAULT
;
let
dataStr
=
''
;
try
{
dataStr
=
JSON
.
stringify
(
_param
.
data
);
}
catch
(
err
){
loger
.
warn
(
'控制消息->JSON转换失败'
);
dataStr
=
_param
.
data
;
}
videoSendPdu
.
data
=
this
.
_rCArrayBufferUtil
.
strToUint8Array
(
"h5"
+
dataStr
);
//开头两个字会乱码
if
(
!
videoSendPdu
.
isPublic
&&
0
!=
videoSendPdu
.
toNodeId
)
{
//发送给制定的人
//loger.log('发送私聊Video消息.');
this
.
send
(
videoSendPdu
);
}
else
{
//发送给所有人
//loger.log('发送公聊Video消息.');
this
.
sendChatUniform
(
videoSendPdu
);
}
return
{
"code"
:
ApeConsts
.
RETURN_SUCCESS
,
"data"
:
""
};
}
//发送到mcu同步(更新数据)
sendTableUpdateHandler
(
_channelInfo
)
{
loger
.
log
(
"video===sendTableUpdateHandler "
);
let
updateModelPdu
=
this
.
packPdu
(
_channelInfo
,
_channelInfo
.
channelId
);
//let updateModelPdu=this.packPdu({},ApeConsts.VIDEO_OBJ_TABLE_ID+2);
if
(
updateModelPdu
==
null
){
loger
.
warn
(
"sendTableUpdateHandler error,updateModelPdu=null"
);
return
;
}
let
tableItemPdu
=
new
pdu
[
'RCRegistryTableItemPdu'
];
tableItemPdu
.
itemIdx
=
_channelInfo
.
channelId
;
//tableItemPdu.itemIdx=ApeConsts.VIDEO_OBJ_TABLE_ID+2;
tableItemPdu
.
owner
=
_channelInfo
.
owner
;
//0收到flash的是这个值,MCU做了了用户掉线处理,30秒之后会清理owner为0
tableItemPdu
.
itemData
=
updateModelPdu
.
toArrayBuffer
();
//insert
let
tableInsertItemPdu
=
new
pdu
[
'RCRegistryTableUpdateItemPdu'
];
tableInsertItemPdu
.
type
=
pdu
.
RCPDU_REG_TABLE_UPDATE_PDU
;
//
tableInsertItemPdu
.
items
.
push
(
tableItemPdu
);
let
updateObjPdu
=
new
pdu
[
'RCRegistryUpdateObjPdu'
];
updateObjPdu
.
objId
=
ApeConsts
.
VIDEO_OBJ_TABLE_ID
;
//
updateObjPdu
.
subType
=
tableInsertItemPdu
.
type
;
updateObjPdu
.
userData
=
tableInsertItemPdu
.
toArrayBuffer
();
//同步
let
adapterItemPdu
=
new
pdu
[
'RCAdapterItemPdu'
];
adapterItemPdu
.
type
=
pdu
.
RCPDU_REG_UPDATE_OBJ
;
adapterItemPdu
.
itemData
=
updateObjPdu
.
toArrayBuffer
();
let
adapterPdu
=
new
pdu
[
'RCAdapterPdu'
];
adapterPdu
.
type
=
pdu
.
RCPDU_REG_ADAPTER
;
adapterPdu
.
item
.
push
(
adapterItemPdu
);
loger
.
log
(
"发送更新VIDEO.itemIdx="
+
tableItemPdu
.
itemIdx
);
this
.
sendUniform
(
adapterPdu
,
true
);
}
/////收到消息处理//////////////////////////////////////////////////
// 视频消息处理,内部处理,不需要告诉应用层
receiveVideoCommandHandler
(
_data
)
{
let
videoReceivePdu
=
pdu
[
'RCVideoSendDataRequestPdu'
].
decode
(
_data
);
if
(
videoReceivePdu
==
null
)
{
loger
.
warn
(
"视频控制消息处理,收到的消息为null,不做处理"
);
return
;
}
videoReceivePdu
.
data
=
this
.
_rCArrayBufferUtil
.
uint8ArrayToStr
(
videoReceivePdu
.
data
,
2
);
//开头两个字会乱码
let
dataObj
=
{};
try
{
dataObj
=
JSON
.
parse
(
videoReceivePdu
.
data
);
}
catch
(
err
){
loger
.
warn
(
'控制消息->JSON转换失败'
);
dataObj
=
videoReceivePdu
.
data
;
}
videoReceivePdu
.
data
=
dataObj
;
//判断接收者的id,如果不是0,并且也不是自己的nodeId,那么消息不做处理
if
(
videoReceivePdu
.
toNodeId
!=
0
&&
videoReceivePdu
.
toNodeId
!=
GlobalConfig
.
nodeId
)
{
loger
.
log
(
'视频消息不处理 toNodeId='
,
videoReceivePdu
.
toNodeId
,
"my nodeId="
,
GlobalConfig
.
nodeId
);
}
else
{
loger
.
log
(
'视频控制消息处理 .'
,
videoReceivePdu
);
//this._emit(MessageTypes.VIDEO_BROADCAST, videoReceivePdu);
}
}
tableUpdateHandler
(
owner
,
itemIdx
,
itemData
,
seek
)
{
// debugger;
let
unpackChannelInfo
=
this
.
unPackPdu
(
owner
,
itemIdx
,
itemData
);
loger
.
log
(
"tableUpdateHandler->channel"
,
itemIdx
,
'status->'
,
unpackChannelInfo
.
status
,
"seek->"
,
seek
);
//****很重要********
//如果owner的值为0,代表的是这个歌频道已经被释放了(mcu服务端对于占用channel的掉线用户,就是把owner设置为0)
if
(
owner
==
0
){
loger
.
log
(
"释放占用的频道,channel"
,
itemIdx
);
unpackChannelInfo
.
status
=
ApeConsts
.
CHANNEL_STATUS_RELEASED
;
unpackChannelInfo
.
streamId
=
""
;
}
this
.
mediaModule
.
mediaChannels
[
itemIdx
]
=
unpackChannelInfo
;
if
(
unpackChannelInfo
&&
unpackChannelInfo
.
fromNodeId
!=
GlobalConfig
.
nodeId
){
let
receiveChannelInfo
=
{};
receiveChannelInfo
.
mediaId
=
unpackChannelInfo
.
channelId
;
receiveChannelInfo
.
fromNodeId
=
unpackChannelInfo
.
fromNodeId
;
receiveChannelInfo
.
userName
=
unpackChannelInfo
.
userName
||
""
;
receiveChannelInfo
.
userRole
=
unpackChannelInfo
.
userRole
||
ApeConsts
.
normal
;
//消息不是自己同步的,需要处理
if
(
unpackChannelInfo
.
status
==
ApeConsts
.
CHANNEL_STATUS_OPENING
){
//正在推流
receiveChannelInfo
.
m3u8Url
=
""
;
receiveChannelInfo
.
rtmpUrl
=
""
;
receiveChannelInfo
.
replay
=
""
;
receiveChannelInfo
.
seek
=
seek
||
0
;
//这个是录制回放时使用的seek
let
m3u8Stream
=
this
.
mediaModule
.
getMediaPlayPath
({
"type"
:
"m3u8"
,
"streamId"
:
unpackChannelInfo
.
streamId
});
let
rtmpStream
=
this
.
mediaModule
.
getMediaPlayPath
({
"type"
:
"rtmp"
,
"streamId"
:
unpackChannelInfo
.
streamId
});
let
replay
=
this
.
mediaModule
.
getMediaRecordPlaybackPath
({
"type"
:
"m3u8"
,
"streamId"
:
unpackChannelInfo
.
streamId
});
if
(
m3u8Stream
.
code
==
0
){
receiveChannelInfo
.
m3u8Url
=
m3u8Stream
.
playUrl
;
}
if
(
rtmpStream
.
code
==
0
){
receiveChannelInfo
.
rtmpUrl
=
rtmpStream
.
playUrl
;
}
if
(
replay
.
code
==
0
){
receiveChannelInfo
.
replay
=
replay
.
playUrl
;
}
loger
.
log
(
"VIDEO_PLAY"
,
receiveChannelInfo
);
//广播播放视频的消息
//this._emit(MessageTypes.VIDEO_PLAY, receiveChannelInfo);
}
else
{
loger
.
log
(
"VIDEO_STOP"
,
receiveChannelInfo
);
//流已经停止
//this._emit(MessageTypes.VIDEO_STOP, receiveChannelInfo);
}
}
else
{
loger
.
warn
(
"视频消息是自己发送的或者是视频消息无效,不需要处理,消息内容如下:"
);
loger
.
log
(
unpackChannelInfo
);
if
(
unpackChannelInfo
.
status
==
ApeConsts
.
CHANNEL_STATUS_OPENING
){
GlobalConfig
.
openCamera
=
EngineUtils
.
creatTimestamp
();
GlobalConfig
.
openMicrophones
=
GlobalConfig
.
openCamera
;
}
else
{
GlobalConfig
.
openCamera
=
0
;
GlobalConfig
.
openMicrophones
=
0
;
}
//this._emit(MessageTypes.USER_DEVICE_STATUS_CHAANGE,{
// nodeId:GlobalConfig.nodeId,
// userRole:GlobalConfig.userRole,
// userName:GlobalConfig.userName,
// userId:GlobalConfig.userId,
// openCamera:GlobalConfig.openCamera,
// openMicrophones:GlobalConfig.openMicrophones
// });
}
MediaModule
.
allMediaChannelsList
[
itemIdx
]
=
unpackChannelInfo
;
console
.
log
(
'MediaModule.allMediaChannelsList'
,
MediaModule
.
allMediaChannelsList
);
//this._emit(MessageTypes.VIDEO_UPDATE, unpackChannelInfo);
}
///////数据的封包和解包/////////////////////////////////////////
packPdu
(
_param
,
_itemIdx
)
{
loger
.
log
(
"packPdu "
);
//验证坐标点集合数组是否合法
if
(
_param
==
null
||
_itemIdx
==
null
)
{
//this._emit(MessageTypes.MCU_ERROR, MessageTypes.ERR_APE_INTERFACE_PARAM_WRONG);
return
null
;
}
//判断type类型,根据type设置不同的参数
let
packPduModel
=
new
pdu
[
'RCVideoChannelInfoPdu'
];
packPduModel
.
status
=
_param
.
status
||
ApeConsts
.
CHANNEL_STATUS_RELEASED
;
packPduModel
.
channelId
=
_itemIdx
;
packPduModel
.
streamId
=
_param
.
streamId
||
""
;
packPduModel
.
siteId
=
_param
.
siteId
||
GlobalConfig
.
siteId
;
//GlobalConfig.siteId;
packPduModel
.
classId
=
parseInt
(
_param
.
classId
)
||
parseInt
(
GlobalConfig
.
classId
);
packPduModel
.
userId
=
_param
.
userId
||
"0"
;
packPduModel
.
mediaType
=
_param
.
mediaType
||
ApeConsts
.
MEDIA_TYPE_VIDEO
;
packPduModel
.
timestamp
=
_param
.
timestamp
||
0
;
packPduModel
.
fromNodeId
=
GlobalConfig
.
nodeId
;
packPduModel
.
userName
=
GlobalConfig
.
userName
||
""
;
packPduModel
.
toNodeId
=
0
;
packPduModel
.
userRole
=
GlobalConfig
.
userRole
||
ApeConsts
.
normal
;
loger
.
log
(
packPduModel
);
return
packPduModel
;
}
unPackPdu
(
owner
,
itemIdx
,
itemData
)
{
loger
.
log
(
"unPackPdu->owner:"
,
owner
,
"itemIdx->"
,
itemIdx
);
if
(
owner
==
null
||
itemIdx
==
null
||
itemData
==
null
)
{
//this._emit(MessageTypes.MCU_ERROR, MessageTypes.ERR_APE_INTERFACE_PARAM_WRONG);
return
null
;
}
try
{
let
videoChannelInfo
=
pdu
[
'RCVideoChannelInfoPdu'
].
decode
(
itemData
);
loger
.
log
(
videoChannelInfo
);
return
videoChannelInfo
;
}
catch
(
err
)
{
loger
.
log
(
"unPackPdu error,itemIdx="
+
itemIdx
+
" err:"
+
err
.
message
);
}
return
null
;
}
}
export
default
RecordApe
;
请
注册
或
登录
后发表评论