video-player.js

  1. /**
  2. * Video player class.
  3. * @author Moez Bouhlel <bmoez.j@gmail.com>
  4. * @license MPL-2.0
  5. * @copyright 2014-2017 Moez Bouhlel
  6. * @module
  7. */
  8. import {
  9. setClipboard,
  10. createNode,
  11. rmChildren,
  12. } from './common.js';
  13. import {
  14. Qlt,
  15. Cdc,
  16. } from './Options.js';
  17. class VP {
  18. constructor(container, options) {
  19. this.attached = false;
  20. this.player = undefined;
  21. this.container = container;
  22. this._srcs = [];
  23. this._style = {};
  24. this._containerStyle = {};
  25. this._props = {};
  26. this._langs = [];
  27. this._containerProps = {};
  28. this._CSSRules = [];
  29. this.styleEl = undefined;
  30. this.options = options;
  31. }
  32. addSrc(url, qlt, cdc) {
  33. this.log("addSrc", qlt, cdc);
  34. this._srcs[Qlt.indexOf(qlt) * 2 + Cdc.indexOf(cdc)] = url;
  35. }
  36. srcs(fmts, wrapper, get) {
  37. let slct;
  38. let i;
  39. let j;
  40. if (!wrapper) {
  41. for (const slct of Object.keys(fmts)) {
  42. i = Qlt.indexOf(slct.split("/")[0]);
  43. j = Cdc.indexOf(slct.split("/")[1]);
  44. this._srcs[i * 2 + j] = fmts[slct];
  45. }
  46. return;
  47. }
  48. for (i = 0; i < Qlt.length; i++) {
  49. for (j = 0; j < Cdc.length; j++) {
  50. slct = Qlt[i] + "/" + Cdc[j];
  51. if (!(slct in wrapper) || !fmts[wrapper[slct]]) continue;
  52. this._srcs[i * 2 + j] =
  53. (get) ? (get(fmts[wrapper[slct]]) || this._srcs[i * 2 + j]) : fmts[wrapper[slct]];
  54. }
  55. }
  56. }
  57. mainSrcIndex() {
  58. let i;
  59. const prefCdc = this.options.get("prefCdc");
  60. const prefQlt = this.options.get("prefQlt");
  61. i = prefQlt;
  62. while (i > -1) {
  63. if (this._srcs[i * 2 + prefCdc]) {
  64. return {
  65. qlt: i,
  66. cdc: prefCdc,
  67. };
  68. } else if (this._srcs[i * 2 + (prefCdc + 1 % 2)]) {
  69. return {
  70. qlt: i,
  71. cdc: prefCdc + 1 % 2,
  72. };
  73. }
  74. i = (i >= prefQlt) ? i + 1 : i - 1;
  75. if (i > 3) i = prefQlt - 1;
  76. }
  77. }
  78. setup() {
  79. let idx = this.mainSrcIndex();
  80. if (!idx) return this.error("Failed to find video url");
  81. this.clean();
  82. // just to force contextmenu id. TODO: fix contextmenu and use createNode
  83. this.container.innerHTML = "<video contextmenu='h5vew-contextmenu'></video>";
  84. this.player = this.container.firstChild;
  85. // if (!this.player) {
  86. // this.player = createNode("video", this._props, this._style);
  87. // }
  88. if (!this.styleEl) this.styleEl = createNode("style");
  89. this.patch(this.player, this._props);
  90. this.patch(this.player, this._style, "style");
  91. this.player.appendChild(createNode(
  92. "source", {
  93. src: this._srcs[idx.qlt * 2 + idx.cdc],
  94. type: "video/" + Cdc[idx.cdc],
  95. }));
  96. this._srcs.forEach((url, i) => {
  97. if (i !== idx.qlt * 2 + idx.cdc) {
  98. this.player.appendChild(createNode("source", {
  99. src: url,
  100. type: "video/" + Cdc[i % 2],
  101. }));
  102. }
  103. });
  104. this.container.appendChild(this.player);
  105. this.container.appendChild(this.styleEl);
  106. this.attached = true;
  107. this.slctLang();
  108. this._CSSRules.forEach((s) =>
  109. this.styleEl.sheet.insertRule(s, this.styleEl.sheet.cssRules.length));
  110. this.patch(this.container, this._containerProps);
  111. this.patch(this.container, this._containerStyle, "style");
  112. this.log("setup");
  113. // if (this.options.get("player") === 1)
  114. // this.setupLBP();
  115. // else
  116. this.setupContextMenu(idx);
  117. }
  118. tracksList(langs, fnct) {
  119. this._langs = langs.sort();
  120. this._slctLang = fnct;
  121. if (this.attached) this.slctLang();
  122. }
  123. slctLang(lang) {
  124. if (!lang) lang = this.options.getLang();
  125. if (!lang && !this._slctLang) return;
  126. if (this._lang) this.player.textTracks.getTrackById(this._lang).mode = "disabled";
  127. let track;
  128. if ((track = this.player.textTracks.getTrackById(lang))) {
  129. track.mode = "showing";
  130. this._lang = lang;
  131. } else {
  132. new Promise((resolve, reject) => this._slctLang(lang, resolve, reject)).then((url) => {
  133. track = createNode(
  134. "track", {
  135. kind: "subtitles",
  136. id: lang,
  137. src: url,
  138. label: lang,
  139. srclang: lang,
  140. });
  141. this.player.appendChild(track);
  142. track.track.mode = "showing";
  143. this._lang = lang;
  144. });
  145. }
  146. }
  147. on(evt, cb) {
  148. this.player["on" + evt] = cb; // TODO
  149. }
  150. stop() {
  151. this.log("stop");
  152. if (!this.player) return;
  153. this.player.pause();
  154. this.player.onended = undefined;
  155. if (this.player.duration) this.player.currentTime = this.player.duration;
  156. }
  157. clean() {
  158. this.log("clean");
  159. if (this.player) {
  160. this.player.pause();
  161. this.player.onended = undefined;
  162. }
  163. // site default video player sometime continue playing on background
  164. let vds = this.container.getElementsByTagName("video");
  165. for (let i = 0; i < vds.length; i++) {
  166. if (this.player === vds[i]) continue;
  167. vds[i].pause();
  168. vds[i].volume = 0;
  169. vds[i].currentTime = 0;
  170. vds[i].srcObject = null;
  171. vds[i].addEventListener("playing", (e) => {
  172. e.currentTarget.pause();
  173. e.currentTarget.volume = 0;
  174. e.currentTarget.currentTime = 0;
  175. e.currentTarget.srcObject = null;
  176. });
  177. }
  178. rmChildren(this.container);
  179. this.container.style.cssText = "";
  180. this.container.className = "";
  181. this.attached = false;
  182. }
  183. end() {
  184. this.log("end");
  185. this.stop();
  186. this.clean();
  187. this._srcs = {};
  188. this._style = {};
  189. this._containerStyle = {};
  190. this._props = {};
  191. this._containerProps = {};
  192. this._sheets = [];
  193. }
  194. addCSSRule(cssText) {
  195. this.log("addCSSRule", cssText);
  196. this._CSSRules.push(cssText);
  197. if (this.attached) this.styleEl.sheet.insertRule(cssText, this.styleEl.sheet.cssRules.length);
  198. }
  199. style(_style) {
  200. this.apply(_style, this.player, "_style", "style");
  201. }
  202. containerStyle(_style) {
  203. this.apply(_style, this.container, "_containerStyle", "style");
  204. }
  205. props(_props) {
  206. this.apply(_props, this.player, "_props");
  207. }
  208. containerProps(_props) {
  209. this.apply(_props, this.container, "_containerProps");
  210. }
  211. error(msg) {
  212. this.log("ERROR Msg:", msg);
  213. this.clean();
  214. if (!this.styleEl) this.styleEl = createNode("style");
  215. this.container.appendChild(
  216. createNode("p", {
  217. textContent: "Ooops! :(",
  218. }, {
  219. padding: "15px",
  220. fontSize: "20px",
  221. }));
  222. this.container.appendChild(createNode("p", {
  223. textContent: msg,
  224. }, {
  225. fontSize: "20px",
  226. }));
  227. this.container.appendChild(this.styleEl);
  228. this._CSSRules.forEach((s) =>
  229. this.styleEl.sheet.insertRule(s, this.styleEl.sheet.cssRules.length));
  230. this.patch(this.container, this._containerProps);
  231. this.patch(this.container, this._containerStyle, "style");
  232. }
  233. setupLBP() {
  234. this.container.className += " leanback-player-video";
  235. LBP.setup();
  236. this.player.style = "";
  237. this.player.style = "";
  238. this.player.style.position = "relative";
  239. this.player.style.height = "inherit";
  240. this.container.style.marginLeft = "0px";
  241. }
  242. setupContextMenu(idx) {
  243. /* jshint maxstatements:false */
  244. this._contextMenu = createNode("menu", {
  245. type: "context", // "popup",
  246. id: "h5vew-contextmenu",
  247. });
  248. let qltMenu = createNode("menu", {
  249. id: "h5vew-menu-qlt",
  250. label: "Video Quality",
  251. });
  252. for (let i = 0; i < Qlt.length; i++) {
  253. qltMenu.appendChild(createNode("menuitem", {
  254. type: "radio",
  255. label: Qlt[i],
  256. radiogroup: "menu-qlt",
  257. checked: (idx.qlt === i),
  258. disabled: !(this._srcs[i * 2] || this._srcs[i * 2 + 1]),
  259. onclick: (e) => {
  260. idx.qlt = Qlt.indexOf(e.target.label);
  261. idx.cdc = (this._srcs[idx.qlt * 2 + idx.cdc]) ? idx.cdc : (idx.cdc + 1 % 2);
  262. let paused = this.player.paused;
  263. this.player.src = this._srcs[idx.qlt * 2 + idx.cdc] + "#t=" + this.player.currentTime;
  264. this.player.load();
  265. this.player.oncanplay = () => {
  266. if (!paused) this.player.play();
  267. this.player.oncanplay = undefined;
  268. };
  269. },
  270. }));
  271. }
  272. let cdcMenu = createNode("menu", {
  273. id: "h5vew-menu-cdc",
  274. label: "Preferred Video Format",
  275. });
  276. for (let i = 0; i < Cdc.length; i++) {
  277. cdcMenu.appendChild(createNode("menuitem", {
  278. type: "radio",
  279. label: Cdc[i],
  280. radiogroup: "menu-cdc",
  281. checked: (this.options.get("prefCdc") === i),
  282. onclick: (e) => chgPref("prefCdc", Cdc.indexOf(e.target.label)),
  283. }));
  284. }
  285. let langMenu = createNode("menu", {
  286. id: "h5vew-menu-lang",
  287. label: "Subtitles",
  288. });
  289. langMenu.appendChild(createNode("menuitem", {
  290. type: "radio",
  291. label: "none",
  292. radiogroup: "menu-lang",
  293. checked: this.options.get("lang") === 0 ||
  294. this._langs.findIndex((l) => l === this.options.getLang()) === -1,
  295. onclick: (e) => {
  296. if (this._lang === undefined) return;
  297. this.player.textTracks.getTrackById(this._lang).mode = "disabled";
  298. this._lang = undefined;
  299. },
  300. }));
  301. for (let i = 0; i < this._langs.length; i++) {
  302. langMenu.appendChild(createNode("menuitem", {
  303. type: "radio",
  304. label: this._langs[i],
  305. radiogroup: "menu-lang",
  306. checked: this._langs[i] === this.options.getLang(),
  307. onclick: (e) => this.slctLang(e.target.label),
  308. }));
  309. }
  310. let loopMenu = createNode("menu", {
  311. id: "h5vew-menu-loop",
  312. label: "Loop Video",
  313. });
  314. ["Never", "Always", "Default"].forEach((n, i) => {
  315. loopMenu.appendChild(createNode("menuitem", {
  316. type: "radio",
  317. label: n,
  318. radiogroup: "menu-loop",
  319. checked: (this.options.get("loop") === i),
  320. onclick: (e) => chgPref("loop", i),
  321. }));
  322. });
  323. let autoNextMenu = createNode("menuitem", {
  324. id: "h5vew-menu-autonext",
  325. type: "checkbox",
  326. label: "Auto Play Next Video",
  327. checked: this.options.get("autoNext"),
  328. onclick: (e) => chgPref("autoNext", e.target.checked),
  329. });
  330. let moreMenu = createNode("menu", {
  331. id: "h5vew-menu-more",
  332. label: "More options",
  333. });
  334. let copyMenu = createNode("menuitem", {
  335. id: "h5vew-menu-copy",
  336. label: "Copy Page URL",
  337. onclick: () => setClipboard(location.href), // TODO
  338. });
  339. let disableMenu = createNode("menuitem", {
  340. id: "h5vew-menu-disable",
  341. label: "Disable " + this.options.moduleName.charAt(0).toUpperCase() +
  342. this.options.moduleName.slice(1) + " Support",
  343. onclick: () => {
  344. self.port.emit("disable");
  345. this._contextMenu.removeChild(disableMenu);
  346. },
  347. });
  348. let aboutMenu = createNode("menuitem", {
  349. id: "h5vew-menu-about",
  350. label: "About HTML5 Video EveryWhere",
  351. onclick: () => window.open(
  352. "http://lejenome.github.io/html5-video-everywhere#v=" + this.options.getVersion() +
  353. "&id=" + this.options.getId(),
  354. "h5vew-about",
  355. "width=550,height=300,menubar=no,toolbar=no,location=no,status=no,chrome=on,modal=on"
  356. ),
  357. });
  358. moreMenu.appendChild(copyMenu);
  359. moreMenu.appendChild(disableMenu);
  360. moreMenu.appendChild(createNode("hr"));
  361. moreMenu.appendChild(aboutMenu);
  362. // const prefChanged = (name) => {
  363. // if (name === "autoNext") autoNextMenu.checked = this.options.get("autoNext");
  364. // };
  365. // onPrefChange.push(prefChanged);
  366. this._contextMenu.appendChild(qltMenu);
  367. this._contextMenu.appendChild(cdcMenu);
  368. if (this._langs.length > 0) this._contextMenu.appendChild(langMenu);
  369. this._contextMenu.appendChild(loopMenu);
  370. this._contextMenu.appendChild(autoNextMenu);
  371. this._contextMenu.appendChild(moreMenu);
  372. this.container.appendChild(this._contextMenu);
  373. // TODO: fix assigning contextMenu and uncommant createNode("video") ^
  374. this.container.contextmenu = "h5vew-contextmenu";
  375. }
  376. apply(props, el, obj, sub) {
  377. for (let prop of Object.keys(props)) {
  378. this[obj][prop] = props[prop];
  379. if (this.attached && sub) {
  380. el[sub][prop] = props[prop];
  381. } else if (this.attached && !sub) {
  382. el[prop] = props[prop];
  383. }
  384. }
  385. }
  386. patch(el, props, sub) {
  387. for (let prop of Object.keys(props)) {
  388. if (sub) {
  389. el[sub][prop] = props[prop];
  390. } else {
  391. el[prop] = props[prop];
  392. }
  393. }
  394. }
  395. log(...args) {
  396. args.unshift("[DRIVER::VP]");
  397. console.log(args.join(" ") + "\n");
  398. }
  399. }
  400. export default VP;