Youtubeにはプレイリスト機能があり、選んだ動画を順番にループ再生することができますが、無料だと途中に広告がはいってしまいます。そこで、指定した動画だけを順番にループ再生させるだけの JavaScriptのサイトをGemini(無料)に作ってもらいました。70回くらいだめだしした結果、わりと使いやすいものができたので公開します。スマホだけでも作れるみたいです
1.ループ再生する仕組み
- Java Script プログラムでYoutubeを順番に読み込んで再生します
- サイトは、Google Blogger を使い、再生と再生リストの2ページを設置します。無料で簡単
- 再生動画のタイトルなどのテロップが流れます
2.Google Bloggerをひらく
スマホでも、こちら(Blogger)からログインすればできそうです
- Google検索の右上のツールボタンから、ブロガー(Blogger)を選びます
- ブロガーはツールの下のほうにあります。右側のスクロールバーで下のほうを探してみてください。スマホの場合は、こちら(Blogger)からログイン
- ブログを作成ボタンをタッチします
- ブログの名前を入力します。好きな名前をつけてかまいません
- 「次へ」ボタンを押します。
- 公開するブログアドレスを入力します。ほかの人が使っている名前はつかえないので、その場合は別の名前をさがしましょう
- 「次へ」ボタンを押します。
- ブログの名前をもう一度入力して、
- 完了ボタンを押します。ブロガーの編集画面がひらきます
3.再生用ページをつくる
- 最初につけたタイトルがサイトのアドレスになるので、まず簡単な名前をつけていったん公開するのがよいみたいです。
- HTMLビューにして、プログラムをコピペします
- タイトルも表示されるので、不要なら後から削除してもかまいません
- 左側のページをタッチします。
左側の項目が見えない場合は、左上の三本線ボタンを押せば、表示されます - 新しいページをタッチして、ページを追加します
- 最初につけたタイトルがアドレスになるので、「play」のような簡単な英語タイトルをつけます
- 「公開」ボタンを押します
- タイトルをつけたページが追加されるので、タッチしてもう一度ひらきます
- 「鉛筆」ボタンをおして、HTMLビューにします
- 必ずHTMLビューにしてください。「鉛筆」ボタン(作成ビュー)だと動きません
- 下のプレーヤープログラムを全部選択して、この中にコピペします。コントロールC、コントロールV、または、長押しメニューでコピペしましょう
- 「公開」ボタンで保存します
- 「目」ボタンで作成したページを表示してみてください。プログラムの中に9VAeきゅうべえのプレイリストが指定されているので、9VAeきゅうべえの動画が順番に再生されれば成功です
◆プレーヤープログラム
このプログラムは生成AI(Gemini)で作成し、筆者が動作確認しました。筆者はWebアプリ初心者なので無駄な記述があってもわかりません。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Youtube ループ プレイヤー</title>
<style>
body { font-family: sans-serif; text-align: center; background: #f0f2f5; margin: 0; padding: 20px; }
h1 { font-size: 20px; margin-bottom: 15px; font-weight: bold; }
.telop-container { width: 100%; max-width: 800px; margin: 0 auto 15px auto; background: #222; color: #fff; padding: 12px; border-radius: 6px; box-sizing: border-box; overflow: hidden; white-space: nowrap; position: relative; font-size: 18px; font-weight: bold; box-shadow: inset 0 2px 4px rgba(0,0,0,0.5); }
.telop-stage { display: inline-block; white-space: nowrap; animation: marquee 25s linear infinite; }
.telop-text { display: inline-block; padding-right: 4em; }
@keyframes marquee { 0% { transform: translate(0, 0); } 100% { transform: translate(-50%, 0); } }
.video-container { position: relative; width: 100%; max-width: 800px; margin: 0 auto; aspect-ratio: 16/9; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden; background: #000; }
iframe { width: 100%; height: 100%; border: none; display: block; }
.info { margin-top: 15px; font-size: 14px; color: #666; }
.status { font-weight: bold; color: #1a73e8; }
.controls { margin-top: 10px; display: flex; justify-content: center; gap: 10px; }
.btn { padding: 8px 18px; font-size: 14px; cursor: pointer; background: #fff; border: 1px solid #ccc; border-radius: 4px; font-weight: bold; transition: background 0.2s; }
.btn:hover { background: #f0f0f0; }
.btn-debug { background: #f0f2f5; color: #666; border: 1px dashed #ccc; font-weight: normal; }
/* モニターボックスの初期状態を非表示(display: none)に設定しました */
.debug-monitor-box { display: none; margin-top: 20px; background: #1e1e1e; color: #00ff00; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 13px; text-align: left; max-width: 800px; margin-left: auto; margin-right: auto; }
</style>
</head>
<body>
<h1>Youtube ループ プレイヤー</h1>
<div class="telop-container">
<div id="telop-stage" class="telop-stage">
<span id="telop-text" class="telop-text">CSVデータを読み込み中...</span><span id="telop-text-clone" class="telop-text">CSVデータを読み込み中...</span>
</div>
</div>
<div class="video-container"><div id="player"></div></div>
<div class="info">
<p>ステータス: <span id="current-status" class="status">アプリを初期化中...</span></p>
<div class="controls">
<button class="btn" onclick="nextVideo()">次の動画へ</button>
<!-- 切り替え用の隠しボタンを追加しました -->
<button class="btn btn-debug" onclick="toggleDebugMonitor()">モニター表示/非表示</button>
</div>
</div>
<!-- モニター領域(コードはそのまま残してあります) -->
<div id="debug-box" class="debug-monitor-box">
<div style="font-weight: bold; border-bottom: 1px solid #00ff00; padding-bottom: 5px; margin-bottom: 5px;">再生ステータスモニター</div>
<div>現在の再生位置: <span id="m-index" style="color:yellow;">-</span></div>
<div>現在の動画ID: <span id="m-id" style="color:yellow;">-</span></div>
</div>
<!-- アプリのプログラム本体 -->
<script>
// 設定: リストを置いている正しい固定ページURL
//============= CSVデータ+応答スクリプトのURL(この中のVSV部分を更新する)=============
const TARGET_FILE = 'https://play-yt.blogspot.com/p/csv.html';
//======================================================================================
let videoIds = [], telopTexts = [], currentIndex = 0, player = null;
function printLog(msg, color = "#00ff00") {
console.log(msg);
const box = document.getElementById('debug-box');
if (box) {
const div = document.createElement('div');
div.style.color = color;
div.style.borderBottom = "1px dashed #333";
div.style.padding = "3px 0";
div.textContent = `${msg}`;
box.appendChild(div);
}
}
function logStatus(msg) {
printLog(`【状態更新】${msg}`, "cyan");
document.getElementById('current-status').textContent = msg;
}
function toggleDebugMonitor() {
const box = document.getElementById('debug-box');
if (box.style.display === 'block') {
box.style.display = 'none';
} else {
box.style.display = 'block';
}
}
function extractVideoId(url) {
if (!url) return null;
let cleanUrl = url.replace(/[\r\n\t\s"]/g, '').trim();
if (cleanUrl.length === 11 && !cleanUrl.includes('http') && !cleanUrl.includes('.')) {
return cleanUrl;
}
let m = cleanUrl.match(/(?:v=|youtu\.be\/|embed\/|^|[^A-Za-z0-9_-])([A-Za-z0-9_-]{11})(?:[^A-Za-z0-9_-]|$)/);
if (m && m[1]) {
return m[1].trim();
}
return null;
}
function updateTelop(text) {
const txt = text.replace(/<br\s*\/?>/gi, ' ') || "(追加情報なし)";
document.getElementById('telop-text').textContent = txt;
document.getElementById('telop-text-clone').textContent = txt;
const stg = document.getElementById('telop-stage');
stg.style.animation = 'none';
stg.offsetHeight;
stg.style.animation = null;
}
function parseCSV(text) {
const cleanText = text.replace(/^\ufeff/, '');
const lines = cleanText.split(/\r?\n/);
return lines.map((line) => {
let r = [], c = '', q = false;
for (let i = 0; i < line.length; i++) {
if (line[i] === '"') q = !q;
else if (line[i] === ',' && !q) { r.push(c.trim()); c = ''; }
else c += line[i];
}
r.push(c.trim());
return r;
});
}
function startSecureBridge() {
printLog("① 固定ページの読み込み(通信)を開始します...", "yellow");
const oldIfr = document.querySelector('.data-bridge-frame');
if (oldIfr && oldIfr.parentNode) oldIfr.parentNode.removeChild(oldIfr);
const ifr = document.createElement('iframe');
ifr.className = 'data-bridge-frame';
ifr.src = `${TARGET_FILE}?cachebust=${new Date().getTime()}`;
ifr.style.display = 'none';
ifr.onload = function() {
printLog("② 固定ページのHTML読み込みが完了。データ要求を送信中...", "yellow");
setTimeout(() => {
ifr.contentWindow.postMessage('REQUEST_CSV_DATA', '*');
}, 1000);
};
document.body.appendChild(ifr);
}
window.addEventListener('message', function(event) {
if (!event.origin.includes('blogspot.com')) return;
if (event.data && event.data.type === 'RESPONSE_CSV_DATA') {
printLog("③ 固定ページ側からCSVテキストの受信に成功しました!", "white");
processCsvData(event.data.text);
const ifr = document.querySelector('.data-bridge-frame');
if (ifr && ifr.parentNode) ifr.parentNode.removeChild(ifr);
}
});
function processCsvData(text) {
printLog(`④ 受信データの文字数: ${text.length} 文字。解析を始めます...`, "white");
const rows = parseCSV(text);
printLog(`⑤ CSVを分解した結果: ${rows.length} 行検出。`, "white");
videoIds = []; telopTexts = [];
rows.forEach((row, rowIdx) => {
if (!row || row.length === 0) return;
let id = null, telop = [];
row.forEach((val, colIdx) => {
if (!val) return;
if (colIdx === 4 || val.includes('http') || val.includes('youtu')) {
let vid = extractVideoId(val);
if (vid) id = vid;
} else if (colIdx < 4) {
let cln = val.replace(/[\r\n"]/g, '').trim();
if (cln) telop.push(cln);
}
});
if (id) {
videoIds.push(id);
telopTexts.push(telop.filter(v => v).join(' | '));
}
});
printLog(`⑥ 有効なYouTube動画IDの抽出数: ${videoIds.length} 個`, "orange");
if (videoIds.length > 0) {
printLog(`【抽出されたID一覧】: ${videoIds.join(', ')}`, "orange");
// 【修正箇所】すでにプレイヤーが存在する場合は、二重生成せず即座に1本目を再生する
if (player) {
printLog(" 2周目以降のループ処理: 既存のプレイヤーを再利用して再生位置をリセットします。", "lime");
currentIndex = 0;
playCurrentVideo();
} else {
printLog("⑦ 初回起動: YouTube起動スクリプトを動的に強制注入します...", "yellow");
window.YT = window.YT || { loading: 0, loaded: 0 };
window.YTConfig = window.YTConfig || { 'host': 'https://youtube.com' };
const tag = document.createElement('script');
tag.src = "https://youtube.com/iframe_api";
const firstScriptTag = document.getElementsByTagName('script')[0];
if (firstScriptTag && firstScriptTag.parentNode) {
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
} else {
document.body.appendChild(tag);
}
logStatus("YouTubeプレイヤーを生成中...");
checkYouTubeApiReady();
}
} else {
printLog(" 警告: CSVデータから動画IDが1つも抽出できませんでした。", "red");
logStatus("エラー: 有効なYouTube動画が見つかりません。");
}
}
function checkYouTubeApiReady() {
printLog("⑧ プレイヤー生成のための待機ループを開始します...", "yellow");
let attempts = 0;
const timer = setInterval(() => {
attempts++;
if (typeof YT !== 'undefined' && YT.Player) {
clearInterval(timer);
printLog(`⑨ 成功: 起動を確認しました(確認回数: ${attempts}回目)。プレイヤーを画面に表示します。`, "lime");
initYouTubePlayer();
} else if (attempts > 10) {
clearInterval(timer);
printLog(" 警告: 公式APIが遮断されているため、直接埋め込みモードに切り替えます。", "red");
initDirectIframePlayer();
}
}, 500);
}
function initYouTubePlayer() {
if (player) return;
try {
player = new YT.Player('player', {
height: '100%', width: '100%', videoId: videoIds[currentIndex],
playerVars: { 'playsinline': 1, 'autoplay': 1, 'mute': 1, 'rel': 0 },
events: {
'onReady': () => { printLog("?? 通常プレイヤー準備完了!動画スタート。", "lime"); playCurrentVideo(); },
'onStateChange': (e) => { if (e.data === YT.PlayerState.ENDED) { nextVideo(); } }
}
});
} catch(e) {
printLog(` 通常生成エラー: ${e.message}。直接埋め込みに切り替えます。`, "red");
initDirectIframePlayer();
}
}
function initDirectIframePlayer() {
const container = document.getElementById('player');
if (!container) return;
logStatus(`再生中 (${currentIndex + 1} / ${videoIds.length})`);
updateTelop(telopTexts[currentIndex]);
document.getElementById('m-index').textContent = `${currentIndex + 1} / ${videoIds.length}`;
document.getElementById('m-id').textContent = videoIds[currentIndex];
container.innerHTML = `<iframe id="direct-video" width="100%" height="100%" src="https://youtube.com/embed/${videoIds[currentIndex]}?autoplay=1&mute=1&playsinline=1&enablejsapi=1" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>`;
printLog(" 直接埋め込みモードで動画を表示しました!", "lime");
}
function playCurrentVideo() {
if (videoIds.length === 0) return;
if (document.getElementById('direct-video')) {
initDirectIframePlayer();
return;
}
const id = videoIds[currentIndex];
logStatus(`再生中 (${currentIndex + 1} / ${videoIds.length})`);
updateTelop(telopTexts[currentIndex]);
document.getElementById('m-index').textContent = `${currentIndex + 1} / ${videoIds.length}`;
document.getElementById('m-id').textContent = id;
if (player && typeof player.loadVideoById === 'function') {
player.loadVideoById({ videoId: id, startSeconds: 0, suggestedQuality: 'default' });
player.mute();
}
}
function nextVideo() {
currentIndex++;
if (currentIndex < videoIds.length) {
playCurrentVideo();
} else {
logStatus('全動画終了。再読込してループします...');
startSecureBridge();
}
}
printLog("【アプリ起動】初期化プロセスを開始しました。", "cyan");
startSecureBridge();
</script>
</body>
</html>
4.プレイリストページを作る
- プレイリストを入れたページを作ります
- <div>タグの中に、CSV形式で、プレイリストをいれます
- 「ページ」を選びます
- 「新しいページ」を追加します
- タイトルを「CSVデータ+プログラム」という名前にしましょう
- HTMLビューであることを確認してください
- 下の「CSVデータ+プログラム」を、この中にコピペします
- 公開ボタンで保存します
◆CSVデータ+プログラム
<!-- CSVデータを囲む箱です -->
<div id="my-csv-data" style="display:none;">
No,SubNo,タイトル,メモ,Youtube動画リンク先
1,,9VAeきゅうべえの使い方,,https://youtu.be/veq_IkXQ5ZI
2,,ベクター方式とは,,https://youtu.be/lvjEFhIz-a8
3,,「つながった図形」,,https://youtu.be/H8YvVea6QD0
4,,「ゴースト」とは,,https://youtu.be/y2YxXJmMvZI
5,,9VAeきゅうべえの点選択,,https://youtu.be/g9o6PiuwJuw
6,,9VAeきゅうべえの3D変形,,https://youtu.be/k3UJ29gskBM
7,,登録、動きグラフ,,https://youtu.be/vk4OGzOTYEM
8,,アニメキャスト1,,https://youtu.be/tjuO1pOsu_8
9,,アニメキャスト2,,https://youtu.be/sDB4s_nse-w
10,,9VAeきゅうべえのグループ化,,https://youtu.be/Elnl7yWcs8I
11,,制御命令とラベル,,https://youtu.be/GTN8ENGAhHA
12,,画像に字幕や音声をいれる方法,,https://youtu.be/ay6_FrDoi2c
13,,便利なコントロールオプション,,https://youtu.be/RI-9SvoUbxs
14,,文字をアウトライン化してロゴを作る,,https://youtu.be/opBsImDaqhM
15,,蜘蛛の糸【Two-Piece 作品】,曲:対自的ユートピア,https://youtu.be/0bqU_skSk2Q
</div>
<script>
// 親ページからの「データちょうだい」の合図を待ち受けます
window.addEventListener('message', function(event) {
// セキュリティチェック(同じBloggerドメインからの通信のみ許可)
if (!event.origin.includes('blogspot.com')) return;
if (event.data === 'REQUEST_CSV_DATA') {
const dataElem = document.getElementById('my-csv-data');
if (dataElem) {
const csvText = dataElem.textContent || dataElem.innerText;
// 安全な通信ルートで親ページへデータを送り届けます
event.source.postMessage({ type: 'RESPONSE_CSV_DATA', text: csvText }, event.origin);
}
}
});
</script>
ページの最初の<div>から</div>の中に、コンマ区切りのCSV形式で、動画のタイトルや、アドレスがはいっています。この部分を変更して、自分のプレイリストを作成します。
5.リストページの公開アドレスをプレイページに書き込む
- CSVデータ+プログラムページのリンクボタンを押します
- リンクコピーボタンで公開アドレスをコピーします
- 右上の「X」ボタンで閉じます
- 「play」ページを押して開きます
- プログラムのまんなかにある、TARGET_FILE (ターゲットファイル) の、httpsからhtmlまでを、コピーした自分のアドレスに差し替えます
- 公開ボタンで保存します
6.再生テスト、デザイン設定
- レイアウト
- テーマ
- 設定
- プレイページの「目」ボタンを押して、自分の作成したプレイリストが再生されるか確認してください
- 左側の「レイアウト」で、表示するものを設定できます。
- 「鉛筆」ボタンを押して、「このウィジットを表示する」を、全部オフにすれば、画面がシンプルになります
- 「テーマ」で、デザインテーマを変更できます。
- 「カスタマイズ」ボタンで、背景画像を変更したり、削除したりできます
- コメント欄は「設定」画面で設定します。
- コメントの設定を「埋め込み」から「非表示」に変更すれば、コメントが表示されなくなります
- play ページの「目」ボタンを押して表示を確認しましょう





