/* * JavaScript file jquery.text-on-youtube.js v0.5 last modified 10/12/28 * * 前バージョンからの追加・修正点: * 動画の画質を指定できるように変更。個別の動画に設定できるが、グローバルオプションとして指定できない。 * 動画ID指定のオブジェクトにforceQualityプロパティを指定し、hd720、large、medium、smallのいずれかのパラメーターを渡す * 字幕データとしてJSONファイルのURLを受け取れるように変更 * */ /* 最低限必要なグローバル変数を宣言 */ var onYouTubePlayerReady, onYoutubeQualityChange, youtubeStateObserver, youtubeErrorChecker; (function(jQuery) { var youtube = {}, currentID = 1000, isPlainObject = jQuery.isPlainObject, isArray = jQuery.isArray, ceil = Math.ceil, floor = Math.floor, settings, videoIDs, videoObjs, subtitles, timer; /* プラグインを呼び出すメソッド */ jQuery.fn.textOnYoutube = function(videoID, options, subtitle) { var self = this; /* SWFObjectライブラリが読み込まれているかチェック */ if (typeof swfobject == 'undefined') return this; /* 字幕データがJSONファイルの場合 */ if (isString(options) || isString(subtitle)) { var jsonURL = isString(options) ? options : subtitle; if (isString(options)) jQuery.getJSON(jsonURL, null, function(data, status) { self.textOnYoutube(videoID, data); }); else jQuery.getJSON(jsonURL, null, function(data, status) { self.textOnYoutube(videoID, options, data); }); return this; } /* デフォルトオプション。後でユーザーオプションに上書きされる */ settings = { width: 640, height: 360, autoplay: 0, loop: 0 }; /* オプションと字幕情報の設定 */ if (options) { if (isPlainObject(options)) { if (!options.videoID) { settings = jQuery.extend(settings, options); } else subtitles = [options]; } else if (isArray(options)) { subtitles = jQuery.map(options, function(object, index) { return jQuery.extend(true, {}, object); }); } } if (subtitle) { if (isArray(subtitle)) { subtitles = jQuery.map(subtitle, function(object, index) { return jQuery.extend(true, {}, object); }); } else subtitles = [subtitle]; } /* 各動画の初期設定 */ videoID = !isPlainObject(videoID) ? videoID : [videoID]; if (isArray(videoID)) { if (isPlainObject(videoID[0])) { videoObjs = self.map(function(index, element) { var result = null; jQuery.each(videoID, function(index, object) { if (element.id == object.elementID) { result = { videoID: object.videoID, width: object.width || settings.width, height: object.height || settings.height, autoplay: object.autoplay || settings.autoplay, loop: object.loop || settings.loop, forceQuality: object.forceQuality, uiWidgetClass: object.uiWidgetClass }; result.baseFontSize = object.baseFontSize || floor(10 * result.width / 320); } }); if (result === null) self.splice(index, 1); return result; }); } else videoObjs = jQuery.map(videoID, function(videoID, index) { return { videoID: videoID, width: settings.width, height: settings.height, baseFontSize: floor(10 * settings.width / 320), autoplay: settings.autoplay, loop: settings.loop }; }); } else videoObjs = [{ videoID: videoID, width: settings.width, height: settings.height, baseFontSize: floor(10 * settings.width / 320), autoplay: settings.autoplay, loop: settings.loop }]; /* 個別に動画埋め込み処理 */ return this.each(function(index) { if (!videoObjs[index]) return; var videoObj = videoObjs[index], videoID = videoObj.videoID || 'video-' + currentID, cacheID = currentID, width = videoObj.width, height = videoObj.height, autoplay = videoObj.autoplay || 0, loop = videoObj.loop || 0, uiWidgetClass = videoObj.uiWidgetClass || 'ui-widget-header', mouseover = false, mute = false, unstarted = true, disabled = true, textLayer, toggle, timer, controller; /* youtubeオブジェクトに各動画の情報が入る */ youtube[videoID] = {}; /* 字幕情報があればプロパティに入れる */ if (subtitles) { jQuery.each(subtitles, function(index, object) { if (object.videoID == videoID) youtube[videoID].subtitles = object; }); } /* 外枠のスタイル指定 */ jQuery(this).css({ width: width, height: height }); /* テキストレイヤー作成 */ jQuery(this) .append( textLayer = jQuery('
', { id: 'text-' + currentID, 'class': 'text_layer', css: { fontSize: videoObj.baseFontSize, width: width, height: height } }) ) .append( jQuery('', { id: 'replace-' + currentID, html: 'この動画を利用するためには、バージョン8以上のFlashプレイヤーとJavaScriptを有効にする必要があります。' }) ); /* 各種プロパティ初期化 */ jQuery.extend(youtube[videoID], { textLayer: textLayer.get(0), currentIndex: 0, currentTime: 0, userQuality: videoObj.forceQuality || null, isFineQuality: true, currentQuality: null, availableQualityLevels: null, totalTime: 0, autoSlide: true, currentState: 'unstarted', errorState: null, mousedown: false }); /* テキストレイヤー+コントロールと動画レイヤーの切り替えボタン作成 */ jQuery(this) .append( toggle = jQuery('', { id: 'toggle-' + currentID, css: { opacity: 0, zIndex: 1000, left: 4, top: 4, width: 24, height: 14 }, title: 'レイヤー切り替え', html: 'toggle' }).hide() .button({ icons: { primary: 'ui-icon-newwin' }, text: false }) .click(function(e) { if (!disabled) { disabled = true; controller.animate({ opacity: 0 }, 'fast').hide(0); jQuery(youtube[videoID].textLayer).css({ zIndex: 1 }); } else { disabled = false; controller.show(0).animate({ opacity: 0.9 }, 'fast'); jQuery(youtube[videoID].textLayer).css({ zIndex: 3 }); } }) ); /* コントロール部分の作成 */ jQuery(this) .append( controller = jQuery('', { id: 'control-' + currentID, 'class': 'video_controller ' + uiWidgetClass, css: { opacity: 0, left: 4, top: (height - 36), width: (width - 28), height: 24 } }).hide() ) .hover( function(e) { if (!unstarted) toggle.show(0).animate({ opacity: 0.9 }, 'fast'); if (!disabled && this != e.relatedTarget && !jQuery.contains(this, e.relatedTarget)) { controller.show(0).animate({ opacity: 0.9 }, 'fast'); } }, function(e) { if (!unstarted) toggle.animate({ opacity: 0 }, 'fast').hide(0); if (!disabled && this != e.relatedTarget && !jQuery.contains(this, e.relatedTarget)) { controller.animate({ opacity: 0 }, 'fast').hide(0); } } ); /* コントロールの各種ボタン作成 */ controller .append( jQuery('', { id: 'play-pause' + currentID, css: { marginRight: 4 }, title: '再生/ポーズ', html: 'play-pause' }) ) .append( jQuery('', { id: 'stop-' + currentID, css: { marginRight: 4 }, title: '停止', html: 'stop' }) ) .append( jQuery('', { id: 'seek-prev' + currentID, css: { marginRight: 4 }, title: '巻き戻し', html: 'prev-seek' }) ) .append( jQuery('', { id: 'seek-next' + currentID, css: { marginRight: 4 }, title: '早送り', html: 'next-seek' }) ) .append( jQuery('', { id: 'volume-' + currentID, css: { marginRight: 8 }, title: '音量オン/オフ', html: 'next-seek' }) ) .append( jQuery('', { id: 'current-time-' + currentID, css: { verticalAlign: 'middle', fontSize: '12px' }, html: '--' }) ) .append( jQuery('', { css: { verticalAlign: 'middle', fontSize: '12px' }, html: ' / ' }) ) .append( jQuery('', { id: 'total-time-' + currentID, css: { verticalAlign: 'middle', fontSize: '12px' }, html: '--' }) ) .append( jQuery('', { id: 'slider-' + currentID, 'class': 'video_slider', css: { width: (width - 270), left: 250, top: 8 } }) ); /* 各ボタンにアイコンやイベントを設定 */ controller.children('button[id^=play-pause]').button({ icons: { primary: 'ui-icon-play' }, text: false }) .click(function(e) { if (youtube[videoID].currentState == 'playing') { youtube[videoID].player.pauseVideo(); jQuery(this).button('option', 'icons', { primary: 'ui-icon-play' }); } else if (youtube[videoID].currentState == 'paused' || youtube[videoID].currentState == 'video-cued') { youtube[videoID].player.playVideo(); jQuery(this).button('option', 'icons', { primary: 'ui-icon-pause' }); } else if (youtube[videoID].currentState == 'ended' || youtube[videoID].currentState == 'unstarted') { youtube[videoID].player.seekTo(0); jQuery(this).button('option', 'icons', { primary: 'ui-icon-pause' }); } }) .next().button({ icons: { primary: 'ui-icon-stop' }, text: false }) .click(function(e) { youtube[videoID].player.stopVideo(); if (youtube[videoID].subtitles) resetSubtitles(videoID); jQuery(this).prev().button('option', 'icons', { primary: 'ui-icon-play' }); }) .next().button({ icons: { primary: 'ui-icon-seek-prev' }, text: false }) .mousedown(function(e) { youtube[videoID].mousedown = true; var timer = setInterval(function() { if (!youtube[videoID].mousedown) clearInterval(timer); var currentTime = youtube[videoID].player.getCurrentTime(), totalTime = youtube[videoID].totalTime; youtube[videoID].player.seekTo(currentTime - ceil(totalTime / 30)); if (youtube[videoID].subtitles) seekCurrentSubtitle(videoID); }, 500); }) .mouseup(function(e) { youtube[videoID].mousedown = false; }) .next().button({ icons: { primary: 'ui-icon-seek-next' }, text: false }) .mousedown(function(e) { youtube[videoID].mousedown = true; var timer = setInterval(function() { if (!youtube[videoID].mousedown) clearInterval(timer); var currentTime = youtube[videoID].player.getCurrentTime(), totalTime = youtube[videoID].totalTime; youtube[videoID].player.seekTo(currentTime + ceil(totalTime / 30)); if (youtube[videoID].subtitles) seekCurrentSubtitle(videoID); }, 300); }) .mouseup(function(e) { youtube[videoID].mousedown = false; }) .next().button({ icons: { primary: 'ui-icon-volume-on' }, text: false }) .click(function(e) { if (!mute) { mute = true; jQuery(this).button('option', 'icons', { primary: 'ui-icon-volume-off' }); youtube[videoID].player.mute(); } else { mute = false; jQuery(this).button('option', 'icons', { primary: 'ui-icon-volume-on' }); youtube[videoID].player.unMute(); } }) .siblings('div:first').slider({ start: function(event, ui) { youtube[videoID].autoSlide = false; }, stop: function(event, ui) { youtube[videoID].autoSlide = true; youtube[videoID].player.seekTo(floor(youtube[videoID].totalTime * ui.value / 100)); if (youtube[videoID].subtitles) seekCurrentSubtitle(videoID); } }) .progressbar({ value: 0 }) .children('a:first').css({ width: 10, height: 24 }); /* 動画が始まったら動画レイヤーからテキストレイヤーに切り替える */ timer = setInterval(jQuery.proxy(function() { if (youtube[videoID].currentState == 'playing') { textLayer.css({ zIndex: 3 }); controller.show(0).animate({ opacity: 0.9 }, 'fast'); toggle.show(0).animate({ opacity: 0.9 }, 'fast'); unstarted = false; disabled = false; jQuery('#control-' + cacheID + '> button').first().button('option', 'icons', { primary: 'ui-icon-pause' }); clearInterval(timer); } }, this), 100); /* SWFObjectライブラリを呼び出して動画を埋め込む */ var params = { allowScriptAccess: 'always', wmode: 'transparent' }, atts = { id: videoID, 'class': 'video_layer' }; swfobject.embedSWF('http://www.youtube.com/apiplayer?enablejsapi=1&playerapiid=' + videoID + '&version=3&autoplay=' + autoplay + '&loop=' + loop, 'replace-' + currentID, width, height, '8', null, null, params, atts); currentID++; }); }; /* 動画プレイヤーの準備ができたら呼び出されるコールバック */ onYouTubePlayerReady = function(playerID) { youtube[playerID].player = document.getElementById(playerID); initVideo(playerID); }; /* 動画の画質が変更されたら呼び出されるコールバック */ onYoutubeQualityChange = function(quality, playerID) { var isInclude = false; youtube[playerID].currentQuality = quality; jQuery.each(youtube[playerID].availableQualityLevels, function(index, level) { if (quality == level) { isInclude = true; return false; } }); youtube[playerID].isFineQuality = isInclude ? true : false; }; /* 動画プレイヤーの状態が変更されたら呼び出されるコールバック */ youtubeStateObserver = function(state, playerID) { if (youtube[playerID].currentState == 'unstarted' && state == 5) { youtube[playerID].currentState = 'video-cued'; } else if (state == -1) { youtube[playerID].currentState = 'unstarted'; } else if (state === 0) { youtube[playerID].currentState = 'ended'; } else if (state == 1) { youtube[playerID].currentState = 'playing'; } else if (state == 2) { youtube[playerID].currentState = 'paused'; } }; /* 動画プレイヤーでエラーが発生したら呼び出されるコールバック */ youtubeErrorChecker = function(code, playerID) { if (code == 100) { youtube[playerID].errorState = 'not-found'; } else if (code === 101 || code === 150) { youtube[playerID].errorState = 'not-allowed'; } }; /* プレイヤー準備終了後に初期設定を行う */ function initVideo(playerID) { if (!youtube[playerID].player) { if (timer) clearTimeout(timer); timer = setTimeout(curryTimerCallback(initVideo, playerID), 30); return; } var element = jQuery('object[id=' + playerID + ']').parent(), controller = element.children('.video_controller:first'), bar = controller.children('div:first'), sliderWidth = (controller.width() - 270), currentTime = 0, totalTime = 0, videoBytesTotal = 0, videoBytesLoaded = 0, count = 0, timer; if (youtube[playerID].subtitles) setupSubtitles(playerID); youtube[playerID].player.addEventListener('onStateChange', '(function(state) { youtubeStateObserver(state, "' + playerID + '"); })'); youtube[playerID].player.addEventListener('onError', '(function(state) { youtubeErrorChecker(state, "' + playerID + '"); })'); youtube[playerID].player.addEventListener('onPlaybackQualityChange', '(function(state) { onYoutubeQualityChange(state, "' + playerID + '"); })'); youtube[playerID].player.cueVideoById(playerID, 0); timer = setInterval(function() { if (totalTime === 0) { totalTime = youtube[playerID].player.getDuration(); if (totalTime > 0) { youtube[playerID].currentQuality = youtube[playerID].player.getPlaybackQuality(); youtube[playerID].availableQualityLevels = youtube[playerID].player.getAvailableQualityLevels(); if (youtube[playerID].userQuality !== null) youtube[playerID].player.setPlaybackQuality(youtube[playerID].userQuality); youtube[playerID].totalTime = totalTime; controller.children('span:eq(2)') .text(floor(totalTime / 60) + ':' + (totalTime % 60 < 10 ? '0' + totalTime % 60 : totalTime % 60)); } } else if (videoBytesTotal === 0) { videoBytesTotal = youtube[playerID].player.getVideoBytesTotal(); } else { currentTime = youtube[playerID].player.getCurrentTime(); videoBytesLoaded = youtube[playerID].player.getVideoBytesLoaded(); youtube[playerID].currentTime = currentTime; if (count % 15 === 0) { var ct = floor(currentTime), rate = floor(100 * ct / totalTime); if (youtube[playerID].autoSlide) { controller.children('span:first') .text(floor(ct / 60) + ':' + (ct % 60 < 10 ? '0' + (ct % 60) : (ct % 60))); bar.slider('value', rate); } bar.progressbar('value', floor(100 * videoBytesLoaded / videoBytesTotal)) } if (youtube[playerID].subtitles && youtube[playerID].currentState == 'playing') { updateSubtitles(playerID, currentTime); } } count++; }, 20); } /* 字幕情報のセットアップ */ function setupSubtitles(playerID) { var subtitles = youtube[playerID].subtitles; if (!isArray(subtitles.timeline)) subtitles.timeline = [subtitles.timeline]; subtitles.timeline.sort(function(a, b) { var a = a.time, b = b.time; return a < b ? -1 : a > b ? 1 : 0; }); jQuery.each(subtitles.timeline, function(index, subtitle) { var element; jQuery.extend(subtitle, { state: 'idle', duration: subtitle.duration || 1 }); jQuery(youtube[playerID].textLayer).append( element = jQuery('', { id: 'subtitle-' + index + '-' +playerID, 'class': 'video_subtitle', css: subtitle.css }).hide() ); if (jQuery.browser.msie) { element.css({ filter: 'progid:DXImageTransform.Microsoft.Shadow(Color=#000000, Direction=135, Strength=1);' }); } if (subtitle.anchor) { element.append( jQuery('', { id: 'anchor-' + index + '-' +playerID, href: subtitle.anchor, html: subtitle.text }) ); } else element.html(subtitle.text); subtitle.element = element.get(0); }); } /* 動画の経過時間に合わせ字幕を表示する */ function updateSubtitles(playerID, currentTime) { var y = youtube[playerID], currentTime = currentTime || y.player.getCurrentTime(), currentSubtitle; if (y.currentIndex < y.subtitles.timeline.length) { currentSubtitle = y.subtitles.timeline[y.currentIndex]; if (currentSubtitle.time <= currentTime) { if (currentSubtitle.effects && currentSubtitle.state == 'idle') { currentSubtitle.state = 'running'; jQuery(currentSubtitle.element).stop(true, false); if (isArray(currentSubtitle.effects)) { if (currentSubtitle.effects[0] != 'show' && currentSubtitle.effects[0] != 'fadeIn' && currentSubtitle.effects[0] != 'slideDown' ) jQuery(currentSubtitle.element).show(0); jQuery(currentSubtitle.element)[currentSubtitle.effects[0]]( currentSubtitle.effects[1], currentSubtitle.effects[2], currentSubtitle.effects[3], currentSubtitle.effects[4] ); } else jQuery(currentSubtitle.element)[currentSubtitle.effects](0); } y.currentIndex++; } } jQuery.each(y.subtitles.timeline, function(index, subtitle) { if (subtitle.effects && subtitle.state == 'running') { if (currentTime > subtitle.time + subtitle.duration) { subtitle.state = 'finished'; jQuery(subtitle.element).hide(0); } } }); } /* 字幕設定を動画スタート時に戻す */ function resetSubtitles(playerID) { var y = youtube[playerID], currentTime = y.player.getCurrentTime(), currentSubtitle; y.currentIndex = 0; jQuery.each(y.subtitles.timeline, function(index, subtitle) { subtitle.state = 'idle'; jQuery(subtitle.element).stop(true, false).hide(0).css(subtitle.css); }); } /* 字幕設定を指定された時間に合わせて変更する */ function seekCurrentSubtitle(playerID) { var y = youtube[playerID], currentTime = y.player.getCurrentTime(), timeline = y.subtitles.timeline, lastSubtitle = timeline[timeline.length - 1], isFoundSubtitle = false; if (currentTime > lastSubtitle.time + lastSubtitle.duration) { y.currentIndex = timeline.length; jQuery.each(timeline, function(index, subtitle) { subtitle.state = 'finished'; jQuery(subtitle.element).stop(true, false).hide(0).css(subtitle.css); }); } else { y.currentIndex = 0; jQuery.each(timeline, function(index, subtitle) { jQuery(subtitle.element).stop(true, false).hide(0).css(subtitle.css); if (!isFoundSubtitle) { if (currentTime <= subtitle.time + subtitle.duration) { subtitle.state = 'idle'; isFoundSubtitle = true; y.currentIndex = index; } else subtitle.state = 'finished'; } else subtitle.state = 'idle'; }); } } /* ユーティリティ関数 */ function curryTimerCallback() { var userFunc = arguments[0], args = Array.prototype.slice.call(arguments, 1); return function() { return userFunc.apply(this, args); }; } function isString(object) { return Object.prototype.toString.call(object) == '[object String]'; } })(jQuery);