目錄
二、.Binary messages not supported
現在有兩種方式:
1).awrtc.js連接java服務端,實現kurento-player-js的功能。
2).kurento-player-js加入awrtc.js裏面的獲取圖片的功能,並對接unity。
先以用awrtc.js連接kurento播放視頻爲目標,因爲awrtc.js和unity的對接已經是做好的了,只要能夠播放視頻,後續的unity內的代碼不用做調整。
awrtc.js是AssetStore裏面的插件中的js部分改造後的。
awrtc相關博客:WebGL實時視頻(5) awrtc.js理解並修改,WebGL實時視頻(6) Unity裏面顯示視頻
awrtc本身就是一個完整的webrtc的客戶端js庫,可以連接自己的服務端,改造後可以連接H5Stream,這次則是要連接kurento的java服務端,這些服務端從概念上講都是信令服務器。
一、測試入口
在複製原來的func_CAPI_H5Stream_GetVideo的基礎上修改出一個func_CAPI_Kurento_GetVideo。
function func_CAPI_Kurento_GetVideo(rtsp,serverUrl) {//[kurento]
console.log("func_CAPI_Kurento_GetVideo",rtsp,serverUrl);
BrowserMediaStream.DEBUG_SHOW_ELEMENTS=true;//在網頁中顯示視頻
var netConf=new NetworkConfig();
netConf.SignalingUrl=serverUrl;
//...
}
二、.Binary messages not supported
serverUrl傳入ws://192.168.1.150:8444/player,其他不變,嘗試連接,結果:
awrtc.js:3171 Websocket closed with code: 1003 Binary messages not supported
原因是出在服務端的PlayerHandler的父類TextWebSocketHandler裏面有
而客戶端這邊,則是在SendVersion那裏用InternalSend發送了個Uint8Array。
因爲客戶端還有其他地方(心跳包)會發送Array,修改服務端,支持BinaryMessage就好了,但是也不用做什麼處理,就是可以接收就行。在PlayerHandler中添加handleBinaryMessage
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
log.info(">> PlayerHandler.handleBinaryMessage");
//...不做處理
}
三、發送start指令
SendVersion等二進制消息不影響後,服務端能夠收到消息了。
原來的H5Stream是否通過call發送一個open指令過去的
var address="{\"type\": \"open\"}";//json字符串,unity傳遞過來的是字符串。不能用單引號。
//必須是這個格式,不然h5stream不會發送後續消息,onmessage也就進不去。
browserCall.Call(address);//=>Connect
而從kurento的客戶端(參考kurento-player-js)和服務端代碼來看,是先發送一個{id:"start",videourl:...,sdpOffer:...},然後開始整個過程的。
1.使用KurentoUtils(不行)
而在kurento-player-js的index.js裏面sdpOffer是通過
webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,
function(error) {
if (error)
return console.error(error);
webRtcPeer.generateOffer(onOffer);
});
這裏的webRtcPeer.generateOffer的回調函數onOffer獲取的,試着把這部分相關代碼拿過來,並把獲取的offerSdp組織到start指令中發送過去。
var mode="video and audio";
var video = document.getElementById('video1');
intWebRtcPeer(mode,video,function(sdp){
var msg={id:"start",videourl:rtsp,sdpOffer:sdp};
browserCall.Call(msg);
});
function intWebRtcPeer(mode,video,callback) {
console.log('Creating WebRtcPeer in ' + mode + ' mode and generating local sdp offer ...');
// Video and audio by default
var userMediaConstraints = {
audio : true,
video : true
}
if (mode == 'video-only') {
userMediaConstraints.audio = false;
} else if (mode == 'audio-only') {
userMediaConstraints.video = false;
}
var options = {
remoteVideo : video,
mediaConstraints : userMediaConstraints,
onicecandidate : onIceCandidate
}
console.info('User media constraints' + userMediaConstraints);
var webRtcPeer = new kurentoUtils.WebRtcPeer.WebRtcPeerRecvonly(options,
function(error) {
if (error)
return console.error(error);
webRtcPeer.generateOffer(function(error, offerSdp){
if(!error){
if(callback!=null){
callback(offerSdp);
}
}
});
});
}
服務端收到後能夠進入start處理,但是後續接不上。
進行下去會出現錯誤:
awrtc.js:3171 DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection': Failed to set remote answer sdp: Called in wrong state: kStable
原因出在,這裏進行下去的處理過程是用原來的awrtc.js的代碼,但是kurento的index.js裏面是用了webRtcPeer的processAnswer部分
function startResponse(message) {
setState(I_CAN_STOP);
console.log('SDP answer received from server. Processing ...');
webRtcPeer.processAnswer(message.sdpAnswer, function(error) {
if (error)
return console.error(error);
});
}
processAnswer內部其實就是setRemoteDescription。
generateOffer和processAnswer是對應的,generateOffer裏面其實就是
pc.createOffer(createOfferOnSuccess, callback, constraints);
而awrtc.js裏面也有相關的createOffer,
InnerAWebRtcPeer.prototype.CreateOffer = function() {
var rtcPeer = this;
console.log("InnerAWebRtcPeer.prototype.CreateOffer",this.mOfferOptions);
var offerPro = this.mPeer.createOffer(this.mOfferOptions);
offerPro.then(function(answer) {
var json = JSON.stringify(answer),
promise = rtcPeer.mPeer.setLocalDescription(answer);
promise.then(function() {
rtcPeer.RtcSetSignalingStarted();
rtcPeer.EnqueueOutgoing(json);
}),
promise.
catch(function(error) {
Debug.LogError(error),
rtcPeer.RtcSetSignalingFailed()
})
}),
offerPro.
catch(function(t) {
Debug.LogError(t),
rtcPeer.RtcSetSignalingFailed()
})
},
也就是說應該想辦法從這裏開始的。
2.使用awrtc的StartSignaling(可行)
CreateOffer是StartSignaling調用的,StartSignaling是UpdateSignalingNetwork裏面判斷的NewConnection分支中調用的
else if (netEvent.Type == NetEventType.NewConnection) {
console.error("InnerWebRtcNetwork.prototype.UpdateSignalingNetwork",netEvent,this.mInSignaling);
var t=this.mInSignaling[netEvent.ConnectionId.id];
if(t){
t.StartSignaling();
}
else{
this.AddIncomingConnection(netEvent.ConnectionId);
}
}
UpdateSignalingNetwork其實是處理OnWebsocketOnMessage中的消息的。
以前連接H5Stream是發送一條{type:open}指令給服務器,然後服務器返回信息,然後開始的。
按照這個思路,經過模式,修改後過程如下。
客戶端發送一個{id:connect}指令
var address={id:"connect"};//對象
browserCall.Call(address);//=>Connect
服務端handleTextMessage裏面處理connect指令,返回一個結果:
try {
switch (id) {
case "connect"://[awrtc]
sendMessage(session,"{\"type\":6,\"id\":1,\"data\":\"1\"}");
break;
這裏的type:6就是上面的NetEventType.NewConnection。
然後客戶端在OnWebsocketOnMessage中增加一條路線處理。
InnerWebsocketNetwork.prototype.OnWebsocketOnMessage = function(e) {
console.log(">>>>>>> OnWebsocketOnMessage",e);
if (this.mStatus != WebsocketConnectionStatus.Disconnecting && this.mStatus != WebsocketConnectionStatus.NotConnected) {
if(e.data instanceof ArrayBuffer){ //原來的路線
var t = new Uint8Array(e.data);
this.ParseMessage(t)
}
else{ //新增的路線,處理h5stream或者kurento的webrtc的信息
var dataObj = JSON.parse(e.data);
console.log(">>>>>>> OnWebsocketOnMessage [H5Stream]",dataObj);
if(dataObj.type && dataObj.id){ //[kurento] 服務端是基於kurento-java修改的
console.log(">>>>>>> OnWebsocketOnMessage [NEW NetworkEvent]",dataObj);
var evnt=new NetworkEvent(dataObj.type,{id:dataObj.id},dataObj.data);
this.HandleIncomingEvent(evnt);
}
else{ //[H5Stream]
//將offer和remoteice作爲NetEventType.ReliableMessageReceived信息處理.
var evnt=new NetworkEvent(NetEventType.ReliableMessageReceived,this.mLastConnectionId,e.data);
//在InnerWebsocketNetwork.prototype.Connect時記錄下this.mLastConnectionId.
this.HandleIncomingEvent(evnt);
}
}
}
},
這裏的 var evnt=new NetworkEvent(dataObj.type,{id:dataObj.id},dataObj.data); 處理好後,就會進入StartSignaling,然後CreateOffer。
不過這裏的把sdp發送給服務端部分(SendNetworkEvent),需要修改一下,添加id屬性,不然無法和服務端處理對接起來。
InnerWebsocketNetwork.prototype.SendNetworkEvent = function(networkEvent) {
/*
InnerWebsocketNetwork.SendNetworkEvent @ awrtc.js:4300
InnerWebsocketNetwork.HandleOutgoingEvents @ awrtc.js:4280
InnerWebsocketNetwork.Flush @ awrtc.js:4343
InnerWebRtcNetwork.Flush @ awrtc.js:3846
InnerBrowserMediaNetwork.Flush @ awrtc.js:5982
InnerAWebRtcCall.Update
*/
console.log(">>>>>>> SendNetworkEvent",networkEvent);
//原來的代碼
// var t = NetworkEvent.toByteArray(networkEvent);
// this.InternalSend(t);
//新的代碼[H5Stream]
if(networkEvent.data instanceof Array){
var t = NetworkEvent.toByteArray(networkEvent);
this.InternalSend(t);//原來的分支1:發送NetworkEvent
}
else if(typeof networkEvent.data == 'string'){
try {
var obj=JSON.parse(networkEvent.data);//不是json格式的會拋出異常
if(obj==null){
var t = NetworkEvent.toByteArray(networkEvent);
this.InternalSend(t);//原來的分支2:發送NetworkEvent
}else{
//新的分支1:發送data裏面的內容的json字符串。對應於"{"type":"open"}"的發送。h5stream必須先發送open指令才能收到onmessage消息
this.SendObject(obj);
}
}
catch (ex) {
console.error(ex);
var t = NetworkEvent.toByteArray(networkEvent);
this.InternalSend(t);//原來的分支2:發送NetworkEvent
}
}
else//[H5Stream]或者[kurento]
{
this.SendObject(networkEvent.data);//新的分支2:發送data裏面的內容的json字符串。對應於answer的發送
}
},
InnerWebsocketNetwork.prototype.SendObject = function(data) {
if(data==null){
console.error("InnerWebsocketNetwork.prototype.SendObject data==null");
return;
}
if(!data.id){//[kurento]添加上id
if(data.candidate){
console.log("onIceCandidate",data);
data.id="onIceCandidate";
var temp=JSON.stringify(data);
data.candidate=JSON.parse(temp);//不能直接用 data.candidate=data,會導致JSON序列化出錯的,死循環吧。
}
else if(data.sdp){
console.log("start",data);
data.id="start";
data.videourl="rtsp://iom:[email protected]:554/cam/realmonitor?channel=1&subtype=0";
//data.sdpOffer=data.sdp;
}
console.info("kurento data",data);
}
var json=JSON.stringify(data);
console.log("send json",json,data);
this.mSocket.send(json);
},
這裏還留下一個問題,videourl如何傳到這裏,測試整個過程先寫死了。
這裏的兩個data.id設置對應於服務端的start和onIceCandidate處理。
四、處理接收指令
而服務端處理後發送過來的消息又在HandleIncomingSignaling裏面處理,把index.js裏面的代碼抄過來,改一下startResponse的部分,對接上原來的 this.CreateAnswer(description) : this.RecAnswer(description) 部分。
InnerAWebRtcPeer.prototype.HandleIncomingSignaling = function() {
/*
InnerAWebRtcPeer.HandleIncomingSignaling @ awrtc.js:3464
InnerAWebRtcPeer.Update @ awrtc.js:3457
InnerMediaPeer.Update @ awrtc.js:5694
InnerWebRtcNetwork.CheckSignalingState @ awrtc.js:3878
InnerWebRtcNetwork.Update @ awrtc.js:3823
InnerBrowserMediaNetwork.Update @ awrtc.js:5942
InnerAWebRtcCall.Update
*/
for (; this.mIncomingSignalingQueue.Count() > 0;) {
var data = this.mIncomingSignalingQueue.Dequeue();
console.info('Received message: ' + data);
t = Helper.tryParseInt(data);
if (null != t) {
console.error("InnerWebRtcNetwork.prototype.HandleIncomingSignaling",data,t);
this.mDidSendRandomNumber && (t < this.mRandomNumerSent ? (SLog.L("Signaling negotiation complete. Starting signaling."), this.StartSignaling()) : t == this.mRandomNumerSent ? this.NegotiateSignaling() : SLog.L("Signaling negotiation complete. Waiting for signaling."));
}
else {
var parsedMessage = JSON.parse(data);
if(parsedMessage.id){ //[kurento]
switch (parsedMessage.id) {
case 'startResponse':
//startResponse(parsedMessage);
var answer={
type: 'answer',
sdp: parsedMessage.sdpAnswer
}
//console.error(">>>>>>>>> parsedMessage.sdpAnswer",answer);
var description = new RTCSessionDescription(answer);
console.error(">>>>>>>>> startResponse Answer",description);
"offer" == description.type ? this.CreateAnswer(description) : this.RecAnswer(description);
//this.CreateAnswer(description);
break;
case 'error':
if (state == I_AM_STARTING) {
setState(I_CAN_START);
}
onError('Error message from server: ' + parsedMessage.message);
break;
case 'playEnd':
playEnd();
break;
case 'videoInfo':
//showVideoData(parsedMessage);
// {"id":"videoInfo","isSeekable":false,"initSeekable":0,"endSeekable":0,"videoDuration":0}
break;
case 'iceCandidate':
//這部分不處理也可以
// console.log(">>>>>>>>> parsedMessage",parsedMessage,parsedMessage.candidate);
// var candidate = new RTCIceCandidate(parsedMessage.candidate);
// console.log(">>>>>>>>> candidate",candidate);
// if (null != candidate) {
// var pro = this.mPeer.addIceCandidate(candidate);
// pro.then(function() {}),
// pro.
// catch(function(error) {
// Debug.LogError(error)
// })
// }
break;
case 'seek':
console.log (parsedMessage.message);
break;
case 'position':
document.getElementById("videoPosition").value = parsedMessage.position;
break;
case 'iceCandidate':
break;
default:
if (state == I_AM_STARTING) {
setState(I_CAN_START);
}
onError('Unrecognized message', parsedMessage);
}
}
else{ //原來的
var answer = parsedMessage;
console.log(">>>>>>>>> InnerAWebRtcPeer.prototype.HandleIncomingSignaling",answer);
if (answer.sdp) {
var description = new RTCSessionDescription(answer);
console.log(">>>>>>>>> Answer",description);
"offer" == description.type ? this.CreateAnswer(description) : this.RecAnswer(description)
} else {
var candidate = new RTCIceCandidate(answer);
console.log(">>>>>>>>> addIceCandidate",candidate);
if (null != candidate) {
var pro = this.mPeer.addIceCandidate(candidate);
pro.then(function() {}),
pro.
catch(function(error) {
Debug.LogError(error)
})
}
}
}
}
}
},
然後................................就好了,可以播放視頻了。
其實我對整個webrtc視頻連接的過程的理解是懵懵懂懂的,大概知道幾個關鍵步驟,具體細節的話有些地方還是不懂,如果自己寫一個原型代碼的話會更加深理解。
//todo:寫個js連接websocket的原型代碼
這個修改過程相當於把兩個有一定兼容性的機器連接起來,這裏的連接的依據就是雙方是基於webrtc的規則來的。把客戶端(awrtc)和服務端(kurento-player)的相關處理過程修改一下,把兩者接起來。
五、暫停等指令接口
服務端提供了幾個控制接口
其實就是發送響應的id,客戶端這邊封裝一下
InnerBrowserWebRtcCall.prototype.SendObject = function(obj) {
console.log(">>>>>>>>>>>>>>>>>> BrowserWebRtcCall.SendObject",obj);
this.mNetwork.mSignalingNetwork.SendObject(obj);
},
InnerBrowserWebRtcCall.prototype.Stop = function() {//[kurento]
this.SendObject({id:"stop"});
this.DisposeInternal();
},
InnerBrowserWebRtcCall.prototype.Resume = function() {//[kurento]
this.SendObject({id:"resume"});
},
InnerBrowserWebRtcCall.prototype.Pause = function() {//[kurento]
this.SendObject({id:"pause"});
},
$('#btnStop').click(function(){
call.Stop();
});
$('#btnPause').click(function(){
call.Pause();
});
$('#btnResume').click(function(){
call.Resume();
});
六、Unity播放
前面的js代碼的修改,都是爲了放到unity裏面。
把awrtc.js代碼拷貝到awrtc.jspre,在awrtc_unity.jslib裏面加上想要的接口
Unity_Kurento_GetVideo: function(a,b)
{
var serverUrl=Pointer_stringify(a);
var videoUrl=Pointer_stringify(b);
console.log("------- Unity_Kurento_GetVideo",serverUrl,videoUrl);
return awrtc.CAPI_Kurento_GetVideo(serverUrl,videoUrl);
},
Unity裏面則是
[DllImport("__Internal")]
public static extern InitState Unity_Kurento_GetVideo(string serverUrl,string videoUrl);
前面加了個VideoUrl參數,Unity裏面需要相應調整。
public class NetworkConfigEx : NetworkConfig
{
public string VideoUrl { get; set; }
}
WebRtcVideo.CreateNetworkConfig:
NetworkConfigEx netConfig = new NetworkConfigEx();
if (string.IsNullOrEmpty(uIceServer) == false)
netConfig.IceServers.Add(new IceServer(uIceServer, uIceServerUser, uIceServerPassword));
if (string.IsNullOrEmpty(uIceServer2) == false)
netConfig.IceServers.Add(new IceServer(uIceServer2));
uSignalingUrl = mUi.InputUrl.text;
videoUrl = mUi.VideoUrls.options[mUi.VideoUrls.value].text;
Debug.Log("uSignalingUrl:"+ uSignalingUrl);
Debug.Log("videoUrl:" + videoUrl);
netConfig.SignalingUrl = uSignalingUrl;
netConfig.VideoUrl = videoUrl;
BrowserMediaNetwork:
string conf = "{\"IceServers\":" + iceServersJson.ToString() + ", \"SignalingUrl\":\"" + signalingUrl + "\", \"IsConference\":\"" + false + "\"}";
if (lNetConfig is NetworkConfigEx)
{
NetworkConfigEx ex = lNetConfig as NetworkConfigEx;
conf = "{\"IceServers\":" + iceServersJson.ToString() +
", \"SignalingUrl\":\"" + signalingUrl +
"\", \"VideoUrl\":\"" + ex.VideoUrl +
"\", \"IsConference\":\"" + false + "\"}";
}
SLog.L("Creating BrowserMediaNetwork config: " + conf, this.GetType().Name);
mReference = CAPI.Unity_MediaNetwork_Create(conf);
界面上再加上一個videoUrl的輸入框,打包webgl測試,可以播放。
七、Unity幀率問題(視頻分辨率)
發現播放一會,幀率就變成了2-4,同時在打印信息中發現視頻分辨率有幾次變化,最後變成1920*1080了。
公司有兩個攝像頭一個是1920*1080,一個是1280*720,低分辨率播放視頻時的幀率在45-50,可能是分辨率問題。
另外發現,js裏面的核心代碼context.drawImage()實際上是可以修改分辨率的,參考:前端JS利用canvas的drawImage()對圖片進行壓縮
原本是mVideoElement.videoWidth的,改成用mVideoElement.width,使用video的大小來獲取圖片。
InnerBrowserMediaStream.prototype.CreateFrame = function() {
// console.log("InnerBrowserMediaStream.prototype.CreateFrame",
// this.mVideoElement.videoWidth,this.mVideoElement.videoHeight,
// this.mVideoElement.width,this.mVideoElement.height);
//var width=this.mVideoElement.videoWidth;
//var height=this.mVideoElement.videoHeight;
var width=this.mVideoElement.width;
var height=this.mVideoElement.height;
this.mCanvasElement.width = width;
this.mCanvasElement.height = height;
var context = this.mCanvasElement.getContext("2d");
context.clearRect(0, 0, this.mCanvasElement.width, this.mCanvasElement.height);
context.drawImage(this.mVideoElement, 0, 0,width,height);
try {
var data = context.getImageData(0, 0, this.mCanvasElement.width, this.mCanvasElement.height).data,
array = new Uint8Array(data.buffer);
return new RawFrame(array, this.mCanvasElement.width, this.mCanvasElement.height)
} catch(error) { (array = new Uint8Array(this.mCanvasElement.width * this.mCanvasElement.height * 4)).fill(255, 0, array.length - 1);
var frame = new RawFrame(array, this.mCanvasElement.width, this.mCanvasElement.height);
return SLog.LogWarning("Firefox workaround: Refused access to the remote video buffer. Retrying next frame..."),
this.DestroyCanvas(),
this.mCanvasElement = this.SetupCanvas(),
frame
}
},
在傳入配置信息中,加上分辨率的設置。
var config={
SignalingUrl:"ws://192.168.1.150:8444/player",
VideoUrl:"rtsp://iom:[email protected]:554/cam/realmonitor?channel=1&subtype=0",
VideoWidth:tmp[0],
VideoHeight:tmp[1]
};
console.log('config',config);
call=awrtc.CAPI_Kurento_GetVideo(JSON.stringify(config));
function func_CAPI_Kurento_GetVideo(configJson) {//[kurento]
console.log("func_CAPI_Kurento_GetVideo",configJson);
var config=JSON.parse(configJson);
console.log("config",config);
BrowserMediaStream.DEBUG_SHOW_ELEMENTS=true;//在網頁中顯示視頻
AWebRtcPeer.SourceType="kurento";//原本用這個區分代碼的
var browserCall=new BrowserWebRtcCall(config);
然後就可以在播放時設置分辨率了。
--------------------------------------------------------------------
測試了幾種分辨率,發現改成4:3的分辨率時獲取到的圖片和Video裏面的視頻不一樣,拉伸了一些,16:9的則是一樣的,這裏可能需要處理一下。
普屏4:3 320*240 640*480
寬屏16:9 480*272 640*360 672*378 720*480 1024*600 1280*720 1920*1080
現在相當於默認都要拉伸的,以後需要的話可以增加不同的方式。
--------------------------------------------------------------------
關鍵的Unity測試結果,從結論上講,確實是受分辨率影響的,在1280*720以下的分辨率的幀率還能接受(30-60),再清晰一些則幀率下降到1-10了,不能接受。但是這裏有個前提是這裏的刷新時用FixedUpdate,0.02s一次。改成用InvokeRepeating的方式,0.1s一次,則就算是1920*1080的分辨率也播放,幀率20-40,還能接受。而1920*1080,0.2s一次,則可以達到40-50。
總之不能用FixedUpdata,沒必要,看監控視頻不是玩遊戲,不需要0.02s刷新一次。0.1-0.2就可以了。
實際上應該根據UI界面的大小和視頻本身分辨率,界面大小小於視頻分辨率的話,使用界面的分辨率,大於的話使用視頻本身的分辨率。
根據界面UI大小設置視頻分辨率:
if (Config.resolution == "AutoUI")
{
//Config.resolution = mUi.uNoCameraTexture.width + "*" + mUi.uNoCameraTexture.height;
RectTransform rectT = GetComponent<RectTransform>();
var rect = rectT.rect;
//ShowHtmlElement.ShowElement(rect,"video1");
Config.resolution = (int)rect.width + "*" + (int)rect.height;
Debug.Log("AutoUI:" + Config.resolution);
}
-------------------------------------------------------------------
八、播放多個攝像頭視頻
整理了一下代碼,原來的代碼裏面UI和控制代碼在一起的,實際上UI部分只有一個RawImage是必要的。
直接將代碼整理一下,和RawImage一起做成一個預設。再複製一下,修改複製後的攝像頭地址。
打包webgl,測試。可以。
當然幀率還是受分辨率影響。
九、顯示Video(並設置位置)
前面的在Unity裏面播放WebRtc的方式的核心是,用一個隱藏的(Html5的)Video來播放WebRtc,通過和js交互,獲取Video的圖片,並不斷刷新。相比於直接只是Video播放多了獲取和顯示在Unity中的兩步,一定程度上會影響Unity內的幀率,還有視頻的質量。不過這種的優點是效果上和Unity完美結合,通過Material還能再三維物體表面播放視頻。
但是對於不需要再三維物體上顯示的需求來說,在Unity裏面也只是在一個界面上顯示視頻而已,直接可以把Video顯示出來,並調整位置,放到Unity前面,達到看起來和Unity界面重疊一致的效果。
相關參考資料:UnityWebGL調研(7) 修改打包模板,UnityWebGL調研(5) 和網頁交互
十、js核心原型代碼