From 41eb58702690ca29cfa17b0050f47c6cdedb8e49 Mon Sep 17 00:00:00 2001 From: zmj <1493694146@qq.com> Date: Thu, 7 Dec 2023 19:05:27 +0800 Subject: [PATCH] shit --- index.html | 2 + package-lock.json | 153 +- package.json | 4 + {src/assets => public}/vue.svg | 0 src/App.vue | 5 +- src/api.js | 69 + src/components/Bill.vue | 25 + src/components/Remake.vue | 4 +- src/components/areaList.vue | 39 +- src/components/header.vue | 89 +- src/components/index/bottomCenter.vue | 234 + src/components/index/bottomLeft.vue | 42 + src/components/index/bottomRight.vue | 43 + src/components/index/topCenter.vue | 373 + src/components/index/topLeft.vue | 401 + src/components/index/topRight.vue | 344 + src/components/scrollTable.vue | 472 + src/components/townDetail/bottomCenter.vue | 0 src/components/townDetail/bottomRight.vue | 123 + src/components/townDetail/bottomleft.vue | 476 + src/components/townDetail/test.js | 26408 ++++++++++++++++ src/components/townDetail/topCenter.vue | 360 + src/components/townDetail/version.js | 1 + src/main.js | 8 +- src/view/Businesses.vue | 27 +- src/view/commodity.vue | 143 +- src/view/finance.vue | 143 +- src/view/index.vue | 688 +- src/view/order.vue | 108 +- src/view/storeLogin.vue | 135 +- src/view/townDetail.vue | 317 +- src/view/utils/position.js | 56 + src/view/utils/rem.js | 16 + static/a/CS.png | Bin 0 -> 623 bytes static/a/CY.png | Bin 0 -> 567 bytes static/a/CZ.png | Bin 0 -> 582 bytes static/a/CZXF.png | Bin 0 -> 709 bytes static/a/DD.png | Bin 0 -> 560 bytes static/a/JE.png | Bin 0 -> 709 bytes static/a/PT.png | Bin 0 -> 359 bytes static/a/PT1.png | Bin 0 -> 364 bytes static/a/WX.png | Bin 0 -> 796 bytes static/a/XZ.png | Bin 0 -> 762 bytes static/a/Y.png | Bin 0 -> 1643 bytes static/a/YC.png | Bin 0 -> 488 bytes static/a/YE.png | Bin 0 -> 442 bytes static/a/YH.png | Bin 0 -> 628 bytes static/a/YJ.png | Bin 0 -> 709 bytes static/a/ZFB.png | Bin 0 -> 816 bytes static/a/ZY.png | Bin 0 -> 649 bytes static/a/bg.png | Bin 13724 -> 12108 bytes static/a/商品管理页_slices.zip | Bin 2393 -> 0 bytes static/a/订单页面_3_slices.zip | Bin 13916 -> 0 bytes static/index/FH.png | Bin 0 -> 1204 bytes static/index/JR.png | Bin 0 -> 6322 bytes static/index/JRC.png | Bin 0 -> 6365 bytes static/index/JS.png | Bin 0 -> 2962 bytes static/index/MC.png | Bin 0 -> 2914 bytes static/index/PHBBG.png | Bin 0 -> 225 bytes static/index/SYBG.png | Bin 0 -> 154773 bytes static/index/actbg.png | Bin 0 -> 2408 bytes static/index/actbg2.png | Bin 0 -> 1900 bytes static/index/btn.png | Bin 0 -> 941 bytes static/index/btn2.png | Bin 0 -> 680 bytes static/storeLogin/DCBG.png | Bin 0 -> 182319 bytes .../town/TABBG_slices/mipmap-hdpi/TABBG.png | Bin 13000 -> 0 bytes .../town/TABBG_slices/mipmap-mdpi/TABBG.png | Bin 5989 -> 0 bytes .../town/TABBG_slices/mipmap-xhdpi/TABBG.png | Bin 21160 -> 0 bytes .../town/TABBG_slices/mipmap-xxhdpi/TABBG.png | Bin 41908 -> 0 bytes static/town/YGB.png | Bin 2704 -> 0 bytes static/town/ZCGB.png | Bin 8677 -> 0 bytes vite.config.js | 17 +- 72 files changed, 30209 insertions(+), 1116 deletions(-) rename {src/assets => public}/vue.svg (100%) create mode 100644 src/api.js create mode 100644 src/components/Bill.vue create mode 100644 src/components/index/bottomCenter.vue create mode 100644 src/components/index/bottomLeft.vue create mode 100644 src/components/index/bottomRight.vue create mode 100644 src/components/index/topCenter.vue create mode 100644 src/components/index/topLeft.vue create mode 100644 src/components/index/topRight.vue create mode 100644 src/components/scrollTable.vue create mode 100644 src/components/townDetail/bottomCenter.vue create mode 100644 src/components/townDetail/bottomRight.vue create mode 100644 src/components/townDetail/bottomleft.vue create mode 100644 src/components/townDetail/test.js create mode 100644 src/components/townDetail/topCenter.vue create mode 100644 src/components/townDetail/version.js create mode 100644 src/view/utils/position.js create mode 100644 src/view/utils/rem.js create mode 100644 static/a/CS.png create mode 100644 static/a/CY.png create mode 100644 static/a/CZ.png create mode 100644 static/a/CZXF.png create mode 100644 static/a/DD.png create mode 100644 static/a/JE.png create mode 100644 static/a/PT.png create mode 100644 static/a/PT1.png create mode 100644 static/a/WX.png create mode 100644 static/a/XZ.png create mode 100644 static/a/Y.png create mode 100644 static/a/YC.png create mode 100644 static/a/YE.png create mode 100644 static/a/YH.png create mode 100644 static/a/YJ.png create mode 100644 static/a/ZFB.png create mode 100644 static/a/ZY.png delete mode 100644 static/a/商品管理页_slices.zip delete mode 100644 static/a/订单页面_3_slices.zip create mode 100644 static/index/FH.png create mode 100644 static/index/JR.png create mode 100644 static/index/JRC.png create mode 100644 static/index/JS.png create mode 100644 static/index/MC.png create mode 100644 static/index/PHBBG.png create mode 100644 static/index/SYBG.png create mode 100644 static/index/actbg.png create mode 100644 static/index/actbg2.png create mode 100644 static/index/btn.png create mode 100644 static/index/btn2.png create mode 100644 static/storeLogin/DCBG.png delete mode 100644 static/town/TABBG_slices/mipmap-hdpi/TABBG.png delete mode 100644 static/town/TABBG_slices/mipmap-mdpi/TABBG.png delete mode 100644 static/town/TABBG_slices/mipmap-xhdpi/TABBG.png delete mode 100644 static/town/TABBG_slices/mipmap-xxhdpi/TABBG.png delete mode 100644 static/town/YGB.png delete mode 100644 static/town/ZCGB.png diff --git a/index.html b/index.html index 8388c4b..8bf8a34 100644 --- a/index.html +++ b/index.html @@ -11,3 +11,5 @@ + + diff --git a/package-lock.json b/package-lock.json index 06b2d16..dbd2000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,16 @@ "dependencies": { "@dataview/datav-vue3": "^0.0.0-test.1672506674342", "@jiaminghi/data-view": "^2.10.0", + "amfe-flexible": "^2.2.1", + "axios": "^1.6.2", "echarts": "^5.4.3", + "postcss-pxtorem": "^5.1.1", "vue": "^3.3.8", "vue-router": "^4.2.5" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.0", + "postcss-pxtorem": "^6.0.0", "sass": "^1.69.5", "sass-loader": "^13.3.2", "vite": "^5.0.0" @@ -1066,6 +1070,11 @@ "ajv": "^6.9.1" } }, + "node_modules/amfe-flexible": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/amfe-flexible/-/amfe-flexible-2.2.1.tgz", + "integrity": "sha512-L2VfvDzoETBjhRptg5u/IUuzHSuxm22JpSRb404p/TBGeRfwWmmNEbB+TFPIP/sS/+pbM18bCFH9QnMojLuPNw==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", @@ -1079,6 +1088,21 @@ "node": ">= 8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1169,6 +1193,17 @@ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", @@ -1181,6 +1216,14 @@ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/echarts": { "version": "5.4.3", "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz", @@ -1353,6 +1396,32 @@ "node": ">=8" } }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1517,8 +1586,6 @@ "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "peer": true, "engines": { "node": ">= 0.6" } @@ -1527,8 +1594,6 @@ "version": "2.1.35", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -1596,6 +1661,20 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-pxtorem": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/postcss-pxtorem/-/postcss-pxtorem-6.0.0.tgz", + "integrity": "sha512-ZRXrD7MLLjLk2RNGV6UA4f5Y7gy+a/j1EqjAfp9NdcNYVjUMvg5HTYduTjSkKBkRkfqbg/iKrjMO70V4g1LZeg==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", @@ -2792,6 +2871,11 @@ "peer": true, "requires": {} }, + "amfe-flexible": { + "version": "2.2.1", + "resolved": "https://registry.npmmirror.com/amfe-flexible/-/amfe-flexible-2.2.1.tgz", + "integrity": "sha512-L2VfvDzoETBjhRptg5u/IUuzHSuxm22JpSRb404p/TBGeRfwWmmNEbB+TFPIP/sS/+pbM18bCFH9QnMojLuPNw==" + }, "anymatch": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", @@ -2802,6 +2886,21 @@ "picomatch": "^2.0.4" } }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2872,6 +2971,14 @@ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", @@ -2884,6 +2991,11 @@ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "echarts": { "version": "5.4.3", "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz", @@ -3027,6 +3139,21 @@ "to-regex-range": "^5.0.1" } }, + "follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -3156,16 +3283,12 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "peer": true + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "peer": true, "requires": { "mime-db": "1.52.0" } @@ -3215,6 +3338,18 @@ "source-map-js": "^1.0.2" } }, + "postcss-pxtorem": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/postcss-pxtorem/-/postcss-pxtorem-6.0.0.tgz", + "integrity": "sha512-ZRXrD7MLLjLk2RNGV6UA4f5Y7gy+a/j1EqjAfp9NdcNYVjUMvg5HTYduTjSkKBkRkfqbg/iKrjMO70V4g1LZeg==", + "dev": true, + "requires": {} + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index d9f0a4f..4f506d6 100644 --- a/package.json +++ b/package.json @@ -11,12 +11,16 @@ "dependencies": { "@dataview/datav-vue3": "^0.0.0-test.1672506674342", "@jiaminghi/data-view": "^2.10.0", + "amfe-flexible": "^2.2.1", + "axios": "^1.6.2", "echarts": "^5.4.3", + "postcss-pxtorem": "^5.1.1", "vue": "^3.3.8", "vue-router": "^4.2.5" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.0", + "postcss-pxtorem": "^6.0.0", "sass": "^1.69.5", "sass-loader": "^13.3.2", "vite": "^5.0.0" diff --git a/src/assets/vue.svg b/public/vue.svg similarity index 100% rename from src/assets/vue.svg rename to public/vue.svg diff --git a/src/App.vue b/src/App.vue index 5795151..a962a49 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,7 @@ \ No newline at end of file diff --git a/src/components/Remake.vue b/src/components/Remake.vue index a46d7bb..fe1359d 100644 --- a/src/components/Remake.vue +++ b/src/components/Remake.vue @@ -1,5 +1,6 @@ diff --git a/src/components/header.vue b/src/components/header.vue index 22a7718..cf9d444 100644 --- a/src/components/header.vue +++ b/src/components/header.vue @@ -35,10 +35,15 @@ alt=""> +
+ + +
泸县 + :src="u('DW')" alt=""> {{ area }}
@@ -69,13 +74,30 @@
\ No newline at end of file diff --git a/src/components/index/bottomLeft.vue b/src/components/index/bottomLeft.vue new file mode 100644 index 0000000..4e1bbcc --- /dev/null +++ b/src/components/index/bottomLeft.vue @@ -0,0 +1,42 @@ + + + \ No newline at end of file diff --git a/src/components/index/bottomRight.vue b/src/components/index/bottomRight.vue new file mode 100644 index 0000000..25ecfe8 --- /dev/null +++ b/src/components/index/bottomRight.vue @@ -0,0 +1,43 @@ + + \ No newline at end of file diff --git a/src/components/index/topCenter.vue b/src/components/index/topCenter.vue new file mode 100644 index 0000000..d6fc1cc --- /dev/null +++ b/src/components/index/topCenter.vue @@ -0,0 +1,373 @@ + + + + + + \ No newline at end of file diff --git a/src/components/index/topLeft.vue b/src/components/index/topLeft.vue new file mode 100644 index 0000000..765073c --- /dev/null +++ b/src/components/index/topLeft.vue @@ -0,0 +1,401 @@ + + + \ No newline at end of file diff --git a/src/components/index/topRight.vue b/src/components/index/topRight.vue new file mode 100644 index 0000000..75c461a --- /dev/null +++ b/src/components/index/topRight.vue @@ -0,0 +1,344 @@ + + + \ No newline at end of file diff --git a/src/components/scrollTable.vue b/src/components/scrollTable.vue new file mode 100644 index 0000000..8188587 --- /dev/null +++ b/src/components/scrollTable.vue @@ -0,0 +1,472 @@ + + + + \ No newline at end of file diff --git a/src/components/townDetail/bottomCenter.vue b/src/components/townDetail/bottomCenter.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/components/townDetail/bottomRight.vue b/src/components/townDetail/bottomRight.vue new file mode 100644 index 0000000..ce30954 --- /dev/null +++ b/src/components/townDetail/bottomRight.vue @@ -0,0 +1,123 @@ + + + + \ No newline at end of file diff --git a/src/components/townDetail/bottomleft.vue b/src/components/townDetail/bottomleft.vue new file mode 100644 index 0000000..d2dd9dd --- /dev/null +++ b/src/components/townDetail/bottomleft.vue @@ -0,0 +1,476 @@ + + + + + diff --git a/src/components/townDetail/test.js b/src/components/townDetail/test.js new file mode 100644 index 0000000..88257bb --- /dev/null +++ b/src/components/townDetail/test.js @@ -0,0 +1,26408 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.jessibuca = factory()); +})(this, (function () { + + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function unwrapExports(x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var defineProperty = createCommonjsModule(function (module) { + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports; + }); + + var _defineProperty = unwrapExports(defineProperty); + + // 播放协议 + const PLAYER_PLAY_PROTOCOL = { + websocket: 0, + fetch: 1, + webrtc: 2 + }; + const DEMUX_TYPE = { + flv: 'flv', + m7s: 'm7s' + }; + const FILE_SUFFIX = { + mp4: 'mp4', + webm: 'webm' + }; + + const DEFAULT_PLAYER_OPTIONS = { + videoBuffer: 1000, + //1000ms 1 second + videoBufferDelay: 1000, + // 1000ms + isResize: true, + isFullResize: false, + // + isFlv: false, + debug: false, + hotKey: false, + // 快捷键 + loadingTimeout: 10, + // loading timeout + heartTimeout: 5, + // heart timeout + timeout: 10, + // second + loadingTimeoutReplay: true, + // loading timeout replay. default is true + heartTimeoutReplay: true, + // heart timeout replay. + loadingTimeoutReplayTimes: 3, + // loading timeout replay fail times + heartTimeoutReplayTimes: 3, + // heart timeout replay fail times + supportDblclickFullscreen: false, + // support double click toggle fullscreen + showBandwidth: false, + // show band width + keepScreenOn: false, + // + isNotMute: false, + // + hasAudio: true, + // has audio + hasVideo: true, + // has video + operateBtns: { + fullscreen: false, + screenshot: false, + play: false, + audio: false, + record: false + }, + controlAutoHide: false, + // control auto hide + hasControl: false, + loadingText: '', + // loading Text + background: '', + decoder: 'decoder.js', + url: '', + // play url + rotate: 0, + // + // text: '', + forceNoOffscreen: true, + // 默认是不采用 + hiddenAutoPause: false, + // + protocol: PLAYER_PLAY_PROTOCOL.fetch, + demuxType: DEMUX_TYPE.flv, + // demux type + useWCS: false, + // + wcsUseVideoRender: true, + // 默认设置为true + useMSE: false, + // + useOffscreen: false, + // + autoWasm: true, + // 自动降级到 wasm 模式 + wasmDecodeErrorReplay: true, + // 解码失败重新播放。 + openWebglAlignment: false, + // https://github.com/langhuihui/jessibuca/issues/152 + wasmDecodeAudioSyncVideo: false, + // wasm 解码之后音视频同步 + recordType: FILE_SUFFIX.webm, + useWebFullScreen: false // use web full screen + + }; + const WORKER_CMD_TYPE = { + init: 'init', + initVideo: 'initVideo', + render: 'render', + playAudio: 'playAudio', + initAudio: 'initAudio', + kBps: 'kBps', + decode: 'decode', + audioCode: 'audioCode', + videoCode: 'videoCode', + wasmError: 'wasmError' + }; + const WASM_ERROR = { + invalidNalUnitSize: 'Invalid NAL unit size' // errorSplittingTheInputIntoNALUnits: 'Error splitting the input into NAL units' + + }; + const MEDIA_TYPE = { + audio: 1, + video: 2 + }; + const FLV_MEDIA_TYPE = { + audio: 8, + video: 9 + }; + const WORKER_SEND_TYPE = { + init: 'init', + decode: 'decode', + audioDecode: 'audioDecode', + videoDecode: 'videoDecode', + close: 'close', + updateConfig: 'updateConfig' + }; // + + const EVENTS = { + fullscreen: 'fullscreen$2', + webFullscreen: 'webFullscreen', + decoderWorkerInit: 'decoderWorkerInit', + play: 'play', + playing: 'playing', + pause: 'pause', + mute: 'mute', + load: 'load', + loading: 'loading', + videoInfo: 'videoInfo', + timeUpdate: 'timeUpdate', + audioInfo: "audioInfo", + log: 'log', + error: "error", + kBps: 'kBps', + timeout: 'timeout', + delayTimeout: 'delayTimeout', + loadingTimeout: 'loadingTimeout', + stats: 'stats', + performance: "performance", + record: 'record', + recording: 'recording', + recordingTimestamp: 'recordingTimestamp', + recordStart: 'recordStart', + recordEnd: 'recordEnd', + recordCreateError: 'recordCreateError', + buffer: 'buffer', + videoFrame: 'videoFrame', + start: 'start', + metadata: 'metadata', + resize: 'resize', + streamEnd: 'streamEnd', + streamSuccess: 'streamSuccess', + streamMessage: 'streamMessage', + streamError: 'streamError', + volumechange: 'volumechange', + destroy: 'destroy', + mseSourceOpen: 'mseSourceOpen', + mseSourceClose: 'mseSourceClose', + mseSourceBufferError: 'mseSourceBufferError', + mseSourceBufferBusy: 'mseSourceBufferBusy', + mseSourceBufferFull: 'mseSourceBufferFull', + videoWaiting: 'videoWaiting', + videoTimeUpdate: 'videoTimeUpdate', + videoSyncAudio: 'videoSyncAudio', + playToRenderTimes: 'playToRenderTimes' + }; + const JESSIBUCA_EVENTS = { + load: EVENTS.load, + timeUpdate: EVENTS.timeUpdate, + videoInfo: EVENTS.videoInfo, + audioInfo: EVENTS.audioInfo, + error: EVENTS.error, + kBps: EVENTS.kBps, + log: EVENTS.log, + start: EVENTS.start, + timeout: EVENTS.timeout, + loadingTimeout: EVENTS.loadingTimeout, + delayTimeout: EVENTS.delayTimeout, + fullscreen: 'fullscreen', + webFullscreen: EVENTS.webFullscreen, + play: EVENTS.play, + pause: EVENTS.pause, + mute: EVENTS.mute, + stats: EVENTS.stats, + volumechange: EVENTS.volumechange, + performance: EVENTS.performance, + recordingTimestamp: EVENTS.recordingTimestamp, + recordStart: EVENTS.recordStart, + recordEnd: EVENTS.recordEnd, + playToRenderTimes: EVENTS.playToRenderTimes + }; + const EVENTS_ERROR = { + playError: 'playIsNotPauseOrUrlIsNull', + fetchError: "fetchError", + websocketError: 'websocketError', + webcodecsH265NotSupport: 'webcodecsH265NotSupport', + webcodecsDecodeError: 'webcodecsDecodeError', + webcodecsWidthOrHeightChange: 'webcodecsWidthOrHeightChange', + mediaSourceH265NotSupport: 'mediaSourceH265NotSupport', + mediaSourceFull: EVENTS.mseSourceBufferFull, + mseSourceBufferError: EVENTS.mseSourceBufferError, + mediaSourceAppendBufferError: 'mediaSourceAppendBufferError', + mediaSourceBufferListLarge: 'mediaSourceBufferListLarge', + mediaSourceAppendBufferEndTimeout: 'mediaSourceAppendBufferEndTimeout', + wasmDecodeError: 'wasmDecodeError', + webglAlignmentError: 'webglAlignmentError' + }; + const WEBSOCKET_STATUS = { + notConnect: 'notConnect', + open: 'open', + close: 'close', + error: 'error' + }; + const SCREENSHOT_TYPE = { + download: 'download', + base64: 'base64', + blob: 'blob' + }; + const VIDEO_ENC_TYPE = { + 7: 'H264(AVC)', + // + 12: 'H265(HEVC)' // + + }; + const VIDEO_ENC_CODE = { + h264: 7, + h265: 12 + }; + const AUDIO_ENC_TYPE = { + 10: 'AAC', + 7: 'ALAW', + 8: 'MULAW' + }; + const CONTROL_HEIGHT = 38; + const SCALE_MODE_TYPE = { + full: 0, + // 视频画面完全填充canvas区域,画面会被拉伸 + auto: 1, + // 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 + fullAuto: 2 // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 + + }; + const CANVAS_RENDER_TYPE = { + webcodecs: 'webcodecs', + webgl: 'webgl', + offscreen: 'offscreen' + }; + const ENCODED_VIDEO_TYPE = { + key: 'key', + delta: 'delta' + }; + const MP4_CODECS = { + avc: 'video/mp4; codecs="avc1.64002A"', + hev: 'video/mp4; codecs="hev1.1.6.L123.b0"' + }; + const MEDIA_SOURCE_STATE = { + ended: 'ended', + open: 'open', + closed: 'closed' + }; // frag duration + const AUDIO_SYNC_VIDEO_DIFF = 1000; + const HOT_KEY = { + esc: 27, + // + arrowUp: 38, + // + arrowDown: 40 // + + }; + const WCS_ERROR = { + keyframeIsRequiredError: 'A key frame is required after configure() or flush()', + canNotDecodeClosedCodec: "Cannot call 'decode' on a closed codec" + }; + const FETCH_ERROR = { + abortError1: 'The user aborted a request', + abortError2: 'AbortError', + abort: 'AbortError' + }; + + class Debug { + constructor(master) { + this.log = function (name) { + if (master._opt.debug) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + console.log(`Jessibuca: [${name}]`, ...args); + } + }; + + this.warn = function (name) { + if (master._opt.debug) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + + console.warn(`Jessibuca: [${name}]`, ...args); + } + }; + + this.error = function (name) { + for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { + args[_key3 - 1] = arguments[_key3]; + } + + console.error(`Jessibuca: [${name}]`, ...args); + }; + } + + } + + class Events { + constructor(master) { + this.destroys = []; + this.proxy = this.proxy.bind(this); + this.master = master; + } + + proxy(target, name, callback) { + let option = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + if (!target) { + return; + } + + if (Array.isArray(name)) { + return name.map(item => this.proxy(target, item, callback, option)); + } + + target.addEventListener(name, callback, option); + + const destroy = () => target.removeEventListener(name, callback, option); + + this.destroys.push(destroy); + return destroy; + } + + destroy() { + this.master.debug && this.master.debug.log(`Events`, 'destroy'); + this.destroys.forEach(event => event()); + } + + } + + var property$1 = (player => { + Object.defineProperty(player, 'rect', { + get: () => { + const clientRect = player.$container.getBoundingClientRect(); + clientRect.width = Math.max(clientRect.width, player.$container.clientWidth); + clientRect.height = Math.max(clientRect.height, player.$container.clientHeight); + return clientRect; + } + }); + ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(key => { + Object.defineProperty(player, key, { + get: () => { + return player.rect[key]; + } + }); + }); + }); + + var screenfull = createCommonjsModule(function (module) { + /*! + * screenfull + * v5.1.0 - 2020-12-24 + * (c) Sindre Sorhus; MIT License + */ + (function () { + + var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {}; + var isCommonjs = module.exports; + + var fn = (function () { + var val; + + var fnMap = [ + [ + 'requestFullscreen', + 'exitFullscreen', + 'fullscreenElement', + 'fullscreenEnabled', + 'fullscreenchange', + 'fullscreenerror' + ], + // New WebKit + [ + 'webkitRequestFullscreen', + 'webkitExitFullscreen', + 'webkitFullscreenElement', + 'webkitFullscreenEnabled', + 'webkitfullscreenchange', + 'webkitfullscreenerror' + + ], + // Old WebKit + [ + 'webkitRequestFullScreen', + 'webkitCancelFullScreen', + 'webkitCurrentFullScreenElement', + 'webkitCancelFullScreen', + 'webkitfullscreenchange', + 'webkitfullscreenerror' + + ], + [ + 'mozRequestFullScreen', + 'mozCancelFullScreen', + 'mozFullScreenElement', + 'mozFullScreenEnabled', + 'mozfullscreenchange', + 'mozfullscreenerror' + ], + [ + 'msRequestFullscreen', + 'msExitFullscreen', + 'msFullscreenElement', + 'msFullscreenEnabled', + 'MSFullscreenChange', + 'MSFullscreenError' + ] + ]; + + var i = 0; + var l = fnMap.length; + var ret = {}; + + for (; i < l; i++) { + val = fnMap[i]; + if (val && val[1] in document) { + for (i = 0; i < val.length; i++) { + ret[fnMap[0][i]] = val[i]; + } + return ret; + } + } + + return false; + })(); + + var eventNameMap = { + change: fn.fullscreenchange, + error: fn.fullscreenerror + }; + + var screenfull = { + request: function (element, options) { + return new Promise(function (resolve, reject) { + var onFullScreenEntered = function () { + this.off('change', onFullScreenEntered); + resolve(); + }.bind(this); + + this.on('change', onFullScreenEntered); + + element = element || document.documentElement; + + var returnPromise = element[fn.requestFullscreen](options); + + if (returnPromise instanceof Promise) { + returnPromise.then(onFullScreenEntered).catch(reject); + } + }.bind(this)); + }, + exit: function () { + return new Promise(function (resolve, reject) { + if (!this.isFullscreen) { + resolve(); + return; + } + + var onFullScreenExit = function () { + this.off('change', onFullScreenExit); + resolve(); + }.bind(this); + + this.on('change', onFullScreenExit); + + var returnPromise = document[fn.exitFullscreen](); + + if (returnPromise instanceof Promise) { + returnPromise.then(onFullScreenExit).catch(reject); + } + }.bind(this)); + }, + toggle: function (element, options) { + return this.isFullscreen ? this.exit() : this.request(element, options); + }, + onchange: function (callback) { + this.on('change', callback); + }, + onerror: function (callback) { + this.on('error', callback); + }, + on: function (event, callback) { + var eventName = eventNameMap[event]; + if (eventName) { + document.addEventListener(eventName, callback, false); + } + }, + off: function (event, callback) { + var eventName = eventNameMap[event]; + if (eventName) { + document.removeEventListener(eventName, callback, false); + } + }, + raw: fn + }; + + if (!fn) { + if (isCommonjs) { + module.exports = { isEnabled: false }; + } else { + window.screenfull = { isEnabled: false }; + } + + return; + } + + Object.defineProperties(screenfull, { + isFullscreen: { + get: function () { + return Boolean(document[fn.fullscreenElement]); + } + }, + element: { + enumerable: true, + get: function () { + return document[fn.fullscreenElement]; + } + }, + isEnabled: { + enumerable: true, + get: function () { + // Coerce to boolean in case of old WebKit + return Boolean(document[fn.fullscreenEnabled]); + } + } + }); + + if (isCommonjs) { + module.exports = screenfull; + } else { + window.screenfull = screenfull; + } + })(); + }); + screenfull.isEnabled; + + function noop() { } + function supportOffscreen($canvas) { + return typeof $canvas.transferControlToOffscreen === 'function'; + } + function supportOffscreenV2() { + return typeof OffscreenCanvas !== "undefined"; + } + function createContextGL($canvas) { + let gl = null; + const validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"]; + let nameIndex = 0; + + while (!gl && nameIndex < validContextNames.length) { + const contextName = validContextNames[nameIndex]; + + try { + let contextOptions = { + preserveDrawingBuffer: true + }; + gl = $canvas.getContext(contextName, contextOptions); + } catch (e) { + gl = null; + } + + if (!gl || typeof gl.getParameter !== "function") { + gl = null; + } + + ++nameIndex; + } + + return gl; + } + function dataURLToFile() { + let dataURL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + const arr = dataURL.split(","); + const bstr = atob(arr[1]); + const type = arr[0].replace("data:", "").replace(";base64", ""); + let n = bstr.length, + u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + return new File([u8arr], 'file', { + type + }); + } + function now() { + return new Date().getTime(); + } + (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); + if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } catch (e) { } + + return false; + })(); + function clamp(num, a, b) { + return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b)); + } + function setStyle(element, key, value) { + if (!element) { + return; + } + + if (typeof key === 'object') { + Object.keys(key).forEach(item => { + setStyle(element, item, key[item]); + }); + } + + element.style[key] = value; + return element; + } + function getStyle(element, key) { + let numberType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + + if (!element) { + return 0; + } + + const value = getComputedStyle(element, null).getPropertyValue(key); + return numberType ? parseFloat(value) : value; + } + function getNowTime() { + if (performance && typeof performance.now === 'function') { + return performance.now(); + } + + return Date.now(); + } + function calculationRate(callback) { + let totalSize = 0; + let lastTime = getNowTime(); + return size => { + totalSize += size; + const thisTime = getNowTime(); + const diffTime = thisTime - lastTime; + + if (diffTime >= 1000) { + callback(totalSize / diffTime * 1000); + lastTime = thisTime; + totalSize = 0; + } + }; + } + function isMobile() { + return /iphone|ipod|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase()); + } + + function supportWCS() { + return "VideoEncoder" in window; + } + function formatVideoDecoderConfigure(avcC) { + let codecArray = avcC.subarray(1, 4); + let codecString = "avc1."; + + for (let j = 0; j < 3; j++) { + let h = codecArray[j].toString(16); + + if (h.length < 2) { + h = "0" + h; + } + + codecString += h; + } + + return { + codec: codecString, + description: avcC + }; + } + function isFullScreen() { + return screenfull.isFullscreen; + } + function bpsSize(value) { + if (null == value || value === '' || parseInt(value) === 0 || isNaN(parseInt(value))) { + return "0KB/s"; + } + + let size = parseFloat(value); + size = size.toFixed(2); + return size + 'KB/s'; + } + function fpsStatus(fps) { + let result = 0; + + if (fps >= 24) { + result = 2; + } else if (fps >= 15) { + result = 1; + } + + return result; + } + function createEmptyImageBitmap(width, height) { + const $canvasElement = document.createElement("canvas"); + $canvasElement.width = width; + $canvasElement.height = height; + return window.createImageBitmap($canvasElement, 0, 0, width, height); + } + function supportMSE() { + return window.MediaSource && window.MediaSource.isTypeSupported(MP4_CODECS.avc); + } + function supportMediaStreamTrack() { + return window.MediaStreamTrackGenerator && typeof window.MediaStreamTrackGenerator === 'function'; + } + function isEmpty(value) { + return value === null || value === undefined; + } + function isBoolean(value) { + return value === true || value === false; + } + function isNotEmpty(value) { + return !isEmpty(value); + } + function initPlayTimes() { + return { + playInitStart: '', + //1 + playStart: '', + // 2 + streamStart: '', + //3 + streamResponse: '', + // 4 + demuxStart: '', + // 5 + decodeStart: '', + // 6 + videoStart: '', + // 7 + playTimestamp: '', + // playStart- playInitStart + streamTimestamp: '', + // streamStart - playStart + streamResponseTimestamp: '', + // streamResponse - streamStart + demuxTimestamp: '', + // demuxStart - streamResponse + decodeTimestamp: '', + // decodeStart - demuxStart + videoTimestamp: '', + // videoStart - decodeStart + allTimestamp: '' // videoStart - playInitStart + + }; + } // create watermark + function formatTimeTips(time) { + var result; // + + if (time > -1) { + var hour = Math.floor(time / 3600); + var min = Math.floor(time / 60) % 60; + var sec = time % 60; + sec = Math.round(sec); + + if (hour < 10) { + result = '0' + hour + ":"; + } else { + result = hour + ":"; + } + + if (min < 10) { + result += "0"; + } + + result += min + ":"; + + if (sec < 10) { + result += "0"; + } + + result += sec.toFixed(0); + } + + return result; + } + function getTarget(e) { + const event = e || window.event; + const target = event.target || event.srcElement; + return target; + } + function isWebglRenderSupport(width) { + return width / 2 % 4 === 0; + } + function getBrowser() { + const UserAgent = navigator.userAgent.toLowerCase(); + const browserInfo = {}; + const browserArray = { + IE: window.ActiveXObject || "ActiveXObject" in window, + // IE + Chrome: UserAgent.indexOf('chrome') > -1 && UserAgent.indexOf('safari') > -1, + // Chrome浏览器 + Firefox: UserAgent.indexOf('firefox') > -1, + // 火狐浏览器 + Opera: UserAgent.indexOf('opera') > -1, + // Opera浏览器 + Safari: UserAgent.indexOf('safari') > -1 && UserAgent.indexOf('chrome') == -1, + // safari浏览器 + Edge: UserAgent.indexOf('edge') > -1, + // Edge浏览器 + QQBrowser: /qqbrowser/.test(UserAgent), + // qq浏览器 + WeixinBrowser: /MicroMessenger/i.test(UserAgent) // 微信浏览器 + + }; // console.log(browserArray) + + for (let i in browserArray) { + if (browserArray[i]) { + let versions = ''; + + if (i === 'IE') { + versions = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/)[2]; + } else if (i === 'Chrome') { + for (let mt in navigator.mimeTypes) { + //检测是否是360浏览器(测试只有pc端的360才起作用) + if (navigator.mimeTypes[mt]['type'] === 'application/360softmgrplugin') { + i = '360'; + } + } + + versions = UserAgent.match(/chrome\/([\d.]+)/)[1]; + } else if (i === 'Firefox') { + versions = UserAgent.match(/firefox\/([\d.]+)/)[1]; + } else if (i === 'Opera') { + versions = UserAgent.match(/opera\/([\d.]+)/)[1]; + } else if (i === 'Safari') { + versions = UserAgent.match(/version\/([\d.]+)/)[1]; + } else if (i === 'Edge') { + versions = UserAgent.match(/edge\/([\d.]+)/)[1]; + } else if (i === 'QQBrowser') { + versions = UserAgent.match(/qqbrowser\/([\d.]+)/)[1]; + } + + browserInfo.type = i; + browserInfo.version = parseInt(versions); + } + } + + return browserInfo; + } + function closeVideoFrame(videoFrame) { + if (videoFrame.close) { + videoFrame.close(); + } else if (videoFrame.destroy) { + videoFrame.destroy(); + } + } + function removeElement(element) { + let result = false; + + if (element) { + if (element.parentNode) { + element.parentNode.removeChild(element); + result = true; + } + } + + return result; + } + + var events$1 = (player => { + try { + const screenfullChange = e => { + if (getTarget(e) === player.$container) { + player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen); // 如果不是fullscreen,则触发下 resize 方法 + + if (!player.fullscreen) { + player.resize(); + } else { + if (player._opt.useMSE) { + player.resize(); + } + } + } + }; + + screenfull.on('change', screenfullChange); + player.events.destroys.push(() => { + screenfull.off('change', screenfullChange); + }); + } catch (error) {// + } // + + + player.on(EVENTS.decoderWorkerInit, () => { + player.debug.log('player', 'has loaded'); + player.loaded = true; + }); // + + player.on(EVENTS.play, () => { + player.loading = false; + }); // + + player.on(EVENTS.fullscreen, value => { + if (value) { + try { + screenfull.request(player.$container).then(() => { }).catch(e => { + if (isMobile() && player._opt.useWebFullScreen) { + player.webFullscreen = true; + } + }); + } catch (e) { + if (isMobile() && player._opt.useWebFullScreen) { + player.webFullscreen = true; + } + } + } else { + try { + screenfull.exit().then(() => { + if (player.webFullscreen) { + player.webFullscreen = false; + } + }).catch(() => { + player.webFullscreen = false; + }); + } catch (e) { + player.webFullscreen = false; + } + } + }); + + if (isMobile()) { + player.on(EVENTS.webFullscreen, value => { + if (value) { + player.$container.classList.add('jessibuca-fullscreen-web'); + } else { + player.$container.classList.remove('jessibuca-fullscreen-web'); + } // + + + player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen); + }); + } // + + + player.on(EVENTS.resize, () => { + player.video && player.video.resize(); + }); + + if (player._opt.debug) { + const ignoreList = [EVENTS.timeUpdate]; + Object.keys(EVENTS).forEach(key => { + player.on(EVENTS[key], value => { + if (ignoreList.includes(key)) { + return; + } + + player.debug.log('player events', EVENTS[key], value); + }); + }); + Object.keys(EVENTS_ERROR).forEach(key => { + player.on(EVENTS_ERROR[key], value => { + player.debug.log('player event error', EVENTS_ERROR[key], value); + }); + }); + } + }); + + class Emitter { + on(name, fn, ctx) { + const e = this.e || (this.e = {}); + (e[name] || (e[name] = [])).push({ + fn, + ctx + }); + return this; + } + + once(name, fn, ctx) { + const self = this; + + function listener() { + self.off(name, listener); + + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + fn.apply(ctx, args); + } + + listener._ = fn; + return this.on(name, listener, ctx); + } + + emit(name) { + const evtArr = ((this.e || (this.e = {}))[name] || []).slice(); + + for (var _len2 = arguments.length, data = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + data[_key2 - 1] = arguments[_key2]; + } + + for (let i = 0; i < evtArr.length; i += 1) { + evtArr[i].fn.apply(evtArr[i].ctx, data); + } + + return this; + } + + off(name, callback) { + const e = this.e || (this.e = {}); + + if (!name) { + Object.keys(e).forEach(key => { + delete e[key]; + }); + delete this.e; + return; + } + + const evts = e[name]; + const liveEvents = []; + + if (evts && callback) { + for (let i = 0, len = evts.length; i < len; i += 1) { + if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i]); + } + } + + if (liveEvents.length) { + e[name] = liveEvents; + } else { + delete e[name]; + } + + return this; + } + + } + + var createWebGL = ((gl, openWebglAlignment) => { + var vertexShaderScript = ['attribute vec4 vertexPos;', 'attribute vec4 texturePos;', 'varying vec2 textureCoord;', 'void main()', '{', 'gl_Position = vertexPos;', 'textureCoord = texturePos.xy;', '}'].join('\n'); + var fragmentShaderScript = ['precision highp float;', 'varying highp vec2 textureCoord;', 'uniform sampler2D ySampler;', 'uniform sampler2D uSampler;', 'uniform sampler2D vSampler;', 'const mat4 YUV2RGB = mat4', '(', '1.1643828125, 0, 1.59602734375, -.87078515625,', '1.1643828125, -.39176171875, -.81296875, .52959375,', '1.1643828125, 2.017234375, 0, -1.081390625,', '0, 0, 0, 1', ');', 'void main(void) {', 'highp float y = texture2D(ySampler, textureCoord).r;', 'highp float u = texture2D(uSampler, textureCoord).r;', 'highp float v = texture2D(vSampler, textureCoord).r;', 'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', '}'].join('\n'); + + if (openWebglAlignment) { + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + } + + var vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexShaderScript); + gl.compileShader(vertexShader); + + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader)); + } + + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentShaderScript); + gl.compileShader(fragmentShader); + + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader)); + } + + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.log('Program failed to compile: ' + gl.getProgramInfoLog(program)); + } + + gl.useProgram(program); // initBuffers + + var vertexPosBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW); + var vertexPosRef = gl.getAttribLocation(program, 'vertexPos'); + gl.enableVertexAttribArray(vertexPosRef); + gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0); + var texturePosBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); + var texturePosRef = gl.getAttribLocation(program, 'texturePos'); + gl.enableVertexAttribArray(texturePosRef); + gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0); + + function _initTexture(name, index) { + var textureRef = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, textureRef); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.uniform1i(gl.getUniformLocation(program, name), index); + return textureRef; + } + + var yTextureRef = _initTexture('ySampler', 0); + + var uTextureRef = _initTexture('uSampler', 1); + + var vTextureRef = _initTexture('vSampler', 2); + + return { + render: function (w, h, y, u, v) { + gl.viewport(0, 0, w, h); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, yTextureRef); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, y); + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, uTextureRef); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, u); + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, vTextureRef); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, v); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + destroy: function () { + try { + gl.deleteProgram(program); + gl.deleteBuffer(vertexPosBuffer); + gl.deleteBuffer(texturePosBuffer); + gl.deleteTexture(yTextureRef); + gl.deleteTexture(uTextureRef); + gl.deleteTexture(vTextureRef); + } catch (e) {// console.error(e); + } + } + }; + }); + + class CommonLoader$1 extends Emitter { + constructor() { + super(); + this.init = false; + } + + resetInit() { + this.init = false; + this.videoInfo = { + width: '', + height: '', + encType: '', + encTypeCode: '' + }; + } + + destroy() { + this.resetInit(); + this.player.$container.removeChild(this.$videoElement); + this.off(); + } // + + + updateVideoInfo(data) { + if (data.encTypeCode) { + this.videoInfo.encType = VIDEO_ENC_TYPE[data.encTypeCode]; + } + + if (data.width) { + this.videoInfo.width = data.width; + } + + if (data.height) { + this.videoInfo.height = data.height; + } // video 基本信息 + + + if (this.videoInfo.encType && this.videoInfo.height && this.videoInfo.width && !this.init) { + this.player.emit(EVENTS.videoInfo, this.videoInfo); + this.init = true; + } + } + + play() { } + + pause() { } + + clearView() { } + + } + + /* + * FileSaver.js + * A saveAs() FileSaver implementation. + * + * By Eli Grey, http://eligrey.com + * + * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) + * source : http://purl.eligrey.com/github/FileSaver.js + */ + // The one and only way of getting global scope in all environments + // https://stackoverflow.com/q/3277182/1008999 + var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : undefined; + + function bom(blob, opts) { + if (typeof opts === 'undefined') opts = { + autoBom: false + }; else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object'); + opts = { + autoBom: !opts + }; + } // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { + type: blob.type + }); + } + + return blob; + } + + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + + xhr.onerror = function () { + console.error('could not download file'); + }; + + xhr.send(); + } + + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker + + xhr.open('HEAD', url, false); + + try { + xhr.send(); + } catch (e) { } + + return xhr.status >= 200 && xhr.status <= 299; + } // `a.click()` doesn't work for all browsers (#465) + + + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); + } catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } // Detect WebView inside a native macOS app by ruling out all browsers + // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too + // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos + + + var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); + var saveAs = // probably in some web worker + typeof window !== 'object' || window !== _global ? function saveAs() { + /* noop */ + } // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561) + + var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + + if (a.origin !== location.origin) { + corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); + } else { + click(a); + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 4E4); // 40s + + setTimeout(function () { + click(a); + }, 0); + } + } // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { + click(a); + }); + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + + if (popup) { + popup.document.title = popup.document.body.innerText = 'downloading...'; + } + + if (typeof blob === 'string') return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + + if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader(); + + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) popup.location.href = url; else location = url; + popup = null; // reverse-tabnabbing #460 + }; + + reader.readAsDataURL(blob); + } else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) popup.location = url; else location.href = url; + popup = null; // reverse-tabnabbing #460 + + setTimeout(function () { + URL.revokeObjectURL(url); + }, 4E4); // 40s + } + }; + + class CanvasVideoLoader extends CommonLoader$1 { + constructor(player) { + super(); + this.player = player; + const $canvasElement = document.createElement("canvas"); + $canvasElement.style.position = "absolute"; + $canvasElement.style.top = 0; + $canvasElement.style.left = 0; + this.$videoElement = $canvasElement; + player.$container.appendChild(this.$videoElement); + this.context2D = null; + this.contextGl = null; + this.contextGlRender = null; + this.contextGlDestroy = null; + this.bitmaprenderer = null; + this.renderType = null; + this.videoInfo = { + width: '', + height: '', + encType: '' + }; // + + this._initCanvasRender(); + + this.player.debug.log('CanvasVideo', 'init'); + } + + destroy() { + super.destroy(); + + if (this.contextGl) { + this.contextGl = null; + } + + if (this.context2D) { + this.context2D = null; + } + + if (this.contextGlRender) { + this.contextGlDestroy && this.contextGlDestroy(); + this.contextGlDestroy = null; + this.contextGlRender = null; + } + + if (this.bitmaprenderer) { + this.bitmaprenderer = null; + } + + this.renderType = null; + this.player.debug.log(`CanvasVideoLoader`, 'destroy'); + } + + _initContextGl() { + this.contextGl = createContextGL(this.$videoElement); + + if (this.contextGl) { + const webgl = createWebGL(this.contextGl, this.player._opt.openWebglAlignment); + this.contextGlRender = webgl.render; + this.contextGlDestroy = webgl.destroy; + } else { + this.player.debug.error(`CanvasVideoLoader`, 'init webgl fail'); + } + } + + _initContext2D() { + this.context2D = this.$videoElement.getContext('2d'); + } // 渲染类型 + + + _initCanvasRender() { + if (this.player._opt.useWCS && !this._supportOffscreen()) { + this.renderType = CANVAS_RENDER_TYPE.webcodecs; + + this._initContext2D(); + } else if (this._supportOffscreen()) { + this.renderType = CANVAS_RENDER_TYPE.offscreen; + + this._bindOffscreen(); + } else { + this.renderType = CANVAS_RENDER_TYPE.webgl; + + this._initContextGl(); + } + } + + _supportOffscreen() { + return supportOffscreen(this.$videoElement) && this.player._opt.useOffscreen; + } // + + + _bindOffscreen() { + this.bitmaprenderer = this.$videoElement.getContext('bitmaprenderer'); + } + + initCanvasViewSize() { + this.$videoElement.width = this.videoInfo.width; + this.$videoElement.height = this.videoInfo.height; + this.resize(); + } // + + + render(msg) { + this.player.videoTimestamp = msg.ts; + + switch (this.renderType) { + case CANVAS_RENDER_TYPE.offscreen: + this.bitmaprenderer.transferFromImageBitmap(msg.buffer); + break; + + case CANVAS_RENDER_TYPE.webgl: + this.contextGlRender(this.$videoElement.width, this.$videoElement.height, msg.output[0], msg.output[1], msg.output[2]); + break; + + case CANVAS_RENDER_TYPE.webcodecs: + // can use createImageBitmap in wexin + this.context2D.drawImage(msg.videoFrame, 0, 0, this.$videoElement.width, this.$videoElement.height); + closeVideoFrame(msg.videoFrame); + break; + } + } + + screenshot(filename, format, quality, type) { + filename = filename || now(); + type = type || SCREENSHOT_TYPE.download; + const formatType = { + png: 'image/png', + jpeg: 'image/jpeg', + webp: 'image/webp' + }; + let encoderOptions = 0.92; + + if (!formatType[format] && SCREENSHOT_TYPE[format]) { + type = format; + format = 'png'; + quality = undefined; + } + + if (typeof quality === "string") { + type = quality; + quality = undefined; + } + + if (typeof quality !== 'undefined') { + encoderOptions = Number(quality); + } + + const dataURL = this.$videoElement.toDataURL(formatType[format] || formatType.png, encoderOptions); + + if (type === SCREENSHOT_TYPE.base64) { + return dataURL; + } else { + const file = dataURLToFile(dataURL); + + if (type === SCREENSHOT_TYPE.blob) { + return file; + } else if (type === SCREENSHOT_TYPE.download) { + // downloadImg(file, filename); + saveAs(file, filename); + } + } + } // + + + clearView() { + switch (this.renderType) { + case CANVAS_RENDER_TYPE.offscreen: + createEmptyImageBitmap(this.$videoElement.width, this.$videoElement.height).then(imageBitMap => { + this.bitmaprenderer.transferFromImageBitmap(imageBitMap); + }); + break; + + case CANVAS_RENDER_TYPE.webgl: + this.contextGl.clear(this.contextGl.COLOR_BUFFER_BIT); + break; + + case CANVAS_RENDER_TYPE.webcodecs: + this.context2D.clearRect(0, 0, this.$videoElement.width, this.$videoElement.height); + break; + } + } + + resize() { + this.player.debug.log('canvasVideo', 'resize'); + const option = this.player._opt; + let width = this.player.width; + let height = this.player.height; + + if (option.hasControl && !option.controlAutoHide) { + if (isMobile() && this.player.fullscreen && option.useWebFullScreen) { + width -= CONTROL_HEIGHT; + } else { + height -= CONTROL_HEIGHT; + } + } + + let resizeWidth = this.$videoElement.width; + let resizeHeight = this.$videoElement.height; + const rotate = option.rotate; + let left = (width - resizeWidth) / 2; + let top = (height - resizeHeight) / 2; + + if (rotate === 270 || rotate === 90) { + resizeWidth = this.$videoElement.height; + resizeHeight = this.$videoElement.width; + } + + const wScale = width / resizeWidth; + const hScale = height / resizeHeight; + let scale = wScale > hScale ? hScale : wScale; // + + if (!option.isResize) { + if (wScale !== hScale) { + scale = wScale + ',' + hScale; + } + } // + + + if (option.isFullResize) { + scale = wScale > hScale ? wScale : hScale; + } + + let transform = "scale(" + scale + ")"; + + if (rotate) { + transform += ' rotate(' + rotate + 'deg)'; + } + + this.$videoElement.style.transform = transform; + this.$videoElement.style.left = left + "px"; + this.$videoElement.style.top = top + "px"; + } + + } + + class VideoLoader extends CommonLoader$1 { + constructor(player) { + super(); + this.player = player; + const $videoElement = document.createElement('video'); + const $canvasElement = document.createElement('canvas'); + $videoElement.muted = true; + $videoElement.style.position = "absolute"; + $videoElement.style.top = 0; + $videoElement.style.left = 0; + this._delayPlay = false; + player.$container.appendChild($videoElement); + this.videoInfo = { + width: '', + height: '', + encType: '' + }; + const _opt = this.player._opt; + + if (_opt.useWCS && _opt.wcsUseVideoRender) { + this.trackGenerator = new MediaStreamTrackGenerator({ + kind: 'video' + }); + $videoElement.srcObject = new MediaStream([this.trackGenerator]); + this.vwriter = this.trackGenerator.writable.getWriter(); + } + + this.$videoElement = $videoElement; + this.$canvasElement = $canvasElement; + this.canvasContext = $canvasElement.getContext('2d'); + this.fixChromeVideoFlashBug(); + this.resize(); + const { + proxy + } = this.player.events; + proxy(this.$videoElement, 'canplay', () => { + this.player.debug.log('Video', `canplay and _delayPlay is ${this._delayPlay}`); + + if (this._delayPlay) { + this._play(); + } + }); + proxy(this.$videoElement, 'waiting', () => { + this.player.emit(EVENTS.videoWaiting); + }); + proxy(this.$videoElement, 'timeupdate', event => { + // this.player.emit(EVENTS.videoTimeUpdate, event.timeStamp); + const timeStamp = parseInt(event.timeStamp, 10); + this.player.emit(EVENTS.timeUpdate, timeStamp); // check is pause; + + if (!this.isPlaying() && this.init) { + this.player.debug.log('Video', `timeupdate and this.isPlaying is false and retry play`); + this.$videoElement.play(); + } + }); + this.player.debug.log('Video', 'init'); + } + + destroy() { + super.destroy(); + this.$canvasElement = null; + this.canvasContext = null; + + if (this.$videoElement) { + this.$videoElement.pause(); + this.$videoElement.currentTime = 0; + this.$videoElement.src = ''; + this.$videoElement.removeAttribute('src'); + this.$videoElement = null; + } + + if (this.trackGenerator) { + this.trackGenerator.stop(); + this.trackGenerator = null; + } + + if (this.vwriter) { + this.vwriter.close(); + this.vwriter = null; + } + + this.player.debug.log('Video', 'destroy'); + } + + fixChromeVideoFlashBug() { + const browser = getBrowser(); + const type = browser.type.toLowerCase(); + + if (type === 'chrome' || type === 'edge') { + const $container = this.player.$container; + $container.style.backdropFilter = 'blur(0px)'; + $container.style.translateZ = '0'; + } + } + + play() { + if (this.$videoElement) { + const readyState = this._getVideoReadyState(); + + this.player.debug.log('Video', `play and readyState: ${readyState}`); + + if (readyState === 0) { + this.player.debug.warn('Video', 'readyState is 0 and set _delayPlay to true'); + this._delayPlay = true; + return; + } + + this._play(); + } + } + + _getVideoReadyState() { + let result = 0; + + if (this.$videoElement) { + result = this.$videoElement.readyState; + } + + return result; + } + + _play() { + this.$videoElement && this.$videoElement.play().then(() => { + this._delayPlay = false; + this.player.debug.log('Video', '_play success'); + setTimeout(() => { + if (!this.isPlaying()) { + this.player.debug.warn('Video', `play failed and retry play`); + + this._play(); + } + }, 100); + }).catch(e => { + this.player.debug.error('Video', '_play error', e); + }); + } + + pause(isNow) { + // 预防 + // https://developer.chrome.com/blog/play-request-was-interrupted/ + // http://alonesuperman.com/?p=23 + if (isNow) { + this.$videoElement && this.$videoElement.pause(); + } else { + setTimeout(() => { + this.$videoElement && this.$videoElement.pause(); + }, 100); + } + } + + clearView() { } + + screenshot(filename, format, quality, type) { + filename = filename || now(); + type = type || SCREENSHOT_TYPE.download; + const formatType = { + png: 'image/png', + jpeg: 'image/jpeg', + webp: 'image/webp' + }; + let encoderOptions = 0.92; + + if (!formatType[format] && SCREENSHOT_TYPE[format]) { + type = format; + format = 'png'; + quality = undefined; + } + + if (typeof quality === "string") { + type = quality; + quality = undefined; + } + + if (typeof quality !== 'undefined') { + encoderOptions = Number(quality); + } + + const $video = this.$videoElement; + let canvas = this.$canvasElement; + canvas.width = $video.videoWidth; + canvas.height = $video.videoHeight; + this.canvasContext.drawImage($video, 0, 0, canvas.width, canvas.height); + const dataURL = canvas.toDataURL(formatType[format] || formatType.png, encoderOptions); // release memory + + this.canvasContext.clearRect(0, 0, canvas.width, canvas.height); + canvas.width = 0; + canvas.height = 0; + + if (type === SCREENSHOT_TYPE.base64) { + return dataURL; + } else { + const file = dataURLToFile(dataURL); + + if (type === SCREENSHOT_TYPE.blob) { + return file; + } else if (type === SCREENSHOT_TYPE.download) { + // downloadImg(file, filename); + saveAs(file, filename); + } + } + } + + initCanvasViewSize() { + this.resize(); + } // + + + render(msg) { + if (this.vwriter) { + this.vwriter.write(msg.videoFrame); + } + } + + resize() { + let width = this.player.width; + let height = this.player.height; + const option = this.player._opt; + const rotate = option.rotate; + + if (option.hasControl && !option.controlAutoHide) { + if (isMobile() && this.player.fullscreen && option.useWebFullScreen) { + width -= CONTROL_HEIGHT; + } else { + height -= CONTROL_HEIGHT; + } + } + + this.$videoElement.width = width; + this.$videoElement.height = height; + + if (rotate === 270 || rotate === 90) { + this.$videoElement.width = height; + this.$videoElement.height = width; + } + + let resizeWidth = this.$videoElement.width; + let resizeHeight = this.$videoElement.height; + let left = (width - resizeWidth) / 2; + let top = (height - resizeHeight) / 2; + let objectFill = 'contain'; // 默认是true + // 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 + // 视频画面完全填充canvas区域,画面会被拉伸 + + if (!option.isResize) { + objectFill = 'fill'; + } // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 + + + if (option.isFullResize) { + objectFill = 'none'; + } + + this.$videoElement.style.objectFit = objectFill; + this.$videoElement.style.transform = 'rotate(' + rotate + 'deg)'; + this.$videoElement.style.left = left + "px"; + this.$videoElement.style.top = top + "px"; + } + + isPlaying() { + return this.$videoElement && !this.$videoElement.paused; + } + + } + + class Video { + constructor(player) { + const Loader = Video.getLoaderFactory(player._opt); + return new Loader(player); + } + + static getLoaderFactory(opt) { + if (opt.useMSE || opt.useWCS && !opt.useOffscreen && opt.wcsUseVideoRender) { + return VideoLoader; + } else { + return CanvasVideoLoader; + } + } + + } + + class AudioContextLoader extends Emitter { + constructor(player) { + super(); + this.bufferList = []; + this.player = player; + this.scriptNode = null; + this.hasInitScriptNode = false; + this.audioContextChannel = null; + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // + + this.gainNode = this.audioContext.createGain(); // Get an AudioBufferSourceNode. + // This is the AudioNode to use when we want to play an AudioBuffer + + const source = this.audioContext.createBufferSource(); // set the buffer in the AudioBufferSourceNode + + source.buffer = this.audioContext.createBuffer(1, 1, 22050); // connect the AudioBufferSourceNode to the + // destination so we can hear the sound + + source.connect(this.audioContext.destination); // noteOn as start + // start the source playing + + if (source.noteOn) { + source.noteOn(0); + } else { + source.start(0); + } + + this.audioBufferSourceNode = source; // + + this.mediaStreamAudioDestinationNode = this.audioContext.createMediaStreamDestination(); // + + this.audioEnabled(true); // default setting 0 + + this.gainNode.gain.value = 0; + this.playing = false; // + + this.audioSyncVideoOption = { + diff: null + }; + this.audioInfo = { + encType: '', + channels: '', + sampleRate: '' + }; + this.init = false; + this.hasAudio = false; // update + + this.on(EVENTS.videoSyncAudio, options => { + // this.player.debug.log('AudioContext', `videoSyncAudio , audioTimestamp: ${options.audioTimestamp},videoTimestamp: ${options.videoTimestamp},diff:${options.diff}`) + this.audioSyncVideoOption = options; + }); + this.player.debug.log('AudioContext', 'init'); + } + + resetInit() { + this.init = false; + this.audioInfo = { + encType: '', + channels: '', + sampleRate: '' + }; + } + + destroy() { + this.closeAudio(); + this.resetInit(); + this.audioContext.close(); + this.audioContext = null; + this.gainNode = null; + this.hasAudio = false; + this.playing = false; + + if (this.scriptNode) { + this.scriptNode.onaudioprocess = noop; + this.scriptNode = null; + } + + this.audioBufferSourceNode = null; + this.mediaStreamAudioDestinationNode = null; + this.hasInitScriptNode = false; + this.audioSyncVideoOption = { + diff: null + }; + this.off(); + this.player.debug.log('AudioContext', 'destroy'); + } + + updateAudioInfo(data) { + if (data.encTypeCode) { + this.audioInfo.encType = AUDIO_ENC_TYPE[data.encTypeCode]; + } + + if (data.channels) { + this.audioInfo.channels = data.channels; + } + + if (data.sampleRate) { + this.audioInfo.sampleRate = data.sampleRate; + } // audio 基本信息 + + + if (this.audioInfo.sampleRate && this.audioInfo.channels && this.audioInfo.encType && !this.init) { + this.player.emit(EVENTS.audioInfo, this.audioInfo); + this.init = true; + } + } // + + + get isPlaying() { + return this.playing; + } + + get isMute() { + return this.gainNode.gain.value === 0; + } + + get volume() { + return this.gainNode.gain.value; + } + + get bufferSize() { + return this.bufferList.length; + } + + initScriptNode() { + this.playing = true; + + if (this.hasInitScriptNode) { + return; + } + + const channels = this.audioInfo.channels; + const scriptNode = this.audioContext.createScriptProcessor(1024, 0, channels); // tips: if audio isStateSuspended onaudioprocess method not working + + scriptNode.onaudioprocess = audioProcessingEvent => { + const outputBuffer = audioProcessingEvent.outputBuffer; + + if (this.bufferList.length && this.playing) { + // just for wasm + if (!this.player._opt.useWCS && !this.player._opt.useMSE && this.player._opt.wasmDecodeAudioSyncVideo) { + // audio > video + // wait + if (this.audioSyncVideoOption.diff > AUDIO_SYNC_VIDEO_DIFF) { + this.player.debug.warn('AudioContext', `audioSyncVideoOption more than diff :${this.audioSyncVideoOption.diff}, waiting`); // wait + + return; + } // audio < video + // throw away then chase video + else if (this.audioSyncVideoOption.diff < -AUDIO_SYNC_VIDEO_DIFF) { + this.player.debug.warn('AudioContext', `audioSyncVideoOption less than diff :${this.audioSyncVideoOption.diff}, dropping`); // + + let bufferItem = this.bufferList.shift(); // + + while (bufferItem.ts - this.player.videoTimestamp < -AUDIO_SYNC_VIDEO_DIFF && this.bufferList.length > 0) { + // this.player.debug.warn('AudioContext', `audioSyncVideoOption less than inner ts is:${bufferItem.ts}, videoTimestamp is ${this.player.videoTimestamp},diff:${bufferItem.ts - this.player.videoTimestamp}`) + bufferItem = this.bufferList.shift(); + } + + if (this.bufferList.length === 0) { + return; + } + } + } + + if (this.bufferList.length === 0) { + return; + } + + const bufferItem = this.bufferList.shift(); // update audio time stamp + + if (bufferItem && bufferItem.ts) { + this.player.audioTimestamp = bufferItem.ts; + } + + for (let channel = 0; channel < channels; channel++) { + const b = bufferItem.buffer[channel]; + const nowBuffering = outputBuffer.getChannelData(channel); + + for (let i = 0; i < 1024; i++) { + nowBuffering[i] = b[i] || 0; + } + } + } + }; + + scriptNode.connect(this.gainNode); + this.scriptNode = scriptNode; + this.gainNode.connect(this.audioContext.destination); + this.gainNode.connect(this.mediaStreamAudioDestinationNode); + this.hasInitScriptNode = true; + } + + mute(flag) { + if (flag) { + if (!this.isMute) { + this.player.emit(EVENTS.mute, flag); + } + + this.setVolume(0); + this.clear(); + } else { + if (this.isMute) { + this.player.emit(EVENTS.mute, flag); + } + + this.setVolume(0.5); + } + } + + setVolume(volume) { + volume = parseFloat(volume).toFixed(2); + + if (isNaN(volume)) { + return; + } + + this.audioEnabled(true); + volume = clamp(volume, 0, 1); + this.gainNode.gain.value = volume; + this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); + this.player.emit(EVENTS.volumechange, this.player.volume); + } + + closeAudio() { + if (this.hasInitScriptNode) { + this.scriptNode && this.scriptNode.disconnect(this.gainNode); + this.gainNode && this.gainNode.disconnect(this.audioContext.destination); + this.gainNode && this.gainNode.disconnect(this.mediaStreamAudioDestinationNode); + } + + this.clear(); + } // 是否播放。。。 + + + audioEnabled(flag) { + if (flag) { + if (this.audioContext.state === 'suspended') { + // resume + this.audioContext.resume(); + } + } else { + if (this.audioContext.state === 'running') { + // suspend + this.audioContext.suspend(); + } + } + } + + isStateRunning() { + return this.audioContext.state === 'running'; + } + + isStateSuspended() { + return this.audioContext.state === 'suspended'; + } + + clear() { + this.bufferList = []; + } + + play(buffer, ts) { + // if is mute + if (this.isMute) { + return; + } + + this.hasAudio = true; + this.bufferList.push({ + buffer, + ts + }); + + if (this.bufferList.length > 20) { + this.player.debug.warn('AudioContext', `bufferList is large: ${this.bufferList.length}`); // out of memory + + if (this.bufferList.length > 50) { + this.bufferList.shift(); + } + } // this.player.debug.log('AudioContext', `bufferList is ${this.bufferList.length}`) + + } + + pause() { + this.audioSyncVideoOption = { + diff: null + }; + this.playing = false; + this.clear(); + } + + resume() { + this.playing = true; + } + + } + + class Audio { + constructor(player) { + const Loader = Audio.getLoaderFactory(); + return new Loader(player); + } + + static getLoaderFactory() { + return AudioContextLoader; + } + + } + + class FetchLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.playing = false; + this.abortController = new AbortController(); // + + this.streamRate = calculationRate(rate => { + player.emit(EVENTS.kBps, (rate / 1024).toFixed(2)); + }); + player.debug.log('FetchStream', 'init'); + } + + destroy() { + this.abort(); + this.off(); + this.streamRate = null; + this.player.debug.log('FetchStream', 'destroy'); + } + /** + * + * @param url + * @param options + */ + + + fetchStream(url) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const { + demux + } = this.player; + this.player.debug.log('FetchStream', 'fetchStream', url, JSON.stringify(options)); + this.player._times.streamStart = now(); + const fetchOptions = Object.assign({ + signal: this.abortController.signal + }, { + headers: options.headers || {} + }); + fetch(url, fetchOptions).then(res => { + const reader = res.body.getReader(); + this.emit(EVENTS.streamSuccess); + + const fetchNext = () => { + reader.read().then(_ref => { + let { + done, + value + } = _ref; + + if (done) { + demux.close(); + } else { + this.streamRate && this.streamRate(value.byteLength); + demux.dispatch(value); + fetchNext(); + } + }).catch(e => { + demux.close(); + const errorString = e.toString(); // aborted a request 。 + + if (errorString.indexOf(FETCH_ERROR.abortError1) !== -1) { + return; + } + + if (errorString.indexOf(FETCH_ERROR.abortError2) !== -1) { + return; + } + + if (e.name === FETCH_ERROR.abort) { + return; + } + + this.abort(); + this.emit(EVENTS_ERROR.fetchError, e); + this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError); + }); + }; + + fetchNext(); + }).catch(e => { + if (e.name === 'AbortError') { + return; + } + + demux.close(); + this.abort(); + this.emit(EVENTS_ERROR.fetchError, e); + this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError); + }); + } + + abort() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + } + + } + + class WebsocketLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.socket = null; + this.socketStatus = WEBSOCKET_STATUS.notConnect; + this.wsUrl = null; // + + this.streamRate = calculationRate(rate => { + player.emit(EVENTS.kBps, (rate / 1024).toFixed(2)); + }); + player.debug.log('WebsocketLoader', 'init'); + } + + destroy() { + if (this.socket) { + this.socket.close(1000, 'Client disconnecting'); + this.socket = null; + } + + this.socketStatus = WEBSOCKET_STATUS.notConnect; + this.streamRate = null; + this.wsUrl = null; + this.off(); + this.player.debug.log('websocketLoader', 'destroy'); + } + + _createWebSocket() { + const player = this.player; + const { + debug, + events: { + proxy + }, + demux + } = player; + this.socket = new WebSocket(this.wsUrl); + this.socket.binaryType = 'arraybuffer'; + proxy(this.socket, 'open', () => { + this.emit(EVENTS.streamSuccess); + debug.log('websocketLoader', 'socket open'); + this.socketStatus = WEBSOCKET_STATUS.open; + }); + proxy(this.socket, 'message', event => { + this.streamRate && this.streamRate(event.data.byteLength); + + this._handleMessage(event.data); + }); + proxy(this.socket, 'close', () => { + debug.log('websocketLoader', 'socket close'); + this.emit(EVENTS.streamEnd); + this.socketStatus = WEBSOCKET_STATUS.close; + }); + proxy(this.socket, 'error', error => { + debug.log('websocketLoader', 'socket error'); + this.emit(EVENTS_ERROR.websocketError, error); + this.player.emit(EVENTS.error, EVENTS_ERROR.websocketError); + this.socketStatus = WEBSOCKET_STATUS.error; + demux.close(); + debug.log('websocketLoader', `socket error:`, error); + }); + } // + + + _handleMessage(message) { + const { + demux + } = this.player; + + if (!demux) { + this.player.debug.warn('websocketLoader', 'websocket handle message demux is null'); + return; + } + + demux.dispatch(message); + } + /** + * + * @param url + * @param options + */ + + + fetchStream(url, options) { + this.player._times.streamStart = now(); + this.wsUrl = url; + + this._createWebSocket(); + } + + } + + class Stream { + constructor(player) { + const Loader = Stream.getLoaderFactory(player._opt.protocol); + return new Loader(player); + } + + static getLoaderFactory(protocol) { + if (protocol === PLAYER_PLAY_PROTOCOL.fetch) { + return FetchLoader; + } else if (protocol === PLAYER_PLAY_PROTOCOL.websocket) { + return WebsocketLoader; + } + } + + } + + var RecordRTC_1 = createCommonjsModule(function (module) { + + // Last time updated: 2021-03-09 3:20:22 AM UTC + + // ________________ + // RecordRTC v5.6.2 + + // Open-Sourced: https://github.com/muaz-khan/RecordRTC + + // -------------------------------------------------- + // Muaz Khan - www.MuazKhan.com + // MIT License - www.WebRTC-Experiment.com/licence + // -------------------------------------------------- + + // ____________ + // RecordRTC.js + + /** + * {@link https://github.com/muaz-khan/RecordRTC|RecordRTC} is a WebRTC JavaScript library for audio/video as well as screen activity recording. It supports Chrome, Firefox, Opera, Android, and Microsoft Edge. Platforms: Linux, Mac and Windows. + * @summary Record audio, video or screen inside the browser. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTC + * @class + * @example + * var recorder = RecordRTC(mediaStream or [arrayOfMediaStream], { + * type: 'video', // audio or video or gif or canvas + * recorderType: MediaStreamRecorder || CanvasRecorder || StereoAudioRecorder || Etc + * }); + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. + * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, desiredSampRate: 16000, video: HTMLVideoElement, etc.} + */ + + function RecordRTC(mediaStream, config) { + if (!mediaStream) { + throw 'First parameter is required.'; + } + + config = config || { + type: 'video' + }; + + config = new RecordRTCConfiguration(mediaStream, config); + + // a reference to user's recordRTC object + var self = this; + + function startRecording(config2) { + if (!config.disableLogs) { + console.log('RecordRTC version: ', self.version); + } + + if (!!config2) { + // allow users to set options using startRecording method + // config2 is similar to main "config" object (second parameter over RecordRTC constructor) + config = new RecordRTCConfiguration(mediaStream, config2); + } + + if (!config.disableLogs) { + console.log('started recording ' + config.type + ' stream.'); + } + + if (mediaRecorder) { + mediaRecorder.clearRecordedData(); + mediaRecorder.record(); + + setState('recording'); + + if (self.recordingDuration) { + handleRecordingDuration(); + } + return self; + } + + initRecorder(function () { + if (self.recordingDuration) { + handleRecordingDuration(); + } + }); + + return self; + } + + function initRecorder(initCallback) { + if (initCallback) { + config.initCallback = function () { + initCallback(); + initCallback = config.initCallback = null; // recorder.initRecorder should be call-backed once. + }; + } + + var Recorder = new GetRecorderType(mediaStream, config); + + mediaRecorder = new Recorder(mediaStream, config); + mediaRecorder.record(); + + setState('recording'); + + if (!config.disableLogs) { + console.log('Initialized recorderType:', mediaRecorder.constructor.name, 'for output-type:', config.type); + } + } + + function stopRecording(callback) { + callback = callback || function () { }; + + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state === 'paused') { + self.resumeRecording(); + + setTimeout(function () { + stopRecording(callback); + }, 1); + return; + } + + if (self.state !== 'recording' && !config.disableLogs) { + console.warn('Recording state should be: "recording", however current state is: ', self.state); + } + + if (!config.disableLogs) { + console.log('Stopped recording ' + config.type + ' stream.'); + } + + if (config.type !== 'gif') { + mediaRecorder.stop(_callback); + } else { + mediaRecorder.stop(); + _callback(); + } + + setState('stopped'); + + function _callback(__blob) { + if (!mediaRecorder) { + if (typeof callback.call === 'function') { + callback.call(self, ''); + } else { + callback(''); + } + return; + } + + Object.keys(mediaRecorder).forEach(function (key) { + if (typeof mediaRecorder[key] === 'function') { + return; + } + + self[key] = mediaRecorder[key]; + }); + + var blob = mediaRecorder.blob; + + if (!blob) { + if (__blob) { + mediaRecorder.blob = blob = __blob; + } else { + throw 'Recording failed.'; + } + } + + if (blob && !config.disableLogs) { + console.log(blob.type, '->', bytesToSize(blob.size)); + } + + if (callback) { + var url; + + try { + url = URL.createObjectURL(blob); + } catch (e) { } + + if (typeof callback.call === 'function') { + callback.call(self, url); + } else { + callback(url); + } + } + + if (!config.autoWriteToDisk) { + return; + } + + getDataURL(function (dataURL) { + var parameter = {}; + parameter[config.type + 'Blob'] = dataURL; + DiskStorage.Store(parameter); + }); + } + } + + function pauseRecording() { + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state !== 'recording') { + if (!config.disableLogs) { + console.warn('Unable to pause the recording. Recording state: ', self.state); + } + return; + } + + setState('paused'); + + mediaRecorder.pause(); + + if (!config.disableLogs) { + console.log('Paused recording.'); + } + } + + function resumeRecording() { + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state !== 'paused') { + if (!config.disableLogs) { + console.warn('Unable to resume the recording. Recording state: ', self.state); + } + return; + } + + setState('recording'); + + // not all libs have this method yet + mediaRecorder.resume(); + + if (!config.disableLogs) { + console.log('Resumed recording.'); + } + } + + function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + } + + function getDataURL(callback, _mediaRecorder) { + if (!callback) { + throw 'Pass a callback function over getDataURL.'; + } + + var blob = _mediaRecorder ? _mediaRecorder.blob : (mediaRecorder || {}).blob; + + if (!blob) { + if (!config.disableLogs) { + console.warn('Blob encoder did not finish its job yet.'); + } + + setTimeout(function () { + getDataURL(callback, _mediaRecorder); + }, 1000); + return; + } + + if (typeof Worker !== 'undefined' && !navigator.mozGetUserMedia) { + var webWorker = processInWebWorker(readFile); + + webWorker.onmessage = function (event) { + callback(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function (event) { + callback(event.target.result); + }; + } + + function processInWebWorker(_function) { + try { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } catch (e) { } + } + } + + function handleRecordingDuration(counter) { + counter = counter || 0; + + if (self.state === 'paused') { + setTimeout(function () { + handleRecordingDuration(counter); + }, 1000); + return; + } + + if (self.state === 'stopped') { + return; + } + + if (counter >= self.recordingDuration) { + stopRecording(self.onRecordingStopped); + return; + } + + counter += 1000; // 1-second + + setTimeout(function () { + handleRecordingDuration(counter); + }, 1000); + } + + function setState(state) { + if (!self) { + return; + } + + self.state = state; + + if (typeof self.onStateChanged.call === 'function') { + self.onStateChanged.call(self, state); + } else { + self.onStateChanged(state); + } + } + + var WARNING = 'It seems that recorder is destroyed or "startRecording" is not invoked for ' + config.type + ' recorder.'; + + function warningLog() { + if (config.disableLogs === true) { + return; + } + + console.warn(WARNING); + } + + var mediaRecorder; + + var returnObject = { + /** + * This method starts the recording. + * @method + * @memberof RecordRTC + * @instance + * @example + * var recorder = RecordRTC(mediaStream, { + * type: 'video' + * }); + * recorder.startRecording(); + */ + startRecording: startRecording, + + /** + * This method stops the recording. It is strongly recommended to get "blob" or "URI" inside the callback to make sure all recorders finished their job. + * @param {function} callback - Callback to get the recorded blob. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * // use either "this" or "recorder" object; both are identical + * video.src = this.toURL(); + * var blob = this.getBlob(); + * }); + */ + stopRecording: stopRecording, + + /** + * This method pauses the recording. You can resume recording using "resumeRecording" method. + * @method + * @memberof RecordRTC + * @instance + * @todo Firefox is unable to pause the recording. Fix it. + * @example + * recorder.pauseRecording(); // pause the recording + * recorder.resumeRecording(); // resume again + */ + pauseRecording: pauseRecording, + + /** + * This method resumes the recording. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.pauseRecording(); // first of all, pause the recording + * recorder.resumeRecording(); // now resume it + */ + resumeRecording: resumeRecording, + + /** + * This method initializes the recording. + * @method + * @memberof RecordRTC + * @instance + * @todo This method should be deprecated. + * @example + * recorder.initRecorder(); + */ + initRecorder: initRecorder, + + /** + * Ask RecordRTC to auto-stop the recording after 5 minutes. + * @method + * @memberof RecordRTC + * @instance + * @example + * var fiveMinutes = 5 * 1000 * 60; + * recorder.setRecordingDuration(fiveMinutes, function() { + * var blob = this.getBlob(); + * video.src = this.toURL(); + * }); + * + * // or otherwise + * recorder.setRecordingDuration(fiveMinutes).onRecordingStopped(function() { + * var blob = this.getBlob(); + * video.src = this.toURL(); + * }); + */ + setRecordingDuration: function (recordingDuration, callback) { + if (typeof recordingDuration === 'undefined') { + throw 'recordingDuration is required.'; + } + + if (typeof recordingDuration !== 'number') { + throw 'recordingDuration must be a number.'; + } + + self.recordingDuration = recordingDuration; + self.onRecordingStopped = callback || function () { }; + + return { + onRecordingStopped: function (callback) { + self.onRecordingStopped = callback; + } + }; + }, + + /** + * This method can be used to clear/reset all the recorded data. + * @method + * @memberof RecordRTC + * @instance + * @todo Figure out the difference between "reset" and "clearRecordedData" methods. + * @example + * recorder.clearRecordedData(); + */ + clearRecordedData: function () { + if (!mediaRecorder) { + warningLog(); + return; + } + + mediaRecorder.clearRecordedData(); + + if (!config.disableLogs) { + console.log('Cleared old recorded data.'); + } + }, + + /** + * Get the recorded blob. Use this method inside the "stopRecording" callback. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * var blob = this.getBlob(); + * + * var file = new File([blob], 'filename.webm', { + * type: 'video/webm' + * }); + * + * var formData = new FormData(); + * formData.append('file', file); // upload "File" object rather than a "Blob" + * uploadToServer(formData); + * }); + * @returns {Blob} Returns recorded data as "Blob" object. + */ + getBlob: function () { + if (!mediaRecorder) { + warningLog(); + return; + } + + return mediaRecorder.blob; + }, + + /** + * Get data-URI instead of Blob. + * @param {function} callback - Callback to get the Data-URI. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * recorder.getDataURL(function(dataURI) { + * video.src = dataURI; + * }); + * }); + */ + getDataURL: getDataURL, + + /** + * Get virtual/temporary URL. Usage of this URL is limited to current tab. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * video.src = this.toURL(); + * }); + * @returns {String} Returns a virtual/temporary URL for the recorded "Blob". + */ + toURL: function () { + if (!mediaRecorder) { + warningLog(); + return; + } + + return URL.createObjectURL(mediaRecorder.blob); + }, + + /** + * Get internal recording object (i.e. internal module) e.g. MutliStreamRecorder, MediaStreamRecorder, StereoAudioRecorder or WhammyRecorder etc. + * @method + * @memberof RecordRTC + * @instance + * @example + * var internalRecorder = recorder.getInternalRecorder(); + * if(internalRecorder instanceof MultiStreamRecorder) { + * internalRecorder.addStreams([newAudioStream]); + * internalRecorder.resetVideoStreams([screenStream]); + * } + * @returns {Object} Returns internal recording object. + */ + getInternalRecorder: function () { + return mediaRecorder; + }, + + /** + * Invoke save-as dialog to save the recorded blob into your disk. + * @param {string} fileName - Set your own file name. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * this.save('file-name'); + * + * // or manually: + * invokeSaveAsDialog(this.getBlob(), 'filename.webm'); + * }); + */ + save: function (fileName) { + if (!mediaRecorder) { + warningLog(); + return; + } + + invokeSaveAsDialog(mediaRecorder.blob, fileName); + }, + + /** + * This method gets a blob from indexed-DB storage. + * @param {function} callback - Callback to get the recorded blob. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.getFromDisk(function(dataURL) { + * video.src = dataURL; + * }); + */ + getFromDisk: function (callback) { + if (!mediaRecorder) { + warningLog(); + return; + } + + RecordRTC.getFromDisk(config.type, callback); + }, + + /** + * This method appends an array of webp images to the recorded video-blob. It takes an "array" object. + * @type {Array.} + * @param {Array} arrayOfWebPImages - Array of webp images. + * @method + * @memberof RecordRTC + * @instance + * @todo This method should be deprecated. + * @example + * var arrayOfWebPImages = []; + * arrayOfWebPImages.push({ + * duration: index, + * image: 'data:image/webp;base64,...' + * }); + * recorder.setAdvertisementArray(arrayOfWebPImages); + */ + setAdvertisementArray: function (arrayOfWebPImages) { + config.advertisement = []; + + var length = arrayOfWebPImages.length; + for (var i = 0; i < length; i++) { + config.advertisement.push({ + duration: i, + image: arrayOfWebPImages[i] + }); + } + }, + + /** + * It is equivalent to "recorder.getBlob()" method. Usage of "getBlob" is recommended, though. + * @property {Blob} blob - Recorded Blob can be accessed using this property. + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * var blob = this.blob; + * + * // below one is recommended + * var blob = this.getBlob(); + * }); + */ + blob: null, + + /** + * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. + * @property {number} bufferSize - Buffer-size used to encode the WAV container + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * alert('Recorder used this buffer-size: ' + this.bufferSize); + * }); + */ + bufferSize: 0, + + /** + * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. + * @property {number} sampleRate - Sample-rates used to encode the WAV container + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * alert('Recorder used these sample-rates: ' + this.sampleRate); + * }); + */ + sampleRate: 0, + + /** + * {recorderType:StereoAudioRecorder} returns ArrayBuffer object. + * @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome. + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * var arrayBuffer = this.buffer; + * alert(arrayBuffer.byteLength); + * }); + */ + buffer: null, + + /** + * This method resets the recorder. So that you can reuse single recorder instance many times. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.reset(); + * recorder.startRecording(); + */ + reset: function () { + if (self.state === 'recording' && !config.disableLogs) { + console.warn('Stop an active recorder.'); + } + + if (mediaRecorder && typeof mediaRecorder.clearRecordedData === 'function') { + mediaRecorder.clearRecordedData(); + } + mediaRecorder = null; + setState('inactive'); + self.blob = null; + }, + + /** + * This method is called whenever recorder's state changes. Use this as an "event". + * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.onStateChanged = function(state) { + * console.log('Recorder state: ', state); + * }; + */ + onStateChanged: function (state) { + if (!config.disableLogs) { + console.log('Recorder state changed:', state); + } + }, + + /** + * A recorder can have inactive, recording, paused or stopped states. + * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. + * @memberof RecordRTC + * @static + * @readonly + * @example + * // this looper function will keep you updated about the recorder's states. + * (function looper() { + * document.querySelector('h1').innerHTML = 'Recorder\'s state is: ' + recorder.state; + * if(recorder.state === 'stopped') return; // ignore+stop + * setTimeout(looper, 1000); // update after every 3-seconds + * })(); + * recorder.startRecording(); + */ + state: 'inactive', + + /** + * Get recorder's readonly state. + * @method + * @memberof RecordRTC + * @example + * var state = recorder.getState(); + * @returns {String} Returns recording state. + */ + getState: function () { + return self.state; + }, + + /** + * Destroy RecordRTC instance. Clear all recorders and objects. + * @method + * @memberof RecordRTC + * @example + * recorder.destroy(); + */ + destroy: function () { + var disableLogsCache = config.disableLogs; + + config = { + disableLogs: true + }; + self.reset(); + setState('destroyed'); + returnObject = self = null; + + if (Storage.AudioContextConstructor) { + Storage.AudioContextConstructor.close(); + Storage.AudioContextConstructor = null; + } + + config.disableLogs = disableLogsCache; + + if (!config.disableLogs) { + console.log('RecordRTC is destroyed.'); + } + }, + + /** + * RecordRTC version number + * @property {String} version - Release version number. + * @memberof RecordRTC + * @static + * @readonly + * @example + * alert(recorder.version); + */ + version: '5.6.2' + }; + + if (!this) { + self = returnObject; + return returnObject; + } + + // if someone wants to use RecordRTC with the "new" keyword. + for (var prop in returnObject) { + this[prop] = returnObject[prop]; + } + + self = this; + + return returnObject; + } + + RecordRTC.version = '5.6.2'; + + { + module.exports = RecordRTC; + } + + RecordRTC.getFromDisk = function (type, callback) { + if (!callback) { + throw 'callback is mandatory.'; + } + + console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!'); + DiskStorage.Fetch(function (dataURL, _type) { + if (type !== 'all' && _type === type + 'Blob' && callback) { + callback(dataURL); + } + + if (type === 'all' && callback) { + callback(dataURL, _type.replace('Blob', '')); + } + }); + }; + + /** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof RecordRTC + * @example + * RecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ + RecordRTC.writeToDisk = function (options) { + console.log('Writing recorded blob(s) to disk!'); + options = options || {}; + if (options.audio && options.video && options.gif) { + options.audio.getDataURL(function (audioDataURL) { + options.video.getDataURL(function (videoDataURL) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + }); + } else if (options.audio && options.video) { + options.audio.getDataURL(function (audioDataURL) { + options.video.getDataURL(function (videoDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL + }); + }); + }); + } else if (options.audio && options.gif) { + options.audio.getDataURL(function (audioDataURL) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.video && options.gif) { + options.video.getDataURL(function (videoDataURL) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.audio) { + options.audio.getDataURL(function (audioDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL + }); + }); + } else if (options.video) { + options.video.getDataURL(function (videoDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL + }); + }); + } else if (options.gif) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + gifBlob: gifDataURL + }); + }); + } + }; + + // __________________________ + // RecordRTC-Configuration.js + + /** + * {@link RecordRTCConfiguration} is an inner/private helper for {@link RecordRTC}. + * @summary It configures the 2nd parameter passed over {@link RecordRTC} and returns a valid "config" object. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTCConfiguration + * @class + * @example + * var options = RecordRTCConfiguration(mediaStream, options); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, getNativeBlob:true, etc.} + */ + + function RecordRTCConfiguration(mediaStream, config) { + if (!config.recorderType && !config.type) { + if (!!config.audio && !!config.video) { + config.type = 'video'; + } else if (!!config.audio && !config.video) { + config.type = 'audio'; + } + } + + if (config.recorderType && !config.type) { + if (config.recorderType === WhammyRecorder || config.recorderType === CanvasRecorder || (typeof WebAssemblyRecorder !== 'undefined' && config.recorderType === WebAssemblyRecorder)) { + config.type = 'video'; + } else if (config.recorderType === GifRecorder) { + config.type = 'gif'; + } else if (config.recorderType === StereoAudioRecorder) { + config.type = 'audio'; + } else if (config.recorderType === MediaStreamRecorder) { + if (getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) { + config.type = 'video'; + } else if (!getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) { + config.type = 'video'; + } else if (getTracks(mediaStream, 'audio').length && !getTracks(mediaStream, 'video').length) { + config.type = 'audio'; + } else; + } + } + + if (typeof MediaStreamRecorder !== 'undefined' && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) { + if (!config.mimeType) { + config.mimeType = 'video/webm'; + } + + if (!config.type) { + config.type = config.mimeType.split('/')[0]; + } + + if (!config.bitsPerSecond); + } + + // consider default type=audio + if (!config.type) { + if (config.mimeType) { + config.type = config.mimeType.split('/')[0]; + } + if (!config.type) { + config.type = 'audio'; + } + } + + return config; + } + + // __________________ + // GetRecorderType.js + + /** + * {@link GetRecorderType} is an inner/private helper for {@link RecordRTC}. + * @summary It returns best recorder-type available for your browser. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef GetRecorderType + * @class + * @example + * var RecorderType = GetRecorderType(options); + * var recorder = new RecorderType(options); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} + */ + + function GetRecorderType(mediaStream, config) { + var recorder; + + // StereoAudioRecorder can work with all three: Edge, Firefox and Chrome + // todo: detect if it is Edge, then auto use: StereoAudioRecorder + if (isChrome || isEdge || isOpera) { + // Media Stream Recording API has not been implemented in chrome yet; + // That's why using WebAudio API to record stereo audio in WAV format + recorder = StereoAudioRecorder; + } + + if (typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype && !isChrome) { + recorder = MediaStreamRecorder; + } + + // video recorder (in WebM format) + if (config.type === 'video' && (isChrome || isOpera)) { + recorder = WhammyRecorder; + + if (typeof WebAssemblyRecorder !== 'undefined' && typeof ReadableStream !== 'undefined') { + recorder = WebAssemblyRecorder; + } + } + + // video recorder (in Gif format) + if (config.type === 'gif') { + recorder = GifRecorder; + } + + // html2canvas recording! + if (config.type === 'canvas') { + recorder = CanvasRecorder; + } + + if (isMediaRecorderCompatible() && recorder !== CanvasRecorder && recorder !== GifRecorder && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) { + if (getTracks(mediaStream, 'video').length || getTracks(mediaStream, 'audio').length) { + // audio-only recording + if (config.type === 'audio') { + if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('audio/webm')) { + recorder = MediaStreamRecorder; + } + // else recorder = StereoAudioRecorder; + } else { + // video or screen tracks + if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('video/webm')) { + recorder = MediaStreamRecorder; + } + } + } + } + + if (mediaStream instanceof Array && mediaStream.length) { + recorder = MultiStreamRecorder; + } + + if (config.recorderType) { + recorder = config.recorderType; + } + + if (!config.disableLogs && !!recorder && !!recorder.name) { + console.log('Using recorderType:', recorder.name || recorder.constructor.name); + } + + if (!recorder && isSafari) { + recorder = MediaStreamRecorder; + } + + return recorder; + } + + // _____________ + // MRecordRTC.js + + /** + * MRecordRTC runs on top of {@link RecordRTC} to bring multiple recordings in a single place, by providing simple API. + * @summary MRecordRTC stands for "Multiple-RecordRTC". + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef MRecordRTC + * @class + * @example + * var recorder = new MRecordRTC(); + * recorder.addStream(MediaStream); + * recorder.mediaType = { + * audio: true, // or StereoAudioRecorder or MediaStreamRecorder + * video: true, // or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder + * gif: true // or GifRecorder + * }; + * // mimeType is optional and should be set only in advance cases. + * recorder.mimeType = { + * audio: 'audio/wav', + * video: 'video/webm', + * gif: 'image/gif' + * }; + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @requires {@link RecordRTC} + */ + + function MRecordRTC(mediaStream) { + + /** + * This method attaches MediaStream object to {@link MRecordRTC}. + * @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API. + * @method + * @memberof MRecordRTC + * @example + * recorder.addStream(MediaStream); + */ + this.addStream = function (_mediaStream) { + if (_mediaStream) { + mediaStream = _mediaStream; + } + }; + + /** + * This property can be used to set the recording type e.g. audio, or video, or gif, or canvas. + * @property {object} mediaType - {audio: true, video: true, gif: true} + * @memberof MRecordRTC + * @example + * var recorder = new MRecordRTC(); + * recorder.mediaType = { + * audio: true, // TRUE or StereoAudioRecorder or MediaStreamRecorder + * video: true, // TRUE or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder + * gif : true // TRUE or GifRecorder + * }; + */ + this.mediaType = { + audio: true, + video: true + }; + + /** + * This method starts recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.startRecording(); + */ + this.startRecording = function () { + var mediaType = this.mediaType; + var recorderType; + var mimeType = this.mimeType || { + audio: null, + video: null, + gif: null + }; + + if (typeof mediaType.audio !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'audio').length) { + mediaType.audio = false; + } + + if (typeof mediaType.video !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) { + mediaType.video = false; + } + + if (typeof mediaType.gif !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) { + mediaType.gif = false; + } + + if (!mediaType.audio && !mediaType.video && !mediaType.gif) { + throw 'MediaStream must have either audio or video tracks.'; + } + + if (!!mediaType.audio) { + recorderType = null; + if (typeof mediaType.audio === 'function') { + recorderType = mediaType.audio; + } + + this.audioRecorder = new RecordRTC(mediaStream, { + type: 'audio', + bufferSize: this.bufferSize, + sampleRate: this.sampleRate, + numberOfAudioChannels: this.numberOfAudioChannels || 2, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.audio, + timeSlice: this.timeSlice, + onTimeStamp: this.onTimeStamp + }); + + if (!mediaType.video) { + this.audioRecorder.startRecording(); + } + } + + if (!!mediaType.video) { + recorderType = null; + if (typeof mediaType.video === 'function') { + recorderType = mediaType.video; + } + + var newStream = mediaStream; + + if (isMediaRecorderCompatible() && !!mediaType.audio && typeof mediaType.audio === 'function') { + var videoTrack = getTracks(mediaStream, 'video')[0]; + + if (isFirefox) { + newStream = new MediaStream(); + newStream.addTrack(videoTrack); + + if (recorderType && recorderType === WhammyRecorder) { + // Firefox does NOT supports webp-encoding yet + // But Firefox do supports WebAssemblyRecorder + recorderType = MediaStreamRecorder; + } + } else { + newStream = new MediaStream(); + newStream.addTrack(videoTrack); + } + } + + this.videoRecorder = new RecordRTC(newStream, { + type: 'video', + video: this.video, + canvas: this.canvas, + frameInterval: this.frameInterval || 10, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.video, + timeSlice: this.timeSlice, + onTimeStamp: this.onTimeStamp, + workerPath: this.workerPath, + webAssemblyPath: this.webAssemblyPath, + frameRate: this.frameRate, // used by WebAssemblyRecorder; values: usually 30; accepts any. + bitrate: this.bitrate // used by WebAssemblyRecorder; values: 0 to 1000+ + }); + + if (!mediaType.audio) { + this.videoRecorder.startRecording(); + } + } + + if (!!mediaType.audio && !!mediaType.video) { + var self = this; + + var isSingleRecorder = isMediaRecorderCompatible() === true; + + if (mediaType.audio instanceof StereoAudioRecorder && !!mediaType.video) { + isSingleRecorder = false; + } else if (mediaType.audio !== true && mediaType.video !== true && mediaType.audio !== mediaType.video) { + isSingleRecorder = false; + } + + if (isSingleRecorder === true) { + self.audioRecorder = null; + self.videoRecorder.startRecording(); + } else { + self.videoRecorder.initRecorder(function () { + self.audioRecorder.initRecorder(function () { + // Both recorders are ready to record things accurately + self.videoRecorder.startRecording(); + self.audioRecorder.startRecording(); + }); + }); + } + } + + if (!!mediaType.gif) { + recorderType = null; + if (typeof mediaType.gif === 'function') { + recorderType = mediaType.gif; + } + this.gifRecorder = new RecordRTC(mediaStream, { + type: 'gif', + frameRate: this.frameRate || 200, + quality: this.quality || 10, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.gif + }); + this.gifRecorder.startRecording(); + } + }; + + /** + * This method stops recording. + * @param {function} callback - Callback function is invoked when all encoders finished their jobs. + * @method + * @memberof MRecordRTC + * @example + * recorder.stopRecording(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + */ + this.stopRecording = function (callback) { + callback = callback || function () { }; + + if (this.audioRecorder) { + this.audioRecorder.stopRecording(function (blobURL) { + callback(blobURL, 'audio'); + }); + } + + if (this.videoRecorder) { + this.videoRecorder.stopRecording(function (blobURL) { + callback(blobURL, 'video'); + }); + } + + if (this.gifRecorder) { + this.gifRecorder.stopRecording(function (blobURL) { + callback(blobURL, 'gif'); + }); + } + }; + + /** + * This method pauses recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.pauseRecording(); + */ + this.pauseRecording = function () { + if (this.audioRecorder) { + this.audioRecorder.pauseRecording(); + } + + if (this.videoRecorder) { + this.videoRecorder.pauseRecording(); + } + + if (this.gifRecorder) { + this.gifRecorder.pauseRecording(); + } + }; + + /** + * This method resumes recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.resumeRecording(); + */ + this.resumeRecording = function () { + if (this.audioRecorder) { + this.audioRecorder.resumeRecording(); + } + + if (this.videoRecorder) { + this.videoRecorder.resumeRecording(); + } + + if (this.gifRecorder) { + this.gifRecorder.resumeRecording(); + } + }; + + /** + * This method can be used to manually get all recorded blobs. + * @param {function} callback - All recorded blobs are passed back to the "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getBlob(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + * // or + * var audioBlob = recorder.getBlob().audio; + * var videoBlob = recorder.getBlob().video; + */ + this.getBlob = function (callback) { + var output = {}; + + if (this.audioRecorder) { + output.audio = this.audioRecorder.getBlob(); + } + + if (this.videoRecorder) { + output.video = this.videoRecorder.getBlob(); + } + + if (this.gifRecorder) { + output.gif = this.gifRecorder.getBlob(); + } + + if (callback) { + callback(output); + } + + return output; + }; + + /** + * Destroy all recorder instances. + * @method + * @memberof MRecordRTC + * @example + * recorder.destroy(); + */ + this.destroy = function () { + if (this.audioRecorder) { + this.audioRecorder.destroy(); + this.audioRecorder = null; + } + + if (this.videoRecorder) { + this.videoRecorder.destroy(); + this.videoRecorder = null; + } + + if (this.gifRecorder) { + this.gifRecorder.destroy(); + this.gifRecorder = null; + } + }; + + /** + * This method can be used to manually get all recorded blobs' DataURLs. + * @param {function} callback - All recorded blobs' DataURLs are passed back to the "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getDataURL(function(recording){ + * var audioDataURL = recording.audio; + * var videoDataURL = recording.video; + * var gifDataURL = recording.gif; + * }); + */ + this.getDataURL = function (callback) { + this.getBlob(function (blob) { + if (blob.audio && blob.video) { + getDataURL(blob.audio, function (_audioDataURL) { + getDataURL(blob.video, function (_videoDataURL) { + callback({ + audio: _audioDataURL, + video: _videoDataURL + }); + }); + }); + } else if (blob.audio) { + getDataURL(blob.audio, function (_audioDataURL) { + callback({ + audio: _audioDataURL + }); + }); + } else if (blob.video) { + getDataURL(blob.video, function (_videoDataURL) { + callback({ + video: _videoDataURL + }); + }); + } + }); + + function getDataURL(blob, callback00) { + if (typeof Worker !== 'undefined') { + var webWorker = processInWebWorker(function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + }); + + webWorker.onmessage = function (event) { + callback00(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function (event) { + callback00(event.target.result); + }; + } + } + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + var url; + if (typeof URL !== 'undefined') { + url = URL; + } else if (typeof webkitURL !== 'undefined') { + url = webkitURL; + } else { + throw 'Neither URL nor webkitURL detected.'; + } + url.revokeObjectURL(blob); + return worker; + } + }; + + /** + * This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage. + * @method + * @memberof MRecordRTC + * @example + * recorder.writeToDisk(); + */ + this.writeToDisk = function () { + RecordRTC.writeToDisk({ + audio: this.audioRecorder, + video: this.videoRecorder, + gif: this.gifRecorder + }); + }; + + /** + * This method can be used to invoke a save-as dialog for all recorded blobs. + * @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'} + * @method + * @memberof MRecordRTC + * @example + * recorder.save({ + * audio: 'audio-file-name', + * video: 'video-file-name', + * gif : 'gif-file-name' + * }); + */ + this.save = function (args) { + args = args || { + audio: true, + video: true, + gif: true + }; + + if (!!args.audio && this.audioRecorder) { + this.audioRecorder.save(typeof args.audio === 'string' ? args.audio : ''); + } + + if (!!args.video && this.videoRecorder) { + this.videoRecorder.save(typeof args.video === 'string' ? args.video : ''); + } + if (!!args.gif && this.gifRecorder) { + this.gifRecorder.save(typeof args.gif === 'string' ? args.gif : ''); + } + }; + } + + /** + * This method can be used to get all recorded blobs from IndexedDB storage. + * @param {string} type - 'all' or 'audio' or 'video' or 'gif' + * @param {function} callback - Callback function to get all stored blobs. + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.getFromDisk('all', function(dataURL, type){ + * if(type === 'audio') { } + * if(type === 'video') { } + * if(type === 'gif') { } + * }); + */ + MRecordRTC.getFromDisk = RecordRTC.getFromDisk; + + /** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ + MRecordRTC.writeToDisk = RecordRTC.writeToDisk; + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.MRecordRTC = MRecordRTC; + } + + var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; + + (function (that) { + if (!that) { + return; + } + + if (typeof window !== 'undefined') { + return; + } + + if (typeof commonjsGlobal === 'undefined') { + return; + } + + commonjsGlobal.navigator = { + userAgent: browserFakeUserAgent, + getUserMedia: function () { } + }; + + if (!commonjsGlobal.console) { + commonjsGlobal.console = {}; + } + + if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') { + commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function () { + console.log(arguments); + }; + } + + if (typeof document === 'undefined') { + /*global document:true */ + that.document = { + documentElement: { + appendChild: function () { + return ''; + } + } + }; + + document.createElement = document.captureStream = document.mozCaptureStream = function () { + var obj = { + getContext: function () { + return obj; + }, + play: function () { }, + pause: function () { }, + drawImage: function () { }, + toDataURL: function () { + return ''; + }, + style: {} + }; + return obj; + }; + + that.HTMLVideoElement = function () { }; + } + + if (typeof location === 'undefined') { + /*global location:true */ + that.location = { + protocol: 'file:', + href: '', + hash: '' + }; + } + + if (typeof screen === 'undefined') { + /*global screen:true */ + that.screen = { + width: 0, + height: 0 + }; + } + + if (typeof URL === 'undefined') { + /*global screen:true */ + that.URL = { + createObjectURL: function () { + return ''; + }, + revokeObjectURL: function () { + return ''; + } + }; + } + + /*global window:true */ + that.window = commonjsGlobal; + })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null); + + // _____________________________ + // Cross-Browser-Declarations.js + + // animation-frame used in WebM recording + + /*jshint -W079 */ + var requestAnimationFrame = window.requestAnimationFrame; + if (typeof requestAnimationFrame === 'undefined') { + if (typeof webkitRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = webkitRequestAnimationFrame; + } else if (typeof mozRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = mozRequestAnimationFrame; + } else if (typeof msRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = msRequestAnimationFrame; + } else if (typeof requestAnimationFrame === 'undefined') { + // via: https://gist.github.com/paulirish/1579671 + var lastTime = 0; + + /*global requestAnimationFrame:true */ + requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + } + + /*jshint -W079 */ + var cancelAnimationFrame = window.cancelAnimationFrame; + if (typeof cancelAnimationFrame === 'undefined') { + if (typeof webkitCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = webkitCancelAnimationFrame; + } else if (typeof mozCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = mozCancelAnimationFrame; + } else if (typeof msCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = msCancelAnimationFrame; + } else if (typeof cancelAnimationFrame === 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = function (id) { + clearTimeout(id); + }; + } + } + + // WebAudio API representer + var AudioContext = window.AudioContext; + + if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } + } + + /*jshint -W079 */ + var URL = window.URL; + + if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; + } + + if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } + } + + var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob); + var isOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1; + var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && ('netscape' in window) && / rv:/.test(navigator.userAgent); + var isChrome = (!isOpera && !isEdge && !!navigator.webkitGetUserMedia) || isElectron() || navigator.userAgent.toLowerCase().indexOf('chrome/') !== -1; + + var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + if (isSafari && !isChrome && navigator.userAgent.indexOf('CriOS') !== -1) { + isSafari = false; + isChrome = true; + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined') { + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function () { + this.getTracks().forEach(function (track) { + track.stop(); + }); + }; + } + } + + // below function via: http://goo.gl/B3ae8c + /** + * Return human-readable file size. + * @param {number} bytes - Pass bytes and get formatted string. + * @returns {string} - formatted string + * @example + * bytesToSize(1024*1024*5) === '5 GB' + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + function bytesToSize(bytes) { + var k = 1000; + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) { + return '0 Bytes'; + } + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); + return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; + } + + /** + * @param {Blob} file - File or Blob object. This parameter is required. + * @param {string} fileName - Optional file name e.g. "Recorded-Video.webm" + * @example + * invokeSaveAsDialog(blob or file, [optional] fileName); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + function invokeSaveAsDialog(file, fileName) { + if (!file) { + throw 'Blob object is required.'; + } + + if (!file.type) { + try { + file.type = 'video/webm'; + } catch (e) { } + } + + var fileExtension = (file.type || 'video/webm').split('/')[1]; + if (fileExtension.indexOf(';') !== -1) { + // extended mimetype, e.g. 'video/webm;codecs=vp8,opus' + fileExtension = fileExtension.split(';')[0]; + } + if (fileName && fileName.indexOf('.') !== -1) { + var splitted = fileName.split('.'); + fileName = splitted[0]; + fileExtension = splitted[1]; + } + + var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension; + + if (typeof navigator.msSaveOrOpenBlob !== 'undefined') { + return navigator.msSaveOrOpenBlob(file, fileFullName); + } else if (typeof navigator.msSaveBlob !== 'undefined') { + return navigator.msSaveBlob(file, fileFullName); + } + + var hyperlink = document.createElement('a'); + hyperlink.href = URL.createObjectURL(file); + hyperlink.download = fileFullName; + + hyperlink.style = 'display:none;opacity:0;color:transparent;'; + (document.body || document.documentElement).appendChild(hyperlink); + + if (typeof hyperlink.click === 'function') { + hyperlink.click(); + } else { + hyperlink.target = '_blank'; + hyperlink.dispatchEvent(new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + })); + } + + URL.revokeObjectURL(hyperlink.href); + } + + /** + * from: https://github.com/cheton/is-electron/blob/master/index.js + **/ + function isElectron() { + // Renderer process + if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') { + return true; + } + + // Main process + if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) { + return true; + } + + // Detect the user agent when the `nodeIntegration` option is set to true + if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) { + return true; + } + + return false; + } + + function getTracks(stream, kind) { + if (!stream || !stream.getTracks) { + return []; + } + + return stream.getTracks().filter(function (t) { + return t.kind === (kind || 'audio'); + }); + } + + function setSrcObject(stream, element) { + if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + element.srcObject = stream; + } + } + + /** + * @param {Blob} file - File or Blob object. + * @param {function} callback - Callback function. + * @example + * getSeekableBlob(blob or file, callback); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + function getSeekableBlob(inputBlob, callback) { + // EBML.js copyrights goes to: https://github.com/legokichi/ts-ebml + if (typeof EBML === 'undefined') { + throw new Error('Please link: https://www.webrtc-experiment.com/EBML.js'); + } + + var reader = new EBML.Reader(); + var decoder = new EBML.Decoder(); + var tools = EBML.tools; + + var fileReader = new FileReader(); + fileReader.onload = function (e) { + var ebmlElms = decoder.decode(this.result); + ebmlElms.forEach(function (element) { + reader.read(element); + }); + reader.stop(); + var refinedMetadataBuf = tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues); + var body = this.result.slice(reader.metadataSize); + var newBlob = new Blob([refinedMetadataBuf, body], { + type: 'video/webm' + }); + + callback(newBlob); + }; + fileReader.readAsArrayBuffer(inputBlob); + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.invokeSaveAsDialog = invokeSaveAsDialog; + RecordRTC.getTracks = getTracks; + RecordRTC.getSeekableBlob = getSeekableBlob; + RecordRTC.bytesToSize = bytesToSize; + RecordRTC.isElectron = isElectron; + } + + // __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 + // Storage.js + + /** + * Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext". + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @example + * Storage.AudioContext === webkitAudioContext + * @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object. + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + var Storage = {}; + + if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; + } else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.Storage = Storage; + } + + function isMediaRecorderCompatible() { + if (isFirefox || isSafari || isEdge) { + return true; + } + var nAgt = navigator.userAgent; + var fullVersion = '' + parseFloat(navigator.appVersion); + var majorVersion = parseInt(navigator.appVersion, 10); + var verOffset, ix; + + if (isChrome || isOpera) { + verOffset = nAgt.indexOf('Chrome'); + fullVersion = nAgt.substring(verOffset + 7); + } + + // trim the fullVersion string at semicolon/space if present + if ((ix = fullVersion.indexOf(';')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + if ((ix = fullVersion.indexOf(' ')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + majorVersion = parseInt('' + fullVersion, 10); + + if (isNaN(majorVersion)) { + fullVersion = '' + parseFloat(navigator.appVersion); + majorVersion = parseInt(navigator.appVersion, 10); + } + + return majorVersion >= 49; + } + + // ______________________ + // MediaStreamRecorder.js + + /** + * MediaStreamRecorder is an abstraction layer for {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. It is used by {@link RecordRTC} to record MediaStream(s) in both Chrome and Firefox. + * @summary Runs top over {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://github.com/muaz-khan|Muaz Khan} + * @typedef MediaStreamRecorder + * @class + * @example + * var config = { + * mimeType: 'video/webm', // vp8, vp9, h264, mkv, opus/vorbis + * audioBitsPerSecond : 256 * 8 * 1024, + * videoBitsPerSecond : 256 * 8 * 1024, + * bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two + * checkForInactiveTracks: true, + * timeSlice: 1000, // concatenate intervals based blobs + * ondataavailable: function() {} // get intervals based blobs + * } + * var recorder = new MediaStreamRecorder(mediaStream, config); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * + * // or + * var blob = recorder.blob; + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {disableLogs:true, initCallback: function, mimeType: "video/webm", timeSlice: 1000} + * @throws Will throw an error if first argument "MediaStream" is missing. Also throws error if "MediaRecorder API" are not supported by the browser. + */ + + function MediaStreamRecorder(mediaStream, config) { + var self = this; + + if (typeof mediaStream === 'undefined') { + throw 'First argument "MediaStream" is required.'; + } + + if (typeof MediaRecorder === 'undefined') { + throw 'Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder.'; + } + + config = config || { + // bitsPerSecond: 256 * 8 * 1024, + mimeType: 'video/webm' + }; + + if (config.type === 'audio') { + if (getTracks(mediaStream, 'video').length && getTracks(mediaStream, 'audio').length) { + var stream; + if (!!navigator.mozGetUserMedia) { + stream = new MediaStream(); + stream.addTrack(getTracks(mediaStream, 'audio')[0]); + } else { + // webkitMediaStream + stream = new MediaStream(getTracks(mediaStream, 'audio')); + } + mediaStream = stream; + } + + if (!config.mimeType || config.mimeType.toString().toLowerCase().indexOf('audio') === -1) { + config.mimeType = isChrome ? 'audio/webm' : 'audio/ogg'; + } + + if (config.mimeType && config.mimeType.toString().toLowerCase() !== 'audio/ogg' && !!navigator.mozGetUserMedia) { + // forcing better codecs on Firefox (via #166) + config.mimeType = 'audio/ogg'; + } + } + + var arrayOfBlobs = []; + + /** + * This method returns array of blobs. Use only with "timeSlice". Its useful to preview recording anytime, without using the "stop" method. + * @method + * @memberof MediaStreamRecorder + * @example + * var arrayOfBlobs = recorder.getArrayOfBlobs(); + * @returns {Array} Returns array of recorded blobs. + */ + this.getArrayOfBlobs = function () { + return arrayOfBlobs; + }; + + /** + * This method records MediaStream. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.record(); + */ + this.record = function () { + // set defaults + self.blob = null; + self.clearRecordedData(); + self.timestamps = []; + allStates = []; + arrayOfBlobs = []; + + var recorderHints = config; + + if (!config.disableLogs) { + console.log('Passing following config over MediaRecorder API.', recorderHints); + } + + if (mediaRecorder) { + // mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page. + mediaRecorder = null; + } + + if (isChrome && !isMediaRecorderCompatible()) { + // to support video-only recording on stable + recorderHints = 'video/vp8'; + } + + if (typeof MediaRecorder.isTypeSupported === 'function' && recorderHints.mimeType) { + if (!MediaRecorder.isTypeSupported(recorderHints.mimeType)) { + if (!config.disableLogs) { + console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType); + } + + recorderHints.mimeType = config.type === 'audio' ? 'audio/webm' : 'video/webm'; + } + } + + // using MediaRecorder API here + try { + mediaRecorder = new MediaRecorder(mediaStream, recorderHints); + + // reset + config.mimeType = recorderHints.mimeType; + } catch (e) { + // chrome-based fallback + mediaRecorder = new MediaRecorder(mediaStream); + } + + // old hack? + if (recorderHints.mimeType && !MediaRecorder.isTypeSupported && 'canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(recorderHints.mimeType) === false) { + if (!config.disableLogs) { + console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType); + } + } + + // Dispatching OnDataAvailable Handler + mediaRecorder.ondataavailable = function (e) { + if (e.data) { + allStates.push('ondataavailable: ' + bytesToSize(e.data.size)); + } + + if (typeof config.timeSlice === 'number') { + if (e.data && e.data.size) { + arrayOfBlobs.push(e.data); + updateTimeStamp(); + + if (typeof config.ondataavailable === 'function') { + // intervals based blobs + var blob = config.getNativeBlob ? e.data : new Blob([e.data], { + type: getMimeType(recorderHints) + }); + config.ondataavailable(blob); + } + } + return; + } + + if (!e.data || !e.data.size || e.data.size < 100 || self.blob) { + // make sure that stopRecording always getting fired + // even if there is invalid data + if (self.recordingCallback) { + self.recordingCallback(new Blob([], { + type: getMimeType(recorderHints) + })); + self.recordingCallback = null; + } + return; + } + + self.blob = config.getNativeBlob ? e.data : new Blob([e.data], { + type: getMimeType(recorderHints) + }); + + if (self.recordingCallback) { + self.recordingCallback(self.blob); + self.recordingCallback = null; + } + }; + + mediaRecorder.onstart = function () { + allStates.push('started'); + }; + + mediaRecorder.onpause = function () { + allStates.push('paused'); + }; + + mediaRecorder.onresume = function () { + allStates.push('resumed'); + }; + + mediaRecorder.onstop = function () { + allStates.push('stopped'); + }; + + mediaRecorder.onerror = function (error) { + if (!error) { + return; + } + + if (!error.name) { + error.name = 'UnknownError'; + } + + allStates.push('error: ' + error); + + if (!config.disableLogs) { + // via: https://w3c.github.io/mediacapture-record/MediaRecorder.html#exception-summary + if (error.name.toString().toLowerCase().indexOf('invalidstate') !== -1) { + console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.', error); + } else if (error.name.toString().toLowerCase().indexOf('notsupported') !== -1) { + console.error('MIME type (', recorderHints.mimeType, ') is not supported.', error); + } else if (error.name.toString().toLowerCase().indexOf('security') !== -1) { + console.error('MediaRecorder security error', error); + } + + // older code below + else if (error.name === 'OutOfMemory') { + console.error('The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.', error); + } else if (error.name === 'IllegalStreamModification') { + console.error('A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.', error); + } else if (error.name === 'OtherRecordingError') { + console.error('Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.', error); + } else if (error.name === 'GenericError') { + console.error('The UA cannot provide the codec or recording option that has been requested.', error); + } else { + console.error('MediaRecorder Error', error); + } + } + + (function (looper) { + if (!self.manuallyStopped && mediaRecorder && mediaRecorder.state === 'inactive') { + delete config.timeslice; + + // 10 minutes, enough? + mediaRecorder.start(10 * 60 * 1000); + return; + } + + setTimeout(looper, 1000); + })(); + + if (mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') { + mediaRecorder.stop(); + } + }; + + if (typeof config.timeSlice === 'number') { + updateTimeStamp(); + mediaRecorder.start(config.timeSlice); + } else { + // default is 60 minutes; enough? + // use config => {timeSlice: 1000} otherwise + + mediaRecorder.start(3.6e+6); + } + + if (config.initCallback) { + config.initCallback(); // old code + } + }; + + /** + * @property {Array} timestamps - Array of time stamps + * @memberof MediaStreamRecorder + * @example + * console.log(recorder.timestamps); + */ + this.timestamps = []; + + function updateTimeStamp() { + self.timestamps.push(new Date().getTime()); + + if (typeof config.onTimeStamp === 'function') { + config.onTimeStamp(self.timestamps[self.timestamps.length - 1], self.timestamps); + } + } + + function getMimeType(secondObject) { + if (mediaRecorder && mediaRecorder.mimeType) { + return mediaRecorder.mimeType; + } + + return secondObject.mimeType || 'video/webm'; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + self.manuallyStopped = true; // used inside the mediaRecorder.onerror + + if (!mediaRecorder) { + return; + } + + this.recordingCallback = callback; + + if (mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + } + + if (typeof config.timeSlice === 'number') { + setTimeout(function () { + self.blob = new Blob(arrayOfBlobs, { + type: getMimeType(config) + }); + + self.recordingCallback(self.blob); + }, 100); + } + }; + + /** + * This method pauses the recording process. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + if (!mediaRecorder) { + return; + } + + if (mediaRecorder.state === 'recording') { + mediaRecorder.pause(); + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + if (!mediaRecorder) { + return; + } + + if (mediaRecorder.state === 'paused') { + mediaRecorder.resume(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (mediaRecorder && mediaRecorder.state === 'recording') { + self.stop(clearRecordedDataCB); + } + + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + arrayOfBlobs = []; + mediaRecorder = null; + self.timestamps = []; + } + + // Reference to "MediaRecorder" object + var mediaRecorder; + + /** + * Access to native MediaRecorder API + * @method + * @memberof MediaStreamRecorder + * @instance + * @example + * var internal = recorder.getInternalRecorder(); + * internal.ondataavailable = function() {}; // override + * internal.stream, internal.onpause, internal.onstop, etc. + * @returns {Object} Returns internal recording object. + */ + this.getInternalRecorder = function () { + return mediaRecorder; + }; + + function isMediaStreamActive() { + if ('active' in mediaStream) { + if (!mediaStream.active) { + return false; + } + } else if ('ended' in mediaStream) { // old hack + if (mediaStream.ended) { + return false; + } + } + return true; + } + + /** + * @property {Blob} blob - Recorded data as "Blob" object. + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + this.blob = null; + + + /** + * Get MediaRecorder readonly state. + * @method + * @memberof MediaStreamRecorder + * @example + * var state = recorder.getState(); + * @returns {String} Returns recording state. + */ + this.getState = function () { + if (!mediaRecorder) { + return 'inactive'; + } + + return mediaRecorder.state || 'inactive'; + }; + + // list of all recording states + var allStates = []; + + /** + * Get MediaRecorder all recording states. + * @method + * @memberof MediaStreamRecorder + * @example + * var state = recorder.getAllStates(); + * @returns {Array} Returns all recording states + */ + this.getAllStates = function () { + return allStates; + }; + + // if any Track within the MediaStream is muted or not enabled at any time, + // the browser will only record black frames + // or silence since that is the content produced by the Track + // so we need to stopRecording as soon as any single track ends. + if (typeof config.checkForInactiveTracks === 'undefined') { + config.checkForInactiveTracks = false; // disable to minimize CPU usage + } + + var self = this; + + // this method checks if media stream is stopped + // or if any track is ended. + (function looper() { + if (!mediaRecorder || config.checkForInactiveTracks === false) { + return; + } + + if (isMediaStreamActive() === false) { + if (!config.disableLogs) { + console.log('MediaStream seems stopped.'); + } + self.stop(); + return; + } + + setTimeout(looper, 1000); // check every second + })(); + + // for debugging + this.name = 'MediaStreamRecorder'; + this.toString = function () { + return this.name; + }; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.MediaStreamRecorder = MediaStreamRecorder; + } + + // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js + // https://github.com/mattdiamond/Recorderjs#license-mit + // ______________________ + // StereoAudioRecorder.js + + /** + * StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome. + * @summary JavaScript standalone object for stereo audio recording. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef StereoAudioRecorder + * @class + * @example + * var recorder = new StereoAudioRecorder(MediaStream, { + * sampleRate: 44100, + * bufferSize: 4096 + * }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.} + */ + + function StereoAudioRecorder(mediaStream, config) { + if (!getTracks(mediaStream, 'audio').length) { + throw 'Your stream has no audio tracks.'; + } + + config = config || {}; + + var self = this; + + // variables + var leftchannel = []; + var rightchannel = []; + var recording = false; + var recordingLength = 0; + var jsAudioNode; + + var numberOfAudioChannels = 2; + + /** + * Set sample rates such as 8K or 16K. Reference: http://stackoverflow.com/a/28977136/552182 + * @property {number} desiredSampRate - Desired Bits per sample * 1000 + * @memberof StereoAudioRecorder + * @instance + * @example + * var recorder = StereoAudioRecorder(mediaStream, { + * desiredSampRate: 16 * 1000 // bits-per-sample * 1000 + * }); + */ + var desiredSampRate = config.desiredSampRate; + + // backward compatibility + if (config.leftChannel === true) { + numberOfAudioChannels = 1; + } + + if (config.numberOfAudioChannels === 1) { + numberOfAudioChannels = 1; + } + + if (!numberOfAudioChannels || numberOfAudioChannels < 1) { + numberOfAudioChannels = 2; + } + + if (!config.disableLogs) { + console.log('StereoAudioRecorder is set to record number of channels: ' + numberOfAudioChannels); + } + + // if any Track within the MediaStream is muted or not enabled at any time, + // the browser will only record black frames + // or silence since that is the content produced by the Track + // so we need to stopRecording as soon as any single track ends. + if (typeof config.checkForInactiveTracks === 'undefined') { + config.checkForInactiveTracks = true; + } + + function isMediaStreamActive() { + if (config.checkForInactiveTracks === false) { + // always return "true" + return true; + } + + if ('active' in mediaStream) { + if (!mediaStream.active) { + return false; + } + } else if ('ended' in mediaStream) { // old hack + if (mediaStream.ended) { + return false; + } + } + return true; + } + + /** + * This method records MediaStream. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.record(); + */ + this.record = function () { + if (isMediaStreamActive() === false) { + throw 'Please make sure MediaStream is active.'; + } + + resetVariables(); + + isAudioProcessStarted = isPaused = false; + recording = true; + + if (typeof config.timeSlice !== 'undefined') { + looper(); + } + }; + + function mergeLeftRightBuffers(config, callback) { + function mergeAudioBuffers(config, cb) { + var numberOfAudioChannels = config.numberOfAudioChannels; + + // todo: "slice(0)" --- is it causes loop? Should be removed? + var leftBuffers = config.leftBuffers.slice(0); + var rightBuffers = config.rightBuffers.slice(0); + var sampleRate = config.sampleRate; + var internalInterleavedLength = config.internalInterleavedLength; + var desiredSampRate = config.desiredSampRate; + + if (numberOfAudioChannels === 2) { + leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); + rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength); + + if (desiredSampRate) { + leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); + rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate); + } + } + + if (numberOfAudioChannels === 1) { + leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); + + if (desiredSampRate) { + leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); + } + } + + // set sample rate as desired sample rate + if (desiredSampRate) { + sampleRate = desiredSampRate; + } + + // for changing the sampling rate, reference: + // http://stackoverflow.com/a/28977136/552182 + function interpolateArray(data, newSampleRate, oldSampleRate) { + var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate)); + var newData = []; + var springFactor = Number((data.length - 1) / (fitCount - 1)); + newData[0] = data[0]; + for (var i = 1; i < fitCount - 1; i++) { + var tmp = i * springFactor; + var before = Number(Math.floor(tmp)).toFixed(); + var after = Number(Math.ceil(tmp)).toFixed(); + var atPoint = tmp - before; + newData[i] = linearInterpolate(data[before], data[after], atPoint); + } + newData[fitCount - 1] = data[data.length - 1]; + return newData; + } + + function linearInterpolate(before, after, atPoint) { + return before + (after - before) * atPoint; + } + + function mergeBuffers(channelBuffer, rLength) { + var result = new Float64Array(rLength); + var offset = 0; + var lng = channelBuffer.length; + + for (var i = 0; i < lng; i++) { + var buffer = channelBuffer[i]; + result.set(buffer, offset); + offset += buffer.length; + } + + return result; + } + + function interleave(leftChannel, rightChannel) { + var length = leftChannel.length + rightChannel.length; + + var result = new Float64Array(length); + + var inputIndex = 0; + + for (var index = 0; index < length;) { + result[index++] = leftChannel[inputIndex]; + result[index++] = rightChannel[inputIndex]; + inputIndex++; + } + return result; + } + + function writeUTFBytes(view, offset, string) { + var lng = string.length; + for (var i = 0; i < lng; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + // interleave both channels together + var interleaved; + + if (numberOfAudioChannels === 2) { + interleaved = interleave(leftBuffers, rightBuffers); + } + + if (numberOfAudioChannels === 1) { + interleaved = leftBuffers; + } + + var interleavedLength = interleaved.length; + + // create wav file + var resultingBufferLength = 44 + interleavedLength * 2; + + var buffer = new ArrayBuffer(resultingBufferLength); + + var view = new DataView(buffer); + + // RIFF chunk descriptor/identifier + writeUTFBytes(view, 0, 'RIFF'); + + // RIFF chunk length + // changed "44" to "36" via #401 + view.setUint32(4, 36 + interleavedLength * 2, true); + + // RIFF type + writeUTFBytes(view, 8, 'WAVE'); + + // format chunk identifier + // FMT sub-chunk + writeUTFBytes(view, 12, 'fmt '); + + // format chunk length + view.setUint32(16, 16, true); + + // sample format (raw) + view.setUint16(20, 1, true); + + // stereo (2 channels) + view.setUint16(22, numberOfAudioChannels, true); + + // sample rate + view.setUint32(24, sampleRate, true); + + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * numberOfAudioChannels * 2, true); + + // block align (channel count * bytes per sample) + view.setUint16(32, numberOfAudioChannels * 2, true); + + // bits per sample + view.setUint16(34, 16, true); + + // data sub-chunk + // data chunk identifier + writeUTFBytes(view, 36, 'data'); + + // data chunk length + view.setUint32(40, interleavedLength * 2, true); + + // write the PCM samples + var lng = interleavedLength; + var index = 44; + var volume = 1; + for (var i = 0; i < lng; i++) { + view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); + index += 2; + } + + if (cb) { + return cb({ + buffer: buffer, + view: view + }); + } + + postMessage({ + buffer: buffer, + view: view + }); + } + + if (config.noWorker) { + mergeAudioBuffers(config, function (data) { + callback(data.buffer, data.view); + }); + return; + } + + + var webWorker = processInWebWorker(mergeAudioBuffers); + + webWorker.onmessage = function (event) { + callback(event.data.buffer, event.data.view); + + // release memory + URL.revokeObjectURL(webWorker.workerURL); + + // kill webworker (or Chrome will kill your page after ~25 calls) + webWorker.terminate(); + }; + + webWorker.postMessage(config); + } + + function processInWebWorker(_function) { + var workerURL = URL.createObjectURL(new Blob([_function.toString(), + ';this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(workerURL); + worker.workerURL = workerURL; + return worker; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + // stop recording + recording = false; + + mergeLeftRightBuffers({ + desiredSampRate: desiredSampRate, + sampleRate: sampleRate, + numberOfAudioChannels: numberOfAudioChannels, + internalInterleavedLength: recordingLength, + leftBuffers: leftchannel, + rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel, + noWorker: config.noWorker + }, function (buffer, view) { + /** + * @property {Blob} blob - The recorded blob object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + self.blob = new Blob([view], { + type: 'audio/wav' + }); + + /** + * @property {ArrayBuffer} buffer - The recorded buffer object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var buffer = recorder.buffer; + * }); + */ + self.buffer = new ArrayBuffer(view.buffer.byteLength); + + /** + * @property {DataView} view - The recorded data-view object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var view = recorder.view; + * }); + */ + self.view = view; + + self.sampleRate = desiredSampRate || sampleRate; + self.bufferSize = bufferSize; + + // recorded audio length + self.length = recordingLength; + + isAudioProcessStarted = false; + + if (callback) { + callback(self.blob); + } + }); + }; + + if (typeof RecordRTC.Storage === 'undefined') { + RecordRTC.Storage = { + AudioContextConstructor: null, + AudioContext: window.AudioContext || window.webkitAudioContext + }; + } + + if (!RecordRTC.Storage.AudioContextConstructor || RecordRTC.Storage.AudioContextConstructor.state === 'closed') { + RecordRTC.Storage.AudioContextConstructor = new RecordRTC.Storage.AudioContext(); + } + + var context = RecordRTC.Storage.AudioContextConstructor; + + // creates an audio node from the microphone incoming stream + var audioInput = context.createMediaStreamSource(mediaStream); + + var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384]; + + /** + * From the spec: This value controls how frequently the audioprocess event is + * dispatched and how many sample-frames need to be processed each call. + * Lower values for buffer size will result in a lower (better) latency. + * Higher values will be necessary to avoid audio breakup and glitches + * The size of the buffer (in sample-frames) which needs to + * be processed each time onprocessaudio is called. + * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). + * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * bufferSize: 4096 + * }); + */ + + // "0" means, let chrome decide the most accurate buffer-size for current platform. + var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize; + + if (legalBufferValues.indexOf(bufferSize) === -1) { + if (!config.disableLogs) { + console.log('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t')); + } + } + + if (context.createJavaScriptNode) { + jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels); + } else if (context.createScriptProcessor) { + jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels); + } else { + throw 'WebAudio API has no support on this browser.'; + } + + // connect the stream to the script processor + audioInput.connect(jsAudioNode); + + if (!config.bufferSize) { + bufferSize = jsAudioNode.bufferSize; // device buffer-size + } + + /** + * The sample rate (in sample-frames per second) at which the + * AudioContext handles audio. It is assumed that all AudioNodes + * in the context run at this rate. In making this assumption, + * sample-rate converters or "varispeed" processors are not supported + * in real-time processing. + * The sampleRate parameter describes the sample-rate of the + * linear PCM audio data in the buffer in sample-frames per second. + * An implementation must support sample-rates in at least + * the range 22050 to 96000. + * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * sampleRate: 44100 + * }); + */ + var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100; + + if (sampleRate < 22050 || sampleRate > 96000) { + // Ref: http://stackoverflow.com/a/26303918/552182 + if (!config.disableLogs) { + console.log('sample-rate must be under range 22050 and 96000.'); + } + } + + if (!config.disableLogs) { + if (config.desiredSampRate) { + console.log('Desired sample-rate: ' + config.desiredSampRate); + } + } + + var isPaused = false; + /** + * This method pauses the recording process. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPaused = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + if (isMediaStreamActive() === false) { + throw 'Please make sure MediaStream is active.'; + } + + if (!recording) { + if (!config.disableLogs) { + console.log('Seems recording has been restarted.'); + } + this.record(); + return; + } + + isPaused = false; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + config.checkForInactiveTracks = false; + + if (recording) { + this.stop(clearRecordedDataCB); + } + + clearRecordedDataCB(); + }; + + function resetVariables() { + leftchannel = []; + rightchannel = []; + recordingLength = 0; + isAudioProcessStarted = false; + recording = false; + isPaused = false; + context = null; + + self.leftchannel = leftchannel; + self.rightchannel = rightchannel; + self.numberOfAudioChannels = numberOfAudioChannels; + self.desiredSampRate = desiredSampRate; + self.sampleRate = sampleRate; + self.recordingLength = recordingLength; + + intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + } + + function clearRecordedDataCB() { + if (jsAudioNode) { + jsAudioNode.onaudioprocess = null; + jsAudioNode.disconnect(); + jsAudioNode = null; + } + + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + + resetVariables(); + } + + // for debugging + this.name = 'StereoAudioRecorder'; + this.toString = function () { + return this.name; + }; + + var isAudioProcessStarted = false; + + function onAudioProcessDataAvailable(e) { + if (isPaused) { + return; + } + + if (isMediaStreamActive() === false) { + if (!config.disableLogs) { + console.log('MediaStream seems stopped.'); + } + jsAudioNode.disconnect(); + recording = false; + } + + if (!recording) { + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + return; + } + + /** + * This method is called on "onaudioprocess" event's first invocation. + * @method {function} onAudioProcessStarted + * @memberof StereoAudioRecorder + * @example + * recorder.onAudioProcessStarted: function() { }; + */ + if (!isAudioProcessStarted) { + isAudioProcessStarted = true; + if (config.onAudioProcessStarted) { + config.onAudioProcessStarted(); + } + + if (config.initCallback) { + config.initCallback(); + } + } + + var left = e.inputBuffer.getChannelData(0); + + // we clone the samples + var chLeft = new Float32Array(left); + leftchannel.push(chLeft); + + if (numberOfAudioChannels === 2) { + var right = e.inputBuffer.getChannelData(1); + var chRight = new Float32Array(right); + rightchannel.push(chRight); + } + + recordingLength += bufferSize; + + // export raw PCM + self.recordingLength = recordingLength; + + if (typeof config.timeSlice !== 'undefined') { + intervalsBasedBuffers.recordingLength += bufferSize; + intervalsBasedBuffers.left.push(chLeft); + + if (numberOfAudioChannels === 2) { + intervalsBasedBuffers.right.push(chRight); + } + } + } + + jsAudioNode.onaudioprocess = onAudioProcessDataAvailable; + + // to prevent self audio to be connected with speakers + if (context.createMediaStreamDestination) { + jsAudioNode.connect(context.createMediaStreamDestination()); + } else { + jsAudioNode.connect(context.destination); + } + + // export raw PCM + this.leftchannel = leftchannel; + this.rightchannel = rightchannel; + this.numberOfAudioChannels = numberOfAudioChannels; + this.desiredSampRate = desiredSampRate; + this.sampleRate = sampleRate; + self.recordingLength = recordingLength; + + // helper for intervals based blobs + var intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + + // this looper is used to support intervals based blobs (via timeSlice+ondataavailable) + function looper() { + if (!recording || typeof config.ondataavailable !== 'function' || typeof config.timeSlice === 'undefined') { + return; + } + + if (intervalsBasedBuffers.left.length) { + mergeLeftRightBuffers({ + desiredSampRate: desiredSampRate, + sampleRate: sampleRate, + numberOfAudioChannels: numberOfAudioChannels, + internalInterleavedLength: intervalsBasedBuffers.recordingLength, + leftBuffers: intervalsBasedBuffers.left, + rightBuffers: numberOfAudioChannels === 1 ? [] : intervalsBasedBuffers.right + }, function (buffer, view) { + var blob = new Blob([view], { + type: 'audio/wav' + }); + config.ondataavailable(blob); + + setTimeout(looper, config.timeSlice); + }); + + intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + } else { + setTimeout(looper, config.timeSlice); + } + } + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.StereoAudioRecorder = StereoAudioRecorder; + } + + // _________________ + // CanvasRecorder.js + + /** + * CanvasRecorder is a standalone class used by {@link RecordRTC} to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}. + * @summary HTML2Canvas recording into video WebM. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef CanvasRecorder + * @class + * @example + * var recorder = new CanvasRecorder(htmlElement, { disableLogs: true, useWhammyRecorder: true }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc. + * @param {object} config - {disableLogs:true, initCallback: function} + */ + + function CanvasRecorder(htmlElement, config) { + if (typeof html2canvas === 'undefined') { + throw 'Please link: https://www.webrtc-experiment.com/screenshot.js'; + } + + config = config || {}; + if (!config.frameInterval) { + config.frameInterval = 10; + } + + // via DetectRTC.js + var isCanvasSupportsStreamCapturing = false; + ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function (item) { + if (item in document.createElement('canvas')) { + isCanvasSupportsStreamCapturing = true; + } + }); + + var _isChrome = (!!window.webkitRTCPeerConnection || !!window.webkitGetUserMedia) && !!window.chrome; + + var chromeVersion = 50; + var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + if (_isChrome && matchArray && matchArray[2]) { + chromeVersion = parseInt(matchArray[2], 10); + } + + if (_isChrome && chromeVersion < 52) { + isCanvasSupportsStreamCapturing = false; + } + + if (config.useWhammyRecorder) { + isCanvasSupportsStreamCapturing = false; + } + + var globalCanvas, mediaStreamRecorder; + + if (isCanvasSupportsStreamCapturing) { + if (!config.disableLogs) { + console.log('Your browser supports both MediRecorder API and canvas.captureStream!'); + } + + if (htmlElement instanceof HTMLCanvasElement) { + globalCanvas = htmlElement; + } else if (htmlElement instanceof CanvasRenderingContext2D) { + globalCanvas = htmlElement.canvas; + } else { + throw 'Please pass either HTMLCanvasElement or CanvasRenderingContext2D.'; + } + } else if (!!navigator.mozGetUserMedia) { + if (!config.disableLogs) { + console.error('Canvas recording is NOT supported in Firefox.'); + } + } + + var isRecording; + + /** + * This method records Canvas. + * @method + * @memberof CanvasRecorder + * @example + * recorder.record(); + */ + this.record = function () { + isRecording = true; + + if (isCanvasSupportsStreamCapturing && !config.useWhammyRecorder) { + // CanvasCaptureMediaStream + var canvasMediaStream; + if ('captureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.captureStream(25); // 25 FPS + } else if ('mozCaptureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.mozCaptureStream(25); + } else if ('webkitCaptureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.webkitCaptureStream(25); + } + + try { + var mdStream = new MediaStream(); + mdStream.addTrack(getTracks(canvasMediaStream, 'video')[0]); + canvasMediaStream = mdStream; + } catch (e) { } + + if (!canvasMediaStream) { + throw 'captureStream API are NOT available.'; + } + + // Note: Jan 18, 2016 status is that, + // Firefox MediaRecorder API can't record CanvasCaptureMediaStream object. + mediaStreamRecorder = new MediaStreamRecorder(canvasMediaStream, { + mimeType: config.mimeType || 'video/webm' + }); + mediaStreamRecorder.record(); + } else { + whammy.frames = []; + lastTime = new Date().getTime(); + drawCanvasFrame(); + } + + if (config.initCallback) { + config.initCallback(); + } + }; + + this.getWebPImages = function (callback) { + if (htmlElement.nodeName.toLowerCase() !== 'canvas') { + callback(); + return; + } + + var framesLength = whammy.frames.length; + whammy.frames.forEach(function (frame, idx) { + var framesRemaining = framesLength - idx; + if (!config.disableLogs) { + console.log(framesRemaining + '/' + framesLength + ' frames remaining'); + } + + if (config.onEncodingCallback) { + config.onEncodingCallback(framesRemaining, framesLength); + } + + var webp = frame.image.toDataURL('image/webp', 1); + whammy.frames[idx].image = webp; + }); + + if (!config.disableLogs) { + console.log('Generating WebM'); + } + + callback(); + }; + + /** + * This method stops recording Canvas. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof CanvasRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + isRecording = false; + + var that = this; + + if (isCanvasSupportsStreamCapturing && mediaStreamRecorder) { + mediaStreamRecorder.stop(callback); + return; + } + + this.getWebPImages(function () { + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof CanvasRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + whammy.compile(function (blob) { + if (!config.disableLogs) { + console.log('Recording finished!'); + } + + that.blob = blob; + + if (that.blob.forEach) { + that.blob = new Blob([], { + type: 'video/webm' + }); + } + + if (callback) { + callback(that.blob); + } + + whammy.frames = []; + }); + }); + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof CanvasRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPausedRecording = true; + + if (mediaStreamRecorder instanceof MediaStreamRecorder) { + mediaStreamRecorder.pause(); + return; + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof CanvasRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPausedRecording = false; + + if (mediaStreamRecorder instanceof MediaStreamRecorder) { + mediaStreamRecorder.resume(); + return; + } + + if (!isRecording) { + this.record(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof CanvasRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (isRecording) { + this.stop(clearRecordedDataCB); + } + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + whammy.frames = []; + isRecording = false; + isPausedRecording = false; + } + + // for debugging + this.name = 'CanvasRecorder'; + this.toString = function () { + return this.name; + }; + + function cloneCanvas() { + //create a new canvas + var newCanvas = document.createElement('canvas'); + var context = newCanvas.getContext('2d'); + + //set dimensions + newCanvas.width = htmlElement.width; + newCanvas.height = htmlElement.height; + + //apply the old canvas to the new one + context.drawImage(htmlElement, 0, 0); + + //return the new canvas + return newCanvas; + } + + function drawCanvasFrame() { + if (isPausedRecording) { + lastTime = new Date().getTime(); + return setTimeout(drawCanvasFrame, 500); + } + + if (htmlElement.nodeName.toLowerCase() === 'canvas') { + var duration = new Date().getTime() - lastTime; + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + whammy.frames.push({ + image: cloneCanvas(), + duration: duration + }); + + if (isRecording) { + setTimeout(drawCanvasFrame, config.frameInterval); + } + return; + } + + html2canvas(htmlElement, { + grabMouse: typeof config.showMousePointer === 'undefined' || config.showMousePointer, + onrendered: function (canvas) { + var duration = new Date().getTime() - lastTime; + if (!duration) { + return setTimeout(drawCanvasFrame, config.frameInterval); + } + + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + whammy.frames.push({ + image: canvas.toDataURL('image/webp', 1), + duration: duration + }); + + if (isRecording) { + setTimeout(drawCanvasFrame, config.frameInterval); + } + } + }); + } + + var lastTime = new Date().getTime(); + + var whammy = new Whammy.Video(100); + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.CanvasRecorder = CanvasRecorder; + } + + // _________________ + // WhammyRecorder.js + + /** + * WhammyRecorder is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It runs top over {@link Whammy}. + * @summary Video recording feature in Chrome. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef WhammyRecorder + * @class + * @example + * var recorder = new WhammyRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {disableLogs: true, initCallback: function, video: HTMLVideoElement, etc.} + */ + + function WhammyRecorder(mediaStream, config) { + + config = config || {}; + + if (!config.frameInterval) { + config.frameInterval = 10; + } + + if (!config.disableLogs) { + console.log('Using frames-interval:', config.frameInterval); + } + + /** + * This method records video. + * @method + * @memberof WhammyRecorder + * @example + * recorder.record(); + */ + this.record = function () { + if (!config.width) { + config.width = 320; + } + + if (!config.height) { + config.height = 240; + } + + if (!config.video) { + config.video = { + width: config.width, + height: config.height + }; + } + + if (!config.canvas) { + config.canvas = { + width: config.width, + height: config.height + }; + } + + canvas.width = config.canvas.width || 320; + canvas.height = config.canvas.height || 240; + + context = canvas.getContext('2d'); + + // setting defaults + if (config.video && config.video instanceof HTMLVideoElement) { + video = config.video.cloneNode(); + + if (config.initCallback) { + config.initCallback(); + } + } else { + video = document.createElement('video'); + + setSrcObject(mediaStream, video); + + video.onloadedmetadata = function () { // "onloadedmetadata" may NOT work in FF? + if (config.initCallback) { + config.initCallback(); + } + }; + + video.width = config.video.width; + video.height = config.video.height; + } + + video.muted = true; + video.play(); + + lastTime = new Date().getTime(); + whammy = new Whammy.Video(); + + if (!config.disableLogs) { + console.log('canvas resolutions', canvas.width, '*', canvas.height); + console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); + } + + drawFrames(config.frameInterval); + }; + + /** + * Draw and push frames to Whammy + * @param {integer} frameInterval - set minimum interval (in milliseconds) between each time we push a frame to Whammy + */ + function drawFrames(frameInterval) { + frameInterval = typeof frameInterval !== 'undefined' ? frameInterval : 10; + + var duration = new Date().getTime() - lastTime; + if (!duration) { + return setTimeout(drawFrames, frameInterval, frameInterval); + } + + if (isPausedRecording) { + lastTime = new Date().getTime(); + return setTimeout(drawFrames, 100); + } + + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + if (video.paused) { + // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 + // Tweak for Android Chrome + video.play(); + } + + context.drawImage(video, 0, 0, canvas.width, canvas.height); + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); + + if (!isStopDrawing) { + setTimeout(drawFrames, frameInterval, frameInterval); + } + } + + function asyncLoop(o) { + var i = -1, + length = o.length; + + (function loop() { + i++; + if (i === length) { + o.callback(); + return; + } + + // "setTimeout" added by Jim McLeod + setTimeout(function () { + o.functionToLoop(loop, i); + }, 1); + })(); + } + + + /** + * remove black frames from the beginning to the specified frame + * @param {Array} _frames - array of frames to be checked + * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) + * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all + * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all + * @returns {Array} - array of frames + */ + // pull#293 by @volodalexey + function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance, callback) { + var localCanvas = document.createElement('canvas'); + localCanvas.width = canvas.width; + localCanvas.height = canvas.height; + var context2d = localCanvas.getContext('2d'); + var resultFrames = []; + + var checkUntilNotBlack = _framesToCheck === -1; + var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? + _framesToCheck : _frames.length; + var sampleColor = { + r: 0, + g: 0, + b: 0 + }; + var maxColorDifference = Math.sqrt( + Math.pow(255, 2) + + Math.pow(255, 2) + + Math.pow(255, 2) + ); + var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; + var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; + var doNotCheckNext = false; + + asyncLoop({ + length: endCheckFrame, + functionToLoop: function (loop, f) { + var matchPixCount, endPixCheck, maxPixCount; + + var finishImage = function () { + if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance); else { + // console.log('frame is passed : ' + f); + if (checkUntilNotBlack) { + doNotCheckNext = true; + } + resultFrames.push(_frames[f]); + } + loop(); + }; + + if (!doNotCheckNext) { + var image = new Image(); + image.onload = function () { + context2d.drawImage(image, 0, 0, canvas.width, canvas.height); + var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); + matchPixCount = 0; + endPixCheck = imageData.data.length; + maxPixCount = imageData.data.length / 4; + + for (var pix = 0; pix < endPixCheck; pix += 4) { + var currentColor = { + r: imageData.data[pix], + g: imageData.data[pix + 1], + b: imageData.data[pix + 2] + }; + var colorDifference = Math.sqrt( + Math.pow(currentColor.r - sampleColor.r, 2) + + Math.pow(currentColor.g - sampleColor.g, 2) + + Math.pow(currentColor.b - sampleColor.b, 2) + ); + // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) + if (colorDifference <= maxColorDifference * pixTolerance) { + matchPixCount++; + } + } + finishImage(); + }; + image.src = _frames[f].image; + } else { + finishImage(); + } + }, + callback: function () { + resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); + + if (resultFrames.length <= 0) { + // at least one last frame should be available for next manipulation + // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... + resultFrames.push(_frames[_frames.length - 1]); + } + callback(resultFrames); + } + }); + } + + var isStopDrawing = false; + + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WhammyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + isStopDrawing = true; + + var _this = this; + // analyse of all frames takes some time! + setTimeout(function () { + // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames + // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames + // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color + dropBlackFrames(whammy.frames, -1, null, null, function (frames) { + whammy.frames = frames; + + // to display advertisement images! + if (config.advertisement && config.advertisement.length) { + whammy.frames = config.advertisement.concat(whammy.frames); + } + + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof WhammyRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + whammy.compile(function (blob) { + _this.blob = blob; + + if (_this.blob.forEach) { + _this.blob = new Blob([], { + type: 'video/webm' + }); + } + + if (callback) { + callback(_this.blob); + } + }); + }); + }, 10); + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof WhammyRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPausedRecording = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof WhammyRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPausedRecording = false; + + if (isStopDrawing) { + this.record(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof WhammyRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (!isStopDrawing) { + this.stop(clearRecordedDataCB); + } + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + whammy.frames = []; + isStopDrawing = true; + isPausedRecording = false; + } + + // for debugging + this.name = 'WhammyRecorder'; + this.toString = function () { + return this.name; + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + var video; + var lastTime; + var whammy; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.WhammyRecorder = WhammyRecorder; + } + + // https://github.com/antimatter15/whammy/blob/master/LICENSE + // _________ + // Whammy.js + + // todo: Firefox now supports webp for webm containers! + // their MediaRecorder implementation works well! + // should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? + + /** + * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} + * @summary A real time javascript webm encoder based on a canvas hack. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + var Whammy = (function () { + // a more abstract-ish API + + function WhammyVideo(duration) { + this.frames = []; + this.duration = duration || 1; + this.quality = 0.8; + } + + /** + * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.add(canvas || context || 'image/webp'); + * @param {string} frame - Canvas || Context || image/webp + * @param {number} duration - Stick a duration (in milliseconds) + */ + WhammyVideo.prototype.add = function (frame, duration) { + if ('canvas' in frame) { //CanvasRenderingContext2D + frame = frame.canvas; + } + + if ('toDataURL' in frame) { + frame = frame.toDataURL('image/webp', this.quality); + } + + if (!(/^data:image\/webp;base64,/ig).test(frame)) { + throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp'; + } + this.frames.push({ + image: frame, + duration: duration || this.duration + }); + }; + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } + + function whammyInWebWorker(frames) { + function ArrayToWebM(frames) { + var info = checkFrames(frames); + if (!info) { + return []; + } + + var clusterMaxDuration = 30000; + + var EBML = [{ + 'id': 0x1a45dfa3, // EBML + 'data': [{ + 'data': 1, + 'id': 0x4286 // EBMLVersion + }, { + 'data': 1, + 'id': 0x42f7 // EBMLReadVersion + }, { + 'data': 4, + 'id': 0x42f2 // EBMLMaxIDLength + }, { + 'data': 8, + 'id': 0x42f3 // EBMLMaxSizeLength + }, { + 'data': 'webm', + 'id': 0x4282 // DocType + }, { + 'data': 2, + 'id': 0x4287 // DocTypeVersion + }, { + 'data': 2, + 'id': 0x4285 // DocTypeReadVersion + }] + }, { + 'id': 0x18538067, // Segment + 'data': [{ + 'id': 0x1549a966, // Info + 'data': [{ + 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) + 'id': 0x2ad7b1 // TimecodeScale + }, { + 'data': 'whammy', + 'id': 0x4d80 // MuxingApp + }, { + 'data': 'whammy', + 'id': 0x5741 // WritingApp + }, { + 'data': doubleToString(info.duration), + 'id': 0x4489 // Duration + }] + }, { + 'id': 0x1654ae6b, // Tracks + 'data': [{ + 'id': 0xae, // TrackEntry + 'data': [{ + 'data': 1, + 'id': 0xd7 // TrackNumber + }, { + 'data': 1, + 'id': 0x73c5 // TrackUID + }, { + 'data': 0, + 'id': 0x9c // FlagLacing + }, { + 'data': 'und', + 'id': 0x22b59c // Language + }, { + 'data': 'V_VP8', + 'id': 0x86 // CodecID + }, { + 'data': 'VP8', + 'id': 0x258688 // CodecName + }, { + 'data': 1, + 'id': 0x83 // TrackType + }, { + 'id': 0xe0, // Video + 'data': [{ + 'data': info.width, + 'id': 0xb0 // PixelWidth + }, { + 'data': info.height, + 'id': 0xba // PixelHeight + }] + }] + }] + }] + }]; + + //Generate clusters (max duration) + var frameNumber = 0; + var clusterTimecode = 0; + while (frameNumber < frames.length) { + + var clusterFrames = []; + var clusterDuration = 0; + do { + clusterFrames.push(frames[frameNumber]); + clusterDuration += frames[frameNumber].duration; + frameNumber++; + } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration); + + var clusterCounter = 0; + var cluster = { + 'id': 0x1f43b675, // Cluster + 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames) + }; //Add cluster to segment + EBML[1].data.push(cluster); + clusterTimecode += clusterDuration; + } + + return generateEBML(EBML); + } + + function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { + return [{ + 'data': clusterTimecode, + 'id': 0xe7 // Timecode + }].concat(clusterFrames.map(function (webp) { + var block = makeSimpleBlock({ + discardable: 0, + frame: webp.data.slice(4), + invisible: 0, + keyframe: 1, + lacing: 0, + trackNum: 1, + timecode: Math.round(clusterCounter) + }); + clusterCounter += webp.duration; + return { + data: block, + id: 0xa3 + }; + })); + } + + // sums the lengths of all the frames and gets the duration + + function checkFrames(frames) { + if (!frames[0]) { + postMessage({ + error: 'Something went wrong. Maybe WebP format is not supported in the current browser.' + }); + return; + } + + var width = frames[0].width, + height = frames[0].height, + duration = frames[0].duration; + + for (var i = 1; i < frames.length; i++) { + duration += frames[i].duration; + } + return { + duration: duration, + width: width, + height: height + }; + } + + function numToBuffer(num) { + var parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); + } + + function strToBuffer(str) { + return new Uint8Array(str.split('').map(function (e) { + return e.charCodeAt(0); + })); + } + + function bitsToBuffer(bits) { + var data = []; + var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (var i = 0; i < bits.length; i += 8) { + data.push(parseInt(bits.substr(i, 8), 2)); + } + return new Uint8Array(data); + } + + function generateEBML(json) { + var ebml = []; + for (var i = 0; i < json.length; i++) { + var data = json[i].data; + + if (typeof data === 'object') { + data = generateEBML(data); + } + + if (typeof data === 'number') { + data = bitsToBuffer(data.toString(2)); + } + + if (typeof data === 'string') { + data = strToBuffer(data); + } + + var len = data.size || data.byteLength || data.length; + var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); + var sizeToString = len.toString(2); + var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString; + var size = (new Array(zeroes)).join('0') + '1' + padded; + + ebml.push(numToBuffer(json[i].id)); + ebml.push(bitsToBuffer(size)); + ebml.push(data); + } + + return new Blob(ebml, { + type: 'video/webm' + }); + } + + function makeSimpleBlock(data) { + var flags = 0; + + if (data.keyframe) { + flags |= 128; + } + + if (data.invisible) { + flags |= 8; + } + + if (data.lacing) { + flags |= (data.lacing << 1); + } + + if (data.discardable) { + flags |= 1; + } + + if (data.trackNum > 127) { + throw 'TrackNumber > 127 not supported'; + } + + var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function (e) { + return String.fromCharCode(e); + }).join('') + data.frame; + + return out; + } + + function parseWebP(riff) { + var VP8 = riff.RIFF[0].WEBP[0]; + + var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header + for (var i = 0, c = []; i < 4; i++) { + c[i] = VP8.charCodeAt(frameStart + 3 + i); + } + + var width, height, tmp; + + //the code below is literally copied verbatim from the bitstream spec + tmp = (c[1] << 8) | c[0]; + width = tmp & 0x3FFF; + tmp = (c[3] << 8) | c[2]; + height = tmp & 0x3FFF; + return { + width: width, + height: height, + data: VP8, + riff: riff + }; + } + + function getStrLength(string, offset) { + return parseInt(string.substr(offset + 4, 4).split('').map(function (i) { + var unpadded = i.charCodeAt(0).toString(2); + return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; + }).join(''), 2); + } + + function parseRIFF(string) { + var offset = 0; + var chunks = {}; + + while (offset < string.length) { + var id = string.substr(offset, 4); + var len = getStrLength(string, offset); + var data = string.substr(offset + 4 + 4, len); + offset += 4 + 4 + len; + chunks[id] = chunks[id] || []; + + if (id === 'RIFF' || id === 'LIST') { + chunks[id].push(parseRIFF(data)); + } else { + chunks[id].push(data); + } + } + return chunks; + } + + function doubleToString(num) { + return [].slice.call( + new Uint8Array((new Float64Array([num])).buffer), 0).map(function (e) { + return String.fromCharCode(e); + }).reverse().join(''); + } + + var webm = new ArrayToWebM(frames.map(function (frame) { + var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); + webp.duration = frame.duration; + return webp; + })); + + postMessage(webm); + } + + /** + * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.compile(function(blob) { + * // blob.size - blob.type + * }); + */ + WhammyVideo.prototype.compile = function (callback) { + var webWorker = processInWebWorker(whammyInWebWorker); + + webWorker.onmessage = function (event) { + if (event.data.error) { + console.error(event.data.error); + return; + } + callback(event.data); + }; + + webWorker.postMessage(this.frames); + }; + + return { + /** + * A more abstract-ish API. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * @param {?number} speed - 0.8 + * @param {?number} quality - 100 + */ + Video: WhammyVideo + }; + })(); + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.Whammy = Whammy; + } + + // ______________ (indexed-db) + // DiskStorage.js + + /** + * DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage. + * @summary Writing blobs into IndexedDB. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + * // DiskStorage.dataStoreName = 'recordRTC'; + * // DiskStorage.onError = function(error) { }; + * @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @property {function} Fetch - This method fetches stored blobs from IndexedDB. + * @property {function} Store - This method stores blobs in IndexedDB. + * @property {function} onError - This function is invoked for any known/unknown error. + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + + var DiskStorage = { + /** + * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.init(); + */ + init: function () { + var self = this; + + if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') { + console.error('IndexedDB API are not available in this browser.'); + return; + } + + var dbVersion = 1; + var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''), + db; + var request = indexedDB.open(dbName, dbVersion); + + function createObjectStore(dataBase) { + dataBase.createObjectStore(self.dataStoreName); + } + + function putInDB() { + var transaction = db.transaction([self.dataStoreName], 'readwrite'); + + if (self.videoBlob) { + transaction.objectStore(self.dataStoreName).put(self.videoBlob, 'videoBlob'); + } + + if (self.gifBlob) { + transaction.objectStore(self.dataStoreName).put(self.gifBlob, 'gifBlob'); + } + + if (self.audioBlob) { + transaction.objectStore(self.dataStoreName).put(self.audioBlob, 'audioBlob'); + } + + function getFromStore(portionName) { + transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function (event) { + if (self.callback) { + self.callback(event.target.result, portionName); + } + }; + } + + getFromStore('audioBlob'); + getFromStore('videoBlob'); + getFromStore('gifBlob'); + } + + request.onerror = self.onError; + + request.onsuccess = function () { + db = request.result; + db.onerror = self.onError; + + if (db.setVersion) { + if (db.version !== dbVersion) { + var setVersion = db.setVersion(dbVersion); + setVersion.onsuccess = function () { + createObjectStore(db); + putInDB(); + }; + } else { + putInDB(); + } + } else { + putInDB(); + } + }; + request.onupgradeneeded = function (event) { + createObjectStore(event.target.result); + }; + }, + /** + * This method fetches stored blobs from IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + */ + Fetch: function (callback) { + this.callback = callback; + this.init(); + + return this; + }, + /** + * This method stores blobs in IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + */ + Store: function (config) { + this.audioBlob = config.audioBlob; + this.videoBlob = config.videoBlob; + this.gifBlob = config.gifBlob; + + this.init(); + + return this; + }, + /** + * This function is invoked for any known/unknown error. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.onError = function(error){ + * alerot( JSON.stringify(error) ); + * }; + */ + onError: function (error) { + console.error(JSON.stringify(error, null, '\t')); + }, + + /** + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.dataStoreName = 'recordRTC'; + */ + dataStoreName: 'recordRTC', + dbName: null + }; + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.DiskStorage = DiskStorage; + } + + // ______________ + // GifRecorder.js + + /** + * GifRecorder is standalone calss used by {@link RecordRTC} to record video or canvas into animated gif. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef GifRecorder + * @class + * @example + * var recorder = new GifRecorder(mediaStream || canvas || context, { onGifPreview: function, onGifRecordingStarted: function, width: 1280, height: 720, frameRate: 200, quality: 10 }); + * recorder.record(); + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object or HTMLCanvasElement or CanvasRenderingContext2D. + * @param {object} config - {disableLogs:true, initCallback: function, width: 320, height: 240, frameRate: 200, quality: 10} + */ + + function GifRecorder(mediaStream, config) { + if (typeof GIFEncoder === 'undefined') { + var script = document.createElement('script'); + script.src = 'https://www.webrtc-experiment.com/gif-recorder.js'; + (document.body || document.documentElement).appendChild(script); + } + + config = config || {}; + + var isHTMLObject = mediaStream instanceof CanvasRenderingContext2D || mediaStream instanceof HTMLCanvasElement; + + /** + * This method records MediaStream. + * @method + * @memberof GifRecorder + * @example + * recorder.record(); + */ + this.record = function () { + if (typeof GIFEncoder === 'undefined') { + setTimeout(self.record, 1000); + return; + } + + if (!isLoadedMetaData) { + setTimeout(self.record, 1000); + return; + } + + if (!isHTMLObject) { + if (!config.width) { + config.width = video.offsetWidth || 320; + } + + if (!config.height) { + config.height = video.offsetHeight || 240; + } + + if (!config.video) { + config.video = { + width: config.width, + height: config.height + }; + } + + if (!config.canvas) { + config.canvas = { + width: config.width, + height: config.height + }; + } + + canvas.width = config.canvas.width || 320; + canvas.height = config.canvas.height || 240; + + video.width = config.video.width || 320; + video.height = config.video.height || 240; + } + + // external library to record as GIF images + gifEncoder = new GIFEncoder(); + + // void setRepeat(int iter) + // Sets the number of times the set of GIF frames should be played. + // Default is 1; 0 means play indefinitely. + gifEncoder.setRepeat(0); + + // void setFrameRate(Number fps) + // Sets frame rate in frames per second. + // Equivalent to setDelay(1000/fps). + // Using "setDelay" instead of "setFrameRate" + gifEncoder.setDelay(config.frameRate || 200); + + // void setQuality(int quality) + // Sets quality of color quantization (conversion of images to the + // maximum 256 colors allowed by the GIF specification). + // Lower values (minimum = 1) produce better colors, + // but slow processing significantly. 10 is the default, + // and produces good color mapping at reasonable speeds. + // Values greater than 20 do not yield significant improvements in speed. + gifEncoder.setQuality(config.quality || 10); + + // Boolean start() + // This writes the GIF Header and returns false if it fails. + gifEncoder.start(); + + if (typeof config.onGifRecordingStarted === 'function') { + config.onGifRecordingStarted(); + } + + function drawVideoFrame(time) { + if (self.clearedRecordedData === true) { + return; + } + + if (isPausedRecording) { + return setTimeout(function () { + drawVideoFrame(time); + }, 100); + } + + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); + + if (typeof lastFrameTime === undefined) { + lastFrameTime = time; + } + + // ~10 fps + if (time - lastFrameTime < 90) { + return; + } + + if (!isHTMLObject && video.paused) { + // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 + // Tweak for Android Chrome + video.play(); + } + + if (!isHTMLObject) { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + } + + if (config.onGifPreview) { + config.onGifPreview(canvas.toDataURL('image/png')); + } + + gifEncoder.addFrame(context); + lastFrameTime = time; + } + + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); + + if (config.initCallback) { + config.initCallback(); + } + }; + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof GifRecorder + * @example + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + if (lastAnimationFrame) { + cancelAnimationFrame(lastAnimationFrame); + } + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof GifRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], { + type: 'image/gif' + }); + + callback(this.blob); + + // bug: find a way to clear old recorded blobs + gifEncoder.stream().bin = []; + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof GifRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPausedRecording = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof GifRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPausedRecording = false; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof GifRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + self.clearedRecordedData = true; + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + if (gifEncoder) { + gifEncoder.stream().bin = []; + } + } + + // for debugging + this.name = 'GifRecorder'; + this.toString = function () { + return this.name; + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + if (isHTMLObject) { + if (mediaStream instanceof CanvasRenderingContext2D) { + context = mediaStream; + canvas = context.canvas; + } else if (mediaStream instanceof HTMLCanvasElement) { + context = mediaStream.getContext('2d'); + canvas = mediaStream; + } + } + + var isLoadedMetaData = true; + + if (!isHTMLObject) { + var video = document.createElement('video'); + video.muted = true; + video.autoplay = true; + video.playsInline = true; + + isLoadedMetaData = false; + video.onloadedmetadata = function () { + isLoadedMetaData = true; + }; + + setSrcObject(mediaStream, video); + + video.play(); + } + + var lastAnimationFrame = null; + var lastFrameTime; + + var gifEncoder; + + var self = this; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.GifRecorder = GifRecorder; + } + + // Last time updated: 2019-06-21 4:09:42 AM UTC + + // ________________________ + // MultiStreamsMixer v1.2.2 + + // Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer + + // -------------------------------------------------- + // Muaz Khan - www.MuazKhan.com + // MIT License - www.WebRTC-Experiment.com/licence + // -------------------------------------------------- + + function MultiStreamsMixer(arrayOfMediaStreams, elementClass) { + + var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; + + (function (that) { + if (typeof RecordRTC !== 'undefined') { + return; + } + + if (!that) { + return; + } + + if (typeof window !== 'undefined') { + return; + } + + if (typeof commonjsGlobal === 'undefined') { + return; + } + + commonjsGlobal.navigator = { + userAgent: browserFakeUserAgent, + getUserMedia: function () { } + }; + + if (!commonjsGlobal.console) { + commonjsGlobal.console = {}; + } + + if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') { + commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function () { + console.log(arguments); + }; + } + + if (typeof document === 'undefined') { + /*global document:true */ + that.document = { + documentElement: { + appendChild: function () { + return ''; + } + } + }; + + document.createElement = document.captureStream = document.mozCaptureStream = function () { + var obj = { + getContext: function () { + return obj; + }, + play: function () { }, + pause: function () { }, + drawImage: function () { }, + toDataURL: function () { + return ''; + }, + style: {} + }; + return obj; + }; + + that.HTMLVideoElement = function () { }; + } + + if (typeof location === 'undefined') { + /*global location:true */ + that.location = { + protocol: 'file:', + href: '', + hash: '' + }; + } + + if (typeof screen === 'undefined') { + /*global screen:true */ + that.screen = { + width: 0, + height: 0 + }; + } + + if (typeof URL === 'undefined') { + /*global screen:true */ + that.URL = { + createObjectURL: function () { + return ''; + }, + revokeObjectURL: function () { + return ''; + } + }; + } + + /*global window:true */ + that.window = commonjsGlobal; + })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null); + + // requires: chrome://flags/#enable-experimental-web-platform-features + + elementClass = elementClass || 'multi-streams-mixer'; + + var videos = []; + var isStopDrawingFrames = false; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.style.opacity = 0; + canvas.style.position = 'absolute'; + canvas.style.zIndex = -1; + canvas.style.top = '-1000em'; + canvas.style.left = '-1000em'; + canvas.className = elementClass; + (document.body || document.documentElement).appendChild(canvas); + + this.disableLogs = false; + this.frameInterval = 10; + + this.width = 360; + this.height = 240; + + // use gain node to prevent echo + this.useGainNode = true; + + var self = this; + + // _____________________________ + // Cross-Browser-Declarations.js + + // WebAudio API representer + var AudioContext = window.AudioContext; + + if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } + } + + /*jshint -W079 */ + var URL = window.URL; + + if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; + } + + if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined') { + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function () { + this.getTracks().forEach(function (track) { + track.stop(); + }); + }; + } + } + + var Storage = {}; + + if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; + } else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; + } + + function setSrcObject(stream, element) { + if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + element.srcObject = stream; + } + } + + this.startDrawingFrames = function () { + drawVideosToCanvas(); + }; + + function drawVideosToCanvas() { + if (isStopDrawingFrames) { + return; + } + + var videosLength = videos.length; + + var fullcanvas = false; + var remaining = []; + videos.forEach(function (video) { + if (!video.stream) { + video.stream = {}; + } + + if (video.stream.fullcanvas) { + fullcanvas = video; + } else { + // todo: video.stream.active or video.stream.live to fix blank frames issues? + remaining.push(video); + } + }); + + if (fullcanvas) { + canvas.width = fullcanvas.stream.width; + canvas.height = fullcanvas.stream.height; + } else if (remaining.length) { + canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width; + + var height = 1; + if (videosLength === 3 || videosLength === 4) { + height = 2; + } + if (videosLength === 5 || videosLength === 6) { + height = 3; + } + if (videosLength === 7 || videosLength === 8) { + height = 4; + } + if (videosLength === 9 || videosLength === 10) { + height = 5; + } + canvas.height = remaining[0].height * height; + } else { + canvas.width = self.width || 360; + canvas.height = self.height || 240; + } + + if (fullcanvas && fullcanvas instanceof HTMLVideoElement) { + drawImage(fullcanvas); + } + + remaining.forEach(function (video, idx) { + drawImage(video, idx); + }); + + setTimeout(drawVideosToCanvas, self.frameInterval); + } + + function drawImage(video, idx) { + if (isStopDrawingFrames) { + return; + } + + var x = 0; + var y = 0; + var width = video.width; + var height = video.height; + + if (idx === 1) { + x = video.width; + } + + if (idx === 2) { + y = video.height; + } + + if (idx === 3) { + x = video.width; + y = video.height; + } + + if (idx === 4) { + y = video.height * 2; + } + + if (idx === 5) { + x = video.width; + y = video.height * 2; + } + + if (idx === 6) { + y = video.height * 3; + } + + if (idx === 7) { + x = video.width; + y = video.height * 3; + } + + if (typeof video.stream.left !== 'undefined') { + x = video.stream.left; + } + + if (typeof video.stream.top !== 'undefined') { + y = video.stream.top; + } + + if (typeof video.stream.width !== 'undefined') { + width = video.stream.width; + } + + if (typeof video.stream.height !== 'undefined') { + height = video.stream.height; + } + + context.drawImage(video, x, y, width, height); + + if (typeof video.stream.onRender === 'function') { + video.stream.onRender(context, x, y, width, height, idx); + } + } + + function getMixedStream() { + isStopDrawingFrames = false; + var mixedVideoStream = getMixedVideoStream(); + + var mixedAudioStream = getMixedAudioStream(); + if (mixedAudioStream) { + mixedAudioStream.getTracks().filter(function (t) { + return t.kind === 'audio'; + }).forEach(function (track) { + mixedVideoStream.addTrack(track); + }); + } + arrayOfMediaStreams.forEach(function (stream) { + if (stream.fullcanvas); + }); + + // mixedVideoStream.prototype.appendStreams = appendStreams; + // mixedVideoStream.prototype.resetVideoStreams = resetVideoStreams; + // mixedVideoStream.prototype.clearRecordedData = clearRecordedData; + + return mixedVideoStream; + } + + function getMixedVideoStream() { + resetVideoStreams(); + + var capturedStream; + + if ('captureStream' in canvas) { + capturedStream = canvas.captureStream(); + } else if ('mozCaptureStream' in canvas) { + capturedStream = canvas.mozCaptureStream(); + } else if (!self.disableLogs) { + console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features'); + } + + var videoStream = new MediaStream(); + + capturedStream.getTracks().filter(function (t) { + return t.kind === 'video'; + }).forEach(function (track) { + videoStream.addTrack(track); + }); + + canvas.stream = videoStream; + + return videoStream; + } + + function getMixedAudioStream() { + // via: @pehrsons + if (!Storage.AudioContextConstructor) { + Storage.AudioContextConstructor = new Storage.AudioContext(); + } + + self.audioContext = Storage.AudioContextConstructor; + + self.audioSources = []; + + if (self.useGainNode === true) { + self.gainNode = self.audioContext.createGain(); + self.gainNode.connect(self.audioContext.destination); + self.gainNode.gain.value = 0; // don't hear self + } + + var audioTracksLength = 0; + arrayOfMediaStreams.forEach(function (stream) { + if (!stream.getTracks().filter(function (t) { + return t.kind === 'audio'; + }).length) { + return; + } + + audioTracksLength++; + + var audioSource = self.audioContext.createMediaStreamSource(stream); + + if (self.useGainNode === true) { + audioSource.connect(self.gainNode); + } + + self.audioSources.push(audioSource); + }); + + if (!audioTracksLength) { + // because "self.audioContext" is not initialized + // that's why we've to ignore rest of the code + return; + } + + self.audioDestination = self.audioContext.createMediaStreamDestination(); + self.audioSources.forEach(function (audioSource) { + audioSource.connect(self.audioDestination); + }); + return self.audioDestination.stream; + } + + function getVideo(stream) { + var video = document.createElement('video'); + + setSrcObject(stream, video); + + video.className = elementClass; + + video.muted = true; + video.volume = 0; + + video.width = stream.width || self.width || 360; + video.height = stream.height || self.height || 240; + + video.play(); + + return video; + } + + this.appendStreams = function (streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + streams.forEach(function (stream) { + var newStream = new MediaStream(); + + if (stream.getTracks().filter(function (t) { + return t.kind === 'video'; + }).length) { + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + + newStream.addTrack(stream.getTracks().filter(function (t) { + return t.kind === 'video'; + })[0]); + } + + if (stream.getTracks().filter(function (t) { + return t.kind === 'audio'; + }).length) { + var audioSource = self.audioContext.createMediaStreamSource(stream); + self.audioDestination = self.audioContext.createMediaStreamDestination(); + audioSource.connect(self.audioDestination); + + newStream.addTrack(self.audioDestination.stream.getTracks().filter(function (t) { + return t.kind === 'audio'; + })[0]); + } + + arrayOfMediaStreams.push(newStream); + }); + }; + + this.releaseStreams = function () { + videos = []; + isStopDrawingFrames = true; + + if (self.gainNode) { + self.gainNode.disconnect(); + self.gainNode = null; + } + + if (self.audioSources.length) { + self.audioSources.forEach(function (source) { + source.disconnect(); + }); + self.audioSources = []; + } + + if (self.audioDestination) { + self.audioDestination.disconnect(); + self.audioDestination = null; + } + + if (self.audioContext) { + self.audioContext.close(); + } + + self.audioContext = null; + + context.clearRect(0, 0, canvas.width, canvas.height); + + if (canvas.stream) { + canvas.stream.stop(); + canvas.stream = null; + } + }; + + this.resetVideoStreams = function (streams) { + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + resetVideoStreams(streams); + }; + + function resetVideoStreams(streams) { + videos = []; + streams = streams || arrayOfMediaStreams; + + // via: @adrian-ber + streams.forEach(function (stream) { + if (!stream.getTracks().filter(function (t) { + return t.kind === 'video'; + }).length) { + return; + } + + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + }); + } + + // for debugging + this.name = 'MultiStreamsMixer'; + this.toString = function () { + return this.name; + }; + + this.getMixedStream = getMixedStream; + + } + + if (typeof RecordRTC === 'undefined') { + { + module.exports = MultiStreamsMixer; + } + } + + // ______________________ + // MultiStreamRecorder.js + + /* + * Video conference recording, using captureStream API along with WebAudio and Canvas2D API. + */ + + /** + * MultiStreamRecorder can record multiple videos in single container. + * @summary Multi-videos recorder. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef MultiStreamRecorder + * @class + * @example + * var options = { + * mimeType: 'video/webm' + * } + * var recorder = new MultiStreamRecorder(ArrayOfMediaStreams, options); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * + * // or + * var blob = recorder.blob; + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStreams} mediaStreams - Array of MediaStreams. + * @param {object} config - {disableLogs:true, frameInterval: 1, mimeType: "video/webm"} + */ + + function MultiStreamRecorder(arrayOfMediaStreams, options) { + arrayOfMediaStreams = arrayOfMediaStreams || []; + var self = this; + + var mixer; + var mediaRecorder; + + options = options || { + elementClass: 'multi-streams-mixer', + mimeType: 'video/webm', + video: { + width: 360, + height: 240 + } + }; + + if (!options.frameInterval) { + options.frameInterval = 10; + } + + if (!options.video) { + options.video = {}; + } + + if (!options.video.width) { + options.video.width = 360; + } + + if (!options.video.height) { + options.video.height = 240; + } + + /** + * This method records all MediaStreams. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.record(); + */ + this.record = function () { + // github/muaz-khan/MultiStreamsMixer + mixer = new MultiStreamsMixer(arrayOfMediaStreams, options.elementClass || 'multi-streams-mixer'); + + if (getAllVideoTracks().length) { + mixer.frameInterval = options.frameInterval || 10; + mixer.width = options.video.width || 360; + mixer.height = options.video.height || 240; + mixer.startDrawingFrames(); + } + + if (options.previewStream && typeof options.previewStream === 'function') { + options.previewStream(mixer.getMixedStream()); + } + + // record using MediaRecorder API + mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream(), options); + mediaRecorder.record(); + }; + + function getAllVideoTracks() { + var tracks = []; + arrayOfMediaStreams.forEach(function (stream) { + getTracks(stream, 'video').forEach(function (track) { + tracks.push(track); + }); + }); + return tracks; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + if (!mediaRecorder) { + return; + } + + mediaRecorder.stop(function (blob) { + self.blob = blob; + + callback(blob); + + self.clearRecordedData(); + }); + }; + + /** + * This method pauses the recording process. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + if (mediaRecorder) { + mediaRecorder.pause(); + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + if (mediaRecorder) { + mediaRecorder.resume(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (mediaRecorder) { + mediaRecorder.clearRecordedData(); + mediaRecorder = null; + } + + if (mixer) { + mixer.releaseStreams(); + mixer = null; + } + }; + + /** + * Add extra media-streams to existing recordings. + * @method + * @memberof MultiStreamRecorder + * @param {MediaStreams} mediaStreams - Array of MediaStreams + * @example + * recorder.addStreams([newAudioStream, newVideoStream]); + */ + this.addStreams = function (streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + arrayOfMediaStreams.concat(streams); + + if (!mediaRecorder || !mixer) { + return; + } + + mixer.appendStreams(streams); + + if (options.previewStream && typeof options.previewStream === 'function') { + options.previewStream(mixer.getMixedStream()); + } + }; + + /** + * Reset videos during live recording. Replace old videos e.g. replace cameras with full-screen. + * @method + * @memberof MultiStreamRecorder + * @param {MediaStreams} mediaStreams - Array of MediaStreams + * @example + * recorder.resetVideoStreams([newVideo1, newVideo2]); + */ + this.resetVideoStreams = function (streams) { + if (!mixer) { + return; + } + + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + mixer.resetVideoStreams(streams); + }; + + /** + * Returns MultiStreamsMixer + * @method + * @memberof MultiStreamRecorder + * @example + * let mixer = recorder.getMixer(); + * mixer.appendStreams([newStream]); + */ + this.getMixer = function () { + return mixer; + }; + + // for debugging + this.name = 'MultiStreamRecorder'; + this.toString = function () { + return this.name; + }; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.MultiStreamRecorder = MultiStreamRecorder; + } + + // _____________________ + // RecordRTC.promises.js + + /** + * RecordRTCPromisesHandler adds promises support in {@link RecordRTC}. Try a {@link https://github.com/muaz-khan/RecordRTC/blob/master/simple-demos/RecordRTCPromisesHandler.html|demo here} + * @summary Promises for {@link RecordRTC} + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTCPromisesHandler + * @class + * @example + * var recorder = new RecordRTCPromisesHandler(mediaStream, options); + * recorder.startRecording() + * .then(successCB) + * .catch(errorCB); + * // Note: You can access all RecordRTC API using "recorder.recordRTC" e.g. + * recorder.recordRTC.onStateChanged = function(state) {}; + * recorder.recordRTC.setRecordingDuration(5000); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. + * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} + * @throws Will throw an error if "new" keyword is not used to initiate "RecordRTCPromisesHandler". Also throws error if first argument "MediaStream" is missing. + * @requires {@link RecordRTC} + */ + + function RecordRTCPromisesHandler(mediaStream, options) { + if (!this) { + throw 'Use "new RecordRTCPromisesHandler()"'; + } + + if (typeof mediaStream === 'undefined') { + throw 'First argument "MediaStream" is required.'; + } + + var self = this; + + /** + * @property {Blob} blob - Access/reach the native {@link RecordRTC} object. + * @memberof RecordRTCPromisesHandler + * @example + * let internal = recorder.recordRTC.getInternalRecorder(); + * alert(internal instanceof MediaStreamRecorder); + * recorder.recordRTC.onStateChanged = function(state) {}; + */ + self.recordRTC = new RecordRTC(mediaStream, options); + + /** + * This method records MediaStream. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.startRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.startRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.startRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method stops the recording. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * var blob = recorder.getBlob(); + * }).catch(errorCB); + */ + this.stopRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.stopRecording(function (url) { + self.blob = self.recordRTC.getBlob(); + + if (!self.blob || !self.blob.size) { + reject('Empty blob.', self.blob); + return; + } + + resolve(url); + }); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method pauses the recording. You can resume recording using "resumeRecording" method. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.pauseRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.pauseRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.pauseRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method resumes the recording. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.resumeRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.resumeRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.resumeRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns data-url for the recorded blob. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * recorder.getDataURL().then(function(dataURL) { + * window.open(dataURL); + * }).catch(errorCB);; + * }).catch(errorCB); + */ + this.getDataURL = function (callback) { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.getDataURL(function (dataURL) { + resolve(dataURL); + }); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns the recorded blob. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * recorder.getBlob().then(function(blob) {}) + * }).catch(errorCB); + */ + this.getBlob = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.getBlob()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns the internal recording object. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * let internalRecorder = await recorder.getInternalRecorder(); + * if(internalRecorder instanceof MultiStreamRecorder) { + * internalRecorder.addStreams([newAudioStream]); + * internalRecorder.resetVideoStreams([screenStream]); + * } + * @returns {Object} + */ + this.getInternalRecorder = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.getInternalRecorder()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method resets the recorder. So that you can reuse single recorder instance many times. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * await recorder.reset(); + * recorder.startRecording(); // record again + */ + this.reset = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.reset()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * Destroy RecordRTC instance. Clear all recorders and objects. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.destroy().then(successCB).catch(errorCB); + */ + this.destroy = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.destroy()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * Get recorder's readonly state. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * let state = await recorder.getState(); + * // or + * recorder.getState().then(state => { console.log(state); }) + * @returns {String} Returns recording state. + */ + this.getState = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.getState()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * @property {Blob} blob - Recorded data as "Blob" object. + * @memberof RecordRTCPromisesHandler + * @example + * await recorder.stopRecording(); + * let blob = recorder.getBlob(); // or "recorder.recordRTC.blob" + * invokeSaveAsDialog(blob); + */ + this.blob = null; + + /** + * RecordRTC version number + * @property {String} version - Release version number. + * @memberof RecordRTCPromisesHandler + * @static + * @readonly + * @example + * alert(recorder.version); + */ + this.version = '5.6.2'; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.RecordRTCPromisesHandler = RecordRTCPromisesHandler; + } + + // ______________________ + // WebAssemblyRecorder.js + + /** + * WebAssemblyRecorder lets you create webm videos in JavaScript via WebAssembly. The library consumes raw RGBA32 buffers (4 bytes per pixel) and turns them into a webm video with the given framerate and quality. This makes it compatible out-of-the-box with ImageData from a CANVAS. With realtime mode you can also use webm-wasm for streaming webm videos. + * @summary Video recording feature in Chrome, Firefox and maybe Edge. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef WebAssemblyRecorder + * @class + * @example + * var recorder = new WebAssemblyRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {webAssemblyPath:'webm-wasm.wasm',workerPath: 'webm-worker.js', frameRate: 30, width: 1920, height: 1080, bitrate: 1024, realtime: true} + */ + function WebAssemblyRecorder(stream, config) { + // based on: github.com/GoogleChromeLabs/webm-wasm + + if (typeof ReadableStream === 'undefined' || typeof WritableStream === 'undefined') { + // because it fixes readable/writable streams issues + console.error('Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js'); + } + + config = config || {}; + + config.width = config.width || 640; + config.height = config.height || 480; + config.frameRate = config.frameRate || 30; + config.bitrate = config.bitrate || 1200; + config.realtime = config.realtime || true; + + var finished; + + function cameraStream() { + return new ReadableStream({ + start: function (controller) { + var cvs = document.createElement('canvas'); + var video = document.createElement('video'); + var first = true; + video.srcObject = stream; + video.muted = true; + video.height = config.height; + video.width = config.width; + video.volume = 0; + video.onplaying = function () { + cvs.width = config.width; + cvs.height = config.height; + var ctx = cvs.getContext('2d'); + var frameTimeout = 1000 / config.frameRate; + var cameraTimer = setInterval(function f() { + if (finished) { + clearInterval(cameraTimer); + controller.close(); + } + + if (first) { + first = false; + if (config.onVideoProcessStarted) { + config.onVideoProcessStarted(); + } + } + + ctx.drawImage(video, 0, 0); + if (controller._controlledReadableStream.state !== 'closed') { + try { + controller.enqueue( + ctx.getImageData(0, 0, config.width, config.height) + ); + } catch (e) { } + } + }, frameTimeout); + }; + video.play(); + } + }); + } + + var worker; + + function startRecording(stream, buffer) { + if (!config.workerPath && !buffer) { + finished = false; + + // is it safe to use @latest ? + + fetch( + 'https://unpkg.com/webm-wasm@latest/dist/webm-worker.js' + ).then(function (r) { + r.arrayBuffer().then(function (buffer) { + startRecording(stream, buffer); + }); + }); + return; + } + + if (!config.workerPath && buffer instanceof ArrayBuffer) { + var blob = new Blob([buffer], { + type: 'text/javascript' + }); + config.workerPath = URL.createObjectURL(blob); + } + + if (!config.workerPath) { + console.error('workerPath parameter is missing.'); + } + + worker = new Worker(config.workerPath); + + worker.postMessage(config.webAssemblyPath || 'https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm'); + worker.addEventListener('message', function (event) { + if (event.data === 'READY') { + worker.postMessage({ + width: config.width, + height: config.height, + bitrate: config.bitrate || 1200, + timebaseDen: config.frameRate || 30, + realtime: config.realtime + }); + + cameraStream().pipeTo(new WritableStream({ + write: function (image) { + if (finished) { + console.error('Got image, but recorder is finished!'); + return; + } + + worker.postMessage(image.data.buffer, [image.data.buffer]); + } + })); + } else if (!!event.data) { + if (!isPaused) { + arrayOfBuffers.push(event.data); + } + } + }); + } + + /** + * This method records video. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.record(); + */ + this.record = function () { + arrayOfBuffers = []; + isPaused = false; + this.blob = null; + startRecording(stream); + + if (typeof config.initCallback === 'function') { + config.initCallback(); + } + }; + + var isPaused; + + /** + * This method pauses the recording process. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPaused = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPaused = false; + }; + + function terminate(callback) { + if (!worker) { + if (callback) { + callback(); + } + + return; + } + + // Wait for null event data to indicate that the encoding is complete + worker.addEventListener('message', function (event) { + if (event.data === null) { + worker.terminate(); + worker = null; + + if (callback) { + callback(); + } + } + }); + + worker.postMessage(null); + } + + var arrayOfBuffers = []; + + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + finished = true; + + var recorder = this; + + terminate(function () { + recorder.blob = new Blob(arrayOfBuffers, { + type: 'video/webm' + }); + + callback(recorder.blob); + }); + }; + + // for debugging + this.name = 'WebAssemblyRecorder'; + this.toString = function () { + return this.name; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + arrayOfBuffers = []; + isPaused = false; + this.blob = null; + + // todo: if recording-ON then STOP it first + }; + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof WebAssemblyRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = null; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.WebAssemblyRecorder = WebAssemblyRecorder; + } + }); + + class RecordRTCLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.fileName = ''; + this.fileType = player._opt.recordType || FILE_SUFFIX.webm; + this.isRecording = false; + this.recordingTimestamp = 0; + this.recordingInterval = null; + player.debug.log('Recorder', 'init'); + } + + destroy() { + this._reset(); + + this.player.debug.log('Recorder', 'destroy'); + } + + setFileName(fileName, fileType) { + this.fileName = fileName; + + if (FILE_SUFFIX.mp4 === fileType || FILE_SUFFIX.webm === fileType) { + this.fileType = fileType; + } + } + + get recording() { + return this.isRecording; + } + + get recordTime() { + return this.recordingTimestamp; + } + + startRecord() { + const debug = this.player.debug; + const options = { + type: 'video', + mimeType: 'video/webm;codecs=h264', + onTimeStamp: timestamp => { + debug.log('Recorder', 'record timestamp :' + timestamp); + }, + disableLogs: !this.player._opt.debug + }; + + try { + const stream = this.player.video.$videoElement.captureStream(25); + + if (this.player.audio && this.player.audio.mediaStreamAudioDestinationNode && this.player.audio.mediaStreamAudioDestinationNode.stream && !this.player.audio.isStateSuspended() && this.player.audio.hasAudio && this.player._opt.hasAudio) { + const audioStream = this.player.audio.mediaStreamAudioDestinationNode.stream; + + if (audioStream.getAudioTracks().length > 0) { + const audioTrack = audioStream.getAudioTracks()[0]; + + if (audioTrack && audioTrack.enabled) { + stream.addTrack(audioTrack); + } + } + } + + this.recorder = RecordRTC_1(stream, options); + } catch (e) { + debug.error('Recorder', 'startRecord error', e); + this.emit(EVENTS.recordCreateError); + } + + if (this.recorder) { + this.isRecording = true; + this.player.emit(EVENTS.recording, true); + this.recorder.startRecording(); + debug.log('Recorder', 'start recording'); + this.player.emit(EVENTS.recordStart); + this.recordingInterval = window.setInterval(() => { + this.recordingTimestamp += 1; + this.player.emit(EVENTS.recordingTimestamp, this.recordingTimestamp); + }, 1000); + } + } + + stopRecordAndSave() { + if (!this.recorder || !this.isRecording) { + return; + } + + this.recorder.stopRecording(() => { + this.player.debug.log('Recorder', 'stop recording'); + this.player.emit(EVENTS.recordEnd); + const fileName = (this.fileName || now()) + '.' + (this.fileType || FILE_SUFFIX.webm); + saveAs(this.recorder.getBlob(), fileName); + + this._reset(); + + this.player.emit(EVENTS.recording, false); + }); + } + + _reset() { + this.isRecording = false; + this.recordingTimestamp = 0; + + if (this.recorder) { + this.recorder.destroy(); + this.recorder = null; + } + + this.fileName = null; + + if (this.recordingInterval) { + clearInterval(this.recordingInterval); + } + + this.recordingInterval = null; + } + + } + + class Recorder { + constructor(player) { + const Loader = Recorder.getLoaderFactory(); + return new Loader(player); + } + + static getLoaderFactory() { + return RecordRTCLoader; + } + + } + + class DecoderWorker { + constructor(player) { + this.player = player; + this.decoderWorker = new Worker(player._opt.decoder); + + this._initDecoderWorker(); + + player.debug.log('decoderWorker', 'init'); + } + + destroy() { + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.close + }); + this.decoderWorker.terminate(); + this.decoderWorker = null; + this.player.debug.log(`decoderWorker`, 'destroy'); + } + + _initDecoderWorker() { + const { + debug, + events: { + proxy + } + } = this.player; + + this.decoderWorker.onmessage = event => { + const msg = event.data; + + switch (msg.cmd) { + case WORKER_CMD_TYPE.init: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.init); + + if (!this.player.loaded) { + this.player.emit(EVENTS.load); + } + + this.player.emit(EVENTS.decoderWorkerInit); + + this._initWork(); + + break; + + case WORKER_CMD_TYPE.videoCode: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.videoCode, msg.code); + + if (!this.player._times.decodeStart) { + this.player._times.decodeStart = now(); + } + + this.player.video.updateVideoInfo({ + encTypeCode: msg.code + }); + break; + + case WORKER_CMD_TYPE.audioCode: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.audioCode, msg.code); + this.player.audio && this.player.audio.updateAudioInfo({ + encTypeCode: msg.code + }); + break; + + case WORKER_CMD_TYPE.initVideo: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initVideo, `width:${msg.w},height:${msg.h}`); + this.player.video.updateVideoInfo({ + width: msg.w, + height: msg.h + }); + + if (!this.player._opt.openWebglAlignment && !isWebglRenderSupport(msg.w)) { + this.player.emit(EVENTS_ERROR.webglAlignmentError); + return; + } + + this.player.video.initCanvasViewSize(); + break; + + case WORKER_CMD_TYPE.initAudio: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initAudio, `channels:${msg.channels},sampleRate:${msg.sampleRate}`); + + if (this.player.audio) { + this.player.audio.updateAudioInfo(msg); + this.player.audio.initScriptNode(msg); + } + + break; + + case WORKER_CMD_TYPE.render: + // debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.render, `msg ts:${msg.ts}`); + this.player.handleRender(); + this.player.video.render(msg); + this.player.emit(EVENTS.timeUpdate, msg.ts); + this.player.updateStats({ + fps: true, + ts: msg.ts, + buf: msg.delay + }); + + if (!this.player._times.videoStart) { + this.player._times.videoStart = now(); + this.player.handlePlayToRenderTimes(); + } + + break; + + case WORKER_CMD_TYPE.playAudio: + // debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.playAudio, `msg ts:${msg.ts}`); + // 只有在 playing 的时候。 + if (this.player.playing && this.player.audio) { + this.player.audio.play(msg.buffer, msg.ts); + } + + break; + + case WORKER_CMD_TYPE.wasmError: + if (msg.message) { + if (msg.message.indexOf(WASM_ERROR.invalidNalUnitSize) !== -1) { + this.player.emitError(EVENTS_ERROR.wasmDecodeError); + } + } + + break; + + default: + this.player[msg.cmd] && this.player[msg.cmd](msg); + } + }; + } + + _initWork() { + const opt = { + debug: this.player._opt.debug, + useOffscreen: this.player._opt.useOffscreen, + useWCS: this.player._opt.useWCS, + videoBuffer: this.player._opt.videoBuffer, + videoBufferDelay: this.player._opt.videoBufferDelay, + openWebglAlignment: this.player._opt.openWebglAlignment + }; + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.init, + opt: JSON.stringify(opt), + sampleRate: this.player.audio && this.player.audio.audioContext.sampleRate || 0 + }); + } + + decodeVideo(arrayBuffer, ts, isIFrame) { + const options = { + type: MEDIA_TYPE.video, + ts: Math.max(ts, 0), + isIFrame + }; // this.player.debug.log('decoderWorker', 'decodeVideo', options); + + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.decode, + buffer: arrayBuffer, + options + }, [arrayBuffer.buffer]); + } + + decodeAudio(arrayBuffer, ts) { + if (this.player._opt.useWCS) { + this._decodeAudioNoDelay(arrayBuffer, ts); + } else if (this.player._opt.useMSE) { + this._decodeAudioNoDelay(arrayBuffer, ts); + } else { + this._decodeAudio(arrayBuffer, ts); + } + } // + + + _decodeAudio(arrayBuffer, ts) { + const options = { + type: MEDIA_TYPE.audio, + ts: Math.max(ts, 0) + }; // this.player.debug.log('decoderWorker', 'decodeAudio',options); + + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.decode, + buffer: arrayBuffer, + options + }, [arrayBuffer.buffer]); + } + + _decodeAudioNoDelay(arrayBuffer, ts) { + // console.log('_decodeAudioNoDelay', arrayBuffer); + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.audioDecode, + buffer: arrayBuffer, + ts: Math.max(ts, 0) + }, [arrayBuffer.buffer]); + } + + updateWorkConfig(config) { + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.updateConfig, + key: config.key, + value: config.value + }); + } + + } + + class CommonLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.stopId = null; + this.firstTimestamp = null; + this.startTimestamp = null; + this.delay = -1; + this.bufferList = []; + this.dropping = false; + this.initInterval(); + } + + destroy() { + if (this.stopId) { + clearInterval(this.stopId); + this.stopId = null; + } + + this.firstTimestamp = null; + this.startTimestamp = null; + this.delay = -1; + this.bufferList = []; + this.dropping = false; + this.off(); + this.player.debug.log('CommonDemux', 'destroy'); + } + + getDelay(timestamp) { + if (!timestamp) { + return -1; + } + + if (!this.firstTimestamp) { + this.firstTimestamp = timestamp; + this.startTimestamp = Date.now(); + this.delay = -1; + } else { + if (timestamp) { + const localTimestamp = Date.now() - this.startTimestamp; + const timeTimestamp = timestamp - this.firstTimestamp; + + if (localTimestamp >= timeTimestamp) { + this.delay = localTimestamp - timeTimestamp; + } else { + this.delay = timeTimestamp - localTimestamp; + } + } + } + + return this.delay; + } + + resetDelay() { + this.firstTimestamp = null; + this.startTimestamp = null; + this.delay = -1; + this.dropping = false; + } // + + + initInterval() { + this.player.debug.log('common dumex', `init Interval`); + + let _loop = () => { + let data; + const videoBuffer = this.player._opt.videoBuffer; + const videoBufferDelay = this.player._opt.videoBufferDelay; + + if (this.player._opt.useMSE && this.player.mseDecoder && this.player.mseDecoder.getSourceBufferUpdating()) { + this.player.debug.warn('CommonDemux', `_loop getSourceBufferUpdating is true and bufferList length is ${this.bufferList.length}`); + return; + } + + if (this.bufferList.length) { + if (this.dropping) { + // this.player.debug.log('common dumex', `is dropping`); + data = this.bufferList.shift(); + + if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) { + this._doDecoderDecode(data); + } + + while (!data.isIFrame && this.bufferList.length) { + data = this.bufferList.shift(); + + if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) { + this._doDecoderDecode(data); + } + } // i frame + + + if (data.isIFrame && this.getDelay(data.ts) <= Math.min(videoBuffer, 200)) { + this.dropping = false; + + this._doDecoderDecode(data); + } + } else { + data = this.bufferList[0]; + + if (this.getDelay(data.ts) === -1) { + // this.player.debug.log('common dumex', `delay is -1`); + this.bufferList.shift(); + + this._doDecoderDecode(data); + } else if (this.delay > videoBuffer + videoBufferDelay) { + // this.player.debug.log('common dumex', `delay is ${this.delay}, set dropping is true`); + this.resetDelay(); + this.dropping = true; + } else { + data = this.bufferList[0]; + + if (this.getDelay(data.ts) > videoBuffer) { + // drop frame + this.bufferList.shift(); + + this._doDecoderDecode(data); + } + } + } + } + }; + + _loop(); + + this.stopId = setInterval(_loop, 10); + } + + _doDecode(payload, type, ts, isIFrame, cts) { + const player = this.player; + let options = { + ts: ts, + cts: cts, + type: type, + isIFrame: false + }; // use offscreen + + if (player._opt.useWCS && !player._opt.useOffscreen) { + if (type === MEDIA_TYPE.video) { + options.isIFrame = isIFrame; + } + + this.pushBuffer(payload, options); + } else if (player._opt.useMSE) { + // use mse + if (type === MEDIA_TYPE.video) { + options.isIFrame = isIFrame; + } + + this.pushBuffer(payload, options); + } else { + // + if (type === MEDIA_TYPE.video) { + player.decoderWorker && player.decoderWorker.decodeVideo(payload, ts, isIFrame); + } else if (type === MEDIA_TYPE.audio) { + if (player._opt.hasAudio) { + player.decoderWorker && player.decoderWorker.decodeAudio(payload, ts); + } + } + } + } + + _doDecoderDecode(data) { + const player = this.player; + const { + webcodecsDecoder, + mseDecoder + } = player; + + if (data.type === MEDIA_TYPE.audio) { + if (player._opt.hasAudio) { + player.decoderWorker && player.decoderWorker.decodeAudio(data.payload, data.ts); + } + } else if (data.type === MEDIA_TYPE.video) { + if (player._opt.useWCS && !player._opt.useOffscreen) { + webcodecsDecoder.decodeVideo(data.payload, data.ts, data.isIFrame); + } else if (player._opt.useMSE) { + mseDecoder.decodeVideo(data.payload, data.ts, data.isIFrame, data.cts); + } + } + } + + pushBuffer(payload, options) { + // 音频 + if (options.type === MEDIA_TYPE.audio) { + this.bufferList.push({ + ts: options.ts, + payload: payload, + type: MEDIA_TYPE.audio + }); + } else if (options.type === MEDIA_TYPE.video) { + this.bufferList.push({ + ts: options.ts, + cts: options.cts, + payload: payload, + type: MEDIA_TYPE.video, + isIFrame: options.isIFrame + }); + } + } + + close() { } + + } + + class FlvLoader extends CommonLoader { + constructor(player) { + super(player); + this.input = this._inputFlv(); + this.flvDemux = this.dispatchFlvData(this.input); + player.debug.log('FlvDemux', 'init'); + } + + destroy() { + super.destroy(); + this.input = null; + this.flvDemux = null; + this.player.debug.log('FlvDemux', 'destroy'); + } + + dispatch(data) { + this.flvDemux(data); + } + + *_inputFlv() { + yield 9; + const tmp = new ArrayBuffer(4); + const tmp8 = new Uint8Array(tmp); + const tmp32 = new Uint32Array(tmp); + const player = this.player; + + while (true) { + tmp8[3] = 0; + const t = yield 15; + const type = t[4]; + tmp8[0] = t[7]; + tmp8[1] = t[6]; + tmp8[2] = t[5]; + const length = tmp32[0]; + tmp8[0] = t[10]; + tmp8[1] = t[9]; + tmp8[2] = t[8]; + let ts = tmp32[0]; + + if (ts === 0xFFFFFF) { + tmp8[3] = t[11]; + ts = tmp32[0]; + } + + const payload = yield length; + + switch (type) { + case FLV_MEDIA_TYPE.audio: + if (player._opt.hasAudio) { + player.updateStats({ + abps: payload.byteLength + }); + + if (payload.byteLength > 0) { + this._doDecode(payload, MEDIA_TYPE.audio, ts); + } + } + + break; + + case FLV_MEDIA_TYPE.video: + if (!player._times.demuxStart) { + player._times.demuxStart = now(); + } + + if (player._opt.hasVideo) { + player.updateStats({ + vbps: payload.byteLength + }); + const isIFrame = payload[0] >> 4 === 1; + + if (payload.byteLength > 0) { + tmp32[0] = payload[4]; + tmp32[1] = payload[3]; + tmp32[2] = payload[2]; + tmp32[3] = 0; + let cts = tmp32[0]; + + this._doDecode(payload, MEDIA_TYPE.video, ts, isIFrame, cts); + } + } + + break; + } + } + } + + dispatchFlvData(input) { + let need = input.next(); + let buffer = null; + return value => { + let data = new Uint8Array(value); + + if (buffer) { + let combine = new Uint8Array(buffer.length + data.length); + combine.set(buffer); + combine.set(data, buffer.length); + data = combine; + buffer = null; + } + + while (data.length >= need.value) { + let remain = data.slice(need.value); + need = input.next(data.slice(0, need.value)); + data = remain; + } + + if (data.length > 0) { + buffer = data; + } + }; + } + + close() { + this.input && this.input.return(null); + } + + } + + class M7sLoader extends CommonLoader { + constructor(player) { + super(player); + player.debug.log('M7sDemux', 'init'); + } + + destroy() { + super.destroy(); + this.player.debug.log('M7sDemux', 'destroy'); + this.player = null; + } + + dispatch(data) { + const player = this.player; + const dv = new DataView(data); + const type = dv.getUint8(0); + const ts = dv.getUint32(1, false); + + switch (type) { + case MEDIA_TYPE.audio: + if (player._opt.hasAudio) { + const payload = new Uint8Array(data, 5); + player.updateStats({ + abps: payload.byteLength + }); + + if (payload.byteLength > 0) { + this._doDecode(payload, type, ts); + } + } + + break; + + case MEDIA_TYPE.video: + if (player._opt.hasVideo) { + if (!player._times.demuxStart) { + player._times.demuxStart = now(); + } + + if (dv.byteLength > 5) { + const payload = new Uint8Array(data, 5); + const isIframe = dv.getUint8(5) >> 4 === 1; + player.updateStats({ + vbps: payload.byteLength + }); + + if (payload.byteLength > 0) { + this._doDecode(payload, type, ts, isIframe); + } + } else { + this.player.debug.warn('M7sDemux', 'dispatch', 'dv byteLength is', dv.byteLength); + } + } + + break; + } + } + + } + + class Demux { + constructor(player) { + const Loader = Demux.getLoaderFactory(player._opt.demuxType); + return new Loader(player); + } + + static getLoaderFactory(type) { + if (type === DEMUX_TYPE.m7s) { + return M7sLoader; + } else if (type === DEMUX_TYPE.flv) { + return FlvLoader; + } + } + + } + + /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // Exponential-Golomb buffer decoder + class ExpGolomb { + constructor(uint8array) { + this.TAG = 'ExpGolomb'; + this._buffer = uint8array; + this._buffer_index = 0; + this._total_bytes = uint8array.byteLength; + this._total_bits = uint8array.byteLength * 8; + this._current_word = 0; + this._current_word_bits_left = 0; + } + + destroy() { + this._buffer = null; + } + + _fillCurrentWord() { + let buffer_bytes_left = this._total_bytes - this._buffer_index; + + let bytes_read = Math.min(4, buffer_bytes_left); + let word = new Uint8Array(4); + word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read)); + this._current_word = new DataView(word.buffer).getUint32(0, false); + this._buffer_index += bytes_read; + this._current_word_bits_left = bytes_read * 8; + } + + readBits(bits) { + + if (bits <= this._current_word_bits_left) { + let result = this._current_word >>> 32 - bits; + this._current_word <<= bits; + this._current_word_bits_left -= bits; + return result; + } + + let result = this._current_word_bits_left ? this._current_word : 0; + result = result >>> 32 - this._current_word_bits_left; + let bits_need_left = bits - this._current_word_bits_left; + + this._fillCurrentWord(); + + let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left); + let result2 = this._current_word >>> 32 - bits_read_next; + this._current_word <<= bits_read_next; + this._current_word_bits_left -= bits_read_next; + result = result << bits_read_next | result2; + return result; + } + + readBool() { + return this.readBits(1) === 1; + } + + readByte() { + return this.readBits(8); + } + + _skipLeadingZero() { + let zero_count; + + for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) { + if (0 !== (this._current_word & 0x80000000 >>> zero_count)) { + this._current_word <<= zero_count; + this._current_word_bits_left -= zero_count; + return zero_count; + } + } + + this._fillCurrentWord(); + + return zero_count + this._skipLeadingZero(); + } + + readUEG() { + // unsigned exponential golomb + let leading_zeros = this._skipLeadingZero(); + + return this.readBits(leading_zeros + 1) - 1; + } + + readSEG() { + // signed exponential golomb + let value = this.readUEG(); + + if (value & 0x01) { + return value + 1 >>> 1; + } else { + return -1 * (value >>> 1); + } + } + + } + + /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + class SPSParser { + static _ebsp2rbsp(uint8array) { + let src = uint8array; + let src_length = src.byteLength; + let dst = new Uint8Array(src_length); + let dst_idx = 0; + + for (let i = 0; i < src_length; i++) { + if (i >= 2) { + // Unescape: Skip 0x03 after 00 00 + if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) { + continue; + } + } + + dst[dst_idx] = src[i]; + dst_idx++; + } + + return new Uint8Array(dst.buffer, 0, dst_idx); + } // 解析 SPS + // https://zhuanlan.zhihu.com/p/27896239 + + + static parseSPS(uint8array) { + let rbsp = SPSParser._ebsp2rbsp(uint8array); + + let gb = new ExpGolomb(rbsp); + gb.readByte(); // 标识当前H.264码流的profile。 + // 我们知道,H.264中定义了三种常用的档次profile: 基准档次:baseline profile;主要档次:main profile; 扩展档次:extended profile; + + let profile_idc = gb.readByte(); // profile_idc + + gb.readByte(); // constraint_set_flags[5] + reserved_zero[3] + // 标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。 + + let level_idc = gb.readByte(); // level_idc + // 表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。 + + gb.readUEG(); // seq_parameter_set_id + + let profile_string = SPSParser.getProfileString(profile_idc); + let level_string = SPSParser.getLevelString(level_idc); + let chroma_format_idc = 1; + let chroma_format = 420; + let chroma_format_table = [0, 420, 422, 444]; + let bit_depth = 8; // + + if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 || profile_idc === 244 || profile_idc === 44 || profile_idc === 83 || profile_idc === 86 || profile_idc === 118 || profile_idc === 128 || profile_idc === 138 || profile_idc === 144) { + // + chroma_format_idc = gb.readUEG(); + + if (chroma_format_idc === 3) { + gb.readBits(1); // separate_colour_plane_flag + } + + if (chroma_format_idc <= 3) { + chroma_format = chroma_format_table[chroma_format_idc]; + } + + bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8 + + gb.readUEG(); // bit_depth_chroma_minus8 + + gb.readBits(1); // qpprime_y_zero_transform_bypass_flag + + if (gb.readBool()) { + // seq_scaling_matrix_present_flag + let scaling_list_count = chroma_format_idc !== 3 ? 8 : 12; + + for (let i = 0; i < scaling_list_count; i++) { + if (gb.readBool()) { + // seq_scaling_list_present_flag + if (i < 6) { + SPSParser._skipScalingList(gb, 16); + } else { + SPSParser._skipScalingList(gb, 64); + } + } + } + } + } // 用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 + + + + gb.readUEG(); // log2_max_frame_num_minus4 + // 表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。 + + let pic_order_cnt_type = gb.readUEG(); + + if (pic_order_cnt_type === 0) { + gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4 + } else if (pic_order_cnt_type === 1) { + gb.readBits(1); // delta_pic_order_always_zero_flag + + gb.readSEG(); // offset_for_non_ref_pic + + gb.readSEG(); // offset_for_top_to_bottom_field + + let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG(); + + for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) { + gb.readSEG(); // offset_for_ref_frame + } + } // 用于表示参考帧的最大数目。 + + + let ref_frames = gb.readUEG(); // max_num_ref_frames + // 标识位,说明frame_num中是否允许不连续的值。 + + gb.readBits(1); // gaps_in_frame_num_value_allowed_flag + // 用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为: + + let pic_width_in_mbs_minus1 = gb.readUEG(); // 使用PicHeightInMapUnits来度量视频中一帧图像的高度。 + // PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为: + + let pic_height_in_map_units_minus1 = gb.readUEG(); // 标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码; + // 该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同, + // 为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。 + + let frame_mbs_only_flag = gb.readBits(1); + + if (frame_mbs_only_flag === 0) { + // 标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。 + gb.readBits(1); // mb_adaptive_frame_field_flag + } // 标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。 + + + gb.readBits(1); // direct_8x8_inference_flag + + let frame_crop_left_offset = 0; + let frame_crop_right_offset = 0; + let frame_crop_top_offset = 0; + let frame_crop_bottom_offset = 0; + let frame_cropping_flag = gb.readBool(); + + if (frame_cropping_flag) { + frame_crop_left_offset = gb.readUEG(); + frame_crop_right_offset = gb.readUEG(); + frame_crop_top_offset = gb.readUEG(); + frame_crop_bottom_offset = gb.readUEG(); + } + + let sar_width = 1, + sar_height = 1; + let fps = 0, + fps_fixed = true, + fps_num = 0, + fps_den = 0; // 标识位,说明SPS中是否存在VUI信息。 + + let vui_parameters_present_flag = gb.readBool(); + + if (vui_parameters_present_flag) { + if (gb.readBool()) { + // aspect_ratio_info_present_flag + let aspect_ratio_idc = gb.readByte(); + let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]; + let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]; + + if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) { + sar_width = sar_w_table[aspect_ratio_idc - 1]; + sar_height = sar_h_table[aspect_ratio_idc - 1]; + } else if (aspect_ratio_idc === 255) { + sar_width = gb.readByte() << 8 | gb.readByte(); + sar_height = gb.readByte() << 8 | gb.readByte(); + } + } + + if (gb.readBool()) { + // overscan_info_present_flag + gb.readBool(); // overscan_appropriate_flag + } + + if (gb.readBool()) { + // video_signal_type_present_flag + gb.readBits(4); // video_format & video_full_range_flag + + if (gb.readBool()) { + // colour_description_present_flag + gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients + } + } + + if (gb.readBool()) { + // chroma_loc_info_present_flag + gb.readUEG(); // chroma_sample_loc_type_top_field + + gb.readUEG(); // chroma_sample_loc_type_bottom_field + } + + if (gb.readBool()) { + // timing_info_present_flag + let num_units_in_tick = gb.readBits(32); + let time_scale = gb.readBits(32); + fps_fixed = gb.readBool(); // fixed_frame_rate_flag + + fps_num = time_scale; + fps_den = num_units_in_tick * 2; + fps = fps_num / fps_den; + } + } + + let sarScale = 1; + + if (sar_width !== 1 || sar_height !== 1) { + sarScale = sar_width / sar_height; + } + + let crop_unit_x = 0, + crop_unit_y = 0; + + if (chroma_format_idc === 0) { + crop_unit_x = 1; + crop_unit_y = 2 - frame_mbs_only_flag; + } else { + let sub_wc = chroma_format_idc === 3 ? 1 : 2; + let sub_hc = chroma_format_idc === 1 ? 2 : 1; + crop_unit_x = sub_wc; + crop_unit_y = sub_hc * (2 - frame_mbs_only_flag); + } + + let codec_width = (pic_width_in_mbs_minus1 + 1) * 16; + let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16); + codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x; + codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y; + let present_width = Math.ceil(codec_width * sarScale); + gb.destroy(); + gb = null; // 解析出来的SPS 内容。 + + return { + profile_string: profile_string, + // baseline, high, high10, ... + level_string: level_string, + // 3, 3.1, 4, 4.1, 5, 5.1, ... + bit_depth: bit_depth, + // 8bit, 10bit, ... + ref_frames: ref_frames, + chroma_format: chroma_format, + // 4:2:0, 4:2:2, ... + chroma_format_string: SPSParser.getChromaFormatString(chroma_format), + frame_rate: { + fixed: fps_fixed, + fps: fps, + fps_den: fps_den, + fps_num: fps_num + }, + sar_ratio: { + width: sar_width, + height: sar_height + }, + codec_size: { + width: codec_width, + height: codec_height + }, + present_size: { + width: present_width, + height: codec_height + } + }; + } + + static _skipScalingList(gb, count) { + let last_scale = 8, + next_scale = 8; + let delta_scale = 0; + + for (let i = 0; i < count; i++) { + if (next_scale !== 0) { + delta_scale = gb.readSEG(); + next_scale = (last_scale + delta_scale + 256) % 256; + } + + last_scale = next_scale === 0 ? last_scale : next_scale; + } + } // profile_idc = 66 → baseline profile; + // profile_idc = 77 → main profile; + // profile_idc = 88 → extended profile; + // 在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High + // 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra + + + static getProfileString(profile_idc) { + switch (profile_idc) { + case 66: + return 'Baseline'; + + case 77: + return 'Main'; + + case 88: + return 'Extended'; + + case 100: + return 'High'; + + case 110: + return 'High10'; + + case 122: + return 'High422'; + + case 244: + return 'High444'; + + default: + return 'Unknown'; + } + } + + static getLevelString(level_idc) { + return (level_idc / 10).toFixed(1); + } + + static getChromaFormatString(chroma) { + switch (chroma) { + case 420: + return '4:2:0'; + + case 422: + return '4:2:2'; + + case 444: + return '4:4:4'; + + default: + return 'Unknown'; + } + } + + } + + function parseAVCDecoderConfigurationRecord(arrayBuffer) { + const meta = {}; + const v = new DataView(arrayBuffer.buffer); + let version = v.getUint8(0); // configurationVersion + + let avcProfile = v.getUint8(1); // avcProfileIndication + + v.getUint8(2); // profile_compatibil + + v.getUint8(3); // AVCLevelIndication + + if (version !== 1 || avcProfile === 0) { + // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord'); + return meta; + } + + const _naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne + + + if (_naluLengthSize !== 3 && _naluLengthSize !== 4) { + // holy shit!!! + // this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Strange NaluLengthSizeMinusOne: ${_naluLengthSize - 1}`); + return meta; + } + + let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets + + if (spsCount === 0) { + // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS'); + return; + } + + let offset = 6; + + for (let i = 0; i < spsCount; i++) { + let len = v.getUint16(offset, false); // sequenceParameterSetLength + + offset += 2; + + if (len === 0) { + continue; + } // Notice: Nalu without startcode header (00 00 00 01) + + + let sps = new Uint8Array(arrayBuffer.buffer, offset, len); + offset += len; // flv.js作者选择了自己来解析这个数据结构,也是迫不得已,因为JS环境下没有ffmpeg,解析这个结构主要是为了提取 sps和pps。虽然理论上sps允许有多个,但其实一般就一个。 + // packetTtype 为 1 表示 NALU,NALU= network abstract layer unit,这是H.264的概念,网络抽象层数据单元,其实简单理解就是一帧视频数据。 + // pps的信息没什么用,所以作者只实现了sps的分析器,说明作者下了很大功夫去学习264的标准,其中的Golomb解码还是挺复杂的,能解对不容易,我在PC和手机平台都是用ffmpeg去解析的。 + // SPS里面包括了视频分辨率,帧率,profile level等视频重要信息。 + + let config = SPSParser.parseSPS(sps); + + if (i !== 0) { + // ignore other sps's config + continue; + } + + meta.codecWidth = config.codec_size.width; + meta.codecHeight = config.codec_size.height; + meta.presentWidth = config.present_size.width; + meta.presentHeight = config.present_size.height; + meta.profile = config.profile_string; + meta.level = config.level_string; + meta.bitDepth = config.bit_depth; + meta.chromaFormat = config.chroma_format; + meta.sarRatio = config.sar_ratio; + meta.frameRate = config.frame_rate; + + if (config.frame_rate.fixed === false || config.frame_rate.fps_num === 0 || config.frame_rate.fps_den === 0) { + meta.frameRate = {}; + } + + let fps_den = meta.frameRate.fps_den; + let fps_num = meta.frameRate.fps_num; + meta.refSampleDuration = meta.timescale * (fps_den / fps_num); + let codecArray = sps.subarray(1, 4); + let codecString = 'avc1.'; + + for (let j = 0; j < 3; j++) { + let h = codecArray[j].toString(16); + + if (h.length < 2) { + h = '0' + h; + } + + codecString += h; + } // codec + + + meta.codec = codecString; + } + + let ppsCount = v.getUint8(offset); // numOfPictureParameterSets + + if (ppsCount === 0) { + // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS'); + return meta; + } + + offset++; + + for (let i = 0; i < ppsCount; i++) { + let len = v.getUint16(offset, false); // pictureParameterSetLength + + offset += 2; + + if (len === 0) { + continue; + } + + new Uint8Array(arrayBuffer.buffer, offset, len); // pps is useless for extracting video information + + offset += len; + } + + meta.videoType = 'avc'; // meta.avcc = arrayBuffer; + + return meta; + } + + class WebcodecsDecoder extends Emitter { + constructor(player) { + super(); + this.player = player; + this.hasInit = false; + this.isDecodeFirstIIframe = false; + this.isInitInfo = false; + this.decoder = null; + this.initDecoder(); + player.debug.log('Webcodecs', 'init'); + } + + destroy() { + if (this.decoder) { + if (this.decoder.state !== 'closed') { + this.decoder.close(); + } + + this.decoder = null; + } + + this.hasInit = false; + this.isInitInfo = false; + this.isDecodeFirstIIframe = false; + this.off(); + this.player.debug.log('Webcodecs', 'destroy'); + } + + initDecoder() { + const _this = this; + + this.decoder = new VideoDecoder({ + output(videoFrame) { + _this.handleDecode(videoFrame); + }, + + error(error) { + _this.handleError(error); + } + + }); + } + + handleDecode(videoFrame) { + if (!this.isInitInfo) { + this.player.video.updateVideoInfo({ + width: videoFrame.codedWidth, + height: videoFrame.codedHeight + }); + this.player.video.initCanvasViewSize(); + this.isInitInfo = true; + } + + if (!this.player._times.videoStart) { + this.player._times.videoStart = now(); + this.player.handlePlayToRenderTimes(); + } + + this.player.handleRender(); + this.player.video.render({ + videoFrame + }); + this.player.updateStats({ + fps: true, + ts: 0, + buf: this.player.demux.delay + }); + } + + handleError(error) { + this.player.debug.error('Webcodecs', 'VideoDecoder handleError', error); + } + + decodeVideo(payload, ts, isIframe) { + // this.player.debug.log('Webcodecs decoder', 'decodeVideo', ts, isIframe); + if (!this.hasInit) { + if (isIframe && payload[1] === 0) { + const videoCodec = payload[0] & 0x0F; + this.player.video.updateVideoInfo({ + encTypeCode: videoCodec + }); // 如果解码出来的是 + + if (videoCodec === VIDEO_ENC_CODE.h265) { + this.emit(EVENTS_ERROR.webcodecsH265NotSupport); + return; + } + + if (!this.player._times.decodeStart) { + this.player._times.decodeStart = now(); + } + + const config = formatVideoDecoderConfigure(payload.slice(5)); + this.decoder.configure(config); + this.hasInit = true; + } + } else { + // check width or height change + if (isIframe && payload[1] === 0) { + let data = payload.slice(5); + const config = parseAVCDecoderConfigurationRecord(data); + const videoInfo = this.player.video.videoInfo; + + if (config.codecWidth !== videoInfo.width || config.codecHeight !== videoInfo.height) { + this.player.debug.log('Webcodecs', `width or height is update, width ${videoInfo.width}-> ${config.codecWidth}, height ${videoInfo.height}-> ${config.codecHeight}`); + this.player.emit(EVENTS_ERROR.webcodecsWidthOrHeightChange); + return; + } + } // fix : Uncaught DOMException: Failed to execute 'decode' on 'VideoDecoder': A key frame is required after configure() or flush(). + + + if (!this.isDecodeFirstIIframe && isIframe) { + this.isDecodeFirstIIframe = true; + } + + if (this.isDecodeFirstIIframe) { + const chunk = new EncodedVideoChunk({ + data: payload.slice(5), + timestamp: ts, + type: isIframe ? ENCODED_VIDEO_TYPE.key : ENCODED_VIDEO_TYPE.delta + }); + this.player.emit(EVENTS.timeUpdate, ts); + + try { + if (this.isDecodeStateClosed()) { + this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeStateClosed true'); + return; + } + + this.decoder.decode(chunk); + } catch (e) { + this.player.debug.error('Webcodecs', 'VideoDecoder', e); + + if (e.toString().indexOf(WCS_ERROR.keyframeIsRequiredError) !== -1) { + this.player.emitError(EVENTS_ERROR.webcodecsDecodeError); + } else if (e.toString().indexOf(WCS_ERROR.canNotDecodeClosedCodec) !== -1) { + this.player.emitError(EVENTS_ERROR.webcodecsDecodeError); + } + } + } else { + this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeFirstIIframe false'); + } + } + } + + isDecodeStateClosed() { + return this.decoder.state === 'closed'; + } + + } + + const iconsMap = { + play: '播放', + pause: '暂停', + audio: '', + mute: '', + screenshot: '截图', + loading: '加载', + fullscreen: '全屏', + fullscreenExit: '退出全屏', + record: '录制', + recordStop: '停止录制' + }; + var icons = Object.keys(iconsMap).reduce((icons, key) => { + icons[key] = ` + + ${iconsMap[key] ? `${iconsMap[key]}` : ''} +`; + return icons; + }, {}); + + var template = ((player, control) => { + if (player._opt.hasControl && player._opt.controlAutoHide) { + player.$container.classList.add('jessibuca-controls-show-auto-hide'); + } else { + player.$container.classList.add('jessibuca-controls-show'); + } + + const options = player._opt; + const operateBtns = options.operateBtns; + player.$container.insertAdjacentHTML('beforeend', ` + ${options.background ? `
` : ''} +
+ ${icons.loading} + ${options.loadingText ? `
${options.loadingText}
` : ''} +
+ ${options.hasControl && operateBtns.play ? `
` : ''} + ${options.hasControl ? ` +
+
+
00:00:01
+
${icons.recordStop}
+
+ ` : ''} + ${options.hasControl ? ` +
+
+
+ ${options.showBandwidth ? `
` : ''} +
+
+ ${operateBtns.audio ? ` +
+ ${icons.audio} + ${icons.mute} +
+
+
+
+
+
+
+ ` : ''} + ${operateBtns.play ? `
${icons.play}
${icons.pause}
` : ''} + ${operateBtns.screenshot ? `
${icons.screenshot}
` : ''} + ${operateBtns.record ? `
${icons.record}
${icons.recordStop}
` : ''} + ${operateBtns.fullscreen ? `
${icons.fullscreen}
${icons.fullscreenExit}
` : ''} +
+
+
+ ` : ''} + + `); + Object.defineProperty(control, '$poster', { + value: player.$container.querySelector('.jessibuca-poster') + }); + Object.defineProperty(control, '$loading', { + value: player.$container.querySelector('.jessibuca-loading') + }); + Object.defineProperty(control, '$play', { + value: player.$container.querySelector('.jessibuca-play') + }); + Object.defineProperty(control, '$playBig', { + value: player.$container.querySelector('.jessibuca-play-big') + }); + Object.defineProperty(control, '$recording', { + value: player.$container.querySelector('.jessibuca-recording') + }); + Object.defineProperty(control, '$recordingTime', { + value: player.$container.querySelector('.jessibuca-recording-time') + }); + Object.defineProperty(control, '$recordingStop', { + value: player.$container.querySelector('.jessibuca-recording-stop') + }); + Object.defineProperty(control, '$pause', { + value: player.$container.querySelector('.jessibuca-pause') + }); + Object.defineProperty(control, '$controls', { + value: player.$container.querySelector('.jessibuca-controls') + }); + Object.defineProperty(control, '$fullscreen', { + value: player.$container.querySelector('.jessibuca-fullscreen') + }); + Object.defineProperty(control, '$fullscreen', { + value: player.$container.querySelector('.jessibuca-fullscreen') + }); + Object.defineProperty(control, '$volume', { + value: player.$container.querySelector('.jessibuca-volume') + }); + Object.defineProperty(control, '$volumePanelWrap', { + value: player.$container.querySelector('.jessibuca-volume-panel-wrap') + }); + Object.defineProperty(control, '$volumePanelText', { + value: player.$container.querySelector('.jessibuca-volume-panel-text') + }); + Object.defineProperty(control, '$volumePanel', { + value: player.$container.querySelector('.jessibuca-volume-panel') + }); + Object.defineProperty(control, '$volumeHandle', { + value: player.$container.querySelector('.jessibuca-volume-panel-handle') + }); + Object.defineProperty(control, '$volumeOn', { + value: player.$container.querySelector('.jessibuca-icon-audio') + }); + Object.defineProperty(control, '$volumeOff', { + value: player.$container.querySelector('.jessibuca-icon-mute') + }); + Object.defineProperty(control, '$fullscreen', { + value: player.$container.querySelector('.jessibuca-fullscreen') + }); + Object.defineProperty(control, '$fullscreenExit', { + value: player.$container.querySelector('.jessibuca-fullscreen-exit') + }); + Object.defineProperty(control, '$record', { + value: player.$container.querySelector('.jessibuca-record') + }); + Object.defineProperty(control, '$recordStop', { + value: player.$container.querySelector('.jessibuca-record-stop') + }); + Object.defineProperty(control, '$screenshot', { + value: player.$container.querySelector('.jessibuca-screenshot') + }); + Object.defineProperty(control, '$speed', { + value: player.$container.querySelector('.jessibuca-speed') + }); + }); + + var observer$1 = ((player, control) => { + const { + events: { + proxy + } + } = player; + const object = document.createElement('object'); + object.setAttribute('aria-hidden', 'true'); + object.setAttribute('tabindex', -1); + object.type = 'text/html'; + object.data = 'about:blank'; + setStyle(object, { + display: 'block', + position: 'absolute', + top: '0', + left: '0', + height: '100%', + width: '100%', + overflow: 'hidden', + pointerEvents: 'none', + zIndex: '-1' + }); + let playerWidth = player.width; + let playerHeight = player.height; + proxy(object, 'load', () => { + proxy(object.contentDocument.defaultView, 'resize', () => { + if (player.width !== playerWidth || player.height !== playerHeight) { + playerWidth = player.width; + playerHeight = player.height; + player.emit(EVENTS.resize); + screenfullH5Control(); + } + }); + }); + player.$container.appendChild(object); + player.on(EVENTS.destroy, () => { + player.$container.removeChild(object); + }); + + function setVolumeHandle(percentage) { + if (percentage === 0) { + setStyle(control.$volumeOn, 'display', 'none'); + setStyle(control.$volumeOff, 'display', 'flex'); + setStyle(control.$volumeHandle, 'top', `${48}px`); + } else { + if (control.$volumeHandle && control.$volumePanel) { + const panelHeight = getStyle(control.$volumePanel, 'height') || 60; + const handleHeight = getStyle(control.$volumeHandle, 'height'); + const top = panelHeight - (panelHeight - handleHeight) * percentage - handleHeight; + setStyle(control.$volumeHandle, 'top', `${top}px`); + setStyle(control.$volumeOn, 'display', 'flex'); + setStyle(control.$volumeOff, 'display', 'none'); + } + } + + control.$volumePanelText && (control.$volumePanelText.innerHTML = parseInt(percentage * 100)); + } + + player.on(EVENTS.volumechange, () => { + setVolumeHandle(player.volume); + }); + player.on(EVENTS.loading, flag => { + setStyle(control.$loading, 'display', flag ? 'flex' : 'none'); + setStyle(control.$poster, 'display', 'none'); + + if (flag) { + setStyle(control.$playBig, 'display', 'none'); + } + }); + + const screenfullChange = fullscreen => { + let isFullScreen = isBoolean(fullscreen) ? fullscreen : player.fullscreen; + setStyle(control.$fullscreenExit, 'display', isFullScreen ? 'flex' : 'none'); + setStyle(control.$fullscreen, 'display', isFullScreen ? 'none' : 'flex'); // control.autoSize(); + }; + + const screenfullH5Control = () => { + if (isMobile() && control.$controls && player._opt.useWebFullScreen) { + setTimeout(() => { + if (player.fullscreen) { + // console.log(player.width, player.height); + let translateX = player.height / 2 - player.width + CONTROL_HEIGHT / 2; + let translateY = player.height / 2 - CONTROL_HEIGHT / 2; + control.$controls.style.transform = `translateX(${-translateX}px) translateY(-${translateY}px) rotate(-90deg)`; + } else { + control.$controls.style.transform = `translateX(0) translateY(0) rotate(0)`; + } + }, 10); + } + }; + + try { + screenfull.on('change', screenfullChange); + player.events.destroys.push(() => { + screenfull.off('change', screenfullChange); + }); + } catch (error) {// + } // + + + player.on(EVENTS.webFullscreen, value => { + screenfullChange(value); + screenfullH5Control(); + }); + player.on(EVENTS.recording, () => { + setStyle(control.$record, 'display', player.recording ? 'none' : 'flex'); + setStyle(control.$recordStop, 'display', player.recording ? 'flex' : 'none'); + setStyle(control.$recording, 'display', player.recording ? 'flex' : 'none'); + }); // + + player.on(EVENTS.recordingTimestamp, timestamp => { + // console.log(timestamp); + control.$recordingTime && (control.$recordingTime.innerHTML = formatTimeTips(timestamp)); + }); + player.on(EVENTS.playing, flag => { + setStyle(control.$play, 'display', flag ? 'none' : 'flex'); + setStyle(control.$playBig, 'display', flag ? 'none' : 'block'); + setStyle(control.$pause, 'display', flag ? 'flex' : 'none'); + setStyle(control.$screenshot, 'display', flag ? 'flex' : 'none'); + setStyle(control.$record, 'display', flag ? 'flex' : 'none'); + setStyle(control.$qualityMenu, 'display', flag ? 'flex' : 'none'); + setStyle(control.$volume, 'display', flag ? 'flex' : 'none'); // setStyle(control.$fullscreen, 'display', flag ? 'flex' : 'none'); + + screenfullChange(); // 不在播放 + + if (!flag) { + control.$speed && (control.$speed.innerHTML = bpsSize('')); + } + }); + player.on(EVENTS.kBps, rate => { + const bps = bpsSize(rate); + control.$speed && (control.$speed.innerHTML = bps); + }); + }); + + var property = ((player, control) => { + Object.defineProperty(control, 'controlsRect', { + get: () => { + return control.$controls.getBoundingClientRect(); + } + }); + }); + + var events = ((player, control) => { + const { + events: { + proxy + }, + debug + } = player; + + function volumeChangeFromEvent(event) { + const { + bottom: panelBottom, + height: panelHeight + } = control.$volumePanel.getBoundingClientRect(); + const { + height: handleHeight + } = control.$volumeHandle.getBoundingClientRect(); + let moveLen = event.y; // if (isMobile() && player.fullscreen) { + // moveLen = event.x; + // } + + const percentage = clamp(panelBottom - moveLen - handleHeight / 2, 0, panelHeight - handleHeight / 2) / (panelHeight - handleHeight); + return percentage; + } // + + + proxy(window, ['click', 'contextmenu'], event => { + if (event.composedPath().indexOf(player.$container) > -1) { + control.isFocus = true; + } else { + control.isFocus = false; + } + }); // + + proxy(window, 'orientationchange', () => { + setTimeout(() => { + player.resize(); + }, 300); + }); + proxy(control.$controls, 'click', e => { + e.stopPropagation(); + }); + proxy(control.$pause, 'click', e => { + player.pause(); + }); // 监听 play 方法 + + proxy(control.$play, 'click', e => { + player.play(); + player.resumeAudioAfterPause(); + }); // 监听 play 方法 + + proxy(control.$playBig, 'click', e => { + player.play(); + player.resumeAudioAfterPause(); + }); + proxy(control.$volume, 'mouseover', () => { + control.$volumePanelWrap.classList.add('jessibuca-volume-panel-wrap-show'); + }); + proxy(control.$volume, 'mouseout', () => { + control.$volumePanelWrap.classList.remove('jessibuca-volume-panel-wrap-show'); + }); + proxy(control.$volumeOn, 'click', e => { + e.stopPropagation(); + setStyle(control.$volumeOn, 'display', 'none'); + setStyle(control.$volumeOff, 'display', 'block'); + const lastVolume = player.volume; + player.volume = 0; + player._lastVolume = lastVolume; + }); + proxy(control.$volumeOff, 'click', e => { + e.stopPropagation(); + setStyle(control.$volumeOn, 'display', 'block'); + setStyle(control.$volumeOff, 'display', 'none'); + player.volume = player.lastVolume || 0.5; + }); + proxy(control.$screenshot, 'click', e => { + e.stopPropagation(); + player.video.screenshot(); + }); + proxy(control.$volumePanel, 'click', event => { + event.stopPropagation(); + player.volume = volumeChangeFromEvent(event); + }); + proxy(control.$volumeHandle, 'mousedown', () => { + control.isVolumeDroging = true; + }); + proxy(control.$volumeHandle, 'mousemove', event => { + if (control.isVolumeDroging) { + player.volume = volumeChangeFromEvent(event); + } + }); + proxy(document, 'mouseup', () => { + if (control.isVolumeDroging) { + control.isVolumeDroging = false; + } + }); + proxy(control.$record, 'click', e => { + e.stopPropagation(); + player.recording = true; + }); + proxy(control.$recordStop, 'click', e => { + e.stopPropagation(); + player.recording = false; + }); + proxy(control.$recordingStop, 'click', e => { + e.stopPropagation(); + player.recording = false; + }); + proxy(control.$fullscreen, 'click', e => { + e.stopPropagation(); + player.fullscreen = true; + }); + proxy(control.$fullscreenExit, 'click', e => { + e.stopPropagation(); + player.fullscreen = false; + }); + + if (player._opt.hasControl && player._opt.controlAutoHide) { + // + proxy(player.$container, 'mouseover', () => { + if (!player.fullscreen) { + setStyle(control.$controls, 'display', 'block'); + startDelayControlHidden(); + } + }); + proxy(player.$container, 'mousemove', () => { + if (player.$container && control.$controls) { + if (!player.fullscreen) { + if (control.$controls.style.display === 'none') { + setStyle(control.$controls, 'display', 'block'); + startDelayControlHidden(); + } + } else { + if (control.$controls.style.display === 'none') { + setStyle(control.$controls, 'display', 'block'); + startDelayControlHidden(); + } + } + } + }); + proxy(player.$container, 'mouseout', () => { + stopDelayControlHidden(); + setStyle(control.$controls, 'display', 'none'); + }); + let delayHiddenTimeout = null; + + const startDelayControlHidden = () => { + stopDelayControlHidden(); + delayHiddenTimeout = setTimeout(() => { + setStyle(control.$controls, 'display', 'none'); + }, 5 * 1000); + }; + + const stopDelayControlHidden = () => { + if (delayHiddenTimeout) { + clearTimeout(delayHiddenTimeout); + delayHiddenTimeout = null; + } + }; + } + }); + + function styleInject(css, ref) { + if (ref === void 0) ref = {}; + var insertAt = ref.insertAt; + + if (!css || typeof document === 'undefined') { return; } + + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.createElement('style'); + style.type = 'text/css'; + + if (insertAt === 'top') { + if (head.firstChild) { + head.insertBefore(style, head.firstChild); + } else { + head.appendChild(style); + } + } else { + head.appendChild(style); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + } + + var css_248z$1 = "@keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes magentaPulse{0%{background-color:#630030;-webkit-box-shadow:0 0 9px #333}50%{background-color:#a9014b;-webkit-box-shadow:0 0 18px #a9014b}to{background-color:#630030;-webkit-box-shadow:0 0 9px #333}}.jessibuca-container .jessibuca-icon{cursor:pointer;width:16px;height:16px}.jessibuca-container .jessibuca-poster{position:absolute;z-index:10;left:0;top:0;right:0;bottom:0;height:100%;width:100%;background-position:50%;background-repeat:no-repeat;background-size:contain;pointer-events:none}.jessibuca-container .jessibuca-play-big{position:absolute;display:none;height:100%;width:100%;background:rgba(0,0,0,.4)}.jessibuca-container .jessibuca-play-big:after{cursor:pointer;content:\"\";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:block;width:48px;height:48px;background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\");background-repeat:no-repeat;background-position:50%}.jessibuca-container .jessibuca-play-big:hover:after{background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\")}.jessibuca-container .jessibuca-recording{display:none;position:absolute;left:50%;top:0;padding:0 3px;transform:translateX(-50%);justify-content:space-around;align-items:center;width:95px;height:20px;background:#000;opacity:1;border-radius:0 0 8px 8px;z-index:1}.jessibuca-container .jessibuca-recording .jessibuca-recording-red-point{width:8px;height:8px;background:#ff1f1f;border-radius:50%;animation:magentaPulse 1s linear infinite}.jessibuca-container .jessibuca-recording .jessibuca-recording-time{font-size:14px;font-weight:500;color:#ddd}.jessibuca-container .jessibuca-recording .jessibuca-icon-recordStop{width:16px;height:16px;cursor:pointer}.jessibuca-container .jessibuca-loading{display:none;flex-direction:column;justify-content:center;align-items:center;position:absolute;z-index:20;left:0;top:0;right:0;bottom:0;width:100%;height:100%;pointer-events:none}.jessibuca-container .jessibuca-loading-text{line-height:20px;font-size:13px;color:#fff;margin-top:10px}.jessibuca-container .jessibuca-controls{background-color:#161616;box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;position:absolute;z-index:40;left:0;right:0;bottom:0;height:38px;width:100%;padding-left:13px;padding-right:13px;font-size:14px;color:#fff;opacity:0;visibility:hidden;transition:all .2s ease-in-out;-webkit-user-select:none;user-select:none;transition:width .5s ease-in}.jessibuca-container .jessibuca-controls .jessibuca-controls-item{position:relative;display:flex;justify-content:center;padding:0 8px}.jessibuca-container .jessibuca-controls .jessibuca-controls-item:hover .icon-title-tips{visibility:visible;opacity:1}.jessibuca-container .jessibuca-controls .jessibuca-fullscreen,.jessibuca-container .jessibuca-controls .jessibuca-fullscreen-exit,.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-microphone-close,.jessibuca-container .jessibuca-controls .jessibuca-pause,.jessibuca-container .jessibuca-controls .jessibuca-play,.jessibuca-container .jessibuca-controls .jessibuca-record,.jessibuca-container .jessibuca-controls .jessibuca-record-stop,.jessibuca-container .jessibuca-controls .jessibuca-screenshot{display:none}.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-icon-mute{z-index:1}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom{display:flex;justify-content:space-between;height:100%}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-left,.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-right{display:flex;align-items:center}.jessibuca-container.jessibuca-controls-show .jessibuca-controls{opacity:1;visibility:visible}.jessibuca-container.jessibuca-controls-show-auto-hide .jessibuca-controls{opacity:.8;visibility:visible;display:none}.jessibuca-container.jessibuca-hide-cursor *{cursor:none!important}.jessibuca-container .jessibuca-icon-loading{width:50px;height:50px;background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%;animation:rotation 1s linear infinite}.jessibuca-container .jessibuca-icon-screenshot{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE5UlEQVRoQ+1YW2sdVRT+1s7JxbsoVkEUrIIX0ouz15zYNA+N1RdtQfCltlUfvLbqL/BCwZ8grbHtizQqPojgBSr0JkiMmT2nxgapqBURtPVCq7HxJCeZJVPmxDlzZubMmXOSEsnAvOy917fXt9e39tp7E5b4R0vcfywTuNgRbBgBx3HuJqLVzPzmYjprjHkcwAlmLqXNm4XAISLaSESPaq2HF4OE67rbRGRYRA7btn1fbgLGmKsA/Azg0gBkGzO/vZAkHMd5hIiqc5wHcCMz/5k0Z2oExsfHV1QqldPAf8lORNu11m8tBAljzFYAYWxRSl1vWdZvuQj4RsYYF4AVBlgIOVVlE55HRIxt23ZuCfmGjuOsJ6LPoiAistW27XfaEYmIbOYhPc9bXywWR1oiEJDYQkR1zrYjEjGyqfqbKd8a7kJVtLgQ+30i8pht2wfyRKIdmJkJBPkQTbILfudJ7CTZNBvVpggEcgpvc/ML38zESbLJsxBNE/A9biX0rdjGyTQXgbxyapdsarb0PMlXtWnGoXbKpm0Essqp3bJpK4E0OXmed3+hUBDP8w5FI91M0rdcyLLILElOCbaZilSWeXMncRx4klTCY1spfG3dhZJWx3GcDUR0EEB3ZMw0ET2gtT6SZWWzjmlrBIJCl0hAKfWgZVmHszqXZVxbCSxpCS2JJA6umIhe8ZKKVLPbaBJ+S9toqVRa53nedgAbAKwIwH4FcAzAa0R0l4i8F7PPz189k6RFRA+LyNcAXojDV0oNW5b1eW4Cxpg9AHZkSaaa6hhzb065uDSCH2LmRB8Sk9gY4293g43Qo/1pV80m8yQMfZSZ781cB1zXHRKRZ2IMpgD8A+DamL4ZItqitX4/jbQx5iEA7wLoihn3V/ACckWMJN/QWj9b1x5tGBsbW6uUOh5pPy0iL3Z2dn6ilJqanp5ep5TaJSLhF4NppdRNaU8gPmapVLrO87yfIoXuWyJ6uVKp+HmFjo6OQSJ6FcBtYT+UUmstyxqvkWuUgDFmP4AnQu2/e563qlgs+u9DNZ8xZhRAX7VRRPbath0XuXk7Y8xeAE+FgL6fnJzsHRwcLIfBR0ZGLunq6poAsDLUvp+Zw7b1r9PGmJMAbg8Z7WDmoThZuK67WkS+DD18fcPMdzSQUBR/EzN/nIC/SUQ+DPXV4dclsTHmHAD/SfHCNzc3t7Kvr++HJKeMMacA3BL0nyuXyzcPDAxMxo0fHR29slAo/Ajg6qD/fE9Pzw29vb1/x42fmJi4vFwu+5G/LOg/y8zXNJLQ2dAES5JANMQ7mfn1jBI6ycx3NiMhItqstf4oAX+ziHwQ6qvDj5NQNIn/ALCKmX+JSeIvABRD7fuY+ekGBPYBeDI05tTMzExvf3+/vz2Hk91/ET8RSeI6/DoCpVJpjed5fmKGvzMAXpqdnT3oed5Ud3d3v4jsAqBr9Ei0Rmv9VRqBBPzvROQVETnq2xJRdRu9tRF+bCVOKWT+Kvl/TSIFk6SW/LAjKfjV5K8rZABi8dOOEv7FI7Z8x6zwEWbemLbyMfJr5qiSiJ96oclymBOR3bZtP9+M89WxxpjdAHY2sN3DzM8ljWl4I3Nd9x7/OE1ENcdpETnmH3e11n41zv0l4J8RkU+J6AAz+xtF4teQQG7PFslwmcAiLfSyhC72Qv9/I/Avns2OT7QJskoAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-screenshot:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAED0lEQVRoQ+2ZycsdRRTFf2ejqHFAMQqiYBTUoElUHLNx3GgCgpuYRF2o0UT9CxwQ/BMkMSbZSKLiQgQHUDCJgjiAxiEiESdEcJbEedgcKaj3UV+/6q7u/jovPPkK3qbr1ql76p5bt6qemPKmKfefeQKHOoLFCNg+H1gi6fFJOmv7VmCvpD1N87Yh8ApwNXCzpB2TIGF7DRDm2inpmt4EbB8LfAMcGUHWSHryYJKwfRMwmuMP4BRJv9TN2RgB2wuB72BWsq+V9MTBIGF7NZBiGzhJ0o+9CIRBtt8FLqgADC6nRDbpVO9Iuqi3hCKB5cDrGZDVkp4aIhIV2aSQyyW9MScCkcQqIOfsnCORkc3I31b5VtyFRmg1IQ7dt0ja3icSQ2C2JhAjUU2ykd+dE7tBNp2i2olAJJFuc+nCt564QTadF6IzgUhiVGiqyinKaQjZpJP2ItBXTkPJZhACXeU0pGwGI9BWTkPLZlACBTldG4o5EA6E1dY66edcyNrs8Q36zg1vVaTazNs7iXPgDVJJzYs7VRvHRzaDEohyugJ4CTi84sg/wHWSdnVxsGQ7aQLXS9pZcqpL/6AEplpCU5HE8YpJ9YrXUKQ6baN1+HPaRm1fBqwFQnKGK2ZoPwCvAo8Ai4FnMpPMHMwapHUj8DFwbw3+Dklv9iZgexOwvktSRduxU2VDlErwmyXV+lCbxLbDdndlCT3TX3vV7JgnKfRuSVflfMkSsL0ZuDMz4E/gL+CETN+/wCpJzzaRtn0D8DRwWMbu1/gCcnSm7zFJd1W/jxGwvQx4r2IYnlbuA14GAomQFw8B6YtBKFSnNj2BxEJ3IvB1pdB9CjwQ8yqYhcg/DJxZ8WOZpA/SbzkC24DbEqOfgPMkBRKzmu23gEuSj1sk5SI3Y2J7C3BHMuZz4FxJf6fgto8APgIWJd+3SUrHjr9O294HnJUMWi8pSGqs2V4CvJ88fH0i6eyChKr4KyS9WIO/Ang+6RvDz0XgABCeFEdtkaQv65yy/QVweuwPY0+T9FuNQ8cAXwHHxf7wdHiypN9r7BfEl8GjYv9+SceXJLQ/mSDYTh2Baog3SHq0pYT2STqno4RWSnqhBn8l8FzSN4bfJol/jkn8bXUS228DFyfft0paVyCwFbg9sQkSDEkctueZZju8iO+tJPEYfo7A0piYKd73wP3xnB+20cvjNnphxdmlkj4sEMjhfwY8COyOY0fb6Bkl/K6FLKxS+M1KpDhJY8mvrG5doRwlf66QZfGbjhLh4pEt35kV3iUp/IvTunU8qtTil/7gaHOY2yjpntaez9b5RmBDYewmSXfX2RRvZLYvbThOh+NuqMa9Ww1+yLnXgO2SwkZR24oEens2oYHzBCa00PMSOtQL/f+NwH+Hg8hAnbrYgQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABA0lEQVRoQ+1YwQqCUBAcfWXXsLr2AXWTPXno8yVB8AP6Aa3oHI+kCDqYaawJljSe133uzO44bx0M/HEG/v1gAd9mkAyQgY4I/F8LJUlyrQFtD2AtIkcNoFEU+Z7n7QD4DfFHEVlocrVmgAUAIAOl3mILPcDgEFcUhyrUKMGUUcroc3NQRimj9XJBGaWMvvPydKN0o6/9QTdKN6rZANxj6EbpRulGuZnjYqs8BbyR8Ub2Izeys+u6yyAIDpo/ehzHM2NMDsA0xFsRmWhyfTIDWSXxCEBmrd2EYXjSHJqm6bQoii2AOYBL5Z0xgFxEVppcrQvQJO0zhgX0iXbdWWSADHRE4AZQ731AhEUeNwAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA7klEQVRoQ+2YSwrCQBBEX6HiVvxsPYDewfN7By/gD9ciQkvERQwJdBSiYs0mEDo96aruombEjy/9+P/jAj7NoBkwA28i8H8tFBFRA9oeWEo6ZgCNiDGwAYpn3TpKmmVytWbABQBmoNRbbqEHGB7iiuJYhRol2DJqGX1uDsuoZdRmLuNZSzGWUcuoZdRHSp/IylNgK2ErYSthK3FHwLcSvpXIjoLt9Jfa6TMwl3TIMBkRE2AH9BriL5KGmVyvWIltJXEfKN6tJJ0ym0bECFgDU+Ba+WZQFCdpkcnVuoBM0i5jXECXaNftZQbMwJsI3AAPN3dAQflHegAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC+UlEQVRoQ+1ZS2sTURT+zlDJYE3XSq219QHVuEjnJDT+Bff9Abqw2voAEfGxqygUqWhVFHGl/yMLu9BwByxk5SNI66ML6U7axjhHbmhgWiftncxoOiV3FcI53z3f/e65594zhIQPSnj86BBot4IdBToKRFyBnbeFlFIScVEiuYvIWC6Xe2YK8pcC7SYA4CMzH4mDQBXAqilQBDsLQLfPf9FxnF4i8kwwmypARI+Wl5dvmIBEsUmlUkNE9NaHsVCpVAZGR0d/m+A2JSAid3K53E0TkCg2pVKpz7KseR/GfKVSGYxMAMA0M1+JEpyJb6lUOm5ZVnkrAsVisaunp+esiByr1Wp3R0ZGvmifzZK4XQQWHMc52MgBpdQuAOcAXABwuB400ZTjONdaIjA7O5u2bVsnWU1EujzP+5nP5xdMVjvIJkCBD8x8VCm1G8AYgAkAAxt8Z5j5YmgCSqlTAJ4D2OcD/AXgATNfbYVEAIFPIvKKiE4D6GuCea8xX6gtpJT6DmBvECgRFRzHeROWRAABE4iWCbwHEFhkPM/L5vP5dyaz+23+KwHXdR3P854S0YG1ILSCuthNMfNM2OC1/RYENLY+ygcBnPfht6ZAA6BYLNr6dyqVokKhsGpaNQ2TWJstreXaE2aed133sojcj41AKyvdzCdAgSXLsk4MDw9/a/i4rntbRPxFNZoC/5jAV2be759DKTUJ4FZSFFi0bbs/k8noy2R9dAjEuWU2YgXkQOK3kD6BMsysi2Z9JC2Jdcw/ALzwPO+xvmcl7Rj177JVEbkO4BARjSflFDJJuW1dBxJPoCIiL4noDIB1BS0pW6j+oJmbm+uuVqvjRKQfLr0bZHnIzJf0f6HeAybahrUJqAPruhLlcnnPysqKfpXp11n/Gv62zoHAroS+AafT6QkiGrIsazKbzX7eVIHEt1US39gCkOzWYthkjNE+tuZujDGZQ8XRXn8N4KT5lLFZ6uaYPt+nwyDuvC80YdhvB9uOAu1WoaNAR4GIK/AHvdr+QAexB7EAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACfUlEQVRoQ+2ZSYsUQRCFvycK4nJXXEbHBdwO4kn/gv9CD467ICIutxEFkREdFUU86T/xojcPntyQcT2INw+uISFVkD1Wd2dWlU7nUHlqisiX+fJFZGREi8yHMt8/HYG5VrBToFOg4QnMPxcyM2t4KE2nT0i6EwvylwIjQOCFpE1tEPgGfI0FamC3AFgazP8IrJL0KwZzkAI3gLMxIA1ttgCPA4w3wHpJP2NwBxG4KOlcDEgTGzNbA8wEGP57vA0CU5JONtlczFwz2wY8HUbAzBYCB4CtwCVJb33OIAXmioC70LoyBsxsEXAQOApsLIhelnS6FgEzW+5BBvwA/FS+SPJFa40KBZ5L2mxmS4AJ4IjHxCzwaUnHkgmY2V7gLrAyAPwOXJN0qg6DCgIvgQfAPsDjo2pcKddLciEz+wCs6AO6W9KjVBIVBGIgahN4BvRLMjslPYlZPbT53wR2AbeBtcUmXEFPdh5U06mbd/shBBzbr/Jx4FCAX0+BEsDMFocEYrNmFcE+BD4XsXZL0oyZnQCutkagzkn3m1NBwDe/Q9L74MAuFEqUn5op8I8JvJO0elacTALnc1HAH3Njkvwx+WeYWUegTa/pwaqIgexdyIN4uyRPmqULZRXEvulPwD3gpr+zcrtGQxfzRHYG2AAczuUWiom3kc4D2RN4BdwH9gM9CS0XFyoLGu9UuN974eIFVDiuSzruH5LqgRhtU20q8kBPV8LMlhVVmVdnYwX+SMdAZVeieAF7eeltmElJr4cpkH1bJfvGVvatxdR4bMu+teZuWxtKxWncXn8I7EldtQV7vz79fp9KwZp//9CksB8F206BuVahU6BToOEJ/Ab7+KdABdTt8AAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAADd0lEQVRoQ+2Zz2sdVRTHv+fJBDW6anDVXen6wZszYxYBiYgtFGst3VSDunKjpS0GpUlqfjVpsVVs6aaL0or4YxMVFCJZ2ZLdPUP+gq5bQnTxtNAkfTnlhnnlkmQy9yV9780rudt77tzv5/y4v4bQ4Y06XD/2ANodwec/AiJygJnvtdvTWfPnRkBEJAiCN8rl8kMfiPn5+Ve7u7v3rays0Orq6lJfX99/PuN2auMDoAD+BvA2M6/mTWSMOUtE48D6AjHGzN/kjdlNvy+AnWOOmQ/lTSYiEwDOWzsimgrDcCRvzG76GwGw8/zJzO9sN6GInAMwbW1UdSSKoqndCMwb6wNwGsB39Q+p6h/M/C4R2dTa1AoHYBWKyCkA1+pqiWi2Wq0e7e/vf7yRoJAAKcQggMtuJKIoOtoxACnE0/xOi/SXMAxPuhCFjUBdpIjYVWXSEf0TM3/g9BeriDMKdSPEz8z8vrU1xgwT0YXCrEJZy1iSJKOqOub0/8jMA0mSfKKqNwoPkHp7ioiGHIhRIvpHVa93BEBa2JcAfOlALAHo6RgAKzRJkk9V1S6xL7kpV4idOM31taxaIKJHqmpPnMMA9hcOQES2PDJkAT1XAAC+ZebPfWB3auNzmLObVsNRUNUXVHUujuM7OxXnMy4XwOcj29mIyOuq+lapVGrYCelKpkEQ3CyXy4tbzdN0AGPMxr2iYZ+sra3FcRybtgCIiK2BKw2rdgaUSqWoUqlIkQAepFDdAF7cBq5ERI9rtdr1OI7tmE2t6SmUEYFHAEaexYW/1QC2EF+ru5GIvg7D0D2GNJxprQY4o6qv1I/b6SpzOYqiLxpWng5oOQAzXxWRWwA+dkRfYOb1p5hGW6sBJpn5KytSRG4D+KguWFXHoyhy7xdeLC0F2ChSRL4H8OFuINoKYIUbY34gogHH3eeZef1K6tPaDpCm068A3nMEDzHzxY4BUNWSiPxORO6z5aDPPlGICNQ9bYyZIaLjjudzIQoFkKbTbwCO+UI0HcB9J/LdeY0xs0R02IGYYObRrWqiFQCfEZEtSHsfmGZm+4qxbbM/hQD8BeBNa0hEM2EYnmgLgP3lFARBT1dXly4vL//b29tbzQNIU+llAHeJaLFSqRzJes5vegR8xGbZLCwsHKzVav8z8/0sm0ID+MDvAfh4qZk2exFopnd9vv0ELrXBQO7fD10AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC/ElEQVRoQ+2Zy49NQRCHvx+ReK6IlZ34E7CUiCAR4xEbTLCyQRATYswwb2IQZDYWgojHZpCQECts+ResiQwLj0RClNSkb9Lu3HtPz7mZc8+V6eXt6tP1VVV3VdcVbT7U5vozC9BqD/7/HjCzlZLet9rS9fbP9ICZvQPWSfqRAmFmS4ClMHm+JiR9S1mXVyYFwIBXwEZJv7I2MrPjQH8A6JN0OWtNM/OpAL7HS0mbsjYzswGgN8gNS+rJWtPM/HQAfJ9nkrY22tDMTgMjQaZH0nAzCmatTQE4ClyNPvQU2CbJQ2vKKB2Aa2hmR4DrkbbPgQ5Jv6sJSgkQILqA0dgTkjraBiBAxPHtPz2UtDuGKK0HKkqamd8qg5HS9yXtjebLdYjrHNRqiAeS9gQvnQGGSnML1bvGzOwc0BfN35PUaWYHgRulBwjW9ju+O4JwqM/AWFsABIgLwKkIYgJY1jYAAeJQuGIXVIVcKTKxh8WfBin9J+AVpx/eFWUEqFkyNACKp0rhgWYArkg6kQibSyylmPOklQdibijBX+fSLHFRJkDid+qKmdlaYENOI0zeEcBNSZ9qbVIEQHWuyGOTNZLetgrAz8ClPFpHa1ZL8rf5lFGEB2oBfAxQi4D5DeDmAP7mGJPka0oD4LnDr9imH/xFe8AP4vLIjBclxWXItCOtaIBjwOKo3HaFRyWdnLbmYUHhAJKumdkt4ECk9JCkSitmWixFAwxKOjt5uZvdBvZH2vZLit8XSSBFA/yjpJndAfY1A9FSgOCJu0BnBNErqfIkzfRCywECxCNgR6Rtt6TzmdqHBmyKXG4ZM4sTWc04NzNPWE+AuG3ZlZInSuGBinXMbBzYGVkrE6JUACGcHgPbUyGKAIj7REmZ18y897o5ghiQ5E/bltRChwE/kF7Xj0jyLkbDYWbzgBfA+iA4LmlXqwD8LydvszjAF0lfswBCKC0E3gBeP22p186f8RBKUbaejJmtAr5L+lBPptQAKfCzAClWmkmZWQ/MpHVTvv0X9iFAQGQyevIAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrUlEQVRoQ+2ZPYgTURCAZzbBXJnCeL2Cnb87b9MEtPBUrrMQFAtrtT5/ClGs9LBWWz0RtbBUFCF4oJDsbO68wsLA2YqQSmLlvpEHu7IuMdlLcus+yUKKhJfZ+ebnvZl5CJY/aLn+MAP41x7M1QPMfFtr/crzvHfTAs8FoNPp1LTWzwHgqIg0lFLvrQHwfX8BER8DwC6jNCIecF13wwoA3/dvIuKNpLJa60Oe560XGoCZd4rICiKeTCtaeABmPg4AJmRqg6xcaABmvg4At4aFRyEBhoVM4UMoCplHADCfJTEL5YEsIVNID5iQAYCHALCYxeq5b6PMfF5EBAAEESthGK7W6/XPRpFWq7W3VCqtZg2ZcT3g+/6i4zjzIlLSWn/yPO/DIGMNLCWY2Sj/+xGRK0qpZfNDEASnROTFVi0fr8+aA8z8Ld6KEfGt67oLYwMAwEUium8EREn7OgeAjwCwPyo/nrque3YSgAtE9GDaAM1mc65arc4Zuf1+P2w0Gt9jJZl5DQAORt+fENG5wgEw8zUAMB/zbBBRwyqAIAjuiMjlSOlNItpjFUCqWl0josMzgChR/9hGAWBbknjmAdPhDdqa0gfZzAMJKyVP4v8hhJYRcSni+0JEu63ahZj5anyQici6UuqIVQDdbrfS6/UqRulyufyTiH5sF8AlIro37VpoWEHIzGZ2tM+sEZFnSqkzk9RCS0R01wjIsZz+mug53hDRia0AnI4bGgDYISItz/M2jYC8Gpp2u30MEWuO4zha665Sqp0ZYFStX/iWchRAItFGzoHSsrJ2ZFl1mHg6bfVYJeGJv85CC++BpIJZ5kSFC6G0ha0e7mYJqcJ7IOkRay84UhD2XjHFIFZf8iW9YcYoYRi+tO6aNeupOs66iU/icV46zf/MAKZpzXFk/QL+JG1PUPhRiQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACSElEQVRoQ+2Zu4sUQRCHf5+C+gf4yBXMfMYHGvjCzEBQDIzV+HwEohipGKupD0QNDE8UEwUFTe68wEDhTMVUMFJ+0tArzbjs9u3Ojt0wBR0M9MzUV1XdXVWNKhcq1189wP/2YKcesH1d0nPgdVvgnQDY3iTpqaT9kuaAt9UA2D4o6aGkzVHpXcByFQC2r0q60lB2D7BUNIDtjZIeSDoyRNGyAWwfiiET4n6YlAtg+7Kka2PCozyAMSHT5CkLIIbMfUlbMhdmOQCZIVOeB2LI3JN0NNPq6bTZe8D2aUmOY72kN8DnoIXt7eF5FSEzkQdsB+OEsFwr6RPwbpixhqYStoPyqVwAbkaAY5KeTWD5wStZHrD9XdJgK34FhBP9H8kFOAvciQBhn3/RAcBHSTvjfx4DJ6cBOAPcbRvA9gZJYQT5DfwYKGl7UdLu+PwIOFUiwCVJYQRZBuZqA7gh6XxUegXYVhtAmq0uAnt7gLhQm9vorBZx74Hcc6D3QLKH/z2JGyVnlYs4pCfzEe4rsLW2XehicpAtAftqAwiZbhhBfgE/ZwVwDrjddi40KiG0HXpHO+KcJ8CJaXKheeBWBOgqnf6W1BwvgcOrATieFDTrJL0HViJAVwXNgVgPrJH0BfiQDTDKtREiNK7KLSnHASQLLacP1PxcVkWWq8PU3emq2yqJJ0b1Qsv2QKpdZp+orBBqmrfq5m5mSJXtgUZI1XnB0YCo94opCal6L/ka3ghtlIXqrllzT9VJ5k19Ek/y0zbf6QHatOYk3/oDujC8QMWgjf4AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-text{font-size:14px;width:30px}.jessibuca-container .jessibuca-speed{font-size:14px;color:#fff}.jessibuca-container .jessibuca-quality-menu-list{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .jessibuca-quality-menu-list.jessibuca-quality-menu-shown{visibility:visible;opacity:1}.jessibuca-container .icon-title-tips{pointer-events:none;position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s ease 0s,opacity .3s ease 0s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .icon-title{display:inline-block;padding:5px 10px;font-size:12px;white-space:nowrap;color:#fff}.jessibuca-container .jessibuca-quality-menu{padding:8px 0}.jessibuca-container .jessibuca-quality-menu-item{display:block;height:25px;margin:0;padding:0 10px;cursor:pointer;font-size:14px;text-align:center;width:50px;color:hsla(0,0%,100%,.5);transition:color .3s,background-color .3s}.jessibuca-container .jessibuca-quality-menu-item:hover{background-color:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-quality-menu-item:focus{outline:none}.jessibuca-container .jessibuca-quality-menu-item.jessibuca-quality-menu-item-active{color:#2298fc}.jessibuca-container .jessibuca-volume-panel-wrap{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%) translateY(22%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px;height:120px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-wrap.jessibuca-volume-panel-wrap-show{visibility:visible;opacity:1}.jessibuca-container .jessibuca-volume-panel{cursor:pointer;position:absolute;top:21px;height:60px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-text{position:absolute;left:0;top:0;width:50px;height:20px;line-height:20px;text-align:center;color:#fff;font-size:12px}.jessibuca-container .jessibuca-volume-panel-handle{position:absolute;top:48px;left:50%;width:12px;height:12px;border-radius:12px;margin-left:-6px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:before{bottom:-54px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:after{bottom:6px;background:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-volume-panel-handle:after,.jessibuca-container .jessibuca-volume-panel-handle:before{content:\"\";position:absolute;display:block;left:50%;width:3px;margin-left:-1px;height:60px}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-controls{width:100vh}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-play-big:after{transform:translate(-50%,-50%) rotate(270deg)}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading{flex-direction:row}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading-text{transform:rotate(270deg)}\n/*# sourceMappingURL=data:application/json;base64,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eWZyYW1lcyByb3RhdGlvbiB7XG4gIGZyb20ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7IH1cbiAgdG8ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMzYwZGVnKTsgfSB9XG5cbkBrZXlmcmFtZXMgbWFnZW50YVB1bHNlIHtcbiAgZnJvbSB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzYzMDAzMDtcbiAgICAtd2Via2l0LWJveC1zaGFkb3c6IDAgMCA5cHggIzMzMzsgfVxuICA1MCUge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICNhOTAxNGI7XG4gICAgLXdlYmtpdC1ib3gtc2hhZG93OiAwIDAgMThweCAjYTkwMTRiOyB9XG4gIHRvIHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjNjMwMDMwO1xuICAgIC13ZWJraXQtYm94LXNoYWRvdzogMCAwIDlweCAjMzMzOyB9IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICB3aWR0aDogMTZweDtcbiAgaGVpZ2h0OiAxNnB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcG9zdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAxMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHdpZHRoOiAxMDAlO1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXIgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIHBvaW50ZXItZXZlbnRzOiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWcge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIGhlaWdodDogMTAwJTtcbiAgd2lkdGg6IDEwMCU7XG4gIGJhY2tncm91bmQ6IHJnYmEoMCwgMCwgMCwgMC40KTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXBsYXktYmlnOmFmdGVyIHtcbiAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgY29udGVudDogJyc7XG4gICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgIGxlZnQ6IDUwJTtcbiAgICB0b3A6IDUwJTtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICB3aWR0aDogNDhweDtcbiAgICBoZWlnaHQ6IDQ4cHg7XG4gICAgYmFja2dyb3VuZC1pbWFnZTogdXJsKFwiLi4vYXNzZXRzL3BsYXkucG5nXCIpO1xuICAgIGJhY2tncm91bmQtcmVwZWF0OiBuby1yZXBlYXQ7XG4gICAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWc6aG92ZXI6YWZ0ZXIge1xuICAgIGJhY2tncm91bmQtaW1hZ2U6IHVybChcIi4uL2Fzc2V0cy9wbGF5LWhvdmVyLnBuZ1wiKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyB7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgbGVmdDogNTAlO1xuICB0b3A6IDA7XG4gIHBhZGRpbmc6IDAgM3B4O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIGp1c3RpZnktY29udGVudDogc3BhY2UtYXJvdW5kO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICB3aWR0aDogOTVweDtcbiAgaGVpZ2h0OiAyMHB4O1xuICBiYWNrZ3JvdW5kOiAjMDAwMDAwO1xuICBvcGFjaXR5OiAxO1xuICBib3JkZXItcmFkaXVzOiAwcHggMHB4IDhweCA4cHg7XG4gIHotaW5kZXg6IDE7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1yZWNvcmRpbmcgLmplc3NpYnVjYS1yZWNvcmRpbmctcmVkLXBvaW50IHtcbiAgICB3aWR0aDogOHB4O1xuICAgIGhlaWdodDogOHB4O1xuICAgIGJhY2tncm91bmQ6ICNGRjFGMUY7XG4gICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgIGFuaW1hdGlvbjogbWFnZW50YVB1bHNlIDFzIGxpbmVhciBpbmZpbml0ZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLXJlY29yZGluZy10aW1lIHtcbiAgICBmb250LXNpemU6IDE0cHg7XG4gICAgZm9udC13ZWlnaHQ6IDUwMDtcbiAgICBjb2xvcjogI0RERERERDsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gICAgd2lkdGg6IDE2cHg7XG4gICAgaGVpZ2h0OiAxNnB4O1xuICAgIGN1cnNvcjogcG9pbnRlcjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmcge1xuICBkaXNwbGF5OiBub25lO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAyMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmctdGV4dCB7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICBmb250LXNpemU6IDEzcHg7XG4gIGNvbG9yOiAjZmZmO1xuICBtYXJnaW4tdG9wOiAxMHB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTYxNjE2O1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBkaXNwbGF5OiBmbGV4O1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGZsZXgtZW5kO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHotaW5kZXg6IDQwO1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDM4cHg7XG4gIHdpZHRoOiAxMDAlO1xuICBwYWRkaW5nLWxlZnQ6IDEzcHg7XG4gIHBhZGRpbmctcmlnaHQ6IDEzcHg7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgY29sb3I6ICNmZmY7XG4gIG9wYWNpdHk6IDA7XG4gIHZpc2liaWxpdHk6IGhpZGRlbjtcbiAgdHJhbnNpdGlvbjogYWxsIDAuMnMgZWFzZS1pbi1vdXQ7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICB0cmFuc2l0aW9uOiB3aWR0aCAuNXMgZWFzZS1pbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbSB7XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gICAgcGFkZGluZzogMCA4cHg7IH1cbiAgICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbTpob3ZlciAuaWNvbi10aXRsZS10aXBzIHtcbiAgICAgIHZpc2liaWxpdHk6IHZpc2libGU7XG4gICAgICBvcGFjaXR5OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1taWNyb3Bob25lLWNsb3NlIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wbGF5IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wYXVzZSB7XG4gICAgZGlzcGxheTogbm9uZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi1leGl0IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1zY3JlZW5zaG90IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1yZWNvcmQge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWZ1bGxzY3JlZW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLXJlY29yZC1zdG9wIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvLCAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtaWNvbi1tdXRlIHtcbiAgICB6LWluZGV4OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20ge1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuO1xuICAgIGhlaWdodDogMTAwJTsgfVxuICAgIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20gLmplc3NpYnVjYS1jb250cm9scy1sZWZ0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG4gICAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWNvbnRyb2xzLWJvdHRvbSAuamVzc2lidWNhLWNvbnRyb2xzLXJpZ2h0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93IC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBvcGFjaXR5OiAxO1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93LWF1dG8taGlkZSAuamVzc2lidWNhLWNvbnRyb2xzIHtcbiAgb3BhY2l0eTogMC44O1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlO1xuICBkaXNwbGF5OiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1oaWRlLWN1cnNvciAqIHtcbiAgY3Vyc29yOiBub25lICFpbXBvcnRhbnQ7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWxvYWRpbmcge1xuICB3aWR0aDogNTBweDtcbiAgaGVpZ2h0OiA1MHB4O1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbG9hZGluZy5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7XG4gIGFuaW1hdGlvbjogcm90YXRpb24gMXMgbGluZWFyIGluZmluaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3NjcmVlbnNob3QucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvc2NyZWVuc2hvdC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheSB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wbGF5LnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheTpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BsYXktaG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXBhdXNlIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BhdXNlLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGF1c2U6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wYXVzZS1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZDpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZFN0b3A6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbiB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9mdWxsc2NyZWVuLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbjpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2Z1bGxzY3JlZW4taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2V4aXQtZnVsbHNjcmVlbi5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvZXhpdC1mdWxsc2NyZWVuLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1hdWRpbyB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9hdWRpby5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWF1ZGlvOmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvYXVkaW8taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGUge1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbXV0ZS5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGU6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9tdXRlLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi10ZXh0IHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICB3aWR0aDogMzBweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXNwZWVkIHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICBjb2xvcjogI2ZmZjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXF1YWxpdHktbWVudS1saXN0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMsIG9wYWNpdHkgMzAwbXM7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMC41KTtcbiAgYm9yZGVyLXJhZGl1czogNHB4OyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWxpc3QuamVzc2lidWNhLXF1YWxpdHktbWVudS1zaG93biB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5pY29uLXRpdGxlLXRpcHMge1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMgZWFzZSAwcywgb3BhY2l0eSAzMDBtcyBlYXNlIDBzO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuaWNvbi10aXRsZSB7XG4gIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgcGFkZGluZzogNXB4IDEwcHg7XG4gIGZvbnQtc2l6ZTogMTJweDtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgY29sb3I6IHdoaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51IHtcbiAgcGFkZGluZzogOHB4IDA7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBoZWlnaHQ6IDI1cHg7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMCAxMHB4O1xuICBjdXJzb3I6IHBvaW50ZXI7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICB3aWR0aDogNTBweDtcbiAgY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC41KTtcbiAgdHJhbnNpdGlvbjogY29sb3IgMzAwbXMsIGJhY2tncm91bmQtY29sb3IgMzAwbXM7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbTpob3ZlciB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyNTUsIDI1NSwgMjU1LCAwLjIpOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW06Zm9jdXMge1xuICAgIG91dGxpbmU6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbS5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW0tYWN0aXZlIHtcbiAgICBjb2xvcjogIzIyOThGQzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC13cmFwIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSkgdHJhbnNsYXRlWSgyMiUpO1xuICB0cmFuc2l0aW9uOiB2aXNpYmlsaXR5IDMwMG1zLCBvcGFjaXR5IDMwMG1zO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgaGVpZ2h0OiAxMjBweDtcbiAgd2lkdGg6IDUwcHg7XG4gIG92ZXJmbG93OiBoaWRkZW47IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtd3JhcC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLXdyYXAtc2hvdyB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMjFweDtcbiAgaGVpZ2h0OiA2MHB4O1xuICB3aWR0aDogNTBweDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC10ZXh0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiAwO1xuICB0b3A6IDA7XG4gIHdpZHRoOiA1MHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGNvbG9yOiAjZmZmO1xuICBmb250LXNpemU6IDEycHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDQ4cHg7XG4gIGxlZnQ6IDUwJTtcbiAgd2lkdGg6IDEycHg7XG4gIGhlaWdodDogMTJweDtcbiAgYm9yZGVyLXJhZGl1czogMTJweDtcbiAgbWFyZ2luLWxlZnQ6IC02cHg7XG4gIGJhY2tncm91bmQ6ICNmZmY7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUge1xuICAgIGJvdHRvbTogLTU0cHg7XG4gICAgYmFja2dyb3VuZDogI2ZmZjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC1oYW5kbGU6OmFmdGVyIHtcbiAgICBib3R0b206IDZweDtcbiAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMik7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUsIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLWhhbmRsZTo6YWZ0ZXIge1xuICAgIGNvbnRlbnQ6ICcnO1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICBsZWZ0OiA1MCU7XG4gICAgd2lkdGg6IDNweDtcbiAgICBtYXJnaW4tbGVmdDogLTFweDtcbiAgICBoZWlnaHQ6IDYwcHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICB3aWR0aDogMTAwdmg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtcGxheS1iaWc6YWZ0ZXIge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKSByb3RhdGUoMjcwZGVnKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nIHtcbiAgZmxleC1kaXJlY3Rpb246IHJvdzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nLXRleHQge1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgyNzBkZWcpOyB9XG4iXX0= */"; + styleInject(css_248z$1); + + // todo: 待定 + var hotkey = ((player, control) => { + const { + events: { + proxy + } + } = player; + const keys = {}; + + function addHotkey(key, event) { + if (keys[key]) { + keys[key].push(event); + } else { + keys[key] = [event]; + } + } // + + + addHotkey(HOT_KEY.esc, () => { + if (player.fullscreen) { + player.fullscreen = false; + } + }); // + + addHotkey(HOT_KEY.arrowUp, () => { + player.volume += 0.05; + }); // + + addHotkey(HOT_KEY.arrowDown, () => { + player.volume -= 0.05; + }); + proxy(window, 'keydown', event => { + if (control.isFocus) { + const tag = document.activeElement.tagName.toUpperCase(); + const editable = document.activeElement.getAttribute('contenteditable'); + + if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') { + const events = keys[event.keyCode]; + + if (events) { + event.preventDefault(); + events.forEach(fn => fn()); + } + } + } + }); + }); + + class Control { + constructor(player) { + this.player = player; + template(player, this); + property(player, this); + observer$1(player, this); + events(player, this); + + if (player._opt.hotKey) { + hotkey(player, this); + } + + this.player.debug.log('Control', 'init'); + } + + destroy() { + if (this.$poster) { + const result = removeElement(this.$poster); + + if (!result) { + const $poster = this.player.$container.querySelector('.jessibuca-poster'); + + if ($poster && this.player.$container) { + this.player.$container.removeChild($poster); + } + } + } + + if (this.$loading) { + const result = removeElement(this.$loading); + + if (!result) { + const $loading = this.player.$container.querySelector('.jessibuca-loading'); + + if ($loading && this.player.$container) { + this.player.$container.removeChild($loading); + } + } + } + + if (this.$controls) { + const result = removeElement(this.$controls); + + if (!result) { + const $controls = this.player.$container.querySelector('.jessibuca-controls'); + + if ($controls && this.player.$container) { + this.player.$container.removeChild($controls); + } + } + } + + if (this.$recording) { + const result = removeElement(this.$recording); + + if (!result) { + const $recording = this.player.$container.querySelector('.jessibuca-recording'); + + if ($recording && this.player.$container) { + this.player.$container.removeChild($recording); + } + } + } + + if (this.$playBig) { + const result = removeElement(this.$playBig); + + if (!result) { + const $playBig = this.player.$container.querySelector('.jessibuca-play-big'); + + if ($playBig && this.player.$container) { + this.player.$container.removeChild($playBig); + } + } + } + + this.player.debug.log('control', 'destroy'); + } + + autoSize() { + const player = this.player; + player.$container.style.padding = '0 0'; + const playerWidth = player.width; + const playerHeight = player.height; + const playerRatio = playerWidth / playerHeight; + const canvasWidth = player.video.$videoElement.width; + const canvasHeight = player.video.$videoElement.height; + const canvasRatio = canvasWidth / canvasHeight; + + if (playerRatio > canvasRatio) { + const padding = (playerWidth - playerHeight * canvasRatio) / 2; + player.$container.style.padding = `0 ${padding}px`; + } else { + const padding = (playerHeight - playerWidth / canvasRatio) / 2; + player.$container.style.padding = `${padding}px 0`; + } + } + + } + + var css_248z = ".jessibuca-container{position:relative;display:block;width:100%;height:100%;overflow:hidden}.jessibuca-container.jessibuca-fullscreen-web{position:fixed;z-index:9999;left:0;top:0;right:0;bottom:0;width:100vw!important;height:100vh!important;background:#000}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEscUJBQ0UsaUJBQWtCLENBQ2xCLGFBQWMsQ0FDZCxVQUFXLENBQ1gsV0FBWSxDQUNaLGVBQWtCLENBQ2xCLDhDQUNFLGNBQWUsQ0FDZixZQUFhLENBQ2IsTUFBTyxDQUNQLEtBQU0sQ0FDTixPQUFRLENBQ1IsUUFBUyxDQUNULHFCQUF1QixDQUN2QixzQkFBd0IsQ0FDeEIsZUFBa0IiLCJmaWxlIjoic3R5bGUuc2NzcyIsInNvdXJjZXNDb250ZW50IjpbIi5qZXNzaWJ1Y2EtY29udGFpbmVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBkaXNwbGF5OiBibG9jaztcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMTAwJTtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIge1xuICAgIHBvc2l0aW9uOiBmaXhlZDtcbiAgICB6LWluZGV4OiA5OTk5O1xuICAgIGxlZnQ6IDA7XG4gICAgdG9wOiAwO1xuICAgIHJpZ2h0OiAwO1xuICAgIGJvdHRvbTogMDtcbiAgICB3aWR0aDogMTAwdncgIWltcG9ydGFudDtcbiAgICBoZWlnaHQ6IDEwMHZoICFpbXBvcnRhbnQ7XG4gICAgYmFja2dyb3VuZDogIzAwMDsgfVxuIl19 */"; + styleInject(css_248z); + + var observer = (player => { + const { + _opt, + debug, + events: { + proxy + } + } = player; + + if (_opt.supportDblclickFullscreen) { + proxy(player.$container, 'dblclick', e => { + const target = getTarget(e); + const nodeName = target.nodeName.toLowerCase(); + + if (nodeName === 'canvas' || nodeName === 'video') { + player.fullscreen = !player.fullscreen; + } + }); + } // + + + proxy(document, 'visibilitychange', () => { + if (_opt.hiddenAutoPause) { + debug.log('visibilitychange', document.visibilityState, player._isPlayingBeforePageHidden); + + if ("visible" === document.visibilityState) { + if (player._isPlayingBeforePageHidden) { + player.play(); + } + } else { + player._isPlayingBeforePageHidden = player.playing; // hidden + + if (player.playing) { + player.pause(); + } + } + } + }); + proxy(window, 'fullscreenchange', () => { + // + if (player.keepScreenOn !== null && "visible" === document.visibilityState) { + player.enableWakeLock(); + } + }); + }); + + class MP4$1 { + static init() { + MP4$1.types = { + avc1: [], + avcC: [], + hvc1: [], + hvcC: [], + btrt: [], + dinf: [], + dref: [], + esds: [], + ftyp: [], + hdlr: [], + mdat: [], + mdhd: [], + mdia: [], + mfhd: [], + minf: [], + moof: [], + moov: [], + mp4a: [], + mvex: [], + mvhd: [], + sdtp: [], + stbl: [], + stco: [], + stsc: [], + stsd: [], + stsz: [], + stts: [], + tfdt: [], + tfhd: [], + traf: [], + trak: [], + trun: [], + trex: [], + tkhd: [], + vmhd: [], + smhd: [] + }; + + for (let name in MP4$1.types) { + if (MP4$1.types.hasOwnProperty(name)) { + MP4$1.types[name] = [name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)]; + } + } + + let constants = MP4$1.constants = {}; + constants.FTYP = new Uint8Array([0x69, 0x73, 0x6F, 0x6D, // major_brand: isom + 0x0, 0x0, 0x0, 0x1, // minor_version: 0x01 + 0x69, 0x73, 0x6F, 0x6D, // isom + 0x61, 0x76, 0x63, 0x31 // avc1 + ]); + constants.STSD_PREFIX = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x01 // entry_count + ]); + constants.STTS = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + constants.STSC = constants.STCO = constants.STTS; + constants.STSZ = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00 // sample_count + ]); + constants.HDLR_VIDEO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x69, 0x64, 0x65, 0x6F, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler + ]); + constants.HDLR_AUDIO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x6F, 0x75, 0x6E, 0x64, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler + ]); + constants.DREF = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0C, // entry_size + 0x75, 0x72, 0x6C, 0x20, // type 'url ' + 0x00, 0x00, 0x00, 0x01 // version(0) + flags + ]); // Sound media header + + constants.SMHD = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2) + ]); // video media header + + constants.VMHD = new Uint8Array([0x00, 0x00, 0x00, 0x01, // version(0) + flags + 0x00, 0x00, // graphicsmode: 2 bytes + 0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes + 0x00, 0x00]); + } // Generate a box + + + static box(type) { + let size = 8; + let result = null; + let datas = Array.prototype.slice.call(arguments, 1); + let arrayCount = datas.length; + + for (let i = 0; i < arrayCount; i++) { + size += datas[i].byteLength; + } + + result = new Uint8Array(size); + result[0] = size >>> 24 & 0xFF; // size + + result[1] = size >>> 16 & 0xFF; + result[2] = size >>> 8 & 0xFF; + result[3] = size & 0xFF; + result.set(type, 4); // type + + let offset = 8; + + for (let i = 0; i < arrayCount; i++) { + // data body + result.set(datas[i], offset); + offset += datas[i].byteLength; + } + + return result; + } // emit ftyp & moov + + + static generateInitSegment(meta) { + let ftyp = MP4$1.box(MP4$1.types.ftyp, MP4$1.constants.FTYP); + let moov = MP4$1.moov(meta); + let result = new Uint8Array(ftyp.byteLength + moov.byteLength); + result.set(ftyp, 0); + result.set(moov, ftyp.byteLength); + return result; + } // Movie metadata box + + + static moov(meta) { + let mvhd = MP4$1.mvhd(meta.timescale, meta.duration); + let trak = MP4$1.trak(meta); + let mvex = MP4$1.mvex(meta); + return MP4$1.box(MP4$1.types.moov, mvhd, trak, mvex); + } // Movie header box + + + static mvhd(timescale, duration) { + return MP4$1.box(MP4$1.types.mvhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + timescale >>> 24 & 0xFF, // timescale: 4 bytes + timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0 + 0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes) + 0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- + 0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes---- + 0xFF, 0xFF, 0xFF, 0xFF // next_track_ID + ])); + } // Track box + + + static trak(meta) { + return MP4$1.box(MP4$1.types.trak, MP4$1.tkhd(meta), MP4$1.mdia(meta)); + } // Track header box + + + static tkhd(meta) { + let trackId = meta.id, + duration = meta.duration; + let width = meta.presentWidth, + height = meta.presentHeight; + return MP4$1.box(MP4$1.types.tkhd, new Uint8Array([0x00, 0x00, 0x00, 0x07, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + trackId >>> 24 & 0xFF, // track_ID: 4 bytes + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes + duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes) + 0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes) + 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- + width >>> 8 & 0xFF, // width and height + width & 0xFF, 0x00, 0x00, height >>> 8 & 0xFF, height & 0xFF, 0x00, 0x00])); + } + + static mdia(meta) { + return MP4$1.box(MP4$1.types.mdia, MP4$1.mdhd(meta), MP4$1.hdlr(meta), MP4$1.minf(meta)); + } // Media header box + + + static mdhd(meta) { + let timescale = meta.timescale; + let duration = meta.duration; + return MP4$1.box(MP4$1.types.mdhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + timescale >>> 24 & 0xFF, // timescale: 4 bytes + timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x55, 0xC4, // language: und (undetermined) + 0x00, 0x00 // pre_defined = 0 + ])); + } // Media handler reference box + + + static hdlr(meta) { + let data = null; + + if (meta.type === 'audio') { + data = MP4$1.constants.HDLR_AUDIO; + } else { + data = MP4$1.constants.HDLR_VIDEO; + } + + return MP4$1.box(MP4$1.types.hdlr, data); + } // Media infomation box + + + static minf(meta) { + let xmhd = null; + + if (meta.type === 'audio') { + xmhd = MP4$1.box(MP4$1.types.smhd, MP4$1.constants.SMHD); + } else { + xmhd = MP4$1.box(MP4$1.types.vmhd, MP4$1.constants.VMHD); + } + + return MP4$1.box(MP4$1.types.minf, xmhd, MP4$1.dinf(), MP4$1.stbl(meta)); + } // Data infomation box + + + static dinf() { + let result = MP4$1.box(MP4$1.types.dinf, MP4$1.box(MP4$1.types.dref, MP4$1.constants.DREF)); + return result; + } // Sample table box + + + static stbl(meta) { + let result = MP4$1.box(MP4$1.types.stbl, // type: stbl + MP4$1.stsd(meta), // Sample Description Table + MP4$1.box(MP4$1.types.stts, MP4$1.constants.STTS), // Time-To-Sample + MP4$1.box(MP4$1.types.stsc, MP4$1.constants.STSC), // Sample-To-Chunk + MP4$1.box(MP4$1.types.stsz, MP4$1.constants.STSZ), // Sample size + MP4$1.box(MP4$1.types.stco, MP4$1.constants.STCO) // Chunk offset + ); + return result; + } // Sample description box + + + static stsd(meta) { + if (meta.type === 'audio') { + // else: aac -> mp4a + return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.mp4a(meta)); + } else { + if (meta.videoType === 'avc') { + // + return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.avc1(meta)); + } else { + // + return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.hvc1(meta)); + } + } + } + + static mp4a(meta) { + let channelCount = meta.channelCount; + let sampleRate = meta.audioSampleRate; + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4) + 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) + 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, channelCount, // channelCount(2) + 0x00, 0x10, // sampleSize(2) + 0x00, 0x00, 0x00, 0x00, // reserved(4) + sampleRate >>> 8 & 0xFF, // Audio sample rate + sampleRate & 0xFF, 0x00, 0x00]); + return MP4$1.box(MP4$1.types.mp4a, data, MP4$1.esds(meta)); + } + + static esds(meta) { + let config = meta.config || []; + let configSize = config.length; + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version 0 + flags + 0x03, // descriptor_type + 0x17 + configSize, // length3 + 0x00, 0x01, // es_id + 0x00, // stream_priority + 0x04, // descriptor_type + 0x0F + configSize, // length + 0x40, // codec: mpeg4_audio + 0x15, // stream_type: Audio + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + 0x05 // descriptor_type + ].concat([configSize]).concat(config).concat([0x06, 0x01, 0x02 // GASpecificConfig + ])); + return MP4$1.box(MP4$1.types.esds, data); + } // avc + + + static avc1(meta) { + let avcc = meta.avcc; + const width = meta.codecWidth; + const height = meta.codecHeight; + let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]); + return MP4$1.box(MP4$1.types.avc1, data, MP4$1.box(MP4$1.types.avcC, avcc)); + } // hvc + + + static hvc1(meta) { + let avcc = meta.avcc; + const width = meta.codecWidth; + const height = meta.codecHeight; + let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]); + return MP4$1.box(MP4$1.types.hvc1, data, MP4$1.box(MP4$1.types.hvcC, avcc)); + } // Movie Extends box + + + static mvex(meta) { + return MP4$1.box(MP4$1.types.mvex, MP4$1.trex(meta)); + } // Track Extends box + + + static trex(meta) { + let trackId = meta.id; + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + trackId >>> 24 & 0xFF, // track_ID + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ]); + return MP4$1.box(MP4$1.types.trex, data); + } // Movie fragment box + + + static moof(track, baseMediaDecodeTime) { + return MP4$1.box(MP4$1.types.moof, MP4$1.mfhd(track.sequenceNumber), MP4$1.traf(track, baseMediaDecodeTime)); + } // + + + static mfhd(sequenceNumber) { + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, sequenceNumber >>> 24 & 0xFF, // sequence_number: int32 + sequenceNumber >>> 16 & 0xFF, sequenceNumber >>> 8 & 0xFF, sequenceNumber & 0xFF]); + return MP4$1.box(MP4$1.types.mfhd, data); + } // Track fragment box + + + static traf(track, baseMediaDecodeTime) { + let trackId = track.id; // Track fragment header box + + let tfhd = MP4$1.box(MP4$1.types.tfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags + trackId >>> 24 & 0xFF, // track_ID + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF])); // Track Fragment Decode Time + + let tfdt = MP4$1.box(MP4$1.types.tfdt, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags + baseMediaDecodeTime >>> 24 & 0xFF, // baseMediaDecodeTime: int32 + baseMediaDecodeTime >>> 16 & 0xFF, baseMediaDecodeTime >>> 8 & 0xFF, baseMediaDecodeTime & 0xFF])); + let sdtp = MP4$1.sdtp(track); + let trun = MP4$1.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8); + return MP4$1.box(MP4$1.types.traf, tfhd, tfdt, trun, sdtp); + } // Sample Dependency Type box + + + static sdtp(track) { + let data = new Uint8Array(4 + 1); + let flags = track.flags; + data[4] = flags.isLeading << 6 | flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; + return MP4$1.box(MP4$1.types.sdtp, data); + } // trun + + + static trun(track, offset) { + let dataSize = 12 + 16; + let data = new Uint8Array(dataSize); + offset += 8 + dataSize; + data.set([0x00, 0x00, 0x0F, 0x01, // version(0) & flags + 0x00, 0x00, 0x00, 0x01, // sample_count + offset >>> 24 & 0xFF, // data_offset + offset >>> 16 & 0xFF, offset >>> 8 & 0xFF, offset & 0xFF], 0); + let duration = track.duration; + let size = track.size; + let flags = track.flags; + let cts = track.cts; + data.set([duration >>> 24 & 0xFF, // sample_duration + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, size >>> 24 & 0xFF, // sample_size + size >>> 16 & 0xFF, size >>> 8 & 0xFF, size & 0xFF, flags.isLeading << 2 | flags.dependsOn, // sample_flags + flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.isNonSync, 0x00, 0x00, // sample_degradation_priority + cts >>> 24 & 0xFF, // sample_composition_time_offset + cts >>> 16 & 0xFF, cts >>> 8 & 0xFF, cts & 0xFF], 12); + return MP4$1.box(MP4$1.types.trun, data); + } // mdat + + + static mdat(data) { + return MP4$1.box(MP4$1.types.mdat, data); + } + + } + + MP4$1.init(); + + class MseDecoder extends Emitter { + constructor(player) { + super(); + this.player = player; + this.isAvc = true; + this.mediaSource = new window.MediaSource(); + this.sourceBuffer = null; + this.hasInit = false; + this.isInitInfo = false; + this.cacheTrack = {}; + this.timeInit = false; + this.sequenceNumber = 0; + this.mediaSourceOpen = false; + this.dropping = false; + this.firstRenderTime = null; + this.mediaSourceAppendBufferError = false; + this.mediaSourceAppendBufferFull = false; + this.isDecodeFirstIIframe = false; + this.player.video.$videoElement.src = window.URL.createObjectURL(this.mediaSource); + const { + debug, + events: { + proxy + } + } = player; + proxy(this.mediaSource, 'sourceopen', () => { + this.mediaSourceOpen = true; + this.player.emit(EVENTS.mseSourceOpen); + }); + proxy(this.mediaSource, 'sourceclose', () => { + this.player.emit(EVENTS.mseSourceClose); + }); + player.debug.log('MediaSource', 'init'); + } + + destroy() { + this.stop(); + this.mediaSource = null; + this.mediaSourceOpen = false; + this.sourceBuffer = null; + this.hasInit = false; + this.isInitInfo = false; + this.sequenceNumber = 0; + this.cacheTrack = null; + this.timeInit = false; + this.mediaSourceAppendBufferError = false; + this.mediaSourceAppendBufferFull = false; + this.isDecodeFirstIIframe = false; + this.off(); + this.player.debug.log('MediaSource', 'destroy'); + } + + get state() { + return this.mediaSource && this.mediaSource.readyState; + } + + get isStateOpen() { + return this.state === MEDIA_SOURCE_STATE.open; + } + + get isStateClosed() { + return this.state === MEDIA_SOURCE_STATE.closed; + } + + get isStateEnded() { + return this.state === MEDIA_SOURCE_STATE.ended; + } + + get duration() { + return this.mediaSource && this.mediaSource.duration; + } + + set duration(duration) { + this.mediaSource.duration = duration; + } + + decodeVideo(payload, ts, isIframe, cts) { + const player = this.player; + + if (!player) { + return; + } + + if (!this.hasInit) { + if (isIframe && payload[1] === 0) { + const videoCodec = payload[0] & 0x0F; + player.video.updateVideoInfo({ + encTypeCode: videoCodec + }); // 如果解码出来的是 + + if (videoCodec === VIDEO_ENC_CODE.h265) { + this.emit(EVENTS_ERROR.mediaSourceH265NotSupport); + return; + } + + if (!player._times.decodeStart) { + player._times.decodeStart = now(); + } + + this._decodeConfigurationRecord(payload, ts, isIframe, videoCodec); + + this.hasInit = true; + } + } else { + if (isIframe && payload[1] === 0) { + let config = parseAVCDecoderConfigurationRecord(payload.slice(5)); + const videoInfo = this.player.video.videoInfo; + + if (videoInfo && videoInfo.width && videoInfo.height && config && config.codecWidth && config.codecHeight && (config.codecWidth !== videoInfo.width || config.codecHeight !== videoInfo.height)) { + this.player.debug.warn('MediaSource', `width or height is update, width ${videoInfo.width}-> ${config.codecWidth}, height ${videoInfo.height}-> ${config.codecHeight}`); + this.isInitInfo = false; + this.player.video.init = false; + } + } + + if (!this.isDecodeFirstIIframe && isIframe) { + this.isDecodeFirstIIframe = true; + } + + if (this.isDecodeFirstIIframe) { + if (this.firstRenderTime === null) { + this.firstRenderTime = ts; + } + + const dts = ts - this.firstRenderTime; + + this._decodeVideo(payload, dts, isIframe, cts); + } else { + this.player.debug.warn('MediaSource', 'decodeVideo isDecodeFirstIIframe false'); + } + } + } + + _decodeConfigurationRecord(payload, ts, isIframe, videoCodec) { + let data = payload.slice(5); + let config = {}; + config = parseAVCDecoderConfigurationRecord(data); + const metaData = { + id: 1, + // video tag data + type: 'video', + timescale: 1000, + duration: 0, + avcc: data, + codecWidth: config.codecWidth, + codecHeight: config.codecHeight, + videoType: config.videoType + }; // ftyp + + const metaBox = MP4$1.generateInitSegment(metaData); + this.isAvc = true; + this.appendBuffer(metaBox.buffer); + this.sequenceNumber = 0; + this.cacheTrack = null; + this.timeInit = false; + } // + + + _decodeVideo(payload, dts, isIframe, cts) { + const player = this.player; + let arrayBuffer = payload.slice(5); + let bytes = arrayBuffer.byteLength; // player.debug.log('MediaSource', '_decodeVideo', ts); + + const $video = player.video.$videoElement; + const videoBufferDelay = player._opt.videoBufferDelay; + + if ($video.buffered.length > 1) { + this.removeBuffer($video.buffered.start(0), $video.buffered.end(0)); + this.timeInit = false; + } + + if (this.dropping && dts - this.cacheTrack.dts > videoBufferDelay) { + this.dropping = false; + this.cacheTrack = {}; + } else if (this.cacheTrack && dts >= this.cacheTrack.dts) { + // 需要额外加8个size + let mdatBytes = 8 + this.cacheTrack.size; + let mdatbox = new Uint8Array(mdatBytes); + mdatbox[0] = mdatBytes >>> 24 & 255; + mdatbox[1] = mdatBytes >>> 16 & 255; + mdatbox[2] = mdatBytes >>> 8 & 255; + mdatbox[3] = mdatBytes & 255; + mdatbox.set(MP4$1.types.mdat, 4); + mdatbox.set(this.cacheTrack.data, 8); + this.cacheTrack.duration = dts - this.cacheTrack.dts; // moof + + let moofbox = MP4$1.moof(this.cacheTrack, this.cacheTrack.dts); + let result = new Uint8Array(moofbox.byteLength + mdatbox.byteLength); + result.set(moofbox, 0); + result.set(mdatbox, moofbox.byteLength); // appendBuffer + + this.appendBuffer(result.buffer); + player.handleRender(); + player.updateStats({ + fps: true, + ts: dts, + buf: player.demux && player.demux.delay || 0 + }); + + if (!player._times.videoStart) { + player._times.videoStart = now(); + player.handlePlayToRenderTimes(); + } + } else { + player.debug.log('MediaSource', 'timeInit set false , cacheTrack = {}'); + this.timeInit = false; + this.cacheTrack = {}; + } + + if (!this.cacheTrack) { + this.cacheTrack = {}; + } + + this.cacheTrack.id = 1; + this.cacheTrack.sequenceNumber = ++this.sequenceNumber; + this.cacheTrack.size = bytes; + this.cacheTrack.dts = dts; + this.cacheTrack.cts = cts; + this.cacheTrack.isKeyframe = isIframe; + this.cacheTrack.data = arrayBuffer; // + + this.cacheTrack.flags = { + isLeading: 0, + dependsOn: isIframe ? 2 : 1, + isDependedOn: isIframe ? 1 : 0, + hasRedundancy: 0, + isNonSync: isIframe ? 0 : 1 + }; // + + if (!this.timeInit && $video.buffered.length === 1) { + player.debug.log('MediaSource', 'timeInit set true'); + this.timeInit = true; + $video.currentTime = $video.buffered.end(0); + } + + if (!this.isInitInfo && $video.videoWidth > 0 && $video.videoHeight > 0) { + player.debug.log('MediaSource', `updateVideoInfo: ${$video.videoWidth},${$video.videoHeight}`); + player.video.updateVideoInfo({ + width: $video.videoWidth, + height: $video.videoHeight + }); + player.video.initCanvasViewSize(); + this.isInitInfo = true; + } + } + + appendBuffer(buffer) { + const { + debug, + events: { + proxy + } + } = this.player; + + if (this.sourceBuffer === null) { + this.sourceBuffer = this.mediaSource.addSourceBuffer(MP4_CODECS.avc); + proxy(this.sourceBuffer, 'error', error => { + this.player.emit(EVENTS.mseSourceBufferError, error); // this.dropSourceBuffer(false) + }); + } + + if (this.mediaSourceAppendBufferError) { + debug.error('MediaSource', `this.mediaSourceAppendBufferError is true`); + return; + } + + if (this.mediaSourceAppendBufferFull) { + debug.error('MediaSource', `this.mediaSourceAppendBufferFull is true`); + return; + } + + if (this.sourceBuffer.updating === false && this.isStateOpen) { + try { + this.sourceBuffer.appendBuffer(buffer); + } catch (e) { + debug.warn('MediaSource', 'this.sourceBuffer.appendBuffer()', e.code, e); + + if (e.code === 22) { + // QuotaExceededError + // The SourceBuffer is full, and cannot free space to append additional buffers + this.stop(); + this.mediaSourceAppendBufferFull = true; + this.emit(EVENTS_ERROR.mediaSourceFull); + } else if (e.code === 11) { + // Failed to execute 'appendBuffer' on 'SourceBuffer': The HTMLMediaElement.error attribute is not null. + this.stop(); + this.mediaSourceAppendBufferError = true; + this.emit(EVENTS_ERROR.mediaSourceAppendBufferError); + } else { + debug.error('MediaSource', 'appendBuffer error', e); + this.player.emit(EVENTS.mseSourceBufferError, e); + } + } + + return; + } + + if (this.isStateClosed) { + this.player.emitError(EVENTS_ERROR.mseSourceBufferError, 'mediaSource is not attached to video or mediaSource is closed'); + } else if (this.isStateEnded) { + this.player.emitError(EVENTS_ERROR.mseSourceBufferError, 'mediaSource is closed'); + } else { + if (this.sourceBuffer.updating === true) { + this.player.emit(EVENTS.mseSourceBufferBusy); // this.dropSourceBuffer(true); + } + } + } + + stop() { + this.abortSourceBuffer(); + this.removeSourceBuffer(); + this.endOfStream(); + } + + dropSourceBuffer(isDropping) { + const $video = this.player.video.$videoElement; + this.dropping = isDropping; + + if ($video.buffered.length > 0) { + if ($video.buffered.end(0) - $video.currentTime > 1) { + this.player.debug.warn('MediaSource', 'dropSourceBuffer', `$video.buffered.end(0) is ${$video.buffered.end(0)} - $video.currentTime ${$video.currentTime}`); + $video.currentTime = $video.buffered.end(0); + } + } + } + + removeBuffer(start, end) { + if (this.isStateOpen && this.sourceBuffer.updating === false) { + try { + this.sourceBuffer.remove(start, end); + } catch (e) { + this.player.debug.warn('MediaSource', 'removeBuffer() error', e); + } + } else { + this.player.debug.warn('MediaSource', 'removeBuffer() this.isStateOpen is', this.isStateOpen, 'this.sourceBuffer.updating', this.sourceBuffer.updating); + } + } + + endOfStream() { + // fix: MediaSource endOfStream before demuxer initialization completes (before HAVE_METADATA) is treated as an error + const $videoElement = this.player.video && this.player.video.$videoElement; + + if (this.isStateOpen && $videoElement && $videoElement.readyState >= 1) { + try { + this.mediaSource.endOfStream(); + } catch (e) { + this.player.debug.warn('MediaSource', 'endOfStream() error', e); + } + } + } + + abortSourceBuffer() { + if (this.isStateOpen) { + if (this.sourceBuffer) { + this.sourceBuffer.abort(); + this.sourceBuffer = null; + } + } + } + + removeSourceBuffer() { + if (!this.isStateClosed) { + if (this.mediaSource && this.sourceBuffer) { + try { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } catch (e) { + this.player.debug.warn('MediaSource', 'removeSourceBuffer() error', e); + } + } + } + } + + getSourceBufferUpdating() { + return this.sourceBuffer && this.sourceBuffer.updating; + } + + } + + // tks: https://github.com/richtr/NoSleep.js + const WEBM = "data:video/webm;base64,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"; + const MP4 = "data:video/mp4;base64,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"; // Detect iOS browsers < version 10 + + const oldIOS = () => typeof navigator !== "undefined" && parseFloat(("" + (/CPU.*OS ([0-9_]{3,4})[0-9_]{0,1}|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ""])[1]).replace("undefined", "3_2").replace("_", ".").replace("_", "")) < 10 && !window.MSStream; // Detect native Wake Lock API support + + + const nativeWakeLock = () => "wakeLock" in navigator; + + class NoSleep { + constructor(player) { + this.player = player; + this.enabled = false; + + if (nativeWakeLock()) { + this._wakeLock = null; + + const handleVisibilityChange = () => { + if (this._wakeLock !== null && document.visibilityState === "visible") { + this.enable(); + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + document.addEventListener("fullscreenchange", handleVisibilityChange); + } else if (oldIOS()) { + this.noSleepTimer = null; + } else { + // Set up no sleep video element + this.noSleepVideo = document.createElement("video"); + this.noSleepVideo.setAttribute("title", "No Sleep"); + this.noSleepVideo.setAttribute("playsinline", ""); + + this._addSourceToVideo(this.noSleepVideo, "webm", WEBM); + + this._addSourceToVideo(this.noSleepVideo, "mp4", MP4); + + this.noSleepVideo.addEventListener("loadedmetadata", () => { + if (this.noSleepVideo.duration <= 1) { + // webm source + this.noSleepVideo.setAttribute("loop", ""); + } else { + // mp4 source + this.noSleepVideo.addEventListener("timeupdate", () => { + if (this.noSleepVideo.currentTime > 0.5) { + this.noSleepVideo.currentTime = Math.random(); + } + }); + } + }); + } + } + + _addSourceToVideo(element, type, dataURI) { + var source = document.createElement("source"); + source.src = dataURI; + source.type = `video/${type}`; + element.appendChild(source); + } + + get isEnabled() { + return this.enabled; + } + + enable() { + const debug = this.player.debug; + + if (nativeWakeLock()) { + return navigator.wakeLock.request("screen").then(wakeLock => { + this._wakeLock = wakeLock; + this.enabled = true; + debug.log('wakeLock', 'Wake Lock active.'); + + this._wakeLock.addEventListener("release", () => { + // ToDo: Potentially emit an event for the page to observe since + // Wake Lock releases happen when page visibility changes. + // (https://web.dev/wakelock/#wake-lock-lifecycle) + debug.log('wakeLock', 'Wake Lock released.'); + }); + }).catch(err => { + this.enabled = false; + debug.error('wakeLock', `${err.name}, ${err.message}`); + throw err; + }); + } else if (oldIOS()) { + this.disable(); + this.noSleepTimer = window.setInterval(() => { + if (!document.hidden) { + window.location.href = window.location.href.split("#")[0]; + window.setTimeout(window.stop, 0); + } + }, 15000); + this.enabled = true; + return Promise.resolve(); + } else { + let playPromise = this.noSleepVideo.play(); + return playPromise.then(res => { + this.enabled = true; + return res; + }).catch(err => { + this.enabled = false; + throw err; + }); + } + } + + disable() { + const debug = this.player.debug; + + if (nativeWakeLock()) { + if (this._wakeLock) { + this._wakeLock.release(); + } + + this._wakeLock = null; + } else if (oldIOS()) { + if (this.noSleepTimer) { + debug.warn('wakeLock', 'NoSleep now disabled for older iOS devices.'); + window.clearInterval(this.noSleepTimer); + this.noSleepTimer = null; + } + } else { + this.noSleepVideo.pause(); + } + + this.enabled = false; + } + + } + + class Player extends Emitter { + constructor(container, options) { + super(); + this.$container = container; + this._opt = Object.assign({}, DEFAULT_PLAYER_OPTIONS, options); + this.debug = new Debug(this); // + + if (this._opt.useWCS) { + this._opt.useWCS = supportWCS(); + } // + + + if (this._opt.useMSE) { + this._opt.useMSE = supportMSE(); + } // + + + if (this._opt.wcsUseVideoRender) { + this._opt.wcsUseVideoRender = supportMediaStreamTrack(); + } // 如果使用mse则强制不允许 webcodecs + + + if (this._opt.useMSE) { + if (this._opt.useWCS) { + this.debug.log('Player', 'useWCS set true->false'); + } + + if (!this._opt.forceNoOffscreen) { + this.debug.log('Player', 'forceNoOffscreen set false->true'); + } + + this._opt.useWCS = false; + this._opt.forceNoOffscreen = true; + } + + if (!this._opt.forceNoOffscreen) { + if (!supportOffscreenV2()) { + this._opt.forceNoOffscreen = true; + this._opt.useOffscreen = false; + } else { + this._opt.useOffscreen = true; + } + } + + if (!this._opt.hasAudio) { + this._opt.operateBtns.audio = false; + } + + this._opt.hasControl = this._hasControl(); // + + this._loading = false; + this._playing = false; + this._hasLoaded = false; // + + this._checkHeartTimeout = null; + this._checkLoadingTimeout = null; + this._checkStatsInterval = null; // + + this._startBpsTime = null; + this._isPlayingBeforePageHidden = false; + this._stats = { + buf: 0, + // 当前缓冲区时长,单位毫秒, + fps: 0, + // 当前视频帧率 + abps: 0, + // 当前音频码率,单位bit + vbps: 0, + // 当前视频码率,单位bit + ts: 0 // 当前视频帧pts,单位毫秒 + + }; // 各个步骤的时间统计 + + this._times = initPlayTimes(); // + + this._videoTimestamp = 0; + this._audioTimestamp = 0; + property$1(this); + this.events = new Events(this); + this.video = new Video(this); + + if (this._opt.hasAudio) { + this.audio = new Audio(this); + } + + this.recorder = new Recorder(this); + + if (!this._onlyMseOrWcsVideo()) { + this.decoderWorker = new DecoderWorker(this); + } else { + this.loaded = true; + } + + this.stream = null; + this.demux = null; + this._lastVolume = null; + + if (this._opt.useWCS) { + this.webcodecsDecoder = new WebcodecsDecoder(this); + this.loaded = true; + } + + if (this._opt.useMSE) { + this.mseDecoder = new MseDecoder(this); + this.loaded = true; + } // + + + this.control = new Control(this); + + if (isMobile()) { + this.keepScreenOn = new NoSleep(this); + } + + events$1(this); + observer(this); + + if (this._opt.useWCS) { + this.debug.log('Player', 'use WCS'); + } + + if (this._opt.useMSE) { + this.debug.log('Player', 'use MSE'); + } + + if (this._opt.useOffscreen) { + this.debug.log('Player', 'use offscreen'); + } + + this.debug.log('Player options', this._opt); + } + + destroy() { + this._loading = false; + this._playing = false; + this._hasLoaded = false; + this._lastVolume = null; + this._times = initPlayTimes(); + + if (this.decoderWorker) { + this.decoderWorker.destroy(); + this.decoderWorker = null; + } + + if (this.video) { + this.video.destroy(); + this.video = null; + } + + if (this.audio) { + this.audio.destroy(); + this.audio = null; + } + + if (this.stream) { + this.stream.destroy(); + this.stream = null; + } + + if (this.recorder) { + this.recorder.destroy(); + this.recorder = null; + } + + if (this.control) { + this.control.destroy(); + this.control = null; + } + + if (this.webcodecsDecoder) { + this.webcodecsDecoder.destroy(); + this.webcodecsDecoder = null; + } + + if (this.mseDecoder) { + this.mseDecoder.destroy(); + this.mseDecoder = null; + } + + if (this.demux) { + this.demux.destroy(); + this.demux = null; + } + + if (this.events) { + this.events.destroy(); + this.events = null; + } + + this.clearCheckHeartTimeout(); + this.clearCheckLoadingTimeout(); + this.clearStatsInterval(); // + + this.releaseWakeLock(); + this.keepScreenOn = null; // reset stats + + this.resetStats(); + this._audioTimestamp = 0; + this._videoTimestamp = 0; // 其他没法解耦的,通过 destroy 方式 + + this.emit('destroy'); // 接触所有绑定事件 + + this.off(); + this.debug.log('play', 'destroy end'); + } + + set fullscreen(value) { + if (isMobile() && this._opt.useWebFullScreen) { + this.emit(EVENTS.webFullscreen, value); + setTimeout(() => { + this.updateOption({ + rotate: value ? 270 : 0 + }); + this.resize(); + }, 10); + } else { + this.emit(EVENTS.fullscreen, value); + } + } + + get fullscreen() { + return isFullScreen() || this.webFullscreen; + } + + set webFullscreen(value) { + this.emit(EVENTS.webFullscreen, value); + } + + get webFullscreen() { + return this.$container.classList.contains('jessibuca-fullscreen-web'); + } + + set loaded(value) { + this._hasLoaded = value; + } + + get loaded() { + return this._hasLoaded; + } // + + + set playing(value) { + if (value) { + // 将loading 设置为 false + this.loading = false; + } + + if (this.playing !== value) { + this._playing = value; + this.emit(EVENTS.playing, value); + this.emit(EVENTS.volumechange, this.volume); + + if (value) { + this.emit(EVENTS.play); + } else { + this.emit(EVENTS.pause); + } + } + } + + get playing() { + return this._playing; + } + + get volume() { + return this.audio && this.audio.volume || 0; + } + + set volume(value) { + if (value !== this.volume) { + this.audio && this.audio.setVolume(value); + this._lastVolume = value; + } + } + + get lastVolume() { + return this._lastVolume; + } + + set loading(value) { + if (this.loading !== value) { + this._loading = value; + this.emit(EVENTS.loading, this._loading); + } + } + + get loading() { + return this._loading; + } + + set recording(value) { + if (value) { + if (this.playing) { + this.recorder && this.recorder.startRecord(); + } + } else { + this.recorder && this.recorder.stopRecordAndSave(); + } + } + + get recording() { + return this.recorder ? this.recorder.recording : false; + } + + set audioTimestamp(value) { + if (value === null) { + return; + } + + this._audioTimestamp = value; + } // + + + get audioTimestamp() { + return this._audioTimestamp; + } // + + + set videoTimestamp(value) { + if (value === null) { + return; + } + + this._videoTimestamp = value; // just for wasm + + if (!this._opt.useWCS && !this._opt.useMSE) { + if (this.audioTimestamp && this.videoTimestamp) { + this.audio && this.audio.emit(EVENTS.videoSyncAudio, { + audioTimestamp: this.audioTimestamp, + videoTimestamp: this.videoTimestamp, + diff: this.audioTimestamp - this.videoTimestamp + }); + } + } + } // + + + get videoTimestamp() { + return this._videoTimestamp; + } + + get isDebug() { + return this._opt.debug === true; + } + /** + * + * @param options + */ + + + updateOption(options) { + this._opt = Object.assign({}, this._opt, options); + } + /** + * + * @returns {Promise} + */ + + + init() { + return new Promise((resolve, reject) => { + if (!this.stream) { + this.stream = new Stream(this); + } + + if (!this.audio) { + if (this._opt.hasAudio) { + this.audio = new Audio(this); + } + } + + if (!this.demux) { + this.demux = new Demux(this); + } + + if (this._opt.useWCS) { + if (!this.webcodecsDecoder) { + this.webcodecsDecoder = new WebcodecsDecoder(this); + } + } + + if (this._opt.useMSE) { + if (!this.mseDecoder) { + this.mseDecoder = new MseDecoder(this); + } + } + + if (!this.decoderWorker && !this._onlyMseOrWcsVideo()) { + this.decoderWorker = new DecoderWorker(this); + this.once(EVENTS.decoderWorkerInit, () => { + resolve(); + }); + } else { + resolve(); + } + }); + } + /** + * + * @param url + * @returns {Promise} + */ + + + play(url, options) { + return new Promise((resolve, reject) => { + if (!url && !this._opt.url) { + return reject(); + } + + this.loading = true; + this.playing = false; + this._times.playInitStart = now(); + + if (!url) { + url = this._opt.url; + } + + this._opt.url = url; + this.clearCheckHeartTimeout(); + this.init().then(() => { + this._times.playStart = now(); // + + if (this._opt.isNotMute) { + this.mute(false); + } + + if (this.webcodecsDecoder) { + this.webcodecsDecoder.once(EVENTS_ERROR.webcodecsH265NotSupport, () => { + this.emit(EVENTS_ERROR.webcodecsH265NotSupport); + + if (!this._opt.autoWasm) { + this.emit(EVENTS.error, EVENTS_ERROR.webcodecsH265NotSupport); + } + }); + } + + if (this.mseDecoder) { + this.mseDecoder.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => { + this.emit(EVENTS_ERROR.mediaSourceH265NotSupport); + + if (!this._opt.autoWasm) { + this.emit(EVENTS.error, EVENTS_ERROR.mediaSourceH265NotSupport); + } + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceFull, () => { + this.emitError(EVENTS_ERROR.mediaSourceFull); + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceAppendBufferError, () => { + this.emitError(EVENTS_ERROR.mediaSourceAppendBufferError); + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceBufferListLarge, () => { + this.emitError(EVENTS_ERROR.mediaSourceBufferListLarge); + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout, () => { + this.emitError(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout); + }); + } + + this.enableWakeLock(); + this.stream.fetchStream(url, options); // + + this.checkLoadingTimeout(); // fetch error + + this.stream.once(EVENTS_ERROR.fetchError, error => { + reject(error); + }); // ws + + this.stream.once(EVENTS_ERROR.websocketError, error => { + reject(error); + }); // stream end + + this.stream.once(EVENTS.streamEnd, () => { + reject(); + }); // success + + this.stream.once(EVENTS.streamSuccess, () => { + resolve(); + this._times.streamResponse = now(); // + + this.video.play(); + this.checkStatsInterval(); + }); + }).catch(e => { + reject(e); + }); + }); + } + /** + * + */ + + + close() { + return new Promise((resolve, reject) => { + this._close().then(() => { + this.video && this.video.clearView(); + resolve(); + }); + }); + } + + resumeAudioAfterPause() { + if (this.lastVolume) { + this.volume = this.lastVolume; + } + } + + _close() { + return new Promise((resolve, reject) => { + // + if (this.stream) { + this.stream.destroy(); + this.stream = null; + } + + if (this.demux) { + this.demux.destroy(); + this.demux = null; + } // + + + if (this.decoderWorker) { + this.decoderWorker.destroy(); + this.decoderWorker = null; + } + + if (this.webcodecsDecoder) { + this.webcodecsDecoder.destroy(); + this.webcodecsDecoder = null; + } + + if (this.mseDecoder) { + this.mseDecoder.destroy(); + this.mseDecoder = null; + } + + if (this.audio) { + this.audio.destroy(); + this.audio = null; + } + + this.clearCheckHeartTimeout(); + this.clearCheckLoadingTimeout(); + this.clearStatsInterval(); + this.playing = false; + this.loading = false; + this.recording = false; + + if (this.video) { + this.video.resetInit(); + this.video.pause(true); + } // release lock + + + this.releaseWakeLock(); // reset stats + + this.resetStats(); // + + this._audioTimestamp = 0; + this._videoTimestamp = 0; // + + this._times = initPlayTimes(); // + + setTimeout(() => { + resolve(); + }, 0); + }); + } + /** + * + * @param flag {boolean} 是否清除画面 + * @returns {Promise} + */ + + + pause() { + let flag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + + if (flag) { + return this.close(); + } else { + return this._close(); + } + } + /** + * + * @param flag + */ + + + mute(flag) { + this.audio && this.audio.mute(flag); + } + /** + * + */ + + + resize() { + this.video.resize(); + } + /** + * + * @param fileName + * @param fileType + */ + + + startRecord(fileName, fileType) { + if (this.recording) { + return; + } + + this.recorder.setFileName(fileName, fileType); + this.recording = true; + } + /** + * + */ + + + stopRecordAndSave() { + if (this.recording) { + this.recording = false; + } + } + + _hasControl() { + let result = false; + let hasBtnShow = false; + Object.keys(this._opt.operateBtns).forEach(key => { + if (this._opt.operateBtns[key]) { + hasBtnShow = true; + } + }); + + if (this._opt.showBandwidth || this._opt.text || hasBtnShow) { + result = true; + } + + return result; + } + + _onlyMseOrWcsVideo() { + return this._opt.hasAudio === false && (this._opt.useMSE || this._opt.useWCS && !this._opt.useOffscreen); + } + + checkHeart() { + this.clearCheckHeartTimeout(); + this.checkHeartTimeout(); + } // 心跳检查,如果渲染间隔暂停了多少时间之后,就会抛出异常 + + + checkHeartTimeout() { + this._checkHeartTimeout = setTimeout(() => { + if (this.playing) { + // check again + if (this._stats.fps !== 0) { + return; + } + + this.pause().then(() => { + this.emit(EVENTS.timeout, EVENTS.delayTimeout); + this.emit(EVENTS.delayTimeout); + }); + } + }, this._opt.heartTimeout * 1000); + } + + checkStatsInterval() { + this._checkStatsInterval = setInterval(() => { + this.updateStats(); + }, 1000); + } // + + + clearCheckHeartTimeout() { + if (this._checkHeartTimeout) { + clearTimeout(this._checkHeartTimeout); + this._checkHeartTimeout = null; + } + } // loading 等待时间 + + + checkLoadingTimeout() { + this._checkLoadingTimeout = setTimeout(() => { + // check again + if (this.playing) { + return; + } + + this.pause().then(() => { + this.emit(EVENTS.timeout, EVENTS.loadingTimeout); + this.emit(EVENTS.loadingTimeout); + }); + }, this._opt.loadingTimeout * 1000); + } + + clearCheckLoadingTimeout() { + if (this._checkLoadingTimeout) { + clearTimeout(this._checkLoadingTimeout); + this._checkLoadingTimeout = null; + } + } + + clearStatsInterval() { + if (this._checkStatsInterval) { + clearInterval(this._checkStatsInterval); + this._checkStatsInterval = null; + } + } + + handleRender() { + if (this.loading) { + this.emit(EVENTS.start); + this.loading = false; + this.clearCheckLoadingTimeout(); + } + + if (!this.playing) { + this.playing = true; + } + + this.checkHeart(); + } // + + + updateStats(options) { + options = options || {}; + + if (!this._startBpsTime) { + this._startBpsTime = now(); + } + + if (isNotEmpty(options.ts)) { + this._stats.ts = options.ts; + } + + if (isNotEmpty(options.buf)) { + this._stats.buf = options.buf; + } + + if (options.fps) { + this._stats.fps += 1; + } + + if (options.abps) { + this._stats.abps += options.abps; + } + + if (options.vbps) { + this._stats.vbps += options.vbps; + } + + const _nowTime = now(); + + const timestamp = _nowTime - this._startBpsTime; + + if (timestamp < 1 * 1000) { + return; + } + + this.emit(EVENTS.stats, this._stats); + this.emit(EVENTS.performance, fpsStatus(this._stats.fps)); + this._stats.fps = 0; + this._stats.abps = 0; + this._stats.vbps = 0; + this._startBpsTime = _nowTime; + } + + resetStats() { + this._startBpsTime = null; + this._stats = { + buf: 0, + //ms + fps: 0, + abps: 0, + vbps: 0, + ts: 0 + }; + } + + enableWakeLock() { + if (this._opt.keepScreenOn) { + this.keepScreenOn && this.keepScreenOn.enable(); + } + } + + releaseWakeLock() { + if (this._opt.keepScreenOn) { + this.keepScreenOn && this.keepScreenOn.disable(); + } + } + + handlePlayToRenderTimes() { + const _times = this._times; + _times.playTimestamp = _times.playStart - _times.playInitStart; + _times.streamTimestamp = _times.streamStart - _times.playStart; + _times.streamResponseTimestamp = _times.streamResponse - _times.streamStart; + _times.demuxTimestamp = _times.demuxStart - _times.streamResponse; + _times.decodeTimestamp = _times.decodeStart - _times.demuxStart; + _times.videoTimestamp = _times.videoStart - _times.decodeStart; + _times.allTimestamp = _times.videoStart - _times.playInitStart; + this.emit(EVENTS.playToRenderTimes, _times); + } + + getOption() { + return this._opt; + } + + emitError(errorType) { + let message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + this.emit(EVENTS.error, errorType, message); + this.emit(errorType, message); + } + + } + + class Jessibuca extends Emitter { + constructor(options) { + super(); + let _opt = options; + let $container = options.container; + + if (typeof options.container === 'string') { + $container = document.querySelector(options.container); + } + + if (!$container) { + throw new Error('Jessibuca need container option'); + } // check container node name + + + if ($container.nodeName === 'CANVAS' || $container.nodeName === 'VIDEO') { + throw new Error(`Jessibuca container type can not be ${$container.nodeName} type`); + } + + if (_opt.videoBuffer >= _opt.heartTimeout) { + throw new Error(`Jessibuca videoBuffer ${_opt.videoBuffer}s must be less than heartTimeout ${_opt.heartTimeout}s`); + } + + $container.classList.add('jessibuca-container'); + delete _opt.container; // 禁用离屏渲染 + + _opt.forceNoOffscreen = true; // 移动端不支持自动关闭控制栏 + + if (isMobile()) { + _opt.controlAutoHide = false; + } // s -> ms + + + if (isNotEmpty(_opt.videoBuffer)) { + _opt.videoBuffer = Number(_opt.videoBuffer) * 1000; + } // setting + + + if (isNotEmpty(_opt.timeout)) { + if (isEmpty(_opt.loadingTimeout)) { + _opt.loadingTimeout = _opt.timeout; + } + + if (isEmpty(_opt.heartTimeout)) { + _opt.heartTimeout = _opt.timeout; + } + } + + this._opt = _opt; + this.$container = $container; + this._loadingTimeoutReplayTimes = 0; + this._heartTimeoutReplayTimes = 0; + this.events = new Events(this); + this.debug = new Debug(this); + + this._initPlayer($container, _opt); + } + /** + * + */ + + + destroy() { + if (this.events) { + this.events.destroy(); + this.events = null; + } + + if (this.player) { + this.player.destroy(); + this.player = null; + } + + this.$container = null; + this._opt = null; + this._loadingTimeoutReplayTimes = 0; + this._heartTimeoutReplayTimes = 0; + this.off(); + } + + _initPlayer($container, options) { + this.player = new Player($container, options); + this.debug.log('jessibuca', '_initPlayer', this.player.getOption()); + + this._bindEvents(); + } + + _resetPlayer() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + this.player.destroy(); + this.player = null; + this._opt = Object.assign(this._opt, options); + this._opt.url = ''; // reset url + + this._initPlayer(this.$container, this._opt); + } + + _bindEvents() { + // 对外的事件 + Object.keys(JESSIBUCA_EVENTS).forEach(key => { + this.player.on(JESSIBUCA_EVENTS[key], value => { + this.emit(key, value); + }); + }); + } + /** + * 是否开启控制台调试打印 + * @param value {Boolean} + */ + + + setDebug(value) { + this.player.updateOption({ + debug: !!value + }); + } + /** + * + */ + + + mute() { + this.player.mute(true); + } + /** + * + */ + + + cancelMute() { + this.player.mute(false); + } + /** + * + * @param value {number} + */ + + + setVolume(value) { + this.player.volume = value; + } + /** + * + */ + + + audioResume() { + this.player.audio && this.player.audio.audioEnabled(true); + } + /** + * 设置超时时长, 单位秒 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件 + * @param value {number} + */ + + + setTimeout(time) { + time = Number(time); + this.player.updateOption({ + timeout: time, + loadingTimeout: time, + heartTimeout: time + }); + } + /** + * + * @param type {number}: 0,1,2 + */ + + + setScaleMode(type) { + type = Number(type); + let options = { + isFullResize: false, + isResize: false + }; + + switch (type) { + case SCALE_MODE_TYPE.full: + options.isFullResize = false; + options.isResize = false; + break; + + case SCALE_MODE_TYPE.auto: + options.isFullResize = false; + options.isResize = true; + break; + + case SCALE_MODE_TYPE.fullAuto: + options.isFullResize = true; + options.isResize = true; + break; + } + + this.player.updateOption(options); + this.resize(); + } + /** + * + * @returns {Promise} + */ + + + pause() { + return new Promise((resolve, reject) => { + if (this.player) { + this.player.pause().then(() => { + resolve(); + }).catch(e => { + reject(e); + }); + } else { + reject('player is null'); + } + }); + } + /** + * + */ + + + close() { + // clear url + this._opt.url = ''; + this._opt.playOptions = {}; + return this.player.close(); + } + /** + * + */ + + + clearView() { + this.player.video.clearView(); + } + /** + * + * @param url {string} + * @param options {object} + * @returns {Promise} + */ + + + play(url) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return new Promise((resolve, reject) => { + if (!url && !this._opt.url) { + this.emit(EVENTS.error, EVENTS_ERROR.playError); + reject('play url is empty'); + return; + } + + if (url) { + // url 相等的时候。 + if (this._opt.url) { + // 存在相同的 url + if (url === this._opt.url) { + // 正在播放 + if (this.player.playing) { + resolve(); + } else { + // pause -> play + this.clearView(); + this.player.play(this._opt.url, this._opt.playOptions).then(() => { + resolve(); // 恢复下之前的音量 + + this.player.resumeAudioAfterPause(); + }).catch(e => { + this.debug.warn('jessibuca', 'pause -> play and play error', e); + this.player.pause().then(() => { + reject(e); + }); + }); + } + } else { + // url 发生改变了 + this.player.pause().then(() => { + // 清除 画面 + this.clearView(); + + this._play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('jessibuca', 'this._play error', e); + reject(e); + }); + }).catch(e => { + this.debug.warn('jessibuca', 'this._opt.url is null and pause error', e); + reject(e); + }); + } + } else { + this._play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('jessibuca', 'this._play error', e); + reject(e); + }); + } + } else { + // url 不存在的时候 + // 就是从 play -> pause -> play + this.player.play(this._opt.url, this._opt.playOptions).then(() => { + resolve(); // 恢复下之前的音量 + + this.player.resumeAudioAfterPause(); + }).catch(e => { + this.debug.warn('jessibuca', 'url is null and play error', e); + this.player.pause().then(() => { + reject(e); + }); + }); + } + }); + } + /** + * + * @param url {string} + * @param options {object} + * @returns {Promise} + * @private + */ + + + _play(url) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return new Promise((resolve, reject) => { + this._opt.url = url; + this._opt.playOptions = options; // 新的url + + const isHttp = url.indexOf("http") === 0; // + + const protocol = isHttp ? PLAYER_PLAY_PROTOCOL.fetch : PLAYER_PLAY_PROTOCOL.websocket; // + + const demuxType = isHttp || url.indexOf(".flv") !== -1 || this._opt.isFlv ? DEMUX_TYPE.flv : DEMUX_TYPE.m7s; + this.player.updateOption({ + protocol, + demuxType + }); + this.player.once(EVENTS_ERROR.webglAlignmentError, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'webglAlignmentError'); + + this._resetPlayer({ + openWebglAlignment: true + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'webglAlignmentError and play success'); + }).catch(() => { + // reject(); + this.debug.log('Jessibuca', 'webglAlignmentError and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => { + this.pause().then(() => { + if (this.player._opt.autoWasm) { + this.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play'); + + this._resetPlayer({ + useMSE: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play success'); + }).catch(() => { + // reject(); + this.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play error'); + }); + } + }); + }); // media source full error + + this.player.once(EVENTS_ERROR.mediaSourceFull, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source full'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source full and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source full and reset player and play error'); + }); + }); + }); // media source append buffer error + + this.player.once(EVENTS_ERROR.mediaSourceAppendBufferError, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source append buffer error'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source append buffer error and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source append buffer error and reset player and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mediaSourceBufferListLarge, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source buffer list large'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source buffer list large and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source buffer list large and reset player and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source append buffer end timeout'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source append buffer end timeout and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source append buffer end timeout and reset player and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mseSourceBufferError, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'mseSourceBufferError close success'); + }); + }); // + + this.player.once(EVENTS_ERROR.webcodecsH265NotSupport, () => { + this.pause().then(() => { + if (this.player._opt.autoWasm) { + this.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play'); + + this._resetPlayer({ + useWCS: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play error'); + }); + } + }); + }); // webcodecs + + this.player.once(EVENTS_ERROR.webcodecsWidthOrHeightChange, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'webcodecs Width Or Height Change reset player and play'); + + this._resetPlayer({ + useWCS: true + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'webcodecs Width Or Height Change reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'webcodecs Width Or Height Change reset player and play error'); + }); + }); + }); // webcodecs + + this.player.once(EVENTS_ERROR.webcodecsDecodeError, () => { + this.pause().then(() => { + if (this.player._opt.autoWasm) { + this.debug.log('Jessibuca', 'webcodecs decode error reset player and play'); + + this._resetPlayer({ + useWCS: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'webcodecs decode error reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'webcodecs decode error reset player and play error'); + }); + } + }); + }); // wasm。 + + this.player.once(EVENTS_ERROR.wasmDecodeError, () => { + if (this.player._opt.wasmDecodeErrorReplay) { + this.pause().then(() => { + this.debug.log('Jessibuca', 'wasm decode error and reset player and play'); + + this._resetPlayer({ + useWCS: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'wasm decode error and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'wasm decode error and reset player and play error'); + }); + }); + } + }); // 监听 delay timeout + + this.player.on(EVENTS.delayTimeout, () => { + if (this.player._opt.heartTimeoutReplay && (this._heartTimeoutReplayTimes < this.player._opt.heartTimeoutReplayTimes || this.player._opt.heartTimeoutReplayTimes === -1)) { + this.debug.log('Jessibuca', `delay timeout replay time is ${this._heartTimeoutReplayTimes}`); + this._heartTimeoutReplayTimes += 1; + this.play(url, options).then(() => { + // resolve(); + this._heartTimeoutReplayTimes = 0; + }).catch(() => {// reject(); + }); + } + }); // 监听 loading timeout + + this.player.on(EVENTS.loadingTimeout, () => { + if (this.player._opt.loadingTimeoutReplay && (this._loadingTimeoutReplayTimes < this.player._opt.loadingTimeoutReplayTimes || this.player._opt.loadingTimeoutReplayTimes === -1)) { + this.debug.log('Jessibuca', `loading timeout replay time is ${this._loadingTimeoutReplayTimes}`); + this._loadingTimeoutReplayTimes += 1; + this.play(url, options).then(() => { + // resolve(); + this._loadingTimeoutReplayTimes = 0; + }).catch(() => {// reject(); + }); + } + }); + + if (this.hasLoaded()) { + this.player.play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('Jessibuca', 'hasLoaded and play error', e); + this.player && this.player.pause().then(() => { + reject(e); + }); + }); + } else { + this.player.once(EVENTS.decoderWorkerInit, () => { + this.player.play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('Jessibuca', 'decoderWorkerInit and play error', e); + this.player && this.player.pause().then(() => { + reject(e); + }); + }); + }); + } + }); + } + /** + * + */ + + + resize() { + this.player.resize(); + } + /** + * + * @param time {number} s + */ + + + setBufferTime(time) { + time = Number(time); // s -> ms + + this.player.updateOption({ + videoBuffer: time * 1000 + }); // update worker config + + this.player.decoderWorker && this.player.decoderWorker.updateWorkConfig({ + key: 'videoBuffer', + value: time * 1000 + }); + } + /** + * + * @param deg {number} + */ + + + setRotate(deg) { + deg = parseInt(deg, 10); + const list = [0, 90, 180, 270]; + + if (this._opt.rotate === deg || list.indexOf(deg) === -1) { + return; + } + + this.player.updateOption({ + rotate: deg + }); + this.resize(); + } + /** + * + * @returns {boolean} + */ + + + hasLoaded() { + return this.player.loaded; + } + /** + * + */ + + + setKeepScreenOn() { + this.player.updateOption({ + keepScreenOn: true + }); + } + /** + * + * @param flag {Boolean} + */ + + + setFullscreen(flag) { + const fullscreen = !!flag; + + if (this.player.fullscreen !== fullscreen) { + this.player.fullscreen = fullscreen; + } + } + /** + * + * @param filename {string} + * @param format {string} + * @param quality {number} + * @param type {string} download,base64,blob + */ + + + screenshot(filename, format, quality, type) { + if (!this.player.video) { + return ''; + } + + return this.player.video.screenshot(filename, format, quality, type); + } + /** + * + * @param fileName {string} + * @param fileType {string} + * @returns {Promise} + */ + + + startRecord(fileName, fileType) { + return new Promise((resolve, reject) => { + if (this.player.playing) { + this.player.startRecord(fileName, fileType); + resolve(); + } else { + reject(); + } + }); + } + + stopRecordAndSave() { + if (this.player.recording) { + this.player.stopRecordAndSave(); + } + } + /** + * + * @returns {Boolean} + */ + + + isPlaying() { + return this.player ? this.player.playing : false; + } + /** + * 是否静音状态 + * @returns {Boolean} + */ + + + isMute() { + return this.player.audio ? this.player.audio.isMute : true; + } + /** + * 是否在录制视频 + * @returns {*} + */ + + + isRecording() { + return this.player.recorder.recording; + } + + } + + _defineProperty(Jessibuca, "ERROR", EVENTS_ERROR); + + _defineProperty(Jessibuca, "TIMEOUT", { + loadingTimeout: EVENTS.loadingTimeout, + delayTimeout: EVENTS.delayTimeout + }); + + window.Jessibuca = Jessibuca; + + return Jessibuca; + +})); +//# sourceMappingURL=jessibuca.js.map +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.jessibuca = factory()); +})(this, (function () { + 'use strict'; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function unwrapExports(x) { + return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; + } + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var defineProperty = createCommonjsModule(function (module) { + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true + }); + } else { + obj[key] = value; + } + + return obj; + } + + module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports; + }); + + var _defineProperty = unwrapExports(defineProperty); + + // 播放协议 + const PLAYER_PLAY_PROTOCOL = { + websocket: 0, + fetch: 1, + webrtc: 2 + }; + const DEMUX_TYPE = { + flv: 'flv', + m7s: 'm7s' + }; + const FILE_SUFFIX = { + mp4: 'mp4', + webm: 'webm' + }; + + const DEFAULT_PLAYER_OPTIONS = { + videoBuffer: 1000, + //1000ms 1 second + videoBufferDelay: 1000, + // 1000ms + isResize: true, + isFullResize: false, + // + isFlv: false, + debug: false, + hotKey: false, + // 快捷键 + loadingTimeout: 10, + // loading timeout + heartTimeout: 5, + // heart timeout + timeout: 10, + // second + loadingTimeoutReplay: true, + // loading timeout replay. default is true + heartTimeoutReplay: true, + // heart timeout replay. + loadingTimeoutReplayTimes: 3, + // loading timeout replay fail times + heartTimeoutReplayTimes: 3, + // heart timeout replay fail times + supportDblclickFullscreen: false, + // support double click toggle fullscreen + showBandwidth: false, + // show band width + keepScreenOn: false, + // + isNotMute: false, + // + hasAudio: true, + // has audio + hasVideo: true, + // has video + operateBtns: { + fullscreen: false, + screenshot: false, + play: false, + audio: false, + record: false + }, + controlAutoHide: false, + // control auto hide + hasControl: false, + loadingText: '', + // loading Text + background: '', + decoder: 'decoder.js', + url: '', + // play url + rotate: 0, + // + // text: '', + forceNoOffscreen: true, + // 默认是不采用 + hiddenAutoPause: false, + // + protocol: PLAYER_PLAY_PROTOCOL.fetch, + demuxType: DEMUX_TYPE.flv, + // demux type + useWCS: false, + // + wcsUseVideoRender: true, + // 默认设置为true + useMSE: false, + // + useOffscreen: false, + // + autoWasm: true, + // 自动降级到 wasm 模式 + wasmDecodeErrorReplay: true, + // 解码失败重新播放。 + openWebglAlignment: false, + // https://github.com/langhuihui/jessibuca/issues/152 + wasmDecodeAudioSyncVideo: false, + // wasm 解码之后音视频同步 + recordType: FILE_SUFFIX.webm, + useWebFullScreen: false // use web full screen + + }; + const WORKER_CMD_TYPE = { + init: 'init', + initVideo: 'initVideo', + render: 'render', + playAudio: 'playAudio', + initAudio: 'initAudio', + kBps: 'kBps', + decode: 'decode', + audioCode: 'audioCode', + videoCode: 'videoCode', + wasmError: 'wasmError' + }; + const WASM_ERROR = { + invalidNalUnitSize: 'Invalid NAL unit size' // errorSplittingTheInputIntoNALUnits: 'Error splitting the input into NAL units' + + }; + const MEDIA_TYPE = { + audio: 1, + video: 2 + }; + const FLV_MEDIA_TYPE = { + audio: 8, + video: 9 + }; + const WORKER_SEND_TYPE = { + init: 'init', + decode: 'decode', + audioDecode: 'audioDecode', + videoDecode: 'videoDecode', + close: 'close', + updateConfig: 'updateConfig' + }; // + + const EVENTS = { + fullscreen: 'fullscreen$2', + webFullscreen: 'webFullscreen', + decoderWorkerInit: 'decoderWorkerInit', + play: 'play', + playing: 'playing', + pause: 'pause', + mute: 'mute', + load: 'load', + loading: 'loading', + videoInfo: 'videoInfo', + timeUpdate: 'timeUpdate', + audioInfo: "audioInfo", + log: 'log', + error: "error", + kBps: 'kBps', + timeout: 'timeout', + delayTimeout: 'delayTimeout', + loadingTimeout: 'loadingTimeout', + stats: 'stats', + performance: "performance", + record: 'record', + recording: 'recording', + recordingTimestamp: 'recordingTimestamp', + recordStart: 'recordStart', + recordEnd: 'recordEnd', + recordCreateError: 'recordCreateError', + buffer: 'buffer', + videoFrame: 'videoFrame', + start: 'start', + metadata: 'metadata', + resize: 'resize', + streamEnd: 'streamEnd', + streamSuccess: 'streamSuccess', + streamMessage: 'streamMessage', + streamError: 'streamError', + volumechange: 'volumechange', + destroy: 'destroy', + mseSourceOpen: 'mseSourceOpen', + mseSourceClose: 'mseSourceClose', + mseSourceBufferError: 'mseSourceBufferError', + mseSourceBufferBusy: 'mseSourceBufferBusy', + mseSourceBufferFull: 'mseSourceBufferFull', + videoWaiting: 'videoWaiting', + videoTimeUpdate: 'videoTimeUpdate', + videoSyncAudio: 'videoSyncAudio', + playToRenderTimes: 'playToRenderTimes' + }; + const JESSIBUCA_EVENTS = { + load: EVENTS.load, + timeUpdate: EVENTS.timeUpdate, + videoInfo: EVENTS.videoInfo, + audioInfo: EVENTS.audioInfo, + error: EVENTS.error, + kBps: EVENTS.kBps, + log: EVENTS.log, + start: EVENTS.start, + timeout: EVENTS.timeout, + loadingTimeout: EVENTS.loadingTimeout, + delayTimeout: EVENTS.delayTimeout, + fullscreen: 'fullscreen', + webFullscreen: EVENTS.webFullscreen, + play: EVENTS.play, + pause: EVENTS.pause, + mute: EVENTS.mute, + stats: EVENTS.stats, + volumechange: EVENTS.volumechange, + performance: EVENTS.performance, + recordingTimestamp: EVENTS.recordingTimestamp, + recordStart: EVENTS.recordStart, + recordEnd: EVENTS.recordEnd, + playToRenderTimes: EVENTS.playToRenderTimes + }; + const EVENTS_ERROR = { + playError: 'playIsNotPauseOrUrlIsNull', + fetchError: "fetchError", + websocketError: 'websocketError', + webcodecsH265NotSupport: 'webcodecsH265NotSupport', + webcodecsDecodeError: 'webcodecsDecodeError', + webcodecsWidthOrHeightChange: 'webcodecsWidthOrHeightChange', + mediaSourceH265NotSupport: 'mediaSourceH265NotSupport', + mediaSourceFull: EVENTS.mseSourceBufferFull, + mseSourceBufferError: EVENTS.mseSourceBufferError, + mediaSourceAppendBufferError: 'mediaSourceAppendBufferError', + mediaSourceBufferListLarge: 'mediaSourceBufferListLarge', + mediaSourceAppendBufferEndTimeout: 'mediaSourceAppendBufferEndTimeout', + wasmDecodeError: 'wasmDecodeError', + webglAlignmentError: 'webglAlignmentError' + }; + const WEBSOCKET_STATUS = { + notConnect: 'notConnect', + open: 'open', + close: 'close', + error: 'error' + }; + const SCREENSHOT_TYPE = { + download: 'download', + base64: 'base64', + blob: 'blob' + }; + const VIDEO_ENC_TYPE = { + 7: 'H264(AVC)', + // + 12: 'H265(HEVC)' // + + }; + const VIDEO_ENC_CODE = { + h264: 7, + h265: 12 + }; + const AUDIO_ENC_TYPE = { + 10: 'AAC', + 7: 'ALAW', + 8: 'MULAW' + }; + const CONTROL_HEIGHT = 38; + const SCALE_MODE_TYPE = { + full: 0, + // 视频画面完全填充canvas区域,画面会被拉伸 + auto: 1, + // 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 + fullAuto: 2 // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 + + }; + const CANVAS_RENDER_TYPE = { + webcodecs: 'webcodecs', + webgl: 'webgl', + offscreen: 'offscreen' + }; + const ENCODED_VIDEO_TYPE = { + key: 'key', + delta: 'delta' + }; + const MP4_CODECS = { + avc: 'video/mp4; codecs="avc1.64002A"', + hev: 'video/mp4; codecs="hev1.1.6.L123.b0"' + }; + const MEDIA_SOURCE_STATE = { + ended: 'ended', + open: 'open', + closed: 'closed' + }; // frag duration + const AUDIO_SYNC_VIDEO_DIFF = 1000; + const HOT_KEY = { + esc: 27, + // + arrowUp: 38, + // + arrowDown: 40 // + + }; + const WCS_ERROR = { + keyframeIsRequiredError: 'A key frame is required after configure() or flush()', + canNotDecodeClosedCodec: "Cannot call 'decode' on a closed codec" + }; + const FETCH_ERROR = { + abortError1: 'The user aborted a request', + abortError2: 'AbortError', + abort: 'AbortError' + }; + + class Debug { + constructor(master) { + this.log = function (name) { + if (master._opt.debug) { + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + console.log(`Jessibuca: [${name}]`, ...args); + } + }; + + this.warn = function (name) { + if (master._opt.debug) { + for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + args[_key2 - 1] = arguments[_key2]; + } + + console.warn(`Jessibuca: [${name}]`, ...args); + } + }; + + this.error = function (name) { + for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { + args[_key3 - 1] = arguments[_key3]; + } + + console.error(`Jessibuca: [${name}]`, ...args); + }; + } + + } + + class Events { + constructor(master) { + this.destroys = []; + this.proxy = this.proxy.bind(this); + this.master = master; + } + + proxy(target, name, callback) { + let option = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + if (!target) { + return; + } + + if (Array.isArray(name)) { + return name.map(item => this.proxy(target, item, callback, option)); + } + + target.addEventListener(name, callback, option); + + const destroy = () => target.removeEventListener(name, callback, option); + + this.destroys.push(destroy); + return destroy; + } + + destroy() { + this.master.debug && this.master.debug.log(`Events`, 'destroy'); + this.destroys.forEach(event => event()); + } + + } + + var property$1 = (player => { + Object.defineProperty(player, 'rect', { + get: () => { + const clientRect = player.$container.getBoundingClientRect(); + clientRect.width = Math.max(clientRect.width, player.$container.clientWidth); + clientRect.height = Math.max(clientRect.height, player.$container.clientHeight); + return clientRect; + } + }); + ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(key => { + Object.defineProperty(player, key, { + get: () => { + return player.rect[key]; + } + }); + }); + }); + + var screenfull = createCommonjsModule(function (module) { + /*! + * screenfull + * v5.1.0 - 2020-12-24 + * (c) Sindre Sorhus; MIT License + */ + (function () { + + var document = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window.document : {}; + var isCommonjs = module.exports; + + var fn = (function () { + var val; + + var fnMap = [ + [ + 'requestFullscreen', + 'exitFullscreen', + 'fullscreenElement', + 'fullscreenEnabled', + 'fullscreenchange', + 'fullscreenerror' + ], + // New WebKit + [ + 'webkitRequestFullscreen', + 'webkitExitFullscreen', + 'webkitFullscreenElement', + 'webkitFullscreenEnabled', + 'webkitfullscreenchange', + 'webkitfullscreenerror' + + ], + // Old WebKit + [ + 'webkitRequestFullScreen', + 'webkitCancelFullScreen', + 'webkitCurrentFullScreenElement', + 'webkitCancelFullScreen', + 'webkitfullscreenchange', + 'webkitfullscreenerror' + + ], + [ + 'mozRequestFullScreen', + 'mozCancelFullScreen', + 'mozFullScreenElement', + 'mozFullScreenEnabled', + 'mozfullscreenchange', + 'mozfullscreenerror' + ], + [ + 'msRequestFullscreen', + 'msExitFullscreen', + 'msFullscreenElement', + 'msFullscreenEnabled', + 'MSFullscreenChange', + 'MSFullscreenError' + ] + ]; + + var i = 0; + var l = fnMap.length; + var ret = {}; + + for (; i < l; i++) { + val = fnMap[i]; + if (val && val[1] in document) { + for (i = 0; i < val.length; i++) { + ret[fnMap[0][i]] = val[i]; + } + return ret; + } + } + + return false; + })(); + + var eventNameMap = { + change: fn.fullscreenchange, + error: fn.fullscreenerror + }; + + var screenfull = { + request: function (element, options) { + return new Promise(function (resolve, reject) { + var onFullScreenEntered = function () { + this.off('change', onFullScreenEntered); + resolve(); + }.bind(this); + + this.on('change', onFullScreenEntered); + + element = element || document.documentElement; + + var returnPromise = element[fn.requestFullscreen](options); + + if (returnPromise instanceof Promise) { + returnPromise.then(onFullScreenEntered).catch(reject); + } + }.bind(this)); + }, + exit: function () { + return new Promise(function (resolve, reject) { + if (!this.isFullscreen) { + resolve(); + return; + } + + var onFullScreenExit = function () { + this.off('change', onFullScreenExit); + resolve(); + }.bind(this); + + this.on('change', onFullScreenExit); + + var returnPromise = document[fn.exitFullscreen](); + + if (returnPromise instanceof Promise) { + returnPromise.then(onFullScreenExit).catch(reject); + } + }.bind(this)); + }, + toggle: function (element, options) { + return this.isFullscreen ? this.exit() : this.request(element, options); + }, + onchange: function (callback) { + this.on('change', callback); + }, + onerror: function (callback) { + this.on('error', callback); + }, + on: function (event, callback) { + var eventName = eventNameMap[event]; + if (eventName) { + document.addEventListener(eventName, callback, false); + } + }, + off: function (event, callback) { + var eventName = eventNameMap[event]; + if (eventName) { + document.removeEventListener(eventName, callback, false); + } + }, + raw: fn + }; + + if (!fn) { + if (isCommonjs) { + module.exports = { isEnabled: false }; + } else { + window.screenfull = { isEnabled: false }; + } + + return; + } + + Object.defineProperties(screenfull, { + isFullscreen: { + get: function () { + return Boolean(document[fn.fullscreenElement]); + } + }, + element: { + enumerable: true, + get: function () { + return document[fn.fullscreenElement]; + } + }, + isEnabled: { + enumerable: true, + get: function () { + // Coerce to boolean in case of old WebKit + return Boolean(document[fn.fullscreenEnabled]); + } + } + }); + + if (isCommonjs) { + module.exports = screenfull; + } else { + window.screenfull = screenfull; + } + })(); + }); + screenfull.isEnabled; + + function noop() { } + function supportOffscreen($canvas) { + return typeof $canvas.transferControlToOffscreen === 'function'; + } + function supportOffscreenV2() { + return typeof OffscreenCanvas !== "undefined"; + } + function createContextGL($canvas) { + let gl = null; + const validContextNames = ["webgl", "experimental-webgl", "moz-webgl", "webkit-3d"]; + let nameIndex = 0; + + while (!gl && nameIndex < validContextNames.length) { + const contextName = validContextNames[nameIndex]; + + try { + let contextOptions = { + preserveDrawingBuffer: true + }; + gl = $canvas.getContext(contextName, contextOptions); + } catch (e) { + gl = null; + } + + if (!gl || typeof gl.getParameter !== "function") { + gl = null; + } + + ++nameIndex; + } + + return gl; + } + function dataURLToFile() { + let dataURL = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; + const arr = dataURL.split(","); + const bstr = atob(arr[1]); + const type = arr[0].replace("data:", "").replace(";base64", ""); + let n = bstr.length, + u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + return new File([u8arr], 'file', { + type + }); + } + function now() { + return new Date().getTime(); + } + (() => { + try { + if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { + const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); + if (module instanceof WebAssembly.Module) return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; + } + } catch (e) { } + + return false; + })(); + function clamp(num, a, b) { + return Math.max(Math.min(num, Math.max(a, b)), Math.min(a, b)); + } + function setStyle(element, key, value) { + if (!element) { + return; + } + + if (typeof key === 'object') { + Object.keys(key).forEach(item => { + setStyle(element, item, key[item]); + }); + } + + element.style[key] = value; + return element; + } + function getStyle(element, key) { + let numberType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + + if (!element) { + return 0; + } + + const value = getComputedStyle(element, null).getPropertyValue(key); + return numberType ? parseFloat(value) : value; + } + function getNowTime() { + if (performance && typeof performance.now === 'function') { + return performance.now(); + } + + return Date.now(); + } + function calculationRate(callback) { + let totalSize = 0; + let lastTime = getNowTime(); + return size => { + totalSize += size; + const thisTime = getNowTime(); + const diffTime = thisTime - lastTime; + + if (diffTime >= 1000) { + callback(totalSize / diffTime * 1000); + lastTime = thisTime; + totalSize = 0; + } + }; + } + function isMobile() { + return /iphone|ipod|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase()); + } + + function supportWCS() { + return "VideoEncoder" in window; + } + function formatVideoDecoderConfigure(avcC) { + let codecArray = avcC.subarray(1, 4); + let codecString = "avc1."; + + for (let j = 0; j < 3; j++) { + let h = codecArray[j].toString(16); + + if (h.length < 2) { + h = "0" + h; + } + + codecString += h; + } + + return { + codec: codecString, + description: avcC + }; + } + function isFullScreen() { + return screenfull.isFullscreen; + } + function bpsSize(value) { + if (null == value || value === '' || parseInt(value) === 0 || isNaN(parseInt(value))) { + return "0KB/s"; + } + + let size = parseFloat(value); + size = size.toFixed(2); + return size + 'KB/s'; + } + function fpsStatus(fps) { + let result = 0; + + if (fps >= 24) { + result = 2; + } else if (fps >= 15) { + result = 1; + } + + return result; + } + function createEmptyImageBitmap(width, height) { + const $canvasElement = document.createElement("canvas"); + $canvasElement.width = width; + $canvasElement.height = height; + return window.createImageBitmap($canvasElement, 0, 0, width, height); + } + function supportMSE() { + return window.MediaSource && window.MediaSource.isTypeSupported(MP4_CODECS.avc); + } + function supportMediaStreamTrack() { + return window.MediaStreamTrackGenerator && typeof window.MediaStreamTrackGenerator === 'function'; + } + function isEmpty(value) { + return value === null || value === undefined; + } + function isBoolean(value) { + return value === true || value === false; + } + function isNotEmpty(value) { + return !isEmpty(value); + } + function initPlayTimes() { + return { + playInitStart: '', + //1 + playStart: '', + // 2 + streamStart: '', + //3 + streamResponse: '', + // 4 + demuxStart: '', + // 5 + decodeStart: '', + // 6 + videoStart: '', + // 7 + playTimestamp: '', + // playStart- playInitStart + streamTimestamp: '', + // streamStart - playStart + streamResponseTimestamp: '', + // streamResponse - streamStart + demuxTimestamp: '', + // demuxStart - streamResponse + decodeTimestamp: '', + // decodeStart - demuxStart + videoTimestamp: '', + // videoStart - decodeStart + allTimestamp: '' // videoStart - playInitStart + + }; + } // create watermark + function formatTimeTips(time) { + var result; // + + if (time > -1) { + var hour = Math.floor(time / 3600); + var min = Math.floor(time / 60) % 60; + var sec = time % 60; + sec = Math.round(sec); + + if (hour < 10) { + result = '0' + hour + ":"; + } else { + result = hour + ":"; + } + + if (min < 10) { + result += "0"; + } + + result += min + ":"; + + if (sec < 10) { + result += "0"; + } + + result += sec.toFixed(0); + } + + return result; + } + function getTarget(e) { + const event = e || window.event; + const target = event.target || event.srcElement; + return target; + } + function isWebglRenderSupport(width) { + return width / 2 % 4 === 0; + } + function getBrowser() { + const UserAgent = navigator.userAgent.toLowerCase(); + const browserInfo = {}; + const browserArray = { + IE: window.ActiveXObject || "ActiveXObject" in window, + // IE + Chrome: UserAgent.indexOf('chrome') > -1 && UserAgent.indexOf('safari') > -1, + // Chrome浏览器 + Firefox: UserAgent.indexOf('firefox') > -1, + // 火狐浏览器 + Opera: UserAgent.indexOf('opera') > -1, + // Opera浏览器 + Safari: UserAgent.indexOf('safari') > -1 && UserAgent.indexOf('chrome') == -1, + // safari浏览器 + Edge: UserAgent.indexOf('edge') > -1, + // Edge浏览器 + QQBrowser: /qqbrowser/.test(UserAgent), + // qq浏览器 + WeixinBrowser: /MicroMessenger/i.test(UserAgent) // 微信浏览器 + + }; // console.log(browserArray) + + for (let i in browserArray) { + if (browserArray[i]) { + let versions = ''; + + if (i === 'IE') { + versions = UserAgent.match(/(msie\s|trident.*rv:)([\w.]+)/)[2]; + } else if (i === 'Chrome') { + for (let mt in navigator.mimeTypes) { + //检测是否是360浏览器(测试只有pc端的360才起作用) + if (navigator.mimeTypes[mt]['type'] === 'application/360softmgrplugin') { + i = '360'; + } + } + + versions = UserAgent.match(/chrome\/([\d.]+)/)[1]; + } else if (i === 'Firefox') { + versions = UserAgent.match(/firefox\/([\d.]+)/)[1]; + } else if (i === 'Opera') { + versions = UserAgent.match(/opera\/([\d.]+)/)[1]; + } else if (i === 'Safari') { + versions = UserAgent.match(/version\/([\d.]+)/)[1]; + } else if (i === 'Edge') { + versions = UserAgent.match(/edge\/([\d.]+)/)[1]; + } else if (i === 'QQBrowser') { + versions = UserAgent.match(/qqbrowser\/([\d.]+)/)[1]; + } + + browserInfo.type = i; + browserInfo.version = parseInt(versions); + } + } + + return browserInfo; + } + function closeVideoFrame(videoFrame) { + if (videoFrame.close) { + videoFrame.close(); + } else if (videoFrame.destroy) { + videoFrame.destroy(); + } + } + function removeElement(element) { + let result = false; + + if (element) { + if (element.parentNode) { + element.parentNode.removeChild(element); + result = true; + } + } + + return result; + } + + var events$1 = (player => { + try { + const screenfullChange = e => { + if (getTarget(e) === player.$container) { + player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen); // 如果不是fullscreen,则触发下 resize 方法 + + if (!player.fullscreen) { + player.resize(); + } else { + if (player._opt.useMSE) { + player.resize(); + } + } + } + }; + + screenfull.on('change', screenfullChange); + player.events.destroys.push(() => { + screenfull.off('change', screenfullChange); + }); + } catch (error) {// + } // + + + player.on(EVENTS.decoderWorkerInit, () => { + player.debug.log('player', 'has loaded'); + player.loaded = true; + }); // + + player.on(EVENTS.play, () => { + player.loading = false; + }); // + + player.on(EVENTS.fullscreen, value => { + if (value) { + try { + screenfull.request(player.$container).then(() => { }).catch(e => { + if (isMobile() && player._opt.useWebFullScreen) { + player.webFullscreen = true; + } + }); + } catch (e) { + if (isMobile() && player._opt.useWebFullScreen) { + player.webFullscreen = true; + } + } + } else { + try { + screenfull.exit().then(() => { + if (player.webFullscreen) { + player.webFullscreen = false; + } + }).catch(() => { + player.webFullscreen = false; + }); + } catch (e) { + player.webFullscreen = false; + } + } + }); + + if (isMobile()) { + player.on(EVENTS.webFullscreen, value => { + if (value) { + player.$container.classList.add('jessibuca-fullscreen-web'); + } else { + player.$container.classList.remove('jessibuca-fullscreen-web'); + } // + + + player.emit(JESSIBUCA_EVENTS.fullscreen, player.fullscreen); + }); + } // + + + player.on(EVENTS.resize, () => { + player.video && player.video.resize(); + }); + + if (player._opt.debug) { + const ignoreList = [EVENTS.timeUpdate]; + Object.keys(EVENTS).forEach(key => { + player.on(EVENTS[key], value => { + if (ignoreList.includes(key)) { + return; + } + + player.debug.log('player events', EVENTS[key], value); + }); + }); + Object.keys(EVENTS_ERROR).forEach(key => { + player.on(EVENTS_ERROR[key], value => { + player.debug.log('player event error', EVENTS_ERROR[key], value); + }); + }); + } + }); + + class Emitter { + on(name, fn, ctx) { + const e = this.e || (this.e = {}); + (e[name] || (e[name] = [])).push({ + fn, + ctx + }); + return this; + } + + once(name, fn, ctx) { + const self = this; + + function listener() { + self.off(name, listener); + + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + fn.apply(ctx, args); + } + + listener._ = fn; + return this.on(name, listener, ctx); + } + + emit(name) { + const evtArr = ((this.e || (this.e = {}))[name] || []).slice(); + + for (var _len2 = arguments.length, data = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { + data[_key2 - 1] = arguments[_key2]; + } + + for (let i = 0; i < evtArr.length; i += 1) { + evtArr[i].fn.apply(evtArr[i].ctx, data); + } + + return this; + } + + off(name, callback) { + const e = this.e || (this.e = {}); + + if (!name) { + Object.keys(e).forEach(key => { + delete e[key]; + }); + delete this.e; + return; + } + + const evts = e[name]; + const liveEvents = []; + + if (evts && callback) { + for (let i = 0, len = evts.length; i < len; i += 1) { + if (evts[i].fn !== callback && evts[i].fn._ !== callback) liveEvents.push(evts[i]); + } + } + + if (liveEvents.length) { + e[name] = liveEvents; + } else { + delete e[name]; + } + + return this; + } + + } + + var createWebGL = ((gl, openWebglAlignment) => { + var vertexShaderScript = ['attribute vec4 vertexPos;', 'attribute vec4 texturePos;', 'varying vec2 textureCoord;', 'void main()', '{', 'gl_Position = vertexPos;', 'textureCoord = texturePos.xy;', '}'].join('\n'); + var fragmentShaderScript = ['precision highp float;', 'varying highp vec2 textureCoord;', 'uniform sampler2D ySampler;', 'uniform sampler2D uSampler;', 'uniform sampler2D vSampler;', 'const mat4 YUV2RGB = mat4', '(', '1.1643828125, 0, 1.59602734375, -.87078515625,', '1.1643828125, -.39176171875, -.81296875, .52959375,', '1.1643828125, 2.017234375, 0, -1.081390625,', '0, 0, 0, 1', ');', 'void main(void) {', 'highp float y = texture2D(ySampler, textureCoord).r;', 'highp float u = texture2D(uSampler, textureCoord).r;', 'highp float v = texture2D(vSampler, textureCoord).r;', 'gl_FragColor = vec4(y, u, v, 1) * YUV2RGB;', '}'].join('\n'); + + if (openWebglAlignment) { + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1); + } + + var vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexShaderScript); + gl.compileShader(vertexShader); + + if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { + console.log('Vertex shader failed to compile: ' + gl.getShaderInfoLog(vertexShader)); + } + + var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentShaderScript); + gl.compileShader(fragmentShader); + + if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { + console.log('Fragment shader failed to compile: ' + gl.getShaderInfoLog(fragmentShader)); + } + + var program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + console.log('Program failed to compile: ' + gl.getProgramInfoLog(program)); + } + + gl.useProgram(program); // initBuffers + + var vertexPosBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 1, -1, 1, 1, -1, -1, -1]), gl.STATIC_DRAW); + var vertexPosRef = gl.getAttribLocation(program, 'vertexPos'); + gl.enableVertexAttribArray(vertexPosRef); + gl.vertexAttribPointer(vertexPosRef, 2, gl.FLOAT, false, 0, 0); + var texturePosBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, texturePosBuffer); + gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([1, 0, 0, 0, 1, 1, 0, 1]), gl.STATIC_DRAW); + var texturePosRef = gl.getAttribLocation(program, 'texturePos'); + gl.enableVertexAttribArray(texturePosRef); + gl.vertexAttribPointer(texturePosRef, 2, gl.FLOAT, false, 0, 0); + + function _initTexture(name, index) { + var textureRef = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, textureRef); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, null); + gl.uniform1i(gl.getUniformLocation(program, name), index); + return textureRef; + } + + var yTextureRef = _initTexture('ySampler', 0); + + var uTextureRef = _initTexture('uSampler', 1); + + var vTextureRef = _initTexture('vSampler', 2); + + return { + render: function (w, h, y, u, v) { + gl.viewport(0, 0, w, h); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, yTextureRef); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w, h, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, y); + gl.activeTexture(gl.TEXTURE1); + gl.bindTexture(gl.TEXTURE_2D, uTextureRef); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, u); + gl.activeTexture(gl.TEXTURE2); + gl.bindTexture(gl.TEXTURE_2D, vTextureRef); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, w / 2, h / 2, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, v); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); + }, + destroy: function () { + try { + gl.deleteProgram(program); + gl.deleteBuffer(vertexPosBuffer); + gl.deleteBuffer(texturePosBuffer); + gl.deleteTexture(yTextureRef); + gl.deleteTexture(uTextureRef); + gl.deleteTexture(vTextureRef); + } catch (e) {// console.error(e); + } + } + }; + }); + + class CommonLoader$1 extends Emitter { + constructor() { + super(); + this.init = false; + } + + resetInit() { + this.init = false; + this.videoInfo = { + width: '', + height: '', + encType: '', + encTypeCode: '' + }; + } + + destroy() { + this.resetInit(); + this.player.$container.removeChild(this.$videoElement); + this.off(); + } // + + + updateVideoInfo(data) { + if (data.encTypeCode) { + this.videoInfo.encType = VIDEO_ENC_TYPE[data.encTypeCode]; + } + + if (data.width) { + this.videoInfo.width = data.width; + } + + if (data.height) { + this.videoInfo.height = data.height; + } // video 基本信息 + + + if (this.videoInfo.encType && this.videoInfo.height && this.videoInfo.width && !this.init) { + this.player.emit(EVENTS.videoInfo, this.videoInfo); + this.init = true; + } + } + + play() { } + + pause() { } + + clearView() { } + + } + + /* + * FileSaver.js + * A saveAs() FileSaver implementation. + * + * By Eli Grey, http://eligrey.com + * + * License : https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md (MIT) + * source : http://purl.eligrey.com/github/FileSaver.js + */ + // The one and only way of getting global scope in all environments + // https://stackoverflow.com/q/3277182/1008999 + var _global = typeof window === 'object' && window.window === window ? window : typeof self === 'object' && self.self === self ? self : typeof global === 'object' && global.global === global ? global : undefined; + + function bom(blob, opts) { + if (typeof opts === 'undefined') opts = { + autoBom: false + }; else if (typeof opts !== 'object') { + console.warn('Deprecated: Expected third argument to be a object'); + opts = { + autoBom: !opts + }; + } // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + + if (opts.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], { + type: blob.type + }); + } + + return blob; + } + + function download(url, name, opts) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.responseType = 'blob'; + + xhr.onload = function () { + saveAs(xhr.response, name, opts); + }; + + xhr.onerror = function () { + console.error('could not download file'); + }; + + xhr.send(); + } + + function corsEnabled(url) { + var xhr = new XMLHttpRequest(); // use sync to avoid popup blocker + + xhr.open('HEAD', url, false); + + try { + xhr.send(); + } catch (e) { } + + return xhr.status >= 200 && xhr.status <= 299; + } // `a.click()` doesn't work for all browsers (#465) + + + function click(node) { + try { + node.dispatchEvent(new MouseEvent('click')); + } catch (e) { + var evt = document.createEvent('MouseEvents'); + evt.initMouseEvent('click', true, true, window, 0, 0, 0, 80, 20, false, false, false, false, 0, null); + node.dispatchEvent(evt); + } + } // Detect WebView inside a native macOS app by ruling out all browsers + // We just need to check for 'Safari' because all other browsers (besides Firefox) include that too + // https://www.whatismybrowser.com/guides/the-latest-user-agent/macos + + + var isMacOSWebView = _global.navigator && /Macintosh/.test(navigator.userAgent) && /AppleWebKit/.test(navigator.userAgent) && !/Safari/.test(navigator.userAgent); + var saveAs = // probably in some web worker + typeof window !== 'object' || window !== _global ? function saveAs() { + /* noop */ + } // Use download attribute first if possible (#193 Lumia mobile) unless this is a macOS WebView + : 'download' in HTMLAnchorElement.prototype && !isMacOSWebView ? function saveAs(blob, name, opts) { + var URL = _global.URL || _global.webkitURL; // Namespace is used to prevent conflict w/ Chrome Poper Blocker extension (Issue #561) + + var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'a'); + name = name || blob.name || 'download'; + a.download = name; + a.rel = 'noopener'; // tabnabbing + // TODO: detect chrome extensions & packaged apps + // a.target = '_blank' + + if (typeof blob === 'string') { + // Support regular links + a.href = blob; + + if (a.origin !== location.origin) { + corsEnabled(a.href) ? download(blob, name, opts) : click(a, a.target = '_blank'); + } else { + click(a); + } + } else { + // Support blobs + a.href = URL.createObjectURL(blob); + setTimeout(function () { + URL.revokeObjectURL(a.href); + }, 4E4); // 40s + + setTimeout(function () { + click(a); + }, 0); + } + } // Use msSaveOrOpenBlob as a second approach + : 'msSaveOrOpenBlob' in navigator ? function saveAs(blob, name, opts) { + name = name || blob.name || 'download'; + + if (typeof blob === 'string') { + if (corsEnabled(blob)) { + download(blob, name, opts); + } else { + var a = document.createElement('a'); + a.href = blob; + a.target = '_blank'; + setTimeout(function () { + click(a); + }); + } + } else { + navigator.msSaveOrOpenBlob(bom(blob, opts), name); + } + } // Fallback to using FileReader and a popup + : function saveAs(blob, name, opts, popup) { + // Open a popup immediately do go around popup blocker + // Mostly only available on user interaction and the fileReader is async so... + popup = popup || open('', '_blank'); + + if (popup) { + popup.document.title = popup.document.body.innerText = 'downloading...'; + } + + if (typeof blob === 'string') return download(blob, name, opts); + var force = blob.type === 'application/octet-stream'; + + var isSafari = /constructor/i.test(_global.HTMLElement) || _global.safari; + + var isChromeIOS = /CriOS\/[\d]+/.test(navigator.userAgent); + + if ((isChromeIOS || force && isSafari || isMacOSWebView) && typeof FileReader !== 'undefined') { + // Safari doesn't allow downloading of blob URLs + var reader = new FileReader(); + + reader.onloadend = function () { + var url = reader.result; + url = isChromeIOS ? url : url.replace(/^data:[^;]*;/, 'data:attachment/file;'); + if (popup) popup.location.href = url; else location = url; + popup = null; // reverse-tabnabbing #460 + }; + + reader.readAsDataURL(blob); + } else { + var URL = _global.URL || _global.webkitURL; + var url = URL.createObjectURL(blob); + if (popup) popup.location = url; else location.href = url; + popup = null; // reverse-tabnabbing #460 + + setTimeout(function () { + URL.revokeObjectURL(url); + }, 4E4); // 40s + } + }; + + class CanvasVideoLoader extends CommonLoader$1 { + constructor(player) { + super(); + this.player = player; + const $canvasElement = document.createElement("canvas"); + $canvasElement.style.position = "absolute"; + $canvasElement.style.top = 0; + $canvasElement.style.left = 0; + this.$videoElement = $canvasElement; + player.$container.appendChild(this.$videoElement); + this.context2D = null; + this.contextGl = null; + this.contextGlRender = null; + this.contextGlDestroy = null; + this.bitmaprenderer = null; + this.renderType = null; + this.videoInfo = { + width: '', + height: '', + encType: '' + }; // + + this._initCanvasRender(); + + this.player.debug.log('CanvasVideo', 'init'); + } + + destroy() { + super.destroy(); + + if (this.contextGl) { + this.contextGl = null; + } + + if (this.context2D) { + this.context2D = null; + } + + if (this.contextGlRender) { + this.contextGlDestroy && this.contextGlDestroy(); + this.contextGlDestroy = null; + this.contextGlRender = null; + } + + if (this.bitmaprenderer) { + this.bitmaprenderer = null; + } + + this.renderType = null; + this.player.debug.log(`CanvasVideoLoader`, 'destroy'); + } + + _initContextGl() { + this.contextGl = createContextGL(this.$videoElement); + + if (this.contextGl) { + const webgl = createWebGL(this.contextGl, this.player._opt.openWebglAlignment); + this.contextGlRender = webgl.render; + this.contextGlDestroy = webgl.destroy; + } else { + this.player.debug.error(`CanvasVideoLoader`, 'init webgl fail'); + } + } + + _initContext2D() { + this.context2D = this.$videoElement.getContext('2d'); + } // 渲染类型 + + + _initCanvasRender() { + if (this.player._opt.useWCS && !this._supportOffscreen()) { + this.renderType = CANVAS_RENDER_TYPE.webcodecs; + + this._initContext2D(); + } else if (this._supportOffscreen()) { + this.renderType = CANVAS_RENDER_TYPE.offscreen; + + this._bindOffscreen(); + } else { + this.renderType = CANVAS_RENDER_TYPE.webgl; + + this._initContextGl(); + } + } + + _supportOffscreen() { + return supportOffscreen(this.$videoElement) && this.player._opt.useOffscreen; + } // + + + _bindOffscreen() { + this.bitmaprenderer = this.$videoElement.getContext('bitmaprenderer'); + } + + initCanvasViewSize() { + this.$videoElement.width = this.videoInfo.width; + this.$videoElement.height = this.videoInfo.height; + this.resize(); + } // + + + render(msg) { + this.player.videoTimestamp = msg.ts; + + switch (this.renderType) { + case CANVAS_RENDER_TYPE.offscreen: + this.bitmaprenderer.transferFromImageBitmap(msg.buffer); + break; + + case CANVAS_RENDER_TYPE.webgl: + this.contextGlRender(this.$videoElement.width, this.$videoElement.height, msg.output[0], msg.output[1], msg.output[2]); + break; + + case CANVAS_RENDER_TYPE.webcodecs: + // can use createImageBitmap in wexin + this.context2D.drawImage(msg.videoFrame, 0, 0, this.$videoElement.width, this.$videoElement.height); + closeVideoFrame(msg.videoFrame); + break; + } + } + + screenshot(filename, format, quality, type) { + filename = filename || now(); + type = type || SCREENSHOT_TYPE.download; + const formatType = { + png: 'image/png', + jpeg: 'image/jpeg', + webp: 'image/webp' + }; + let encoderOptions = 0.92; + + if (!formatType[format] && SCREENSHOT_TYPE[format]) { + type = format; + format = 'png'; + quality = undefined; + } + + if (typeof quality === "string") { + type = quality; + quality = undefined; + } + + if (typeof quality !== 'undefined') { + encoderOptions = Number(quality); + } + + const dataURL = this.$videoElement.toDataURL(formatType[format] || formatType.png, encoderOptions); + + if (type === SCREENSHOT_TYPE.base64) { + return dataURL; + } else { + const file = dataURLToFile(dataURL); + + if (type === SCREENSHOT_TYPE.blob) { + return file; + } else if (type === SCREENSHOT_TYPE.download) { + // downloadImg(file, filename); + saveAs(file, filename); + } + } + } // + + + clearView() { + switch (this.renderType) { + case CANVAS_RENDER_TYPE.offscreen: + createEmptyImageBitmap(this.$videoElement.width, this.$videoElement.height).then(imageBitMap => { + this.bitmaprenderer.transferFromImageBitmap(imageBitMap); + }); + break; + + case CANVAS_RENDER_TYPE.webgl: + this.contextGl.clear(this.contextGl.COLOR_BUFFER_BIT); + break; + + case CANVAS_RENDER_TYPE.webcodecs: + this.context2D.clearRect(0, 0, this.$videoElement.width, this.$videoElement.height); + break; + } + } + + resize() { + this.player.debug.log('canvasVideo', 'resize'); + const option = this.player._opt; + let width = this.player.width; + let height = this.player.height; + + if (option.hasControl && !option.controlAutoHide) { + if (isMobile() && this.player.fullscreen && option.useWebFullScreen) { + width -= CONTROL_HEIGHT; + } else { + height -= CONTROL_HEIGHT; + } + } + + let resizeWidth = this.$videoElement.width; + let resizeHeight = this.$videoElement.height; + const rotate = option.rotate; + let left = (width - resizeWidth) / 2; + let top = (height - resizeHeight) / 2; + + if (rotate === 270 || rotate === 90) { + resizeWidth = this.$videoElement.height; + resizeHeight = this.$videoElement.width; + } + + const wScale = width / resizeWidth; + const hScale = height / resizeHeight; + let scale = wScale > hScale ? hScale : wScale; // + + if (!option.isResize) { + if (wScale !== hScale) { + scale = wScale + ',' + hScale; + } + } // + + + if (option.isFullResize) { + scale = wScale > hScale ? wScale : hScale; + } + + let transform = "scale(" + scale + ")"; + + if (rotate) { + transform += ' rotate(' + rotate + 'deg)'; + } + + this.$videoElement.style.transform = transform; + this.$videoElement.style.left = left + "px"; + this.$videoElement.style.top = top + "px"; + } + + } + + class VideoLoader extends CommonLoader$1 { + constructor(player) { + super(); + this.player = player; + const $videoElement = document.createElement('video'); + const $canvasElement = document.createElement('canvas'); + $videoElement.muted = true; + $videoElement.style.position = "absolute"; + $videoElement.style.top = 0; + $videoElement.style.left = 0; + this._delayPlay = false; + player.$container.appendChild($videoElement); + this.videoInfo = { + width: '', + height: '', + encType: '' + }; + const _opt = this.player._opt; + + if (_opt.useWCS && _opt.wcsUseVideoRender) { + this.trackGenerator = new MediaStreamTrackGenerator({ + kind: 'video' + }); + $videoElement.srcObject = new MediaStream([this.trackGenerator]); + this.vwriter = this.trackGenerator.writable.getWriter(); + } + + this.$videoElement = $videoElement; + this.$canvasElement = $canvasElement; + this.canvasContext = $canvasElement.getContext('2d'); + this.fixChromeVideoFlashBug(); + this.resize(); + const { + proxy + } = this.player.events; + proxy(this.$videoElement, 'canplay', () => { + this.player.debug.log('Video', `canplay and _delayPlay is ${this._delayPlay}`); + + if (this._delayPlay) { + this._play(); + } + }); + proxy(this.$videoElement, 'waiting', () => { + this.player.emit(EVENTS.videoWaiting); + }); + proxy(this.$videoElement, 'timeupdate', event => { + // this.player.emit(EVENTS.videoTimeUpdate, event.timeStamp); + const timeStamp = parseInt(event.timeStamp, 10); + this.player.emit(EVENTS.timeUpdate, timeStamp); // check is pause; + + if (!this.isPlaying() && this.init) { + this.player.debug.log('Video', `timeupdate and this.isPlaying is false and retry play`); + this.$videoElement.play(); + } + }); + this.player.debug.log('Video', 'init'); + } + + destroy() { + super.destroy(); + this.$canvasElement = null; + this.canvasContext = null; + + if (this.$videoElement) { + this.$videoElement.pause(); + this.$videoElement.currentTime = 0; + this.$videoElement.src = ''; + this.$videoElement.removeAttribute('src'); + this.$videoElement = null; + } + + if (this.trackGenerator) { + this.trackGenerator.stop(); + this.trackGenerator = null; + } + + if (this.vwriter) { + this.vwriter.close(); + this.vwriter = null; + } + + this.player.debug.log('Video', 'destroy'); + } + + fixChromeVideoFlashBug() { + const browser = getBrowser(); + const type = browser.type.toLowerCase(); + + if (type === 'chrome' || type === 'edge') { + const $container = this.player.$container; + $container.style.backdropFilter = 'blur(0px)'; + $container.style.translateZ = '0'; + } + } + + play() { + if (this.$videoElement) { + const readyState = this._getVideoReadyState(); + + this.player.debug.log('Video', `play and readyState: ${readyState}`); + + if (readyState === 0) { + this.player.debug.warn('Video', 'readyState is 0 and set _delayPlay to true'); + this._delayPlay = true; + return; + } + + this._play(); + } + } + + _getVideoReadyState() { + let result = 0; + + if (this.$videoElement) { + result = this.$videoElement.readyState; + } + + return result; + } + + _play() { + this.$videoElement && this.$videoElement.play().then(() => { + this._delayPlay = false; + this.player.debug.log('Video', '_play success'); + setTimeout(() => { + if (!this.isPlaying()) { + this.player.debug.warn('Video', `play failed and retry play`); + + this._play(); + } + }, 100); + }).catch(e => { + this.player.debug.error('Video', '_play error', e); + }); + } + + pause(isNow) { + // 预防 + // https://developer.chrome.com/blog/play-request-was-interrupted/ + // http://alonesuperman.com/?p=23 + if (isNow) { + this.$videoElement && this.$videoElement.pause(); + } else { + setTimeout(() => { + this.$videoElement && this.$videoElement.pause(); + }, 100); + } + } + + clearView() { } + + screenshot(filename, format, quality, type) { + filename = filename || now(); + type = type || SCREENSHOT_TYPE.download; + const formatType = { + png: 'image/png', + jpeg: 'image/jpeg', + webp: 'image/webp' + }; + let encoderOptions = 0.92; + + if (!formatType[format] && SCREENSHOT_TYPE[format]) { + type = format; + format = 'png'; + quality = undefined; + } + + if (typeof quality === "string") { + type = quality; + quality = undefined; + } + + if (typeof quality !== 'undefined') { + encoderOptions = Number(quality); + } + + const $video = this.$videoElement; + let canvas = this.$canvasElement; + canvas.width = $video.videoWidth; + canvas.height = $video.videoHeight; + this.canvasContext.drawImage($video, 0, 0, canvas.width, canvas.height); + const dataURL = canvas.toDataURL(formatType[format] || formatType.png, encoderOptions); // release memory + + this.canvasContext.clearRect(0, 0, canvas.width, canvas.height); + canvas.width = 0; + canvas.height = 0; + + if (type === SCREENSHOT_TYPE.base64) { + return dataURL; + } else { + const file = dataURLToFile(dataURL); + + if (type === SCREENSHOT_TYPE.blob) { + return file; + } else if (type === SCREENSHOT_TYPE.download) { + // downloadImg(file, filename); + saveAs(file, filename); + } + } + } + + initCanvasViewSize() { + this.resize(); + } // + + + render(msg) { + if (this.vwriter) { + this.vwriter.write(msg.videoFrame); + } + } + + resize() { + let width = this.player.width; + let height = this.player.height; + const option = this.player._opt; + const rotate = option.rotate; + + if (option.hasControl && !option.controlAutoHide) { + if (isMobile() && this.player.fullscreen && option.useWebFullScreen) { + width -= CONTROL_HEIGHT; + } else { + height -= CONTROL_HEIGHT; + } + } + + this.$videoElement.width = width; + this.$videoElement.height = height; + + if (rotate === 270 || rotate === 90) { + this.$videoElement.width = height; + this.$videoElement.height = width; + } + + let resizeWidth = this.$videoElement.width; + let resizeHeight = this.$videoElement.height; + let left = (width - resizeWidth) / 2; + let top = (height - resizeHeight) / 2; + let objectFill = 'contain'; // 默认是true + // 视频画面做等比缩放后,高或宽对齐canvas区域,画面不被拉伸,但有黑边 + // 视频画面完全填充canvas区域,画面会被拉伸 + + if (!option.isResize) { + objectFill = 'fill'; + } // 视频画面做等比缩放后,完全填充canvas区域,画面不被拉伸,没有黑边,但画面显示不全 + + + if (option.isFullResize) { + objectFill = 'none'; + } + + this.$videoElement.style.objectFit = objectFill; + this.$videoElement.style.transform = 'rotate(' + rotate + 'deg)'; + this.$videoElement.style.left = left + "px"; + this.$videoElement.style.top = top + "px"; + } + + isPlaying() { + return this.$videoElement && !this.$videoElement.paused; + } + + } + + class Video { + constructor(player) { + const Loader = Video.getLoaderFactory(player._opt); + return new Loader(player); + } + + static getLoaderFactory(opt) { + if (opt.useMSE || opt.useWCS && !opt.useOffscreen && opt.wcsUseVideoRender) { + return VideoLoader; + } else { + return CanvasVideoLoader; + } + } + + } + + class AudioContextLoader extends Emitter { + constructor(player) { + super(); + this.bufferList = []; + this.player = player; + this.scriptNode = null; + this.hasInitScriptNode = false; + this.audioContextChannel = null; + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); // + + this.gainNode = this.audioContext.createGain(); // Get an AudioBufferSourceNode. + // This is the AudioNode to use when we want to play an AudioBuffer + + const source = this.audioContext.createBufferSource(); // set the buffer in the AudioBufferSourceNode + + source.buffer = this.audioContext.createBuffer(1, 1, 22050); // connect the AudioBufferSourceNode to the + // destination so we can hear the sound + + source.connect(this.audioContext.destination); // noteOn as start + // start the source playing + + if (source.noteOn) { + source.noteOn(0); + } else { + source.start(0); + } + + this.audioBufferSourceNode = source; // + + this.mediaStreamAudioDestinationNode = this.audioContext.createMediaStreamDestination(); // + + this.audioEnabled(true); // default setting 0 + + this.gainNode.gain.value = 0; + this.playing = false; // + + this.audioSyncVideoOption = { + diff: null + }; + this.audioInfo = { + encType: '', + channels: '', + sampleRate: '' + }; + this.init = false; + this.hasAudio = false; // update + + this.on(EVENTS.videoSyncAudio, options => { + // this.player.debug.log('AudioContext', `videoSyncAudio , audioTimestamp: ${options.audioTimestamp},videoTimestamp: ${options.videoTimestamp},diff:${options.diff}`) + this.audioSyncVideoOption = options; + }); + this.player.debug.log('AudioContext', 'init'); + } + + resetInit() { + this.init = false; + this.audioInfo = { + encType: '', + channels: '', + sampleRate: '' + }; + } + + destroy() { + this.closeAudio(); + this.resetInit(); + this.audioContext.close(); + this.audioContext = null; + this.gainNode = null; + this.hasAudio = false; + this.playing = false; + + if (this.scriptNode) { + this.scriptNode.onaudioprocess = noop; + this.scriptNode = null; + } + + this.audioBufferSourceNode = null; + this.mediaStreamAudioDestinationNode = null; + this.hasInitScriptNode = false; + this.audioSyncVideoOption = { + diff: null + }; + this.off(); + this.player.debug.log('AudioContext', 'destroy'); + } + + updateAudioInfo(data) { + if (data.encTypeCode) { + this.audioInfo.encType = AUDIO_ENC_TYPE[data.encTypeCode]; + } + + if (data.channels) { + this.audioInfo.channels = data.channels; + } + + if (data.sampleRate) { + this.audioInfo.sampleRate = data.sampleRate; + } // audio 基本信息 + + + if (this.audioInfo.sampleRate && this.audioInfo.channels && this.audioInfo.encType && !this.init) { + this.player.emit(EVENTS.audioInfo, this.audioInfo); + this.init = true; + } + } // + + + get isPlaying() { + return this.playing; + } + + get isMute() { + return this.gainNode.gain.value === 0; + } + + get volume() { + return this.gainNode.gain.value; + } + + get bufferSize() { + return this.bufferList.length; + } + + initScriptNode() { + this.playing = true; + + if (this.hasInitScriptNode) { + return; + } + + const channels = this.audioInfo.channels; + const scriptNode = this.audioContext.createScriptProcessor(1024, 0, channels); // tips: if audio isStateSuspended onaudioprocess method not working + + scriptNode.onaudioprocess = audioProcessingEvent => { + const outputBuffer = audioProcessingEvent.outputBuffer; + + if (this.bufferList.length && this.playing) { + // just for wasm + if (!this.player._opt.useWCS && !this.player._opt.useMSE && this.player._opt.wasmDecodeAudioSyncVideo) { + // audio > video + // wait + if (this.audioSyncVideoOption.diff > AUDIO_SYNC_VIDEO_DIFF) { + this.player.debug.warn('AudioContext', `audioSyncVideoOption more than diff :${this.audioSyncVideoOption.diff}, waiting`); // wait + + return; + } // audio < video + // throw away then chase video + else if (this.audioSyncVideoOption.diff < -AUDIO_SYNC_VIDEO_DIFF) { + this.player.debug.warn('AudioContext', `audioSyncVideoOption less than diff :${this.audioSyncVideoOption.diff}, dropping`); // + + let bufferItem = this.bufferList.shift(); // + + while (bufferItem.ts - this.player.videoTimestamp < -AUDIO_SYNC_VIDEO_DIFF && this.bufferList.length > 0) { + // this.player.debug.warn('AudioContext', `audioSyncVideoOption less than inner ts is:${bufferItem.ts}, videoTimestamp is ${this.player.videoTimestamp},diff:${bufferItem.ts - this.player.videoTimestamp}`) + bufferItem = this.bufferList.shift(); + } + + if (this.bufferList.length === 0) { + return; + } + } + } + + if (this.bufferList.length === 0) { + return; + } + + const bufferItem = this.bufferList.shift(); // update audio time stamp + + if (bufferItem && bufferItem.ts) { + this.player.audioTimestamp = bufferItem.ts; + } + + for (let channel = 0; channel < channels; channel++) { + const b = bufferItem.buffer[channel]; + const nowBuffering = outputBuffer.getChannelData(channel); + + for (let i = 0; i < 1024; i++) { + nowBuffering[i] = b[i] || 0; + } + } + } + }; + + scriptNode.connect(this.gainNode); + this.scriptNode = scriptNode; + this.gainNode.connect(this.audioContext.destination); + this.gainNode.connect(this.mediaStreamAudioDestinationNode); + this.hasInitScriptNode = true; + } + + mute(flag) { + if (flag) { + if (!this.isMute) { + this.player.emit(EVENTS.mute, flag); + } + + this.setVolume(0); + this.clear(); + } else { + if (this.isMute) { + this.player.emit(EVENTS.mute, flag); + } + + this.setVolume(0.5); + } + } + + setVolume(volume) { + volume = parseFloat(volume).toFixed(2); + + if (isNaN(volume)) { + return; + } + + this.audioEnabled(true); + volume = clamp(volume, 0, 1); + this.gainNode.gain.value = volume; + this.gainNode.gain.setValueAtTime(volume, this.audioContext.currentTime); + this.player.emit(EVENTS.volumechange, this.player.volume); + } + + closeAudio() { + if (this.hasInitScriptNode) { + this.scriptNode && this.scriptNode.disconnect(this.gainNode); + this.gainNode && this.gainNode.disconnect(this.audioContext.destination); + this.gainNode && this.gainNode.disconnect(this.mediaStreamAudioDestinationNode); + } + + this.clear(); + } // 是否播放。。。 + + + audioEnabled(flag) { + if (flag) { + if (this.audioContext.state === 'suspended') { + // resume + this.audioContext.resume(); + } + } else { + if (this.audioContext.state === 'running') { + // suspend + this.audioContext.suspend(); + } + } + } + + isStateRunning() { + return this.audioContext.state === 'running'; + } + + isStateSuspended() { + return this.audioContext.state === 'suspended'; + } + + clear() { + this.bufferList = []; + } + + play(buffer, ts) { + // if is mute + if (this.isMute) { + return; + } + + this.hasAudio = true; + this.bufferList.push({ + buffer, + ts + }); + + if (this.bufferList.length > 20) { + this.player.debug.warn('AudioContext', `bufferList is large: ${this.bufferList.length}`); // out of memory + + if (this.bufferList.length > 50) { + this.bufferList.shift(); + } + } // this.player.debug.log('AudioContext', `bufferList is ${this.bufferList.length}`) + + } + + pause() { + this.audioSyncVideoOption = { + diff: null + }; + this.playing = false; + this.clear(); + } + + resume() { + this.playing = true; + } + + } + + class Audio { + constructor(player) { + const Loader = Audio.getLoaderFactory(); + return new Loader(player); + } + + static getLoaderFactory() { + return AudioContextLoader; + } + + } + + class FetchLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.playing = false; + this.abortController = new AbortController(); // + + this.streamRate = calculationRate(rate => { + player.emit(EVENTS.kBps, (rate / 1024).toFixed(2)); + }); + player.debug.log('FetchStream', 'init'); + } + + destroy() { + this.abort(); + this.off(); + this.streamRate = null; + this.player.debug.log('FetchStream', 'destroy'); + } + /** + * + * @param url + * @param options + */ + + + fetchStream(url) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + const { + demux + } = this.player; + this.player.debug.log('FetchStream', 'fetchStream', url, JSON.stringify(options)); + this.player._times.streamStart = now(); + const fetchOptions = Object.assign({ + signal: this.abortController.signal + }, { + headers: options.headers || {} + }); + fetch(url, fetchOptions).then(res => { + const reader = res.body.getReader(); + this.emit(EVENTS.streamSuccess); + + const fetchNext = () => { + reader.read().then(_ref => { + let { + done, + value + } = _ref; + + if (done) { + demux.close(); + } else { + this.streamRate && this.streamRate(value.byteLength); + demux.dispatch(value); + fetchNext(); + } + }).catch(e => { + demux.close(); + const errorString = e.toString(); // aborted a request 。 + + if (errorString.indexOf(FETCH_ERROR.abortError1) !== -1) { + return; + } + + if (errorString.indexOf(FETCH_ERROR.abortError2) !== -1) { + return; + } + + if (e.name === FETCH_ERROR.abort) { + return; + } + + this.abort(); + this.emit(EVENTS_ERROR.fetchError, e); + this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError); + }); + }; + + fetchNext(); + }).catch(e => { + if (e.name === 'AbortError') { + return; + } + + demux.close(); + this.abort(); + this.emit(EVENTS_ERROR.fetchError, e); + this.player.emit(EVENTS.error, EVENTS_ERROR.fetchError); + }); + } + + abort() { + if (this.abortController) { + this.abortController.abort(); + this.abortController = null; + } + } + + } + + class WebsocketLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.socket = null; + this.socketStatus = WEBSOCKET_STATUS.notConnect; + this.wsUrl = null; // + + this.streamRate = calculationRate(rate => { + player.emit(EVENTS.kBps, (rate / 1024).toFixed(2)); + }); + player.debug.log('WebsocketLoader', 'init'); + } + + destroy() { + if (this.socket) { + this.socket.close(1000, 'Client disconnecting'); + this.socket = null; + } + + this.socketStatus = WEBSOCKET_STATUS.notConnect; + this.streamRate = null; + this.wsUrl = null; + this.off(); + this.player.debug.log('websocketLoader', 'destroy'); + } + + _createWebSocket() { + const player = this.player; + const { + debug, + events: { + proxy + }, + demux + } = player; + this.socket = new WebSocket(this.wsUrl); + this.socket.binaryType = 'arraybuffer'; + proxy(this.socket, 'open', () => { + this.emit(EVENTS.streamSuccess); + debug.log('websocketLoader', 'socket open'); + this.socketStatus = WEBSOCKET_STATUS.open; + }); + proxy(this.socket, 'message', event => { + this.streamRate && this.streamRate(event.data.byteLength); + + this._handleMessage(event.data); + }); + proxy(this.socket, 'close', () => { + debug.log('websocketLoader', 'socket close'); + this.emit(EVENTS.streamEnd); + this.socketStatus = WEBSOCKET_STATUS.close; + }); + proxy(this.socket, 'error', error => { + debug.log('websocketLoader', 'socket error'); + this.emit(EVENTS_ERROR.websocketError, error); + this.player.emit(EVENTS.error, EVENTS_ERROR.websocketError); + this.socketStatus = WEBSOCKET_STATUS.error; + demux.close(); + debug.log('websocketLoader', `socket error:`, error); + }); + } // + + + _handleMessage(message) { + const { + demux + } = this.player; + + if (!demux) { + this.player.debug.warn('websocketLoader', 'websocket handle message demux is null'); + return; + } + + demux.dispatch(message); + } + /** + * + * @param url + * @param options + */ + + + fetchStream(url, options) { + this.player._times.streamStart = now(); + this.wsUrl = url; + + this._createWebSocket(); + } + + } + + class Stream { + constructor(player) { + const Loader = Stream.getLoaderFactory(player._opt.protocol); + return new Loader(player); + } + + static getLoaderFactory(protocol) { + if (protocol === PLAYER_PLAY_PROTOCOL.fetch) { + return FetchLoader; + } else if (protocol === PLAYER_PLAY_PROTOCOL.websocket) { + return WebsocketLoader; + } + } + + } + + var RecordRTC_1 = createCommonjsModule(function (module) { + + // Last time updated: 2021-03-09 3:20:22 AM UTC + + // ________________ + // RecordRTC v5.6.2 + + // Open-Sourced: https://github.com/muaz-khan/RecordRTC + + // -------------------------------------------------- + // Muaz Khan - www.MuazKhan.com + // MIT License - www.WebRTC-Experiment.com/licence + // -------------------------------------------------- + + // ____________ + // RecordRTC.js + + /** + * {@link https://github.com/muaz-khan/RecordRTC|RecordRTC} is a WebRTC JavaScript library for audio/video as well as screen activity recording. It supports Chrome, Firefox, Opera, Android, and Microsoft Edge. Platforms: Linux, Mac and Windows. + * @summary Record audio, video or screen inside the browser. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTC + * @class + * @example + * var recorder = RecordRTC(mediaStream or [arrayOfMediaStream], { + * type: 'video', // audio or video or gif or canvas + * recorderType: MediaStreamRecorder || CanvasRecorder || StereoAudioRecorder || Etc + * }); + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. + * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, desiredSampRate: 16000, video: HTMLVideoElement, etc.} + */ + + function RecordRTC(mediaStream, config) { + if (!mediaStream) { + throw 'First parameter is required.'; + } + + config = config || { + type: 'video' + }; + + config = new RecordRTCConfiguration(mediaStream, config); + + // a reference to user's recordRTC object + var self = this; + + function startRecording(config2) { + if (!config.disableLogs) { + console.log('RecordRTC version: ', self.version); + } + + if (!!config2) { + // allow users to set options using startRecording method + // config2 is similar to main "config" object (second parameter over RecordRTC constructor) + config = new RecordRTCConfiguration(mediaStream, config2); + } + + if (!config.disableLogs) { + console.log('started recording ' + config.type + ' stream.'); + } + + if (mediaRecorder) { + mediaRecorder.clearRecordedData(); + mediaRecorder.record(); + + setState('recording'); + + if (self.recordingDuration) { + handleRecordingDuration(); + } + return self; + } + + initRecorder(function () { + if (self.recordingDuration) { + handleRecordingDuration(); + } + }); + + return self; + } + + function initRecorder(initCallback) { + if (initCallback) { + config.initCallback = function () { + initCallback(); + initCallback = config.initCallback = null; // recorder.initRecorder should be call-backed once. + }; + } + + var Recorder = new GetRecorderType(mediaStream, config); + + mediaRecorder = new Recorder(mediaStream, config); + mediaRecorder.record(); + + setState('recording'); + + if (!config.disableLogs) { + console.log('Initialized recorderType:', mediaRecorder.constructor.name, 'for output-type:', config.type); + } + } + + function stopRecording(callback) { + callback = callback || function () { }; + + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state === 'paused') { + self.resumeRecording(); + + setTimeout(function () { + stopRecording(callback); + }, 1); + return; + } + + if (self.state !== 'recording' && !config.disableLogs) { + console.warn('Recording state should be: "recording", however current state is: ', self.state); + } + + if (!config.disableLogs) { + console.log('Stopped recording ' + config.type + ' stream.'); + } + + if (config.type !== 'gif') { + mediaRecorder.stop(_callback); + } else { + mediaRecorder.stop(); + _callback(); + } + + setState('stopped'); + + function _callback(__blob) { + if (!mediaRecorder) { + if (typeof callback.call === 'function') { + callback.call(self, ''); + } else { + callback(''); + } + return; + } + + Object.keys(mediaRecorder).forEach(function (key) { + if (typeof mediaRecorder[key] === 'function') { + return; + } + + self[key] = mediaRecorder[key]; + }); + + var blob = mediaRecorder.blob; + + if (!blob) { + if (__blob) { + mediaRecorder.blob = blob = __blob; + } else { + throw 'Recording failed.'; + } + } + + if (blob && !config.disableLogs) { + console.log(blob.type, '->', bytesToSize(blob.size)); + } + + if (callback) { + var url; + + try { + url = URL.createObjectURL(blob); + } catch (e) { } + + if (typeof callback.call === 'function') { + callback.call(self, url); + } else { + callback(url); + } + } + + if (!config.autoWriteToDisk) { + return; + } + + getDataURL(function (dataURL) { + var parameter = {}; + parameter[config.type + 'Blob'] = dataURL; + DiskStorage.Store(parameter); + }); + } + } + + function pauseRecording() { + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state !== 'recording') { + if (!config.disableLogs) { + console.warn('Unable to pause the recording. Recording state: ', self.state); + } + return; + } + + setState('paused'); + + mediaRecorder.pause(); + + if (!config.disableLogs) { + console.log('Paused recording.'); + } + } + + function resumeRecording() { + if (!mediaRecorder) { + warningLog(); + return; + } + + if (self.state !== 'paused') { + if (!config.disableLogs) { + console.warn('Unable to resume the recording. Recording state: ', self.state); + } + return; + } + + setState('recording'); + + // not all libs have this method yet + mediaRecorder.resume(); + + if (!config.disableLogs) { + console.log('Resumed recording.'); + } + } + + function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + } + + function getDataURL(callback, _mediaRecorder) { + if (!callback) { + throw 'Pass a callback function over getDataURL.'; + } + + var blob = _mediaRecorder ? _mediaRecorder.blob : (mediaRecorder || {}).blob; + + if (!blob) { + if (!config.disableLogs) { + console.warn('Blob encoder did not finish its job yet.'); + } + + setTimeout(function () { + getDataURL(callback, _mediaRecorder); + }, 1000); + return; + } + + if (typeof Worker !== 'undefined' && !navigator.mozGetUserMedia) { + var webWorker = processInWebWorker(readFile); + + webWorker.onmessage = function (event) { + callback(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function (event) { + callback(event.target.result); + }; + } + + function processInWebWorker(_function) { + try { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } catch (e) { } + } + } + + function handleRecordingDuration(counter) { + counter = counter || 0; + + if (self.state === 'paused') { + setTimeout(function () { + handleRecordingDuration(counter); + }, 1000); + return; + } + + if (self.state === 'stopped') { + return; + } + + if (counter >= self.recordingDuration) { + stopRecording(self.onRecordingStopped); + return; + } + + counter += 1000; // 1-second + + setTimeout(function () { + handleRecordingDuration(counter); + }, 1000); + } + + function setState(state) { + if (!self) { + return; + } + + self.state = state; + + if (typeof self.onStateChanged.call === 'function') { + self.onStateChanged.call(self, state); + } else { + self.onStateChanged(state); + } + } + + var WARNING = 'It seems that recorder is destroyed or "startRecording" is not invoked for ' + config.type + ' recorder.'; + + function warningLog() { + if (config.disableLogs === true) { + return; + } + + console.warn(WARNING); + } + + var mediaRecorder; + + var returnObject = { + /** + * This method starts the recording. + * @method + * @memberof RecordRTC + * @instance + * @example + * var recorder = RecordRTC(mediaStream, { + * type: 'video' + * }); + * recorder.startRecording(); + */ + startRecording: startRecording, + + /** + * This method stops the recording. It is strongly recommended to get "blob" or "URI" inside the callback to make sure all recorders finished their job. + * @param {function} callback - Callback to get the recorded blob. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * // use either "this" or "recorder" object; both are identical + * video.src = this.toURL(); + * var blob = this.getBlob(); + * }); + */ + stopRecording: stopRecording, + + /** + * This method pauses the recording. You can resume recording using "resumeRecording" method. + * @method + * @memberof RecordRTC + * @instance + * @todo Firefox is unable to pause the recording. Fix it. + * @example + * recorder.pauseRecording(); // pause the recording + * recorder.resumeRecording(); // resume again + */ + pauseRecording: pauseRecording, + + /** + * This method resumes the recording. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.pauseRecording(); // first of all, pause the recording + * recorder.resumeRecording(); // now resume it + */ + resumeRecording: resumeRecording, + + /** + * This method initializes the recording. + * @method + * @memberof RecordRTC + * @instance + * @todo This method should be deprecated. + * @example + * recorder.initRecorder(); + */ + initRecorder: initRecorder, + + /** + * Ask RecordRTC to auto-stop the recording after 5 minutes. + * @method + * @memberof RecordRTC + * @instance + * @example + * var fiveMinutes = 5 * 1000 * 60; + * recorder.setRecordingDuration(fiveMinutes, function() { + * var blob = this.getBlob(); + * video.src = this.toURL(); + * }); + * + * // or otherwise + * recorder.setRecordingDuration(fiveMinutes).onRecordingStopped(function() { + * var blob = this.getBlob(); + * video.src = this.toURL(); + * }); + */ + setRecordingDuration: function (recordingDuration, callback) { + if (typeof recordingDuration === 'undefined') { + throw 'recordingDuration is required.'; + } + + if (typeof recordingDuration !== 'number') { + throw 'recordingDuration must be a number.'; + } + + self.recordingDuration = recordingDuration; + self.onRecordingStopped = callback || function () { }; + + return { + onRecordingStopped: function (callback) { + self.onRecordingStopped = callback; + } + }; + }, + + /** + * This method can be used to clear/reset all the recorded data. + * @method + * @memberof RecordRTC + * @instance + * @todo Figure out the difference between "reset" and "clearRecordedData" methods. + * @example + * recorder.clearRecordedData(); + */ + clearRecordedData: function () { + if (!mediaRecorder) { + warningLog(); + return; + } + + mediaRecorder.clearRecordedData(); + + if (!config.disableLogs) { + console.log('Cleared old recorded data.'); + } + }, + + /** + * Get the recorded blob. Use this method inside the "stopRecording" callback. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * var blob = this.getBlob(); + * + * var file = new File([blob], 'filename.webm', { + * type: 'video/webm' + * }); + * + * var formData = new FormData(); + * formData.append('file', file); // upload "File" object rather than a "Blob" + * uploadToServer(formData); + * }); + * @returns {Blob} Returns recorded data as "Blob" object. + */ + getBlob: function () { + if (!mediaRecorder) { + warningLog(); + return; + } + + return mediaRecorder.blob; + }, + + /** + * Get data-URI instead of Blob. + * @param {function} callback - Callback to get the Data-URI. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * recorder.getDataURL(function(dataURI) { + * video.src = dataURI; + * }); + * }); + */ + getDataURL: getDataURL, + + /** + * Get virtual/temporary URL. Usage of this URL is limited to current tab. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * video.src = this.toURL(); + * }); + * @returns {String} Returns a virtual/temporary URL for the recorded "Blob". + */ + toURL: function () { + if (!mediaRecorder) { + warningLog(); + return; + } + + return URL.createObjectURL(mediaRecorder.blob); + }, + + /** + * Get internal recording object (i.e. internal module) e.g. MutliStreamRecorder, MediaStreamRecorder, StereoAudioRecorder or WhammyRecorder etc. + * @method + * @memberof RecordRTC + * @instance + * @example + * var internalRecorder = recorder.getInternalRecorder(); + * if(internalRecorder instanceof MultiStreamRecorder) { + * internalRecorder.addStreams([newAudioStream]); + * internalRecorder.resetVideoStreams([screenStream]); + * } + * @returns {Object} Returns internal recording object. + */ + getInternalRecorder: function () { + return mediaRecorder; + }, + + /** + * Invoke save-as dialog to save the recorded blob into your disk. + * @param {string} fileName - Set your own file name. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.stopRecording(function() { + * this.save('file-name'); + * + * // or manually: + * invokeSaveAsDialog(this.getBlob(), 'filename.webm'); + * }); + */ + save: function (fileName) { + if (!mediaRecorder) { + warningLog(); + return; + } + + invokeSaveAsDialog(mediaRecorder.blob, fileName); + }, + + /** + * This method gets a blob from indexed-DB storage. + * @param {function} callback - Callback to get the recorded blob. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.getFromDisk(function(dataURL) { + * video.src = dataURL; + * }); + */ + getFromDisk: function (callback) { + if (!mediaRecorder) { + warningLog(); + return; + } + + RecordRTC.getFromDisk(config.type, callback); + }, + + /** + * This method appends an array of webp images to the recorded video-blob. It takes an "array" object. + * @type {Array.} + * @param {Array} arrayOfWebPImages - Array of webp images. + * @method + * @memberof RecordRTC + * @instance + * @todo This method should be deprecated. + * @example + * var arrayOfWebPImages = []; + * arrayOfWebPImages.push({ + * duration: index, + * image: 'data:image/webp;base64,...' + * }); + * recorder.setAdvertisementArray(arrayOfWebPImages); + */ + setAdvertisementArray: function (arrayOfWebPImages) { + config.advertisement = []; + + var length = arrayOfWebPImages.length; + for (var i = 0; i < length; i++) { + config.advertisement.push({ + duration: i, + image: arrayOfWebPImages[i] + }); + } + }, + + /** + * It is equivalent to "recorder.getBlob()" method. Usage of "getBlob" is recommended, though. + * @property {Blob} blob - Recorded Blob can be accessed using this property. + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * var blob = this.blob; + * + * // below one is recommended + * var blob = this.getBlob(); + * }); + */ + blob: null, + + /** + * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. + * @property {number} bufferSize - Buffer-size used to encode the WAV container + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * alert('Recorder used this buffer-size: ' + this.bufferSize); + * }); + */ + bufferSize: 0, + + /** + * This works only with {recorderType:StereoAudioRecorder}. Use this property on "stopRecording" to verify the encoder's sample-rates. + * @property {number} sampleRate - Sample-rates used to encode the WAV container + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * alert('Recorder used these sample-rates: ' + this.sampleRate); + * }); + */ + sampleRate: 0, + + /** + * {recorderType:StereoAudioRecorder} returns ArrayBuffer object. + * @property {ArrayBuffer} buffer - Audio ArrayBuffer, supported only in Chrome. + * @memberof RecordRTC + * @instance + * @readonly + * @example + * recorder.stopRecording(function() { + * var arrayBuffer = this.buffer; + * alert(arrayBuffer.byteLength); + * }); + */ + buffer: null, + + /** + * This method resets the recorder. So that you can reuse single recorder instance many times. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.reset(); + * recorder.startRecording(); + */ + reset: function () { + if (self.state === 'recording' && !config.disableLogs) { + console.warn('Stop an active recorder.'); + } + + if (mediaRecorder && typeof mediaRecorder.clearRecordedData === 'function') { + mediaRecorder.clearRecordedData(); + } + mediaRecorder = null; + setState('inactive'); + self.blob = null; + }, + + /** + * This method is called whenever recorder's state changes. Use this as an "event". + * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. + * @method + * @memberof RecordRTC + * @instance + * @example + * recorder.onStateChanged = function(state) { + * console.log('Recorder state: ', state); + * }; + */ + onStateChanged: function (state) { + if (!config.disableLogs) { + console.log('Recorder state changed:', state); + } + }, + + /** + * A recorder can have inactive, recording, paused or stopped states. + * @property {String} state - A recorder's state can be: recording, paused, stopped or inactive. + * @memberof RecordRTC + * @static + * @readonly + * @example + * // this looper function will keep you updated about the recorder's states. + * (function looper() { + * document.querySelector('h1').innerHTML = 'Recorder\'s state is: ' + recorder.state; + * if(recorder.state === 'stopped') return; // ignore+stop + * setTimeout(looper, 1000); // update after every 3-seconds + * })(); + * recorder.startRecording(); + */ + state: 'inactive', + + /** + * Get recorder's readonly state. + * @method + * @memberof RecordRTC + * @example + * var state = recorder.getState(); + * @returns {String} Returns recording state. + */ + getState: function () { + return self.state; + }, + + /** + * Destroy RecordRTC instance. Clear all recorders and objects. + * @method + * @memberof RecordRTC + * @example + * recorder.destroy(); + */ + destroy: function () { + var disableLogsCache = config.disableLogs; + + config = { + disableLogs: true + }; + self.reset(); + setState('destroyed'); + returnObject = self = null; + + if (Storage.AudioContextConstructor) { + Storage.AudioContextConstructor.close(); + Storage.AudioContextConstructor = null; + } + + config.disableLogs = disableLogsCache; + + if (!config.disableLogs) { + console.log('RecordRTC is destroyed.'); + } + }, + + /** + * RecordRTC version number + * @property {String} version - Release version number. + * @memberof RecordRTC + * @static + * @readonly + * @example + * alert(recorder.version); + */ + version: '5.6.2' + }; + + if (!this) { + self = returnObject; + return returnObject; + } + + // if someone wants to use RecordRTC with the "new" keyword. + for (var prop in returnObject) { + this[prop] = returnObject[prop]; + } + + self = this; + + return returnObject; + } + + RecordRTC.version = '5.6.2'; + + { + module.exports = RecordRTC; + } + + RecordRTC.getFromDisk = function (type, callback) { + if (!callback) { + throw 'callback is mandatory.'; + } + + console.log('Getting recorded ' + (type === 'all' ? 'blobs' : type + ' blob ') + ' from disk!'); + DiskStorage.Fetch(function (dataURL, _type) { + if (type !== 'all' && _type === type + 'Blob' && callback) { + callback(dataURL); + } + + if (type === 'all' && callback) { + callback(dataURL, _type.replace('Blob', '')); + } + }); + }; + + /** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof RecordRTC + * @example + * RecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ + RecordRTC.writeToDisk = function (options) { + console.log('Writing recorded blob(s) to disk!'); + options = options || {}; + if (options.audio && options.video && options.gif) { + options.audio.getDataURL(function (audioDataURL) { + options.video.getDataURL(function (videoDataURL) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + }); + } else if (options.audio && options.video) { + options.audio.getDataURL(function (audioDataURL) { + options.video.getDataURL(function (videoDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + videoBlob: videoDataURL + }); + }); + }); + } else if (options.audio && options.gif) { + options.audio.getDataURL(function (audioDataURL) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.video && options.gif) { + options.video.getDataURL(function (videoDataURL) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL, + gifBlob: gifDataURL + }); + }); + }); + } else if (options.audio) { + options.audio.getDataURL(function (audioDataURL) { + DiskStorage.Store({ + audioBlob: audioDataURL + }); + }); + } else if (options.video) { + options.video.getDataURL(function (videoDataURL) { + DiskStorage.Store({ + videoBlob: videoDataURL + }); + }); + } else if (options.gif) { + options.gif.getDataURL(function (gifDataURL) { + DiskStorage.Store({ + gifBlob: gifDataURL + }); + }); + } + }; + + // __________________________ + // RecordRTC-Configuration.js + + /** + * {@link RecordRTCConfiguration} is an inner/private helper for {@link RecordRTC}. + * @summary It configures the 2nd parameter passed over {@link RecordRTC} and returns a valid "config" object. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTCConfiguration + * @class + * @example + * var options = RecordRTCConfiguration(mediaStream, options); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, getNativeBlob:true, etc.} + */ + + function RecordRTCConfiguration(mediaStream, config) { + if (!config.recorderType && !config.type) { + if (!!config.audio && !!config.video) { + config.type = 'video'; + } else if (!!config.audio && !config.video) { + config.type = 'audio'; + } + } + + if (config.recorderType && !config.type) { + if (config.recorderType === WhammyRecorder || config.recorderType === CanvasRecorder || (typeof WebAssemblyRecorder !== 'undefined' && config.recorderType === WebAssemblyRecorder)) { + config.type = 'video'; + } else if (config.recorderType === GifRecorder) { + config.type = 'gif'; + } else if (config.recorderType === StereoAudioRecorder) { + config.type = 'audio'; + } else if (config.recorderType === MediaStreamRecorder) { + if (getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) { + config.type = 'video'; + } else if (!getTracks(mediaStream, 'audio').length && getTracks(mediaStream, 'video').length) { + config.type = 'video'; + } else if (getTracks(mediaStream, 'audio').length && !getTracks(mediaStream, 'video').length) { + config.type = 'audio'; + } else; + } + } + + if (typeof MediaStreamRecorder !== 'undefined' && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) { + if (!config.mimeType) { + config.mimeType = 'video/webm'; + } + + if (!config.type) { + config.type = config.mimeType.split('/')[0]; + } + + if (!config.bitsPerSecond); + } + + // consider default type=audio + if (!config.type) { + if (config.mimeType) { + config.type = config.mimeType.split('/')[0]; + } + if (!config.type) { + config.type = 'audio'; + } + } + + return config; + } + + // __________________ + // GetRecorderType.js + + /** + * {@link GetRecorderType} is an inner/private helper for {@link RecordRTC}. + * @summary It returns best recorder-type available for your browser. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef GetRecorderType + * @class + * @example + * var RecorderType = GetRecorderType(options); + * var recorder = new RecorderType(options); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {type:"video", disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} + */ + + function GetRecorderType(mediaStream, config) { + var recorder; + + // StereoAudioRecorder can work with all three: Edge, Firefox and Chrome + // todo: detect if it is Edge, then auto use: StereoAudioRecorder + if (isChrome || isEdge || isOpera) { + // Media Stream Recording API has not been implemented in chrome yet; + // That's why using WebAudio API to record stereo audio in WAV format + recorder = StereoAudioRecorder; + } + + if (typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype && !isChrome) { + recorder = MediaStreamRecorder; + } + + // video recorder (in WebM format) + if (config.type === 'video' && (isChrome || isOpera)) { + recorder = WhammyRecorder; + + if (typeof WebAssemblyRecorder !== 'undefined' && typeof ReadableStream !== 'undefined') { + recorder = WebAssemblyRecorder; + } + } + + // video recorder (in Gif format) + if (config.type === 'gif') { + recorder = GifRecorder; + } + + // html2canvas recording! + if (config.type === 'canvas') { + recorder = CanvasRecorder; + } + + if (isMediaRecorderCompatible() && recorder !== CanvasRecorder && recorder !== GifRecorder && typeof MediaRecorder !== 'undefined' && 'requestData' in MediaRecorder.prototype) { + if (getTracks(mediaStream, 'video').length || getTracks(mediaStream, 'audio').length) { + // audio-only recording + if (config.type === 'audio') { + if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('audio/webm')) { + recorder = MediaStreamRecorder; + } + // else recorder = StereoAudioRecorder; + } else { + // video or screen tracks + if (typeof MediaRecorder.isTypeSupported === 'function' && MediaRecorder.isTypeSupported('video/webm')) { + recorder = MediaStreamRecorder; + } + } + } + } + + if (mediaStream instanceof Array && mediaStream.length) { + recorder = MultiStreamRecorder; + } + + if (config.recorderType) { + recorder = config.recorderType; + } + + if (!config.disableLogs && !!recorder && !!recorder.name) { + console.log('Using recorderType:', recorder.name || recorder.constructor.name); + } + + if (!recorder && isSafari) { + recorder = MediaStreamRecorder; + } + + return recorder; + } + + // _____________ + // MRecordRTC.js + + /** + * MRecordRTC runs on top of {@link RecordRTC} to bring multiple recordings in a single place, by providing simple API. + * @summary MRecordRTC stands for "Multiple-RecordRTC". + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef MRecordRTC + * @class + * @example + * var recorder = new MRecordRTC(); + * recorder.addStream(MediaStream); + * recorder.mediaType = { + * audio: true, // or StereoAudioRecorder or MediaStreamRecorder + * video: true, // or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder + * gif: true // or GifRecorder + * }; + * // mimeType is optional and should be set only in advance cases. + * recorder.mimeType = { + * audio: 'audio/wav', + * video: 'video/webm', + * gif: 'image/gif' + * }; + * recorder.startRecording(); + * @see For further information: + * @see {@link https://github.com/muaz-khan/RecordRTC/tree/master/MRecordRTC|MRecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @requires {@link RecordRTC} + */ + + function MRecordRTC(mediaStream) { + + /** + * This method attaches MediaStream object to {@link MRecordRTC}. + * @param {MediaStream} mediaStream - A MediaStream object, either fetched using getUserMedia API, or generated using captureStreamUntilEnded or WebAudio API. + * @method + * @memberof MRecordRTC + * @example + * recorder.addStream(MediaStream); + */ + this.addStream = function (_mediaStream) { + if (_mediaStream) { + mediaStream = _mediaStream; + } + }; + + /** + * This property can be used to set the recording type e.g. audio, or video, or gif, or canvas. + * @property {object} mediaType - {audio: true, video: true, gif: true} + * @memberof MRecordRTC + * @example + * var recorder = new MRecordRTC(); + * recorder.mediaType = { + * audio: true, // TRUE or StereoAudioRecorder or MediaStreamRecorder + * video: true, // TRUE or WhammyRecorder or MediaStreamRecorder or WebAssemblyRecorder or CanvasRecorder + * gif : true // TRUE or GifRecorder + * }; + */ + this.mediaType = { + audio: true, + video: true + }; + + /** + * This method starts recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.startRecording(); + */ + this.startRecording = function () { + var mediaType = this.mediaType; + var recorderType; + var mimeType = this.mimeType || { + audio: null, + video: null, + gif: null + }; + + if (typeof mediaType.audio !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'audio').length) { + mediaType.audio = false; + } + + if (typeof mediaType.video !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) { + mediaType.video = false; + } + + if (typeof mediaType.gif !== 'function' && isMediaRecorderCompatible() && !getTracks(mediaStream, 'video').length) { + mediaType.gif = false; + } + + if (!mediaType.audio && !mediaType.video && !mediaType.gif) { + throw 'MediaStream must have either audio or video tracks.'; + } + + if (!!mediaType.audio) { + recorderType = null; + if (typeof mediaType.audio === 'function') { + recorderType = mediaType.audio; + } + + this.audioRecorder = new RecordRTC(mediaStream, { + type: 'audio', + bufferSize: this.bufferSize, + sampleRate: this.sampleRate, + numberOfAudioChannels: this.numberOfAudioChannels || 2, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.audio, + timeSlice: this.timeSlice, + onTimeStamp: this.onTimeStamp + }); + + if (!mediaType.video) { + this.audioRecorder.startRecording(); + } + } + + if (!!mediaType.video) { + recorderType = null; + if (typeof mediaType.video === 'function') { + recorderType = mediaType.video; + } + + var newStream = mediaStream; + + if (isMediaRecorderCompatible() && !!mediaType.audio && typeof mediaType.audio === 'function') { + var videoTrack = getTracks(mediaStream, 'video')[0]; + + if (isFirefox) { + newStream = new MediaStream(); + newStream.addTrack(videoTrack); + + if (recorderType && recorderType === WhammyRecorder) { + // Firefox does NOT supports webp-encoding yet + // But Firefox do supports WebAssemblyRecorder + recorderType = MediaStreamRecorder; + } + } else { + newStream = new MediaStream(); + newStream.addTrack(videoTrack); + } + } + + this.videoRecorder = new RecordRTC(newStream, { + type: 'video', + video: this.video, + canvas: this.canvas, + frameInterval: this.frameInterval || 10, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.video, + timeSlice: this.timeSlice, + onTimeStamp: this.onTimeStamp, + workerPath: this.workerPath, + webAssemblyPath: this.webAssemblyPath, + frameRate: this.frameRate, // used by WebAssemblyRecorder; values: usually 30; accepts any. + bitrate: this.bitrate // used by WebAssemblyRecorder; values: 0 to 1000+ + }); + + if (!mediaType.audio) { + this.videoRecorder.startRecording(); + } + } + + if (!!mediaType.audio && !!mediaType.video) { + var self = this; + + var isSingleRecorder = isMediaRecorderCompatible() === true; + + if (mediaType.audio instanceof StereoAudioRecorder && !!mediaType.video) { + isSingleRecorder = false; + } else if (mediaType.audio !== true && mediaType.video !== true && mediaType.audio !== mediaType.video) { + isSingleRecorder = false; + } + + if (isSingleRecorder === true) { + self.audioRecorder = null; + self.videoRecorder.startRecording(); + } else { + self.videoRecorder.initRecorder(function () { + self.audioRecorder.initRecorder(function () { + // Both recorders are ready to record things accurately + self.videoRecorder.startRecording(); + self.audioRecorder.startRecording(); + }); + }); + } + } + + if (!!mediaType.gif) { + recorderType = null; + if (typeof mediaType.gif === 'function') { + recorderType = mediaType.gif; + } + this.gifRecorder = new RecordRTC(mediaStream, { + type: 'gif', + frameRate: this.frameRate || 200, + quality: this.quality || 10, + disableLogs: this.disableLogs, + recorderType: recorderType, + mimeType: mimeType.gif + }); + this.gifRecorder.startRecording(); + } + }; + + /** + * This method stops recording. + * @param {function} callback - Callback function is invoked when all encoders finished their jobs. + * @method + * @memberof MRecordRTC + * @example + * recorder.stopRecording(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + */ + this.stopRecording = function (callback) { + callback = callback || function () { }; + + if (this.audioRecorder) { + this.audioRecorder.stopRecording(function (blobURL) { + callback(blobURL, 'audio'); + }); + } + + if (this.videoRecorder) { + this.videoRecorder.stopRecording(function (blobURL) { + callback(blobURL, 'video'); + }); + } + + if (this.gifRecorder) { + this.gifRecorder.stopRecording(function (blobURL) { + callback(blobURL, 'gif'); + }); + } + }; + + /** + * This method pauses recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.pauseRecording(); + */ + this.pauseRecording = function () { + if (this.audioRecorder) { + this.audioRecorder.pauseRecording(); + } + + if (this.videoRecorder) { + this.videoRecorder.pauseRecording(); + } + + if (this.gifRecorder) { + this.gifRecorder.pauseRecording(); + } + }; + + /** + * This method resumes recording. + * @method + * @memberof MRecordRTC + * @example + * recorder.resumeRecording(); + */ + this.resumeRecording = function () { + if (this.audioRecorder) { + this.audioRecorder.resumeRecording(); + } + + if (this.videoRecorder) { + this.videoRecorder.resumeRecording(); + } + + if (this.gifRecorder) { + this.gifRecorder.resumeRecording(); + } + }; + + /** + * This method can be used to manually get all recorded blobs. + * @param {function} callback - All recorded blobs are passed back to the "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getBlob(function(recording){ + * var audioBlob = recording.audio; + * var videoBlob = recording.video; + * var gifBlob = recording.gif; + * }); + * // or + * var audioBlob = recorder.getBlob().audio; + * var videoBlob = recorder.getBlob().video; + */ + this.getBlob = function (callback) { + var output = {}; + + if (this.audioRecorder) { + output.audio = this.audioRecorder.getBlob(); + } + + if (this.videoRecorder) { + output.video = this.videoRecorder.getBlob(); + } + + if (this.gifRecorder) { + output.gif = this.gifRecorder.getBlob(); + } + + if (callback) { + callback(output); + } + + return output; + }; + + /** + * Destroy all recorder instances. + * @method + * @memberof MRecordRTC + * @example + * recorder.destroy(); + */ + this.destroy = function () { + if (this.audioRecorder) { + this.audioRecorder.destroy(); + this.audioRecorder = null; + } + + if (this.videoRecorder) { + this.videoRecorder.destroy(); + this.videoRecorder = null; + } + + if (this.gifRecorder) { + this.gifRecorder.destroy(); + this.gifRecorder = null; + } + }; + + /** + * This method can be used to manually get all recorded blobs' DataURLs. + * @param {function} callback - All recorded blobs' DataURLs are passed back to the "callback" function. + * @method + * @memberof MRecordRTC + * @example + * recorder.getDataURL(function(recording){ + * var audioDataURL = recording.audio; + * var videoDataURL = recording.video; + * var gifDataURL = recording.gif; + * }); + */ + this.getDataURL = function (callback) { + this.getBlob(function (blob) { + if (blob.audio && blob.video) { + getDataURL(blob.audio, function (_audioDataURL) { + getDataURL(blob.video, function (_videoDataURL) { + callback({ + audio: _audioDataURL, + video: _videoDataURL + }); + }); + }); + } else if (blob.audio) { + getDataURL(blob.audio, function (_audioDataURL) { + callback({ + audio: _audioDataURL + }); + }); + } else if (blob.video) { + getDataURL(blob.video, function (_videoDataURL) { + callback({ + video: _videoDataURL + }); + }); + } + }); + + function getDataURL(blob, callback00) { + if (typeof Worker !== 'undefined') { + var webWorker = processInWebWorker(function readFile(_blob) { + postMessage(new FileReaderSync().readAsDataURL(_blob)); + }); + + webWorker.onmessage = function (event) { + callback00(event.data); + }; + + webWorker.postMessage(blob); + } else { + var reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onload = function (event) { + callback00(event.target.result); + }; + } + } + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + var url; + if (typeof URL !== 'undefined') { + url = URL; + } else if (typeof webkitURL !== 'undefined') { + url = webkitURL; + } else { + throw 'Neither URL nor webkitURL detected.'; + } + url.revokeObjectURL(blob); + return worker; + } + }; + + /** + * This method can be used to ask {@link MRecordRTC} to write all recorded blobs into IndexedDB storage. + * @method + * @memberof MRecordRTC + * @example + * recorder.writeToDisk(); + */ + this.writeToDisk = function () { + RecordRTC.writeToDisk({ + audio: this.audioRecorder, + video: this.videoRecorder, + gif: this.gifRecorder + }); + }; + + /** + * This method can be used to invoke a save-as dialog for all recorded blobs. + * @param {object} args - {audio: 'audio-name', video: 'video-name', gif: 'gif-name'} + * @method + * @memberof MRecordRTC + * @example + * recorder.save({ + * audio: 'audio-file-name', + * video: 'video-file-name', + * gif : 'gif-file-name' + * }); + */ + this.save = function (args) { + args = args || { + audio: true, + video: true, + gif: true + }; + + if (!!args.audio && this.audioRecorder) { + this.audioRecorder.save(typeof args.audio === 'string' ? args.audio : ''); + } + + if (!!args.video && this.videoRecorder) { + this.videoRecorder.save(typeof args.video === 'string' ? args.video : ''); + } + if (!!args.gif && this.gifRecorder) { + this.gifRecorder.save(typeof args.gif === 'string' ? args.gif : ''); + } + }; + } + + /** + * This method can be used to get all recorded blobs from IndexedDB storage. + * @param {string} type - 'all' or 'audio' or 'video' or 'gif' + * @param {function} callback - Callback function to get all stored blobs. + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.getFromDisk('all', function(dataURL, type){ + * if(type === 'audio') { } + * if(type === 'video') { } + * if(type === 'gif') { } + * }); + */ + MRecordRTC.getFromDisk = RecordRTC.getFromDisk; + + /** + * This method can be used to store recorded blobs into IndexedDB storage. + * @param {object} options - {audio: Blob, video: Blob, gif: Blob} + * @method + * @memberof MRecordRTC + * @example + * MRecordRTC.writeToDisk({ + * audio: audioBlob, + * video: videoBlob, + * gif : gifBlob + * }); + */ + MRecordRTC.writeToDisk = RecordRTC.writeToDisk; + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.MRecordRTC = MRecordRTC; + } + + var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; + + (function (that) { + if (!that) { + return; + } + + if (typeof window !== 'undefined') { + return; + } + + if (typeof commonjsGlobal === 'undefined') { + return; + } + + commonjsGlobal.navigator = { + userAgent: browserFakeUserAgent, + getUserMedia: function () { } + }; + + if (!commonjsGlobal.console) { + commonjsGlobal.console = {}; + } + + if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') { + commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function () { + console.log(arguments); + }; + } + + if (typeof document === 'undefined') { + /*global document:true */ + that.document = { + documentElement: { + appendChild: function () { + return ''; + } + } + }; + + document.createElement = document.captureStream = document.mozCaptureStream = function () { + var obj = { + getContext: function () { + return obj; + }, + play: function () { }, + pause: function () { }, + drawImage: function () { }, + toDataURL: function () { + return ''; + }, + style: {} + }; + return obj; + }; + + that.HTMLVideoElement = function () { }; + } + + if (typeof location === 'undefined') { + /*global location:true */ + that.location = { + protocol: 'file:', + href: '', + hash: '' + }; + } + + if (typeof screen === 'undefined') { + /*global screen:true */ + that.screen = { + width: 0, + height: 0 + }; + } + + if (typeof URL === 'undefined') { + /*global screen:true */ + that.URL = { + createObjectURL: function () { + return ''; + }, + revokeObjectURL: function () { + return ''; + } + }; + } + + /*global window:true */ + that.window = commonjsGlobal; + })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null); + + // _____________________________ + // Cross-Browser-Declarations.js + + // animation-frame used in WebM recording + + /*jshint -W079 */ + var requestAnimationFrame = window.requestAnimationFrame; + if (typeof requestAnimationFrame === 'undefined') { + if (typeof webkitRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = webkitRequestAnimationFrame; + } else if (typeof mozRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = mozRequestAnimationFrame; + } else if (typeof msRequestAnimationFrame !== 'undefined') { + /*global requestAnimationFrame:true */ + requestAnimationFrame = msRequestAnimationFrame; + } else if (typeof requestAnimationFrame === 'undefined') { + // via: https://gist.github.com/paulirish/1579671 + var lastTime = 0; + + /*global requestAnimationFrame:true */ + requestAnimationFrame = function (callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = setTimeout(function () { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + } + } + + /*jshint -W079 */ + var cancelAnimationFrame = window.cancelAnimationFrame; + if (typeof cancelAnimationFrame === 'undefined') { + if (typeof webkitCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = webkitCancelAnimationFrame; + } else if (typeof mozCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = mozCancelAnimationFrame; + } else if (typeof msCancelAnimationFrame !== 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = msCancelAnimationFrame; + } else if (typeof cancelAnimationFrame === 'undefined') { + /*global cancelAnimationFrame:true */ + cancelAnimationFrame = function (id) { + clearTimeout(id); + }; + } + } + + // WebAudio API representer + var AudioContext = window.AudioContext; + + if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } + } + + /*jshint -W079 */ + var URL = window.URL; + + if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; + } + + if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } + } + + var isEdge = navigator.userAgent.indexOf('Edge') !== -1 && (!!navigator.msSaveBlob || !!navigator.msSaveOrOpenBlob); + var isOpera = !!window.opera || navigator.userAgent.indexOf('OPR/') !== -1; + var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1 && ('netscape' in window) && / rv:/.test(navigator.userAgent); + var isChrome = (!isOpera && !isEdge && !!navigator.webkitGetUserMedia) || isElectron() || navigator.userAgent.toLowerCase().indexOf('chrome/') !== -1; + + var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + if (isSafari && !isChrome && navigator.userAgent.indexOf('CriOS') !== -1) { + isSafari = false; + isChrome = true; + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined') { + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function () { + this.getTracks().forEach(function (track) { + track.stop(); + }); + }; + } + } + + // below function via: http://goo.gl/B3ae8c + /** + * Return human-readable file size. + * @param {number} bytes - Pass bytes and get formatted string. + * @returns {string} - formatted string + * @example + * bytesToSize(1024*1024*5) === '5 GB' + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + function bytesToSize(bytes) { + var k = 1000; + var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + if (bytes === 0) { + return '0 Bytes'; + } + var i = parseInt(Math.floor(Math.log(bytes) / Math.log(k)), 10); + return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]; + } + + /** + * @param {Blob} file - File or Blob object. This parameter is required. + * @param {string} fileName - Optional file name e.g. "Recorded-Video.webm" + * @example + * invokeSaveAsDialog(blob or file, [optional] fileName); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + function invokeSaveAsDialog(file, fileName) { + if (!file) { + throw 'Blob object is required.'; + } + + if (!file.type) { + try { + file.type = 'video/webm'; + } catch (e) { } + } + + var fileExtension = (file.type || 'video/webm').split('/')[1]; + if (fileExtension.indexOf(';') !== -1) { + // extended mimetype, e.g. 'video/webm;codecs=vp8,opus' + fileExtension = fileExtension.split(';')[0]; + } + if (fileName && fileName.indexOf('.') !== -1) { + var splitted = fileName.split('.'); + fileName = splitted[0]; + fileExtension = splitted[1]; + } + + var fileFullName = (fileName || (Math.round(Math.random() * 9999999999) + 888888888)) + '.' + fileExtension; + + if (typeof navigator.msSaveOrOpenBlob !== 'undefined') { + return navigator.msSaveOrOpenBlob(file, fileFullName); + } else if (typeof navigator.msSaveBlob !== 'undefined') { + return navigator.msSaveBlob(file, fileFullName); + } + + var hyperlink = document.createElement('a'); + hyperlink.href = URL.createObjectURL(file); + hyperlink.download = fileFullName; + + hyperlink.style = 'display:none;opacity:0;color:transparent;'; + (document.body || document.documentElement).appendChild(hyperlink); + + if (typeof hyperlink.click === 'function') { + hyperlink.click(); + } else { + hyperlink.target = '_blank'; + hyperlink.dispatchEvent(new MouseEvent('click', { + view: window, + bubbles: true, + cancelable: true + })); + } + + URL.revokeObjectURL(hyperlink.href); + } + + /** + * from: https://github.com/cheton/is-electron/blob/master/index.js + **/ + function isElectron() { + // Renderer process + if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') { + return true; + } + + // Main process + if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) { + return true; + } + + // Detect the user agent when the `nodeIntegration` option is set to true + if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) { + return true; + } + + return false; + } + + function getTracks(stream, kind) { + if (!stream || !stream.getTracks) { + return []; + } + + return stream.getTracks().filter(function (t) { + return t.kind === (kind || 'audio'); + }); + } + + function setSrcObject(stream, element) { + if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + element.srcObject = stream; + } + } + + /** + * @param {Blob} file - File or Blob object. + * @param {function} callback - Callback function. + * @example + * getSeekableBlob(blob or file, callback); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + function getSeekableBlob(inputBlob, callback) { + // EBML.js copyrights goes to: https://github.com/legokichi/ts-ebml + if (typeof EBML === 'undefined') { + throw new Error('Please link: https://www.webrtc-experiment.com/EBML.js'); + } + + var reader = new EBML.Reader(); + var decoder = new EBML.Decoder(); + var tools = EBML.tools; + + var fileReader = new FileReader(); + fileReader.onload = function (e) { + var ebmlElms = decoder.decode(this.result); + ebmlElms.forEach(function (element) { + reader.read(element); + }); + reader.stop(); + var refinedMetadataBuf = tools.makeMetadataSeekable(reader.metadatas, reader.duration, reader.cues); + var body = this.result.slice(reader.metadataSize); + var newBlob = new Blob([refinedMetadataBuf, body], { + type: 'video/webm' + }); + + callback(newBlob); + }; + fileReader.readAsArrayBuffer(inputBlob); + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.invokeSaveAsDialog = invokeSaveAsDialog; + RecordRTC.getTracks = getTracks; + RecordRTC.getSeekableBlob = getSeekableBlob; + RecordRTC.bytesToSize = bytesToSize; + RecordRTC.isElectron = isElectron; + } + + // __________ (used to handle stuff like http://goo.gl/xmE5eg) issue #129 + // Storage.js + + /** + * Storage is a standalone object used by {@link RecordRTC} to store reusable objects e.g. "new AudioContext". + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @example + * Storage.AudioContext === webkitAudioContext + * @property {webkitAudioContext} AudioContext - Keeps a reference to AudioContext object. + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + var Storage = {}; + + if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; + } else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.Storage = Storage; + } + + function isMediaRecorderCompatible() { + if (isFirefox || isSafari || isEdge) { + return true; + } + var nAgt = navigator.userAgent; + var fullVersion = '' + parseFloat(navigator.appVersion); + var majorVersion = parseInt(navigator.appVersion, 10); + var verOffset, ix; + + if (isChrome || isOpera) { + verOffset = nAgt.indexOf('Chrome'); + fullVersion = nAgt.substring(verOffset + 7); + } + + // trim the fullVersion string at semicolon/space if present + if ((ix = fullVersion.indexOf(';')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + if ((ix = fullVersion.indexOf(' ')) !== -1) { + fullVersion = fullVersion.substring(0, ix); + } + + majorVersion = parseInt('' + fullVersion, 10); + + if (isNaN(majorVersion)) { + fullVersion = '' + parseFloat(navigator.appVersion); + majorVersion = parseInt(navigator.appVersion, 10); + } + + return majorVersion >= 49; + } + + // ______________________ + // MediaStreamRecorder.js + + /** + * MediaStreamRecorder is an abstraction layer for {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. It is used by {@link RecordRTC} to record MediaStream(s) in both Chrome and Firefox. + * @summary Runs top over {@link https://w3c.github.io/mediacapture-record/MediaRecorder.html|MediaRecorder API}. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://github.com/muaz-khan|Muaz Khan} + * @typedef MediaStreamRecorder + * @class + * @example + * var config = { + * mimeType: 'video/webm', // vp8, vp9, h264, mkv, opus/vorbis + * audioBitsPerSecond : 256 * 8 * 1024, + * videoBitsPerSecond : 256 * 8 * 1024, + * bitsPerSecond: 256 * 8 * 1024, // if this is provided, skip above two + * checkForInactiveTracks: true, + * timeSlice: 1000, // concatenate intervals based blobs + * ondataavailable: function() {} // get intervals based blobs + * } + * var recorder = new MediaStreamRecorder(mediaStream, config); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * + * // or + * var blob = recorder.blob; + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {disableLogs:true, initCallback: function, mimeType: "video/webm", timeSlice: 1000} + * @throws Will throw an error if first argument "MediaStream" is missing. Also throws error if "MediaRecorder API" are not supported by the browser. + */ + + function MediaStreamRecorder(mediaStream, config) { + var self = this; + + if (typeof mediaStream === 'undefined') { + throw 'First argument "MediaStream" is required.'; + } + + if (typeof MediaRecorder === 'undefined') { + throw 'Your browser does not support the Media Recorder API. Please try other modules e.g. WhammyRecorder or StereoAudioRecorder.'; + } + + config = config || { + // bitsPerSecond: 256 * 8 * 1024, + mimeType: 'video/webm' + }; + + if (config.type === 'audio') { + if (getTracks(mediaStream, 'video').length && getTracks(mediaStream, 'audio').length) { + var stream; + if (!!navigator.mozGetUserMedia) { + stream = new MediaStream(); + stream.addTrack(getTracks(mediaStream, 'audio')[0]); + } else { + // webkitMediaStream + stream = new MediaStream(getTracks(mediaStream, 'audio')); + } + mediaStream = stream; + } + + if (!config.mimeType || config.mimeType.toString().toLowerCase().indexOf('audio') === -1) { + config.mimeType = isChrome ? 'audio/webm' : 'audio/ogg'; + } + + if (config.mimeType && config.mimeType.toString().toLowerCase() !== 'audio/ogg' && !!navigator.mozGetUserMedia) { + // forcing better codecs on Firefox (via #166) + config.mimeType = 'audio/ogg'; + } + } + + var arrayOfBlobs = []; + + /** + * This method returns array of blobs. Use only with "timeSlice". Its useful to preview recording anytime, without using the "stop" method. + * @method + * @memberof MediaStreamRecorder + * @example + * var arrayOfBlobs = recorder.getArrayOfBlobs(); + * @returns {Array} Returns array of recorded blobs. + */ + this.getArrayOfBlobs = function () { + return arrayOfBlobs; + }; + + /** + * This method records MediaStream. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.record(); + */ + this.record = function () { + // set defaults + self.blob = null; + self.clearRecordedData(); + self.timestamps = []; + allStates = []; + arrayOfBlobs = []; + + var recorderHints = config; + + if (!config.disableLogs) { + console.log('Passing following config over MediaRecorder API.', recorderHints); + } + + if (mediaRecorder) { + // mandatory to make sure Firefox doesn't fails to record streams 3-4 times without reloading the page. + mediaRecorder = null; + } + + if (isChrome && !isMediaRecorderCompatible()) { + // to support video-only recording on stable + recorderHints = 'video/vp8'; + } + + if (typeof MediaRecorder.isTypeSupported === 'function' && recorderHints.mimeType) { + if (!MediaRecorder.isTypeSupported(recorderHints.mimeType)) { + if (!config.disableLogs) { + console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType); + } + + recorderHints.mimeType = config.type === 'audio' ? 'audio/webm' : 'video/webm'; + } + } + + // using MediaRecorder API here + try { + mediaRecorder = new MediaRecorder(mediaStream, recorderHints); + + // reset + config.mimeType = recorderHints.mimeType; + } catch (e) { + // chrome-based fallback + mediaRecorder = new MediaRecorder(mediaStream); + } + + // old hack? + if (recorderHints.mimeType && !MediaRecorder.isTypeSupported && 'canRecordMimeType' in mediaRecorder && mediaRecorder.canRecordMimeType(recorderHints.mimeType) === false) { + if (!config.disableLogs) { + console.warn('MediaRecorder API seems unable to record mimeType:', recorderHints.mimeType); + } + } + + // Dispatching OnDataAvailable Handler + mediaRecorder.ondataavailable = function (e) { + if (e.data) { + allStates.push('ondataavailable: ' + bytesToSize(e.data.size)); + } + + if (typeof config.timeSlice === 'number') { + if (e.data && e.data.size) { + arrayOfBlobs.push(e.data); + updateTimeStamp(); + + if (typeof config.ondataavailable === 'function') { + // intervals based blobs + var blob = config.getNativeBlob ? e.data : new Blob([e.data], { + type: getMimeType(recorderHints) + }); + config.ondataavailable(blob); + } + } + return; + } + + if (!e.data || !e.data.size || e.data.size < 100 || self.blob) { + // make sure that stopRecording always getting fired + // even if there is invalid data + if (self.recordingCallback) { + self.recordingCallback(new Blob([], { + type: getMimeType(recorderHints) + })); + self.recordingCallback = null; + } + return; + } + + self.blob = config.getNativeBlob ? e.data : new Blob([e.data], { + type: getMimeType(recorderHints) + }); + + if (self.recordingCallback) { + self.recordingCallback(self.blob); + self.recordingCallback = null; + } + }; + + mediaRecorder.onstart = function () { + allStates.push('started'); + }; + + mediaRecorder.onpause = function () { + allStates.push('paused'); + }; + + mediaRecorder.onresume = function () { + allStates.push('resumed'); + }; + + mediaRecorder.onstop = function () { + allStates.push('stopped'); + }; + + mediaRecorder.onerror = function (error) { + if (!error) { + return; + } + + if (!error.name) { + error.name = 'UnknownError'; + } + + allStates.push('error: ' + error); + + if (!config.disableLogs) { + // via: https://w3c.github.io/mediacapture-record/MediaRecorder.html#exception-summary + if (error.name.toString().toLowerCase().indexOf('invalidstate') !== -1) { + console.error('The MediaRecorder is not in a state in which the proposed operation is allowed to be executed.', error); + } else if (error.name.toString().toLowerCase().indexOf('notsupported') !== -1) { + console.error('MIME type (', recorderHints.mimeType, ') is not supported.', error); + } else if (error.name.toString().toLowerCase().indexOf('security') !== -1) { + console.error('MediaRecorder security error', error); + } + + // older code below + else if (error.name === 'OutOfMemory') { + console.error('The UA has exhaused the available memory. User agents SHOULD provide as much additional information as possible in the message attribute.', error); + } else if (error.name === 'IllegalStreamModification') { + console.error('A modification to the stream has occurred that makes it impossible to continue recording. An example would be the addition of a Track while recording is occurring. User agents SHOULD provide as much additional information as possible in the message attribute.', error); + } else if (error.name === 'OtherRecordingError') { + console.error('Used for an fatal error other than those listed above. User agents SHOULD provide as much additional information as possible in the message attribute.', error); + } else if (error.name === 'GenericError') { + console.error('The UA cannot provide the codec or recording option that has been requested.', error); + } else { + console.error('MediaRecorder Error', error); + } + } + + (function (looper) { + if (!self.manuallyStopped && mediaRecorder && mediaRecorder.state === 'inactive') { + delete config.timeslice; + + // 10 minutes, enough? + mediaRecorder.start(10 * 60 * 1000); + return; + } + + setTimeout(looper, 1000); + })(); + + if (mediaRecorder.state !== 'inactive' && mediaRecorder.state !== 'stopped') { + mediaRecorder.stop(); + } + }; + + if (typeof config.timeSlice === 'number') { + updateTimeStamp(); + mediaRecorder.start(config.timeSlice); + } else { + // default is 60 minutes; enough? + // use config => {timeSlice: 1000} otherwise + + mediaRecorder.start(3.6e+6); + } + + if (config.initCallback) { + config.initCallback(); // old code + } + }; + + /** + * @property {Array} timestamps - Array of time stamps + * @memberof MediaStreamRecorder + * @example + * console.log(recorder.timestamps); + */ + this.timestamps = []; + + function updateTimeStamp() { + self.timestamps.push(new Date().getTime()); + + if (typeof config.onTimeStamp === 'function') { + config.onTimeStamp(self.timestamps[self.timestamps.length - 1], self.timestamps); + } + } + + function getMimeType(secondObject) { + if (mediaRecorder && mediaRecorder.mimeType) { + return mediaRecorder.mimeType; + } + + return secondObject.mimeType || 'video/webm'; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + self.manuallyStopped = true; // used inside the mediaRecorder.onerror + + if (!mediaRecorder) { + return; + } + + this.recordingCallback = callback; + + if (mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + } + + if (typeof config.timeSlice === 'number') { + setTimeout(function () { + self.blob = new Blob(arrayOfBlobs, { + type: getMimeType(config) + }); + + self.recordingCallback(self.blob); + }, 100); + } + }; + + /** + * This method pauses the recording process. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + if (!mediaRecorder) { + return; + } + + if (mediaRecorder.state === 'recording') { + mediaRecorder.pause(); + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + if (!mediaRecorder) { + return; + } + + if (mediaRecorder.state === 'paused') { + mediaRecorder.resume(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof MediaStreamRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (mediaRecorder && mediaRecorder.state === 'recording') { + self.stop(clearRecordedDataCB); + } + + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + arrayOfBlobs = []; + mediaRecorder = null; + self.timestamps = []; + } + + // Reference to "MediaRecorder" object + var mediaRecorder; + + /** + * Access to native MediaRecorder API + * @method + * @memberof MediaStreamRecorder + * @instance + * @example + * var internal = recorder.getInternalRecorder(); + * internal.ondataavailable = function() {}; // override + * internal.stream, internal.onpause, internal.onstop, etc. + * @returns {Object} Returns internal recording object. + */ + this.getInternalRecorder = function () { + return mediaRecorder; + }; + + function isMediaStreamActive() { + if ('active' in mediaStream) { + if (!mediaStream.active) { + return false; + } + } else if ('ended' in mediaStream) { // old hack + if (mediaStream.ended) { + return false; + } + } + return true; + } + + /** + * @property {Blob} blob - Recorded data as "Blob" object. + * @memberof MediaStreamRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + this.blob = null; + + + /** + * Get MediaRecorder readonly state. + * @method + * @memberof MediaStreamRecorder + * @example + * var state = recorder.getState(); + * @returns {String} Returns recording state. + */ + this.getState = function () { + if (!mediaRecorder) { + return 'inactive'; + } + + return mediaRecorder.state || 'inactive'; + }; + + // list of all recording states + var allStates = []; + + /** + * Get MediaRecorder all recording states. + * @method + * @memberof MediaStreamRecorder + * @example + * var state = recorder.getAllStates(); + * @returns {Array} Returns all recording states + */ + this.getAllStates = function () { + return allStates; + }; + + // if any Track within the MediaStream is muted or not enabled at any time, + // the browser will only record black frames + // or silence since that is the content produced by the Track + // so we need to stopRecording as soon as any single track ends. + if (typeof config.checkForInactiveTracks === 'undefined') { + config.checkForInactiveTracks = false; // disable to minimize CPU usage + } + + var self = this; + + // this method checks if media stream is stopped + // or if any track is ended. + (function looper() { + if (!mediaRecorder || config.checkForInactiveTracks === false) { + return; + } + + if (isMediaStreamActive() === false) { + if (!config.disableLogs) { + console.log('MediaStream seems stopped.'); + } + self.stop(); + return; + } + + setTimeout(looper, 1000); // check every second + })(); + + // for debugging + this.name = 'MediaStreamRecorder'; + this.toString = function () { + return this.name; + }; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.MediaStreamRecorder = MediaStreamRecorder; + } + + // source code from: http://typedarray.org/wp-content/projects/WebAudioRecorder/script.js + // https://github.com/mattdiamond/Recorderjs#license-mit + // ______________________ + // StereoAudioRecorder.js + + /** + * StereoAudioRecorder is a standalone class used by {@link RecordRTC} to bring "stereo" audio-recording in chrome. + * @summary JavaScript standalone object for stereo audio recording. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef StereoAudioRecorder + * @class + * @example + * var recorder = new StereoAudioRecorder(MediaStream, { + * sampleRate: 44100, + * bufferSize: 4096 + * }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {sampleRate: 44100, bufferSize: 4096, numberOfAudioChannels: 1, etc.} + */ + + function StereoAudioRecorder(mediaStream, config) { + if (!getTracks(mediaStream, 'audio').length) { + throw 'Your stream has no audio tracks.'; + } + + config = config || {}; + + var self = this; + + // variables + var leftchannel = []; + var rightchannel = []; + var recording = false; + var recordingLength = 0; + var jsAudioNode; + + var numberOfAudioChannels = 2; + + /** + * Set sample rates such as 8K or 16K. Reference: http://stackoverflow.com/a/28977136/552182 + * @property {number} desiredSampRate - Desired Bits per sample * 1000 + * @memberof StereoAudioRecorder + * @instance + * @example + * var recorder = StereoAudioRecorder(mediaStream, { + * desiredSampRate: 16 * 1000 // bits-per-sample * 1000 + * }); + */ + var desiredSampRate = config.desiredSampRate; + + // backward compatibility + if (config.leftChannel === true) { + numberOfAudioChannels = 1; + } + + if (config.numberOfAudioChannels === 1) { + numberOfAudioChannels = 1; + } + + if (!numberOfAudioChannels || numberOfAudioChannels < 1) { + numberOfAudioChannels = 2; + } + + if (!config.disableLogs) { + console.log('StereoAudioRecorder is set to record number of channels: ' + numberOfAudioChannels); + } + + // if any Track within the MediaStream is muted or not enabled at any time, + // the browser will only record black frames + // or silence since that is the content produced by the Track + // so we need to stopRecording as soon as any single track ends. + if (typeof config.checkForInactiveTracks === 'undefined') { + config.checkForInactiveTracks = true; + } + + function isMediaStreamActive() { + if (config.checkForInactiveTracks === false) { + // always return "true" + return true; + } + + if ('active' in mediaStream) { + if (!mediaStream.active) { + return false; + } + } else if ('ended' in mediaStream) { // old hack + if (mediaStream.ended) { + return false; + } + } + return true; + } + + /** + * This method records MediaStream. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.record(); + */ + this.record = function () { + if (isMediaStreamActive() === false) { + throw 'Please make sure MediaStream is active.'; + } + + resetVariables(); + + isAudioProcessStarted = isPaused = false; + recording = true; + + if (typeof config.timeSlice !== 'undefined') { + looper(); + } + }; + + function mergeLeftRightBuffers(config, callback) { + function mergeAudioBuffers(config, cb) { + var numberOfAudioChannels = config.numberOfAudioChannels; + + // todo: "slice(0)" --- is it causes loop? Should be removed? + var leftBuffers = config.leftBuffers.slice(0); + var rightBuffers = config.rightBuffers.slice(0); + var sampleRate = config.sampleRate; + var internalInterleavedLength = config.internalInterleavedLength; + var desiredSampRate = config.desiredSampRate; + + if (numberOfAudioChannels === 2) { + leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); + rightBuffers = mergeBuffers(rightBuffers, internalInterleavedLength); + + if (desiredSampRate) { + leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); + rightBuffers = interpolateArray(rightBuffers, desiredSampRate, sampleRate); + } + } + + if (numberOfAudioChannels === 1) { + leftBuffers = mergeBuffers(leftBuffers, internalInterleavedLength); + + if (desiredSampRate) { + leftBuffers = interpolateArray(leftBuffers, desiredSampRate, sampleRate); + } + } + + // set sample rate as desired sample rate + if (desiredSampRate) { + sampleRate = desiredSampRate; + } + + // for changing the sampling rate, reference: + // http://stackoverflow.com/a/28977136/552182 + function interpolateArray(data, newSampleRate, oldSampleRate) { + var fitCount = Math.round(data.length * (newSampleRate / oldSampleRate)); + var newData = []; + var springFactor = Number((data.length - 1) / (fitCount - 1)); + newData[0] = data[0]; + for (var i = 1; i < fitCount - 1; i++) { + var tmp = i * springFactor; + var before = Number(Math.floor(tmp)).toFixed(); + var after = Number(Math.ceil(tmp)).toFixed(); + var atPoint = tmp - before; + newData[i] = linearInterpolate(data[before], data[after], atPoint); + } + newData[fitCount - 1] = data[data.length - 1]; + return newData; + } + + function linearInterpolate(before, after, atPoint) { + return before + (after - before) * atPoint; + } + + function mergeBuffers(channelBuffer, rLength) { + var result = new Float64Array(rLength); + var offset = 0; + var lng = channelBuffer.length; + + for (var i = 0; i < lng; i++) { + var buffer = channelBuffer[i]; + result.set(buffer, offset); + offset += buffer.length; + } + + return result; + } + + function interleave(leftChannel, rightChannel) { + var length = leftChannel.length + rightChannel.length; + + var result = new Float64Array(length); + + var inputIndex = 0; + + for (var index = 0; index < length;) { + result[index++] = leftChannel[inputIndex]; + result[index++] = rightChannel[inputIndex]; + inputIndex++; + } + return result; + } + + function writeUTFBytes(view, offset, string) { + var lng = string.length; + for (var i = 0; i < lng; i++) { + view.setUint8(offset + i, string.charCodeAt(i)); + } + } + + // interleave both channels together + var interleaved; + + if (numberOfAudioChannels === 2) { + interleaved = interleave(leftBuffers, rightBuffers); + } + + if (numberOfAudioChannels === 1) { + interleaved = leftBuffers; + } + + var interleavedLength = interleaved.length; + + // create wav file + var resultingBufferLength = 44 + interleavedLength * 2; + + var buffer = new ArrayBuffer(resultingBufferLength); + + var view = new DataView(buffer); + + // RIFF chunk descriptor/identifier + writeUTFBytes(view, 0, 'RIFF'); + + // RIFF chunk length + // changed "44" to "36" via #401 + view.setUint32(4, 36 + interleavedLength * 2, true); + + // RIFF type + writeUTFBytes(view, 8, 'WAVE'); + + // format chunk identifier + // FMT sub-chunk + writeUTFBytes(view, 12, 'fmt '); + + // format chunk length + view.setUint32(16, 16, true); + + // sample format (raw) + view.setUint16(20, 1, true); + + // stereo (2 channels) + view.setUint16(22, numberOfAudioChannels, true); + + // sample rate + view.setUint32(24, sampleRate, true); + + // byte rate (sample rate * block align) + view.setUint32(28, sampleRate * numberOfAudioChannels * 2, true); + + // block align (channel count * bytes per sample) + view.setUint16(32, numberOfAudioChannels * 2, true); + + // bits per sample + view.setUint16(34, 16, true); + + // data sub-chunk + // data chunk identifier + writeUTFBytes(view, 36, 'data'); + + // data chunk length + view.setUint32(40, interleavedLength * 2, true); + + // write the PCM samples + var lng = interleavedLength; + var index = 44; + var volume = 1; + for (var i = 0; i < lng; i++) { + view.setInt16(index, interleaved[i] * (0x7FFF * volume), true); + index += 2; + } + + if (cb) { + return cb({ + buffer: buffer, + view: view + }); + } + + postMessage({ + buffer: buffer, + view: view + }); + } + + if (config.noWorker) { + mergeAudioBuffers(config, function (data) { + callback(data.buffer, data.view); + }); + return; + } + + + var webWorker = processInWebWorker(mergeAudioBuffers); + + webWorker.onmessage = function (event) { + callback(event.data.buffer, event.data.view); + + // release memory + URL.revokeObjectURL(webWorker.workerURL); + + // kill webworker (or Chrome will kill your page after ~25 calls) + webWorker.terminate(); + }; + + webWorker.postMessage(config); + } + + function processInWebWorker(_function) { + var workerURL = URL.createObjectURL(new Blob([_function.toString(), + ';this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(workerURL); + worker.workerURL = workerURL; + return worker; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + // stop recording + recording = false; + + mergeLeftRightBuffers({ + desiredSampRate: desiredSampRate, + sampleRate: sampleRate, + numberOfAudioChannels: numberOfAudioChannels, + internalInterleavedLength: recordingLength, + leftBuffers: leftchannel, + rightBuffers: numberOfAudioChannels === 1 ? [] : rightchannel, + noWorker: config.noWorker + }, function (buffer, view) { + /** + * @property {Blob} blob - The recorded blob object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + self.blob = new Blob([view], { + type: 'audio/wav' + }); + + /** + * @property {ArrayBuffer} buffer - The recorded buffer object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var buffer = recorder.buffer; + * }); + */ + self.buffer = new ArrayBuffer(view.buffer.byteLength); + + /** + * @property {DataView} view - The recorded data-view object. + * @memberof StereoAudioRecorder + * @example + * recorder.stop(function(){ + * var view = recorder.view; + * }); + */ + self.view = view; + + self.sampleRate = desiredSampRate || sampleRate; + self.bufferSize = bufferSize; + + // recorded audio length + self.length = recordingLength; + + isAudioProcessStarted = false; + + if (callback) { + callback(self.blob); + } + }); + }; + + if (typeof RecordRTC.Storage === 'undefined') { + RecordRTC.Storage = { + AudioContextConstructor: null, + AudioContext: window.AudioContext || window.webkitAudioContext + }; + } + + if (!RecordRTC.Storage.AudioContextConstructor || RecordRTC.Storage.AudioContextConstructor.state === 'closed') { + RecordRTC.Storage.AudioContextConstructor = new RecordRTC.Storage.AudioContext(); + } + + var context = RecordRTC.Storage.AudioContextConstructor; + + // creates an audio node from the microphone incoming stream + var audioInput = context.createMediaStreamSource(mediaStream); + + var legalBufferValues = [0, 256, 512, 1024, 2048, 4096, 8192, 16384]; + + /** + * From the spec: This value controls how frequently the audioprocess event is + * dispatched and how many sample-frames need to be processed each call. + * Lower values for buffer size will result in a lower (better) latency. + * Higher values will be necessary to avoid audio breakup and glitches + * The size of the buffer (in sample-frames) which needs to + * be processed each time onprocessaudio is called. + * Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384). + * @property {number} bufferSize - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * bufferSize: 4096 + * }); + */ + + // "0" means, let chrome decide the most accurate buffer-size for current platform. + var bufferSize = typeof config.bufferSize === 'undefined' ? 4096 : config.bufferSize; + + if (legalBufferValues.indexOf(bufferSize) === -1) { + if (!config.disableLogs) { + console.log('Legal values for buffer-size are ' + JSON.stringify(legalBufferValues, null, '\t')); + } + } + + if (context.createJavaScriptNode) { + jsAudioNode = context.createJavaScriptNode(bufferSize, numberOfAudioChannels, numberOfAudioChannels); + } else if (context.createScriptProcessor) { + jsAudioNode = context.createScriptProcessor(bufferSize, numberOfAudioChannels, numberOfAudioChannels); + } else { + throw 'WebAudio API has no support on this browser.'; + } + + // connect the stream to the script processor + audioInput.connect(jsAudioNode); + + if (!config.bufferSize) { + bufferSize = jsAudioNode.bufferSize; // device buffer-size + } + + /** + * The sample rate (in sample-frames per second) at which the + * AudioContext handles audio. It is assumed that all AudioNodes + * in the context run at this rate. In making this assumption, + * sample-rate converters or "varispeed" processors are not supported + * in real-time processing. + * The sampleRate parameter describes the sample-rate of the + * linear PCM audio data in the buffer in sample-frames per second. + * An implementation must support sample-rates in at least + * the range 22050 to 96000. + * @property {number} sampleRate - Buffer-size for how frequently the audioprocess event is dispatched. + * @memberof StereoAudioRecorder + * @example + * recorder = new StereoAudioRecorder(mediaStream, { + * sampleRate: 44100 + * }); + */ + var sampleRate = typeof config.sampleRate !== 'undefined' ? config.sampleRate : context.sampleRate || 44100; + + if (sampleRate < 22050 || sampleRate > 96000) { + // Ref: http://stackoverflow.com/a/26303918/552182 + if (!config.disableLogs) { + console.log('sample-rate must be under range 22050 and 96000.'); + } + } + + if (!config.disableLogs) { + if (config.desiredSampRate) { + console.log('Desired sample-rate: ' + config.desiredSampRate); + } + } + + var isPaused = false; + /** + * This method pauses the recording process. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPaused = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + if (isMediaStreamActive() === false) { + throw 'Please make sure MediaStream is active.'; + } + + if (!recording) { + if (!config.disableLogs) { + console.log('Seems recording has been restarted.'); + } + this.record(); + return; + } + + isPaused = false; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof StereoAudioRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + config.checkForInactiveTracks = false; + + if (recording) { + this.stop(clearRecordedDataCB); + } + + clearRecordedDataCB(); + }; + + function resetVariables() { + leftchannel = []; + rightchannel = []; + recordingLength = 0; + isAudioProcessStarted = false; + recording = false; + isPaused = false; + context = null; + + self.leftchannel = leftchannel; + self.rightchannel = rightchannel; + self.numberOfAudioChannels = numberOfAudioChannels; + self.desiredSampRate = desiredSampRate; + self.sampleRate = sampleRate; + self.recordingLength = recordingLength; + + intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + } + + function clearRecordedDataCB() { + if (jsAudioNode) { + jsAudioNode.onaudioprocess = null; + jsAudioNode.disconnect(); + jsAudioNode = null; + } + + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + + resetVariables(); + } + + // for debugging + this.name = 'StereoAudioRecorder'; + this.toString = function () { + return this.name; + }; + + var isAudioProcessStarted = false; + + function onAudioProcessDataAvailable(e) { + if (isPaused) { + return; + } + + if (isMediaStreamActive() === false) { + if (!config.disableLogs) { + console.log('MediaStream seems stopped.'); + } + jsAudioNode.disconnect(); + recording = false; + } + + if (!recording) { + if (audioInput) { + audioInput.disconnect(); + audioInput = null; + } + return; + } + + /** + * This method is called on "onaudioprocess" event's first invocation. + * @method {function} onAudioProcessStarted + * @memberof StereoAudioRecorder + * @example + * recorder.onAudioProcessStarted: function() { }; + */ + if (!isAudioProcessStarted) { + isAudioProcessStarted = true; + if (config.onAudioProcessStarted) { + config.onAudioProcessStarted(); + } + + if (config.initCallback) { + config.initCallback(); + } + } + + var left = e.inputBuffer.getChannelData(0); + + // we clone the samples + var chLeft = new Float32Array(left); + leftchannel.push(chLeft); + + if (numberOfAudioChannels === 2) { + var right = e.inputBuffer.getChannelData(1); + var chRight = new Float32Array(right); + rightchannel.push(chRight); + } + + recordingLength += bufferSize; + + // export raw PCM + self.recordingLength = recordingLength; + + if (typeof config.timeSlice !== 'undefined') { + intervalsBasedBuffers.recordingLength += bufferSize; + intervalsBasedBuffers.left.push(chLeft); + + if (numberOfAudioChannels === 2) { + intervalsBasedBuffers.right.push(chRight); + } + } + } + + jsAudioNode.onaudioprocess = onAudioProcessDataAvailable; + + // to prevent self audio to be connected with speakers + if (context.createMediaStreamDestination) { + jsAudioNode.connect(context.createMediaStreamDestination()); + } else { + jsAudioNode.connect(context.destination); + } + + // export raw PCM + this.leftchannel = leftchannel; + this.rightchannel = rightchannel; + this.numberOfAudioChannels = numberOfAudioChannels; + this.desiredSampRate = desiredSampRate; + this.sampleRate = sampleRate; + self.recordingLength = recordingLength; + + // helper for intervals based blobs + var intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + + // this looper is used to support intervals based blobs (via timeSlice+ondataavailable) + function looper() { + if (!recording || typeof config.ondataavailable !== 'function' || typeof config.timeSlice === 'undefined') { + return; + } + + if (intervalsBasedBuffers.left.length) { + mergeLeftRightBuffers({ + desiredSampRate: desiredSampRate, + sampleRate: sampleRate, + numberOfAudioChannels: numberOfAudioChannels, + internalInterleavedLength: intervalsBasedBuffers.recordingLength, + leftBuffers: intervalsBasedBuffers.left, + rightBuffers: numberOfAudioChannels === 1 ? [] : intervalsBasedBuffers.right + }, function (buffer, view) { + var blob = new Blob([view], { + type: 'audio/wav' + }); + config.ondataavailable(blob); + + setTimeout(looper, config.timeSlice); + }); + + intervalsBasedBuffers = { + left: [], + right: [], + recordingLength: 0 + }; + } else { + setTimeout(looper, config.timeSlice); + } + } + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.StereoAudioRecorder = StereoAudioRecorder; + } + + // _________________ + // CanvasRecorder.js + + /** + * CanvasRecorder is a standalone class used by {@link RecordRTC} to bring HTML5-Canvas recording into video WebM. It uses HTML2Canvas library and runs top over {@link Whammy}. + * @summary HTML2Canvas recording into video WebM. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef CanvasRecorder + * @class + * @example + * var recorder = new CanvasRecorder(htmlElement, { disableLogs: true, useWhammyRecorder: true }); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {HTMLElement} htmlElement - querySelector/getElementById/getElementsByTagName[0]/etc. + * @param {object} config - {disableLogs:true, initCallback: function} + */ + + function CanvasRecorder(htmlElement, config) { + if (typeof html2canvas === 'undefined') { + throw 'Please link: https://www.webrtc-experiment.com/screenshot.js'; + } + + config = config || {}; + if (!config.frameInterval) { + config.frameInterval = 10; + } + + // via DetectRTC.js + var isCanvasSupportsStreamCapturing = false; + ['captureStream', 'mozCaptureStream', 'webkitCaptureStream'].forEach(function (item) { + if (item in document.createElement('canvas')) { + isCanvasSupportsStreamCapturing = true; + } + }); + + var _isChrome = (!!window.webkitRTCPeerConnection || !!window.webkitGetUserMedia) && !!window.chrome; + + var chromeVersion = 50; + var matchArray = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); + if (_isChrome && matchArray && matchArray[2]) { + chromeVersion = parseInt(matchArray[2], 10); + } + + if (_isChrome && chromeVersion < 52) { + isCanvasSupportsStreamCapturing = false; + } + + if (config.useWhammyRecorder) { + isCanvasSupportsStreamCapturing = false; + } + + var globalCanvas, mediaStreamRecorder; + + if (isCanvasSupportsStreamCapturing) { + if (!config.disableLogs) { + console.log('Your browser supports both MediRecorder API and canvas.captureStream!'); + } + + if (htmlElement instanceof HTMLCanvasElement) { + globalCanvas = htmlElement; + } else if (htmlElement instanceof CanvasRenderingContext2D) { + globalCanvas = htmlElement.canvas; + } else { + throw 'Please pass either HTMLCanvasElement or CanvasRenderingContext2D.'; + } + } else if (!!navigator.mozGetUserMedia) { + if (!config.disableLogs) { + console.error('Canvas recording is NOT supported in Firefox.'); + } + } + + var isRecording; + + /** + * This method records Canvas. + * @method + * @memberof CanvasRecorder + * @example + * recorder.record(); + */ + this.record = function () { + isRecording = true; + + if (isCanvasSupportsStreamCapturing && !config.useWhammyRecorder) { + // CanvasCaptureMediaStream + var canvasMediaStream; + if ('captureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.captureStream(25); // 25 FPS + } else if ('mozCaptureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.mozCaptureStream(25); + } else if ('webkitCaptureStream' in globalCanvas) { + canvasMediaStream = globalCanvas.webkitCaptureStream(25); + } + + try { + var mdStream = new MediaStream(); + mdStream.addTrack(getTracks(canvasMediaStream, 'video')[0]); + canvasMediaStream = mdStream; + } catch (e) { } + + if (!canvasMediaStream) { + throw 'captureStream API are NOT available.'; + } + + // Note: Jan 18, 2016 status is that, + // Firefox MediaRecorder API can't record CanvasCaptureMediaStream object. + mediaStreamRecorder = new MediaStreamRecorder(canvasMediaStream, { + mimeType: config.mimeType || 'video/webm' + }); + mediaStreamRecorder.record(); + } else { + whammy.frames = []; + lastTime = new Date().getTime(); + drawCanvasFrame(); + } + + if (config.initCallback) { + config.initCallback(); + } + }; + + this.getWebPImages = function (callback) { + if (htmlElement.nodeName.toLowerCase() !== 'canvas') { + callback(); + return; + } + + var framesLength = whammy.frames.length; + whammy.frames.forEach(function (frame, idx) { + var framesRemaining = framesLength - idx; + if (!config.disableLogs) { + console.log(framesRemaining + '/' + framesLength + ' frames remaining'); + } + + if (config.onEncodingCallback) { + config.onEncodingCallback(framesRemaining, framesLength); + } + + var webp = frame.image.toDataURL('image/webp', 1); + whammy.frames[idx].image = webp; + }); + + if (!config.disableLogs) { + console.log('Generating WebM'); + } + + callback(); + }; + + /** + * This method stops recording Canvas. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof CanvasRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + isRecording = false; + + var that = this; + + if (isCanvasSupportsStreamCapturing && mediaStreamRecorder) { + mediaStreamRecorder.stop(callback); + return; + } + + this.getWebPImages(function () { + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof CanvasRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + whammy.compile(function (blob) { + if (!config.disableLogs) { + console.log('Recording finished!'); + } + + that.blob = blob; + + if (that.blob.forEach) { + that.blob = new Blob([], { + type: 'video/webm' + }); + } + + if (callback) { + callback(that.blob); + } + + whammy.frames = []; + }); + }); + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof CanvasRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPausedRecording = true; + + if (mediaStreamRecorder instanceof MediaStreamRecorder) { + mediaStreamRecorder.pause(); + return; + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof CanvasRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPausedRecording = false; + + if (mediaStreamRecorder instanceof MediaStreamRecorder) { + mediaStreamRecorder.resume(); + return; + } + + if (!isRecording) { + this.record(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof CanvasRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (isRecording) { + this.stop(clearRecordedDataCB); + } + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + whammy.frames = []; + isRecording = false; + isPausedRecording = false; + } + + // for debugging + this.name = 'CanvasRecorder'; + this.toString = function () { + return this.name; + }; + + function cloneCanvas() { + //create a new canvas + var newCanvas = document.createElement('canvas'); + var context = newCanvas.getContext('2d'); + + //set dimensions + newCanvas.width = htmlElement.width; + newCanvas.height = htmlElement.height; + + //apply the old canvas to the new one + context.drawImage(htmlElement, 0, 0); + + //return the new canvas + return newCanvas; + } + + function drawCanvasFrame() { + if (isPausedRecording) { + lastTime = new Date().getTime(); + return setTimeout(drawCanvasFrame, 500); + } + + if (htmlElement.nodeName.toLowerCase() === 'canvas') { + var duration = new Date().getTime() - lastTime; + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + whammy.frames.push({ + image: cloneCanvas(), + duration: duration + }); + + if (isRecording) { + setTimeout(drawCanvasFrame, config.frameInterval); + } + return; + } + + html2canvas(htmlElement, { + grabMouse: typeof config.showMousePointer === 'undefined' || config.showMousePointer, + onrendered: function (canvas) { + var duration = new Date().getTime() - lastTime; + if (!duration) { + return setTimeout(drawCanvasFrame, config.frameInterval); + } + + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + whammy.frames.push({ + image: canvas.toDataURL('image/webp', 1), + duration: duration + }); + + if (isRecording) { + setTimeout(drawCanvasFrame, config.frameInterval); + } + } + }); + } + + var lastTime = new Date().getTime(); + + var whammy = new Whammy.Video(100); + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.CanvasRecorder = CanvasRecorder; + } + + // _________________ + // WhammyRecorder.js + + /** + * WhammyRecorder is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It runs top over {@link Whammy}. + * @summary Video recording feature in Chrome. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef WhammyRecorder + * @class + * @example + * var recorder = new WhammyRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {disableLogs: true, initCallback: function, video: HTMLVideoElement, etc.} + */ + + function WhammyRecorder(mediaStream, config) { + + config = config || {}; + + if (!config.frameInterval) { + config.frameInterval = 10; + } + + if (!config.disableLogs) { + console.log('Using frames-interval:', config.frameInterval); + } + + /** + * This method records video. + * @method + * @memberof WhammyRecorder + * @example + * recorder.record(); + */ + this.record = function () { + if (!config.width) { + config.width = 320; + } + + if (!config.height) { + config.height = 240; + } + + if (!config.video) { + config.video = { + width: config.width, + height: config.height + }; + } + + if (!config.canvas) { + config.canvas = { + width: config.width, + height: config.height + }; + } + + canvas.width = config.canvas.width || 320; + canvas.height = config.canvas.height || 240; + + context = canvas.getContext('2d'); + + // setting defaults + if (config.video && config.video instanceof HTMLVideoElement) { + video = config.video.cloneNode(); + + if (config.initCallback) { + config.initCallback(); + } + } else { + video = document.createElement('video'); + + setSrcObject(mediaStream, video); + + video.onloadedmetadata = function () { // "onloadedmetadata" may NOT work in FF? + if (config.initCallback) { + config.initCallback(); + } + }; + + video.width = config.video.width; + video.height = config.video.height; + } + + video.muted = true; + video.play(); + + lastTime = new Date().getTime(); + whammy = new Whammy.Video(); + + if (!config.disableLogs) { + console.log('canvas resolutions', canvas.width, '*', canvas.height); + console.log('video width/height', video.width || canvas.width, '*', video.height || canvas.height); + } + + drawFrames(config.frameInterval); + }; + + /** + * Draw and push frames to Whammy + * @param {integer} frameInterval - set minimum interval (in milliseconds) between each time we push a frame to Whammy + */ + function drawFrames(frameInterval) { + frameInterval = typeof frameInterval !== 'undefined' ? frameInterval : 10; + + var duration = new Date().getTime() - lastTime; + if (!duration) { + return setTimeout(drawFrames, frameInterval, frameInterval); + } + + if (isPausedRecording) { + lastTime = new Date().getTime(); + return setTimeout(drawFrames, 100); + } + + // via #206, by Jack i.e. @Seymourr + lastTime = new Date().getTime(); + + if (video.paused) { + // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 + // Tweak for Android Chrome + video.play(); + } + + context.drawImage(video, 0, 0, canvas.width, canvas.height); + whammy.frames.push({ + duration: duration, + image: canvas.toDataURL('image/webp') + }); + + if (!isStopDrawing) { + setTimeout(drawFrames, frameInterval, frameInterval); + } + } + + function asyncLoop(o) { + var i = -1, + length = o.length; + + (function loop() { + i++; + if (i === length) { + o.callback(); + return; + } + + // "setTimeout" added by Jim McLeod + setTimeout(function () { + o.functionToLoop(loop, i); + }, 1); + })(); + } + + + /** + * remove black frames from the beginning to the specified frame + * @param {Array} _frames - array of frames to be checked + * @param {number} _framesToCheck - number of frame until check will be executed (-1 - will drop all frames until frame not matched will be found) + * @param {number} _pixTolerance - 0 - very strict (only black pixel color) ; 1 - all + * @param {number} _frameTolerance - 0 - very strict (only black frame color) ; 1 - all + * @returns {Array} - array of frames + */ + // pull#293 by @volodalexey + function dropBlackFrames(_frames, _framesToCheck, _pixTolerance, _frameTolerance, callback) { + var localCanvas = document.createElement('canvas'); + localCanvas.width = canvas.width; + localCanvas.height = canvas.height; + var context2d = localCanvas.getContext('2d'); + var resultFrames = []; + + var checkUntilNotBlack = _framesToCheck === -1; + var endCheckFrame = (_framesToCheck && _framesToCheck > 0 && _framesToCheck <= _frames.length) ? + _framesToCheck : _frames.length; + var sampleColor = { + r: 0, + g: 0, + b: 0 + }; + var maxColorDifference = Math.sqrt( + Math.pow(255, 2) + + Math.pow(255, 2) + + Math.pow(255, 2) + ); + var pixTolerance = _pixTolerance && _pixTolerance >= 0 && _pixTolerance <= 1 ? _pixTolerance : 0; + var frameTolerance = _frameTolerance && _frameTolerance >= 0 && _frameTolerance <= 1 ? _frameTolerance : 0; + var doNotCheckNext = false; + + asyncLoop({ + length: endCheckFrame, + functionToLoop: function (loop, f) { + var matchPixCount, endPixCheck, maxPixCount; + + var finishImage = function () { + if (!doNotCheckNext && maxPixCount - matchPixCount <= maxPixCount * frameTolerance); else { + // console.log('frame is passed : ' + f); + if (checkUntilNotBlack) { + doNotCheckNext = true; + } + resultFrames.push(_frames[f]); + } + loop(); + }; + + if (!doNotCheckNext) { + var image = new Image(); + image.onload = function () { + context2d.drawImage(image, 0, 0, canvas.width, canvas.height); + var imageData = context2d.getImageData(0, 0, canvas.width, canvas.height); + matchPixCount = 0; + endPixCheck = imageData.data.length; + maxPixCount = imageData.data.length / 4; + + for (var pix = 0; pix < endPixCheck; pix += 4) { + var currentColor = { + r: imageData.data[pix], + g: imageData.data[pix + 1], + b: imageData.data[pix + 2] + }; + var colorDifference = Math.sqrt( + Math.pow(currentColor.r - sampleColor.r, 2) + + Math.pow(currentColor.g - sampleColor.g, 2) + + Math.pow(currentColor.b - sampleColor.b, 2) + ); + // difference in color it is difference in color vectors (r1,g1,b1) <=> (r2,g2,b2) + if (colorDifference <= maxColorDifference * pixTolerance) { + matchPixCount++; + } + } + finishImage(); + }; + image.src = _frames[f].image; + } else { + finishImage(); + } + }, + callback: function () { + resultFrames = resultFrames.concat(_frames.slice(endCheckFrame)); + + if (resultFrames.length <= 0) { + // at least one last frame should be available for next manipulation + // if total duration of all frames will be < 1000 than ffmpeg doesn't work well... + resultFrames.push(_frames[_frames.length - 1]); + } + callback(resultFrames); + } + }); + } + + var isStopDrawing = false; + + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WhammyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + isStopDrawing = true; + + var _this = this; + // analyse of all frames takes some time! + setTimeout(function () { + // e.g. dropBlackFrames(frames, 10, 1, 1) - will cut all 10 frames + // e.g. dropBlackFrames(frames, 10, 0.5, 0.5) - will analyse 10 frames + // e.g. dropBlackFrames(frames, 10) === dropBlackFrames(frames, 10, 0, 0) - will analyse 10 frames with strict black color + dropBlackFrames(whammy.frames, -1, null, null, function (frames) { + whammy.frames = frames; + + // to display advertisement images! + if (config.advertisement && config.advertisement.length) { + whammy.frames = config.advertisement.concat(whammy.frames); + } + + /** + * @property {Blob} blob - Recorded frames in video/webm blob. + * @memberof WhammyRecorder + * @example + * recorder.stop(function() { + * var blob = recorder.blob; + * }); + */ + whammy.compile(function (blob) { + _this.blob = blob; + + if (_this.blob.forEach) { + _this.blob = new Blob([], { + type: 'video/webm' + }); + } + + if (callback) { + callback(_this.blob); + } + }); + }); + }, 10); + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof WhammyRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPausedRecording = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof WhammyRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPausedRecording = false; + + if (isStopDrawing) { + this.record(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof WhammyRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (!isStopDrawing) { + this.stop(clearRecordedDataCB); + } + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + whammy.frames = []; + isStopDrawing = true; + isPausedRecording = false; + } + + // for debugging + this.name = 'WhammyRecorder'; + this.toString = function () { + return this.name; + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + var video; + var lastTime; + var whammy; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.WhammyRecorder = WhammyRecorder; + } + + // https://github.com/antimatter15/whammy/blob/master/LICENSE + // _________ + // Whammy.js + + // todo: Firefox now supports webp for webm containers! + // their MediaRecorder implementation works well! + // should we provide an option to record via Whammy.js or MediaRecorder API is a better solution? + + /** + * Whammy is a standalone class used by {@link RecordRTC} to bring video recording in Chrome. It is written by {@link https://github.com/antimatter15|antimatter15} + * @summary A real time javascript webm encoder based on a canvas hack. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef Whammy + * @class + * @example + * var recorder = new Whammy().Video(15); + * recorder.add(context || canvas || dataURL); + * var output = recorder.compile(); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + var Whammy = (function () { + // a more abstract-ish API + + function WhammyVideo(duration) { + this.frames = []; + this.duration = duration || 1; + this.quality = 0.8; + } + + /** + * Pass Canvas or Context or image/webp(string) to {@link Whammy} encoder. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.add(canvas || context || 'image/webp'); + * @param {string} frame - Canvas || Context || image/webp + * @param {number} duration - Stick a duration (in milliseconds) + */ + WhammyVideo.prototype.add = function (frame, duration) { + if ('canvas' in frame) { //CanvasRenderingContext2D + frame = frame.canvas; + } + + if ('toDataURL' in frame) { + frame = frame.toDataURL('image/webp', this.quality); + } + + if (!(/^data:image\/webp;base64,/ig).test(frame)) { + throw 'Input must be formatted properly as a base64 encoded DataURI of type image/webp'; + } + this.frames.push({ + image: frame, + duration: duration || this.duration + }); + }; + + function processInWebWorker(_function) { + var blob = URL.createObjectURL(new Blob([_function.toString(), + 'this.onmessage = function (eee) {' + _function.name + '(eee.data);}' + ], { + type: 'application/javascript' + })); + + var worker = new Worker(blob); + URL.revokeObjectURL(blob); + return worker; + } + + function whammyInWebWorker(frames) { + function ArrayToWebM(frames) { + var info = checkFrames(frames); + if (!info) { + return []; + } + + var clusterMaxDuration = 30000; + + var EBML = [{ + 'id': 0x1a45dfa3, // EBML + 'data': [{ + 'data': 1, + 'id': 0x4286 // EBMLVersion + }, { + 'data': 1, + 'id': 0x42f7 // EBMLReadVersion + }, { + 'data': 4, + 'id': 0x42f2 // EBMLMaxIDLength + }, { + 'data': 8, + 'id': 0x42f3 // EBMLMaxSizeLength + }, { + 'data': 'webm', + 'id': 0x4282 // DocType + }, { + 'data': 2, + 'id': 0x4287 // DocTypeVersion + }, { + 'data': 2, + 'id': 0x4285 // DocTypeReadVersion + }] + }, { + 'id': 0x18538067, // Segment + 'data': [{ + 'id': 0x1549a966, // Info + 'data': [{ + 'data': 1e6, //do things in millisecs (num of nanosecs for duration scale) + 'id': 0x2ad7b1 // TimecodeScale + }, { + 'data': 'whammy', + 'id': 0x4d80 // MuxingApp + }, { + 'data': 'whammy', + 'id': 0x5741 // WritingApp + }, { + 'data': doubleToString(info.duration), + 'id': 0x4489 // Duration + }] + }, { + 'id': 0x1654ae6b, // Tracks + 'data': [{ + 'id': 0xae, // TrackEntry + 'data': [{ + 'data': 1, + 'id': 0xd7 // TrackNumber + }, { + 'data': 1, + 'id': 0x73c5 // TrackUID + }, { + 'data': 0, + 'id': 0x9c // FlagLacing + }, { + 'data': 'und', + 'id': 0x22b59c // Language + }, { + 'data': 'V_VP8', + 'id': 0x86 // CodecID + }, { + 'data': 'VP8', + 'id': 0x258688 // CodecName + }, { + 'data': 1, + 'id': 0x83 // TrackType + }, { + 'id': 0xe0, // Video + 'data': [{ + 'data': info.width, + 'id': 0xb0 // PixelWidth + }, { + 'data': info.height, + 'id': 0xba // PixelHeight + }] + }] + }] + }] + }]; + + //Generate clusters (max duration) + var frameNumber = 0; + var clusterTimecode = 0; + while (frameNumber < frames.length) { + + var clusterFrames = []; + var clusterDuration = 0; + do { + clusterFrames.push(frames[frameNumber]); + clusterDuration += frames[frameNumber].duration; + frameNumber++; + } while (frameNumber < frames.length && clusterDuration < clusterMaxDuration); + + var clusterCounter = 0; + var cluster = { + 'id': 0x1f43b675, // Cluster + 'data': getClusterData(clusterTimecode, clusterCounter, clusterFrames) + }; //Add cluster to segment + EBML[1].data.push(cluster); + clusterTimecode += clusterDuration; + } + + return generateEBML(EBML); + } + + function getClusterData(clusterTimecode, clusterCounter, clusterFrames) { + return [{ + 'data': clusterTimecode, + 'id': 0xe7 // Timecode + }].concat(clusterFrames.map(function (webp) { + var block = makeSimpleBlock({ + discardable: 0, + frame: webp.data.slice(4), + invisible: 0, + keyframe: 1, + lacing: 0, + trackNum: 1, + timecode: Math.round(clusterCounter) + }); + clusterCounter += webp.duration; + return { + data: block, + id: 0xa3 + }; + })); + } + + // sums the lengths of all the frames and gets the duration + + function checkFrames(frames) { + if (!frames[0]) { + postMessage({ + error: 'Something went wrong. Maybe WebP format is not supported in the current browser.' + }); + return; + } + + var width = frames[0].width, + height = frames[0].height, + duration = frames[0].duration; + + for (var i = 1; i < frames.length; i++) { + duration += frames[i].duration; + } + return { + duration: duration, + width: width, + height: height + }; + } + + function numToBuffer(num) { + var parts = []; + while (num > 0) { + parts.push(num & 0xff); + num = num >> 8; + } + return new Uint8Array(parts.reverse()); + } + + function strToBuffer(str) { + return new Uint8Array(str.split('').map(function (e) { + return e.charCodeAt(0); + })); + } + + function bitsToBuffer(bits) { + var data = []; + var pad = (bits.length % 8) ? (new Array(1 + 8 - (bits.length % 8))).join('0') : ''; + bits = pad + bits; + for (var i = 0; i < bits.length; i += 8) { + data.push(parseInt(bits.substr(i, 8), 2)); + } + return new Uint8Array(data); + } + + function generateEBML(json) { + var ebml = []; + for (var i = 0; i < json.length; i++) { + var data = json[i].data; + + if (typeof data === 'object') { + data = generateEBML(data); + } + + if (typeof data === 'number') { + data = bitsToBuffer(data.toString(2)); + } + + if (typeof data === 'string') { + data = strToBuffer(data); + } + + var len = data.size || data.byteLength || data.length; + var zeroes = Math.ceil(Math.ceil(Math.log(len) / Math.log(2)) / 8); + var sizeToString = len.toString(2); + var padded = (new Array((zeroes * 7 + 7 + 1) - sizeToString.length)).join('0') + sizeToString; + var size = (new Array(zeroes)).join('0') + '1' + padded; + + ebml.push(numToBuffer(json[i].id)); + ebml.push(bitsToBuffer(size)); + ebml.push(data); + } + + return new Blob(ebml, { + type: 'video/webm' + }); + } + + function makeSimpleBlock(data) { + var flags = 0; + + if (data.keyframe) { + flags |= 128; + } + + if (data.invisible) { + flags |= 8; + } + + if (data.lacing) { + flags |= (data.lacing << 1); + } + + if (data.discardable) { + flags |= 1; + } + + if (data.trackNum > 127) { + throw 'TrackNumber > 127 not supported'; + } + + var out = [data.trackNum | 0x80, data.timecode >> 8, data.timecode & 0xff, flags].map(function (e) { + return String.fromCharCode(e); + }).join('') + data.frame; + + return out; + } + + function parseWebP(riff) { + var VP8 = riff.RIFF[0].WEBP[0]; + + var frameStart = VP8.indexOf('\x9d\x01\x2a'); // A VP8 keyframe starts with the 0x9d012a header + for (var i = 0, c = []; i < 4; i++) { + c[i] = VP8.charCodeAt(frameStart + 3 + i); + } + + var width, height, tmp; + + //the code below is literally copied verbatim from the bitstream spec + tmp = (c[1] << 8) | c[0]; + width = tmp & 0x3FFF; + tmp = (c[3] << 8) | c[2]; + height = tmp & 0x3FFF; + return { + width: width, + height: height, + data: VP8, + riff: riff + }; + } + + function getStrLength(string, offset) { + return parseInt(string.substr(offset + 4, 4).split('').map(function (i) { + var unpadded = i.charCodeAt(0).toString(2); + return (new Array(8 - unpadded.length + 1)).join('0') + unpadded; + }).join(''), 2); + } + + function parseRIFF(string) { + var offset = 0; + var chunks = {}; + + while (offset < string.length) { + var id = string.substr(offset, 4); + var len = getStrLength(string, offset); + var data = string.substr(offset + 4 + 4, len); + offset += 4 + 4 + len; + chunks[id] = chunks[id] || []; + + if (id === 'RIFF' || id === 'LIST') { + chunks[id].push(parseRIFF(data)); + } else { + chunks[id].push(data); + } + } + return chunks; + } + + function doubleToString(num) { + return [].slice.call( + new Uint8Array((new Float64Array([num])).buffer), 0).map(function (e) { + return String.fromCharCode(e); + }).reverse().join(''); + } + + var webm = new ArrayToWebM(frames.map(function (frame) { + var webp = parseWebP(parseRIFF(atob(frame.image.slice(23)))); + webp.duration = frame.duration; + return webp; + })); + + postMessage(webm); + } + + /** + * Encodes frames in WebM container. It uses WebWorkinvoke to invoke 'ArrayToWebM' method. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * recorder.compile(function(blob) { + * // blob.size - blob.type + * }); + */ + WhammyVideo.prototype.compile = function (callback) { + var webWorker = processInWebWorker(whammyInWebWorker); + + webWorker.onmessage = function (event) { + if (event.data.error) { + console.error(event.data.error); + return; + } + callback(event.data); + }; + + webWorker.postMessage(this.frames); + }; + + return { + /** + * A more abstract-ish API. + * @method + * @memberof Whammy + * @example + * recorder = new Whammy().Video(0.8, 100); + * @param {?number} speed - 0.8 + * @param {?number} quality - 100 + */ + Video: WhammyVideo + }; + })(); + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.Whammy = Whammy; + } + + // ______________ (indexed-db) + // DiskStorage.js + + /** + * DiskStorage is a standalone object used by {@link RecordRTC} to store recorded blobs in IndexedDB storage. + * @summary Writing blobs into IndexedDB. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + * // DiskStorage.dataStoreName = 'recordRTC'; + * // DiskStorage.onError = function(error) { }; + * @property {function} init - This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @property {function} Fetch - This method fetches stored blobs from IndexedDB. + * @property {function} Store - This method stores blobs in IndexedDB. + * @property {function} onError - This function is invoked for any known/unknown error. + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + */ + + + var DiskStorage = { + /** + * This method must be called once to initialize IndexedDB ObjectStore. Though, it is auto-used internally. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.init(); + */ + init: function () { + var self = this; + + if (typeof indexedDB === 'undefined' || typeof indexedDB.open === 'undefined') { + console.error('IndexedDB API are not available in this browser.'); + return; + } + + var dbVersion = 1; + var dbName = this.dbName || location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''), + db; + var request = indexedDB.open(dbName, dbVersion); + + function createObjectStore(dataBase) { + dataBase.createObjectStore(self.dataStoreName); + } + + function putInDB() { + var transaction = db.transaction([self.dataStoreName], 'readwrite'); + + if (self.videoBlob) { + transaction.objectStore(self.dataStoreName).put(self.videoBlob, 'videoBlob'); + } + + if (self.gifBlob) { + transaction.objectStore(self.dataStoreName).put(self.gifBlob, 'gifBlob'); + } + + if (self.audioBlob) { + transaction.objectStore(self.dataStoreName).put(self.audioBlob, 'audioBlob'); + } + + function getFromStore(portionName) { + transaction.objectStore(self.dataStoreName).get(portionName).onsuccess = function (event) { + if (self.callback) { + self.callback(event.target.result, portionName); + } + }; + } + + getFromStore('audioBlob'); + getFromStore('videoBlob'); + getFromStore('gifBlob'); + } + + request.onerror = self.onError; + + request.onsuccess = function () { + db = request.result; + db.onerror = self.onError; + + if (db.setVersion) { + if (db.version !== dbVersion) { + var setVersion = db.setVersion(dbVersion); + setVersion.onsuccess = function () { + createObjectStore(db); + putInDB(); + }; + } else { + putInDB(); + } + } else { + putInDB(); + } + }; + request.onupgradeneeded = function (event) { + createObjectStore(event.target.result); + }; + }, + /** + * This method fetches stored blobs from IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Fetch(function(dataURL, type) { + * if(type === 'audioBlob') { } + * if(type === 'videoBlob') { } + * if(type === 'gifBlob') { } + * }); + */ + Fetch: function (callback) { + this.callback = callback; + this.init(); + + return this; + }, + /** + * This method stores blobs in IndexedDB. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.Store({ + * audioBlob: yourAudioBlob, + * videoBlob: yourVideoBlob, + * gifBlob : yourGifBlob + * }); + */ + Store: function (config) { + this.audioBlob = config.audioBlob; + this.videoBlob = config.videoBlob; + this.gifBlob = config.gifBlob; + + this.init(); + + return this; + }, + /** + * This function is invoked for any known/unknown error. + * @method + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.onError = function(error){ + * alerot( JSON.stringify(error) ); + * }; + */ + onError: function (error) { + console.error(JSON.stringify(error, null, '\t')); + }, + + /** + * @property {string} dataStoreName - Name of the ObjectStore created in IndexedDB storage. + * @memberof DiskStorage + * @internal + * @example + * DiskStorage.dataStoreName = 'recordRTC'; + */ + dataStoreName: 'recordRTC', + dbName: null + }; + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.DiskStorage = DiskStorage; + } + + // ______________ + // GifRecorder.js + + /** + * GifRecorder is standalone calss used by {@link RecordRTC} to record video or canvas into animated gif. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef GifRecorder + * @class + * @example + * var recorder = new GifRecorder(mediaStream || canvas || context, { onGifPreview: function, onGifRecordingStarted: function, width: 1280, height: 720, frameRate: 200, quality: 10 }); + * recorder.record(); + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object or HTMLCanvasElement or CanvasRenderingContext2D. + * @param {object} config - {disableLogs:true, initCallback: function, width: 320, height: 240, frameRate: 200, quality: 10} + */ + + function GifRecorder(mediaStream, config) { + if (typeof GIFEncoder === 'undefined') { + var script = document.createElement('script'); + script.src = 'https://www.webrtc-experiment.com/gif-recorder.js'; + (document.body || document.documentElement).appendChild(script); + } + + config = config || {}; + + var isHTMLObject = mediaStream instanceof CanvasRenderingContext2D || mediaStream instanceof HTMLCanvasElement; + + /** + * This method records MediaStream. + * @method + * @memberof GifRecorder + * @example + * recorder.record(); + */ + this.record = function () { + if (typeof GIFEncoder === 'undefined') { + setTimeout(self.record, 1000); + return; + } + + if (!isLoadedMetaData) { + setTimeout(self.record, 1000); + return; + } + + if (!isHTMLObject) { + if (!config.width) { + config.width = video.offsetWidth || 320; + } + + if (!config.height) { + config.height = video.offsetHeight || 240; + } + + if (!config.video) { + config.video = { + width: config.width, + height: config.height + }; + } + + if (!config.canvas) { + config.canvas = { + width: config.width, + height: config.height + }; + } + + canvas.width = config.canvas.width || 320; + canvas.height = config.canvas.height || 240; + + video.width = config.video.width || 320; + video.height = config.video.height || 240; + } + + // external library to record as GIF images + gifEncoder = new GIFEncoder(); + + // void setRepeat(int iter) + // Sets the number of times the set of GIF frames should be played. + // Default is 1; 0 means play indefinitely. + gifEncoder.setRepeat(0); + + // void setFrameRate(Number fps) + // Sets frame rate in frames per second. + // Equivalent to setDelay(1000/fps). + // Using "setDelay" instead of "setFrameRate" + gifEncoder.setDelay(config.frameRate || 200); + + // void setQuality(int quality) + // Sets quality of color quantization (conversion of images to the + // maximum 256 colors allowed by the GIF specification). + // Lower values (minimum = 1) produce better colors, + // but slow processing significantly. 10 is the default, + // and produces good color mapping at reasonable speeds. + // Values greater than 20 do not yield significant improvements in speed. + gifEncoder.setQuality(config.quality || 10); + + // Boolean start() + // This writes the GIF Header and returns false if it fails. + gifEncoder.start(); + + if (typeof config.onGifRecordingStarted === 'function') { + config.onGifRecordingStarted(); + } + + function drawVideoFrame(time) { + if (self.clearedRecordedData === true) { + return; + } + + if (isPausedRecording) { + return setTimeout(function () { + drawVideoFrame(time); + }, 100); + } + + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); + + if (typeof lastFrameTime === undefined) { + lastFrameTime = time; + } + + // ~10 fps + if (time - lastFrameTime < 90) { + return; + } + + if (!isHTMLObject && video.paused) { + // via: https://github.com/muaz-khan/WebRTC-Experiment/pull/316 + // Tweak for Android Chrome + video.play(); + } + + if (!isHTMLObject) { + context.drawImage(video, 0, 0, canvas.width, canvas.height); + } + + if (config.onGifPreview) { + config.onGifPreview(canvas.toDataURL('image/png')); + } + + gifEncoder.addFrame(context); + lastFrameTime = time; + } + + lastAnimationFrame = requestAnimationFrame(drawVideoFrame); + + if (config.initCallback) { + config.initCallback(); + } + }; + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof GifRecorder + * @example + * recorder.stop(function(blob) { + * img.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + callback = callback || function () { }; + + if (lastAnimationFrame) { + cancelAnimationFrame(lastAnimationFrame); + } + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof GifRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = new Blob([new Uint8Array(gifEncoder.stream().bin)], { + type: 'image/gif' + }); + + callback(this.blob); + + // bug: find a way to clear old recorded blobs + gifEncoder.stream().bin = []; + }; + + var isPausedRecording = false; + + /** + * This method pauses the recording process. + * @method + * @memberof GifRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPausedRecording = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof GifRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPausedRecording = false; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof GifRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + self.clearedRecordedData = true; + clearRecordedDataCB(); + }; + + function clearRecordedDataCB() { + if (gifEncoder) { + gifEncoder.stream().bin = []; + } + } + + // for debugging + this.name = 'GifRecorder'; + this.toString = function () { + return this.name; + }; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + if (isHTMLObject) { + if (mediaStream instanceof CanvasRenderingContext2D) { + context = mediaStream; + canvas = context.canvas; + } else if (mediaStream instanceof HTMLCanvasElement) { + context = mediaStream.getContext('2d'); + canvas = mediaStream; + } + } + + var isLoadedMetaData = true; + + if (!isHTMLObject) { + var video = document.createElement('video'); + video.muted = true; + video.autoplay = true; + video.playsInline = true; + + isLoadedMetaData = false; + video.onloadedmetadata = function () { + isLoadedMetaData = true; + }; + + setSrcObject(mediaStream, video); + + video.play(); + } + + var lastAnimationFrame = null; + var lastFrameTime; + + var gifEncoder; + + var self = this; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.GifRecorder = GifRecorder; + } + + // Last time updated: 2019-06-21 4:09:42 AM UTC + + // ________________________ + // MultiStreamsMixer v1.2.2 + + // Open-Sourced: https://github.com/muaz-khan/MultiStreamsMixer + + // -------------------------------------------------- + // Muaz Khan - www.MuazKhan.com + // MIT License - www.WebRTC-Experiment.com/licence + // -------------------------------------------------- + + function MultiStreamsMixer(arrayOfMediaStreams, elementClass) { + + var browserFakeUserAgent = 'Fake/5.0 (FakeOS) AppleWebKit/123 (KHTML, like Gecko) Fake/12.3.4567.89 Fake/123.45'; + + (function (that) { + if (typeof RecordRTC !== 'undefined') { + return; + } + + if (!that) { + return; + } + + if (typeof window !== 'undefined') { + return; + } + + if (typeof commonjsGlobal === 'undefined') { + return; + } + + commonjsGlobal.navigator = { + userAgent: browserFakeUserAgent, + getUserMedia: function () { } + }; + + if (!commonjsGlobal.console) { + commonjsGlobal.console = {}; + } + + if (typeof commonjsGlobal.console.log === 'undefined' || typeof commonjsGlobal.console.error === 'undefined') { + commonjsGlobal.console.error = commonjsGlobal.console.log = commonjsGlobal.console.log || function () { + console.log(arguments); + }; + } + + if (typeof document === 'undefined') { + /*global document:true */ + that.document = { + documentElement: { + appendChild: function () { + return ''; + } + } + }; + + document.createElement = document.captureStream = document.mozCaptureStream = function () { + var obj = { + getContext: function () { + return obj; + }, + play: function () { }, + pause: function () { }, + drawImage: function () { }, + toDataURL: function () { + return ''; + }, + style: {} + }; + return obj; + }; + + that.HTMLVideoElement = function () { }; + } + + if (typeof location === 'undefined') { + /*global location:true */ + that.location = { + protocol: 'file:', + href: '', + hash: '' + }; + } + + if (typeof screen === 'undefined') { + /*global screen:true */ + that.screen = { + width: 0, + height: 0 + }; + } + + if (typeof URL === 'undefined') { + /*global screen:true */ + that.URL = { + createObjectURL: function () { + return ''; + }, + revokeObjectURL: function () { + return ''; + } + }; + } + + /*global window:true */ + that.window = commonjsGlobal; + })(typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : null); + + // requires: chrome://flags/#enable-experimental-web-platform-features + + elementClass = elementClass || 'multi-streams-mixer'; + + var videos = []; + var isStopDrawingFrames = false; + + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + canvas.style.opacity = 0; + canvas.style.position = 'absolute'; + canvas.style.zIndex = -1; + canvas.style.top = '-1000em'; + canvas.style.left = '-1000em'; + canvas.className = elementClass; + (document.body || document.documentElement).appendChild(canvas); + + this.disableLogs = false; + this.frameInterval = 10; + + this.width = 360; + this.height = 240; + + // use gain node to prevent echo + this.useGainNode = true; + + var self = this; + + // _____________________________ + // Cross-Browser-Declarations.js + + // WebAudio API representer + var AudioContext = window.AudioContext; + + if (typeof AudioContext === 'undefined') { + if (typeof webkitAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = webkitAudioContext; + } + + if (typeof mozAudioContext !== 'undefined') { + /*global AudioContext:true */ + AudioContext = mozAudioContext; + } + } + + /*jshint -W079 */ + var URL = window.URL; + + if (typeof URL === 'undefined' && typeof webkitURL !== 'undefined') { + /*global URL:true */ + URL = webkitURL; + } + + if (typeof navigator !== 'undefined' && typeof navigator.getUserMedia === 'undefined') { // maybe window.navigator? + if (typeof navigator.webkitGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.webkitGetUserMedia; + } + + if (typeof navigator.mozGetUserMedia !== 'undefined') { + navigator.getUserMedia = navigator.mozGetUserMedia; + } + } + + var MediaStream = window.MediaStream; + + if (typeof MediaStream === 'undefined' && typeof webkitMediaStream !== 'undefined') { + MediaStream = webkitMediaStream; + } + + /*global MediaStream:true */ + if (typeof MediaStream !== 'undefined') { + // override "stop" method for all browsers + if (typeof MediaStream.prototype.stop === 'undefined') { + MediaStream.prototype.stop = function () { + this.getTracks().forEach(function (track) { + track.stop(); + }); + }; + } + } + + var Storage = {}; + + if (typeof AudioContext !== 'undefined') { + Storage.AudioContext = AudioContext; + } else if (typeof webkitAudioContext !== 'undefined') { + Storage.AudioContext = webkitAudioContext; + } + + function setSrcObject(stream, element) { + if ('srcObject' in element) { + element.srcObject = stream; + } else if ('mozSrcObject' in element) { + element.mozSrcObject = stream; + } else { + element.srcObject = stream; + } + } + + this.startDrawingFrames = function () { + drawVideosToCanvas(); + }; + + function drawVideosToCanvas() { + if (isStopDrawingFrames) { + return; + } + + var videosLength = videos.length; + + var fullcanvas = false; + var remaining = []; + videos.forEach(function (video) { + if (!video.stream) { + video.stream = {}; + } + + if (video.stream.fullcanvas) { + fullcanvas = video; + } else { + // todo: video.stream.active or video.stream.live to fix blank frames issues? + remaining.push(video); + } + }); + + if (fullcanvas) { + canvas.width = fullcanvas.stream.width; + canvas.height = fullcanvas.stream.height; + } else if (remaining.length) { + canvas.width = videosLength > 1 ? remaining[0].width * 2 : remaining[0].width; + + var height = 1; + if (videosLength === 3 || videosLength === 4) { + height = 2; + } + if (videosLength === 5 || videosLength === 6) { + height = 3; + } + if (videosLength === 7 || videosLength === 8) { + height = 4; + } + if (videosLength === 9 || videosLength === 10) { + height = 5; + } + canvas.height = remaining[0].height * height; + } else { + canvas.width = self.width || 360; + canvas.height = self.height || 240; + } + + if (fullcanvas && fullcanvas instanceof HTMLVideoElement) { + drawImage(fullcanvas); + } + + remaining.forEach(function (video, idx) { + drawImage(video, idx); + }); + + setTimeout(drawVideosToCanvas, self.frameInterval); + } + + function drawImage(video, idx) { + if (isStopDrawingFrames) { + return; + } + + var x = 0; + var y = 0; + var width = video.width; + var height = video.height; + + if (idx === 1) { + x = video.width; + } + + if (idx === 2) { + y = video.height; + } + + if (idx === 3) { + x = video.width; + y = video.height; + } + + if (idx === 4) { + y = video.height * 2; + } + + if (idx === 5) { + x = video.width; + y = video.height * 2; + } + + if (idx === 6) { + y = video.height * 3; + } + + if (idx === 7) { + x = video.width; + y = video.height * 3; + } + + if (typeof video.stream.left !== 'undefined') { + x = video.stream.left; + } + + if (typeof video.stream.top !== 'undefined') { + y = video.stream.top; + } + + if (typeof video.stream.width !== 'undefined') { + width = video.stream.width; + } + + if (typeof video.stream.height !== 'undefined') { + height = video.stream.height; + } + + context.drawImage(video, x, y, width, height); + + if (typeof video.stream.onRender === 'function') { + video.stream.onRender(context, x, y, width, height, idx); + } + } + + function getMixedStream() { + isStopDrawingFrames = false; + var mixedVideoStream = getMixedVideoStream(); + + var mixedAudioStream = getMixedAudioStream(); + if (mixedAudioStream) { + mixedAudioStream.getTracks().filter(function (t) { + return t.kind === 'audio'; + }).forEach(function (track) { + mixedVideoStream.addTrack(track); + }); + } + arrayOfMediaStreams.forEach(function (stream) { + if (stream.fullcanvas); + }); + + // mixedVideoStream.prototype.appendStreams = appendStreams; + // mixedVideoStream.prototype.resetVideoStreams = resetVideoStreams; + // mixedVideoStream.prototype.clearRecordedData = clearRecordedData; + + return mixedVideoStream; + } + + function getMixedVideoStream() { + resetVideoStreams(); + + var capturedStream; + + if ('captureStream' in canvas) { + capturedStream = canvas.captureStream(); + } else if ('mozCaptureStream' in canvas) { + capturedStream = canvas.mozCaptureStream(); + } else if (!self.disableLogs) { + console.error('Upgrade to latest Chrome or otherwise enable this flag: chrome://flags/#enable-experimental-web-platform-features'); + } + + var videoStream = new MediaStream(); + + capturedStream.getTracks().filter(function (t) { + return t.kind === 'video'; + }).forEach(function (track) { + videoStream.addTrack(track); + }); + + canvas.stream = videoStream; + + return videoStream; + } + + function getMixedAudioStream() { + // via: @pehrsons + if (!Storage.AudioContextConstructor) { + Storage.AudioContextConstructor = new Storage.AudioContext(); + } + + self.audioContext = Storage.AudioContextConstructor; + + self.audioSources = []; + + if (self.useGainNode === true) { + self.gainNode = self.audioContext.createGain(); + self.gainNode.connect(self.audioContext.destination); + self.gainNode.gain.value = 0; // don't hear self + } + + var audioTracksLength = 0; + arrayOfMediaStreams.forEach(function (stream) { + if (!stream.getTracks().filter(function (t) { + return t.kind === 'audio'; + }).length) { + return; + } + + audioTracksLength++; + + var audioSource = self.audioContext.createMediaStreamSource(stream); + + if (self.useGainNode === true) { + audioSource.connect(self.gainNode); + } + + self.audioSources.push(audioSource); + }); + + if (!audioTracksLength) { + // because "self.audioContext" is not initialized + // that's why we've to ignore rest of the code + return; + } + + self.audioDestination = self.audioContext.createMediaStreamDestination(); + self.audioSources.forEach(function (audioSource) { + audioSource.connect(self.audioDestination); + }); + return self.audioDestination.stream; + } + + function getVideo(stream) { + var video = document.createElement('video'); + + setSrcObject(stream, video); + + video.className = elementClass; + + video.muted = true; + video.volume = 0; + + video.width = stream.width || self.width || 360; + video.height = stream.height || self.height || 240; + + video.play(); + + return video; + } + + this.appendStreams = function (streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + streams.forEach(function (stream) { + var newStream = new MediaStream(); + + if (stream.getTracks().filter(function (t) { + return t.kind === 'video'; + }).length) { + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + + newStream.addTrack(stream.getTracks().filter(function (t) { + return t.kind === 'video'; + })[0]); + } + + if (stream.getTracks().filter(function (t) { + return t.kind === 'audio'; + }).length) { + var audioSource = self.audioContext.createMediaStreamSource(stream); + self.audioDestination = self.audioContext.createMediaStreamDestination(); + audioSource.connect(self.audioDestination); + + newStream.addTrack(self.audioDestination.stream.getTracks().filter(function (t) { + return t.kind === 'audio'; + })[0]); + } + + arrayOfMediaStreams.push(newStream); + }); + }; + + this.releaseStreams = function () { + videos = []; + isStopDrawingFrames = true; + + if (self.gainNode) { + self.gainNode.disconnect(); + self.gainNode = null; + } + + if (self.audioSources.length) { + self.audioSources.forEach(function (source) { + source.disconnect(); + }); + self.audioSources = []; + } + + if (self.audioDestination) { + self.audioDestination.disconnect(); + self.audioDestination = null; + } + + if (self.audioContext) { + self.audioContext.close(); + } + + self.audioContext = null; + + context.clearRect(0, 0, canvas.width, canvas.height); + + if (canvas.stream) { + canvas.stream.stop(); + canvas.stream = null; + } + }; + + this.resetVideoStreams = function (streams) { + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + resetVideoStreams(streams); + }; + + function resetVideoStreams(streams) { + videos = []; + streams = streams || arrayOfMediaStreams; + + // via: @adrian-ber + streams.forEach(function (stream) { + if (!stream.getTracks().filter(function (t) { + return t.kind === 'video'; + }).length) { + return; + } + + var video = getVideo(stream); + video.stream = stream; + videos.push(video); + }); + } + + // for debugging + this.name = 'MultiStreamsMixer'; + this.toString = function () { + return this.name; + }; + + this.getMixedStream = getMixedStream; + + } + + if (typeof RecordRTC === 'undefined') { + { + module.exports = MultiStreamsMixer; + } + } + + // ______________________ + // MultiStreamRecorder.js + + /* + * Video conference recording, using captureStream API along with WebAudio and Canvas2D API. + */ + + /** + * MultiStreamRecorder can record multiple videos in single container. + * @summary Multi-videos recorder. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef MultiStreamRecorder + * @class + * @example + * var options = { + * mimeType: 'video/webm' + * } + * var recorder = new MultiStreamRecorder(ArrayOfMediaStreams, options); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * + * // or + * var blob = recorder.blob; + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStreams} mediaStreams - Array of MediaStreams. + * @param {object} config - {disableLogs:true, frameInterval: 1, mimeType: "video/webm"} + */ + + function MultiStreamRecorder(arrayOfMediaStreams, options) { + arrayOfMediaStreams = arrayOfMediaStreams || []; + var self = this; + + var mixer; + var mediaRecorder; + + options = options || { + elementClass: 'multi-streams-mixer', + mimeType: 'video/webm', + video: { + width: 360, + height: 240 + } + }; + + if (!options.frameInterval) { + options.frameInterval = 10; + } + + if (!options.video) { + options.video = {}; + } + + if (!options.video.width) { + options.video.width = 360; + } + + if (!options.video.height) { + options.video.height = 240; + } + + /** + * This method records all MediaStreams. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.record(); + */ + this.record = function () { + // github/muaz-khan/MultiStreamsMixer + mixer = new MultiStreamsMixer(arrayOfMediaStreams, options.elementClass || 'multi-streams-mixer'); + + if (getAllVideoTracks().length) { + mixer.frameInterval = options.frameInterval || 10; + mixer.width = options.video.width || 360; + mixer.height = options.video.height || 240; + mixer.startDrawingFrames(); + } + + if (options.previewStream && typeof options.previewStream === 'function') { + options.previewStream(mixer.getMixedStream()); + } + + // record using MediaRecorder API + mediaRecorder = new MediaStreamRecorder(mixer.getMixedStream(), options); + mediaRecorder.record(); + }; + + function getAllVideoTracks() { + var tracks = []; + arrayOfMediaStreams.forEach(function (stream) { + getTracks(stream, 'video').forEach(function (track) { + tracks.push(track); + }); + }); + return tracks; + } + + /** + * This method stops recording MediaStream. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + if (!mediaRecorder) { + return; + } + + mediaRecorder.stop(function (blob) { + self.blob = blob; + + callback(blob); + + self.clearRecordedData(); + }); + }; + + /** + * This method pauses the recording process. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + if (mediaRecorder) { + mediaRecorder.pause(); + } + }; + + /** + * This method resumes the recording process. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + if (mediaRecorder) { + mediaRecorder.resume(); + } + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof MultiStreamRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + if (mediaRecorder) { + mediaRecorder.clearRecordedData(); + mediaRecorder = null; + } + + if (mixer) { + mixer.releaseStreams(); + mixer = null; + } + }; + + /** + * Add extra media-streams to existing recordings. + * @method + * @memberof MultiStreamRecorder + * @param {MediaStreams} mediaStreams - Array of MediaStreams + * @example + * recorder.addStreams([newAudioStream, newVideoStream]); + */ + this.addStreams = function (streams) { + if (!streams) { + throw 'First parameter is required.'; + } + + if (!(streams instanceof Array)) { + streams = [streams]; + } + + arrayOfMediaStreams.concat(streams); + + if (!mediaRecorder || !mixer) { + return; + } + + mixer.appendStreams(streams); + + if (options.previewStream && typeof options.previewStream === 'function') { + options.previewStream(mixer.getMixedStream()); + } + }; + + /** + * Reset videos during live recording. Replace old videos e.g. replace cameras with full-screen. + * @method + * @memberof MultiStreamRecorder + * @param {MediaStreams} mediaStreams - Array of MediaStreams + * @example + * recorder.resetVideoStreams([newVideo1, newVideo2]); + */ + this.resetVideoStreams = function (streams) { + if (!mixer) { + return; + } + + if (streams && !(streams instanceof Array)) { + streams = [streams]; + } + + mixer.resetVideoStreams(streams); + }; + + /** + * Returns MultiStreamsMixer + * @method + * @memberof MultiStreamRecorder + * @example + * let mixer = recorder.getMixer(); + * mixer.appendStreams([newStream]); + */ + this.getMixer = function () { + return mixer; + }; + + // for debugging + this.name = 'MultiStreamRecorder'; + this.toString = function () { + return this.name; + }; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.MultiStreamRecorder = MultiStreamRecorder; + } + + // _____________________ + // RecordRTC.promises.js + + /** + * RecordRTCPromisesHandler adds promises support in {@link RecordRTC}. Try a {@link https://github.com/muaz-khan/RecordRTC/blob/master/simple-demos/RecordRTCPromisesHandler.html|demo here} + * @summary Promises for {@link RecordRTC} + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef RecordRTCPromisesHandler + * @class + * @example + * var recorder = new RecordRTCPromisesHandler(mediaStream, options); + * recorder.startRecording() + * .then(successCB) + * .catch(errorCB); + * // Note: You can access all RecordRTC API using "recorder.recordRTC" e.g. + * recorder.recordRTC.onStateChanged = function(state) {}; + * recorder.recordRTC.setRecordingDuration(5000); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - Single media-stream object, array of media-streams, html-canvas-element, etc. + * @param {object} config - {type:"video", recorderType: MediaStreamRecorder, disableLogs: true, numberOfAudioChannels: 1, bufferSize: 0, sampleRate: 0, video: HTMLVideoElement, etc.} + * @throws Will throw an error if "new" keyword is not used to initiate "RecordRTCPromisesHandler". Also throws error if first argument "MediaStream" is missing. + * @requires {@link RecordRTC} + */ + + function RecordRTCPromisesHandler(mediaStream, options) { + if (!this) { + throw 'Use "new RecordRTCPromisesHandler()"'; + } + + if (typeof mediaStream === 'undefined') { + throw 'First argument "MediaStream" is required.'; + } + + var self = this; + + /** + * @property {Blob} blob - Access/reach the native {@link RecordRTC} object. + * @memberof RecordRTCPromisesHandler + * @example + * let internal = recorder.recordRTC.getInternalRecorder(); + * alert(internal instanceof MediaStreamRecorder); + * recorder.recordRTC.onStateChanged = function(state) {}; + */ + self.recordRTC = new RecordRTC(mediaStream, options); + + /** + * This method records MediaStream. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.startRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.startRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.startRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method stops the recording. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * var blob = recorder.getBlob(); + * }).catch(errorCB); + */ + this.stopRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.stopRecording(function (url) { + self.blob = self.recordRTC.getBlob(); + + if (!self.blob || !self.blob.size) { + reject('Empty blob.', self.blob); + return; + } + + resolve(url); + }); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method pauses the recording. You can resume recording using "resumeRecording" method. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.pauseRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.pauseRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.pauseRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method resumes the recording. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.resumeRecording() + * .then(successCB) + * .catch(errorCB); + */ + this.resumeRecording = function () { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.resumeRecording(); + resolve(); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns data-url for the recorded blob. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * recorder.getDataURL().then(function(dataURL) { + * window.open(dataURL); + * }).catch(errorCB);; + * }).catch(errorCB); + */ + this.getDataURL = function (callback) { + return new Promise(function (resolve, reject) { + try { + self.recordRTC.getDataURL(function (dataURL) { + resolve(dataURL); + }); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns the recorded blob. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.stopRecording().then(function() { + * recorder.getBlob().then(function(blob) {}) + * }).catch(errorCB); + */ + this.getBlob = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.getBlob()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method returns the internal recording object. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * let internalRecorder = await recorder.getInternalRecorder(); + * if(internalRecorder instanceof MultiStreamRecorder) { + * internalRecorder.addStreams([newAudioStream]); + * internalRecorder.resetVideoStreams([screenStream]); + * } + * @returns {Object} + */ + this.getInternalRecorder = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.getInternalRecorder()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * This method resets the recorder. So that you can reuse single recorder instance many times. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * await recorder.reset(); + * recorder.startRecording(); // record again + */ + this.reset = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.reset()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * Destroy RecordRTC instance. Clear all recorders and objects. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * recorder.destroy().then(successCB).catch(errorCB); + */ + this.destroy = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.destroy()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * Get recorder's readonly state. + * @method + * @memberof RecordRTCPromisesHandler + * @example + * let state = await recorder.getState(); + * // or + * recorder.getState().then(state => { console.log(state); }) + * @returns {String} Returns recording state. + */ + this.getState = function () { + return new Promise(function (resolve, reject) { + try { + resolve(self.recordRTC.getState()); + } catch (e) { + reject(e); + } + }); + }; + + /** + * @property {Blob} blob - Recorded data as "Blob" object. + * @memberof RecordRTCPromisesHandler + * @example + * await recorder.stopRecording(); + * let blob = recorder.getBlob(); // or "recorder.recordRTC.blob" + * invokeSaveAsDialog(blob); + */ + this.blob = null; + + /** + * RecordRTC version number + * @property {String} version - Release version number. + * @memberof RecordRTCPromisesHandler + * @static + * @readonly + * @example + * alert(recorder.version); + */ + this.version = '5.6.2'; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.RecordRTCPromisesHandler = RecordRTCPromisesHandler; + } + + // ______________________ + // WebAssemblyRecorder.js + + /** + * WebAssemblyRecorder lets you create webm videos in JavaScript via WebAssembly. The library consumes raw RGBA32 buffers (4 bytes per pixel) and turns them into a webm video with the given framerate and quality. This makes it compatible out-of-the-box with ImageData from a CANVAS. With realtime mode you can also use webm-wasm for streaming webm videos. + * @summary Video recording feature in Chrome, Firefox and maybe Edge. + * @license {@link https://github.com/muaz-khan/RecordRTC/blob/master/LICENSE|MIT} + * @author {@link https://MuazKhan.com|Muaz Khan} + * @typedef WebAssemblyRecorder + * @class + * @example + * var recorder = new WebAssemblyRecorder(mediaStream); + * recorder.record(); + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + * @see {@link https://github.com/muaz-khan/RecordRTC|RecordRTC Source Code} + * @param {MediaStream} mediaStream - MediaStream object fetched using getUserMedia API or generated using captureStreamUntilEnded or WebAudio API. + * @param {object} config - {webAssemblyPath:'webm-wasm.wasm',workerPath: 'webm-worker.js', frameRate: 30, width: 1920, height: 1080, bitrate: 1024, realtime: true} + */ + function WebAssemblyRecorder(stream, config) { + // based on: github.com/GoogleChromeLabs/webm-wasm + + if (typeof ReadableStream === 'undefined' || typeof WritableStream === 'undefined') { + // because it fixes readable/writable streams issues + console.error('Following polyfill is strongly recommended: https://unpkg.com/@mattiasbuelens/web-streams-polyfill/dist/polyfill.min.js'); + } + + config = config || {}; + + config.width = config.width || 640; + config.height = config.height || 480; + config.frameRate = config.frameRate || 30; + config.bitrate = config.bitrate || 1200; + config.realtime = config.realtime || true; + + var finished; + + function cameraStream() { + return new ReadableStream({ + start: function (controller) { + var cvs = document.createElement('canvas'); + var video = document.createElement('video'); + var first = true; + video.srcObject = stream; + video.muted = true; + video.height = config.height; + video.width = config.width; + video.volume = 0; + video.onplaying = function () { + cvs.width = config.width; + cvs.height = config.height; + var ctx = cvs.getContext('2d'); + var frameTimeout = 1000 / config.frameRate; + var cameraTimer = setInterval(function f() { + if (finished) { + clearInterval(cameraTimer); + controller.close(); + } + + if (first) { + first = false; + if (config.onVideoProcessStarted) { + config.onVideoProcessStarted(); + } + } + + ctx.drawImage(video, 0, 0); + if (controller._controlledReadableStream.state !== 'closed') { + try { + controller.enqueue( + ctx.getImageData(0, 0, config.width, config.height) + ); + } catch (e) { } + } + }, frameTimeout); + }; + video.play(); + } + }); + } + + var worker; + + function startRecording(stream, buffer) { + if (!config.workerPath && !buffer) { + finished = false; + + // is it safe to use @latest ? + + fetch( + 'https://unpkg.com/webm-wasm@latest/dist/webm-worker.js' + ).then(function (r) { + r.arrayBuffer().then(function (buffer) { + startRecording(stream, buffer); + }); + }); + return; + } + + if (!config.workerPath && buffer instanceof ArrayBuffer) { + var blob = new Blob([buffer], { + type: 'text/javascript' + }); + config.workerPath = URL.createObjectURL(blob); + } + + if (!config.workerPath) { + console.error('workerPath parameter is missing.'); + } + + worker = new Worker(config.workerPath); + + worker.postMessage(config.webAssemblyPath || 'https://unpkg.com/webm-wasm@latest/dist/webm-wasm.wasm'); + worker.addEventListener('message', function (event) { + if (event.data === 'READY') { + worker.postMessage({ + width: config.width, + height: config.height, + bitrate: config.bitrate || 1200, + timebaseDen: config.frameRate || 30, + realtime: config.realtime + }); + + cameraStream().pipeTo(new WritableStream({ + write: function (image) { + if (finished) { + console.error('Got image, but recorder is finished!'); + return; + } + + worker.postMessage(image.data.buffer, [image.data.buffer]); + } + })); + } else if (!!event.data) { + if (!isPaused) { + arrayOfBuffers.push(event.data); + } + } + }); + } + + /** + * This method records video. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.record(); + */ + this.record = function () { + arrayOfBuffers = []; + isPaused = false; + this.blob = null; + startRecording(stream); + + if (typeof config.initCallback === 'function') { + config.initCallback(); + } + }; + + var isPaused; + + /** + * This method pauses the recording process. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.pause(); + */ + this.pause = function () { + isPaused = true; + }; + + /** + * This method resumes the recording process. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.resume(); + */ + this.resume = function () { + isPaused = false; + }; + + function terminate(callback) { + if (!worker) { + if (callback) { + callback(); + } + + return; + } + + // Wait for null event data to indicate that the encoding is complete + worker.addEventListener('message', function (event) { + if (event.data === null) { + worker.terminate(); + worker = null; + + if (callback) { + callback(); + } + } + }); + + worker.postMessage(null); + } + + var arrayOfBuffers = []; + + /** + * This method stops recording video. + * @param {function} callback - Callback function, that is used to pass recorded blob back to the callee. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.stop(function(blob) { + * video.src = URL.createObjectURL(blob); + * }); + */ + this.stop = function (callback) { + finished = true; + + var recorder = this; + + terminate(function () { + recorder.blob = new Blob(arrayOfBuffers, { + type: 'video/webm' + }); + + callback(recorder.blob); + }); + }; + + // for debugging + this.name = 'WebAssemblyRecorder'; + this.toString = function () { + return this.name; + }; + + /** + * This method resets currently recorded data. + * @method + * @memberof WebAssemblyRecorder + * @example + * recorder.clearRecordedData(); + */ + this.clearRecordedData = function () { + arrayOfBuffers = []; + isPaused = false; + this.blob = null; + + // todo: if recording-ON then STOP it first + }; + + /** + * @property {Blob} blob - The recorded blob object. + * @memberof WebAssemblyRecorder + * @example + * recorder.stop(function(){ + * var blob = recorder.blob; + * }); + */ + this.blob = null; + } + + if (typeof RecordRTC !== 'undefined') { + RecordRTC.WebAssemblyRecorder = WebAssemblyRecorder; + } + }); + + class RecordRTCLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.fileName = ''; + this.fileType = player._opt.recordType || FILE_SUFFIX.webm; + this.isRecording = false; + this.recordingTimestamp = 0; + this.recordingInterval = null; + player.debug.log('Recorder', 'init'); + } + + destroy() { + this._reset(); + + this.player.debug.log('Recorder', 'destroy'); + } + + setFileName(fileName, fileType) { + this.fileName = fileName; + + if (FILE_SUFFIX.mp4 === fileType || FILE_SUFFIX.webm === fileType) { + this.fileType = fileType; + } + } + + get recording() { + return this.isRecording; + } + + get recordTime() { + return this.recordingTimestamp; + } + + startRecord() { + const debug = this.player.debug; + const options = { + type: 'video', + mimeType: 'video/webm;codecs=h264', + onTimeStamp: timestamp => { + debug.log('Recorder', 'record timestamp :' + timestamp); + }, + disableLogs: !this.player._opt.debug + }; + + try { + const stream = this.player.video.$videoElement.captureStream(25); + + if (this.player.audio && this.player.audio.mediaStreamAudioDestinationNode && this.player.audio.mediaStreamAudioDestinationNode.stream && !this.player.audio.isStateSuspended() && this.player.audio.hasAudio && this.player._opt.hasAudio) { + const audioStream = this.player.audio.mediaStreamAudioDestinationNode.stream; + + if (audioStream.getAudioTracks().length > 0) { + const audioTrack = audioStream.getAudioTracks()[0]; + + if (audioTrack && audioTrack.enabled) { + stream.addTrack(audioTrack); + } + } + } + + this.recorder = RecordRTC_1(stream, options); + } catch (e) { + debug.error('Recorder', 'startRecord error', e); + this.emit(EVENTS.recordCreateError); + } + + if (this.recorder) { + this.isRecording = true; + this.player.emit(EVENTS.recording, true); + this.recorder.startRecording(); + debug.log('Recorder', 'start recording'); + this.player.emit(EVENTS.recordStart); + this.recordingInterval = window.setInterval(() => { + this.recordingTimestamp += 1; + this.player.emit(EVENTS.recordingTimestamp, this.recordingTimestamp); + }, 1000); + } + } + + stopRecordAndSave() { + if (!this.recorder || !this.isRecording) { + return; + } + + this.recorder.stopRecording(() => { + this.player.debug.log('Recorder', 'stop recording'); + this.player.emit(EVENTS.recordEnd); + const fileName = (this.fileName || now()) + '.' + (this.fileType || FILE_SUFFIX.webm); + saveAs(this.recorder.getBlob(), fileName); + + this._reset(); + + this.player.emit(EVENTS.recording, false); + }); + } + + _reset() { + this.isRecording = false; + this.recordingTimestamp = 0; + + if (this.recorder) { + this.recorder.destroy(); + this.recorder = null; + } + + this.fileName = null; + + if (this.recordingInterval) { + clearInterval(this.recordingInterval); + } + + this.recordingInterval = null; + } + + } + + class Recorder { + constructor(player) { + const Loader = Recorder.getLoaderFactory(); + return new Loader(player); + } + + static getLoaderFactory() { + return RecordRTCLoader; + } + + } + + class DecoderWorker { + constructor(player) { + this.player = player; + this.decoderWorker = new Worker(player._opt.decoder); + + this._initDecoderWorker(); + + player.debug.log('decoderWorker', 'init'); + } + + destroy() { + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.close + }); + this.decoderWorker.terminate(); + this.decoderWorker = null; + this.player.debug.log(`decoderWorker`, 'destroy'); + } + + _initDecoderWorker() { + const { + debug, + events: { + proxy + } + } = this.player; + + this.decoderWorker.onmessage = event => { + const msg = event.data; + + switch (msg.cmd) { + case WORKER_CMD_TYPE.init: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.init); + + if (!this.player.loaded) { + this.player.emit(EVENTS.load); + } + + this.player.emit(EVENTS.decoderWorkerInit); + + this._initWork(); + + break; + + case WORKER_CMD_TYPE.videoCode: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.videoCode, msg.code); + + if (!this.player._times.decodeStart) { + this.player._times.decodeStart = now(); + } + + this.player.video.updateVideoInfo({ + encTypeCode: msg.code + }); + break; + + case WORKER_CMD_TYPE.audioCode: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.audioCode, msg.code); + this.player.audio && this.player.audio.updateAudioInfo({ + encTypeCode: msg.code + }); + break; + + case WORKER_CMD_TYPE.initVideo: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initVideo, `width:${msg.w},height:${msg.h}`); + this.player.video.updateVideoInfo({ + width: msg.w, + height: msg.h + }); + + if (!this.player._opt.openWebglAlignment && !isWebglRenderSupport(msg.w)) { + this.player.emit(EVENTS_ERROR.webglAlignmentError); + return; + } + + this.player.video.initCanvasViewSize(); + break; + + case WORKER_CMD_TYPE.initAudio: + debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.initAudio, `channels:${msg.channels},sampleRate:${msg.sampleRate}`); + + if (this.player.audio) { + this.player.audio.updateAudioInfo(msg); + this.player.audio.initScriptNode(msg); + } + + break; + + case WORKER_CMD_TYPE.render: + // debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.render, `msg ts:${msg.ts}`); + this.player.handleRender(); + this.player.video.render(msg); + this.player.emit(EVENTS.timeUpdate, msg.ts); + this.player.updateStats({ + fps: true, + ts: msg.ts, + buf: msg.delay + }); + + if (!this.player._times.videoStart) { + this.player._times.videoStart = now(); + this.player.handlePlayToRenderTimes(); + } + + break; + + case WORKER_CMD_TYPE.playAudio: + // debug.log(`decoderWorker`, 'onmessage:', WORKER_CMD_TYPE.playAudio, `msg ts:${msg.ts}`); + // 只有在 playing 的时候。 + if (this.player.playing && this.player.audio) { + this.player.audio.play(msg.buffer, msg.ts); + } + + break; + + case WORKER_CMD_TYPE.wasmError: + if (msg.message) { + if (msg.message.indexOf(WASM_ERROR.invalidNalUnitSize) !== -1) { + this.player.emitError(EVENTS_ERROR.wasmDecodeError); + } + } + + break; + + default: + this.player[msg.cmd] && this.player[msg.cmd](msg); + } + }; + } + + _initWork() { + const opt = { + debug: this.player._opt.debug, + useOffscreen: this.player._opt.useOffscreen, + useWCS: this.player._opt.useWCS, + videoBuffer: this.player._opt.videoBuffer, + videoBufferDelay: this.player._opt.videoBufferDelay, + openWebglAlignment: this.player._opt.openWebglAlignment + }; + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.init, + opt: JSON.stringify(opt), + sampleRate: this.player.audio && this.player.audio.audioContext.sampleRate || 0 + }); + } + + decodeVideo(arrayBuffer, ts, isIFrame) { + const options = { + type: MEDIA_TYPE.video, + ts: Math.max(ts, 0), + isIFrame + }; // this.player.debug.log('decoderWorker', 'decodeVideo', options); + + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.decode, + buffer: arrayBuffer, + options + }, [arrayBuffer.buffer]); + } + + decodeAudio(arrayBuffer, ts) { + if (this.player._opt.useWCS) { + this._decodeAudioNoDelay(arrayBuffer, ts); + } else if (this.player._opt.useMSE) { + this._decodeAudioNoDelay(arrayBuffer, ts); + } else { + this._decodeAudio(arrayBuffer, ts); + } + } // + + + _decodeAudio(arrayBuffer, ts) { + const options = { + type: MEDIA_TYPE.audio, + ts: Math.max(ts, 0) + }; // this.player.debug.log('decoderWorker', 'decodeAudio',options); + + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.decode, + buffer: arrayBuffer, + options + }, [arrayBuffer.buffer]); + } + + _decodeAudioNoDelay(arrayBuffer, ts) { + // console.log('_decodeAudioNoDelay', arrayBuffer); + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.audioDecode, + buffer: arrayBuffer, + ts: Math.max(ts, 0) + }, [arrayBuffer.buffer]); + } + + updateWorkConfig(config) { + this.decoderWorker.postMessage({ + cmd: WORKER_SEND_TYPE.updateConfig, + key: config.key, + value: config.value + }); + } + + } + + class CommonLoader extends Emitter { + constructor(player) { + super(); + this.player = player; + this.stopId = null; + this.firstTimestamp = null; + this.startTimestamp = null; + this.delay = -1; + this.bufferList = []; + this.dropping = false; + this.initInterval(); + } + + destroy() { + if (this.stopId) { + clearInterval(this.stopId); + this.stopId = null; + } + + this.firstTimestamp = null; + this.startTimestamp = null; + this.delay = -1; + this.bufferList = []; + this.dropping = false; + this.off(); + this.player.debug.log('CommonDemux', 'destroy'); + } + + getDelay(timestamp) { + if (!timestamp) { + return -1; + } + + if (!this.firstTimestamp) { + this.firstTimestamp = timestamp; + this.startTimestamp = Date.now(); + this.delay = -1; + } else { + if (timestamp) { + const localTimestamp = Date.now() - this.startTimestamp; + const timeTimestamp = timestamp - this.firstTimestamp; + + if (localTimestamp >= timeTimestamp) { + this.delay = localTimestamp - timeTimestamp; + } else { + this.delay = timeTimestamp - localTimestamp; + } + } + } + + return this.delay; + } + + resetDelay() { + this.firstTimestamp = null; + this.startTimestamp = null; + this.delay = -1; + this.dropping = false; + } // + + + initInterval() { + this.player.debug.log('common dumex', `init Interval`); + + let _loop = () => { + let data; + const videoBuffer = this.player._opt.videoBuffer; + const videoBufferDelay = this.player._opt.videoBufferDelay; + + if (this.player._opt.useMSE && this.player.mseDecoder && this.player.mseDecoder.getSourceBufferUpdating()) { + this.player.debug.warn('CommonDemux', `_loop getSourceBufferUpdating is true and bufferList length is ${this.bufferList.length}`); + return; + } + + if (this.bufferList.length) { + if (this.dropping) { + // this.player.debug.log('common dumex', `is dropping`); + data = this.bufferList.shift(); + + if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) { + this._doDecoderDecode(data); + } + + while (!data.isIFrame && this.bufferList.length) { + data = this.bufferList.shift(); + + if (data.type === MEDIA_TYPE.audio && data.payload[1] === 0) { + this._doDecoderDecode(data); + } + } // i frame + + + if (data.isIFrame && this.getDelay(data.ts) <= Math.min(videoBuffer, 200)) { + this.dropping = false; + + this._doDecoderDecode(data); + } + } else { + data = this.bufferList[0]; + + if (this.getDelay(data.ts) === -1) { + // this.player.debug.log('common dumex', `delay is -1`); + this.bufferList.shift(); + + this._doDecoderDecode(data); + } else if (this.delay > videoBuffer + videoBufferDelay) { + // this.player.debug.log('common dumex', `delay is ${this.delay}, set dropping is true`); + this.resetDelay(); + this.dropping = true; + } else { + data = this.bufferList[0]; + + if (this.getDelay(data.ts) > videoBuffer) { + // drop frame + this.bufferList.shift(); + + this._doDecoderDecode(data); + } + } + } + } + }; + + _loop(); + + this.stopId = setInterval(_loop, 10); + } + + _doDecode(payload, type, ts, isIFrame, cts) { + const player = this.player; + let options = { + ts: ts, + cts: cts, + type: type, + isIFrame: false + }; // use offscreen + + if (player._opt.useWCS && !player._opt.useOffscreen) { + if (type === MEDIA_TYPE.video) { + options.isIFrame = isIFrame; + } + + this.pushBuffer(payload, options); + } else if (player._opt.useMSE) { + // use mse + if (type === MEDIA_TYPE.video) { + options.isIFrame = isIFrame; + } + + this.pushBuffer(payload, options); + } else { + // + if (type === MEDIA_TYPE.video) { + player.decoderWorker && player.decoderWorker.decodeVideo(payload, ts, isIFrame); + } else if (type === MEDIA_TYPE.audio) { + if (player._opt.hasAudio) { + player.decoderWorker && player.decoderWorker.decodeAudio(payload, ts); + } + } + } + } + + _doDecoderDecode(data) { + const player = this.player; + const { + webcodecsDecoder, + mseDecoder + } = player; + + if (data.type === MEDIA_TYPE.audio) { + if (player._opt.hasAudio) { + player.decoderWorker && player.decoderWorker.decodeAudio(data.payload, data.ts); + } + } else if (data.type === MEDIA_TYPE.video) { + if (player._opt.useWCS && !player._opt.useOffscreen) { + webcodecsDecoder.decodeVideo(data.payload, data.ts, data.isIFrame); + } else if (player._opt.useMSE) { + mseDecoder.decodeVideo(data.payload, data.ts, data.isIFrame, data.cts); + } + } + } + + pushBuffer(payload, options) { + // 音频 + if (options.type === MEDIA_TYPE.audio) { + this.bufferList.push({ + ts: options.ts, + payload: payload, + type: MEDIA_TYPE.audio + }); + } else if (options.type === MEDIA_TYPE.video) { + this.bufferList.push({ + ts: options.ts, + cts: options.cts, + payload: payload, + type: MEDIA_TYPE.video, + isIFrame: options.isIFrame + }); + } + } + + close() { } + + } + + class FlvLoader extends CommonLoader { + constructor(player) { + super(player); + this.input = this._inputFlv(); + this.flvDemux = this.dispatchFlvData(this.input); + player.debug.log('FlvDemux', 'init'); + } + + destroy() { + super.destroy(); + this.input = null; + this.flvDemux = null; + this.player.debug.log('FlvDemux', 'destroy'); + } + + dispatch(data) { + this.flvDemux(data); + } + + *_inputFlv() { + yield 9; + const tmp = new ArrayBuffer(4); + const tmp8 = new Uint8Array(tmp); + const tmp32 = new Uint32Array(tmp); + const player = this.player; + + while (true) { + tmp8[3] = 0; + const t = yield 15; + const type = t[4]; + tmp8[0] = t[7]; + tmp8[1] = t[6]; + tmp8[2] = t[5]; + const length = tmp32[0]; + tmp8[0] = t[10]; + tmp8[1] = t[9]; + tmp8[2] = t[8]; + let ts = tmp32[0]; + + if (ts === 0xFFFFFF) { + tmp8[3] = t[11]; + ts = tmp32[0]; + } + + const payload = yield length; + + switch (type) { + case FLV_MEDIA_TYPE.audio: + if (player._opt.hasAudio) { + player.updateStats({ + abps: payload.byteLength + }); + + if (payload.byteLength > 0) { + this._doDecode(payload, MEDIA_TYPE.audio, ts); + } + } + + break; + + case FLV_MEDIA_TYPE.video: + if (!player._times.demuxStart) { + player._times.demuxStart = now(); + } + + if (player._opt.hasVideo) { + player.updateStats({ + vbps: payload.byteLength + }); + const isIFrame = payload[0] >> 4 === 1; + + if (payload.byteLength > 0) { + tmp32[0] = payload[4]; + tmp32[1] = payload[3]; + tmp32[2] = payload[2]; + tmp32[3] = 0; + let cts = tmp32[0]; + + this._doDecode(payload, MEDIA_TYPE.video, ts, isIFrame, cts); + } + } + + break; + } + } + } + + dispatchFlvData(input) { + let need = input.next(); + let buffer = null; + return value => { + let data = new Uint8Array(value); + + if (buffer) { + let combine = new Uint8Array(buffer.length + data.length); + combine.set(buffer); + combine.set(data, buffer.length); + data = combine; + buffer = null; + } + + while (data.length >= need.value) { + let remain = data.slice(need.value); + need = input.next(data.slice(0, need.value)); + data = remain; + } + + if (data.length > 0) { + buffer = data; + } + }; + } + + close() { + this.input && this.input.return(null); + } + + } + + class M7sLoader extends CommonLoader { + constructor(player) { + super(player); + player.debug.log('M7sDemux', 'init'); + } + + destroy() { + super.destroy(); + this.player.debug.log('M7sDemux', 'destroy'); + this.player = null; + } + + dispatch(data) { + const player = this.player; + const dv = new DataView(data); + const type = dv.getUint8(0); + const ts = dv.getUint32(1, false); + + switch (type) { + case MEDIA_TYPE.audio: + if (player._opt.hasAudio) { + const payload = new Uint8Array(data, 5); + player.updateStats({ + abps: payload.byteLength + }); + + if (payload.byteLength > 0) { + this._doDecode(payload, type, ts); + } + } + + break; + + case MEDIA_TYPE.video: + if (player._opt.hasVideo) { + if (!player._times.demuxStart) { + player._times.demuxStart = now(); + } + + if (dv.byteLength > 5) { + const payload = new Uint8Array(data, 5); + const isIframe = dv.getUint8(5) >> 4 === 1; + player.updateStats({ + vbps: payload.byteLength + }); + + if (payload.byteLength > 0) { + this._doDecode(payload, type, ts, isIframe); + } + } else { + this.player.debug.warn('M7sDemux', 'dispatch', 'dv byteLength is', dv.byteLength); + } + } + + break; + } + } + + } + + class Demux { + constructor(player) { + const Loader = Demux.getLoaderFactory(player._opt.demuxType); + return new Loader(player); + } + + static getLoaderFactory(type) { + if (type === DEMUX_TYPE.m7s) { + return M7sLoader; + } else if (type === DEMUX_TYPE.flv) { + return FlvLoader; + } + } + + } + + /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + // Exponential-Golomb buffer decoder + class ExpGolomb { + constructor(uint8array) { + this.TAG = 'ExpGolomb'; + this._buffer = uint8array; + this._buffer_index = 0; + this._total_bytes = uint8array.byteLength; + this._total_bits = uint8array.byteLength * 8; + this._current_word = 0; + this._current_word_bits_left = 0; + } + + destroy() { + this._buffer = null; + } + + _fillCurrentWord() { + let buffer_bytes_left = this._total_bytes - this._buffer_index; + + let bytes_read = Math.min(4, buffer_bytes_left); + let word = new Uint8Array(4); + word.set(this._buffer.subarray(this._buffer_index, this._buffer_index + bytes_read)); + this._current_word = new DataView(word.buffer).getUint32(0, false); + this._buffer_index += bytes_read; + this._current_word_bits_left = bytes_read * 8; + } + + readBits(bits) { + + if (bits <= this._current_word_bits_left) { + let result = this._current_word >>> 32 - bits; + this._current_word <<= bits; + this._current_word_bits_left -= bits; + return result; + } + + let result = this._current_word_bits_left ? this._current_word : 0; + result = result >>> 32 - this._current_word_bits_left; + let bits_need_left = bits - this._current_word_bits_left; + + this._fillCurrentWord(); + + let bits_read_next = Math.min(bits_need_left, this._current_word_bits_left); + let result2 = this._current_word >>> 32 - bits_read_next; + this._current_word <<= bits_read_next; + this._current_word_bits_left -= bits_read_next; + result = result << bits_read_next | result2; + return result; + } + + readBool() { + return this.readBits(1) === 1; + } + + readByte() { + return this.readBits(8); + } + + _skipLeadingZero() { + let zero_count; + + for (zero_count = 0; zero_count < this._current_word_bits_left; zero_count++) { + if (0 !== (this._current_word & 0x80000000 >>> zero_count)) { + this._current_word <<= zero_count; + this._current_word_bits_left -= zero_count; + return zero_count; + } + } + + this._fillCurrentWord(); + + return zero_count + this._skipLeadingZero(); + } + + readUEG() { + // unsigned exponential golomb + let leading_zeros = this._skipLeadingZero(); + + return this.readBits(leading_zeros + 1) - 1; + } + + readSEG() { + // signed exponential golomb + let value = this.readUEG(); + + if (value & 0x01) { + return value + 1 >>> 1; + } else { + return -1 * (value >>> 1); + } + } + + } + + /* + * Copyright (C) 2016 Bilibili. All Rights Reserved. + * + * @author zheng qian + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + class SPSParser { + static _ebsp2rbsp(uint8array) { + let src = uint8array; + let src_length = src.byteLength; + let dst = new Uint8Array(src_length); + let dst_idx = 0; + + for (let i = 0; i < src_length; i++) { + if (i >= 2) { + // Unescape: Skip 0x03 after 00 00 + if (src[i] === 0x03 && src[i - 1] === 0x00 && src[i - 2] === 0x00) { + continue; + } + } + + dst[dst_idx] = src[i]; + dst_idx++; + } + + return new Uint8Array(dst.buffer, 0, dst_idx); + } // 解析 SPS + // https://zhuanlan.zhihu.com/p/27896239 + + + static parseSPS(uint8array) { + let rbsp = SPSParser._ebsp2rbsp(uint8array); + + let gb = new ExpGolomb(rbsp); + gb.readByte(); // 标识当前H.264码流的profile。 + // 我们知道,H.264中定义了三种常用的档次profile: 基准档次:baseline profile;主要档次:main profile; 扩展档次:extended profile; + + let profile_idc = gb.readByte(); // profile_idc + + gb.readByte(); // constraint_set_flags[5] + reserved_zero[3] + // 标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。 + + let level_idc = gb.readByte(); // level_idc + // 表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。 + + gb.readUEG(); // seq_parameter_set_id + + let profile_string = SPSParser.getProfileString(profile_idc); + let level_string = SPSParser.getLevelString(level_idc); + let chroma_format_idc = 1; + let chroma_format = 420; + let chroma_format_table = [0, 420, 422, 444]; + let bit_depth = 8; // + + if (profile_idc === 100 || profile_idc === 110 || profile_idc === 122 || profile_idc === 244 || profile_idc === 44 || profile_idc === 83 || profile_idc === 86 || profile_idc === 118 || profile_idc === 128 || profile_idc === 138 || profile_idc === 144) { + // + chroma_format_idc = gb.readUEG(); + + if (chroma_format_idc === 3) { + gb.readBits(1); // separate_colour_plane_flag + } + + if (chroma_format_idc <= 3) { + chroma_format = chroma_format_table[chroma_format_idc]; + } + + bit_depth = gb.readUEG() + 8; // bit_depth_luma_minus8 + + gb.readUEG(); // bit_depth_chroma_minus8 + + gb.readBits(1); // qpprime_y_zero_transform_bypass_flag + + if (gb.readBool()) { + // seq_scaling_matrix_present_flag + let scaling_list_count = chroma_format_idc !== 3 ? 8 : 12; + + for (let i = 0; i < scaling_list_count; i++) { + if (gb.readBool()) { + // seq_scaling_list_present_flag + if (i < 6) { + SPSParser._skipScalingList(gb, 16); + } else { + SPSParser._skipScalingList(gb, 64); + } + } + } + } + } // 用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 + + + + gb.readUEG(); // log2_max_frame_num_minus4 + // 表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。 + + let pic_order_cnt_type = gb.readUEG(); + + if (pic_order_cnt_type === 0) { + gb.readUEG(); // log2_max_pic_order_cnt_lsb_minus_4 + } else if (pic_order_cnt_type === 1) { + gb.readBits(1); // delta_pic_order_always_zero_flag + + gb.readSEG(); // offset_for_non_ref_pic + + gb.readSEG(); // offset_for_top_to_bottom_field + + let num_ref_frames_in_pic_order_cnt_cycle = gb.readUEG(); + + for (let i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) { + gb.readSEG(); // offset_for_ref_frame + } + } // 用于表示参考帧的最大数目。 + + + let ref_frames = gb.readUEG(); // max_num_ref_frames + // 标识位,说明frame_num中是否允许不连续的值。 + + gb.readBits(1); // gaps_in_frame_num_value_allowed_flag + // 用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为: + + let pic_width_in_mbs_minus1 = gb.readUEG(); // 使用PicHeightInMapUnits来度量视频中一帧图像的高度。 + // PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为: + + let pic_height_in_map_units_minus1 = gb.readUEG(); // 标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码; + // 该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同, + // 为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。 + + let frame_mbs_only_flag = gb.readBits(1); + + if (frame_mbs_only_flag === 0) { + // 标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。 + gb.readBits(1); // mb_adaptive_frame_field_flag + } // 标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。 + + + gb.readBits(1); // direct_8x8_inference_flag + + let frame_crop_left_offset = 0; + let frame_crop_right_offset = 0; + let frame_crop_top_offset = 0; + let frame_crop_bottom_offset = 0; + let frame_cropping_flag = gb.readBool(); + + if (frame_cropping_flag) { + frame_crop_left_offset = gb.readUEG(); + frame_crop_right_offset = gb.readUEG(); + frame_crop_top_offset = gb.readUEG(); + frame_crop_bottom_offset = gb.readUEG(); + } + + let sar_width = 1, + sar_height = 1; + let fps = 0, + fps_fixed = true, + fps_num = 0, + fps_den = 0; // 标识位,说明SPS中是否存在VUI信息。 + + let vui_parameters_present_flag = gb.readBool(); + + if (vui_parameters_present_flag) { + if (gb.readBool()) { + // aspect_ratio_info_present_flag + let aspect_ratio_idc = gb.readByte(); + let sar_w_table = [1, 12, 10, 16, 40, 24, 20, 32, 80, 18, 15, 64, 160, 4, 3, 2]; + let sar_h_table = [1, 11, 11, 11, 33, 11, 11, 11, 33, 11, 11, 33, 99, 3, 2, 1]; + + if (aspect_ratio_idc > 0 && aspect_ratio_idc < 16) { + sar_width = sar_w_table[aspect_ratio_idc - 1]; + sar_height = sar_h_table[aspect_ratio_idc - 1]; + } else if (aspect_ratio_idc === 255) { + sar_width = gb.readByte() << 8 | gb.readByte(); + sar_height = gb.readByte() << 8 | gb.readByte(); + } + } + + if (gb.readBool()) { + // overscan_info_present_flag + gb.readBool(); // overscan_appropriate_flag + } + + if (gb.readBool()) { + // video_signal_type_present_flag + gb.readBits(4); // video_format & video_full_range_flag + + if (gb.readBool()) { + // colour_description_present_flag + gb.readBits(24); // colour_primaries & transfer_characteristics & matrix_coefficients + } + } + + if (gb.readBool()) { + // chroma_loc_info_present_flag + gb.readUEG(); // chroma_sample_loc_type_top_field + + gb.readUEG(); // chroma_sample_loc_type_bottom_field + } + + if (gb.readBool()) { + // timing_info_present_flag + let num_units_in_tick = gb.readBits(32); + let time_scale = gb.readBits(32); + fps_fixed = gb.readBool(); // fixed_frame_rate_flag + + fps_num = time_scale; + fps_den = num_units_in_tick * 2; + fps = fps_num / fps_den; + } + } + + let sarScale = 1; + + if (sar_width !== 1 || sar_height !== 1) { + sarScale = sar_width / sar_height; + } + + let crop_unit_x = 0, + crop_unit_y = 0; + + if (chroma_format_idc === 0) { + crop_unit_x = 1; + crop_unit_y = 2 - frame_mbs_only_flag; + } else { + let sub_wc = chroma_format_idc === 3 ? 1 : 2; + let sub_hc = chroma_format_idc === 1 ? 2 : 1; + crop_unit_x = sub_wc; + crop_unit_y = sub_hc * (2 - frame_mbs_only_flag); + } + + let codec_width = (pic_width_in_mbs_minus1 + 1) * 16; + let codec_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 + 1) * 16); + codec_width -= (frame_crop_left_offset + frame_crop_right_offset) * crop_unit_x; + codec_height -= (frame_crop_top_offset + frame_crop_bottom_offset) * crop_unit_y; + let present_width = Math.ceil(codec_width * sarScale); + gb.destroy(); + gb = null; // 解析出来的SPS 内容。 + + return { + profile_string: profile_string, + // baseline, high, high10, ... + level_string: level_string, + // 3, 3.1, 4, 4.1, 5, 5.1, ... + bit_depth: bit_depth, + // 8bit, 10bit, ... + ref_frames: ref_frames, + chroma_format: chroma_format, + // 4:2:0, 4:2:2, ... + chroma_format_string: SPSParser.getChromaFormatString(chroma_format), + frame_rate: { + fixed: fps_fixed, + fps: fps, + fps_den: fps_den, + fps_num: fps_num + }, + sar_ratio: { + width: sar_width, + height: sar_height + }, + codec_size: { + width: codec_width, + height: codec_height + }, + present_size: { + width: present_width, + height: codec_height + } + }; + } + + static _skipScalingList(gb, count) { + let last_scale = 8, + next_scale = 8; + let delta_scale = 0; + + for (let i = 0; i < count; i++) { + if (next_scale !== 0) { + delta_scale = gb.readSEG(); + next_scale = (last_scale + delta_scale + 256) % 256; + } + + last_scale = next_scale === 0 ? last_scale : next_scale; + } + } // profile_idc = 66 → baseline profile; + // profile_idc = 77 → main profile; + // profile_idc = 88 → extended profile; + // 在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High + // 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra + + + static getProfileString(profile_idc) { + switch (profile_idc) { + case 66: + return 'Baseline'; + + case 77: + return 'Main'; + + case 88: + return 'Extended'; + + case 100: + return 'High'; + + case 110: + return 'High10'; + + case 122: + return 'High422'; + + case 244: + return 'High444'; + + default: + return 'Unknown'; + } + } + + static getLevelString(level_idc) { + return (level_idc / 10).toFixed(1); + } + + static getChromaFormatString(chroma) { + switch (chroma) { + case 420: + return '4:2:0'; + + case 422: + return '4:2:2'; + + case 444: + return '4:4:4'; + + default: + return 'Unknown'; + } + } + + } + + function parseAVCDecoderConfigurationRecord(arrayBuffer) { + const meta = {}; + const v = new DataView(arrayBuffer.buffer); + let version = v.getUint8(0); // configurationVersion + + let avcProfile = v.getUint8(1); // avcProfileIndication + + v.getUint8(2); // profile_compatibil + + v.getUint8(3); // AVCLevelIndication + + if (version !== 1 || avcProfile === 0) { + // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord'); + return meta; + } + + const _naluLengthSize = (v.getUint8(4) & 3) + 1; // lengthSizeMinusOne + + + if (_naluLengthSize !== 3 && _naluLengthSize !== 4) { + // holy shit!!! + // this._onError(DemuxErrors.FORMAT_ERROR, `Flv: Strange NaluLengthSizeMinusOne: ${_naluLengthSize - 1}`); + return meta; + } + + let spsCount = v.getUint8(5) & 31; // numOfSequenceParameterSets + + if (spsCount === 0) { + // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No SPS'); + return; + } + + let offset = 6; + + for (let i = 0; i < spsCount; i++) { + let len = v.getUint16(offset, false); // sequenceParameterSetLength + + offset += 2; + + if (len === 0) { + continue; + } // Notice: Nalu without startcode header (00 00 00 01) + + + let sps = new Uint8Array(arrayBuffer.buffer, offset, len); + offset += len; // flv.js作者选择了自己来解析这个数据结构,也是迫不得已,因为JS环境下没有ffmpeg,解析这个结构主要是为了提取 sps和pps。虽然理论上sps允许有多个,但其实一般就一个。 + // packetTtype 为 1 表示 NALU,NALU= network abstract layer unit,这是H.264的概念,网络抽象层数据单元,其实简单理解就是一帧视频数据。 + // pps的信息没什么用,所以作者只实现了sps的分析器,说明作者下了很大功夫去学习264的标准,其中的Golomb解码还是挺复杂的,能解对不容易,我在PC和手机平台都是用ffmpeg去解析的。 + // SPS里面包括了视频分辨率,帧率,profile level等视频重要信息。 + + let config = SPSParser.parseSPS(sps); + + if (i !== 0) { + // ignore other sps's config + continue; + } + + meta.codecWidth = config.codec_size.width; + meta.codecHeight = config.codec_size.height; + meta.presentWidth = config.present_size.width; + meta.presentHeight = config.present_size.height; + meta.profile = config.profile_string; + meta.level = config.level_string; + meta.bitDepth = config.bit_depth; + meta.chromaFormat = config.chroma_format; + meta.sarRatio = config.sar_ratio; + meta.frameRate = config.frame_rate; + + if (config.frame_rate.fixed === false || config.frame_rate.fps_num === 0 || config.frame_rate.fps_den === 0) { + meta.frameRate = {}; + } + + let fps_den = meta.frameRate.fps_den; + let fps_num = meta.frameRate.fps_num; + meta.refSampleDuration = meta.timescale * (fps_den / fps_num); + let codecArray = sps.subarray(1, 4); + let codecString = 'avc1.'; + + for (let j = 0; j < 3; j++) { + let h = codecArray[j].toString(16); + + if (h.length < 2) { + h = '0' + h; + } + + codecString += h; + } // codec + + + meta.codec = codecString; + } + + let ppsCount = v.getUint8(offset); // numOfPictureParameterSets + + if (ppsCount === 0) { + // this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid AVCDecoderConfigurationRecord: No PPS'); + return meta; + } + + offset++; + + for (let i = 0; i < ppsCount; i++) { + let len = v.getUint16(offset, false); // pictureParameterSetLength + + offset += 2; + + if (len === 0) { + continue; + } + + new Uint8Array(arrayBuffer.buffer, offset, len); // pps is useless for extracting video information + + offset += len; + } + + meta.videoType = 'avc'; // meta.avcc = arrayBuffer; + + return meta; + } + + class WebcodecsDecoder extends Emitter { + constructor(player) { + super(); + this.player = player; + this.hasInit = false; + this.isDecodeFirstIIframe = false; + this.isInitInfo = false; + this.decoder = null; + this.initDecoder(); + player.debug.log('Webcodecs', 'init'); + } + + destroy() { + if (this.decoder) { + if (this.decoder.state !== 'closed') { + this.decoder.close(); + } + + this.decoder = null; + } + + this.hasInit = false; + this.isInitInfo = false; + this.isDecodeFirstIIframe = false; + this.off(); + this.player.debug.log('Webcodecs', 'destroy'); + } + + initDecoder() { + const _this = this; + + this.decoder = new VideoDecoder({ + output(videoFrame) { + _this.handleDecode(videoFrame); + }, + + error(error) { + _this.handleError(error); + } + + }); + } + + handleDecode(videoFrame) { + if (!this.isInitInfo) { + this.player.video.updateVideoInfo({ + width: videoFrame.codedWidth, + height: videoFrame.codedHeight + }); + this.player.video.initCanvasViewSize(); + this.isInitInfo = true; + } + + if (!this.player._times.videoStart) { + this.player._times.videoStart = now(); + this.player.handlePlayToRenderTimes(); + } + + this.player.handleRender(); + this.player.video.render({ + videoFrame + }); + this.player.updateStats({ + fps: true, + ts: 0, + buf: this.player.demux.delay + }); + } + + handleError(error) { + this.player.debug.error('Webcodecs', 'VideoDecoder handleError', error); + } + + decodeVideo(payload, ts, isIframe) { + // this.player.debug.log('Webcodecs decoder', 'decodeVideo', ts, isIframe); + if (!this.hasInit) { + if (isIframe && payload[1] === 0) { + const videoCodec = payload[0] & 0x0F; + this.player.video.updateVideoInfo({ + encTypeCode: videoCodec + }); // 如果解码出来的是 + + if (videoCodec === VIDEO_ENC_CODE.h265) { + this.emit(EVENTS_ERROR.webcodecsH265NotSupport); + return; + } + + if (!this.player._times.decodeStart) { + this.player._times.decodeStart = now(); + } + + const config = formatVideoDecoderConfigure(payload.slice(5)); + this.decoder.configure(config); + this.hasInit = true; + } + } else { + // check width or height change + if (isIframe && payload[1] === 0) { + let data = payload.slice(5); + const config = parseAVCDecoderConfigurationRecord(data); + const videoInfo = this.player.video.videoInfo; + + if (config.codecWidth !== videoInfo.width || config.codecHeight !== videoInfo.height) { + this.player.debug.log('Webcodecs', `width or height is update, width ${videoInfo.width}-> ${config.codecWidth}, height ${videoInfo.height}-> ${config.codecHeight}`); + this.player.emit(EVENTS_ERROR.webcodecsWidthOrHeightChange); + return; + } + } // fix : Uncaught DOMException: Failed to execute 'decode' on 'VideoDecoder': A key frame is required after configure() or flush(). + + + if (!this.isDecodeFirstIIframe && isIframe) { + this.isDecodeFirstIIframe = true; + } + + if (this.isDecodeFirstIIframe) { + const chunk = new EncodedVideoChunk({ + data: payload.slice(5), + timestamp: ts, + type: isIframe ? ENCODED_VIDEO_TYPE.key : ENCODED_VIDEO_TYPE.delta + }); + this.player.emit(EVENTS.timeUpdate, ts); + + try { + if (this.isDecodeStateClosed()) { + this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeStateClosed true'); + return; + } + + this.decoder.decode(chunk); + } catch (e) { + this.player.debug.error('Webcodecs', 'VideoDecoder', e); + + if (e.toString().indexOf(WCS_ERROR.keyframeIsRequiredError) !== -1) { + this.player.emitError(EVENTS_ERROR.webcodecsDecodeError); + } else if (e.toString().indexOf(WCS_ERROR.canNotDecodeClosedCodec) !== -1) { + this.player.emitError(EVENTS_ERROR.webcodecsDecodeError); + } + } + } else { + this.player.debug.warn('Webcodecs', 'VideoDecoder isDecodeFirstIIframe false'); + } + } + } + + isDecodeStateClosed() { + return this.decoder.state === 'closed'; + } + + } + + const iconsMap = { + play: '播放', + pause: '暂停', + audio: '', + mute: '', + screenshot: '截图', + loading: '加载', + fullscreen: '全屏', + fullscreenExit: '退出全屏', + record: '录制', + recordStop: '停止录制' + }; + var icons = Object.keys(iconsMap).reduce((icons, key) => { + icons[key] = ` + + ${iconsMap[key] ? `${iconsMap[key]}` : ''} +`; + return icons; + }, {}); + + var template = ((player, control) => { + if (player._opt.hasControl && player._opt.controlAutoHide) { + player.$container.classList.add('jessibuca-controls-show-auto-hide'); + } else { + player.$container.classList.add('jessibuca-controls-show'); + } + + const options = player._opt; + const operateBtns = options.operateBtns; + player.$container.insertAdjacentHTML('beforeend', ` + ${options.background ? `
` : ''} +
+ ${icons.loading} + ${options.loadingText ? `
${options.loadingText}
` : ''} +
+ ${options.hasControl && operateBtns.play ? `
` : ''} + ${options.hasControl ? ` +
+
+
00:00:01
+
${icons.recordStop}
+
+ ` : ''} + ${options.hasControl ? ` +
+
+
+ ${options.showBandwidth ? `
` : ''} +
+
+ ${operateBtns.audio ? ` +
+ ${icons.audio} + ${icons.mute} +
+
+
+
+
+
+
+ ` : ''} + ${operateBtns.play ? `
${icons.play}
${icons.pause}
` : ''} + ${operateBtns.screenshot ? `
${icons.screenshot}
` : ''} + ${operateBtns.record ? `
${icons.record}
${icons.recordStop}
` : ''} + ${operateBtns.fullscreen ? `
${icons.fullscreen}
${icons.fullscreenExit}
` : ''} +
+
+
+ ` : ''} + + `); + Object.defineProperty(control, '$poster', { + value: player.$container.querySelector('.jessibuca-poster') + }); + Object.defineProperty(control, '$loading', { + value: player.$container.querySelector('.jessibuca-loading') + }); + Object.defineProperty(control, '$play', { + value: player.$container.querySelector('.jessibuca-play') + }); + Object.defineProperty(control, '$playBig', { + value: player.$container.querySelector('.jessibuca-play-big') + }); + Object.defineProperty(control, '$recording', { + value: player.$container.querySelector('.jessibuca-recording') + }); + Object.defineProperty(control, '$recordingTime', { + value: player.$container.querySelector('.jessibuca-recording-time') + }); + Object.defineProperty(control, '$recordingStop', { + value: player.$container.querySelector('.jessibuca-recording-stop') + }); + Object.defineProperty(control, '$pause', { + value: player.$container.querySelector('.jessibuca-pause') + }); + Object.defineProperty(control, '$controls', { + value: player.$container.querySelector('.jessibuca-controls') + }); + Object.defineProperty(control, '$fullscreen', { + value: player.$container.querySelector('.jessibuca-fullscreen') + }); + Object.defineProperty(control, '$fullscreen', { + value: player.$container.querySelector('.jessibuca-fullscreen') + }); + Object.defineProperty(control, '$volume', { + value: player.$container.querySelector('.jessibuca-volume') + }); + Object.defineProperty(control, '$volumePanelWrap', { + value: player.$container.querySelector('.jessibuca-volume-panel-wrap') + }); + Object.defineProperty(control, '$volumePanelText', { + value: player.$container.querySelector('.jessibuca-volume-panel-text') + }); + Object.defineProperty(control, '$volumePanel', { + value: player.$container.querySelector('.jessibuca-volume-panel') + }); + Object.defineProperty(control, '$volumeHandle', { + value: player.$container.querySelector('.jessibuca-volume-panel-handle') + }); + Object.defineProperty(control, '$volumeOn', { + value: player.$container.querySelector('.jessibuca-icon-audio') + }); + Object.defineProperty(control, '$volumeOff', { + value: player.$container.querySelector('.jessibuca-icon-mute') + }); + Object.defineProperty(control, '$fullscreen', { + value: player.$container.querySelector('.jessibuca-fullscreen') + }); + Object.defineProperty(control, '$fullscreenExit', { + value: player.$container.querySelector('.jessibuca-fullscreen-exit') + }); + Object.defineProperty(control, '$record', { + value: player.$container.querySelector('.jessibuca-record') + }); + Object.defineProperty(control, '$recordStop', { + value: player.$container.querySelector('.jessibuca-record-stop') + }); + Object.defineProperty(control, '$screenshot', { + value: player.$container.querySelector('.jessibuca-screenshot') + }); + Object.defineProperty(control, '$speed', { + value: player.$container.querySelector('.jessibuca-speed') + }); + }); + + var observer$1 = ((player, control) => { + const { + events: { + proxy + } + } = player; + const object = document.createElement('object'); + object.setAttribute('aria-hidden', 'true'); + object.setAttribute('tabindex', -1); + object.type = 'text/html'; + object.data = 'about:blank'; + setStyle(object, { + display: 'block', + position: 'absolute', + top: '0', + left: '0', + height: '100%', + width: '100%', + overflow: 'hidden', + pointerEvents: 'none', + zIndex: '-1' + }); + let playerWidth = player.width; + let playerHeight = player.height; + proxy(object, 'load', () => { + proxy(object.contentDocument.defaultView, 'resize', () => { + if (player.width !== playerWidth || player.height !== playerHeight) { + playerWidth = player.width; + playerHeight = player.height; + player.emit(EVENTS.resize); + screenfullH5Control(); + } + }); + }); + player.$container.appendChild(object); + player.on(EVENTS.destroy, () => { + player.$container.removeChild(object); + }); + + function setVolumeHandle(percentage) { + if (percentage === 0) { + setStyle(control.$volumeOn, 'display', 'none'); + setStyle(control.$volumeOff, 'display', 'flex'); + setStyle(control.$volumeHandle, 'top', `${48}px`); + } else { + if (control.$volumeHandle && control.$volumePanel) { + const panelHeight = getStyle(control.$volumePanel, 'height') || 60; + const handleHeight = getStyle(control.$volumeHandle, 'height'); + const top = panelHeight - (panelHeight - handleHeight) * percentage - handleHeight; + setStyle(control.$volumeHandle, 'top', `${top}px`); + setStyle(control.$volumeOn, 'display', 'flex'); + setStyle(control.$volumeOff, 'display', 'none'); + } + } + + control.$volumePanelText && (control.$volumePanelText.innerHTML = parseInt(percentage * 100)); + } + + player.on(EVENTS.volumechange, () => { + setVolumeHandle(player.volume); + }); + player.on(EVENTS.loading, flag => { + setStyle(control.$loading, 'display', flag ? 'flex' : 'none'); + setStyle(control.$poster, 'display', 'none'); + + if (flag) { + setStyle(control.$playBig, 'display', 'none'); + } + }); + + const screenfullChange = fullscreen => { + let isFullScreen = isBoolean(fullscreen) ? fullscreen : player.fullscreen; + setStyle(control.$fullscreenExit, 'display', isFullScreen ? 'flex' : 'none'); + setStyle(control.$fullscreen, 'display', isFullScreen ? 'none' : 'flex'); // control.autoSize(); + }; + + const screenfullH5Control = () => { + if (isMobile() && control.$controls && player._opt.useWebFullScreen) { + setTimeout(() => { + if (player.fullscreen) { + // console.log(player.width, player.height); + let translateX = player.height / 2 - player.width + CONTROL_HEIGHT / 2; + let translateY = player.height / 2 - CONTROL_HEIGHT / 2; + control.$controls.style.transform = `translateX(${-translateX}px) translateY(-${translateY}px) rotate(-90deg)`; + } else { + control.$controls.style.transform = `translateX(0) translateY(0) rotate(0)`; + } + }, 10); + } + }; + + try { + screenfull.on('change', screenfullChange); + player.events.destroys.push(() => { + screenfull.off('change', screenfullChange); + }); + } catch (error) {// + } // + + + player.on(EVENTS.webFullscreen, value => { + screenfullChange(value); + screenfullH5Control(); + }); + player.on(EVENTS.recording, () => { + setStyle(control.$record, 'display', player.recording ? 'none' : 'flex'); + setStyle(control.$recordStop, 'display', player.recording ? 'flex' : 'none'); + setStyle(control.$recording, 'display', player.recording ? 'flex' : 'none'); + }); // + + player.on(EVENTS.recordingTimestamp, timestamp => { + // console.log(timestamp); + control.$recordingTime && (control.$recordingTime.innerHTML = formatTimeTips(timestamp)); + }); + player.on(EVENTS.playing, flag => { + setStyle(control.$play, 'display', flag ? 'none' : 'flex'); + setStyle(control.$playBig, 'display', flag ? 'none' : 'block'); + setStyle(control.$pause, 'display', flag ? 'flex' : 'none'); + setStyle(control.$screenshot, 'display', flag ? 'flex' : 'none'); + setStyle(control.$record, 'display', flag ? 'flex' : 'none'); + setStyle(control.$qualityMenu, 'display', flag ? 'flex' : 'none'); + setStyle(control.$volume, 'display', flag ? 'flex' : 'none'); // setStyle(control.$fullscreen, 'display', flag ? 'flex' : 'none'); + + screenfullChange(); // 不在播放 + + if (!flag) { + control.$speed && (control.$speed.innerHTML = bpsSize('')); + } + }); + player.on(EVENTS.kBps, rate => { + const bps = bpsSize(rate); + control.$speed && (control.$speed.innerHTML = bps); + }); + }); + + var property = ((player, control) => { + Object.defineProperty(control, 'controlsRect', { + get: () => { + return control.$controls.getBoundingClientRect(); + } + }); + }); + + var events = ((player, control) => { + const { + events: { + proxy + }, + debug + } = player; + + function volumeChangeFromEvent(event) { + const { + bottom: panelBottom, + height: panelHeight + } = control.$volumePanel.getBoundingClientRect(); + const { + height: handleHeight + } = control.$volumeHandle.getBoundingClientRect(); + let moveLen = event.y; // if (isMobile() && player.fullscreen) { + // moveLen = event.x; + // } + + const percentage = clamp(panelBottom - moveLen - handleHeight / 2, 0, panelHeight - handleHeight / 2) / (panelHeight - handleHeight); + return percentage; + } // + + + proxy(window, ['click', 'contextmenu'], event => { + if (event.composedPath().indexOf(player.$container) > -1) { + control.isFocus = true; + } else { + control.isFocus = false; + } + }); // + + proxy(window, 'orientationchange', () => { + setTimeout(() => { + player.resize(); + }, 300); + }); + proxy(control.$controls, 'click', e => { + e.stopPropagation(); + }); + proxy(control.$pause, 'click', e => { + player.pause(); + }); // 监听 play 方法 + + proxy(control.$play, 'click', e => { + player.play(); + player.resumeAudioAfterPause(); + }); // 监听 play 方法 + + proxy(control.$playBig, 'click', e => { + player.play(); + player.resumeAudioAfterPause(); + }); + proxy(control.$volume, 'mouseover', () => { + control.$volumePanelWrap.classList.add('jessibuca-volume-panel-wrap-show'); + }); + proxy(control.$volume, 'mouseout', () => { + control.$volumePanelWrap.classList.remove('jessibuca-volume-panel-wrap-show'); + }); + proxy(control.$volumeOn, 'click', e => { + e.stopPropagation(); + setStyle(control.$volumeOn, 'display', 'none'); + setStyle(control.$volumeOff, 'display', 'block'); + const lastVolume = player.volume; + player.volume = 0; + player._lastVolume = lastVolume; + }); + proxy(control.$volumeOff, 'click', e => { + e.stopPropagation(); + setStyle(control.$volumeOn, 'display', 'block'); + setStyle(control.$volumeOff, 'display', 'none'); + player.volume = player.lastVolume || 0.5; + }); + proxy(control.$screenshot, 'click', e => { + e.stopPropagation(); + player.video.screenshot(); + }); + proxy(control.$volumePanel, 'click', event => { + event.stopPropagation(); + player.volume = volumeChangeFromEvent(event); + }); + proxy(control.$volumeHandle, 'mousedown', () => { + control.isVolumeDroging = true; + }); + proxy(control.$volumeHandle, 'mousemove', event => { + if (control.isVolumeDroging) { + player.volume = volumeChangeFromEvent(event); + } + }); + proxy(document, 'mouseup', () => { + if (control.isVolumeDroging) { + control.isVolumeDroging = false; + } + }); + proxy(control.$record, 'click', e => { + e.stopPropagation(); + player.recording = true; + }); + proxy(control.$recordStop, 'click', e => { + e.stopPropagation(); + player.recording = false; + }); + proxy(control.$recordingStop, 'click', e => { + e.stopPropagation(); + player.recording = false; + }); + proxy(control.$fullscreen, 'click', e => { + e.stopPropagation(); + player.fullscreen = true; + }); + proxy(control.$fullscreenExit, 'click', e => { + e.stopPropagation(); + player.fullscreen = false; + }); + + if (player._opt.hasControl && player._opt.controlAutoHide) { + // + proxy(player.$container, 'mouseover', () => { + if (!player.fullscreen) { + setStyle(control.$controls, 'display', 'block'); + startDelayControlHidden(); + } + }); + proxy(player.$container, 'mousemove', () => { + if (player.$container && control.$controls) { + if (!player.fullscreen) { + if (control.$controls.style.display === 'none') { + setStyle(control.$controls, 'display', 'block'); + startDelayControlHidden(); + } + } else { + if (control.$controls.style.display === 'none') { + setStyle(control.$controls, 'display', 'block'); + startDelayControlHidden(); + } + } + } + }); + proxy(player.$container, 'mouseout', () => { + stopDelayControlHidden(); + setStyle(control.$controls, 'display', 'none'); + }); + let delayHiddenTimeout = null; + + const startDelayControlHidden = () => { + stopDelayControlHidden(); + delayHiddenTimeout = setTimeout(() => { + setStyle(control.$controls, 'display', 'none'); + }, 5 * 1000); + }; + + const stopDelayControlHidden = () => { + if (delayHiddenTimeout) { + clearTimeout(delayHiddenTimeout); + delayHiddenTimeout = null; + } + }; + } + }); + + function styleInject(css, ref) { + if (ref === void 0) ref = {}; + var insertAt = ref.insertAt; + + if (!css || typeof document === 'undefined') { return; } + + var head = document.head || document.getElementsByTagName('head')[0]; + var style = document.createElement('style'); + style.type = 'text/css'; + + if (insertAt === 'top') { + if (head.firstChild) { + head.insertBefore(style, head.firstChild); + } else { + head.appendChild(style); + } + } else { + head.appendChild(style); + } + + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.appendChild(document.createTextNode(css)); + } + } + + var css_248z$1 = "@keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes magentaPulse{0%{background-color:#630030;-webkit-box-shadow:0 0 9px #333}50%{background-color:#a9014b;-webkit-box-shadow:0 0 18px #a9014b}to{background-color:#630030;-webkit-box-shadow:0 0 9px #333}}.jessibuca-container .jessibuca-icon{cursor:pointer;width:16px;height:16px}.jessibuca-container .jessibuca-poster{position:absolute;z-index:10;left:0;top:0;right:0;bottom:0;height:100%;width:100%;background-position:50%;background-repeat:no-repeat;background-size:contain;pointer-events:none}.jessibuca-container .jessibuca-play-big{position:absolute;display:none;height:100%;width:100%;background:rgba(0,0,0,.4)}.jessibuca-container .jessibuca-play-big:after{cursor:pointer;content:\"\";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:block;width:48px;height:48px;background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\");background-repeat:no-repeat;background-position:50%}.jessibuca-container .jessibuca-play-big:hover:after{background-image:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\")}.jessibuca-container .jessibuca-recording{display:none;position:absolute;left:50%;top:0;padding:0 3px;transform:translateX(-50%);justify-content:space-around;align-items:center;width:95px;height:20px;background:#000;opacity:1;border-radius:0 0 8px 8px;z-index:1}.jessibuca-container .jessibuca-recording .jessibuca-recording-red-point{width:8px;height:8px;background:#ff1f1f;border-radius:50%;animation:magentaPulse 1s linear infinite}.jessibuca-container .jessibuca-recording .jessibuca-recording-time{font-size:14px;font-weight:500;color:#ddd}.jessibuca-container .jessibuca-recording .jessibuca-icon-recordStop{width:16px;height:16px;cursor:pointer}.jessibuca-container .jessibuca-loading{display:none;flex-direction:column;justify-content:center;align-items:center;position:absolute;z-index:20;left:0;top:0;right:0;bottom:0;width:100%;height:100%;pointer-events:none}.jessibuca-container .jessibuca-loading-text{line-height:20px;font-size:13px;color:#fff;margin-top:10px}.jessibuca-container .jessibuca-controls{background-color:#161616;box-sizing:border-box;display:flex;flex-direction:column;justify-content:flex-end;position:absolute;z-index:40;left:0;right:0;bottom:0;height:38px;width:100%;padding-left:13px;padding-right:13px;font-size:14px;color:#fff;opacity:0;visibility:hidden;transition:all .2s ease-in-out;-webkit-user-select:none;user-select:none;transition:width .5s ease-in}.jessibuca-container .jessibuca-controls .jessibuca-controls-item{position:relative;display:flex;justify-content:center;padding:0 8px}.jessibuca-container .jessibuca-controls .jessibuca-controls-item:hover .icon-title-tips{visibility:visible;opacity:1}.jessibuca-container .jessibuca-controls .jessibuca-fullscreen,.jessibuca-container .jessibuca-controls .jessibuca-fullscreen-exit,.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-microphone-close,.jessibuca-container .jessibuca-controls .jessibuca-pause,.jessibuca-container .jessibuca-controls .jessibuca-play,.jessibuca-container .jessibuca-controls .jessibuca-record,.jessibuca-container .jessibuca-controls .jessibuca-record-stop,.jessibuca-container .jessibuca-controls .jessibuca-screenshot{display:none}.jessibuca-container .jessibuca-controls .jessibuca-icon-audio,.jessibuca-container .jessibuca-controls .jessibuca-icon-mute{z-index:1}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom{display:flex;justify-content:space-between;height:100%}.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-left,.jessibuca-container .jessibuca-controls .jessibuca-controls-bottom .jessibuca-controls-right{display:flex;align-items:center}.jessibuca-container.jessibuca-controls-show .jessibuca-controls{opacity:1;visibility:visible}.jessibuca-container.jessibuca-controls-show-auto-hide .jessibuca-controls{opacity:.8;visibility:visible;display:none}.jessibuca-container.jessibuca-hide-cursor *{cursor:none!important}.jessibuca-container .jessibuca-icon-loading{width:50px;height:50px;background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%;animation:rotation 1s linear infinite}.jessibuca-container .jessibuca-icon-screenshot{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAE5UlEQVRoQ+1YW2sdVRT+1s7JxbsoVkEUrIIX0ouz15zYNA+N1RdtQfCltlUfvLbqL/BCwZ8grbHtizQqPojgBSr0JkiMmT2nxgapqBURtPVCq7HxJCeZJVPmxDlzZubMmXOSEsnAvOy917fXt9e39tp7E5b4R0vcfywTuNgRbBgBx3HuJqLVzPzmYjprjHkcwAlmLqXNm4XAISLaSESPaq2HF4OE67rbRGRYRA7btn1fbgLGmKsA/Azg0gBkGzO/vZAkHMd5hIiqc5wHcCMz/5k0Z2oExsfHV1QqldPAf8lORNu11m8tBAljzFYAYWxRSl1vWdZvuQj4RsYYF4AVBlgIOVVlE55HRIxt23ZuCfmGjuOsJ6LPoiAistW27XfaEYmIbOYhPc9bXywWR1oiEJDYQkR1zrYjEjGyqfqbKd8a7kJVtLgQ+30i8pht2wfyRKIdmJkJBPkQTbILfudJ7CTZNBvVpggEcgpvc/ML38zESbLJsxBNE/A9biX0rdjGyTQXgbxyapdsarb0PMlXtWnGoXbKpm0Essqp3bJpK4E0OXmed3+hUBDP8w5FI91M0rdcyLLILElOCbaZilSWeXMncRx4klTCY1spfG3dhZJWx3GcDUR0EEB3ZMw0ET2gtT6SZWWzjmlrBIJCl0hAKfWgZVmHszqXZVxbCSxpCS2JJA6umIhe8ZKKVLPbaBJ+S9toqVRa53nedgAbAKwIwH4FcAzAa0R0l4i8F7PPz189k6RFRA+LyNcAXojDV0oNW5b1eW4Cxpg9AHZkSaaa6hhzb065uDSCH2LmRB8Sk9gY4293g43Qo/1pV80m8yQMfZSZ781cB1zXHRKRZ2IMpgD8A+DamL4ZItqitX4/jbQx5iEA7wLoihn3V/ACckWMJN/QWj9b1x5tGBsbW6uUOh5pPy0iL3Z2dn6ilJqanp5ep5TaJSLhF4NppdRNaU8gPmapVLrO87yfIoXuWyJ6uVKp+HmFjo6OQSJ6FcBtYT+UUmstyxqvkWuUgDFmP4AnQu2/e563qlgs+u9DNZ8xZhRAX7VRRPbath0XuXk7Y8xeAE+FgL6fnJzsHRwcLIfBR0ZGLunq6poAsDLUvp+Zw7b1r9PGmJMAbg8Z7WDmoThZuK67WkS+DD18fcPMdzSQUBR/EzN/nIC/SUQ+DPXV4dclsTHmHAD/SfHCNzc3t7Kvr++HJKeMMacA3BL0nyuXyzcPDAxMxo0fHR29slAo/Ajg6qD/fE9Pzw29vb1/x42fmJi4vFwu+5G/LOg/y8zXNJLQ2dAES5JANMQ7mfn1jBI6ycx3NiMhItqstf4oAX+ziHwQ6qvDj5NQNIn/ALCKmX+JSeIvABRD7fuY+ekGBPYBeDI05tTMzExvf3+/vz2Hk91/ET8RSeI6/DoCpVJpjed5fmKGvzMAXpqdnT3oed5Ud3d3v4jsAqBr9Ei0Rmv9VRqBBPzvROQVETnq2xJRdRu9tRF+bCVOKWT+Kvl/TSIFk6SW/LAjKfjV5K8rZABi8dOOEv7FI7Z8x6zwEWbemLbyMfJr5qiSiJ96oclymBOR3bZtP9+M89WxxpjdAHY2sN3DzM8ljWl4I3Nd9x7/OE1ENcdpETnmH3e11n41zv0l4J8RkU+J6AAz+xtF4teQQG7PFslwmcAiLfSyhC72Qv9/I/Avns2OT7QJskoAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-screenshot:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAED0lEQVRoQ+2ZycsdRRTFf2ejqHFAMQqiYBTUoElUHLNx3GgCgpuYRF2o0UT9CxwQ/BMkMSbZSKLiQgQHUDCJgjiAxiEiESdEcJbEedgcKaj3UV+/6q7u/jovPPkK3qbr1ql76p5bt6qemPKmKfefeQKHOoLFCNg+H1gi6fFJOmv7VmCvpD1N87Yh8ApwNXCzpB2TIGF7DRDm2inpmt4EbB8LfAMcGUHWSHryYJKwfRMwmuMP4BRJv9TN2RgB2wuB72BWsq+V9MTBIGF7NZBiGzhJ0o+9CIRBtt8FLqgADC6nRDbpVO9Iuqi3hCKB5cDrGZDVkp4aIhIV2aSQyyW9MScCkcQqIOfsnCORkc3I31b5VtyFRmg1IQ7dt0ja3icSQ2C2JhAjUU2ykd+dE7tBNp2i2olAJJFuc+nCt564QTadF6IzgUhiVGiqyinKaQjZpJP2ItBXTkPJZhACXeU0pGwGI9BWTkPLZlACBTldG4o5EA6E1dY66edcyNrs8Q36zg1vVaTazNs7iXPgDVJJzYs7VRvHRzaDEohyugJ4CTi84sg/wHWSdnVxsGQ7aQLXS9pZcqpL/6AEplpCU5HE8YpJ9YrXUKQ6baN1+HPaRm1fBqwFQnKGK2ZoPwCvAo8Ai4FnMpPMHMwapHUj8DFwbw3+Dklv9iZgexOwvktSRduxU2VDlErwmyXV+lCbxLbDdndlCT3TX3vV7JgnKfRuSVflfMkSsL0ZuDMz4E/gL+CETN+/wCpJzzaRtn0D8DRwWMbu1/gCcnSm7zFJd1W/jxGwvQx4r2IYnlbuA14GAomQFw8B6YtBKFSnNj2BxEJ3IvB1pdB9CjwQ8yqYhcg/DJxZ8WOZpA/SbzkC24DbEqOfgPMkBRKzmu23gEuSj1sk5SI3Y2J7C3BHMuZz4FxJf6fgto8APgIWJd+3SUrHjr9O294HnJUMWi8pSGqs2V4CvJ88fH0i6eyChKr4KyS9WIO/Ang+6RvDz0XgABCeFEdtkaQv65yy/QVweuwPY0+T9FuNQ8cAXwHHxf7wdHiypN9r7BfEl8GjYv9+SceXJLQ/mSDYTh2Baog3SHq0pYT2STqno4RWSnqhBn8l8FzSN4bfJol/jkn8bXUS228DFyfft0paVyCwFbg9sQkSDEkctueZZju8iO+tJPEYfo7A0piYKd73wP3xnB+20cvjNnphxdmlkj4sEMjhfwY8COyOY0fb6Bkl/K6FLKxS+M1KpDhJY8mvrG5doRwlf66QZfGbjhLh4pEt35kV3iUp/IvTunU8qtTil/7gaHOY2yjpntaez9b5RmBDYewmSXfX2RRvZLYvbThOh+NuqMa9Ww1+yLnXgO2SwkZR24oEens2oYHzBCa00PMSOtQL/f+NwH+Hg8hAnbrYgQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACgklEQVRoQ+3ZPYsTQRjA8eeZZCFlWttAwCIkZOaZJt8hlvkeHrlccuAFT6wEG0FQOeQQLCIWih6chQgKgkkKIyqKCVYip54IWmiQkTmyYhFvd3Zn3yDb7szu/7cv7GaDkPEFM94PK0DSZ9DzDAyHw7uI2HRDlVJX5/N5r9FoHCYdr/fvCRiNRmpJ6AEidoUQ15NG+AH8BgD2n9AHANAmohdJQfwAfgGA4xF4bjabnW21Whob62ILoKNfAsAGEd2PU2ATcNSNiDf0/cE5/xAHxDpgEf0NADaJ6HLUiKgAbvcjpdSGlPJZVJCoAUfdSqkLxWLxTLlc/mkbEgtgET1TSnWklLdtIuIEuN23crlcp16vv7cBSQKgu38AwBYRXQyLSArg3hsjRDxNRE+CQhIF/BN9qVAobFYqle+mkLQAdLd+8K0T0U0TRJoAbvc9fVkJId75gaQRoLv1C2STiPTb7rFLWgE6+g0RncwyYEJEtawCvjDGmpzzp5kD6NfxfD7frtVqB17xen2a7oG3ALBm+oMoFQBEPD+dTvtBfpImDXjIGFvjnD/3c7ksG5MU4HDxWeZa0HB3XhKAXcdxOn5vUi9gnIDXSqm2lHLPK8pkfVyAbSLqm4T5HRs1YB8RO0KIid8g03FRAT4rpbpSyh3TINPxUQB2GGM9zvkn05gg420CJovLZT9ISNA5tgB9ItoOGhFmnh/AcZ/X9xhj65zzV2Eiwsz1A1j2B8dHAOgS0W6YnduY6wkYj8d3lFKn/j66Ea84jtOrVqtfbQSE3YYnYDAY5Eql0hYAnNDv6kKIx2F3anO+J8DmzqLY1goQxVE12ebqDJgcrSjGrs5AFEfVZJt/AF0m+jHzUTtnAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-play:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACEElEQVRoQ+2ZXStEQRjH/3/yIXwDdz7J+i7kvdisXCk3SiFJW27kglBcSFFKbqwQSa4krykuKB09Naf2Yndn5jgzc06d53Znd36/mWfeniVyHsw5PwqB0DOonYEoijYBlOpAFwCMkHwLDS/9mwhEDUCfAAyTXA4tYSLwC6CtCegegH6S56FETAR+AHRoACcBTJAUWa+RloBAXwAYIrnt0yBNgZi7qtbHgw8RFwLC/QFglOScawlXAjH3gUqrE1cirgVi7mkAYyS/0xbxJSDcdwAGSa6nKeFTIOZeUyL3aYiEEBDuLwDjJGf+KxFKIOY+BdBL8iipSGiBmHtWbbuftiJZERBuOfgGSK7aSGRJIObeUml1ayKSRQHhlgtkiaTcdltGVgUE+ppkV54FaiS78yrwqlLoOI8Cch2XV548W7WRpTVwA6DP9kGUFYEpAOUkT9LQAvtq1M+0udKkQSgBqSlJWWYxKXj8vRACK+o6bbRIdYI+Ba7U7rKjg7L53JdAhWTZBsy0rWuBXZUuNVMg23auBF7UIl2yBbJt70JAoKV6/WwLk6R9mgKSJlJ1kLTxFmkJyCla8UZd15GJQKvyumyJ8gy8DAEvfZoINPqD41EtUjmUgoaJwAaAnjrKebVI34OSq85NBNqlCAWgE0CV5GEWwI3vQlmCbcSinYFCwPEIFDPgeIC1P1/MgHaIHDf4Aydx2TF7wnKeAAAAAElFTkSuQmCC\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABA0lEQVRoQ+1YwQqCUBAcfWXXsLr2AXWTPXno8yVB8AP6Aa3oHI+kCDqYaawJljSe133uzO44bx0M/HEG/v1gAd9mkAyQgY4I/F8LJUlyrQFtD2AtIkcNoFEU+Z7n7QD4DfFHEVlocrVmgAUAIAOl3mILPcDgEFcUhyrUKMGUUcroc3NQRimj9XJBGaWMvvPydKN0o6/9QTdKN6rZANxj6EbpRulGuZnjYqs8BbyR8Ub2Izeys+u6yyAIDpo/ehzHM2NMDsA0xFsRmWhyfTIDWSXxCEBmrd2EYXjSHJqm6bQoii2AOYBL5Z0xgFxEVppcrQvQJO0zhgX0iXbdWWSADHRE4AZQ731AhEUeNwAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-pause:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAA7klEQVRoQ+2YSwrCQBBEX6HiVvxsPYDewfN7By/gD9ciQkvERQwJdBSiYs0mEDo96aruombEjy/9+P/jAj7NoBkwA28i8H8tFBFRA9oeWEo6ZgCNiDGwAYpn3TpKmmVytWbABQBmoNRbbqEHGB7iiuJYhRol2DJqGX1uDsuoZdRmLuNZSzGWUcuoZdRHSp/IylNgK2ErYSthK3FHwLcSvpXIjoLt9Jfa6TMwl3TIMBkRE2AH9BriL5KGmVyvWIltJXEfKN6tJJ0ym0bECFgDU+Ba+WZQFCdpkcnVuoBM0i5jXECXaNftZQbMwJsI3AAPN3dAQflHegAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC+UlEQVRoQ+1ZS2sTURT+zlDJYE3XSq219QHVuEjnJDT+Bff9Abqw2voAEfGxqygUqWhVFHGl/yMLu9BwByxk5SNI66ML6U7axjhHbmhgWiftncxoOiV3FcI53z3f/e65594zhIQPSnj86BBot4IdBToKRFyBnbeFlFIScVEiuYvIWC6Xe2YK8pcC7SYA4CMzH4mDQBXAqilQBDsLQLfPf9FxnF4i8kwwmypARI+Wl5dvmIBEsUmlUkNE9NaHsVCpVAZGR0d/m+A2JSAid3K53E0TkCg2pVKpz7KseR/GfKVSGYxMAMA0M1+JEpyJb6lUOm5ZVnkrAsVisaunp+esiByr1Wp3R0ZGvmifzZK4XQQWHMc52MgBpdQuAOcAXABwuB400ZTjONdaIjA7O5u2bVsnWU1EujzP+5nP5xdMVjvIJkCBD8x8VCm1G8AYgAkAAxt8Z5j5YmgCSqlTAJ4D2OcD/AXgATNfbYVEAIFPIvKKiE4D6GuCea8xX6gtpJT6DmBvECgRFRzHeROWRAABE4iWCbwHEFhkPM/L5vP5dyaz+23+KwHXdR3P854S0YG1ILSCuthNMfNM2OC1/RYENLY+ygcBnPfht6ZAA6BYLNr6dyqVokKhsGpaNQ2TWJstreXaE2aed133sojcj41AKyvdzCdAgSXLsk4MDw9/a/i4rntbRPxFNZoC/5jAV2be759DKTUJ4FZSFFi0bbs/k8noy2R9dAjEuWU2YgXkQOK3kD6BMsysi2Z9JC2Jdcw/ALzwPO+xvmcl7Rj177JVEbkO4BARjSflFDJJuW1dBxJPoCIiL4noDIB1BS0pW6j+oJmbm+uuVqvjRKQfLr0bZHnIzJf0f6HeAybahrUJqAPruhLlcnnPysqKfpXp11n/Gv62zoHAroS+AafT6QkiGrIsazKbzX7eVIHEt1US39gCkOzWYthkjNE+tuZujDGZQ8XRXn8N4KT5lLFZ6uaYPt+nwyDuvC80YdhvB9uOAu1WoaNAR4GIK/AHvdr+QAexB7EAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-record:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACfUlEQVRoQ+2ZSYsUQRCFvycK4nJXXEbHBdwO4kn/gv9CD467ICIutxEFkREdFUU86T/xojcPntyQcT2INw+uISFVkD1Wd2dWlU7nUHlqisiX+fJFZGREi8yHMt8/HYG5VrBToFOg4QnMPxcyM2t4KE2nT0i6EwvylwIjQOCFpE1tEPgGfI0FamC3AFgazP8IrJL0KwZzkAI3gLMxIA1ttgCPA4w3wHpJP2NwBxG4KOlcDEgTGzNbA8wEGP57vA0CU5JONtlczFwz2wY8HUbAzBYCB4CtwCVJb33OIAXmioC70LoyBsxsEXAQOApsLIhelnS6FgEzW+5BBvwA/FS+SPJFa40KBZ5L2mxmS4AJ4IjHxCzwaUnHkgmY2V7gLrAyAPwOXJN0qg6DCgIvgQfAPsDjo2pcKddLciEz+wCs6AO6W9KjVBIVBGIgahN4BvRLMjslPYlZPbT53wR2AbeBtcUmXEFPdh5U06mbd/shBBzbr/Jx4FCAX0+BEsDMFocEYrNmFcE+BD4XsXZL0oyZnQCutkagzkn3m1NBwDe/Q9L74MAuFEqUn5op8I8JvJO0elacTALnc1HAH3Njkvwx+WeYWUegTa/pwaqIgexdyIN4uyRPmqULZRXEvulPwD3gpr+zcrtGQxfzRHYG2AAczuUWiom3kc4D2RN4BdwH9gM9CS0XFyoLGu9UuN974eIFVDiuSzruH5LqgRhtU20q8kBPV8LMlhVVmVdnYwX+SMdAZVeieAF7eeltmElJr4cpkH1bJfvGVvatxdR4bMu+teZuWxtKxWncXn8I7EldtQV7vz79fp9KwZp//9CksB8F206BuVahU6BToOEJ/Ab7+KdABdTt8AAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-recordStop:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreen:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAADd0lEQVRoQ+2Zz2sdVRTHv+fJBDW6anDVXen6wZszYxYBiYgtFGst3VSDunKjpS0GpUlqfjVpsVVs6aaL0or4YxMVFCJZ2ZLdPUP+gq5bQnTxtNAkfTnlhnnlkmQy9yV9780rudt77tzv5/y4v4bQ4Y06XD/2ANodwec/AiJygJnvtdvTWfPnRkBEJAiCN8rl8kMfiPn5+Ve7u7v3rays0Orq6lJfX99/PuN2auMDoAD+BvA2M6/mTWSMOUtE48D6AjHGzN/kjdlNvy+AnWOOmQ/lTSYiEwDOWzsimgrDcCRvzG76GwGw8/zJzO9sN6GInAMwbW1UdSSKoqndCMwb6wNwGsB39Q+p6h/M/C4R2dTa1AoHYBWKyCkA1+pqiWi2Wq0e7e/vf7yRoJAAKcQggMtuJKIoOtoxACnE0/xOi/SXMAxPuhCFjUBdpIjYVWXSEf0TM3/g9BeriDMKdSPEz8z8vrU1xgwT0YXCrEJZy1iSJKOqOub0/8jMA0mSfKKqNwoPkHp7ioiGHIhRIvpHVa93BEBa2JcAfOlALAHo6RgAKzRJkk9V1S6xL7kpV4idOM31taxaIKJHqmpPnMMA9hcOQES2PDJkAT1XAAC+ZebPfWB3auNzmLObVsNRUNUXVHUujuM7OxXnMy4XwOcj29mIyOuq+lapVGrYCelKpkEQ3CyXy4tbzdN0AGPMxr2iYZ+sra3FcRybtgCIiK2BKw2rdgaUSqWoUqlIkQAepFDdAF7cBq5ERI9rtdr1OI7tmE2t6SmUEYFHAEaexYW/1QC2EF+ru5GIvg7D0D2GNJxprQY4o6qv1I/b6SpzOYqiLxpWng5oOQAzXxWRWwA+dkRfYOb1p5hGW6sBJpn5KytSRG4D+KguWFXHoyhy7xdeLC0F2ChSRL4H8OFuINoKYIUbY34gogHH3eeZef1K6tPaDpCm068A3nMEDzHzxY4BUNWSiPxORO6z5aDPPlGICNQ9bYyZIaLjjudzIQoFkKbTbwCO+UI0HcB9J/LdeY0xs0R02IGYYObRrWqiFQCfEZEtSHsfmGZm+4qxbbM/hQD8BeBNa0hEM2EYnmgLgP3lFARBT1dXly4vL//b29tbzQNIU+llAHeJaLFSqRzJes5vegR8xGbZLCwsHKzVav8z8/0sm0ID+MDvAfh4qZk2exFopnd9vv0ELrXBQO7fD10AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-fullscreenExit:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAC/ElEQVRoQ+2Zy49NQRCHvx+ReK6IlZ34E7CUiCAR4xEbTLCyQRATYswwb2IQZDYWgojHZpCQECts+ResiQwLj0RClNSkb9Lu3HtPz7mZc8+V6eXt6tP1VVV3VdcVbT7U5vozC9BqD/7/HjCzlZLet9rS9fbP9ICZvQPWSfqRAmFmS4ClMHm+JiR9S1mXVyYFwIBXwEZJv7I2MrPjQH8A6JN0OWtNM/OpAL7HS0mbsjYzswGgN8gNS+rJWtPM/HQAfJ9nkrY22tDMTgMjQaZH0nAzCmatTQE4ClyNPvQU2CbJQ2vKKB2Aa2hmR4DrkbbPgQ5Jv6sJSgkQILqA0dgTkjraBiBAxPHtPz2UtDuGKK0HKkqamd8qg5HS9yXtjebLdYjrHNRqiAeS9gQvnQGGSnML1bvGzOwc0BfN35PUaWYHgRulBwjW9ju+O4JwqM/AWFsABIgLwKkIYgJY1jYAAeJQuGIXVIVcKTKxh8WfBin9J+AVpx/eFWUEqFkyNACKp0rhgWYArkg6kQibSyylmPOklQdibijBX+fSLHFRJkDid+qKmdlaYENOI0zeEcBNSZ9qbVIEQHWuyGOTNZLetgrAz8ClPFpHa1ZL8rf5lFGEB2oBfAxQi4D5DeDmAP7mGJPka0oD4LnDr9imH/xFe8AP4vLIjBclxWXItCOtaIBjwOKo3HaFRyWdnLbmYUHhAJKumdkt4ECk9JCkSitmWixFAwxKOjt5uZvdBvZH2vZLit8XSSBFA/yjpJndAfY1A9FSgOCJu0BnBNErqfIkzfRCywECxCNgR6Rtt6TzmdqHBmyKXG4ZM4sTWc04NzNPWE+AuG3ZlZInSuGBinXMbBzYGVkrE6JUACGcHgPbUyGKAIj7REmZ18y897o5ghiQ5E/bltRChwE/kF7Xj0jyLkbDYWbzgBfA+iA4LmlXqwD8LydvszjAF0lfswBCKC0E3gBeP22p186f8RBKUbaejJmtAr5L+lBPptQAKfCzAClWmkmZWQ/MpHVTvv0X9iFAQGQyevIAAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACrUlEQVRoQ+2ZPYgTURCAZzbBXJnCeL2Cnb87b9MEtPBUrrMQFAtrtT5/ClGs9LBWWz0RtbBUFCF4oJDsbO68wsLA2YqQSmLlvpEHu7IuMdlLcus+yUKKhJfZ+ebnvZl5CJY/aLn+MAP41x7M1QPMfFtr/crzvHfTAs8FoNPp1LTWzwHgqIg0lFLvrQHwfX8BER8DwC6jNCIecF13wwoA3/dvIuKNpLJa60Oe560XGoCZd4rICiKeTCtaeABmPg4AJmRqg6xcaABmvg4At4aFRyEBhoVM4UMoCplHADCfJTEL5YEsIVNID5iQAYCHALCYxeq5b6PMfF5EBAAEESthGK7W6/XPRpFWq7W3VCqtZg2ZcT3g+/6i4zjzIlLSWn/yPO/DIGMNLCWY2Sj/+xGRK0qpZfNDEASnROTFVi0fr8+aA8z8Ld6KEfGt67oLYwMAwEUium8EREn7OgeAjwCwPyo/nrque3YSgAtE9GDaAM1mc65arc4Zuf1+P2w0Gt9jJZl5DQAORt+fENG5wgEw8zUAMB/zbBBRwyqAIAjuiMjlSOlNItpjFUCqWl0josMzgChR/9hGAWBbknjmAdPhDdqa0gfZzAMJKyVP4v8hhJYRcSni+0JEu63ahZj5anyQici6UuqIVQDdbrfS6/UqRulyufyTiH5sF8AlIro37VpoWEHIzGZ2tM+sEZFnSqkzk9RCS0R01wjIsZz+mug53hDRia0AnI4bGgDYISItz/M2jYC8Gpp2u30MEWuO4zha665Sqp0ZYFStX/iWchRAItFGzoHSsrJ2ZFl1mHg6bfVYJeGJv85CC++BpIJZ5kSFC6G0ha0e7mYJqcJ7IOkRay84UhD2XjHFIFZf8iW9YcYoYRi+tO6aNeupOs66iU/icV46zf/MAKZpzXFk/QL+JG1PUPhRiQAAAABJRU5ErkJggg==\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-audio:hover{background:url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAACSElEQVRoQ+2Zu4sUQRCHf5+C+gf4yBXMfMYHGvjCzEBQDIzV+HwEohipGKupD0QNDE8UEwUFTe68wEDhTMVUMFJ+0tArzbjs9u3Ojt0wBR0M9MzUV1XdXVWNKhcq1189wP/2YKcesH1d0nPgdVvgnQDY3iTpqaT9kuaAt9UA2D4o6aGkzVHpXcByFQC2r0q60lB2D7BUNIDtjZIeSDoyRNGyAWwfiiET4n6YlAtg+7Kka2PCozyAMSHT5CkLIIbMfUlbMhdmOQCZIVOeB2LI3JN0NNPq6bTZe8D2aUmOY72kN8DnoIXt7eF5FSEzkQdsB+OEsFwr6RPwbpixhqYStoPyqVwAbkaAY5KeTWD5wStZHrD9XdJgK34FhBP9H8kFOAvciQBhn3/RAcBHSTvjfx4DJ6cBOAPcbRvA9gZJYQT5DfwYKGl7UdLu+PwIOFUiwCVJYQRZBuZqA7gh6XxUegXYVhtAmq0uAnt7gLhQm9vorBZx74Hcc6D3QLKH/z2JGyVnlYs4pCfzEe4rsLW2XehicpAtAftqAwiZbhhBfgE/ZwVwDrjddi40KiG0HXpHO+KcJ8CJaXKheeBWBOgqnf6W1BwvgcOrATieFDTrJL0HViJAVwXNgVgPrJH0BfiQDTDKtREiNK7KLSnHASQLLacP1PxcVkWWq8PU3emq2yqJJ0b1Qsv2QKpdZp+orBBqmrfq5m5mSJXtgUZI1XnB0YCo94opCal6L/ka3ghtlIXqrllzT9VJ5k19Ek/y0zbf6QHatOYk3/oDujC8QMWgjf4AAAAASUVORK5CYII=\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-mute:hover{background:url(\"data:image/png;base64,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\") no-repeat 50%;background-size:100% 100%}.jessibuca-container .jessibuca-icon-text{font-size:14px;width:30px}.jessibuca-container .jessibuca-speed{font-size:14px;color:#fff}.jessibuca-container .jessibuca-quality-menu-list{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .jessibuca-quality-menu-list.jessibuca-quality-menu-shown{visibility:visible;opacity:1}.jessibuca-container .icon-title-tips{pointer-events:none;position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%);transition:visibility .3s ease 0s,opacity .3s ease 0s;background-color:rgba(0,0,0,.5);border-radius:4px}.jessibuca-container .icon-title{display:inline-block;padding:5px 10px;font-size:12px;white-space:nowrap;color:#fff}.jessibuca-container .jessibuca-quality-menu{padding:8px 0}.jessibuca-container .jessibuca-quality-menu-item{display:block;height:25px;margin:0;padding:0 10px;cursor:pointer;font-size:14px;text-align:center;width:50px;color:hsla(0,0%,100%,.5);transition:color .3s,background-color .3s}.jessibuca-container .jessibuca-quality-menu-item:hover{background-color:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-quality-menu-item:focus{outline:none}.jessibuca-container .jessibuca-quality-menu-item.jessibuca-quality-menu-item-active{color:#2298fc}.jessibuca-container .jessibuca-volume-panel-wrap{position:absolute;left:50%;bottom:100%;visibility:hidden;opacity:0;transform:translateX(-50%) translateY(22%);transition:visibility .3s,opacity .3s;background-color:rgba(0,0,0,.5);border-radius:4px;height:120px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-wrap.jessibuca-volume-panel-wrap-show{visibility:visible;opacity:1}.jessibuca-container .jessibuca-volume-panel{cursor:pointer;position:absolute;top:21px;height:60px;width:50px;overflow:hidden}.jessibuca-container .jessibuca-volume-panel-text{position:absolute;left:0;top:0;width:50px;height:20px;line-height:20px;text-align:center;color:#fff;font-size:12px}.jessibuca-container .jessibuca-volume-panel-handle{position:absolute;top:48px;left:50%;width:12px;height:12px;border-radius:12px;margin-left:-6px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:before{bottom:-54px;background:#fff}.jessibuca-container .jessibuca-volume-panel-handle:after{bottom:6px;background:hsla(0,0%,100%,.2)}.jessibuca-container .jessibuca-volume-panel-handle:after,.jessibuca-container .jessibuca-volume-panel-handle:before{content:\"\";position:absolute;display:block;left:50%;width:3px;margin-left:-1px;height:60px}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-controls{width:100vh}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-play-big:after{transform:translate(-50%,-50%) rotate(270deg)}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading{flex-direction:row}.jessibuca-container.jessibuca-fullscreen-web .jessibuca-loading-text{transform:rotate(270deg)}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsb0JBQ0UsR0FDRSw4QkFBaUMsQ0FDbkMsR0FDRSwrQkFBbUMsQ0FBRSxDQUV6Qyx3QkFDRSxHQUNFLHdCQUF5QixDQUN6QiwrQkFBa0MsQ0FDcEMsSUFDRSx3QkFBeUIsQ0FDekIsbUNBQXNDLENBQ3hDLEdBQ0Usd0JBQXlCLENBQ3pCLCtCQUFrQyxDQUFFLENBRXhDLHFDQUNFLGNBQWUsQ0FDZixVQUFXLENBQ1gsV0FBYyxDQUVoQix1Q0FDRSxpQkFBa0IsQ0FDbEIsVUFBVyxDQUNYLE1BQU8sQ0FDUCxLQUFNLENBQ04sT0FBUSxDQUNSLFFBQVMsQ0FDVCxXQUFZLENBQ1osVUFBVyxDQUNYLHVCQUFrQyxDQUNsQywyQkFBNEIsQ0FDNUIsdUJBQXdCLENBQ3hCLG1CQUFzQixDQUV4Qix5Q0FDRSxpQkFBa0IsQ0FDbEIsWUFBYSxDQUNiLFdBQVksQ0FDWixVQUFXLENBQ1gseUJBQWdDLENBQ2hDLCtDQUNFLGNBQWUsQ0FDZixVQUFXLENBQ1gsaUJBQWtCLENBQ2xCLFFBQVMsQ0FDVCxPQUFRLENBQ1IsOEJBQWdDLENBQ2hDLGFBQWMsQ0FDZCxVQUFXLENBQ1gsV0FBWSxDQUNaLGs5QkFBMkMsQ0FDM0MsMkJBQTRCLENBQzVCLHVCQUE2QixDQUMvQixxREFDRSwwekJBQW1ELENBRXZELDBDQUNFLFlBQWEsQ0FDYixpQkFBa0IsQ0FDbEIsUUFBUyxDQUNULEtBQU0sQ0FDTixhQUFjLENBQ2QsMEJBQTJCLENBQzNCLDRCQUE2QixDQUM3QixrQkFBbUIsQ0FDbkIsVUFBVyxDQUNYLFdBQVksQ0FDWixlQUFtQixDQUNuQixTQUFVLENBQ1YseUJBQThCLENBQzlCLFNBQVksQ0FDWix5RUFDRSxTQUFVLENBQ1YsVUFBVyxDQUNYLGtCQUFtQixDQUNuQixpQkFBa0IsQ0FDbEIseUNBQTRDLENBQzlDLG9FQUNFLGNBQWUsQ0FDZixlQUFnQixDQUNoQixVQUFnQixDQUNsQixxRUFDRSxVQUFXLENBQ1gsV0FBWSxDQUNaLGNBQWlCLENBRXJCLHdDQUNFLFlBQWEsQ0FDYixxQkFBc0IsQ0FDdEIsc0JBQXVCLENBQ3ZCLGtCQUFtQixDQUNuQixpQkFBa0IsQ0FDbEIsVUFBVyxDQUNYLE1BQU8sQ0FDUCxLQUFNLENBQ04sT0FBUSxDQUNSLFFBQVMsQ0FDVCxVQUFXLENBQ1gsV0FBWSxDQUNaLG1CQUFzQixDQUV4Qiw2Q0FDRSxnQkFBaUIsQ0FDakIsY0FBZSxDQUNmLFVBQVcsQ0FDWCxlQUFrQixDQUVwQix5Q0FDRSx3QkFBeUIsQ0FDekIscUJBQXNCLENBQ3RCLFlBQWEsQ0FDYixxQkFBc0IsQ0FDdEIsd0JBQXlCLENBQ3pCLGlCQUFrQixDQUNsQixVQUFXLENBQ1gsTUFBTyxDQUNQLE9BQVEsQ0FDUixRQUFTLENBQ1QsV0FBWSxDQUNaLFVBQVcsQ0FDWCxpQkFBa0IsQ0FDbEIsa0JBQW1CLENBQ25CLGNBQWUsQ0FDZixVQUFXLENBQ1gsU0FBVSxDQUNWLGlCQUFrQixDQUNsQiw4QkFBZ0MsQ0FDaEMsd0JBQWlCLENBQWpCLGdCQUFpQixDQUNqQiw0QkFBK0IsQ0FDL0Isa0VBQ0UsaUJBQWtCLENBQ2xCLFlBQWEsQ0FDYixzQkFBdUIsQ0FDdkIsYUFBZ0IsQ0FDaEIseUZBQ0Usa0JBQW1CLENBQ25CLFNBQVksQ0FpQmhCLG9qQkFDRSxZQUFlLENBQ2pCLDZIQUNFLFNBQVksQ0FDZCxvRUFDRSxZQUFhLENBQ2IsNkJBQThCLENBQzlCLFdBQWMsQ0FJZCwyTEFGRSxZQUFhLENBQ2Isa0JBR3FCLENBRTNCLGlFQUNFLFNBQVUsQ0FDVixrQkFBcUIsQ0FFdkIsMkVBQ0UsVUFBWSxDQUNaLGtCQUFtQixDQUNuQixZQUFlLENBRWpCLDZDQUNFLHFCQUF5QixDQUUzQiw2Q0FDRSxVQUFXLENBQ1gsV0FBWSxDQUNaLGtnRkFBeUQsQ0FDekQseUJBQTBCLENBQzFCLHFDQUF3QyxDQUUxQyxnREFDRSwwd0RBQTRELENBQzVELHlCQUE0QixDQUM1QixzREFDRSw4K0NBQWtFLENBQ2xFLHlCQUE0QixDQUVoQywwQ0FDRSwwOUJBQXNELENBQ3RELHlCQUE0QixDQUM1QixnREFDRSxrMEJBQTRELENBQzVELHlCQUE0QixDQUVoQywyQ0FDRSw4ZEFBdUQsQ0FDdkQseUJBQTRCLENBQzVCLGlEQUNFLGtjQUE2RCxDQUM3RCx5QkFBNEIsQ0FFaEMsNENBQ0UsMG5DQUF3RCxDQUN4RCx5QkFBNEIsQ0FDNUIsa0RBQ0UsczlCQUE4RCxDQUM5RCx5QkFBNEIsQ0FFaEMsZ0RBQ0Usa3BFQUE2RCxDQUM3RCx5QkFBNEIsQ0FDNUIsc0RBQ0UsOHFGQUFtRSxDQUNuRSx5QkFBNEIsQ0FFaEMsZ0RBQ0UsOGpGQUE0RCxDQUM1RCx5QkFBNEIsQ0FDNUIsc0RBQ0UsMGlFQUFrRSxDQUNsRSx5QkFBNEIsQ0FFaEMsb0RBQ0Usa3lDQUFpRSxDQUNqRSx5QkFBNEIsQ0FDNUIsMERBQ0UsOG5DQUF1RSxDQUN2RSx5QkFBNEIsQ0FFaEMsMkNBQ0Usc2hDQUF1RCxDQUN2RCx5QkFBNEIsQ0FDNUIsaURBQ0UsODRCQUE2RCxDQUM3RCx5QkFBNEIsQ0FFaEMsMENBQ0UsMGxIQUFzRCxDQUN0RCx5QkFBNEIsQ0FDNUIsZ0RBQ0Usc3NGQUE0RCxDQUM1RCx5QkFBNEIsQ0FFaEMsMENBQ0UsY0FBZSxDQUNmLFVBQWEsQ0FFZixzQ0FDRSxjQUFlLENBQ2YsVUFBYSxDQUVmLGtEQUNFLGlCQUFrQixDQUNsQixRQUFTLENBQ1QsV0FBWSxDQUNaLGlCQUFrQixDQUNsQixTQUFVLENBQ1YsMEJBQTJCLENBQzNCLHFDQUEyQyxDQUMzQywrQkFBb0MsQ0FDcEMsaUJBQW9CLENBQ3BCLCtFQUNFLGtCQUFtQixDQUNuQixTQUFZLENBRWhCLHNDQUNFLG1CQUFvQixDQUNwQixpQkFBa0IsQ0FDbEIsUUFBUyxDQUNULFdBQVksQ0FDWixpQkFBa0IsQ0FDbEIsU0FBVSxDQUNWLDBCQUEyQixDQUMzQixxREFBMkQsQ0FDM0QsK0JBQW9DLENBQ3BDLGlCQUFvQixDQUV0QixpQ0FDRSxvQkFBcUIsQ0FDckIsZ0JBQWlCLENBQ2pCLGNBQWUsQ0FDZixrQkFBbUIsQ0FDbkIsVUFBYyxDQUVoQiw2Q0FDRSxhQUFnQixDQUVsQixrREFDRSxhQUFjLENBQ2QsV0FBWSxDQUNaLFFBQVMsQ0FDVCxjQUFlLENBQ2YsY0FBZSxDQUNmLGNBQWUsQ0FDZixpQkFBa0IsQ0FDbEIsVUFBVyxDQUNYLHdCQUErQixDQUMvQix5Q0FBaUQsQ0FDakQsd0RBQ0UsbUNBQTRDLENBQzlDLHdEQUNFLFlBQWUsQ0FDakIscUZBQ0UsYUFBZ0IsQ0FFcEIsa0RBQ0UsaUJBQWtCLENBQ2xCLFFBQVMsQ0FDVCxXQUFZLENBQ1osaUJBQWtCLENBQ2xCLFNBQVUsQ0FDViwwQ0FBMkMsQ0FDM0MscUNBQTJDLENBQzNDLCtCQUFvQyxDQUNwQyxpQkFBa0IsQ0FDbEIsWUFBYSxDQUNiLFVBQVcsQ0FDWCxlQUFrQixDQUNsQixtRkFDRSxrQkFBbUIsQ0FDbkIsU0FBWSxDQUVoQiw2Q0FDRSxjQUFlLENBQ2YsaUJBQWtCLENBQ2xCLFFBQVMsQ0FDVCxXQUFZLENBQ1osVUFBVyxDQUNYLGVBQWtCLENBRXBCLGtEQUNFLGlCQUFrQixDQUNsQixNQUFPLENBQ1AsS0FBTSxDQUNOLFVBQVcsQ0FDWCxXQUFZLENBQ1osZ0JBQWlCLENBQ2pCLGlCQUFrQixDQUNsQixVQUFXLENBQ1gsY0FBaUIsQ0FFbkIsb0RBQ0UsaUJBQWtCLENBQ2xCLFFBQVMsQ0FDVCxRQUFTLENBQ1QsVUFBVyxDQUNYLFdBQVksQ0FDWixrQkFBbUIsQ0FDbkIsZ0JBQWlCLENBQ2pCLGVBQWtCLENBQ2xCLDJEQUNFLFlBQWEsQ0FDYixlQUFrQixDQUNwQiwwREFDRSxVQUFXLENBQ1gsNkJBQXNDLENBQ3hDLHFIQUNFLFVBQVcsQ0FDWCxpQkFBa0IsQ0FDbEIsYUFBYyxDQUNkLFFBQVMsQ0FDVCxTQUFVLENBQ1YsZ0JBQWlCLENBQ2pCLFdBQWMsQ0FFbEIsa0VBQ0UsV0FBYyxDQUVoQix3RUFDRSw2Q0FBaUQsQ0FFbkQsaUVBQ0Usa0JBQXFCLENBRXZCLHNFQUNFLHdCQUEyQiIsImZpbGUiOiJzdHlsZS5zY3NzIiwic291cmNlc0NvbnRlbnQiOlsiQGtleWZyYW1lcyByb3RhdGlvbiB7XG4gIGZyb20ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7IH1cbiAgdG8ge1xuICAgIC13ZWJraXQtdHJhbnNmb3JtOiByb3RhdGUoMzYwZGVnKTsgfSB9XG5cbkBrZXlmcmFtZXMgbWFnZW50YVB1bHNlIHtcbiAgZnJvbSB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogIzYzMDAzMDtcbiAgICAtd2Via2l0LWJveC1zaGFkb3c6IDAgMCA5cHggIzMzMzsgfVxuICA1MCUge1xuICAgIGJhY2tncm91bmQtY29sb3I6ICNhOTAxNGI7XG4gICAgLXdlYmtpdC1ib3gtc2hhZG93OiAwIDAgMThweCAjYTkwMTRiOyB9XG4gIHRvIHtcbiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjNjMwMDMwO1xuICAgIC13ZWJraXQtYm94LXNoYWRvdzogMCAwIDlweCAjMzMzOyB9IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICB3aWR0aDogMTZweDtcbiAgaGVpZ2h0OiAxNnB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcG9zdGVyIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAxMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDEwMCU7XG4gIHdpZHRoOiAxMDAlO1xuICBiYWNrZ3JvdW5kLXBvc2l0aW9uOiBjZW50ZXIgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXJlcGVhdDogbm8tcmVwZWF0O1xuICBiYWNrZ3JvdW5kLXNpemU6IGNvbnRhaW47XG4gIHBvaW50ZXItZXZlbnRzOiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWcge1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIGhlaWdodDogMTAwJTtcbiAgd2lkdGg6IDEwMCU7XG4gIGJhY2tncm91bmQ6IHJnYmEoMCwgMCwgMCwgMC40KTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXBsYXktYmlnOmFmdGVyIHtcbiAgICBjdXJzb3I6IHBvaW50ZXI7XG4gICAgY29udGVudDogJyc7XG4gICAgcG9zaXRpb246IGFic29sdXRlO1xuICAgIGxlZnQ6IDUwJTtcbiAgICB0b3A6IDUwJTtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICB3aWR0aDogNDhweDtcbiAgICBoZWlnaHQ6IDQ4cHg7XG4gICAgYmFja2dyb3VuZC1pbWFnZTogdXJsKFwiLi4vYXNzZXRzL3BsYXkucG5nXCIpO1xuICAgIGJhY2tncm91bmQtcmVwZWF0OiBuby1yZXBlYXQ7XG4gICAgYmFja2dyb3VuZC1wb3NpdGlvbjogY2VudGVyOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcGxheS1iaWc6aG92ZXI6YWZ0ZXIge1xuICAgIGJhY2tncm91bmQtaW1hZ2U6IHVybChcIi4uL2Fzc2V0cy9wbGF5LWhvdmVyLnBuZ1wiKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyB7XG4gIGRpc3BsYXk6IG5vbmU7XG4gIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgbGVmdDogNTAlO1xuICB0b3A6IDA7XG4gIHBhZGRpbmc6IDAgM3B4O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIGp1c3RpZnktY29udGVudDogc3BhY2UtYXJvdW5kO1xuICBhbGlnbi1pdGVtczogY2VudGVyO1xuICB3aWR0aDogOTVweDtcbiAgaGVpZ2h0OiAyMHB4O1xuICBiYWNrZ3JvdW5kOiAjMDAwMDAwO1xuICBvcGFjaXR5OiAxO1xuICBib3JkZXItcmFkaXVzOiAwcHggMHB4IDhweCA4cHg7XG4gIHotaW5kZXg6IDE7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1yZWNvcmRpbmcgLmplc3NpYnVjYS1yZWNvcmRpbmctcmVkLXBvaW50IHtcbiAgICB3aWR0aDogOHB4O1xuICAgIGhlaWdodDogOHB4O1xuICAgIGJhY2tncm91bmQ6ICNGRjFGMUY7XG4gICAgYm9yZGVyLXJhZGl1czogNTAlO1xuICAgIGFuaW1hdGlvbjogbWFnZW50YVB1bHNlIDFzIGxpbmVhciBpbmZpbml0ZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLXJlY29yZGluZy10aW1lIHtcbiAgICBmb250LXNpemU6IDE0cHg7XG4gICAgZm9udC13ZWlnaHQ6IDUwMDtcbiAgICBjb2xvcjogI0RERERERDsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXJlY29yZGluZyAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gICAgd2lkdGg6IDE2cHg7XG4gICAgaGVpZ2h0OiAxNnB4O1xuICAgIGN1cnNvcjogcG9pbnRlcjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmcge1xuICBkaXNwbGF5OiBub25lO1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGNlbnRlcjtcbiAgYWxpZ24taXRlbXM6IGNlbnRlcjtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB6LWluZGV4OiAyMDtcbiAgbGVmdDogMDtcbiAgdG9wOiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICB3aWR0aDogMTAwJTtcbiAgaGVpZ2h0OiAxMDAlO1xuICBwb2ludGVyLWV2ZW50czogbm9uZTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWxvYWRpbmctdGV4dCB7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICBmb250LXNpemU6IDEzcHg7XG4gIGNvbG9yOiAjZmZmO1xuICBtYXJnaW4tdG9wOiAxMHB4OyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBiYWNrZ3JvdW5kLWNvbG9yOiAjMTYxNjE2O1xuICBib3gtc2l6aW5nOiBib3JkZXItYm94O1xuICBkaXNwbGF5OiBmbGV4O1xuICBmbGV4LWRpcmVjdGlvbjogY29sdW1uO1xuICBqdXN0aWZ5LWNvbnRlbnQ6IGZsZXgtZW5kO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHotaW5kZXg6IDQwO1xuICBsZWZ0OiAwO1xuICByaWdodDogMDtcbiAgYm90dG9tOiAwO1xuICBoZWlnaHQ6IDM4cHg7XG4gIHdpZHRoOiAxMDAlO1xuICBwYWRkaW5nLWxlZnQ6IDEzcHg7XG4gIHBhZGRpbmctcmlnaHQ6IDEzcHg7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgY29sb3I6ICNmZmY7XG4gIG9wYWNpdHk6IDA7XG4gIHZpc2liaWxpdHk6IGhpZGRlbjtcbiAgdHJhbnNpdGlvbjogYWxsIDAuMnMgZWFzZS1pbi1vdXQ7XG4gIHVzZXItc2VsZWN0OiBub25lO1xuICB0cmFuc2l0aW9uOiB3aWR0aCAuNXMgZWFzZS1pbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbSB7XG4gICAgcG9zaXRpb246IHJlbGF0aXZlO1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBjZW50ZXI7XG4gICAgcGFkZGluZzogMCA4cHg7IH1cbiAgICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtY29udHJvbHMtaXRlbTpob3ZlciAuaWNvbi10aXRsZS10aXBzIHtcbiAgICAgIHZpc2liaWxpdHk6IHZpc2libGU7XG4gICAgICBvcGFjaXR5OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1taWNyb3Bob25lLWNsb3NlIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wbGF5IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1wYXVzZSB7XG4gICAgZGlzcGxheTogbm9uZTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi1leGl0IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1zY3JlZW5zaG90IHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1yZWNvcmQge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWZ1bGxzY3JlZW4ge1xuICAgIGRpc3BsYXk6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLXJlY29yZC1zdG9wIHtcbiAgICBkaXNwbGF5OiBub25lOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1pY29uLWF1ZGlvLCAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWNvbnRyb2xzIC5qZXNzaWJ1Y2EtaWNvbi1tdXRlIHtcbiAgICB6LWluZGV4OiAxOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20ge1xuICAgIGRpc3BsYXk6IGZsZXg7XG4gICAganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuO1xuICAgIGhlaWdodDogMTAwJTsgfVxuICAgIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtY29udHJvbHMgLmplc3NpYnVjYS1jb250cm9scy1ib3R0b20gLmplc3NpYnVjYS1jb250cm9scy1sZWZ0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG4gICAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1jb250cm9scyAuamVzc2lidWNhLWNvbnRyb2xzLWJvdHRvbSAuamVzc2lidWNhLWNvbnRyb2xzLXJpZ2h0IHtcbiAgICAgIGRpc3BsYXk6IGZsZXg7XG4gICAgICBhbGlnbi1pdGVtczogY2VudGVyOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93IC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICBvcGFjaXR5OiAxO1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1jb250cm9scy1zaG93LWF1dG8taGlkZSAuamVzc2lidWNhLWNvbnRyb2xzIHtcbiAgb3BhY2l0eTogMC44O1xuICB2aXNpYmlsaXR5OiB2aXNpYmxlO1xuICBkaXNwbGF5OiBub25lOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyLmplc3NpYnVjYS1oaWRlLWN1cnNvciAqIHtcbiAgY3Vyc29yOiBub25lICFpbXBvcnRhbnQ7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWxvYWRpbmcge1xuICB3aWR0aDogNTBweDtcbiAgaGVpZ2h0OiA1MHB4O1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbG9hZGluZy5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7XG4gIGFuaW1hdGlvbjogcm90YXRpb24gMXMgbGluZWFyIGluZmluaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3NjcmVlbnNob3QucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1zY3JlZW5zaG90OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvc2NyZWVuc2hvdC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheSB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wbGF5LnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGxheTpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BsYXktaG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXBhdXNlIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3BhdXNlLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcGF1c2U6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9wYXVzZS1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkIHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZDpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL3JlY29yZC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tcmVjb3JkU3RvcCB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLXJlY29yZFN0b3A6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9yZWNvcmQtc3RvcC1ob3Zlci5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbiB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9mdWxsc2NyZWVuLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICBiYWNrZ3JvdW5kLXNpemU6IDEwMCUgMTAwJTsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLWljb24tZnVsbHNjcmVlbjpob3ZlciB7XG4gICAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2Z1bGxzY3JlZW4taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0IHtcbiAgYmFja2dyb3VuZDogdXJsKFwiLi4vYXNzZXRzL2V4aXQtZnVsbHNjcmVlbi5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWZ1bGxzY3JlZW5FeGl0OmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvZXhpdC1mdWxsc2NyZWVuLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi1hdWRpbyB7XG4gIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9hdWRpby5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLWF1ZGlvOmhvdmVyIHtcbiAgICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvYXVkaW8taG92ZXIucG5nXCIpIG5vLXJlcGVhdCBjZW50ZXI7XG4gICAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGUge1xuICBiYWNrZ3JvdW5kOiB1cmwoXCIuLi9hc3NldHMvbXV0ZS5wbmdcIikgbm8tcmVwZWF0IGNlbnRlcjtcbiAgYmFja2dyb3VuZC1zaXplOiAxMDAlIDEwMCU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1pY29uLW11dGU6aG92ZXIge1xuICAgIGJhY2tncm91bmQ6IHVybChcIi4uL2Fzc2V0cy9tdXRlLWhvdmVyLnBuZ1wiKSBuby1yZXBlYXQgY2VudGVyO1xuICAgIGJhY2tncm91bmQtc2l6ZTogMTAwJSAxMDAlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtaWNvbi10ZXh0IHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICB3aWR0aDogMzBweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXNwZWVkIHtcbiAgZm9udC1zaXplOiAxNHB4O1xuICBjb2xvcjogI2ZmZjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXF1YWxpdHktbWVudS1saXN0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMsIG9wYWNpdHkgMzAwbXM7XG4gIGJhY2tncm91bmQtY29sb3I6IHJnYmEoMCwgMCwgMCwgMC41KTtcbiAgYm9yZGVyLXJhZGl1czogNHB4OyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWxpc3QuamVzc2lidWNhLXF1YWxpdHktbWVudS1zaG93biB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5pY29uLXRpdGxlLXRpcHMge1xuICBwb2ludGVyLWV2ZW50czogbm9uZTtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7XG4gIHRyYW5zaXRpb246IHZpc2liaWxpdHkgMzAwbXMgZWFzZSAwcywgb3BhY2l0eSAzMDBtcyBlYXNlIDBzO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuaWNvbi10aXRsZSB7XG4gIGRpc3BsYXk6IGlubGluZS1ibG9jaztcbiAgcGFkZGluZzogNXB4IDEwcHg7XG4gIGZvbnQtc2l6ZTogMTJweDtcbiAgd2hpdGUtc3BhY2U6IG5vd3JhcDtcbiAgY29sb3I6IHdoaXRlOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51IHtcbiAgcGFkZGluZzogOHB4IDA7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbSB7XG4gIGRpc3BsYXk6IGJsb2NrO1xuICBoZWlnaHQ6IDI1cHg7XG4gIG1hcmdpbjogMDtcbiAgcGFkZGluZzogMCAxMHB4O1xuICBjdXJzb3I6IHBvaW50ZXI7XG4gIGZvbnQtc2l6ZTogMTRweDtcbiAgdGV4dC1hbGlnbjogY2VudGVyO1xuICB3aWR0aDogNTBweDtcbiAgY29sb3I6IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC41KTtcbiAgdHJhbnNpdGlvbjogY29sb3IgMzAwbXMsIGJhY2tncm91bmQtY29sb3IgMzAwbXM7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbTpob3ZlciB7XG4gICAgYmFja2dyb3VuZC1jb2xvcjogcmdiYSgyNTUsIDI1NSwgMjU1LCAwLjIpOyB9XG4gIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW06Zm9jdXMge1xuICAgIG91dGxpbmU6IG5vbmU7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS1xdWFsaXR5LW1lbnUtaXRlbS5qZXNzaWJ1Y2EtcXVhbGl0eS1tZW51LWl0ZW0tYWN0aXZlIHtcbiAgICBjb2xvcjogIzIyOThGQzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC13cmFwIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiA1MCU7XG4gIGJvdHRvbTogMTAwJTtcbiAgdmlzaWJpbGl0eTogaGlkZGVuO1xuICBvcGFjaXR5OiAwO1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSkgdHJhbnNsYXRlWSgyMiUpO1xuICB0cmFuc2l0aW9uOiB2aXNpYmlsaXR5IDMwMG1zLCBvcGFjaXR5IDMwMG1zO1xuICBiYWNrZ3JvdW5kLWNvbG9yOiByZ2JhKDAsIDAsIDAsIDAuNSk7XG4gIGJvcmRlci1yYWRpdXM6IDRweDtcbiAgaGVpZ2h0OiAxMjBweDtcbiAgd2lkdGg6IDUwcHg7XG4gIG92ZXJmbG93OiBoaWRkZW47IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtd3JhcC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLXdyYXAtc2hvdyB7XG4gICAgdmlzaWJpbGl0eTogdmlzaWJsZTtcbiAgICBvcGFjaXR5OiAxOyB9XG5cbi5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsIHtcbiAgY3Vyc29yOiBwb2ludGVyO1xuICBwb3NpdGlvbjogYWJzb2x1dGU7XG4gIHRvcDogMjFweDtcbiAgaGVpZ2h0OiA2MHB4O1xuICB3aWR0aDogNTBweDtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC10ZXh0IHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICBsZWZ0OiAwO1xuICB0b3A6IDA7XG4gIHdpZHRoOiA1MHB4O1xuICBoZWlnaHQ6IDIwcHg7XG4gIGxpbmUtaGVpZ2h0OiAyMHB4O1xuICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gIGNvbG9yOiAjZmZmO1xuICBmb250LXNpemU6IDEycHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlIHtcbiAgcG9zaXRpb246IGFic29sdXRlO1xuICB0b3A6IDQ4cHg7XG4gIGxlZnQ6IDUwJTtcbiAgd2lkdGg6IDEycHg7XG4gIGhlaWdodDogMTJweDtcbiAgYm9yZGVyLXJhZGl1czogMTJweDtcbiAgbWFyZ2luLWxlZnQ6IC02cHg7XG4gIGJhY2tncm91bmQ6ICNmZmY7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUge1xuICAgIGJvdHRvbTogLTU0cHg7XG4gICAgYmFja2dyb3VuZDogI2ZmZjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lciAuamVzc2lidWNhLXZvbHVtZS1wYW5lbC1oYW5kbGU6OmFmdGVyIHtcbiAgICBib3R0b206IDZweDtcbiAgICBiYWNrZ3JvdW5kOiByZ2JhKDI1NSwgMjU1LCAyNTUsIDAuMik7IH1cbiAgLmplc3NpYnVjYS1jb250YWluZXIgLmplc3NpYnVjYS12b2x1bWUtcGFuZWwtaGFuZGxlOjpiZWZvcmUsIC5qZXNzaWJ1Y2EtY29udGFpbmVyIC5qZXNzaWJ1Y2Etdm9sdW1lLXBhbmVsLWhhbmRsZTo6YWZ0ZXIge1xuICAgIGNvbnRlbnQ6ICcnO1xuICAgIHBvc2l0aW9uOiBhYnNvbHV0ZTtcbiAgICBkaXNwbGF5OiBibG9jaztcbiAgICBsZWZ0OiA1MCU7XG4gICAgd2lkdGg6IDNweDtcbiAgICBtYXJnaW4tbGVmdDogLTFweDtcbiAgICBoZWlnaHQ6IDYwcHg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtY29udHJvbHMge1xuICB3aWR0aDogMTAwdmg7IH1cblxuLmplc3NpYnVjYS1jb250YWluZXIuamVzc2lidWNhLWZ1bGxzY3JlZW4td2ViIC5qZXNzaWJ1Y2EtcGxheS1iaWc6YWZ0ZXIge1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZSgtNTAlLCAtNTAlKSByb3RhdGUoMjcwZGVnKTsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nIHtcbiAgZmxleC1kaXJlY3Rpb246IHJvdzsgfVxuXG4uamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIgLmplc3NpYnVjYS1sb2FkaW5nLXRleHQge1xuICB0cmFuc2Zvcm06IHJvdGF0ZSgyNzBkZWcpOyB9XG4iXX0= */"; + styleInject(css_248z$1); + + // todo: 待定 + var hotkey = ((player, control) => { + const { + events: { + proxy + } + } = player; + const keys = {}; + + function addHotkey(key, event) { + if (keys[key]) { + keys[key].push(event); + } else { + keys[key] = [event]; + } + } // + + + addHotkey(HOT_KEY.esc, () => { + if (player.fullscreen) { + player.fullscreen = false; + } + }); // + + addHotkey(HOT_KEY.arrowUp, () => { + player.volume += 0.05; + }); // + + addHotkey(HOT_KEY.arrowDown, () => { + player.volume -= 0.05; + }); + proxy(window, 'keydown', event => { + if (control.isFocus) { + const tag = document.activeElement.tagName.toUpperCase(); + const editable = document.activeElement.getAttribute('contenteditable'); + + if (tag !== 'INPUT' && tag !== 'TEXTAREA' && editable !== '' && editable !== 'true') { + const events = keys[event.keyCode]; + + if (events) { + event.preventDefault(); + events.forEach(fn => fn()); + } + } + } + }); + }); + + class Control { + constructor(player) { + this.player = player; + template(player, this); + property(player, this); + observer$1(player, this); + events(player, this); + + if (player._opt.hotKey) { + hotkey(player, this); + } + + this.player.debug.log('Control', 'init'); + } + + destroy() { + if (this.$poster) { + const result = removeElement(this.$poster); + + if (!result) { + const $poster = this.player.$container.querySelector('.jessibuca-poster'); + + if ($poster && this.player.$container) { + this.player.$container.removeChild($poster); + } + } + } + + if (this.$loading) { + const result = removeElement(this.$loading); + + if (!result) { + const $loading = this.player.$container.querySelector('.jessibuca-loading'); + + if ($loading && this.player.$container) { + this.player.$container.removeChild($loading); + } + } + } + + if (this.$controls) { + const result = removeElement(this.$controls); + + if (!result) { + const $controls = this.player.$container.querySelector('.jessibuca-controls'); + + if ($controls && this.player.$container) { + this.player.$container.removeChild($controls); + } + } + } + + if (this.$recording) { + const result = removeElement(this.$recording); + + if (!result) { + const $recording = this.player.$container.querySelector('.jessibuca-recording'); + + if ($recording && this.player.$container) { + this.player.$container.removeChild($recording); + } + } + } + + if (this.$playBig) { + const result = removeElement(this.$playBig); + + if (!result) { + const $playBig = this.player.$container.querySelector('.jessibuca-play-big'); + + if ($playBig && this.player.$container) { + this.player.$container.removeChild($playBig); + } + } + } + + this.player.debug.log('control', 'destroy'); + } + + autoSize() { + const player = this.player; + player.$container.style.padding = '0 0'; + const playerWidth = player.width; + const playerHeight = player.height; + const playerRatio = playerWidth / playerHeight; + const canvasWidth = player.video.$videoElement.width; + const canvasHeight = player.video.$videoElement.height; + const canvasRatio = canvasWidth / canvasHeight; + + if (playerRatio > canvasRatio) { + const padding = (playerWidth - playerHeight * canvasRatio) / 2; + player.$container.style.padding = `0 ${padding}px`; + } else { + const padding = (playerHeight - playerWidth / canvasRatio) / 2; + player.$container.style.padding = `${padding}px 0`; + } + } + + } + + var css_248z = ".jessibuca-container{position:relative;display:block;width:100%;height:100%;overflow:hidden}.jessibuca-container.jessibuca-fullscreen-web{position:fixed;z-index:9999;left:0;top:0;right:0;bottom:0;width:100vw!important;height:100vh!important;background:#000}\n/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInN0eWxlLnNjc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEscUJBQ0UsaUJBQWtCLENBQ2xCLGFBQWMsQ0FDZCxVQUFXLENBQ1gsV0FBWSxDQUNaLGVBQWtCLENBQ2xCLDhDQUNFLGNBQWUsQ0FDZixZQUFhLENBQ2IsTUFBTyxDQUNQLEtBQU0sQ0FDTixPQUFRLENBQ1IsUUFBUyxDQUNULHFCQUF1QixDQUN2QixzQkFBd0IsQ0FDeEIsZUFBa0IiLCJmaWxlIjoic3R5bGUuc2NzcyIsInNvdXJjZXNDb250ZW50IjpbIi5qZXNzaWJ1Y2EtY29udGFpbmVyIHtcbiAgcG9zaXRpb246IHJlbGF0aXZlO1xuICBkaXNwbGF5OiBibG9jaztcbiAgd2lkdGg6IDEwMCU7XG4gIGhlaWdodDogMTAwJTtcbiAgb3ZlcmZsb3c6IGhpZGRlbjsgfVxuICAuamVzc2lidWNhLWNvbnRhaW5lci5qZXNzaWJ1Y2EtZnVsbHNjcmVlbi13ZWIge1xuICAgIHBvc2l0aW9uOiBmaXhlZDtcbiAgICB6LWluZGV4OiA5OTk5O1xuICAgIGxlZnQ6IDA7XG4gICAgdG9wOiAwO1xuICAgIHJpZ2h0OiAwO1xuICAgIGJvdHRvbTogMDtcbiAgICB3aWR0aDogMTAwdncgIWltcG9ydGFudDtcbiAgICBoZWlnaHQ6IDEwMHZoICFpbXBvcnRhbnQ7XG4gICAgYmFja2dyb3VuZDogIzAwMDsgfVxuIl19 */"; + styleInject(css_248z); + + var observer = (player => { + const { + _opt, + debug, + events: { + proxy + } + } = player; + + if (_opt.supportDblclickFullscreen) { + proxy(player.$container, 'dblclick', e => { + const target = getTarget(e); + const nodeName = target.nodeName.toLowerCase(); + + if (nodeName === 'canvas' || nodeName === 'video') { + player.fullscreen = !player.fullscreen; + } + }); + } // + + + proxy(document, 'visibilitychange', () => { + if (_opt.hiddenAutoPause) { + debug.log('visibilitychange', document.visibilityState, player._isPlayingBeforePageHidden); + + if ("visible" === document.visibilityState) { + if (player._isPlayingBeforePageHidden) { + player.play(); + } + } else { + player._isPlayingBeforePageHidden = player.playing; // hidden + + if (player.playing) { + player.pause(); + } + } + } + }); + proxy(window, 'fullscreenchange', () => { + // + if (player.keepScreenOn !== null && "visible" === document.visibilityState) { + player.enableWakeLock(); + } + }); + }); + + class MP4$1 { + static init() { + MP4$1.types = { + avc1: [], + avcC: [], + hvc1: [], + hvcC: [], + btrt: [], + dinf: [], + dref: [], + esds: [], + ftyp: [], + hdlr: [], + mdat: [], + mdhd: [], + mdia: [], + mfhd: [], + minf: [], + moof: [], + moov: [], + mp4a: [], + mvex: [], + mvhd: [], + sdtp: [], + stbl: [], + stco: [], + stsc: [], + stsd: [], + stsz: [], + stts: [], + tfdt: [], + tfhd: [], + traf: [], + trak: [], + trun: [], + trex: [], + tkhd: [], + vmhd: [], + smhd: [] + }; + + for (let name in MP4$1.types) { + if (MP4$1.types.hasOwnProperty(name)) { + MP4$1.types[name] = [name.charCodeAt(0), name.charCodeAt(1), name.charCodeAt(2), name.charCodeAt(3)]; + } + } + + let constants = MP4$1.constants = {}; + constants.FTYP = new Uint8Array([0x69, 0x73, 0x6F, 0x6D, // major_brand: isom + 0x0, 0x0, 0x0, 0x1, // minor_version: 0x01 + 0x69, 0x73, 0x6F, 0x6D, // isom + 0x61, 0x76, 0x63, 0x31 // avc1 + ]); + constants.STSD_PREFIX = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x01 // entry_count + ]); + constants.STTS = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + constants.STSC = constants.STCO = constants.STTS; + constants.STSZ = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00 // sample_count + ]); + constants.HDLR_VIDEO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x69, 0x64, 0x65, 0x6F, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: VideoHandler + ]); + constants.HDLR_AUDIO = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6F, 0x75, 0x6E, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved: 3 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x6F, 0x75, 0x6E, 0x64, 0x48, 0x61, 0x6E, 0x64, 0x6C, 0x65, 0x72, 0x00 // name: SoundHandler + ]); + constants.DREF = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0C, // entry_size + 0x75, 0x72, 0x6C, 0x20, // type 'url ' + 0x00, 0x00, 0x00, 0x01 // version(0) + flags + ]); // Sound media header + + constants.SMHD = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00 // balance(2) + reserved(2) + ]); // video media header + + constants.VMHD = new Uint8Array([0x00, 0x00, 0x00, 0x01, // version(0) + flags + 0x00, 0x00, // graphicsmode: 2 bytes + 0x00, 0x00, 0x00, 0x00, // opcolor: 3 * 2 bytes + 0x00, 0x00]); + } // Generate a box + + + static box(type) { + let size = 8; + let result = null; + let datas = Array.prototype.slice.call(arguments, 1); + let arrayCount = datas.length; + + for (let i = 0; i < arrayCount; i++) { + size += datas[i].byteLength; + } + + result = new Uint8Array(size); + result[0] = size >>> 24 & 0xFF; // size + + result[1] = size >>> 16 & 0xFF; + result[2] = size >>> 8 & 0xFF; + result[3] = size & 0xFF; + result.set(type, 4); // type + + let offset = 8; + + for (let i = 0; i < arrayCount; i++) { + // data body + result.set(datas[i], offset); + offset += datas[i].byteLength; + } + + return result; + } // emit ftyp & moov + + + static generateInitSegment(meta) { + let ftyp = MP4$1.box(MP4$1.types.ftyp, MP4$1.constants.FTYP); + let moov = MP4$1.moov(meta); + let result = new Uint8Array(ftyp.byteLength + moov.byteLength); + result.set(ftyp, 0); + result.set(moov, ftyp.byteLength); + return result; + } // Movie metadata box + + + static moov(meta) { + let mvhd = MP4$1.mvhd(meta.timescale, meta.duration); + let trak = MP4$1.trak(meta); + let mvex = MP4$1.mvex(meta); + return MP4$1.box(MP4$1.types.moov, mvhd, trak, mvex); + } // Movie header box + + + static mvhd(timescale, duration) { + return MP4$1.box(MP4$1.types.mvhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + timescale >>> 24 & 0xFF, // timescale: 4 bytes + timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x01, 0x00, 0x00, // Preferred rate: 1.0 + 0x01, 0x00, 0x00, 0x00, // PreferredVolume(1.0, 2bytes) + reserved(2bytes) + 0x00, 0x00, 0x00, 0x00, // reserved: 4 + 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- + 0x00, 0x00, 0x00, 0x00, // ----begin pre_defined 6 * 4 bytes---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ----end pre_defined 6 * 4 bytes---- + 0xFF, 0xFF, 0xFF, 0xFF // next_track_ID + ])); + } // Track box + + + static trak(meta) { + return MP4$1.box(MP4$1.types.trak, MP4$1.tkhd(meta), MP4$1.mdia(meta)); + } // Track header box + + + static tkhd(meta) { + let trackId = meta.id, + duration = meta.duration; + let width = meta.presentWidth, + height = meta.presentHeight; + return MP4$1.box(MP4$1.types.tkhd, new Uint8Array([0x00, 0x00, 0x00, 0x07, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + trackId >>> 24 & 0xFF, // track_ID: 4 bytes + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 4 bytes + duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // layer(2bytes) + alternate_group(2bytes) + 0x00, 0x00, 0x00, 0x00, // volume(2bytes) + reserved(2bytes) + 0x00, 0x01, 0x00, 0x00, // ----begin composition matrix---- + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, // ----end composition matrix---- + width >>> 8 & 0xFF, // width and height + width & 0xFF, 0x00, 0x00, height >>> 8 & 0xFF, height & 0xFF, 0x00, 0x00])); + } + + static mdia(meta) { + return MP4$1.box(MP4$1.types.mdia, MP4$1.mdhd(meta), MP4$1.hdlr(meta), MP4$1.minf(meta)); + } // Media header box + + + static mdhd(meta) { + let timescale = meta.timescale; + let duration = meta.duration; + return MP4$1.box(MP4$1.types.mdhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + timescale >>> 24 & 0xFF, // timescale: 4 bytes + timescale >>> 16 & 0xFF, timescale >>> 8 & 0xFF, timescale & 0xFF, duration >>> 24 & 0xFF, // duration: 4 bytes + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, 0x55, 0xC4, // language: und (undetermined) + 0x00, 0x00 // pre_defined = 0 + ])); + } // Media handler reference box + + + static hdlr(meta) { + let data = null; + + if (meta.type === 'audio') { + data = MP4$1.constants.HDLR_AUDIO; + } else { + data = MP4$1.constants.HDLR_VIDEO; + } + + return MP4$1.box(MP4$1.types.hdlr, data); + } // Media infomation box + + + static minf(meta) { + let xmhd = null; + + if (meta.type === 'audio') { + xmhd = MP4$1.box(MP4$1.types.smhd, MP4$1.constants.SMHD); + } else { + xmhd = MP4$1.box(MP4$1.types.vmhd, MP4$1.constants.VMHD); + } + + return MP4$1.box(MP4$1.types.minf, xmhd, MP4$1.dinf(), MP4$1.stbl(meta)); + } // Data infomation box + + + static dinf() { + let result = MP4$1.box(MP4$1.types.dinf, MP4$1.box(MP4$1.types.dref, MP4$1.constants.DREF)); + return result; + } // Sample table box + + + static stbl(meta) { + let result = MP4$1.box(MP4$1.types.stbl, // type: stbl + MP4$1.stsd(meta), // Sample Description Table + MP4$1.box(MP4$1.types.stts, MP4$1.constants.STTS), // Time-To-Sample + MP4$1.box(MP4$1.types.stsc, MP4$1.constants.STSC), // Sample-To-Chunk + MP4$1.box(MP4$1.types.stsz, MP4$1.constants.STSZ), // Sample size + MP4$1.box(MP4$1.types.stco, MP4$1.constants.STCO) // Chunk offset + ); + return result; + } // Sample description box + + + static stsd(meta) { + if (meta.type === 'audio') { + // else: aac -> mp4a + return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.mp4a(meta)); + } else { + if (meta.videoType === 'avc') { + // + return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.avc1(meta)); + } else { + // + return MP4$1.box(MP4$1.types.stsd, MP4$1.constants.STSD_PREFIX, MP4$1.hvc1(meta)); + } + } + } + + static mp4a(meta) { + let channelCount = meta.channelCount; + let sampleRate = meta.audioSampleRate; + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // reserved(4) + 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) + 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, 0x00, channelCount, // channelCount(2) + 0x00, 0x10, // sampleSize(2) + 0x00, 0x00, 0x00, 0x00, // reserved(4) + sampleRate >>> 8 & 0xFF, // Audio sample rate + sampleRate & 0xFF, 0x00, 0x00]); + return MP4$1.box(MP4$1.types.mp4a, data, MP4$1.esds(meta)); + } + + static esds(meta) { + let config = meta.config || []; + let configSize = config.length; + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version 0 + flags + 0x03, // descriptor_type + 0x17 + configSize, // length3 + 0x00, 0x01, // es_id + 0x00, // stream_priority + 0x04, // descriptor_type + 0x0F + configSize, // length + 0x40, // codec: mpeg4_audio + 0x15, // stream_type: Audio + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + 0x05 // descriptor_type + ].concat([configSize]).concat(config).concat([0x06, 0x01, 0x02 // GASpecificConfig + ])); + return MP4$1.box(MP4$1.types.esds, data); + } // avc + + + static avc1(meta) { + let avcc = meta.avcc; + const width = meta.codecWidth; + const height = meta.codecHeight; + let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]); + return MP4$1.box(MP4$1.types.avc1, data, MP4$1.box(MP4$1.types.avcC, avcc)); + } // hvc + + + static hvc1(meta) { + let avcc = meta.avcc; + const width = meta.codecWidth; + const height = meta.codecHeight; + let data = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width >>> 8 & 255, width & 255, height >>> 8 & 255, height & 255, 0, 72, 0, 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, 255, 255]); + return MP4$1.box(MP4$1.types.hvc1, data, MP4$1.box(MP4$1.types.hvcC, avcc)); + } // Movie Extends box + + + static mvex(meta) { + return MP4$1.box(MP4$1.types.mvex, MP4$1.trex(meta)); + } // Track Extends box + + + static trex(meta) { + let trackId = meta.id; + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) + flags + trackId >>> 24 & 0xFF, // track_ID + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF, 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ]); + return MP4$1.box(MP4$1.types.trex, data); + } // Movie fragment box + + + static moof(track, baseMediaDecodeTime) { + return MP4$1.box(MP4$1.types.moof, MP4$1.mfhd(track.sequenceNumber), MP4$1.traf(track, baseMediaDecodeTime)); + } // + + + static mfhd(sequenceNumber) { + let data = new Uint8Array([0x00, 0x00, 0x00, 0x00, sequenceNumber >>> 24 & 0xFF, // sequence_number: int32 + sequenceNumber >>> 16 & 0xFF, sequenceNumber >>> 8 & 0xFF, sequenceNumber & 0xFF]); + return MP4$1.box(MP4$1.types.mfhd, data); + } // Track fragment box + + + static traf(track, baseMediaDecodeTime) { + let trackId = track.id; // Track fragment header box + + let tfhd = MP4$1.box(MP4$1.types.tfhd, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags + trackId >>> 24 & 0xFF, // track_ID + trackId >>> 16 & 0xFF, trackId >>> 8 & 0xFF, trackId & 0xFF])); // Track Fragment Decode Time + + let tfdt = MP4$1.box(MP4$1.types.tfdt, new Uint8Array([0x00, 0x00, 0x00, 0x00, // version(0) & flags + baseMediaDecodeTime >>> 24 & 0xFF, // baseMediaDecodeTime: int32 + baseMediaDecodeTime >>> 16 & 0xFF, baseMediaDecodeTime >>> 8 & 0xFF, baseMediaDecodeTime & 0xFF])); + let sdtp = MP4$1.sdtp(track); + let trun = MP4$1.trun(track, sdtp.byteLength + 16 + 16 + 8 + 16 + 8 + 8); + return MP4$1.box(MP4$1.types.traf, tfhd, tfdt, trun, sdtp); + } // Sample Dependency Type box + + + static sdtp(track) { + let data = new Uint8Array(4 + 1); + let flags = track.flags; + data[4] = flags.isLeading << 6 | flags.dependsOn << 4 | flags.isDependedOn << 2 | flags.hasRedundancy; + return MP4$1.box(MP4$1.types.sdtp, data); + } // trun + + + static trun(track, offset) { + let dataSize = 12 + 16; + let data = new Uint8Array(dataSize); + offset += 8 + dataSize; + data.set([0x00, 0x00, 0x0F, 0x01, // version(0) & flags + 0x00, 0x00, 0x00, 0x01, // sample_count + offset >>> 24 & 0xFF, // data_offset + offset >>> 16 & 0xFF, offset >>> 8 & 0xFF, offset & 0xFF], 0); + let duration = track.duration; + let size = track.size; + let flags = track.flags; + let cts = track.cts; + data.set([duration >>> 24 & 0xFF, // sample_duration + duration >>> 16 & 0xFF, duration >>> 8 & 0xFF, duration & 0xFF, size >>> 24 & 0xFF, // sample_size + size >>> 16 & 0xFF, size >>> 8 & 0xFF, size & 0xFF, flags.isLeading << 2 | flags.dependsOn, // sample_flags + flags.isDependedOn << 6 | flags.hasRedundancy << 4 | flags.isNonSync, 0x00, 0x00, // sample_degradation_priority + cts >>> 24 & 0xFF, // sample_composition_time_offset + cts >>> 16 & 0xFF, cts >>> 8 & 0xFF, cts & 0xFF], 12); + return MP4$1.box(MP4$1.types.trun, data); + } // mdat + + + static mdat(data) { + return MP4$1.box(MP4$1.types.mdat, data); + } + + } + + MP4$1.init(); + + class MseDecoder extends Emitter { + constructor(player) { + super(); + this.player = player; + this.isAvc = true; + this.mediaSource = new window.MediaSource(); + this.sourceBuffer = null; + this.hasInit = false; + this.isInitInfo = false; + this.cacheTrack = {}; + this.timeInit = false; + this.sequenceNumber = 0; + this.mediaSourceOpen = false; + this.dropping = false; + this.firstRenderTime = null; + this.mediaSourceAppendBufferError = false; + this.mediaSourceAppendBufferFull = false; + this.isDecodeFirstIIframe = false; + this.player.video.$videoElement.src = window.URL.createObjectURL(this.mediaSource); + const { + debug, + events: { + proxy + } + } = player; + proxy(this.mediaSource, 'sourceopen', () => { + this.mediaSourceOpen = true; + this.player.emit(EVENTS.mseSourceOpen); + }); + proxy(this.mediaSource, 'sourceclose', () => { + this.player.emit(EVENTS.mseSourceClose); + }); + player.debug.log('MediaSource', 'init'); + } + + destroy() { + this.stop(); + this.mediaSource = null; + this.mediaSourceOpen = false; + this.sourceBuffer = null; + this.hasInit = false; + this.isInitInfo = false; + this.sequenceNumber = 0; + this.cacheTrack = null; + this.timeInit = false; + this.mediaSourceAppendBufferError = false; + this.mediaSourceAppendBufferFull = false; + this.isDecodeFirstIIframe = false; + this.off(); + this.player.debug.log('MediaSource', 'destroy'); + } + + get state() { + return this.mediaSource && this.mediaSource.readyState; + } + + get isStateOpen() { + return this.state === MEDIA_SOURCE_STATE.open; + } + + get isStateClosed() { + return this.state === MEDIA_SOURCE_STATE.closed; + } + + get isStateEnded() { + return this.state === MEDIA_SOURCE_STATE.ended; + } + + get duration() { + return this.mediaSource && this.mediaSource.duration; + } + + set duration(duration) { + this.mediaSource.duration = duration; + } + + decodeVideo(payload, ts, isIframe, cts) { + const player = this.player; + + if (!player) { + return; + } + + if (!this.hasInit) { + if (isIframe && payload[1] === 0) { + const videoCodec = payload[0] & 0x0F; + player.video.updateVideoInfo({ + encTypeCode: videoCodec + }); // 如果解码出来的是 + + if (videoCodec === VIDEO_ENC_CODE.h265) { + this.emit(EVENTS_ERROR.mediaSourceH265NotSupport); + return; + } + + if (!player._times.decodeStart) { + player._times.decodeStart = now(); + } + + this._decodeConfigurationRecord(payload, ts, isIframe, videoCodec); + + this.hasInit = true; + } + } else { + if (isIframe && payload[1] === 0) { + let config = parseAVCDecoderConfigurationRecord(payload.slice(5)); + const videoInfo = this.player.video.videoInfo; + + if (videoInfo && videoInfo.width && videoInfo.height && config && config.codecWidth && config.codecHeight && (config.codecWidth !== videoInfo.width || config.codecHeight !== videoInfo.height)) { + this.player.debug.warn('MediaSource', `width or height is update, width ${videoInfo.width}-> ${config.codecWidth}, height ${videoInfo.height}-> ${config.codecHeight}`); + this.isInitInfo = false; + this.player.video.init = false; + } + } + + if (!this.isDecodeFirstIIframe && isIframe) { + this.isDecodeFirstIIframe = true; + } + + if (this.isDecodeFirstIIframe) { + if (this.firstRenderTime === null) { + this.firstRenderTime = ts; + } + + const dts = ts - this.firstRenderTime; + + this._decodeVideo(payload, dts, isIframe, cts); + } else { + this.player.debug.warn('MediaSource', 'decodeVideo isDecodeFirstIIframe false'); + } + } + } + + _decodeConfigurationRecord(payload, ts, isIframe, videoCodec) { + let data = payload.slice(5); + let config = {}; + config = parseAVCDecoderConfigurationRecord(data); + const metaData = { + id: 1, + // video tag data + type: 'video', + timescale: 1000, + duration: 0, + avcc: data, + codecWidth: config.codecWidth, + codecHeight: config.codecHeight, + videoType: config.videoType + }; // ftyp + + const metaBox = MP4$1.generateInitSegment(metaData); + this.isAvc = true; + this.appendBuffer(metaBox.buffer); + this.sequenceNumber = 0; + this.cacheTrack = null; + this.timeInit = false; + } // + + + _decodeVideo(payload, dts, isIframe, cts) { + const player = this.player; + let arrayBuffer = payload.slice(5); + let bytes = arrayBuffer.byteLength; // player.debug.log('MediaSource', '_decodeVideo', ts); + + const $video = player.video.$videoElement; + const videoBufferDelay = player._opt.videoBufferDelay; + + if ($video.buffered.length > 1) { + this.removeBuffer($video.buffered.start(0), $video.buffered.end(0)); + this.timeInit = false; + } + + if (this.dropping && dts - this.cacheTrack.dts > videoBufferDelay) { + this.dropping = false; + this.cacheTrack = {}; + } else if (this.cacheTrack && dts >= this.cacheTrack.dts) { + // 需要额外加8个size + let mdatBytes = 8 + this.cacheTrack.size; + let mdatbox = new Uint8Array(mdatBytes); + mdatbox[0] = mdatBytes >>> 24 & 255; + mdatbox[1] = mdatBytes >>> 16 & 255; + mdatbox[2] = mdatBytes >>> 8 & 255; + mdatbox[3] = mdatBytes & 255; + mdatbox.set(MP4$1.types.mdat, 4); + mdatbox.set(this.cacheTrack.data, 8); + this.cacheTrack.duration = dts - this.cacheTrack.dts; // moof + + let moofbox = MP4$1.moof(this.cacheTrack, this.cacheTrack.dts); + let result = new Uint8Array(moofbox.byteLength + mdatbox.byteLength); + result.set(moofbox, 0); + result.set(mdatbox, moofbox.byteLength); // appendBuffer + + this.appendBuffer(result.buffer); + player.handleRender(); + player.updateStats({ + fps: true, + ts: dts, + buf: player.demux && player.demux.delay || 0 + }); + + if (!player._times.videoStart) { + player._times.videoStart = now(); + player.handlePlayToRenderTimes(); + } + } else { + player.debug.log('MediaSource', 'timeInit set false , cacheTrack = {}'); + this.timeInit = false; + this.cacheTrack = {}; + } + + if (!this.cacheTrack) { + this.cacheTrack = {}; + } + + this.cacheTrack.id = 1; + this.cacheTrack.sequenceNumber = ++this.sequenceNumber; + this.cacheTrack.size = bytes; + this.cacheTrack.dts = dts; + this.cacheTrack.cts = cts; + this.cacheTrack.isKeyframe = isIframe; + this.cacheTrack.data = arrayBuffer; // + + this.cacheTrack.flags = { + isLeading: 0, + dependsOn: isIframe ? 2 : 1, + isDependedOn: isIframe ? 1 : 0, + hasRedundancy: 0, + isNonSync: isIframe ? 0 : 1 + }; // + + if (!this.timeInit && $video.buffered.length === 1) { + player.debug.log('MediaSource', 'timeInit set true'); + this.timeInit = true; + $video.currentTime = $video.buffered.end(0); + } + + if (!this.isInitInfo && $video.videoWidth > 0 && $video.videoHeight > 0) { + player.debug.log('MediaSource', `updateVideoInfo: ${$video.videoWidth},${$video.videoHeight}`); + player.video.updateVideoInfo({ + width: $video.videoWidth, + height: $video.videoHeight + }); + player.video.initCanvasViewSize(); + this.isInitInfo = true; + } + } + + appendBuffer(buffer) { + const { + debug, + events: { + proxy + } + } = this.player; + + if (this.sourceBuffer === null) { + this.sourceBuffer = this.mediaSource.addSourceBuffer(MP4_CODECS.avc); + proxy(this.sourceBuffer, 'error', error => { + this.player.emit(EVENTS.mseSourceBufferError, error); // this.dropSourceBuffer(false) + }); + } + + if (this.mediaSourceAppendBufferError) { + debug.error('MediaSource', `this.mediaSourceAppendBufferError is true`); + return; + } + + if (this.mediaSourceAppendBufferFull) { + debug.error('MediaSource', `this.mediaSourceAppendBufferFull is true`); + return; + } + + if (this.sourceBuffer.updating === false && this.isStateOpen) { + try { + this.sourceBuffer.appendBuffer(buffer); + } catch (e) { + debug.warn('MediaSource', 'this.sourceBuffer.appendBuffer()', e.code, e); + + if (e.code === 22) { + // QuotaExceededError + // The SourceBuffer is full, and cannot free space to append additional buffers + this.stop(); + this.mediaSourceAppendBufferFull = true; + this.emit(EVENTS_ERROR.mediaSourceFull); + } else if (e.code === 11) { + // Failed to execute 'appendBuffer' on 'SourceBuffer': The HTMLMediaElement.error attribute is not null. + this.stop(); + this.mediaSourceAppendBufferError = true; + this.emit(EVENTS_ERROR.mediaSourceAppendBufferError); + } else { + debug.error('MediaSource', 'appendBuffer error', e); + this.player.emit(EVENTS.mseSourceBufferError, e); + } + } + + return; + } + + if (this.isStateClosed) { + this.player.emitError(EVENTS_ERROR.mseSourceBufferError, 'mediaSource is not attached to video or mediaSource is closed'); + } else if (this.isStateEnded) { + this.player.emitError(EVENTS_ERROR.mseSourceBufferError, 'mediaSource is closed'); + } else { + if (this.sourceBuffer.updating === true) { + this.player.emit(EVENTS.mseSourceBufferBusy); // this.dropSourceBuffer(true); + } + } + } + + stop() { + this.abortSourceBuffer(); + this.removeSourceBuffer(); + this.endOfStream(); + } + + dropSourceBuffer(isDropping) { + const $video = this.player.video.$videoElement; + this.dropping = isDropping; + + if ($video.buffered.length > 0) { + if ($video.buffered.end(0) - $video.currentTime > 1) { + this.player.debug.warn('MediaSource', 'dropSourceBuffer', `$video.buffered.end(0) is ${$video.buffered.end(0)} - $video.currentTime ${$video.currentTime}`); + $video.currentTime = $video.buffered.end(0); + } + } + } + + removeBuffer(start, end) { + if (this.isStateOpen && this.sourceBuffer.updating === false) { + try { + this.sourceBuffer.remove(start, end); + } catch (e) { + this.player.debug.warn('MediaSource', 'removeBuffer() error', e); + } + } else { + this.player.debug.warn('MediaSource', 'removeBuffer() this.isStateOpen is', this.isStateOpen, 'this.sourceBuffer.updating', this.sourceBuffer.updating); + } + } + + endOfStream() { + // fix: MediaSource endOfStream before demuxer initialization completes (before HAVE_METADATA) is treated as an error + const $videoElement = this.player.video && this.player.video.$videoElement; + + if (this.isStateOpen && $videoElement && $videoElement.readyState >= 1) { + try { + this.mediaSource.endOfStream(); + } catch (e) { + this.player.debug.warn('MediaSource', 'endOfStream() error', e); + } + } + } + + abortSourceBuffer() { + if (this.isStateOpen) { + if (this.sourceBuffer) { + this.sourceBuffer.abort(); + this.sourceBuffer = null; + } + } + } + + removeSourceBuffer() { + if (!this.isStateClosed) { + if (this.mediaSource && this.sourceBuffer) { + try { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } catch (e) { + this.player.debug.warn('MediaSource', 'removeSourceBuffer() error', e); + } + } + } + } + + getSourceBufferUpdating() { + return this.sourceBuffer && this.sourceBuffer.updating; + } + + } + + // tks: https://github.com/richtr/NoSleep.js + const WEBM = "data:video/webm;base64,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"; + const MP4 = "data:video/mp4;base64,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"; // Detect iOS browsers < version 10 + + const oldIOS = () => typeof navigator !== "undefined" && parseFloat(("" + (/CPU.*OS ([0-9_]{3,4})[0-9_]{0,1}|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0, ""])[1]).replace("undefined", "3_2").replace("_", ".").replace("_", "")) < 10 && !window.MSStream; // Detect native Wake Lock API support + + + const nativeWakeLock = () => "wakeLock" in navigator; + + class NoSleep { + constructor(player) { + this.player = player; + this.enabled = false; + + if (nativeWakeLock()) { + this._wakeLock = null; + + const handleVisibilityChange = () => { + if (this._wakeLock !== null && document.visibilityState === "visible") { + this.enable(); + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + document.addEventListener("fullscreenchange", handleVisibilityChange); + } else if (oldIOS()) { + this.noSleepTimer = null; + } else { + // Set up no sleep video element + this.noSleepVideo = document.createElement("video"); + this.noSleepVideo.setAttribute("title", "No Sleep"); + this.noSleepVideo.setAttribute("playsinline", ""); + + this._addSourceToVideo(this.noSleepVideo, "webm", WEBM); + + this._addSourceToVideo(this.noSleepVideo, "mp4", MP4); + + this.noSleepVideo.addEventListener("loadedmetadata", () => { + if (this.noSleepVideo.duration <= 1) { + // webm source + this.noSleepVideo.setAttribute("loop", ""); + } else { + // mp4 source + this.noSleepVideo.addEventListener("timeupdate", () => { + if (this.noSleepVideo.currentTime > 0.5) { + this.noSleepVideo.currentTime = Math.random(); + } + }); + } + }); + } + } + + _addSourceToVideo(element, type, dataURI) { + var source = document.createElement("source"); + source.src = dataURI; + source.type = `video/${type}`; + element.appendChild(source); + } + + get isEnabled() { + return this.enabled; + } + + enable() { + const debug = this.player.debug; + + if (nativeWakeLock()) { + return navigator.wakeLock.request("screen").then(wakeLock => { + this._wakeLock = wakeLock; + this.enabled = true; + debug.log('wakeLock', 'Wake Lock active.'); + + this._wakeLock.addEventListener("release", () => { + // ToDo: Potentially emit an event for the page to observe since + // Wake Lock releases happen when page visibility changes. + // (https://web.dev/wakelock/#wake-lock-lifecycle) + debug.log('wakeLock', 'Wake Lock released.'); + }); + }).catch(err => { + this.enabled = false; + debug.error('wakeLock', `${err.name}, ${err.message}`); + throw err; + }); + } else if (oldIOS()) { + this.disable(); + this.noSleepTimer = window.setInterval(() => { + if (!document.hidden) { + window.location.href = window.location.href.split("#")[0]; + window.setTimeout(window.stop, 0); + } + }, 15000); + this.enabled = true; + return Promise.resolve(); + } else { + let playPromise = this.noSleepVideo.play(); + return playPromise.then(res => { + this.enabled = true; + return res; + }).catch(err => { + this.enabled = false; + throw err; + }); + } + } + + disable() { + const debug = this.player.debug; + + if (nativeWakeLock()) { + if (this._wakeLock) { + this._wakeLock.release(); + } + + this._wakeLock = null; + } else if (oldIOS()) { + if (this.noSleepTimer) { + debug.warn('wakeLock', 'NoSleep now disabled for older iOS devices.'); + window.clearInterval(this.noSleepTimer); + this.noSleepTimer = null; + } + } else { + this.noSleepVideo.pause(); + } + + this.enabled = false; + } + + } + + class Player extends Emitter { + constructor(container, options) { + super(); + this.$container = container; + this._opt = Object.assign({}, DEFAULT_PLAYER_OPTIONS, options); + this.debug = new Debug(this); // + + if (this._opt.useWCS) { + this._opt.useWCS = supportWCS(); + } // + + + if (this._opt.useMSE) { + this._opt.useMSE = supportMSE(); + } // + + + if (this._opt.wcsUseVideoRender) { + this._opt.wcsUseVideoRender = supportMediaStreamTrack(); + } // 如果使用mse则强制不允许 webcodecs + + + if (this._opt.useMSE) { + if (this._opt.useWCS) { + this.debug.log('Player', 'useWCS set true->false'); + } + + if (!this._opt.forceNoOffscreen) { + this.debug.log('Player', 'forceNoOffscreen set false->true'); + } + + this._opt.useWCS = false; + this._opt.forceNoOffscreen = true; + } + + if (!this._opt.forceNoOffscreen) { + if (!supportOffscreenV2()) { + this._opt.forceNoOffscreen = true; + this._opt.useOffscreen = false; + } else { + this._opt.useOffscreen = true; + } + } + + if (!this._opt.hasAudio) { + this._opt.operateBtns.audio = false; + } + + this._opt.hasControl = this._hasControl(); // + + this._loading = false; + this._playing = false; + this._hasLoaded = false; // + + this._checkHeartTimeout = null; + this._checkLoadingTimeout = null; + this._checkStatsInterval = null; // + + this._startBpsTime = null; + this._isPlayingBeforePageHidden = false; + this._stats = { + buf: 0, + // 当前缓冲区时长,单位毫秒, + fps: 0, + // 当前视频帧率 + abps: 0, + // 当前音频码率,单位bit + vbps: 0, + // 当前视频码率,单位bit + ts: 0 // 当前视频帧pts,单位毫秒 + + }; // 各个步骤的时间统计 + + this._times = initPlayTimes(); // + + this._videoTimestamp = 0; + this._audioTimestamp = 0; + property$1(this); + this.events = new Events(this); + this.video = new Video(this); + + if (this._opt.hasAudio) { + this.audio = new Audio(this); + } + + this.recorder = new Recorder(this); + + if (!this._onlyMseOrWcsVideo()) { + this.decoderWorker = new DecoderWorker(this); + } else { + this.loaded = true; + } + + this.stream = null; + this.demux = null; + this._lastVolume = null; + + if (this._opt.useWCS) { + this.webcodecsDecoder = new WebcodecsDecoder(this); + this.loaded = true; + } + + if (this._opt.useMSE) { + this.mseDecoder = new MseDecoder(this); + this.loaded = true; + } // + + + this.control = new Control(this); + + if (isMobile()) { + this.keepScreenOn = new NoSleep(this); + } + + events$1(this); + observer(this); + + if (this._opt.useWCS) { + this.debug.log('Player', 'use WCS'); + } + + if (this._opt.useMSE) { + this.debug.log('Player', 'use MSE'); + } + + if (this._opt.useOffscreen) { + this.debug.log('Player', 'use offscreen'); + } + + this.debug.log('Player options', this._opt); + } + + destroy() { + this._loading = false; + this._playing = false; + this._hasLoaded = false; + this._lastVolume = null; + this._times = initPlayTimes(); + + if (this.decoderWorker) { + this.decoderWorker.destroy(); + this.decoderWorker = null; + } + + if (this.video) { + this.video.destroy(); + this.video = null; + } + + if (this.audio) { + this.audio.destroy(); + this.audio = null; + } + + if (this.stream) { + this.stream.destroy(); + this.stream = null; + } + + if (this.recorder) { + this.recorder.destroy(); + this.recorder = null; + } + + if (this.control) { + this.control.destroy(); + this.control = null; + } + + if (this.webcodecsDecoder) { + this.webcodecsDecoder.destroy(); + this.webcodecsDecoder = null; + } + + if (this.mseDecoder) { + this.mseDecoder.destroy(); + this.mseDecoder = null; + } + + if (this.demux) { + this.demux.destroy(); + this.demux = null; + } + + if (this.events) { + this.events.destroy(); + this.events = null; + } + + this.clearCheckHeartTimeout(); + this.clearCheckLoadingTimeout(); + this.clearStatsInterval(); // + + this.releaseWakeLock(); + this.keepScreenOn = null; // reset stats + + this.resetStats(); + this._audioTimestamp = 0; + this._videoTimestamp = 0; // 其他没法解耦的,通过 destroy 方式 + + this.emit('destroy'); // 接触所有绑定事件 + + this.off(); + this.debug.log('play', 'destroy end'); + } + + set fullscreen(value) { + if (isMobile() && this._opt.useWebFullScreen) { + this.emit(EVENTS.webFullscreen, value); + setTimeout(() => { + this.updateOption({ + rotate: value ? 270 : 0 + }); + this.resize(); + }, 10); + } else { + this.emit(EVENTS.fullscreen, value); + } + } + + get fullscreen() { + return isFullScreen() || this.webFullscreen; + } + + set webFullscreen(value) { + this.emit(EVENTS.webFullscreen, value); + } + + get webFullscreen() { + return this.$container.classList.contains('jessibuca-fullscreen-web'); + } + + set loaded(value) { + this._hasLoaded = value; + } + + get loaded() { + return this._hasLoaded; + } // + + + set playing(value) { + if (value) { + // 将loading 设置为 false + this.loading = false; + } + + if (this.playing !== value) { + this._playing = value; + this.emit(EVENTS.playing, value); + this.emit(EVENTS.volumechange, this.volume); + + if (value) { + this.emit(EVENTS.play); + } else { + this.emit(EVENTS.pause); + } + } + } + + get playing() { + return this._playing; + } + + get volume() { + return this.audio && this.audio.volume || 0; + } + + set volume(value) { + if (value !== this.volume) { + this.audio && this.audio.setVolume(value); + this._lastVolume = value; + } + } + + get lastVolume() { + return this._lastVolume; + } + + set loading(value) { + if (this.loading !== value) { + this._loading = value; + this.emit(EVENTS.loading, this._loading); + } + } + + get loading() { + return this._loading; + } + + set recording(value) { + if (value) { + if (this.playing) { + this.recorder && this.recorder.startRecord(); + } + } else { + this.recorder && this.recorder.stopRecordAndSave(); + } + } + + get recording() { + return this.recorder ? this.recorder.recording : false; + } + + set audioTimestamp(value) { + if (value === null) { + return; + } + + this._audioTimestamp = value; + } // + + + get audioTimestamp() { + return this._audioTimestamp; + } // + + + set videoTimestamp(value) { + if (value === null) { + return; + } + + this._videoTimestamp = value; // just for wasm + + if (!this._opt.useWCS && !this._opt.useMSE) { + if (this.audioTimestamp && this.videoTimestamp) { + this.audio && this.audio.emit(EVENTS.videoSyncAudio, { + audioTimestamp: this.audioTimestamp, + videoTimestamp: this.videoTimestamp, + diff: this.audioTimestamp - this.videoTimestamp + }); + } + } + } // + + + get videoTimestamp() { + return this._videoTimestamp; + } + + get isDebug() { + return this._opt.debug === true; + } + /** + * + * @param options + */ + + + updateOption(options) { + this._opt = Object.assign({}, this._opt, options); + } + /** + * + * @returns {Promise} + */ + + + init() { + return new Promise((resolve, reject) => { + if (!this.stream) { + this.stream = new Stream(this); + } + + if (!this.audio) { + if (this._opt.hasAudio) { + this.audio = new Audio(this); + } + } + + if (!this.demux) { + this.demux = new Demux(this); + } + + if (this._opt.useWCS) { + if (!this.webcodecsDecoder) { + this.webcodecsDecoder = new WebcodecsDecoder(this); + } + } + + if (this._opt.useMSE) { + if (!this.mseDecoder) { + this.mseDecoder = new MseDecoder(this); + } + } + + if (!this.decoderWorker && !this._onlyMseOrWcsVideo()) { + this.decoderWorker = new DecoderWorker(this); + this.once(EVENTS.decoderWorkerInit, () => { + resolve(); + }); + } else { + resolve(); + } + }); + } + /** + * + * @param url + * @returns {Promise} + */ + + + play(url, options) { + return new Promise((resolve, reject) => { + if (!url && !this._opt.url) { + return reject(); + } + + this.loading = true; + this.playing = false; + this._times.playInitStart = now(); + + if (!url) { + url = this._opt.url; + } + + this._opt.url = url; + this.clearCheckHeartTimeout(); + this.init().then(() => { + this._times.playStart = now(); // + + if (this._opt.isNotMute) { + this.mute(false); + } + + if (this.webcodecsDecoder) { + this.webcodecsDecoder.once(EVENTS_ERROR.webcodecsH265NotSupport, () => { + this.emit(EVENTS_ERROR.webcodecsH265NotSupport); + + if (!this._opt.autoWasm) { + this.emit(EVENTS.error, EVENTS_ERROR.webcodecsH265NotSupport); + } + }); + } + + if (this.mseDecoder) { + this.mseDecoder.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => { + this.emit(EVENTS_ERROR.mediaSourceH265NotSupport); + + if (!this._opt.autoWasm) { + this.emit(EVENTS.error, EVENTS_ERROR.mediaSourceH265NotSupport); + } + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceFull, () => { + this.emitError(EVENTS_ERROR.mediaSourceFull); + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceAppendBufferError, () => { + this.emitError(EVENTS_ERROR.mediaSourceAppendBufferError); + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceBufferListLarge, () => { + this.emitError(EVENTS_ERROR.mediaSourceBufferListLarge); + }); + this.mseDecoder.once(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout, () => { + this.emitError(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout); + }); + } + + this.enableWakeLock(); + this.stream.fetchStream(url, options); // + + this.checkLoadingTimeout(); // fetch error + + this.stream.once(EVENTS_ERROR.fetchError, error => { + reject(error); + }); // ws + + this.stream.once(EVENTS_ERROR.websocketError, error => { + reject(error); + }); // stream end + + this.stream.once(EVENTS.streamEnd, () => { + reject(); + }); // success + + this.stream.once(EVENTS.streamSuccess, () => { + resolve(); + this._times.streamResponse = now(); // + + this.video.play(); + this.checkStatsInterval(); + }); + }).catch(e => { + reject(e); + }); + }); + } + /** + * + */ + + + close() { + return new Promise((resolve, reject) => { + this._close().then(() => { + this.video && this.video.clearView(); + resolve(); + }); + }); + } + + resumeAudioAfterPause() { + if (this.lastVolume) { + this.volume = this.lastVolume; + } + } + + _close() { + return new Promise((resolve, reject) => { + // + if (this.stream) { + this.stream.destroy(); + this.stream = null; + } + + if (this.demux) { + this.demux.destroy(); + this.demux = null; + } // + + + if (this.decoderWorker) { + this.decoderWorker.destroy(); + this.decoderWorker = null; + } + + if (this.webcodecsDecoder) { + this.webcodecsDecoder.destroy(); + this.webcodecsDecoder = null; + } + + if (this.mseDecoder) { + this.mseDecoder.destroy(); + this.mseDecoder = null; + } + + if (this.audio) { + this.audio.destroy(); + this.audio = null; + } + + this.clearCheckHeartTimeout(); + this.clearCheckLoadingTimeout(); + this.clearStatsInterval(); + this.playing = false; + this.loading = false; + this.recording = false; + + if (this.video) { + this.video.resetInit(); + this.video.pause(true); + } // release lock + + + this.releaseWakeLock(); // reset stats + + this.resetStats(); // + + this._audioTimestamp = 0; + this._videoTimestamp = 0; // + + this._times = initPlayTimes(); // + + setTimeout(() => { + resolve(); + }, 0); + }); + } + /** + * + * @param flag {boolean} 是否清除画面 + * @returns {Promise} + */ + + + pause() { + let flag = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; + + if (flag) { + return this.close(); + } else { + return this._close(); + } + } + /** + * + * @param flag + */ + + + mute(flag) { + this.audio && this.audio.mute(flag); + } + /** + * + */ + + + resize() { + this.video.resize(); + } + /** + * + * @param fileName + * @param fileType + */ + + + startRecord(fileName, fileType) { + if (this.recording) { + return; + } + + this.recorder.setFileName(fileName, fileType); + this.recording = true; + } + /** + * + */ + + + stopRecordAndSave() { + if (this.recording) { + this.recording = false; + } + } + + _hasControl() { + let result = false; + let hasBtnShow = false; + Object.keys(this._opt.operateBtns).forEach(key => { + if (this._opt.operateBtns[key]) { + hasBtnShow = true; + } + }); + + if (this._opt.showBandwidth || this._opt.text || hasBtnShow) { + result = true; + } + + return result; + } + + _onlyMseOrWcsVideo() { + return this._opt.hasAudio === false && (this._opt.useMSE || this._opt.useWCS && !this._opt.useOffscreen); + } + + checkHeart() { + this.clearCheckHeartTimeout(); + this.checkHeartTimeout(); + } // 心跳检查,如果渲染间隔暂停了多少时间之后,就会抛出异常 + + + checkHeartTimeout() { + this._checkHeartTimeout = setTimeout(() => { + if (this.playing) { + // check again + if (this._stats.fps !== 0) { + return; + } + + this.pause().then(() => { + this.emit(EVENTS.timeout, EVENTS.delayTimeout); + this.emit(EVENTS.delayTimeout); + }); + } + }, this._opt.heartTimeout * 1000); + } + + checkStatsInterval() { + this._checkStatsInterval = setInterval(() => { + this.updateStats(); + }, 1000); + } // + + + clearCheckHeartTimeout() { + if (this._checkHeartTimeout) { + clearTimeout(this._checkHeartTimeout); + this._checkHeartTimeout = null; + } + } // loading 等待时间 + + + checkLoadingTimeout() { + this._checkLoadingTimeout = setTimeout(() => { + // check again + if (this.playing) { + return; + } + + this.pause().then(() => { + this.emit(EVENTS.timeout, EVENTS.loadingTimeout); + this.emit(EVENTS.loadingTimeout); + }); + }, this._opt.loadingTimeout * 1000); + } + + clearCheckLoadingTimeout() { + if (this._checkLoadingTimeout) { + clearTimeout(this._checkLoadingTimeout); + this._checkLoadingTimeout = null; + } + } + + clearStatsInterval() { + if (this._checkStatsInterval) { + clearInterval(this._checkStatsInterval); + this._checkStatsInterval = null; + } + } + + handleRender() { + if (this.loading) { + this.emit(EVENTS.start); + this.loading = false; + this.clearCheckLoadingTimeout(); + } + + if (!this.playing) { + this.playing = true; + } + + this.checkHeart(); + } // + + + updateStats(options) { + options = options || {}; + + if (!this._startBpsTime) { + this._startBpsTime = now(); + } + + if (isNotEmpty(options.ts)) { + this._stats.ts = options.ts; + } + + if (isNotEmpty(options.buf)) { + this._stats.buf = options.buf; + } + + if (options.fps) { + this._stats.fps += 1; + } + + if (options.abps) { + this._stats.abps += options.abps; + } + + if (options.vbps) { + this._stats.vbps += options.vbps; + } + + const _nowTime = now(); + + const timestamp = _nowTime - this._startBpsTime; + + if (timestamp < 1 * 1000) { + return; + } + + this.emit(EVENTS.stats, this._stats); + this.emit(EVENTS.performance, fpsStatus(this._stats.fps)); + this._stats.fps = 0; + this._stats.abps = 0; + this._stats.vbps = 0; + this._startBpsTime = _nowTime; + } + + resetStats() { + this._startBpsTime = null; + this._stats = { + buf: 0, + //ms + fps: 0, + abps: 0, + vbps: 0, + ts: 0 + }; + } + + enableWakeLock() { + if (this._opt.keepScreenOn) { + this.keepScreenOn && this.keepScreenOn.enable(); + } + } + + releaseWakeLock() { + if (this._opt.keepScreenOn) { + this.keepScreenOn && this.keepScreenOn.disable(); + } + } + + handlePlayToRenderTimes() { + const _times = this._times; + _times.playTimestamp = _times.playStart - _times.playInitStart; + _times.streamTimestamp = _times.streamStart - _times.playStart; + _times.streamResponseTimestamp = _times.streamResponse - _times.streamStart; + _times.demuxTimestamp = _times.demuxStart - _times.streamResponse; + _times.decodeTimestamp = _times.decodeStart - _times.demuxStart; + _times.videoTimestamp = _times.videoStart - _times.decodeStart; + _times.allTimestamp = _times.videoStart - _times.playInitStart; + this.emit(EVENTS.playToRenderTimes, _times); + } + + getOption() { + return this._opt; + } + + emitError(errorType) { + let message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ''; + this.emit(EVENTS.error, errorType, message); + this.emit(errorType, message); + } + + } + + class Jessibuca extends Emitter { + constructor(options) { + super(); + let _opt = options; + let $container = options.container; + + if (typeof options.container === 'string') { + $container = document.querySelector(options.container); + } + + if (!$container) { + throw new Error('Jessibuca need container option'); + } // check container node name + + + if ($container.nodeName === 'CANVAS' || $container.nodeName === 'VIDEO') { + throw new Error(`Jessibuca container type can not be ${$container.nodeName} type`); + } + + if (_opt.videoBuffer >= _opt.heartTimeout) { + throw new Error(`Jessibuca videoBuffer ${_opt.videoBuffer}s must be less than heartTimeout ${_opt.heartTimeout}s`); + } + + $container.classList.add('jessibuca-container'); + delete _opt.container; // 禁用离屏渲染 + + _opt.forceNoOffscreen = true; // 移动端不支持自动关闭控制栏 + + if (isMobile()) { + _opt.controlAutoHide = false; + } // s -> ms + + + if (isNotEmpty(_opt.videoBuffer)) { + _opt.videoBuffer = Number(_opt.videoBuffer) * 1000; + } // setting + + + if (isNotEmpty(_opt.timeout)) { + if (isEmpty(_opt.loadingTimeout)) { + _opt.loadingTimeout = _opt.timeout; + } + + if (isEmpty(_opt.heartTimeout)) { + _opt.heartTimeout = _opt.timeout; + } + } + + this._opt = _opt; + this.$container = $container; + this._loadingTimeoutReplayTimes = 0; + this._heartTimeoutReplayTimes = 0; + this.events = new Events(this); + this.debug = new Debug(this); + + this._initPlayer($container, _opt); + } + /** + * + */ + + + destroy() { + if (this.events) { + this.events.destroy(); + this.events = null; + } + + if (this.player) { + this.player.destroy(); + this.player = null; + } + + this.$container = null; + this._opt = null; + this._loadingTimeoutReplayTimes = 0; + this._heartTimeoutReplayTimes = 0; + this.off(); + } + + _initPlayer($container, options) { + this.player = new Player($container, options); + this.debug.log('jessibuca', '_initPlayer', this.player.getOption()); + + this._bindEvents(); + } + + _resetPlayer() { + let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + this.player.destroy(); + this.player = null; + this._opt = Object.assign(this._opt, options); + this._opt.url = ''; // reset url + + this._initPlayer(this.$container, this._opt); + } + + _bindEvents() { + // 对外的事件 + Object.keys(JESSIBUCA_EVENTS).forEach(key => { + this.player.on(JESSIBUCA_EVENTS[key], value => { + this.emit(key, value); + }); + }); + } + /** + * 是否开启控制台调试打印 + * @param value {Boolean} + */ + + + setDebug(value) { + this.player.updateOption({ + debug: !!value + }); + } + /** + * + */ + + + mute() { + this.player.mute(true); + } + /** + * + */ + + + cancelMute() { + this.player.mute(false); + } + /** + * + * @param value {number} + */ + + + setVolume(value) { + this.player.volume = value; + } + /** + * + */ + + + audioResume() { + this.player.audio && this.player.audio.audioEnabled(true); + } + /** + * 设置超时时长, 单位秒 在连接成功之前和播放中途,如果超过设定时长无数据返回,则回调timeout事件 + * @param value {number} + */ + + + setTimeout(time) { + time = Number(time); + this.player.updateOption({ + timeout: time, + loadingTimeout: time, + heartTimeout: time + }); + } + /** + * + * @param type {number}: 0,1,2 + */ + + + setScaleMode(type) { + type = Number(type); + let options = { + isFullResize: false, + isResize: false + }; + + switch (type) { + case SCALE_MODE_TYPE.full: + options.isFullResize = false; + options.isResize = false; + break; + + case SCALE_MODE_TYPE.auto: + options.isFullResize = false; + options.isResize = true; + break; + + case SCALE_MODE_TYPE.fullAuto: + options.isFullResize = true; + options.isResize = true; + break; + } + + this.player.updateOption(options); + this.resize(); + } + /** + * + * @returns {Promise} + */ + + + pause() { + return new Promise((resolve, reject) => { + if (this.player) { + this.player.pause().then(() => { + resolve(); + }).catch(e => { + reject(e); + }); + } else { + reject('player is null'); + } + }); + } + /** + * + */ + + + close() { + // clear url + this._opt.url = ''; + this._opt.playOptions = {}; + return this.player.close(); + } + /** + * + */ + + + clearView() { + this.player.video.clearView(); + } + /** + * + * @param url {string} + * @param options {object} + * @returns {Promise} + */ + + + play(url) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return new Promise((resolve, reject) => { + if (!url && !this._opt.url) { + this.emit(EVENTS.error, EVENTS_ERROR.playError); + reject('play url is empty'); + return; + } + + if (url) { + // url 相等的时候。 + if (this._opt.url) { + // 存在相同的 url + if (url === this._opt.url) { + // 正在播放 + if (this.player.playing) { + resolve(); + } else { + // pause -> play + this.clearView(); + this.player.play(this._opt.url, this._opt.playOptions).then(() => { + resolve(); // 恢复下之前的音量 + + this.player.resumeAudioAfterPause(); + }).catch(e => { + this.debug.warn('jessibuca', 'pause -> play and play error', e); + this.player.pause().then(() => { + reject(e); + }); + }); + } + } else { + // url 发生改变了 + this.player.pause().then(() => { + // 清除 画面 + this.clearView(); + + this._play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('jessibuca', 'this._play error', e); + reject(e); + }); + }).catch(e => { + this.debug.warn('jessibuca', 'this._opt.url is null and pause error', e); + reject(e); + }); + } + } else { + this._play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('jessibuca', 'this._play error', e); + reject(e); + }); + } + } else { + // url 不存在的时候 + // 就是从 play -> pause -> play + this.player.play(this._opt.url, this._opt.playOptions).then(() => { + resolve(); // 恢复下之前的音量 + + this.player.resumeAudioAfterPause(); + }).catch(e => { + this.debug.warn('jessibuca', 'url is null and play error', e); + this.player.pause().then(() => { + reject(e); + }); + }); + } + }); + } + /** + * + * @param url {string} + * @param options {object} + * @returns {Promise} + * @private + */ + + + _play(url) { + let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + return new Promise((resolve, reject) => { + this._opt.url = url; + this._opt.playOptions = options; // 新的url + + const isHttp = url.indexOf("http") === 0; // + + const protocol = isHttp ? PLAYER_PLAY_PROTOCOL.fetch : PLAYER_PLAY_PROTOCOL.websocket; // + + const demuxType = isHttp || url.indexOf(".flv") !== -1 || this._opt.isFlv ? DEMUX_TYPE.flv : DEMUX_TYPE.m7s; + this.player.updateOption({ + protocol, + demuxType + }); + this.player.once(EVENTS_ERROR.webglAlignmentError, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'webglAlignmentError'); + + this._resetPlayer({ + openWebglAlignment: true + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'webglAlignmentError and play success'); + }).catch(() => { + // reject(); + this.debug.log('Jessibuca', 'webglAlignmentError and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mediaSourceH265NotSupport, () => { + this.pause().then(() => { + if (this.player._opt.autoWasm) { + this.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play'); + + this._resetPlayer({ + useMSE: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play success'); + }).catch(() => { + // reject(); + this.debug.log('Jessibuca', 'auto wasm [mse-> wasm] reset player and play error'); + }); + } + }); + }); // media source full error + + this.player.once(EVENTS_ERROR.mediaSourceFull, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source full'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source full and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source full and reset player and play error'); + }); + }); + }); // media source append buffer error + + this.player.once(EVENTS_ERROR.mediaSourceAppendBufferError, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source append buffer error'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source append buffer error and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source append buffer error and reset player and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mediaSourceBufferListLarge, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source buffer list large'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source buffer list large and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source buffer list large and reset player and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mediaSourceAppendBufferEndTimeout, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'media source append buffer end timeout'); + + this._resetPlayer(); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'media source append buffer end timeout and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'media source append buffer end timeout and reset player and play error'); + }); + }); + }); + this.player.once(EVENTS_ERROR.mseSourceBufferError, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'mseSourceBufferError close success'); + }); + }); // + + this.player.once(EVENTS_ERROR.webcodecsH265NotSupport, () => { + this.pause().then(() => { + if (this.player._opt.autoWasm) { + this.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play'); + + this._resetPlayer({ + useWCS: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'auto wasm [wcs-> wasm] reset player and play error'); + }); + } + }); + }); // webcodecs + + this.player.once(EVENTS_ERROR.webcodecsWidthOrHeightChange, () => { + this.pause().then(() => { + this.debug.log('Jessibuca', 'webcodecs Width Or Height Change reset player and play'); + + this._resetPlayer({ + useWCS: true + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'webcodecs Width Or Height Change reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'webcodecs Width Or Height Change reset player and play error'); + }); + }); + }); // webcodecs + + this.player.once(EVENTS_ERROR.webcodecsDecodeError, () => { + this.pause().then(() => { + if (this.player._opt.autoWasm) { + this.debug.log('Jessibuca', 'webcodecs decode error reset player and play'); + + this._resetPlayer({ + useWCS: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'webcodecs decode error reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'webcodecs decode error reset player and play error'); + }); + } + }); + }); // wasm。 + + this.player.once(EVENTS_ERROR.wasmDecodeError, () => { + if (this.player._opt.wasmDecodeErrorReplay) { + this.pause().then(() => { + this.debug.log('Jessibuca', 'wasm decode error and reset player and play'); + + this._resetPlayer({ + useWCS: false + }); + + this.play(url, options).then(() => { + // resolve(); + this.debug.log('Jessibuca', 'wasm decode error and reset player and play success'); + }).catch(() => { + // reject(); + this.debug.warn('Jessibuca', 'wasm decode error and reset player and play error'); + }); + }); + } + }); // 监听 delay timeout + + this.player.on(EVENTS.delayTimeout, () => { + if (this.player._opt.heartTimeoutReplay && (this._heartTimeoutReplayTimes < this.player._opt.heartTimeoutReplayTimes || this.player._opt.heartTimeoutReplayTimes === -1)) { + this.debug.log('Jessibuca', `delay timeout replay time is ${this._heartTimeoutReplayTimes}`); + this._heartTimeoutReplayTimes += 1; + this.play(url, options).then(() => { + // resolve(); + this._heartTimeoutReplayTimes = 0; + }).catch(() => {// reject(); + }); + } + }); // 监听 loading timeout + + this.player.on(EVENTS.loadingTimeout, () => { + if (this.player._opt.loadingTimeoutReplay && (this._loadingTimeoutReplayTimes < this.player._opt.loadingTimeoutReplayTimes || this.player._opt.loadingTimeoutReplayTimes === -1)) { + this.debug.log('Jessibuca', `loading timeout replay time is ${this._loadingTimeoutReplayTimes}`); + this._loadingTimeoutReplayTimes += 1; + this.play(url, options).then(() => { + // resolve(); + this._loadingTimeoutReplayTimes = 0; + }).catch(() => {// reject(); + }); + } + }); + + if (this.hasLoaded()) { + this.player.play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('Jessibuca', 'hasLoaded and play error', e); + this.player && this.player.pause().then(() => { + reject(e); + }); + }); + } else { + this.player.once(EVENTS.decoderWorkerInit, () => { + this.player.play(url, options).then(() => { + resolve(); + }).catch(e => { + this.debug.warn('Jessibuca', 'decoderWorkerInit and play error', e); + this.player && this.player.pause().then(() => { + reject(e); + }); + }); + }); + } + }); + } + /** + * + */ + + + resize() { + this.player.resize(); + } + /** + * + * @param time {number} s + */ + + + setBufferTime(time) { + time = Number(time); // s -> ms + + this.player.updateOption({ + videoBuffer: time * 1000 + }); // update worker config + + this.player.decoderWorker && this.player.decoderWorker.updateWorkConfig({ + key: 'videoBuffer', + value: time * 1000 + }); + } + /** + * + * @param deg {number} + */ + + + setRotate(deg) { + deg = parseInt(deg, 10); + const list = [0, 90, 180, 270]; + + if (this._opt.rotate === deg || list.indexOf(deg) === -1) { + return; + } + + this.player.updateOption({ + rotate: deg + }); + this.resize(); + } + /** + * + * @returns {boolean} + */ + + + hasLoaded() { + return this.player.loaded; + } + /** + * + */ + + + setKeepScreenOn() { + this.player.updateOption({ + keepScreenOn: true + }); + } + /** + * + * @param flag {Boolean} + */ + + + setFullscreen(flag) { + const fullscreen = !!flag; + + if (this.player.fullscreen !== fullscreen) { + this.player.fullscreen = fullscreen; + } + } + /** + * + * @param filename {string} + * @param format {string} + * @param quality {number} + * @param type {string} download,base64,blob + */ + + + screenshot(filename, format, quality, type) { + if (!this.player.video) { + return ''; + } + + return this.player.video.screenshot(filename, format, quality, type); + } + /** + * + * @param fileName {string} + * @param fileType {string} + * @returns {Promise} + */ + + + startRecord(fileName, fileType) { + return new Promise((resolve, reject) => { + if (this.player.playing) { + this.player.startRecord(fileName, fileType); + resolve(); + } else { + reject(); + } + }); + } + + stopRecordAndSave() { + if (this.player.recording) { + this.player.stopRecordAndSave(); + } + } + /** + * + * @returns {Boolean} + */ + + + isPlaying() { + return this.player ? this.player.playing : false; + } + /** + * 是否静音状态 + * @returns {Boolean} + */ + + + isMute() { + return this.player.audio ? this.player.audio.isMute : true; + } + /** + * 是否在录制视频 + * @returns {*} + */ + + + isRecording() { + return this.player.recorder.recording; + } + + } + + _defineProperty(Jessibuca, "ERROR", EVENTS_ERROR); + + _defineProperty(Jessibuca, "TIMEOUT", { + loadingTimeout: EVENTS.loadingTimeout, + delayTimeout: EVENTS.delayTimeout + }); + + window.Jessibuca = Jessibuca; + + return Jessibuca; + +})); +//# sourceMappingURL=jessibuca.js.map diff --git a/src/components/townDetail/topCenter.vue b/src/components/townDetail/topCenter.vue new file mode 100644 index 0000000..17c6de1 --- /dev/null +++ b/src/components/townDetail/topCenter.vue @@ -0,0 +1,360 @@ + + + \ No newline at end of file diff --git a/src/components/townDetail/version.js b/src/components/townDetail/version.js new file mode 100644 index 0000000..0e66498 --- /dev/null +++ b/src/components/townDetail/version.js @@ -0,0 +1 @@ +export const VERSION = '#VERSION#' \ No newline at end of file diff --git a/src/main.js b/src/main.js index a6f2b5b..ff021f5 100644 --- a/src/main.js +++ b/src/main.js @@ -1,12 +1,12 @@ import { createApp } from 'vue' -// import './style.css' import App from './App.vue' import dataV from '@jiaminghi/data-view' - -// import DataV, { setClassNamePrefix } from '@dataview/datav-vue3'; import router from "./router"; +// import '@/components/townDetail/test.js' -const app=createApp(App) +import 'amfe-flexible' + +const app = createApp(App) app.use(router) app.use(dataV) app.mount('#app') \ No newline at end of file diff --git a/src/view/Businesses.vue b/src/view/Businesses.vue index a5b056f..2ded8b6 100644 --- a/src/view/Businesses.vue +++ b/src/view/Businesses.vue @@ -4,11 +4,9 @@
-
正常开启的商户
-
已关闭的商户
+
正常开启的商户
+
已关闭的商户
- -
@@ -25,7 +23,7 @@ import { ref, reactive, onMounted } from "vue" import { useRouter } from 'vue-router' const route = useRouter() -const ShwostoreType = ref(false) +const ShwostoreType = ref(true) const test = () => { console.log(6) } @@ -135,13 +133,28 @@ onMounted(() => { .btns { display: flex; + position: relative; .btn { color: white; - border: 1px solid #BF6D5D; - padding: 3px 10px; + // border: 1px solid #BF6D5D; + background-image: url('static/index/btn.png'); + background-size: 100% 100%; + padding: 1vh 1vw; border-radius: 20px; margin-right: 10px; + cursor: pointer; + font-size: 14px; + cursor: pointer; + } + + .act-btn { + + background-image: url('/static/index/actbg.png'); + background-size: 100% 100%; + border: none; + // border-bottom: 6px solid #BF6D5D + // border-bottom: 6px solid #BF6D5D; } } diff --git a/src/view/commodity.vue b/src/view/commodity.vue index e409ed3..a4c5a39 100644 --- a/src/view/commodity.vue +++ b/src/view/commodity.vue @@ -3,35 +3,19 @@
-
- - - - - - -
- - - -
aniu
- - - - - -
+
+
商品分类
+
商品管理
+
-
出售中商品
+
出售中商品
仓库中商品
待审核商品
审核未通过商品
-
+
@@ -46,11 +30,25 @@ @@ -219,6 +221,7 @@ onMounted(() => { transform: translate(-50%, -50%); // background-color: #fff; box-sizing: border-box; + // position: absolute; .content { width: 100%; @@ -235,29 +238,51 @@ onMounted(() => { .btn { color: white; // border: 1px solid #BF6D5D; - padding: 3px 10px; + background-image: url('static/index/btn.png'); + background-size: 100% 100%; + padding: 1vh 1vw; border-radius: 20px; margin-right: 10px; - position: relative; - // background-image: url(''); - // background-size: cover; + cursor: pointer; + font-size: 14px; + } + + .act-btn { + + background-image: url('/static/index/actbg.png'); + background-size: 100% 100%; + border: none; + // border-bottom: 6px solid #BF6D5D + // border-bottom: 6px solid #BF6D5D; } } .table { width: 100%; height: 75vh; - margin-top: 5vh; + margin-top: 2vh; } } .manageBtn { - padding: 2px 10px; - border: 1px solid #6377C8; + padding: 0.75vh 0.75vw; border-radius: 20px; margin-right: 10px; + font-size: 14px; + // line-height: 10px; + background-image: url('/static/index/btn2.png'); + background-size: 100% 100%; + + } +.actmanageBtn { + background-image: url('/static/index/actbg2.png'); + // background-size: 100% 100%; + +} + + :deep(.ceil) { padding: 0 !important; margin-bottom: 5px; diff --git a/src/view/finance.vue b/src/view/finance.vue index 570c35f..8271bb7 100644 --- a/src/view/finance.vue +++ b/src/view/finance.vue @@ -4,11 +4,23 @@
-
提现记录
-
资金记录
-
账单管理
+
+
提现记录
+
资金记录
+
账单管理
+
+
+
+
日账单
+
月账单
+
+
+
+ +
-
@@ -107,7 +118,6 @@
- @@ -121,6 +131,85 @@ // 102B3 import { ref, reactive, onMounted } from "vue" import { useRouter } from 'vue-router' +import Bill from "@/components/Bill.vue" + + +const billList = reactive([ + { + icon: "DD", + text: "订单总金额收入", + num: 1245 + }, + { + icon: "CZ", + text: "退款支出金额", + num: 1245 + }, + { + icon: "YJ", + text: "佣金支出金额", + num: 1245 + }, + { + icon: "PT", + text: "平台手续费", + num: 1245 + }, + { + icon: "CZ", + text: "充值金额", + num: 1245 + }, + { + icon: "CZXF", + text: "充值消费金额", + num: 1245 + }, + { + icon: "CS", + text: "产生交易的商户数", + num: 1245 + }, + { + icon: "YH", + text: "优惠券", + num: 1245 + }, + { + icon: "CS", + text: "入口商户金额", + num: 1245 + }, + { + icon: "YC", + text: "云仓库佣金", + num: 1245 + }, + { + icon: "XZ", + text: "小组服务佣金", + num: 1245 + }, + { + icon: "CY", + text: "村佣金", + num: 1245 + }, + { + icon: "ZY", + text: "镇佣金", + num: 1245 + }, + { + icon: "PT", + text: "平台剩余手续费", + num: 1245 + }, +]) + + + + const route = useRouter() const ShwostoreType = ref(false) const changeTable = (type) => { @@ -316,30 +405,64 @@ onMounted(() => { z-index: 999999; position: relative; cursor: pointer; + justify-content: space-between; .btn { color: white; - border: 1px solid #BF6D5D; - padding: 3px 10px; + // border: 1px solid #BF6D5D; + background-image: url('static/index/btn.png'); + background-size: 100% 100%; + padding: 1vh 1vw; border-radius: 20px; margin-right: 10px; + cursor: pointer; + font-size: 14px; } + + .act-btn { + + background-image: url('/static/index/actbg.png'); + background-size: 100% 100%; + border: none; + // border-bottom: 6px solid #BF6D5D + // border-bottom: 6px solid #BF6D5D; + } + } + + .bill { + width: 95vw; + display: flex; + flex-wrap: wrap; + // justify-content: space-between; + // background-color: #fff; } .table { width: 100%; - height: 79vh; + height: 60vh; margin-top: 2vh; } } .manageBtn { - padding: 2px 10px; - border: 1px solid #6377C8; + padding: 0.75vh 0.75vw; border-radius: 20px; margin-right: 10px; + font-size: 14px; + // line-height: 10px; + background-image: url('/static/index/btn2.png'); + background-size: 100% 100%; + + } +.actmanageBtn { + background-image: url('/static/index/actbg2.png'); + // background-size: 100% 100%; + +} + + :deep(.ceil) { padding: 0 !important; margin-bottom: 5px; diff --git a/src/view/index.vue b/src/view/index.vue index e72968a..ed130f6 100644 --- a/src/view/index.vue +++ b/src/view/index.vue @@ -1,185 +1,25 @@