tcplayer 源码改造第四弹 -> 字幕(srt)

文章目录

  • 前序
    • 简介
    • 人群
    • git地址
  • 源码改造tcplayer.js(各位客官请自行格式化代码)
    • 修改思路
    • 添加配置参数
    • 在视频中加入字幕
      • 添加显示字幕内容的节点
      • 修改字幕内容节点的样式
    • 在底部栏加上字幕切换按钮
      • 复制切换清晰度的代码,并修改
      • 加入字幕按钮
  • 使用说明
    • 参数说明
    • 使用示例
  • 相关推荐

前序

简介

  • 主要介绍了基于tcplayer的源码改造,加入字幕功能(仅支持srt字幕文件)
  • 不涉及tcplayer的使用以及框架如何调用,详情请看腾讯云点播文档
  • 源码解析中有些注释是笔者加的,如需定位,请不要复制注释
  • 以下示例的代码为重新混淆压缩过,可能与原来的tcplayer.js函数名不同,不可直接复制使用,请务必跟着笔者一步步执行

人群

  • 不想自己写播放器而使用tcplayer,但是又受限于播放器本身不带有字幕功能的开发人员
  • 不适合没有任何前端基础的小白,请谨慎观看

git地址

https://github.com/HaverLee1/hls-player

源码改造tcplayer.js(各位客官请自行格式化代码)

修改思路

  • 在视频中加入字幕
    • 浏览器中打开播放器页面,打开开发者工具,定位到视频播放的节点,可以看到视频的根节点是"vcp-player",video标签就是该节点的下一级节点,所以可以考虑在节点"vcp-player"下加一个节点,作为video的同层节点来显示字幕文字
      tcplayer 源码改造第四弹 -> 字幕(srt)_第1张图片
  • 在底部栏加上字幕切换按钮
    • 在开发者工具中定位到底部栏,可以看到底部栏的节点是"vcp-controls-panel",所以考虑在底部栏下加入一个新节点,作为字幕的切换按钮
    • 由于字幕的操作和切换清晰度的操作类似,则可以直接拷贝清晰度的代码,加以修改,如图中的节点"vcp-clarityswitcher"
    • tcplayer 源码改造第四弹 -> 字幕(srt)_第2张图片

添加配置参数

在代码中定位videoSource,在第一个的位置,即初始化赋值的同层如下参数(带有注释的则是笔者加入的参数)

t.TcPlayer = function (e) {
      function t(i, o) {
        n(this, t);
        var s = l(o);
        M = ["od", "hd", "sd"];
        var a = {
          owner: i,
          videoSource: s,
          src: s.curUrl,
          autoplay: o.autoplay,
          live: o.live,
          flash: o.flash,
          flashUrl: o.flashUrl,
          poster: o.poster,
          width: o.width,
          height: o.height,
          volume: o.volume,
          listener: o.listener,
          wording: o.wording,
          controls: o.controls,
          clarity: o.clarity,
          clarityLabel: o.clarityLabel,
          showLoading: "boolean" != typeof o.showLoading || o.showLoading,
          pausePosterEnabled: void 0 === o.pausePosterEnabled || o.pausePosterEnabled,
          fullscreenEnabled: void 0 === o.fuScrnEnabled || o.fuScrnEnabled,
          systemFullscreen: o.systemFullscreen || !1,
          hls: o.hls || "0.12.4",
          h5_flv: o.h5_flv,
          x5_player: o.x5_player !== !1,
          x5_type: o.x5_type,
          x5_fullscreen: o.x5_fullscreen,
          x5_orientation: o.x5_orientation,
          x5_playsinline: o.x5_playsinline,
          preload: o.preload || "auto",
          hlsConfig: o.hlsConfig,
          flvConfig: o.flvConfig,
          // 是否展示字幕
          subtitle_display: o.subtitle_display !== !1,
          // 字幕文件 srt类型
          subtitle_srt: o.subtitle_srt,
          // 字幕文件字体大小
          subtitle_fontsize: o.subtitle_fontsize ? o.subtitle_fontsize : '16px',
          // 全屏 -> 字幕文件字体大小
          subtitle_fullscreen_fontsize: o.subtitle_fullscreen_fontsize ? o.subtitle_fullscreen_fontsize : '32px',
        };
        return r(this, e.call(this, a))
      }

在视频中加入字幕

添加显示字幕内容的节点

在tcplayer.js中定位字符串"vcp-player",可以定位到如下代码:

t.Player = function () {
      function e(t) {
        r(this, e), this.options = t, this.ready = !1, this.hasPlay = !1;
        var i = t.owner;
        return i ? (this.guid = x.guid(), this.listener = this.options.listener, d.sub("*", "*", x.bind(this, this.handleMsg), this), i = P.get(i), this.mtaReport = new L["default"](this, this.options), void this.render(i)) : console.error("Player need a container")
      }

      return e.prototype.render = function (e) {
        var t = "vcp-player";
        if (C.TOUCH_ENABLED && (t += " touchable"), this.el = P.createEl("div", {"class": t}), e.appendChild(this.el), this.errortips = new T["default"](this), this.errortips.render(this.el), this.loading = new E["default"](this), this.loading.render(this.el), this.options.width = this.options.width || e.offsetWidth, this.options.height = this.options.height || e.offsetHeight, this.size(this.options.width, this.options.height), !this.verifyOptions()) {
          return this.listener({type: "error", code: 5}), x.console.error("create failed")
        }

修改如下:

t.Player = function () {
      function e(t) {
        r(this, e), this.options = t, this.ready = !1, this.hasPlay = !1;
        var i = t.owner;
        return i ? (this.guid = x.guid(), this.listener = this.options.listener, d.sub("*", "*", x.bind(this, this.handleMsg), this), i = P.get(i), this.mtaReport = new L["default"](this, this.options), void this.render(i)) : console.error("Player need a container")
      }

      // 字符串转浮点数
      function toSeconds(t) {
        var s = 0.0;

        if (t) {
          var p = t.split(':');
          for (i = 0; i < p.length; i++) {
            s = s * 60 + parseFloat(p[i].replace(',', '.'));
          }
        }

        return s;
      }

      /**
       * 把 SRT 格式的字幕文件解析为字幕的对象数组,格式为:
       * [
       *      {sn: "0", startTime: 0.89, endTime: 7.89, content: "这里是一系列与Hadoop↵有关的其他开源项目:"},
       *      {sn: "1", startTime: 8.38, endTime: 14.85, content: "Eclipse是一个IBM贡献到开源社区里的集成开发环境(IDE)。"}
       * ]
       *
       * @param  srt 字幕文件的内容
       * @return Array
       */
      function parseSrtSubtitles(srt) {
        var subtitles = [];
        var textSubtitles = srt.replace(/\r/g, '').split('\n\n'); // 每条字幕的信息,包含了序号,时间,字幕内容

        for (var i = 0; i < textSubtitles.length; ++i) {
          var textSubtitle = textSubtitles[i].split('\n');

          if (textSubtitle.length >= 2) {
            var sn = textSubtitle[0], textSubtitleArray = textSubtitle[1].trim().split(' --> '); // 字幕的序号
            var startTime = toSeconds(textSubtitleArray[0]); // 字幕的开始时间
            var endTime = toSeconds(textSubtitleArray[1]); // 字幕的结束时间
            var content = textSubtitle[2]; // 字幕的内容

            // 字幕可能有多行
            if (textSubtitle.length > 2) {
              for (var j = 3; j < textSubtitle.length; j++) {
                content += '\n' + textSubtitle[j];
              }
            }

            // 字幕对象
            var subtitle = {
              sn: sn,
              startTime: startTime,
              endTime: endTime,
              content: content
            };

            subtitles.push(subtitle);
          }
        }

        return subtitles;
      }

      // 获取当前时间对应的字幕,存在当前时间没有字幕的情况
      function getCurrentSubtitle(currentTime, display) {
        // 如果不展示字幕,则不显示字幕
        if (!display) {
          return "";
        }
        let item = subtitlesArray[subtitleIndex];
        if (item.startTime <= currentTime && currentTime < item.endTime) {
          return item.content;
        }
        // 按顺序播放,大于当前字幕结束时间时,会小于下一个字幕的结束时间
        if (currentTime > item.endTime) {
          subtitleIndex ++;
          item = subtitlesArray[subtitleIndex];
          if (currentTime < item.startTime) {
            // 在[current.endTime, (current+1).startTime)区间内没有字幕
            return "";
          } else if (item.startTime <= currentTime && currentTime < item.endTime) {
            return item.content;
          }
        }
        // 判断当前的时间是否还在第一句话前
        if (currentTime < subtitlesArray[0].startTime) {
          return "";
        }
        // 判断当前的时间是否还在第一句话
        if (currentTime < subtitlesArray[0].endTime) {
          return subtitlesArray[0].content;
        }
        const length = subtitlesArray.length;
        // 判断当前的时间是否已经大于了字幕最后一句话的结束时间
        if (currentTime > subtitlesArray[length - 1].endTime) {
          return "";
        }
        // 跨度查询,减少查询时间
        const step = 10;
        if (length <= step) {
          for (let i = 0; i < length; i++) {
            const item = subtitlesArray[i];
            if (item.startTime <= currentTime && currentTime < item.endTime) {
              subtitleIndex = i;
              return item.content;
            }
            // 按顺序播放,大于当前字幕结束时间时,会小于下一个字幕的结束时间
            if (currentTime > item.endTime && currentTime < subtitlesArray[i + 1].startTime) {
              // 在[current.endTime, (current+1).startTime)区间内没有字幕
              return "";
            }
          }
        } else {
          for (let i = 0; i < length; i += step) {
            // 判断当前字幕的结束时间是否大于了当前时间
            if (currentTime < subtitlesArray[i].endTime) {
              // 是的话,则进行小范围遍历,直到找到对应的索引
              for (let j = i - step; j < i; j++) {
                const item = subtitlesArray[j];
                if (item.startTime <= currentTime && currentTime < item.endTime) {
                  subtitleIndex = j;
                  return item.content;
                }
                // 按顺序播放,大于当前字幕结束时间时,会小于下一个字幕的结束时间
                if (currentTime > item.endTime && currentTime < subtitlesArray[j + 1].startTime) {
                  // 在[current.endTime, (current+1).startTime)区间内没有字幕
                  return "";
                }
              }
            }
          }
          // 若没有找到对应的时间索引,则所当前时间处于[length-10:length-1]
          for (let i = length - step; i < length; i++) {
            const item = subtitlesArray[i];
            if (item.startTime <= currentTime && currentTime < item.endTime) {
              subtitleIndex = i;
              return item.content;
            }
            // 按顺序播放,大于当前字幕结束时间时,会小于下一个字幕的结束时间
            if (currentTime > item.endTime && currentTime < subtitlesArray[i + 1].startTime) {
              // 在[current.endTime, (current+1).startTime)区间内没有字幕
              return "";
            }
          }
        }
      }

      return e.prototype.render = function (e) {
        var t = "vcp-player", o = this.options, subtitleElment = P.createEl("span", {"class": "oneweek-video-subtitle", "id": "oneweek-video-subtitle"});
        // 加载字幕
        if (o.subtitle_srt) {
          subtitleElment.style.fontSize = o.subtitle_fontsize;
          // 防止onreadystatechange被调用多次
          let hasReceived = false;
          const httpRequest = new XMLHttpRequest();//第一步:建立所需的对象
          httpRequest.open('GET', o.subtitle_srt);//第二步:打开连接  将请求参数写在url中
          httpRequest.send();//第三步:发送请求  将请求参数写在URL中
          /**
           * 获取数据后的处理程序
           */
          httpRequest.onreadystatechange = function () {
            if (httpRequest.status === 200 && httpRequest.responseText && !hasReceived) {
              hasReceived = true;
              subtitlesArray = parseSrtSubtitles(httpRequest.responseText);
              subtitleElment.innerHTML = getCurrentSubtitle(i.currentTime(), o.subtitle_display);
              // 定时切换字幕
              setInterval(() => {
                subtitleElment.innerHTML = getCurrentSubtitle(i.currentTime(), o.subtitle_display);
              }, 500);
            }
          };
        }
        if (C.TOUCH_ENABLED && (t += " touchable"), this.el = P.createEl("div", {"class": t}),
          // 添加防盗录节点span
          this.el.appendChild(spanElment),
          this.el.appendChild(subtitleElment),
          e.appendChild(this.el), this.errortips = new T["default"](this), this.errortips.render(this.el), this.loading = new E["default"](this), this.loading.render(this.el), this.options.width = this.options.width || e.offsetWidth, this.options.height = this.options.height || e.offsetHeight, this.size(this.options.width, this.options.height), !this.verifyOptions()) {
          return this.listener({type: "error", code: 5}), x.console.error("create failed")
        }

此时,使用浏览器打开播放页面和开发者工具,定位到视频播放节点,可以看到在"vcp-player"出现了我们自定义的span节点,但此时还看不见对应的字幕内容,接下来需要修改字幕样式
tcplayer 源码改造第四弹 -> 字幕(srt)_第3张图片

修改字幕内容节点的样式

依旧定位字符串"vcp-player",可以找到如下的样式信息

    t = e.exports = i(8)(), t.push([e.id, ".vcp-player{position:relative;z-index:0;font-family:Tahoma,\\\\5FAE\\8F6F\\96C5\\9ED1,\\u5b8b\\u4f53,Verdana,Arial,sans-serif;background-color:#000}.vcp-player video{display:block;overflow:hidden}.vcp-fullscreen.vcp-player,.vcp-fullscreen video,body.vcp-full-window{width:100%!important;height:100%!important}body.vcp-full-window{overflow-y:auto}.vcp-full-window .vcp-player{position:fixed;left:0;top:0;z-index:2147483647}.vcp-pre-flash,.vcp-video{width:100%;height:100%}.vcp-pre-flash{z-index:999;background:#000;position:absolute;top:0;left:0}.vcp-controls-panel{position:absolute;bottom:0;width:100%;font-size:16px;height:3em;z-index:1000}.vcp-controls-panel.show{-webkit-animation:fadeIn ease .8s;animation:fadeIn ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}.vcp-controls-panel.hide{-webkit-animation:fadeOut ease .8s;animation:fadeOut ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}.vcp-panel-bg{width:100%;height:100%;position:absolute;left:0;top:0;background-color:#242424;opacity:.8;filter:alpha(opacity=80);z-index:1000}.vcp-playtoggle{cursor:pointer;position:relative;z-index:1001;width:3em;height:100%;float:left;background-image:url(" + i(9) + ");background-image:url(" + i(10) + ")\\0}.vcp-playtoggle:focus,.vcp-playtoggle:hover{background-color:#708090;opacity:.9;filter:alpha(opacity=90)}.touchable .vcp-playtoggle:hover{background-color:transparent;opacity:1}.vcp-playing .vcp-playtoggle{background-image:url(" + i(11) + ");background-image:url(" + i(12) + ")\\0}.vcp-bigplay{width:100%;height:80%;position:absolute;background-color:white\\0;filter:alpha(opacity=0);opacity:0;z-index:1000;top:0;left:0}.vcp-slider{position:relative;z-index:1001;float:left;background:#c4c4c4;height:10px;opacity:.8;filter:alpha(opacity=80);cursor:pointer}.vcp-slider .vcp-slider-track{width:0;height:100%;margin-top:0;opacity:1;filter:alpha(opacity=100);background-color:#1e90ff}.vcp-slider .vcp-slider-thumb{cursor:pointer;background-color:#fff;position:absolute;top:0;left:0;border-radius:1em!important;height:10px;margin-left:-5px;width:10px}.vcp-slider-vertical{position:relative;width:.5em;height:8em;top:-5.6em;z-index:1001;background-color:#1c1c1c;opacity:.9;filter:alpha(opacity=90);cursor:pointer}.vcp-slider-vertical .vcp-slider-track{background-color:#1275cf;width:.5em;height:100%;opacity:.8;filter:alpha(opacity=80)}.vcp-slider-vertical .vcp-slider-thumb{cursor:pointer;position:absolute;background-color:#f0f8ff;width:.8em;height:.8em;border-radius:.8em!important;margin-top:-.4em;top:0;left:-.15em}.vcp-timeline{top:-10px;left:0;height:10px;position:absolute;z-index:1001;width:100%}.vcp-timeline .vcp-slider-thumb{top:-4px}.vcp-timeline .vcp-slider{margin-top:8px;height:2px;width:100%}.vcp-timeline:hover .vcp-slider{margin-top:0;height:10px}.vcp-timeline:hover .vcp-slider-thumb{display:block;width:16px;height:16px;top:-3px;margin-left:-8px}.vcp-timelabel{display:inline-block;line-height:3em;float:left;color:#fff;padding:0 9px}.vcp-timelabel,.vcp-volume{height:3em;z-index:1001;position:relative}.vcp-volume{width:3em;cursor:pointer;float:right;background-color:transparent;opacity:.9;filter:alpha(opacity=90)}.vcp-volume-icon{background-image:url(" + i(13) + ");background-image:url(" + i(14) + ")\\0;display:inline-block;width:3em;height:3em;position:absolute;left:0;top:0}.vcp-volume-muted .vcp-volume-icon{background-image:url(" + i(15) + ");background-image:url(" + i(16) + ")\\0}.vcp-volume .vcp-slider-vertical{top:-8.4em;left:1em;display:none}.vcp-volume .vcp-slider-track{position:absolute;bottom:0}.vcp-volume:hover .vcp-slider-vertical{display:block}.vcp-volume .vcp-volume-bg{height:8.8em;width:2em;position:absolute;left:.25em;top:-8.8em;background:#242424;display:none}.vcp-volume:hover .vcp-slider-vertical,.vcp-volume:hover .vcp-volume-bg{display:block}.vcp-fullscreen-toggle{position:relative;width:3em;height:3em;float:right;cursor:pointer;z-index:1001;background-image:url(" + i(17) + ");background-image:url(" + i(18) + ")\\0}.vcp-fullscreen .vcp-fullscreen-toggle{background-image:url(" + i(19) + ");background-image:url(" + i(20) + ')\\0}.vcp-loading{box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;text-indent:-9999em}.vcp-loading:before{box-sizing:inherit;content:"";display:block;width:100%;height:100%;border-radius:50%;border:3px solid hsla(0,0%,100%,0);border-left-color:#fff;border-right-color:#fff;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-animation:load8 1.1s infinite linear;animation:load8 1.1s infinite linear}@-webkit-keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.vcp-poster{position:absolute;left:0;top:0;overflow:hidden;z-index:1000;width:100%;height:100%;display:none}.vcp-poster-pic{position:relative}.vcp-poster-pic.cover,.vcp-poster-pic.default{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.vcp-poster-pic.cover{width:100%}.vcp-poster-pic.stretch{width:100%;height:100%}.vcp-error-tips{position:absolute;z-index:1001;width:100%;height:4.5em;left:0;top:50%;color:#ff4500;margin-top:-5.25em;text-align:center;display:none}.vcp-clarityswitcher{height:3em;width:3em;cursor:pointer;position:relative;z-index:1001;float:right;background-color:transparent;opacity:.9}.vcp-vertical-switcher-container{width:3em;position:absolute;left:0;bottom:2.4em;background:#242424;display:none}.vcp-vertical-switcher-current{display:block;color:#fff;text-align:center;line-height:3em}.vcp-vertical-switcher-item{display:block;color:#fff;text-align:center;line-height:2em}.vcp-vertical-switcher-item.current{color:#888}.vcp-share>a{width:3em;height:3em;cursor:pointer;background-image:url(' + i(21) + ");opacity:.9;display:block}.vcp-share{width:3em;height:3em;position:relative;float:right;z-index:1001}.vcp-vertical-share-container{width:auto;height:auto;position:absolute;background:rgba(36,36,36,.8);padding:.5em;overflow:hidden;display:none}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation:fadeOut ease .8s;animation:fadeOut ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation:fadeIn ease .8s;animation:fadeIn ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}", ""])

在其中加入防盗录节点"oneweek-video-subtitle"的样式,如下:

    // 加入节点(class="oneweek-video-subtitle")的样式,设置层级为1,保证不被视频遮挡即可,并且设置为字体为白色黑边,居中等
    t = e.exports = i(8)(), t.push([e.id, ".vcp-player .oneweek-video-subtitle{position:absolute;z-index:1;bottom:2%;color:#fff;width:100%;text-align:center;-webkit-text-stroke:1px black;text-stroke:1px black;font-weight:bolder;}.vcp-player{position:relative;z-index:0;font-family:Tahoma,\\\\5FAE\\8F6F\\96C5\\9ED1,\\u5b8b\\u4f53,Verdana,Arial,sans-serif;background-color:#000}.vcp-player video{display:block;overflow:hidden}.vcp-fullscreen.vcp-player,.vcp-fullscreen video,body.vcp-full-window{width:100%!important;height:100%!important}body.vcp-full-window{overflow-y:auto}.vcp-full-window .vcp-player{position:fixed;left:0;top:0;z-index:2147483647}.vcp-pre-flash,.vcp-video{width:100%;height:100%}.vcp-pre-flash{z-index:999;background:#000;position:absolute;top:0;left:0}.vcp-controls-panel{position:absolute;bottom:0;width:100%;font-size:16px;height:3em;z-index:1000}.vcp-controls-panel.show{-webkit-animation:fadeIn ease .8s;animation:fadeIn ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}.vcp-controls-panel.hide{-webkit-animation:fadeOut ease .8s;animation:fadeOut ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}.vcp-panel-bg{width:100%;height:100%;position:absolute;left:0;top:0;background-color:#242424;opacity:.8;filter:alpha(opacity=80);z-index:1000}.vcp-playtoggle{cursor:pointer;position:relative;z-index:1001;width:3em;height:100%;float:left;background-image:url(" + i(9) + ");background-image:url(" + i(10) + ")\\0}.vcp-playtoggle:focus,.vcp-playtoggle:hover{background-color:#708090;opacity:.9;filter:alpha(opacity=90)}.touchable .vcp-playtoggle:hover{background-color:transparent;opacity:1}.vcp-playing .vcp-playtoggle{background-image:url(" + i(11) + ");background-image:url(" + i(12) + ")\\0}.vcp-bigplay{width:100%;height:80%;position:absolute;background-color:white\\0;filter:alpha(opacity=0);opacity:0;z-index:1000;top:0;left:0}.vcp-slider{position:relative;z-index:1001;float:left;background:#c4c4c4;height:10px;opacity:.8;filter:alpha(opacity=80);cursor:pointer}.vcp-slider .vcp-slider-track{width:0;height:100%;margin-top:0;opacity:1;filter:alpha(opacity=100);background-color:#1e90ff}.vcp-slider .vcp-slider-thumb{cursor:pointer;background-color:#fff;position:absolute;top:0;left:0;border-radius:1em!important;height:10px;margin-left:-5px;width:10px}.vcp-slider-vertical{position:relative;width:.5em;height:8em;top:-5.6em;z-index:1001;background-color:#1c1c1c;opacity:.9;filter:alpha(opacity=90);cursor:pointer}.vcp-slider-vertical .vcp-slider-track{background-color:#1275cf;width:.5em;height:100%;opacity:.8;filter:alpha(opacity=80)}.vcp-slider-vertical .vcp-slider-thumb{cursor:pointer;position:absolute;background-color:#f0f8ff;width:.8em;height:.8em;border-radius:.8em!important;margin-top:-.4em;top:0;left:-.15em}.vcp-timeline{top:-10px;left:0;height:10px;position:absolute;z-index:1001;width:100%}.vcp-timeline .vcp-slider-thumb{top:-4px}.vcp-timeline .vcp-slider{margin-top:8px;height:2px;width:100%}.vcp-timeline:hover .vcp-slider{margin-top:0;height:10px}.vcp-timeline:hover .vcp-slider-thumb{display:block;width:16px;height:16px;top:-3px;margin-left:-8px}.vcp-timelabel{display:inline-block;line-height:3em;float:left;color:#fff;padding:0 9px}.vcp-timelabel,.vcp-volume{height:3em;z-index:1001;position:relative}.vcp-volume{width:3em;cursor:pointer;float:right;background-color:transparent;opacity:.9;filter:alpha(opacity=90)}.vcp-volume-icon{background-image:url(" + i(13) + ");background-image:url(" + i(14) + ")\\0;display:inline-block;width:3em;height:3em;position:absolute;left:0;top:0}.vcp-volume-muted .vcp-volume-icon{background-image:url(" + i(15) + ");background-image:url(" + i(16) + ")\\0}.vcp-volume .vcp-slider-vertical{top:-8.4em;left:1em;display:none}.vcp-volume .vcp-slider-track{position:absolute;bottom:0}.vcp-volume:hover .vcp-slider-vertical{display:block}.vcp-volume .vcp-volume-bg{height:8.8em;width:2em;position:absolute;left:.25em;top:-8.8em;background:#242424;display:none}.vcp-volume:hover .vcp-slider-vertical,.vcp-volume:hover .vcp-volume-bg{display:block}.vcp-fullscreen-toggle{position:relative;width:3em;height:3em;float:right;cursor:pointer;z-index:1001;background-image:url(" + i(17) + ");background-image:url(" + i(18) + ")\\0}.vcp-fullscreen .vcp-fullscreen-toggle{background-image:url(" + i(19) + ");background-image:url(" + i(20) + ')\\0}.vcp-loading{box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;text-indent:-9999em}.vcp-loading:before{box-sizing:inherit;content:"";display:block;width:100%;height:100%;border-radius:50%;border:3px solid hsla(0,0%,100%,0);border-left-color:#fff;border-right-color:#fff;-webkit-transform:translateZ(0);transform:translateZ(0);-webkit-animation:load8 1.1s infinite linear;animation:load8 1.1s infinite linear}@-webkit-keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes load8{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.vcp-poster{position:absolute;left:0;top:0;overflow:hidden;z-index:1000;width:100%;height:100%;display:none}.vcp-poster-pic{position:relative}.vcp-poster-pic.cover,.vcp-poster-pic.default{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.vcp-poster-pic.cover{width:100%}.vcp-poster-pic.stretch{width:100%;height:100%}.vcp-error-tips{position:absolute;z-index:1001;width:100%;height:4.5em;left:0;top:50%;color:#ff4500;margin-top:-5.25em;text-align:center;display:none}.vcp-clarityswitcher{height:3em;width:3em;cursor:pointer;position:relative;z-index:1001;float:right;background-color:transparent;opacity:.9}.vcp-vertical-switcher-container{width:3em;position:absolute;left:0;bottom:2.4em;background:#242424;display:none}.vcp-vertical-switcher-current{display:block;color:#fff;text-align:center;line-height:3em}.vcp-vertical-switcher-item{display:block;color:#fff;text-align:center;line-height:2em}.vcp-vertical-switcher-item.current{color:#888}.vcp-share>a{width:3em;height:3em;cursor:pointer;background-image:url(' + i(21) + ");opacity:.9;display:block}.vcp-share{width:3em;height:3em;position:relative;float:right;z-index:1001}.vcp-vertical-share-container{width:auto;height:auto;position:absolute;background:rgba(36,36,36,.8);padding:.5em;overflow:hidden;display:none}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.fadeOut{-webkit-animation:fadeOut ease .8s;animation:fadeOut ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.fadeIn{-webkit-animation:fadeIn ease .8s;animation:fadeIn ease .8s;animation-fill-mode:forwards;-webkit-animation-fill-mode:forwards}", ""])

此时若读者调用播放器时设置了如下参数(即笔者在文章上方设置的几个参数):

      subtitle_display: true,
      subtitle_srt: '字幕srt文件',
      subtitle_fontsize: '28px',
      subtitle_fullscreen_fontsize: '56px',

则可在播放视频的同时看到对应时间点的字幕,如下图:
tcplayer 源码改造第四弹 -> 字幕(srt)_第4张图片

在底部栏加上字幕切换按钮

以上,就已经将字幕加入到视频播放中了,下面将在顶部栏加入字幕按钮,使用户自己自动切换是否使用字幕,若在配置播放器时没有配置参数"subtitle_srt",则不在底部栏展示字幕按钮

复制切换清晰度的代码,并修改

考虑到倍速播放的样式与切换清晰度类似(其实就是一样啦),可以定位到切换清晰度的代码,进行复制黏贴.
在chrome中打开开发者工具,定位到视频下方的控制栏中清晰度,可以找到对应的节点vcp-clarityswitcher.
tcplayer 源码改造第四弹 -> 字幕(srt)_第5张图片
在代码中搜索"vcp-clarityswitcher",会搜索到一些样式和实际添加节点的代码,如下:

        return a(t, e), t.prototype.render = function (t) {
          this.show = !1, this.createEl("div", {"class": "vcp-clarityswitcher"}), this.current = p.createEl("a", {"class": "vcp-vertical-switcher-current"}), this.container = p.createEl("div", {"class": "vcp-vertical-switcher-container"}), this.items = [], this.currentItem = "";
          var i = this.options.videoSource;
          this.current.innerHTML = f[i.curDef], this.el.appendChild(this.current);
          for (var o = 0; o < i.definitions.length; o++) {
            var n = p.createEl("a", {"class": "vcp-vertical-switcher-item"});
            n.innerHTML = f[i.definitions[o]], i.definitions[o] == i.curDef && (p.addClass(n, "current"), this.currentItem = n), n.setAttribute("data-def", i.definitions[o]), this.items.push(n), this.container.appendChild(n)
          }
          return this.el.appendChild(this.container), e.prototype.render.call(this, t)
        }

复制整个function,如下是整个function:

, function (e, t, i) {
    "use strict";

    function o(e) {
      if (e && e.__esModule) return e;
      var t = {};
      if (null != e) for (var i in e) Object.prototype.hasOwnProperty.call(e, i) && (t[i] = e[i]);
      return t["default"] = e, t
    }

    function n(e) {
      return e && e.__esModule ? e : {"default": e}
    }

    function r(e, t) {
      if (!(e instanceof t)) throw new TypeError("Cannot call a class as a function")
    }

    function s(e, t) {
      if (!e) throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
      return !t || "object" != typeof t && "function" != typeof t ? e : t
    }

    function a(e, t) {
      if ("function" != typeof t && null !== t) throw new TypeError("Super expression must either be null or a function, not " + typeof t);
      e.prototype = Object.create(t && t.prototype, {
        constructor: {
          value: e,
          enumerable: !1,
          writable: !0,
          configurable: !0
        }
      }), t && (Object.setPrototypeOf ? Object.setPrototypeOf(e, t) : e.__proto__ = t)
    }

    t.__esModule = !0;
    var l = i(24), c = n(l), u = i(2), p = o(u), h = i(3), d = o(h), f = {od: "超清", hd: "高清", sd: "标清"},
      y = function (e) {
        function t(i) {
          r(this, t);
          var o = s(this, e.call(this, i, "ClaritySwitcher"));
          return f = d.extend({}, i.options.clarityLabel, f), i.claritySwitcher = o, o
        }

        return a(t, e), t.prototype.render = function (t) {
          this.show = !1, this.createEl("div", {"class": "vcp-clarityswitcher"}), this.current = p.createEl("a", {"class": "vcp-vertical-switcher-current"}), this.container = p.createEl("div", {"class": "vcp-vertical-switcher-container"}), this.items = [], this.currentItem = "";
          var i = this.options.videoSource;
          this.current.innerHTML = f[i.curDef], this.el.appendChild(this.current);
          for (var o = 0; o < i.definitions.length; o++) {
            var n = p.createEl("a", {"class": "vcp-vertical-switcher-item"});
            n.innerHTML = f[i.definitions[o]], i.definitions[o] == i.curDef && (p.addClass(n, "current"), this.currentItem = n), n.setAttribute("data-def", i.definitions[o]), this.items.push(n), this.container.appendChild(n)
          }
          return this.el.appendChild(this.container), e.prototype.render.call(this, t)
        }, t.prototype.setup = function () {
          this.on("click", this.onClick), this.on("mouseenter", this.onMouseEnter), this.on("mouseleave", this.onMouseLeave)
        }, t.prototype.onClick = function (e) {
          var t = e.target.getAttribute("data-def");
          t ? (this.current.innerHTML = f[t], p.removeClass(this.currentItem, "current"), p.addClass(e.target, "current"), this.currentItem = e.target, this.player._switchClarity(t)) : !this.show
        }, t.prototype.onMouseLeave = function () {
          this.container.style.display = "none", this.show = !1
        }, t.prototype.onMouseEnter = function () {
          this.container.style.display = "block", this.show = !0
        }, t.prototype.setClarity = function (e) {
          e && (this.current.innerHTML = f[e], p.removeClass(document.querySelector(".vcp-vertical-switcher-item.current"), "current"), p.addClass(document.querySelector('.vcp-vertical-switcher-item[data-def="' + e + '"]'), "current"), this.currentItem = document.querySelector('.vcp-vertical-switcher-item[data-def="' + e + '"]'), this.player._switchClarity(e))
        }, t
      }(c["default"]);
    t["default"] = y
  }

黏贴到同层级的最下方,代码拉到最下方,在下图鼠标光标所在位置黏贴:
同层级最下方
再改动绑定的数据

    t.__esModule = !0;
    // 修改f数组,为悬浮在字幕按钮上展示的列表
    var l = i(24), c = n(l), u = i(2), p = o(u), h = i(3), d = o(h), f = ['on', 'off'],
      y = function (e) {
        function t(i) {
          return r(this, t), s(this, e.call(this, i, "ClaritySwitcher"))
        }

        return a(t, e), t.prototype.render = function (t) {
          this.show = !1, this.createEl("div", {"class": "vcp-clarityswitcher"}), this.current = p.createEl("a", {"class": "vcp-vertical-switcher-current"}), this.container = p.createEl("div", {"class": "vcp-vertical-switcher-container"}), this.items = [], this.currentItem = "";
          // subtitle_display 表示是否开启字幕
          var i = this.options.subtitle_display ? 0 : 1;
          // 在底部栏直接展示字幕二字
          this.current.innerHTML = "字幕", this.el.appendChild(this.current);
          for (var o = 0; o < f.length; o++) {
            var n = p.createEl("a", {"class": "vcp-vertical-switcher-item"});
            n.innerHTML = f[o], f[o] === f[i] && (p.addClass(n, "current"), this.currentItem = n), n.setAttribute("data-def", o), this.items.push(n), this.container.appendChild(n)
          }
          return this.el.appendChild(this.container), e.prototype.render.call(this, t)
        }, t.prototype.setup = function () {
          this.on("click", this.onClick), this.on("mouseenter", this.onMouseEnter), this.on("mouseleave", this.onMouseLeave)
        }, t.prototype.onClick = function (e) {
          var t = e.target.getAttribute("data-def");
          // 去掉this.current.innerHTML,即不修改在底部栏展示的字幕按钮信息,加入判断t==0 即f[t]='on'时,将subtitle_display置为true,即展示字幕
          t ? (p.removeClass(this.currentItem, "current"), p.addClass(e.target, "current"), this.currentItem = e.target, this.options.subtitle_display = t == 0) : !this.show
        }, t.prototype.onMouseLeave = function () {
          this.container.style.display = "none", this.show = !1
        }, t.prototype.onMouseEnter = function () {
          this.container.style.display = "block", this.show = !0
        }, t // 去除了无效的setClarity函数,也可不去,对功能无影响,只是代码洁癖
      }(c["default"]);

加入字幕按钮

tcplayer.js文件中定位"vcp-controls-panel",可以搜索到实际添加按钮的代码:

    t.__esModule = !0;
    var l = i(24), c = n(l), u = i(28), p = n(u), h = i(29), d = n(h), f = i(30), y = i(31), A = n(y), v = i(32),
      m = n(v), g = i(33), w = n(g), b = i(34), M = n(b), I = i(4), S = i(2), E = o(S), _ = i(3), T = o(_), D = i(1),
      L = o(D), O = function (e) {
        function t(i) {
          return r(this, t), s(this, e.call(this, i, "Panel"))
        }

        return a(t, e), t.prototype.render = function (t) {
          return this.createEl("div", {"class": "vcp-controls-panel"}), this.el.appendChild(E.createEl("div", {"class": "vcp-panel-bg"})), this.playToggle = new p["default"](this.player), this.playToggle.render(this.el), this.timelabel = new m["default"](this.player), this.timelabel.render(this.el), this.timeline = new A["default"](this.player), this.timeline.render(this.el), this.options.fullscreenEnabled === !0 && (this.fullscreen = new d["default"](this.player), this.fullscreen.render(this.el)), L.IS_MOBILE || (this.volume = new w["default"](this.player), this.volume.render(this.el)), this.options.videoSource && this.options.videoSource.definitions.length > 1 && !L.IS_MOBILE && (this.claritySwitcher = new M["default"](this.player), this.claritySwitcher.render(this.el)), e.prototype.render.call(this, t)
        }

将其改为:

    t.__esModule = !0;
    // i为对应的esmodule,M["default"]表示n[i(34)]个,即分辨率是第34个function,相应的,我们将字幕放在最后,就是第40个
    var l = i(24), c = n(l), u = i(28), p = n(u), h = i(29), d = n(h), f = i(30), y = i(31), A = n(y), v = i(32),
      m = n(v), g = i(33), w = n(g), b = i(34), M = n(b), I = i(4), S = i(2), E = o(S), _ = i(3), T = o(_), D = i(1),
      L = o(D), subtitle = i(40), Subtitle = n(subtitle), O = function (e) {
        function t(i) {
          return r(this, t), s(this, e.call(this, i, "Panel"))
        }

        return a(t, e), t.prototype.render = function (t) {
          // 如果配置了subtitle_srt参数,即传入了srt字幕文件的地址,则加入字幕节点
          return this.createEl("div", {"class": "vcp-controls-panel"}), this.el.appendChild(E.createEl("div", {"class": "vcp-panel-bg"})), this.playToggle = new p["default"](this.player), this.playToggle.render(this.el), this.timelabel = new m["default"](this.player), this.timelabel.render(this.el), this.timeline = new A["default"](this.player), this.timeline.render(this.el), this.options.fullscreenEnabled === !0 && (this.fullscreen = new d["default"](this.player), this.fullscreen.render(this.el)), L.IS_MOBILE || (this.volume = new w["default"](this.player), this.volume.render(this.el)), this.options.videoSource && this.options.videoSource.definitions.length > 1 && (this.claritySwitcher = new M["default"](this.player), this.claritySwitcher.render(this.el)) && (this.options.subtitle_srt ? (this.subtitleSwitcher = new Subtitle["default"](this.player), this.subtitleSwitcher.render(this.el)) : ""), e.prototype.render.call(this, t)
        }

使用说明

使用时请先压缩js文件

参数说明

在原有播放器支持的参数下添加了两个参数

参数 类型 默认值 参数说明
subtitle_display Boolean false 是否显示字幕
subtitle_srt String srt字幕文件地址,不设置则不在底部栏显示字幕按钮
subtitle_fontsize String “16px” 字幕字体大小
subtitle_fullscreen_fontsize String “32px” 全屏时字幕字体大小

使用示例

var player = new TcPlayer('id_test_video', {
	"m3u8": "http://2157.liveplay.myqcloud.com/2157_358535a.m3u8", //请替换成实际可用的播放地址
	"autoplay" : true,      //iOS 下 safari 浏览器,以及大部分移动端浏览器是不开放视频自动播放这个能力的
	"poster" : "http://www.test.com/myimage.jpg",
	"width" :  '480',//视频的显示宽度,请尽量使用视频分辨率宽度
	"height" : '320'//视频的显示高度,请尽量使用视频分辨率高度
	"subtitle_display": true,
	"subtitle_srt": "字幕文件",
	"subtitle_fontsize": "28px",
	"subtitle_fullscreen_fontsize": "56px",
});

相关推荐

  • tcplayer源码改造第一弹 -> 自定义hls加密播放器
  • tcplayer源码改造第二弹 -> 加入倍速播放
  • tcplayer源码改造第三弹 -> 防盗录
  • tcplayer 源码改造第四弹 -> 字幕(srt)
  • tcplayer源码改造第五弹 -> 兼容sarafi/遨游

你可能感兴趣的:(播放器,js)