295 lines
7.6 KiB
JavaScript
295 lines
7.6 KiB
JavaScript
JSMpeg.Player = (function(){ "use strict";
|
|
|
|
var Player = function(url, options) {
|
|
options = options || {};
|
|
this.options = options;
|
|
|
|
if (options.source) {
|
|
this.source = new options.source(url, options);
|
|
options.streaming = !!this.source.streaming;
|
|
}
|
|
else if (url.match(/^wss?:\/\//)) {
|
|
this.source = new JSMpeg.Source.WebSocket(url, options);
|
|
options.streaming = true;
|
|
}
|
|
else if (options.progressive !== false) {
|
|
this.source = new JSMpeg.Source.AjaxProgressive(url, options);
|
|
options.streaming = false;
|
|
}
|
|
else {
|
|
this.source = new JSMpeg.Source.Ajax(url, options);
|
|
options.streaming = false;
|
|
}
|
|
|
|
this.maxAudioLag = options.maxAudioLag || 0.25;
|
|
this.loop = options.loop !== false;
|
|
this.autoplay = !!options.autoplay || options.streaming;
|
|
|
|
this.demuxer = new JSMpeg.Demuxer.TS(options);
|
|
this.source.connect(this.demuxer);
|
|
|
|
if (options.video !== false) {
|
|
this.video = new JSMpeg.Decoder.MPEG1Video(options);
|
|
this.renderer = !options.disableGl && JSMpeg.Renderer.WebGL.IsSupported()
|
|
? new JSMpeg.Renderer.WebGL(options)
|
|
: new JSMpeg.Renderer.Canvas2D(options);
|
|
this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.VIDEO_1, this.video);
|
|
this.video.connect(this.renderer);
|
|
}
|
|
|
|
if (options.audio !== false && JSMpeg.AudioOutput.WebAudio.IsSupported()) {
|
|
this.audio = new JSMpeg.Decoder.MP2Audio(options);
|
|
this.audioOut = new JSMpeg.AudioOutput.WebAudio(options);
|
|
this.demuxer.connect(JSMpeg.Demuxer.TS.STREAM.AUDIO_1, this.audio);
|
|
this.audio.connect(this.audioOut);
|
|
}
|
|
|
|
//注册事件
|
|
var events = [
|
|
'onended', //当媒介已到达结尾时
|
|
'oncanplay', //当文件就绪可以开始播放时(缓冲已足够开始时)
|
|
'onpreload', //预加载时,实时回调
|
|
'onplay' //每次从暂停或者加载完成开始播放时
|
|
];
|
|
for(var i=0,eNum=events.length;i<eNum;i++){
|
|
if(typeof options[events[i]]==='function'){
|
|
this[events[i]] = options[events[i]];
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(this, 'currentTime', {
|
|
get: this.getCurrentTime,
|
|
set: this.setCurrentTime
|
|
});
|
|
Object.defineProperty(this, 'volume', {
|
|
get: this.getVolume,
|
|
set: this.setVolume
|
|
});
|
|
|
|
this.unpauseOnShow = false;
|
|
if (options.pauseWhenHidden !== false) {
|
|
document.addEventListener('visibilitychange', this.showHide.bind(this));
|
|
}
|
|
|
|
this.source.start();
|
|
|
|
this.wantsToPlay = this.lastWantsToPlay = false;
|
|
|
|
if(options.preload||this.autoplay){
|
|
this.preload();
|
|
if (this.autoplay) {
|
|
this.play();
|
|
}
|
|
}
|
|
};
|
|
|
|
Player.prototype.showHide = function(ev) {
|
|
if (document.visibilityState === 'hidden') {
|
|
this.unpauseOnShow = this.wantsToPlay;
|
|
this.pause();
|
|
}
|
|
else if (this.unpauseOnShow) {
|
|
this.play();
|
|
}
|
|
};
|
|
|
|
Player.prototype.play = function(ev) {
|
|
this.lastWantsToPlay = this.wantsToPlay;
|
|
this.wantsToPlay = true;
|
|
this.animationId = requestAnimationFrame(this.update.bind(this));
|
|
};
|
|
|
|
Player.prototype.preload = function(ev) {
|
|
if (!this.source.established) {
|
|
requestAnimationFrame(this.preload.bind(this));
|
|
if (this.renderer) {
|
|
if(this.onpreload){
|
|
this.onpreload(this.source.progress)
|
|
}
|
|
this.renderer.renderProgress(this.source.progress);
|
|
}
|
|
}else if(this.oncanplay){
|
|
this.oncanplay();
|
|
this.oncanplay = false;
|
|
}
|
|
};
|
|
|
|
Player.prototype.pause = function(ev) {
|
|
cancelAnimationFrame(this.animationId);
|
|
this.lastWantsToPlay = this.wantsToPlay;
|
|
this.wantsToPlay = false;
|
|
this.isPlaying = false;
|
|
|
|
if (this.audio && this.audio.canPlay) {
|
|
// Seek to the currentTime again - audio may already be enqueued a bit
|
|
// further, so we have to rewind it.
|
|
this.audioOut.stop();
|
|
this.seek(this.currentTime);
|
|
}
|
|
};
|
|
|
|
Player.prototype.getVolume = function() {
|
|
return this.audioOut ? this.audioOut.volume : 0;
|
|
};
|
|
|
|
Player.prototype.setVolume = function(volume) {
|
|
if (this.audioOut) {
|
|
this.audioOut.volume = volume;
|
|
}
|
|
};
|
|
|
|
Player.prototype.stop = function(ev) {
|
|
this.pause();
|
|
this.seek(0);
|
|
if (this.video && this.options.decodeFirstFrame !== false) {
|
|
this.video.decode();
|
|
}
|
|
};
|
|
|
|
Player.prototype.destroy = function() {
|
|
this.pause();
|
|
this.source.destroy();
|
|
this.renderer.destroy();
|
|
this.audioOut.destroy();
|
|
};
|
|
|
|
Player.prototype.seek = function(time) {
|
|
var startOffset = this.audio && this.audio.canPlay
|
|
? this.audio.startTime
|
|
: this.video.startTime;
|
|
|
|
if (this.video) {
|
|
this.video.seek(time + startOffset);
|
|
}
|
|
if (this.audio) {
|
|
this.audio.seek(time + startOffset);
|
|
}
|
|
|
|
this.startTime = JSMpeg.Now() - time;
|
|
};
|
|
|
|
Player.prototype.getCurrentTime = function() {
|
|
return this.audio && this.audio.canPlay
|
|
? this.audio.currentTime - this.audio.startTime
|
|
: this.video.currentTime - this.video.startTime;
|
|
};
|
|
|
|
Player.prototype.setCurrentTime = function(time) {
|
|
this.seek(time);
|
|
};
|
|
|
|
Player.prototype.update = function() {
|
|
this.animationId = requestAnimationFrame(this.update.bind(this));
|
|
|
|
//还没加载好或者还不想开始
|
|
if(!this.wantsToPlay||!this.source.established)
|
|
return;
|
|
|
|
//从暂停到播放,要调用事件
|
|
if(!this.lastWantsToPlay&&this.onplay){
|
|
this.onplay();
|
|
}
|
|
|
|
if (!this.isPlaying) {
|
|
this.isPlaying = true;
|
|
this.startTime = JSMpeg.Now() - this.currentTime;
|
|
}
|
|
|
|
if (this.options.streaming) {
|
|
this.updateForStreaming();
|
|
}
|
|
else {
|
|
this.updateForStaticFile();
|
|
}
|
|
};
|
|
|
|
Player.prototype.updateForStreaming = function() {
|
|
// When streaming, immediately decode everything we have buffered up until
|
|
// now to minimize playback latency.
|
|
|
|
if (this.video) {
|
|
this.video.decode();
|
|
}
|
|
|
|
if (this.audio) {
|
|
var decoded = false;
|
|
do {
|
|
// If there's a lot of audio enqueued already, disable output and
|
|
// catch up with the encoding.
|
|
if (this.audioOut.enqueuedTime > this.maxAudioLag) {
|
|
this.audioOut.resetEnqueuedTime();
|
|
this.audioOut.enabled = false;
|
|
}
|
|
decoded = this.audio.decode();
|
|
} while (decoded);
|
|
this.audioOut.enabled = true;
|
|
}
|
|
};
|
|
|
|
Player.prototype.updateForStaticFile = function() {
|
|
var notEnoughData = false,
|
|
headroom = 0;
|
|
|
|
// If we have an audio track, we always try to sync the video to the audio.
|
|
// Gaps and discontinuities are far more percetable in audio than in video.
|
|
|
|
if (this.audio && this.audio.canPlay) {
|
|
// Do we have to decode and enqueue some more audio data?
|
|
while (
|
|
!notEnoughData &&
|
|
this.audio.decodedTime - this.audio.currentTime < 0.25
|
|
) {
|
|
notEnoughData = !this.audio.decode();
|
|
}
|
|
|
|
// Sync video to audio
|
|
if (this.video && this.video.currentTime < this.audio.currentTime) {
|
|
notEnoughData = !this.video.decode();
|
|
}
|
|
|
|
headroom = this.demuxer.currentTime - this.audio.currentTime;
|
|
}
|
|
|
|
else if (this.video) {
|
|
// Video only - sync it to player's wallclock
|
|
var targetTime = (JSMpeg.Now() - this.startTime) + this.video.startTime,
|
|
lateTime = targetTime - this.video.currentTime,
|
|
frameTime = 1/this.video.frameRate;
|
|
|
|
if (this.video && lateTime > 0) {
|
|
// If the video is too far behind (>2 frames), simply reset the
|
|
// target time to the next frame instead of trying to catch up.
|
|
if (lateTime > frameTime * 2) {
|
|
this.startTime += lateTime;
|
|
}
|
|
|
|
notEnoughData = !this.video.decode();
|
|
}
|
|
|
|
headroom = this.demuxer.currentTime - targetTime;
|
|
}
|
|
|
|
// Notify the source of the playhead headroom, so it can decide whether to
|
|
// continue loading further data.
|
|
this.source.resume(headroom);
|
|
|
|
// If we failed to decode and the source is complete, it means we reached
|
|
// the end of our data. We may want to loop.
|
|
if (notEnoughData && this.source.completed) {
|
|
if(this.onended){
|
|
this.onended();
|
|
}
|
|
if (this.loop) {
|
|
this.seek(0);
|
|
}
|
|
else {
|
|
this.pause();
|
|
}
|
|
}
|
|
};
|
|
|
|
return Player;
|
|
|
|
})();
|
|
|