Kaynağa Gözat

feat: 支持连续播放

showgao 3 yıl önce
ebeveyn
işleme
43eafaf29d

+ 4 - 0
web/.eslintignore

@@ -0,0 +1,4 @@
+# dependencies
+node_modules
+dist
+demo

+ 17 - 0
web/.eslintrc.js

@@ -0,0 +1,17 @@
+module.exports = {
+    root: true,
+    env: {
+        browser: true,
+    },
+    parserOptions: {
+        parser: '@typescript-eslint/parser',
+        ecmaVersion: 'latest',
+        sourceType: 'module',
+    },
+    extends: [
+        'eslint:recommended',
+        'plugin:@typescript-eslint/recommended',
+        'plugin:prettier/recommended', // @extends prettier
+    ],
+    plugins: ['@typescript-eslint', 'prettier'],
+};

+ 3 - 0
web/.prettierignore

@@ -0,0 +1,3 @@
+node_modules/**
+dist/**
+demo/**

+ 14 - 0
web/.prettierrc

@@ -0,0 +1,14 @@
+{
+    "semi": true,
+    "trailingComma": "es5",
+    "arrowParens": "always",
+    "singleQuote": true,
+    "bracketSpacing": true,
+    "useTabs": false,
+    "tabWidth": 2,
+    "printWidth": 120,
+    "proseWrap": "preserve",
+    "endOfLine": "lf",
+    "eslintIntegration": true,
+    "htmlWhitespaceSensitivity": "strict"
+}

+ 1 - 1
web/demo/src/components/HelloWorld.vue

@@ -27,7 +27,7 @@ export default {
         return
       }
       const that = this
-      this.vap = new Vap(Object.assign({}, {
+      this.vap = new Vap().play(Object.assign({}, {
         container: this.$refs.anim,
         // 素材视频链接
         src: this.url,

+ 9 - 4
web/dist/gl-util.d.ts

@@ -1,4 +1,9 @@
-export declare function createShader(gl: any, type: any, source: any): any;
-export declare function createProgram(gl: any, vertexShader: any, fragmentShader: any): any;
-export declare function createTexture(gl: any, index: number, imgData?: TexImageSource): any;
-export declare function cleanWebGL(gl: any, shaders: any, program: any, textures: any, buffers: any): void;
+export declare function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader;
+export declare function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader): WebGLProgram;
+export declare function createTexture(gl: WebGLRenderingContext, index: number, imgData?: TexImageSource): WebGLTexture;
+export declare function cleanWebGL(gl: WebGLRenderingContext, { shaders, program, textures, buffers }: {
+    shaders?: any[];
+    program?: any;
+    textures?: any[];
+    buffers?: any[];
+}): void;

+ 3 - 2
web/dist/index.d.ts

@@ -1,8 +1,9 @@
-import { VapConfig } from "./type";
+import { VapConfig } from './type';
 import WebglRenderVap from './webgl-render-vap';
 /**
  * @param options
  * @constructor
  * @return {null}
  */
-export default function (options: VapConfig): WebglRenderVap;
+export default function (options?: VapConfig): WebglRenderVap;
+export declare function canWebGL(): boolean;

+ 2 - 2
web/dist/type.d.ts

@@ -4,11 +4,11 @@ export interface VapConfig {
     config: string | {
         [key: string]: any;
     };
-    width: number;
-    height: number;
     fps?: number;
+    loop: boolean;
     mute?: boolean;
     precache?: boolean;
+    accurate: boolean;
     onLoadError?: (e: ErrorEvent) => void;
     onDestory?: () => void;
     [key: string]: any;

+ 26 - 26
web/dist/vap-frame-parser.d.ts

@@ -1,26 +1,26 @@
-export default class FrameParser {
-    constructor(source: any, headData: any);
-    private config;
-    private headData;
-    private frame;
-    private textureMap;
-    private canvas;
-    private ctx;
-    private srcData;
-    init(): Promise<this>;
-    initCanvas(): void;
-    loadImg(url: string): Promise<unknown>;
-    parseSrc(dataJson: any): Promise<[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]>;
-    /**
-     * 下载json文件
-     * @param jsonUrl json外链
-     * @returns {Promise}
-     */
-    getConfigBySrc(jsonUrl: string): Promise<unknown>;
-    /**
-     * 文字转换图片
-     * @param item
-     */
-    makeTextImg(item: any): ImageData;
-    getFrame(frame: any): any;
-}
+export default class FrameParser {
+    constructor(source: any, headData: any);
+    config: any;
+    private headData;
+    private frame;
+    textureMap: any;
+    private canvas;
+    private ctx;
+    srcData: any;
+    init(): Promise<this>;
+    initCanvas(): void;
+    loadImg(url: string): Promise<unknown>;
+    parseSrc(dataJson: any): Promise<void>;
+    /**
+     * 下载json文件
+     * @param jsonUrl json外链
+     * @returns {Promise}
+     */
+    getConfigBySrc(jsonUrl: string): Promise<unknown>;
+    /**
+     * 文字转换图片
+     * @param item
+     */
+    makeTextImg(item: any): ImageData;
+    getFrame(frame: any): any;
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 410 - 355
web/dist/vap.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
web/dist/vap.min.js


+ 34 - 33
web/dist/video.d.ts

@@ -1,33 +1,34 @@
-/// <reference types="node" />
-import { VapConfig } from "./type";
-export default class VapVideo {
-    constructor(options: any);
-    options: VapConfig;
-    private fps;
-    requestAnim: Function;
-    container: HTMLElement;
-    video: HTMLVideoElement;
-    protected events: any;
-    private _drawFrame;
-    protected animId: number;
-    protected useFrameCallback: boolean;
-    private firstPlaying;
-    private setBegin;
-    private customEvent;
-    precacheSource(source: any): Promise<string>;
-    initVideo(): void;
-    drawFrame(_: any, info: any): void;
-    play(): void;
-    pause(): void;
-    setTime(t: any): void;
-    requestAnimFunc(): ((cb: any) => number) | ((cb: any) => NodeJS.Timeout);
-    cancelRequestAnimation(): void;
-    destroy(): void;
-    clear(): void;
-    on(event: any, callback: any): this;
-    onplaying(): void;
-    onpause(): void;
-    onended(): void;
-    oncanplay(): void;
-    onerror(err: any): void;
-}
+import { VapConfig } from './type';
+export default class VapVideo {
+    options: VapConfig;
+    requestAnim: (cb: any) => number;
+    container: HTMLElement;
+    video: HTMLVideoElement;
+    protected events: {
+        [key: string]: Array<(...info: any[]) => void>;
+    };
+    private _drawFrame;
+    protected animId: number;
+    protected useFrameCallback: boolean;
+    private firstPlaying;
+    private setBegin;
+    private customEvent;
+    setOptions(options: VapConfig): this;
+    precacheSource(source: any): Promise<string>;
+    initVideo(): void;
+    drawFrame(_: any, _info: any): void;
+    play(): void;
+    pause(): void;
+    setTime(t: any): void;
+    requestAnimFunc(): (cb: any) => number;
+    cancelRequestAnimation(): void;
+    clear(): void;
+    destroy(): void;
+    on(event: any, callback: any): this;
+    once(event: any, callback: any): this;
+    trigger(eventName: any, ...e: any[]): void;
+    offAll(): this;
+    onplaying(): void;
+    oncanplay(): void;
+    onerror(err: any): void;
+}

+ 32 - 34
web/dist/webgl-render-vap.d.ts

@@ -1,34 +1,32 @@
-import { VapConfig } from "./type";
-import VapVideo from './video';
-export default class WebglRenderVap extends VapVideo {
-    constructor(options: VapConfig);
-    private insType;
-    private textures;
-    private buffers;
-    private shaders;
-    private vapFrameParser;
-    private resources;
-    private instance;
-    private program;
-    private videoTexture;
-    private aPosition;
-    private aTexCoord;
-    private aAlphaTexCoord;
-    private _imagePos;
-    init(): Promise<void>;
-    setCanvas(): void;
-    initWebGL(): any;
-    /**
-     * 顶点着色器
-     */
-    initVertexShader(): any;
-    /**
-     * 片元着色器
-     */
-    initFragmentShader(): any;
-    initTexture(): void;
-    initVideoTexture(): void;
-    drawFrame(_: any, info: any): void;
-    destroy(): void;
-    clearMemoryCache(): void;
-}
+import { VapConfig } from './type';
+import VapVideo from './video';
+export default class WebglRenderVap extends VapVideo {
+    private canvas;
+    private gl;
+    private vertexShader;
+    private fragmentShader;
+    private program;
+    private textures;
+    private buffers;
+    private vapFrameParser;
+    private aPosition;
+    private aTexCoord;
+    private aAlphaTexCoord;
+    private _imagePos;
+    constructor(options?: VapConfig);
+    play(options?: VapConfig): this;
+    initWebGL(): any;
+    /**
+     * 顶点着色器
+     */
+    initVertexShader(gl: WebGLRenderingContext): WebGLShader;
+    /**
+     * 片元着色器
+     */
+    initFragmentShader(gl: WebGLRenderingContext): WebGLShader;
+    initTexture(): void;
+    initVideoTexture(): void;
+    drawFrame(_: any, info: any): void;
+    clear(): void;
+    destroy(): void;
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 1123 - 3
web/package-lock.json


+ 8 - 1
web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "video-animation-player",
-  "version": "0.2.11",
+  "version": "1.0.1",
   "description": "webgl动画特效组件",
   "main": "dist/vap.js",
   "scripts": {
@@ -22,9 +22,16 @@
     "@babel/preset-env": "^7.11.5",
     "@babel/preset-typescript": "^7.10.4",
     "@rollup/plugin-node-resolve": "^9.0.0",
+    "@typescript-eslint/eslint-plugin": "5.4.0",
+    "@typescript-eslint/parser": "5.4.0",
     "babel-plugin-async-to-promises": "^1.0.5",
     "babel-preset-env": "^1.7.0",
     "babel-preset-typescript": "^7.0.0-alpha.19",
+    "eslint": "8.2.0",
+    "eslint-config-prettier": "8.3.0",
+    "eslint-plugin-import": "2.25.3",
+    "eslint-plugin-prettier": "4.0.0",
+    "prettier": "^2.7.1",
     "rollup": "^2.28.2",
     "rollup-plugin-babel": "^4.4.0",
     "rollup-plugin-commonjs": "^10.1.0",

+ 8 - 10
web/src/gl-util.ts

@@ -13,7 +13,7 @@
  * either express or implied. See the License for the specific language governing permissions and
  * limitations under the License.
  */
-export function createShader(gl, type, source) {
+export function createShader(gl: WebGLRenderingContext, type: number, source: string) {
   const shader = gl.createShader(type);
   gl.shaderSource(shader, source);
   gl.compileShader(shader);
@@ -23,7 +23,7 @@ export function createShader(gl, type, source) {
   return shader;
 }
 
-export function createProgram(gl, vertexShader, fragmentShader) {
+export function createProgram(gl: WebGLRenderingContext, vertexShader: WebGLShader, fragmentShader: WebGLShader) {
   const program = gl.createProgram();
   gl.attachShader(program, vertexShader);
   gl.attachShader(program, fragmentShader);
@@ -35,7 +35,7 @@ export function createProgram(gl, vertexShader, fragmentShader) {
   return program;
 }
 
-export function createTexture(gl, index:number, imgData?:TexImageSource) {
+export function createTexture(gl: WebGLRenderingContext, index: number, imgData?: TexImageSource) {
   const texture = gl.createTexture();
   const textrueIndex = gl.TEXTURE0 + index;
   gl.activeTexture(textrueIndex);
@@ -51,20 +51,18 @@ export function createTexture(gl, index:number, imgData?:TexImageSource) {
   return texture;
 }
 
-export function cleanWebGL(gl, shaders, program, textures, buffers) {
+export function cleanWebGL(gl: WebGLRenderingContext, { shaders = [], program = null, textures = [], buffers = [] }) {
   try {
-    gl.clear(gl.COLOR_BUFFER_BIT);
-    textures.forEach(t => {
+    textures.forEach((t) => {
       gl.deleteTexture(t);
     });
-    buffers.forEach(b => {
+    buffers.forEach((b) => {
       gl.deleteBuffer(b);
     });
-    shaders.forEach(shader => {
+    shaders.forEach((shader) => {
       gl.detachShader(program, shader);
       gl.deleteShader(shader);
     });
-    gl.clear(gl.COLOR_BUFFER_BIT);
-    gl.deleteProgram(program)
+    gl.deleteProgram(program);
   } catch (e) {}
 }

+ 6 - 6
web/src/index.ts

@@ -13,23 +13,23 @@
  * either express or implied. See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {VapConfig} from "./type";
-import WebglRenderVap from './webgl-render-vap'
-let isCanWebGL: Boolean;
+import { VapConfig } from './type';
+import WebglRenderVap from './webgl-render-vap';
+let isCanWebGL: boolean;
 /**
  * @param options
  * @constructor
  * @return {null}
  */
-export default function(options: VapConfig) {
+export default function (options?: VapConfig) {
   if (canWebGL()) {
-    return new WebglRenderVap(Object.assign({}, options));
+    return new WebglRenderVap(options);
   } else {
     throw new Error('your browser not support webgl');
   }
 }
 
-function canWebGL(): Boolean {
+export function canWebGL(): boolean {
   if (typeof isCanWebGL !== 'undefined') {
     return isCanWebGL;
   }

+ 7 - 5
web/src/type.ts

@@ -1,13 +1,15 @@
 export interface VapConfig {
   container: HTMLElement;
   src: string;
-  config: string | {[key:string]:any};
-  width: number;
-  height: number;
+  config: string | { [key: string]: any };
   fps?: number;
+  // 循环播放
+  loop: boolean;
   mute?: boolean;
   precache?: boolean;
+  // 使用requestVideoFrameCallback对齐帧数据
+  accurate: boolean;
   onLoadError?: (e: ErrorEvent) => void;
   onDestory?: () => void;
-  [key:string]:any;
-}
+  [key: string]: any;
+}

+ 48 - 39
web/src/vap-frame-parser.ts

@@ -18,74 +18,79 @@ export default class FrameParser {
     this.config = source || {};
     this.headData = headData;
     this.frame = [];
-    this.textureMap = {}
+    this.textureMap = {};
   }
 
-  private config;
+  public config;
   private headData;
   private frame;
-  private textureMap;
-  private canvas:HTMLCanvasElement;
-  private ctx:CanvasRenderingContext2D | null;
-  private srcData;
+  public textureMap;
+  private canvas: HTMLCanvasElement;
+  private ctx: CanvasRenderingContext2D | null;
+  public srcData;
 
   async init() {
-    this.initCanvas();
     // 判断是url还是json对象
-    if(/\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\.json$/.test(this.config)){
+    if (/\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\.json/.test(this.config)) {
       this.config = await this.getConfigBySrc(this.config);
     }
     await this.parseSrc(this.config);
-    this.canvas.parentNode.removeChild(this.canvas);
     this.frame = this.config.frame || [];
     return this;
   }
 
   initCanvas() {
-    const canvas = document.createElement('canvas');
-    const ctx = canvas.getContext('2d');
-    canvas.style.display = 'none';
-    document.body.appendChild(canvas);
-    this.ctx = ctx;
-    this.canvas = canvas;
+    if (!this.canvas) {
+      const canvas = document.createElement('canvas');
+      const ctx = canvas.getContext('2d');
+      canvas.style.display = 'none';
+      document.body.appendChild(canvas);
+      this.ctx = ctx;
+      this.canvas = canvas;
+    }
   }
 
-  loadImg(url:string) {
+  loadImg(url: string) {
     return new Promise((resolve, reject) => {
       // console.log('load img:', url)
       const img = new Image();
       img.crossOrigin = 'anonymous';
-      img.onload = function() {
+      img.onload = function () {
         resolve(this);
       };
-      img.onerror = function(e) {
+      img.onerror = function (e) {
         console.error('frame 资源加载失败:' + url);
         reject(new Error('frame 资源加载失败:' + url));
       };
       img.src = url;
-    })
+    });
   }
 
   parseSrc(dataJson) {
     const src = (this.srcData = {});
     return Promise.all(
-      (dataJson.src || []).map(async item => {
+      (dataJson.src || []).map(async (item) => {
         item.img = null;
         if (!this.headData[item.srcTag.slice(1, item.srcTag.length - 1)] && !this.headData[item.srcTag]) {
           console.warn(`vap: 融合信息没有传入:${item.srcTag}`);
         } else {
           if (item.srcType === 'txt') {
             if (this.headData['fontStyle'] && !item['fontStyle']) {
-              item['fontStyle'] = this.headData['fontStyle']
+              item['fontStyle'] = this.headData['fontStyle'];
             }
-            item.textStr = this.headData[item.srcTag] || item.srcTag.replace(/\[(.*)\]/, ($0, $1) => {
-              return this.headData[$1];
-            });
+            item.textStr =
+              this.headData[item.srcTag] ||
+              item.srcTag.replace(/\[(.*)\]/, ($0, $1) => {
+                return this.headData[$1];
+              });
+            this.initCanvas();
             item.img = this.makeTextImg(item);
           } else if (item.srcType === 'img') {
-            item.imgUrl = this.headData[item.srcTag] || item.srcTag.replace(/\[(.*)\]/, ($0, $1) => {
-              return this.headData[$1]
-            });
+            item.imgUrl =
+              this.headData[item.srcTag] ||
+              item.srcTag.replace(/\[(.*)\]/, ($0, $1) => {
+                return this.headData[$1];
+              });
             try {
               item.img = await this.loadImg(item.imgUrl);
             } catch (e) {}
@@ -95,7 +100,11 @@ export default class FrameParser {
           }
         }
       })
-    )
+    ).then(() => {
+      if (this.canvas) {
+        this.canvas.parentNode.removeChild(this.canvas);
+      }
+    });
   }
 
   /**
@@ -103,17 +112,17 @@ export default class FrameParser {
    * @param jsonUrl json外链
    * @returns {Promise}
    */
-  getConfigBySrc(jsonUrl:string) {
+  getConfigBySrc(jsonUrl: string) {
     return new Promise((resolve, reject) => {
       const xhr = new XMLHttpRequest();
-      xhr.open("GET", jsonUrl, true);
-      xhr.responseType = "json";
-      xhr.onload = function() {
-        if (xhr.status === 200 || xhr.status === 304 && xhr.response) {
+      xhr.open('GET', jsonUrl, true);
+      xhr.responseType = 'json';
+      xhr.onload = function () {
+        if (xhr.status === 200 || (xhr.status === 304 && xhr.response)) {
           const res = xhr.response;
           resolve(res);
         } else {
-          reject(new Error("http response invalid" + xhr.status));
+          reject(new Error('http response invalid' + xhr.status));
         }
       };
       xhr.send();
@@ -125,20 +134,20 @@ export default class FrameParser {
    * @param item
    */
   makeTextImg(item) {
-    const { textStr, w, h, color, style, fontStyle } = item
+    const { textStr, w, h, color, style, fontStyle } = item;
     const ctx = this.ctx;
     ctx.canvas.width = w;
     ctx.canvas.height = h;
     ctx.textBaseline = 'middle';
     ctx.textAlign = 'center';
-    const getFontStyle = function() {
+    const getFontStyle = function () {
       const fontSize = Math.min(w / textStr.length, h - 8); // 需留一定间隙
       const font = [`${fontSize}px`, 'Arial'];
       if (style === 'b') {
         font.unshift('bold');
       }
       return font.join(' ');
-    }
+    };
     if (!fontStyle) {
       ctx.font = getFontStyle();
       ctx.fillStyle = color;
@@ -159,8 +168,8 @@ export default class FrameParser {
     return ctx.getImageData(0, 0, w, h);
   }
   getFrame(frame) {
-    return this.frame.find(item => {
+    return this.frame.find((item) => {
       return item.i === frame;
-    })
+    });
   }
 }

+ 124 - 91
web/src/video.ts

@@ -13,10 +13,22 @@
  * either express or implied. See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {VapConfig} from "./type";
+import { VapConfig } from './type';
 
 export default class VapVideo {
-  constructor(options) {
+  public options: VapConfig;
+  public requestAnim: (cb: any) => number;
+  public container: HTMLElement;
+  public video: HTMLVideoElement;
+  protected events: { [key: string]: Array<(...info: any[]) => void> } = {};
+  private _drawFrame: () => void;
+  protected animId: number;
+  protected useFrameCallback: boolean;
+  private firstPlaying: boolean;
+  private setBegin: boolean;
+  private customEvent: Array<string> = ['frame', 'percentage', ''];
+
+  setOptions(options: VapConfig) {
     if (!options.container || !options.src) {
       console.warn('[Alpha video]: options container and src cannot be empty!');
     }
@@ -27,10 +39,6 @@ export default class VapVideo {
         // 循环播放
         loop: false,
         fps: 20,
-        // 视频宽度
-        width: 375,
-        // 视频高度
-        height: 375,
         // 容器
         container: null,
         // 是否预加载视频资源
@@ -44,55 +52,36 @@ export default class VapVideo {
       },
       options
     );
-    this.fps = 20;
     this.setBegin = true;
     this.useFrameCallback = false;
-    this.requestAnim = this.requestAnimFunc();
     this.container = this.options.container;
     if (!this.options.src || !this.options.config || !this.options.container) {
       console.error('参数出错:src(视频地址)、config(配置文件地址)、container(dom容器)');
-    } else {
-      // 创建video
-      this.initVideo();
     }
+    return this;
   }
 
-  public options:VapConfig;
-  private fps:number;
-  public requestAnim:Function;
-  public container:HTMLElement;
-  public video:HTMLVideoElement;
-  protected events;
-  private _drawFrame: Function;
-  protected animId: number;
-  protected useFrameCallback: boolean;
-  private firstPlaying: boolean;
-  private setBegin: boolean;
-  private customEvent: Array<string> = ['frame', 'percentage']
-
   precacheSource(source): Promise<string> {
     const URL = (window as any).webkitURL || window.URL;
     return new Promise((resolve, reject) => {
       const xhr = new XMLHttpRequest();
-      xhr.open("GET", source, true);
-      xhr.responseType = "blob";
-      xhr.onload = function() {
+      xhr.open('GET', source, true);
+      xhr.responseType = 'blob';
+      xhr.onload = function () {
         if (xhr.status === 200 || xhr.status === 304) {
           const res = xhr.response;
           if (/iphone|ipad|ipod/i.test(navigator.userAgent)) {
             const fileReader = new FileReader();
 
-            fileReader.onloadend = function() {
-              const resultStr = (fileReader.result as string);
-              const raw = atob(
-                resultStr.slice(resultStr.indexOf(",") + 1)
-              );
+            fileReader.onloadend = function () {
+              const resultStr = fileReader.result as string;
+              const raw = atob(resultStr.slice(resultStr.indexOf(',') + 1));
               const buf = Array(raw.length);
               for (let d = 0; d < raw.length; d++) {
                 buf[d] = raw.charCodeAt(d);
               }
               const arr = new Uint8Array(buf);
-              const blob = new Blob([arr], { type: "video/mp4" });
+              const blob = new Blob([arr], { type: 'video/mp4' });
               resolve(URL.createObjectURL(blob));
             };
             fileReader.readAsDataURL(xhr.response);
@@ -100,80 +89,90 @@ export default class VapVideo {
             resolve(URL.createObjectURL(res));
           }
         } else {
-          reject(new Error("http response invalid" + xhr.status));
+          reject(new Error('http response invalid' + xhr.status));
         }
       };
       xhr.send();
     });
   }
 
-
   initVideo() {
     const options = this.options;
     // 创建video
-    const video = (this.video = document.createElement('video'));
+    let video = this.video;
+    if (!video) {
+      video = this.video = document.createElement('video');
+    }
     video.crossOrigin = 'anonymous';
     video.autoplay = false;
     video.preload = 'auto';
-    video.setAttribute('playsinline','')
-    video.setAttribute('webkit-playsinline','')
-    if(options.mute){
+    video.setAttribute('playsinline', '');
+    video.setAttribute('webkit-playsinline', '');
+    if (options.mute) {
       video.muted = true;
       video.volume = 0;
     }
     video.style.display = 'none';
     video.loop = !!options.loop;
-    if(options.precache) {
+    if (options.precache) {
       this.precacheSource(options.src)
-        .then(blob => {
-          console.log("sample precached.");
+        .then((blob) => {
+          console.log('sample precached.');
           video.src = blob;
           document.body.appendChild(video);
         })
-        .catch(e=>{
+        .catch((e) => {
           console.error(e);
         });
-    }else{
+    } else {
       video.src = options.src;
       // 这里要插在body上,避免container移动带来无法播放的问题
       document.body.appendChild(this.video);
       video.load();
     }
-    if ( 'requestVideoFrameCallback' in this.video ) {
+
+    if ('requestVideoFrameCallback' in this.video) {
       this.useFrameCallback = !!this.options.accurate;
     }
+    this.cancelRequestAnimation();
+
     // 绑定事件
-    this.events = {}
-    ;['playing', 'pause', 'ended', 'error', 'canplay'].forEach(item => {
+    this.offAll();
+    ['playing', 'error', 'canplay'].forEach((item) => {
       this.on(item, this['on' + item].bind(this));
-    })
+    });
   }
-  drawFrame(_, info) {
-    this._drawFrame = this._drawFrame || this.drawFrame.bind(this, _, info);
-    if ( this.useFrameCallback ) {
+
+  drawFrame(_, _info) {
+    this._drawFrame = this._drawFrame || this.drawFrame.bind(this);
+    if (this.useFrameCallback) {
       // @ts-ignore
-      this.animId = this.video.requestVideoFrameCallback( this.drawFrame.bind(this) );
+      this.animId = this.video.requestVideoFrameCallback(this._drawFrame);
     } else {
       this.animId = this.requestAnim(this._drawFrame);
     }
   }
 
   play() {
-    const prom = this.video && this.video.play();
+    if (this.useFrameCallback) {
+      // @ts-ignore
+      this.animId = this.video.requestVideoFrameCallback(this.drawFrame.bind(this));
+    } else {
+      this.requestAnim = this.requestAnimFunc();
+    }
 
+    const prom = this.video && this.video.play();
     if (prom && prom.then) {
-      prom.catch(e => {
+      prom.catch((e) => {
         if (!this.video) {
           return;
         }
         this.video.muted = true;
         this.video.volume = 0;
-        this.video.play().catch(e => {
-          (this.events.error || []).forEach(item => {
-            item(e);
-          })
-        })
-      })
+        this.video.play().catch((e) => {
+          this.trigger('error', e);
+        });
+      });
     }
   }
 
@@ -183,85 +182,119 @@ export default class VapVideo {
 
   setTime(t) {
     if (this.video) {
-      this.video.currentTime = t
+      this.video.currentTime = t;
     }
   }
 
   requestAnimFunc() {
-    const me = this;
+    const { fps = 30 } = this.options;
     if (window.requestAnimationFrame) {
       let index = -1;
-      return function(cb) {
+      return (cb) => {
         index++;
         return requestAnimationFrame(() => {
-          if (!(index % (60 / me.fps))) {
+          if (!(index % (60 / fps))) {
             return cb();
           }
-          me.animId = me.requestAnim(cb);
-        })
-      }
-    }
-    return function(cb) {
-      return setTimeout(cb, 1000 / me.fps)
+          this.animId = this.requestAnim(cb);
+        });
+      };
     }
+    return function (cb) {
+      return window.setTimeout(cb, 1000 / fps);
+    };
   }
 
   cancelRequestAnimation() {
+    if (!this.animId) {
+      return;
+    }
     if (this.useFrameCallback) {
       try {
         // @ts-ignore
         this.video.cancelVideoFrameCallback(this.animId);
-      } catch (e) {}
-    }else if (window.cancelAnimationFrame) {
+      } catch (e) {
+        console.error(e);
+      }
+    } else if (window.cancelAnimationFrame) {
       cancelAnimationFrame(this.animId);
     } else {
       clearTimeout(this.animId);
     }
+    this.animId = 0;
+  }
+
+  clear() {
+    this.cancelRequestAnimation();
   }
 
   destroy() {
     this.cancelRequestAnimation();
     if (this.video) {
+      this.offAll();
       this.video.parentNode && this.video.parentNode.removeChild(this.video);
-      this.video = null
+      this.video = null;
     }
-    this.options.onDestory && this.options.onDestory();
-  }
-
-  clear() {
-    this.destroy();
+    this.options.onDestroy && this.options.onDestroy();
   }
 
-  on(event, callback:any) {
+  on(event, callback: any) {
     const cbs = this.events[event] || [];
     cbs.push(callback);
     this.events[event] = cbs;
     if (this.customEvent.indexOf(event) === -1) {
       this.video.addEventListener(event, callback);
     }
-    return this
+    return this;
+  }
+
+  once(event, callback: any) {
+    const once = (...e) => {
+      const cbs = this.events[event];
+      cbs.splice(cbs.indexOf(once), 1);
+      this.video.removeEventListener(event, once);
+      callback(...e);
+    };
+    return this.on(event, once);
+  }
+
+  trigger(eventName, ...e) {
+    try {
+      (this.events[eventName] || []).forEach((item) => {
+        item(...e);
+      });
+    } catch (e) {
+      console.error(e);
+    }
+  }
+
+  offAll() {
+    Object.keys(this.events).forEach((name) => {
+      const cbs = this.events[name];
+      if (cbs && cbs.length) {
+        cbs.forEach((cb) => {
+          this.video.removeEventListener(name, cb);
+        });
+      }
+    });
+    this.events = {};
+    return this;
   }
 
   onplaying() {
     if (!this.firstPlaying) {
       this.firstPlaying = true;
-      if ( !this.useFrameCallback ) {
-        this.drawFrame(null, null)
+      if (!this.useFrameCallback) {
+        this.drawFrame(null, null);
       }
     }
   }
 
-  onpause() {}
-
-  onended() {
-    this.destroy();
-  }
-
   oncanplay() {
-    const begin = this.options.beginPoint
+    const begin = this.options.beginPoint;
     if (begin && this.setBegin) {
-      this.setBegin = false
-      this.video.currentTime = begin
+      this.setBegin = false;
+      this.video.currentTime = begin;
     }
   }
 

+ 175 - 140
web/src/webgl-render-vap.ts

@@ -13,117 +13,124 @@
  * either express or implied. See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {VapConfig} from "./type";
+import { VapConfig } from './type';
 import VapFrameParser from './vap-frame-parser';
 import * as glUtil from './gl-util';
 import VapVideo from './video';
 
+type ResourceCache = {
+  canvas: HTMLCanvasElement;
+  gl: WebGLRenderingContext;
+  vertexShader: WebGLShader;
+  fragmentShader: WebGLShader;
+  program: WebGLProgram;
+};
+
 let clearTimer = null;
-let instances = {};
+let cachedInstance: ResourceCache = null;
 const PER_SIZE = 9;
 
-function computeCoord(x:number, y:number, w:number, h:number, vw:number, vh:number) {
+function computeCoord(x: number, y: number, w: number, h: number, vw: number, vh: number) {
   // leftX rightX bottomY topY
-  return [x / vw, (x + w) / vw, (vh - y - h) / vh, (vh - y) / vh]
+  return [x / vw, (x + w) / vw, (vh - y - h) / vh, (vh - y) / vh];
 }
 
 export default class WebglRenderVap extends VapVideo {
-  constructor(options:VapConfig) {
-    super(options);
-    if ( this.useFrameCallback ) {
-      // @ts-ignore
-      this.animId = this.video.requestVideoFrameCallback( this.drawFrame.bind(this) );
-    }
-    this.insType = this.options.type;
-    if (instances[this.insType]) {
-      this.instance = instances[this.insType]
-    } else {
-      this.instance = instances[this.insType] = {}
+  private canvas: HTMLCanvasElement;
+  private gl: WebGLRenderingContext;
+  private vertexShader: WebGLShader;
+  private fragmentShader: WebGLShader;
+  private program: WebGLProgram;
+  private textures: Array<WebGLTexture> = [];
+  private buffers: Array<WebGLBuffer> = [];
+  private vapFrameParser: VapFrameParser;
+  private aPosition: number;
+  private aTexCoord: number;
+  private aAlphaTexCoord: number;
+  private _imagePos: WebGLUniformLocation;
+
+  constructor(options?: VapConfig) {
+    super();
+    if (options) {
+      this.play(options);
     }
-    this.textures = [];
-    this.buffers = [];
-    this.shaders = [];
-    this.init();
   }
 
-  private insType;
-  private textures;
-  private buffers;
-  private shaders;
-  private vapFrameParser;
-  private resources;
-  private instance;
-  private program;
-  private videoTexture;
-  private aPosition;
-  private aTexCoord;
-  private aAlphaTexCoord;
-  private _imagePos;
-
-  async init() {
-    this.setCanvas();
-    if (this.options.config) {
-      try {
-        this.vapFrameParser = await new VapFrameParser(this.options.config, this.options).init();
-        this.resources = this.vapFrameParser.srcData;
-      } catch (e) {
-        console.error('[Alpha video] parse vap frame error.', e);
-      }
+  play(options?: VapConfig) {
+    if (options) {
+      this.setOptions(options);
     }
-    this.resources = this.resources || {};
-    this.initWebGL();
-    this.play();
-  }
-  setCanvas() {
-    let canvas = this.instance.canvas;
-    const { width, height } = this.options;
-    if (!canvas) {
-      canvas = this.instance.canvas = document.createElement('canvas');
+    if (!this.options?.config) {
+      console.error(`options.config cannot be empty.`);
+      return this;
     }
-    canvas.width = width;
-    canvas.height = height;
-    this.container.appendChild(canvas);
+    if (options) {
+      this.initVideo();
+      // 重新解析
+      this.vapFrameParser = new VapFrameParser(this.options.config, this.options);
+      this.vapFrameParser
+        .init()
+        .then(() => {
+          this.initWebGL();
+          this.initTexture();
+          this.initVideoTexture();
+          this.options.fps = this.vapFrameParser.config.info.fps || 30;
+          super.play();
+        })
+        .catch((e) => {
+          this.vapFrameParser = null;
+          console.error('[Alpha video] parse vap frame error.', e);
+          return this;
+        });
+    } else {
+      super.play();
+    }
+    return this;
   }
 
   initWebGL() {
-    const { canvas } = this.instance;
-    let { gl, vertexShader, fragmentShader, program } = this.instance;
+    let { canvas, gl, vertexShader, fragmentShader, program } = (cachedInstance || this) as any;
+    // 防止被其他实例访问
+    cachedInstance = null;
+
     if (!canvas) {
-      return
+      canvas = document.createElement('canvas');
     }
+    const { w, h } = this.vapFrameParser.config.info;
+    canvas.width = w;
+    canvas.height = h;
+    this.container.appendChild(canvas);
+
     if (!gl) {
-      this.instance.gl = gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
+      gl = canvas.getContext('webgl') || (canvas.getContext('experimental-webgl') as WebGLRenderingContext);
       gl.disable(gl.BLEND);
       gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
       gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
     }
+    gl.viewport(0, 0, w, h);
 
-    // 清除界面,解决同类型type切换MP4时,第一帧是上一个mp4最后一帧的问题
-    gl.clear(gl.COLOR_BUFFER_BIT);
-
-    if (gl) {
-      gl.viewport(0, 0, canvas.width, canvas.height);
-      if (!vertexShader) {
-        vertexShader = this.instance.vertexShader = this.initVertexShader();
-      }
-      if (!fragmentShader) {
-        fragmentShader = this.instance.fragmentShader = this.initFragmentShader();
-      }
-      if (!program) {
-        program = this.instance.program = glUtil.createProgram(gl, vertexShader, fragmentShader);
-      }
-      this.program = program;
-      this.initTexture();
-      this.initVideoTexture();
-      return gl;
+    if (!vertexShader) {
+      vertexShader = this.initVertexShader(gl);
+    }
+    if (!fragmentShader) {
+      fragmentShader = this.initFragmentShader(gl);
     }
+    if (!program) {
+      program = glUtil.createProgram(gl, vertexShader, fragmentShader);
+    }
+
+    this.canvas = canvas;
+    this.gl = gl;
+    this.vertexShader = vertexShader;
+    this.fragmentShader = fragmentShader;
+    this.program = program;
+    return gl;
   }
 
   /**
    * 顶点着色器
    */
-  initVertexShader() {
-    const { gl } = this.instance;
+  initVertexShader(gl: WebGLRenderingContext) {
     return glUtil.createShader(
       gl,
       gl.VERTEX_SHADER,
@@ -143,8 +150,7 @@ export default class WebglRenderVap extends VapVideo {
   /**
    * 片元着色器
    */
-  initFragmentShader() {
-    const { gl } = this.instance;
+  initFragmentShader(gl: WebGLRenderingContext) {
     const bgColor = `vec4(texture2D(u_image_video, v_texcoord).rgb, texture2D(u_image_video,v_alpha_texCoord).r);`;
     const textureSize = gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) - 1;
     // const textureSize =0
@@ -158,8 +164,8 @@ export default class WebglRenderVap extends VapVideo {
           `if(ndx == ${i + 1}){
                         color = texture2D(u_image${i + 1},uv);
                     }`
-        )
-        samplers.push(`uniform sampler2D u_image${i + 1};`)
+        );
+        samplers.push(`uniform sampler2D u_image${i + 1};`);
       }
 
       sourceUniform = `
@@ -207,7 +213,7 @@ export default class WebglRenderVap extends VapVideo {
             `;
     }
 
-    const fragmentSharder = `
+    const fragmentShader = `
         precision lowp float;
         varying vec2 v_texcoord;
         varying vec2 v_alpha_texCoord;
@@ -221,14 +227,18 @@ export default class WebglRenderVap extends VapVideo {
             gl_FragColor = bgColor;
         }
         `;
-    return glUtil.createShader(gl, gl.FRAGMENT_SHADER, fragmentSharder)
+    return glUtil.createShader(gl, gl.FRAGMENT_SHADER, fragmentShader);
   }
 
   initTexture() {
-    const { gl } = this.instance;
+    const { gl } = this;
     let i = 1;
     if (!this.vapFrameParser || !this.vapFrameParser.srcData) {
-      return
+      return;
+    }
+    if (this.textures.length) {
+      glUtil.cleanWebGL(this.gl, { textures: this.textures });
+      this.textures = [];
     }
     const resources = this.vapFrameParser.srcData;
     for (const key in resources) {
@@ -242,18 +252,22 @@ export default class WebglRenderVap extends VapVideo {
     gl.activeTexture(gl.TEXTURE0);
     gl.bindTexture(gl.TEXTURE_2D, dumpTexture);
     gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
-
-    this.videoTexture = glUtil.createTexture(gl, i);
+    // video texture
+    this.textures.push(glUtil.createTexture(gl, i));
     const sampler = gl.getUniformLocation(this.program, `u_image_video`);
     gl.uniform1i(sampler, i);
   }
 
   initVideoTexture() {
-    const { gl } = this.instance;
+    const { gl } = this;
+    if (this.buffers.length) {
+      glUtil.cleanWebGL(gl, { buffers: this.buffers });
+      this.buffers = [];
+    }
     const vertexBuffer = gl.createBuffer();
     this.buffers.push(vertexBuffer);
     if (!this.vapFrameParser || !this.vapFrameParser.config || !this.vapFrameParser.config.info) {
-      return
+      return;
     }
     const info = this.vapFrameParser.config.info;
     const ver = [];
@@ -284,71 +298,92 @@ export default class WebglRenderVap extends VapVideo {
   }
 
   drawFrame(_, info) {
-    const timePoint = (info && info.mediaTime >= 0) ? info.mediaTime : this.video.currentTime
-    const frame = info && info.presentedFrames > 0 ? info.presentedFrames - 1 : Math.round(timePoint * this.options.fps) + this.options.offset;
-    const frameCbs = this.events['frame'] || []
-    frameCbs.forEach(cb => {
-      cb(frame + 1, timePoint)
-    })
-    const gl = this.instance.gl;
+    const { gl } = this;
     if (!gl) {
       super.drawFrame(_, info);
-      return
+      return;
     }
-    gl.clear(gl.COLOR_BUFFER_BIT);
-    if (this.vapFrameParser) {
-      const frameData = this.vapFrameParser.getFrame(frame);
-      let posArr = [];
 
-      if (frameData && frameData.obj) {
-        frameData.obj.forEach((frame, index) => {
-          posArr[posArr.length] = +this.vapFrameParser.textureMap[frame.srcId];
+    const frame =
+      !this.options.loop && info?.presentedFrames > 0
+        ? info.presentedFrames - 1
+        : Math.round(this.video.currentTime * this.options.fps) + this.options.offset;
+    // console.info('frame:', info.presentedFrames - 1, Math.round(this.video.currentTime * this.options.fps));
+    const frameData = this.vapFrameParser.getFrame(frame);
+    let posArr = [];
 
-          const info = this.vapFrameParser.config.info;
-          const { videoW: vW, videoH: vH } = info;
-          // frame坐标是最终展示坐标,这里glsl中计算使用视频坐标
-          const [rgbX, rgbY] = info.rgbFrame;
-          const [x, y, w, h] = frame.frame;
-          const [mX, mY, mW, mH] = frame.mFrame;
-          const coord = computeCoord(x + rgbX, y + rgbY, w, h, vW, vH);
-          const mCoord = computeCoord(mX, mY, mW, mH, vW, vH);
-          posArr = posArr.concat(coord).concat(mCoord);
-        })
-      }
-      //
-      const size = (gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) - 1) * PER_SIZE;
-      posArr = posArr.concat(new Array(size - posArr.length).fill(0));
-      this._imagePos = this._imagePos || gl.getUniformLocation(this.program, 'image_pos');
-      gl.uniform1fv(this._imagePos, new Float32Array(posArr));
+    if (frameData && frameData.obj) {
+      const { videoW: vW, videoH: vH, rgbFrame } = this.vapFrameParser.config.info;
+      frameData.obj.forEach((frame) => {
+        posArr[posArr.length] = +this.vapFrameParser.textureMap[frame.srcId];
+
+        // frame坐标是最终展示坐标,这里glsl中计算使用视频坐标
+        const [rgbX, rgbY] = rgbFrame;
+        const [x, y, w, h] = frame.frame;
+        const [mX, mY, mW, mH] = frame.mFrame;
+        const coord = computeCoord(x + rgbX, y + rgbY, w, h, vW, vH);
+        const mCoord = computeCoord(mX, mY, mW, mH, vW, vH);
+        posArr = posArr.concat(coord).concat(mCoord);
+      });
     }
+
+    this.trigger('frame', frame + 1, frameData, this.vapFrameParser.config);
+    gl.clear(gl.COLOR_BUFFER_BIT);
+    const size = (gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) - 1) * PER_SIZE;
+    posArr = posArr.concat(new Array(size - posArr.length).fill(0));
+    this._imagePos = this._imagePos || gl.getUniformLocation(this.program, 'image_pos');
+    gl.uniform1fv(this._imagePos, new Float32Array(posArr));
     gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.video); // 指定二维纹理方式
     gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
     super.drawFrame(_, info);
   }
 
-  destroy() {
-    const { canvas, gl } = this.instance;
-    if (this.textures && this.textures.length) {
-      for (let i = 0; i < this.textures.length; i++) {
-        gl.deleteTexture(this.textures[i]);
-      }
-    }
-    if (canvas) {
-      canvas.parentNode && canvas.parentNode.removeChild(canvas);
-    }
+  // 清理数据,为下一次播放做准备
+  clear() {
+    super.clear();
+    const { gl, textures, buffers } = this;
+    glUtil.cleanWebGL(gl, { textures, buffers });
+    // 清除界面,解决同类型type切换MP4时,第一帧是上一个mp4最后一帧的问题
     gl.clear(gl.COLOR_BUFFER_BIT);
-    // glUtil.cleanWebGL(gl, this.shaders, this.program, this.textures, this.buffers)
-    super.destroy();
-    this.clearMemoryCache();
+    this.textures = [];
+    this.buffers = [];
   }
 
-  clearMemoryCache() {
-    if (clearTimer) {
-      clearTimeout(clearTimer);
+  // 销毁,释放webgl资源,销毁后调用play,资源会重新初始化
+  destroy() {
+    const { canvas, gl, vertexShader, fragmentShader, program } = this;
+    if (gl) {
+      this.clear();
+      if (canvas) {
+        canvas.parentNode && canvas.parentNode.removeChild(canvas);
+      }
+      this.canvas = null;
+      this.gl = null;
+      this.vertexShader = null;
+      this.fragmentShader = null;
+      this.program = null;
+      super.destroy();
+      clearMemoryCache({ canvas, gl, vertexShader, fragmentShader, program });
     }
+  }
+}
 
-    clearTimer = setTimeout(() => {
-      instances = {};
-    }, 30 * 60 * 1000);
+function clearMemoryCache(instance) {
+  if (clearTimer) {
+    clearTimeout(clearTimer);
+  }
+  if (cachedInstance) {
+    glUtil.cleanWebGL(cachedInstance.gl, {
+      program: cachedInstance.program,
+      shaders: [cachedInstance.vertexShader, cachedInstance.fragmentShader],
+    });
   }
+  cachedInstance = instance;
+  clearTimer = setTimeout(() => {
+    glUtil.cleanWebGL(cachedInstance.gl, {
+      program: cachedInstance.program,
+      shaders: [cachedInstance.vertexShader, cachedInstance.fragmentShader],
+    });
+    cachedInstance = null;
+  }, 30 * 60 * 1000);
 }

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor