video.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. },
  41. options
  42. );
  43. this.fps = 20;
  44. this.requestAnim = this.requestAnimFunc();
  45. this.container = this.options.container;
  46. if (!this.options.src || !this.options.config || !this.options.container) {
  47. console.error('参数出错:src(视频地址)、config(配置文件地址)、container(dom容器)');
  48. } else {
  49. // 创建video
  50. this.initVideo();
  51. }
  52. }
  53. public options:VapConfig;
  54. private fps:number;
  55. public requestAnim:Function;
  56. public container:HTMLElement;
  57. public video:HTMLVideoElement;
  58. private events;
  59. private _drawFrame: Function;
  60. private animId: number;
  61. private firstPlaying: boolean;
  62. precacheSource(source): Promise<string> {
  63. const URL = (window as any).webkitURL || window.URL;
  64. return new Promise((resolve, reject) => {
  65. const xhr = new XMLHttpRequest();
  66. xhr.open("GET", source, true);
  67. xhr.responseType = "blob";
  68. xhr.onload = function() {
  69. if (xhr.status === 200 || xhr.status === 304) {
  70. const res = xhr.response;
  71. if (/iphone|ipad|ipod/i.test(navigator.userAgent)) {
  72. const fileReader = new FileReader();
  73. fileReader.onloadend = function() {
  74. const resultStr = (fileReader.result as string);
  75. const raw = atob(
  76. resultStr.slice(resultStr.indexOf(",") + 1)
  77. );
  78. const buf = Array(raw.length);
  79. for (let d = 0; d < raw.length; d++) {
  80. buf[d] = raw.charCodeAt(d);
  81. }
  82. const arr = new Uint8Array(buf);
  83. const blob = new Blob([arr], { type: "video/mp4" });
  84. resolve(URL.createObjectURL(blob));
  85. };
  86. fileReader.readAsDataURL(xhr.response);
  87. } else {
  88. resolve(URL.createObjectURL(res));
  89. }
  90. } else {
  91. reject(new Error("http response invalid" + xhr.status));
  92. }
  93. };
  94. xhr.send();
  95. });
  96. }
  97. initVideo() {
  98. const options = this.options;
  99. // 创建video
  100. const video = (this.video = document.createElement('video'));
  101. video.crossOrigin = 'anonymous';
  102. video.autoplay = false;
  103. video.preload = 'auto';
  104. video.setAttribute('playsinline','')
  105. video.setAttribute('webkit-playsinline','')
  106. if(options.mute){
  107. video.muted = true;
  108. video.volume = 0;
  109. }
  110. video.style.display = 'none';
  111. video.loop = !!options.loop;
  112. if(options.precache) {
  113. this.precacheSource(options.src)
  114. .then(blob => {
  115. console.log("sample precached.");
  116. video.src = blob;
  117. document.body.appendChild(video);
  118. })
  119. .catch(e=>{
  120. console.error(e);
  121. });
  122. }else{
  123. video.src = options.src;
  124. // 这里要插在body上,避免container移动带来无法播放的问题
  125. document.body.appendChild(this.video);
  126. video.load();
  127. }
  128. // 绑定事件
  129. this.events = {}
  130. ;['playing', 'pause', 'ended', 'error'].forEach(item => {
  131. this.on(item, this['on' + item].bind(this));
  132. })
  133. }
  134. drawFrame() {
  135. this._drawFrame = this._drawFrame || this.drawFrame.bind(this);
  136. this.animId = this.requestAnim(this._drawFrame);
  137. }
  138. play() {
  139. const prom = this.video && this.video.play();
  140. if (prom && prom.then) {
  141. prom.catch(e => {
  142. if (!this.video) {
  143. return;
  144. }
  145. this.video.muted = true;
  146. this.video.volume = 0;
  147. this.video.play().catch(e => {
  148. (this.events.error || []).forEach(item => {
  149. item(e);
  150. })
  151. })
  152. })
  153. }
  154. }
  155. requestAnimFunc() {
  156. const me = this;
  157. if (window.requestAnimationFrame) {
  158. let index = -1;
  159. return function(cb) {
  160. index++;
  161. return requestAnimationFrame(() => {
  162. if (!(index % (60 / me.fps))) {
  163. return cb();
  164. }
  165. me.animId = me.requestAnim(cb);
  166. })
  167. }
  168. }
  169. return function(cb) {
  170. return setTimeout(cb, 1000 / me.fps)
  171. }
  172. }
  173. cancelRequestAnimation() {
  174. if (window.cancelAnimationFrame) {
  175. cancelAnimationFrame(this.animId);
  176. }
  177. clearTimeout(this.animId);
  178. }
  179. destroy() {
  180. if (this.video) {
  181. this.video.parentNode && this.video.parentNode.removeChild(this.video);
  182. this.video = null
  183. }
  184. this.cancelRequestAnimation();
  185. this.options.onDestory && this.options.onDestory();
  186. }
  187. clear() {
  188. this.destroy();
  189. }
  190. on(event, callback:EventListenerObject) {
  191. const cbs = this.events[event] || [];
  192. cbs.push(callback);
  193. this.events[event] = cbs;
  194. this.video.addEventListener(event, callback);
  195. return this
  196. }
  197. onplaying() {
  198. if (!this.firstPlaying) {
  199. this.firstPlaying = true;
  200. this.drawFrame()
  201. }
  202. }
  203. onpause() {}
  204. onended() {
  205. this.destroy();
  206. }
  207. onerror(err) {
  208. console.error('[Alpha video]: play error: ', err);
  209. this.destroy();
  210. this.options.onLoadError && this.options.onLoadError(err);
  211. }
  212. }