1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Youtube動画を広告を出さずに連続再生するだけのサイトの作り方

1
Last updated at Posted at 2026-06-26

Youtubeにはプレイリスト機能があり、選んだ動画を順番にループ再生することができますが、無料だと途中に広告がはいってしまいます。そこで、指定した動画だけを順番にループ再生させるだけの JavaScriptのサイトをGemini(無料)に作ってもらいました。70回くらいだめだしした結果、わりと使いやすいものができたので公開します。スマホだけでも作れるみたいです

aaa.gif

1.ループ再生する仕組み

  • Java Script プログラムでYoutubeを順番に読み込んで再生します
  • サイトは、Google Blogger を使い、再生と再生リストの2ページを設置します。無料で簡単
  • 再生動画のタイトルなどのテロップが流れます

2.Google Bloggerをひらく

スマホでも、こちら(Blogger)からログインすればできそうです

bbb.gif

  1. Google検索の右上のツールボタンから、ブロガー(Blogger)を選びます
  2. ブロガーはツールの下のほうにあります。右側のスクロールバーで下のほうを探してみてください。スマホの場合は、こちら(Blogger)からログイン
  3. ブログを作成ボタンをタッチします
  4. ブログの名前を入力します。好きな名前をつけてかまいません
  5. 「次へ」ボタンを押します。
  6. 公開するブログアドレスを入力します。ほかの人が使っている名前はつかえないので、その場合は別の名前をさがしましょう
  7. 「次へ」ボタンを押します。
  8. ブログの名前をもう一度入力して、
  9. 完了ボタンを押します。ブロガーの編集画面がひらきます

3.再生用ページをつくる

  • 最初につけたタイトルがサイトのアドレスになるので、まず簡単な名前をつけていったん公開するのがよいみたいです。
  • HTMLビューにして、プログラムをコピペします
  • タイトルも表示されるので、不要なら後から削除してもかまいません

ccc.gif

  1. 左側のページをタッチします。
    左側の項目が見えない場合は、左上の三本線ボタンを押せば、表示されます
  2. 新しいページをタッチして、ページを追加します
  3. 最初につけたタイトルがアドレスになるので、「play」のような簡単な英語タイトルをつけます
  4. 「公開」ボタンを押します
  5. タイトルをつけたページが追加されるので、タッチしてもう一度ひらきます
  6. 「鉛筆」ボタンをおして、HTMLビューにします
  7. 必ずHTMLビューにしてください。「鉛筆」ボタン(作成ビュー)だと動きません
  8. 下のプレーヤープログラムを全部選択して、この中にコピペします。コントロールC、コントロールV、または、長押しメニューでコピペしましょう
  9. 「公開」ボタンで保存します
  10. 「目」ボタンで作成したページを表示してみてください。プログラムの中に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形式で、プレイリストをいれます

ddd.gif

  1. 「ページ」を選びます
  2. 「新しいページ」を追加します
  3. タイトルを「CSVデータ+プログラム」という名前にしましょう
  4. HTMLビューであることを確認してください
  5. 下の「CSVデータ+プログラム」を、この中にコピペします
  6. 公開ボタンで保存します

◆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.リストページの公開アドレスをプレイページに書き込む

eee.gif

  1. CSVデータ+プログラムページのリンクボタンを押します
  2. リンクコピーボタンで公開アドレスをコピーします
  3. 右上の「X」ボタンで閉じます
  4. 「play」ページを押して開きます
  5. プログラムのまんなかにある、TARGET_FILE (ターゲットファイル) の、httpsからhtmlまでを、コピーした自分のアドレスに差し替えます
  6. 公開ボタンで保存します

6.再生テスト、デザイン設定

  • レイアウト
  • テーマ
  • 設定

fff.gif

  1. プレイページの「目」ボタンを押して、自分の作成したプレイリストが再生されるか確認してください
  2. 左側の「レイアウト」で、表示するものを設定できます。
  3. 「鉛筆」ボタンを押して、「このウィジットを表示する」を、全部オフにすれば、画面がシンプルになります
  4. 「テーマ」で、デザインテーマを変更できます。
  5. 「カスタマイズ」ボタンで、背景画像を変更したり、削除したりできます
  6. コメント欄は「設定」画面で設定します。
  7. コメントの設定を「埋め込み」から「非表示」に変更すれば、コメントが表示されなくなります
  8. play ページの「目」ボタンを押して表示を確認しましょう
  • この記事の説明用のGIFは、9VAeきゅうべえで作成しました。
  • Xiaomi Pad(Android)に、キーボードをつけて作成しました。動画も同時に作成。記事の文章と、動画のせりふが共通なので簡単
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?