video.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. /*
  2. * Tencent is pleased to support the open source community by making vap available.
  3. *
  4. * Copyright (C) 2020 THL A29 Limited, a Tencent company. All rights reserved.
  5. *
  6. * Licensed under the MIT License (the "License"); you may not use this file except in
  7. * compliance with the License. You may obtain a copy of the License at
  8. *
  9. * http://opensource.org/licenses/MIT
  10. *
  11. * Unless required by applicable law or agreed to in writing, software distributed under the License is
  12. * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  13. * either express or implied. See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. import {VapConfig} from "./type";
  17. export default class VapVideo {
  18. constructor(options) {
  19. if (!options.container || !options.src) {
  20. console.warn('[Alpha video]: options container and src cannot be empty!');
  21. }
  22. this.options = Object.assign(
  23. {
  24. // 视频url
  25. src: '',
  26. // 循环播放
  27. loop: false,
  28. fps: 20,
  29. // 视频宽度
  30. width: 375,
  31. // 视频高度
  32. height: 375,
  33. // 容器
  34. container: null,
  35. // 是否预加载视频资源
  36. precache: false,
  37. // 是否静音播放
  38. mute: false,
  39. config: '',
  40. accurate: false
  41. },
  42. options
  43. );
  44. this.fps = 20;
  45. this.setBegin = true;
  46. this.useFrameCallback = false;
  47. this.requestAnim = this.requestAnimFunc();
  48. this.container = this.options.container;
  49. if (!this.options.src || !this.options.config || !this.options.container) {
  50. console.error('参数出错:src(视频地址)、config(配置文件地址)、container(dom容器)');
  51. } else {
  52. // 创建video
  53. this.initVideo();
  54. }
  55. }
  56. public options:VapConfig;
  57. private fps:number;
  58. public requestAnim:Function;
  59. public container:HTMLElement;
  60. public video:HTMLVideoElement;
  61. private events;
  62. private _drawFrame: Function;
  63. private animId: number;
  64. private useFrameCallback: boolean;
  65. private firstPlaying: boolean;
  66. private setBegin: boolean;
  67. precacheSource(source): Promise<string> {
  68. const URL = (window as any).webkitURL || window.URL;
  69. return new Promise((resolve, reject) => {
  70. const xhr = new XMLHttpRequest();
  71. xhr.open("GET", source, true);
  72. xhr.responseType = "blob";
  73. xhr.onload = function() {
  74. if (xhr.status === 200 || xhr.status === 304) {
  75. const res = xhr.response;
  76. if (/iphone|ipad|ipod/i.test(navigator.userAgent)) {
  77. const fileReader = new FileReader();
  78. fileReader.onloadend = function() {
  79. const resultStr = (fileReader.result as string);
  80. const raw = atob(
  81. resultStr.slice(resultStr.indexOf(",") + 1)
  82. );
  83. const buf = Array(raw.length);
  84. for (let d = 0; d < raw.length; d++) {
  85. buf[d] = raw.charCodeAt(d);
  86. }
  87. const arr = new Uint8Array(buf);
  88. const blob = new Blob([arr], { type: "video/mp4" });
  89. resolve(URL.createObjectURL(blob));
  90. };
  91. fileReader.readAsDataURL(xhr.response);
  92. } else {
  93. resolve(URL.createObjectURL(res));
  94. }
  95. } else {
  96. reject(new Error("http response invalid" + xhr.status));
  97. }
  98. };
  99. xhr.send();
  100. });
  101. }
  102. initVideo() {
  103. const options = this.options;
  104. // 创建video
  105. const video = (this.video = document.createElement('video'));
  106. video.crossOrigin = 'anonymous';
  107. video.autoplay = false;
  108. video.preload = 'auto';
  109. video.setAttribute('playsinline','')
  110. video.setAttribute('webkit-playsinline','')
  111. if(options.mute){
  112. video.muted = true;
  113. video.volume = 0;
  114. }
  115. video.style.display = 'none';
  116. video.loop = !!options.loop;
  117. if(options.precache) {
  118. this.precacheSource(options.src)
  119. .then(blob => {
  120. console.log("sample precached.");
  121. video.src = blob;
  122. document.body.appendChild(video);
  123. })
  124. .catch(e=>{
  125. console.error(e);
  126. });
  127. }else{
  128. video.src = options.src;
  129. // 这里要插在body上,避免container移动带来无法播放的问题
  130. document.body.appendChild(this.video);
  131. video.load();
  132. }
  133. if ( 'requestVideoFrameCallback' in this.video ) {
  134. this.useFrameCallback = !!this.options.accurate;
  135. }
  136. // 绑定事件
  137. this.events = {}
  138. ;['playing', 'pause', 'ended', 'error', 'canplay'].forEach(item => {
  139. this.on(item, this['on' + item].bind(this));
  140. })
  141. }
  142. drawFrame(_, info) {
  143. this._drawFrame = this._drawFrame || this.drawFrame.bind(this, _, info);
  144. if ( this.useFrameCallback ) {
  145. // @ts-ignore
  146. this.animId = this.video.requestVideoFrameCallback( this.drawFrame.bind(this) );
  147. } else {
  148. this.animId = this.requestAnim(this._drawFrame);
  149. }
  150. }
  151. play() {
  152. const prom = this.video && this.video.play();
  153. if (prom && prom.then) {
  154. prom.catch(e => {
  155. if (!this.video) {
  156. return;
  157. }
  158. this.video.muted = true;
  159. this.video.volume = 0;
  160. this.video.play().catch(e => {
  161. (this.events.error || []).forEach(item => {
  162. item(e);
  163. })
  164. })
  165. })
  166. }
  167. }
  168. pause() {
  169. this.video && this.video.pause();
  170. }
  171. setTime(t) {
  172. if (this.video) {
  173. this.video.currentTime = t
  174. }
  175. }
  176. requestAnimFunc() {
  177. const me = this;
  178. if (window.requestAnimationFrame) {
  179. let index = -1;
  180. return function(cb) {
  181. index++;
  182. return requestAnimationFrame(() => {
  183. if (!(index % (60 / me.fps))) {
  184. return cb();
  185. }
  186. me.animId = me.requestAnim(cb);
  187. })
  188. }
  189. }
  190. return function(cb) {
  191. return setTimeout(cb, 1000 / me.fps)
  192. }
  193. }
  194. cancelRequestAnimation() {
  195. if (this.useFrameCallback) {
  196. try {
  197. // @ts-ignore
  198. this.video.cancelVideoFrameCallback(this.animId);
  199. } catch (e) {}
  200. }else if (window.cancelAnimationFrame) {
  201. cancelAnimationFrame(this.animId);
  202. } else {
  203. clearTimeout(this.animId);
  204. }
  205. }
  206. destroy() {
  207. this.cancelRequestAnimation();
  208. if (this.video) {
  209. this.video.parentNode && this.video.parentNode.removeChild(this.video);
  210. this.video = null
  211. }
  212. this.options.onDestory && this.options.onDestory();
  213. }
  214. clear() {
  215. this.destroy();
  216. }
  217. on(event, callback:EventListenerObject) {
  218. const cbs = this.events[event] || [];
  219. cbs.push(callback);
  220. this.events[event] = cbs;
  221. this.video.addEventListener(event, callback);
  222. return this
  223. }
  224. onplaying() {
  225. if (!this.firstPlaying) {
  226. this.firstPlaying = true;
  227. if ( this.useFrameCallback ) {
  228. // @ts-ignore
  229. this.animId = this.video.requestVideoFrameCallback( this.drawFrame.bind(this) );
  230. } else {
  231. this.drawFrame(null, null)
  232. }
  233. }
  234. }
  235. onpause() {}
  236. onended() {
  237. this.destroy();
  238. }
  239. oncanplay() {
  240. const begin = this.options.beginPoint
  241. if (begin && this.setBegin) {
  242. this.setBegin = false
  243. this.video.currentTime = begin
  244. }
  245. }
  246. onerror(err) {
  247. console.error('[Alpha video]: play error: ', err);
  248. this.destroy();
  249. this.options.onLoadError && this.options.onLoadError(err);
  250. }
  251. }