yjz-web-sdk 1.0.8-beta.14 → 1.0.8-beta.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,6 @@ export declare class WebRTCClient extends EventEmitter {
23
23
  private rafId;
24
24
  private currentMedia;
25
25
  private fileImage?;
26
- private ua;
27
26
  constructor(config: WebRTCConfig);
28
27
  startPush(useBackCamera?: boolean): Promise<void>;
29
28
  handleOffer(offerSdp: string): void;
@@ -43,7 +42,7 @@ export declare class WebRTCClient extends EventEmitter {
43
42
  private configDataChannel;
44
43
  private handleDataChannelMessage;
45
44
  private checkStats;
46
- private processStats;
45
+ private processStatsOptimized;
47
46
  private getConnectionType;
48
47
  /** 获取或创建 video 元素(懒初始化) */
49
48
  private getFileVideo;
@@ -3568,6 +3568,7 @@ class FileTypeUtils {
3568
3568
  }
3569
3569
  }
3570
3570
  class WebRTCClient extends EventEmitter {
3571
+ // private isFireFox: boolean = false;
3571
3572
  constructor(config) {
3572
3573
  super();
3573
3574
  __publicField(this, "config");
@@ -3591,7 +3592,6 @@ class WebRTCClient extends EventEmitter {
3591
3592
  __publicField(this, "rafId", 0);
3592
3593
  __publicField(this, "currentMedia", null);
3593
3594
  __publicField(this, "fileImage");
3594
- __publicField(this, "ua", "");
3595
3595
  __publicField(this, "startPushLocal", async (file) => {
3596
3596
  var _a;
3597
3597
  if (this.isPushingLocalStream || !file) return;
@@ -3624,7 +3624,6 @@ class WebRTCClient extends EventEmitter {
3624
3624
  }
3625
3625
  });
3626
3626
  this.config = config;
3627
- this.ua = navigator.userAgent;
3628
3627
  }
3629
3628
  async startPush(useBackCamera = true) {
3630
3629
  if (this.isPushingLocalStream) {
@@ -3771,7 +3770,7 @@ class WebRTCClient extends EventEmitter {
3771
3770
  const rotatedStream = await this.getRotatedStream(this.localStream, useBackCamera);
3772
3771
  this.rotatedStream = rotatedStream;
3773
3772
  rotatedStream.getTracks().forEach((track) => {
3774
- track.contentHint = "detail";
3773
+ track.contentHint = "motion";
3775
3774
  const sender = this.peerConnection.addTrack(track, rotatedStream);
3776
3775
  senders.push(sender);
3777
3776
  });
@@ -3844,15 +3843,26 @@ class WebRTCClient extends EventEmitter {
3844
3843
  }
3845
3844
  async getRotatedStream(localStream, useBackCamera) {
3846
3845
  const canvas = document.createElement("canvas");
3846
+ canvas.width = 640;
3847
+ canvas.height = 480;
3848
+ canvas.style.position = "absolute";
3849
+ canvas.style.top = "-9999px";
3850
+ canvas.style.left = "-9999px";
3851
+ canvas.style.width = "1px";
3852
+ canvas.style.height = "1px";
3853
+ canvas.style.opacity = "0";
3854
+ document.body.appendChild(canvas);
3847
3855
  const ctx = canvas.getContext("2d");
3848
3856
  const video = document.createElement("video");
3849
3857
  video.srcObject = localStream;
3850
3858
  video.muted = true;
3859
+ video.playsInline = true;
3860
+ video.autoplay = true;
3861
+ video.setAttribute("playsinline", "true");
3862
+ video.setAttribute("webkit-playsinline", "true");
3851
3863
  await video.play();
3852
3864
  const width = video.videoWidth;
3853
3865
  const height = video.videoHeight;
3854
- canvas.width = 640;
3855
- canvas.height = 480;
3856
3866
  const drawFrame = () => {
3857
3867
  ctx.clearRect(0, 0, canvas.width, canvas.height);
3858
3868
  ctx.save();
@@ -3876,7 +3886,10 @@ class WebRTCClient extends EventEmitter {
3876
3886
  rafId = requestAnimationFrame(drawLoop);
3877
3887
  const stream = canvas.captureStream(fps);
3878
3888
  stream.getTracks().forEach((track) => {
3879
- const stopLoop = () => cancelAnimationFrame(rafId);
3889
+ const stopLoop = () => {
3890
+ cancelAnimationFrame(rafId);
3891
+ canvas.remove();
3892
+ };
3880
3893
  track.addEventListener("ended", stopLoop);
3881
3894
  track.addEventListener("stop", stopLoop);
3882
3895
  });
@@ -3993,13 +4006,19 @@ class WebRTCClient extends EventEmitter {
3993
4006
  }
3994
4007
  }
3995
4008
  checkStats() {
3996
- this.statsTimer = setInterval(() => this.processStats(), 1e3);
3997
- }
3998
- processStats() {
3999
- if (!this.peerConnection) {
4000
- return;
4001
- }
4002
- this.peerConnection.getStats(null).then((statsReport) => {
4009
+ this.statsTimer = setInterval(() => this.processStatsOptimized(), 1e3);
4010
+ }
4011
+ async processStatsOptimized() {
4012
+ const pc = this.peerConnection;
4013
+ if (!pc) return;
4014
+ const ua = navigator.userAgent;
4015
+ const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua);
4016
+ const isMobile = /iPhone|iPad|Android/i.test(ua);
4017
+ const minInterval = isSafari || isMobile ? 3e3 : 1e3;
4018
+ const now = Date.now();
4019
+ if (this.lastReportTime && now - this.lastReportTime < minInterval) return;
4020
+ try {
4021
+ const statsReport = await pc.getStats(null);
4003
4022
  let videoFps = 0;
4004
4023
  let totalDecodeTime = 0;
4005
4024
  let roundTripTime = 0;
@@ -4007,22 +4026,19 @@ class WebRTCClient extends EventEmitter {
4007
4026
  let packetsReceived = 0;
4008
4027
  let bytesReceived = 0;
4009
4028
  let framesDecoded = 0;
4010
- let pliCount = 0;
4011
4029
  let framesReceived = 0;
4030
+ let pliCount = 0;
4012
4031
  let connectionType = "";
4013
- const currentTime = Date.now();
4014
- statsReport.forEach((report) => {
4015
- if (report.type === "inbound-rtp") {
4032
+ for (const report of statsReport.values()) {
4033
+ if (report.type === "inbound-rtp" && report.kind === "video") {
4016
4034
  bytesReceived += report.bytesReceived || 0;
4017
4035
  packetsLost += report.packetsLost || 0;
4018
4036
  packetsReceived += report.packetsReceived || 0;
4019
- if (report.kind === "video") {
4020
- videoFps = report.framesPerSecond || 0;
4021
- totalDecodeTime = report.totalDecodeTime || 0;
4022
- framesDecoded = report.framesDecoded || 0;
4023
- framesReceived = report.framesReceived || 0;
4024
- pliCount = (report.pliCount || 0) + (report.firCount || 0);
4025
- }
4037
+ videoFps = report.framesPerSecond || 0;
4038
+ totalDecodeTime = report.totalDecodeTime || 0;
4039
+ framesDecoded = report.framesDecoded || 0;
4040
+ framesReceived = report.framesReceived || 0;
4041
+ pliCount = (report.pliCount || 0) + (report.firCount || 0);
4026
4042
  }
4027
4043
  if (report.type === "candidate-pair" && report.state === "succeeded") {
4028
4044
  roundTripTime = typeof report.currentRoundTripTime !== "undefined" ? report.currentRoundTripTime : report.responsesReceived > 0 ? report.totalRoundTripTime / report.responsesReceived : 0;
@@ -4032,28 +4048,30 @@ class WebRTCClient extends EventEmitter {
4032
4048
  connectionType = this.getConnectionType(local, remote);
4033
4049
  }
4034
4050
  }
4035
- });
4036
- if (!this.ua.includes("Firefox")) {
4037
- roundTripTime = Math.floor((roundTripTime || 0) * 1e3);
4038
4051
  }
4039
- const deltaTime = currentTime - this.lastReportTime;
4040
- const bytesDiff = bytesReceived - this.lastBytesReceived;
4052
+ if (roundTripTime > 1e3) {
4053
+ roundTripTime = Math.floor(roundTripTime / 1e3);
4054
+ } else if (roundTripTime < 1) {
4055
+ roundTripTime = Math.floor(roundTripTime * 1e3);
4056
+ }
4057
+ const deltaTime = now - (this.lastReportTime || now);
4058
+ const bytesDiff = bytesReceived - (this.lastBytesReceived || 0);
4041
4059
  const bytesPerSecond = deltaTime > 0 ? bytesDiff * 1e3 / deltaTime : 0;
4042
4060
  const kilobytesPerSecond = bytesPerSecond / 1024;
4043
4061
  const megabytesPerSecond = kilobytesPerSecond / 1024;
4044
4062
  this.lastBytesReceived = bytesReceived;
4045
- this.lastReportTime = currentTime;
4063
+ this.lastReportTime = now;
4064
+ const lostPackets = packetsLost - (this.lastPacketsLost || 0);
4065
+ const receivedPackets = packetsReceived - (this.lastPacketsReceived || 0);
4046
4066
  let lossRate = 0;
4047
- const lostPackets = packetsLost - this.lastPacketsLost;
4048
- const receivedPackets = packetsReceived - this.lastPacketsReceived;
4049
4067
  if (lostPackets > 0 && receivedPackets > 0) {
4050
4068
  lossRate = lostPackets / (lostPackets + receivedPackets);
4051
4069
  this.maxLostRate = Math.max(this.maxLostRate || 0, lossRate);
4052
- this.lostPacketCount++;
4070
+ this.lostPacketCount = (this.lostPacketCount || 0) + 1;
4053
4071
  }
4054
4072
  this.lastPacketsLost = packetsLost;
4055
4073
  this.lastPacketsReceived = packetsReceived;
4056
- const fps = typeof videoFps !== "undefined" ? videoFps : framesDecoded - this.lastSecondDecodedCount;
4074
+ const fps = videoFps || framesDecoded - (this.lastSecondDecodedCount || 0);
4057
4075
  this.lastSecondDecodedCount = framesDecoded;
4058
4076
  const bitrate = kilobytesPerSecond < 1024 ? `${Math.floor(kilobytesPerSecond)} KB/s` : `${Math.floor(megabytesPerSecond)} MB/s`;
4059
4077
  const averageDecodeTime = framesDecoded > 0 ? Math.floor(1e4 * totalDecodeTime / framesDecoded) : 0;
@@ -4062,7 +4080,6 @@ class WebRTCClient extends EventEmitter {
4062
4080
  framesPerSecond: fps,
4063
4081
  currentRoundTripTime: roundTripTime,
4064
4082
  lostRate: Math.floor(lossRate * 100),
4065
- // percent
4066
4083
  bitrate,
4067
4084
  pliCount,
4068
4085
  averageDecodeTime,
@@ -4070,9 +4087,9 @@ class WebRTCClient extends EventEmitter {
4070
4087
  framesReceived
4071
4088
  };
4072
4089
  this.emit(EmitType.statisticInfo, screenInfo);
4073
- }).catch((error) => {
4090
+ } catch (error) {
4074
4091
  this.emit(EmitType.webrtcError, createWebRtcError(FailCode.STREAM_STATE, error));
4075
- });
4092
+ }
4076
4093
  }
4077
4094
  getConnectionType(local, remote) {
4078
4095
  if (local.candidateType === "host" && remote.candidateType === "host") return "直连";
@@ -66,10 +66,12 @@ sdp:Re};switch(i.browser){case"chrome":if(!q||!H||!t.shimChrome)return n("Chrome
66
66
  * @param data 清晰度数据
67
67
  */static clarity(e){return new He("ActionClarity",this.formatData(e))}static switchAudio(e){return new He("ActionCommandEvent",this.formatData(e))}static changeSender(e){return new He("ActionTrack",this.formatData(e))}}class $e{
68
68
  /** 读取文件头并判断类型 */
69
- static async detectFileType(e){const t=await e.slice(0,16).arrayBuffer(),n=new Uint8Array(t);return 255===n[0]&&216===n[1]&&255===n[2]||137===n[0]&&80===n[1]&&78===n[2]&&71===n[3]||71===n[0]&&73===n[1]&&70===n[2]&&56===n[3]||82===n[0]&&73===n[1]&&70===n[2]&&70===n[3]&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image":"ftyp"===String.fromCharCode(...n.slice(4,8))||26===n[0]&&69===n[1]&&223===n[2]&&163===n[3]||79===n[0]&&103===n[1]&&103===n[2]&&83===n[3]?"video":"unknown"}}class qe extends d{constructor(e){super(),i(this,"config"),i(this,"peerConnection",null),i(this,"localStream",null),i(this,"rotatedStream",null),i(this,"isPushingStream",!1),i(this,"isPushingLocalStream",!1),i(this,"dataChannel",null),i(this,"statsTimer"),i(this,"lastReportTime",0),i(this,"lastBytesReceived",0),i(this,"lastPacketsLost",0),i(this,"lastPacketsReceived",0),i(this,"lostPacketCount",0),i(this,"maxLostRate",0),i(this,"lastSecondDecodedCount",0),i(this,"fileVideo"),i(this,"canvas"),i(this,"canvasStream",null),i(this,"rafId",0),i(this,"currentMedia",null),i(this,"fileImage"),i(this,"ua",""),i(this,"startPushLocal",async e=>{var t;if(!this.isPushingLocalStream&&e)try{await this.loadMedia(e),this.startCanvasStream();const n=[];null==(t=this.canvasStream)||t.getTracks().forEach(e=>{e.contentHint="detail";const t=this.peerConnection.addTrack(e,this.canvasStream);n.push(t)}),n.forEach(e=>this.setVideoParams(e)),await xe(this.peerConnection,e=>{this.emit(y.sendOffer,e)}),this.isPushingLocalStream=!0}catch(n){let e;this.isPushingLocalStream=!1,e=n instanceof Error?n.message:String(n),this.emit(y.cameraError,C(m.LOCAL_STREAM_FAIL,e))}}),this.config=e,this.ua=navigator.userAgent}async startPush(e=!0){this.isPushingLocalStream&&this.stopLocal();try{this.isPushingStream=!0,await this.readyCapture(e)}catch(t){let e;this.isPushingStream=!1,e=t instanceof Error?t.message:String(t),this.emit(y.cameraError,C(m.CAMERA_STREAM_FAIL,e))}}handleOffer(e){this.resetPeerConnection(),((e,t,n,i)=>{w.info("信息日志:","设置远程offer Description=======>");const r=new RTCSessionDescription({type:"offer",sdp:t});e.setRemoteDescription(r).then(()=>Me(e,n,i)).catch(e=>null==i?void 0:i(g(u.HANDLE_OFFER,e)))})(this.peerConnection,e,e=>{this.emit(y.sendAnswer,e)},e=>this.emit(y.webrtcError,e))}handleAnswer(e){((e,t,n)=>{const i=new RTCSessionDescription({type:"answer",sdp:t});e.setRemoteDescription(i).catch(e=>null==n?void 0:n(g(u.REMOTE_DES,e)))})(this.peerConnection,e??"",e=>this.emit(y.webrtcError,e))}handleIceCandidate(e){((e,t,n)=>{w.info("信息日志:","接收远程ice 并设置=======>"),e.addIceCandidate(t).catch(e=>null==n?void 0:n(g(u.HANDLE_ICE,e)))})(this.peerConnection,e,e=>this.emit(y.webrtcError,e))}sendChannelData(e,t){var n;try{let i=null;switch(e){case je.ClickData:i=He.click(t);break;case je.ClipboardData:i=He.clipboard(t);break;case je.ActionInput:i=He.input(t,this.config.myId);break;case je.ActionChinese:i=He.chinese(t);break;case je.ActionRequestCloudDeviceInfo:i=He.requestCloudDeviceInfo();break;case je.ActionClarity:i=He.clarity(t);break;case je.ActionWheel:i=He.wheel(t);break;case je.ActionGesture:i=He.gesture(t);break;case je.ActionCommand:i=He.action(t);break;case je.ActionCommandEvent:i=He.switchAudio(t);break;case je.ActionTrack:i=He.changeSender(t)}if(i){const e=JSON.stringify(i),t=(new TextEncoder).encode(e).buffer;null==(n=this.dataChannel)||n.send(t),i=null}}catch(i){this.emit(y.webrtcError,g(u.DATACHANNEL_ERR,i))}}closeConnection(){w.info("信息日志:","关闭webrtc连接=======>"),this.statsTimer&&(clearInterval(this.statsTimer),this.statsTimer=void 0),this.stopPush(),this.stopLocal(),this.peerConnection&&("function"==typeof this.peerConnection.getSenders&&this.peerConnection.getSenders&&this.peerConnection.getSenders().forEach(e=>{var t,n;e.track&&(null==(n=(t=e.track).stop)||n.call(t))}),"function"==typeof this.peerConnection.getReceivers&&this.peerConnection.getReceivers&&this.peerConnection.getReceivers().forEach(e=>{var t,n;e.track&&(null==(n=(t=e.track).stop)||n.call(t))}),this.peerConnection.getTransceivers&&this.peerConnection.getTransceivers().forEach(e=>{var t;null==(t=e.stop)||t.call(e)}),this.removeAllListeners(),this.dataChannel=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.ondatachannel=null,this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onsignalingstatechange=null,this.peerConnection.onnegotiationneeded=null,this.peerConnection.onicegatheringstatechange=null,this.peerConnection.close(),this.peerConnection=null)}async readyCapture(e){this.resetPeerConnection(),this.stopPush(),w.info("信息日志:","启用摄像头推流到云机=======>",e);try{this.localStream=await navigator.mediaDevices.getUserMedia({video:{width:{ideal:640},height:{ideal:480},facingMode:e?{ideal:"environment"}:{ideal:"user"},frameRate:{ideal:20,max:30}},audio:!0});const t=[],n=await this.getRotatedStream(this.localStream,e);this.rotatedStream=n,n.getTracks().forEach(e=>{e.contentHint="detail";const i=this.peerConnection.addTrack(e,n);t.push(i)}),await xe(this.peerConnection,e=>{this.emit(y.sendOffer,e)}),t.forEach(e=>this.setVideoParams(e))}catch(t){if(t instanceof DOMException)switch(t.name){case"NotAllowedError":throw new Error("用户拒绝了摄像头或麦克风权限");case"NotFoundError":throw new Error("未找到摄像头或麦克风设备");case"OverconstrainedError":throw new Error("设备无法满足指定约束");case"NotReadableError":throw new Error("设备忙或无法访问");default:throw new Error(`其他错误: ${t.message}`)}throw new Error(`mediaDevices 异常: ${t}`)}}
69
+ static async detectFileType(e){const t=await e.slice(0,16).arrayBuffer(),n=new Uint8Array(t);return 255===n[0]&&216===n[1]&&255===n[2]||137===n[0]&&80===n[1]&&78===n[2]&&71===n[3]||71===n[0]&&73===n[1]&&70===n[2]&&56===n[3]||82===n[0]&&73===n[1]&&70===n[2]&&70===n[3]&&87===n[8]&&69===n[9]&&66===n[10]&&80===n[11]?"image":"ftyp"===String.fromCharCode(...n.slice(4,8))||26===n[0]&&69===n[1]&&223===n[2]&&163===n[3]||79===n[0]&&103===n[1]&&103===n[2]&&83===n[3]?"video":"unknown"}}class qe extends d{
70
+ // private isFireFox: boolean = false;
71
+ constructor(e){super(),i(this,"config"),i(this,"peerConnection",null),i(this,"localStream",null),i(this,"rotatedStream",null),i(this,"isPushingStream",!1),i(this,"isPushingLocalStream",!1),i(this,"dataChannel",null),i(this,"statsTimer"),i(this,"lastReportTime",0),i(this,"lastBytesReceived",0),i(this,"lastPacketsLost",0),i(this,"lastPacketsReceived",0),i(this,"lostPacketCount",0),i(this,"maxLostRate",0),i(this,"lastSecondDecodedCount",0),i(this,"fileVideo"),i(this,"canvas"),i(this,"canvasStream",null),i(this,"rafId",0),i(this,"currentMedia",null),i(this,"fileImage"),i(this,"startPushLocal",async e=>{var t;if(!this.isPushingLocalStream&&e)try{await this.loadMedia(e),this.startCanvasStream();const n=[];null==(t=this.canvasStream)||t.getTracks().forEach(e=>{e.contentHint="detail";const t=this.peerConnection.addTrack(e,this.canvasStream);n.push(t)}),n.forEach(e=>this.setVideoParams(e)),await xe(this.peerConnection,e=>{this.emit(y.sendOffer,e)}),this.isPushingLocalStream=!0}catch(n){let e;this.isPushingLocalStream=!1,e=n instanceof Error?n.message:String(n),this.emit(y.cameraError,C(m.LOCAL_STREAM_FAIL,e))}}),this.config=e}async startPush(e=!0){this.isPushingLocalStream&&this.stopLocal();try{this.isPushingStream=!0,await this.readyCapture(e)}catch(t){let e;this.isPushingStream=!1,e=t instanceof Error?t.message:String(t),this.emit(y.cameraError,C(m.CAMERA_STREAM_FAIL,e))}}handleOffer(e){this.resetPeerConnection(),((e,t,n,i)=>{w.info("信息日志:","设置远程offer Description=======>");const r=new RTCSessionDescription({type:"offer",sdp:t});e.setRemoteDescription(r).then(()=>Me(e,n,i)).catch(e=>null==i?void 0:i(g(u.HANDLE_OFFER,e)))})(this.peerConnection,e,e=>{this.emit(y.sendAnswer,e)},e=>this.emit(y.webrtcError,e))}handleAnswer(e){((e,t,n)=>{const i=new RTCSessionDescription({type:"answer",sdp:t});e.setRemoteDescription(i).catch(e=>null==n?void 0:n(g(u.REMOTE_DES,e)))})(this.peerConnection,e??"",e=>this.emit(y.webrtcError,e))}handleIceCandidate(e){((e,t,n)=>{w.info("信息日志:","接收远程ice 并设置=======>"),e.addIceCandidate(t).catch(e=>null==n?void 0:n(g(u.HANDLE_ICE,e)))})(this.peerConnection,e,e=>this.emit(y.webrtcError,e))}sendChannelData(e,t){var n;try{let i=null;switch(e){case je.ClickData:i=He.click(t);break;case je.ClipboardData:i=He.clipboard(t);break;case je.ActionInput:i=He.input(t,this.config.myId);break;case je.ActionChinese:i=He.chinese(t);break;case je.ActionRequestCloudDeviceInfo:i=He.requestCloudDeviceInfo();break;case je.ActionClarity:i=He.clarity(t);break;case je.ActionWheel:i=He.wheel(t);break;case je.ActionGesture:i=He.gesture(t);break;case je.ActionCommand:i=He.action(t);break;case je.ActionCommandEvent:i=He.switchAudio(t);break;case je.ActionTrack:i=He.changeSender(t)}if(i){const e=JSON.stringify(i),t=(new TextEncoder).encode(e).buffer;null==(n=this.dataChannel)||n.send(t),i=null}}catch(i){this.emit(y.webrtcError,g(u.DATACHANNEL_ERR,i))}}closeConnection(){w.info("信息日志:","关闭webrtc连接=======>"),this.statsTimer&&(clearInterval(this.statsTimer),this.statsTimer=void 0),this.stopPush(),this.stopLocal(),this.peerConnection&&("function"==typeof this.peerConnection.getSenders&&this.peerConnection.getSenders&&this.peerConnection.getSenders().forEach(e=>{var t,n;e.track&&(null==(n=(t=e.track).stop)||n.call(t))}),"function"==typeof this.peerConnection.getReceivers&&this.peerConnection.getReceivers&&this.peerConnection.getReceivers().forEach(e=>{var t,n;e.track&&(null==(n=(t=e.track).stop)||n.call(t))}),this.peerConnection.getTransceivers&&this.peerConnection.getTransceivers().forEach(e=>{var t;null==(t=e.stop)||t.call(e)}),this.removeAllListeners(),this.dataChannel=null,this.peerConnection.onicecandidate=null,this.peerConnection.ontrack=null,this.peerConnection.ondatachannel=null,this.peerConnection.onconnectionstatechange=null,this.peerConnection.oniceconnectionstatechange=null,this.peerConnection.onsignalingstatechange=null,this.peerConnection.onnegotiationneeded=null,this.peerConnection.onicegatheringstatechange=null,this.peerConnection.close(),this.peerConnection=null)}async readyCapture(e){this.resetPeerConnection(),this.stopPush(),w.info("信息日志:","启用摄像头推流到云机=======>",e);try{this.localStream=await navigator.mediaDevices.getUserMedia({video:{width:{ideal:640},height:{ideal:480},facingMode:e?{ideal:"environment"}:{ideal:"user"},frameRate:{ideal:20,max:30}},audio:!0});const t=[],n=await this.getRotatedStream(this.localStream,e);this.rotatedStream=n,n.getTracks().forEach(e=>{e.contentHint="motion";const i=this.peerConnection.addTrack(e,n);t.push(i)}),await xe(this.peerConnection,e=>{this.emit(y.sendOffer,e)}),t.forEach(e=>this.setVideoParams(e))}catch(t){if(t instanceof DOMException)switch(t.name){case"NotAllowedError":throw new Error("用户拒绝了摄像头或麦克风权限");case"NotFoundError":throw new Error("未找到摄像头或麦克风设备");case"OverconstrainedError":throw new Error("设备无法满足指定约束");case"NotReadableError":throw new Error("设备忙或无法访问");default:throw new Error(`其他错误: ${t.message}`)}throw new Error(`mediaDevices 异常: ${t}`)}}
70
72
  /**
71
73
  * ✅ 切换前后摄像头(无缝)
72
- */async switchCamera(e){var t;try{w.info("信息日志:","切换摄像头 => "+(e?"后置":"前置")),this.isPushingStream=!0,this.localStream=await navigator.mediaDevices.getUserMedia({video:{width:{ideal:640},height:{ideal:480},facingMode:e?{ideal:"environment"}:{ideal:"user"},frameRate:{ideal:20,max:30}},audio:!1});const n=await this.getRotatedStream(this.localStream,e);this.rotatedStream=n;const i=this.rotatedStream.getVideoTracks()[0],r=null==(t=this.peerConnection)?void 0:t.getSenders().find(e=>e.track&&"video"===e.track.kind);r&&await r.replaceTrack(i)}catch(n){if(this.isPushingStream=!1,n instanceof DOMException)switch(n.name){case"NotAllowedError":throw new Error("用户拒绝了摄像头或麦克风权限");case"NotFoundError":throw new Error("未找到摄像头或麦克风设备");case"OverconstrainedError":throw new Error("设备无法满足指定约束");case"NotReadableError":throw new Error("设备忙或无法访问");default:throw new Error(`其他错误: ${n.message}`)}throw new Error(`mediaDevices 异常: ${n}`)}}async getRotatedStream(e,t){const n=document.createElement("canvas"),i=n.getContext("2d"),r=document.createElement("video");r.srcObject=e,r.muted=!0,await r.play();const o=r.videoWidth,s=r.videoHeight;n.width=640,n.height=480;let a,c=0;const d=e=>{e-c>=40&&((()=>{i.clearRect(0,0,n.width,n.height),i.save(),i.translate(n.width/2,n.height/2);const e=t?-Math.PI/2:Math.PI/2;i.rotate(e),i.drawImage(r,-o/2,-s/2,o,s),i.restore()})(),c=e),a=requestAnimationFrame(d)};a=requestAnimationFrame(d);const l=n.captureStream(25);return l.getTracks().forEach(e=>{const t=()=>cancelAnimationFrame(a);e.addEventListener("ended",t),e.addEventListener("stop",t)}),l}
74
+ */async switchCamera(e){var t;try{w.info("信息日志:","切换摄像头 => "+(e?"后置":"前置")),this.isPushingStream=!0,this.localStream=await navigator.mediaDevices.getUserMedia({video:{width:{ideal:640},height:{ideal:480},facingMode:e?{ideal:"environment"}:{ideal:"user"},frameRate:{ideal:20,max:30}},audio:!1});const n=await this.getRotatedStream(this.localStream,e);this.rotatedStream=n;const i=this.rotatedStream.getVideoTracks()[0],r=null==(t=this.peerConnection)?void 0:t.getSenders().find(e=>e.track&&"video"===e.track.kind);r&&await r.replaceTrack(i)}catch(n){if(this.isPushingStream=!1,n instanceof DOMException)switch(n.name){case"NotAllowedError":throw new Error("用户拒绝了摄像头或麦克风权限");case"NotFoundError":throw new Error("未找到摄像头或麦克风设备");case"OverconstrainedError":throw new Error("设备无法满足指定约束");case"NotReadableError":throw new Error("设备忙或无法访问");default:throw new Error(`其他错误: ${n.message}`)}throw new Error(`mediaDevices 异常: ${n}`)}}async getRotatedStream(e,t){const n=document.createElement("canvas");n.width=640,n.height=480,n.style.position="absolute",n.style.top="-9999px",n.style.left="-9999px",n.style.width="1px",n.style.height="1px",n.style.opacity="0",document.body.appendChild(n);const i=n.getContext("2d"),r=document.createElement("video");r.srcObject=e,r.muted=!0,r.playsInline=!0,r.autoplay=!0,r.setAttribute("playsinline","true"),r.setAttribute("webkit-playsinline","true"),await r.play();const o=r.videoWidth,s=r.videoHeight;let a,c=0;const d=e=>{e-c>=40&&((()=>{i.clearRect(0,0,n.width,n.height),i.save(),i.translate(n.width/2,n.height/2);const e=t?-Math.PI/2:Math.PI/2;i.rotate(e),i.drawImage(r,-o/2,-s/2,o,s),i.restore()})(),c=e),a=requestAnimationFrame(d)};a=requestAnimationFrame(d);const l=n.captureStream(25);return l.getTracks().forEach(e=>{const t=()=>{cancelAnimationFrame(a),n.remove()};e.addEventListener("ended",t),e.addEventListener("stop",t)}),l}
73
75
  // private async setVideoParams(sender: RTCRtpSender) {
74
76
  // Logger.info('信息日志:', '设置推流视频参数=======>')
75
77
  // const params = sender.getParameters();
@@ -82,9 +84,7 @@ static async detectFileType(e){const t=await e.slice(0,16).arrayBuffer(),n=new U
82
84
  // });
83
85
  // await sender.setParameters(params);
84
86
  // }
85
- async setVideoParams(e){const t=e.getParameters(),n=/Android|iPhone|iPad/i.test(navigator.userAgent)?6e5:18e5;t.degradationPreference="maintain-resolution",t.encodings&&0!==t.encodings.length||(t.encodings=[{}]),t.encodings.forEach(e=>{e.maxBitrate=n,e.maxFramerate=30,e.scaleResolutionDownBy=1,e.priority="high"});try{await e.setParameters(t),w.info("信息日志:",`设置推流视频参数成功:${n/1e3}kbps / 30fps`)}catch(i){w.error("设置推流参数失败:",i)}}stopPush(){var e,t,n;this.isPushingStream&&(this.isPushingStream=!1,w.info("信息日志:","停止推流到云机=======>"),null==(e=this.localStream)||e.getTracks().forEach(e=>e.stop()),this.localStream=null,null==(t=this.rotatedStream)||t.getTracks().forEach(e=>e.stop()),this.rotatedStream=null,null==(n=this.peerConnection)||n.getSenders().forEach(e=>{var t;try{null==(t=this.peerConnection)||t.removeTrack(e)}catch(n){}}))}resetPeerConnection(){var e,t,n,i,r;this.peerConnection||(this.peerConnection=(e=>{w.info("信息日志:","初始化 Webrtc PeerConnection=======>");const t=[{urls:e.stunServerUri},{urls:e.turnServerUri,username:e.turnServerUserName,credential:e.turnServerPassword}];return new RTCPeerConnection({iceServers:t,iceTransportPolicy:"all",bundlePolicy:"max-bundle"})})(this.config),e=this.peerConnection,t=e=>{this.emit(y.sendICEMessage,e)},n=e=>{this.emit(y.streamTrack,e)},i=e=>{this.emit(y.iceConnectionState,e),"connected"===e&&this.checkStats()},r=e=>this.emit(y.webrtcError,e),e.onicecandidate=e=>{if(!e.candidate)return;const n={sdp:e.candidate.candidate,sdpMid:e.candidate.sdpMid,sdpMLineIndex:e.candidate.sdpMLineIndex};w.debug("信息日志:",`webrtc 生成的icecandidate===>${JSON.stringify(n)}`),t(JSON.stringify(n))},e.onconnectionstatechange=()=>{const t=e.iceConnectionState;t&&(w.debug("信息日志:","webrtc p2p连接状态===>",t),"failed"!==t&&"disconnected"!==t&&"closed"!==t||null==r||r(g(u.ICE_STATE,"failed")),null==i||i(t))},e.ontrack=e=>{w.debug("信息日志:","webrtc p2p连接后获取的音视频track");const t=e.track;t.contentHint="motion",null==n||n(t)},this.configDataChannel())}configDataChannel(){this.peerConnection.ondatachannel=e=>{this.dataChannel=e.channel,this.dataChannel.onmessage=e=>this.handleDataChannelMessage(e),this.dataChannel.onerror=e=>this.emit(y.webrtcError,g(u.DATACHANNEL_ERR,e)),this.sendChannelData(je.ActionRequestCloudDeviceInfo,"")}}handleDataChannelMessage(e){const t=JSON.parse(e.data);if(t.type===je.ActionCommandEvent){const{action:e,value:n,cameraId:i}=JSON.parse(t.data);"ACTION_CONTROL_VIDEO"===e&&("ENABLE"===n?this.isPushingStream?this.switchCamera(0===Number(i)):this.startPush(0===Number(i)):(this.stopPush(),this.stopLocal()))}else if(t.type===je.ActionUpdateCloudStatus){const{rotation:e,screenWidth:n,screenHeight:i,gestureMode:r,level:o,isClarity:s}=JSON.parse(t.data),a={direction:[Ve.ROTATION_0,Ve.ROTATION_180].includes(e)?ze.Vertical:ze.Horizontal,screenWidth:n,screenHeight:i,gestureMode:r,clarityLevel:o,isClarity:s};this.emit(y.cloudStatusChanged,a)}else t.type===je.CloudClipData&&this.emit(y.cloudClipData,t.data)}checkStats(){this.statsTimer=setInterval(()=>this.processStats(),1e3)}processStats(){this.peerConnection&&this.peerConnection.getStats(null).then(e=>{let t=0,n=0,i=0,r=0,o=0,s=0,a=0,c=0,d=0,l="";const h=Date.now();e.forEach(h=>{if("inbound-rtp"===h.type&&(s+=h.bytesReceived||0,r+=h.packetsLost||0,o+=h.packetsReceived||0,"video"===h.kind&&(t=h.framesPerSecond||0,n=h.totalDecodeTime||0,a=h.framesDecoded||0,d=h.framesReceived||0,c=(h.pliCount||0)+(h.firCount||0))),"candidate-pair"===h.type&&"succeeded"===h.state){i=void 0!==h.currentRoundTripTime?h.currentRoundTripTime:h.responsesReceived>0?h.totalRoundTripTime/h.responsesReceived:0;const t=e.get(h.localCandidateId),n=e.get(h.remoteCandidateId);t&&n&&(l=this.getConnectionType(t,n))}}),this.ua.includes("Firefox")||(i=Math.floor(1e3*(i||0)));const p=h-this.lastReportTime,u=s-this.lastBytesReceived,f=(p>0?1e3*u/p:0)/1024,m=f/1024;this.lastBytesReceived=s,this.lastReportTime=h;let g=0;const C=r-this.lastPacketsLost,v=o-this.lastPacketsReceived;C>0&&v>0&&(g=C/(C+v),this.maxLostRate=Math.max(this.maxLostRate||0,g),this.lostPacketCount++),this.lastPacketsLost=r,this.lastPacketsReceived=o;const S=void 0!==t?t:a-this.lastSecondDecodedCount;this.lastSecondDecodedCount=a;const T=f<1024?`${Math.floor(f)} KB/s`:`${Math.floor(m)} MB/s`,R=a>0?Math.floor(1e4*n/a):0,b={connectionType:l,framesPerSecond:S,currentRoundTripTime:i,lostRate:Math.floor(100*g),
86
- // percent
87
- bitrate:T,pliCount:c,averageDecodeTime:R,framesDecoded:a,framesReceived:d};this.emit(y.statisticInfo,b)}).catch(e=>{this.emit(y.webrtcError,g(u.STREAM_STATE,e))})}getConnectionType(e,t){return"host"===e.candidateType&&"host"===t.candidateType?"直连":"relay"===e.candidateType||"relay"===t.candidateType?"中继":"NAT"}
87
+ async setVideoParams(e){const t=e.getParameters(),n=/Android|iPhone|iPad/i.test(navigator.userAgent)?6e5:18e5;t.degradationPreference="maintain-resolution",t.encodings&&0!==t.encodings.length||(t.encodings=[{}]),t.encodings.forEach(e=>{e.maxBitrate=n,e.maxFramerate=30,e.scaleResolutionDownBy=1,e.priority="high"});try{await e.setParameters(t),w.info("信息日志:",`设置推流视频参数成功:${n/1e3}kbps / 30fps`)}catch(i){w.error("设置推流参数失败:",i)}}stopPush(){var e,t,n;this.isPushingStream&&(this.isPushingStream=!1,w.info("信息日志:","停止推流到云机=======>"),null==(e=this.localStream)||e.getTracks().forEach(e=>e.stop()),this.localStream=null,null==(t=this.rotatedStream)||t.getTracks().forEach(e=>e.stop()),this.rotatedStream=null,null==(n=this.peerConnection)||n.getSenders().forEach(e=>{var t;try{null==(t=this.peerConnection)||t.removeTrack(e)}catch(n){}}))}resetPeerConnection(){var e,t,n,i,r;this.peerConnection||(this.peerConnection=(e=>{w.info("信息日志:","初始化 Webrtc PeerConnection=======>");const t=[{urls:e.stunServerUri},{urls:e.turnServerUri,username:e.turnServerUserName,credential:e.turnServerPassword}];return new RTCPeerConnection({iceServers:t,iceTransportPolicy:"all",bundlePolicy:"max-bundle"})})(this.config),e=this.peerConnection,t=e=>{this.emit(y.sendICEMessage,e)},n=e=>{this.emit(y.streamTrack,e)},i=e=>{this.emit(y.iceConnectionState,e),"connected"===e&&this.checkStats()},r=e=>this.emit(y.webrtcError,e),e.onicecandidate=e=>{if(!e.candidate)return;const n={sdp:e.candidate.candidate,sdpMid:e.candidate.sdpMid,sdpMLineIndex:e.candidate.sdpMLineIndex};w.debug("信息日志:",`webrtc 生成的icecandidate===>${JSON.stringify(n)}`),t(JSON.stringify(n))},e.onconnectionstatechange=()=>{const t=e.iceConnectionState;t&&(w.debug("信息日志:","webrtc p2p连接状态===>",t),"failed"!==t&&"disconnected"!==t&&"closed"!==t||null==r||r(g(u.ICE_STATE,"failed")),null==i||i(t))},e.ontrack=e=>{w.debug("信息日志:","webrtc p2p连接后获取的音视频track");const t=e.track;t.contentHint="motion",null==n||n(t)},this.configDataChannel())}configDataChannel(){this.peerConnection.ondatachannel=e=>{this.dataChannel=e.channel,this.dataChannel.onmessage=e=>this.handleDataChannelMessage(e),this.dataChannel.onerror=e=>this.emit(y.webrtcError,g(u.DATACHANNEL_ERR,e)),this.sendChannelData(je.ActionRequestCloudDeviceInfo,"")}}handleDataChannelMessage(e){const t=JSON.parse(e.data);if(t.type===je.ActionCommandEvent){const{action:e,value:n,cameraId:i}=JSON.parse(t.data);"ACTION_CONTROL_VIDEO"===e&&("ENABLE"===n?this.isPushingStream?this.switchCamera(0===Number(i)):this.startPush(0===Number(i)):(this.stopPush(),this.stopLocal()))}else if(t.type===je.ActionUpdateCloudStatus){const{rotation:e,screenWidth:n,screenHeight:i,gestureMode:r,level:o,isClarity:s}=JSON.parse(t.data),a={direction:[Ve.ROTATION_0,Ve.ROTATION_180].includes(e)?ze.Vertical:ze.Horizontal,screenWidth:n,screenHeight:i,gestureMode:r,clarityLevel:o,isClarity:s};this.emit(y.cloudStatusChanged,a)}else t.type===je.CloudClipData&&this.emit(y.cloudClipData,t.data)}checkStats(){this.statsTimer=setInterval(()=>this.processStatsOptimized(),1e3)}async processStatsOptimized(){const e=this.peerConnection;if(!e)return;const t=navigator.userAgent,n=/Safari/.test(t)&&!/Chrome/.test(t),i=/iPhone|iPad|Android/i.test(t),r=n||i?3e3:1e3,o=Date.now();if(!(this.lastReportTime&&o-this.lastReportTime<r))try{const t=await e.getStats(null);let n=0,i=0,r=0,s=0,a=0,c=0,d=0,l=0,h=0,p="";for(const e of t.values())if("inbound-rtp"===e.type&&"video"===e.kind&&(c+=e.bytesReceived||0,s+=e.packetsLost||0,a+=e.packetsReceived||0,n=e.framesPerSecond||0,i=e.totalDecodeTime||0,d=e.framesDecoded||0,l=e.framesReceived||0,h=(e.pliCount||0)+(e.firCount||0)),"candidate-pair"===e.type&&"succeeded"===e.state){r=void 0!==e.currentRoundTripTime?e.currentRoundTripTime:e.responsesReceived>0?e.totalRoundTripTime/e.responsesReceived:0;const n=t.get(e.localCandidateId),i=t.get(e.remoteCandidateId);n&&i&&(p=this.getConnectionType(n,i))}r>1e3?r=Math.floor(r/1e3):r<1&&(r=Math.floor(1e3*r));const u=o-(this.lastReportTime||o),f=c-(this.lastBytesReceived||0),m=(u>0?1e3*f/u:0)/1024,g=m/1024;this.lastBytesReceived=c,this.lastReportTime=o;const C=s-(this.lastPacketsLost||0),v=a-(this.lastPacketsReceived||0);let S=0;C>0&&v>0&&(S=C/(C+v),this.maxLostRate=Math.max(this.maxLostRate||0,S),this.lostPacketCount=(this.lostPacketCount||0)+1),this.lastPacketsLost=s,this.lastPacketsReceived=a;const T=n||d-(this.lastSecondDecodedCount||0);this.lastSecondDecodedCount=d;const R=m<1024?`${Math.floor(m)} KB/s`:`${Math.floor(g)} MB/s`,b=d>0?Math.floor(1e4*i/d):0,w={connectionType:p,framesPerSecond:T,currentRoundTripTime:r,lostRate:Math.floor(100*S),bitrate:R,pliCount:h,averageDecodeTime:b,framesDecoded:d,framesReceived:l};this.emit(y.statisticInfo,w)}catch(s){this.emit(y.webrtcError,g(u.STREAM_STATE,s))}}getConnectionType(e,t){return"host"===e.candidateType&&"host"===t.candidateType?"直连":"relay"===e.candidateType||"relay"===t.candidateType?"中继":"NAT"}
88
88
  /** 获取或创建 video 元素(懒初始化) */getFileVideo(){return this.fileVideo||(this.fileVideo=document.createElement("video"),this.fileVideo.muted=!0,this.fileVideo.playsInline=!0),this.fileVideo}
89
89
  /** 获取或创建 canvas 元素(懒初始化) */getCanvas(){return this.canvas||(this.canvas=document.createElement("canvas"),this.canvas.width=640,this.canvas.height=480),this.canvas}async loadMedia(e){var t;const n=this.getFileVideo(),i=this.getFileImage(),r=e.type.toLowerCase(),o=null==(t=e.name.split(".").pop())?void 0:t.toLowerCase();let s="unknown";if(r.startsWith("video/")||o&&["mp4","webm","ogg","mov","mkv"].includes(o)?s="video":(r.startsWith("image/")||o&&["jpg","jpeg","png","gif","bmp","webp"].includes(o))&&(s="image"),"unknown"===s&&(s=await $e.detectFileType(e)),"video"===s)return new Promise((t,i)=>{const r=URL.createObjectURL(e);n.src=r,n.onloadedmetadata=()=>t(),n.onerror=()=>i(new Error(`视频文件加载失败: ${e.name}`)),n.play().catch(e=>i(new Error(`视频播放失败: ${e}`))),this.currentMedia=n});if("image"===s)return new Promise((t,n)=>{const r=URL.createObjectURL(e);i.src=r,i.onload=()=>t(),i.onerror=()=>n(new Error(`图片文件加载失败: ${e.name}`)),this.currentMedia=i});throw new Error(`不支持的文件类型: ${r||o}`)}startCanvasStream(e=30){w.info("信息日志:","初始化,使用本地文件推流到云机=======>");const t=this.getCanvas(),n=t.getContext("2d"),i=this.currentMedia;if(!i)throw new Error("请先加载媒体文件");let r=0;const o=1e3/e;let s=1;const a=e=>{if(e-r>=o){let o,s;if(n.clearRect(0,0,t.width,t.height),n.save(),n.translate(t.width/2,t.height/2),n.rotate(Math.PI/2),n.scale(-1,1),i instanceof HTMLVideoElement)o=i.videoWidth,s=i.videoHeight;else{if(!(i instanceof HTMLImageElement))throw new Error("不支持的媒体类型");o=i.width,s=i.height}const a=Math.min(t.height/o,t.width/s),c=o*a,d=s*a;n.drawImage(i,-c/2,-d/2,c,d),n.restore(),r=e}s=requestAnimationFrame(a)};return a(0),this.rafId=requestAnimationFrame(a),this.canvasStream=t.captureStream(e),this.canvasStream.getTracks().forEach(e=>{const t=()=>cancelAnimationFrame(s);e.addEventListener("ended",t),e.addEventListener("stop",t)}),this.canvasStream}getFileImage(){if(!this.fileImage){const e=document.createElement("img");e.style.display="none",document.body.appendChild(e),this.fileImage=e}return this.fileImage}
90
90
  /** 停止推流并释放资源 */stopLocal(){var e,t;this.isPushingLocalStream&&(this.isPushingLocalStream=!1,cancelAnimationFrame(this.rafId),null==(e=this.canvasStream)||e.getTracks().forEach(e=>e.stop()),this.canvasStream=null,null==(t=this.peerConnection)||t.getSenders().forEach(e=>{var t;try{null==(t=this.peerConnection)||t.removeTrack(e)}catch(n){w.error("错误日志:","移除音视频轨道失败=====>",n)}}),this.fileVideo&&(this.fileVideo.pause(),this.fileVideo.src=""))}}const Xe=async(e,t=600)=>{const n=e.map(e=>({urls:e,username:"yangyj",credential:"hb@2025@168"})).map(e=>((e,t=600)=>new Promise(n=>{const i=performance.now();let r=!1;const o=new RTCPeerConnection({iceServers:[e],iceTransportPolicy:"relay"});o.createDataChannel("test"),o.createOffer().then(e=>o.setLocalDescription(e)).catch(()=>{r||(r=!0,o.close(),n({...e,rtt:1/0}))}),o.onicecandidate=t=>{if(!r){if(t.candidate&&t.candidate.candidate.includes("relay")){const t=Math.trunc(performance.now()-i);r=!0,o.close(),n({...e,rtt:t})}null===t.candidate&&(r=!0,o.close(),n({...e,rtt:1/0}))}},setTimeout(()=>{r||(r=!0,o.close(),n({...e,rtt:1/0}))},t)}))(e,t).then(e=>e).catch(t=>(w.warn("警告日志:","中继计算超时=====>",t),{...e,rtt:1/0}))),i=await Promise.all(n);w.debug("调试日志:","信令计算结果======>",i);const r=i.filter(e=>e.rtt!==1/0);if(0===r.length)throw new Error("All TURN servers are unreachable or slow (RTT = Infinity).");const o=r.sort((e,t)=>e.rtt-t.rtt)[0];return{best:o?Ye(o):void 0,all:i.map(Ye)}},Ye=e=>({urls:e.urls,rtt:e.rtt}),Qe=e=>!(e.hostTurn&&0!==e.hostTurn.length||e.spareTurn&&0!==e.spareTurn.length);class Ze extends d{
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yjz-web-sdk",
3
3
  "private": false,
4
- "version": "1.0.8-beta.14",
4
+ "version": "1.0.8-beta.15",
5
5
  "type": "module",
6
6
  "description": "针对于亚矩阵项目的云手机投屏和屏幕控制",
7
7
  "license": "Apache-2.0",