Commit 9d8dc2e60d319cc296cb9ee77c050f5c7b81829e
1 parent
cd8d442f
开发完成水电抄表
Showing
18 changed files
with
2911 additions
and
134 deletions
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 | 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 | 85 | \ No newline at end of file | ... | ... |
src/components/fee/addMeterWater.vue
| ... | ... | @@ -202,7 +202,6 @@ export default { |
| 202 | 202 | price: 0, |
| 203 | 203 | communityId: getCommunityId() |
| 204 | 204 | } |
| 205 | - this.$refs.form && this.$refs.form.resetFields() | |
| 206 | 205 | }, |
| 207 | 206 | handleClose() { |
| 208 | 207 | this.resetForm() |
| ... | ... | @@ -211,7 +210,7 @@ export default { |
| 211 | 210 | try { |
| 212 | 211 | const data = await getDict('pay_fee_config', 'fee_type_cd') |
| 213 | 212 | this.feeTypeOptions = data.map(item => ({ |
| 214 | - value: item.value, | |
| 213 | + value: item.name, | |
| 215 | 214 | label: item.label |
| 216 | 215 | })) |
| 217 | 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 | 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 | 95 | \ No newline at end of file | ... | ... |
src/components/fee/importMeterWaterFee.vue
| ... | ... | @@ -147,8 +147,8 @@ export default { |
| 147 | 147 | try { |
| 148 | 148 | const data = await getDict('pay_fee_config', 'fee_type_cd') |
| 149 | 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 | 153 | } catch (error) { |
| 154 | 154 | console.error('Failed to load fee types:', error) | ... | ... |
src/components/fee/importMeterWaterFee2.vue
| ... | ... | @@ -148,8 +148,8 @@ export default { |
| 148 | 148 | try { |
| 149 | 149 | const data = await getDict('pay_fee_config', 'fee_type_cd') |
| 150 | 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 | 154 | } catch (error) { |
| 155 | 155 | console.error('Failed to load fee types:', error) | ... | ... |
src/components/fee/roomMeterQrcode.vue
| 1 | 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 | 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 | 8 | </el-select> |
| 29 | 9 | </el-form-item> |
| 30 | 10 | |
| 31 | 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 | 15 | </el-select> |
| 45 | 16 | <div class="form-tip"> |
| 46 | 17 | {{ $t('meterWater.feeItemTip') }} |
| ... | ... | @@ -48,27 +19,14 @@ |
| 48 | 19 | </el-form-item> |
| 49 | 20 | |
| 50 | 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 | 25 | </el-select> |
| 64 | 26 | </el-form-item> |
| 65 | 27 | |
| 66 | 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 | 30 | </el-form-item> |
| 73 | 31 | |
| 74 | 32 | <el-form-item v-if="showQRCode" :label="$t('meterWater.qrCode')"> |
| ... | ... | @@ -138,8 +96,8 @@ export default { |
| 138 | 96 | this.resetForm() |
| 139 | 97 | if (params) { |
| 140 | 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 | 101 | : params.roomName |
| 144 | 102 | } |
| 145 | 103 | this.dialogVisible = true |
| ... | ... | @@ -155,7 +113,6 @@ export default { |
| 155 | 113 | communityId: getCommunityId() |
| 156 | 114 | } |
| 157 | 115 | this.showQRCode = false |
| 158 | - this.$refs.form && this.$refs.form.resetFields() | |
| 159 | 116 | this.clearQRCode() |
| 160 | 117 | }, |
| 161 | 118 | handleClose() { |
| ... | ... | @@ -171,8 +128,8 @@ export default { |
| 171 | 128 | try { |
| 172 | 129 | const data = await getDict('pay_fee_config', 'fee_type_cd') |
| 173 | 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 | 134 | } catch (error) { |
| 178 | 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 | 41 | this.buildTreeData(units) |
| 42 | 42 | } catch (error) { |
| 43 | 43 | console.error('Failed to load tree data:', error) |
| 44 | + this.$message.error(this.$t('roomTree.loadError')) | |
| 44 | 45 | } |
| 45 | 46 | }, |
| 46 | 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 | 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 | 54 | id: `f_${unit.floorId}`, |
| 54 | 55 | floorId: unit.floorId, |
| 55 | 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 | 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 | 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 | 82 | } else if (data.id.startsWith('r_')) { |
| 84 | 83 | this.$emit('selectRoom', { |
| 85 | 84 | roomId: data.roomId, |
| ... | ... | @@ -87,48 +86,32 @@ export default { |
| 87 | 86 | }) |
| 88 | 87 | } |
| 89 | 88 | }, |
| 90 | - async loadRooms(node) { | |
| 89 | + async loadRooms(unitData, node) { | |
| 91 | 90 | try { |
| 92 | - const {rooms} = await queryRoomsTree({ | |
| 93 | - unitId: node.unitId, | |
| 91 | + const { rooms } = await queryRoomsTree({ | |
| 92 | + unitId: unitData.unitId, | |
| 94 | 93 | communityId: this.communityId, |
| 95 | 94 | page: 1, |
| 96 | 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 | 112 | } catch (error) { |
| 131 | 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 | 1 | import { messages as contractCreateFeeMessages } from '../views/fee/contractCreateFeeLang' |
| 2 | 2 | import { messages as meterWaterManageMessages } from '../views/fee/meterWaterManageLang' |
| 3 | +import { messages as meterTypeManageMessages } from '../views/fee/meterTypeManageLang' | |
| 3 | 4 | export const messages = { |
| 4 | 5 | en: { |
| 5 | 6 | ...contractCreateFeeMessages.en, |
| 6 | 7 | ...meterWaterManageMessages.en, |
| 8 | + ...meterTypeManageMessages.en, | |
| 7 | 9 | }, |
| 8 | 10 | zh: { |
| 9 | 11 | ...contractCreateFeeMessages.zh, |
| 10 | 12 | ...meterWaterManageMessages.zh, |
| 13 | + ...meterTypeManageMessages.zh, | |
| 11 | 14 | } |
| 12 | 15 | } |
| 13 | 16 | \ No newline at end of file | ... | ... |
src/router/feeRouter.js
| 1 | 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 | 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 | 18 | \ No newline at end of file | ... | ... |
src/router/index.js
| ... | ... | @@ -688,7 +688,7 @@ const router = new VueRouter({ |
| 688 | 688 | // 路由守卫 |
| 689 | 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 | 692 | return next(); // 直接放行 |
| 693 | 693 | } |
| 694 | 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 | 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 | 139 | \ No newline at end of file | ... | ... |
src/views/fee/meterWaterManageList.vue
| ... | ... | @@ -6,7 +6,7 @@ |
| 6 | 6 | </el-col> |
| 7 | 7 | <el-col :span="20"> |
| 8 | 8 | <el-card class="box-card"> |
| 9 | - <div slot="header" class="clearfix"> | |
| 9 | + <div slot="header" class="flex justify-between"> | |
| 10 | 10 | <span>{{ $t('meterWater.queryConditions') }}</span> |
| 11 | 11 | </div> |
| 12 | 12 | <el-form :inline="true" :model="conditions" class="demo-form-inline"> |
| ... | ... | @@ -29,7 +29,7 @@ |
| 29 | 29 | </el-card> |
| 30 | 30 | |
| 31 | 31 | <el-card class="box-card margin-top"> |
| 32 | - <div slot="header" class="clearfix"> | |
| 32 | + <div slot="header" class="flex justify-between"> | |
| 33 | 33 | <span>{{ $t('meterWater.meterReadingInfo') }}</span> |
| 34 | 34 | <div style="float: right"> |
| 35 | 35 | <el-button v-if="conditions.objId" type="primary" size="small" @click="handleOpenAddMeter"> |
| ... | ... | @@ -94,7 +94,7 @@ import { |
| 94 | 94 | listMeterTypes |
| 95 | 95 | } from '@/api/fee/meterWaterManageApi' |
| 96 | 96 | import { getCommunityId } from '@/api/community/communityApi' |
| 97 | -import RoomTreeDiv from '@/components/fee/roomTreeDiv' | |
| 97 | +import RoomTreeDiv from '@/components/room/roomTreeDiv' | |
| 98 | 98 | import AddMeterWater from '@/components/fee/addMeterWater' |
| 99 | 99 | import EditMeterWater from '@/components/fee/editMeterWater' |
| 100 | 100 | import DeleteMeterWater from '@/components/fee/deleteMeterWater' |
| ... | ... | @@ -209,7 +209,7 @@ export default { |
| 209 | 209 | }) |
| 210 | 210 | }, |
| 211 | 211 | handleOpenMeterType() { |
| 212 | - this.$router.push('/fee/meterTypeManage') | |
| 212 | + this.$router.push('/views/fee/meterTypeManage') | |
| 213 | 213 | }, |
| 214 | 214 | handleSuccess() { |
| 215 | 215 | this.loadData() | ... | ... |