Commit 9d8dc2e60d319cc296cb9ee77c050f5c7b81829e

Authored by wuxw
1 parent cd8d442f

开发完成水电抄表

public/js/jessibuca/jessibuca.d.ts 0 → 100644
  1 +declare namespace Jessibuca {
  2 +
  3 + /** 超时信息 */
  4 + enum TIMEOUT {
  5 + /** 当play()的时候,如果没有数据返回 */
  6 + loadingTimeout = 'loadingTimeout',
  7 + /** 当播放过程中,如果超过timeout之后没有数据渲染 */
  8 + delayTimeout = 'delayTimeout',
  9 + }
  10 +
  11 + /** 错误信息 */
  12 + enum ERROR {
  13 + /** 播放错误,url 为空的时候,调用 play 方法 */
  14 + playError = 'playError',
  15 + /** http 请求失败 */
  16 + fetchError = 'fetchError',
  17 + /** websocket 请求失败 */
  18 + websocketError = 'websocketError',
  19 + /** webcodecs 解码 h265 失败 */
  20 + webcodecsH265NotSupport = 'webcodecsH265NotSupport',
  21 + /** mediaSource 解码 h265 失败 */
  22 + mediaSourceH265NotSupport = 'mediaSourceH265NotSupport',
  23 + /** wasm 解码失败 */
  24 + wasmDecodeError = 'wasmDecodeError',
  25 + }
  26 +
  27 + interface Config {
  28 + /**
  29 + * 播放器容器
  30 + * * 若为 string ,则底层调用的是 document.getElementById('id')
  31 + * */
  32 + container: HTMLElement | string;
  33 + /**
  34 + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟
  35 + */
  36 + videoBuffer?: number;
  37 + /**
  38 + * worker地址
  39 + * * 默认引用的是根目录下面的decoder.js文件 ,decoder.js 与 decoder.wasm文件必须是放在同一个目录下面。 */
  40 + decoder?: string;
  41 + /**
  42 + * 是否不使用离屏模式(提升渲染能力)
  43 + */
  44 + forceNoOffscreen?: boolean;
  45 + /**
  46 + * 是否开启当页面的'visibilityState'变为'hidden'的时候,自动暂停播放。
  47 + */
  48 + hiddenAutoPause?: boolean;
  49 + /**
  50 + * 是否有音频,如果设置`false`,则不对音频数据解码,提升性能。
  51 + */
  52 + hasAudio?: boolean;
  53 + /**
  54 + * 设置旋转角度,只支持,0(默认),180,270 三个值
  55 + */
  56 + rotate?: boolean;
  57 + /**
  58 + * 1. 当为`true`的时候:视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边。 等同于 `setScaleMode(1)`
  59 + * 2. 当为`false`的时候:视频画面完全填充canvas区域,画面会被拉伸。等同于 `setScaleMode(0)`
  60 + */
  61 + isResize?: boolean;
  62 + /**
  63 + * 1. 当为`true`的时候:视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全。等同于 `setScaleMode(2)`
  64 + */
  65 + isFullResize?: boolean;
  66 + /**
  67 + * 1. 当为`true`的时候:ws协议不检验是否以.flv为依据,进行协议解析。
  68 + */
  69 + isFlv?: boolean;
  70 + /**
  71 + * 是否开启控制台调试打
  72 + */
  73 + debug?: boolean;
  74 + /**
  75 + * 1. 设置超时时长, 单位秒
  76 + * 2. 在连接成功之前(loading)和播放中途(heart),如果超过设定时长无数据返回,则回调timeout事件
  77 + */
  78 + timeout?: number;
  79 + /**
  80 + * 1. 设置超时时长, 单位秒
  81 + * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件
  82 + */
  83 + heartTimeout?: number;
  84 + /**
  85 + * 1. 设置超时时长, 单位秒
  86 + * 2. 在连接成功之前,如果超过设定时长无数据返回,则回调timeout事件
  87 + */
  88 + loadingTimeout?: number;
  89 + /**
  90 + * 是否支持屏幕的双击事件,触发全屏,取消全屏事件
  91 + */
  92 + supportDblclickFullscreen?: boolean;
  93 + /**
  94 + * 是否显示网
  95 + */
  96 + showBandwidth?: boolean;
  97 + /**
  98 + * 配置操作按钮
  99 + */
  100 + operateBtns?: {
  101 + /** 是否显示全屏按钮 */
  102 + fullscreen?: boolean;
  103 + /** 是否显示截图按钮 */
  104 + screenshot?: boolean;
  105 + /** 是否显示播放暂停按钮 */
  106 + play?: boolean;
  107 + /** 是否显示声音按钮 */
  108 + audio?: boolean;
  109 + /** 是否显示录制按 */
  110 + record?: boolean;
  111 + };
  112 + /**
  113 + * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮
  114 + */
  115 + keepScreenOn?: boolean;
  116 + /**
  117 + * 是否开启声音,默认是关闭声音播放的
  118 + */
  119 + isNotMute?: boolean;
  120 + /**
  121 + * 加载过程中文案
  122 + */
  123 + loadingText?: string;
  124 + /**
  125 + * 背景图片
  126 + */
  127 + background?: string;
  128 + /**
  129 + * 是否开启MediaSource硬解码
  130 + * * 视频编码只支持H.264视频(Safari on iOS不支持)
  131 + * * 不支持 forceNoOffscreen 为 false (开启离屏渲染)
  132 + */
  133 + useMSE?: boolean;
  134 + /**
  135 + * 是否开启Webcodecs硬解码
  136 + * * 视频编码只支持H.264视频 (需在chrome 94版本以上,需要https或者localhost环境)
  137 + * * 支持 forceNoOffscreen 为 false (开启离屏渲染)
  138 + * */
  139 + useWCS?: boolean;
  140 + /**
  141 + * 是否开启键盘快捷键
  142 + * 目前支持的键盘快捷键有:esc -> 退出全屏;arrowUp -> 声音增加;arrowDown -> 声音减少;
  143 + */
  144 + hotKey?: boolean;
  145 + /**
  146 + * 在使用MSE或者Webcodecs 播放H265的时候,是否自动降级到wasm模式。
  147 + * 设置为false 则直接关闭播放,抛出Error 异常,设置为true 则会自动切换成wasm模式播放。
  148 + */
  149 + autoWasm?: boolean;
  150 + /**
  151 + * heartTimeout 心跳超时之后自动再播放,不再抛出异常,而直接重新播放视频地址。
  152 + */
  153 + heartTimeoutReplay?: boolean,
  154 + /**
  155 + * heartTimeoutReplay 从试次数,超过之后,不再自动播放
  156 + */
  157 + heartTimeoutReplayTimes?: number,
  158 + /**
  159 + * loadingTimeout loading之后自动再播放,不再抛出异常,而直接重新播放视频地址。
  160 + */
  161 + loadingTimeoutReplay?: boolean,
  162 + /**
  163 + * heartTimeoutReplay 从试次数,超过之后,不再自动播放
  164 + */
  165 + loadingTimeoutReplayTimes?: number
  166 + /**
  167 + * wasm解码报错之后,不再抛出异常,而是直接重新播放视频地址。
  168 + */
  169 + wasmDecodeErrorReplay?: boolean,
  170 + /**
  171 + * https://github.com/langhuihui/jessibuca/issues/152 解决方案
  172 + * 例如:WebGL图像预处理默认每次取4字节的数据,但是540x960分辨率下的U、V分量宽度是540/2=270不能被4整除,导致绿屏。
  173 + */
  174 + openWebglAlignment?: boolean
  175 + }
  176 +}
  177 +
  178 +
  179 +declare class Jessibuca {
  180 +
  181 + constructor(config?: Jessibuca.Config);
  182 +
  183 + /**
  184 + * 是否开启控制台调试打印
  185 + @example
  186 + // 开启
  187 + jessibuca.setDebug(true)
  188 + // 关闭
  189 + jessibuca.setDebug(false)
  190 + */
  191 + setDebug(flag: boolean): void;
  192 +
  193 + /**
  194 + * 静音
  195 + @example
  196 + jessibuca.mute()
  197 + */
  198 + mute(): void;
  199 +
  200 + /**
  201 + * 取消静音
  202 + @example
  203 + jessibuca.cancelMute()
  204 + */
  205 + cancelMute(): void;
  206 +
  207 + /**
  208 + * 留给上层用户操作来触发音频恢复的方法。
  209 + *
  210 + * iPhone,chrome等要求自动播放时,音频必须静音,需要由一个真实的用户交互操作来恢复,不能使用代码。
  211 + *
  212 + * https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
  213 + */
  214 + audioResume(): void;
  215 +
  216 + /**
  217 + *
  218 + * 设置超时时长, 单位秒
  219 + * 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件
  220 +
  221 + @example
  222 + jessibuca.setTimeout(10)
  223 +
  224 + jessibuca.on('timeout',function(){
  225 + //
  226 + });
  227 + */
  228 + setTimeout(): void;
  229 +
  230 + /**
  231 + * @param mode
  232 + * 0 视频画面完全填充canvas区域,画面会被拉伸 等同于参数 `isResize` 为false
  233 + *
  234 + * 1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 等同于参数 `isResize` 为true
  235 + *
  236 + * 2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 等同于参数 `isFullResize` 为true
  237 + @example
  238 + jessibuca.setScaleMode(0)
  239 +
  240 + jessibuca.setScaleMode(1)
  241 +
  242 + jessibuca.setScaleMode(2)
  243 + */
  244 + setScaleMode(mode: number): void;
  245 +
  246 + /**
  247 + * 暂停播放
  248 + *
  249 + * 可以在pause 之后,再调用 `play()`方法就继续播放之前的流。
  250 + @example
  251 + jessibuca.pause().then(()=>{
  252 + console.log('pause success')
  253 +
  254 + jessibuca.play().then(()=>{
  255 +
  256 + }).catch((e)=>{
  257 +
  258 + })
  259 +
  260 + }).catch((e)=>{
  261 + console.log('pause error',e);
  262 + })
  263 + */
  264 + pause(): Promise<void>;
  265 +
  266 + /**
  267 + * 关闭视频,不释放底层资源
  268 + @example
  269 + jessibuca.close();
  270 + */
  271 + close(): void;
  272 +
  273 + /**
  274 + * 关闭视频,释放底层资源
  275 + @example
  276 + jessibuca.destroy()
  277 + */
  278 + destroy(): void;
  279 +
  280 + /**
  281 + * 清理画布为黑色背景
  282 + @example
  283 + jessibuca.clearView()
  284 + */
  285 + clearView(): void;
  286 +
  287 + /**
  288 + * 播放视频
  289 + @example
  290 +
  291 + jessibuca.play('url').then(()=>{
  292 + console.log('play success')
  293 + }).catch((e)=>{
  294 + console.log('play error',e)
  295 + })
  296 + //
  297 + jessibuca.play()
  298 + */
  299 + play(url?: string): Promise<void>;
  300 +
  301 + /**
  302 + * 重新调整视图大小
  303 + */
  304 + resize(): void;
  305 +
  306 + /**
  307 + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟。
  308 + *
  309 + * 等同于 `videoBuffer` 参数。
  310 + *
  311 + @example
  312 + // 设置 200ms 缓冲
  313 + jessibuca.setBufferTime(0.2)
  314 + */
  315 + setBufferTime(time: number): void;
  316 +
  317 + /**
  318 + * 设置旋转角度,只支持,0(默认) ,180,270 三个值。
  319 + *
  320 + * > 可用于实现监控画面小窗和全屏效果,由于iOS没有全屏API,此方法可以模拟页面内全屏效果而且多端效果一致。 *
  321 + @example
  322 + jessibuca.setRotate(0)
  323 +
  324 + jessibuca.setRotate(90)
  325 +
  326 + jessibuca.setRotate(270)
  327 + */
  328 + setRotate(deg: number): void;
  329 +
  330 + /**
  331 + *
  332 + * 设置音量大小,取值0 — 1
  333 + *
  334 + * > 区别于 mute 和 cancelMute 方法,虽然设置setVolume(0) 也能达到 mute方法,但是mute 方法是不调用底层播放音频的,能提高性能。而setVolume(0)只是把声音设置为0 ,以达到效果。
  335 + * @param volume 当为0时,完全无声;当为1时,最大音量,默认值
  336 + @example
  337 + jessibuca.setVolume(0.2)
  338 +
  339 + jessibuca.setVolume(0)
  340 +
  341 + jessibuca.setVolume(1)
  342 + */
  343 + setVolume(volume: number): void;
  344 +
  345 + /**
  346 + * 返回是否加载完毕
  347 + @example
  348 + var result = jessibuca.hasLoaded()
  349 + console.log(result) // true
  350 + */
  351 + hasLoaded(): boolean;
  352 +
  353 + /**
  354 + * 开启屏幕常亮,在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮。
  355 + * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面
  356 + * 其余平台为模拟实现,此时为兼容实现,并不保证所有浏览器都支持
  357 + @example
  358 + jessibuca.setKeepScreenOn()
  359 + */
  360 + setKeepScreenOn(): boolean;
  361 +
  362 + /**
  363 + * 全屏(取消全屏)播放视频
  364 + @example
  365 + jessibuca.setFullscreen(true)
  366 + //
  367 + jessibuca.setFullscreen(false)
  368 + */
  369 + setFullscreen(flag: boolean): void;
  370 +
  371 + /**
  372 + *
  373 + * 截图,调用后弹出下载框保存截图
  374 + * @param filename 可选参数, 保存的文件名, 默认 `时间戳`
  375 + * @param format 可选参数, 截图的格式,可选png或jpeg或者webp ,默认 `png`
  376 + * @param quality 可选参数, 当格式是jpeg或者webp时,压缩质量,取值0 ~ 1 ,默认 `0.92`
  377 + * @param type 可选参数, 可选download或者base64或者blob,默认`download`
  378 +
  379 + @example
  380 +
  381 + jessibuca.screenshot("test","png",0.5)
  382 +
  383 + const base64 = jessibuca.screenshot("test","png",0.5,'base64')
  384 +
  385 + const fileBlob = jessibuca.screenshot("test",'blob')
  386 + */
  387 + screenshot(filename?: string, format?: string, quality?: number, type?: string): void;
  388 +
  389 + /**
  390 + * 开始录制。
  391 + * @param fileName 可选,默认时间戳
  392 + * @param fileType 可选,默认webm,支持webm 和mp4 格式
  393 +
  394 + @example
  395 + jessibuca.startRecord('xxx','webm')
  396 + */
  397 + startRecord(fileName: string, fileType: string): void;
  398 +
  399 + /**
  400 + * 暂停录制并下载。
  401 + @example
  402 + jessibuca.stopRecordAndSave()
  403 + */
  404 + stopRecordAndSave(): void;
  405 +
  406 + /**
  407 + * 返回是否正在播放中状态。
  408 + @example
  409 + var result = jessibuca.isPlaying()
  410 + console.log(result) // true
  411 + */
  412 + isPlaying(): boolean;
  413 +
  414 + /**
  415 + * 返回是否静音。
  416 + @example
  417 + var result = jessibuca.isMute()
  418 + console.log(result) // true
  419 + */
  420 + isMute(): boolean;
  421 +
  422 + /**
  423 + * 返回是否正在录制。
  424 + @example
  425 + var result = jessibuca.isRecording()
  426 + console.log(result) // true
  427 + */
  428 + isRecording(): boolean;
  429 +
  430 +
  431 + /**
  432 + * 监听 jessibuca 初始化事件
  433 + * @example
  434 + * jessibuca.on("load",function(){console.log('load')})
  435 + */
  436 + on(event: 'load', callback: () => void): void;
  437 +
  438 + /**
  439 + * 视频播放持续时间,单位ms
  440 + * @example
  441 + * jessibuca.on('timeUpdate',function (ts) {console.log('timeUpdate',ts);})
  442 + */
  443 + on(event: 'timeUpdate', callback: () => void): void;
  444 +
  445 + /**
  446 + * 当解析出视频信息时回调,2个回调参数
  447 + * @example
  448 + * jessibuca.on("videoInfo",function(data){console.log('width:',data.width,'height:',data.width)})
  449 + */
  450 + on(event: 'videoInfo', callback: (data: {
  451 + /** 视频宽 */
  452 + width: number;
  453 + /** 视频高 */
  454 + height: number;
  455 + }) => void): void;
  456 +
  457 + /**
  458 + * 当解析出音频信息时回调,2个回调参数
  459 + * @example
  460 + * jessibuca.on("audioInfo",function(data){console.log('numOfChannels:',data.numOfChannels,'sampleRate',data.sampleRate)})
  461 + */
  462 + on(event: 'audioInfo', callback: (data: {
  463 + /** 声频通道 */
  464 + numOfChannels: number;
  465 + /** 采样率 */
  466 + sampleRate: number;
  467 + }) => void): void;
  468 +
  469 + /**
  470 + * 信息,包含错误信息
  471 + * @example
  472 + * jessibuca.on("log",function(data){console.log('data:',data)})
  473 + */
  474 + on(event: 'log', callback: () => void): void;
  475 +
  476 + /**
  477 + * 错误信息
  478 + * @example
  479 + * jessibuca.on("error",function(error){
  480 + if(error === Jessibuca.ERROR.fetchError){
  481 + //
  482 + }
  483 + else if(error === Jessibuca.ERROR.webcodecsH265NotSupport){
  484 + //
  485 + }
  486 + console.log('error:',error)
  487 + })
  488 + */
  489 + on(event: 'error', callback: (err: Jessibuca.ERROR) => void): void;
  490 +
  491 + /**
  492 + * 当前网速, 单位KB 每秒1次,
  493 + * @example
  494 + * jessibuca.on("kBps",function(data){console.log('kBps:',data)})
  495 + */
  496 + on(event: 'kBps', callback: (value: number) => void): void;
  497 +
  498 + /**
  499 + * 渲染开始
  500 + * @example
  501 + * jessibuca.on("start",function(){console.log('start render')})
  502 + */
  503 + on(event: 'start', callback: () => void): void;
  504 +
  505 + /**
  506 + * 当设定的超时时间内无数据返回,则回调
  507 + * @example
  508 + * jessibuca.on("timeout",function(error){console.log('timeout:',error)})
  509 + */
  510 + on(event: 'timeout', callback: (error: Jessibuca.TIMEOUT) => void): void;
  511 +
  512 + /**
  513 + * 当play()的时候,如果没有数据返回,则回调
  514 + * @example
  515 + * jessibuca.on("loadingTimeout",function(){console.log('timeout')})
  516 + */
  517 + on(event: 'loadingTimeout', callback: () => void): void;
  518 +
  519 + /**
  520 + * 当播放过程中,如果超过timeout之后没有数据渲染,则抛出异常。
  521 + * @example
  522 + * jessibuca.on("delayTimeout",function(){console.log('timeout')})
  523 + */
  524 + on(event: 'delayTimeout', callback: () => void): void;
  525 +
  526 + /**
  527 + * 当前是否全屏
  528 + * @example
  529 + * jessibuca.on("fullscreen",function(flag){console.log('is fullscreen',flag)})
  530 + */
  531 + on(event: 'fullscreen', callback: () => void): void;
  532 +
  533 + /**
  534 + * 触发播放事件
  535 + * @example
  536 + * jessibuca.on("play",function(flag){console.log('play')})
  537 + */
  538 + on(event: 'play', callback: () => void): void;
  539 +
  540 + /**
  541 + * 触发暂停事件
  542 + * @example
  543 + * jessibuca.on("pause",function(flag){console.log('pause')})
  544 + */
  545 + on(event: 'pause', callback: () => void): void;
  546 +
  547 + /**
  548 + * 触发声音事件,返回boolean值
  549 + * @example
  550 + * jessibuca.on("mute",function(flag){console.log('is mute',flag)})
  551 + */
  552 + on(event: 'mute', callback: () => void): void;
  553 +
  554 + /**
  555 + * 流状态统计,流开始播放后回调,每秒1次。
  556 + * @example
  557 + * jessibuca.on("stats",function(s){console.log("stats is",s)})
  558 + */
  559 + on(event: 'stats', callback: (stats: {
  560 + /** 当前缓冲区时长,单位毫秒 */
  561 + buf: number;
  562 + /** 当前视频帧率 */
  563 + fps: number;
  564 + /** 当前音频码率,单位byte */
  565 + abps: number;
  566 + /** 当前视频码率,单位byte */
  567 + vbps: number;
  568 + /** 当前视频帧pts,单位毫秒 */
  569 + ts: number;
  570 + }) => void): void;
  571 +
  572 + /**
  573 + * 渲染性能统计,流开始播放后回调,每秒1次。
  574 + * @param performance 0: 表示卡顿,1: 表示流畅,2: 表示非常流程
  575 + * @example
  576 + * jessibuca.on("performance",function(performance){console.log("performance is",performance)})
  577 + */
  578 + on(event: 'performance', callback: (performance: 0 | 1 | 2) => void): void;
  579 +
  580 + /**
  581 + * 录制开始的事件
  582 +
  583 + * @example
  584 + * jessibuca.on("recordStart",function(){console.log("record start")})
  585 + */
  586 + on(event: 'recordStart', callback: () => void): void;
  587 +
  588 + /**
  589 + * 录制结束的事件
  590 +
  591 + * @example
  592 + * jessibuca.on("recordEnd",function(){console.log("record end")})
  593 + */
  594 + on(event: 'recordEnd', callback: () => void): void;
  595 +
  596 + /**
  597 + * 录制的时候,返回的录制时长,1s一次
  598 +
  599 + * @example
  600 + * jessibuca.on("recordingTimestamp",function(timestamp){console.log("recordingTimestamp is",timestamp)})
  601 + */
  602 + on(event: 'recordingTimestamp', callback: (timestamp: number) => void): void;
  603 +
  604 + /**
  605 + * 监听调用play方法 经过 初始化-> 网络请求-> 解封装 -> 解码 -> 渲染 一系列过程的时间消耗
  606 + * @param event
  607 + * @param callback
  608 + */
  609 + on(event: 'playToRenderTimes', callback: (times: {
  610 + playInitStart: number, // 1 初始化
  611 + playStart: number, // 2 初始化
  612 + streamStart: number, // 3 网络请求
  613 + streamResponse: number, // 4 网络请求
  614 + demuxStart: number, // 5 解封装
  615 + decodeStart: number, // 6 解码
  616 + videoStart: number, // 7 渲染
  617 + playTimestamp: number,// playStart- playInitStart
  618 + streamTimestamp: number,// streamStart - playStart
  619 + streamResponseTimestamp: number,// streamResponse - streamStart
  620 + demuxTimestamp: number, // demuxStart - streamResponse
  621 + decodeTimestamp: number, // decodeStart - demuxStart
  622 + videoTimestamp: number,// videoStart - decodeStart
  623 + allTimestamp: number // videoStart - playInitStart
  624 + }) => void): void
  625 +
  626 + /**
  627 + * 监听方法
  628 + *
  629 + @example
  630 +
  631 + jessibuca.on("load",function(){console.log('load')})
  632 + */
  633 + on(event: string, callback: Function): void;
  634 +
  635 +}
  636 +
  637 +export default Jessibuca;
public/js/jessibuca/jessibuca.js 0 → 100644
No preview for this file type
public/js/jessibuca/renderer.js 0 → 100644
  1 +!(function () {
  2 + /**
  3 + * @param opt
  4 + * container: DOM 容器
  5 + * contextOptions:
  6 + * videoBuffer:
  7 + * forceNoGL:
  8 + * isNotMute:
  9 + * decoder:
  10 + * @constructor
  11 + */
  12 + function Jessibuca(opt) {
  13 + this._opt = opt;
  14 +
  15 + if (typeof opt.container === "string") {
  16 + this._opt.container = document.getElementById(opt.container);
  17 + }
  18 + if (!this._opt.container) {
  19 + throw new Error('Jessibuca need container option');
  20 + return;
  21 + }
  22 +
  23 + this._canvasElement = document.createElement("canvas");
  24 + this._canvasElement.style.position = "absolute";
  25 + this._canvasElement.style.top = 0;
  26 + this._canvasElement.style.left = 0;
  27 + this._opt.container.appendChild(this._canvasElement);
  28 + this._container = this._opt.container;
  29 + this._container.style.overflow = "hidden";
  30 + this._containerOldPostion = {
  31 + position: this._container.style.position,
  32 + top: this._container.style.top,
  33 + left: this._container.style.left,
  34 + width: this._container.style.width,
  35 + height: this._container.style.height
  36 + }
  37 + if (this._containerOldPostion.position != "absolute") {
  38 + this._container.style.position = "relative"
  39 + }
  40 + this._opt.videoBuffer = opt.videoBuffer || 0;
  41 + this._opt.text = opt.text || '';
  42 + //
  43 + this._opt.isResize = opt.isResize === false ? opt.isResize : true;
  44 + this._opt.isFullResize = opt.isFullResize === true ? opt.isFullResize : false;
  45 + this._opt.isDebug = opt.debug === true;
  46 + this._opt.timeout = typeof opt.timeout === 'number' ? opt.timeout : 30;
  47 + this._opt.supportDblclickFullscreen = opt.supportDblclickFullscreen === true;
  48 + this._opt.showBandwidth = opt.showBandwidth === true;
  49 + this._opt.operateBtns = Object.assign({
  50 + fullscreen: false,
  51 + screenshot: false,
  52 + play: false,
  53 + audio: false
  54 + }, opt.operateBtns || {});
  55 + this._opt.keepScreenOn = opt.keepScreenOn === true;
  56 +
  57 + if (!opt.forceNoGL) this._initContextGL();
  58 + this._audioContext = new (window.AudioContext || window.webkitAudioContext)();
  59 + this._audioEnabled(true);
  60 + if (!opt.isNotMute) this._audioEnabled(false);
  61 + if (this._contextGL) {
  62 + this._initProgram();
  63 + this._initBuffers();
  64 + this._initTextures();
  65 + }
  66 + this._onresize = () => this.resize();
  67 + this._onfullscreenchange = () => this._fullscreenchange();
  68 + window.addEventListener("resize", this._onresize);
  69 + document.addEventListener('fullscreenchange', this._onfullscreenchange);
  70 + this._decoderWorker = new Worker(opt.decoder || 'ff.js')
  71 + var _this = this;
  72 + this._hasLoaded = false;
  73 + this._stats = {
  74 + buf: 0,
  75 + fps: 0,
  76 + abps: '',
  77 + vbps: '',
  78 + ts: ''
  79 + };
  80 +
  81 + if (this._opt.supportDblclickFullscreen) {
  82 + this._canvasElement.addEventListener('dblclick', function () {
  83 + _this.fullscreen = !_this.fullscreen;
  84 + }, false);
  85 + }
  86 + this.onPlay = noop;
  87 + this.onPause = noop;
  88 + this.onRecord = noop;
  89 + this.onFullscreen = noop;
  90 + this.onMute = noop;
  91 + this.onLoad = noop;
  92 + this.onLog = noop;
  93 + this.onError = noop;
  94 + this.onTimeUpdate = noop;
  95 + this.onInitSize = noop;
  96 + this._onMessage();
  97 + this._initDom();
  98 + this._initStatus();
  99 + this._initEventListener();
  100 + this._hideBtns();
  101 + //
  102 + this._initWakeLock();
  103 + this._enableWakeLock();
  104 + };
  105 +
  106 + function noop() {
  107 +
  108 + }
  109 +
  110 + Jessibuca.prototype._initDom = function () {
  111 + var playBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAARVJREFUSMe9laEOglAUhs+5k9lJFpsJ5QWMJoNGbEY0mEy+gr6GNo0a3SiQCegMRILzGdw4hl+Cd27KxPuXb2zA/91z2YXoGRERkX4fvN3A2QxUiv4dFM3n8jZRBLbbVfd+ubJuF4xjiCyXkksueb1uSKCIZYGLBTEx8ekEoV7PkICeVgs8HiGyXoO2bUigCDM4HoPnM7bI8wwJ6Gk0sEXbLSay30Oo2TQkoGcwgFCSQMhxDAvoETEscDiQkJC4LjMz8+XyZ4HrFYWjEQqHQ1asWGWZfmdFAsVINxuw00HhbvfpydpvxWkKTqdYaRCUfUPJCdzv4Gr1uqfli0tOIAzByUT/iCrL6+84y3Bw+D6ui5Ou+jwA8FnIO++FACgAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAEl0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvYm9mYW5nLnN2Z8fICi0AAAAASUVORK5CYII=';
  112 + var pauseBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAHVJREFUSMftkCESwCAMBEOnCtdXVMKHeC7oInkEeQJXkRoEZWraipxZc8lsQqQZBACAlIS1oqGhhTCdu3oyxyyMcdRf79c5J7SWDBky+z4173rbJvR+VF/e/qwKqIAKqMBDgZyFzAQCoZTpxq7HLDyOrw/9b07l3z4dDnI2IAAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMIo/vCcAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDD7YgSbAAAASnRFWHRzdmc6YmFzZS11cmkAZmlsZTovLy9ob21lL2FkbWluL2ljb24tZm9udC90bXAvaWNvbl9nZjcwMHM3YjNmdy96YW50aW5nLnN2ZxqNZJkAAAAASUVORK5CYII=';
  113 + var screenshotBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAaxJREFUSMfNlLFOAkEQhmevAZMjR6OGRBJKsFBzdkYNpYSaWkopIOFRCBWh1ieA+ALGRgutjK0HzV2H5SX7W/zsmY3cnTEhcZovOzcz9+/s7Ir8d4OGht7fBwAgjvEri2OTl1ffSf0xAMBxRIkS1e3Se3+vcszEMe/6OqmT/aN2m1wsNu/o5YVsNHI7BgA4PCRfXzfXCwKy1RLbcXZG9nrkzc12jvT8nPU/PtatOThgAx8fuS4WyZ0de2e+T87n5OcnuVqRsxl5cpImQDnKUc7DA1fVqpimZCu+vCSjiNH9PlmpJNTQ0INBErfeafZRAakC6FWKfH9nwU7H/l6rGdqCOx3y7c3U+aOARsMMp+1vNskwTLjulB23XJL1epqA9OshIiKeJxAIoug7UyA4OuLi6Ynr52deu+NjOy4MSc9Ln8rMDpTLybBpaOjdXbJUIqdTm8a/t2fn/RSQewR24HicTLmGhnbdzcPquvYtGY3+PIR24UKBUXd35v6Sk4lN47+9NXm/FBAEedfGTjw9JYdDm76fm6+hoS8ujGAxT6L9Im7bTKeurvIEb92+AES1b6x283XSAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABJdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L2NhbWVyYS5zdmeyubWEAAAAAElFTkSuQmCC';
  114 + var fullscreenBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAALZJREFUSMftVbsORUAQVSj8DomChvh3lU5CoSVCQq2RObeYu8XG3deVoHCak81kds7Oaz3vxRcAAMwztOg6vX9d6/3XFQQC+b7iAoFhYE7Tvx9EIFAcy/ftO3MQGAQkCfM4MmeZWyajiLnvmYuCeduMAuSzvRBVYNluFHCssSgFp7Sq9ALKkjnPf9ubRtkDL27HNT3QtsY9cAjsNAVheHIKBOwD2wpxFHDbJpwmaHH2L1iWx+2BDy8RbXXtqbRBAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABTdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L3F1YW5waW5nenVpZGFodWEuc3ZnTBoI7AAAAABJRU5ErkJggg==';
  115 + var minScreenBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAYJJREFUSMfdVbGKwkAQnQn+geAfWBixUTsVgp3YGKxSWflVNmIjARULwc5KO40ipNHWRgs/wGLniucKa+Jd5ODuuGle5u3szGRmd5bor4iIiMhuB3Sc+HXXBdp2/Lpta7v4dccRJUrUdhtNQIkSVa3C8HwG1uumg34f2OnEB+h0tF1Sv5b+YIsttpZLEhKSdhvscPi8IXFF74GJiYnHY7Cex8zMvFgkbInjmJnv98kqoO30vmhLtaRMB60WtEbDNDudgMUiKiQSzfjOMzFxoQAyCPSfw7/nQZ/PUYnpNGV6OR6BmYzJbzYIoBQCzGaRBDQvJCTdLnTLolg5HN5t6f8V1h/oUT4PrVKJWBotmEzQw+vV3J9Ow851P2/BaoX9Yfh0BrJZYKlk8uUyHOpDeLuBHwzMBJtN2PV6IPUhXK9Nf5cLMAxfluanrmGkRBggtRo03wfq66P/6CsJAnOg+f6rgfZI4BGYiYlHIx048eR6krcnq34kkj1GuVz8+jceo9+SD5A8yGh8CTq7AAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABNdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L3p1aXhpYW9odWEuc3ZnoCFr0AAAAABJRU5ErkJggg==';
  116 + var quietBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAR9JREFUSMfVlD0LglAYhe9VkwgNihpsjbYQf4JTS7+iuaGxpcGfJjS0NFRLk2NDi6MogafhJGRIX9yEzvJwrx/nvPd9VYh/F3LkyBuN2g3J1QoAgCQhPe/Hxq5Lo+0WlfJ9dYYAgGaTDAIyy/BUnwcwWJlhcLnZkN2ugIBAuy2kkEL2ep8F73S4kjfFcfn6cMj9KLodrWVBiXyf75tMyOOR+4MBOZ8XLXzorboA5UpnM/J0Ivd7+vX7xX2asqGpVKtFXi5sqWmypXefrfIWAACmU/JwKCoun8hu9zA0uk6u13wgirg+n7+bAcsibbt6SB3n9TQXPxwAwHJJpum7M6BcDDQa0SgMaw9QPkJNIxcLMo4ZcDz+eYDqQFLWbqxKV57EtW1WtMbmAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAwij+8JwAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wMS0wOFQxNjo0Mjo1MyswODowMPtiBJsAAABKdEVYdHN2ZzpiYXNlLXVyaQBmaWxlOi8vL2hvbWUvYWRtaW4vaWNvbi1mb250L3RtcC9pY29uX2dmNzAwczdiM2Z3L2ppbmd5aW4uc3ZnIlMYaQAAAABJRU5ErkJggg==';
  117 + var playAudioBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAU5JREFUSMftkzGKwlAURf9PULBQwULSCKK1bZAgNuoaFFyAC3AdZg0uQCwshWzAShEEO7Gy0soUCu9Occ3An5nMGCfdzGsO7+Xy3/03iVL/lbAAACiVIBCI77O37Vi9QCDZbEqLm03ycEBUAoHk818v7nYpul5Jz4tf8HBKYa1mcjwmbzd8rG8NFIsU7ffk8UjmcjE3XK+RtB4G2PT75GbDeblMttumfjSKMRCGLxsQCKTReE9KIJDJxDw/SmKxiOZWWh+ntrSlre2WXRAorbTSrZapip7X66kbMKtQUFBQCENznsmQ93vqBhh5r8fO85jAcsnIrcce1yV3uxgD8zl5uZgU+dGBVlrp6GbTKRPwffaDAek45Gz2/M0AAJ0OeTol+w0rFYrOZ3K1MhNJEjEAwHF4cBA8Z8B1zcXV6msv+JMR2yaHQ1LrXx/8Z+sNRxsWcwZeb6UAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAEt0RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvc2hlbmd5aW4uc3ZnFog1MQAAAABJRU5ErkJggg==';
  118 + var recordBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAPRJREFUSMflVDEOwjAQO0e8gr2sZYVunREbD6ISfAgmkBjpC/hBEQ+AtTWD6QAI0gBlqRfLp+TiXC5n1nXgMUCS5HBoNBqj6IOMMFwuEpsNAABl6d3HihWrOJaBsuRPkGW+c929HAxuYefb6L+R0ZgkMrJYiItCnCT1sl5Y1jwXj0bNniJNJWqujfX7LyrwJh8AYDxWgulU0dPp20IFlxoODm61kpE4VnS9/puBXyPYgH7LbKY3PhwUnUw+NdC4CdW9+71UgyZspwIBB9No3O0klktxUahyx+Pz+lYG0Xzu84lXRqTqwRQAGAzns8R223gUdxZXGcAK5Hp0ClIAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6NDI6NTMrMDg6MDCKP7wnAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjQyOjUzKzA4OjAw+2IEmwAAAE50RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fZ2Y3MDBzN2IzZncvbHV6aGlzaGlwaW4uc3Zn5Zd7GQAAAABJRU5ErkJggg==';
  119 + var recordingBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAAAAAAAA+UO7fwAAAAlwSFlzAAAASAAAAEgARslrPgAAAahJREFUSMdjYBjpgBFd4NZK+f+soQYG//T+yzFuUFUl2cApjEWM/758UZvysPDn3127GBkZGBgY/v4l6ICb9xTWsRbp6/9f9W8N44Jz5xgCGI4wfGFiIttrR/5n/3/U3KyR8rj8t0RdHS5lcAv+//yXzzhZTY1ii2FAmsGZocna+maD3GnWY62tNzbJBbDOffLkxie5eJYwa2uYMhaigzb2/zyGguPH/y9mTGKYYGlJUIMiYxDjHCen/4oMDAxznJzg4k8Z/jP+l5LCCAFCQP30Y5dfXVZWDI7/zzIs8PNjNGJ4/7/r+XNKA4rkoNZ4/lj0V9TmzUxJv0J+F+jrM3YyvPq/acsWujmA2oBkB9y4LifLxhoa+teAzYFtwtWr/8sZxBj9fHxo7oCbprJ72MqOHWNgZGBkYFy1isGGoZahTFSU0hAgOhcQnfph4P7/df9T9u1jPMn4nyHmxIn/bAzLGe7GxTHsZyj+f+zpUwYGBmmG6bQsiMr+L/v/rqlJY9Njm9889fW4lGEUxXCHwAomUgH3vxBG8c+f1WWf9P98sns3oaJ4FAAAbtWqHTT84QYAAAAldEVYdGRhdGU6Y3JlYXRlADIwMjEtMDEtMDhUMTY6MzU6MjMrMDg6MDBLHbvEAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIxLTAxLTA4VDE2OjM1OjIzKzA4OjAwOkADeAAAAE50RVh0c3ZnOmJhc2UtdXJpAGZpbGU6Ly8vaG9tZS9hZG1pbi9pY29uLWZvbnQvdG1wL2ljb25fcTM1YTFhNHBtY2MvbHV6aGlzaGlwaW4uc3Zn6xlv1QAAAABJRU5ErkJggg==';
  120 + var gifBase64 = 'data:image/gif;base64,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';
  121 + var playBigBase64 = 'data:image/png;base64,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';
  122 +
  123 + function _setStyle(dom, cssObj) {
  124 + Object.keys(cssObj).forEach(function (key) {
  125 + dom.style[key] = cssObj[key];
  126 + })
  127 + }
  128 +
  129 + var doms = {};
  130 +
  131 + var fragment = document.createDocumentFragment();
  132 + var btnWrap = document.createElement('div');
  133 + var control1 = document.createElement('div');
  134 + var control2 = document.createElement('div');
  135 + var textDom = document.createElement('div');
  136 + var speedDom = document.createElement('div');
  137 + var playDom = document.createElement('div');
  138 + var playBigDom = document.createElement('div');
  139 + var pauseDom = document.createElement('div');
  140 + var screenshotsDom = document.createElement('div');
  141 + var fullscreenDom = document.createElement('div');
  142 + var minScreenDom = document.createElement('div');
  143 + var loadingDom = document.createElement('div');
  144 + var loadingTextDom = document.createElement('div');
  145 + var quietAudioDom = document.createElement('div');
  146 + var playAudioDom = document.createElement('div');
  147 + var recordDom = document.createElement('div');
  148 + var recordingDom = document.createElement('div');
  149 + var bgDom = document.createElement('div');
  150 +
  151 + loadingTextDom.innerText = this._opt.loadingText || '';
  152 + textDom.innerText = this._opt.text || '';
  153 + speedDom.innerText = '';
  154 + playDom.title = '播放';
  155 + pauseDom.title = '暂停';
  156 + screenshotsDom.title = '截屏';
  157 + fullscreenDom.title = '全屏';
  158 + minScreenDom.title = '退出全屏';
  159 + quietAudioDom.title = '静音';
  160 + playAudioDom.title = '取消静音';
  161 + recordDom.title = '录制';
  162 + recordingDom.title = '取消录制';
  163 +
  164 + var wrapStyle = {
  165 + height: '38px',
  166 + zIndex: 11,
  167 + position: 'absolute',
  168 + left: 0,
  169 + bottom: 0,
  170 + width: '100%',
  171 + background: 'rgba(0,0,0)'
  172 + };
  173 +
  174 + var bgStyle = {
  175 + position: 'absolute',
  176 + width: '100%',
  177 + height: '100%',
  178 + };
  179 +
  180 + if (this._opt.background) {
  181 + bgStyle = Object.assign({}, bgStyle, {
  182 + backgroundRepeat: "no-repeat",
  183 + backgroundPosition: "center",
  184 + backgroundSize: '100%',
  185 + backgroundImage: "url('" + this._opt.background + "')"
  186 + })
  187 + }
  188 +
  189 + //
  190 + var loadingStyle = {
  191 + position: 'absolute',
  192 + width: '100%',
  193 + height: '100%',
  194 + textAlign: 'center',
  195 + color: "#fff",
  196 + display: 'none',
  197 + backgroundImage: "url('" + gifBase64 + "')",
  198 + backgroundRepeat: "no-repeat",
  199 + backgroundPosition: "center",
  200 + backgroundSize: "40px 40px",
  201 + };
  202 +
  203 + var playBigStyle = {
  204 + position: 'absolute',
  205 + width: '100%',
  206 + height: '100%',
  207 + display: 'none',
  208 + background: 'rgba(0,0,0,0.4)',
  209 + backgroundImage: "url('" + playBigBase64 + "')",
  210 + backgroundRepeat: "no-repeat",
  211 + backgroundPosition: "center",
  212 + backgroundSize: "48px 48px",
  213 + cursor: "pointer"
  214 + };
  215 +
  216 + var loadingTextStyle = {
  217 + position: 'absolute',
  218 + width: "100%",
  219 + top: '60%',
  220 + textAlign: 'center',
  221 + }
  222 + var controlStyle = {
  223 + position: 'absolute',
  224 + top: 0,
  225 + height: '100%',
  226 + display: 'flex',
  227 + alignItems: 'center',
  228 + };
  229 + var styleObj = {
  230 + display: 'none',
  231 + position: 'relative',
  232 + fontSize: '13px',
  233 + color: '#fff',
  234 + lineHeight: '20px',
  235 + marginLeft: '5px',
  236 + marginRight: '5px',
  237 + userSelect: 'none'
  238 + };
  239 + var styleObj2 = {
  240 + display: 'none',
  241 + position: 'relative',
  242 + width: '16px',
  243 + height: '16px',
  244 + marginLeft: '8px',
  245 + marginRight: '8px',
  246 + backgroundRepeat: "no-repeat",
  247 + backgroundPosition: "center",
  248 + backgroundSize: '100%',
  249 + cursor: 'pointer',
  250 + };
  251 + _setStyle(bgDom, bgStyle);
  252 + _setStyle(btnWrap, wrapStyle);
  253 + _setStyle(loadingDom, loadingStyle);
  254 + _setStyle(playBigDom, playBigStyle);
  255 + _setStyle(loadingTextDom, loadingTextStyle);
  256 + _setStyle(control1, Object.assign({}, controlStyle, {
  257 + left: 0
  258 + }));
  259 + _setStyle(control2, Object.assign({}, controlStyle, {
  260 + right: 0
  261 + }));
  262 + _setStyle(textDom, styleObj);
  263 + _setStyle(speedDom, styleObj);
  264 + _setStyle(playDom, Object.assign({}, styleObj2, {
  265 + backgroundImage: "url('" + playBase64 + "')",
  266 + }));
  267 +
  268 + _setStyle(pauseDom, Object.assign({}, styleObj2, {
  269 + backgroundImage: "url('" + pauseBase64 + "')"
  270 + }));
  271 +
  272 + _setStyle(screenshotsDom, Object.assign({}, styleObj2, {
  273 + backgroundImage: "url('" + screenshotBase64 + "')"
  274 + }));
  275 +
  276 + _setStyle(fullscreenDom, Object.assign({}, styleObj2, {
  277 + backgroundImage: "url('" + fullscreenBase64 + "')"
  278 + }));
  279 +
  280 + _setStyle(minScreenDom, Object.assign({}, styleObj2, {
  281 + backgroundImage: "url('" + minScreenBase64 + "')"
  282 + }));
  283 +
  284 + _setStyle(quietAudioDom, Object.assign({}, styleObj2, {
  285 + backgroundImage: "url('" + quietBase64 + "')"
  286 + }));
  287 +
  288 + _setStyle(playAudioDom, Object.assign({}, styleObj2, {
  289 + backgroundImage: "url('" + playAudioBase64 + "')"
  290 + }));
  291 +
  292 + _setStyle(recordDom, Object.assign({}, styleObj2, {
  293 + backgroundImage: "url('" + recordBase64 + "')"
  294 + }));
  295 +
  296 + _setStyle(recordingDom, Object.assign({}, styleObj2, {
  297 + backgroundImage: "url('" + recordingBase64 + "')"
  298 + }));
  299 +
  300 + loadingDom.appendChild(loadingTextDom);
  301 + if (this._opt.text) {
  302 + control1.appendChild(textDom);
  303 + doms.textDom = textDom;
  304 + }
  305 + if (this._opt.showBandwidth) {
  306 + control1.appendChild(speedDom);
  307 + doms.speedDom = speedDom;
  308 + }
  309 +
  310 + // record
  311 + //control2.appendChild(recordingDom);
  312 + //control2.appendChild(recordDom);
  313 +
  314 + // screenshots
  315 + if (this._opt.operateBtns.screenshot) {
  316 + control2.appendChild(screenshotsDom);
  317 + doms.screenshotsDom = screenshotsDom;
  318 + }
  319 +
  320 + // play stop
  321 + if (this._opt.operateBtns.play) {
  322 + control2.appendChild(playDom);
  323 + control2.appendChild(pauseDom);
  324 + doms.playDom = playDom;
  325 + doms.pauseDom = pauseDom;
  326 + }
  327 +
  328 + // audio
  329 + if (this._opt.operateBtns.audio) {
  330 + control2.appendChild(playAudioDom);
  331 + control2.appendChild(quietAudioDom);
  332 + doms.playAudioDom = playAudioDom;
  333 + doms.quietAudioDom = quietAudioDom;
  334 + }
  335 +
  336 + // fullscreen
  337 + if (this._opt.operateBtns.fullscreen) {
  338 + control2.appendChild(fullscreenDom);
  339 + control2.appendChild(minScreenDom);
  340 + doms.fullscreenDom = fullscreenDom;
  341 + doms.minScreenDom = minScreenDom;
  342 + }
  343 +
  344 + btnWrap.appendChild(control1);
  345 + btnWrap.appendChild(control2);
  346 +
  347 + fragment.appendChild(bgDom);
  348 + doms.bgDom = bgDom;
  349 + fragment.appendChild(loadingDom);
  350 + doms.loadingDom = loadingDom;
  351 + if (this._showControl()) {
  352 + fragment.appendChild(btnWrap);
  353 + }
  354 + if (this._opt.operateBtns.play) {
  355 + fragment.appendChild(playBigDom);
  356 + doms.playBigDom = playBigDom;
  357 + }
  358 + this._container.appendChild(fragment);
  359 + this._doms = doms;
  360 + };
  361 +
  362 + Jessibuca.prototype._initWakeLock = function () {
  363 + this._wakeLock = null;
  364 + var _this = this;
  365 + var handleWakeLock = () => {
  366 + if (this._wakeLock !== null && "visible" === document.visibilityState) {
  367 + _this._enableWakeLock();
  368 + }
  369 + };
  370 +
  371 + document.addEventListener('visibilitychange', handleWakeLock);
  372 + document.addEventListener('fullscreenchange', handleWakeLock);
  373 + };
  374 +
  375 + Jessibuca.prototype._enableWakeLock = function () {
  376 + if (this._opt.keepScreenOn) {
  377 + if ("wakeLock" in navigator) {
  378 + var _this = this;
  379 + navigator.wakeLock.request("screen").then((lock) => {
  380 + _this._wakeLock = lock;
  381 + _this._wakeLock.addEventListener('release', function () {
  382 + });
  383 + })
  384 + }
  385 + }
  386 + };
  387 +
  388 +
  389 + Jessibuca.prototype._initGainNode = function () {
  390 + var gainNode = this._audioContext.createGain();
  391 + var _this = this;
  392 + var source;
  393 + if (!navigator.mediaDevices.getUserMedia) {
  394 + console.log('getUserMedia not supported on your browser!');
  395 + return;
  396 + }
  397 +
  398 + navigator.mediaDevices.getUserMedia(
  399 + // constraints - only audio needed for this app
  400 + {
  401 + audio: true
  402 + },
  403 +
  404 + // Success callback
  405 + function (stream) {
  406 + source = _this._audioContext.createMediaStreamSource(stream);
  407 + source.connect(gainNode);
  408 + gainNode.connect(_this._audioContext.destination);
  409 + _this._gainNode = gainNode;
  410 + },
  411 +
  412 + // Error callback
  413 + function (err) {
  414 + console.log('The following gUM error occurred: ' + err);
  415 + }
  416 + );
  417 + };
  418 +
  419 + Jessibuca.prototype._showControl = function () {
  420 + var result = false;
  421 +
  422 + var hasBtnShow = false;
  423 + Object.keys(this._opt.operateBtns).forEach((key) => {
  424 + if (this._opt.operateBtns[key]) {
  425 + hasBtnShow = true;
  426 + }
  427 + });
  428 +
  429 + if (this._opt.showBandwidth || this._opt.text || hasBtnShow) {
  430 + result = true;
  431 + }
  432 +
  433 + return result;
  434 + };
  435 +
  436 + Jessibuca.prototype._onMessage = function () {
  437 + var _this = this;
  438 + this._decoderWorker.onmessage = function (event) {
  439 + var msg = event.data;
  440 + switch (msg.cmd) {
  441 + case "init":
  442 + _this._opt.isDebug && console.log("decoder worker init")
  443 + _this.setBufferTime(_this._opt.videoBuffer);
  444 + if (!_this._hasLoaded) {
  445 + _this._opt.isDebug && console.log("has loaded");
  446 + _this._hasLoaded = true;
  447 + _this.onLoad();
  448 + _this._trigger('load');
  449 + }
  450 + break
  451 + case "initSize":
  452 + _this._canvasElement.width = msg.w;
  453 + _this._canvasElement.height = msg.h;
  454 + _this.onInitSize();
  455 + _this.resize();
  456 + _this._trigger('videoInfo', {w: msg.w, h: msg.h});
  457 + if (_this.isWebGL()) {
  458 +
  459 + } else {
  460 + _this._initRGB(msg.w, msg.h)
  461 + }
  462 + break
  463 + case "render":
  464 + if (_this._contextGL) {
  465 + _this._drawNextOutputPictureGL(msg.output);
  466 + } else {
  467 + _this._drawNextOutputPictureRGBA(msg.buffer);
  468 + }
  469 + if (_this.loading) {
  470 + _this.loading = false;
  471 + _this.playing = true;
  472 + _this._opt.isDebug && console.log("clear check loading timeout");
  473 + _this._clearCheckLoading();
  474 + }
  475 + _this._trigger('timeUpdate', msg.ts);
  476 + _this.onTimeUpdate(msg.ts);
  477 + _this._updateStats({bps: msg.bps, ts: msg.ts});
  478 + _this._checkHeart();
  479 + break
  480 + case "initAudio":
  481 + _this._initAudioPlay(msg.frameCount, msg.samplerate, msg.channels)
  482 + _this._trigger('audioInfo', {
  483 + numOfChannels: msg.channels, // 声频通道
  484 + length: msg.frameCount, // 帧数
  485 + sampleRate: msg.samplerate // 采样率
  486 + });
  487 + break
  488 + case "playAudio":
  489 + _this._playAudio(msg.buffer)
  490 + break
  491 + case "print":
  492 + _this.onLog(msg.text)
  493 + this._trigger('log', msg.text);
  494 + _this._opt.isDebug && console.log(msg.text);
  495 + break
  496 + case "printErr":
  497 + _this.onLog(msg.text);
  498 + this._trigger('log', msg.text);
  499 + _this.onError(msg.text);
  500 + this._trigger('error', msg.text);
  501 + _this._opt.isDebug && console.error(msg.text);
  502 + break;
  503 + case "initAudioPlanar":
  504 + _this._initAudioPlanar(msg);
  505 + _this._trigger('audioInfo', {
  506 + numOfChannels: msg.channels, // 声频通道
  507 + length: undefined, // 帧数
  508 + sampleRate: msg.samplerate // 采样率
  509 + });
  510 + break;
  511 + default:
  512 + _this._opt.isDebug && console.log(msg);
  513 + _this[msg.cmd](msg)
  514 + }
  515 + };
  516 + };
  517 +
  518 + Jessibuca.prototype._initEventListener = function () {
  519 + var _this = this;
  520 +
  521 + this._doms.playDom && this._doms.playDom.addEventListener('click', function (e) {
  522 + e.stopPropagation();
  523 + _this.play();
  524 + }, false);
  525 +
  526 + this._doms.playBigDom && this._doms.playBigDom.addEventListener('click', function (e) {
  527 + e.stopPropagation();
  528 + _this.play();
  529 + }, false);
  530 +
  531 + this._doms.pauseDom && this._doms.pauseDom.addEventListener('click', function (e) {
  532 + e.stopPropagation();
  533 + _this.pause();
  534 + }, false);
  535 +
  536 + // screenshots
  537 + this._doms.screenshotsDom && this._doms.screenshotsDom.addEventListener('click', function (e) {
  538 + e.stopPropagation();
  539 + var filename = _this._opt.text + '' + _now();
  540 + _this._screenshot(filename);
  541 + }, false);
  542 + //
  543 + this._doms.fullscreenDom && this._doms.fullscreenDom.addEventListener('click', function (e) {
  544 + e.stopPropagation();
  545 + _this.fullscreen = true;
  546 + }, false);
  547 + //
  548 + this._doms.minScreenDom && this._doms.minScreenDom.addEventListener('click', function (e) {
  549 + e.stopPropagation();
  550 + _this.fullscreen = false;
  551 + }, false);
  552 + //
  553 + this._doms.recordDom && this._doms.recordDom.addEventListener('click', function (e) {
  554 + e.stopPropagation();
  555 + _this.recording = true;
  556 + }, false);
  557 + //
  558 + this._doms.recordingDom && this._doms.recordingDom.addEventListener('click', function (e) {
  559 + e.stopPropagation();
  560 + _this.recording = false;
  561 + }, false);
  562 +
  563 + this._doms.quietAudioDom && this._doms.quietAudioDom.addEventListener('click', function (e) {
  564 + e.stopPropagation();
  565 + _this.cancelMute();
  566 + }, false);
  567 +
  568 + this._doms.playAudioDom && this._doms.playAudioDom.addEventListener('click', function (e) {
  569 + e.stopPropagation();
  570 + _this.mute();
  571 + }, false);
  572 + };
  573 + /**
  574 + * set debug
  575 + * @param flag
  576 + */
  577 + Jessibuca.prototype.setDebug = function (flag) {
  578 + this._opt.isDebug = !!flag;
  579 + };
  580 + /**
  581 + * mute
  582 + */
  583 + Jessibuca.prototype.mute = function () {
  584 + this._audioEnabled(false);
  585 + this.quieting = true;
  586 + };
  587 +
  588 + /**
  589 + * cancel mute
  590 + */
  591 + Jessibuca.prototype.cancelMute = function () {
  592 + this._audioEnabled(true);
  593 + this.quieting = false;
  594 + };
  595 +
  596 + /**
  597 + * 设置旋转角度
  598 + */
  599 + Jessibuca.prototype.setRotate = function (deg) {
  600 +
  601 + };
  602 +
  603 + Jessibuca.prototype._initStatus = function () {
  604 + this._loading = true;
  605 + this.loading = true;
  606 + this._recording = false;
  607 + this.recording = false;
  608 + this._playing = false;
  609 + this.playing = false;
  610 + this._quieting = this._opt.isNotMute ? false : true;
  611 + this.quieting = this._opt.isNotMute ? false : true;
  612 + this._fullscreen = false;
  613 + this.fullscreen = false;
  614 + }
  615 +
  616 + Jessibuca.prototype._initBtns = function () {
  617 + // show
  618 + _domToggle(this._doms.pauseDom, true);
  619 + _domToggle(this._doms.screenshotsDom, true);
  620 + _domToggle(this._doms.fullscreenDom, true);
  621 + _domToggle(this._doms.quietAudioDom, true);
  622 + _domToggle(this._doms.textDom, true);
  623 + _domToggle(this._doms.speedDom, true);
  624 + _domToggle(this._doms.recordDom, true);
  625 + // hide
  626 + _domToggle(this._doms.loadingDom, false);
  627 + _domToggle(this._doms.playDom, false);
  628 + _domToggle(this._doms.playBigDom, false);
  629 + _domToggle(this._doms.bgDom, false);
  630 + };
  631 +
  632 + Jessibuca.prototype._hideBtns = function () {
  633 + var _this = this;
  634 + Object.keys(this._doms).forEach(function (dom) {
  635 + if (dom !== 'bgDom') {
  636 + _domToggle(_this._doms[dom], false);
  637 + }
  638 + })
  639 + };
  640 +
  641 + function _checkFull() {
  642 + var isFull = document.fullscreenElement || window.webkitFullscreenElement || document.msFullscreenElement;
  643 + if (isFull === undefined) isFull = false;
  644 + return !!isFull;
  645 + }
  646 +
  647 + Jessibuca.prototype._updateStats = function (options) {
  648 + options = options || {};
  649 +
  650 + if (!this._startBpsTime) {
  651 + this._startBpsTime = _now();
  652 + }
  653 + var _nowTime = _now();
  654 + var timestamp = _nowTime - this._startBpsTime;
  655 +
  656 + if (timestamp < 1 * 1000) {
  657 + this._bps += (options.bps || 0);
  658 + this._stats.fps += 1;
  659 + this._stats.vbps += parseInt((options.bps || 0));
  660 + return;
  661 + }
  662 + this._stats.ts = options.ts;
  663 + this._doms.speedDom && (this._doms.speedDom.innerText = _bpsSize(this._bps));
  664 + this._trigger('bps', this._bps);
  665 + this._trigger('stats', this._stats);
  666 + this._trigger('performance', _fpsStatus(this._stats.fps));
  667 + this._bps = 0;
  668 + this._stats.fps = 0;
  669 + this._stats.vbps = 0;
  670 + this._startBpsTime = _nowTime;
  671 + };
  672 +
  673 +
  674 + Jessibuca.prototype._checkHeart = function () {
  675 + if (this._checkHeartTimeout) {
  676 + clearTimeout(this._checkHeartTimeout);
  677 + this._checkHeartTimeout = null;
  678 + }
  679 + var _this = this;
  680 + this._checkHeartTimeout = setTimeout(function () {
  681 + _this._opt.isDebug && console.log('check heart timeout');
  682 + _this._trigger('timeout');
  683 + _this.recording = false;
  684 + _this.playing = false;
  685 + _this._close();
  686 + }, this._opt.timeout * 1000);
  687 + };
  688 +
  689 + Jessibuca.prototype._checkLoading = function () {
  690 + if (this._checkLoadingTimeout) {
  691 + clearTimeout(this._checkLoadingTimeout);
  692 + this._checkLoadingTimeout = null;
  693 + }
  694 + var _this = this;
  695 + this._checkLoadingTimeout = setTimeout(function () {
  696 + _this._opt.isDebug && console.log('check loading timeout');
  697 + _this._trigger('timeout');
  698 + _this.playing = false;
  699 + _this._close();
  700 + _domToggle(_this._doms.loadingDom, false);
  701 + }, this._opt.timeout * 1000);
  702 + };
  703 +
  704 + Jessibuca.prototype._clearCheckLoading = function () {
  705 + if (this._checkLoadingTimeout) {
  706 + clearTimeout(this._checkLoadingTimeout);
  707 + this._checkLoadingTimeout = null;
  708 + }
  709 + };
  710 +
  711 + Jessibuca.prototype._initCheckVariable = function () {
  712 + this._startBpsTime = '';
  713 + this._bps = 0;
  714 + if (this._checkHeartTimeout) {
  715 + clearTimeout(this._checkHeartTimeout);
  716 + this._checkHeartTimeout = null;
  717 + }
  718 + }
  719 + //
  720 + Jessibuca.prototype._initAudioPlanar = function (msg) {
  721 + var channels = msg.channels
  722 + var samplerate = msg.samplerate
  723 + var context = this._audioContext;
  724 + var isPlaying = false;
  725 + var audioBuffers = [];
  726 + if (!context) return false;
  727 + var _this = this
  728 + this._playAudio = function (buffer) {
  729 + var frameCount = buffer[0][0].length
  730 + var audioBuffer = context.createBuffer(channels, frameCount * buffer.length, samplerate);
  731 + var copyToCtxBuffer = function (fromBuffer) {
  732 + for (var channel = 0; channel < channels; channel++) {
  733 + var nowBuffering = audioBuffer.getChannelData(channel);
  734 + for (var j = 0; j < buffer.length; j++) {
  735 + for (var i = 0; i < frameCount; i++) {
  736 + nowBuffering[i + j * frameCount] = fromBuffer[j][channel][i]
  737 + }
  738 + //postMessage({ cmd: "setBufferA", buffer: fromBuffer[j] }, '*', fromBuffer[j].map(x => x.buffer))
  739 + }
  740 + }
  741 + }
  742 + var playNextBuffer = function () {
  743 + isPlaying = false;
  744 + //console.log("~", audioBuffers.length)
  745 + if (audioBuffers.length) {
  746 + playAudio(audioBuffers.shift());
  747 + }
  748 + //if (audioBuffers.length > 1) audioBuffers.shift();
  749 + };
  750 + var playAudio = function (fromBuffer) {
  751 + if (!fromBuffer) return
  752 + if (isPlaying) {
  753 + audioBuffers.push(fromBuffer);
  754 + //console.log(audioBuffers.length)
  755 + return;
  756 + }
  757 + isPlaying = true;
  758 + copyToCtxBuffer(fromBuffer);
  759 + var source = context.createBufferSource();
  760 + source.buffer = audioBuffer;
  761 + source.connect(context.destination);
  762 + // source.onended = playNextBuffer;
  763 + source.start();
  764 + };
  765 + _this._playAudio = playAudio
  766 + _this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000);
  767 + playAudio(buffer)
  768 + };
  769 + }
  770 +
  771 + function _unlock(context) {
  772 + context.resume();
  773 + var source = context.createBufferSource();
  774 + source.buffer = context.createBuffer(1, 1, 22050);
  775 + source.connect(context.destination);
  776 + if (source.noteOn)
  777 + source.noteOn(0);
  778 + else
  779 + source.start(0);
  780 + }
  781 +
  782 + function _domToggle(dom, toggle) {
  783 + if (dom) {
  784 + dom.style.display = toggle ? 'block' : "none";
  785 + }
  786 + }
  787 +
  788 + function _dataURLToFile(dataURL) {
  789 + const arr = dataURL.split(",");
  790 + const bstr = atob(arr[1]);
  791 + const type = arr[0].replace("data:", "").replace(";base64", "")
  792 + let n = bstr.length, u8arr = new Uint8Array(n);
  793 + while (n--) {
  794 + u8arr[n] = bstr.charCodeAt(n);
  795 + }
  796 + return new File([u8arr], 'file', {type});
  797 + }
  798 +
  799 + function _downloadImg(content, fileName) {
  800 + const aLink = document.createElement("a");
  801 + aLink.download = fileName;
  802 + aLink.href = URL.createObjectURL(content);
  803 + aLink.click();
  804 + URL.revokeObjectURL(content);
  805 + }
  806 +
  807 + function _bpsSize(value) {
  808 + if (null == value || value === '') {
  809 + return "0 KB/S";
  810 + }
  811 + var srcsize = parseFloat(value);
  812 + var size = srcsize / 1024;
  813 + size = size.toFixed(2);
  814 + return size + 'KB/S';
  815 + }
  816 +
  817 + function _fpsStatus(fps) {
  818 + var result = 0;
  819 + if (fps >= 24) {
  820 + result = 2;
  821 + } else if (fps >= 15) {
  822 + result = 1;
  823 + }
  824 +
  825 + return result;
  826 + }
  827 +
  828 + /**
  829 + * set audio
  830 + * @param flag
  831 + */
  832 + Jessibuca.prototype._audioEnabled = function (flag) {
  833 + if (flag) {
  834 + _unlock(this._audioContext)
  835 + this._audioEnabled = function (flag) {
  836 + if (flag) {
  837 + // 恢复
  838 + this._audioContext.resume();
  839 +
  840 + } else {
  841 + // 暂停
  842 + this._audioContext.suspend();
  843 + }
  844 + }
  845 + } else {
  846 + this._audioContext.suspend();
  847 + }
  848 + }
  849 +
  850 + Jessibuca.prototype._playAudio = function (data) {
  851 + var context = this._audioContext;
  852 + var isPlaying = false;
  853 + var isDecoding = false;
  854 + if (!context) return false;
  855 + var audioBuffers = [];
  856 + var decodeQueue = []
  857 + var _this = this
  858 + var playNextBuffer = function (e) {
  859 + if (audioBuffers.length) {
  860 + playBuffer(audioBuffers.shift())
  861 + }
  862 + };
  863 + var playBuffer = function (buffer) {
  864 + isPlaying = true;
  865 + var audioBufferSouceNode = context.createBufferSource();
  866 + audioBufferSouceNode.buffer = buffer;
  867 + audioBufferSouceNode.connect(context.destination);
  868 + // audioBufferSouceNode.onended = playNextBuffer;
  869 + audioBufferSouceNode.start();
  870 + if (!_this.audioInterval) {
  871 + _this.audioInterval = setInterval(playNextBuffer, buffer.duration * 1000 - 1);
  872 + }
  873 + }
  874 + var decodeAudio = function () {
  875 + if (decodeQueue.length) {
  876 + context.decodeAudioData(decodeQueue.shift(), tryPlay, decodeAudio);
  877 + } else {
  878 + isDecoding = false
  879 + }
  880 + }
  881 + var tryPlay = function (buffer) {
  882 + decodeAudio()
  883 + if (isPlaying) {
  884 + audioBuffers.push(buffer);
  885 + } else {
  886 + playBuffer(buffer)
  887 + }
  888 + }
  889 + var playAudio = function (data) {
  890 + decodeQueue.push(...data)
  891 + if (!isDecoding) {
  892 + isDecoding = true
  893 + decodeAudio()
  894 + }
  895 + }
  896 + this._playAudio = playAudio
  897 + playAudio(data)
  898 + }
  899 + Jessibuca.prototype._initAudioPlay = function (frameCount, samplerate, channels) {
  900 + var context = this._audioContext;
  901 + var isPlaying = false;
  902 + var audioBuffers = [];
  903 + if (!context) return false;
  904 + var _this = this
  905 + var resampled = samplerate < 22050;
  906 + if (resampled) {
  907 + _this._opt.isDebug && console.log("resampled!")
  908 + }
  909 + var audioBuffer = resampled ? context.createBuffer(channels, frameCount << 1, samplerate << 1) : context.createBuffer(channels, frameCount, samplerate);
  910 + var playNextBuffer = function () {
  911 + isPlaying = false;
  912 + //console.log("~", audioBuffers.length)
  913 + if (audioBuffers.length) {
  914 + playAudio(audioBuffers.shift());
  915 + }
  916 + };
  917 +
  918 + var copyToCtxBuffer = channels > 1 ? function (fromBuffer) {
  919 + for (var channel = 0; channel < channels; channel++) {
  920 + var nowBuffering = audioBuffer.getChannelData(channel);
  921 + if (resampled) {
  922 + for (var i = 0; i < frameCount; i++) {
  923 + nowBuffering[i * 2] = nowBuffering[i * 2 + 1] = fromBuffer[i * (channel + 1)] / 32768;
  924 + }
  925 + } else
  926 + for (var i = 0; i < frameCount; i++) {
  927 + nowBuffering[i] = fromBuffer[i * (channel + 1)] / 32768;
  928 + }
  929 +
  930 + }
  931 + } : function (fromBuffer) {
  932 + var nowBuffering = audioBuffer.getChannelData(0);
  933 + for (var i = 0; i < nowBuffering.length; i++) {
  934 + nowBuffering[i] = fromBuffer[i] / 32768;
  935 + }
  936 + };
  937 + var playAudio = function (fromBuffer) {
  938 + if (isPlaying) {
  939 + audioBuffers.push(fromBuffer);
  940 + return;
  941 + }
  942 + isPlaying = true;
  943 + copyToCtxBuffer(fromBuffer);
  944 + var source = context.createBufferSource();
  945 + source.buffer = audioBuffer;
  946 + source.connect(context.destination);
  947 + if (!_this.audioInterval) {
  948 + _this.audioInterval = setInterval(playNextBuffer, audioBuffer.duration * 1000);
  949 + }
  950 + source.start();
  951 + };
  952 + this._playAudio = playAudio;
  953 + }
  954 + /**
  955 + * Returns true if the canvas supports WebGL
  956 + */
  957 + Jessibuca.prototype.isWebGL = function () {
  958 + return !!this._contextGL;
  959 + };
  960 + /**
  961 + * set timeout
  962 + * @param time
  963 + */
  964 + Jessibuca.prototype.setTimeout = function (time) {
  965 + if (typeof time === 'number') {
  966 + this._opt.timeout = Number(time);
  967 + }
  968 + };
  969 +
  970 + /**
  971 + * @desc 视频缩放模式, 当视频分辨率比例与canvas显示区域比例不同时,缩放效果不同:
  972 + 0 视频画面完全填充canvas区域,画面会被拉伸
  973 + 1 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边(默认)
  974 + 2 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全
  975 + * @param type
  976 + *
  977 + */
  978 + Jessibuca.prototype.setScaleMode = function (type) {
  979 + if (type === 0) {
  980 + this._opt.isFullResize = false;
  981 + this._opt.isResize = false;
  982 + } else if (type === 1) {
  983 + this._opt.isFullResize = false;
  984 + this._opt.isResize = true;
  985 + } else if (type === 2) {
  986 + this._opt.isFullResize = true;
  987 + }
  988 + this.resize();
  989 + };
  990 +
  991 + /**
  992 + * Create the GL context from the canvas element
  993 + */
  994 + Jessibuca.prototype._initContextGL = function () {
  995 + var canvas = this._canvasElement;
  996 + var gl = null;
  997 +
  998 + var validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"];
  999 + var nameIndex = 0;
  1000 +
  1001 + while (!gl && nameIndex < validContextNames.length) {
  1002 + var contextName = validContextNames[nameIndex];
  1003 +
  1004 + try {
  1005 + var contextOptions = {preserveDrawingBuffer: true};
  1006 + if (this._opt.contextOptions) {
  1007 + contextOptions = Object.assign(contextOptions, this._opt.contextOptions);
  1008 + }
  1009 +
  1010 + gl = canvas.getContext(contextName, contextOptions);
  1011 + } catch (e) {
  1012 + gl = null;
  1013 + }
  1014 +
  1015 + if (!gl || typeof gl.getParameter !== "function") {
  1016 + gl = null;
  1017 + }
  1018 +
  1019 + ++nameIndex;
  1020 + }
  1021 + ;
  1022 +
  1023 + this._contextGL = gl;
  1024 + };
  1025 +
  1026 + /**
  1027 + * Initialize GL shader program
  1028 + */
  1029 + Jessibuca.prototype._initProgram = function () {
  1030 + var gl = this._contextGL;
  1031 +
  1032 + var vertexShaderScript = [
  1033 + 'attribute vec4 vertexPos;',
  1034 + 'attribute vec4 texturePos;',
  1035 + 'varying vec2 textureCoord;',
  1036 +
  1037 + 'void main()',
  1038 + '{',
  1039 + 'gl_Position = vertexPos;',
  1040 + 'textureCoord = texturePos.xy;',
  1041 + '}'
  1042 + ].join('\n');
  1043 +
  1044 + var fragmentShaderScript = [
  1045 + 'precision highp float;',
  1046 + 'varying highp vec2 textureCoord;',
  1047 + 'uniform sampler2D ySampler;',
  1048 + 'uniform sampler2D uSampler;',
  1049 + 'uniform sampler2D vSampler;',
  1050 + 'const mat4 YUV2RGB = mat4',
  1051 + '(',
  1052 + '1.1643828125, 0, 1.59602734375, -.87078515625,',
  1053 + '1.1643828125, -.39176171875, -.81296875, .52959375,',
  1054 + '1.1643828125, 2.017234375, 0, -1.081390625,',
  1055 + '0, 0, 0, 1',
  1056 + ');',
  1057 +
  1058 + 'void main(void) {',
  1059 + 'highp float y = texture2D(ySampler, textureCoord).r;',
  1060 + 'highp float u = texture2D(uSampler, textureCoord).r;',
  1061 + 'highp float v = texture2D(vSampler, textureCoord).r;',
  1062 + 'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;',
  1063 + '}'
  1064 + ].join('\n');
  1065 +
  1066 + var vertexShader = gl.createShader(gl.VERTEX_SHADER);
  1067 + gl.shaderSource(vertexShader, vertexShaderScript);
  1068 + gl.compileShader(vertexShader);
  1069 + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
  1070 + this._opt.isDebug && console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader));
  1071 + }
  1072 +
  1073 + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
  1074 + gl.shaderSource(fragmentShader, fragmentShaderScript);
  1075 + gl.compileShader(fragmentShader);
  1076 + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) {
  1077 + this._opt.isDebug && console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader));
  1078 + }
  1079 +
  1080 + var program = gl.createProgram();
  1081 + gl.attachShader(program, vertexShader);
  1082 + gl.attachShader(program, fragmentShader);
  1083 + gl.linkProgram(program);
  1084 + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  1085 + this._opt.isDebug && console.log('Program failed to compile: ' + gl.getProgramInfoLog(program));
  1086 + }
  1087 +
  1088 + gl.useProgram(program);
  1089 +
  1090 + this._shaderProgram = program;
  1091 + };
  1092 +
  1093 + /**
  1094 + * Initialize vertex buffers and attach to shader program
  1095 + */
  1096 + Jessibuca.prototype._initBuffers = function () {
  1097 + var gl = this._contextGL;
  1098 + var program = this._shaderProgram;
  1099 +
  1100 + var vertexPosBuffer = gl.createBuffer();
  1101 + gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
  1102 + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW);
  1103 +
  1104 + var vertexPosRef = gl.getAttribLocation(program, 'vertexPos');
  1105 + gl.enableVertexAttribArray(vertexPosRef);
  1106 + gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0);
  1107 +
  1108 + var texturePosBuffer = gl.createBuffer();
  1109 + gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
  1110 + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW);
  1111 +
  1112 + var texturePosRef = gl.getAttribLocation(program, 'texturePos');
  1113 + gl.enableVertexAttribArray(texturePosRef);
  1114 + gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0);
  1115 +
  1116 + this._texturePosBuffer = texturePosBuffer;
  1117 + };
  1118 +
  1119 + /**
  1120 + * Initialize GL textures and attach to shader program
  1121 + */
  1122 + Jessibuca.prototype._initTextures = function () {
  1123 + var gl = this._contextGL;
  1124 + var program = this._shaderProgram;
  1125 +
  1126 + var yTextureRef = this._initTexture();
  1127 + var ySamplerRef = gl.getUniformLocation(program, 'ySampler');
  1128 + gl.uniform1i(ySamplerRef, 0);
  1129 + this._yTextureRef = yTextureRef;
  1130 +
  1131 + var uTextureRef = this._initTexture();
  1132 + var uSamplerRef = gl.getUniformLocation(program, 'uSampler');
  1133 + gl.uniform1i(uSamplerRef, 1);
  1134 + this._uTextureRef = uTextureRef;
  1135 +
  1136 + var vTextureRef = this._initTexture();
  1137 + var vSamplerRef = gl.getUniformLocation(program, 'vSampler');
  1138 + gl.uniform1i(vSamplerRef, 2);
  1139 + this._vTextureRef = vTextureRef;
  1140 + };
  1141 +
  1142 + /**
  1143 + * Create and configure a single texture
  1144 + */
  1145 + Jessibuca.prototype._initTexture = function () {
  1146 + var gl = this._contextGL;
  1147 +
  1148 + var textureRef = gl.createTexture();
  1149 + gl.bindTexture(gl.TEXTURE_2D, textureRef);
  1150 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  1151 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  1152 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  1153 + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  1154 + gl.bindTexture(gl.TEXTURE_2D, null);
  1155 +
  1156 + return textureRef;
  1157 + };
  1158 +
  1159 + /**
  1160 + * Draw picture data to the canvas.
  1161 + * If this object is using WebGL, the data must be an I420 formatted ArrayBuffer,
  1162 + * Otherwise, data must be an RGBA formatted ArrayBuffer.
  1163 + */
  1164 + Jessibuca.prototype._drawNextOutputPicture = function (data) {
  1165 + if (this._contextGL) {
  1166 + this._drawNextOutputPictureGL(data);
  1167 + } else {
  1168 + this._drawNextOutputPictureRGBA(data);
  1169 + }
  1170 + };
  1171 +
  1172 + /**
  1173 + * Draw the next output picture using WebGL
  1174 + */
  1175 + Jessibuca.prototype._drawNextOutputPictureGL = function (data) {
  1176 + var gl = this._contextGL;
  1177 + var texturePosBuffer = this._texturePosBuffer;
  1178 + var yTextureRef = this._yTextureRef;
  1179 + var uTextureRef = this._uTextureRef;
  1180 + var vTextureRef = this._vTextureRef;
  1181 + var croppingParams = this.croppingParams
  1182 + var width = this._canvasElement.width
  1183 + var height = this._canvasElement.height
  1184 + if (croppingParams) {
  1185 + gl.viewport(0, 0, croppingParams.width, croppingParams.height);
  1186 + var tTop = croppingParams.top / height;
  1187 + var tLeft = croppingParams.left / width;
  1188 + var tBottom = croppingParams.height / height;
  1189 + var tRight = croppingParams.width / width;
  1190 + var texturePosValues = new Float32Array([tRight, tTop, tLeft, tTop, tRight, tBottom, tLeft, tBottom]);
  1191 +
  1192 + gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer);
  1193 + gl.bufferData(gl.ARRAY_BUFFER, texturePosValues, gl.DYNAMIC_DRAW);
  1194 + } else {
  1195 + gl.viewport(0, 0, this._canvasElement.width, this._canvasElement.height);
  1196 + }
  1197 + gl.activeTexture(gl.TEXTURE0);
  1198 + gl.bindTexture(gl.TEXTURE_2D, yTextureRef);
  1199 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[0]);
  1200 +
  1201 + gl.activeTexture(gl.TEXTURE1);
  1202 + gl.bindTexture(gl.TEXTURE_2D, uTextureRef);
  1203 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width / 2, height / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[1]);
  1204 +
  1205 + gl.activeTexture(gl.TEXTURE2);
  1206 + gl.bindTexture(gl.TEXTURE_2D, vTextureRef);
  1207 + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, width / 2, height / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, data[2]);
  1208 +
  1209 + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  1210 + };
  1211 +
  1212 + /**
  1213 + * Draw next output picture using ARGB data on a 2d canvas.
  1214 + */
  1215 + Jessibuca.prototype._drawNextOutputPictureRGBA = function (data) {
  1216 + this.imageData.data.set(data);
  1217 + var croppingParams = this.croppingParams
  1218 + if (!croppingParams) {
  1219 + this.ctx2d.putImageData(this.imageData, 0, 0);
  1220 + } else {
  1221 + this.ctx2d.putImageData(this.imageData, -croppingParams.left, -croppingParams.top, 0, 0, croppingParams.width, croppingParams.height);
  1222 + }
  1223 + };
  1224 + Jessibuca.prototype.ctx2d = null;
  1225 + Jessibuca.prototype.imageData = null;
  1226 + Jessibuca.prototype._initRGB = function (width, height) {
  1227 + this.ctx2d = this._canvasElement.getContext('2d');
  1228 + this.imageData = this.ctx2d.getImageData(0, 0, width, height);
  1229 + this.clear = function () {
  1230 + this.ctx2d.clearRect(0, 0, width, height)
  1231 + };
  1232 + };
  1233 +
  1234 + Jessibuca.prototype.pause = function () {
  1235 + this._close();
  1236 + if (this.loading) {
  1237 + _domToggle(this._doms.loadingDom, false);
  1238 + }
  1239 + this.recording = false;
  1240 + this.playing = false;
  1241 + };
  1242 +
  1243 + Jessibuca.prototype._close = function () {
  1244 + if (this.audioInterval) {
  1245 + clearInterval(this.audioInterval)
  1246 + }
  1247 + delete this._playAudio
  1248 + this._decoderWorker.postMessage({cmd: "close"})
  1249 +
  1250 + if (this._wakeLock) {
  1251 + this._wakeLock.release();
  1252 + this._wakeLock = null;
  1253 + }
  1254 +
  1255 + // this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
  1256 + this._initCheckVariable();
  1257 + }
  1258 + /**
  1259 + * destroy
  1260 + * @desc delete worker,
  1261 + */
  1262 + Jessibuca.prototype.destroy = function () {
  1263 + // destroy
  1264 + this._decoderWorker.terminate()
  1265 + window.removeEventListener("resize", this._onresize);
  1266 + window.removeEventListener('fullscreenchange', this._onfullscreenchange);
  1267 + this._initCheckVariable();
  1268 + this._clearCheckLoading();
  1269 + this._off();
  1270 + this._hasLoaded = false;
  1271 + // remove dom
  1272 + while (this._container.firstChild) {
  1273 + this._container.removeChild(this._container.firstChild);
  1274 + }
  1275 + if (this._wakeLock) {
  1276 + this._wakeLock.release();
  1277 + }
  1278 + }
  1279 +
  1280 + /**
  1281 + * 清理画布为黑色背景
  1282 + * 用于canvas重用进行多个流切换播放时,将上一个画面清理
  1283 + * 避免后一个视频播放之前出现前一个视频最后一个画面
  1284 + */
  1285 + Jessibuca.prototype.clearView = function () {
  1286 + this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
  1287 + };
  1288 + /**
  1289 + * play
  1290 + * @param url
  1291 + */
  1292 + Jessibuca.prototype.play = function (url) {
  1293 + if (!this.playUrl && !url) {
  1294 + return;
  1295 + }
  1296 + var needDelay = false;
  1297 + if (url) {
  1298 + if (this.playUrl) {
  1299 + this._close();
  1300 + needDelay = true;
  1301 + this._contextGL.clear(this._contextGL.COLOR_BUFFER_BIT);
  1302 + }
  1303 + this.loading = true;
  1304 + _domToggle(this._doms.bgDom, false);
  1305 + this._checkLoading();
  1306 + this.playUrl = url;
  1307 + } else if (this.playUrl) {
  1308 + // retry
  1309 + if (this.loading) {
  1310 + this._hideBtns();
  1311 + _domToggle(this._doms.fullscreenDom, true);
  1312 + _domToggle(this._doms.pauseDom, true);
  1313 + _domToggle(this._doms.loadingDom, true);
  1314 + this._checkLoading();
  1315 + } else {
  1316 + this.playing = true;
  1317 + }
  1318 + }
  1319 + this._initCheckVariable();
  1320 +
  1321 + if (needDelay) {
  1322 + var _this = this;
  1323 + setTimeout(function () {
  1324 + _this._decoderWorker.postMessage({cmd: "play", url: _this.playUrl, isWebGL: _this.isWebGL()})
  1325 + }, 300);
  1326 + } else {
  1327 + this._decoderWorker.postMessage({cmd: "play", url: this.playUrl, isWebGL: this.isWebGL()})
  1328 + }
  1329 + };
  1330 + /**
  1331 + * has loaded
  1332 + * @returns {boolean}
  1333 + */
  1334 + Jessibuca.prototype.hasLoaded = function () {
  1335 + return this._hasLoaded;
  1336 + };
  1337 +
  1338 + Object.defineProperty(Jessibuca.prototype, "fullscreen", {
  1339 + set(value) {
  1340 + if (value) {
  1341 + if (!_checkFull()) {
  1342 + this._container.requestFullscreen();
  1343 + }
  1344 + _domToggle(this._doms.minScreenDom, true);
  1345 + _domToggle(this._doms.fullscreenDom, false);
  1346 + } else {
  1347 + if (_checkFull()) {
  1348 + document.exitFullscreen();
  1349 + }
  1350 + _domToggle(this._doms.minScreenDom, false);
  1351 + _domToggle(this._doms.fullscreenDom, true);
  1352 + }
  1353 +
  1354 + if (this._fullscreen !== value) {
  1355 + this.onFullscreen(value);
  1356 + this._trigger('fullscreen', value);
  1357 + }
  1358 + this._fullscreen = value;
  1359 + },
  1360 + get() {
  1361 + return this._fullscreen;
  1362 + }
  1363 + });
  1364 +
  1365 + Object.defineProperty(Jessibuca.prototype, 'playing', {
  1366 + set(value) {
  1367 + if (value) {
  1368 + _domToggle(this._doms.playBigDom, false);
  1369 + _domToggle(this._doms.playDom, false);
  1370 + _domToggle(this._doms.pauseDom, true);
  1371 +
  1372 + _domToggle(this._doms.screenshotsDom, true);
  1373 + _domToggle(this._doms.recordDom, true);
  1374 + if (this._quieting) {
  1375 + _domToggle(this._doms.quietAudioDom, true);
  1376 + _domToggle(this._doms.playAudioDom, false);
  1377 + } else {
  1378 + _domToggle(this._doms.quietAudioDom, false);
  1379 + _domToggle(this._doms.playAudioDom, true);
  1380 + }
  1381 + } else {
  1382 + this._doms.speedDom && (this._doms.speedDom.innerText = '');
  1383 + if (this.playUrl) {
  1384 + _domToggle(this._doms.playDom, true);
  1385 + _domToggle(this._doms.playBigDom, true);
  1386 + _domToggle(this._doms.pauseDom, false);
  1387 + }
  1388 +
  1389 + // 在停止状态下录像,截屏,音量是非激活,只有播放,最大化时可点击
  1390 + _domToggle(this._doms.recordDom, false);
  1391 + _domToggle(this._doms.recordingDom, false);
  1392 + _domToggle(this._doms.screenshotsDom, false);
  1393 + _domToggle(this._doms.quietAudioDom, false);
  1394 + _domToggle(this._doms.playAudioDom, false);
  1395 + }
  1396 +
  1397 + if (this._playing !== value) {
  1398 + if (value) {
  1399 + this.onPlay();
  1400 + this._trigger('play');
  1401 + } else {
  1402 + this.onPause();
  1403 + this._trigger('pause');
  1404 + }
  1405 + }
  1406 + this._playing = value;
  1407 + },
  1408 + get() {
  1409 + return this._playing;
  1410 + }
  1411 + });
  1412 +
  1413 + Object.defineProperty(Jessibuca.prototype, 'recording', {
  1414 + set(value) {
  1415 + if (value) {
  1416 + _domToggle(this._doms.recordDom, false);
  1417 + _domToggle(this._doms.recordingDom, true);
  1418 + } else {
  1419 + _domToggle(this._doms.recordDom, true);
  1420 + _domToggle(this._doms.recordingDom, false);
  1421 +
  1422 + }
  1423 + if (this._recording !== value) {
  1424 + this.onRecord(value);
  1425 + this._trigger('record', value);
  1426 + this._recording = value;
  1427 + }
  1428 + },
  1429 + get() {
  1430 + return this._recording;
  1431 + }
  1432 + });
  1433 +
  1434 + Object.defineProperty(Jessibuca.prototype, 'quieting', {
  1435 + set(value) {
  1436 + if (value) {
  1437 + _domToggle(this._doms.quietAudioDom, true);
  1438 + _domToggle(this._doms.playAudioDom, false);
  1439 + } else {
  1440 + _domToggle(this._doms.quietAudioDom, false);
  1441 + _domToggle(this._doms.playAudioDom, true);
  1442 + }
  1443 + if (this._quieting !== value) {
  1444 + this.onMute(value);
  1445 + this._trigger('mute', value);
  1446 + }
  1447 + this._quieting = value;
  1448 + },
  1449 + get() {
  1450 + return this._quieting;
  1451 + }
  1452 + });
  1453 +
  1454 + Object.defineProperty(Jessibuca.prototype, 'loading', {
  1455 + set(value) {
  1456 + if (value) {
  1457 + this._hideBtns();
  1458 + _domToggle(this._doms.fullscreenDom, true);
  1459 + _domToggle(this._doms.pauseDom, true);
  1460 + _domToggle(this._doms.loadingDom, true);
  1461 + } else {
  1462 + this._initBtns();
  1463 + }
  1464 + this._loading = value;
  1465 + },
  1466 + get() {
  1467 + return this._loading;
  1468 + }
  1469 + });
  1470 +
  1471 + /**
  1472 + * resize
  1473 + */
  1474 + Jessibuca.prototype.resize = function () {
  1475 + var width = this._container.clientWidth;
  1476 + var height = this._container.clientHeight;
  1477 + if (this._showControl()) {
  1478 + height -= 38;
  1479 + }
  1480 + var resizeWidth = this._canvasElement.width;
  1481 + var resizeHeight = this._canvasElement.height;
  1482 + var wScale = width / resizeWidth;
  1483 + var hScale = height / resizeHeight;
  1484 + var scale = wScale > hScale ? hScale : wScale;
  1485 + if (!this._opt.isResize) {
  1486 + if (wScale !== hScale) {
  1487 + scale = wScale + ',' + hScale;
  1488 + }
  1489 + }
  1490 + //
  1491 + if (this._opt.isFullResize) {
  1492 + scale = wScale > hScale ? wScale : hScale;
  1493 + }
  1494 +
  1495 + this._opt.isDebug && console.log('wScale', wScale, 'hScale', hScale, 'scale', scale);
  1496 + this._canvasElement.style.transform = "scale(" + scale + ")"
  1497 + this._canvasElement.style.left = ((width - resizeWidth) / 2) + "px"
  1498 + this._canvasElement.style.top = ((height - resizeHeight) / 2) + "px"
  1499 + }
  1500 +
  1501 + Jessibuca.prototype._fullscreenchange = function () {
  1502 + this.fullscreen = _checkFull();
  1503 + }
  1504 +
  1505 + /**
  1506 + * change buffer
  1507 + * @param buffer
  1508 + */
  1509 + Jessibuca.prototype.changeBuffer = function (buffer) {
  1510 + this._stats.buf = Number(buffer) * 1000;
  1511 + this._decoderWorker.postMessage({cmd: "setVideoBuffer", time: Number(buffer)});
  1512 + };
  1513 + /**
  1514 + * 设置最大缓冲时长,单位秒,播放器会自动消除延迟。
  1515 + * @param buffer
  1516 + */
  1517 + Jessibuca.prototype.setBufferTime = function (buffer) {
  1518 + this.changeBuffer(buffer);
  1519 + };
  1520 +
  1521 + /**
  1522 + * 设置音量大小,取值0.0 — 1.0
  1523 + * 当为0.0时,完全无声
  1524 + * 当为1.0时,最大音量,默认值
  1525 + * @param volume
  1526 + */
  1527 + Jessibuca.prototype.setVolume = function (volume) {
  1528 + if (this._gainNode) {
  1529 + this._gainNode.gain.setValueAtTime(volume, this._audioContext.currentTime);
  1530 + }
  1531 + };
  1532 +
  1533 + /**
  1534 + * 开启屏幕常亮, 在play前调用
  1535 + * 在手机浏览器上, canvas标签渲染视频并不会像video标签那样保持屏幕常亮
  1536 + * H5目前在chrome\edge 84, android chrome 84及以上有原生亮屏API, 需要是https页面
  1537 + * 其余平台为模拟实现,此时为兼容实现,并不保证所有浏览器都支持
  1538 + */
  1539 + Jessibuca.prototype.setKeepScreenOn = function () {
  1540 + this._opt.keepScreenOn = true;
  1541 + };
  1542 +
  1543 +
  1544 + /**
  1545 + * set fullscreen
  1546 + * @param flag
  1547 + */
  1548 + Jessibuca.prototype.setFullscreen = function (flag) {
  1549 + var fullscreen = !!flag;
  1550 + if (this.fullscreen !== fullscreen) {
  1551 + this.fullscreen = fullscreen;
  1552 + }
  1553 + };
  1554 +
  1555 + function _now() {
  1556 + return new Date().getTime();
  1557 + }
  1558 +
  1559 + Jessibuca.prototype._screenshot = function (filename, format, quality) {
  1560 + filename = filename || _now();
  1561 + var formatType = {
  1562 + png: 'image/png',
  1563 + jpeg: 'image/jpeg',
  1564 + webp: 'image/webp'
  1565 + };
  1566 + var encoderOptions = 0.92;
  1567 +
  1568 + if (typeof quality !== 'undefined') {
  1569 + encoderOptions = Number(quality);
  1570 + }
  1571 +
  1572 + var dataURL = this._canvasElement.toDataURL(formatType[format] || formatType.png, encoderOptions);
  1573 + _downloadImg(_dataURLToFile(dataURL), filename);
  1574 + }
  1575 +
  1576 + /**
  1577 + * 截图,调用后弹出下载框保存截图
  1578 + * @param filename 保存的文件名 默认时间戳
  1579 + * @param format 截图的格式,可选png或jpeg或者webp
  1580 + * @param quality 可选参数,当格式是jpeg或者webp时,压缩质量,取值0.0 ~ 1.0
  1581 + */
  1582 + Jessibuca.prototype.screenshot = function (filename, format, quality) {
  1583 + this._screenshot(filename, format, quality);
  1584 + };
  1585 +
  1586 +
  1587 + var eventSplitter = /\s+/;
  1588 +
  1589 + // Execute callbacks
  1590 + function _callEach(list, args, context) {
  1591 + if (list) {
  1592 + for (var i = 0, len = list.length; i < len; i += 1) {
  1593 + list[i].apply(context, args);
  1594 + }
  1595 + }
  1596 + }
  1597 +
  1598 + /**
  1599 + *
  1600 + * @param events
  1601 + * @param callback
  1602 + * @returns {Jessibuca}
  1603 + */
  1604 + Jessibuca.prototype.on = function (events, callback) {
  1605 + var cache, event, list;
  1606 + if (!callback) return this;
  1607 + cache = this.__events || (this.__events = {});
  1608 + events = events.split(eventSplitter);
  1609 + while (event = events.shift()) {
  1610 + list = cache[event] || (cache[event] = []);
  1611 + list.push(callback);
  1612 + }
  1613 + return this;
  1614 + };
  1615 + /**
  1616 + *
  1617 + * @param events
  1618 + * @param callback
  1619 + * @returns {Jessibuca}
  1620 + * @private
  1621 + */
  1622 + Jessibuca.prototype._off = function () {
  1623 + var cache;
  1624 + if (!(cache = this.__events)) return this;
  1625 + delete this.__events;
  1626 + return this;
  1627 + };
  1628 +
  1629 + /**
  1630 + *
  1631 + * @param events
  1632 + * @returns {Jessibuca}
  1633 + * @private
  1634 + */
  1635 + Jessibuca.prototype._trigger = function (events) {
  1636 + var cache, event, all, list, i, len, rest = [], args;
  1637 + if (!(cache = this.__events)) return this;
  1638 + events = events.split(eventSplitter);
  1639 + // Fill up `rest` with the callback arguments. Since we're only copying
  1640 + // the tail of `arguments`, a loop is much faster than Array#slice.
  1641 + for (i = 1, len = arguments.length; i < len; i++) {
  1642 + rest[i - 1] = arguments[i];
  1643 + }
  1644 + // For each event, walk through the list of callbacks twice, first to
  1645 + // trigger the event, then to trigger any `"all"` callbacks.
  1646 + while (event = events.shift()) {
  1647 + if (list = cache[event]) list = list.slice();
  1648 + // Execute event callbacks.
  1649 + _callEach(list, rest, this);
  1650 + }
  1651 + return this;
  1652 + }
  1653 +
  1654 + if (typeof define === 'function') {
  1655 + define(function () {
  1656 + return Jessibuca;
  1657 + });
  1658 + } else if (typeof exports !== 'undefined') {
  1659 + module.exports = Jessibuca;
  1660 + } else {
  1661 + window.Jessibuca = Jessibuca;
  1662 + }
  1663 +})();
src/api/fee/meterTypeManageApi.js 0 → 100644
  1 +import request from '@/utils/request'
  2 +import { getCommunityId } from '@/api/community/communityApi'
  3 +
  4 +// 获取抄表类型列表
  5 +export function listMeterType(params) {
  6 + return new Promise((resolve, reject) => {
  7 + request({
  8 + url: '/meterType.listMeterType',
  9 + method: 'get',
  10 + params: {
  11 + ...params,
  12 + communityId: getCommunityId()
  13 + }
  14 + }).then(response => {
  15 + const res = response.data
  16 + resolve(res)
  17 + }).catch(error => {
  18 + reject(error)
  19 + })
  20 + })
  21 +}
  22 +
  23 +// 添加抄表类型
  24 +export function saveMeterType(data) {
  25 + return new Promise((resolve, reject) => {
  26 + request({
  27 + url: '/meterType.saveMeterType',
  28 + method: 'post',
  29 + data: {
  30 + ...data,
  31 + communityId: getCommunityId()
  32 + }
  33 + }).then(response => {
  34 + const res = response.data
  35 + resolve(res)
  36 + }).catch(error => {
  37 + reject(error)
  38 + })
  39 + })
  40 +}
  41 +
  42 +// 更新抄表类型
  43 +export function updateMeterType(data) {
  44 + return new Promise((resolve, reject) => {
  45 + request({
  46 + url: '/meterType.updateMeterType',
  47 + method: 'post',
  48 + data: {
  49 + ...data,
  50 + communityId: getCommunityId()
  51 + }
  52 + }).then(response => {
  53 + const res = response.data
  54 + resolve(res)
  55 + }).catch(error => {
  56 + reject(error)
  57 + })
  58 + })
  59 +}
  60 +
  61 +// 删除抄表类型
  62 +export function deleteMeterType(data) {
  63 + return new Promise((resolve, reject) => {
  64 + request({
  65 + url: '/meterType.deleteMeterType',
  66 + method: 'post',
  67 + data: {
  68 + ...data,
  69 + communityId: getCommunityId()
  70 + }
  71 + }).then(response => {
  72 + const res = response.data
  73 + resolve(res)
  74 + }).catch(error => {
  75 + reject(error)
  76 + })
  77 + })
  78 +}
0 \ No newline at end of file 79 \ No newline at end of file
src/components/fee/addMeterType.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterTypeManage.addTitle')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  9 + <el-form-item :label="$t('meterTypeManage.typeName')" prop="typeName">
  10 + <el-input
  11 + v-model="form.typeName"
  12 + :placeholder="$t('meterTypeManage.typeNamePlaceholder')"
  13 + />
  14 + </el-form-item>
  15 + <el-form-item :label="$t('meterTypeManage.remark')" prop="remark">
  16 + <el-input
  17 + v-model="form.remark"
  18 + type="textarea"
  19 + :rows="3"
  20 + :placeholder="$t('meterTypeManage.remarkPlaceholder')"
  21 + />
  22 + </el-form-item>
  23 + </el-form>
  24 + <div slot="footer" class="dialog-footer">
  25 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  26 + <el-button type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
  27 + </div>
  28 + </el-dialog>
  29 +</template>
  30 +
  31 +<script>
  32 +import { saveMeterType } from '@/api/fee/meterTypeManageApi'
  33 +import { getCommunityId } from '@/api/community/communityApi'
  34 +
  35 +export default {
  36 + name: 'AddMeterType',
  37 + data() {
  38 + return {
  39 + visible: false,
  40 + form: {
  41 + typeName: '',
  42 + remark: '',
  43 + communityId: ''
  44 + },
  45 + rules: {
  46 + typeName: [
  47 + { required: true, message: this.$t('meterTypeManage.typeNameRequired'), trigger: 'blur' },
  48 + { max: 12, message: this.$t('meterTypeManage.typeNameMaxLength'), trigger: 'blur' }
  49 + ],
  50 + remark: [
  51 + { required: true, message: this.$t('meterTypeManage.remarkRequired'), trigger: 'blur' },
  52 + { max: 200, message: this.$t('meterTypeManage.remarkMaxLength'), trigger: 'blur' }
  53 + ]
  54 + }
  55 + }
  56 + },
  57 + methods: {
  58 + open() {
  59 + this.visible = true
  60 + this.form.communityId = getCommunityId()
  61 + this.$nextTick(() => {
  62 + this.$refs.form && this.$refs.form.resetFields()
  63 + })
  64 + },
  65 + handleClose() {
  66 + this.$refs.form.resetFields()
  67 + },
  68 + handleSubmit() {
  69 + this.$refs.form.validate(async valid => {
  70 + if (valid) {
  71 + try {
  72 + await saveMeterType(this.form)
  73 + this.$message.success(this.$t('meterTypeManage.addSuccess'))
  74 + this.visible = false
  75 + this.$emit('success')
  76 + } catch (error) {
  77 + this.$message.error(error.message || this.$t('meterTypeManage.addFailed'))
  78 + }
  79 + }
  80 + })
  81 + }
  82 + }
  83 +}
  84 +</script>
0 \ No newline at end of file 85 \ No newline at end of file
src/components/fee/addMeterWater.vue
@@ -202,7 +202,6 @@ export default { @@ -202,7 +202,6 @@ export default {
202 price: 0, 202 price: 0,
203 communityId: getCommunityId() 203 communityId: getCommunityId()
204 } 204 }
205 - this.$refs.form && this.$refs.form.resetFields()  
206 }, 205 },
207 handleClose() { 206 handleClose() {
208 this.resetForm() 207 this.resetForm()
@@ -211,7 +210,7 @@ export default { @@ -211,7 +210,7 @@ export default {
211 try { 210 try {
212 const data = await getDict('pay_fee_config', 'fee_type_cd') 211 const data = await getDict('pay_fee_config', 'fee_type_cd')
213 this.feeTypeOptions = data.map(item => ({ 212 this.feeTypeOptions = data.map(item => ({
214 - value: item.value, 213 + value: item.name,
215 label: item.label 214 label: item.label
216 })) 215 }))
217 } catch (error) { 216 } catch (error) {
src/components/fee/deleteMeterType.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterTypeManage.deleteTitle')"
  4 + :visible.sync="visible"
  5 + width="30%"
  6 + @close="handleClose"
  7 + >
  8 + <div class="delete-content">
  9 + <p>{{ $t('meterTypeManage.deleteConfirm') }}</p>
  10 + <p class="delete-tip">{{ $t('meterTypeManage.deleteTip') }}</p>
  11 + </div>
  12 + <div slot="footer" class="dialog-footer">
  13 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  14 + <el-button type="primary" @click="handleConfirm" :loading="loading">{{ $t('common.confirm') }}</el-button>
  15 + </div>
  16 + </el-dialog>
  17 +</template>
  18 +
  19 +<script>
  20 +import { deleteMeterType } from '@/api/fee/meterTypeManageApi'
  21 +import { getCommunityId } from '@/api/community/communityApi'
  22 +
  23 +export default {
  24 + name: 'DeleteMeterType',
  25 + data() {
  26 + return {
  27 + visible: false,
  28 + loading: false,
  29 + form: {
  30 + typeId: '',
  31 + communityId: ''
  32 + }
  33 + }
  34 + },
  35 + methods: {
  36 + open(row) {
  37 + this.form = {
  38 + typeId: row.typeId,
  39 + communityId: getCommunityId()
  40 + }
  41 + this.visible = true
  42 + },
  43 + handleClose() {
  44 + this.visible = false
  45 + this.loading = false
  46 + },
  47 + async handleConfirm() {
  48 + try {
  49 + this.loading = true
  50 + await deleteMeterType(this.form)
  51 + this.$message.success(this.$t('meterTypeManage.deleteSuccess'))
  52 + this.visible = false
  53 + this.$emit('success')
  54 + } catch (error) {
  55 + this.$message.error(error.message || this.$t('meterTypeManage.deleteFailed'))
  56 + } finally {
  57 + this.loading = false
  58 + }
  59 + }
  60 + }
  61 +}
  62 +</script>
  63 +
  64 +<style scoped>
  65 +.delete-content {
  66 + text-align: center;
  67 + padding: 20px 0;
  68 +}
  69 +.delete-tip {
  70 + color: #f56c6c;
  71 + margin-top: 10px;
  72 +}
  73 +</style>
0 \ No newline at end of file 74 \ No newline at end of file
src/components/fee/editMeterType.vue 0 → 100644
  1 +<template>
  2 + <el-dialog
  3 + :title="$t('meterTypeManage.editTitle')"
  4 + :visible.sync="visible"
  5 + width="50%"
  6 + @close="handleClose"
  7 + >
  8 + <el-form ref="form" :model="form" :rules="rules" label-width="120px">
  9 + <el-form-item :label="$t('meterTypeManage.typeId')" prop="typeId">
  10 + <el-input v-model="form.typeId" disabled />
  11 + </el-form-item>
  12 + <el-form-item :label="$t('meterTypeManage.typeName')" prop="typeName">
  13 + <el-input
  14 + v-model="form.typeName"
  15 + :placeholder="$t('meterTypeManage.typeNamePlaceholder')"
  16 + />
  17 + </el-form-item>
  18 + <el-form-item :label="$t('meterTypeManage.remark')" prop="remark">
  19 + <el-input
  20 + v-model="form.remark"
  21 + type="textarea"
  22 + :rows="3"
  23 + :placeholder="$t('meterTypeManage.remarkPlaceholder')"
  24 + />
  25 + </el-form-item>
  26 + </el-form>
  27 + <div slot="footer" class="dialog-footer">
  28 + <el-button @click="visible = false">{{ $t('common.cancel') }}</el-button>
  29 + <el-button type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</el-button>
  30 + </div>
  31 + </el-dialog>
  32 +</template>
  33 +
  34 +<script>
  35 +import { updateMeterType } from '@/api/fee/meterTypeManageApi'
  36 +import { getCommunityId } from '@/api/community/communityApi'
  37 +
  38 +export default {
  39 + name: 'EditMeterType',
  40 + data() {
  41 + return {
  42 + visible: false,
  43 + form: {
  44 + typeId: '',
  45 + typeName: '',
  46 + remark: '',
  47 + communityId: ''
  48 + },
  49 + rules: {
  50 + typeId: [
  51 + { required: true, message: this.$t('meterTypeManage.typeIdRequired'), trigger: 'blur' }
  52 + ],
  53 + typeName: [
  54 + { required: true, message: this.$t('meterTypeManage.typeNameRequired'), trigger: 'blur' },
  55 + { max: 12, message: this.$t('meterTypeManage.typeNameMaxLength'), trigger: 'blur' }
  56 + ],
  57 + remark: [
  58 + { required: true, message: this.$t('meterTypeManage.remarkRequired'), trigger: 'blur' },
  59 + { max: 200, message: this.$t('meterTypeManage.remarkMaxLength'), trigger: 'blur' }
  60 + ]
  61 + }
  62 + }
  63 + },
  64 + methods: {
  65 + open(row) {
  66 + this.form = {
  67 + ...row,
  68 + communityId: getCommunityId()
  69 + }
  70 + this.visible = true
  71 + this.$nextTick(() => {
  72 + this.$refs.form && this.$refs.form.clearValidate()
  73 + })
  74 + },
  75 + handleClose() {
  76 + this.$refs.form.resetFields()
  77 + },
  78 + handleSubmit() {
  79 + this.$refs.form.validate(async valid => {
  80 + if (valid) {
  81 + try {
  82 + await updateMeterType(this.form)
  83 + this.$message.success(this.$t('meterTypeManage.editSuccess'))
  84 + this.visible = false
  85 + this.$emit('success')
  86 + } catch (error) {
  87 + this.$message.error(error.message || this.$t('meterTypeManage.editFailed'))
  88 + }
  89 + }
  90 + })
  91 + }
  92 + }
  93 +}
  94 +</script>
0 \ No newline at end of file 95 \ No newline at end of file
src/components/fee/importMeterWaterFee.vue
@@ -147,8 +147,8 @@ export default { @@ -147,8 +147,8 @@ export default {
147 try { 147 try {
148 const data = await getDict('pay_fee_config', 'fee_type_cd') 148 const data = await getDict('pay_fee_config', 'fee_type_cd')
149 this.feeTypeOptions = data.map(item => ({ 149 this.feeTypeOptions = data.map(item => ({
150 - value: item.value,  
151 - label: item.label 150 + value: item.statusCd,
  151 + label: item.name
152 })) 152 }))
153 } catch (error) { 153 } catch (error) {
154 console.error('Failed to load fee types:', error) 154 console.error('Failed to load fee types:', error)
src/components/fee/importMeterWaterFee2.vue
@@ -148,8 +148,8 @@ export default { @@ -148,8 +148,8 @@ export default {
148 try { 148 try {
149 const data = await getDict('pay_fee_config', 'fee_type_cd') 149 const data = await getDict('pay_fee_config', 'fee_type_cd')
150 this.feeTypeOptions = data.map(item => ({ 150 this.feeTypeOptions = data.map(item => ({
151 - value: item.value,  
152 - label: item.label 151 + value: item.statusCd,
  152 + label: item.name
153 })) 153 }))
154 } catch (error) { 154 } catch (error) {
155 console.error('Failed to load fee types:', error) 155 console.error('Failed to load fee types:', error)
src/components/fee/roomMeterQrcode.vue
1 <template> 1 <template>
2 - <el-dialog  
3 - :title="$t('meterWater.meterQRCode')"  
4 - :visible.sync="dialogVisible"  
5 - width="50%"  
6 - @close="handleClose"  
7 - >  
8 - <el-form  
9 - ref="form"  
10 - :model="form"  
11 - :rules="rules"  
12 - label-width="120px"  
13 - label-position="right"  
14 - > 2 + <el-dialog :title="$t('meterWater.meterQRCode')" :visible.sync="dialogVisible" width="50%" @close="handleClose">
  3 + <el-form ref="form" :model="form" :rules="rules" label-width="120px" label-position="right">
15 <el-form-item :label="$t('meterWater.feeType')" prop="feeTypeCd"> 4 <el-form-item :label="$t('meterWater.feeType')" prop="feeTypeCd">
16 - <el-select  
17 - v-model="form.feeTypeCd"  
18 - :placeholder="$t('meterWater.selectFeeType')"  
19 - style="width: 100%"  
20 - @change="handleFeeTypeChange"  
21 - >  
22 - <el-option  
23 - v-for="item in feeTypeOptions"  
24 - :key="item.value"  
25 - :label="item.label"  
26 - :value="item.value"  
27 - /> 5 + <el-select v-model="form.feeTypeCd" :placeholder="$t('meterWater.selectFeeType')" style="width: 100%"
  6 + @change="handleFeeTypeChange">
  7 + <el-option v-for="item in feeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
28 </el-select> 8 </el-select>
29 </el-form-item> 9 </el-form-item>
30 10
31 <el-form-item :label="$t('meterWater.feeItem')" prop="configId"> 11 <el-form-item :label="$t('meterWater.feeItem')" prop="configId">
32 - <el-select  
33 - v-model="form.configId"  
34 - :placeholder="$t('meterWater.selectFeeItem')"  
35 - style="width: 100%"  
36 - @change="generateQRCode"  
37 - >  
38 - <el-option  
39 - v-for="item in feeConfigs"  
40 - :key="item.configId"  
41 - :label="item.feeName"  
42 - :value="item.configId"  
43 - /> 12 + <el-select v-model="form.configId" :placeholder="$t('meterWater.selectFeeItem')" style="width: 100%"
  13 + @change="generateQRCode">
  14 + <el-option v-for="item in feeConfigs" :key="item.configId" :label="item.feeName" :value="item.configId" />
44 </el-select> 15 </el-select>
45 <div class="form-tip"> 16 <div class="form-tip">
46 {{ $t('meterWater.feeItemTip') }} 17 {{ $t('meterWater.feeItemTip') }}
@@ -48,27 +19,14 @@ @@ -48,27 +19,14 @@
48 </el-form-item> 19 </el-form-item>
49 20
50 <el-form-item :label="$t('meterWater.meterType')" prop="meterType"> 21 <el-form-item :label="$t('meterWater.meterType')" prop="meterType">
51 - <el-select  
52 - v-model="form.meterType"  
53 - :placeholder="$t('meterWater.selectMeterType')"  
54 - style="width: 100%"  
55 - @change="generateQRCode"  
56 - >  
57 - <el-option  
58 - v-for="item in meterTypes"  
59 - :key="item.typeId"  
60 - :label="item.typeName"  
61 - :value="item.typeId"  
62 - /> 22 + <el-select v-model="form.meterType" :placeholder="$t('meterWater.selectMeterType')" style="width: 100%"
  23 + @change="generateQRCode">
  24 + <el-option v-for="item in meterTypes" :key="item.typeId" :label="item.typeName" :value="item.typeId" />
63 </el-select> 25 </el-select>
64 </el-form-item> 26 </el-form-item>
65 27
66 <el-form-item :label="$t('meterWater.room')"> 28 <el-form-item :label="$t('meterWater.room')">
67 - <el-input  
68 - v-model="form.ownerName"  
69 - :placeholder="$t('meterWater.inputRoom')"  
70 - disabled  
71 - /> 29 + <el-input v-model="form.ownerName" :placeholder="$t('meterWater.inputRoom')" disabled />
72 </el-form-item> 30 </el-form-item>
73 31
74 <el-form-item v-if="showQRCode" :label="$t('meterWater.qrCode')"> 32 <el-form-item v-if="showQRCode" :label="$t('meterWater.qrCode')">
@@ -138,8 +96,8 @@ export default { @@ -138,8 +96,8 @@ export default {
138 this.resetForm() 96 this.resetForm()
139 if (params) { 97 if (params) {
140 this.form.roomId = params.roomId 98 this.form.roomId = params.roomId
141 - this.form.ownerName = params.ownerName  
142 - ? `${params.roomName}(${params.ownerName})` 99 + this.form.ownerName = params.ownerName
  100 + ? `${params.roomName}(${params.ownerName})`
143 : params.roomName 101 : params.roomName
144 } 102 }
145 this.dialogVisible = true 103 this.dialogVisible = true
@@ -155,7 +113,6 @@ export default { @@ -155,7 +113,6 @@ export default {
155 communityId: getCommunityId() 113 communityId: getCommunityId()
156 } 114 }
157 this.showQRCode = false 115 this.showQRCode = false
158 - this.$refs.form && this.$refs.form.resetFields()  
159 this.clearQRCode() 116 this.clearQRCode()
160 }, 117 },
161 handleClose() { 118 handleClose() {
@@ -171,8 +128,8 @@ export default { @@ -171,8 +128,8 @@ export default {
171 try { 128 try {
172 const data = await getDict('pay_fee_config', 'fee_type_cd') 129 const data = await getDict('pay_fee_config', 'fee_type_cd')
173 this.feeTypeOptions = data.map(item => ({ 130 this.feeTypeOptions = data.map(item => ({
174 - value: item.value,  
175 - label: item.label 131 + value: item.statusCd,
  132 + label: item.name
176 })) 133 }))
177 } catch (error) { 134 } catch (error) {
178 console.error('Failed to load fee types:', error) 135 console.error('Failed to load fee types:', error)
src/components/fee/roomTreeDiv.vue renamed to src/components/room/roomTreeDiv.vue
@@ -41,45 +41,44 @@ export default { @@ -41,45 +41,44 @@ export default {
41 this.buildTreeData(units) 41 this.buildTreeData(units)
42 } catch (error) { 42 } catch (error) {
43 console.error('Failed to load tree data:', error) 43 console.error('Failed to load tree data:', error)
  44 + this.$message.error(this.$t('roomTree.loadError'))
44 } 45 }
45 }, 46 },
46 buildTreeData(units) { 47 buildTreeData(units) {
47 - const treeMap = new Map() 48 + const floorMap = {}
48 49
49 - // Build floor nodes 50 + // Build floor nodes and unit nodes
50 units.forEach(unit => { 51 units.forEach(unit => {
51 - if (!treeMap.has(unit.floorId)) {  
52 - treeMap.set(unit.floorId, { 52 + if (!floorMap[unit.floorId]) {
  53 + floorMap[unit.floorId] = {
53 id: `f_${unit.floorId}`, 54 id: `f_${unit.floorId}`,
54 floorId: unit.floorId, 55 floorId: unit.floorId,
55 floorNum: unit.floorNum, 56 floorNum: unit.floorNum,
56 - icon: 'el-icon-office-building',  
57 - text: `${unit.floorNum}栋`, 57 + icon: "/img/floor.png",
  58 + text: `${unit.floorNum}${this.$t('room.floorUnitTree.building')}`,
58 children: [] 59 children: []
59 - }) 60 + }
60 } 61 }
61 - })  
62 62
63 - // Build unit nodes  
64 - units.forEach(unit => {  
65 - const floorNode = treeMap.get(unit.floorId)  
66 - if (floorNode) {  
67 - floorNode.children.push({  
68 - id: `u_${unit.unitId}`,  
69 - unitId: unit.unitId,  
70 - unitNum: unit.unitNum,  
71 - icon: 'el-icon-connection',  
72 - text: `${unit.unitNum}单元`,  
73 - children: []  
74 - })  
75 - } 63 + floorMap[unit.floorId].children.push({
  64 + id: `u_${unit.unitId}`,
  65 + unitId: unit.unitId,
  66 + unitNum: unit.unitNum,
  67 + floorId: unit.floorId, // Add floorId reference
  68 + icon: "/img/unit.png",
  69 + text: `${unit.unitNum}${this.$t('room.floorUnitTree.unit')}`,
  70 + children: []
  71 + })
76 }) 72 })
77 73
78 - this.treeData = Array.from(treeMap.values()) 74 + this.treeData = Object.values(floorMap)
79 }, 75 },
80 - async handleNodeClick(data) { 76 + async handleNodeClick(data, node) {
81 if (data.id.startsWith('u_')) { 77 if (data.id.startsWith('u_')) {
82 - await this.loadRooms(data) 78 + if (!node.expanded) {
  79 + await this.loadRooms(data, node)
  80 + node.expanded = true
  81 + }
83 } else if (data.id.startsWith('r_')) { 82 } else if (data.id.startsWith('r_')) {
84 this.$emit('selectRoom', { 83 this.$emit('selectRoom', {
85 roomId: data.roomId, 84 roomId: data.roomId,
@@ -87,48 +86,32 @@ export default { @@ -87,48 +86,32 @@ export default {
87 }) 86 })
88 } 87 }
89 }, 88 },
90 - async loadRooms(node) { 89 + async loadRooms(unitData, node) {
91 try { 90 try {
92 - const {rooms} = await queryRoomsTree({  
93 - unitId: node.unitId, 91 + const { rooms } = await queryRoomsTree({
  92 + unitId: unitData.unitId,
94 communityId: this.communityId, 93 communityId: this.communityId,
95 page: 1, 94 page: 1,
96 row: 1000 95 row: 1000
97 }) 96 })
98 97
99 - const roomNodes = rooms.map(room => ({  
100 - id: `r_${room.roomId}`,  
101 - roomId: room.roomId,  
102 - roomName: `${room.floorNum}-${room.unitNum}-${room.roomNum}`,  
103 - icon: 'el-icon-house',  
104 - text: room.ownerName  
105 - ? `${room.roomNum}室(${room.ownerName})`  
106 - : `${room.roomNum}室`  
107 - })) 98 + if (rooms && rooms.length > 0) {
  99 + const roomNodes = rooms.map(room => ({
  100 + id: `r_${room.roomId}`,
  101 + roomId: room.roomId,
  102 + roomName: `${room.floorNum}-${room.unitNum}-${room.roomNum}`,
  103 + icon: "/img/room.png",
  104 + text: room.ownerName
  105 + ? `${room.roomNum}${room.ownerName})`
  106 + : `${room.roomNum}`
  107 + }))
108 108
109 - // Update tree data  
110 - const floorNode = this.treeData.find(  
111 - item => item.id === `f_${node.floorId}`  
112 - )  
113 - if (floorNode) {  
114 - const unitNode = floorNode.children.find(  
115 - item => item.id === `u_${node.unitId}`  
116 - )  
117 - if (unitNode) {  
118 - this.$set(unitNode, 'children', roomNodes)  
119 - this.$refs.tree.updateKeyChildren(unitNode.id, roomNodes)  
120 -  
121 - // Auto select first room  
122 - if (roomNodes.length > 0) {  
123 - this.$emit('selectRoom', {  
124 - roomId: roomNodes[0].roomId,  
125 - roomName: roomNodes[0].roomName  
126 - })  
127 - }  
128 - } 109 + // Update the node's children
  110 + this.$set(node.data, 'children', roomNodes)
129 } 111 }
130 } catch (error) { 112 } catch (error) {
131 console.error('Failed to load rooms:', error) 113 console.error('Failed to load rooms:', error)
  114 + this.$message.error(this.$t('roomTree.loadRoomError'))
132 } 115 }
133 } 116 }
134 } 117 }
src/i18n/feeI18n.js
1 import { messages as contractCreateFeeMessages } from '../views/fee/contractCreateFeeLang' 1 import { messages as contractCreateFeeMessages } from '../views/fee/contractCreateFeeLang'
2 import { messages as meterWaterManageMessages } from '../views/fee/meterWaterManageLang' 2 import { messages as meterWaterManageMessages } from '../views/fee/meterWaterManageLang'
  3 +import { messages as meterTypeManageMessages } from '../views/fee/meterTypeManageLang'
3 export const messages = { 4 export const messages = {
4 en: { 5 en: {
5 ...contractCreateFeeMessages.en, 6 ...contractCreateFeeMessages.en,
6 ...meterWaterManageMessages.en, 7 ...meterWaterManageMessages.en,
  8 + ...meterTypeManageMessages.en,
7 }, 9 },
8 zh: { 10 zh: {
9 ...contractCreateFeeMessages.zh, 11 ...contractCreateFeeMessages.zh,
10 ...meterWaterManageMessages.zh, 12 ...meterWaterManageMessages.zh,
  13 + ...meterTypeManageMessages.zh,
11 } 14 }
12 } 15 }
13 \ No newline at end of file 16 \ No newline at end of file
src/router/feeRouter.js
1 export default [ 1 export default [
2 { 2 {
3 - path:'/pages/property/contractCreateFee',  
4 - name:'/pages/property/contractCreateFee', 3 + path: '/pages/property/contractCreateFee',
  4 + name: '/pages/property/contractCreateFee',
5 component: () => import('@/views/fee/contractCreateFeeList.vue') 5 component: () => import('@/views/fee/contractCreateFeeList.vue')
6 - },  
7 - {  
8 - path:'/pages/property/meterWaterManage',  
9 - name:'/pages/property/meterWaterManage',  
10 - component: () => import('@/views/fee/meterWaterManageList.vue')  
11 - }, 6 + },
  7 + {
  8 + path: '/pages/property/meterWaterManage',
  9 + name: '/pages/property/meterWaterManage',
  10 + component: () => import('@/views/fee/meterWaterManageList.vue')
  11 + },
  12 + {
  13 + path: '/views/fee/meterTypeManage',
  14 + name: '/views/fee/meterTypeManage',
  15 + component: () => import('@/views/fee/meterTypeManageList.vue')
  16 + },
12 ] 17 ]
13 \ No newline at end of file 18 \ No newline at end of file
src/router/index.js
@@ -688,7 +688,7 @@ const router = new VueRouter({ @@ -688,7 +688,7 @@ const router = new VueRouter({
688 // 路由守卫 688 // 路由守卫
689 router.beforeEach((to, from, next) => { 689 router.beforeEach((to, from, next) => {
690 // 排除静态资源路径 690 // 排除静态资源路径
691 - if (to.path.startsWith('/img/') || to.path.startsWith('/static/')) { 691 + if (to.path.startsWith('/img/') || to.path.startsWith('/static/') || to.path.startsWith('/js/')) {
692 return next(); // 直接放行 692 return next(); // 直接放行
693 } 693 }
694 if (to.path.endsWith('.xlsx')) { 694 if (to.path.endsWith('.xlsx')) {
src/views/fee/meterTypeManageLang.js 0 → 100644
  1 +export const messages = {
  2 + en: {
  3 + meterTypeManage: {
  4 + title: 'Meter Type Management',
  5 + typeId: 'Meter Type ID',
  6 + typeName: 'Name',
  7 + remark: 'Description',
  8 + createTime: 'Create Time',
  9 + operation: 'Operation',
  10 + tip: 'If a property has multiple electricity/water meters, you can create multiple types here, e.g. Electricity Meter 1, Electricity Meter 2',
  11 + addTitle: 'Add Meter Type',
  12 + editTitle: 'Edit Meter Type',
  13 + deleteTitle: 'Confirm Operation',
  14 + deleteConfirm: 'Are you sure to delete this meter type?',
  15 + deleteTip: 'This operation cannot be undone!',
  16 + typeNamePlaceholder: 'Required, please enter name',
  17 + remarkPlaceholder: 'Required, please enter description',
  18 + typeNameRequired: 'Name is required',
  19 + typeNameMaxLength: 'Name cannot exceed 12 characters',
  20 + remarkRequired: 'Description is required',
  21 + remarkMaxLength: 'Description cannot exceed 200 characters',
  22 + typeIdRequired: 'Meter Type ID is required',
  23 + addSuccess: 'Added successfully',
  24 + addFailed: 'Failed to add',
  25 + editSuccess: 'Updated successfully',
  26 + editFailed: 'Failed to update',
  27 + deleteSuccess: 'Deleted successfully',
  28 + deleteFailed: 'Failed to delete',
  29 + fetchError: 'Failed to fetch data'
  30 + }
  31 + },
  32 + zh: {
  33 + meterTypeManage: {
  34 + title: '抄表类型管理',
  35 + typeId: '抄表类型ID',
  36 + typeName: '名称',
  37 + remark: '说明',
  38 + createTime: '创建时间',
  39 + operation: '操作',
  40 + tip: '如果一个房屋有多个电表水表可以在这里新建多个 比如电表1 电表2',
  41 + addTitle: '添加抄表类型',
  42 + editTitle: '修改抄表类型',
  43 + deleteTitle: '确认操作',
  44 + deleteConfirm: '确定删除该抄表类型吗?',
  45 + deleteTip: '此操作不可撤销!',
  46 + typeNamePlaceholder: '必填,请填写名称',
  47 + remarkPlaceholder: '必填,请填写说明',
  48 + typeNameRequired: '名称不能为空',
  49 + typeNameMaxLength: '名称不能超过12个字符',
  50 + remarkRequired: '说明不能为空',
  51 + remarkMaxLength: '说明不能超过200个字符',
  52 + typeIdRequired: '抄表类型ID不能为空',
  53 + addSuccess: '添加成功',
  54 + addFailed: '添加失败',
  55 + editSuccess: '修改成功',
  56 + editFailed: '修改失败',
  57 + deleteSuccess: '删除成功',
  58 + deleteFailed: '删除失败',
  59 + fetchError: '获取数据失败'
  60 + }
  61 + }
  62 +}
0 \ No newline at end of file 63 \ No newline at end of file
src/views/fee/meterTypeManageList.vue 0 → 100644
  1 +<template>
  2 + <div class="meter-type-manage-container">
  3 + <el-card class="box-card">
  4 + <div slot="header" class="flex justify-between">
  5 + <span>{{ $t('meterTypeManage.title') }}</span>
  6 + <div class="header-tools">
  7 + <el-button type="primary" size="small" @click="openAddMeterTypeModal">
  8 + <i class="el-icon-plus"></i>{{ $t('common.add') }}
  9 + </el-button>
  10 + <el-button size="small" @click="goBack">{{ $t('common.back') }}</el-button>
  11 + </div>
  12 + </div>
  13 +
  14 + <el-table v-loading="loading" :data="meterTypeList" border style="width: 100%">
  15 + <el-table-column prop="typeId" :label="$t('meterTypeManage.typeId')" align="center" />
  16 + <el-table-column prop="typeName" :label="$t('meterTypeManage.typeName')" align="center" />
  17 + <el-table-column prop="remark" :label="$t('meterTypeManage.remark')" align="center" />
  18 + <el-table-column prop="createTime" :label="$t('meterTypeManage.createTime')" align="center" />
  19 + <el-table-column :label="$t('common.operation')" align="center" width="200">
  20 + <template slot-scope="scope">
  21 + <el-button size="mini" @click="openEditMeterTypeModal(scope.row)">{{ $t('common.edit') }}</el-button>
  22 + <el-button size="mini" type="danger" @click="openDeleteMeterTypeModal(scope.row)">{{ $t('common.delete')
  23 + }}</el-button>
  24 + </template>
  25 + </el-table-column>
  26 + </el-table>
  27 +
  28 + <el-row class="margin-top">
  29 + <el-col :span="18" class="text-left">
  30 + <div>{{ $t('meterTypeManage.tip') }}</div>
  31 + </el-col>
  32 + <el-col :span="6">
  33 + <el-pagination :current-page.sync="pagination.current" :page-sizes="[10, 20, 30, 50]"
  34 + :page-size="pagination.size" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
  35 + @size-change="handleSizeChange" @current-change="handleCurrentChange" />
  36 + </el-col>
  37 + </el-row>
  38 + </el-card>
  39 +
  40 + <add-meter-type ref="addMeterType" @success="handleSuccess" />
  41 + <edit-meter-type ref="editMeterType" @success="handleSuccess" />
  42 + <delete-meter-type ref="deleteMeterType" @success="handleSuccess" />
  43 + </div>
  44 +</template>
  45 +
  46 +<script>
  47 +import { listMeterType } from '@/api/fee/meterTypeManageApi'
  48 +import AddMeterType from '@/components/fee/addMeterType'
  49 +import EditMeterType from '@/components/fee/editMeterType'
  50 +import DeleteMeterType from '@/components/fee/deleteMeterType'
  51 +import { getCommunityId } from '@/api/community/communityApi'
  52 +
  53 +export default {
  54 + name: 'MeterTypeManageList',
  55 + components: {
  56 + AddMeterType,
  57 + EditMeterType,
  58 + DeleteMeterType
  59 + },
  60 + data() {
  61 + return {
  62 + loading: false,
  63 + meterTypeList: [],
  64 + pagination: {
  65 + current: 1,
  66 + size: 10,
  67 + total: 0
  68 + },
  69 + communityId: ''
  70 + }
  71 + },
  72 + created() {
  73 + this.communityId = getCommunityId()
  74 + this.getList()
  75 + },
  76 + methods: {
  77 + async getList() {
  78 + try {
  79 + this.loading = true
  80 + const params = {
  81 + page: this.pagination.current,
  82 + row: this.pagination.size,
  83 + communityId: this.communityId
  84 + }
  85 + const { data, total } = await listMeterType(params)
  86 + this.meterTypeList = data
  87 + this.pagination.total = total
  88 + } catch (error) {
  89 + this.$message.error(this.$t('meterTypeManage.fetchError'))
  90 + } finally {
  91 + this.loading = false
  92 + }
  93 + },
  94 + openAddMeterTypeModal() {
  95 + this.$refs.addMeterType.open()
  96 + },
  97 + openEditMeterTypeModal(row) {
  98 + this.$refs.editMeterType.open(row)
  99 + },
  100 + openDeleteMeterTypeModal(row) {
  101 + this.$refs.deleteMeterType.open(row)
  102 + },
  103 + handleSuccess() {
  104 + this.getList()
  105 + },
  106 + handleSizeChange(val) {
  107 + this.pagination.size = val
  108 + this.getList()
  109 + },
  110 + handleCurrentChange(val) {
  111 + this.pagination.current = val
  112 + this.getList()
  113 + },
  114 + goBack() {
  115 + this.$router.go(-1)
  116 + }
  117 + }
  118 +}
  119 +</script>
  120 +
  121 +<style lang="scss" scoped>
  122 +.meter-type-manage-container {
  123 + padding: 20px;
  124 +
  125 + .box-card {
  126 + margin-bottom: 20px;
  127 + }
  128 +
  129 + .header-tools {
  130 + float: right;
  131 + margin-top: -5px;
  132 + }
  133 +
  134 + .margin-top {
  135 + margin-top: 20px;
  136 + }
  137 +}
  138 +</style>
0 \ No newline at end of file 139 \ No newline at end of file
src/views/fee/meterWaterManageList.vue
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 </el-col> 6 </el-col>
7 <el-col :span="20"> 7 <el-col :span="20">
8 <el-card class="box-card"> 8 <el-card class="box-card">
9 - <div slot="header" class="clearfix"> 9 + <div slot="header" class="flex justify-between">
10 <span>{{ $t('meterWater.queryConditions') }}</span> 10 <span>{{ $t('meterWater.queryConditions') }}</span>
11 </div> 11 </div>
12 <el-form :inline="true" :model="conditions" class="demo-form-inline"> 12 <el-form :inline="true" :model="conditions" class="demo-form-inline">
@@ -29,7 +29,7 @@ @@ -29,7 +29,7 @@
29 </el-card> 29 </el-card>
30 30
31 <el-card class="box-card margin-top"> 31 <el-card class="box-card margin-top">
32 - <div slot="header" class="clearfix"> 32 + <div slot="header" class="flex justify-between">
33 <span>{{ $t('meterWater.meterReadingInfo') }}</span> 33 <span>{{ $t('meterWater.meterReadingInfo') }}</span>
34 <div style="float: right"> 34 <div style="float: right">
35 <el-button v-if="conditions.objId" type="primary" size="small" @click="handleOpenAddMeter"> 35 <el-button v-if="conditions.objId" type="primary" size="small" @click="handleOpenAddMeter">
@@ -94,7 +94,7 @@ import { @@ -94,7 +94,7 @@ import {
94 listMeterTypes 94 listMeterTypes
95 } from '@/api/fee/meterWaterManageApi' 95 } from '@/api/fee/meterWaterManageApi'
96 import { getCommunityId } from '@/api/community/communityApi' 96 import { getCommunityId } from '@/api/community/communityApi'
97 -import RoomTreeDiv from '@/components/fee/roomTreeDiv' 97 +import RoomTreeDiv from '@/components/room/roomTreeDiv'
98 import AddMeterWater from '@/components/fee/addMeterWater' 98 import AddMeterWater from '@/components/fee/addMeterWater'
99 import EditMeterWater from '@/components/fee/editMeterWater' 99 import EditMeterWater from '@/components/fee/editMeterWater'
100 import DeleteMeterWater from '@/components/fee/deleteMeterWater' 100 import DeleteMeterWater from '@/components/fee/deleteMeterWater'
@@ -209,7 +209,7 @@ export default { @@ -209,7 +209,7 @@ export default {
209 }) 209 })
210 }, 210 },
211 handleOpenMeterType() { 211 handleOpenMeterType() {
212 - this.$router.push('/fee/meterTypeManage') 212 + this.$router.push('/views/fee/meterTypeManage')
213 }, 213 },
214 handleSuccess() { 214 handleSuccess() {
215 this.loadData() 215 this.loadData()