video-player.js

/**
 * Video player class.
 * @author Moez Bouhlel <bmoez.j@gmail.com>
 * @license MPL-2.0
 * @copyright 2014-2017 Moez Bouhlel
 * @module
 */

import {
  setClipboard,
  createNode,
  rmChildren,
} from './common.js';
import {
  Qlt,
  Cdc,
} from './Options.js';

class VP {
  constructor(container, options) {
    this.attached = false;
    this.player = undefined;
    this.container = container;
    this._srcs = [];
    this._style = {};
    this._containerStyle = {};
    this._props = {};
    this._langs = [];
    this._containerProps = {};
    this._CSSRules = [];
    this.styleEl = undefined;
    this.options = options;
  }

  addSrc(url, qlt, cdc) {
    this.log("addSrc", qlt, cdc);
    this._srcs[Qlt.indexOf(qlt) * 2 + Cdc.indexOf(cdc)] = url;
  }

  srcs(fmts, wrapper, get) {
    let slct;
    let i;
    let j;
    if (!wrapper) {
      for (const slct of Object.keys(fmts)) {
        i = Qlt.indexOf(slct.split("/")[0]);
        j = Cdc.indexOf(slct.split("/")[1]);
        this._srcs[i * 2 + j] = fmts[slct];
      }
      return;
    }
    for (i = 0; i < Qlt.length; i++) {
      for (j = 0; j < Cdc.length; j++) {
        slct = Qlt[i] + "/" + Cdc[j];
        if (!(slct in wrapper) || !fmts[wrapper[slct]]) continue;
        this._srcs[i * 2 + j] =
          (get) ? (get(fmts[wrapper[slct]]) || this._srcs[i * 2 + j]) : fmts[wrapper[slct]];
      }
    }
  }

  mainSrcIndex() {
    let i;
    const prefCdc = this.options.get("prefCdc");
    const prefQlt = this.options.get("prefQlt");
    i = prefQlt;
    while (i > -1) {
      if (this._srcs[i * 2 + prefCdc]) {
        return {
          qlt: i,
          cdc: prefCdc,
        };
      } else if (this._srcs[i * 2 + (prefCdc + 1 % 2)]) {
        return {
          qlt: i,
          cdc: prefCdc + 1 % 2,
        };
      }
      i = (i >= prefQlt) ? i + 1 : i - 1;
      if (i > 3) i = prefQlt - 1;
    }
  }

  setup() {
    let idx = this.mainSrcIndex();
    if (!idx) return this.error("Failed to find video url");
    this.clean();
    // just to force contextmenu id. TODO: fix contextmenu and use createNode
    this.container.innerHTML = "<video contextmenu='h5vew-contextmenu'></video>";
    this.player = this.container.firstChild;
    //    if (!this.player) {
    //        this.player = createNode("video", this._props, this._style);
    //    }
    if (!this.styleEl) this.styleEl = createNode("style");
    this.patch(this.player, this._props);
    this.patch(this.player, this._style, "style");
    this.player.appendChild(createNode(
      "source", {
        src: this._srcs[idx.qlt * 2 + idx.cdc],
        type: "video/" + Cdc[idx.cdc],
      }));
    this._srcs.forEach((url, i) => {
      if (i !== idx.qlt * 2 + idx.cdc) {
        this.player.appendChild(createNode("source", {
          src: url,
          type: "video/" + Cdc[i % 2],
        }));
      }
    });
    this.container.appendChild(this.player);
    this.container.appendChild(this.styleEl);
    this.attached = true;
    this.slctLang();
    this._CSSRules.forEach((s) =>
      this.styleEl.sheet.insertRule(s, this.styleEl.sheet.cssRules.length));
    this.patch(this.container, this._containerProps);
    this.patch(this.container, this._containerStyle, "style");
    this.log("setup");
    // if (this.options.get("player") === 1)
    //   this.setupLBP();
    // else
    this.setupContextMenu(idx);
  }

  tracksList(langs, fnct) {
    this._langs = langs.sort();
    this._slctLang = fnct;
    if (this.attached) this.slctLang();
  }

  slctLang(lang) {
    if (!lang) lang = this.options.getLang();
    if (!lang && !this._slctLang) return;
    if (this._lang) this.player.textTracks.getTrackById(this._lang).mode = "disabled";
    let track;
    if ((track = this.player.textTracks.getTrackById(lang))) {
      track.mode = "showing";
      this._lang = lang;
    } else {
      new Promise((resolve, reject) => this._slctLang(lang, resolve, reject)).then((url) => {
        track = createNode(
          "track", {
            kind: "subtitles",
            id: lang,
            src: url,
            label: lang,
            srclang: lang,
          });
        this.player.appendChild(track);
        track.track.mode = "showing";
        this._lang = lang;
      });
    }
  }

  on(evt, cb) {
    this.player["on" + evt] = cb; // TODO
  }

  stop() {
    this.log("stop");
    if (!this.player) return;
    this.player.pause();
    this.player.onended = undefined;
    if (this.player.duration) this.player.currentTime = this.player.duration;
  }

  clean() {
    this.log("clean");
    if (this.player) {
      this.player.pause();
      this.player.onended = undefined;
    }
    // site default video player sometime continue playing on background
    let vds = this.container.getElementsByTagName("video");
    for (let i = 0; i < vds.length; i++) {
      if (this.player === vds[i]) continue;
      vds[i].pause();
      vds[i].volume = 0;
      vds[i].currentTime = 0;
      vds[i].srcObject = null;
      vds[i].addEventListener("playing", (e) => {
        e.currentTarget.pause();
        e.currentTarget.volume = 0;
        e.currentTarget.currentTime = 0;
        e.currentTarget.srcObject = null;
      });
    }
    rmChildren(this.container);
    this.container.style.cssText = "";
    this.container.className = "";
    this.attached = false;
  }

  end() {
    this.log("end");
    this.stop();
    this.clean();
    this._srcs = {};
    this._style = {};
    this._containerStyle = {};
    this._props = {};
    this._containerProps = {};
    this._sheets = [];
  }

  addCSSRule(cssText) {
    this.log("addCSSRule", cssText);
    this._CSSRules.push(cssText);
    if (this.attached) this.styleEl.sheet.insertRule(cssText, this.styleEl.sheet.cssRules.length);
  }

  style(_style) {
    this.apply(_style, this.player, "_style", "style");
  }

  containerStyle(_style) {
    this.apply(_style, this.container, "_containerStyle", "style");
  }

  props(_props) {
    this.apply(_props, this.player, "_props");
  }

  containerProps(_props) {
    this.apply(_props, this.container, "_containerProps");
  }

  error(msg) {
    this.log("ERROR Msg:", msg);
    this.clean();
    if (!this.styleEl) this.styleEl = createNode("style");
    this.container.appendChild(
      createNode("p", {
        textContent: "Ooops! :(",
      }, {
        padding: "15px",
        fontSize: "20px",
      }));
    this.container.appendChild(createNode("p", {
      textContent: msg,
    }, {
      fontSize: "20px",
    }));
    this.container.appendChild(this.styleEl);
    this._CSSRules.forEach((s) =>
      this.styleEl.sheet.insertRule(s, this.styleEl.sheet.cssRules.length));
    this.patch(this.container, this._containerProps);
    this.patch(this.container, this._containerStyle, "style");
  }

  setupLBP() {
    this.container.className += " leanback-player-video";
    LBP.setup();
    this.player.style = "";
    this.player.style = "";
    this.player.style.position = "relative";
    this.player.style.height = "inherit";
    this.container.style.marginLeft = "0px";
  }

  setupContextMenu(idx) {
    /* jshint maxstatements:false */
    this._contextMenu = createNode("menu", {
      type: "context", // "popup",
      id: "h5vew-contextmenu",
    });
    let qltMenu = createNode("menu", {
      id: "h5vew-menu-qlt",
      label: "Video Quality",
    });
    for (let i = 0; i < Qlt.length; i++) {
      qltMenu.appendChild(createNode("menuitem", {
        type: "radio",
        label: Qlt[i],
        radiogroup: "menu-qlt",
        checked: (idx.qlt === i),
        disabled: !(this._srcs[i * 2] || this._srcs[i * 2 + 1]),
        onclick: (e) => {
          idx.qlt = Qlt.indexOf(e.target.label);
          idx.cdc = (this._srcs[idx.qlt * 2 + idx.cdc]) ? idx.cdc : (idx.cdc + 1 % 2);
          let paused = this.player.paused;
          this.player.src = this._srcs[idx.qlt * 2 + idx.cdc] + "#t=" + this.player.currentTime;
          this.player.load();
          this.player.oncanplay = () => {
            if (!paused) this.player.play();
            this.player.oncanplay = undefined;
          };
        },
      }));
    }
    let cdcMenu = createNode("menu", {
      id: "h5vew-menu-cdc",
      label: "Preferred Video Format",
    });
    for (let i = 0; i < Cdc.length; i++) {
      cdcMenu.appendChild(createNode("menuitem", {
        type: "radio",
        label: Cdc[i],
        radiogroup: "menu-cdc",
        checked: (this.options.get("prefCdc") === i),
        onclick: (e) => chgPref("prefCdc", Cdc.indexOf(e.target.label)),
      }));
    }
    let langMenu = createNode("menu", {
      id: "h5vew-menu-lang",
      label: "Subtitles",
    });
    langMenu.appendChild(createNode("menuitem", {
      type: "radio",
      label: "none",
      radiogroup: "menu-lang",
      checked: this.options.get("lang") === 0 ||
        this._langs.findIndex((l) => l === this.options.getLang()) === -1,
      onclick: (e) => {
        if (this._lang === undefined) return;
        this.player.textTracks.getTrackById(this._lang).mode = "disabled";
        this._lang = undefined;
      },
    }));
    for (let i = 0; i < this._langs.length; i++) {
      langMenu.appendChild(createNode("menuitem", {
        type: "radio",
        label: this._langs[i],
        radiogroup: "menu-lang",
        checked: this._langs[i] === this.options.getLang(),
        onclick: (e) => this.slctLang(e.target.label),
      }));
    }
    let loopMenu = createNode("menu", {
      id: "h5vew-menu-loop",
      label: "Loop Video",
    });
    ["Never", "Always", "Default"].forEach((n, i) => {
      loopMenu.appendChild(createNode("menuitem", {
        type: "radio",
        label: n,
        radiogroup: "menu-loop",
        checked: (this.options.get("loop") === i),
        onclick: (e) => chgPref("loop", i),
      }));
    });
    let autoNextMenu = createNode("menuitem", {
      id: "h5vew-menu-autonext",
      type: "checkbox",
      label: "Auto Play Next Video",
      checked: this.options.get("autoNext"),
      onclick: (e) => chgPref("autoNext", e.target.checked),
    });
    let moreMenu = createNode("menu", {
      id: "h5vew-menu-more",
      label: "More options",
    });
    let copyMenu = createNode("menuitem", {
      id: "h5vew-menu-copy",
      label: "Copy Page URL",
      onclick: () => setClipboard(location.href), // TODO
    });
    let disableMenu = createNode("menuitem", {
      id: "h5vew-menu-disable",
      label: "Disable " + this.options.moduleName.charAt(0).toUpperCase() +
        this.options.moduleName.slice(1) + " Support",
      onclick: () => {
        self.port.emit("disable");
        this._contextMenu.removeChild(disableMenu);
      },
    });
    let aboutMenu = createNode("menuitem", {
      id: "h5vew-menu-about",
      label: "About HTML5 Video EveryWhere",
      onclick: () => window.open(
        "http://lejenome.github.io/html5-video-everywhere#v=" + this.options.getVersion() +
        "&id=" + this.options.getId(),
        "h5vew-about",
        "width=550,height=300,menubar=no,toolbar=no,location=no,status=no,chrome=on,modal=on"
      ),
    });
    moreMenu.appendChild(copyMenu);
    moreMenu.appendChild(disableMenu);
    moreMenu.appendChild(createNode("hr"));
    moreMenu.appendChild(aboutMenu);

    // const prefChanged = (name) => {
    //   if (name === "autoNext") autoNextMenu.checked = this.options.get("autoNext");
    // };
    // onPrefChange.push(prefChanged);
    this._contextMenu.appendChild(qltMenu);
    this._contextMenu.appendChild(cdcMenu);
    if (this._langs.length > 0) this._contextMenu.appendChild(langMenu);
    this._contextMenu.appendChild(loopMenu);
    this._contextMenu.appendChild(autoNextMenu);
    this._contextMenu.appendChild(moreMenu);
    this.container.appendChild(this._contextMenu);
    // TODO: fix assigning contextMenu and uncommant createNode("video") ^
    this.container.contextmenu = "h5vew-contextmenu";
  }

  apply(props, el, obj, sub) {
    for (let prop of Object.keys(props)) {
      this[obj][prop] = props[prop];
      if (this.attached && sub) {
        el[sub][prop] = props[prop];
      } else if (this.attached && !sub) {
        el[prop] = props[prop];
      }
    }
  }

  patch(el, props, sub) {
    for (let prop of Object.keys(props)) {
      if (sub) {
        el[sub][prop] = props[prop];
      } else {
        el[prop] = props[prop];
      }
    }
  }

  log(...args) {
    args.unshift("[DRIVER::VP]");
    console.log(args.join(" ") + "\n");
  }
}

export default VP;