soiz1's picture
Update index.html
5a8c753
raw
history blame
67.8 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="https://soiz1-eruda3.hf.space/eruda.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高度な音声動画プレイヤー</title>
<style>
/* テクノロジー風背景スタイル */
.tech-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -2;
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
overflow: hidden;
}
.circuit-line {
position: absolute;
background: rgba(0, 255, 255, 0.1);
box-shadow: 0 0 10px rgba(0, 255, 255, 0.3);
}
.grid-dot {
position: absolute;
width: 2px;
height: 2px;
background: rgba(0, 255, 255, 0.3);
border-radius: 50%;
}
.hexagon {
position: absolute;
width: 40px;
height: 23px;
background: rgba(0, 255, 255, 0.05);
border: 1px solid rgba(0, 255, 255, 0.1);
box-shadow: 0 0 5px rgba(0, 255, 255, 0.2);
}
.hexagon:before, .hexagon:after {
content: "";
position: absolute;
width: 0;
border-left: 20px solid transparent;
border-right: 20px solid transparent;
}
.hexagon:before {
bottom: 100%;
border-bottom: 11.5px solid rgba(0, 255, 255, 0.05);
}
.hexagon:after {
top: 100%;
width: 0;
border-top: 11.5px solid rgba(0, 255, 255, 0.05);
}
.pulse {
position: absolute;
width: 10px;
height: 10px;
background: rgba(0, 255, 255, 0.7);
border-radius: 50%;
box-shadow: 0 0 10px 5px rgba(0, 255, 255, 0.5);
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(0.8); opacity: 0.7; }
50% { transform: scale(1.2); opacity: 1; }
100% { transform: scale(0.8); opacity: 0.7; }
}
@keyframes float {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
100% { transform: translateY(0) rotate(0deg); }
}
/* メインスタイル */
body {
font-family: 'Arial', sans-serif;
background-color: rgba(10, 25, 47, 0.8);
color: #e6f1ff;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
h1 {
color: #64ffda;
text-align: center;
margin-bottom: 30px;
border-bottom: 1px solid #64ffda;
padding-bottom: 10px;
width: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
background-color: rgba(17, 34, 64, 0.5);
border-radius: 10px;
padding: 20px;
box-shadow: 0 0 20px rgba(100, 255, 218, 0.2);
backdrop-filter: blur(5px);
border: 1px solid rgba(100, 255, 218, 0.1);
}
.video-container {
position: relative;
width: 100%;
margin-bottom: 20px;
}
video {
width: 100%;
border-radius: 5px;
background-color: #000;
display: block;
cursor: pointer;
}
.video-controls {
background-color: rgba(17, 34, 64, 0.6);
padding: 10px;
border-radius: 0 0 5px 5px;
display: flex;
flex-direction: column;
gap: 10px;
}
.progress-container {
width: 100%;
height: 10px;
background-color: #1e2a47;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.progress-bar {
height: 100%;
background-color: #64ffda;
border-radius: 5px;
width: 0%;
position: relative;
}
.progress-time {
position: absolute;
top: -25px;
transform: translateX(-50%);
background-color: rgba(30, 42, 71, 0.9);
padding: 3px 6px;
border-radius: 3px;
font-size: 12px;
display: none;
white-space: nowrap;
}
.main-controls {
display: flex;
align-items: center;
gap: 15px;
}
.control-button {
background: none;
border: none;
color: #e6f1ff;
font-size: 18px;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
}
.control-button:hover {
background-color: rgba(100, 255, 218, 0.2);
}
.time-display {
font-size: 14px;
color: #ccd6f6;
white-space: nowrap;
}
.volume-control {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
}
.volume-button {
background: none;
border: none;
color: #e6f1ff;
font-size: 18px;
cursor: pointer;
padding: 5px;
}
.volume-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 3px;
outline: none;
opacity: 0;
transition: opacity 0.3s, width 0.3s;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.volume-control:hover .volume-slider {
opacity: 1;
width: 100px;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.speed-control {
display: flex;
align-items: center;
gap: 5px;
}
.speed-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 3px;
outline: none;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.speed-value {
font-size: 14px;
min-width: 30px;
text-align: center;
}
.fullscreen-button {
margin-left: 10px;
}
.audio-controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.audio-item {
display: flex;
align-items: center;
gap: 10px;
}
.audio-item label {
min-width: 50px;
color: #64ffda;
}
.audio-slider {
flex-grow: 1;
height: 8px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 5px;
outline: none;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.audio-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.settings {
background-color: rbga(30, 42, 71, 0.3);
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.setting-item:last-child {
margin-bottom: 0;
}
.setting-item label {
color: #ccd6f6;
}
.global-volume-container, .playback-speed-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.global-volume-slider, .playback-speed-slider {
flex-grow: 1;
height: 8px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 5px;
outline: none;
background-image: linear-gradient(#64ffda, #64ffda);
background-size: 100% 100%;
background-repeat: no-repeat;
}
.global-volume-slider::-webkit-slider-thumb,
.playback-speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.slider-value {
min-width: 40px;
text-align: right;
}
input[type="number"], input[type="checkbox"], select {
background-color: #112240;
border: 1px solid #64ffda;
color: #e6f1ff;
padding: 5px;
border-radius: 3px;
}
.tech-decoration {
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #64ffda, transparent);
margin: 20px 0;
}
/* 全画面時のスタイル */
.video-container:-webkit-full-screen {
width: 100%;
height: 100%;
background-color: black;
}
.video-container:-webkit-full-screen video {
width: 100%;
height: 100%;
}
.video-container:-webkit-full-screen .video-controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
border-radius: 0;
}
/* ローディングアニメーション */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
transition: opacity 1s ease-out;
}
.spinner-box {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
background-color: transparent;
}
/* 軌道スタイル */
.leo {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
}
.blue-orbit {
width: 165px;
height: 165px;
border: 1px solid #91daffa5;
animation: spin3D 3s linear .2s infinite;
}
.green-orbit {
width: 120px;
height: 120px;
border: 1px solid #91ffbfa5;
animation: spin3D 2s linear 0s infinite;
}
.red-orbit {
width: 90px;
height: 90px;
border: 1px solid #ffca91a5;
animation: spin3D 1s linear 0s infinite;
}
.white-orbit {
width: 60px;
height: 60px;
border: 2px solid #ffffff;
animation: spin3D 10s linear 0s infinite;
}
.w1 {
transform: rotate3D(1, 1, 1, 90deg);
}
.w2 {
transform: rotate3D(1, 2, .5, 90deg);
}
.w3 {
transform: rotate3D(.5, 1, 2, 90deg);
}
/* キーフレームアニメーション */
@keyframes spin3D {
from {
transform: rotate3d(.5,.5,.5, 360deg);
}
to {
transform: rotate3d(0,0,0, 0deg);
}
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.time-set-button {
background-color: #112240;
border: 1px solid #64ffda;
color: #e6f1ff;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
font-size: 12px;
margin-left: 5px;
transition: background-color 0.3s;
}
.time-set-button:hover {
background-color: rgba(100, 255, 218, 0.2);
}
/* 合成ボタンスタイル */
.combine-button {
background-color: #64ffda;
color: #0a192f;
border: none;
padding: 10px 20px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
margin-top: 20px;
transition: all 0.3s;
font-weight: bold;
}
.combine-button:hover {
background-color: #52e0c4;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(100, 255, 218, 0.4);
}
.combine-button:disabled {
background-color: #3a5a78;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* 合成ステータスメッセージ */
.combine-status {
margin-top: 10px;
color: #64ffda;
font-size: 14px;
height: 20px;
}
/* プレビューセクション */
.preview-section {
margin-top: 20px;
padding: 15px;
background-color: rgba(17, 34, 64, 0.7);
border-radius: 5px;
display: none;
}
.preview-section h3 {
margin-top: 0;
color: #64ffda;
border-bottom: 1px solid #64ffda;
padding-bottom: 5px;
}
/* 無効状態のオーバーレイ */
.disabled-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(10, 25, 47, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 10;
border-radius: 5px;
}
.disabled-message {
background-color: rgba(30, 42, 71, 0.9);
padding: 20px;
border-radius: 5px;
text-align: center;
max-width: 80%;
}
.disabled-message p {
margin-bottom: 15px;
}
#buffering-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 10;
display: none;
}
.sync-status {
position: absolute;
bottom: 60px;
left: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: #64ffda;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
z-index: 5;
}
</style>
</head>
<body>
<script>
// 動画パスを再設定
document.addEventListener('DOMContentLoaded', function() {
const urlParams = new URLSearchParams(window.location.search);
const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
const videoSource = document.querySelector('#video source');
if (isTMode && videoSource) {
videoSource.src = '/t/v.mp4';
const video = document.getElementById('video');
video.load();
}
});
// sw-register.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js').then(registration => {
alert('SW registered:' + registration);
}).catch(error => {
alert('SW registration failed:' + error);
});
});
}
</script>
<!-- テクノロジー風背景 -->
<div class="tech-background" id="techBg"></div>
<!-- ローディングオーバーレイ -->
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner-box">
<div class="blue-orbit leo"></div>
<div class="green-orbit leo"></div>
<div class="red-orbit leo"></div>
<div class="white-orbit w1 leo"></div>
<div class="white-orbit w2 leo"></div>
<div class="white-orbit w3 leo"></div>
</div>
</div>
<h1>高度な音声動画プレイヤー</h1>
<div class="settings">
<h2>サービスワーカー設定</h2>
<div class="setting-item">
<label><input type="checkbox" id="sw-video" checked> 動画ファイル (/v.mp4)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-video" checked> 動画ファイル (/t/v.mp4)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-piano" checked> ピアノ音声 (/p.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-soprano" checked> ソプラノ音声 (/s.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-alto" checked> アルト音声 (/a.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-tenor" checked> テノール音声 (/t.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-combined" checked> 全体音声 (/k.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-piano" checked> ピアノ音声 (/t/p.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-soprano" checked> ソプラノ音声 (/t/s.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-alto" checked> アルト音声 (/t/a.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-tenor" checked> テノール音声 (/t/t.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-t-combined" checked> 全体音声 (/t/k.mp3)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-index" checked> インデックスファイル (/index.html)</label>
</div>
<div class="setting-item">
<label><input type="checkbox" id="sw-root" checked> ルートファイル (/)</label>
</div>
<button class="combine-button" id="sw-register-btn">登録を開始</button>
<div class="combine-status" id="sw-status"></div>
</div>
<div class="container">
<div class="video-container" id="video-container">
<!-- バッファリングインジケーター -->
<div id="buffering-indicator">読み込み中...</div>
<!-- 同期ステータス -->
<div class="sync-status" id="sync-status"></div>
<!-- 無効状態のオーバーレイ -->
<div class="disabled-overlay" id="disabledOverlay">
<div class="disabled-message">
<p>音声の合成が完了していません</p>
<p>下の音声コントロールで各パートの音量を調整し、「合成」ボタンを押してください</p>
</div>
</div>
<video id="video" muted>
<source src="v.mp4" type="video/mp4">
</video>
<div class="video-controls">
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
<div class="progress-time" id="progress-time">00:00</div>
</div>
<div class="main-controls">
<button class="control-button" id="play-pause-btn" disabled></button>
<div class="time-display" id="time-display">00:00.00 / 00:00.00</div>
<div class="volume-control">
<button class="volume-button" id="volume-btn" disabled>🔊</button>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1" disabled>
</div>
<div class="speed-control">
<span class="speed-value" id="speed-value">1.00x</span>
<input type="range" class="speed-slider" id="speed-slider" min="0.5" max="2" step="0.01" value="1" disabled>
</div>
<button class="control-button fullscreen-button" id="fullscreen-btn" disabled></button>
</div>
</div>
</div>
<div class="tech-decoration"></div>
<div class="settings">
<h2>設定</h2>
<div class="setting-item">
<label for="start-time">再生開始秒数:</label>
<div>
<input type="number" id="start-time" min="0" value="0" step="0.01" disabled>
<button class="time-set-button" id="set-start-time" disabled>現在の秒数に設定</button>
</div>
</div>
<div class="setting-item">
<label for="end-time">再生終了秒数:</label>
<div>
<input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
<button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
</div>
</div>
<div class="setting-item">
<label for="loop">ループ再生:</label>
<input type="checkbox" id="loop" disabled>
</div>
<div class="setting-item">
<div class="global-volume-container">
<label>全体音量係数:</label>
<input type="range" class="global-volume-slider" id="global-volume" min="0" max="10" step="0.01" value="0.5" disabled>
<span class="slider-value" id="global-volume-value">0.5</span>
</div>
</div>
<div class="setting-item">
<div class="playback-speed-container">
<label>再生速度:</label>
<input type="range" class="playback-speed-slider" id="playback-speed" min="0.5" max="2" step="0.01" value="1" disabled>
<span class="slider-value" id="playback-speed-value">1.00x</span>
</div>
</div>
</div>
<div class="tech-decoration"></div>
<div class="audio-controls">
<h2>音声コントロール</h2>
<div class="audio-item">
<label>ピアノ</label>
<input type="range" class="audio-slider" data-audio="p" min="0" max="1" step="0.01" value="0">
<span class="slider-value volume-value">0.00</span>
</div>
<div class="audio-item">
<label>ソプラノ</label>
<input type="range" class="audio-slider" data-audio="s" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>アルト</label>
<input type="range" class="audio-slider" data-audio="a" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>テノール</label>
<input type="range" class="audio-slider" data-audio="t" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>全体(非推奨)</label>
<input type="range" class="audio-slider" data-audio="k" min="0" max="1" step="0.01" value="0">
<span class="slider-value volume-value">0.00</span>
</div>
<!-- 合成ボタンとステータス -->
<button class="combine-button" id="combine-button">音声を合成</button>
<div class="combine-status" id="combine-status"></div>
</div>
<!-- プレビューセクション -->
<div class="preview-section" id="preview-section">
<h3 hidden>プレビュー</h3>
<p hidden>合成された音声をプレビューできます。再生ボタンをクリックして確認してください。</p>
<button class="control-button" id="preview-button" hidden></button>
<span id="preview-time" hidden>00:00 / 00:00</span>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 同期管理用の変数
let lastSyncTime = 0;
let isBuffering = false;
let syncDriftLog = [];
let syncCheckInterval;
let audioContext;
try {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
console.error('Web Audio APIがサポートされていません:', e);
}
// テクノロジー風背景を生成
function createTechBackground() {
const bg = document.getElementById('techBg');
for (let i = 0; i < 200; i++) {
const line = document.createElement('div');
line.className = 'circuit-line';
const isHorizontal = Math.random() > 0.5;
if (isHorizontal) {
line.style.width = `${Math.random() * 300 + 100}px`;
line.style.height = '1px';
} else {
line.style.width = '1px';
line.style.height = `${Math.random() * 300 + 100}px`;
}
line.style.left = `${Math.random() * 100}%`;
line.style.top = `${Math.random() * 100}%`;
line.style.opacity = Math.random() * 0.5 + 0.1;
bg.appendChild(line);
}
for (let i = 0; i < 200; i++) {
const dot = document.createElement('div');
dot.className = 'grid-dot';
dot.style.left = `${Math.random() * 100}%`;
dot.style.top = `${Math.random() * 100}%`;
bg.appendChild(dot);
}
for (let i = 0; i < 15; i++) {
const hex = document.createElement('div');
hex.className = 'hexagon';
hex.style.left = `${Math.random() * 100}%`;
hex.style.top = `${Math.random() * 100}%`;
hex.style.transform = `rotate(${Math.random() * 360}deg)`;
hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
bg.appendChild(hex);
}
for (let i = 0; i < 8; i++) {
const pulse = document.createElement('div');
pulse.className = 'pulse';
pulse.style.left = `${Math.random() * 100}%`;
pulse.style.top = `${Math.random() * 100}%`;
pulse.style.animationDelay = `${Math.random() * 2}s`;
bg.appendChild(pulse);
}
}
createTechBackground();
const urlParams = new URLSearchParams(window.location.search);
const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
const basePath = isTMode ? '/t/' : '/';
// ローディング状態を管理
let loadingCount = 0;
let totalToLoad = 6; // 動画 + 5つの音声ファイル
let lastUpdateTime = 0;
const updateInterval = 1;
function checkLoadingComplete() {
loadingCount++;
if (loadingCount >= totalToLoad) {
setTimeout(function() {
const loadingOverlay = document.getElementById('loadingOverlay');
loadingOverlay.style.opacity = '0';
setTimeout(function() {
loadingOverlay.style.display = 'none';
}, 1000);
}, 500);
}
}
function handleError(error, message) {
console.error(message, error);
window.alert(`${message}\n\nエラー詳細: ${error.message}`);
}
async function registerServiceWorker() {
const statusElement = document.getElementById('sw-status');
const registerBtn = document.getElementById('sw-register-btn');
if (!('serviceWorker' in navigator)) {
statusElement.textContent = "このブラウザはService Workerをサポートしていません";
return;
}
try {
// チェックボックスの状態を収集
const checkboxStates = {
'sw-video': document.getElementById('sw-video').checked,
'sw-t-video': document.getElementById('sw-t-video').checked,
'sw-piano': document.getElementById('sw-piano').checked,
'sw-soprano': document.getElementById('sw-soprano').checked,
'sw-alto': document.getElementById('sw-alto').checked,
'sw-tenor': document.getElementById('sw-tenor').checked,
'sw-combined': document.getElementById('sw-combined').checked,
'sw-t-piano': document.getElementById('sw-t-piano').checked,
'sw-t-soprano': document.getElementById('sw-t-soprano').checked,
'sw-t-alto': document.getElementById('sw-t-alto').checked,
'sw-t-tenor': document.getElementById('sw-t-tenor').checked,
'sw-t-combined': document.getElementById('sw-t-combined').checked,
'sw-index': document.getElementById('sw-index').checked,
'sw-root': document.getElementById('sw-root').checked
};
// チェックされたファイルを収集
const filesToCache = [];
// 通常のメディアファイル
if (checkboxStates['sw-video']) filesToCache.push('/v.mp4');
if (checkboxStates['sw-piano']) filesToCache.push('/p.mp3');
if (checkboxStates['sw-soprano']) filesToCache.push('/s.mp3');
if (checkboxStates['sw-alto']) filesToCache.push('/a.mp3');
if (checkboxStates['sw-tenor']) filesToCache.push('/t.mp3');
if (checkboxStates['sw-combined']) filesToCache.push('/k.mp3');
// t/ ディレクトリのメディアファイル
if (checkboxStates['sw-t-video']) filesToCache.push('/t/v.mp4');
if (checkboxStates['sw-t-piano']) filesToCache.push('/t/p.mp3');
if (checkboxStates['sw-t-soprano']) filesToCache.push('/t/s.mp3');
if (checkboxStates['sw-t-alto']) filesToCache.push('/t/a.mp3');
if (checkboxStates['sw-t-tenor']) filesToCache.push('/t/t.mp3');
if (checkboxStates['sw-t-combined']) filesToCache.push('/t/k.mp3');
registerBtn.disabled = true;
statusElement.textContent = "サービスワーカーを登録中...";
// Service Workerを登録
const registration = await navigator.serviceWorker.register('/service-worker.js');
// Service Workerがアクティブになるのを待つ
await new Promise((resolve, reject) => {
if (registration.active) {
resolve();
} else {
const worker = registration.installing || registration.waiting;
if (worker) {
worker.addEventListener('statechange', () => {
if (worker.state === 'activated') {
resolve();
} else if (worker.state === 'rejected') {
reject(new Error('Service Workerの登録が拒否されました'));
}
});
} else {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'activated') {
resolve();
} else if (newWorker.state === 'rejected') {
reject(new Error('Service Workerの登録が拒否されました'));
}
});
});
}
}
});
// キャッシュするファイルをService Workerに送信
registration.active.postMessage({
type: 'CACHE_FILES',
files: filesToCache,
checkboxStates: checkboxStates
});
// キャッシュが完了するのを待つ
statusElement.textContent = "ファイルをキャッシュ中...";
// キャッシュが完了したか確認する
await verifyCache(filesToCache);
statusElement.textContent = "サービスワーカーが正常に登録され、ファイルがキャッシュされました";
registerBtn.disabled = false;
} catch (error) {
console.error('Service Worker登録エラー:', error);
statusElement.textContent = `サービスワーカー登録に失敗しました: ${error.message}`;
registerBtn.disabled = false;
}
}
async function verifyCache(filesToCache) {
const maxAttempts = 10;
const delay = 500; // 500msごとに確認
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const cache = await caches.open('media-player-cache-v1');
const cachedRequests = await cache.keys();
const cachedUrls = cachedRequests.map(request => request.url.replace(location.origin, ''));
// ログはここに移動すると、キャッシュ状況が見えやすい
console.log('filesToCache:', filesToCache);
console.log('cachedUrls:', cachedUrls);
// すべてのファイルがキャッシュされているか確認
const allCached = filesToCache.every(file => cachedUrls.includes(file));
if (allCached) {
return true;
}
// 最後の試行でなければ待機
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('キャッシュが完了しませんでした');
}
async function restoreCheckboxStates() {
try {
const cache = await caches.open('settings-cache');
const response = await cache.match('checkbox-states');
if (response) {
const checkboxStates = await response.json();
for (const [id, checked] of Object.entries(checkboxStates)) {
const checkbox = document.getElementById(id);
if (checkbox) {
checkbox.checked = checked;
}
}
}
} catch (error) {
console.error('チェックボックス状態の復元に失敗:', error);
}
}
// オフライン状態をチェック
function checkOnlineStatus() {
const settingsSection = document.querySelector('.settings');
if (!navigator.onLine) {
settingsSection.style.display = 'none';
} else {
settingsSection.style.display = 'block';
}
}
// 全画面時のUI表示制御
function setupFullscreenUI() {
const videoContainer = document.getElementById('video-container');
const controls = document.querySelector('.video-controls');
let hideTimeout;
let isMouseOverControls = false;
// マウスがコントロール上にあるかどうかを追跡
controls.addEventListener('mouseenter', () => {
isMouseOverControls = true;
clearTimeout(hideTimeout);
controls.style.opacity = '1';
});
controls.addEventListener('mouseleave', () => {
isMouseOverControls = false;
startHideTimeout();
});
function startHideTimeout() {
clearTimeout(hideTimeout);
if (isFullscreen && !isMouseOverControls) {
hideTimeout = setTimeout(() => {
controls.style.opacity = '0';
}, 1500);
}
}
// 動画コンテナでマウス移動を検出
videoContainer.addEventListener('mousemove', () => {
if (isFullscreen) {
controls.style.opacity = '1';
startHideTimeout();
}
});
}
// DOMContentLoaded時の初期化
document.addEventListener('DOMContentLoaded', function() {
// チェックボックスをデフォルトでオフに
document.querySelectorAll('.settings input[type="checkbox"]').forEach(checkbox => {
checkbox.checked = false;
});
// index.htmlのチェックボックスは必須なのでオンに
document.getElementById('sw-index').checked = true;
document.getElementById('sw-index').disabled = true;
// ルートのチェックボックスは非表示に
document.getElementById('sw-root').parentElement.style.display = 'none';
// チェックボックスの状態を復元
restoreCheckboxStates();
// オンライン状態をチェック
checkOnlineStatus();
window.addEventListener('online', checkOnlineStatus);
window.addEventListener('offline', checkOnlineStatus);
// 全画面UI制御を設定
setupFullscreenUI();
});
// 登録ボタンにイベントリスナーを追加
document.getElementById('sw-register-btn').addEventListener('click', registerServiceWorker);
// 要素を取得
const video = document.getElementById('video');
const videoContainer = document.getElementById('video-container');
const playPauseBtn = document.getElementById('play-pause-btn');
const timeDisplay = document.getElementById('time-display');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressTime = document.getElementById('progress-time');
const volumeBtn = document.getElementById('volume-btn');
const volumeSlider = document.getElementById('volume-slider');
const speedSlider = document.getElementById('speed-slider');
const speedValue = document.getElementById('speed-value');
const playbackSpeedSlider = document.getElementById('playback-speed');
const playbackSpeedValue = document.getElementById('playback-speed-value');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const startTimeInput = document.getElementById('start-time');
const endTimeInput = document.getElementById('end-time');
const loopCheckbox = document.getElementById('loop');
const globalVolumeSlider = document.getElementById('global-volume');
const globalVolumeValue = document.getElementById('global-volume-value');
const audioSliders = document.querySelectorAll('.audio-slider');
const volumeValues = document.querySelectorAll('.volume-value');
const setStartTimeBtn = document.getElementById('set-start-time');
const setEndTimeBtn = document.getElementById('set-end-time');
const disabledOverlay = document.getElementById('disabledOverlay');
const combineButton = document.getElementById('combine-button');
const combineStatus = document.getElementById('combine-status');
const previewSection = document.getElementById('preview-section');
const previewButton = document.getElementById('preview-button');
const previewTime = document.getElementById('preview-time');
const bufferingIndicator = document.getElementById('buffering-indicator');
const syncStatus = document.getElementById('sync-status');
// 音声オブジェクトを作成
const audioElements = {};
const audioBuffers = {};
const audioFiles = ['p', 'a', 't', 's', 'k'];
let combinedAudioBuffer = null;
let combinedAudioSource = null;
let isAudioCombined = false;
let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
// 初期化
let videoDuration = 0;
let isPlaying = false;
let lastVolume = 1;
let currentPlaybackRate = 1;
let isFullscreen = false;
// 動画のバッファリング状態を監視
video.addEventListener('waiting', function() {
isBuffering = true;
bufferingIndicator.style.display = 'block';
if (combinedAudioSource) {
combinedAudioSource.stop();
combinedAudioSource = null;
}
});
video.addEventListener('playing', function() {
isBuffering = false;
bufferingIndicator.style.display = 'none';
if (!combinedAudioSource && isPlaying) {
syncAudioWithVideo();
}
});
video.addEventListener('suspend', function() {
console.log('動画の読み込みが一時停止しました');
});
video.addEventListener('stalled', function() {
console.log('動画の読み込みが停滞しました');
if (isPlaying) {
pauseMedia();
}
});
// 動画と音声の同期をチェックする関数
function startSyncCheck() {
if (syncCheckInterval) clearInterval(syncCheckInterval);
syncCheckInterval = setInterval(checkSync, 500); // 100msごとにチェック
}
function stopSyncCheck() {
if (syncCheckInterval) clearInterval(syncCheckInterval);
}
function checkSync() {
if (!isAudioCombined || !isPlaying || isBuffering) return;
const videoTime = video.currentTime;
const audioTime = audioContext.currentTime - (combinedAudioSource?.startTime || 0);
const drift = videoTime - audioTime;
// ズレを記録(直近5回分)
syncDriftLog.push(drift);
if (syncDriftLog.length > 5) syncDriftLog.shift();
// 平均ズレを計算
const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
// ズレ表示を更新
syncStatus.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
// ズレが大きい場合(0.1秒以上)に修正
if (Math.abs(avgDrift) > 0.1) {
console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`);
syncAudioWithVideo();
}
}
// 音声を動画に同期させる関数
function syncAudioWithVideo() {
if (!isAudioCombined || !isPlaying) return;
const currentTime = video.currentTime;
if (combinedAudioSource) {
combinedAudioSource.stop();
}
combinedAudioSource = audioContext.createBufferSource();
combinedAudioSource.buffer = combinedAudioBuffer;
combinedAudioSource.connect(audioContext.destination);
combinedAudioSource.start(0, currentTime);
combinedAudioSource.playbackRate.value = currentPlaybackRate;
// ズレ記録をリセット
syncDriftLog = [];
lastSyncTime = performance.now();
}
// 音声ファイルをロード
function loadAudioFiles() {
audioFiles.forEach(file => {
try {
const audio = new Audio(`${basePath}${file}.mp3`);
audio.preload = 'auto';
audio.loop = false;
audioElements[file] = audio;
audio.addEventListener('loadedmetadata', function() {
console.log(`${basePath}${file}.mp3 loaded`);
checkLoadingComplete();
});
audio.addEventListener('error', function() {
console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, audio.error);
checkLoadingComplete();
});
} catch (error) {
console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, error);
checkLoadingComplete();
}
});
}
// 音声を結合する関数
async function combineAudio() {
if (!audioContext) {
combineStatus.textContent = "Web Audio APIが利用できません";
return;
}
combineButton.disabled = true;
combineStatus.textContent = "音声を合成中...";
try {
// 現在の音量設定を保存
audioFiles.forEach(file => {
currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
});
// 各音声ファイルをデコード
const audioBufferPromises = audioFiles.map(async file => {
const audio = audioElements[file];
if (!audio) return null;
const response = await fetch(`${basePath}${file}.mp3`);
const arrayBuffer = await response.arrayBuffer();
return await audioContext.decodeAudioData(arrayBuffer);
});
// すべての音声バッファを取得
const buffers = await Promise.all(audioBufferPromises);
audioFiles.forEach((file, index) => {
audioBuffers[file] = buffers[index];
});
// 最長の音声バッファの長さを取得
const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
// 新しい音声バッファを作成
combinedAudioBuffer = audioContext.createBuffer(
2, // ステレオ
audioContext.sampleRate * maxDuration,
audioContext.sampleRate
);
// 各音声バッファを結合
for (let file of audioFiles) {
if (!audioBuffers[file]) continue;
const buffer = audioBuffers[file];
const volume = currentVolumes[file];
// 音量が0の場合はスキップ
if (volume === 0) continue;
// 各チャンネルに音声を加算
for (let channel = 0; channel < 2; channel++) {
const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
const outputData = combinedAudioBuffer.getChannelData(channel);
for (let i = 0; i < inputData.length; i++) {
outputData[i] += inputData[i] * volume;
}
}
}
// 音量を正規化 (クリッピング防止)
for (let channel = 0; channel < 2; channel++) {
const outputData = combinedAudioBuffer.getChannelData(channel);
let max = 0;
for (let i = 0; i < outputData.length; i++) {
if (Math.abs(outputData[i]) > max) {
max = Math.abs(outputData[i]);
}
}
if (max > 1) {
for (let i = 0; i < outputData.length; i++) {
outputData[i] /= max;
}
}
}
isAudioCombined = true;
combineStatus.textContent = "音声の合成が完了しました";
enablePlayerControls();
previewSection.style.display = 'block';
// 合成後に音量と再生速度を適用
applyVolume();
applyPlaybackRate();
} catch (error) {
console.error('音声合成エラー:', error);
combineStatus.textContent = "音声の合成に失敗しました";
combineButton.disabled = false;
}
}
function applyVolume() {
if (!isAudioCombined) return;
// ベース音量 (0-1)
const baseVolume = parseFloat(volumeSlider.value);
// グローバル音量係数 (0-10)
const globalVolume = parseFloat(globalVolumeSlider.value) / 10; // 0-1に変換
// 最終音量 (0-1)
const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
// 動画の音量を設定
video.volume = finalVolume;
// 音量アイコンを更新
updateVolumeIcon();
}
// 再生速度を適用(ピッチ維持)
function applyPlaybackRate() {
if (!isAudioCombined) return;
const speed = parseFloat(playbackSpeedSlider.value);
currentPlaybackRate = speed;
video.playbackRate = speed;
if (combinedAudioSource) {
combinedAudioSource.playbackRate.value = speed;
// ピッチを維持する設定
if ('preservesPitch' in combinedAudioSource) {
combinedAudioSource.preservesPitch = true;
} else if ('webkitPreservesPitch' in combinedAudioSource) {
combinedAudioSource.webkitPreservesPitch = true;
} else if ('mozPreservesPitch' in combinedAudioSource) {
combinedAudioSource.mozPreservesPitch = true;
}
}
speedValue.textContent = speed.toFixed(2) + 'x';
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
speedSlider.value = speed;
}
// プレイヤーコントロールを有効化
function enablePlayerControls() {
disabledOverlay.style.display = 'none';
playPauseBtn.disabled = false;
volumeBtn.disabled = false;
volumeSlider.disabled = false;
speedSlider.disabled = false;
fullscreenBtn.disabled = false;
startTimeInput.disabled = false;
endTimeInput.disabled = false;
loopCheckbox.disabled = false;
globalVolumeSlider.disabled = false;
setStartTimeBtn.disabled = false;
setEndTimeBtn.disabled = false;
playbackSpeedSlider.disabled = false;
}
// プレビュー再生
function togglePreview() {
if (!isAudioCombined || !combinedAudioBuffer) return;
if (previewButton.textContent === '▶') {
// 再生
if (combinedAudioSource) {
combinedAudioSource.stop();
}
combinedAudioSource = audioContext.createBufferSource();
combinedAudioSource.buffer = combinedAudioBuffer;
combinedAudioSource.connect(audioContext.destination);
combinedAudioSource.start(0);
previewButton.textContent = '⏸';
// プレビューの時間表示を更新
const updatePreviewTime = () => {
if (!combinedAudioSource || !isAudioCombined) return;
const currentTime = audioContext.currentTime - combinedAudioSource.startTime;
const duration = combinedAudioBuffer.duration;
if (currentTime >= duration) {
previewButton.textContent = '▶';
previewTime.textContent = `00:00 / ${formatTime(duration)}`;
return;
}
previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
requestAnimationFrame(updatePreviewTime);
};
updatePreviewTime();
combinedAudioSource.onended = () => {
previewButton.textContent = '▶';
previewTime.textContent = `00:00 / ${formatTime(combinedAudioBuffer.duration)}`;
};
} else {
// 一時停止
if (combinedAudioSource) {
combinedAudioSource.stop();
combinedAudioSource = null;
}
previewButton.textContent = '▶';
}
}
// 時間をフォーマットするヘルパー関数
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
// 動画のメタデータが読み込まれたら
video.addEventListener('loadedmetadata', function() {
try {
videoDuration = video.duration;
endTimeInput.value = videoDuration.toFixed(2);
endTimeInput.max = videoDuration;
startTimeInput.max = videoDuration - 0.1;
updateTimeDisplay();
checkLoadingComplete();
} catch (error) {
handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
}
});
// 動画エラー処理
video.addEventListener('error', function() {
handleError(video.error, '動画読み込み中にエラーが発生しました');
});
// 再生ボタンクリック
playPauseBtn.addEventListener('click', function() {
const endTime = parseFloat(endTimeInput.value) || videoDuration;
if (video.currentTime >= endTime) {
const startTime = parseFloat(startTimeInput.value) || 0;
seekMedia(startTime);
}
togglePlayPause();
});
// 時間表示を更新
function updateTimeDisplay() {
const now = performance.now();
if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
lastUpdateTime = now;
try {
const currentTime = video.currentTime;
const duration = video.duration || videoDuration;
const currentMinutes = Math.floor(currentTime / 60);
const currentSeconds = Math.floor(currentTime % 60);
const currentMilliseconds = Math.floor((currentTime % 1) * 100);
const durationMinutes = Math.floor(duration / 60);
const durationSeconds = Math.floor(duration % 60);
const durationMilliseconds = Math.floor((duration % 1) * 100);
timeDisplay.textContent =
`${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
`${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
} catch (error) {
console.error('時間表示更新エラー:', error);
}
}
// 再生/一時停止をトグル
function togglePlayPause() {
if (isPlaying) {
pauseMedia();
} else {
playMedia();
}
}
// 再生関数 (改良版)
function playMedia() {
try {
const duration = video.duration || videoDuration;
const startTime = parseFloat(startTimeInput.value) || 0;
const endTime = parseFloat(endTimeInput.value) || duration;
if (video.currentTime >= endTime) {
video.currentTime = startTime;
}
const playPromise = video.play();
if (playPromise !== undefined) {
playPromise.then(() => {
isPlaying = true;
playPauseBtn.textContent = '⏸';
syncAudioWithVideo();
startSyncCheck();
video.playbackRate = currentPlaybackRate;
}).catch(error => {
console.error('動画再生エラー:', error);
});
}
} catch (error) {
console.error('メディア再生エラー:', error);
}
}
// 一時停止関数
function pauseMedia() {
try {
video.pause();
isPlaying = false;
playPauseBtn.textContent = '▶';
stopSyncCheck();
if (combinedAudioSource) {
combinedAudioSource.stop();
combinedAudioSource = null;
}
} catch (error) {
console.error('メディア一時停止エラー:', error);
}
}
// 時間更新時の処理
video.addEventListener('timeupdate', function() {
const duration = video.duration || videoDuration;
const endTime = parseFloat(endTimeInput.value) || duration;
if (video.currentTime >= endTime && endTime > 0) {
if (loopCheckbox.checked) {
const startTime = parseFloat(startTimeInput.value) || 0;
video.currentTime = startTime;
syncAudioWithVideo();
} else {
pauseMedia();
video.currentTime = endTime;
}
}
updateTimeDisplay();
});
// プログレスバークリックでシーク
progressContainer.addEventListener('click', function(e) {
if (!video.duration) return;
const rect = this.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const seekTime = pos * video.duration;
seekMedia(seekTime);
});
// 指定した時間にシーク (改良版)
function seekMedia(time) {
try {
const duration = video.duration || videoDuration;
const startTime = parseFloat(startTimeInput.value) || 0;
const endTime = parseFloat(endTimeInput.value) || duration;
const seekTime = Math.max(startTime, Math.min(time, endTime));
video.currentTime = seekTime;
if (combinedAudioSource) {
combinedAudioSource.stop();
combinedAudioSource = audioContext.createBufferSource();
combinedAudioSource.buffer = combinedAudioBuffer;
combinedAudioSource.connect(audioContext.destination);
if (isPlaying) {
combinedAudioSource.start(0, seekTime);
combinedAudioSource.playbackRate.value = currentPlaybackRate;
}
}
} catch (error) {
console.error('メディアシークエラー:', error);
}
}
// プログレスバー上でマウス移動時に時間を表示
progressContainer.addEventListener('mousemove', function(e) {
if (!video.duration) return;
const rect = this.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const hoverTime = pos * video.duration;
const minutes = Math.floor(hoverTime / 60);
const seconds = Math.floor(hoverTime % 60);
const milliseconds = Math.floor((hoverTime % 1) * 100);
progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
progressTime.style.display = 'block';
progressTime.style.left = `${pos * 100}%`;
});
progressContainer.addEventListener('mouseleave', function() {
progressTime.style.display = 'none';
});
// 動画クリックで再生/一時停止
video.addEventListener('click', function() {
togglePlayPause();
});
volumeSlider.addEventListener('input', function() {
if (!isAudioCombined) return;
lastVolume = parseFloat(this.value);
applyVolume();
});
volumeBtn.addEventListener('click', function() {
if (!isAudioCombined) return;
if (video.volume > 0) {
lastVolume = parseFloat(volumeSlider.value);
volumeSlider.value = 0;
} else {
volumeSlider.value = lastVolume;
}
applyVolume();
});
// 音量アイコンを更新
function updateVolumeIcon() {
if (video.volume === 0) {
volumeBtn.textContent = '🔇';
} else if (video.volume < 0.5) {
volumeBtn.textContent = '🔈';
} else {
volumeBtn.textContent = '🔊';
}
}
// 再生速度スライダー (動画プレイヤー)
speedSlider.addEventListener('input', function() {
if (!isAudioCombined) return;
const speed = parseFloat(this.value);
speedValue.textContent = speed.toFixed(2) + 'x';
playbackSpeedSlider.value = speed;
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
updatePlaybackRate(speed);
});
// 再生速度スライダー (設定メニュー)
playbackSpeedSlider.addEventListener('input', function() {
if (!isAudioCombined) return;
const speed = parseFloat(this.value);
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
speedSlider.value = speed;
speedValue.textContent = speed.toFixed(2) + 'x';
updatePlaybackRate(speed);
});
function updatePlaybackRate(speed) {
if (!isAudioCombined) return;
currentPlaybackRate = speed;
video.playbackRate = speed;
if (combinedAudioSource) {
combinedAudioSource.playbackRate.value = speed;
}
}
// 全画面ボタン
fullscreenBtn.addEventListener('click', function() {
if (!isFullscreen) {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
} else if (videoContainer.webkitRequestFullscreen) {
videoContainer.webkitRequestFullscreen();
} else if (videoContainer.msRequestFullscreen) {
videoContainer.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
// 全画面変更イベント
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
function handleFullscreenChange() {
isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
video.controls = false;
}
// キーボードイベント (ESCで全画面終了)
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isFullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
});
// ボリュームスライダーのイベント
audioSliders.forEach((slider, index) => {
slider.addEventListener('input', function() {
const value = parseFloat(this.value);
volumeValues[index].textContent = value.toFixed(2);
const percent = value * 100;
this.style.backgroundSize = `${percent}% 100%`;
});
});
globalVolumeSlider.addEventListener('input', function() {
const value = parseFloat(this.value);
globalVolumeValue.textContent = value.toFixed(1);
const percent = (value - this.min) / (this.max - this.min) * 100;
this.style.backgroundSize = `${percent}% 100%`;
applyVolume();
});
// ループ設定変更時
loopCheckbox.addEventListener('change', function() {
// 合成音声ではループは動画に依存する
});
// 現在の秒数を開始時間に設定
setStartTimeBtn.addEventListener('click', function() {
startTimeInput.value = video.currentTime.toFixed(2);
});
// 現在の秒数を終了時間に設定
setEndTimeBtn.addEventListener('click', function() {
endTimeInput.value = video.currentTime.toFixed(2);
});
// 合成ボタンクリック
combineButton.addEventListener('click', combineAudio);
// プレビューボタンクリック
previewButton.addEventListener('click', togglePreview);
// 初期化
loadAudioFiles();
updateVolumeIcon();
volumeSlider.value = video.volume;
video.controls = false;
// スライダーの背景を初期化
function initSliderBackgrounds() {
const sliders = [
volumeSlider,
speedSlider,
globalVolumeSlider,
playbackSpeedSlider,
...audioSliders
];
sliders.forEach(slider => {
if (slider) {
slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
slider.style.backgroundRepeat = 'no-repeat';
if (slider === globalVolumeSlider) {
const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
slider.style.backgroundSize = `${percent}% 100%`;
globalVolumeValue.textContent = slider.value;
} else {
slider.style.backgroundSize = `${slider.value * 100}% 100%`;
}
}
});
}
initSliderBackgrounds();
startSyncCheck(); // 同期チェックを開始
});
</script>