video-player-honban / index.html
soiz1's picture
Update index.html
a36d759 verified
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>動画連続再生プレイヤー</title>
<style>
:root {
--primary-color: #00a8ff;
--secondary-color: #0097e6;
--dark-color: #192a56;
--light-color: #dcdde1;
--accent-color: #00d2d3;
--error-color: #e84118;
}
body {
font-family: 'Arial', sans-serif;
background-color: #1e272e;
color: var(--light-color);
margin: 0;
padding: 20px;
overflow-x: hidden;
}
.container {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: var(--primary-color);
text-align: center;
text-shadow: 0 0 10px rgba(0, 168, 255, 0.5);
font-size: 2.5rem;
margin-bottom: 20px;
}
.main-content {
display: flex;
gap: 20px;
}
.video-tiles {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.video-tile {
background: linear-gradient(145deg, #2f3640, #1e272e);
border: 1px solid var(--primary-color);
border-radius: 8px;
padding: 15px;
box-shadow: 0 0 15px rgba(0, 168, 255, 0.2);
position: relative;
transition: all 0.3s ease;
}
.video-tile:hover {
transform: translateY(-5px);
box-shadow: 0 5px 20px rgba(0, 168, 255, 0.4);
}
.video-tile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.video-tile-title {
font-weight: bold;
color: var(--primary-color);
}
.video-tile-remove {
background-color: var(--error-color);
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.video-tile-remove:hover {
transform: scale(1.1);
}
.video-preview {
width: 100%;
height: 150px;
background-color: #000;
margin-bottom: 10px;
border: 1px solid var(--secondary-color);
}
.video-preview video {
width: 100%;
height: 100%;
object-fit: contain;
}
.control-group {
margin-bottom: 10px;
}
.control-group label {
display: block;
margin-bottom: 5px;
color: var(--accent-color);
font-size: 0.9rem;
}
.control-group input[type="number"],
.control-group input[type="range"],
.control-group select {
width: 100%;
padding: 5px;
background-color: #2f3640;
border: 1px solid var(--primary-color);
border-radius: 4px;
color: white;
}
.control-group input[type="number"] {
width: calc(100% - 12px);
}
.add-video-btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 50%;
width: 50px;
height: 50px;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin: 20px auto;
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(0, 168, 255, 0.3);
}
.add-video-btn:hover {
background-color: var(--secondary-color);
transform: scale(1.1);
}
.global-controls {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 20px;
}
.global-btn {
padding: 10px 15px;
background-color: var(--dark-color);
color: white;
border: 1px solid var(--primary-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}
.global-btn:hover {
background-color: var(--primary-color);
}
.loop-checkbox {
display: flex;
align-items: center;
gap: 10px;
}
.loop-checkbox input {
width: auto;
}
/* フルスクリーン動画表示 */
.fullscreen-video {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
z-index: 1000;
display: none;
}
.fullscreen-video video {
width: 100%;
height: 100%;
object-fit: contain;
}
.close-fullscreen {
position: absolute;
top: 20px;
right: 20px;
background-color: var(--error-color);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 1.2rem;
cursor: pointer;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
}
.video-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 15px;
background-color: rgba(30, 39, 46, 0.8);
padding: 10px 20px;
border-radius: 8px;
z-index: 1001;
}
.control-btn {
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
transition: all 0.2s ease;
}
.control-btn:hover {
color: var(--primary-color);
}
.volume-control, .speed-control {
display: flex;
align-items: center;
gap: 5px;
}
.volume-control input, .speed-control input {
width: 80px;
}
/* ドラッグ可能なタイルのスタイル */
.video-tile.dragging {
opacity: 0.5;
border: 2px dashed var(--accent-color);
}
/* コンテキストメニュー */
.context-menu {
position: absolute;
background-color: #2f3640;
border: 1px solid var(--primary-color);
border-radius: 4px;
padding: 5px 0;
z-index: 1002;
display: none;
}
.context-menu-item {
padding: 8px 15px;
cursor: pointer;
}
.context-menu-item:hover {
background-color: var(--dark-color);
color: var(--primary-color);
}
/* サイバー風の装飾 */
.cyber-decoration {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
opacity: 0.1;
background:
linear-gradient(90deg, transparent 49%, var(--primary-color) 49%, var(--primary-color) 51%, transparent 51%),
linear-gradient(transparent 49%, var(--primary-color) 49%, var(--primary-color) 51%, transparent 51%);
background-size: 50px 50px;
}
/* プログレスバー */
.progress-container {
position: absolute;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 5px;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 2px;
z-index: 1001;
cursor: pointer;
}
.progress-bar {
height: 100%;
background-color: var(--primary-color);
border-radius: 2px;
width: 0%;
}
.thumbnails {
position: absolute;
bottom: 90px;
left: 0;
background-color: rgba(30, 39, 46, 0.9);
border: 1px solid var(--primary-color);
border-radius: 4px;
padding: 5px;
display: none;
z-index: 1002;
}
.thumbnails img {
width: 160px;
height: 90px;
display: block;
}
/* 動画選択オプション */
.video-options-container {
background: linear-gradient(145deg, #2f3640, #1e272e);
border: 1px solid var(--primary-color);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.video-type-selector {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-bottom: 20px;
justify-content: center;
}
.video-type-selector div {
display: flex;
align-items: center;
gap: 5px;
}
.video-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.video-options div {
display: flex;
align-items: center;
gap: 5px;
}
.invalid-selection {
color: var(--error-color);
margin-top: 10px;
display: none;
}
.modal {
display: none;
position: fixed;
z-index: 1002;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.8);
}
.modal-content {
background-color: #2f3640;
margin: 5% auto;
padding: 25px;
border: 2px solid var(--primary-color);
width: 90%;
max-width: 500px;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 168, 255, 0.5);
}
.close-modal {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.change-video-btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
margin-right: 5px;
cursor: pointer;
}
.modal .invalid-selection {
color: var(--error-color);
margin: 10px 0;
padding: 5px;
background-color: rgba(232, 65, 24, 0.2);
border-radius: 4px;
text-align: center;
}
/* 新しいPiPボタンスタイル */
.pip-btn {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
padding: 10px 15px;
cursor: pointer;
transition: all 0.2s ease;
margin-left: 10px;
}
.pip-btn:hover {
background-color: var(--secondary-color);
}
/* 再生/一時停止ボタン */
.play-pause-btn {
font-size: 1.8rem;
background: none;
border: none;
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.play-pause-btn:hover {
color: var(--primary-color);
}
/* フルスクリーンコントロールの改善 */
.fullscreen-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 15px;
background-color: rgba(30, 39, 46, 0.8);
padding: 15px 25px;
border-radius: 8px;
z-index: 1001;
width: 80%;
max-width: 800px;
justify-content: center;
}
.fullscreen-controls-left, .fullscreen-controls-center, .fullscreen-controls-right {
display: flex;
align-items: center;
gap: 15px;
}
.fullscreen-controls-center {
flex: 1;
justify-content: center;
}
.time-display {
font-size: 0.9rem;
color: white;
min-width: 80px;
text-align: center;
}
#saveVideoSettings {
background-color: var(--primary-color);
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
display: block;
margin: 20px auto 0;
font-size: 1rem;
transition: all 0.2s ease;
}
#saveVideoSettings:hover {
background-color: var(--secondary-color);
}
.modal h3 {
color: var(--primary-color);
margin-top: 0;
text-align: center;
font-size: 1.5rem;
}
/* チェックボックスと順序変更ボタンのスタイルを追加 */
.video-tile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
gap: 10px;
}
.video-tile-title {
font-weight: bold;
color: var(--primary-color);
flex-grow: 1;
}
.video-tile-checkbox {
margin-right: 10px;
}
.video-order-controls {
display: flex;
gap: 5px;
}
.order-btn {
background-color: var(--dark-color);
color: white;
border: 1px solid var(--primary-color);
border-radius: 4px;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.order-btn:hover {
background-color: var(--primary-color);
}
/* カスタム動画アップロードのスタイル */
#customVideoUpload {
margin-top: 10px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
#customVideoFile {
margin-top: 5px;
width: 100%;
}
</style>
</head>
<body>
<div class="cyber-decoration"></div>
<div class="container">
<h1>動画連続再生プレイヤー</h1>
<!-- モーダルダイアログ -->
<div class="modal" id="videoModal">
<div class="modal-content">
<span class="close-modal">&times;</span>
<h3>動画設定</h3>
<!-- 動画タイプセレクターにカスタムオプションを追加 -->
<div class="video-type-selector">
<div>
<input type="radio" id="gangnamStyle" name="videoType" value="k" checked>
<label for="gangnamStyle">カンナムスタイル</label>
</div>
<div>
<input type="radio" id="drillStyle" name="videoType" value="m">
<label for="drillStyle">槇原ドリル</label>
</div>
<div>
<input type="radio" id="yellStyle" name="videoType" value="e">
<label for="yellStyle">エール</label>
</div>
<div>
<input type="radio" id="customStyle" name="videoType" value="c">
<label for="customStyle">カスタム動画</label>
</div>
</div>
<!-- カスタム動画ファイルアップロード用の要素を追加 -->
<div id="customVideoUpload" style="display: none;">
<label for="customVideoFile">カスタム動画ファイル:</label>
<input type="file" id="customVideoFile" accept="video/*">
</div>
<div class="video-options">
<div>
<input type="checkbox" id="modalBackOption" name="modalBackOption">
<label for="modalBackOption">白背景(背景削除)</label>
</div>
<div>
<input type="checkbox" id="modalStickOption" name="modalStickOption">
<label for="modalStickOption">棒人間</label>
</div>
<div>
<input type="checkbox" id="modalHumanOption" name="modalHumanOption" checked>
<label for="modalHumanOption">人間</label>
</div>
<div>
<input type="checkbox" id="modalFlipMode" name="modalFlipMode">
<label for="modalFlipMode">左右反転</label>
</div>
</div>
<button id="saveVideoSettings">保存</button>
</div>
</div>
<!-- グローバル設定 -->
<div class="video-options-container">
<h3>グローバル設定 (新規追加時に適用)</h3>
<!-- 動画タイプセレクターにカスタムオプションを追加 -->
<div class="video-type-selector">
<div>
<input type="radio" id="gangnamStyle" name="videoType" value="k" checked>
<label for="gangnamStyle">カンナムスタイル</label>
</div>
<div>
<input type="radio" id="drillStyle" name="videoType" value="m">
<label for="drillStyle">槇原ドリル</label>
</div>
<div>
<input type="radio" id="yellStyle" name="videoType" value="e">
<label for="yellStyle">エール</label>
</div>
<div>
<input type="radio" id="customStyle" name="videoType" value="c">
<label for="customStyle">カスタム動画</label>
</div>
</div>
<!-- カスタム動画ファイルアップロード用の要素を追加 -->
<div id="customVideoUpload" style="display: none;">
<label for="customVideoFile">カスタム動画ファイル:</label>
<input type="file" id="customVideoFile" accept="video/*">
</div>
<div class="video-options">
<div>
<input type="checkbox" id="backOption" name="backOption">
<label for="backOption">白背景(背景削除))</label>
</div>
<div>
<input type="checkbox" id="stickOption" name="stickOption">
<label for="stickOption">棒人間</label>
</div>
<div>
<input type="checkbox" id="humanOption" name="humanOption" checked>
<label for="humanOption">人間</label>
</div>
<div>
<input type="checkbox" id="flipMode" name="flipMode">
<label for="flipMode">左右反転</label>
</div>
</div>
</div>
<div class="main-content">
<div class="video-tiles" id="videoTiles">
<!-- 動画タイルがここに追加されます -->
</div>
</div>
<button class="add-video-btn" id="addVideoBtn">+</button>
<div class="global-controls">
<button class="global-btn" id="startBtn">再生開始</button>
<button class="global-btn" id="muteBtn">消音</button>
<button class="global-btn" id="hideVideoBtn">消画</button>
<button class="pip-btn" id="pipBtn">PiPモード</button>
<div class="loop-checkbox">
<input type="checkbox" id="loopCheckbox">
<label for="loopCheckbox">ループ再生</label>
</div>
</div>
</div>
<!-- フルスクリーン動画表示 -->
<div class="fullscreen-video" id="fullscreenVideo">
<video id="mainVideo"></video>
<button class="close-fullscreen" id="closeFullscreen">×</button>
<div class="progress-container" id="progressContainer">
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="thumbnails" id="thumbnails"></div>
<!-- 改善されたコントロールパネル -->
<div class="fullscreen-controls">
<div class="fullscreen-controls-left">
<button class="play-pause-btn" id="playPauseBtn"></button>
<span class="time-display" id="currentTime">0:00</span>
<span>/</span>
<span class="time-display" id="durationTime">0:00</span>
</div>
<div class="fullscreen-controls-center">
<button class="control-btn" id="prevBtn"></button>
<button class="control-btn" id="restartBtn"></button>
<button class="control-btn" id="nextBtn"></button>
</div>
<div class="fullscreen-controls-right">
<div class="volume-control">
<span>🔊</span>
<input type="range" id="volumeSlider" min="0" max="1" step="0.01" value="1">
</div>
<div class="speed-control">
<span>⏱️</span>
<input type="range" id="speedSlider" min="0.5" max="2" step="0.1" value="1">
<input type="number" id="speedInput" min="0.5" max="2" step="0.1" value="1" style="width: 50px;">
</div>
</div>
</div>
</div>
<!-- サムネイル取得用の非表示動画要素 -->
<video id="thumbnailVideo" style="display: none;"></video>
<script>
// 許可された動画リスト
const allowedVideos = [
'/videos/k-back-human.mp4',
'/videos/k-back-stick-human.mp4',
'/videos/k-back-stick.mp4',
'/videos/k-human.mp4',
'/videos/k-stick-human.mp4',
'/videos/m-back-human.mp4',
'/videos/m-back-stick-human.mp4',
'/videos/m-back-stick.mp4',
'/videos/m-human.mp4',
'/videos/m-stick-human.mp4',
'/videos/e-back-human.mp4',
'/videos/e-back-stick-human.mp4',
'/videos/e-back-stick.mp4',
'/videos/e-human.mp4',
'/videos/e-stick-human.mp4'
];
// グローバル変数
let videoTiles = [];
let currentVideoIndex = 0;
let isPlayingSequence = false;
let isLoopEnabled = false;
let isMuted = false;
let isVideoHidden = false;
let currentEditingTileId = null;
let isPlaying = false;
let customVideos = {};
// DOM要素
const videoTilesContainer = document.getElementById('videoTiles');
const addVideoBtn = document.getElementById('addVideoBtn');
const fullscreenVideo = document.getElementById('fullscreenVideo');
const mainVideo = document.getElementById('mainVideo');
const closeFullscreen = document.getElementById('closeFullscreen');
const startBtn = document.getElementById('startBtn');
const muteBtn = document.getElementById('muteBtn');
const hideVideoBtn = document.getElementById('hideVideoBtn');
const pipBtn = document.getElementById('pipBtn');
const loopCheckbox = document.getElementById('loopCheckbox');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const restartBtn = document.getElementById('restartBtn');
const playPauseBtn = document.getElementById('playPauseBtn');
const volumeSlider = document.getElementById('volumeSlider');
const speedSlider = document.getElementById('speedSlider');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const thumbnails = document.getElementById('thumbnails');
const currentTimeDisplay = document.getElementById('currentTime');
const durationTimeDisplay = document.getElementById('durationTime');
const videoModal = document.getElementById('videoModal');
const modalGangnamStyle = document.getElementById('modalGangnamStyle');
const modalDrillStyle = document.getElementById('modalDrillStyle');
const modalYellStyle = document.getElementById('modalYellStyle');
const modalBackOption = document.getElementById('modalBackOption');
const modalStickOption = document.getElementById('modalStickOption');
const modalHumanOption = document.getElementById('modalHumanOption');
const modalFlipMode = document.getElementById('modalFlipMode');
const saveVideoSettingsBtn = document.getElementById('saveVideoSettings');
const customVideoUpload = document.getElementById('customVideoUpload');
const customVideoFile = document.getElementById('customVideoFile');
const gangnamStyle = document.getElementById('gangnamStyle');
const drillStyle = document.getElementById('drillStyle');
const yellStyle = document.getElementById('yellStyle');
const customStyle = document.getElementById('customStyle');
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
updateViaCache: 'none' // キャッシュ無視して最新を取る
});
console.log('✅ Service Worker registered');
// Service Worker がページ制御を開始するまで待つ(重要!)
await navigator.serviceWorker.ready;
console.log('✅ Service Worker is active and controlling the page');
// 更新を強制的にチェック(でも失敗しても静かにスキップ)
registration.update().catch(err => {
alert('ServiceWorker update check failed:', err);
});
// 新しい SW が見つかったら、状態変化を監視
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
console.log('🔁 New Service Worker installed, reloading...');
window.location.reload(); // ユーザーがオフラインなら必要
}
});
}
});
} catch (error) {
console.error('❌ ServiceWorker registration failed:', error);
}
});
}
// 初期化
document.addEventListener('DOMContentLoaded', () => {
loadCustomVideos();
loadVideoTiles();
// モーダルイベント
document.querySelector('.close-modal').addEventListener('click', () => {
videoModal.style.display = 'none';
});
saveVideoSettingsBtn.addEventListener('click', saveVideoSettings);
// 動画タイプ変更時のイベント
gangnamStyle.addEventListener('change', updateVideoType);
drillStyle.addEventListener('change', updateVideoType);
yellStyle.addEventListener('change', updateVideoType);
customStyle.addEventListener('change', updateVideoType);
// 動画変更ボタンクリック
document.addEventListener('click', (e) => {
if (e.target.classList.contains('change-video-btn')) {
currentEditingTileId = e.target.dataset.tileId;
openVideoModal(currentEditingTileId);
}
if (e.target.classList.contains('video-tile-remove')) {
removeVideoTile(e.target.dataset.tileId);
}
if (e.target.classList.contains('order-up-btn')) {
moveTileUp(e.target.dataset.tileId);
}
if (e.target.classList.contains('order-down-btn')) {
moveTileDown(e.target.dataset.tileId);
}
});
// スライダーと入力ボックスの同期
speedSlider.addEventListener('input', function() {
speedInput.value = this.value;
updatePlaybackSpeed();
});
speedInput.addEventListener('change', function() {
speedSlider.value = this.value;
updatePlaybackSpeed();
});
// グローバルコントロール
addVideoBtn.addEventListener('click', addVideoTile);
startBtn.addEventListener('click', startVideoSequence);
muteBtn.addEventListener('click', toggleMute);
hideVideoBtn.addEventListener('click', toggleVideoHide);
pipBtn.addEventListener('click', togglePipMode);
closeFullscreen.addEventListener('click', closeFullscreenVideo);
loopCheckbox.addEventListener('change', () => {
isLoopEnabled = loopCheckbox.checked;
saveVideoTiles();
});
// フルスクリーンコントロール
prevBtn.addEventListener('click', playPreviousVideo);
nextBtn.addEventListener('click', playNextVideo);
restartBtn.addEventListener('click', restartCurrentVideo);
playPauseBtn.addEventListener('click', togglePlayPause);
volumeSlider.addEventListener('input', updateVolume);
speedSlider.addEventListener('input', updatePlaybackSpeed);
// プログレスバー
progressContainer.addEventListener('mousemove', showThumbnail);
progressContainer.addEventListener('mouseout', hideThumbnail);
progressContainer.addEventListener('click', seekVideo);
// プログレスバー更新開始
mainVideo.addEventListener('play', () => {
isPlaying = true;
playPauseBtn.textContent = '⏸';
updateProgressBar();
updateTimeDisplay();
});
mainVideo.addEventListener('pause', () => {
isPlaying = false;
playPauseBtn.textContent = '▶';
});
mainVideo.addEventListener('timeupdate', updateTimeDisplay);
mainVideo.addEventListener('loadedmetadata', updateDurationDisplay);
// カスタム動画ファイルアップロード
customVideoFile.addEventListener('change', handleCustomVideoUpload);
});
// 動画タイプが変更された時の処理
function updateVideoType() {
if (customStyle.checked) {
customVideoUpload.style.display = 'block';
} else {
customVideoUpload.style.display = 'none';
}
}
// カスタム動画をアップロード
function handleCustomVideoUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(event) {
const videoId = 'custom-' + Date.now();
customVideos[videoId] = {
name: file.name,
data: event.target.result
};
saveCustomVideos();
};
reader.readAsDataURL(file);
}
// カスタム動画を保存
function saveCustomVideos() {
localStorage.setItem('customVideos', JSON.stringify(customVideos));
}
// カスタム動画を読み込み
function loadCustomVideos() {
const savedVideos = localStorage.getItem('customVideos');
if (savedVideos) {
customVideos = JSON.parse(savedVideos);
}
}
// 動画タイルを追加
function addVideoTile() {
const tileId = Date.now();
const tile = document.createElement('div');
tile.className = 'video-tile';
tile.id = `tile-${tileId}`;
tile.draggable = true;
// グローバル設定を取得
const videoType = document.querySelector('input[name="videoType"]:checked').value;
const back = document.getElementById('backOption').checked;
const stick = document.getElementById('stickOption').checked;
const human = document.getElementById('humanOption').checked;
const flip = document.getElementById('flipMode').checked;
tile.innerHTML = `
<div class="video-tile-header">
<input type="checkbox" class="video-tile-checkbox" id="enable-${tileId}" checked>
<span class="video-tile-title">動画 ${videoTiles.length + 1}</span>
<div class="video-order-controls">
<button class="order-btn order-up-btn" data-tile-id="${tileId}">◁</button>
<button class="order-btn order-down-btn" data-tile-id="${tileId}">▷</button>
</div>
<div>
<button class="change-video-btn" data-tile-id="${tileId}">動画を変更</button>
<button class="video-tile-remove" data-tile-id="${tileId}">×</button>
</div>
</div>
<div class="video-preview">
<video id="preview-${tileId}" muted></video>
</div>
<div class="control-group">
<label for="startTime-${tileId}">再生開始秒数</label>
<input type="number" id="startTime-${tileId}" value="0" min="0" step="0.1">
</div>
<div class="control-group">
<label for="endTime-${tileId}">再生停止秒数</label>
<input type="number" id="endTime-${tileId}" value="" min="0" step="0.1" placeholder="未設定で動画最後まで">
</div>
<div class="control-group">
<label for="fadeIn-${tileId}">フェードイン秒数</label>
<input type="number" id="fadeIn-${tileId}" value="0" min="0" step="0.1">
</div>
<div class="control-group">
<label for="fadeOut-${tileId}">フェードアウト秒数</label>
<input type="number" id="fadeOut-${tileId}" value="0" min="0" step="0.1">
</div>
<div class="control-group">
<label for="waitTime-${tileId}">次の動画までの待機秒数</label>
<input type="number" id="waitTime-${tileId}" value="1" min="0" step="0.1">
</div>
<div class="control-group">
<label for="playbackRate-${tileId}">再生速度</label>
<input type="number" id="playbackRate-${tileId}" value="1" min="0.1" max="4" step="0.1">
</div>
<div class="control-group">
<label for="volume-${tileId}">音量 (0-1)</label>
<input type="number" id="volume-${tileId}" value="1" min="0" max="1" step="0.1">
</div>
<input type="hidden" id="videoType-${tileId}" value="${videoType}">
<input type="hidden" id="backOption-${tileId}" value="${back}">
<input type="hidden" id="stickOption-${tileId}" value="${stick}">
<input type="hidden" id="humanOption-${tileId}" value="${human}">
<input type="hidden" id="flipMode-${tileId}" value="${flip}">
<input type="hidden" id="customVideoId-${tileId}" value="">
`;
videoTilesContainer.appendChild(tile);
videoTiles.push(tileId);
// プレビューの更新
updateVideoPreview(tileId);
// 入力変更時のプレビュー更新
const inputs = tile.querySelectorAll('input');
inputs.forEach(input => {
if (!input.type.includes('hidden')) {
input.addEventListener('change', () => {
updateVideoPreview(tileId);
saveVideoTiles();
});
input.addEventListener('input', () => updateVideoPreview(tileId));
}
});
// チェックボックスのイベント
document.getElementById(`enable-${tileId}`).addEventListener('change', saveVideoTiles);
saveVideoTiles();
return tileId;
}
// タイルを上に移動
function moveTileUp(tileId) {
const index = videoTiles.indexOf(parseInt(tileId));
if (index > 0) {
const temp = videoTiles[index];
videoTiles[index] = videoTiles[index - 1];
videoTiles[index - 1] = temp;
const tileElement = document.getElementById(`tile-${tileId}`);
const prevTileElement = document.getElementById(`tile-${videoTiles[index]}`);
tileElement.parentNode.insertBefore(tileElement, prevTileElement);
updateTileTitles();
saveVideoTiles();
}
}
// タイルを下に移動
function moveTileDown(tileId) {
const index = videoTiles.indexOf(parseInt(tileId));
if (index < videoTiles.length - 1) {
const temp = videoTiles[index];
videoTiles[index] = videoTiles[index + 1];
videoTiles[index + 1] = temp;
const tileElement = document.getElementById(`tile-${tileId}`);
const nextTileElement = document.getElementById(`tile-${videoTiles[index]}`);
nextTileElement.parentNode.insertBefore(nextTileElement, tileElement);
updateTileTitles();
saveVideoTiles();
}
}
// モーダルを開く
function openVideoModal(tileId) {
// 現在の設定をモーダルに反映
modalGangnamStyle.checked = document.getElementById(`videoType-${tileId}`).value === 'k';
modalDrillStyle.checked = document.getElementById(`videoType-${tileId}`).value === 'm';
modalYellStyle.checked = document.getElementById(`videoType-${tileId}`).value === 'e';
modalBackOption.checked = document.getElementById(`backOption-${tileId}`).value === 'true';
modalStickOption.checked = document.getElementById(`stickOption-${tileId}`).value === 'true';
modalHumanOption.checked = document.getElementById(`humanOption-${tileId}`).value === 'true';
modalFlipMode.checked = document.getElementById(`flipMode-${tileId}`).value === 'true';
videoModal.style.display = 'block';
}
// 動画設定を保存
function saveVideoSettings() {
if (!currentEditingTileId) return;
// モーダルの設定を取得
const videoType = document.querySelector('input[name="modalVideoType"]:checked').value;
const back = modalBackOption.checked;
const stick = modalStickOption.checked;
const human = modalHumanOption.checked;
document.getElementById(`videoType-${currentEditingTileId}`).value = videoType;
document.getElementById(`backOption-${currentEditingTileId}`).value = back;
document.getElementById(`stickOption-${currentEditingTileId}`).value = stick;
document.getElementById(`humanOption-${currentEditingTileId}`).value = human;
document.getElementById(`flipMode-${currentEditingTileId}`).value = modalFlipMode.checked;
updateVideoPreview(currentEditingTileId);
videoModal.style.display = 'none';
saveVideoTiles();
}
// 動画プレビューを更新
function updateVideoPreview(tileId) {
const videoUrl = getVideoUrl(tileId);
const previewVideo = document.getElementById(`preview-${tileId}`);
if (!previewVideo) return;
if (previewVideo.src !== videoUrl) {
previewVideo.src = videoUrl;
}
const startTime = parseFloat(document.getElementById(`startTime-${tileId}`).value) || 0;
const endTime = parseFloat(document.getElementById(`endTime-${tileId}`).value);
const playbackRate = parseFloat(document.getElementById(`playbackRate-${tileId}`).value) || 1;
const volume = parseFloat(document.getElementById(`volume-${tileId}`).value) || 1;
const flip = document.getElementById(`flipMode-${tileId}`).value === 'true';
previewVideo.currentTime = startTime;
previewVideo.playbackRate = playbackRate;
previewVideo.volume = volume;
previewVideo.style.transform = flip ? 'scaleX(-1)' : 'scaleX(1)';
previewVideo.controls = true;
if (endTime && !isNaN(endTime)) {
previewVideo.addEventListener('timeupdate', function onTimeUpdate() {
if (this.currentTime >= endTime) {
this.pause();
this.removeEventListener('timeupdate', onTimeUpdate);
}
});
}
previewVideo.play().catch(e => console.log("プレビュー自動再生がブロックされました:", e));
}
// 動画URLを取得
function getVideoUrl(tileId) {
const videoType = document.getElementById(`videoType-${tileId}`).value;
// カスタム動画の場合
if (videoType === 'c') {
const customVideoId = document.getElementById(`customVideoId-${tileId}`).value;
if (customVideoId && customVideos[customVideoId]) {
return customVideos[customVideoId].data;
}
return null;
}
// 通常の動画の場合
const back = document.getElementById(`backOption-${tileId}`).value === 'true';
const stick = document.getElementById(`stickOption-${tileId}`).value === 'true';
const human = document.getElementById(`humanOption-${tileId}`).value === 'true';
let parts = [videoType];
if (back) parts.push('back');
if (stick) parts.push('stick');
if (human) parts.push('human');
// 何も選択されていない場合は人間をデフォルトで追加
if (parts.length === 1) parts.push('human');
const fileName = parts.join('-') + '.mp4';
const videoUrl = `/videos/${fileName}`;
return allowedVideos.includes(videoUrl) ? videoUrl : null;
}
// 動画タイルを削除
function removeVideoTile(tileId) {
if (videoTiles.length <= 1) {
alert("少なくとも1つの動画タイルが必要です");
return;
}
const tileIndex = videoTiles.indexOf(parseInt(tileId));
if (tileIndex !== -1) {
videoTiles.splice(tileIndex, 1);
const tileElement = document.getElementById(`tile-${tileId}`);
tileElement.remove();
// タイル番号を更新
updateTileTitles();
saveVideoTiles();
}
}
// タイルのタイトルを更新
function updateTileTitles() {
const tiles = document.querySelectorAll('.video-tile');
tiles.forEach((tile, index) => {
const title = tile.querySelector('.video-tile-title');
if (title) {
title.textContent = `動画 ${index + 1}`;
}
});
}
// データを保存
function saveVideoTiles() {
const tilesData = videoTiles.map(tileId => {
return {
id: tileId,
videoType: document.getElementById(`videoType-${tileId}`).value,
backOption: document.getElementById(`backOption-${tileId}`).value,
stickOption: document.getElementById(`stickOption-${tileId}`).value,
humanOption: document.getElementById(`humanOption-${tileId}`).value,
flipMode: document.getElementById(`flipMode-${tileId}`).value,
startTime: document.getElementById(`startTime-${tileId}`).value,
endTime: document.getElementById(`endTime-${tileId}`).value,
fadeIn: document.getElementById(`fadeIn-${tileId}`).value,
fadeOut: document.getElementById(`fadeOut-${tileId}`).value,
waitTime: document.getElementById(`waitTime-${tileId}`).value,
playbackRate: document.getElementById(`playbackRate-${tileId}`).value,
volume: document.getElementById(`volume-${tileId}`).value,
enabled: document.getElementById(`enable-${tileId}`).checked,
customVideoId: document.getElementById(`customVideoId-${tileId}`).value
};
});
localStorage.setItem('videoPlayerData', JSON.stringify({
tiles: tilesData,
loop: isLoopEnabled,
customVideos: customVideos
}));
}
// データを読み込み
function loadVideoTiles() {
const defaultData = {"tiles":[{"id":1747349402295,"videoType":"k","backOption":"false","stickOption":"true","humanOption":"true","flipMode":"false","startTime":"45","endTime":"","fadeIn":"10","fadeOut":"0","waitTime":"1","playbackRate":"1","volume":"1","enabled":true},{"id":1747349402296,"videoType":"m","backOption":"true","stickOption":"true","humanOption":"true","flipMode":"false","startTime":"0","endTime":"","fadeIn":"0","fadeOut":"0","waitTime":"1","playbackRate":"1","volume":"1","enabled":true}],"loop":false};
const savedData = localStorage.getItem('videoPlayerData');
const data = savedData ? JSON.parse(savedData) : defaultData;
isLoopEnabled = data.loop;
loopCheckbox.checked = isLoopEnabled;
if (data.customVideos) {
customVideos = data.customVideos;
}
// 保存されたタイルを再作成
videoTilesContainer.innerHTML = '';
videoTiles = [];
data.tiles.forEach(tileData => {
const tileId = addVideoTile();
// 各設定を復元
document.getElementById(`startTime-${tileId}`).value = tileData.startTime || 0;
document.getElementById(`endTime-${tileId}`).value = tileData.endTime || '';
document.getElementById(`fadeIn-${tileId}`).value = tileData.fadeIn || 0;
document.getElementById(`fadeOut-${tileId}`).value = tileData.fadeOut || 0;
document.getElementById(`waitTime-${tileId}`).value = tileData.waitTime || 1;
document.getElementById(`playbackRate-${tileId}`).value = tileData.playbackRate || 1;
document.getElementById(`volume-${tileId}`).value = tileData.volume || 1;
document.getElementById(`enable-${tileId}`).checked = tileData.enabled !== false;
// 動画オプションを復元
document.getElementById(`videoType-${tileId}`).value = tileData.videoType || 'k';
document.getElementById(`backOption-${tileId}`).value = tileData.backOption || 'false';
document.getElementById(`stickOption-${tileId}`).value = tileData.stickOption || 'false';
document.getElementById(`humanOption-${tileId}`).value = tileData.humanOption || 'true';
document.getElementById(`flipMode-${tileId}`).value = tileData.flipMode || 'false';
// カスタム動画IDを復元
if (tileData.customVideoId) {
document.getElementById(`customVideoId-${tileId}`).value = tileData.customVideoId;
}
// プレビューを更新
updateVideoPreview(tileId);
});
}
// 動画シーケンスを開始
function startVideoSequence() {
if (videoTiles.length === 0) {
alert("再生する動画がありません");
return;
}
// 有効な動画だけをフィルタリング
const enabledTiles = videoTiles.filter(tileId => {
return document.getElementById(`enable-${tileId}`).checked;
});
if (enabledTiles.length === 0) {
alert("有効な動画がありません");
return;
}
isPlayingSequence = true;
currentVideoIndex = 0;
// フルスクリーン表示
fullscreenVideo.style.display = 'block';
document.body.style.overflow = 'hidden';
stopAllPreviews();
// 最初の動画を再生
playVideoByIndex(currentVideoIndex, enabledTiles);
}
// 全てのプレビュー動画を一時停止
function stopAllPreviews() {
const previewVideos = document.querySelectorAll('.video-preview video');
previewVideos.forEach(video => {
video.pause();
video.currentTime = 0;
video.controls = false;
});
}
// 指定したインデックスの動画を再生
function playVideoByIndex(index, enabledTiles = null) {
if (!enabledTiles) {
enabledTiles = videoTiles.filter(tileId => {
return document.getElementById(`enable-${tileId}`).checked;
});
}
if (index < 0 || index >= enabledTiles.length) {
if (isLoopEnabled) {
index = 0;
currentVideoIndex = 0;
} else {
closeFullscreenVideo();
return;
}
}
const tileId = enabledTiles[index];
const videoUrl = getVideoUrl(tileId);
if (!videoUrl) {
alert("無効な動画設定です。動画を確認してください。");
return;
}
const startTime = parseFloat(document.getElementById(`startTime-${tileId}`).value) || 0;
const endTime = parseFloat(document.getElementById(`endTime-${tileId}`).value);
const fadeIn = parseFloat(document.getElementById(`fadeIn-${tileId}`).value) || 0;
const fadeOut = parseFloat(document.getElementById(`fadeOut-${tileId}`).value) || 0;
const waitTime = parseFloat(document.getElementById(`waitTime-${tileId}`).value) || 1;
const playbackRate = parseFloat(document.getElementById(`playbackRate-${tileId}`).value) || 1;
const volume = parseFloat(document.getElementById(`volume-${tileId}`).value) || 1;
const flip = document.getElementById(`flipMode-${tileId}`).value === 'true';
// 既存のイベントリスナーをクリア
mainVideo.onloadedmetadata = null;
mainVideo.onended = null;
mainVideo.ontimeupdate = null;
// メインビデオを設定
mainVideo.src = videoUrl;
mainVideo.currentTime = startTime;
mainVideo.playbackRate = playbackRate;
mainVideo.volume = 0; // 最初は音量0(フェードインのために)
mainVideo.loop = false;
mainVideo.style.transform = flip ? 'scaleX(-1)' : 'scaleX(1)';
mainVideo.style.opacity = '1'; // 常に表示
// フェードイン処理
if (fadeIn > 0) {
const targetVolume = isMuted ? 0 : volume;
const fadeInInterval = 50; // ミリ秒
const steps = fadeIn * 1000 / fadeInInterval;
let currentStep = 0;
const fadeInIntervalId = setInterval(() => {
currentStep++;
const progress = currentStep / steps;
mainVideo.volume = targetVolume * progress;
if (currentStep >= steps) {
clearInterval(fadeInIntervalId);
mainVideo.volume = targetVolume;
}
}, fadeInInterval);
} else {
mainVideo.volume = isMuted ? 0 : volume;
}
mainVideo.onloadedmetadata = () => {
thumbnailVideo.src = videoUrl;
// 終了時間が設定されている場合
if (endTime && !isNaN(endTime)) {
mainVideo.addEventListener('timeupdate', function onTimeUpdate() {
const remainingTime = endTime - this.currentTime;
// フェードアウト処理
if (remainingTime <= fadeOut && fadeOut > 0) {
const fadeOutProgress = remainingTime / fadeOut;
const targetVolume = isMuted ? 0 : volume;
mainVideo.volume = targetVolume * fadeOutProgress;
}
if (this.currentTime >= endTime) {
this.pause();
this.removeEventListener('timeupdate', onTimeUpdate);
setTimeout(() => {
currentVideoIndex++;
playVideoByIndex(currentVideoIndex, enabledTiles);
}, waitTime * 1000);
}
});
} else {
// 終了時間が設定されていない場合
mainVideo.onended = () => {
setTimeout(() => {
currentVideoIndex++;
playVideoByIndex(currentVideoIndex, enabledTiles);
}, waitTime * 1000);
};
}
mainVideo.play().catch(e => console.log("動画再生がブロックされました:", e));
};
}
// フルスクリーン動画を閉じる
function closeFullscreenVideo() {
fullscreenVideo.style.display = 'none';
document.body.style.overflow = 'auto';
mainVideo.pause();
isPlayingSequence = false;
isPlaying = false;
playPauseBtn.textContent = '▶';
}
// 前の動画を再生
function playPreviousVideo() {
if (!isPlayingSequence) return;
const enabledTiles = videoTiles.filter(tileId => {
return document.getElementById(`enable-${tileId}`).checked;
});
currentVideoIndex--;
if (currentVideoIndex < 0) {
currentVideoIndex = enabledTiles.length - 1;
}
playVideoByIndex(currentVideoIndex, enabledTiles);
}
// 次の動画を再生
function playNextVideo() {
if (!isPlayingSequence) return;
const enabledTiles = videoTiles.filter(tileId => {
return document.getElementById(`enable-${tileId}`).checked;
});
currentVideoIndex++;
playVideoByIndex(currentVideoIndex, enabledTiles);
}
// 現在の動画を最初から再生
function restartCurrentVideo() {
if (!isPlayingSequence) return;
const enabledTiles = videoTiles.filter(tileId => {
return document.getElementById(`enable-${tileId}`).checked;
});
playVideoByIndex(currentVideoIndex, enabledTiles);
}
// 再生/一時停止を切り替え
function togglePlayPause() {
if (!isPlayingSequence) return;
if (isPlaying) {
mainVideo.pause();
} else {
mainVideo.play();
}
}
// 音量を更新
function updateVolume() {
mainVideo.volume = volumeSlider.value;
isMuted = mainVideo.volume === 0;
muteBtn.textContent = isMuted ? "音を戻す" : "消音";
}
// 再生速度を更新
function updatePlaybackSpeed() {
mainVideo.playbackRate = speedSlider.value;
}
// ミュートを切り替え
function toggleMute() {
isMuted = !isMuted;
mainVideo.volume = isMuted ? 0 : volumeSlider.value;
muteBtn.textContent = isMuted ? "音を戻す" : "消音";
}
// PiPモードを切り替え
function togglePipMode() {}
// ビデオ表示を切り替え
function toggleVideoHide() {
isVideoHidden = !isVideoHidden;
mainVideo.style.display = isVideoHidden ? 'none' : 'block';
hideVideoBtn.textContent = isVideoHidden ? "表示" : "消画";
}
// プログレスバーの更新
function updateProgressBar() {
if (!isPlayingSequence) return;
const duration = mainVideo.duration || 1;
const currentTime = mainVideo.currentTime || 0;
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
requestAnimationFrame(updateProgressBar);
}
// 時間表示を更新
function updateTimeDisplay() {
currentTimeDisplay.textContent = formatTime(mainVideo.currentTime || 0);
}
// 動画の長さを表示
function updateDurationDisplay() {
durationTimeDisplay.textContent = formatTime(mainVideo.duration || 0);
}
// サムネイルを表示
function showThumbnail(e) {
if (!isPlayingSequence) return;
const rect = progressContainer.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const duration = mainVideo.duration || 1;
const time = pos * duration;
// サムネイルビデオを設定
thumbnailVideo.currentTime = time;
// サムネイルを表示
thumbnails.style.display = 'block';
thumbnails.style.left = `${e.clientX - 80}px`;
// キャンバスを使用してサムネイルを生成
const canvas = document.createElement('canvas');
canvas.width = 160;
canvas.height = 90;
const ctx = canvas.getContext('2d');
ctx.drawImage(thumbnailVideo, 0, 0, canvas.width, canvas.height);
thumbnails.innerHTML = '';
thumbnails.appendChild(canvas);
// 時間表示を追加
const timeLabel = document.createElement('div');
timeLabel.textContent = formatTime(time);
thumbnails.appendChild(timeLabel);
}
// サムネイルを非表示
function hideThumbnail() {
thumbnails.style.display = 'none';
}
// 時間をフォーマット
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
}
// ビデオをシーク
function seekVideo(e) {
if (!isPlayingSequence) return;
const rect = progressContainer.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const duration = mainVideo.duration || 1;
mainVideo.currentTime = pos * duration;
}
</script>
</body>
</html>