flv-player.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. /*
  2. * Copyright (C) 2016 Bilibili. All Rights Reserved.
  3. *
  4. * @author zheng qian <xqq@xqq.im>
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. import EventEmitter from 'events';
  19. import Log from '../utils/logger.js';
  20. import Browser from '../utils/browser.js';
  21. import PlayerEvents from './player-events.js';
  22. import Transmuxer from '../core/transmuxer.js';
  23. import TransmuxingEvents from '../core/transmuxing-events.js';
  24. import MSEController from '../core/mse-controller.js';
  25. import MSEEvents from '../core/mse-events.js';
  26. import {ErrorTypes, ErrorDetails} from './player-errors.js';
  27. import {createDefaultConfig} from '../config.js';
  28. import {InvalidArgumentException, IllegalStateException} from '../utils/exception.js';
  29. class FlvPlayer {
  30. constructor(mediaDataSource, config) {
  31. this.TAG = 'FlvPlayer';
  32. this._type = 'FlvPlayer';
  33. this._emitter = new EventEmitter();
  34. this._config = createDefaultConfig();
  35. if (typeof config === 'object') {
  36. Object.assign(this._config, config);
  37. }
  38. if (mediaDataSource.type.toLowerCase() !== 'flv') {
  39. throw new InvalidArgumentException('FlvPlayer requires an flv MediaDataSource input!');
  40. }
  41. if (mediaDataSource.isLive === true) {
  42. this._config.isLive = true;
  43. }
  44. this.e = {
  45. onvLoadedMetadata: this._onvLoadedMetadata.bind(this),
  46. onvSeeking: this._onvSeeking.bind(this),
  47. onvCanPlay: this._onvCanPlay.bind(this),
  48. onvStalled: this._onvStalled.bind(this),
  49. onvProgress: this._onvProgress.bind(this)
  50. };
  51. if (self.performance && self.performance.now) {
  52. this._now = self.performance.now.bind(self.performance);
  53. } else {
  54. this._now = Date.now;
  55. }
  56. this._pendingSeekTime = null; // in seconds
  57. this._requestSetTime = false;
  58. this._seekpointRecord = null;
  59. this._progressChecker = null;
  60. this._mediaDataSource = mediaDataSource;
  61. this._mediaElement = null;
  62. this._msectl = null;
  63. this._transmuxer = null;
  64. this._mseSourceOpened = false;
  65. this._hasPendingLoad = false;
  66. this._receivedCanPlay = false;
  67. this._mediaInfo = null;
  68. this._statisticsInfo = null;
  69. let chromeNeedIDRFix = (Browser.chrome &&
  70. (Browser.version.major < 50 ||
  71. (Browser.version.major === 50 && Browser.version.build < 2661)));
  72. this._alwaysSeekKeyframe = (chromeNeedIDRFix || Browser.msedge || Browser.msie) ? true : false;
  73. if (this._alwaysSeekKeyframe) {
  74. this._config.accurateSeek = false;
  75. }
  76. }
  77. destroy() {
  78. if (this._progressChecker != null) {
  79. window.clearInterval(this._progressChecker);
  80. this._progressChecker = null;
  81. }
  82. if (this._transmuxer) {
  83. this.unload();
  84. }
  85. if (this._mediaElement) {
  86. this.detachMediaElement();
  87. }
  88. this.e = null;
  89. this._mediaDataSource = null;
  90. this._emitter.removeAllListeners();
  91. this._emitter = null;
  92. }
  93. on(event, listener) {
  94. if (event === PlayerEvents.MEDIA_INFO) {
  95. if (this._mediaInfo != null) {
  96. Promise.resolve().then(() => {
  97. this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo);
  98. });
  99. }
  100. } else if (event === PlayerEvents.STATISTICS_INFO) {
  101. if (this._statisticsInfo != null) {
  102. Promise.resolve().then(() => {
  103. this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo);
  104. });
  105. }
  106. }
  107. this._emitter.addListener(event, listener);
  108. }
  109. off(event, listener) {
  110. this._emitter.removeListener(event, listener);
  111. }
  112. attachMediaElement(mediaElement) {
  113. this._mediaElement = mediaElement;
  114. mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata);
  115. mediaElement.addEventListener('seeking', this.e.onvSeeking);
  116. mediaElement.addEventListener('canplay', this.e.onvCanPlay);
  117. mediaElement.addEventListener('stalled', this.e.onvStalled);
  118. mediaElement.addEventListener('progress', this.e.onvProgress);
  119. this._msectl = new MSEController(this._config);
  120. this._msectl.on(MSEEvents.UPDATE_END, this._onmseUpdateEnd.bind(this));
  121. this._msectl.on(MSEEvents.BUFFER_FULL, this._onmseBufferFull.bind(this));
  122. this._msectl.on(MSEEvents.SOURCE_OPEN, () => {
  123. this._mseSourceOpened = true;
  124. if (this._hasPendingLoad) {
  125. this._hasPendingLoad = false;
  126. this.load();
  127. }
  128. });
  129. this._msectl.on(MSEEvents.ERROR, (info) => {
  130. this._emitter.emit(PlayerEvents.ERROR,
  131. ErrorTypes.MEDIA_ERROR,
  132. ErrorDetails.MEDIA_MSE_ERROR,
  133. info
  134. );
  135. });
  136. this._msectl.attachMediaElement(mediaElement);
  137. if (this._pendingSeekTime != null) {
  138. try {
  139. mediaElement.currentTime = this._pendingSeekTime;
  140. this._pendingSeekTime = null;
  141. } catch (e) {
  142. // IE11 may throw InvalidStateError if readyState === 0
  143. // We can defer set currentTime operation after loadedmetadata
  144. }
  145. }
  146. }
  147. detachMediaElement() {
  148. if (this._mediaElement) {
  149. this._msectl.detachMediaElement();
  150. this._mediaElement.removeEventListener('loadedmetadata', this.e.onvLoadedMetadata);
  151. this._mediaElement.removeEventListener('seeking', this.e.onvSeeking);
  152. this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);
  153. this._mediaElement.removeEventListener('stalled', this.e.onvStalled);
  154. this._mediaElement.removeEventListener('progress', this.e.onvProgress);
  155. this._mediaElement = null;
  156. }
  157. if (this._msectl) {
  158. this._msectl.destroy();
  159. this._msectl = null;
  160. }
  161. }
  162. load() {
  163. if (!this._mediaElement) {
  164. throw new IllegalStateException('HTMLMediaElement must be attached before load()!');
  165. }
  166. if (this._transmuxer) {
  167. throw new IllegalStateException('FlvPlayer.load() has been called, please call unload() first!');
  168. }
  169. if (this._hasPendingLoad) {
  170. return;
  171. }
  172. if (this._config.deferLoadAfterSourceOpen && this._mseSourceOpened === false) {
  173. this._hasPendingLoad = true;
  174. return;
  175. }
  176. if (this._mediaElement.readyState > 0) {
  177. this._requestSetTime = true;
  178. // IE11 may throw InvalidStateError if readyState === 0
  179. this._mediaElement.currentTime = 0;
  180. }
  181. this._transmuxer = new Transmuxer(this._mediaDataSource, this._config);
  182. this._transmuxer.on(TransmuxingEvents.INIT_SEGMENT, (type, is) => {
  183. this._msectl.appendInitSegment(is);
  184. });
  185. this._transmuxer.on(TransmuxingEvents.MEDIA_SEGMENT, (type, ms) => {
  186. this._msectl.appendMediaSegment(ms);
  187. // lazyLoad check
  188. if (this._config.lazyLoad && !this._config.isLive) {
  189. let currentTime = this._mediaElement.currentTime;
  190. if (ms.info.endDts >= (currentTime + this._config.lazyLoadMaxDuration) * 1000) {
  191. if (this._progressChecker == null) {
  192. Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
  193. this._suspendTransmuxer();
  194. }
  195. }
  196. }
  197. });
  198. this._transmuxer.on(TransmuxingEvents.LOADING_COMPLETE, () => {
  199. this._msectl.endOfStream();
  200. this._emitter.emit(PlayerEvents.LOADING_COMPLETE);
  201. });
  202. this._transmuxer.on(TransmuxingEvents.RECOVERED_EARLY_EOF, () => {
  203. this._emitter.emit(PlayerEvents.RECOVERED_EARLY_EOF);
  204. });
  205. this._transmuxer.on(TransmuxingEvents.IO_ERROR, (detail, info) => {
  206. this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, detail, info);
  207. });
  208. this._transmuxer.on(TransmuxingEvents.DEMUX_ERROR, (detail, info) => {
  209. this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, detail, {code: -1, msg: info});
  210. });
  211. this._transmuxer.on(TransmuxingEvents.MEDIA_INFO, (mediaInfo) => {
  212. this._mediaInfo = mediaInfo;
  213. this._emitter.emit(PlayerEvents.MEDIA_INFO, Object.assign({}, mediaInfo));
  214. });
  215. this._transmuxer.on(TransmuxingEvents.METADATA_ARRIVED, (metadata) => {
  216. this._emitter.emit(PlayerEvents.METADATA_ARRIVED, metadata);
  217. });
  218. this._transmuxer.on(TransmuxingEvents.SCRIPTDATA_ARRIVED, (data) => {
  219. this._emitter.emit(PlayerEvents.SCRIPTDATA_ARRIVED, data);
  220. });
  221. this._transmuxer.on(TransmuxingEvents.STATISTICS_INFO, (statInfo) => {
  222. this._statisticsInfo = this._fillStatisticsInfo(statInfo);
  223. this._emitter.emit(PlayerEvents.STATISTICS_INFO, Object.assign({}, this._statisticsInfo));
  224. });
  225. this._transmuxer.on(TransmuxingEvents.RECOMMEND_SEEKPOINT, (milliseconds) => {
  226. if (this._mediaElement && !this._config.accurateSeek) {
  227. this._requestSetTime = true;
  228. this._mediaElement.currentTime = milliseconds / 1000;
  229. }
  230. });
  231. this._transmuxer.open();
  232. }
  233. unload() {
  234. if (this._mediaElement) {
  235. this._mediaElement.pause();
  236. }
  237. if (this._msectl) {
  238. this._msectl.seek(0);
  239. }
  240. if (this._transmuxer) {
  241. this._transmuxer.close();
  242. this._transmuxer.destroy();
  243. this._transmuxer = null;
  244. }
  245. }
  246. play() {
  247. return this._mediaElement.play();
  248. }
  249. pause() {
  250. this._mediaElement.pause();
  251. }
  252. get type() {
  253. return this._type;
  254. }
  255. get buffered() {
  256. return this._mediaElement.buffered;
  257. }
  258. get duration() {
  259. return this._mediaElement.duration;
  260. }
  261. get volume() {
  262. return this._mediaElement.volume;
  263. }
  264. set volume(value) {
  265. this._mediaElement.volume = value;
  266. }
  267. get muted() {
  268. return this._mediaElement.muted;
  269. }
  270. set muted(muted) {
  271. this._mediaElement.muted = muted;
  272. }
  273. get currentTime() {
  274. if (this._mediaElement) {
  275. return this._mediaElement.currentTime;
  276. }
  277. return 0;
  278. }
  279. set currentTime(seconds) {
  280. if (this._mediaElement) {
  281. this._internalSeek(seconds);
  282. } else {
  283. this._pendingSeekTime = seconds;
  284. }
  285. }
  286. get mediaInfo() {
  287. return Object.assign({}, this._mediaInfo);
  288. }
  289. get statisticsInfo() {
  290. if (this._statisticsInfo == null) {
  291. this._statisticsInfo = {};
  292. }
  293. this._statisticsInfo = this._fillStatisticsInfo(this._statisticsInfo);
  294. return Object.assign({}, this._statisticsInfo);
  295. }
  296. _fillStatisticsInfo(statInfo) {
  297. statInfo.playerType = this._type;
  298. if (!(this._mediaElement instanceof HTMLVideoElement)) {
  299. return statInfo;
  300. }
  301. let hasQualityInfo = true;
  302. let decoded = 0;
  303. let dropped = 0;
  304. if (this._mediaElement.getVideoPlaybackQuality) {
  305. let quality = this._mediaElement.getVideoPlaybackQuality();
  306. decoded = quality.totalVideoFrames;
  307. dropped = quality.droppedVideoFrames;
  308. } else if (this._mediaElement.webkitDecodedFrameCount != undefined) {
  309. decoded = this._mediaElement.webkitDecodedFrameCount;
  310. dropped = this._mediaElement.webkitDroppedFrameCount;
  311. } else {
  312. hasQualityInfo = false;
  313. }
  314. if (hasQualityInfo) {
  315. statInfo.decodedFrames = decoded;
  316. statInfo.droppedFrames = dropped;
  317. }
  318. return statInfo;
  319. }
  320. _onmseUpdateEnd() {
  321. if (!this._config.lazyLoad || this._config.isLive) {
  322. return;
  323. }
  324. let buffered = this._mediaElement.buffered;
  325. let currentTime = this._mediaElement.currentTime;
  326. let currentRangeStart = 0;
  327. let currentRangeEnd = 0;
  328. for (let i = 0; i < buffered.length; i++) {
  329. let start = buffered.start(i);
  330. let end = buffered.end(i);
  331. if (start <= currentTime && currentTime < end) {
  332. currentRangeStart = start;
  333. currentRangeEnd = end;
  334. break;
  335. }
  336. }
  337. if (currentRangeEnd >= currentTime + this._config.lazyLoadMaxDuration && this._progressChecker == null) {
  338. Log.v(this.TAG, 'Maximum buffering duration exceeded, suspend transmuxing task');
  339. this._suspendTransmuxer();
  340. }
  341. }
  342. _onmseBufferFull() {
  343. Log.v(this.TAG, 'MSE SourceBuffer is full, suspend transmuxing task');
  344. if (this._progressChecker == null) {
  345. this._suspendTransmuxer();
  346. }
  347. }
  348. _suspendTransmuxer() {
  349. if (this._transmuxer) {
  350. this._transmuxer.pause();
  351. if (this._progressChecker == null) {
  352. this._progressChecker = window.setInterval(this._checkProgressAndResume.bind(this), 1000);
  353. }
  354. }
  355. }
  356. _checkProgressAndResume() {
  357. let currentTime = this._mediaElement.currentTime;
  358. let buffered = this._mediaElement.buffered;
  359. let needResume = false;
  360. for (let i = 0; i < buffered.length; i++) {
  361. let from = buffered.start(i);
  362. let to = buffered.end(i);
  363. if (currentTime >= from && currentTime < to) {
  364. if (currentTime >= to - this._config.lazyLoadRecoverDuration) {
  365. needResume = true;
  366. }
  367. break;
  368. }
  369. }
  370. if (needResume) {
  371. window.clearInterval(this._progressChecker);
  372. this._progressChecker = null;
  373. if (needResume) {
  374. Log.v(this.TAG, 'Continue loading from paused position');
  375. this._transmuxer.resume();
  376. }
  377. }
  378. }
  379. _isTimepointBuffered(seconds) {
  380. let buffered = this._mediaElement.buffered;
  381. for (let i = 0; i < buffered.length; i++) {
  382. let from = buffered.start(i);
  383. let to = buffered.end(i);
  384. if (seconds >= from && seconds < to) {
  385. return true;
  386. }
  387. }
  388. return false;
  389. }
  390. _internalSeek(seconds) {
  391. let directSeek = this._isTimepointBuffered(seconds);
  392. let directSeekBegin = false;
  393. let directSeekBeginTime = 0;
  394. if (seconds < 1.0 && this._mediaElement.buffered.length > 0) {
  395. let videoBeginTime = this._mediaElement.buffered.start(0);
  396. if ((videoBeginTime < 1.0 && seconds < videoBeginTime) || Browser.safari) {
  397. directSeekBegin = true;
  398. // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid
  399. directSeekBeginTime = Browser.safari ? 0.1 : videoBeginTime;
  400. }
  401. }
  402. if (directSeekBegin) { // seek to video begin, set currentTime directly if beginPTS buffered
  403. this._requestSetTime = true;
  404. this._mediaElement.currentTime = directSeekBeginTime;
  405. } else if (directSeek) { // buffered position
  406. if (!this._alwaysSeekKeyframe) {
  407. this._requestSetTime = true;
  408. this._mediaElement.currentTime = seconds;
  409. } else {
  410. let idr = this._msectl.getNearestKeyframe(Math.floor(seconds * 1000));
  411. this._requestSetTime = true;
  412. if (idr != null) {
  413. this._mediaElement.currentTime = idr.dts / 1000;
  414. } else {
  415. this._mediaElement.currentTime = seconds;
  416. }
  417. }
  418. if (this._progressChecker != null) {
  419. this._checkProgressAndResume();
  420. }
  421. } else {
  422. if (this._progressChecker != null) {
  423. window.clearInterval(this._progressChecker);
  424. this._progressChecker = null;
  425. }
  426. this._msectl.seek(seconds);
  427. this._transmuxer.seek(Math.floor(seconds * 1000)); // in milliseconds
  428. // no need to set mediaElement.currentTime if non-accurateSeek,
  429. // just wait for the recommend_seekpoint callback
  430. if (this._config.accurateSeek) {
  431. this._requestSetTime = true;
  432. this._mediaElement.currentTime = seconds;
  433. }
  434. }
  435. }
  436. _checkAndApplyUnbufferedSeekpoint() {
  437. if (this._seekpointRecord) {
  438. if (this._seekpointRecord.recordTime <= this._now() - 100) {
  439. let target = this._mediaElement.currentTime;
  440. this._seekpointRecord = null;
  441. if (!this._isTimepointBuffered(target)) {
  442. if (this._progressChecker != null) {
  443. window.clearTimeout(this._progressChecker);
  444. this._progressChecker = null;
  445. }
  446. // .currentTime is consists with .buffered timestamp
  447. // Chrome/Edge use DTS, while FireFox/Safari use PTS
  448. this._msectl.seek(target);
  449. this._transmuxer.seek(Math.floor(target * 1000));
  450. // set currentTime if accurateSeek, or wait for recommend_seekpoint callback
  451. if (this._config.accurateSeek) {
  452. this._requestSetTime = true;
  453. this._mediaElement.currentTime = target;
  454. }
  455. }
  456. } else {
  457. window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);
  458. }
  459. }
  460. }
  461. _checkAndResumeStuckPlayback(stalled) {
  462. let media = this._mediaElement;
  463. if (stalled || !this._receivedCanPlay || media.readyState < 2) { // HAVE_CURRENT_DATA
  464. let buffered = media.buffered;
  465. if (buffered.length > 0 && media.currentTime < buffered.start(0)) {
  466. Log.w(this.TAG, `Playback seems stuck at ${media.currentTime}, seek to ${buffered.start(0)}`);
  467. this._requestSetTime = true;
  468. this._mediaElement.currentTime = buffered.start(0);
  469. this._mediaElement.removeEventListener('progress', this.e.onvProgress);
  470. }
  471. } else {
  472. // Playback didn't stuck, remove progress event listener
  473. this._mediaElement.removeEventListener('progress', this.e.onvProgress);
  474. }
  475. }
  476. _onvLoadedMetadata(e) {
  477. if (this._pendingSeekTime != null) {
  478. this._mediaElement.currentTime = this._pendingSeekTime;
  479. this._pendingSeekTime = null;
  480. }
  481. }
  482. _onvSeeking(e) { // handle seeking request from browser's progress bar
  483. let target = this._mediaElement.currentTime;
  484. let buffered = this._mediaElement.buffered;
  485. if (this._requestSetTime) {
  486. this._requestSetTime = false;
  487. return;
  488. }
  489. if (target < 1.0 && buffered.length > 0) {
  490. // seek to video begin, set currentTime directly if beginPTS buffered
  491. let videoBeginTime = buffered.start(0);
  492. if ((videoBeginTime < 1.0 && target < videoBeginTime) || Browser.safari) {
  493. this._requestSetTime = true;
  494. // also workaround for Safari: Seek to 0 may cause video stuck, use 0.1 to avoid
  495. this._mediaElement.currentTime = Browser.safari ? 0.1 : videoBeginTime;
  496. return;
  497. }
  498. }
  499. if (this._isTimepointBuffered(target)) {
  500. if (this._alwaysSeekKeyframe) {
  501. let idr = this._msectl.getNearestKeyframe(Math.floor(target * 1000));
  502. if (idr != null) {
  503. this._requestSetTime = true;
  504. this._mediaElement.currentTime = idr.dts / 1000;
  505. }
  506. }
  507. if (this._progressChecker != null) {
  508. this._checkProgressAndResume();
  509. }
  510. return;
  511. }
  512. this._seekpointRecord = {
  513. seekPoint: target,
  514. recordTime: this._now()
  515. };
  516. window.setTimeout(this._checkAndApplyUnbufferedSeekpoint.bind(this), 50);
  517. }
  518. _onvCanPlay(e) {
  519. this._receivedCanPlay = true;
  520. this._mediaElement.removeEventListener('canplay', this.e.onvCanPlay);
  521. }
  522. _onvStalled(e) {
  523. this._checkAndResumeStuckPlayback(true);
  524. }
  525. _onvProgress(e) {
  526. this._checkAndResumeStuckPlayback();
  527. }
  528. }
  529. export default FlvPlayer;