player.js 8.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. JSMpeg.Player = (function(){ "use strict";
  2. var Player = function(url, options) {
  3. this.options = options || {};
  4. if (options.source) {
  5. this.source = new options.source(url, options);
  6. options.streaming = !!this.source.streaming;
  7. }
  8. else if (url.match(/^wss?:\/\//)) {
  9. this.source = new JSMpeg.Source.WebSocket(url, options);
  10. options.streaming = true;
  11. }
  12. else if (url.toLowerCase() === 'dynamicwebsocketurl') {
  13. this.source = new JSMpeg.Source.WebSocket(url, options);
  14. options.streaming = true;
  15. }
  16. else if (options.progressive !== false) {
  17. this.source = new JSMpeg.Source.AjaxProgressive(url, options);
  18. options.streaming = false;
  19. }
  20. else {
  21. this.source = new JSMpeg.Source.Ajax(url, options);
  22. options.streaming = false;
  23. }
  24. this.maxAudioLag = options.maxAudioLag || 0.25;
  25. this.loop = options.loop !== false;
  26. this.autoplay = !!options.autoplay || options.streaming;
  27. this.demuxer = new JSMpeg.Demuxer.TS(options);
  28. this.source.connect(this.demuxer);
  29. if (!options.disableWebAssembly && JSMpeg.WASMModule.IsSupported()) {
  30. this.wasmModule = JSMpeg.WASMModule.GetModule();
  31. options.wasmModule = this.wasmModule;
  32. }
  33. if (options.video !== false) {
  34. this.video = options.wasmModule
  35. ? new JSMpeg.Decoder.MPEG1VideoWASM(options)
  36. : new JSMpeg.Decoder.MPEG1Video(options);
  37. this.renderer = !options.disableGl && JSMpeg.Renderer.WebGL.IsSupported()
  38. ? new JSMpeg.Renderer.WebGL(options)
  39. : new JSMpeg.Renderer.Canvas2D(options);
  40. this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.VIDEO_1, this.video);
  41. this.video.connect(this.renderer);
  42. }
  43. if (options.audio !== false && JSMpeg.AudioOutput.WebAudio.IsSupported()) {
  44. this.audio = options.wasmModule
  45. ? new JSMpeg.Decoder.MP2AudioWASM(options)
  46. : new JSMpeg.Decoder.MP2Audio(options);
  47. this.audioOut = new JSMpeg.AudioOutput.WebAudio(options);
  48. this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.AUDIO_1, this.audio);
  49. this.audio.connect(this.audioOut);
  50. }
  51. Object.defineProperty(this, 'currentTime', {
  52. get: this.getCurrentTime,
  53. set: this.setCurrentTime
  54. });
  55. Object.defineProperty(this, 'volume', {
  56. get: this.getVolume,
  57. set: this.setVolume
  58. });
  59. this.paused = true;
  60. this.unpauseOnShow = false;
  61. if (options.pauseWhenHidden !== false) {
  62. document.addEventListener('visibilitychange', this.showHide.bind(this));
  63. }
  64. // If we have WebAssembly support, wait until the module is compiled before
  65. // loading the source. Otherwise the decoders won't know what to do with
  66. // the source data.
  67. if (this.wasmModule) {
  68. if (this.wasmModule.ready) {
  69. this.startLoading();
  70. }
  71. else if (JSMpeg.WASM_BINARY_INLINED) {
  72. var wasm = JSMpeg.Base64ToArrayBuffer(JSMpeg.WASM_BINARY_INLINED);
  73. this.wasmModule.loadFromBuffer(wasm, this.startLoading.bind(this));
  74. }
  75. else {
  76. this.wasmModule.loadFromFile('jsmpeg.wasm', this.startLoading.bind(this));
  77. }
  78. }
  79. else {
  80. this.startLoading();
  81. }
  82. };
  83. Player.prototype.startLoading = function() {
  84. this.source.start();
  85. if (this.autoplay) {
  86. this.play();
  87. }
  88. };
  89. Player.prototype.showHide = function(ev) {
  90. if (document.visibilityState === 'hidden') {
  91. this.unpauseOnShow = this.wantsToPlay;
  92. this.pause();
  93. }
  94. else if (this.unpauseOnShow) {
  95. this.play();
  96. }
  97. };
  98. Player.prototype.play = function(ev) {
  99. if (this.animationId) {
  100. return;
  101. }
  102. this.animationId = requestAnimationFrame(this.update.bind(this));
  103. this.wantsToPlay = true;
  104. this.paused = false;
  105. };
  106. Player.prototype.pause = function(ev) {
  107. if (this.paused) {
  108. return;
  109. }
  110. cancelAnimationFrame(this.animationId);
  111. this.animationId = null;
  112. this.wantsToPlay = false;
  113. this.isPlaying = false;
  114. this.paused = true;
  115. if (this.audio && this.audio.canPlay) {
  116. // Seek to the currentTime again - audio may already be enqueued a bit
  117. // further, so we have to rewind it.
  118. this.audioOut.stop();
  119. this.seek(this.currentTime);
  120. }
  121. if (this.options.onPause) {
  122. this.options.onPause(this);
  123. }
  124. };
  125. Player.prototype.getVolume = function() {
  126. return this.audioOut ? this.audioOut.volume : 0;
  127. };
  128. Player.prototype.setVolume = function(volume) {
  129. if (this.audioOut) {
  130. this.audioOut.volume = volume;
  131. }
  132. };
  133. Player.prototype.stop = function(ev) {
  134. this.pause();
  135. this.seek(0);
  136. if (this.video && this.options.decodeFirstFrame !== false) {
  137. this.video.decode();
  138. }
  139. };
  140. Player.prototype.destroy = function() {
  141. this.pause();
  142. this.source.destroy();
  143. this.video && this.video.destroy();
  144. this.renderer && this.renderer.destroy();
  145. this.audio && this.audio.destroy();
  146. this.audioOut && this.audioOut.destroy();
  147. };
  148. Player.prototype.seek = function(time) {
  149. var startOffset = this.audio && this.audio.canPlay
  150. ? this.audio.startTime
  151. : this.video.startTime;
  152. if (this.video) {
  153. this.video.seek(time + startOffset);
  154. }
  155. if (this.audio) {
  156. this.audio.seek(time + startOffset);
  157. }
  158. this.startTime = JSMpeg.Now() - time;
  159. };
  160. Player.prototype.getCurrentTime = function() {
  161. return this.audio && this.audio.canPlay
  162. ? this.audio.currentTime - this.audio.startTime
  163. : this.video.currentTime - this.video.startTime;
  164. };
  165. Player.prototype.setCurrentTime = function(time) {
  166. this.seek(time);
  167. };
  168. Player.prototype.update = function() {
  169. this.animationId = requestAnimationFrame(this.update.bind(this));
  170. if (!this.source.established) {
  171. if (this.renderer) {
  172. this.renderer.renderProgress(this.source.progress);
  173. }
  174. return;
  175. }
  176. if (!this.isPlaying) {
  177. this.isPlaying = true;
  178. this.startTime = JSMpeg.Now() - this.currentTime;
  179. if (this.options.onPlay) {
  180. this.options.onPlay(this);
  181. }
  182. }
  183. if (this.options.streaming) {
  184. this.updateForStreaming();
  185. }
  186. else {
  187. this.updateForStaticFile();
  188. }
  189. };
  190. Player.prototype.updateForStreaming = function() {
  191. // When streaming, immediately decode everything we have buffered up until
  192. // now to minimize playback latency.
  193. if (this.video) {
  194. this.video.decode();
  195. }
  196. if (this.audio) {
  197. var decoded = false;
  198. do {
  199. // If there's a lot of audio enqueued already, disable output and
  200. // catch up with the encoding.
  201. if (this.audioOut.enqueuedTime > this.maxAudioLag) {
  202. this.audioOut.resetEnqueuedTime();
  203. this.audioOut.enabled = false;
  204. }
  205. decoded = this.audio.decode();
  206. } while (decoded);
  207. this.audioOut.enabled = true;
  208. }
  209. };
  210. Player.prototype.nextFrame = function() {
  211. if (this.source.established && this.video) {
  212. return this.video.decode();
  213. }
  214. return false;
  215. };
  216. Player.prototype.updateForStaticFile = function() {
  217. var notEnoughData = false,
  218. headroom = 0;
  219. // If we have an audio track, we always try to sync the video to the audio.
  220. // Gaps and discontinuities are far more percetable in audio than in video.
  221. if (this.audio && this.audio.canPlay) {
  222. // Do we have to decode and enqueue some more audio data?
  223. while (
  224. !notEnoughData &&
  225. this.audio.decodedTime - this.audio.currentTime < 0.25
  226. ) {
  227. notEnoughData = !this.audio.decode();
  228. }
  229. // Sync video to audio
  230. if (this.video && this.video.currentTime < this.audio.currentTime) {
  231. notEnoughData = !this.video.decode();
  232. }
  233. headroom = this.demuxer.currentTime - this.audio.currentTime;
  234. }
  235. else if (this.video) {
  236. // Video only - sync it to player's wallclock
  237. var targetTime = (JSMpeg.Now() - this.startTime) + this.video.startTime,
  238. lateTime = targetTime - this.video.currentTime,
  239. frameTime = 1/this.video.frameRate;
  240. if (this.video && lateTime > 0) {
  241. // If the video is too far behind (>2 frames), simply reset the
  242. // target time to the next frame instead of trying to catch up.
  243. if (lateTime > frameTime * 2) {
  244. this.startTime += lateTime;
  245. }
  246. notEnoughData = !this.video.decode();
  247. }
  248. headroom = this.demuxer.currentTime - targetTime;
  249. }
  250. // Notify the source of the playhead headroom, so it can decide whether to
  251. // continue loading further data.
  252. this.source.resume(headroom);
  253. // If we failed to decode and the source is complete, it means we reached
  254. // the end of our data. We may want to loop.
  255. if (notEnoughData && this.source.completed) {
  256. if (this.loop) {
  257. this.seek(0);
  258. }
  259. else {
  260. this.pause();
  261. if (this.options.onEnded) {
  262. this.options.onEnded(this);
  263. }
  264. }
  265. }
  266. // If there's not enough data and the source is not completed, we have
  267. // just stalled.
  268. else if (notEnoughData && this.options.onStalled) {
  269. this.options.onStalled(this);
  270. }
  271. };
  272. return Player;
  273. })();