Source: index.js

import Promise from 'jraiser/promise/1.2/promise';
import PubSub from 'jraiser/pubsub/1.2/pubsub';

import UploadManager from './upload';
import { Queue } from './queue';
import { Pool } from './pool';
import {
  generateFileData,
  isContainFileMimeType
} from './utils';

// 队列的上传状态
const STATUS = {
  NOT_STARTED: 'notStarted',
  UPLOADING: 'uploading',
};

class PlvVideoUpload extends PubSub {
  /**
   * 封装一个上传视频文件到polyv云空间的插件
   * @example
   * const videoUpload = new PlvVideoUpload({
   *  events: {
   *    Error: (err) => { console.log(err); },
   *    UploadComplete: () => {}
   *  }
   * });
   * // 更新用户数据(由于sign等用户信息有效期为3分钟,需要每隔3分钟更新一次)
   * videoUpload.updateUserData({
   *  userid: data.userid,
   *  ptime: data.ts,
   *  sign: data.sign,
   *  hash: data.hash
   * });
   * // 开始上传所有文件
   * videoUpload.startAll();
   * // 添加文件到上传列表
   * const uploader = videoUpload.addFile(file, {
   *  FileStarted: () => {},
   *  FileStopped: () => {}
   * }, fileSetting);
   * // 暂停上传指定文件
   * videoUpload.stopFile(uploader.id);
   * @param {Object} [config] 用户设置
   * @param {Object} config.events 事件回调。包括Error、UploadComplete
   */
  constructor(config = {}) {
    super(config.events);

    this.config = {
      partSize: config.partSize, // 分片大小,不能小于100k。单位为Bytes。默认按文件大小自动分片
      threadCount: config.threadCount, // 上传并发线程数
      retryCount: config.retryCount, // 网络原因失败时,重新上传次数
      acceptedMimeType: config.acceptedMimeType, // 用户自定义在一定范围内允许上传的文件类型
      region: config.region || 'line1' // 上传线路, 默认line1, 华南
    };

    let parallelFileLimit = config.parallelFileLimit || 5; // 并行上传的文件数目;最大5个
    if (parallelFileLimit < 0 || parallelFileLimit > 5) {
      parallelFileLimit = 5;
    }

    // 文件队列
    this.fileQueue = new Queue();

    // 已添加但未允许开始上传的队列(未点击开始按钮或是暂停状态)
    this.waitQueue = new Queue();

    // 上传队列
    this.uploadPool = new Pool(uploader => uploader._start(), parallelFileLimit);

    this.userData = {};

    // 未添加then的promise数组,监控各视频文件的上传情况
    this.newUploadPromiseList = [];

    // 整个文件队列的上传状态
    this.status = STATUS.NOT_STARTED;
  }

  /**
   * 更新sign、ptime等授权验证信息,授权验证信息3分钟内有效
   * @param {UserData} userData
   */
  updateUserData(userData) {
    // 通过不改变对象的地址来保证可以同时更新所有UploadManager实例里面的userData属性
    for (const key in userData) {
      this.userData[key] = userData[key];
    }
  }

  /**
   * 修改指定文件的文件信息
   * @param {String} uploaderid UploadManager实例的id
   * @param {FileData} fileData 文件信息
   */
  updateFileData(uploaderid, fileData) {
    if (typeof fileData !== 'object') {
      return;
    }

    // 文件已经开始上传或已上传完毕,禁止修改文件信息
    const uploader = this.waitQueue.find(uploaderid);
    if (!uploader || uploader.statusCode !== 1) {
      this._emitError({
        code: 112,
        message: '文件已经开始上传或已上传完毕,禁止修改文件信息',
        data: {
          uploaderid
        }
      });
      return;
    }

    uploader.updateFileData(fileData);
  }

  /**
   * 添加文件到文件列表
   * @param {File} file 文件对象
   * @param {
     Object
   } [events] 事件回调。 包括FileStarted、 FileStopped、 FileSucceed、 FileProgress、 FileFailed
   * @param {Object} [fileSetting] 针对该文件的设置
   * @param {String} fileSetting.desc 文件描述
   * @param {Number} fileSetting.cataid=1 分类目录id
   * @param {String} fileSetting.tag 文件标签,不同标签之间使用英文逗号分隔
   * @param {Number} fileSetting.luping=0 开启视频课件优化处理,对于上传录屏类视频清晰度有所优化:0为不开启,1为开启
   * @param {Number} fileSetting.keepsource=0 源文件播放(不对源文件进行编码):0为编码,1为不编码
   * @param {String} fileSetting.title=file.name 文件名称
   * @param {} fileSetting.state 自定义信息,会在上传完成的回调中返回
   * @return {UploadManager}
   */
  addFile(file, events = {}, fileSetting = {}) {
    const fileData = generateFileData(file, fileSetting, this.userData);

    // 拦截重复文件
    if (this.fileQueue.find(fileData.id)) {
      this._emitError({
        code: 110,
        message: '文件重复',
        data: {
          filename: fileData.title,
        }
      });
      return null;
    }

    // 拦截文件类型不在acceptedMimeType中的文件
    if (!isContainFileMimeType(file, this.config.acceptedMimeType)) {
      this._emitError({
        code: 111,
        message: '文件类型错误',
        data: {
          filename: fileData.title
        }
      });
      return null;
    }

    this.fileQueue.enqueue(fileData);
    const uploader = new UploadManager(this.userData, fileData, events, this.config);
    if (this.status === STATUS.NOT_STARTED) {
      this.waitQueue.enqueue(uploader);
    } else {
      this.newUploadPromiseList.push(this.uploadPool.enqueue(uploader));
    }
    /** @type {UploadManager} */
    return uploader;
  }

  /**
   * 删除指定文件
   * @param {String} id 文件id,和对应的UploadManager实例的id一致
   */
  removeFile(id) {
    const uploader = this.uploadPool.remove(id);
    if (uploader) {
      uploader.isDeleted = true;
      uploader._stop();
    } else {
      // 同一个文件不能同时存在于waitQueue和uploadPool
      this.waitQueue.remove(id);
    }
    this.fileQueue.remove(id);
  }

  /**
   * 开始/继续上传指定文件
   * @param {String} id 文件id,和对应的UploadManager实例的id一致
   */
  resumeFile(id) {
    const uploader = this.waitQueue.remove(id);
    if (!uploader) {
      return;
    }
    this.newUploadPromiseList.push(this.uploadPool.enqueue(uploader));
    if (this.status === STATUS.NOT_STARTED) {
      this._onPromiseEnd();
    }
  }

  /**
   * 暂停上传指定文件
   * @param {String} id 文件id
   */
  stopFile(id) {
    const uploader = this.uploadPool.remove(id);
    if (uploader) {
      uploader._stop();
      this.waitQueue.enqueue(uploader);
    }
  }

  /**
   * 清空文件列表
   */
  clearAll() {
    this._stopAll(true); // 此时已清空uploadPool
    this.waitQueue.clear();
    this.fileQueue.clear();
  }

  /**
   * 开始上传所有文件
   */
  startAll() {
    while (this.waitQueue.size > 0) {
      const uploader = this.waitQueue.dequeue();
      if (uploader.statusCode !== -1) {
        this.newUploadPromiseList.push(this.uploadPool.enqueue(uploader));
      }
    }
    this.status = STATUS.UPLOADING;
    this._onPromiseEnd();
  }

  /**
   * 停止上传所有文件
   */
  stopAll() {
    this._stopAll();
  }

  // 停止上传,根据参数决定是否删除该文件
  _stopAll(isDeleted = false) {
    while (this.uploadPool.size > 0) {
      const uploader = this.uploadPool.dequeue();
      if (isDeleted) {
        uploader.isDeleted = true;
      }
      uploader._stop();
      // 未开始上传的文件按原顺序放到waitQueue
      if (uploader.statusCode === 1) {
        this.waitQueue.enqueue(uploader);
      }
    }
    this.status = STATUS.NOT_STARTED;
  }

  // 监听所有上传promise
  _onPromiseEnd() {
    const uploadPromiseList = [...this.newUploadPromiseList];
    this.newUploadPromiseList = [];

    // 判断所有文件上传是否结束
    Promise.all(uploadPromiseList)
      .then(() => {
        if (this.newUploadPromiseList.length > 0) { // 还有未监听到的promise
          this._onPromiseEnd();
        } else if (this.uploadPool.size === 0) {
          this.status = STATUS.NOT_STARTED;

          if (this.waitQueue.size === 0 && this.fileQueue.size !== 0) {

            /**
             * 所有文件上传结束
             * @fires PlvVideoUpload#UploadComplete
             */
            this.trigger('UploadComplete');
          }
        }
      });

    // 处理文件上传状态发生改变或上传报错的情况
    for (let i = 0; i < uploadPromiseList.length; i++) {
      uploadPromiseList[i]
        .then(res => {
          if (!res || !res.code) {
            return;
          }
          this._handleUploadStatusChange(res);
        })
        .catch(err => {
          this._emitError(err);
        });
    }
  }

  // 文件上传状态发生改变
  _handleUploadStatusChange(res) {
    const data = res.data;
    switch (res.code) {
      case 100: { // 完成上传
        this.waitQueue.remove(data.id);
        break;
      }
      case 102: { // 用户剩余空间不足
        this.waitQueue.unshift(data.uploader);
        this._emitError(res);
        break;
      }
      case 101: // init请求失败,或,multipartUpload上传失败
      case 104: // 暂停上传
      case 105: { // Multipart Upload ID 不存在
        if (data.uploader && !data.uploader.isDeleted) {
          this.waitQueue.unshift(data.uploader);
        }
        break;
      }
      case 106: // token过期,正在重试
      case 107: { // 上传错误,正在重试
        this.newUploadPromiseList.push(this.uploadPool.enqueue(data.uploader));
        if (this.status === STATUS.NOT_STARTED) {
          this._onPromiseEnd();
        }
        break;
      }
      default:
        break;
    }
  }

  _emitError(err) {
    /**
     * 文件上传出错
     * @fires PlvVideoUpload#Error
     */
    this.trigger('Error', err);
  }

  /**
   * 获取上传文件列表
   * @type {FileData[]}
   */
  get files() {
    return this.fileQueue.list;
  }
}

/**
 * @typedef {Object} ErrorData
 * @property {String} type - 错误类型
 * @property {String} message - 错误信息
 * @property {Number} code - 错误代码
 */

/**
 * @typedef {Object} FileData
 * @property {String} desc - 视频文件的描述内容
 * @property {Number} cataid=1 - 上传目录id
 * @property {String} tag - 指定视频文件的标签
 * @property {Number} luping=0 - 开启视频课件优化处理,对于上传录屏类视频清晰度有所优化:0为不开启,1为开启
 * @property {Number} keepsource=0 - 源文件播放(不对源文件进行编码):0为编码,1为不编码
 * @property {file} file - 文件对象(只读)
 * @property {String} title - 文件名称
 * @property {Number} size - 文件大小,单位Bytes(只读)
 * @property {Number} filesize - 文件大小,单位Bytes(只读)
 * @property {String} vid - vid(只读)
 * @property {} state - 自定义信息,会在上传完成的回调中返回
 */

/**
 * @typedef {Object} UserData
 * @property {String} userid - [主账号]userid。需要在点播后台中获取。
 * @property {Number} ptime - [主账号]13位的毫秒级时间戳
 * @property {String} sign - [主账号]校验值其一,计算方式:md5(`${secretkey}${ptime}`)。secretkey需要在点播后台中获取。
 * @property {String} hash - [主账号]校验值其二,计算方式:md5(`${ptime}${writeToken}`)。writeToken需要在点播后台中获取。
 * @property {String} appId - [子账号]appId。需要在点播后台中获取。
 * @property {Number} timestamp - [子账号]13位的毫秒级时间戳
 * @property {String} sign - [子账号]校验值,计算方式:md5(`${secretkey}appId${appId}timestamp${timestamp}${secretkey}`).toUpperCase()。secretkey和appId需要在点播后台中获取。
 * @description 主账号/子账号的用户信息及校验值。这里的校验值有一定的时间期限,需要使用{@link PlvVideoUpload#updateUserData}方法每隔3分钟更新一次所有参数。
 */

/**
 * @typedef {Object} UploadManager
 * @property {String} id - 每个上传实例的唯一标识
 */

/**
 * 所有文件上传完成时触发。
 * @event PlvVideoUpload#UploadComplete
 */

/**
 * 上传过程出错时触发。
 * @event PlvVideoUpload#Error
 * @type {Object}
 * @property {Number} code 错误代码(110:文件重复,111:拦截文件类型不在acceptedMimeType中的文件,112:文件已经开始上传或已上传完毕,禁止修改文件信息,102:用户剩余空间不足)
 * @property {String} message 错误信息
 * @property {Object} data
 */

/**
 * 文件开始上传时触发。
 * @event PlvVideoUpload#FileStarted
 * @type {Object}
 * @property {String} uploaderid 触发事件的UploadManager的id
 * @property {FileData} fileData 文件信息
 */

/**
 * 文件暂停上传时触发。
 * @event PlvVideoUpload#FileStopped
 * @type {Object}
 * @property {String} uploaderid 触发事件的UploadManager的id
 * @property {FileData} fileData 文件信息
 */

/**
 * 文件上传过程返回上传进度信息时触发。
 * @event PlvVideoUpload#FileProgress
 * @type {Object}
 * @property {String} uploaderid 触发事件的UploadManager的id
 * @property {FileData} fileData 文件信息
 * @property {Number} progress 上传进度,范围为0~1
 */

/**
 * 文件上传成功时触发。
 * @event PlvVideoUpload#FileSucceed
 * @type {Object}
 * @property {String} uploaderid 触发事件的UploadManager的id
 * @property {FileData} fileData 文件信息
 */

/**
 * 文件上传失败时触发。
 * @event PlvVideoUpload#FileFailed
 * @property {String} uploaderid 触发事件的UploadManager的id
 * @property {FileData} fileData 文件信息
 * @property {ErrorData} errData 报错信息
 */

export default PlvVideoUpload;