<div id="ytp-widget-a1b2c3" class="ytp-widget-a1b2c3">
<style>
#ytp-widget-a1b2c3 {
--ytp-bg: #0f1115;
--ytp-panel: #171a21;
--ytp-text: #f3f5f7;
--ytp-sub: #aab4c0;
--ytp-border: #2a3140;
--ytp-btn: #202734;
--ytp-btn-hover: #2a3344;
--ytp-radius: 14px;
--ytp-gap: 10px;
--ytp-font: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-family: var(--ytp-font);
color: var(--ytp-text);
background: var(--ytp-bg);
border: 1px solid var(--ytp-border);
border-radius: var(--ytp-radius);
padding: 14px;
box-sizing: border-box;
width: 100%;
max-width: 960px;
}
#ytp-widget-a1b2c3 *,
#ytp-widget-a1b2c3 *::before,
#ytp-widget-a1b2c3 *::after {
box-sizing: border-box;
}
#ytp-widget-a1b2c3 .ytp-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 10px;
flex-wrap: wrap;
}
#ytp-widget-a1b2c3 .ytp-title {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.02em;
margin: 0;
}
#ytp-widget-a1b2c3 .ytp-badge {
font-size: 12px;
color: var(--ytp-sub);
border: 1px solid var(--ytp-border);
border-radius: 999px;
padding: 4px 10px;
background: rgba(255,255,255,0.02);
}
#ytp-widget-a1b2c3 .ytp-player-wrap {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background: #000;
border-radius: 12px;
overflow: hidden;
}
#ytp-widget-a1b2c3 .ytp-player {
width: 100%;
height: 100%;
}
#ytp-widget-a1b2c3 .ytp-controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 12px;
}
#ytp-widget-a1b2c3 .ytp-btn {
appearance: none;
border: 1px solid var(--ytp-border);
background: var(--ytp-btn);
color: var(--ytp-text);
padding: 10px 14px;
border-radius: 10px;
cursor: pointer;
font: inherit;
line-height: 1;
transition: background 0.2s ease, transform 0.08s ease;
}
#ytp-widget-a1b2c3 .ytp-btn:hover {
background: var(--ytp-btn-hover);
}
#ytp-widget-a1b2c3 .ytp-btn:active {
transform: translateY(1px);
}
#ytp-widget-a1b2c3 .ytp-meta {
margin-top: 12px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
#ytp-widget-a1b2c3 .ytp-card {
background: var(--ytp-panel);
border: 1px solid var(--ytp-border);
border-radius: 10px;
padding: 10px 12px;
min-width: 0;
}
#ytp-widget-a1b2c3 .ytp-label {
display: block;
font-size: 11px;
color: var(--ytp-sub);
margin-bottom: 4px;
}
#ytp-widget-a1b2c3 .ytp-value {
display: block;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#ytp-widget-a1b2c3 .ytp-debug {
margin-top: 10px;
font-size: 11px;
color: var(--ytp-sub);
white-space: normal;
word-break: break-word;
}
@media (max-width: 640px) {
#ytp-widget-a1b2c3 .ytp-meta {
grid-template-columns: 1fr;
}
#ytp-widget-a1b2c3 {
padding: 12px;
}
}
</style>
<div class="ytp-head">
<h2 class="ytp-title">Playlist Player</h2>
<div class="ytp-badge">GA4 direct</div>
</div>
<div class="ytp-player-wrap">
<div id="ytp-player-a1b2c3" class="ytp-player"></div>
</div>
<div class="ytp-controls">
<button type="button" class="ytp-btn" data-act="play">再生</button>
<button type="button" class="ytp-btn" data-act="pause">停止</button>
<button type="button" class="ytp-btn" data-act="prev">前へ</button>
<button type="button" class="ytp-btn" data-act="next">次へ</button>
</div>
<div class="ytp-meta">
<div class="ytp-card">
<span class="ytp-label">状態</span>
<span class="ytp-value" data-role="state">未初期化</span>
</div>
<div class="ytp-card">
<span class="ytp-label">動画ID</span>
<span class="ytp-value" data-role="video-id">-</span>
</div>
<div class="ytp-card">
<span class="ytp-label">Playlist index</span>
<span class="ytp-value" data-role="playlist-index">-</span>
</div>
<div class="ytp-card">
<span class="ytp-label">再生秒数</span>
<span class="ytp-value" data-role="current-time">0</span>
</div>
</div>
<div class="ytp-debug" data-role="debug"></div>
<script>
(function () {
const ROOT_ID = 'ytp-widget-a1b2c3';
const PLAYER_ID = 'ytp-player-a1b2c3';
const root = document.getElementById(ROOT_ID);
if (!root) return;
const CONFIG = {
gaMeasurementId: 'G-VYN4SZK0E6', // 例: G-XXXXXXXXXX
playlistId: 'PLKWIg8_Q-4Urj1UYEUudqpU7ySBROEVAK', // 例: PLxxxxxxxxxxxxxxxx
widgetName: 'yt_playlist_widget',
progressIntervalSec: 10
};
const els = {
state: root.querySelector('[data-role="state"]'),
videoId: root.querySelector('[data-role="video-id"]'),
playlistIndex: root.querySelector('[data-role="playlist-index"]'),
currentTime: root.querySelector('[data-role="current-time"]'),
debug: root.querySelector('[data-role="debug"]'),
buttons: root.querySelectorAll('.ytp-btn')
};
let player = null;
let progressTimer = null;
let lastProgressBucket = -1;
let lastVideoId = '';
let lastState = null;
function debug(msg, extra) {
const payload = Object.assign({ msg: msg }, extra || {});
els.debug.textContent = JSON.stringify(payload);
}
function ensureGA4(callback) {
if (window.gtag && window.__ytpGaReadyA1B2C3) {
callback();
return;
}
if (!document.getElementById('ga4-script-a1b2c3')) {
const s = document.createElement('script');
s.id = 'ga4-script-a1b2c3';
s.async = true;
s.src = 'https://www.googletagmanager.com/gtag/js?id=' + encodeURIComponent(CONFIG.gaMeasurementId);
document.head.appendChild(s);
}
window.dataLayer = window.dataLayer || [];
window.gtag = window.gtag || function(){ window.dataLayer.push(arguments); };
if (!window.__ytpGaReadyA1B2C3) {
window.gtag('js', new Date());
window.gtag('config', CONFIG.gaMeasurementId, {
send_page_view: true
});
window.__ytpGaReadyA1B2C3 = true;
}
callback();
}
function track(eventName, params) {
ensureGA4(function () {
window.gtag('event', eventName, Object.assign({
widget_name: CONFIG.widgetName,
playlist_id: CONFIG.playlistId
}, params || {}));
});
}
function stateLabel(state) {
if (!window.YT || !window.YT.PlayerState) return '不明';
switch (state) {
case YT.PlayerState.UNSTARTED: return '未開始';
case YT.PlayerState.ENDED: return '終了';
case YT.PlayerState.PLAYING: return '再生中';
case YT.PlayerState.PAUSED: return '停止中';
case YT.PlayerState.BUFFERING: return '読込中';
case YT.PlayerState.CUED: return '準備完了';
default: return '不明';
}
}
function getVideoId() {
try {
return player && player.getVideoData ? (player.getVideoData().video_id || '') : '';
} catch (e) {
return '';
}
}
function getPlaylistIndex() {
try {
const idx = player && player.getPlaylistIndex ? player.getPlaylistIndex() : null;
return idx === null || idx === undefined ? '' : String(idx);
} catch (e) {
return '';
}
}
function getCurrentTime() {
try {
return Math.floor(player && player.getCurrentTime ? player.getCurrentTime() : 0);
} catch (e) {
return 0;
}
}
function updateUI() {
els.videoId.textContent = getVideoId() || '-';
els.playlistIndex.textContent = getPlaylistIndex() || '-';
els.currentTime.textContent = String(getCurrentTime());
}
function stopProgressTracking() {
if (progressTimer) {
clearInterval(progressTimer);
progressTimer = null;
}
}
function startProgressTracking() {
stopProgressTracking();
progressTimer = setInterval(function () {
if (!player || !window.YT) return;
if (player.getPlayerState() !== YT.PlayerState.PLAYING) return;
const sec = getCurrentTime();
const bucket = Math.floor(sec / CONFIG.progressIntervalSec);
updateUI();
if (bucket !== lastProgressBucket) {
lastProgressBucket = bucket;
track('yt_playlist_progress', {
video_id: getVideoId(),
playlist_index: getPlaylistIndex(),
progress_seconds: sec
});
}
}, 1000);
}
function bindButtons() {
els.buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
if (!player) return;
const act = btn.getAttribute('data-act');
if (act === 'play') player.playVideo();
if (act === 'pause') player.pauseVideo();
if (act === 'prev') player.previousVideo();
if (act === 'next') player.nextVideo();
track('yt_playlist_control_click', {
control_name: act,
video_id: getVideoId(),
playlist_index: getPlaylistIndex(),
current_time_seconds: getCurrentTime()
});
});
});
}
function onPlayerReady() {
updateUI();
bindButtons();
track('yt_playlist_ready', {
video_id: getVideoId(),
playlist_index: getPlaylistIndex()
});
debug('player_ready', {
playlistId: CONFIG.playlistId
});
}
function onPlayerStateChange(event) {
const state = event.data;
const videoId = getVideoId();
els.state.textContent = stateLabel(state);
updateUI();
if (videoId && videoId !== lastVideoId) {
lastVideoId = videoId;
lastProgressBucket = -1;
track('yt_playlist_video_change', {
video_id: videoId,
playlist_index: getPlaylistIndex()
});
}
if (state !== lastState) {
track('yt_playlist_state_change', {
video_id: videoId,
playlist_index: getPlaylistIndex(),
player_state_code: state,
player_state_label: stateLabel(state),
current_time_seconds: getCurrentTime()
});
lastState = state;
}
if (window.YT && state === YT.PlayerState.PLAYING) {
startProgressTracking();
} else {
stopProgressTracking();
}
if (window.YT && state === YT.PlayerState.ENDED) {
track('yt_playlist_video_complete', {
video_id: videoId,
playlist_index: getPlaylistIndex(),
watched_seconds: getCurrentTime()
});
}
}
function onPlayerError(event) {
track('yt_playlist_error', {
video_id: getVideoId(),
playlist_index: getPlaylistIndex(),
error_code: event.data
});
debug('player_error', { code: event.data });
}
function createPlayer() {
if (!window.YT || !window.YT.Player) return;
player = new YT.Player(PLAYER_ID, {
width: '100%',
height: '100%',
playerVars: {
listType: 'playlist',
list: CONFIG.playlistId,
autoplay: 0,
rel: 0,
modestbranding: 1,
playsinline: 1
},
events: {
onReady: onPlayerReady,
onStateChange: onPlayerStateChange,
onError: onPlayerError
}
});
}
function loadYouTubeAPI() {
if (window.YT && window.YT.Player) {
createPlayer();
return;
}
const previous = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = function () {
if (typeof previous === 'function') previous();
createPlayer();
};
if (!document.getElementById('yt-iframe-api-a1b2c3')) {
const tag = document.createElement('script');
tag.id = 'yt-iframe-api-a1b2c3';
tag.src = 'https://www.youtube.com/iframe_api';
document.head.appendChild(tag);
}
}
ensureGA4(function () {
track('yt_playlist_widget_view', {
component_id: ROOT_ID
});
});
loadYouTubeAPI();
})();
</script>
</div>
情報にとって
美とは何か
