player.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. import Promise from 'promise-polyfill';
  2. import utils from './utils';
  3. import handleOption from './options';
  4. import i18n from './i18n';
  5. import Template from './template';
  6. import Icons from './icons';
  7. import Danmaku from './danmaku';
  8. import Events from './events';
  9. import FullScreen from './fullscreen';
  10. import User from './user';
  11. import Subtitle from './subtitle';
  12. import Bar from './bar';
  13. import Timer from './timer';
  14. import Bezel from './bezel';
  15. import Controller from './controller';
  16. import Setting from './setting';
  17. import Comment from './comment';
  18. import HotKey from './hotkey';
  19. import ContextMenu from './contextmenu';
  20. import InfoPanel from './info-panel';
  21. import tplVideo from '../template/video.art';
  22. let index = 0;
  23. const instances = [];
  24. class DPlayer {
  25. /**
  26. * DPlayer constructor function
  27. *
  28. * @param {Object} options - See README
  29. * @constructor
  30. */
  31. constructor (options) {
  32. this.options = handleOption(options);
  33. if (this.options.video.quality) {
  34. this.qualityIndex = this.options.video.defaultQuality;
  35. this.quality = this.options.video.quality[this.options.video.defaultQuality];
  36. }
  37. this.tran = new i18n(this.options.lang).tran;
  38. this.events = new Events();
  39. this.user = new User(this);
  40. this.container = this.options.container;
  41. this.container.classList.add('dplayer');
  42. if (!this.options.danmaku) {
  43. this.container.classList.add('dplayer-no-danmaku');
  44. }
  45. if (this.options.live) {
  46. this.container.classList.add('dplayer-live');
  47. }
  48. if (utils.isMobile) {
  49. this.container.classList.add('dplayer-mobile');
  50. }
  51. this.arrow = this.container.offsetWidth <= 500;
  52. if (this.arrow) {
  53. this.container.classList.add('dplayer-arrow');
  54. }
  55. this.template = new Template({
  56. container: this.container,
  57. options: this.options,
  58. index: index,
  59. tran: this.tran,
  60. });
  61. this.video = this.template.video;
  62. this.bar = new Bar(this.template);
  63. this.bezel = new Bezel(this.template.bezel);
  64. this.fullScreen = new FullScreen(this);
  65. this.controller = new Controller(this);
  66. if (this.options.danmaku) {
  67. this.danmaku = new Danmaku({
  68. container: this.template.danmaku,
  69. opacity: this.user.get('opacity'),
  70. callback: () => {
  71. setTimeout(() => {
  72. this.template.danmakuLoading.style.display = 'none';
  73. // autoplay
  74. if (this.options.autoplay) {
  75. this.play();
  76. }
  77. }, 0);
  78. },
  79. error: (msg) => {
  80. this.notice(msg);
  81. },
  82. apiBackend: this.options.apiBackend,
  83. borderColor: this.options.theme,
  84. height: this.arrow ? 24 : 30,
  85. time: () => this.video.currentTime,
  86. unlimited: this.user.get('unlimited'),
  87. api: {
  88. id: this.options.danmaku.id,
  89. address: this.options.danmaku.api,
  90. token: this.options.danmaku.token,
  91. maximum: this.options.danmaku.maximum,
  92. addition: this.options.danmaku.addition,
  93. user: this.options.danmaku.user,
  94. },
  95. events: this.events,
  96. tran: (msg) => this.tran(msg),
  97. });
  98. this.comment = new Comment(this);
  99. }
  100. this.setting = new Setting(this);
  101. document.addEventListener('click', () => {
  102. this.focus = false;
  103. }, true);
  104. this.container.addEventListener('click', () => {
  105. this.focus = true;
  106. }, true);
  107. this.paused = true;
  108. this.timer = new Timer(this);
  109. this.hotkey = new HotKey(this);
  110. this.contextmenu = new ContextMenu(this);
  111. this.initVideo(this.video, this.quality && this.quality.type || this.options.video.type);
  112. this.infoPanel = new InfoPanel(this);
  113. if (!this.danmaku && this.options.autoplay) {
  114. this.play();
  115. }
  116. index++;
  117. instances.push(this);
  118. }
  119. /**
  120. * Seek video
  121. */
  122. seek (time) {
  123. time = Math.max(time, 0);
  124. if (this.video.duration) {
  125. time = Math.min(time, this.video.duration);
  126. }
  127. if (this.video.currentTime < time) {
  128. this.notice(`${this.tran('FF')} ${(time - this.video.currentTime).toFixed(0)} ${this.tran('s')}`);
  129. }
  130. else if (this.video.currentTime > time) {
  131. this.notice(`${this.tran('REW')} ${(this.video.currentTime - time).toFixed(0)} ${this.tran('s')}`);
  132. }
  133. this.video.currentTime = time;
  134. if (this.danmaku) {
  135. this.danmaku.seek();
  136. }
  137. this.bar.set('played', time / this.video.duration, 'width');
  138. this.template.ptime.innerHTML = utils.secondToTime(time);
  139. }
  140. /**
  141. * Play video
  142. */
  143. play () {
  144. this.paused = false;
  145. if (this.video.paused) {
  146. this.bezel.switch(Icons.play);
  147. }
  148. this.template.playButton.innerHTML = Icons.pause;
  149. const playedPromise = Promise.resolve(this.video.play());
  150. playedPromise.catch(() => {
  151. this.pause();
  152. }).then(() => {
  153. });
  154. this.timer.enable('loading');
  155. this.container.classList.remove('dplayer-paused');
  156. this.container.classList.add('dplayer-playing');
  157. if (this.danmaku) {
  158. this.danmaku.play();
  159. }
  160. if (this.options.mutex) {
  161. for (let i = 0; i < instances.length; i++) {
  162. if (this !== instances[i]) {
  163. instances[i].pause();
  164. }
  165. }
  166. }
  167. }
  168. /**
  169. * Pause video
  170. */
  171. pause () {
  172. this.paused = true;
  173. this.container.classList.remove('dplayer-loading');
  174. if (!this.video.paused) {
  175. this.bezel.switch(Icons.pause);
  176. }
  177. this.template.playButton.innerHTML = Icons.play;
  178. this.video.pause();
  179. this.timer.disable('loading');
  180. this.container.classList.remove('dplayer-playing');
  181. this.container.classList.add('dplayer-paused');
  182. if (this.danmaku) {
  183. this.danmaku.pause();
  184. }
  185. }
  186. switchVolumeIcon () {
  187. if (this.volume() >= 0.95) {
  188. this.template.volumeIcon.innerHTML = Icons.volumeUp;
  189. }
  190. else if (this.volume() > 0) {
  191. this.template.volumeIcon.innerHTML = Icons.volumeDown;
  192. }
  193. else {
  194. this.template.volumeIcon.innerHTML = Icons.volumeOff;
  195. }
  196. }
  197. /**
  198. * Set volume
  199. */
  200. volume (percentage, nostorage, nonotice) {
  201. percentage = parseFloat(percentage);
  202. if (!isNaN(percentage)) {
  203. percentage = Math.max(percentage, 0);
  204. percentage = Math.min(percentage, 1);
  205. this.bar.set('volume', percentage, 'width');
  206. const formatPercentage = `${(percentage * 100).toFixed(0)}%`;
  207. this.template.volumeBarWrapWrap.dataset.balloon = formatPercentage;
  208. if (!nostorage) {
  209. this.user.set('volume', percentage);
  210. }
  211. if (!nonotice) {
  212. this.notice(`${this.tran('Volume')} ${(percentage * 100).toFixed(0)}%`);
  213. }
  214. this.video.volume = percentage;
  215. if (this.video.muted) {
  216. this.video.muted = false;
  217. }
  218. this.switchVolumeIcon();
  219. }
  220. return this.video.volume;
  221. }
  222. /**
  223. * Toggle between play and pause
  224. */
  225. toggle () {
  226. if (this.video.paused) {
  227. this.play();
  228. }
  229. else {
  230. this.pause();
  231. }
  232. }
  233. /**
  234. * attach event
  235. */
  236. on (name, callback) {
  237. this.events.on(name, callback);
  238. }
  239. /**
  240. * Switch to a new video
  241. *
  242. * @param {Object} video - new video info
  243. * @param {Object} danmaku - new danmaku info
  244. */
  245. switchVideo (video, danmakuAPI) {
  246. this.pause();
  247. this.video.poster = video.pic ? video.pic : '';
  248. this.video.src = video.url;
  249. this.initMSE(this.video, video.type || 'auto');
  250. if (danmakuAPI) {
  251. this.template.danmakuLoading.style.display = 'block';
  252. this.bar.set('played', 0, 'width');
  253. this.bar.set('loaded', 0, 'width');
  254. this.template.ptime.innerHTML = '00:00';
  255. this.template.danmaku.innerHTML = '';
  256. if (this.danmaku) {
  257. this.danmaku.reload({
  258. id: danmakuAPI.id,
  259. address: danmakuAPI.api,
  260. token: danmakuAPI.token,
  261. maximum: danmakuAPI.maximum,
  262. addition: danmakuAPI.addition,
  263. user: danmakuAPI.user,
  264. });
  265. }
  266. }
  267. }
  268. initMSE (video, type) {
  269. this.type = type;
  270. if (this.options.video.customType && this.options.video.customType[type]) {
  271. if (Object.prototype.toString.call(this.options.video.customType[type]) === '[object Function]') {
  272. this.options.video.customType[type](this.video, this);
  273. }
  274. else {
  275. console.error(`Illegal customType: ${type}`);
  276. }
  277. }
  278. else {
  279. if (this.type === 'auto') {
  280. if (/m3u8(#|\?|$)/i.exec(video.src)) {
  281. this.type = 'hls';
  282. }
  283. else if (/.flv(#|\?|$)/i.exec(video.src)) {
  284. this.type = 'flv';
  285. }
  286. else if (/.mpd(#|\?|$)/i.exec(video.src)) {
  287. this.type = 'dash';
  288. }
  289. else {
  290. this.type = 'normal';
  291. }
  292. }
  293. if (this.type === 'hls' && (video.canPlayType('application/x-mpegURL') || video.canPlayType('application/vnd.apple.mpegURL'))) {
  294. this.type = 'normal';
  295. }
  296. switch (this.type) {
  297. // https://github.com/video-dev/hls.js
  298. case 'hls':
  299. if (Hls) {
  300. if (Hls.isSupported()) {
  301. const hls = new Hls();
  302. hls.loadSource(video.src);
  303. hls.attachMedia(video);
  304. }
  305. else {
  306. this.notice('Error: Hls is not supported.');
  307. }
  308. }
  309. else {
  310. this.notice('Error: Can\'t find Hls.');
  311. }
  312. break;
  313. // https://github.com/Bilibili/flv.js
  314. case 'flv':
  315. if (flvjs && flvjs.isSupported()) {
  316. if (flvjs.isSupported()) {
  317. const flvPlayer = flvjs.createPlayer({
  318. type: 'flv',
  319. url: video.src
  320. });
  321. flvPlayer.attachMediaElement(video);
  322. flvPlayer.load();
  323. }
  324. else {
  325. this.notice('Error: flvjs is not supported.');
  326. }
  327. }
  328. else {
  329. this.notice('Error: Can\'t find flvjs.');
  330. }
  331. break;
  332. // https://github.com/Dash-Industry-Forum/dash.js
  333. case 'dash':
  334. if (dashjs) {
  335. dashjs.MediaPlayer().create().initialize(video, video.src, false);
  336. }
  337. else {
  338. this.notice('Error: Can\'t find dashjs.');
  339. }
  340. break;
  341. // https://github.com/webtorrent/webtorrent
  342. case 'webtorrent':
  343. if (WebTorrent) {
  344. if (WebTorrent.WEBRTC_SUPPORT) {
  345. this.container.classList.add('dplayer-loading');
  346. const client = new WebTorrent();
  347. const torrentId = video.src;
  348. client.add(torrentId, (torrent) => {
  349. const file = torrent.files.find((file) => file.name.endsWith('.mp4'));
  350. file.renderTo(this.video, {
  351. autoplay: this.options.autoplay
  352. }, () => {
  353. this.container.classList.remove('dplayer-loading');
  354. });
  355. });
  356. }
  357. else {
  358. this.notice('Error: Webtorrent is not supported.');
  359. }
  360. }
  361. else {
  362. this.notice('Error: Can\'t find Webtorrent.');
  363. }
  364. break;
  365. }
  366. }
  367. }
  368. initVideo (video, type) {
  369. this.initMSE(video, type);
  370. /**
  371. * video events
  372. */
  373. // show video time: the metadata has loaded or changed
  374. this.on('durationchange', () => {
  375. // compatibility: Android browsers will output 1 or Infinity at first
  376. if (video.duration !== 1 && video.duration !== Infinity) {
  377. this.template.dtime.innerHTML = utils.secondToTime(video.duration);
  378. }
  379. });
  380. // show video loaded bar: to inform interested parties of progress downloading the media
  381. this.on('progress', () => {
  382. const percentage = video.buffered.length ? video.buffered.end(video.buffered.length - 1) / video.duration : 0;
  383. this.bar.set('loaded', percentage, 'width');
  384. });
  385. // video download error: an error occurs
  386. this.on('error', () => {
  387. if (!this.video.error) {
  388. // Not a video load error, may be poster load failed, see #307
  389. return;
  390. }
  391. this.tran && this.notice && this.type !== 'webtorrent' & this.notice(this.tran('Video load failed'), -1);
  392. });
  393. // video end
  394. this.on('ended', () => {
  395. this.bar.set('played', 1, 'width');
  396. if (!this.setting.loop) {
  397. this.pause();
  398. }
  399. else {
  400. this.seek(0);
  401. this.play();
  402. }
  403. if (this.danmaku) {
  404. this.danmaku.danIndex = 0;
  405. }
  406. });
  407. this.on('play', () => {
  408. if (this.paused) {
  409. this.play();
  410. }
  411. });
  412. this.on('pause', () => {
  413. if (!this.paused) {
  414. this.pause();
  415. }
  416. });
  417. this.on('timeupdate', () => {
  418. this.bar.set('played', this.video.currentTime / this.video.duration, 'width');
  419. const currentTime = utils.secondToTime(this.video.currentTime);
  420. if (this.template.ptime.innerHTML !== currentTime) {
  421. this.template.ptime.innerHTML = currentTime;
  422. }
  423. });
  424. for (let i = 0; i < this.events.videoEvents.length; i++) {
  425. video.addEventListener(this.events.videoEvents[i], () => {
  426. this.events.trigger(this.events.videoEvents[i]);
  427. });
  428. }
  429. this.volume(this.user.get('volume'), true, true);
  430. if (this.options.subtitle) {
  431. this.subtitle = new Subtitle(this.template.subtitle, this.video, this.options.subtitle, this.events);
  432. if (!this.user.get('subtitle')) {
  433. this.subtitle.hide();
  434. }
  435. }
  436. }
  437. switchQuality (index) {
  438. if (this.qualityIndex === index || this.switchingQuality) {
  439. return;
  440. }
  441. else {
  442. this.qualityIndex = index;
  443. }
  444. this.switchingQuality = true;
  445. this.quality = this.options.video.quality[index];
  446. this.template.qualityButton.innerHTML = this.quality.name;
  447. const paused = this.video.paused;
  448. this.video.pause();
  449. const videoHTML = tplVideo({
  450. current: false,
  451. pic: null,
  452. screenshot: this.options.screenshot,
  453. preload: 'auto',
  454. url: this.quality.url,
  455. subtitle: this.options.subtitle
  456. });
  457. const videoEle = new DOMParser().parseFromString(videoHTML, 'text/html').body.firstChild;
  458. this.template.videoWrap.insertBefore(videoEle, this.template.videoWrap.getElementsByTagName('div')[0]);
  459. this.prevVideo = this.video;
  460. this.video = videoEle;
  461. this.initVideo(this.video, this.quality.type || this.options.video.type);
  462. this.seek(this.prevVideo.currentTime);
  463. this.notice(`${this.tran('Switching to')} ${this.quality.name} ${this.tran('quality')}`, -1);
  464. this.events.trigger('quality_start', this.quality);
  465. this.on('canplay', () => {
  466. if (this.prevVideo) {
  467. if (this.video.currentTime !== this.prevVideo.currentTime) {
  468. this.seek(this.prevVideo.currentTime);
  469. return;
  470. }
  471. this.template.videoWrap.removeChild(this.prevVideo);
  472. this.video.classList.add('dplayer-video-current');
  473. if (!paused) {
  474. this.video.play();
  475. }
  476. this.prevVideo = null;
  477. this.notice(`${this.tran('Switched to')} ${this.quality.name} ${this.tran('quality')}`);
  478. this.switchingQuality = false;
  479. this.events.trigger('quality_end');
  480. }
  481. });
  482. }
  483. notice (text, time = 2000, opacity = 0.8) {
  484. this.template.notice.innerHTML = text;
  485. this.template.notice.style.opacity = opacity;
  486. if (this.noticeTime) {
  487. clearTimeout(this.noticeTime);
  488. }
  489. this.events.trigger('notice_show', text);
  490. if (time > 0) {
  491. this.noticeTime = setTimeout(() => {
  492. this.template.notice.style.opacity = 0;
  493. this.events.trigger('notice_hide');
  494. }, time);
  495. }
  496. }
  497. resize () {
  498. if (this.danmaku) {
  499. this.danmaku.resize();
  500. }
  501. if (this.controller.thumbnails) {
  502. this.controller.thumbnails.resize(160, this.video.videoHeight / this.video.videoWidth * 160, this.template.barWrap.offsetWidth);
  503. }
  504. this.events.trigger('resize');
  505. }
  506. speed (rate) {
  507. this.video.playbackRate = rate;
  508. }
  509. destroy () {
  510. instances.splice(instances.indexOf(this), 1);
  511. this.pause();
  512. this.controller.destroy();
  513. this.timer.destroy();
  514. this.video.src = '';
  515. this.container.innerHTML = '';
  516. this.events.trigger('destroy');
  517. }
  518. static get version () {
  519. /* global DPLAYER_VERSION */
  520. return DPLAYER_VERSION;
  521. }
  522. }
  523. export default DPlayer;