<commit_before>/**
 * Constructor for an analogue of the TimeRanges class
 * returned by various HTMLMediaElement properties
 *
 * Pass an array of two-element arrays, each containing a start and end time.
 */
OGVTimeRanges = window.OGVTimeRanges = function(ranges) {
	Object.defineProperty(this, 'length', {
		get: function getLength() {
			return ranges.length;
		}
	});
	this.start = function(i) {
		return ranges[i][0];
	};
	this.end = function(i) {
		return ranges[i][1];
	};
	return this;
};

/**
 * Player class -- instantiate one of these to get an 'ogvjs' HTML element
 * which has a similar interface to the HTML audio/video elements.
 *
 * @param options: optional dictionary of options:
 *                 'base': string; base URL for additional resources, such as Flash audio shim
 *                 'webGL': bool; pass true to use WebGL acceleration if available
 *                 'forceWebGL': bool; pass true to require WebGL even if not detected
 */
OGVPlayer = window.OGVPlayer = function(options) {
	options = options || {};

	var codecClassName = null,
		codecClassFile = null,
		codecClass = null;

	var webGLdetected = WebGLFrameSink.isAvailable();
	var useWebGL = !!options.webGL && webGLdetected;
	if(!!options.forceWebGL) {
		useWebGL = true;
		if(!webGLdetected) {
			console.log("No support for WebGL detected, but WebGL forced on!");
		}
	}
	
	var State = {
		INITIAL: 'INITIAL',
		SEEKING_END: 'SEEKING_END',
		LOADED: 'LOADED',
		READY: 'READY',
		PLAYING: 'PLAYING',
		PAUSED: 'PAUSED',
		SEEKING: 'SEEKING',
		ENDED: 'ENDED'
	}, state = State.INITIAL;
	
	var SeekState = {
		NOT_SEEKING: 'NOT_SEEKING',
		BISECT_TO_TARGET: 'BISECT_TO_TARGET',
		BISECT_TO_KEYPOINT: 'BISECT_TO_KEYPOINT',
		LINEAR_TO_TARGET: 'LINEAR_TO_TARGET'
	}, seekState = SeekState.NOT_SEEKING;
	
	var audioOptions = {};
	if (typeof options.base === 'string') {
		// Pass the resource dir down to AudioFeeder,
		// so it can load the dynamicaudio.swf
		audioOptions.base = options.base;
	}
	if (typeof options.audioContext !== 'undefined') {
		// Try passing a pre-created audioContext in?
		audioOptions.audioContext = options.audioContext;
	}
	
	var canvas = document.createElement('canvas');
	var frameSink;
	
	// Return a magical custom element!
	var self = document.createElement('ogvjs');
	self.style.display = 'inline-block';
	self.style.position = 'relative';
	self.style.width = '0px'; // size will be expanded later
	self.style.height = '0px';

	canvas.style.position = 'absolute';
	canvas.style.top = '0';
	canvas.style.left = '0';
	canvas.style.width = '100%';
	canvas.style.height = '100%';
	self.appendChild(canvas);

	var getTimestamp;
	if (window.performance === undefined || window.performance.now === undefined) {
		getTimestamp = Date.now;
	} else {
		getTimestamp = window.performance.now.bind(window.performance);
	}

	var placeboCodec, codec, audioFeeder;
	var muted = false,
		initialAudioPosition = 0.0,
		initialAudioOffset = 0.0;
	function initAudioFeeder() {
		audioFeeder = new AudioFeeder( audioOptions );
		if (muted) {
			audioFeeder.mute();
		}
		audioFeeder.onstarved = function() {
			// If we're in a background tab, timers may be throttled.
			// When audio buffers run out, go decode some more stuff.
			pingProcessing();
		};
		audioFeeder.init(audioInfo.channels, audioInfo.rate);
	}
	
	function startAudio(offset) {
		audioFeeder.start();
		var state = audioFeeder.getPlaybackState();
		initialAudioPosition = state.playbackPosition;
		if (offset !== undefined) {
			initialAudioOffset = offset;
		}
	}
	
	function stopAudio() {
		initialAudioOffset = getAudioTime();
		audioFeeder.stop();
	}
	
	/**
	 * Get audio playback time position in file's units
	 *
	 * @return {number} seconds since file start
	 */
	function getAudioTime(state) {
		state = state || audioFeeder.getPlaybackState();
		return (state.playbackPosition - initialAudioPosition) + initialAudioOffset;
	}

	var stream,
		byteLength = 0,
		duration = null,
		lastSeenTimestamp = null,
		nextProcessingTimer,
		started = false,
		paused = true,
		ended = false,
		loadedMetadata = false,
		startedPlaybackInDocument = false;
	
	var framesPlayed = 0;
	// Benchmark data, exposed via getPlaybackStats()
	var framesProcessed = 0, // frames
		targetPerFrameTime = 1000 / 60, // ms
		demuxingTime = 0, // ms
		videoDecodingTime = 0, // ms
		audioDecodingTime = 0, // ms
		bufferTime = 0, // ms
		drawingTime = 0, // ms
		totalJitter = 0; // sum of ms we're off from expected frame delivery time
	// Benchmark data that doesn't clear
	var droppedAudio = 0; // number of times we were starved for audio

	function stopVideo() {
		// kill the previous video if any
		state = State.INITIAL;
		started = false;
		paused = true;
		ended = true;
		loadedMetadata = false;
		continueVideo = null;
		frameEndTimestamp = 0.0;
		lastFrameDecodeTime = 0.0;
		
		if (stream) {
			stream.abort();
			stream = null;
		}
		if (placeboCodec) {
			placeboCodec.destroy();
			placeboCodec = null;
		}
		if (codec) {
			codec.destroy();
			codec = null;
		}
		if (audioFeeder) {
			audioFeeder.close();
			audioFeeder = undefined;
		}
		if (nextProcessingTimer) {
			clearTimeout(nextProcessingTimer);
			nextProcessingTimer = null;
		}
	}
	
	function togglePauseVideo() {
		if (self.paused) {
			self.play();
		} else {
			self.pause();
		}
	}
	
	var continueVideo = null;
	
	var lastFrameTime = getTimestamp(),
		frameEndTimestamp = 0.0,
		yCbCrBuffer = null;
	var lastFrameDecodeTime = 0.0;		
	var targetFrameTime;
	var lastFrameTimestamp = 0.0;

	function processFrame() {
		yCbCrBuffer = codec.dequeueFrame();
		frameEndTimestamp = yCbCrBuffer.timestamp;
	}

	function drawFrame() {
		if (thumbnail) {
			self.removeChild(thumbnail);
			thumbnail = null;
		}

		var start, delta;

		start = getTimestamp();

		frameSink.drawFrame(yCbCrBuffer);

		delta = getTimestamp() - start;
		lastFrameDecodeTime += delta;
		drawingTime += delta;

		framesProcessed++;
		framesPlayed++;

		doFrameComplete();
	}

	function doFrameComplete() {
		if (startedPlaybackInDocument && !document.body.contains(self)) {
			// We've been de-parented since we last ran
			// Stop playback at next opportunity!
			setTimeout(function() {
				self.stop();
			}, 0);
		}

		var newFrameTimestamp = getTimestamp(),
			wallClockTime = newFrameTimestamp - lastFrameTimestamp,
			jitter = Math.abs(wallClockTime - 1000 / fps);
		totalJitter += jitter;

		if (self.onframecallback) {
			self.onframecallback({
				cpuTime: lastFrameDecodeTime,
				clockTime: wallClockTime
			});
		}
		lastFrameDecodeTime = 0;
		lastFrameTimestamp = newFrameTimestamp;
	}


	// -- seek functions
	var seekTargetTime = 0.0,
		seekTargetKeypoint = 0.0,
		bisectTargetTime = 0.0,
		lastSeekPosition,
		lastFrameSkipped,
		seekBisector;

	function startBisection(targetTime) {
		bisectTargetTime = targetTime;
		seekBisector = new Bisector({
			start: 0,
			end: stream.bytesTotal - 1,
			process: function(start, end, position) {
				if (position == lastSeekPosition) {
					return false;
				} else {
					lastSeekPosition = position;
					lastFrameSkipped = false;
					codec.flush();
					stream.seek(position);
					stream.readBytes();
					return true;
				}
			}
		});
		seekBisector.start();
	}

	function seek(toTime) {
		if (stream.bytesTotal === 0) {
			throw new Error('Cannot bisect a non-seekable stream');
		}
		state = State.SEEKING;
		seekTargetTime = toTime;
		seekTargetKeypoint = -1;
		lastFrameSkipped = false;
		lastSeekPosition = -1;
		codec.flush();
		
		if (codec.hasAudio && audioFeeder) {
			stopAudio();
		}
		
		var offset = codec.getKeypointOffset(toTime);
		if (offset > 0) {
			// This file has an index!
			//
			// Start at the keypoint, then decode forward to the desired time.
			//
			seekState = SeekState.LINEAR_TO_TARGET;
			stream.seek(offset);
			stream.readBytes();
		} else {
			// No index.
			//
			// Bisect through the file finding our target time, then we'll
			// have to do it again to reach the keypoint, and *then* we'll
			// have to decode forward back to the desired time.
			//
			seekState = SeekState.BISECT_TO_TARGET;
			startBisection(seekTargetTime);
		}
	}
	
	function continueSeekedPlayback() {
		seekState = SeekState.NOT_SEEKING;
		state = State.PLAYING;
		frameEndTimestamp = codec.frameTimestamp;
		if (codec.hasAudio) {
			seekTargetTime = codec.audioTimestamp;
			startAudio(seekTargetTime);
		} else {
			seekTargetTime = codec.frameTimestamp;
		}
	}
	
	/**
	 * @return {boolean} true to continue processing, false to wait for input data
	 */
	function doProcessLinearSeeking() {
		var frameDuration;
		if (codec.hasVideo) {
			frameDuration = 1 / videoInfo.fps;
		} else {
			frameDuration = 1 / 256; // approximate packet audio size, fake!
		}
		
		if (codec.hasVideo) {
			if (!codec.frameReady) {
				// Haven't found a frame yet, process more data
				return true;
			} else if (codec.frameTimestamp < 0 || codec.frameTimestamp + frameDuration < seekTargetTime) {
				// Haven't found a time yet, or haven't reached the target time.
				// Decode it in case we're at our keyframe or a following intraframe...
				if (codec.decodeFrame()) {
					codec.dequeueFrame();
				}
				return true;
			} else {
				// Reached or surpassed the target time. 
				if (codec.hasAudio) {
					// Keep processing the audio track
				} else {
					continueSeekedPlayback();
					return false;
				}
			}
		}
		if (codec.hasAudio) {
			if (!codec.audioReady) {
				// Haven't found an audio packet yet, process more data
				return true;
			}
			if (codec.audioTimestamp < 0 || codec.audioTimestamp + frameDuration < seekTargetTime) {
				// Haven't found a time yet, or haven't reached the target time.
				// Decode it so when we reach the target we've got consistent data.
				if (codec.decodeAudio()) {
					codec.dequeueAudio();
				}
				return true;
			} else {
				continueSeekedPlayback();
				return false;
			}
		}
		return true;
	}
	
	function doProcessBisectionSeek() {
		var frameDuration,
			timestamp;
		if (codec.hasVideo) {
			if (!codec.frameReady) {
				// Haven't found a frame yet, process more data
				return true;
			}
			timestamp = codec.frameTimestamp;
			frameDuration = 1 / videoInfo.fps;
		} else if (codec.hasAudio) {
			if (!codec.audioReady) {
				// Haven't found an audio packet yet, process more data
				return true;
			}
			timestamp = codec.audioTimestamp;
			frameDuration = 1 / 256; // approximate packet audio size, fake!
		} else {
			throw new Error('Invalid seek state; no audio or video track available');
		}

		if (timestamp < 0) {
			// Haven't found a time yet.
			// Decode in case we're at our keyframe or a following intraframe...
			if (codec.frameReady) {
				if (codec.decodeFrame()) {
					codec.dequeueFrame();
				}
			}
			if (codec.audioReady) {
				if (codec.decodeAudio()) {
					codec.dequeueAudio();
				}
			}
			return true;
		} else if (timestamp - frameDuration > bisectTargetTime) {
			if (seekBisector.left()) {
				// wait for new data to come in
			} else {
				seekTargetTime = codec.frameTimestamp;
				continueSeekedPlayback();
			}
			return false;
		} else if (timestamp + frameDuration < bisectTargetTime) {
			if (seekBisector.right()) {
				// wait for new data to come in
			} else {
				seekTargetTime = codec.frameTimestamp;
				continueSeekedPlayback();
			}
			return false;
		} else {
			// Reached the bisection target!
			if (seekState == SeekState.BISECT_TO_TARGET && (codec.hasVideo && codec.keyframeTimestamp < codec.frameTimestamp)) {
				// We have to go back and find a keyframe. Sigh.
				seekState = SeekState.BISECT_TO_KEYPOINT;
				startBisection(codec.keyframeTimestamp);
				return false;
			} else {
				// Switch to linear mode to find the final target.
				seekState = SeekState.LINEAR_TO_TARGET;
				return true;
			}
		}
		return true;
	}
	

	/**
	 * In IE, pushing data to the Flash shim is expensive.
	 * Combine multiple small Vorbis packet outputs into
	 * larger buffers so we don't have to make as many calls.
	 */
	function joinAudioBuffers(buffers) {
		if (buffers.length == 1) {
			return buffers[0];
		}
		var sampleCount = 0,
			channelCount = buffers[0].length,
			i,
			c,
			out = [];
		for (i = 0; i < buffers.length; i++) {
			sampleCount += buffers[i][0].length;
		}
		for (c = 0; c < channelCount; c++) {
			var channelOut = new Float32Array(sampleCount);
			var position = 0;
			for (i = 0; i < buffers.length; i++) {
				var channelIn = buffers[i][c];
				channelOut.set(channelIn, position);
				position += channelIn.length;
			}
			out.push(channelOut);
		}
		return out;
	}

	function doProcessing() {
		nextProcessingTimer = null;
		
		var audioBuffers = [];
		function queueAudio() {
			if (audioBuffers.length > 0) {
				var start = getTimestamp();
				audioFeeder.bufferData(joinAudioBuffers(audioBuffers));
				var delta = (getTimestamp() - start);
				lastFrameDecodeTime += delta;
				bufferTime += delta;

				if (!codec.hasVideo) {
					framesProcessed++; // pretend!
					doFrameComplete();
				}
			}
		}
		
		var audioBufferedDuration = 0,
			decodedSamples = 0,
			audioState = null,
			audioPlaybackPosition = 0;

		var n = 0;
		while (true) {
			n++;
			if (n > 100) {
				//throw new Error("Got stuck in the loop!");
				console.log("Got stuck in the loop!");
				pingProcessing(10);
				return;
			}

			if (state == State.INITIAL) {
				if (placeboCodec) {
					placeboCodec.process();
				}
				var more = codec.process();

				if (loadedMetadata) {
					// we just fell over from headers into content; call onloadedmetadata etc
					if (!codec.hasVideo && !codec.hasAudio) {
						throw new Error('No audio or video found, something is wrong');
					}
					if (duration === null) {
						if (stream.seekable) {
							state = State.SEEKING_END;
							lastSeenTimestamp = -1;
							codec.flush();
							stream.seek(Math.max(0, stream.bytesTotal - 65536 * 2));
							stream.readBytes();
							return;
						} else {
							console.log('Stream not seekable and no x-content-duration; assuming infinite stream.');
							state = State.LOADED;
							continue;
						}
					} else {
						// We already know the duration.
						state = State.LOADED;
						continue;
					}
				}

				if (!more) {
					// Read more data!
					stream.readBytes();
					return;
				} else {
					// Keep processing headers
					continue;
				}
			}
			
			if (state == State.SEEKING_END) {
				// Look for the last item.
				var more = codec.process();
				
				if (codec.hasVideo && codec.frameReady) {
					lastSeenTimestamp = Math.max(lastSeenTimestamp, codec.frameTimestamp);
					codec.discardFrame();
				}
				if (codec.hasAudio && codec.audioReady) {
					lastSeenTimestamp = Math.max(lastSeenTimestamp, codec.audioTimestamp);
					if (codec.decodeAudio()) {
						codec.dequeueAudio();
					}
				}
				
				if (!more) {
					// Read more data!
					if (stream.bytesRead < stream.bytesTotal) {
						stream.readBytes();
						return;
					} else {
						// We are at the end!
						if (lastSeenTimestamp > 0) {
							duration = lastSeenTimestamp;
							console.log('detected duration ' + duration + ' from end');
						} else {
							console.log('did not find a duration');
						}
						
						// Ok, seek back to the beginning and resync the streams.
						state = State.LOADED;
						codec.flush();
						stream.seek(0);
						stream.readBytes();
						return;
					}
				} else {
					// Keep processing headers
					continue;
				}
			}
			
			if (state == State.LOADED) {
				state = State.READY;
				if (self.onloadedmetadata) {
					self.onloadedmetadata();
				}
				if (paused) {
					// Paused? stop here.
					return;
				} else {
					// Not paused? Continue on to play processing.
					continue;
				}
			}
			
			if (state == State.READY) {
				state = State.PLAYING;
				lastFrameTimestamp = getTimestamp();
				targetFrameTime = lastFrameTimestamp + 1000.0 / fps;
				if (codec.hasAudio) {
					initAudioFeeder();
					audioFeeder.waitUntilReady(function() {
						startAudio(0.0);
						pingProcessing(0);
					});
				} else {
					pingProcessing(0);
				}

				// Fall over to play processing
				return;
			}
			
			if (state == State.SEEKING) {
				if (!codec.process()) {
					stream.readBytes();
					return;
				}
				if (seekState == SeekState.NOT_SEEKING) {
					throw new Error('seeking in invalid state (not seeking?)');
				} else if (seekState == SeekState.BISECT_TO_TARGET) {
					doProcessBisectionSeek();
				} else if (seekState == SeekState.BISECT_TO_KEYPOINT) {
					doProcessBisectionSeek();
				} else if (seekState == SeekState.LINEAR_TO_TARGET) {
					doProcessLinearSeeking();
				}
				
				// Back to the loop to process more data
				continue;
			}
			
			// Process until we run out of data or
			// completely decode a video frame...
			var currentTime = getTimestamp();
			var start = getTimestamp();
			
			if (placeboCodec) {
				placeboCodec.process();
			}
			var more = codec.process();
			
			var delta = (getTimestamp() - start);
			lastFrameDecodeTime += delta;
			demuxingTime += delta;

			if (!more) {
				queueAudio();
				if (stream) {
					// Ran out of buffered input
					stream.readBytes();
				} else {
					// Ran out of stream!
					var finalDelay = 0;
					if (codec.hasAudio) {
						// This doesn't seem to be enough with Flash audio shim.
						// Not quite sure why.
						finalDelay = audioBufferedDuration;
					}
					setTimeout(function() {
						stopVideo();
						ended = true;
						if (self.onended) {
							self.onended();
						}
					}, finalDelay);
				}
				return;
			}
			
			if ((codec.hasAudio || codec.hasVideo) && !(codec.audioReady || codec.frameReady)) {
				// Have to process some more pages to find data. Continue the loop.
				continue;
			}

			if (codec.hasAudio && audioFeeder) {
				if (!audioState) {
					audioState = audioFeeder.getPlaybackState();
					audioPlaybackPosition = getAudioTime(audioState);
					audioBufferedDuration = (audioState.samplesQueued / audioFeeder.targetRate) * 1000;
					droppedAudio = audioState.dropped;
				}

				// Drive on the audio clock!
				var fudgeDelta = 0.1,
					readyForAudio = audioState.samplesQueued <= (audioFeeder.bufferSize * 2),
					frameDelay = (frameEndTimestamp - audioPlaybackPosition) * 1000,
					readyForFrame = (frameDelay <= fudgeDelta);

				var startTimeSpent = getTimestamp();
				if (codec.audioReady && readyForAudio) {
					var start = getTimestamp();
					var ok = codec.decodeAudio();
					var delta = (getTimestamp() - start);
					lastFrameDecodeTime += delta;
					audioDecodingTime += delta;

					var start = getTimestamp();
					if (ok) {
						var buffer = codec.dequeueAudio();
						//audioFeeder.bufferData(buffer);
						audioBuffers.push(buffer);
						audioBufferedDuration += (buffer[0].length / audioInfo.rate) * 1000;
						decodedSamples += buffer[0].length;
					}
				}
				if (codec.frameReady && readyForFrame) {
					var start = getTimestamp();
					var ok = codec.decodeFrame();
					var delta = (getTimestamp() - start);
					lastFrameDecodeTime += delta;
					videoDecodingTime += delta;
					if (ok) {
						processFrame();
						drawFrame();
					} else {
						// Bad packet or something.
						console.log('Bad video packet or something');
					}
					targetFrameTime = currentTime + 1000.0 / fps;
				}
			
				// Check in when all audio runs out
				var bufferDuration = (audioFeeder.bufferSize / audioFeeder.targetRate) * 1000;
				var nextDelays = [];
				if (audioBufferedDuration <= bufferDuration * 2) {
					// NEED MOAR BUFFERS
				} else {
					// Check in when the audio buffer runs low again...
					nextDelays.push(bufferDuration / 2);
					
					if (codec.hasVideo) {
						// Check in when the next frame is due
						// Subtract time we already spent decoding
						var deltaTimeSpent = getTimestamp() - startTimeSpent;
						nextDelays.push(frameDelay - deltaTimeSpent);
					}
				}
				
				var nextDelay = Math.min.apply(Math, nextDelays);
				if (nextDelays.length > 0) {
					if (placeboCodec) {
						// We've primed the JIT compiler... or something... by now;
						// throw away the placebo copy.
						placeboCodec.destroy();
						placeboCodec = null;
					}

					// Keep track of how much time we spend queueing audio as well
					// This is slow when using the Flash shim on IE 10/11
					var start = getTimestamp();
					queueAudio();
					var delta = getTimestamp() - start;
					pingProcessing(Math.max(0, nextDelay - delta));
					return;
				}
			} else if (codec.hasVideo) {
				// Video-only: drive on the video clock
				if (codec.frameReady && getTimestamp() >= targetFrameTime) {
					if (placeboCodec) {
						// We've primed the JIT compiler... or something... by now;
						// throw away the placebo copy.
						placeboCodec.destroy();
						placeboCodec = null;
					}

					// it's time to draw
					var start = getTimestamp();
					var ok = codec.decodeFrame();
					var delta = (getTimestamp() - start);
					lastFrameDecodeTime += delta;
					videoDecodingTime += delta;
					if (ok) {
						processFrame();
						drawFrame();
						targetFrameTime += 1000.0 / fps;
						pingProcessing(0);
					} else {
						console.log('Bad video packet or something');
						pingProcessing(Math.max(0, targetFrameTime - getTimestamp()));
					}
				} else {
					// check in again soon!
					pingProcessing(Math.max(0, targetFrameTime - getTimestamp()));
				}
				return;
			} else {
				// Ok we're just waiting for more input.
			}
		}
	}

	function pingProcessing(delay) {
		if (delay === undefined) {
			delay = -1;
		}
		if (delay >= 0) {
			if (nextProcessingTimer) {
				// already scheduled
				return;
			}<commit_msg>timer cleanup<commit_after>/**
 * Constructor for an analogue of the TimeRanges class
 * returned by various HTMLMediaElement properties
 *
 * Pass an array of two-element arrays, each containing a start and end time.
 */
OGVTimeRanges = window.OGVTimeRanges = function(ranges) {
	Object.defineProperty(this, 'length', {
		get: function getLength() {
			return ranges.length;
		}
	});
	this.start = function(i) {
		return ranges[i][0];
	};
	this.end = function(i) {
		return ranges[i][1];
	};
	return this;
};

/**
 * Player class -- instantiate one of these to get an 'ogvjs' HTML element
 * which has a similar interface to the HTML audio/video elements.
 *
 * @param options: optional dictionary of options:
 *                 'base': string; base URL for additional resources, such as Flash audio shim
 *                 'webGL': bool; pass true to use WebGL acceleration if available
 *                 'forceWebGL': bool; pass true to require WebGL even if not detected
 */
OGVPlayer = window.OGVPlayer = function(options) {
	options = options || {};

	var codecClassName = null,
		codecClassFile = null,
		codecClass = null;

	var webGLdetected = WebGLFrameSink.isAvailable();
	var useWebGL = !!options.webGL && webGLdetected;
	if(!!options.forceWebGL) {
		useWebGL = true;
		if(!webGLdetected) {
			console.log("No support for WebGL detected, but WebGL forced on!");
		}
	}
	
	var State = {
		INITIAL: 'INITIAL',
		SEEKING_END: 'SEEKING_END',
		LOADED: 'LOADED',
		READY: 'READY',
		PLAYING: 'PLAYING',
		PAUSED: 'PAUSED',
		SEEKING: 'SEEKING',
		ENDED: 'ENDED'
	}, state = State.INITIAL;
	
	var SeekState = {
		NOT_SEEKING: 'NOT_SEEKING',
		BISECT_TO_TARGET: 'BISECT_TO_TARGET',
		BISECT_TO_KEYPOINT: 'BISECT_TO_KEYPOINT',
		LINEAR_TO_TARGET: 'LINEAR_TO_TARGET'
	}, seekState = SeekState.NOT_SEEKING;
	
	var audioOptions = {};
	if (typeof options.base === 'string') {
		// Pass the resource dir down to AudioFeeder,
		// so it can load the dynamicaudio.swf
		audioOptions.base = options.base;
	}
	if (typeof options.audioContext !== 'undefined') {
		// Try passing a pre-created audioContext in?
		audioOptions.audioContext = options.audioContext;
	}
	
	var canvas = document.createElement('canvas');
	var frameSink;
	
	// Return a magical custom element!
	var self = document.createElement('ogvjs');
	self.style.display = 'inline-block';
	self.style.position = 'relative';
	self.style.width = '0px'; // size will be expanded later
	self.style.height = '0px';

	canvas.style.position = 'absolute';
	canvas.style.top = '0';
	canvas.style.left = '0';
	canvas.style.width = '100%';
	canvas.style.height = '100%';
	self.appendChild(canvas);

	var getTimestamp;
	if (window.performance === undefined || window.performance.now === undefined) {
		getTimestamp = Date.now;
	} else {
		getTimestamp = window.performance.now.bind(window.performance);
	}
	function time(cb) {
		var start = getTimestamp();
		cb();
		var delta = getTimestamp() - start;
		lastFrameDecodeTime += delta;
		return delta;
	}

	var placeboCodec, codec, audioFeeder;
	var muted = false,
		initialAudioPosition = 0.0,
		initialAudioOffset = 0.0;
	function initAudioFeeder() {
		audioFeeder = new AudioFeeder( audioOptions );
		if (muted) {
			audioFeeder.mute();
		}
		audioFeeder.onstarved = function() {
			// If we're in a background tab, timers may be throttled.
			// When audio buffers run out, go decode some more stuff.
			pingProcessing();
		};
		audioFeeder.init(audioInfo.channels, audioInfo.rate);
	}
	
	function startAudio(offset) {
		audioFeeder.start();
		var state = audioFeeder.getPlaybackState();
		initialAudioPosition = state.playbackPosition;
		if (offset !== undefined) {
			initialAudioOffset = offset;
		}
	}
	
	function stopAudio() {
		initialAudioOffset = getAudioTime();
		audioFeeder.stop();
	}
	
	/**
	 * Get audio playback time position in file's units
	 *
	 * @return {number} seconds since file start
	 */
	function getAudioTime(state) {
		state = state || audioFeeder.getPlaybackState();
		return (state.playbackPosition - initialAudioPosition) + initialAudioOffset;
	}

	var stream,
		byteLength = 0,
		duration = null,
		lastSeenTimestamp = null,
		nextProcessingTimer,
		started = false,
		paused = true,
		ended = false,
		loadedMetadata = false,
		startedPlaybackInDocument = false;
	
	var framesPlayed = 0;
	// Benchmark data, exposed via getPlaybackStats()
	var framesProcessed = 0, // frames
		targetPerFrameTime = 1000 / 60, // ms
		demuxingTime = 0, // ms
		videoDecodingTime = 0, // ms
		audioDecodingTime = 0, // ms
		bufferTime = 0, // ms
		drawingTime = 0, // ms
		totalJitter = 0; // sum of ms we're off from expected frame delivery time
	// Benchmark data that doesn't clear
	var droppedAudio = 0; // number of times we were starved for audio

	function stopVideo() {
		// kill the previous video if any
		state = State.INITIAL;
		started = false;
		paused = true;
		ended = true;
		loadedMetadata = false;
		continueVideo = null;
		frameEndTimestamp = 0.0;
		lastFrameDecodeTime = 0.0;
		
		if (stream) {
			stream.abort();
			stream = null;
		}
		if (placeboCodec) {
			placeboCodec.destroy();
			placeboCodec = null;
		}
		if (codec) {
			codec.destroy();
			codec = null;
		}
		if (audioFeeder) {
			audioFeeder.close();
			audioFeeder = undefined;
		}
		if (nextProcessingTimer) {
			clearTimeout(nextProcessingTimer);
			nextProcessingTimer = null;
		}
	}
	
	function togglePauseVideo() {
		if (self.paused) {
			self.play();
		} else {
			self.pause();
		}
	}
	
	var continueVideo = null;
	
	var lastFrameTime = getTimestamp(),
		frameEndTimestamp = 0.0,
		yCbCrBuffer = null;
	var lastFrameDecodeTime = 0.0;		
	var targetFrameTime;
	var lastFrameTimestamp = 0.0;

	function processFrame() {
		yCbCrBuffer = codec.dequeueFrame();
		frameEndTimestamp = yCbCrBuffer.timestamp;
	}

	function drawFrame() {
		if (thumbnail) {
			self.removeChild(thumbnail);
			thumbnail = null;
		}

		drawingTime += time(function() {
			frameSink.drawFrame(yCbCrBuffer);
		});

		framesProcessed++;
		framesPlayed++;

		doFrameComplete();
	}

	function doFrameComplete() {
		if (startedPlaybackInDocument && !document.body.contains(self)) {
			// We've been de-parented since we last ran
			// Stop playback at next opportunity!
			setTimeout(function() {
				self.stop();
			}, 0);
		}

		var newFrameTimestamp = getTimestamp(),
			wallClockTime = newFrameTimestamp - lastFrameTimestamp,
			jitter = Math.abs(wallClockTime - 1000 / fps);
		totalJitter += jitter;

		if (self.onframecallback) {
			self.onframecallback({
				cpuTime: lastFrameDecodeTime,
				clockTime: wallClockTime
			});
		}
		lastFrameDecodeTime = 0;
		lastFrameTimestamp = newFrameTimestamp;
	}


	// -- seek functions
	var seekTargetTime = 0.0,
		seekTargetKeypoint = 0.0,
		bisectTargetTime = 0.0,
		lastSeekPosition,
		lastFrameSkipped,
		seekBisector;

	function startBisection(targetTime) {
		bisectTargetTime = targetTime;
		seekBisector = new Bisector({
			start: 0,
			end: stream.bytesTotal - 1,
			process: function(start, end, position) {
				if (position == lastSeekPosition) {
					return false;
				} else {
					lastSeekPosition = position;
					lastFrameSkipped = false;
					codec.flush();
					stream.seek(position);
					stream.readBytes();
					return true;
				}
			}
		});
		seekBisector.start();
	}

	function seek(toTime) {
		if (stream.bytesTotal === 0) {
			throw new Error('Cannot bisect a non-seekable stream');
		}
		state = State.SEEKING;
		seekTargetTime = toTime;
		seekTargetKeypoint = -1;
		lastFrameSkipped = false;
		lastSeekPosition = -1;
		codec.flush();
		
		if (codec.hasAudio && audioFeeder) {
			stopAudio();
		}
		
		var offset = codec.getKeypointOffset(toTime);
		if (offset > 0) {
			// This file has an index!
			//
			// Start at the keypoint, then decode forward to the desired time.
			//
			seekState = SeekState.LINEAR_TO_TARGET;
			stream.seek(offset);
			stream.readBytes();
		} else {
			// No index.
			//
			// Bisect through the file finding our target time, then we'll
			// have to do it again to reach the keypoint, and *then* we'll
			// have to decode forward back to the desired time.
			//
			seekState = SeekState.BISECT_TO_TARGET;
			startBisection(seekTargetTime);
		}
	}
	
	function continueSeekedPlayback() {
		seekState = SeekState.NOT_SEEKING;
		state = State.PLAYING;
		frameEndTimestamp = codec.frameTimestamp;
		if (codec.hasAudio) {
			seekTargetTime = codec.audioTimestamp;
			startAudio(seekTargetTime);
		} else {
			seekTargetTime = codec.frameTimestamp;
		}
	}
	
	/**
	 * @return {boolean} true to continue processing, false to wait for input data
	 */
	function doProcessLinearSeeking() {
		var frameDuration;
		if (codec.hasVideo) {
			frameDuration = 1 / videoInfo.fps;
		} else {
			frameDuration = 1 / 256; // approximate packet audio size, fake!
		}
		
		if (codec.hasVideo) {
			if (!codec.frameReady) {
				// Haven't found a frame yet, process more data
				return true;
			} else if (codec.frameTimestamp < 0 || codec.frameTimestamp + frameDuration < seekTargetTime) {
				// Haven't found a time yet, or haven't reached the target time.
				// Decode it in case we're at our keyframe or a following intraframe...
				if (codec.decodeFrame()) {
					codec.dequeueFrame();
				}
				return true;
			} else {
				// Reached or surpassed the target time. 
				if (codec.hasAudio) {
					// Keep processing the audio track
				} else {
					continueSeekedPlayback();
					return false;
				}
			}
		}
		if (codec.hasAudio) {
			if (!codec.audioReady) {
				// Haven't found an audio packet yet, process more data
				return true;
			}
			if (codec.audioTimestamp < 0 || codec.audioTimestamp + frameDuration < seekTargetTime) {
				// Haven't found a time yet, or haven't reached the target time.
				// Decode it so when we reach the target we've got consistent data.
				if (codec.decodeAudio()) {
					codec.dequeueAudio();
				}
				return true;
			} else {
				continueSeekedPlayback();
				return false;
			}
		}
		return true;
	}
	
	function doProcessBisectionSeek() {
		var frameDuration,
			timestamp;
		if (codec.hasVideo) {
			if (!codec.frameReady) {
				// Haven't found a frame yet, process more data
				return true;
			}
			timestamp = codec.frameTimestamp;
			frameDuration = 1 / videoInfo.fps;
		} else if (codec.hasAudio) {
			if (!codec.audioReady) {
				// Haven't found an audio packet yet, process more data
				return true;
			}
			timestamp = codec.audioTimestamp;
			frameDuration = 1 / 256; // approximate packet audio size, fake!
		} else {
			throw new Error('Invalid seek state; no audio or video track available');
		}

		if (timestamp < 0) {
			// Haven't found a time yet.
			// Decode in case we're at our keyframe or a following intraframe...
			if (codec.frameReady) {
				if (codec.decodeFrame()) {
					codec.dequeueFrame();
				}
			}
			if (codec.audioReady) {
				if (codec.decodeAudio()) {
					codec.dequeueAudio();
				}
			}
			return true;
		} else if (timestamp - frameDuration > bisectTargetTime) {
			if (seekBisector.left()) {
				// wait for new data to come in
			} else {
				seekTargetTime = codec.frameTimestamp;
				continueSeekedPlayback();
			}
			return false;
		} else if (timestamp + frameDuration < bisectTargetTime) {
			if (seekBisector.right()) {
				// wait for new data to come in
			} else {
				seekTargetTime = codec.frameTimestamp;
				continueSeekedPlayback();
			}
			return false;
		} else {
			// Reached the bisection target!
			if (seekState == SeekState.BISECT_TO_TARGET && (codec.hasVideo && codec.keyframeTimestamp < codec.frameTimestamp)) {
				// We have to go back and find a keyframe. Sigh.
				seekState = SeekState.BISECT_TO_KEYPOINT;
				startBisection(codec.keyframeTimestamp);
				return false;
			} else {
				// Switch to linear mode to find the final target.
				seekState = SeekState.LINEAR_TO_TARGET;
				return true;
			}
		}
		return true;
	}
	

	/**
	 * In IE, pushing data to the Flash shim is expensive.
	 * Combine multiple small Vorbis packet outputs into
	 * larger buffers so we don't have to make as many calls.
	 */
	function joinAudioBuffers(buffers) {
		if (buffers.length == 1) {
			return buffers[0];
		}
		var sampleCount = 0,
			channelCount = buffers[0].length,
			i,
			c,
			out = [];
		for (i = 0; i < buffers.length; i++) {
			sampleCount += buffers[i][0].length;
		}
		for (c = 0; c < channelCount; c++) {
			var channelOut = new Float32Array(sampleCount);
			var position = 0;
			for (i = 0; i < buffers.length; i++) {
				var channelIn = buffers[i][c];
				channelOut.set(channelIn, position);
				position += channelIn.length;
			}
			out.push(channelOut);
		}
		return out;
	}

	function doProcessing() {
		nextProcessingTimer = null;
		
		var audioBuffers = [];
		function queueAudio() {
			if (audioBuffers.length > 0) {
				bufferTime += time(function() {
					audioFeeder.bufferData(joinAudioBuffers(audioBuffers));
				});

				if (!codec.hasVideo) {
					framesProcessed++; // pretend!
					doFrameComplete();
				}
			}
		}
		
		var audioBufferedDuration = 0,
			decodedSamples = 0,
			audioState = null,
			audioPlaybackPosition = 0;

		var n = 0;
		while (true) {
			n++;
			if (n > 100) {
				//throw new Error("Got stuck in the loop!");
				console.log("Got stuck in the loop!");
				pingProcessing(10);
				return;
			}

			if (state == State.INITIAL) {
				if (placeboCodec) {
					placeboCodec.process();
				}
				var more = codec.process();

				if (loadedMetadata) {
					// we just fell over from headers into content; call onloadedmetadata etc
					if (!codec.hasVideo && !codec.hasAudio) {
						throw new Error('No audio or video found, something is wrong');
					}
					if (duration === null) {
						if (stream.seekable) {
							state = State.SEEKING_END;
							lastSeenTimestamp = -1;
							codec.flush();
							stream.seek(Math.max(0, stream.bytesTotal - 65536 * 2));
							stream.readBytes();
							return;
						} else {
							console.log('Stream not seekable and no x-content-duration; assuming infinite stream.');
							state = State.LOADED;
							continue;
						}
					} else {
						// We already know the duration.
						state = State.LOADED;
						continue;
					}
				}

				if (!more) {
					// Read more data!
					stream.readBytes();
					return;
				} else {
					// Keep processing headers
					continue;
				}
			}
			
			if (state == State.SEEKING_END) {
				// Look for the last item.
				var more = codec.process();
				
				if (codec.hasVideo && codec.frameReady) {
					lastSeenTimestamp = Math.max(lastSeenTimestamp, codec.frameTimestamp);
					codec.discardFrame();
				}
				if (codec.hasAudio && codec.audioReady) {
					lastSeenTimestamp = Math.max(lastSeenTimestamp, codec.audioTimestamp);
					if (codec.decodeAudio()) {
						codec.dequeueAudio();
					}
				}
				
				if (!more) {
					// Read more data!
					if (stream.bytesRead < stream.bytesTotal) {
						stream.readBytes();
						return;
					} else {
						// We are at the end!
						if (lastSeenTimestamp > 0) {
							duration = lastSeenTimestamp;
							console.log('detected duration ' + duration + ' from end');
						} else {
							console.log('did not find a duration');
						}
						
						// Ok, seek back to the beginning and resync the streams.
						state = State.LOADED;
						codec.flush();
						stream.seek(0);
						stream.readBytes();
						return;
					}
				} else {
					// Keep processing headers
					continue;
				}
			}
			
			if (state == State.LOADED) {
				state = State.READY;
				if (self.onloadedmetadata) {
					self.onloadedmetadata();
				}
				if (paused) {
					// Paused? stop here.
					return;
				} else {
					// Not paused? Continue on to play processing.
					continue;
				}
			}
			
			if (state == State.READY) {
				state = State.PLAYING;
				lastFrameTimestamp = getTimestamp();
				targetFrameTime = lastFrameTimestamp + 1000.0 / fps;
				if (codec.hasAudio) {
					initAudioFeeder();
					audioFeeder.waitUntilReady(function() {
						startAudio(0.0);
						pingProcessing(0);
					});
				} else {
					pingProcessing(0);
				}

				// Fall over to play processing
				return;
			}
			
			if (state == State.SEEKING) {
				if (!codec.process()) {
					stream.readBytes();
					return;
				}
				if (seekState == SeekState.NOT_SEEKING) {
					throw new Error('seeking in invalid state (not seeking?)');
				} else if (seekState == SeekState.BISECT_TO_TARGET) {
					doProcessBisectionSeek();
				} else if (seekState == SeekState.BISECT_TO_KEYPOINT) {
					doProcessBisectionSeek();
				} else if (seekState == SeekState.LINEAR_TO_TARGET) {
					doProcessLinearSeeking();
				}
				
				// Back to the loop to process more data
				continue;
			}
			
			// Process until we run out of data or
			// completely decode a video frame...
			var currentTime = getTimestamp();
			var more;
			demuxingTime += time(function() {
				if (placeboCodec) {
					placeboCodec.process();
				}
				more = codec.process();
			});

			if (!more) {
				queueAudio();
				if (stream) {
					// Ran out of buffered input
					stream.readBytes();
				} else {
					// Ran out of stream!
					var finalDelay = 0;
					if (codec.hasAudio) {
						// This doesn't seem to be enough with Flash audio shim.
						// Not quite sure why.
						finalDelay = audioBufferedDuration;
					}
					setTimeout(function() {
						stopVideo();
						ended = true;
						if (self.onended) {
							self.onended();
						}
					}, finalDelay);
				}
				return;
			}
			
			if ((codec.hasAudio || codec.hasVideo) && !(codec.audioReady || codec.frameReady)) {
				// Have to process some more pages to find data. Continue the loop.
				continue;
			}

			if (codec.hasAudio && audioFeeder) {
				if (!audioState) {
					audioState = audioFeeder.getPlaybackState();
					audioPlaybackPosition = getAudioTime(audioState);
					audioBufferedDuration = (audioState.samplesQueued / audioFeeder.targetRate) * 1000;
					droppedAudio = audioState.dropped;
				}

				// Drive on the audio clock!
				var fudgeDelta = 0.1,
					readyForAudio = audioState.samplesQueued <= (audioFeeder.bufferSize * 2),
					frameDelay = (frameEndTimestamp - audioPlaybackPosition) * 1000,
					readyForFrame = (frameDelay <= fudgeDelta);

				var startTimeSpent = getTimestamp();
				if (codec.audioReady && readyForAudio) {
					var ok;
					audioDecodingTime += time(function() {
						ok = codec.decodeAudio();
					});

					if (ok) {
						var buffer = codec.dequeueAudio();
						//audioFeeder.bufferData(buffer);
						audioBuffers.push(buffer);
						audioBufferedDuration += (buffer[0].length / audioInfo.rate) * 1000;
						decodedSamples += buffer[0].length;
					}
				}
				if (codec.frameReady && readyForFrame) {
					var ok;
					videoDecodingTime += time(function() {
						ok = codec.decodeFrame();
					});
					if (ok) {
						processFrame();
						drawFrame();
					} else {
						// Bad packet or something.
						console.log('Bad video packet or something');
					}
					targetFrameTime = currentTime + 1000.0 / fps;
				}
			
				// Check in when all audio runs out
				var bufferDuration = (audioFeeder.bufferSize / audioFeeder.targetRate) * 1000;
				var nextDelays = [];
				if (audioBufferedDuration <= bufferDuration * 2) {
					// NEED MOAR BUFFERS
				} else {
					// Check in when the audio buffer runs low again...
					nextDelays.push(bufferDuration / 2);
					
					if (codec.hasVideo) {
						// Check in when the next frame is due
						// Subtract time we already spent decoding
						var deltaTimeSpent = getTimestamp() - startTimeSpent;
						nextDelays.push(frameDelay - deltaTimeSpent);
					}
				}
				
				var nextDelay = Math.min.apply(Math, nextDelays);
				if (nextDelays.length > 0) {
					if (placeboCodec) {
						// We've primed the JIT compiler... or something... by now;
						// throw away the placebo copy.
						placeboCodec.destroy();
						placeboCodec = null;
					}

					// Keep track of how much time we spend queueing audio as well
					// This is slow when using the Flash shim on IE 10/11
					var start = getTimestamp();
					queueAudio();
					var delta = getTimestamp() - start;
					pingProcessing(Math.max(0, nextDelay - delta));
					return;
				}
			} else if (codec.hasVideo) {
				// Video-only: drive on the video clock
				if (codec.frameReady && getTimestamp() >= targetFrameTime) {
					if (placeboCodec) {
						// We've primed the JIT compiler... or something... by now;
						// throw away the placebo copy.
						placeboCodec.destroy();
						placeboCodec = null;
					}

					// it's time to draw
					var ok;
					videoDecodingTime += time(function() {
						ok = codec.decodeFrame();
					});
					if (ok) {
						processFrame();
						drawFrame();
						targetFrameTime += 1000.0 / fps;
						pingProcessing(0);
					} else {
						console.log('Bad video packet or something');
						pingProcessing(Math.max(0, targetFrameTime - getTimestamp()));
					}
				} else {
					// check in again soon!
					pingProcessing(Math.max(0, targetFrameTime - getTimestamp()));
				}
				return;
			} else {
				// Ok we're just waiting for more input.
			}
		}
	}

	function pingProcessing(delay) {
		if (delay === undefined) {
			delay = -1;
		}
		if (delay >= 0) {
			if (nextProcessingTimer) {
				// already scheduled
				return;
			}
{'fraction_non_alphanumeric': 0.1177619191520026, 'fraction_numerical': 0.007345591482833167, 'mean_word_length': 2.722419312970494, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6839768', 'n_tokens_mistral': 17462, 'n_tokens_neox': 13416, 'n_words': 3667}