text:
/*************************************************************************/
/*  editor_resource_preview.cpp                                          */
/*************************************************************************/
/*                       This file is part of:                           */
/*                           GODOT ENGINE                                */
/*                      https://godotengine.org                          */
/*************************************************************************/
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.                 */
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md).   */
/*                                                                       */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the       */
/* "Software"), to deal in the Software without restriction, including   */
/* without limitation the rights to use, copy, modify, merge, publish,   */
/* distribute, sublicense, and/or sell copies of the Software, and to    */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions:                                             */
/*                                                                       */
/* The above copyright notice and this permission notice shall be        */
/* included in all copies or substantial portions of the Software.       */
/*                                                                       */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
/*************************************************************************/

#include "editor_resource_preview.h"

#include "core/method_bind_ext.gen.inc"

#include "core/io/resource_loader.h"
#include "core/io/resource_saver.h"
#include "core/message_queue.h"
#include "core/os/file_access.h"
#include "core/project_settings.h"
#include "editor_node.h"
#include "editor_scale.h"
#include "editor_settings.h"

bool EditorResourcePreviewGenerator::handles(const String &p_type) const {
	if (get_script_instance() && get_script_instance()->has_method("handles")) {
		return get_script_instance()->call("handles", p_type);
	}
	ERR_FAIL_V_MSG(false, "EditorResourcePreviewGenerator::handles needs to be overridden.");
}

Ref<Texture2D> EditorResourcePreviewGenerator::generate(const RES &p_from, const Size2 &p_size) const {
	if (get_script_instance() && get_script_instance()->has_method("generate")) {
		return get_script_instance()->call("generate", p_from, p_size);
	}
	ERR_FAIL_V_MSG(Ref<Texture2D>(), "EditorResourcePreviewGenerator::generate needs to be overridden.");
}

Ref<Texture2D> EditorResourcePreviewGenerator::generate_from_path(const String &p_path, const Size2 &p_size) const {
	if (get_script_instance() && get_script_instance()->has_method("generate_from_path")) {
		return get_script_instance()->call("generate_from_path", p_path, p_size);
	}

	RES res = ResourceLoader::load(p_path);
	if (!res.is_valid()) {
		return res;
	}
	return generate(res, p_size);
}

bool EditorResourcePreviewGenerator::generate_small_preview_automatically() const {
	if (get_script_instance() && get_script_instance()->has_method("generate_small_preview_automatically")) {
		return get_script_instance()->call("generate_small_preview_automatically");
	}

	return false;
}

bool EditorResourcePreviewGenerator::can_generate_small_preview() const {
	if (get_script_instance() && get_script_instance()->has_method("can_generate_small_preview")) {
		return get_script_instance()->call("can_generate_small_preview");
	}

	return false;
}

void EditorResourcePreviewGenerator::_bind_methods() {
	ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "handles", PropertyInfo(Variant::STRING, "type")));
	ClassDB::add_virtual_method(get_class_static(), MethodInfo(CLASS_INFO(Texture2D), "generate", PropertyInfo(Variant::OBJECT, "from", PROPERTY_HINT_RESOURCE_TYPE, "Resource"), PropertyInfo(Variant::VECTOR2, "size")));
	ClassDB::add_virtual_method(get_class_static(), MethodInfo(CLASS_INFO(Texture2D), "generate_from_path", PropertyInfo(Variant::STRING, "path", PROPERTY_HINT_FILE), PropertyInfo(Variant::VECTOR2, "size")));
	ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "generate_small_preview_automatically"));
	ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::BOOL, "can_generate_small_preview"));
}

EditorResourcePreviewGenerator::EditorResourcePreviewGenerator() {
}

EditorResourcePreview *EditorResourcePreview::singleton = nullptr;

void EditorResourcePreview::_thread_func(void *ud) {
	EditorResourcePreview *erp = (EditorResourcePreview *)ud;
	erp->_thread();
}

void EditorResourcePreview::_preview_ready(const String &p_str, const Ref<Texture2D> &p_texture, const Ref<Texture2D> &p_small_texture, ObjectID id, const StringName &p_func, const Variant &p_ud) {
	String path = p_str;
	{
		MutexLock lock(preview_mutex);

		uint32_t hash = 0;
		uint64_t modified_time = 0;

		if (p_str.begins_with("ID:")) {
			hash = uint32_t(p_str.get_slicec(':', 2).to_int());
			path = "ID:" + p_str.get_slicec(':', 1);
		} else {
			modified_time = FileAccess::get_modified_time(path);
		}

		Item item;
		item.order = order++;
		item.preview = p_texture;
		item.small_preview = p_small_texture;
		item.last_hash = hash;
		item.modified_time = modified_time;

		cache[path] = item;
	}

	MessageQueue::get_singleton()->push_call(id, p_func, path, p_texture, p_small_texture, p_ud);
}

void EditorResourcePreview::_generate_preview(Ref<ImageTexture> &r_texture, Ref<ImageTexture> &r_small_texture, const QueueItem &p_item, const String &cache_base) {
	String type;

	if (p_item.resource.is_valid()) {
		type = p_item.resource->get_class();
	} else {
		type = ResourceLoader::get_resource_type(p_item.path);
	}

	if (type == "") {
		r_texture = Ref<ImageTexture>();
		r_small_texture = Ref<ImageTexture>();
		return; //could not guess type
	}

	int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size");
	thumbnail_size *= EDSCALE;

	r_texture = Ref<ImageTexture>();
	r_small_texture = Ref<ImageTexture>();

	for (int i = 0; i < preview_generators.size(); i++) {
		if (!preview_generators[i]->handles(type)) {
			continue;
		}

		Ref<Texture2D> generated;
		if (p_item.resource.is_valid()) {
			generated = preview_generators[i]->generate(p_item.resource, Vector2(thumbnail_size, thumbnail_size));
		} else {
			generated = preview_generators[i]->generate_from_path(p_item.path, Vector2(thumbnail_size, thumbnail_size));
		}
		r_texture = generated;

		int small_thumbnail_size = EditorNode::get_singleton()->get_theme_base()->get_theme_icon("Object", "EditorIcons")->get_width(); // Kind of a workaround to retrieve the default icon size
		small_thumbnail_size *= EDSCALE;

		if (preview_generators[i]->can_generate_small_preview()) {
			Ref<Texture2D> generated_small;
			if (p_item.resource.is_valid()) {
				generated_small = preview_generators[i]->generate(p_item.resource, Vector2(small_thumbnail_size, small_thumbnail_size));
			} else {
				generated_small = preview_generators[i]->generate_from_path(p_item.path, Vector2(small_thumbnail_size, small_thumbnail_size));
			}
			r_small_texture = generated_small;
		}

		if (!r_small_texture.is_valid() && r_texture.is_valid() && preview_generators[i]->generate_small_preview_automatically()) {
			Ref<Image> small_image = r_texture->get_data();
			small_image = small_image->duplicate();
			small_image->resize(small_thumbnail_size, small_thumbnail_size, Image::INTERPOLATE_CUBIC);
			r_small_texture.instance();
			r_small_texture->create_from_image(small_image);
		}

		break;
	}

	if (!p_item.resource.is_valid()) {
		// cache the preview in case it's a resource on disk
		if (r_texture.is_valid()) {
			//wow it generated a preview... save cache
			bool has_small_texture = r_small_texture.is_valid();
			ResourceSaver::save(cache_base + ".png", r_texture);
			if (has_small_texture) {
				ResourceSaver::save(cache_base + "_small.png", r_small_texture);
			}
			FileAccess *f = FileAccess::open(cache_base + ".txt", FileAccess::WRITE);
			ERR_FAIL_COND_MSG(!f, "Cannot create file '" + cache_base + ".txt'. Check user write permissions.");
			f->store_line(itos(thumbnail_size));
			f->store_line(itos(has_small_texture));
			f->store_line(itos(FileAccess::get_modified_time(p_item.path)));
			f->store_line(FileAccess::get_md5(p_item.path));
			f->close();
			memdelete(f);
		}
	}
}

void EditorResourcePreview::_thread() {
	exited = false;
	while (!exit) {
		preview_sem.wait();
		preview_mutex.lock();

		if (queue.size()) {
			QueueItem item = queue.front()->get();
			queue.pop_front();

			if (cache.has(item.path)) {
				//already has it because someone loaded it, just let it know it's ready
				String path = item.path;
				if (item.resource.is_valid()) {
					path += ":" + itos(cache[item.path].last_hash); //keep last hash (see description of what this is in condition below)
				}

				_preview_ready(path, cache[item.path].preview, cache[item.path].small_preview, item.id, item.function, item.userdata);

				preview_mutex.unlock();
			} else {
				preview_mutex.unlock();

				Ref<ImageTexture> texture;
				Ref<ImageTexture> small_texture;

				int thumbnail_size = EditorSettings::get_singleton()->get("filesystem/file_dialog/thumbnail_size");
				thumbnail_size *= EDSCALE;

				if (item.resource.is_valid()) {
					_generate_preview(texture, small_texture, item, String());

					//adding hash to the end of path (should be ID:<objid>:<hash>) because of 5 argument limit to call_deferred
					_preview_ready(item.path + ":" + itos(item.resource->hash_edited_version()), texture, small_texture, item.id, item.function, item.userdata);

				} else {
					String temp_path = EditorSettings::get_singleton()->get_cache_dir();
					String cache_base = ProjectSettings::get_singleton()->globalize_path(item.path).md5_text();
					cache_base = temp_path.plus_file("resthumb-" + cache_base);

					//does not have it, try to load a cached thumbnail

					String file = cache_base + ".txt";
					FileAccess *f = FileAccess::open(file, FileAccess::READ);
					if (!f) {
						// No cache found, generate
						_generate_preview(texture, small_texture, item, cache_base);
					} else {
						uint64_t modtime = FileAccess::get_modified_time(item.path);
						int tsize = f->get_line().to_int();
						bool has_small_texture = f->get_line().to_int();
						uint64_t last_modtime = f->get_line().to_int();

						bool cache_valid = true;

						if (tsize != thumbnail_size) {
							cache_valid = false;
							memdelete(f);
						} else if (last_modtime != modtime) {
							String last_md5 = f->get_line();
							String md5 = FileAccess::get_md5(item.path);
							memdelete(f);

							if (last_md5 != md5) {
								cache_valid = false;

							} else {
								//update modified time

								f = FileAccess::open(file, FileAccess::WRITE);
								if (!f) {
									// Not returning as this would leave the thread hanging and would require
									// some proper cleanup/disabling of resource preview generation.
									ERR_PRINT("Cannot create file '" + file + "'. Check user write permissions.");
								} else {
									f->store_line(itos(thumbnail_size));
									f->store_line(itos(has_small_texture));
									f->store_line(itos(modtime));
									f->store_line(md5);
									memdelete(f);
								}
							}
						} else {
							memdelete(f);
						}

						if (cache_valid) {
							Ref<Image> img;
							img.instance();
							Ref<Image> small_img;
							small_img.instance();

							if (img->load(cache_base + ".png") != OK) {
								cache_valid = false;
							} else {
								texture.instance();
								texture->create_from_image(img);

								if (has_small_texture) {
									if (small_img->load(cache_base + "_small.png") != OK) {
										cache_valid = false;
									} else {
										small_texture.instance();
										small_texture->create_from_image(small_img);
									}
								}
							}
						}

						if (!cache_valid) {
							_generate_preview(texture, small_texture, item, cache_base);
						}
					}
					_preview_ready(item.path, texture, small_texture, item.id, item.function, item.userdata);
				}
			}

		} else {
			preview_mutex.unlock();
		}
	}
	exited = true;
}

void EditorResourcePreview::queue_edited_resource_preview(const Ref<Resource> &p_res, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) {
	ERR_FAIL_NULL(p_receiver);
	ERR_FAIL_COND(!p_res.is_valid());

	{
		MutexLock lock(preview_mutex);

		String path_id = "ID:" + itos(p_res->get_instance_id());

		if (cache.has(path_id) && cache[path_id].last_hash == p_res->hash_edited_version()) {
			cache[path_id].order = order++;
			p_receiver->call(p_receiver_func, path_id, cache[path_id].preview, cache[path_id].small_preview, p_userdata);
			return;
		}

		cache.erase(path_id); //erase if exists, since it will be regen

		QueueItem item;
		item.function = p_receiver_func;
		item.id = p_receiver->get_instance_id();
		item.resource = p_res;
		item.path = path_id;
		item.userdata = p_userdata;

		queue.push_back(item);
	}
	preview_sem.post();
}

void EditorResourcePreview::queue_resource_preview(const String &p_path, Object *p_receiver, const StringName &p_receiver_func, const Variant &p_userdata) {
	ERR_FAIL_NULL(p_receiver);
	{
		MutexLock lock(preview_mutex);

		if (cache.has(p_path)) {
			cache[p_path].order = order++;
			p_receiver->call(p_receiver_func, p_path, cache[p_path].preview, cache[p_path].small_preview, p_userdata);
			return;
		}

		QueueItem item;
		item.function = p_receiver_func;
		item.id = p_receiver->get_instance_id();
		item.path = p_path;
		item.userdata = p_userdata;

		queue.push_back(item);
	}
	preview_sem.post();
}

void EditorResourcePreview::add_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) {
	preview_generators.push_back(p_generator);
}

void EditorResourcePreview::remove_preview_generator(const Ref<EditorResourcePreviewGenerator> &p_generator) {
	preview_generators.erase(p_generator);
}

EditorResourcePreview *EditorResourcePreview::get_singleton() {
	return singleton;
}

void EditorResourcePreview::_bind_methods() {
	ClassDB::bind_method("_preview_ready", &EditorResourcePreview::_preview_ready);

	ClassDB::bind_method(D_METHOD("queue_resource_preview", "path", "receiver", "receiver_func", "userdata"), &EditorResourcePreview::queue_resource_preview);
	ClassDB::bind_method(D_METHOD("queue_edited_resource_preview", "resource", "receiver", "receiver_func", "userdata"), &EditorResourcePreview::queue_edited_resource_preview);
	ClassDB::bind_method(D_METHOD("add_preview_generator", "generator"), &EditorResourcePreview::add_preview_generator);
	ClassDB::bind_method(D_METHOD("remove_preview_generator", "generator"), &EditorResourcePreview::remove_preview_generator);
	ClassDB::bind_method(D_METHOD("check_for_invalidation", "path"), &EditorResourcePreview::check_for_invalidation);

	ADD_SIGNAL(MethodInfo("preview_invalidated", PropertyInfo(Variant::STRING, "path")));
}

void EditorResourcePreview::check_for_invalidation(const String &p_path) {
	bool call_invalidated = false;
	{
		MutexLock lock(preview_mutex);

		if (cache.has(p_path)) {
			uint64_t modified_time = FileAccess::get_modified_time(p_path);
			if (modified_time != cache[p_path].modified_time) {
				cache.erase(p_path);
				call_invalidated = true;
			}
		}
	}

	if (call_invalidated) { //do outside mutex
		call_deferred("emit_signal", "preview_invalidated", p_path);
	}
}

void EditorResourcePreview::start() {
	ERR_FAIL_COND_MSG(thread, "Thread already started.");
	thread = Thread::create(_thread_func, this);
}

void EditorResourcePreview::stop() {
	if (thread) {
		exit = true;
		preview_sem.post();
		while (!exited) {
			OS::get_singleton()->delay_usec(10000);
			RenderingServer::get_singleton()->sync(); //sync pending stuff, as thread may be blocked on visual server
		}
		Thread::wait_to_finish(thread);
		memdelete(thread);
		thread = nullptr;
	}
}

EditorResourcePreview::EditorResourcePreview() {
	thread = nullptr;
	singleton = this;
	order = 0;
	exit = false;
	exited = false;
}

EditorResourcePreview::~EditorResourcePreview() {
	stop();
}


text:
package io.datarouter.websocket.auth;

import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
public class GuiceWebSocketAuthenticationFilter extends WebSocketAuthenticationFilter{

	@Inject
	private UserTokenRetriever userTokenRetriever;

	@Override
	protected UserTokenRetriever getUserTokenRetriever(){
		return userTokenRetriever;
	}

}

redpajama_set_name: RedPajamaGithub


text:
goog.provide('shaka.player.StreamVideoSource');

goog.require('shaka.asserts');
goog.require('shaka.features');
goog.require('shaka.log');
goog.require('shaka.media.IAbrManager');
goog.require('shaka.media.IStream');
goog.require('shaka.media.ManifestInfo');
goog.require('shaka.media.ManifestUpdater');
goog.require('shaka.media.PeriodInfo');
goog.require('shaka.media.SimpleAbrManager');
goog.require('shaka.media.Stream');
goog.require('shaka.media.StreamInfo');
goog.require('shaka.media.StreamInfoProcessor');
goog.require('shaka.media.StreamSetInfo');
goog.require('shaka.media.TextStream');
goog.require('shaka.player.AudioTrack');
goog.require('shaka.player.Defaults');
goog.require('shaka.player.IVideoSource');
goog.require('shaka.player.Restrictions');
goog.require('shaka.player.VideoTrack');
goog.require('shaka.util.EmeUtils');
goog.require('shaka.util.EventManager');
goog.require('shaka.util.FakeEventTarget');
goog.require('shaka.util.IBandwidthEstimator');
goog.require('shaka.util.LanguageUtils');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StringUtils');

/**
 * @event shaka.player.StreamVideoSource.SeekRangeChangedEvent
 * @description Fired when the seekable range changes.
 * @property {string} type 'seekrangechanged'
 * @property {boolean} bubbles true
 * @property {number} start The earliest time that can be seeked to, in seconds.
 * @property {number} end The latest time that can be seeked to, in seconds.
 * @export
 */

/**
 * @event shaka.player.StreamVideoSource.TracksChangedEvent
 * @description Fired when one or more audio, video, or text tracks become
 *     available or unavailable.
 * @property {string} type 'trackschanged'
 * @property {boolean} bubbles true
 * @export
 */

/**
 * Creates a StreamVideoSource.
 * The new StreamVideoSource takes ownership of |manifestInfo|.
 *
 * @param {shaka.media.ManifestInfo} manifestInfo
 * @param {!shaka.util.IBandwidthEstimator} estimator
 * @param {!shaka.media.IAbrManager} abrManager
 *
 * @fires shaka.player.StreamVideoSource.SeekRangeChangedEvent
 * @listens shaka.media.Stream.EndedEvent
 * @listens shaka.media.Stream.StartedEvent
 * @listens shaka.util.IBandwidthEstimator.BandwidthEvent
 *
 * @constructor
 * @struct
 * @implements {shaka.player.IVideoSource}
 * @extends {shaka.util.FakeEventTarget}
 * @export
 */
shaka.player.StreamVideoSource = function(manifestInfo, estimator, abrManager) {
  shaka.util.FakeEventTarget.call(this, null);

  /** @protected {shaka.media.ManifestInfo} */
  this.manifestInfo = manifestInfo;

  /** @protected {!shaka.util.IBandwidthEstimator} */
  this.estimator = estimator;

  /** @protected {!shaka.util.EventManager} */
  this.eventManager = new shaka.util.EventManager();

  /** @protected {!MediaSource} */
  this.mediaSource = new MediaSource();

  /** @protected {HTMLVideoElement} */
  this.video = null;

  /** @protected {number} */
  this.mpdRequestTimeout = shaka.player.Defaults.MPD_REQUEST_TIMEOUT;

  /**
   * All usable StreamSetInfos from the manifest. Each StreamInfo contained
   * within is mutually compatible with all other StreamInfos of the same type.
   * Populated in selectConfigurations().
   * @protected {!shaka.util.MultiMap.<!shaka.media.StreamSetInfo>}
   * TODO(story 1890046): Support multiple periods.
   */
  this.streamSetsByType = new shaka.util.MultiMap();

  /** @private {!shaka.media.IAbrManager} */
  this.abrManager_ = abrManager;
  this.abrManager_.initialize(estimator, this);

  /** @private {boolean} */
  this.loaded_ = false;

  /** @private {string} */
  this.lang_ = shaka.player.Defaults.PREFERRED_LANGUAGE;

  /** @private {boolean} */
  this.subsNeeded_ = false;

  /** @private {shaka.player.Stats} */
  this.stats_ = null;

  /** @private {!shaka.util.PublicPromise} */
  this.attachPromise_ = new shaka.util.PublicPromise();

  /** @private {!shaka.player.Restrictions} */
  this.restrictions_ = new shaka.player.Restrictions();

  /** @private {?number} */
  this.ignoreSeek_ = null;

  /** @private {number} */
  this.originalPlaybackRate_ = 1;

  /** @private {!Object.<string, !shaka.media.IStream>} */
  this.streamsByType_ = {};

  /** @private {!shaka.util.PublicPromise} */
  this.proceedPromise_ = new shaka.util.PublicPromise();

  /** @private {number} */
  this.liveEdgeOffset_ = 0;

  /** @private {number} */
  this.liveEndTime_ = 0;

  /** @private {number} */
  this.liveStreamEndTimeout_ = shaka.player.Defaults.STREAM_BUFFER_SIZE;

  /** @private {?number} */
  this.liveEndedTimer_ = null;

  /** @private {boolean} */
  this.canSwitch_ = false;

  /**
   * @private {!Object.<string, {streamInfo: !shaka.media.StreamInfo,
   *                             clearBuffer: boolean,
   *                             clearBufferOffset: (number|undefined)}>}
   */
  this.deferredSwitches_ = {};

  /** @private {?number} */
  this.updateTimer_ = null;

  /** @private {?number} */
  this.seekRangeTimer_ = null;

  /** @private {?number} */
  this.playbackStartTime_ = null;

  /** @private {!Object.<string, *>} */
  this.streamConfig_ = {};
};
goog.inherits(shaka.player.StreamVideoSource, shaka.util.FakeEventTarget);

/**
 * @const {number}
 * @private
 */
shaka.player.StreamVideoSource.MIN_UPDATE_PERIOD_ = 3;

/**
 * @const {number}
 * @private
 */
shaka.player.StreamVideoSource.SEEK_TOLERANCE_ = 0.01;

/**
 * @const {number}
 * @private
 */
shaka.player.StreamVideoSource.SEEK_OFFSET_ = 0.5;

/**
 *

 * Configures the StreamVideoSource options.
 *

 *
 * The following configuration options are supported:
 *

 *

 *   enableAdaptation: boolean

 *   Enables or disables automatic bitrate adaptation.
 *
 *

 *   streamBufferSize: number

 *   Sets the maximum amount of content, in seconds, that audio and video
 *   streams will buffer ahead of the playhead.  For DASH streams, this will
 *   be overridden if 'minBufferTime' is larger.
 *
 *

 *   mpdRequestTimeout: number

 *   Sets the MPD request timeout in seconds. A value of zero indicates no
 *   timeout.
 *
 *

 *   segmentRequestTimeout: number

 *   Sets the segment request timeout in seconds. A value of zero indicates no
 *   timeout.
 *
 *

 *   preferredLanguage: string

 *   Sets the preferred language (the default is 'en').
 *
 *

 *   restrictions: shaka.player.Restrictions

 *   Sets the video track restrictions.
 *

 *
 * @example
 *     streamVideoSouce.configure({'streamBufferSize': 20});
 *
 * @param {!Object.<string, *>} config A configuration object, which contains
 *     the configuration options as key-value pairs. All fields should have
 *     already been validated.
 * @override
 */
shaka.player.StreamVideoSource.prototype.configure = function(config) {
  if (config['streamBufferSize'] != null) {
    this.streamConfig_['streamBufferSize'] = config['streamBufferSize'];
  }

  if (config['segmentRequestTimeout'] != null) {
    this.streamConfig_['segmentRequestTimeout'] =
        config['segmentRequestTimeout'];
  }

  this.configureStreams_();

  if (config['enableAdaptation'] != null) {
    this.abrManager_.enable(Boolean(config['enableAdaptation']));
  }

  if (config['mpdRequestTimeout'] != null) {
    this.mpdRequestTimeout = Number(config['mpdRequestTimeout']);
  }

  if (config['liveStreamEndTimeout'] != null) {
    this.liveStreamEndTimeout_ = Number(config['liveStreamEndTimeout']);
  }

  if (config['preferredLanguage'] != null) {
    this.lang_ =
        shaka.util.LanguageUtils.normalize(String(config['preferredLanguage']));
  }

  if (config['restrictions'] != null) {
    var restrictions = /** @type {!shaka.player.Restrictions} */(
        config['restrictions']);
    this.restrictions_ = restrictions;
    if (this.loaded_) {
      this.applyRestrictions_();
    }
  }
};

/**
 * @override
 * @suppress {checkTypes} to set otherwise non-nullable types to null.
 */
shaka.player.StreamVideoSource.prototype.destroy = function() {
  this.attachPromise_.destroy();
  this.proceedPromise_.destroy();

  this.attachPromise_ = null;
  this.proceedPromise_ = null;

  this.cancelSeekRangeTimer_();
  this.cancelUpdateTimer_();

  this.deferredSwitches_ = null;

  this.eventManager.destroy();
  this.eventManager = null;

  shaka.util.MapUtils.values(this.streamsByType_).forEach(
      function(stream) {
        stream.destroy();
      });
  this.streamsByType_ = null;

  this.streamSetsByType = null;

  if (this.manifestInfo) {
    this.manifestInfo.destroy();
    this.manifestInfo = null;
  }

  this.abrManager_.destroy();
  this.abrManager_ = null;
  this.estimator = null;

  this.mediaSource = null;
  this.video = null;
  this.stats_ = null;
  this.restrictions_ = null;

  this.parent = null;
};

/** @override */
shaka.player.StreamVideoSource.prototype.attach = function(player, video) {
  if (!this.loaded_) {
    var error = new Error('Cannot call attach() right now.');
    error.type = 'app';
    return Promise.reject(error);
  }

  this.parent = player;
  this.video = video;
  this.stats_ = player.getStats();

  // The "sourceopen" event fires after setting the video element's "src"
  // attribute.
  this.eventManager.listen(
      this.mediaSource,
      'sourceopen',
      this.onMediaSourceOpen_.bind(this));

  this.eventManager.listen(
      this.estimator,
      'bandwidth',
      this.onBandwidth_.bind(this));

  if (shaka.features.Live && this.manifestInfo.live) {
    this.eventManager.listen(
        player,
        'bufferingStart',
        this.onBufferingStart_.bind(this));

    this.eventManager.listen(
        player,
        'bufferingEnd',
        this.onBufferingEnd_.bind(this));
  }

  // When re-using a video tag in Chrome, mediaKeys can get cleared by Chrome
  // when src is set for the second (or subsequent) time.  This feels like a
  // bug in Chrome.
  // See also: http://crbug.com/459702

  // To work around this, back up the old value and ensure that it is set again
  // before the attach promise is resolved.  This fixes bug #18614098.
  var backupMediaKeys = this.video.mediaKeys;
  this.video.src = window.URL.createObjectURL(this.mediaSource);
  var restorePromise = this.video.setMediaKeys(backupMediaKeys);

  // Return a promise which encompasses both attach and the restoration of
  // mediaKeys.
  return Promise.all([this.attachPromise_, restorePromise]);
};

/** @override */
shaka.player.StreamVideoSource.prototype.load = function() {
  if (this.loaded_) {
    var error = new Error('Cannot call load() right now.');
    error.type = 'app';
    return Promise.reject(error);
  }

  if (!this.manifestInfo || this.manifestInfo.periodInfos.length == 0) {
    var error = new Error('The manifest does not specify any content.');
    error.type = 'stream';
    return Promise.reject(error);
  }

  if (!shaka.features.Live && this.manifestInfo.live) {
    var error = new Error('Live manifest support not enabled.');
    error.type = 'app';
    return Promise.reject(error);
  }

  var periodInfos = this.manifestInfo.periodInfos;
  (new shaka.media.StreamInfoProcessor()).process(periodInfos);

  // TODO(story 1890046): Support multiple periods.
  if (this.manifestInfo.periodInfos.length == 0 ||
      this.manifestInfo.periodInfos[0].streamSetInfos.length == 0) {
    var error = new Error('The manifest specifies content that cannot ' +
                          'be displayed on this browser/platform.');
    error.type = 'stream';
    return Promise.reject(error);
  }

  this.loaded_ = true;

  // Set the Streams' initial buffer sizes.
  this.streamConfig_['initialStreamBufferSize'] =
      this.manifestInfo.minBufferTime;
  this.configureStreams_();

  this.applyRestrictions_();

  return Promise.resolve();
};

if (shaka.features.Live) {
  /**
   * Updates the manifest.
   *
   * @param {boolean} useLocal Whether to update using the local manifest.
   * @private
   */
  shaka.player.StreamVideoSource.prototype.onUpdateManifest_ =
      function(useLocal) {
    shaka.asserts.assert(this.loaded_);
    shaka.asserts.assert(this.manifestInfo.updatePeriod != null);
    shaka.asserts.assert(this.manifestInfo.updateUrl != null);

    shaka.log.info('Updating manifest...');

    var startTime = Date.now();
    this.updateTimer_ = null;

    /** @type {shaka.media.ManifestUpdater} */
    var updater = null;

    var url = /** @type {!shaka.util.FailoverUri} */
        (this.manifestInfo.updateUrl);
    var p = useLocal ?
        this.onUpdateLocalManifest() :
        this.onUpdateManifest(url);

    p.then(shaka.util.TypedBind(this,
        /** @param {!shaka.media.ManifestInfo} newManifestInfo */
        function(newManifestInfo) {
          updater = new shaka.media.ManifestUpdater(newManifestInfo);
          return updater.update(
              /** @type {!shaka.media.ManifestInfo} */ (this.manifestInfo));
        })
    ).then(shaka.util.TypedBind(this,
        /** @param {!Array.<!shaka.media.StreamInfo>} removedStreamInfos */
        function(removedStreamInfos) {
          shaka.log.info('Manifest updated!');

          updater.destroy();
          updater = null;

          for (var i = 0; i < removedStreamInfos.length; ++i) {
            // ManifestUpdater will have already removed the StreamInfo from the
            // manifest, but if the StreamInfo is currently being used then we
            // need to switch to another StreamInfo.
            this.removeStream_(removedStreamInfos[i]);
          }

          // Reconfigure the Streams because |minBufferTime| may have changed.
          this.streamConfig_['initialStreamBufferSize'] =
              this.manifestInfo.minBufferTime;
          this.configureStreams_();

          this.applyRestrictions_();

          if (shaka.util.MapUtils.empty(this.streamsByType_)) {
            // createAndStartStreams_() failed the first time it was called.
            // When createAndStartStreams_() succeeds then beginPlayback_()
            // will call onUpdateManifest_().
            this.createAndStartStreams_();
          } else {
            // Ensure the next update occurs within |manifestInfo.updatePeriod|
            // seconds by taking into account the time it took to update the
            // manifest.
            var endTime = Date.now();
            this.setUpdateTimer_((endTime - startTime) / 1000.0);
          }
        })
    ).catch(shaka.util.TypedBind(this,
        /** @param {*} error */
        function(error) {
          if (updater) {
            updater.destroy();
            updater = null;
          }

          if (error.type != 'aborted') {
            var event = shaka.util.FakeEvent.createErrorEvent(error);
            this.dispatchEvent(event);

            // Try updating again, but ensure we haven't been destroyed.
            if (this.manifestInfo) {
              this.setUpdateTimer_(0);
            }
          }
        })
    );
  };

  /**
   * Update manifest hook. The caller takes ownership of the returned manifest.
   *
   * @param {!shaka.util.FailoverUri} url
   * @return {!Promise.<!shaka.media.ManifestInfo>}
   * @protected
   */
  shaka.player.StreamVideoSource.prototype.onUpdateManifest = function(url) {
    shaka.asserts.notImplemented();
    var error = 'Cannot update manifest with this VideoSource implementation.';
    error.type = 'stream';
    return Promise.reject(error);
  };

  /**
   * Update local manifest hook.  The caller takes ownership of the returned
   * manifest.  This CANNOT return this.manifestInfo.
   *
   * @return {!Promise.<!shaka.media.ManifestInfo>}
   * @protected
   */
  shaka.player.StreamVideoSource.prototype.onUpdateLocalManifest = function() {
    shaka.asserts.notImplemented();
    var error = 'Cannot update manifest with this VideoSource implementation.';
    error.type = 'stream';
    return Promise.reject(error);
  };

  /**
   * Sets the update timer to updated the manifest using the local copy.
   *
   * @private
   */
  shaka.player.StreamVideoSource.prototype.setUpdateLocalManifest_ =
      function() {
    shaka.asserts.assert(!this.updateTimer_);
    shaka.asserts.assert(this.manifestInfo.live);
    shaka.asserts.assert(this.manifestInfo.availabilityStartTime);

    var currentTime = Date.now() / 1000;
    var ast = this.manifestInfo.availabilityStartTime;
    var updateInterval = Math.max(ast - currentTime,
        shaka.player.StreamVideoSource.MIN_UPDATE_PERIOD_);
    shaka.log.debug('updateLocalInterval', updateInterval);

    var callback = this.onUpdateManifest_.bind(this, true);
    this.updateTimer_ = window.setTimeout(callback, 1000 * updateInterval);
  };

  /**
   * Sets the update timer. Does nothing if the manifest does not specify
   * an update period.
   *
   * @param {number} offset An offset, in seconds, to apply to the manifest's
   *     update period.
   * @private
   */
  shaka.player.StreamVideoSource.prototype.setUpdateTimer_ = function(offset) {
    if (this.manifestInfo.updatePeriod == null) {
      return;
    }
    shaka.asserts.assert(this.updateTimer_ == null);

    var updatePeriod =
        Math.max(this.manifestInfo.updatePeriod,
                 shaka.player.StreamVideoSource.MIN_UPDATE_PERIOD_);
    var updateInterval = Math.max(updatePeriod - offset, 0);
    shaka.log.debug('updateInterval', updateInterval);

    var callback = this.onUpdateManifest_.bind(this, false);
    this.updateTimer_ = window.setTimeout(callback, 1000 * updateInterval);
  };
}  // shaka.features.Live

/**
 * Removes the given StreamInfo. Handles removing an active stream.
 *
 * @param {!shaka.media.StreamInfo} streamInfo
 * @private
 */
shaka.player.StreamVideoSource.prototype.removeStream_ = function(streamInfo) {
  var type = streamInfo.getContentType();
  var stream = this.streamsByType_[type];

  if (stream && (stream.getStreamInfo() == streamInfo)) {
    var usableStreamSetInfos =
        this.streamSetsByType.get(streamInfo.getContentType());
    var newStreamInfos = usableStreamSetInfos
        .map(function(streamSetInfo) { return streamSetInfo.streamInfos; })
        .reduce(function(all, part) { return all.concat(part); }, [])
        .filter(function(streamInfo) { return streamInfo.usable(); });
    if (newStreamInfos.length == 0) {
      shaka.log.warning(
          'The stream', streamInfo.id,
          'was removed but an alternate stream does not exist.');
      // Put the StreamInfo back into its StreamSetInfo since we cannot
      // properly remove it.
      usableStreamSetInfos.push(streamInfo);
      return;
    }

    if (this.deferredSwitches_[type].streamInfo == streamInfo) {
      delete this.deferredSwitches_[type];
    }

    // Just ignore |canSwitch_| and switch right now.
    stream.switch(newStreamInfos[0], true /* clearBuffer */);

    streamInfo.destroy();
  }

  shaka.log.info('Removed stream', streamInfo.id);
  streamInfo.destroy();
};

/**
 * @override
 * @export
 */
shaka.player.StreamVideoSource.prototype.getVideoTracks = function() {
  if (!this.streamSetsByType.has('video')) {
    return [];
  }

  var stream = this.streamsByType_['video'];
  var activeStreamInfo = stream ? stream.getStreamInfo() : null;
  var activeId = activeStreamInfo ? activeStreamInfo.uniqueId : 0;

  /** @type {!Array.<!shaka.player.VideoTrack>} */
  var tracks = [];

  var videoSets = this.streamSetsByType.get('video');
  for (var i = 0; i < videoSets.length; ++i) {
    var streamSetInfo = videoSets[i];
    for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) {
      var streamInfo = streamSetInfo.streamInfos[j];
      if (!streamInfo.usable()) continue;

      var id = streamInfo.uniqueId;
      var bandwidth = streamInfo.bandwidth;
      var width = streamInfo.width;
      var height = streamInfo.height;

      var videoTrack =
          new shaka.player.VideoTrack(id, bandwidth, width, height);
      if (id == activeId) {
        videoTrack.active = true;
      }
      tracks.push(videoTrack);
    }
  }

  return tracks;
};

/**
 * @override
 * @export
 */
shaka.player.StreamVideoSource.prototype.getAudioTracks = function() {
  if (!this.streamSetsByType.has('audio')) {
    return [];
  }

  var stream = this.streamsByType_['audio'];
  var activeStreamInfo = stream ? stream.getStreamInfo() : null;
  var activeId = activeStreamInfo ? activeStreamInfo.uniqueId : 0;

  /** @type {!Array.<!shaka.player.AudioTrack>} */
  var tracks = [];

  var audioSets = this.streamSetsByType.get('audio');
  for (var i = 0; i < audioSets.length; ++i) {
    var streamSetInfo = audioSets[i];
    var lang = streamSetInfo.lang;

    for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) {
      var streamInfo = streamSetInfo.streamInfos[j];
      if (!streamInfo.usable()) continue;

      var id = streamInfo.uniqueId;
      var bandwidth = streamInfo.bandwidth;

      var audioTrack = new shaka.player.AudioTrack(id, bandwidth, lang);
      if (id == activeId) {
        audioTrack.active = true;
      }
      tracks.push(audioTrack);
    }
  }

  return tracks;
};

/**
 * @override
 * @export
 */
shaka.player.StreamVideoSource.prototype.getTextTracks = function() {
  if (!this.streamSetsByType.has('text')) {
    return [];
  }

  var stream = this.streamsByType_['text'];
  var activeStreamInfo = stream ? stream.getStreamInfo() : null;
  var activeId = activeStreamInfo ? activeStreamInfo.uniqueId : 0;

  /** @type {!Array.<!shaka.player.TextTrack>} */
  var tracks = [];

  var textSets = this.streamSetsByType.get('text');
  for (var i = 0; i < textSets.length; ++i) {
    var streamSetInfo = textSets[i];
    var lang = streamSetInfo.lang;

    for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) {
      var streamInfo = streamSetInfo.streamInfos[j];
      var id = streamInfo.uniqueId;

      var textTrack = new shaka.player.TextTrack(id, lang);
      if (id == activeId) {
        textTrack.active = true;
        shaka.asserts.assert(stream != null);
        textTrack.enabled = stream.getEnabled();
      }
      tracks.push(textTrack);
    }
  }

  return tracks;
};

/** @override */
shaka.player.StreamVideoSource.prototype.getBufferingGoal = function() {
  return Number(this.streamConfig_['initialStreamBufferSize']);
};

/** @override */
shaka.player.StreamVideoSource.prototype.getConfigurations =
    function() {
  // TODO(story 1890046): Support multiple periods.
  return this.loaded_ ? this.manifestInfo.periodInfos[0].getConfigs() : [];
};

/** @override */
shaka.player.StreamVideoSource.prototype.selectConfigurations = function(
    configs) {
  if (!this.loaded_) {
    shaka.log.warning('Cannot call selectConfigurations() right now.');
    return;
  }

  // Map the stream sets by ID.
  var streamSetsById = {};
  // TODO(story 1890046): Support multiple periods.
  var period = this.manifestInfo.periodInfos[0];
  for (var i = 0; i < period.streamSetInfos.length; ++i) {
    var streamSet = period.streamSetInfos[i];
    streamSetsById[streamSet.uniqueId] = streamSet;
  }

  // Use the IDs to convert the map of configs into a map of stream sets.
  this.streamSetsByType.clear();
  var types = configs.keys();
  for (var i = 0; i < types.length; ++i) {
    var type = types[i];
    var cfgList = configs.get(type);

    if (type == 'video') {
      // We only choose one video stream set.
      var id = cfgList[0].id;
      this.streamSetsByType.push(type, streamSetsById[id]);
    } else if (type == 'audio') {
      // We choose mutually compatible stream sets for audio.
      var basicMimeType = cfgList[0].getBasicMimeType();
      for (var j = 0; j < cfgList.length; ++j) {
        var cfg = cfgList[j];
        if (cfg.getBasicMimeType() != basicMimeType) continue;
        this.streamSetsByType.push(type, streamSetsById[cfg.id]);
      }
    } else {
      // We choose all stream sets otherwise.
      for (var j = 0; j < cfgList.length; ++j) {
        var id = cfgList[j].id;
        this.streamSetsByType.push(type, streamSetsById[id]);
      }
    }
  }

  // Assume subs will be needed.
  this.subsNeeded_ = true;
  var LanguageUtils = shaka.util.LanguageUtils;

  var audioSets = this.streamSetsByType.get('audio');
  if (audioSets) {
    this.sortByLanguage_(audioSets);
    this.streamSetsByType.set('audio', audioSets);

    // If the manifest did not specify a language, assume it is the right one.
    // This means that content creators who omit language because they serve a
    // monolingual demographic will not have annoyed users who have to disable
    // subtitles every single time they play a video.
    var lang = audioSets[0].lang || this.lang_;

    // If the audio language matches the user's language preference, then subs
    // are not needed.
    if (LanguageUtils.match(LanguageUtils.MatchType.MAX, this.lang_, lang)) {
      this.subsNeeded_ = false;
    }
  }

  var textSets = this.streamSetsByType.get('text');
  if (textSets) {
    this.sortByLanguage_(textSets);
    this.streamSetsByType.set('text', textSets);

    var lang = textSets[0].lang || this.lang_;

    // If there is no text track to match the user's language preference,
    // do not turn subs on by default.
    if (!LanguageUtils.match(LanguageUtils.MatchType.MAX, this.lang_, lang)) {
      this.subsNeeded_ = false;
    }
  }
};

/** @override */
shaka.player.StreamVideoSource.prototype.selectVideoTrack =
    function(id, clearBuffer, opt_clearBufferOffset) {
  return this.selectTrack_('video', id, clearBuffer, opt_clearBufferOffset);
};

/** @override */
shaka.player.StreamVideoSource.prototype.selectAudioTrack =
    function(id, clearBuffer) {
  return this.selectTrack_('audio', id, clearBuffer);
};

/** @override */
shaka.player.StreamVideoSource.prototype.selectTextTrack =
    function(id, clearBuffer) {
  return this.selectTrack_('text', id, clearBuffer);
};

/** @override */
shaka.player.StreamVideoSource.prototype.enableTextTrack = function(enabled) {
  var textStream = this.streamsByType_['text'];
  if (textStream) {
    textStream.setEnabled(enabled);
  }
};

/** @override */
shaka.player.StreamVideoSource.prototype.setPlaybackStartTime =
    function(startTime) {
  this.playbackStartTime_ = startTime;
};

/**
 * Applies the video track restrictions, if any.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.applyRestrictions_ = function() {
  shaka.asserts.assert(this.manifestInfo);
  shaka.asserts.assert(this.loaded_);

  if (!this.restrictions_) {
    return;
  }

  var tracksChanged = false;

  // Note that the *Info objects contained within this.manifestInfo are the same
  // objects contained within this.streamSetsByType.
  for (var i = 0; i < this.manifestInfo.periodInfos.length; ++i) {
    var periodInfo = this.manifestInfo.periodInfos[i];

    for (var j = 0; j < periodInfo.streamSetInfos.length; ++j) {
      var streamSetInfo = periodInfo.streamSetInfos[j];

      if (streamSetInfo.contentType != 'video') continue;

      for (var k = 0; k < streamSetInfo.streamInfos.length; ++k) {
        var streamInfo = streamSetInfo.streamInfos[k];

        var originalAllowed = streamInfo.allowedByApplication;
        streamInfo.allowedByApplication = true;

        if (this.restrictions_.maxWidth &&
            streamInfo.width > this.restrictions_.maxWidth) {
          streamInfo.allowedByApplication = false;
        }

        if (this.restrictions_.maxHeight &&
            streamInfo.height > this.restrictions_.maxHeight) {
          streamInfo.allowedByApplication = false;
        }

        if (this.restrictions_.maxBandwidth &&
            streamInfo.bandwidth > this.restrictions_.maxBandwidth) {
          streamInfo.allowedByApplication = false;
        }

        if (this.restrictions_.minBandwidth &&
            streamInfo.bandwidth < this.restrictions_.minBandwidth) {
          streamInfo.allowedByApplication = false;
        }

        if (originalAllowed == streamInfo.allowedByApplication) continue;

        shaka.log.info(
            streamInfo.allowedByApplication ? 'Permitting' : 'Restricting',
            'stream', streamInfo.id + '.',
            'The application has applied new video track restrictions.');

        tracksChanged = true;
      }  // for k
    }  // for j
  }  // for i

  // If selectConfigurations() has not been called yet then there are no tracks
  // yet.
  if (this.streamSetsByType.getAll().length == 0 || !tracksChanged) {
    return;
  }

  this.fireTracksChangedEvent_();

  shaka.asserts.assert(this.streamSetsByType.has('video'));
  var videoTracks = this.getVideoTracks();
  if (videoTracks.length > 0) {
    return;
  }

  // Raise an error but don't explicity stop playback (leave that to the
  // application).
  var error = new Error('The application has restricted all video tracks!');
  error.type = 'app';
  var errorEvent = shaka.util.FakeEvent.createErrorEvent(error);
  this.dispatchEvent(errorEvent);
};

/** @override */
shaka.player.StreamVideoSource.prototype.getSessionIds = function() {
  return [];
};

/** @override */
shaka.player.StreamVideoSource.prototype.isOffline = function() {
  return false;
};

/** @override */
shaka.player.StreamVideoSource.prototype.isLive = function() {
  return this.manifestInfo ? this.manifestInfo.live : false;
};

/** @override */
shaka.player.StreamVideoSource.prototype.onKeyStatusesChange = function(
    keyStatusByKeyId) {
  if (!COMPILED) {
    for (var keyId in keyStatusByKeyId) {
      var prettyKeyId = shaka.util.StringUtils.formatHexString(keyId);
      shaka.log.debug(
          'Key status:', prettyKeyId + ': ' + keyStatusByKeyId[keyId]);
    }
  }

  var tracksChanged = false;

  var streamInfosByKeyId = new shaka.util.MultiMap();

  var streamSetInfos = this.streamSetsByType.getAll();
  for (var i = 0; i < streamSetInfos.length; ++i) {
    var streamSetInfo = streamSetInfos[i];
    for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) {
      var streamInfo = streamSetInfo.streamInfos[j];
      streamInfo.keyIds.forEach(
          function(keyId) { streamInfosByKeyId.push(keyId, streamInfo); });
    }
  }

  for (var keyId in keyStatusByKeyId) {
    var keyStatus = keyStatusByKeyId[keyId];
    var message = shaka.util.EmeUtils.getKeyStatusErrorMessage(keyStatus);

    var streamInfos = streamInfosByKeyId.get(keyId);
    if (!streamInfos) {
      var prettyKeyId = shaka.util.StringUtils.formatHexString(keyId);
      shaka.log.debug('Key', prettyKeyId, 'was not specified in the manifest.');
      if (message) {
        shaka.log.warning('Key', prettyKeyId, 'is not usable.', message);
      }
      continue;
    }

    for (var i = 0; i < streamInfos.length; ++i) {
      var streamInfo = streamInfos[i];
      var originalAllowed = streamInfo.allowedByKeySystem;
      streamInfo.allowedByKeySystem = !message;
      if (originalAllowed == streamInfo.allowedByKeySystem) continue;
      shaka.log.info(
          streamInfo.allowedByKeySystem ? 'Permitting' : 'Restricting',
          'stream', streamInfo.id + '.', message || '');
      tracksChanged = true;
    }
  }

  if (!tracksChanged) {
    return;
  }

  this.fireTracksChangedEvent_();

  var audioTracks = this.getAudioTracks();
  var videoTracks = this.getVideoTracks();

  var noAudio = this.streamSetsByType.has('audio') && (audioTracks.length == 0);
  var noVideo = this.streamSetsByType.has('video') && (videoTracks.length == 0);

  if (!noAudio && !noVideo) {
    return;
  }

  // Raise an error but don't explicitly stop playback (leave that to the
  // application).
  var suffix;
  if (noAudio && noVideo) {
    suffix = 'audio and video tracks.';
  } else if (noAudio) {
    suffix = 'audio tracks.';
  } else {
    suffix = 'video tracks.';
  }
  var error = new Error('The key system has restricted all ' + suffix);
  error.type = 'drm';
  var errorEvent = shaka.util.FakeEvent.createErrorEvent(error);
  this.dispatchEvent(errorEvent);
};

/**
 * Fires a 'trackschanged' event.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.fireTracksChangedEvent_ = function() {
  var event = shaka.util.FakeEvent.create({
    'type': 'trackschanged',
    'bubbles': true
  });

  this.dispatchEvent(event);
};

/**
 * Select a track by ID.
 *
 * @param {string} type The type of track to change, such as 'video', 'audio',
 *     or 'text'.
 * @param {number} id The |uniqueId| field of the desired StreamInfo.
 * @param {boolean} clearBuffer
 * @param {number=} opt_clearBufferOffset if |clearBuffer| and
 *     |opt_clearBufferOffset| are truthy, clear the stream buffer from the
 *     offset (in front of video currentTime) to the end of the stream.
 *
 * @return {boolean} True on success.
 * @private
 */
shaka.player.StreamVideoSource.prototype.selectTrack_ =
    function(type, id, clearBuffer, opt_clearBufferOffset) {
  if (!this.streamSetsByType.has(type)) {
    shaka.log.warning(
        'Cannot select', type, 'track', id,
        'because there are no', type, 'tracks.');
    return false;
  }

  if (!this.streamsByType_[type]) {
    shaka.log.warning(
        'Cannot select', type, 'track', id,
        'because there are no', type, 'streams yet.');
    return false;
  }

  var sets = this.streamSetsByType.get(type);
  for (var i = 0; i < sets.length; ++i) {
    var streamSetInfo = sets[i];
    for (var j = 0; j < streamSetInfo.streamInfos.length; ++j) {
      var streamInfo = streamSetInfo.streamInfos[j];
      if (streamInfo.uniqueId != id) continue;

      if (!streamInfo.allowedByKeySystem) {
        shaka.log.warning(
            'Cannot select', type, 'track', id,
            'because the track is not allowed by the key system.');
        return false;
      }

      if (!streamInfo.allowedByApplication) {
        shaka.log.warning(
            'Cannot select', type, 'track', id,
            'because the track is not allowed by the application.');
        return false;
      }

      if (type != 'text' && !this.canSwitch_) {
        // Note that stream switching is disabled until all SegmentIndexes have
        // been created. This ensures that gaps do not get introduced into the
        // buffer. For example, if all SegmentIndexes are behind by 5 seconds,
        // and we switch to a StreamInfo whose SegmentIndex has not been
        // corrected yet, the next segment will be offset by +5 seconds, which
        // will create a gap in the buffer.
        //
        // However, we still want to process these stream switches, so defer
        // them until later, see onAllStreamsStarted_().
        //
        // Note that if clearBuffer is true for any switch of a particular type
        // then any deferred switch of that type will also set clearBuffer to
        // true.
        var tuple = this.deferredSwitches_[type];

        this.deferredSwitches_[type] = {
          streamInfo: streamInfo,
          clearBuffer: (tuple != null && tuple.clearBuffer) || clearBuffer,
          clearBufferOffset:
              (tuple != null && tuple.clearBufferOffset) ||
              opt_clearBufferOffset
        };
        return true;
      }

      shaka.asserts.assert(this.stats_);
      this.stats_.logStreamChange(streamInfo);

      this.streamsByType_[type].switch(
          streamInfo, clearBuffer, opt_clearBufferOffset);
      return true;
    }
  }

  shaka.log.warning(
      'Cannot select', type, 'track', id, 'because it does not exist.');
  return false;
};

/**
 * Move the best language match to the front of the array.
 *
 * @param {!Array.<!shaka.media.StreamSetInfo>} streamSets
 * @private
 */
shaka.player.StreamVideoSource.prototype.sortByLanguage_ =
    function(streamSets) {
  // Alias.
  var LanguageUtils = shaka.util.LanguageUtils;

  // Do a fuzzy match and stop on the lowest successful fuzz level.
  for (var fuzz = LanguageUtils.MatchType.MIN;
       fuzz <= LanguageUtils.MatchType.MAX;
       ++fuzz) {
    for (var i = 0; i < streamSets.length; ++i) {
      var set = streamSets[i];
      if (LanguageUtils.match(fuzz, this.lang_, set.lang)) {
        // It's a match, so this set should go to the front.
        streamSets.splice(i, 1);
        streamSets.splice(0, 0, set);
        return;
      }
    }
  }

  // If no languages matched, move the "main" set, if any, to the front.
  for (var i = 0; i < streamSets.length; ++i) {
    var set = streamSets[i];
    if (set.main) {
      streamSets.splice(i, 1);
      streamSets.splice(0, 0, set);
      return;
    }
  }
};

/**
 * Called when the MediaSource transitions into the 'open' state. Only
 * called after load() has been called.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.StreamVideoSource.prototype.onMediaSourceOpen_ = function(event) {
  this.eventManager.unlisten(this.mediaSource, 'sourceopen');

  this.createAndStartStreams_().then(shaka.util.TypedBind(this,
      function() {
        if (this.attachPromise_) {
          this.attachPromise_.resolve();
        }
      })
  ).catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (this.attachPromise_) {
          this.attachPromise_.reject(error);
        }
      })
  );
};

/**
 * Instantiates the Streams and begins stream startup.
 *
 * Stream startup consists of starting a Stream for each available content
 * type, waiting for each of these Streams to complete their initialization
 * sequence (see {@link shaka.media.IStream}), applying the global timestamp
 * correction, and then beginning playback.
 *
 * This function resolves the returned Promise when it instantiates and starts
 * the initial set of Streams; at this time, each Stream (i.e., each Stream in
 * this.streamsByType_) will be in the 'startup' state; however, actual
 * playback will not have begun, and the video's playback rate should be zero.
 *
 * When each Stream completes startup we call
 * {@link shaka.player.StreamVideoSource#onAllStreamsStarted_}; at this time,
 * each Stream will be in the 'waiting' state.
 *
 * @return {!Promise} A Promise that resolves when this function creates
 *     the initial set of Streams and begins stream startup. If the manifest
 *     specifies live content then this function will always resolve the
 *     returned Promise; otherwise, this function may reject the returned
 *     Promise.
 * @private
 */
shaka.player.StreamVideoSource.prototype.createAndStartStreams_ = function() {
  /** @type {!Array.<!shaka.media.StreamSetInfo>} */
  var selectedStreamSetInfos = [];

  // For each desired type, select the first StreamSetInfo.
  var desiredTypes = ['audio', 'video', 'text'];
  for (var i = 0; i < desiredTypes.length; ++i) {
    var type = desiredTypes[i];
    if (this.streamSetsByType.has(type)) {
      selectedStreamSetInfos.push(this.streamSetsByType.get(type)[0]);
    }
  }

  /** @type {!Object.<string, !shaka.media.StreamInfo>} */
  var selectedStreamInfosByType =
      this.selectStreamInfos_(selectedStreamSetInfos);
  for (var i = 0; i < desiredTypes.length; ++i) {
    var type = desiredTypes[i];
    if (this.streamSetsByType.has(type) && !selectedStreamInfosByType[type]) {
      var error = new Error(
          'Unable to select an initial ' + type + ' stream: ' +
          'all ' + type + ' streams have been restricted ' +
          '(by the application or by the key system).');
      error.type = 'stream';
      return Promise.reject(error);
    }
  }

  // Create/fetch the SegmentIndex for each selected StreamInfo.
  var async = shaka.util.MapUtils.values(selectedStreamInfosByType).map(
      function(streamInfo) {
        return streamInfo.segmentIndexSource.create();
      });
  return Promise.all(async).then(shaka.util.TypedBind(this,
      /** @param {!Array.<!shaka.media.SegmentIndex>} segmentIndexes */
      function(segmentIndexes) {
        // Ensure all streams are available.
        if (!segmentIndexes.every(function(index) { return index.length(); })) {
          shaka.log.debug('At least one SegmentIndex is empty.');
          var error = new Error('Some streams are not available.');
          error.type = 'stream';
          return Promise.reject(error);
        }

        // Compute the initial stream limits.
        var streamLimits = this.computeStreamLimits_(segmentIndexes);
        if (!streamLimits) {
          // This may occur if the manifest is not well formed or if the
          // streams have just become available, such that the streams' media
          // timelines do not intersect, i.e., the streams do not share any
          // timestamps in common.
          var error = new Error('Some streams are not available.');
          error.type = 'stream';
          return Promise.reject(error);
        }

        // Create the Stream objects.
        if (!this.createStreams_(selectedStreamInfosByType)) {
          var error = new Error('Failed to create Stream objects.');
          error.type = 'stream';
          return Promise.reject(error);
        }

        this.abrManager_.start();
        this.startStreams_(selectedStreamInfosByType, streamLimits);
        return Promise.resolve();
      })
  ).catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (error.type == 'aborted') {
          return;
        }

        shaka.asserts.assert(shaka.util.MapUtils.empty(this.streamsByType_));

        // If the manifest specifies live content then suppress the error, we
        // will try to create and start the streams again from
        // onUpdateManifest_().
        if (this.manifestInfo.live) {
          shaka.log.warning(error.message);
          // If @availabilityStartTime is in the future, simply re-process the
          // local manifest once it become available; otherwise fetch a new
          // manifest and process that.
          if (shaka.util.Clock.now() <
              this.manifestInfo.availabilityStartTime) {
            this.setUpdateLocalManifest_();
          } else {
            this.setUpdateTimer_(0);
          }
          return Promise.resolve();
        } else {
          return Promise.reject(error);
        }
      })
  );
};

/**
 * Selects the initial StreamInfos from the given StreamSetsInfos.
 *
 * @param {!Array.<!shaka.media.StreamSetInfo>} streamSetInfos
 * @return {!Object.<string, !shaka.media.StreamInfo>}
 * @private
 */
shaka.player.StreamVideoSource.prototype.selectStreamInfos_ = function(
    streamSetInfos) {
  /** @type {!Object.<string, !shaka.media.StreamInfo>} */
  var selectedStreamInfosByType = {};

  for (var i = 0; i < streamSetInfos.length; ++i) {
    var streamSetInfo = streamSetInfos[i];

    var streamInfo = null;
    if (streamSetInfo.contentType == 'video') {
      // Ask AbrManager which video StreamInfo to start with.
      var trackId = this.abrManager_.getInitialVideoTrackId();
      if (trackId == null) {
        shaka.log.debug('No initial video streams to select.');
        continue;
      }

      var streamInfos = streamSetInfo.streamInfos.filter(
          function(streamInfo) { return streamInfo.uniqueId == trackId; });

      // Ensure AbrManager selected a video StreamInfo from |streamSetInfo|.
      if (streamInfos.length == 0) {
        shaka.log.debug(
            'No initial video streams to select from the selected ' +
            'StreamSetInfo.');
        continue;
      }

      shaka.asserts.assert(streamInfos.length == 1);
      shaka.asserts.assert(streamInfos[0].usable());
      streamInfo = streamInfos[0];
    } else if (streamSetInfo.contentType == 'audio') {
      var usableStreamInfos = streamSetInfo.streamInfos.filter(
          function(streamInfo) { return streamInfo.usable(); });
      if (usableStreamInfos.length == 0) {
        shaka.log.debug('No initial audio streams to select.');
        continue;
      }

      // In lieu of audio adaptation, choose the middle stream from the
      // usable ones.  If we have high, medium, and low quality audio, this
      // is medium.  If we only have high and low, this is high.
      var index = Math.floor(usableStreamInfos.length / 2);
      streamInfo = streamSetInfo.streamInfos[index];
    } else if (streamSetInfo.streamInfos.length > 0) {
      streamInfo = streamSetInfo.streamInfos[0];
    }
    shaka.asserts.assert(streamInfo);

    selectedStreamInfosByType[streamSetInfo.contentType] =
        /** @type {!shaka.media.StreamInfo} */(streamInfo);
  }

  return selectedStreamInfosByType;
};

/**
 * Creates the initial set of Stream objects. Populates |streamsByType_| on
 * success.
 *
 * @param {!Object.<string, !shaka.media.StreamInfo>} streamInfosByType
 * @return {boolean} True on success; otherwise, return false.
 * @private
 */
shaka.player.StreamVideoSource.prototype.createStreams_ = function(
    streamInfosByType) {
  /** @type {!Object.<string, !shaka.media.IStream>} */
  var streamsByType = {};

  for (var type in streamInfosByType) {
    var streamInfo = streamInfosByType[type];

    var stream = type == 'text' ?
                 this.createTextStream_() :
                 this.createStream_(streamInfo);

    if (!stream) {
      var fullMimeType = streamInfo.getFullMimeType();
      shaka.log.error('Failed to create', fullMimeType, 'stream.');
      shaka.util.MapUtils.values(streamsByType).forEach(
          function(stream) {
            stream.destroy();
          });
      return false;
    }

    streamsByType[type] = stream;
  }

  this.streamsByType_ = streamsByType;
  return true;
};

/**
 * Creates a Stream object.
 *
 * @param {!shaka.media.StreamInfo} streamInfo
 * @return {!shaka.media.Stream}
 * @private
 */
shaka.player.StreamVideoSource.prototype.createStream_ = function(streamInfo) {
  shaka.asserts.assert(this.video);
  var fullMimeType = streamInfo.getFullMimeType();
  var stream = new shaka.media.Stream(
      this,
      /** @type {!HTMLVideoElement} */(this.video),
      this.mediaSource,
      streamInfo.getFullMimeType(),
      this.estimator);
  stream.configure(this.streamConfig_);
  return stream;
};

/**
 * Creates a TextStream object.
 *
 * @return {!shaka.media.TextStream}
 * @private
 */
shaka.player.StreamVideoSource.prototype.createTextStream_ = function() {
  shaka.asserts.assert(this.video);
  var video = /** @type {!HTMLVideoElement} */ (this.video);
  return new shaka.media.TextStream(this, video);
};

/**
 * Starts each Stream.
 *
 * @param {!Object.<string, !shaka.media.StreamInfo>} streamInfosByType
 * @param {{start: number, end: number, last: number}} streamLimits The initial
 *     stream limits.
 * @private
 */
shaka.player.StreamVideoSource.prototype.startStreams_ = function(
    streamInfosByType, streamLimits) {
  // If we apply a global timestamp correction after the Streams complete
  // startup then we will also have to adjust the video's current time (see
  // onAllStreamsStarted_), so don't begin playback yet.
  this.originalPlaybackRate_ = this.video.playbackRate;
  this.video.playbackRate = 0;

  this.setUpMediaSource_(streamLimits);

  // Determine the stream start time.
  var streamStartTime;
  if (this.manifestInfo.live) {
    shaka.asserts.assert(streamLimits.end != Number.POSITIVE_INFINITY);
    streamStartTime = streamLimits.end;
  } else {
    // If a specific start time was set, and it's within the stream limits, use
    // that as the start time.
    if (this.playbackStartTime_ &&
        this.playbackStartTime_ <= streamLimits.end &&
        this.playbackStartTime_ >= streamLimits.start) {
      streamStartTime = this.playbackStartTime_;
    } else {
      streamStartTime = streamLimits.start;
    }
  }

  shaka.log.info('Starting each stream from', streamStartTime);

  // Start listening to 'seeking' events right away as we must handle seeking
  // during stream startup.
  this.eventManager.listen(this.video, 'seeking', this.onSeeking_.bind(this));

  if (this.video.currentTime != streamStartTime) {
    // Set the video's current time before starting the streams so that the
    // streams start buffering at the stream start time.
    this.video.currentTime = streamStartTime;

    // Ignore the resulting 'seeking' event since there's no need to resync the
    // streams before buffering (see onSeeking_).
    this.ignoreSeek_ = streamStartTime;
    shaka.log.debug('Ignoring pending seek to', this.ignoreSeek_);
  }

  // Inform the application of the initial seek range.
  this.fireSeekRangeChangedEvent_(streamLimits.start, streamLimits.end);

  // Start the streams.
  var async = [];

  for (var type in this.streamsByType_) {
    var stream = this.streamsByType_[type];

    async.push(stream.started(this.proceedPromise_));

    this.eventManager.listen(
        stream,
        'ended',
        this.onStreamEnded_.bind(this));

    var streamInfo = streamInfosByType[type];
    this.stats_.logStreamChange(streamInfo);

    stream.switch(streamInfo, false /* clearBuffer */);
  }

  Promise.all(async).then(
      this.onAllStreamsStarted_.bind(this)
  ).catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        // One or more Streams encountered an unrecoverable error during stream
        // startup. There's nothing else to do.
        shaka.asserts.assert(error.type != 'aborted');

        if (error.type != 'destroy') {
          shaka.log.error('Stream startup failed!');

          var event = shaka.util.FakeEvent.createErrorEvent(error);
          this.dispatchEvent(event);
        }
      })
  );

  // Enable the subtitle display by default iff the subs are needed.
  this.enableTextTrack(this.subsNeeded_);
};

/**
 * Called when each Stream has completed startup.
 *
 * Computes a global timestamp correction and immediately applies it to the
 * initial set of SegmentIndexes and then begins playback. In parallel,
 * creates/fetches all SegmentIndexes and fetches all initialization segments.
 *
 * @param {!Array.<number>} timestampCorrections The initial set of streams'
 *     timestamp corrections.
 * @private
 */
shaka.player.StreamVideoSource.prototype.onAllStreamsStarted_ = function(
    timestampCorrections) {
  shaka.log.info('All Streams have completed startup!');

  shaka.asserts.assert(
      timestampCorrections &&
      timestampCorrections.length == Object.keys(this.streamsByType_).length,
      'There should be a timestamp correction for each Stream.');

  // Compute a global timestamp correction (TSC) to correct the
  // SegmentIndexes.
  //
  // For example, consider two streams, where the manifest specified that they
  // should start at 2 seconds but the first one is offset by 2 seconds and the
  // second one is offset by 3 seconds. After we buffer 2 segments we would see
  // [1] below (if the SegmentIndexes were accurate, we would see [2] below).
  //
  // Legend:
  // a: 1st segment's content.
  // b: 2nd segment's content.
  //
  // [1]
  // <---|---|---aaaaaaaabbbbbbbb--------->
  // <---|---|-----aaaaaaaabbbbbbbb------->
  //     0   2   4 5 6   8   A   B
  //
  // [2]
  // <---|---aaaaaaaabbbbbbbb------------->
  // <---|---aaaaaaaabbbbbbbb------------->
  //     0   2   4   6   8   A   B
  //
  // These timestamp offsets exist because a manifest may not provide exact
  // timestamp signalling, especially for live streams. For example, for DASH,
  // inaccurate timestamp signalling is explicitly allowed (i.e., MPD start
  // times are approximations of media presentation start times).
  //
  // If we did not compensate for these timestamp offsets then we would
  // encounter two issues:
  // 1. the user could seek such that the playhead could end up in an
  //    unbuffered region, e.g., if the user seeks to 6.1 seconds then each
  //    Stream would fetch segment two, which would leave the playhead in an
  //    unbuffered region;
  // 2. the playhead would be behind the first buffered range before playback
  //    even begins.
  // Note that if the timestamp offsets were negative, we would encounter
  // similar issues.
  //
  // So, to compensate for these timestamp offsets we use TSCs: if a stream is
  // offset by N seconds then it requires an N second TSC.
  //
  // We compute a single global TSC, which is the maximum TSC among the initial
  // set of streams' TSCs, and then correct every SegmentIndex with the global
  // TSC. So, in the future when the Streams fetch segments that should start
  // at M seconds, the segments will either start exactly at M seconds or at
  // some small amount of time before M seconds. Furthermore, we adjust the
  // playhead before we begin playback to ensure the playhead is within a
  // buffered range (see beginPlayback_).
  //
  // We assume the TSCs between streams are similar, so this solution is not
  // perfect as it does not solve issue 1 in all cases; however, this
  // assumption is almost always valid and it frees us from having to compute a
  // TSC for every stream that we switch to, which is difficult when we have
  // already buffered one or more segments.
  var minTimestampCorrection = Number.POSITIVE_INFINITY;
  var maxTimestampCorrection = Number.NEGATIVE_INFINITY;
  for (var i = 0; i < timestampCorrections.length; ++i) {
    var tsc = timestampCorrections[i];
    minTimestampCorrection = Math.min(minTimestampCorrection, tsc);
    maxTimestampCorrection = Math.max(maxTimestampCorrection, tsc);
  }
  shaka.log.info('Timestamp correction', maxTimestampCorrection);

  // |minTimestampCorrection| and |maxTimestampCorrection| should have the
  // same sign.
  if (minTimestampCorrection * maxTimestampCorrection < 0) {
    shaka.log.warning(
        'Some streams\' media timestamps are ahead of their SegmentIndexes,',
        'while other streams\' timestamps are behind.',
        'The content may have errors in it.');
  }

  // Correct the initial set of SegmentIndexes and then begin playback.
  var segmentIndexes = this.getSegmentIndexes_();
  for (var i = 0; i < segmentIndexes.length; ++i) {
    segmentIndexes[i].correct(maxTimestampCorrection);
  }
  this.beginPlayback_(segmentIndexes, maxTimestampCorrection);

  // In parallel, create/fetch all SegmentIndexes and fetch all initialization
  // segments so that they are available to the Streams right away. This
  // reduces latency when stream switching.
  var async = this.streamSetsByType.getAll()
      .map(function(streamSetInfo) { return streamSetInfo.streamInfos; })
      .reduce(function(all, part) { return all.concat(part); }, [])
      .map(function(streamInfo) {
        var async = [streamInfo.segmentIndexSource.create()];
        if (streamInfo.segmentInitSource) {
          async.push(streamInfo.segmentInitSource.create());
        }
        return Promise.all(async);
      });
  Promise.all(async).then(shaka.util.TypedBind(this,
      /** @param {!Array.<!Array>} results */
      function(results) {
        for (var i = 0; i < results.length; ++i) {
          /** @type {!shaka.media.SegmentIndex} */
          var segmentIndex = results[i][0];
          segmentIndex.correct(maxTimestampCorrection);
        }

        shaka.log.debug(
            'Created/fetched all SegmentIndexes and initialization segments!');

        // Enable stream switching and process all deferred switches.
        this.canSwitch_ = true;
        this.processDeferredSwitches_();
      })
  ).catch(shaka.util.TypedBind(this,
      /** @param {*} error */
      function(error) {
        if (error.type != 'aborted') {
          var event = shaka.util.FakeEvent.createErrorEvent(error);
          this.dispatchEvent(event);
        }
      })
  );
};

/**
 * Processes all deferred switches.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.processDeferredSwitches_ = function() {
  for (var type in this.deferredSwitches_) {
    var tuple = this.deferredSwitches_[type];

    var stream = this.streamsByType_[type];
    shaka.asserts.assert(stream);

    shaka.asserts.assert(this.stats_);
    this.stats_.logStreamChange(tuple.streamInfo);

    stream.switch(tuple.streamInfo, tuple.clearBuffer, tuple.clearBufferOffset);
  }

  this.deferredSwitches_ = {};
};

/**
 * Corrects the MediaSource's duration and append windows, moves the playhead
 * to the start of the first buffered range, and then starts playback by
 * signalling each Stream to proceed.
 *
 * @param {!Array.<!shaka.media.SegmentIndex>} segmentIndexes The initial set
 *     of SegmentIndexes.
 * @param {number} timestampCorrection The global timestamp correction.
 * @private
 */
shaka.player.StreamVideoSource.prototype.beginPlayback_ = function(
    segmentIndexes, timestampCorrection) {
  shaka.log.debug('beginPlayback_');

  var streamLimits = this.computeStreamLimits_(segmentIndexes);
  shaka.asserts.assert(streamLimits, 'Stream limits should not be null.');
  if (streamLimits) {
    this.setUpMediaSource_(streamLimits);
    this.fireSeekRangeChangedEvent_(streamLimits.start, streamLimits.end);
  }

  // If there is a global timestamp correction then the playhead may be in an
  // unbuffered region (see onAllStreamsStarted_), so adjust the video's
  // current time as necessary. If |timestampCorrection| is 0 then don't modify
  // video.currentTime so we don't fire an unnecessary 'seeking' event.
  var correctedCurrentTime;
  if (timestampCorrection != 0) {
    shaka.log.debug(
        'Adjusting video.currentTime by', timestampCorrection, 'seconds.');
    correctedCurrentTime = this.video.currentTime + timestampCorrection;
    this.video.currentTime = correctedCurrentTime;

    // Ignore the resulting 'seeking' event since there's no need to resync the
    // streams (see onSeeking_).
    this.ignoreSeek_ = correctedCurrentTime;
    shaka.log.debug('Ignoring pending seek to', this.ignoreSeek_);
  } else {
    correctedCurrentTime = this.video.currentTime;
  }
  shaka.asserts.assert(correctedCurrentTime != null);

  // Sanity check: check that |correctedCurrentTime| is within the stream
  // limits.
  if (!COMPILED && streamLimits) {
    // For live content, if the available bandwidth is really low (e.g., lower
    // than the bandwidth specified in the manifest) then it's possible for
    // |correctedCurrentTime| to be legitimately outside of the stream limits,
    // i.e., the seek window may move past the corrected playhead.
    //
    // Note: since video.currentTime may have less precision than
    // |timestampCorrection|, include a tolerance.
    var tolerance = 10e-6;

    if (this.manifestInfo.live &&
        correctedCurrentTime < streamLimits.start - tolerance) {
      shaka.log.debug(
          'correctedCurrentTime (' + correctedCurrentTime + ')',
          'is outside of the stream limits before beginning playback!');
    }

    if ((!this.manifestInfo.live &&
         correctedCurrentTime < streamLimits.start - tolerance) ||
        (correctedCurrentTime > streamLimits.end + tolerance)) {
      shaka.log.error(
          'correctedCurrentTime (' + correctedCurrentTime + ')',
          'should be within the stream limits',
          [streamLimits.start, streamLimits.end]);
    }
  }

  if (this.manifestInfo.live && streamLimits) {
    // While the streams are starting the live-edge is moving. So, the video's
    // current time may be behind the live-edge by a few seconds (note that
    // |streamLimits| has already been corrected). This may cause a poor UX
    // since the UI might say "-00:03" instead of "LIVE". So, record an offset
    // to apply to the stream limits in the future.
    this.liveEdgeOffset_ = streamLimits.end - correctedCurrentTime;

    // Sanity check: check that |liveEdgeOffset_| is non-negative.
    if (!COMPILED) {
      // Note: since video.currentTime may have less precision than
      // |timestampCorrection|, include a tolerance.
      var tolerance = 10e-6;
      shaka.asserts.assert(this.liveEdgeOffset_ >= -tolerance,
                           'liveEdgeOffset_ should not be less than zero.');
    }

    this.liveEdgeOffset_ = Math.max(this.liveEdgeOffset_, 0);
    shaka.log.debug('Live-edge offset', this.liveEdgeOffset_);
  }

  // TODO: If the playback rate is set by the application between the set and
  // load of originalPlaybackRate_, that rate will be ignored.  Fix this race
  // between StreamVideoSource and the application.  In the mean time,
  // applications should use setPlaybackRate either before loading the source
  // or after playback begins.
  this.video.playbackRate = this.originalPlaybackRate_;

  if (this.manifestInfo.live && this.manifestInfo.updatePeriod != null) {
    // Ensure the next update occurs within |manifestInfo.updatePeriod| seconds
    // by taking into account the time it took to start the streams.
    shaka.asserts.assert(this.updateTimer_ == null);
    this.setUpdateTimer_(this.liveEdgeOffset_);
  }

  this.setSeekRangeTimer_();
  this.proceedPromise_.resolve();
};

/**
 * Sets the MediaSource's duration and the SourceBuffers' append windows.
 * Before calling this function, the MediaSource must be in the 'open' state.
 * This function may be called any number of times.
 *
 * @param {{start: number, end: number, last: number}} streamLimits The current
 *     stream limits.
 * @private
 */
shaka.player.StreamVideoSource.prototype.setUpMediaSource_ = function(
    streamLimits) {
  shaka.asserts.assert(this.mediaSource.readyState == 'open',
                       'The MediaSource should be in the \'open\' state.');

  // We need to set the MediaSource's duration so that we can append segments
  // and allow the user to seek.
  if (this.manifestInfo.live) {
    // For live content we usually don't know the content's duration, so we
    // don't need to set the duration to a precise value. We should be able to
    // set the MediaSource's duration to POSITIVE_INFINITY but on some browsers
    // this does not work as intended. So, just set the MediaSource's duration
    // to a "large" value.
    if (isNaN(this.mediaSource.duration)) {
      this.mediaSource.duration = streamLimits.end + (60 * 60 * 24 * 30);
    }
  } else {
    // For static content we know the content's duration, and we must set the
    // duration to a precise value so we don't modify the video's duration
    // during playback (e.g., by inserting a segment into the SourceBuffer that
    // ends after the MediaSource's duration) and so we can support normal
    // end-of-stream behavior (i.e., the browser should pause the video when
    // the playhead reaches the end of the video, and the browser should be
    // able to loop the video).
    //
    // So, set the MediaSource's duration to the stream end time. However, note
    // that some streams (either from the initial set of streams or from the
    // set of all streams) may start before the stream start time or may end
    // before the stream end time because the streams' media timelines may not
    // be aligned with eachother (this is normal, and is true both before and
    // after we apply the global timestamp correction). If we insert a segment
    // into the SourceBuffer that ends after the MediaSource's duration then
    // the MediaSource's duration will increase, and if the MediaSource's
    // duration increases beyond the seekable range (by some non-negligible
    // amount) then the playhead will be able to move into an unseekable range
    // during playback, which will interfere with normal end-of-stream behavior
    // and buffering detection (see #155).
    //
    // So, set the SourceBuffers' append windows so that the MediaSource's
    // duration cannot increase past the stream end time.
    //
    // TODO: If the new duration is less than the old duration then changing
    // the duration starts the 'duration change algorithm'.
    // See {@link http://www.w3.org/TR/media-source/#duration-change-algorithm}.
    // Handle this case.
    if (isNaN(this.mediaSource.duration) ||
        streamLimits.end > this.mediaSource.duration) {
      shaka.log.debug('Setting MediaSource duration to', streamLimits.end);
      this.mediaSource.duration = streamLimits.end;
      for (var i = 0; i < this.mediaSource.sourceBuffers.length; ++i) {
        this.mediaSource.sourceBuffers[i].appendWindowEnd = streamLimits.end;
      }
    }
  }
};

/**
 * Computes a new seek range and fires a 'seekrangechanged' event. Also clamps
 * the playhead to the seek start time during playback.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.onUpdateSeekRange_ = function() {
  this.seekRangeTimer_ = null;
  this.setSeekRangeTimer_();

  var streamLimits = this.computeStreamLimits_(this.getSegmentIndexes_());
  shaka.asserts.assert(streamLimits, 'Stream limits should not be null.');
  if (!streamLimits) {
    return;
  }

  if (this.manifestInfo.live && this.liveEndTime_ != streamLimits.last) {
    this.liveEndTime_ = streamLimits.last;
    if (this.liveEndedTimer_ != null) {
      shaka.log.debug('Not the end of the live stream.');
      window.clearTimeout(this.liveEndedTimer_);
      this.liveEndedTimer_ = null;
    }
  }

  this.fireSeekRangeChangedEvent_(streamLimits.start, streamLimits.end);

  if (this.video.paused) {
    return;
  }

  // Clamping the playhead to the right here ensures that if the user pauses
  // and then plays the video then the playhead is moved into the seekable
  // range. Note that if the playhead moves to the right of the seekable range
  // during playback then some of the streams must be buffering, so there's no
  // need to clamp the playhead.
  // TODO: Add live integration test that covers this case.
  var currentTime = this.video.currentTime;
  var start = streamLimits.start;
  var end = streamLimits.end;
  if (this.clampPlayheadToRight_(currentTime, start, end)) {
    shaka.log.warning(
        'Playhead is outside of the seekable range:',
        'seekable', [start, end],
        'attempted', currentTime,
        'Adjusting...');
    // If the video's current time was clamped then there will be a
    // 'seeking' event which is handled by onSeeking_().
  }
};

/**
 * Fires a 'seekrangechanged' event.
 *
 * @param {number} start
 * @param {number} end
 * @private
 */
shaka.player.StreamVideoSource.prototype.fireSeekRangeChangedEvent_ = function(
    start, end) {
  var event = shaka.util.FakeEvent.create({
    'type': 'seekrangechanged',
    'bubbles': true,
    'start': start,
    'end': end
  });

  shaka.log.v1('Seek range', [start, end], end - start);
  this.dispatchEvent(event);
};

/**
 * Video seeking callback.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.StreamVideoSource.prototype.onSeeking_ = function(event) {
  shaka.log.v1('onSeeking_', event);

  var currentTime = this.video.currentTime;

  if (this.ignoreSeek_ != null) {
    var tolerance = shaka.player.StreamVideoSource.SEEK_TOLERANCE_;
    if ((currentTime >= this.ignoreSeek_ - tolerance) &&
        (currentTime <= this.ignoreSeek_ + tolerance)) {
      shaka.log.debug('Ignored seek to', this.ignoreSeek_);
      this.ignoreSeek_ = null;
      return;
    }
    this.ignoreSeek_ = null;
  }

  var streamLimits = this.computeStreamLimits_(this.getSegmentIndexes_());
  shaka.asserts.assert(streamLimits, 'Stream limits should not be null.');
  if (!streamLimits) {
    return;
  }

  var start = streamLimits.start;
  var end = streamLimits.end;
  if (this.clampPlayheadToRight_(currentTime, start, end) ||
      this.clampPlayheadToLeft_(currentTime, end)) {
    shaka.log.warning(
        'Playhead has been moved outside of the seekable range:',
        'seekable', [start, end],
        'attempted', currentTime,
        'Adjusting...');
    // If the video's current time was clamped then there will be another
    // 'seeking' event, so this function will get called again and the 'else'
    // branch below will be executed.
  } else {
    for (var type in this.streamsByType_) {
      this.streamsByType_[type].resync();
    }
  }
};

/**
 * Clamps the video's current time to the right of the given start time
 * (inclusive).
 *
 * @param {number} currentTime The video's current time.
 * @param {number} start The start time in seconds.
 * @param {number} end The end time in seconds, which must be provided to
 *     ensure that the playhead is not adjusted too far right.
 * @return {boolean} True if the video's current was clamped, in which case a
 *     'seeking' event will be fired by the video.
 * @private
 */
shaka.player.StreamVideoSource.prototype.clampPlayheadToRight_ = function(
    currentTime, start, end) {
  if (currentTime >= start - shaka.player.StreamVideoSource.SEEK_TOLERANCE_) {
    return false;
  }

  // For live content, if we re-position the playhead too close to the seek
  // start time then we may end up outside of the seek range again, as the seek
  // window may be moving or we may have to buffer after we re-position. So,
  // re-position the playhead ahead of the seek start time to compensate.
  var compensation = 0;
  if (this.manifestInfo.live) {
    compensation = shaka.player.StreamVideoSource.SEEK_OFFSET_;

    // Search all of the streams to see if they have buffered the start time.
    // If one of them hasn't, then add some compensation.
    for (var type in this.streamsByType_) {
      if (!this.streamsByType_[type].isBuffered(start + compensation)) {
        compensation = this.manifestInfo.minBufferTime;
        break;
      }
    }
  }

  this.video.currentTime = Math.min(start + compensation, end);
  return true;
};

/**
 * Clamps the video's current time to the left of the given end time
 * (inclusive).
 *
 * @param {number} currentTime The video's current time.
 * @param {number} end The end time in seconds.
 * @return {boolean} True if the video's current was clamped, in which case a
 *     'seeking' event will be fired by the video.
 * @private
 */
shaka.player.StreamVideoSource.prototype.clampPlayheadToLeft_ = function(
    currentTime, end) {
  if (currentTime <= end + shaka.player.StreamVideoSource.SEEK_TOLERANCE_) {
    return false;
  }

  this.video.currentTime = end;
  return true;
};

/**
 * Stream ended callback.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.StreamVideoSource.prototype.onStreamEnded_ = function(event) {
  if (this.manifestInfo.live) {
    return;
  }

  for (var type in this.streamsByType_) {
    if (!this.streamsByType_[type].hasEnded()) {
      // Not all streams have ended.
      return;
    }
  }

  this.endOfStream_();
};

/**
 * The buffering start event callback.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.StreamVideoSource.prototype.onBufferingStart_ = function(event) {
  shaka.asserts.assert(this.manifestInfo.live);

  var ended = shaka.util.MapUtils.values(this.streamsByType_)
      .every(function(stream) { return stream.hasEnded(); });
  if (ended) {
    // We are in a buffering state and all the streams have ended.  We assume
    // that either the manifest does not update or it has updated recently
    // enough to give the newest segments; therefore, we are probably at the
    // end of the live stream.
    shaka.log.info('Possible end of live stream.');
    this.liveEndedTimer_ =
        window.setTimeout(this.endOfStream_.bind(this),
            1000 * this.liveStreamEndTimeout_);
  }
};

/**
 * The buffering end event callback.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.StreamVideoSource.prototype.onBufferingEnd_ = function(event) {
  shaka.asserts.assert(this.manifestInfo.live);

  if (this.liveEndedTimer_ != null) {
    shaka.log.debug('Not the end of the live stream.');
    window.clearTimeout(this.liveEndedTimer_);
    this.liveEndedTimer_ = null;
  }
};

/**
 * Bandwidth statistics update callback.
 *
 * @param {!Event} event
 * @private
 */
shaka.player.StreamVideoSource.prototype.onBandwidth_ = function(event) {
  shaka.asserts.assert(this.stats_);
  this.stats_.logBandwidth(this.estimator.getBandwidth());
};

/**
 * Signals the end of the video.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.endOfStream_ = function() {
  // |mediaSource| must be in the 'open' state before calling endOfStream().
  shaka.asserts.assert(this.mediaSource.readyState == 'open',
                       'The MediaSource should be in the \'open\' state.');

  // Sanity check: endOfStream() sets the MediaSource's duration to the end of
  // the video's buffered range. However, it should not actually modify the
  // MediaSource's duration (significantly) because the streams' last segments
  // should end very close to the MediaSource's duration.
  // See {@link http://www.w3.org/TR/media-source/#end-of-stream-algorithm}.
  //
  // Note that if the current streams are the initial streams then one of the
  // stream's last segment should end exactly at the MediaSource's duration.
  // However, if this is not the case then either one of the stream's last
  // segment ends after the MediaSource's duration, in which case the append
  // window will cut it off, so the MediaSource's duration won't be modified;
  // or both of the streams' last segments end before the MediaSource's
  // duration, which for well formed content should still not modify the
  // MediaSource's duration.
  if (!COMPILED) {
    var durationBefore = this.mediaSource.duration;
    var durationAfter;
  }

  shaka.log.info('Signalling end-of-stream.');
  this.liveEndedTimer_ = null;
  this.mediaSource.endOfStream();

  if (!COMPILED) {
    var durationAfter = this.mediaSource.duration;
    if (durationAfter != durationBefore) {
      shaka.log.warning(
          'endOfStream() should not modify the MediaSource\'s duration:',
          'before', durationBefore,
          'after', durationAfter,
          'delta', durationAfter - durationBefore);
    }

    var updating = false;
    for (var i = 0; i < this.mediaSource.sourceBuffers.length; ++i) {
      updating = this.mediaSource.sourceBuffers[i].updating;
    }
    shaka.asserts.assert(
        !updating,
        'endOfStream() should not trigger the duration change algorithm.');
  }
};

/**
 * Gets the active Streams' SegmentIndexes.
 *
 * @return {!Array.<!shaka.media.SegmentIndex>}
 * @private
 */
shaka.player.StreamVideoSource.prototype.getSegmentIndexes_ = function() {
  return shaka.util.MapUtils.values(this.streamsByType_)
      .map(function(stream) { return stream.getSegmentIndex(); })
      .filter(function(index) { return index != null; });
};

/**
 * Computes the stream limits, i.e., a stream start time and stream end time,
 * of the given SegmentIndexes. The stream limits define the video's seekable
 * range, so the video's current time should always be within the stream
 * limits.
 *
 * The stream limits are a subset of each individual SegmentIndex's seek range;
 * however, the converse is not true.
 *
 * @param {!Array.<!shaka.media.SegmentIndex>} segmentIndexes
 * @return {?{start: number, end: number, last: number}} The stream limits on
 *     success; otherwise, return null if a stream end time could not be
 *     computed or the streams' media timelines do not intersect.
 * @private
 */
shaka.player.StreamVideoSource.prototype.computeStreamLimits_ = function(
    segmentIndexes) {
  shaka.asserts.assert(this.manifestInfo);

  var startTime = 0;
  var endTime = Number.POSITIVE_INFINITY;
  var lastTime = Number.POSITIVE_INFINITY;

  for (var i = 0; i < segmentIndexes.length; ++i) {
    var seekRange = segmentIndexes[i].getSeekRange();
    startTime = Math.max(startTime, seekRange.start);
    if (seekRange.end != null) {
      endTime = Math.min(endTime, seekRange.end);
    }
    if (segmentIndexes[i].length()) {
      lastTime = Math.min(lastTime, segmentIndexes[i].last().endTime);
    }
  }

  // Fallback to the period's duration if necessary.
  if (endTime == Number.POSITIVE_INFINITY) {
    // TODO(story 1890046): Support multiple periods.
    var period = this.manifestInfo.periodInfos[0];
    if (period.duration) {
      endTime = (period.start || 0) + period.duration;
    } else {
      shaka.log.debug('Failed to compute a stream end time.');
      return null;
    }
  }

  if (this.manifestInfo.live) {
    // Ensure that we can actually buffer the minimum buffer size by offsetting
    // the stream end time.
    var offset = this.manifestInfo.minBufferTime + this.liveEdgeOffset_;
    endTime = Math.max(endTime - offset, startTime);
  }

  if (startTime > endTime) {
    shaka.log.debug('The streams\' media timelines do not intersect.');
    return null;
  }

  return { start: startTime, end: endTime, last: lastTime };
};

/**
 * Cancels the update timer, if any.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.cancelUpdateTimer_ = function() {
  if (this.updateTimer_) {
    window.clearTimeout(this.updateTimer_);
    this.updateTimer_ = null;
  }
};

/**
 * Sets the seek range timer.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.setSeekRangeTimer_ = function() {
  if (!this.manifestInfo.live) {
    return;
  }
  shaka.asserts.assert(this.seekRangeTimer_ == null);

  var callback = this.onUpdateSeekRange_.bind(this);
  this.seekRangeTimer_ = window.setTimeout(callback, 1000);
};

/**
 * Cancels the seek range timer, if any.
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.cancelSeekRangeTimer_ = function() {
  if (this.seekRangeTimer_) {
    window.clearTimeout(this.seekRangeTimer_);
    this.seekRangeTimer_ = null;
  }
};

/**
 * Configures each Stream with |streamConfig_|
 *
 * @private
 */
shaka.player.StreamVideoSource.prototype.configureStreams_ = function() {
  for (var type in this.streamsByType_) {
    var stream = this.streamsByType_[type];
    stream.configure(this.streamConfig_);
  }
};


text:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>Examples — ARTview 1.3.5.dev+dba3dca documentation</title>

    <link rel="stylesheet" href="../_static/scipy.css" type="text/css" />
    <link rel="stylesheet" href="../_static/pygments.css" type="text/css" />

    <link rel="index" title="Index" href="../genindex.html" />
    <link rel="search" title="Search" href="../search.html" />
    <link rel="top" title="ARTview 1.3.5.dev+dba3dca documentation" href="../index.html" />

  <link rel="stylesheet" href="../_static/custom.css" type="text/css" />

  <meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />

Examples¶

Related Topics

Documentation overview

This Page

Show Source

Quick search

    <form class="search" action="../search.html" method="get">

<input type="text" name="q" />

<input type="submit" value="Go" />

      <input type="hidden" name="check_keywords" value="yes" />
      <input type="hidden" name="area" value="default" />
    </form>

      ©2015, Nick Guy.

      |
      Powered by Sphinx 1.4.9
      & Alabaster 0.7.8

      |
      Page source


text:
package org.dartlang.analysis.server.protocol;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import com.google.dart.server.utilities.general.JsonUtilities;
import com.google.dart.server.utilities.general.ObjectUtilities;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import java.util.ArrayList;
import java.util.Iterator;
import org.apache.commons.lang3.StringUtils;

/**
 * A description of a class member that is implemented or overridden.
 *
 * @coverage dart.server.generated.types
 */
@SuppressWarnings("unused")
public class ImplementedMember {

  public static final ImplementedMember[] EMPTY_ARRAY = new ImplementedMember[0];

  public static final List<ImplementedMember> EMPTY_LIST = Lists.newArrayList();

  /**
   * The offset of the name of the implemented member.
   */
  private final int offset;

  /**
   * The length of the name of the implemented member.
   */
  private final int length;

  /**
   * Constructor for {@link ImplementedMember}.
   */
  public ImplementedMember(int offset, int length) {
    this.offset = offset;
    this.length = length;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof ImplementedMember) {
      ImplementedMember other = (ImplementedMember) obj;
      return
        other.offset == offset &&
        other.length == length;
    }
    return false;
  }

  public static ImplementedMember fromJson(JsonObject jsonObject) {
    int offset = jsonObject.get("offset").getAsInt();
    int length = jsonObject.get("length").getAsInt();
    return new ImplementedMember(offset, length);
  }

  public static List<ImplementedMember> fromJsonArray(JsonArray jsonArray) {
    if (jsonArray == null) {
      return EMPTY_LIST;
    }
    ArrayList<ImplementedMember> list = new ArrayList<ImplementedMember>(jsonArray.size());
    Iterator<JsonElement> iterator = jsonArray.iterator();
    while (iterator.hasNext()) {
      list.add(fromJson(iterator.next().getAsJsonObject()));
    }
    return list;
  }

  /**
   * The length of the name of the implemented member.
   */
  public int getLength() {
    return length;
  }

  /**
   * The offset of the name of the implemented member.
   */
  public int getOffset() {
    return offset;
  }

  @Override
  public int hashCode() {
    HashCodeBuilder builder = new HashCodeBuilder();
    builder.append(offset);
    builder.append(length);
    return builder.toHashCode();
  }

  public JsonObject toJson() {
    JsonObject jsonObject = new JsonObject();
    jsonObject.addProperty("offset", offset);
    jsonObject.addProperty("length", length);
    return jsonObject;
  }

  @Override
  public String toString() {
    StringBuilder builder = new StringBuilder();
    builder.append("[");
    builder.append("offset=");
    builder.append(offset + ", ");
    builder.append("length=");
    builder.append(length);
    builder.append("]");
    return builder.toString();
  }

}


text:
class BalancingActivities < ActiveRecord::Migration
  def change
    create_table :balancing_activities do |t|
      t.integer :chakra_id
      t.string :activity
      t.string :description
      t.timestamps
    end
  end
end


text:
package com.github.kubatatami.richedittext.writer;

import android.widget.EditText;

public interface Writer {

    void write(EditText editText, String text);

    void delete(EditText editText);

}


text:
require "formula"

class Boxtape < Formula
  homepage "http://boxtape.io"
  url "http://localhost:8000/boxtape-0.4.zip"

  def install
      inreplace "brew/boxtape", "##PREFIX##", "#{prefix}"
      prefix.install "lib"
      bin.install "bin/boxtape"
  end
end