video.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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. if(options.mute){
  105. video.muted = true;
  106. video.volume = 0;
  107. }
  108. video.style.display = 'none';
  109. video.loop = !!options.loop;
  110. if(options.precache) {
  111. this.precacheSource(options.src)
  112. .then(blob => {
  113. console.log("sample precached.");
  114. video.src = blob;
  115. document.body.appendChild(video);
  116. })
  117. .catch(e=>{
  118. console.error(e);
  119. });
  120. }else{
  121. video.src = options.src;
  122. // 这里要插在body上,避免container移动带来无法播放的问题
  123. document.body.appendChild(this.video);
  124. video.load();
  125. }
  126. // 绑定事件
  127. this.events = {}
  128. ;['playing', 'pause', 'ended', 'error'].forEach(item => {
  129. this.on(item, this['on' + item].bind(this));
  130. })
  131. }
  132. drawFrame() {
  133. this._drawFrame = this._drawFrame || this.drawFrame.bind(this);
  134. this.animId = this.requestAnim(this._drawFrame);
  135. }
  136. play() {
  137. const prom = this.video && this.video.play();
  138. if (prom && prom.then) {
  139. prom.catch(e => {
  140. if (!this.video) {
  141. return;
  142. }
  143. this.video.muted = true;
  144. this.video.volume = 0;
  145. this.video.play().catch(e => {
  146. (this.events.error || []).forEach(item => {
  147. item(e);
  148. })
  149. })
  150. })
  151. }
  152. }
  153. requestAnimFunc() {
  154. const me = this;
  155. if (window.requestAnimationFrame) {
  156. let index = -1;
  157. return function(cb) {
  158. index++;
  159. return requestAnimationFrame(() => {
  160. if (!(index % (60 / me.fps))) {
  161. return cb();
  162. }
  163. me.animId = me.requestAnim(cb);
  164. })
  165. }
  166. }
  167. return function(cb) {
  168. return setTimeout(cb, 1000 / me.fps)
  169. }
  170. }
  171. cancelRequestAnimation() {
  172. if (window.cancelAnimationFrame) {
  173. cancelAnimationFrame(this.animId);
  174. }
  175. clearTimeout(this.animId);
  176. }
  177. destroy() {
  178. if (this.video) {
  179. this.video.parentNode && this.video.parentNode.removeChild(this.video);
  180. this.video = null
  181. }
  182. this.cancelRequestAnimation();
  183. }
  184. clear() {
  185. this.destroy();
  186. }
  187. on(event, callback:EventListenerObject) {
  188. const cbs = this.events[event] || [];
  189. cbs.push(callback);
  190. this.events[event] = cbs;
  191. this.video.addEventListener(event, callback);
  192. return this
  193. }
  194. onplaying() {
  195. if (!this.firstPlaying) {
  196. this.firstPlaying = true;
  197. this.drawFrame()
  198. }
  199. }
  200. onpause() {}
  201. onended() {
  202. this.destroy();
  203. }
  204. onerror(err) {
  205. console.error('[Alpha video]: play error: ', err);
  206. this.destroy();
  207. }
  208. }