diff --git a/README.md b/README.md index 480f9ba1..45dfc905 100755 --- a/README.md +++ b/README.md @@ -171,3 +171,14 @@ java -jar android/bundletool.jar build-apks --bundle=build/app/outputs/bundle/sk ```bash java -jar android/bundletool.jar install-apks --apks=build/app/outputs/bundle/skyRelease/app-sky-release.aab.apks ``` + +## Jpush相关 + +极光推送,目前app这边只依赖极光的透传能力,推送能力通过截取极光拿到的各个厂商的推送token,然后将推送token上报到自己业务服务器直接调用各个厂商推送通道进行消息推送,所以对极光的flutter sdk进行了私有化定制改造,改造点如下: + +* Android,iOS平台原生代码中截取jpush获取到的厂商推送token,将token回传到flutter业务应用层 + * Android通过Jpush统一集成的各个厂商推送sdk,统一获取到token + * iOS通过原生token回调接口获取到token +* flutter端,将获取到的厂商token,厂商标识上报到业务服务器 + +定制jpush_flutter:http://code-internal.star-lock.cn/StarlockTeam/jpush_flutter \ No newline at end of file diff --git a/assets/html/h264.html b/assets/html/h264.html index 191f47a5..97143565 100644 --- a/assets/html/h264.html +++ b/assets/html/h264.html @@ -7,2654 +7,36 @@ play + + - - diff --git a/assets/html/jmuxer.min.js b/assets/html/jmuxer.min.js new file mode 100644 index 00000000..6e31d497 --- /dev/null +++ b/assets/html/jmuxer.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("stream")):"function"==typeof define&&define.amd?define(["stream"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).JMuxer=t(e.stream)}(this,(function(e){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t(e)}function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){for(var n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n=e.length?{done:!0}:{done:!1,value:e[r++]}},e:function(e){throw e},f:i}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var a,s=!0,o=!1;return{s:function(){n=n.call(e)},n:function(){var e=n.next();return s=e.done,e},e:function(e){o=!0,a=e},f:function(){try{s||null==n.return||n.return()}finally{if(o)throw a}}}}var v,m;function k(e){if(v){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r1?t-1:0),r=1;r>5,this.ntype=31&this.payload[0],this.isvcl=1==this.ntype||5==this.ntype,this.stype="",this.isfmb=!1}return i(e,[{key:"toString",value:function(){return"".concat(e.type(this),": NRI: ").concat(this.getNri())}},{key:"getNri",value:function(){return this.nri}},{key:"type",value:function(){return this.ntype}},{key:"isKeyframe",value:function(){return this.ntype===e.IDR}},{key:"getPayload",value:function(){return this.payload}},{key:"getPayloadSize",value:function(){return this.payload.byteLength}},{key:"getSize",value:function(){return 4+this.getPayloadSize()}},{key:"getData",value:function(){var e=new Uint8Array(this.getSize());return new DataView(e.buffer).setUint32(0,this.getSize()-4),e.set(this.getPayload(),4),e}}],[{key:"NDR",get:function(){return 1}},{key:"IDR",get:function(){return 5}},{key:"SEI",get:function(){return 6}},{key:"SPS",get:function(){return 7}},{key:"PPS",get:function(){return 8}},{key:"AUD",get:function(){return 9}},{key:"TYPES",get:function(){var t;return a(t={},e.IDR,"IDR"),a(t,e.SEI,"SEI"),a(t,e.SPS,"SPS"),a(t,e.PPS,"PPS"),a(t,e.NDR,"NDR"),a(t,e.AUD,"AUD"),t}},{key:"type",value:function(t){return t.ntype in e.TYPES?e.TYPES[t.ntype]:"UNKNOWN"}}]),e}();function S(e,t){var n=new Uint8Array((0|e.byteLength)+(0|t.byteLength));return n.set(e,0),n.set(t,0|e.byteLength),n}var w=function(){function e(t){n(this,e),this.data=t,this.index=0,this.bitLength=8*t.byteLength}return i(e,[{key:"setData",value:function(e){this.data=e,this.index=0,this.bitLength=8*e.byteLength}},{key:"bitsAvailable",get:function(){return this.bitLength-this.index}},{key:"skipBits",value:function(e){if(this.bitsAvailable1&&void 0!==arguments[1])||arguments[1],n=this.getBits(e,this.index,t);return n}},{key:"getBits",value:function(e,t){var n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];if(this.bitsAvailable>>r,a=8-r;if(a>=e)return n&&(this.index+=e),i>>a-e;n&&(this.index+=a);var s=e-a;return i<>>1:-1*(e>>>1)}},{key:"readBoolean",value:function(){return 1===this.readBits(1)}},{key:"readUByte",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;return this.readBits(8*e)}},{key:"readUShort",value:function(){return this.readBits(16)}},{key:"readUInt",value:function(){return this.readBits(32)}}]),e}(),x=function(){function e(t){n(this,e),this.remuxer=t,this.track=t.mp4track}return i(e,[{key:"parseSPS",value:function(t){var n=e.readSPS(new Uint8Array(t));this.track.fps=n.fps,this.track.width=n.width,this.track.height=n.height,this.track.sps=[new Uint8Array(t)],this.track.codec="avc1.";for(var r=new DataView(t.buffer,t.byteOffset+1,4),i=0;i<3;++i){var a=r.getUint8(i).toString(16);a.length<2&&(a="0"+a),this.track.codec+=a}}},{key:"parsePPS",value:function(e){this.track.pps=[new Uint8Array(e)]}},{key:"parseNAL",value:function(e){if(!e)return!1;var t=!1;switch(e.type()){case b.IDR:case b.NDR:t=!0;break;case b.PPS:this.track.pps||(this.parsePPS(e.getPayload()),!this.remuxer.readyToDecode&&this.track.pps&&this.track.sps&&(this.remuxer.readyToDecode=!0)),t=!0;break;case b.SPS:this.track.sps||(this.parseSPS(e.getPayload()),!this.remuxer.readyToDecode&&this.track.pps&&this.track.sps&&(this.remuxer.readyToDecode=!0)),t=!0;break;case b.AUD:k("AUD - ignoing");break;case b.SEI:k("SEI - ignoing")}return t}}],[{key:"extractNALu",value:function(e){for(var t,n,r=0,i=e.byteLength,a=0,s=[],o=0;r0&&x[1]>0&&(h=x[0]/x[1])}if(u.readBoolean()&&u.skipBits(1),u.readBoolean()&&(u.skipBits(4),u.readBoolean()&&u.skipBits(24)),u.readBoolean()&&(u.skipUEG(),u.skipUEG()),u.readBoolean()){var A=u.readUInt(),U=u.readUInt();u.readBoolean()&&(p=U/(2*A))}}return{fps:p>0?p:void 0,width:Math.ceil((16*(i+1)-2*c-2*f)*h),height:(2-s)*(a+1)*16-(s?2:4)*(l+d)}}},{key:"parseHeader",value:function(e){var t=new w(e.getPayload());t.readUByte(),e.isfmb=0===t.readUEG(),e.stype=t.readUEG()}}]),e}(),A=function(){function e(t){n(this,e),this.remuxer=t,this.track=t.mp4track}return i(e,[{key:"setAACConfig",value:function(){var t,n,r,i=new Uint8Array(2),a=e.aacHeader;a&&(t=1+((192&a[2])>>>6),n=(60&a[2])>>>2,r=(1&a[2])<<2,r|=(192&a[3])>>>6,i[0]=t<<3,i[0]|=(14&n)>>1,i[1]|=(1&n)<<7,i[1]|=r<<3,this.track.codec="mp4a.40."+t,this.track.channelCount=r,this.track.config=i,this.remuxer.readyToDecode=!0)}}],[{key:"samplingRateMap",get:function(){return[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350]}},{key:"getHeaderLength",value:function(e){return 1&e[1]?7:9}},{key:"getFrameLength",value:function(e){return(3&e[3])<<11|e[4]<<3|(224&e[5])>>>5}},{key:"isAACPattern",value:function(e){return 255===e[0]&&240==(240&e[1])&&0==(6&e[1])}},{key:"extractAAC",value:function(t){var n,r,i=0,a=t.byteLength,s=[];if(!e.isAACPattern(t))return g("Invalid ADTS audio format"),s;for(n=e.getHeaderLength(t),e.aacHeader||(e.aacHeader=t.subarray(0,n));i-1&&this.listener[e].splice(n,1),!0}return!1}},{key:"offAll",value:function(){this.listener={}}},{key:"dispatch",value:function(e,t){return!!this.listener[e]&&(this.listener[e].map((function(e){e.apply(null,[t])})),!0)}}]),e}(),B=function(){function e(){n(this,e)}return i(e,null,[{key:"init",value:function(){var t;for(t in e.types={avc1:[],avcC:[],btrt:[],dinf:[],dref:[],esds:[],ftyp:[],hdlr:[],mdat:[],mdhd:[],mdia:[],mfhd:[],minf:[],moof:[],moov:[],mp4a:[],mvex:[],mvhd:[],sdtp:[],stbl:[],stco:[],stsc:[],stsd:[],stsz:[],stts:[],tfdt:[],tfhd:[],traf:[],trak:[],trun:[],trex:[],tkhd:[],vmhd:[],smhd:[]},e.types)e.types.hasOwnProperty(t)&&(e.types[t]=[t.charCodeAt(0),t.charCodeAt(1),t.charCodeAt(2),t.charCodeAt(3)]);var n=new Uint8Array([0,0,0,0,0,0,0,0,118,105,100,101,0,0,0,0,0,0,0,0,0,0,0,0,86,105,100,101,111,72,97,110,100,108,101,114,0]),r=new Uint8Array([0,0,0,0,0,0,0,0,115,111,117,110,0,0,0,0,0,0,0,0,0,0,0,0,83,111,117,110,100,72,97,110,100,108,101,114,0]);e.HDLR_TYPES={video:n,audio:r};var i=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,12,117,114,108,32,0,0,0,1]),a=new Uint8Array([0,0,0,0,0,0,0,0]);e.STTS=e.STSC=e.STCO=a,e.STSZ=new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0]),e.VMHD=new Uint8Array([0,0,0,1,0,0,0,0,0,0,0,0]),e.SMHD=new Uint8Array([0,0,0,0,0,0,0,0]),e.STSD=new Uint8Array([0,0,0,0,0,0,0,1]);var s=new Uint8Array([105,115,111,109]),o=new Uint8Array([97,118,99,49]),u=new Uint8Array([0,0,0,1]);e.FTYP=e.box(e.types.ftyp,s,u,s,o),e.DINF=e.box(e.types.dinf,e.box(e.types.dref,i))}},{key:"box",value:function(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r>24&255,i[1]=a>>16&255,i[2]=a>>8&255,i[3]=255&a,i.set(e,4),s=0,a=8;s>24&255,t>>16&255,t>>8&255,255&t,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))}},{key:"mdia",value:function(t){return e.box(e.types.mdia,e.mdhd(t.timescale,t.duration),e.hdlr(t.type),e.minf(t))}},{key:"mfhd",value:function(t){return e.box(e.types.mfhd,new Uint8Array([0,0,0,0,t>>24,t>>16&255,t>>8&255,255&t]))}},{key:"minf",value:function(t){return"audio"===t.type?e.box(e.types.minf,e.box(e.types.smhd,e.SMHD),e.DINF,e.stbl(t)):e.box(e.types.minf,e.box(e.types.vmhd,e.VMHD),e.DINF,e.stbl(t))}},{key:"moof",value:function(t,n,r){return e.box(e.types.moof,e.mfhd(t),e.traf(r,n))}},{key:"moov",value:function(t,n,r){for(var i=t.length,a=[];i--;)a[i]=e.trak(t[i]);return e.box.apply(null,[e.types.moov,e.mvhd(r,n)].concat(a).concat(e.mvex(t)))}},{key:"mvex",value:function(t){for(var n=t.length,r=[];n--;)r[n]=e.trex(t[n]);return e.box.apply(null,[e.types.mvex].concat(r))}},{key:"mvhd",value:function(t,n){var r=new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,2,t>>24&255,t>>16&255,t>>8&255,255&t,n>>24&255,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return e.box(e.types.mvhd,r)}},{key:"sdtp",value:function(t){var n,r,i=t.samples||[],a=new Uint8Array(4+i.length);for(r=0;r>>8&255),a.push(255&i),a=a.concat(Array.prototype.slice.call(r));for(n=0;n>>8&255),s.push(255&i),s=s.concat(Array.prototype.slice.call(r));var o=e.box(e.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|t.sps.length].concat(a).concat([t.pps.length]).concat(s))),u=t.width,c=t.height;return e.box(e.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,u>>8&255,255&u,c>>8&255,255&c,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,98,105,110,101,108,112,114,111,46,114,117,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,e.box(e.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])))}},{key:"esds",value:function(e){var t=e.config.byteLength,n=new Uint8Array(26+t+3);return n.set([0,0,0,0,3,23+t,0,1,0,4,15+t,64,21,0,0,0,0,0,0,0,0,0,0,0,5,t]),n.set(e.config,26),n.set([6,1,2],26+t),n}},{key:"mp4a",value:function(t){var n=t.audiosamplerate;return e.box(e.types.mp4a,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,n>>8&255,255&n,0,0]),e.box(e.types.esds,e.esds(t)))}},{key:"stsd",value:function(t){return"audio"===t.type?e.box(e.types.stsd,e.STSD,e.mp4a(t)):e.box(e.types.stsd,e.STSD,e.avc1(t))}},{key:"tkhd",value:function(t){var n=t.id,r=t.duration,i=t.width,a=t.height,s=t.volume;return e.box(e.types.tkhd,new Uint8Array([0,0,0,7,0,0,0,0,0,0,0,0,n>>24&255,n>>16&255,n>>8&255,255&n,0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,0,0,0,0,0,0,0,0,0,s>>0&255,s%1*10>>0&255,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,i>>8&255,255&i,0,0,a>>8&255,255&a,0,0]))}},{key:"traf",value:function(t,n){var r=e.sdtp(t),i=t.id;return e.box(e.types.traf,e.box(e.types.tfhd,new Uint8Array([0,0,0,0,i>>24,i>>16&255,i>>8&255,255&i])),e.box(e.types.tfdt,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),e.trun(t,r.length+16+16+8+16+8+8),r)}},{key:"trak",value:function(t){return t.duration=t.duration||4294967295,e.box(e.types.trak,e.tkhd(t),e.mdia(t))}},{key:"trex",value:function(t){var n=t.id;return e.box(e.types.trex,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))}},{key:"trun",value:function(t,n){var r,i,a,s,o,u,c=t.samples||[],f=c.length,l=12+16*f,d=new Uint8Array(l);for(n+=8+l,d.set([0,0,15,1,f>>>24&255,f>>>16&255,f>>>8&255,255&f,n>>>24&255,n>>>16&255,n>>>8&255,255&n],0),r=0;r>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,u>>>24&255,u>>>16&255,u>>>8&255,255&u],12+16*r);return e.box(e.types.trun,d)}},{key:"initSegment",value:function(t,n,r){e.types||e.init();var i,a=e.moov(t,n,r);return(i=new Uint8Array(e.FTYP.byteLength+a.byteLength)).set(e.FTYP),i.set(a,e.FTYP.byteLength),i}}]),e}(),D=1,C=function(){function e(){n(this,e)}return i(e,[{key:"flush",value:function(){this.mp4track.len=0,this.mp4track.samples=[]}},{key:"isReady",value:function(){return!(!this.readyToDecode||!this.samples.length)||null}}],[{key:"getTrackID",value:function(){return D++}}]),e}(),E=function(e){s(r,e);var t=l(r);function r(e){var i;return n(this,r),(i=t.call(this)).readyToDecode=!1,i.nextDts=0,i.dts=0,i.mp4track={id:C.getTrackID(),type:"audio",channelCount:0,len:0,fragmented:!0,timescale:e,duration:e,samples:[],config:"",codec:""},i.samples=[],i.aac=new A(c(i)),i}return i(r,[{key:"resetTrack",value:function(){this.readyToDecode=!1,this.mp4track.codec="",this.mp4track.channelCount="",this.mp4track.config="",this.mp4track.timescale=this.timescale,this.nextDts=0,this.dts=0}},{key:"remux",value:function(e){if(e.length>0)for(var t=0;t0&&this.readyToDecode&&(this.mp4track.len+=s,this.samples.push({units:a,size:s,keyFrame:i.keyFrame,duration:i.duration,compositionTimeOffset:i.compositionTimeOffset}))}}catch(e){n.e(e)}finally{n.f()}}},{key:"getPayload",value:function(){if(!this.isReady())return null;var e,t,n=new Uint8Array(this.mp4track.len),r=0,i=this.mp4track.samples;for(this.dts=this.nextDts;this.samples.length;){var a=this.samples.shift(),s=a.units;if((t=a.duration)<=0)k("remuxer: invalid sample duration at DTS: ".concat(this.nextDts," :").concat(t)),this.mp4track.len-=a.size;else{this.nextDts+=t,e={size:a.size,duration:t,cts:a.compositionTimeOffset||0,flags:{isLeading:0,isDependedOn:0,hasRedundancy:0,degradPrio:0,isNonSync:a.keyFrame?0:1,dependsOn:a.keyFrame?2:1}};var o,u=y(s);try{for(u.s();!(o=u.n()).done;){var c=o.value;n.set(c.getData(),r),r+=c.getSize()}}catch(e){u.e(e)}finally{u.f()}i.push(e)}}return i.length?new Uint8Array(n.buffer,0,this.mp4track.len):null}}]),r}(C),P=function(e){s(r,e);var t=l(r);function r(e){var i;return n(this,r),(i=t.call(this,"remuxer")).initialized=!1,i.trackTypes=[],i.tracks={},i.seq=1,i.env=e,i.timescale=1e3,i.mediaDuration=0,i.aacParser=null,i}return i(r,[{key:"addTrack",value:function(e){if("video"!==e&&"both"!==e||(this.tracks.video=new T(this.timescale),this.trackTypes.push("video")),"audio"===e||"both"===e){var t=new E(this.timescale);this.aacParser=t.getAacParser(),this.tracks.audio=t,this.trackTypes.push("audio")}}},{key:"reset",value:function(){var e,t=y(this.trackTypes);try{for(t.s();!(e=t.n()).done;){var n=e.value;this.tracks[n].resetTrack()}}catch(e){t.e(e)}finally{t.f()}this.initialized=!1}},{key:"destroy",value:function(){this.tracks={},this.offAll()}},{key:"flush",value:function(){if(this.initialized){var e,t=y(this.trackTypes);try{for(t.s();!(e=t.n()).done;){var n=e.value,r=this.tracks[n],i=r.getPayload();if(i&&i.byteLength){var a={type:n,payload:S(B.moof(this.seq,r.dts,r.mp4track),B.mdat(i)),dts:r.dts};"video"===n&&(a.fps=r.mp4track.fps),this.dispatch("buffer",a);var s=(o=r.dts/this.timescale,u=void 0,c=void 0,f=void 0,l=void 0,l="",u=Math.floor(o),(c=parseInt(u/3600,10)%24)>0&&(l+=(c<10?"0"+c:c)+":"),l+=((f=parseInt(u/60,10)%60)<10?"0"+f:f)+":"+((u=u<0?0:u%60)<10?"0"+u:u));k("put segment (".concat(n,"): dts: ").concat(r.dts," frames: ").concat(r.mp4track.samples.length," second: ").concat(s)),r.flush(),this.seq++}}}catch(e){t.e(e)}finally{t.f()}}else this.isReady()&&(this.dispatch("ready"),this.initSegment(),this.initialized=!0,this.flush());var o,u,c,f,l}},{key:"initSegment",value:function(){var e,t=[],n=y(this.trackTypes);try{for(n.s();!(e=n.n()).done;){var r=e.value,i=this.tracks[r];if("browser"==this.env){var a={type:r,payload:B.initSegment([i.mp4track],this.mediaDuration,this.timescale)};this.dispatch("buffer",a)}else t.push(i.mp4track)}}catch(e){n.e(e)}finally{n.f()}if("node"==this.env){var s={type:"all",payload:B.initSegment(t,this.mediaDuration,this.timescale)};this.dispatch("buffer",s)}k("Initial segment generated.")}},{key:"isReady",value:function(){var e,t=y(this.trackTypes);try{for(t.s();!(e=t.n()).done;){var n=e.value;if(!this.tracks[n].readyToDecode||!this.tracks[n].samples.length)return!1}}catch(e){t.e(e)}finally{t.f()}return!0}},{key:"remux",value:function(e){var t,n=y(this.trackTypes);try{for(n.s();!(t=n.n()).done;){var r=t.value,i=e[r];"audio"===r&&this.tracks.video&&!this.tracks.video.readyToDecode||i.length>0&&this.tracks[r].remux(i)}}catch(e){n.e(e)}finally{n.f()}this.flush()}}]),r}(U),L=function(e){s(r,e);var t=l(r);function r(e,i){var a;return n(this,r),(a=t.call(this,"buffer")).type=i,a.queue=new Uint8Array,a.cleaning=!1,a.pendingCleaning=0,a.cleanOffset=30,a.cleanRanges=[],a.sourceBuffer=e,a.sourceBuffer.addEventListener("updateend",(function(){a.pendingCleaning>0&&(a.initCleanup(a.pendingCleaning),a.pendingCleaning=0),a.cleaning=!1,a.cleanRanges.length&&a.doCleanup()})),a.sourceBuffer.addEventListener("error",(function(){a.dispatch("error",{type:a.type,name:"buffer",error:"buffer error"})})),a}return i(r,[{key:"destroy",value:function(){this.queue=null,this.sourceBuffer=null,this.offAll()}},{key:"doCleanup",value:function(){if(this.cleanRanges.length){var e=this.cleanRanges.shift();k("".concat(this.type," remove range [").concat(e[0]," - ").concat(e[1],")")),this.cleaning=!0,this.sourceBuffer.remove(e[0],e[1])}else this.cleaning=!1}},{key:"initCleanup",value:function(e){try{if(this.sourceBuffer.updating)return void(this.pendingCleaning=e);if(this.sourceBuffer.buffered&&this.sourceBuffer.buffered.length&&!this.cleaning){for(var t=0;tthis.cleanOffset&&n<(r=e-this.cleanOffset)&&this.cleanRanges.push([n,r])}this.doCleanup()}}catch(e){g("Error occured while cleaning ".concat(this.type," buffer - ").concat(e.name,": ").concat(e.message))}}},{key:"doAppend",value:function(){if(this.queue.length&&this.sourceBuffer&&!this.sourceBuffer.updating)try{this.sourceBuffer.appendBuffer(this.queue),this.queue=new Uint8Array}catch(t){var e="unexpectedError";"QuotaExceededError"===t.name?(k("".concat(this.type," buffer quota full")),e="QuotaExceeded"):(g("Error occured while appending ".concat(this.type," buffer - ").concat(t.name,": ").concat(t.message)),e="InvalidStateError"),this.dispatch("error",{type:this.type,name:e,error:"buffer error"})}}},{key:"feed",value:function(e){this.queue=S(this.queue,e)}}]),r}(U);return function(r){s(o,r);var a=l(o);function o(e){var r;n(this,o),(r=a.call(this,"jmuxer")).isReset=!1;return r.options=Object.assign({},{node:"",mode:"both",flushingTime:500,maxDelay:500,clearBuffer:!0,fps:30,readFpsFromTrack:!1,debug:!1,onReady:function(){},onData:function(){},onError:function(){},onMissingVideoFrames:function(){},onMissingAudioFrames:function(){}},e),r.env="object"===("undefined"==typeof process?"undefined":t(process))&&"undefined"==typeof window?"node":"browser",r.options.debug&&(v=console.log,m=console.error),r.options.fps||(r.options.fps=30),r.frameDuration=1e3/r.options.fps|0,r.remuxController=new P(r.env),r.remuxController.addTrack(r.options.mode),r.initData(),r.remuxController.on("buffer",r.onBuffer.bind(c(r))),"browser"==r.env&&(r.remuxController.on("ready",r.createBuffer.bind(c(r))),r.initBrowser()),r}return i(o,[{key:"initData",value:function(){this.lastCleaningTime=Date.now(),this.kfPosition=[],this.kfCounter=0,this.pendingUnits={},this.remainingData=new Uint8Array,this.startInterval()}},{key:"initBrowser",value:function(){"string"==typeof this.options.node&&""==this.options.node&&g("no video element were found to render, provide a valid video element"),this.node="string"==typeof this.options.node?document.getElementById(this.options.node):this.options.node,this.mseReady=!1,this.setupMSE()}},{key:"createStream",value:function(){var t=this.feed.bind(this),n=this.destroy.bind(this);return this.stream=new e.Duplex({writableObjectMode:!0,read:function(e){},write:function(e,n,r){t(e),r()},final:function(e){n(),e()}}),this.stream}},{key:"setupMSE",value:function(){if(window.MediaSource=window.MediaSource||window.WebKitMediaSource||window.ManagedMediaSource,!window.MediaSource)throw"Oops! Browser does not support Media Source Extension or Managed Media Source (IOS 17+).";if(this.isMSESupported=!!window.MediaSource,this.mediaSource=new window.MediaSource,this.url=URL.createObjectURL(this.mediaSource),window.MediaSource===window.ManagedMediaSource)try{this.node.removeAttribute("src"),this.node.disableRemotePlayback=!0;var e=document.createElement("source");e.type="video/mp4",e.src=this.url,this.node.appendChild(e),this.node.load()}catch(e){this.node.src=this.url}else this.node.src=this.url;this.mseEnded=!1,this.mediaSource.addEventListener("sourceopen",this.onMSEOpen.bind(this)),this.mediaSource.addEventListener("sourceclose",this.onMSEClose.bind(this)),this.mediaSource.addEventListener("webkitsourceopen",this.onMSEOpen.bind(this)),this.mediaSource.addEventListener("webkitsourceclose",this.onMSEClose.bind(this))}},{key:"endMSE",value:function(){if(!this.mseEnded)try{this.mseEnded=!0,this.mediaSource.endOfStream()}catch(e){g("mediasource is not available to end")}}},{key:"feed",value:function(e){var t,n,r,i=!1,a={video:[],audio:[]};if(e&&this.remuxController){if(r=e.duration?parseInt(e.duration):0,e.video){e.video=S(this.remainingData,e.video);var s=d(x.extractNALu(e.video),2);if(t=s[0],n=s[1],this.remainingData=n||new Uint8Array,!(t.length>0))return g("Failed to extract any NAL units from video data:",n),void("function"==typeof this.options.onMissingVideoFrames&&this.options.onMissingVideoFrames.call(null,e));a.video=this.getVideoFrames(t,r,e.compositionTimeOffset),i=!0}if(e.audio){if(!((t=A.extractAAC(e.audio)).length>0))return g("Failed to extract audio data from:",e.audio),void("function"==typeof this.options.onMissingAudioFrames&&this.options.onMissingAudioFrames.call(null,e));a.audio=this.getAudioFrames(t,r),i=!0}i?this.remuxController.remux(a):g("Input object must have video and/or audio property. Make sure it is a valid typed array")}}},{key:"getVideoFrames",value:function(e,t,n){var r,i=this,a=[],s=[],o=0,u=!1,c=!1;this.pendingUnits.units&&(a=this.pendingUnits.units,c=this.pendingUnits.vcl,u=this.pendingUnits.keyFrame,this.pendingUnits={});var f,l=y(e);try{for(l.s();!(f=l.n()).done;){var d=f.value,h=new b(d);h.type()!==b.IDR&&h.type()!==b.NDR||x.parseHeader(h),a.length&&c&&(h.isfmb||!h.isvcl)&&(s.push({units:a,keyFrame:u}),a=[],u=!1,c=!1),a.push(h),u=u||h.isKeyframe(),c=c||h.isvcl}}catch(e){l.e(e)}finally{l.f()}if(a.length)if(t)if(c)s.push({units:a,keyFrame:u});else{var p=s.length-1;p>=0&&(s[p].units=s[p].units.concat(a))}else this.pendingUnits={units:a,keyFrame:u,vcl:c};return r=t?t/s.length|0:this.frameDuration,o=t?t-r*s.length:0,s.map((function(e){e.duration=r,e.compositionTimeOffset=n,o>0&&(e.duration++,o--),i.kfCounter++,e.keyFrame&&i.options.clearBuffer&&i.kfPosition.push(i.kfCounter*r/1e3)})),k("jmuxer: No. of frames of the last chunk: ".concat(s.length)),s}},{key:"getAudioFrames",value:function(e,t){var n,r,i=[],a=0,s=y(e);try{for(s.s();!(r=s.n()).done;){var o=r.value;i.push({units:o})}}catch(e){s.e(e)}finally{s.f()}return n=t?t/i.length|0:this.frameDuration,a=t?t-n*i.length:0,i.map((function(e){e.duration=n,a>0&&(e.duration++,a--)})),i}},{key:"destroy",value:function(){if(this.stopInterval(),this.stream&&(this.remuxController.flush(),this.stream.push(null),this.stream=null),this.remuxController&&(this.remuxController.destroy(),this.remuxController=null),this.bufferControllers){for(var e in this.bufferControllers)this.bufferControllers[e].destroy();this.bufferControllers=null,this.endMSE()}this.node=!1,this.mseReady=!1,this.videoStarted=!1,this.mediaSource=null}},{key:"reset",value:function(){if(this.stopInterval(),this.isReset=!0,this.node.pause(),this.remuxController&&this.remuxController.reset(),this.bufferControllers){for(var e in this.bufferControllers)this.bufferControllers[e].destroy();this.bufferControllers=null,this.endMSE()}this.initData(),"browser"==this.env&&this.initBrowser(),k("JMuxer was reset")}},{key:"createBuffer",value:function(){if(this.mseReady&&this.remuxController&&this.remuxController.isReady()&&!this.bufferControllers)for(var e in this.bufferControllers={},this.remuxController.tracks){var t=this.remuxController.tracks[e];if(!o.isSupported("".concat(e,'/mp4; codecs="').concat(t.mp4track.codec,'"')))return g("Browser does not support codec"),!1;var n=this.mediaSource.addSourceBuffer("".concat(e,'/mp4; codecs="').concat(t.mp4track.codec,'"'));this.bufferControllers[e]=new L(n,e),this.bufferControllers[e].on("error",this.onBufferError.bind(this))}}},{key:"startInterval",value:function(){var e=this;this.interval=setInterval((function(){e.options.flushingTime?e.applyAndClearBuffer():e.bufferControllers&&e.cancelDelay()}),this.options.flushingTime||1e3)}},{key:"stopInterval",value:function(){this.interval&&clearInterval(this.interval)}},{key:"cancelDelay",value:function(){if(this.node.buffered&&this.node.buffered.length>0&&!this.node.seeking){var e=this.node.buffered.end(0);e-this.node.currentTime>this.options.maxDelay/1e3&&(console.log("delay"),this.node.currentTime=e-.001)}}},{key:"releaseBuffer",value:function(){for(var e in this.bufferControllers)this.bufferControllers[e].doAppend()}},{key:"applyAndClearBuffer",value:function(){this.bufferControllers&&(this.releaseBuffer(),this.clearBuffer())}},{key:"getSafeClearOffsetOfBuffer",value:function(e){for(var t,n="audio"===this.options.mode&&e||0,r=0;r=e);r++)t=this.kfPosition[r];return t&&(this.kfPosition=this.kfPosition.filter((function(e){return e=t}))),n}},{key:"clearBuffer",value:function(){if(this.options.clearBuffer&&Date.now()-this.lastCleaningTime>1e4){for(var e in this.bufferControllers){var t=this.getSafeClearOffsetOfBuffer(this.node.currentTime);this.bufferControllers[e].initCleanup(t)}this.lastCleaningTime=Date.now()}}},{key:"onBuffer",value:function(e){this.options.readFpsFromTrack&&void 0!==e.fps&&this.options.fps!=e.fps&&(this.options.fps=e.fps,this.frameDuration=Math.ceil(1e3/e.fps),k("JMuxer changed FPS to ".concat(e.fps," from track data"))),"browser"==this.env?this.bufferControllers&&this.bufferControllers[e.type]&&this.bufferControllers[e.type].feed(e.payload):this.stream&&this.stream.push(e.payload),this.options.onData&&this.options.onData(e.payload),0===this.options.flushingTime&&this.applyAndClearBuffer()}},{key:"onMSEOpen",value:function(){this.mseReady=!0,URL.revokeObjectURL(this.url),"function"==typeof this.options.onReady&&this.options.onReady.call(null,this.isReset)}},{key:"onMSEClose",value:function(){this.mseReady=!1,this.videoStarted=!1}},{key:"onBufferError",value:function(e){if("QuotaExceeded"==e.name)return k("JMuxer cleaning ".concat(e.type," buffer due to QuotaExceeded error")),void this.bufferControllers[e.type].initCleanup(this.node.currentTime);"InvalidStateError"==e.name?(k("JMuxer is reseting due to InvalidStateError"),this.reset()):this.endMSE(),"function"==typeof this.options.onError&&this.options.onError.call(null,e)}}],[{key:"isSupported",value:function(e){return window.MediaSource&&window.MediaSource.isTypeSupported(e)}}]),o}(U)})); diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m index 76d97654..b648e544 100755 --- a/ios/Runner/AppDelegate.m +++ b/ios/Runner/AppDelegate.m @@ -76,12 +76,25 @@ /* * 苹果推送注册成功回调,将苹果返回的deviceToken上传到CloudPush服务器 */ -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { +//- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { +// NSString *tokenString = [self hexStringFromData:deviceToken]; +// NSLog(@"starlock didRegisterForRemoteNotificationsWithDeviceToken token: %@", tokenString); +// /// Required - 注册 DeviceToken +// [JPUSHService registerDeviceToken:deviceToken]; +// +//} - /// Required - 注册 DeviceToken - [JPUSHService registerDeviceToken:deviceToken]; +- (NSString *)hexStringFromData:(NSData *)data { + const unsigned char *dataBuffer = (const unsigned char *)[data bytes]; + NSMutableString *hexString = [NSMutableString stringWithCapacity:data.length * 2]; + for (NSInteger i = 0; i < data.length; i++) { + [hexString appendFormat:@"%02x", dataBuffer[i]]; + } + + return [hexString copy]; } + /* * 苹果推送注册失败回调 */ diff --git a/lib/appRouters.dart b/lib/appRouters.dart index c7d403a1..e1fa7672 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -61,6 +61,7 @@ import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFe import 'package:star_lock/mine/valueAddedServices/advancedFunctionRecord/advancedFunctionRecord_page.dart'; import 'package:star_lock/mine/valueAddedServices/valueAddedServicesRecord/value_added_services_record_page.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_page.dart'; +import 'package:star_lock/talk/starChart/webView/h264_web_view.dart'; import 'common/safetyVerification/safetyVerification_page.dart'; import 'login/forgetPassword/starLock_forgetPassword_page.dart'; @@ -515,6 +516,7 @@ abstract class Routers { static const String doubleLockLinkPage = '/doubleLockLinkPage'; //双锁联动 static const String starChartPage = '/starChartPage'; //星图 static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面 + static const String h264WebView = '/h264WebView'; //星图对讲页面 } abstract class AppRouters { @@ -1195,5 +1197,6 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), + GetPage(name: Routers.h264WebView, page: () => H264WebView()), ]; } diff --git a/lib/main/lockDetail/doorLockLog/doorLockLog_page.dart b/lib/main/lockDetail/doorLockLog/doorLockLog_page.dart index 3a5bcdd8..51b74482 100755 --- a/lib/main/lockDetail/doorLockLog/doorLockLog_page.dart +++ b/lib/main/lockDetail/doorLockLog/doorLockLog_page.dart @@ -267,26 +267,31 @@ class _DoorLockLogPageState extends State with RouteAware { color: Colors.white, borderRadius: BorderRadius.circular(16.w), ), - child: Obx( - () => state.lockLogItemList.isNotEmpty - ? Timeline.tileBuilder( - builder: _timelineBuilderWidget(), - theme: TimelineThemeData( - nodePosition: 0.04, //居左侧距离 - connectorTheme: const ConnectorThemeData( - thickness: 1.0, - color: AppColors.greyLineColor, - indent: 0.5, + child: Obx(() => EasyRefreshTool( + onRefresh: () async { + logic.mockNetworkDataRequest(isRefresh: true); + }, + onLoad: () async { + logic.mockNetworkDataRequest(isRefresh: false); + }, + child: state.lockLogItemList.isNotEmpty + ? Timeline.tileBuilder( + builder: _timelineBuilderWidget(), + theme: TimelineThemeData( + nodePosition: 0.04, //居左侧距离 + connectorTheme: const ConnectorThemeData( + thickness: 1.0, + color: AppColors.greyLineColor, + indent: 0.5, + ), + indicatorTheme: const IndicatorThemeData( + size: 8.0, + color: AppColors.greyLineColor, + position: 0.4, + ), ), - indicatorTheme: const IndicatorThemeData( - size: 8.0, - color: AppColors.greyLineColor, - position: 0.45, - ), - ), - ) - : NoData(), - ), + ) + : NoData())), ); } @@ -347,6 +352,9 @@ class _DoorLockLogPageState extends State with RouteAware { if (recordData.videoUrl != null && recordData.videoUrl!.isNotEmpty) { final lockLogItemList = state.lockLogItemList.value; final list = lockLogItemList + .where((e) => + (e.videoUrl != null && e.videoUrl!.isNotEmpty) || + (e.imagesUrl != null && e.imagesUrl!.isNotEmpty)) .map( (e) => RecordListData( videoUrl: e.videoUrl, diff --git a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart index c5b6b7cd..a95b1c78 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; +import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -158,16 +159,13 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle void _handleVideoH264(TalkData talkData) { final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame(); talkDataH264Frame.mergeFromBuffer(talkData.content); - // AppLog.log('H264 TalkData :$talkDataH264Frame'); - // talkDataRepository.addTalkData(talkData); + frameHandler.handleFrame(talkDataH264Frame); } /// 处理图片数据 void _handleVideoImage(TalkData talkData) async { final List processCompletePayload = await _processCompletePayload(Uint8List.fromList(talkData.content)); - // AppLog.log('得到完整的帧:${processCompletePayload.length}'); // 循环发送每一帧的数据 - processCompletePayload.forEach((element) { talkData.content = element; talkDataRepository.addTalkData(talkData); @@ -181,7 +179,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle // // 转pcm数据 // List pcmBytes = G711().convertList(g711Data); // talkData.content = pcmBytes; - talkDataRepository.addTalkData(talkData); + // talkDataRepository.addTalkData(talkData); } catch (e) { print('Error decoding G.711 to PCM: $e'); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index f6c90d0e..c55043bc 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -73,9 +73,19 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 启动对讲请求超时定时器 talkeRequestOverTimeTimerManager.start(); // 收到呼叫请求,跳转到接听页面 - Get.toNamed( - Routers.starChartTalkView, - ); + if (startChartManage + .getDefaultTalkExpect() + .videoType + .indexOf(VideoTypeE.H264) == + -1) { + Get.toNamed( + Routers.starChartTalkView, + ); + } else { + Get.toNamed( + Routers.h264WebView, + ); + } } // 收到来电请求时进行本地通知 diff --git a/lib/talk/starChart/handle/other/h264_frame_buffer.dart b/lib/talk/starChart/handle/other/h264_frame_buffer.dart new file mode 100644 index 00000000..5ace9001 --- /dev/null +++ b/lib/talk/starChart/handle/other/h264_frame_buffer.dart @@ -0,0 +1,22 @@ +import 'dart:typed_data'; + +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; + +class H264FrameBuffer { + List frames = []; + + void addFrame(TalkDataH264Frame frame) { + frames.add(frame); + } + + Uint8List getCompleteStream() { + final List completeStream = []; + for (final frame in frames) { + // 添加起始码(假设为 0x00 0x00 0x01) + completeStream.addAll([0x00, 0x00, 0x01]); + // 添加帧数据 + completeStream.addAll(frame.frameData); + } + return Uint8List.fromList(completeStream); + } +} diff --git a/lib/talk/starChart/handle/other/h264_frame_handler.dart b/lib/talk/starChart/handle/other/h264_frame_handler.dart new file mode 100644 index 00000000..5eaa27df --- /dev/null +++ b/lib/talk/starChart/handle/other/h264_frame_handler.dart @@ -0,0 +1,84 @@ +import 'package:star_lock/app_settings/app_settings.dart'; +import '../../proto/talk_data_h264_frame.pb.dart'; + +class H264FrameHandler { + final Map _frameBuffer = {}; + final void Function(List frameData) onCompleteFrame; + int _lastProcessedSeq = -1; + + H264FrameHandler({required this.onCompleteFrame}); + + void handleFrame(TalkDataH264Frame frame) { + // 存储帧 + _frameBuffer[frame.frameSeq] = frame; + + // 检查是否可以组装完整的 GOP (Group of Pictures) + _tryAssembleFrames(frame.frameSeq); + } + + void _tryAssembleFrames(int currentSeq) { + // 找到连续的帧序列 + final List sortedSeqs = _frameBuffer.keys.toList()..sort(); + final List framesToProcess = []; + + // 从当前帧开始向前找到最近的 I 帧或 P 帧 + int? startFrameSeq; + for (var seq in sortedSeqs.reversed) { + final frame = _frameBuffer[seq]; + if (frame?.frameType == TalkDataH264Frame_FrameTypeE.I) { + startFrameSeq = seq; + break; + } else if (frame?.frameType == TalkDataH264Frame_FrameTypeE.P) { + // 检查 P 帧是否有对应的 I 帧 + if (_frameBuffer.containsKey(frame?.frameSeqI)) { + startFrameSeq = seq; + break; + } else { + // 丢弃没有对应 I 帧的 P 帧 + _frameBuffer.remove(seq); + } + } + } + + if (startFrameSeq != null) { + // 收集从 I 帧或 P 帧开始的连续帧 + int expectedSeq = startFrameSeq; + for (var seq in sortedSeqs.where((s) => s >= startFrameSeq!)) { + if (seq != expectedSeq) break; + framesToProcess.add(seq); + expectedSeq++; + } + + if (framesToProcess.isNotEmpty) { + _processFrames(framesToProcess); + } + } else { + _clearOldFrames(currentSeq); + } + } + + void _clearOldFrames(int currentSeq) { + // 清理比当前帧序列旧的帧 + _frameBuffer.removeWhere((seq, frame) => seq < currentSeq - 200); // 调整阈值 + } + + void _processFrames(List frameSeqs) { + // 按顺序组装帧数据 + final List assembledData = []; + + for (var seq in frameSeqs) { + final frame = _frameBuffer[seq]!; + assembledData.addAll(frame.frameData); + + // 处理完后从缓冲区移除 + _frameBuffer.remove(seq); + } + + // 回调完整的帧数据 + onCompleteFrame(assembledData); + } + + void clear() { + _frameBuffer.clear(); + } +} diff --git a/lib/talk/starChart/handle/other/talk_data_repository.dart b/lib/talk/starChart/handle/other/talk_data_repository.dart index 90fc77df..864aa4aa 100644 --- a/lib/talk/starChart/handle/other/talk_data_repository.dart +++ b/lib/talk/starChart/handle/other/talk_data_repository.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; class TalkDataRepository { @@ -27,6 +26,9 @@ class TalkDataRepository { bool _isListening = false; + // 用于存储数据的缓冲区 + final List _buffer = []; + // 提供一个方法来获取 Stream Stream get talkDataStream => _talkDataStreamController.stream.transform( @@ -41,14 +43,11 @@ class TalkDataRepository { }, ), ); - final List _buffer = []; // 用于存储数据的缓冲区 // 提供一个方法来添加 TalkData 到 Stream - void addTalkData(TalkData talkData) async { + void addTalkData(TalkData talkData) { if (_isListening) { - Future.microtask(() { - _talkDataStreamController.add(talkData); - }); + _talkDataStreamController.add(talkData); } } diff --git a/lib/talk/starChart/handle/scp_message_base_handle.dart b/lib/talk/starChart/handle/scp_message_base_handle.dart index 4cfc04c1..c1225f0e 100644 --- a/lib/talk/starChart/handle/scp_message_base_handle.dart +++ b/lib/talk/starChart/handle/scp_message_base_handle.dart @@ -14,6 +14,7 @@ import 'package:star_lock/talk/starChart/constant/payload_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/udp_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; +import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/handle/other/talke_data_over_time_timer_manager.dart'; @@ -52,6 +53,15 @@ class ScpMessageBaseHandle { final audioManager = AudioPlayerManager(); + // 处理出完整帧数据后的回调 + final H264FrameHandler frameHandler = + H264FrameHandler(onCompleteFrame: (frameData) { + // 处理完整的帧数据 + TalkDataRepository.instance.addTalkData( + TalkData(contentType: TalkData_ContentTypeE.H264, content: frameData), + ); + }); + // 回复成功消息 void replySuccessMessage(ScpMessage scpMessage) { startChartManage.sendGenericRespSuccessMessage( diff --git a/lib/talk/starChart/proto/talk_data_h264_frame.pb.dart b/lib/talk/starChart/proto/talk_data_h264_frame.pb.dart index 4a77a31a..e0239875 100644 --- a/lib/talk/starChart/proto/talk_data_h264_frame.pb.dart +++ b/lib/talk/starChart/proto/talk_data_h264_frame.pb.dart @@ -22,6 +22,7 @@ class TalkDataH264Frame extends $pb.GeneratedMessage { $core.int? frameSeq, TalkDataH264Frame_FrameTypeE? frameType, $core.List<$core.int>? frameData, + $core.int? frameSeqI, }) { final $result = create(); if (frameSeq != null) { @@ -33,6 +34,9 @@ class TalkDataH264Frame extends $pb.GeneratedMessage { if (frameData != null) { $result.frameData = frameData; } + if (frameSeqI != null) { + $result.frameSeqI = frameSeqI; + } return $result; } TalkDataH264Frame._() : super(); @@ -43,6 +47,7 @@ class TalkDataH264Frame extends $pb.GeneratedMessage { ..a<$core.int>(1, _omitFieldNames ? '' : 'FrameSeq', $pb.PbFieldType.OU3, protoName: 'FrameSeq') ..e(2, _omitFieldNames ? '' : 'FrameType', $pb.PbFieldType.OE, protoName: 'FrameType', defaultOrMaker: TalkDataH264Frame_FrameTypeE.NONE, valueOf: TalkDataH264Frame_FrameTypeE.valueOf, enumValues: TalkDataH264Frame_FrameTypeE.values) ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'FrameData', $pb.PbFieldType.OY, protoName: 'FrameData') + ..a<$core.int>(4, _omitFieldNames ? '' : 'FrameSeqI', $pb.PbFieldType.OU3, protoName: 'FrameSeqI') ..hasRequiredFields = false ; @@ -95,6 +100,16 @@ class TalkDataH264Frame extends $pb.GeneratedMessage { $core.bool hasFrameData() => $_has(2); @$pb.TagNumber(3) void clearFrameData() => clearField(3); + + /// 帧序号I + @$pb.TagNumber(4) + $core.int get frameSeqI => $_getIZ(3); + @$pb.TagNumber(4) + set frameSeqI($core.int v) { $_setUnsignedInt32(3, v); } + @$pb.TagNumber(4) + $core.bool hasFrameSeqI() => $_has(3); + @$pb.TagNumber(4) + void clearFrameSeqI() => clearField(4); } diff --git a/lib/talk/starChart/proto/talk_data_h264_frame.pbjson.dart b/lib/talk/starChart/proto/talk_data_h264_frame.pbjson.dart index bc7b066c..6bf9aa9b 100644 --- a/lib/talk/starChart/proto/talk_data_h264_frame.pbjson.dart +++ b/lib/talk/starChart/proto/talk_data_h264_frame.pbjson.dart @@ -20,6 +20,7 @@ const TalkDataH264Frame$json = { {'1': 'FrameSeq', '3': 1, '4': 1, '5': 13, '10': 'FrameSeq'}, {'1': 'FrameType', '3': 2, '4': 1, '5': 14, '6': '.main.TalkDataH264Frame.FrameTypeE', '10': 'FrameType'}, {'1': 'FrameData', '3': 3, '4': 1, '5': 12, '10': 'FrameData'}, + {'1': 'FrameSeqI', '3': 4, '4': 1, '5': 13, '10': 'FrameSeqI'}, ], '4': [TalkDataH264Frame_FrameTypeE$json], }; @@ -38,6 +39,6 @@ const TalkDataH264Frame_FrameTypeE$json = { final $typed_data.Uint8List talkDataH264FrameDescriptor = $convert.base64Decode( 'ChFUYWxrRGF0YUgyNjRGcmFtZRIaCghGcmFtZVNlcRgBIAEoDVIIRnJhbWVTZXESQAoJRnJhbW' 'VUeXBlGAIgASgOMiIubWFpbi5UYWxrRGF0YUgyNjRGcmFtZS5GcmFtZVR5cGVFUglGcmFtZVR5' - 'cGUSHAoJRnJhbWVEYXRhGAMgASgMUglGcmFtZURhdGEiJAoKRnJhbWVUeXBlRRIICgROT05FEA' - 'ASBQoBSRABEgUKAVAQAg=='); + 'cGUSHAoJRnJhbWVEYXRhGAMgASgMUglGcmFtZURhdGESHAoJRnJhbWVTZXFJGAQgASgNUglGcm' + 'FtZVNlcUkiJAoKRnJhbWVUeXBlRRIICgROT05FEAASBQoBSRABEgUKAVAQAg=='); diff --git a/lib/talk/starChart/proto/talk_data_h264_frame.proto b/lib/talk/starChart/proto/talk_data_h264_frame.proto index 09815d16..1d08adef 100644 --- a/lib/talk/starChart/proto/talk_data_h264_frame.proto +++ b/lib/talk/starChart/proto/talk_data_h264_frame.proto @@ -15,4 +15,6 @@ message TalkDataH264Frame { FrameTypeE FrameType = 2; // 帧数据 bytes FrameData = 3; + // 帧序号I + uint32 FrameSeqI = 4; } diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 1a3d2a85..91617794 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -112,7 +112,7 @@ class StartChartManage { // 默认通话的期望数据格式 TalkExpectReq _defaultTalkExpect = TalkExpectReq( - videoType: [VideoTypeE.IMAGE], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); @@ -419,9 +419,15 @@ class StartChartManage { if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { // 停止播放铃声 // AudioPlayerManager().playRingtone(); - Get.toNamed( - Routers.starChartTalkView, - ); + if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { + Get.toNamed( + Routers.h264WebView, + ); + } else { + Get.toNamed( + Routers.starChartTalkView, + ); + } } talkRequestTimer ??= Timer.periodic( Duration( @@ -1113,15 +1119,19 @@ class StartChartManage { void reSetDefaultTalkExpect() { _defaultTalkExpect = TalkExpectReq( - videoType: [VideoTypeE.IMAGE], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); } + TalkExpectReq getDefaultTalkExpect() { + return _defaultTalkExpect; + } + /// 修改预期接收到的数据 void sendOnlyImageVideoTalkExpectData() { final talkExpectReq = TalkExpectReq( - videoType: [VideoTypeE.IMAGE], + videoType: [VideoTypeE.H264], audioType: [], ); changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( @@ -1131,7 +1141,7 @@ class StartChartManage { /// 修改预期接收到的数据 void sendImageVideoAndG711AudioTalkExpectData() { final talkExpectReq = TalkExpectReq( - videoType: [VideoTypeE.IMAGE], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart new file mode 100644 index 00000000..e8e28784 --- /dev/null +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -0,0 +1,434 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math'; // Import the math package to use sqrt + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; +import 'package:flutter_screen_recording/flutter_screen_recording.dart'; +import 'package:flutter_voice_processor/flutter_voice_processor.dart'; +import 'package:gallery_saver/gallery_saver.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/login/login/entity/LoginEntity.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart'; +import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; +import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/talk/call/g711.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:star_lock/talk/starChart/webView/h264_web_view_state.dart'; +import 'package:star_lock/tools/bugly/bugly_tool.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import '../../../../tools/baseGetXController.dart'; + +class H264WebViewLogic extends BaseGetXController { + final H264WebViewState state = H264WebViewState(); + + final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; + + @override + void onInit() { + super.onInit(); + // 初始化 WebView 控制器 + state.webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..enableZoom(false) + ..addJavaScriptChannel( + 'Flutter', + onMessageReceived: (message) { + print("来自 HTML 的消息: ${message.message}"); + }, + ); + + state.isShowLoading.value = true; + // 加载本地 HTML + _loadLocalHtml(); + // 创建流数据监听 + _createFramesStreamListen(); + + _startListenTalkStatus(); + state.talkStatus.value = state.startChartTalkStatus.status; + // 初始化音频播放器 + _initFlutterPcmSound(); + // 初始化录音控制器 + _initAudioRecorder(); + } + + /// 初始化音频录制器 + void _initAudioRecorder() { + state.voiceProcessor = VoiceProcessor.instance; + } + + /// 初始化音频播放器 + void _initFlutterPcmSound() { + const int sampleRate = 8000; + FlutterPcmSound.setLogLevel(LogLevel.none); + FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1); + // 设置 feed 阈值 + if (Platform.isAndroid) { + FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理 + } else { + FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理 + } + } + + void _createFramesStreamListen() async { + state.talkDataRepository.talkDataStream.listen((TalkData event) async { + // 发送数据给js处理 + _sendBufferedData(event.content); + }); + } + + /// 加载html文件 + Future _loadLocalHtml() async { + // 加载 HTML 文件内容 + final String fileHtmlContent = + await rootBundle.loadString('assets/html/h264.html'); + + // 加载 JS 文件内容 + final String jsContent = + await rootBundle.loadString('assets/html/jmuxer.min.js'); + + // 将 JS 文件内容嵌入到 HTML 中 + final String htmlWithJs = fileHtmlContent.replaceAll( + '', // 替换掉引用外部 JS 的标签 + '' // 使用内联方式嵌入 JS 内容 + ); + + // 加载最终的 HTML 字符串到 WebView 中 + if (state.webViewController != null) { + state.webViewController.loadHtmlString(htmlWithJs); // 设置 baseUrl 避免资源加载问题 + } + } + + // 修改后的发送方法 + _sendBufferedData(List buffer) async { + // 原始发送逻辑 + String jsCode = "feedDataFromFlutter($buffer);"; + await state.webViewController.runJavaScript(jsCode); + + if (state.isShowLoading.isTrue) { + await Future.delayed(Duration(seconds: 1)); + state.isShowLoading.value = false; + } + } + + /// 监听对讲状态 + void _startListenTalkStatus() { + state.startChartTalkStatus.statusStream.listen((talkStatus) { + state.talkStatus.value = talkStatus; + switch (talkStatus) { + case TalkStatus.rejected: + case TalkStatus.hangingUpDuring: + case TalkStatus.notTalkData: + case TalkStatus.notTalkPing: + case TalkStatus.end: + _handleInvalidTalkStatus(); + break; + case TalkStatus.answeredSuccessfully: + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer ??= + Timer.periodic(const Duration(seconds: 1), (Timer t) { + if (state.isShowLoading.isFalse) { + state.oneMinuteTime.value++; + if (state.oneMinuteTime.value >= 60) { + t.cancel(); // 取消定时器 + state.oneMinuteTime.value = 0; + } + } + }); + break; + default: + // 其他状态的处理 + break; + } + }); + } + + /// 更新发送预期数据 + void updateTalkExpect() { + TalkExpectReq talkExpectReq = TalkExpectReq(); + state.isOpenVoice.value = !state.isOpenVoice.value; + if (!state.isOpenVoice.value) { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [], + ); + showToast('已静音'.tr); + } else { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [AudioTypeE.G711], + ); + } + + /// 修改发送预期数据 + StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: talkExpectReq); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() {} + + /// 截图并保存到相册 + Future captureAndSavePng() async { + try { + if (state.globalKey.currentContext == null) { + AppLog.log('截图失败: 未找到当前上下文'); + return; + } + final RenderRepaintBoundary boundary = state.globalKey.currentContext! + .findRenderObject()! as RenderRepaintBoundary; + final ui.Image image = await boundary.toImage(); + final ByteData? byteData = + await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData == null) { + AppLog.log('截图失败: 图像数据为空'); + return; + } + final Uint8List pngBytes = byteData.buffer.asUint8List(); + + // 获取应用程序的文档目录 + final Directory directory = await getApplicationDocumentsDirectory(); + final String imagePath = '${directory.path}/screenshot.png'; + + // 将截图保存为文件 + final File imgFile = File(imagePath); + await imgFile.writeAsBytes(pngBytes); + + // 将截图保存到相册 + await ImageGallerySaver.saveFile(imagePath); + + AppLog.log('截图保存路径: $imagePath'); + showToast('截图已保存到相册'.tr); + } catch (e) { + AppLog.log('截图失败: $e'); + } + } + + // 发起接听命令 + void initiateAnswerCommand() { + StartChartManage().startTalkAcceptTimer(); + } + + //开始录音 + Future startProcessingAudio() async { + // 增加录音帧监听器和错误监听器 + state.voiceProcessor?.addFrameListener(_onFrame); + state.voiceProcessor?.addErrorListener(_onError); + try { + if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) { + await state.voiceProcessor?.start(state.frameLength, state.sampleRate); + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.startRecordingAudioTime.value = DateTime.now(); + } else { + // state.errorMessage.value = 'Recording permission not granted'; + } + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to start recorder: $ex'; + } + state.isOpenVoice.value = false; + } + + /// 停止录音 + Future stopProcessingAudio() async { + try { + await state.voiceProcessor?.stop(); + state.voiceProcessor?.removeFrameListener(_onFrame); + state.udpSendDataFrameNumber = 0; + // 记录结束时间 + state.endRecordingAudioTime.value = DateTime.now(); + + // 计算录音的持续时间 + final duration = state.endRecordingAudioTime.value! + .difference(state.startRecordingAudioTime.value!); + + state.recordingAudioTime.value = duration.inSeconds; + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to stop recorder: $ex'; + } finally { + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.isOpenVoice.value = true; + } + } + + // 音频帧处理 + Future _onFrame(List frame) async { + // 预处理和转码操作放到异步计算线程 + // final processedFrame = await compute(preprocessAudio, frame); + // final list = listLinearToALaw(processedFrame); + final List processedFrame = preprocessAudio(frame); + final List list = listLinearToALaw(processedFrame); + + final int ms = DateTime.now().millisecondsSinceEpoch - + state.startRecordingAudioTime.value.millisecondsSinceEpoch; + + // 发送音频数据到UDP + await StartChartManage().sendTalkDataMessage( + talkData: TalkData( + content: list, + contentType: TalkData_ContentTypeE.G711, + durationMs: ms, + ), + ); + } + + /// 挂断 + void udpHangUpAction() async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 如果是通话中就挂断 + StartChartManage().startTalkHangupMessageTimer(); + } else { + // 拒绝 + StartChartManage().startTalkRejectMessageTimer(); + } + Get.back(); + } + + // 远程开锁 + Future remoteOpenLock() async { + final lockPeerId = StartChartManage().lockPeerId; + final lockListPeerId = StartChartManage().lockListPeerId; + int lockId = lockDetailState.keyInfos.value.lockId ?? 0; + + // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId + // 从列表中遍历出对应的peerId + lockListPeerId.forEach((element) { + if (element.network?.peerId == lockPeerId) { + lockId = element.lockId ?? 0; + } + }); + + final LockSetInfoEntity lockSetInfoEntity = + await ApiRepository.to.getLockSettingInfoData( + lockId: lockId.toString(), + ); + if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { + if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && + lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; + } + } else { + showToast('该锁的远程开锁功能未启用'.tr); + } + } + } + + List preprocessAudio(List pcmList) { + // 简单的降噪处理 + final List processedList = []; + for (int pcmVal in pcmList) { + // 简单的降噪示例:将小于阈值的信号置为0 + if (pcmVal.abs() < 200) { + pcmVal = 0; + } + processedList.add(pcmVal); + } + return processedList; + } + + List listLinearToALaw(List pcmList) { + final List aLawList = []; + for (int pcmVal in pcmList) { + final int aLawVal = linearToALaw(pcmVal); + aLawList.add(aLawVal); + } + return aLawList; + } + + int linearToALaw(int pcmVal) { + const int ALAW_MAX = 0x7FFF; // 32767 + const int ALAW_BIAS = 0x84; // 132 + + int mask; + int seg; + int aLawVal; + + // Handle sign + if (pcmVal < 0) { + pcmVal = -pcmVal; + mask = 0x7F; // 127 (sign bit is 1) + } else { + mask = 0xFF; // 255 (sign bit is 0) + } + + // Add bias and clamp to ALAW_MAX + pcmVal += ALAW_BIAS; + if (pcmVal > ALAW_MAX) { + pcmVal = ALAW_MAX; + } + + // Determine segment + seg = search(pcmVal); + + // Calculate A-law value + if (seg >= 8) { + aLawVal = 0x7F ^ mask; // Clamp to maximum value + } else { + int quantized = (pcmVal >> (seg + 3)) & 0xF; + aLawVal = (seg << 4) | quantized; + aLawVal ^= 0xD5; // XOR with 0xD5 to match standard A-law table + } + + return aLawVal; + } + + int search(int val) { + final List table = [ + 0xFF, // Segment 0 + 0x1FF, // Segment 1 + 0x3FF, // Segment 2 + 0x7FF, // Segment 3 + 0xFFF, // Segment 4 + 0x1FFF, // Segment 5 + 0x3FFF, // Segment 6 + 0x7FFF // Segment 7 + ]; + const int size = 8; + for (int i = 0; i < size; i++) { + if (val <= table[i]) { + return i; + } + } + return size; + } + +// 错误监听 + void _onError(VoiceProcessorException error) { + AppLog.log(error.message!); + } + + @override + void dispose() { + // TODO: implement dispose + super.dispose(); + StartChartManage().startTalkHangupMessageTimer(); + state.animationController.dispose(); + state.webViewController.clearCache(); + state.webViewController.reload(); + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + stopProcessingAudio(); + StartChartManage().reSetDefaultTalkExpect(); + } +} diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index 2392c5fa..51aa30d8 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -1,11 +1,18 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show ByteData, Uint8List, rootBundle; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_colors.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/webView/h264_web_logic.dart'; +import 'package:star_lock/talk/starChart/webView/h264_web_view_state.dart'; import 'package:star_lock/tools/titleAppBar.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -14,169 +21,400 @@ class H264WebView extends StatefulWidget { _H264WebViewState createState() => _H264WebViewState(); } -class _H264WebViewState extends State { - late final WebViewController _controller; - Timer? timer; - Timer? _sendTimer; - - // 私有缓冲区,外部无法直接访问 - final List _buffer = []; - - // 发送数据至html文件间隔时间 - final int sendDataToHtmlIntervalTime = 820; - - // 通话数据流的单例流数据处理类 - final TalkDataRepository talkDataRepository = TalkDataRepository.instance; +class _H264WebViewState extends State + with TickerProviderStateMixin { + final H264WebViewLogic logic = Get.put(H264WebViewLogic()); + final H264WebViewState state = Get.find().state; @override void initState() { + // TODO: implement initState super.initState(); + state.animationController = AnimationController( + vsync: this, // 确保使用的TickerProvider是当前Widget + duration: const Duration(seconds: 1), + ); - _controller = WebViewController() - ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..enableZoom(false) - ..addJavaScriptChannel( - 'Flutter', - onMessageReceived: (message) { - print("来自 HTML 的消息: ${message.message}"); - }, - ); - - // 加载本地 HTML - _loadLocalHtml(); - simulateStreamFromAsset(); - _sendFramesToHtml(); - } - - void simulateStreamFromAsset() async { - // 读取 assets 文件 - final ByteData data = await rootBundle.load('assets/talk.h264'); - final List byteData = data.buffer.asUint8List(); - int current = 0; - int start = 0; - int end = 0; - final List chunks = extractChunks(byteData); - // 定时器控制发送数据块的节奏 - timer ??= Timer.periodic(Duration(milliseconds: 10), (timer) { - if (current >= chunks.length) { - print('数据已经发完,重新进行发送'); - start = 0; - end = 0; - current = 0; - timer.cancel(); - return; - } - // 提取 NALU 边界并生成 chunks - end = chunks[current]; - current++; - List frameData = byteData.sublist(start, end); - if (frameData.length == 0) timer.cancel(); - - talkDataRepository.addTalkData(TalkData(contentType: TalkData_ContentTypeE.H264,content: frameData)); - start = end; - }); - } - - void _sendFramesToHtml() async { - // 接收到流数据,保存到缓冲区 - // talkDataRepository.talkDataStream.listen((TalkData event) async { - // _buffer.addAll(event.content); - // }); - // 缓冲800ms的数据,定时发送 - _sendTimer ??= Timer.periodic( - Duration(milliseconds: sendDataToHtmlIntervalTime), (timer) async { - // 发送累积的数据 - if (_buffer.isNotEmpty) { - await _sendBufferedData(_buffer); - _buffer.clear(); // 清空缓冲区 + state.animationController.repeat(); + //动画开始、结束、向前移动或向后移动时会调用StatusListener + state.animationController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + state.animationController.reset(); + state.animationController.forward(); + } else if (status == AnimationStatus.dismissed) { + state.animationController.reset(); + state.animationController.forward(); } }); } - // 提取 NALU 边界并生成 chunks - List extractChunks(List byteData) { - int i = 0; - int length = byteData.length; - int naluCount = 0; - int value; - int state = 0; - int lastIndex = 0; - List result = []; - const minNaluPerChunk = 22; // 每个数据块包含的最小NALU数量 - - while (i < length) { - value = byteData[i++]; - // finding 3 or 4-byte start codes (00 00 01 OR 00 00 00 01) - switch (state) { - case 0: - if (value == 0) { - state = 1; - } - break; - case 1: - if (value == 0) { - state = 2; - } else { - state = 0; - } - break; - case 2: - case 3: - if (value == 0) { - state = 3; - } else if (value == 1 && i < length) { - if (lastIndex > 0) { - naluCount++; - } - if (naluCount >= minNaluPerChunk) { - result.add(lastIndex - state - 1); - naluCount = 0; - } - state = 0; - lastIndex = i; - } else { - state = 0; - } - break; - default: - break; - } - } - - if (naluCount > 0) { - result.add(lastIndex); - } - - - - return result; - } - - /// 加载html文件 - Future _loadLocalHtml() async { - final String fileHtmlContent = - await rootBundle.loadString('assets/html/h264.html'); - _controller.loadHtmlString(fileHtmlContent); - } - - // 发送数据给js处理 - _sendBufferedData(List buffer) async { - String jsCode = "feedDataFromFlutter(${buffer});"; - await _controller.runJavaScript(jsCode); - } - @override Widget build(BuildContext context) { - return WebViewWidget(controller: _controller); + return WillPopScope( + onWillPop: () async { + // 返回 false 表示禁止退出 + return false; + }, + child: SizedBox( + width: 1.sw, + height: 1.sh, + child: Stack( + alignment: Alignment.center, + children: [ + Obx(() { + final double screenWidth = MediaQuery.of(context).size.width; + final double screenHeight = MediaQuery.of(context).size.height; + return state.isShowLoading.value + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : WebViewWidget( + controller: state.webViewController, + ); + }), + Obx( + () => state.isShowLoading.value + ? Positioned( + bottom: 310.h, + child: Text( + '正在创建安全连接...'.tr, + style: TextStyle(color: Colors.black, fontSize: 26.sp), + ), + ) + : Container(), + ), + Obx( + () => state.isShowLoading.isFalse + ? Positioned( + top: ScreenUtil().statusBarHeight + 75.h, + width: 1.sw, + child: Obx( + () { + final String sec = (state.oneMinuteTime.value % 60) + .toString() + .padLeft(2, '0'); + final String min = (state.oneMinuteTime.value ~/ 60) + .toString() + .padLeft(2, '0'); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '$min:$sec', + style: TextStyle( + fontSize: 26.sp, color: Colors.white), + ), + ], + ); + }, + ), + ) + : Container(), + ), + Positioned( + bottom: 10.w, + child: Container( + width: 1.sw - 30.w * 2, + // height: 300.h, + margin: EdgeInsets.all(30.w), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.2), + borderRadius: BorderRadius.circular(20.h)), + child: Column( + children: [ + SizedBox(height: 20.h), + bottomTopBtnWidget(), + SizedBox(height: 20.h), + bottomBottomBtnWidget(), + SizedBox(height: 20.h), + ], + ), + ), + ), + Obx(() => state.isShowLoading.isTrue + ? buildRotationTransition() + : Container()), + Obx(() => state.isLongPressing.value + ? Positioned( + top: 80.h, + left: 0, + right: 0, + child: Center( + child: Container( + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(10.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.mic, color: Colors.white, size: 24.w), + SizedBox(width: 10.w), + Text( + '正在说话...'.tr, + style: TextStyle( + fontSize: 20.sp, color: Colors.white), + ), + ], + ), + ), + ), + ) + : Container()), + ], + ), + ), + ); } + Widget bottomTopBtnWidget() { + return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + // 打开关闭声音 + GestureDetector( + onTap: () { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 打开关闭声音 + logic.updateTalkExpect(); + } + }, + child: Container( + width: 50.w, + height: 50.w, + padding: EdgeInsets.all(5.w), + child: Obx(() => Image( + width: 40.w, + height: 40.w, + image: state.isOpenVoice.value + ? const AssetImage( + 'images/main/icon_lockDetail_monitoringOpenVoice.png') + : const AssetImage( + 'images/main/icon_lockDetail_monitoringCloseVoice.png'))), + ), + ), + SizedBox(width: 50.w), + // 截图 + GestureDetector( + onTap: () async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + await logic.captureAndSavePng(); + } + }, + child: Container( + width: 50.w, + height: 50.w, + padding: EdgeInsets.all(5.w), + child: Image( + width: 40.w, + height: 40.w, + image: const AssetImage( + 'images/main/icon_lockDetail_monitoringScreenshot.png')), + ), + ), + SizedBox(width: 50.w), + // 录制 + GestureDetector( + onTap: () async { + logic.showToast('功能暂未开放'.tr); + // if ( + // state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // if (state.isRecordingScreen.value) { + // await logic.stopRecording(); + // } else { + // await logic.startRecording(); + // } + // } + }, + child: Container( + width: 50.w, + height: 50.w, + padding: EdgeInsets.all(5.w), + child: Image( + width: 40.w, + height: 40.w, + fit: BoxFit.fill, + image: const AssetImage( + 'images/main/icon_lockDetail_monitoringScreenRecording.png'), + ), + ), + ), + SizedBox(width: 50.w), + GestureDetector( + onTap: () { + logic.showToast('功能暂未开放'.tr); + }, + child: Image( + width: 28.w, + height: 28.w, + fit: BoxFit.fill, + image: const AssetImage('images/main/icon_lockDetail_rectangle.png'), + ), + ), + ]); + } + + Widget bottomBottomBtnWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 接听 + Obx( + () => bottomBtnItemWidget( + getAnswerBtnImg(), + getAnswerBtnName(), + Colors.white, + longPress: () async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 启动录音 + logic.startProcessingAudio(); + state.isLongPressing.value = true; + } + }, + longPressUp: () async { + // 停止录音 + logic.stopProcessingAudio(); + state.isLongPressing.value = false; + }, + onClick: () async { + if (state.talkStatus.value == + TalkStatus.passiveCallWaitingAnswer) { + // 接听 + logic.initiateAnswerCommand(); + } + }, + ), + ), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_hangUp.png', '挂断'.tr, Colors.red, + onClick: () { + // 挂断 + logic.udpHangUpAction(); + }), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_monitoringUnlock.png', + '开锁'.tr, + AppColors.mainColor, + onClick: () { + // if (state.talkStatus.value == TalkStatus.answeredSuccessfully && + // state.listData.value.length > 0) { + // logic.udpOpenDoorAction(); + logic.remoteOpenLock(); + // } + // if (UDPManage().remoteUnlock == 1) { + // logic.udpOpenDoorAction(); + // showDeletPasswordAlertDialog(context); + // } else { + // logic.showToast('请在锁设置中开启远程开锁'.tr); + // } + }, + ) + ]); + } + + String getAnswerBtnImg() { + switch (state.talkStatus.value) { + case TalkStatus.passiveCallWaitingAnswer: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + case TalkStatus.answeredSuccessfully: + case TalkStatus.proactivelyCallWaitingAnswer: + return 'images/main/icon_lockDetail_monitoringUnTalkback.png'; + default: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + } + } + + String getAnswerBtnName() { + switch (state.talkStatus.value) { + case TalkStatus.passiveCallWaitingAnswer: + return '接听'.tr; + case TalkStatus.proactivelyCallWaitingAnswer: + case TalkStatus.answeredSuccessfully: + return '长按说话'.tr; + default: + return '接听'.tr; + } + } + + Widget bottomBtnItemWidget( + String iconUrl, + String name, + Color backgroundColor, { + required Function() onClick, + Function()? longPress, + Function()? longPressUp, + }) { + double wh = 80.w; + return GestureDetector( + onTap: onClick, + onLongPress: longPress, + onLongPressUp: longPressUp, + child: SizedBox( + height: 160.w, + width: 140.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: wh, + height: wh, + constraints: BoxConstraints( + minWidth: wh, + ), + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2), + ), + padding: EdgeInsets.all(20.w), + child: Image.asset(iconUrl, fit: BoxFit.fitWidth), + ), + SizedBox(height: 20.w), + Text( + name, + style: TextStyle(fontSize: 20.sp, color: Colors.white), + textAlign: TextAlign.center, // 当文本超出指定行数时,使用省略号表示 + maxLines: 2, // 设置最大行数为1 + ) + ], + ), + ), + ); + } + + //旋转动画 + Widget buildRotationTransition() { + return Positioned( + left: ScreenUtil().screenWidth / 2 - 220.w / 2, + top: ScreenUtil().screenHeight / 2 - 220.w / 2 - 150.h, + child: GestureDetector( + child: RotationTransition( + //设置动画的旋转中心 + alignment: Alignment.center, + //动画控制器 + turns: state.animationController, + //将要执行动画的子view + child: AnimatedOpacity( + opacity: 0.5, + duration: const Duration(seconds: 2), + child: Image.asset( + 'images/main/realTime_connecting.png', + width: 220.w, + height: 220.w, + ), + ), + ), + onTap: () { + state.animationController.forward(); + }, + ), + ); + } @override void dispose() { - timer?.cancel(); - timer = null; - _sendTimer?.cancel(); - timer = null; - // talkDataRepository.dispose(); + state.animationController.dispose(); // 确保释放控制器 super.dispose(); + } } diff --git a/lib/talk/starChart/webView/h264_web_view_state.dart b/lib/talk/starChart/webView/h264_web_view_state.dart new file mode 100644 index 00000000..2ae11041 --- /dev/null +++ b/lib/talk/starChart/webView/h264_web_view_state.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_voice_processor/flutter_voice_processor.dart'; +import 'package:get/get.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class H264WebViewState { + GlobalKey globalKey = GlobalKey(); + int udpSendDataFrameNumber = 0; // 帧序号 + late AnimationController animationController; + + // webview 控制器 + late final WebViewController webViewController; + + // 获取 startChartTalkStatus 的唯一实例 + final StartChartTalkStatus startChartTalkStatus = + StartChartTalkStatus.instance; + Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 + + RxBool isShowLoading = true.obs; + + Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面 + RxInt oneMinuteTime = 0.obs; // 定时器秒数 + + RxBool isLongPressing = false.obs; // 是否长按说话 + final TalkDataRepository talkDataRepository = TalkDataRepository.instance; + RxInt lastFrameTimestamp = 0.obs; // 上一帧的时间戳,用来判断网络环境 + Rx networkStatus = + NetworkStatus.normal.obs; // 网络状态:0-正常 1-网络卡顿 2-网络延迟 3-网络丢包 + RxInt alertCount = 0.obs; // 网络状态提示计数器 + RxInt maxAlertNumber = 3.obs; // 网络状态提示最大提示次数 + RxBool isOpenVoice = true.obs; // 是否打开声音 + RxBool isRecordingScreen = false.obs; // 是否录屏中 + RxBool isRecordingAudio = false.obs; // 是否录音中 + Rx startRecordingAudioTime = DateTime.now().obs; // 开始录音时间 + Rx endRecordingAudioTime = DateTime.now().obs; // 结束录音时间 + RxInt recordingAudioTime = 0.obs; // 录音时间持续时间 + RxDouble fps = 0.0.obs; // 添加 FPS 计数 + late VoiceProcessor? voiceProcessor; // 音频处理器、录音 + final int frameLength = 320; //录音视频帧长度为640 + final int sampleRate = 8000; //录音频采样率为8000 + List recordingAudioAllFrames = []; // 录制音频的所有帧 + List lockRecordingAudioAllFrames = []; // 录制音频的所有帧 + RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) + RxBool hasAudioData = false.obs; // 是否有音频数据 + RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 +} diff --git a/pubspec.lock b/pubspec.lock index f09c8feb..40584305 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -992,8 +992,8 @@ packages: dependency: "direct main" description: path: "." - ref: main - resolved-ref: aa93729f48762421658675800be68aee27b6d8fb + ref: "807ddb8e396c2dce16919df84efe795072404dde" + resolved-ref: "807ddb8e396c2dce16919df84efe795072404dde" url: "git@code-internal.star-lock.cn:StarlockTeam/jpush_flutter.git" source: git version: "2.5.8" diff --git a/pubspec.yaml b/pubspec.yaml index 9109e0a9..885185dc 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -214,7 +214,7 @@ dependencies: jpush_flutter: git: url: git@code-internal.star-lock.cn:StarlockTeam/jpush_flutter.git - ref: main + ref: 807ddb8e396c2dce16919df84efe795072404dde #视频播放器 video_player: ^2.9.2 @@ -316,6 +316,7 @@ flutter: - images/lockType/ - assets/ - assets/html/h264.html + - assets/html/jmuxer.min.js - lan/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware