video.ts 7.3 KB

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