Greasy Fork is available in English.

kusa5

ニコ動html5表示

このスクリプトは削除されました。

質問やレビューの投稿はこちらへ、スクリプトの通報はこちらへどうぞ。
// ==UserScript==
// @name        kusa5
// @namespace   net.buhoho.kusa5
// @include     http://www.nicovideo.jp/watch/*
// @version     1
// @license     MIT License
// @icon        data:image/png;base64,AAABAAEAEBAQAAEABAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFjQMAEk9DgA0NDQAFlwAAIRtEAAqhQ4A8OrSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAADAAMzMzMzMzMwA3d3d3d3dzADd3dyJ3d3MAN1d3d3d1cwA3B3d3d3BzADd3d3d3d3MAN3d3d3d3cwA3d3d3d3dzADd3d3d3d3MAMzMzMzMzMwAAABAAEAAAAAAEBAEEAAAAAGAAYABAAAAAAAAAAAAAD//wAA3/sAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAAD7vwAA9V8AAO7vAAD//wAA
// @grant       none
// @description ニコ動html5表示
// ==/UserScript==

/*
メモ
localStorage.kusa5buffer          : バッファリング有効
localStorage.kusa5disableAutoplay : 自動再生無効
localStorage.kusa5disableCmd      : コメントの色、改行などを無効
localStorage.kusa5disableNgfilter : NGフィルタの無効
開発ツールのコンソールにてlocalStorage.kusa5buffer = true;
みたいに打ち込んで設定する。
*/


'use strict';

$('.playerContainer').hide();
$('#playlist').hide(); //お好み
$('#playerContainerSlideArea').attr('id', 'kusa5');
$('#playerContainerWrapper').insertBefore('.videoHeaderOuter'); // お好み
$(".notify_update_flash_player").hide(); //Flash未インストール警告

const OPT = {
	buffer: localStorage.kusa5buffer, // たぶんfirefoxだけで動く
};

const FLAPI = 'http://flapi.nicovideo.jp/api';
const WATCH = 'http://www.nicovideo.jp/watch/';
const ICON = "https://cdnjs.cloudflare.com/ajax/libs/foundicons/3.0.0/svgs/";
const ICON2 = "https://cdnjs.cloudflare.com/ajax/libs/topcoat-icons/0.1.0/svg/";
const LAUNCH_ID = JSON.parse($('#watchAPIDataContainer').text()).videoDetail.v;
const IS_IFRAME = window != parent;
const GET_QUERY = {
	type:'GET',
	contentType: "text/plain",
	dataType: 'text',
	crossDomain: true,
	cache: false,
	xhrFields: {'withCredentials': true} // Cookie認証が必要
};

addGlobalStyle(`
#kusa5 {
	position: relative;
	background-color: #000;
	width: 640px;
	height: 360px;
	overflow: hidden;
	margin: 0 auto;
}
#kusa5 video {
	display: block;
	background-color: #000;
	height: 100%;
	max-width: 100%; /* 画面外にはみ出ないように */
	margin: 0 auto;
}
#wallImageContainer .wallImageCenteringArea .wallAlignmentArea.image2{
	z-index: 3;
	background-color: #CCCEC3;
}
#playerContainerWrapper {
	padding: 60px 0;
}

/*
 コントロールパネル関係
 ******************************************************************************/
.controle-panel {
	z-index: 10; /* コメントより手前 */
	color: rgba(255, 255, 255, 0.87);
	position:absolute;
	bottom: 0;
	width: 100%;
	background: #0C2529;
	transition: transform .2s;
	transform: translate3d(0, 47px, 0);
	overflow: hidden;
	cursor: default;
	height: 50px;
}
#kusa5:hover .controle-panel {
	transform: translate3d(0, 0, 0);
}
.controle-panel .btn,
input+label {
	color: rgba(255, 255, 255, 0.87);
	font-size: 18px;
	border: none;
	background-color: transparent;
}
.controle-panel .r {float: right;}

.controle-panel .progressBar {
	cursor: pointer;
	position: relative;
	height: 14px;
	background-color: rgba(255, 255, 255, 0.24);
	width: 100%;
}
.controle-panel .progressBar span {
	position: absolute;;
	top: 0;
	left: 0;
	width: 0;
	height: 100%;
}
.controle-panel .progressBar.seek .mainbar {
	background-color: #8CD6E7;
}
.controle-panel .progressBar.seek .bufferbar {
	background-color: rgba(168, 191, 210, 0.37);
}
.controle-panel .progressBar.buf       { height: 2px;}
.controle-panel .progressBar.buf .bar  { background-color: #ff00c0; }
button.btn.ico {
	color: #000;
	position: relative;
	background-repeat: no-repeat;
	background-size: contain;
	filter: invert(1) opacity(0.8);
	-webkit-filter: invert(1) opacity(0.8);
	width: 25px;
	height: 25px;
	background-size: 18px;
	background-position: 5px;
	margin: 4px 5px;
	cursor: pointer;
}
button.btn.ico:hover {
	filter: invert(1) opacity(1);
	-webkit-filter: invert(1) opacity(1);
}
button.btn.ico.play {
	background-image: url("${ICON}fi-play.svg");
}
#kusa5.playing button.btn.ico.play {
	/* 再生中 */
	background-image: url("${ICON}fi-pause.svg");
}
button.btn.ico:active {
	transform: scale(0.9);
}

#volume-slider {
	height: 35px;
	width: 60px;
	cursor: pointer;
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
#volume-slider:focus {
	outline: none;
}
#volume-slider::-moz-focus-outer {
	border: 0;
}
#volume-slider::-moz-range-track {
	background-color: rgba(255, 255, 255, 0.24);
}
button.btn.ico.speaker {
	background-image: url("${ICON}fi-volume.svg");
}
button.btn.ico.speaker.muted {
	background-image: url("${ICON}fi-volume-strike.svg");
}
button.btn.ico.full {
	background-image: url("${ICON}fi-arrows-expand.svg");
}

#kusa5:-moz-full-screen button.btn.ico.full {
	background-image: url("${ICON}fi-arrows-compress.svg");
}
#kusa5:-webkit-full-screen button.btn.ico.full {
	background-image: url("${ICON}fi-arrows-compress.svg");
}
.controle-panel .playtime {
	line-height: 32px;
}
.controle-panel .playtime .duration {
	opacity: 0.8;
}
button.btn.ico.comment-hidden {
	background-image: url(${ICON2}chat.svg);
}
/* 非表示状態 */
#kusa5.comment-hidden .msg { opacity: 0;}
button.comment-hidden {
	opacity: 1;
}
#kusa5.comment-hidden button.comment-hidden {
	opacity: 0.5;
}



input.btn {
	display: none;
}
input.btn+label {
	color: #999;
	display: inline-block;
	background-color: hsla(0. 0%, 0%, 0.3);
	text-align: center;
	font-size: 14px;
}
input.btn+label:hover,
input.btn:checked+label {
	color: #fff;
	text-decoration-line: underline;
}
input.btn+label span{
	font-size:0.5em;
}
div.ratepanel {
	display: inline-block;
	text-align: center;
}


/*
 コメント要素関連
 ******************************************************************************/
#kusa5 .msg {
	z-index: 5;
	display: inline-block;
	word-break: keep-all;
	font-size: 1.8em;
	color: white;
	padding: 0 .5em;
	position: absolute;
	transition-duration: 6s;
	transition-timing-function: linear;
	transition-property: transform;
	transform: translate3d(105% ,0,0); /* 画面外に配置するので */
	text-shadow:
		 0px  0.05em  1px #222,
		 0.05em  0px  1px #222,
		 0px -0.05em  1px #222,
		-0.05em  0px  1px #444;
	top: 0;
}
#kusa5 .msg.l1 { top: calc(1.4em * 0);}
#kusa5 .msg.l2 { top: calc(1.4em * 1);}
#kusa5 .msg.l3 { top: calc(1.4em * 2);}
#kusa5 .msg.l4 { top: calc(1.4em * 3);}
#kusa5 .msg.l5 { top: calc(1.4em * 4);}
#kusa5 .msg.l6 { top: calc(1.4em * 5);}
#kusa5 .msg.l7 { top: calc(1.4em * 6);}
#kusa5 .msg.l8 { top: calc(1.4em * 7);}
#kusa5 .msg.l9 { top: calc(1.4em * 8);}
#kusa5 .msg.l10 { top: calc(1.4em * 9);}
#kusa5 .msg.l11 { top: calc(1.4em * 10);}
#kusa5 .msg.l12 { top: calc(1.4em * 11);}
#kusa5 .msg.l13 { top: calc(1.4em * 12);}

/*
 フルスクリーン関連
 ******************************************************************************/
/* 何故か一つづつfont-size指定しないと効かない */
#kusa5:-moz-full-screen .msg {font-size: 3.5em; }
#kusa5:-webkit-full-screen .msg {font-size: 3.5em; } 
#kusa5:-webkit-full-screen {
	width: 100%;
	height: 100%;
}
/*
 左上に縮小表示中
 ******************************************************************************/
body.size_small.no_setting_panel.videoExplorer #kusa5 {
	height: 100%;
	width: 100%;
	margin: 0;
}
body.size_small.no_setting_panel.videoExplorer #kusa5 .msg{
	font-size: 12px;
}
@media (max-width: 1399px) and (min-width: 1140px) {
	#kusa5 { width: 854px; height: 480px; }
	#kusa5 .msg{ font-size: 2.0em; }
}
/** 大画面向け */
@media (min-width: 1400px) {
	#kusa5 { width: 1280px; height: 720px; }
	#kusa5 .msg{ font-size: 2.8em; }
}
`);

const colortable = {
	white:   '#FFFFFF',
	red:     '#FF0000',
	pink:    '#FF8080',
	orange:  '#FFC000',
	yellow:  '#FFFF00',
	green:   '#00FF00',
	cyan:    '#00FFFF',
	blue:    '#0000FF',
	purple:  '#C000FF',
	black:   '#000000',
	white2:  '#CCCC99',
	niconicowhite: '#CCCC99',
	red2:    '#CC0033',
	truered: '#CC0033',
	pink2:   '#FF33CC',
	orange2: '#FF6600',
	passionorange: '#FF6600',
	yellow2: '#999900',
	madyellow: '#999900',
	green2:  '#00CC66',
	elementalgreen: '#00CC66',
	cyan2:   '#00CCCC',
	blue2:   '#3399FF',
	marineblue: '#3399FF',
	purple2: '#6633CC',
	nobleviolet: '#6633CC',
	black2:  '#666666',
};

const $video = $(`<video type="video/mp4"
			codecs="avc1.42E01E, mp4a.40.2"
			autoplay />`)
	.on('ended', buffShift)
	.on('pause', ev => {
		$("#kusa5").removeClass("playing");
		localStorage.nicoRate = ev.target.playbackRate;
	})
	.on('play',  ev => {
		const v = ev.target;
		$("#kusa5").addClass("playing");

		// レート情報の記憶
		$('input[value="'+ localStorage.nicoRate +'"]').click();
		v.playbackRate = localStorage.nicoRate;

		// 音量再現
		v.muted  = localStorage.nicoMuted === 'true';
		$('#volume-slider').val(v.muted? 0: localStorage.nicoVolume);
		$('#kusa5 button.speaker').toggleClass('muted', v.muted);

		if (!IS_IFRAME)
			return;
		// バッファー再生用のプレーヤーは処理を重くしないためにrata1
		v.playbackRate = 1;
		v.muted = true;
		$(v).off();
	});

if (localStorage.kusa5disableAutoplay === "true")
	$video.removeAttr('autoplay');

$video.videoToggle = () => {
	var v = $video[0];
	v.paused ? v.play() : v.pause();
};

$video.click($video.videoToggle);

function addGlobalStyle(css) {
	var styleSeet = $('<style type="text/css">');
	styleSeet.text(css);
	$('head').append(styleSeet);
}

/* 現在のページのDOMから次動画のIDを取得する。
 * なので次の次の動画のIDを取るなら事前にヘッダーを書き換えておく必要がある。
 */
function getNextId(currentID) {
	const id = /\W?(s[mo]\d{3,10})\W?/.source; // 現状見受けられる動画は8桁
	const next = "(?:" + ["次","next","つづ","続","最","終"]
			.map(s => s + ".{0,4}")
			.join("|") + ")";

	// スペースに使われそうな文字(出現しないかもしれない)
	const s = /[\s_|::]?/.source; 

	const arrows = [
		" - ",
		"←",
		"→","⇒",
		":", ":",
		"<","<<","<<","≪","«",
		">",">>",">>","≫","»",
		"[<<≪«][-ー==]", // 二文字組み合わせやじるし
		"[-ー==][>>≫»]"];
	const _A_ = s + "(?:" + arrows.join("|") + ")" + s;
	const next_id = next + _A_ + id  ;
	const id_next = id   + _A_ + next;
	const description = $('.description').text();
	var m = _.reduce([next_id, id_next], (c,re) => {
		return c || description.match(new RegExp(re, 'i'));
	},false);

	if (m && m[1])
		return m[1];

	// 投稿者コメントから切り出し失敗した場合は
	// この動画IDに一番近くて若いIDを切り出す(次動画とは限らないけど)
	const small_id = _.chain($('.description a.watch'))
			.map(e => e.href.match(/s[mo](\d{3,10})/))
			.filter(e => e[1] > currentID.substring(2))
			.min([1])
			.value();

	return small_id && small_id[0]? small_id[0]: false;
}

/**
 * ページの遷移処理。実際にはコンテンツを入れ替えるだけで
 * フロントのページは遷移させない
 */
function buffShift() {
	$('.progressBar.buf .bar').css('width', '0');
	const $nextPage = $('#buf-video').contents().find('body');

	const $buf = $nextPage.find('#kusa5 video');
	if (!OPT.buffer || $buf.size() == 0) {
		FullScreen.cancel();
		return;
	}

	// 上部のコメントとかタイトル書き換え
	$('.videoHeaderTitle').text($nextPage.find('.videoHeaderTitle').text());
	$('#topVideoInfo').remove();
	$('#videoDetailInformation').append($nextPage.find('#topVideoInfo'));
	$video.attr('src',  $buf.attr('src'));

	const shiftId = JSON.parse($nextPage.find('#watchAPIDataContainer')
			.text()).videoDetail.v;
	loadApiInfo(shiftId)
		.then(createMsgRequest)
		.then(loadMsg); // メッセージ取得 && 整形登録
	history.pushState(null,null, WATCH + shiftId); // url書き換え

	const nextid = getNextId(shiftId);

	if (nextid) {
		setTimeout(()=>createBuf(nextid), 10000);
	} else {
		$('#buf-video').remove();
	}
}

function createBuf(id) {
	$('#buf-video').remove();
console.log('next-id', id);
	if (!id)
		return;
	$('#kusa5').append(`<iframe id="buf-video" src="${WATCH + id}"
					width="10px" height="10px" />`);
	// 次ページの動画読み込み進捗を取得
	setTimeout(() => {
		const v = $('#buf-video').contents().find('#kusa5 video')[0];
		const p = $('.progressBar.buf .bar');
		$(v).off('timeupdate').on('timeupdate', _.throttle(ev => {
			var w = 100 * v.currentTime / v.duration;
			p.css('width', w+'%');
		}, 10000));
	}, 20000);
}

function ngfilter(ch) {
	if (localStorage.kusa5disableNgfilter === 'true')
		return true;

	// 1秒以内。いわゆる0秒コメ
	if (ch.t < 100)
		return false;

	//参考:http://dic.nicovideo.jp/a/ng推奨コマンドの一覧
	if (ch.m.match(/(shita|big)/))
		return false;

	// NGワード
	return _.reduce([
			/^.{32}/, // 長文はカクつくので
			/[韓荒\[\]声]/,
			/(くない|くせえ|アンチ|びみょ|チョン)/,
			/(イライラ|いらいら)/,
			/(キモ|きも|パク|ぱく|エミュ|ウザ|うざ)/,
			/(うぜ|ウゼ)[えぇエェ]/,
			/(推奨|注意|NG|NG|自演)/,
			/(朝鮮|創価|在日)/,
			/(イラ|いら)[イいつ]?/,
			/(嫌|いや|イヤ)なら/,
			/(ゆとり|信者|名人様|視聴者様|赤字|水色|餓鬼)/,
			/(萎え|挙手)/,
			/(つま|ツマ)[ラら]?[なねんナネン]/,
			/(eco|eco|エコノミ|画質|時報|3DS|倍速)/,
			/^[ノノ]$/,
			/^[\//@@※←↑↓##♯]/,
		], (cary, re) => cary && !ch.c.match(re), true);
}

function xml2chats(xml) {
	return _.chain($(xml).find('chat'))
		.map(ch => 
			({ t: $(ch).attr('vpos') -0, //cast
			m: $(ch).attr('mail') || '',
			c: $(ch).text()}))
		.filter(ngfilter)
		.sortBy(c => c.t);
}

// 参考:https://gist.github.com/fushihara/fe39d97868036800d928
function createMsgRequest(info) {
	const getChanneleMsg = $.extend({
		'url': FLAPI + '/getthreadkey?thread=' + info.thread_id
							+ '&language_id=0',
	}, GET_QUERY);
	return info.needs_key == 1 ?
		// チャンネル
		$.ajax(getChanneleMsg).then(result => {
			info.threadkey = result.match(/threadkey=(.+?)&/)[1];
			info.requestXml =
				`<packet>
				<thread_leaves thread="${info.thread_id}"
					user_id="${info.user_id}"
					threadkey="${info.threadkey}"
					force_184="1"
					scores="1" nicoru="1"
					>0-${Math.ceil(info.l/60)}:100,1000</thread_leaves>
				</packet>`;
			return info;
		}):
		// 通常動画
		$.extend({requestXml :
			`<packet><thread thread="${info.thread_id}"
				version="20061206" res_from="-5000" scores="1"/>
				</packet>`}, info);
}

function loadMsg(info) {
	return $.ajax({
		type: 'POST',
		url: info.ms,
		// サーバーによってCORSで弾かれたりバッドリクエスト判定されたり
		// するので application/xmlでもなくtext/xmlでもなく
		// この値に落ち着いた
		contentType: "text/plain",
		dataType: 'xml',
		data: info.requestXml,
		crossDomain: true,
		cache: false,
	}).then(
		xml2chats,
		data => console.log('メッセージロード失敗', data)
	).done(chats => {
		var lastT = 0;
		// 次動画への遷移などで複数回登録させるのでoff()
		$video.off('timeupdate').on('timeupdate', _.throttle(ev => {
			// 時間イベントの発火で、対象メッセージがあれば流す
			// chat.vpos is 1/100 sec.
			var v = ev.target;
			var t = Math.round(v.currentTime * 100);
			chats.filter(ch => lastT < ch.t && ch.t < t)
				.forEach(_.throttle(marqueeMsg, 50));
			lastT = t;//更新

			// ついでに動画の進捗バーを更新
			var w = 100 * v.currentTime / v.duration; //in %
			$('.progressBar.seek .mainbar').css('width', w+'%');
			$('.controle-panel .current')
				.text(UTIL.sec2HHMMSS(v.currentTime));
			$('.controle-panel .duration')
				.text(UTIL.sec2HHMMSS(v.duration));
		}, 1000));
		$video.off('progress').on('progress', _.throttle(ev => {
			var v = ev.target;
			if (v.buffered.length == 0)
				return;
			var bufTime = v.buffered.end(v.buffered.length-1);
			var bw = 100 * bufTime / v.duration;
			$('.progressBar.seek .bufferbar').css('width', bw+'%');
		}, 1000));
	});
}

/** 動画URLなどの情報を取得してPromiseを返す。
 * キャリーされる値はクエリストリングをオブジェクトにした奴
 */
function loadApiInfo(id) {
	const getInfo = $.extend({'url': FLAPI + '/getflv?v=' + id},
					GET_QUERY);
	return $.ajax(getInfo)
	.then(qs => _.reduce(qs.split('&'), (o, k_v)=>{
		var a = _.map(k_v.split('='), decodeURIComponent);
		o[a[0]] = a[1];
		return o; // クエリストリングをオブジェクトにした奴
	},{}));
}

function marqueeMsg(ch) {
	const baseW = $('#kusa5').width() + 10;
	const hasMsg = $('#kusa5 .msg').size() > 0;
	const $m = $('<span class="msg"/>').text(ch.c);

	if (localStorage.kusa5disableCmd !== 'true') {
		// 改行を有効にする。冗長なのは XSS を懸念
		$m.html($m.text().replace(/\n/, '<br>'));
		// コマンドによる色の指定
		_.some(ch.m.split(' '), color => {
			if (color in colortable) {
				$m.css('color', colortable[color]);
				return true;
			} else if (color[0] === '#') {
				$m.css('color', color);
				return true;
			}
		});
	}

	$m.css('transform', `translate3d(${baseW}px, 0, 0)`);
	$video.after($m);

	function hasRightSpace(l) {
		// 一番右端にあるmsgの右端の位置
		var bigwidth = _.max(_.map($('#kusa5').find(l),
				// offsetLeftだと0が返る
				l => $(l).position().left + l.scrollWidth));
		var rigthSpace = baseW - bigwidth;
		// 比率係数は適当。文字が重なるようなら要調整
		// transition速度(つまりアニメーション再生時間)と関係
		return rigthSpace > $m.width() * 0.45;
	}

	const line = !hasMsg || hasRightSpace('.l1') ? 'l1' :
		hasRightSpace('.l2') ? 'l2' :
		hasRightSpace('.l3') ? 'l3' :
		hasRightSpace('.l4') ? 'l4' :
		hasRightSpace('.l5') ? 'l5' :
		hasRightSpace('.l6') ? 'l6' :
		hasRightSpace('.l7') ? 'l7' :
		hasRightSpace('.l8') ? 'l8' :
		hasRightSpace('.l9') ? 'l9' :
		hasRightSpace('.l10') ? 'l10' :
		hasRightSpace('.l11') ? 'l11' :
		hasRightSpace('.l12') ? 'l12' :
		'l13';

	$m.addClass(line);
	//オーバーシュート
	$m.css('transform', `translate3d(-${$m.width() + 10}px, 0, 0)`);
	//アニメ停止で自動削除
	$m.on('transitionend', ev => $(ev.target).remove());
}

var UTIL = {};
UTIL.sec2HHMMSS = function (sec) {
	var sec_num = parseInt(sec, 10); // don't forget the second param
	var hours = Math.floor(sec_num / 3600);
	var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
	var seconds = sec_num - (hours * 3600) - (minutes * 60);

	if (hours   < 10) {hours   = "0"+hours;}
	if (minutes < 10) {minutes = "0"+minutes;}
	if (seconds < 10) {seconds = "0"+seconds;}
	return (hours > 0? hours+':' :'') + minutes+':'+seconds;
};

const FullScreen = {};
FullScreen.isOpen = () =>
	document.mozFullScreen || document.webkitIsFullScreen ||
	(document.fullScreenElement && document.fullScreenElement !== null);
FullScreen.req = (e) =>
	!!e.mozRequestFullScreen && e.mozRequestFullScreen() ||
	!!e.requestFullScreen && e.requestFullScreen() ||
	!!e.webkitRequestFullScreen && e.webkitRequestFullScreen();
FullScreen.cancel = () =>
	!!document.mozCancelFullScreen && document.mozCancelFullScreen() ||
	!!document.cancelFullScreen && document.cancelFullScreen() ||
	!!document.webkitCancelFullScreen && document.webkitCancelFullScreen();
FullScreen.toggle = () =>
	FullScreen.isOpen() ?
		FullScreen.cancel() :
		FullScreen.req($('#kusa5')[0]);

function rateForm() {
	var rd = [1, 1.3463246851125779, 1.6678900230322302,
		1.9680012082762555, 2.249342814692259, 2.514125064795459,
		2.764189394992108, 3.001086195676507 ]
		.map(v=>
			`<input name="nicorate" type="radio" id="rd${v}"
				class="btn" value="${v}">
			<label for="rd${v}">${v.toFixed(1)}<span>x</span></label>`);
	return `<div class="ratepanel">${rd.join('')}</div>`;
}

const COMMENT = `
<div class="comment">
	<input type="text" class="l" /><button class="btn l">投稿</button>
</div>`;

const CONTROLE_PANEL = `
<div class="controle-panel">
	<div class="progressBar seek">
		<span class="bufferbar"/>
		<span class="mainbar"/>
	</div>
	<div class="progressBar buf"><span class="bar"/></div>
	<button class="btn toggle ico play"></button>
	${rateForm()}

	<button class="btn full ico r"></button>
	<input  class="r" id="volume-slider"type="range" name="bar" step="0.01"
			min="0" max="1" value="1" />
	<button class="btn speaker ico r"></button>
	<button class="btn comment-hidden ico r"></button>
	<div class="playtime r">
		<span class="current"></span>
		/
		<span class="duration"></span>
	</div>
</div>`;

function ctrPanel() {
	var $panel = $(CONTROLE_PANEL);
	$panel.find('.btn.full').click(FullScreen.toggle);
	$panel.find('.btn.toggle').click($video.videoToggle);
	return $panel;
}

/**
 * @param e MouseEvent
 */
function updateProgressBar(e) {
	var bar = $('.progressBar.seek');
	var offset = e.pageX - bar.offset().left; //Click pos
	var ratio = Math.min(1, Math.max(0, offset / bar.width()));
	//Update bar and video currenttime
	$('.progressBar.seek .mainbar').css('width', ratio * 100 +'%');
	$video[0].currentTime = $video[0].duration * ratio;
}


/** main というかエントリーポイント */
;(function () {
	const kusa5 = $('#kusa5')
		.append($video)
		.append(ctrPanel());

	var promise = loadApiInfo(LAUNCH_ID).then(info => {
		$video.attr('src', info.url);
		return info;
	});

	if (IS_IFRAME)
		return; // 以降はフォワードページのみの処理




	localStorage.nicoVolume = localStorage.nicoVolume || 0.25;
	localStorage.nicoMuted  = localStorage.nicoMuted  || false;

	$('input[name=nicorate]').change(ev => {
		localStorage.nicoRate = $video[0].playbackRate =
						parseFloat(ev.target.value);
	})
	.find('[value="'+ localStorage.nicoRate +'"]').click();

	$('#volume-slider').on('input', _.throttle(e => {
		// 音量再現
		$video[0].muted  = localStorage.nicoMuted  = false;
		$video[0].volume = localStorage.nicoVolume = e.target.value;
		kusa5.find('button.speaker').removeClass('muted');
	}, 100))
	.val(localStorage.nicoVolume); // 初期化

	$('#kusa5 button.speaker').click(function() {
		//ミュートトグル
		$video[0].muted = localStorage.nicoMuted =
					localStorage.nicoMuted === 'false';
		$(this).toggleClass('muted', $video[0].muted);
		$('#volume-slider').val($video[0].muted ?
					0 : localStorage.nicoVolume);
	});

	$('#kusa5 button.comment-hidden')
		.click(ev => kusa5.toggleClass('comment-hidden'));

	/* シークバーのドラッグ処理*/
	$('.progressBar.seek').mousedown(e => {
		updateProgressBar(e);
		$(document)
			.mouseup(e => {
				updateProgressBar(e);
				$(document).off('mouseup mousemove');
			})
			.mousemove(_.throttle(updateProgressBar, 100));
	});

	// ボタン押された時の動作登録
	var keyTbl = [];
	keyTbl[32] = $video.videoToggle; //スペースキー
	$(document)
		.keyup(e => {
			if (!keyTbl[e.keyCode])
				return;
			keyTbl[e.keyCode]();
			e.preventDefault();
		})
		//ボタンの処理が登録されてたらブラウザの動作をうちけす
		.keydown(e => keyTbl[e.keyCode] && e.preventDefault());


	//メッセージ取得、文字流しとかのループイベント登録
	promise.then(createMsgRequest).then(loadMsg);

	if (OPT.buffer) // バッファ用のiFrameを作成する
		setTimeout(() => createBuf(getNextId(LAUNCH_ID)), 10000);
})();