实现HTML5的video标签视频播放器

HTML5的video标签

  video标签提供了直接在网页上播放视频的方式,摆脱了flash插件。让实现变得更简单,只是video标签兼容性还有些问题:不兼容ie8及以下版本的浏览器。

video视频播放器功能简介

  在这里简单做了一个video播放器。介绍一下功能吧。

  • 可以自定义播放控件,把自带的标准控件关掉,配合提供的函数就可以实现
  • 指定播放位置
  • 改变音量
  • 改变播放窗口大小
  • 改变播放速度(暂且只有 Chrome 和 Safari 支持)
  • 设置与取消静音
  • 各种事件监听(如:正在播放时,播放结束时,声音改变时…)
  • 主要提供了以下函数
player.play()				//播放
player.pause()				//暂停
player.volume(value)		//设置或者返回音量,不传参数则返回当前音量
player.currentTime(value)	//设置或者返回当前播放位置
player.reloadFromURL(url)	//从指定路径加载视频资源
player.seekTo(value)		//视频跳到指定位置开始播放
player.size(width,height)	//设置或者返回视频播放窗口的宽和高
player.loop(value)			//设置或返回视频是否应该在结束时再次播放,即循环播放
player.getFormatPlayTime(d,c)//获取格式化的已播放时长与总时长 如: 00:00 / 03:20
player.muted(value)			//设置或者返回视频是否应该被静音
player.getCanPlayType()		//返回浏览器最有可能支持的视频格式
player.timeupdate(callback)	//监听播放走动
player.timeEnded(callback)	//监听播放结束
player.seekedEvent(callback)//监听跳转指定位置完成
player.canplay(callback)	//监听视频能够播放时
... ...                     还有其他函数以及事件监听

创建实例示范

let videoPlayer = new VideoPlayer({
	selector: '#video',
	url:'./resources/1', // 只需要文件名,不需要格式。只提供三种格式:mp4,ogg,webm
	volume:0.5,		  // 声音的值为 0 - 1
	events: {
		timeupdate: function(){
		},
		timeEnded: function(){
		}
	}
});

创建实例的配置项说明

  • 格式: 除了 selectorurl 必填,其他全部可选
config = {
      selector: string,   // 视频元素选择器 (必填)
      url: string,        // 视频资源url   (必填)
      volume: number,     // 音量:0 - 1
      controls: boolean,  // true:打开控件,false:关闭控件(默认)
      loop: boolean,      // true:循环播放,false:不循环播放(默认)
      autoplay: boolean,  // true:加载完成后自动播放,false:加载完成后不自动播放(默认)
      muted: boolean,     // true:静音,false:不静音(默认)
      preload: number,    // 0:页面加载则开始加载视频,1:页面加载后仅加载视频的元数据,2:页面加载后不应加载视频
      events: {
        timeupdate: function,     // 监听 timeupdate 事件。当视频播放时触发。callback:传入:当前播放位置,已播放时长与总时长的比例,时间字符串,是否播放结束
        timeEnded: function,      // 监听 ended 事件。当视频播放结束时触发
        abortEvent: function,     // 监听 abort 事件。当视频的加载已放弃时触发
        waitingEvent: function,   // 监听 waiting 事件 。当视频由于需要缓冲下一帧而停止触发。callback:传入:当前播放位置
        seekingEvent: function,   // 监听 seeking 事件 。当用户开始跳跃到视频中的新位置时触发。callback:传入:当前播放位置
        seekedEvent: function,    // 监听 seeked 事件 。当用户已经移动/跳跃到视频中的新位置时触发。callback:传入:当前播放位置
        stalledEvent: function,   // 监听 stalled 事件 。当浏览器尝试获取媒体数据,但数据不可用时触发。callback:传入:当前播放位置
        playEvent: function,      // 监听 play 事件 。当视频已开始或不再暂停时触发。callback:传入:当前播放位置
        pauseEvent: function,     // 监听 pause 事件 。当视频由于需要缓冲下一帧而停止时触发。callback:传入:当前播放位置
        loadstart: function,      // 监听 loadstart 事件。当视频加载过程开始时触发
        durationchange: function, // 监听 durationchange 事件。当视频总时长发生改变时触发。callback:传入:视频总时长
        loadedmetadata: function, // 监听 loadedmetadata 事件。当视频的元数据已加载时触发
        loadeddata: function,     // 监听 loadeddata 事件。当当前帧的数据已加载。但没有足够的数据来播放指定视频的下一帧时触发
        progress: function,       // 监听 progress 事件。当浏览器下载视频时触发
        canplay: function,        // 监听 canplay 事件。当视频能够播放时触发
        canplaythrough: function, // 监听 canplaythrough 事件。当浏览器预计能够在不停下来进行缓冲的情况下持续播放指定的音频/视频时触发
      }
    }

JS源码

  • 注意:此js文件依赖 jQuery ,此处使用的是 jQuery 1.11.0 版本
  • 可直接添加jQuery的cdn引用:<script src="https://cdn.bootcss.com/jquery/1.11.0/jquery.min.js" type="text/javascript" charset="utf-8"></script>
;(function($, win){
  
  /**
   * @description 视频播放器
   * @param {object} config - 视频播放器的配置项
   * @returns new VideoPlayer()
   */
  function VideoPlayer(config)
  {
    return new VideoPlayer.prototype.init(config);
  }

  VideoPlayer.prototype = {
    constructor: VideoPlayer,
    /**
     * @description 初始化视频播放器
     * @param {object} config - 视频播放器的配置项
     * @returns void
     */
    init: function init(config){
      this.config = config;
      this.$video = $(config.selector).eq(0);
      this.video = this.$video.get(0);
      this._voice = 0.5;

      this._configure(config);
    },

    /**
     * @description 根据配置项设置播放器
     * @param {object} config - 视频播放器的配置项
     * @returns void
     */
    _configure: function _configure(config){
      // config: selector,url,volume,controls,loop,autoplay,muted,preload,timeupdate-callback
      for (const key of Object.keys(config)) {
        switch (key) {
          // 设置属性
          case 'url':             this.reloadFromURL(config[key]);  break;
          case 'volume':          this.volume(config[key]);         break;
          case 'controls':        this.controls(config[key]);       break;
          case 'loop':            this.loop(config[key]);           break;
          case 'autoplay':        this.autoplay(config[key]);       break;
          case 'muted':           this.muted(config[key]);          break;
          case 'preload':         this.preload(config[key]);        break;
          // 下面是添加事件监听
          case 'events':          this._addEvent(config[key]);       break;
          default:  break;
        }
      }
    },

    /**
     * @description 添加事件监听
     * @param {object} events - 事件监听处理函数集
     */
    _addEvent: function _addEvent(events){
      for (const eventName of Object.keys(events)) {
        switch (eventName) {
          case 'timeupdate':      this.timeupdate(events[eventName]);       break;
          case 'timeEnded':       this.timeEnded(events[eventName]);        break;
          case 'abortEvent':      this.abortEvent(events[eventName]);       break;
          case 'waitingEvent':    this.waitingEvent(events[eventName]);     break;
          case 'seekingEvent':    this.seekingEvent(events[eventName]);     break;
          case 'seekedEvent':     this.seekedEvent(events[eventName]);      break;
          case 'stalledEvent':    this.stalledEvent(events[eventName]);     break;
          case 'playEvent':       this.playEvent(events[eventName]);        break;
          case 'pauseEvent':      this.pauseEvent(events[eventName]);       break;
          case 'loadstart':       this.loadstart(events[eventName]);        break;
          case 'durationchange':  this.durationchange(events[eventName]);   break;
          case 'loadedmetadata':  this.loadedmetadata(events[eventName]);   break;
          case 'loadeddata':      this.loadeddata(events[eventName]);       break;
          case 'progress':        this.progress(events[eventName]);         break;
          case 'canplay':         this.canplay(events[eventName]);          break;
          case 'canplaythrough':  this.canplaythrough(events[eventName]);   break;
          default:  break;
        }
      }
    },

    /**
     * @description 设置或返回视频是否应该在结束时再次播放
     * @param {boolean} isLoop - true(循环播放) | false(默认,不循环播放)
     * @returns boolean : true(循环播放) | false(默认,不循环播放)
     */
    loop: function (isLoop){
      if(isLoop === undefined) {
        return this.video.loop;
      }
      this.video.loop = Boolean(isLoop);
      this.load();
    },

    /**
     * @description 设置或返回是否在加载完成后随即自动播放视频
     * @param {boolean} isAutoplay - true(自动播放) | false(默认,不自动播放)
     * @returns boolean - true(自动播放) | false(默认,不自动播放)
     */
    autoplay: function (isAutoplay){
      if(isAutoplay === undefined){
        return this.video.autoplay;
      }
      this.video.autoplay = Boolean(isAutoplay);
      this.load();
    },

    /**
     * @description 设置视频是否默认静音。暂且只有 Google Chrome 支持 defaultMuted 属性
     * @param {boolean} isDefaultMuted - true(默认静音) | false(默认,视频默认不是静音的)
     * @returns void
     */
    setDefaultMuted: function (isDefaultMuted){
      this.video.defaultMuted = Boolean(isDefaultMuted);
    },

    /**
     * @description 设置或返回视频的当前播放速度。暂且只有 Google Chrome 和 Safari 支持 playbackRate 属性
     * @param {number} speed - 例值:1.0:正常速度;        0.5:半速(更慢;   2.0:倍速(更快);
     *                              -1.0:向后,正常速度;  -0.5:向后,半速
     * @returns void
     */
    playSpeed: function (speed){
      if(speed === undefined) {
        return this.video.playbackRate;
      }
      this.video.playbackRate = speed;
    },

    /**
     * @description 返回已缓冲视频的时间范围。如果用户在视频中跳跃播放,会得到多个缓冲范围。
     * @returns array - 例:只有一个缓冲范围:[{start:0,end:5}]
     */
    buffered: function (){
      let bufferdArr = [];
      // video对象的 TimeRanges 对象。TimeRanges 对象表示用户的已缓冲视频的时间范围
      // TimeRanges 对象属性:length - 获得音视频中已缓冲范围的数量。start(index) - 获得某个已缓冲范围的开始位置。end(index) - 获得某个已缓冲范围的结束位置
      let buff = this.video.buffered; 
      let len = buff.length;
      for(let i = 0; i < len; i++) {
        bufferdArr.push({
          start: buff.start(i),
          end: buff.end(i),
        });
      }
      return bufferdArr;
    },

    /**
     * @description 返回视频中可寻址(移动播放位置)的时间范围
     * @returns array - 例:只有一个缓冲范围:[{start:0,end:5}]
     */
    seekable: function (){
      let seekableArr = [];
      // seekable 属性返回 TimeRanges 对象
      let seekable = this.video.seekable; 
      let len = seekable.length;
      for(let i = 0; i < len; i++) {
        seekableArr.push({
          start: seekable.start(i),
          end: seekable.end(i),
        });
      }
      return seekableArr;
    },

    /**
     * @description 设置或者返回视频是否应该被静音(关闭声音)
     * @param {boolean} isMuted - true(静音) | false(默认,视频不静音)
     * @returns boolean : true(静音) | false(默认,视频不静音)
     */
    muted: function (isMuted){
      if(isMuted === undefined) {
        return this.video.muted;
      }
      this.video.muted = Boolean(isMuted);
    },

    /**
     * @description 设置或者返回视频当前播放位置,以秒计
     * @param {number} time - 指定播放位置
     * @returns number - 当前播放位置
     */
    currentTime: function (time){
      if(time === undefined) {
        return this.video.currentTime;
      }
      if(time>=0 && time<=this.getDuration()) {
        this.video.currentTime = time;
      }
    },

    /**
     * @description 设置或者返回是否在页面加载后立即加载视频
     * @param {number} value - 可选值为:0:指示一旦页面加载,则开始加载视频。
     *                                  1:指示当页面加载后仅加载视频的元数据。
     *                                  2:指示页面加载后不应加载视频。
     * @returns string:auto | metadata | none
     */
    preload: function (value){
      if(value === undefined) {
        return this.video.preload;
      }
      if(value>=0 && value<=2) {
        this.video.preload = ['auto','metadata','none'][value];
        this.load();
      }
    },

    /**
     * @description 设置或者返回视频播放窗口的宽和高
     * @param {number} width 
     * @param {number} height 
     * @returns - object: width , height
     */
    size: function (width, height){
      if(width && height) {
        this.video.setAttribute('width', width+'px');
        this.video.setAttribute('height', height+'px');
        return;
      } else if(width && !height) {
        this.video.setAttribute('width', width+'px');
        this.video.setAttribute('height', width+'px');
        return;
      }

      let _this = this;
      return {
        width: _this.video.getAttribute('width'),
        height: _this.video.getAttribute('height'),
      };
    },

    /**
     * @description 设置或者返回视频的当前音量
     * @param {number} volumeValue - 必须是介于 0.0 与 1.0 之间的数字
     * @returns number - 音量
     */
    volume: function (volumeValue){
      if(volumeValue === undefined) {
        return this.video.volume;
      }

      volumeValue = volumeValue<0 ? 0 : (volumeValue>1 ? 1 : volumeValue);
      this.video.volume = volumeValue;
      this._voice = volumeValue;// 保存声音
      return volumeValue;
    },

    /**
     * @description 返回视频是否已经播放结束
     * @returns boolean - true:已播放结束,false:未播放结束
     */
    getEnded: function (){
      return this.video.ended;
    },
    
    /**
     * @description 返回视频是否已经暂停
     * @returns boolean - true:视频已暂停, false:视频未暂停
     */
    getPaused: function (){
      return this.video.paused;
    },

    /**
     * @description 返回当前视频的 url
     * @returns string - url
     */
    getCurrentSrc: function (){
      return this.video.currentSrc;
    },

    /**
     * @description 返回当前视频的长度,以秒计
     * @returns number - 当前视频的长度
     */
    getDuration: function (){
      return this.video.duration;
    },

    /**
     * @description 设置或者返回浏览器是否显示标准的视频控件
     * @param {boolean} isShowControls -  true:显示控件。false:默认,不显示控件
     * @returns boolean - true:显示控件。false:默认,不显示控件
     */
    controls: function (isShowControls){
      if(isShowControls === undefined) {
        return this.video.controls;
      }
      this.video.controls = Boolean(isShowControls);
    },

    /**
     * 获取格式化的已播放时长与总时长 如: 00:00 / 03:20
     * @param {number} duration - 视频总时长
     * @param {number} currentTime - 视频已播放时长
     * @returns string - 返回格式化时间字符串
     */
    getFormatPlayTime: function(duration,currentTime){
      // 获取总时长的分钟数 和 秒数
      let endMunite = parseInt(duration / 60),
          endSecond = parseInt(duration % 60);

      let currentMunite = parseInt(currentTime / 60),
          currentSecond = parseInt(currentTime % 60);
      // 时长小于10,使用 0 站位
      endMunite = endMunite < 10 ? '0'+endMunite : endMunite;
      endSecond = endSecond < 10 ? '0'+endSecond : endSecond;

      currentMunite = currentMunite < 10 ? '0'+currentMunite : currentMunite;
      currentSecond = currentSecond < 10 ? '0'+currentSecond : currentSecond;

      return currentMunite+':'+currentSecond+' / '+endMunite+':'+endSecond;
    },

    /**
     * @description 从指定路径加载视频资源
     * @param {string} url - 视频资源路径
     * @returns void
     */
    reloadFromURL: function (url){
      let srcSet = this.$video.find('source');
      let trueURL = '';
      try {
        $.each(srcSet, function (index, value){
          let $src = $(value);
          switch ($src.attr('type')) {
            case "video/mp4":  trueURL = url+'.mp4';   break;
            case "video/ogg":  trueURL = url+'.ogg';   break;
            case "video/webm": trueURL = url+'.webm';  break;
            default: break;
          }
          $src.attr('src', trueURL);
        });
      } catch (error) {
        console.error('加载视频资源错误',error);
      }

      this.load();
    },

    /**
     * @description 返回浏览器最有可能支持的视频格式,只检测 mp4,ogg,webm 三种视频格式
     * @returns object:mp4,ogg,webm
     */
    getCanPlayType: function (){
      let _this = this;
      let can = {
        mp4: false,
        ogg: false,
        webm: false,
      };
      let videoType = ['video/mp4','video/ogg','video/webm'],
          videoTypeCodecs = ["avc1.4D401E, mp4a.40.2","theora, vorbis","vp8.0, vorbis"];
      
      videoType.forEach(function (index, value){
        let type = value.slice(value.indexOf('/')+1);
        let support=_this.video.canPlayType(value+';codecs="'+videoTypeCodecs[index]+'"');
        if(support==="probably" || support==="maybe") {
          can[type] = true;
        } else { // support === ''
          can[type] = false;
        }
      });

      return can;
    },

    /**
     * @description 监听 timeupdate 事件。当视频播放走动时,会触发 timeupdate 事件
     * @param {function} callback - 视频播放走动回调函数,传入:当前播放位置,已播放时长与总时长的比例,时间字符串,是否播放结束
     * @returns void
     */
    timeupdate: function (callback){
      let _this = this,
          duration = 0,
          currentTime = 0,
          timeStr = '',
          scale = 0,
          isEnd = false;
      // 为 video 添加 timeupdate 事件 - 播放时间变化事件
      this.video.addEventListener('timeupdate',function(){
        // 获取已播放时长 和 总时长
        currentTime = _this.currentTime();
        duration = _this.getDuration();
        // 已播放时长与总时长的时间格式化字符串
        timeStr = _this.getFormatPlayTime(duration, currentTime);
        // 歌曲是否播放结束
        isEnd =  _this.getEnded();
        // 计算比例,保存两位小数
        scale = parseInt((currentTime/duration)*100) / 100;

        // 回调函数传入当前播放时刻, 比例 和 时间格式化字符串,是否播放结束
        callback(currentTime, scale, timeStr, isEnd);
      });
    },

    /**
     * @description 监听 ended 事件 。当视频播放结束时,会触发 ended 事件
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    timeEnded: function (callback){
      let _this = this;
      this.video.addEventListener('ended',function(){
        callback();
      });
    },

    /**
     * @description 监听 abort 事件 。当音频/视频的加载已放弃时 abort 事件
     * @param {function} callback - 事件回调函数。
     * @returns void
     */
    abortEvent: function (callback){
      let _this = this;
      this.video.addEventListener('abort',function(){
        callback();
      });
    },

    /**
     * @description 监听 volumechange 事件 。当音量已更改时,会触发 volumechange 事件
     * @param {function} callback - 事件回调函数。传入:当前音量
     * @returns void
     */
    volumechange: function (callback){
      let _this = this;
      this.video.addEventListener('volumechange',function(){
        callback(_this.volume());
      });
    },

    /**
     * @description 监听 waiting 事件 。当视频由于需要缓冲下一帧而停止,会触发 waiting 事件
     * @param {function} callback - 事件回调函数。传入:当前播放位置
     * @returns void
     */
    waitingEvent: function (callback){
      let _this = this;
      this.video.addEventListener('waiting',function(){
        callback(_this.currentTime());
      });
    },

    /**
     * @description 监听 seeking 事件 。当用户开始跳跃到视频中的新位置时,会触发 seeking 事件
     * @param {function} callback - 事件回调函数。传入:当前播放位置
     * @returns void
     */
    seekingEvent: function (callback){
      let _this = this;
      this.video.addEventListener('seeking',function(){
        callback(_this.currentTime());
      });
    },
    
    /**
     * @description 监听 seeked 事件 。当用户已经移动/跳跃到视频中的新位置时,会触发 seeked 事件
     * @param {function} callback - 事件回调函数。传入:当前播放位置
     * @returns void
     */
    seekedEvent: function (callback){
      let _this = this;
      this.video.addEventListener('seeked',function(){
        callback(_this.currentTime());
      });
    },
    
    /**
     * @description 监听 stalled 事件 。当浏览器尝试获取媒体数据,但数据不可用时,会触发 stalled 事件
     * @param {function} callback - 事件回调函数。传入:当前播放位置
     * @returns void
     */
    stalledEvent: function (callback){
      let _this = this;
      this.video.addEventListener('stalled',function(){
        callback(_this.currentTime());
      });
    },

    /**
     * @description 监听 play 事件 。当视频已开始或不再暂停时,会触发 play 事件
     * @param {function} callback - 事件回调函数。传入:当前播放位置
     * @returns void
     */
    playEvent: function (callback){
      let _this = this;
      this.video.addEventListener('play',function(){
        callback(_this.currentTime());
      });
    },

    /**
     * @description 监听 pause 事件 。当视频由于需要缓冲下一帧而停止,会触发 pause 事件
     * @param {function} callback - 事件回调函数。传入:当前播放位置
     * @returns void
     */
    pauseEvent: function (callback){
      let _this = this;
      this.video.addEventListener('pause',function(){
        callback(_this.currentTime());
      });
    },

    /*
      当视频处于加载过程中时,会依次发生以下事件:
        loadstart
        durationchange
        loadedmetadata
        loadeddata
        progress
        canplay
        canplaythrough
      注释:Internet Explorer 8 或更早的浏览器不支持此系列事件
    */

    /**
     * @description 监听 loadstart 事件 。当浏览器开始寻找指定的视频时,会发生 loadstart 事件。即当加载过程开始时。
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    loadstart: function (callback){
      let _this = this;
      this.video.addEventListener('loadstart',function(){
        callback();
      });
    },

    /**
     * @description 监听 durationchange 事件 。当指定视频的时长数据发生变化时,发生 durationchange 事件。视频加载后,时长将由 "NaN" 变为音频/视频的实际时长
     * @param {function} callback - 事件回调函数:传入:视频总时长
     * @returns void
     */
    durationchange: function (callback){
      let _this = this;
      this.video.addEventListener('durationchange',function(){
        callback(_this.getDuration());
      });
    },

    /**
     * @description 监听 loadedmetadata 事件 。当指定的视频的元数据已加载时,会发生 loadedmetadata 事件。视频的元数据包括:时长、尺寸(仅视频)以及文本轨道。
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    loadedmetadata: function (callback){
      let _this = this;
      this.video.addEventListener('loadedmetadata',function(){
        callback();
      });
    },

    /**
     * @description 监听 loadeddata 事件 。当当前帧的数据已加载,但没有足够的数据来播放指定视频的下一帧时,会发生 loadeddata 事件。
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    loadeddata: function (callback){
      let _this = this;
      this.video.addEventListener('loadeddata',function(){
        callback();
      });
    },

    /**
     * @description 监听 progress 事件。当浏览器正在下载指定的视频时,会发生 progress 事件
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    progress: function (callback){
      let _this = this;
      this.video.addEventListener('progress',function(){
        callback();
      });
    },

    /**
     * @description 监听 canplay 事件。当浏览器能够开始播放指定的视频时,当跳转到指定播放位置时,会发生 canplay 事件。
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    canplay: function (callback){
      let _this = this;
      this.video.addEventListener('canplay',function(){
        callback();
      });
    },

    /**
     * @description 监听 canplaythrough 事件。当浏览器预计能够在不停下来进行缓冲的情况下持续播放指定的视频时,会发生 canplaythrough 事件
     * @param {function} callback - 事件回调函数
     * @returns void
     */
    canplaythrough: function (callback){
      let _this = this;
      this.video.addEventListener('canplaythrough',function(){
        callback();
      });
    },

    /**
     * @description 视频跳到指定位置开始播放
     * @param {number} scale 介于 0 - 1 之间的小数
     * @returns number - 跳转到的位置
     */
    seekTo: function (scale){
      if(typeof scale !== 'number') {
        return;
      }
      let cur = 0;
      scale = scale<0 ? 0 : (scale>1 ? 1 : scale);
      cur = this.getDuration() * scale;
      this.currentTime(cur);
      return cur;
    },

     /**
     * @description 重新加载url视频资源,或者更新视频元素设置
     * @returns void
     */
    load: function (){
      this.video.load();
    },

    /**
     * @description 播放
     * @returns void
     */
    play: function (){
      this.video.play();
    },

    /**
     * @description 暂停播放
     * @returns void
     */
    pause: function (){
      this.video.pause();
    }
  };

  VideoPlayer.prototype.init.prototype = VideoPlayer.prototype;

  win.VideoPlayer = VideoPlayer;
  
}(window.jQuery, window));
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值