soiz1 commited on
Commit
63fc1b7
·
1 Parent(s): 13f6f63

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1091 -1065
index.html CHANGED
@@ -379,7 +379,7 @@ document.addEventListener('DOMContentLoaded', function () {
379
  /* プログレスバーのマーカー */
380
  .progress-marker {
381
  position: absolute;
382
- bottom: -10px;
383
  width: 0;
384
  height: 0;
385
  border-left: 5px solid transparent;
@@ -1200,29 +1200,14 @@ window.addEventListener('load', async () => {
1200
  <button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button>
1201
  </div>
1202
  </div>
1203
- <div class="setting-item">
1204
- <button class="combine-button" id="apply-time-btn" disabled>時間設定を適用</button>
1205
- </div>
1206
- <h2>設定</h2>
1207
- <div class="setting-item">
1208
- <label for="start-time">再生開始秒数:</label>
1209
- <div>
1210
- <input type="number" id="start-time" min="0" value="0" step="0.01" disabled>
1211
- <button class="time-set-button" id="set-start-time" disabled>現在の秒数に設定</button>
1212
- </div>
1213
- </div>
1214
- <div class="setting-item">
1215
- <label for="end-time">再生終了秒数:</label>
1216
- <div>
1217
- <input type="number" id="end-time" min="0" value="0" step="0.01" disabled>
1218
- <button class="time-set-button" id="set-end-time" disabled>現在の秒数に設定</button>
1219
- <button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button>
1220
- </div>
1221
- </div>
1222
  <div class="setting-item">
1223
  <label for="loop">ループ再生:</label>
1224
  <input type="checkbox" id="loop" disabled>
1225
  </div>
 
 
 
 
1226
  <div class="setting-item">
1227
  <div class="global-volume-container">
1228
  <label>全体音量係数:</label>
@@ -1249,418 +1234,444 @@ window.addEventListener('load', async () => {
1249
  <!-- 全画面時のロックボタン -->
1250
  <button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button>
1251
  <script>
1252
- document.addEventListener('DOMContentLoaded', function() {
1253
- // 同期管理用の変数
1254
- let lastSyncTime = 0;
1255
- let isBuffering = false;
1256
- let syncDriftLog = [];
1257
- let syncCheckInterval;
1258
- let audioContext;
1259
- let controlsHideTimeout;
1260
- let isControlsLocked = false;
1261
- let controlsVisible = true;
1262
- let isCheckingSync = false;
1263
- let isInBackgroundTab = false;
1264
-
1265
- try {
1266
- audioContext = new (window.AudioContext || window.webkitAudioContext)();
1267
- } catch (e) {
1268
- console.error('Web Audio APIがサポートされていません:', e);
1269
- }
1270
-
1271
- // テクノロジー風背景を生成
1272
- function createTechBackground() {
1273
- const bg = document.getElementById('techBg');
1274
-
1275
- for (let i = 0; i < 200; i++) {
1276
- const line = document.createElement('div');
1277
- line.className = 'circuit-line';
1278
-
1279
- const isHorizontal = Math.random() > 0.5;
1280
- if (isHorizontal) {
1281
- line.style.width = `${Math.random() * 300 + 100}px`;
1282
- line.style.height = '1px';
1283
- } else {
1284
- line.style.width = '1px';
1285
- line.style.height = `${Math.random() * 300 + 100}px`;
1286
- }
1287
-
1288
- line.style.left = `${Math.random() * 100}%`;
1289
- line.style.top = `${Math.random() * 100}%`;
1290
- line.style.opacity = Math.random() * 0.5 + 0.1;
1291
- bg.appendChild(line);
1292
- }
1293
-
1294
- for (let i = 0; i < 200; i++) {
1295
- const dot = document.createElement('div');
1296
- dot.className = 'grid-dot';
1297
- dot.style.left = `${Math.random() * 100}%`;
1298
- dot.style.top = `${Math.random() * 100}%`;
1299
- bg.appendChild(dot);
1300
- }
1301
-
1302
- for (let i = 0; i < 15; i++) {
1303
- const hex = document.createElement('div');
1304
- hex.className = 'hexagon';
1305
- hex.style.left = `${Math.random() * 100}%`;
1306
- hex.style.top = `${Math.random() * 100}%`;
1307
- hex.style.transform = `rotate(${Math.random() * 360}deg)`;
1308
- hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
1309
- bg.appendChild(hex);
1310
- }
1311
-
1312
- for (let i = 0; i < 8; i++) {
1313
- const pulse = document.createElement('div');
1314
- pulse.className = 'pulse';
1315
- pulse.style.left = `${Math.random() * 100}%`;
1316
- pulse.style.top = `${Math.random() * 100}%`;
1317
- pulse.style.animationDelay = `${Math.random() * 2}s`;
1318
- bg.appendChild(pulse);
1319
- }
1320
- }
1321
-
1322
- createTechBackground();
1323
-
1324
- const urlParams = new URLSearchParams(window.location.search);
1325
- const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
1326
- const basePath = isTMode ? '/t/' : '/';
1327
-
1328
- // ローディング状態を管理
1329
- let loadingCount = 0;
1330
- let totalToLoad = 6; // 動画 + 5つの音声ファイル
1331
- let lastUpdateTime = 0;
1332
- const updateInterval = 1;
1333
-
1334
- function checkLoadingComplete() {
1335
- loadingCount++;
1336
- if (loadingCount >= totalToLoad) {
1337
- setTimeout(function() {
1338
- const loadingOverlay = document.getElementById('loadingOverlay');
1339
- loadingOverlay.style.opacity = '0';
1340
- setTimeout(function() {
1341
- loadingOverlay.style.display = 'none';
1342
- }, 1000);
1343
- }, 500);
1344
- }
1345
- }
1346
-
1347
- function handleError(error, message) {
1348
- console.error(message, error);
1349
- window.alert(`${message}\n\nエラー詳細: ${error.message}`);
1350
- }
1351
-
1352
- // 要素を取得
1353
- const video = document.getElementById('video');
1354
- video.preservesPitch = true;
1355
- video.mozPreservesPitch = true; // Firefox用
1356
- video.webkitPreservesPitch = true; // 古いWebKit用
1357
- const videoContainer = document.getElementById('video-container');
1358
- const playPauseBtn = document.getElementById('play-pause-btn');
1359
- const timeDisplay = document.getElementById('time-display');
1360
- const progressContainer = document.getElementById('progress-container');
1361
- const progressBar = document.getElementById('progress-bar');
1362
- const progressTime = document.getElementById('progress-time');
1363
- const volumeBtn = document.getElementById('volume-btn');
1364
- const volumeSlider = document.getElementById('volume-slider');
1365
- const speedSlider = document.getElementById('speed-slider');
1366
- const speedValue = document.getElementById('speed-value');
1367
- const playbackSpeedSlider = document.getElementById('playback-speed');
1368
- const playbackSpeedValue = document.getElementById('playback-speed-value');
1369
- const fullscreenBtn = document.getElementById('fullscreen-btn');
1370
- const startTimeInput = document.getElementById('start-time');
1371
- const endTimeInput = document.getElementById('end-time');
1372
- const loopCheckbox = document.getElementById('loop');
1373
- const globalVolumeSlider = document.getElementById('global-volume');
1374
- const globalVolumeValue = document.getElementById('global-volume-value');
1375
- const audioSliders = document.querySelectorAll('.audio-slider');
1376
- const volumeValues = document.querySelectorAll('.volume-value');
1377
- const setStartTimeBtn = document.getElementById('set-start-time');
1378
- const setEndTimeBtn = document.getElementById('set-end-time');
1379
- const resetEndTimeBtn = document.getElementById('reset-end-time');
1380
- const disabledOverlay = document.getElementById('disabledOverlay');
1381
- const combineButton = document.getElementById('combine-button');
1382
- const combineStatus = document.getElementById('combine-status');
1383
- const previewSection = document.getElementById('preview-section');
1384
- const previewButton = document.getElementById('preview-button');
1385
- const previewTime = document.getElementById('preview-time');
1386
- const bufferingIndicator = document.getElementById('buffering-indicator');
1387
- const syncStatus = document.getElementById('sync-status');
1388
- const syncStatusText = document.getElementById('sync-status-text');
1389
- const syncStatusClose = document.getElementById('sync-status-close');
1390
- const lockControlsBtn = document.getElementById('lock-controls-btn');
1391
- const startMarker = document.getElementById('start-marker');
1392
- const endMarker = document.getElementById('end-marker');
1393
- const tempoInput = document.getElementById('tempo');
1394
- const tempoSpeedValue = document.getElementById('tempo-speed-value');
1395
- const videoControls = document.querySelector('.video-controls');
1396
- const applyTimeBtn = document.getElementById('apply-time-btn');
1397
-
1398
- // 音声オブジェクトを作成
1399
- const audioElements = {};
1400
- const audioBuffers = {};
1401
- const audioFiles = ['p', 'a', 't', 's', 'k'];
1402
- let combinedAudioElement = null;
1403
- let isAudioCombined = false;
1404
- let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
1405
-
1406
- // 初期化
1407
- let videoDuration = 0;
1408
- let isPlaying = false;
1409
- let lastVolume = 1;
1410
- let currentPlaybackRate = 1;
1411
- let isFullscreen = false;
1412
-
1413
- async function enterPiP() {
1414
- if (!document.pictureInPictureElement && !video.paused) {
1415
- try {
1416
- await video.requestPictureInPicture();
1417
- } catch (err) {
1418
- console.warn('PiP開始失敗:', err);
1419
- }
1420
- }
1421
- }
1422
-
1423
- async function exitPiP() {
1424
- if (document.pictureInPictureElement) {
1425
- try {
1426
- await document.exitPictureInPicture();
1427
- } catch (err) {
1428
- console.warn('PiP終了失敗:', err);
1429
- }
1430
- }
1431
- }
1432
-
1433
-
1434
- // 動画のバッファリング状態を監視
1435
- video.addEventListener('waiting', function() {
1436
- isBuffering = true;
1437
- bufferingIndicator.style.display = 'block';
1438
- if (combinedAudioElement) {
1439
- combinedAudioElement.pause();
1440
- }
1441
- });
1442
-
1443
- video.addEventListener('playing', function() {
1444
- isBuffering = false;
1445
- bufferingIndicator.style.display = 'none';
1446
- if (combinedAudioElement && isPlaying) {
1447
- syncAudioWithVideo();
1448
- }
1449
- });
1450
-
1451
- video.addEventListener('suspend', function() {
1452
- console.log('動画の読み込みが一時停止しました');
1453
- });
1454
-
1455
- video.addEventListener('stalled', function() {
1456
- console.log('動画の読み込みが停滞しました');
1457
- if (isPlaying) {
1458
- pauseMedia();
1459
- }
1460
- });
1461
-
1462
- function startSyncCheck() {
1463
- if (isCheckingSync) return;
1464
- isCheckingSync = true;
1465
- if (syncCheckInterval) clearInterval(syncCheckInterval);
1466
- syncCheckInterval = setInterval(checkSync, 1000);
1467
- }
1468
 
1469
- function stopSyncCheck() {
1470
- isCheckingSync = false;
1471
- if (syncCheckInterval) clearInterval(syncCheckInterval);
 
 
 
 
1472
  }
1473
 
1474
- function checkSync() {
1475
- if (!isAudioCombined || !isPlaying || isBuffering || isInBackgroundTab) return;
1476
-
1477
- const videoTime = video.currentTime;
1478
- const audioTime = combinedAudioElement.currentTime;
1479
- const drift = videoTime - audioTime;
1480
-
1481
- syncDriftLog.push(drift);
1482
- if (syncDriftLog.length > 5) syncDriftLog.shift();
1483
-
1484
- const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
1485
- syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
1486
-
1487
- if (Math.abs(avgDrift) > 0.1) {
1488
- console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`);
1489
- syncAudioWithVideo();
1490
- }
1491
- }
1492
 
1493
- applyTimeBtn.addEventListener('click', function() {
1494
- // 現在再生中なら一時停止
1495
- const wasPlaying = isPlaying;
1496
- if (isPlaying) {
1497
- pauseMedia();
 
 
 
 
 
 
 
 
 
 
 
 
 
1498
  }
1499
 
1500
- // 開始時間と終了時間を取得
1501
- const startTime = parseFloat(startTimeInput.value) || 0;
1502
- const endTime = parseFloat(endTimeInput.value) || video.duration;
 
 
 
 
 
 
 
 
1503
 
1504
- // 現在位置が開始時間より前なら開始時間に移動
1505
- if (video.currentTime < startTime) {
1506
- video.currentTime = startTime;
1507
- if (combinedAudioElement) {
1508
- combinedAudioElement.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1509
  }
1510
  }
1511
- // 現在位置が終了時間より後なら開始時間に移動
1512
- else if (video.currentTime > endTime) {
1513
- video.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1514
  if (combinedAudioElement) {
1515
- combinedAudioElement.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1516
  }
 
 
 
 
 
 
 
1517
  }
1518
 
1519
- // 再生中だった場合は再開
1520
- if (wasPlaying) {
1521
- playMedia();
1522
  }
1523
 
1524
- // マーカーを更新
1525
- updateProgressMarkers();
1526
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1527
 
1528
- // 音声を動画に同期させる関数
1529
- function syncAudioWithVideo() {
1530
- if (!isAudioCombined || !isPlaying) return;
1531
-
1532
- const currentTime = video.currentTime;
1533
-
1534
- if (combinedAudioElement) {
1535
- combinedAudioElement.currentTime = currentTime;
1536
- if (isPlaying) {
1537
- combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1538
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1539
  }
1540
-
1541
- // ズレ記録をリセット
1542
- syncDriftLog = [];
1543
- lastSyncTime = performance.now();
1544
  }
1545
-
1546
- // 音声ファイルをロード
1547
- function loadAudioFiles() {
1548
- audioFiles.forEach(file => {
1549
- try {
1550
- const audio = new Audio(`${basePath}${file}.mp3`);
1551
- audio.preload = 'auto';
1552
- audio.loop = false;
1553
- audioElements[file] = audio;
1554
-
1555
- audio.addEventListener('loadedmetadata', function() {
1556
- console.log(`${basePath}${file}.mp3 loaded`);
1557
- checkLoadingComplete();
1558
- });
1559
-
1560
- audio.addEventListener('error', function() {
1561
- console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, audio.error);
1562
- checkLoadingComplete();
1563
- });
1564
- } catch (error) {
1565
- console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, error);
1566
- checkLoadingComplete();
1567
- }
1568
- });
1569
  }
1570
-
1571
- // 音声を結合する関数
1572
- async function combineAudio() {
1573
- combineButton.disabled = true;
1574
- combineStatus.textContent = "音声を合成中...";
1575
-
1576
- try {
1577
- // 現在の音量設定を保存
1578
- audioFiles.forEach(file => {
1579
- currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
1580
- });
1581
-
1582
- // 各音声ファイルをデコード
1583
- const audioBufferPromises = audioFiles.map(async file => {
1584
- const audio = audioElements[file];
1585
- if (!audio) return null;
1586
-
1587
- // 音量が0の場合はスキップ
1588
- if (currentVolumes[file] === 0) return null;
1589
-
1590
- const response = await fetch(`${basePath}${file}.mp3`);
1591
- const arrayBuffer = await response.arrayBuffer();
1592
- return await audioContext.decodeAudioData(arrayBuffer);
1593
- });
1594
-
1595
- // すべての音声バッファを取得
1596
- const buffers = await Promise.all(audioBufferPromises);
1597
- audioFiles.forEach((file, index) => {
1598
- audioBuffers[file] = buffers[index];
1599
- });
1600
-
1601
- // 最長の音声バッファの長さを取得
1602
- const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
1603
-
1604
- // 新しい音声バッファを作成
1605
- const combinedAudioBuffer = audioContext.createBuffer(
1606
- 2, // ステレオ
1607
- audioContext.sampleRate * maxDuration,
1608
- audioContext.sampleRate
1609
- );
1610
-
1611
- // 各音声バッファを結合
1612
- for (let file of audioFiles) {
1613
- if (!audioBuffers[file] || currentVolumes[file] === 0) continue;
1614
-
1615
- const buffer = audioBuffers[file];
1616
- const volume = currentVolumes[file];
1617
-
1618
- // 各チャンネルに音声を加算
1619
- for (let channel = 0; channel < 2; channel++) {
1620
- const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
1621
- const outputData = combinedAudioBuffer.getChannelData(channel);
1622
-
1623
- for (let i = 0; i < inputData.length; i++) {
1624
- outputData[i] += inputData[i] * volume;
1625
- }
1626
- }
1627
- }
1628
-
1629
- // 音量を正規化 (クリッピング防止)
1630
- for (let channel = 0; channel < 2; channel++) {
1631
- const outputData = combinedAudioBuffer.getChannelData(channel);
1632
- let max = 0;
1633
-
1634
- for (let i = 0; i < outputData.length; i++) {
1635
- if (Math.abs(outputData[i]) > max) {
1636
- max = Math.abs(outputData[i]);
1637
- }
1638
- }
1639
-
1640
- if (max > 1) {
1641
- for (let i = 0; i < outputData.length; i++) {
1642
- outputData[i] /= max;
1643
- }
1644
- }
1645
- }
1646
-
1647
- // AudioBufferをBlobに変換
1648
- const blob = bufferToWave(combinedAudioBuffer);
1649
- const url = URL.createObjectURL(blob);
1650
-
1651
- // 新しいaudio要素を作成
1652
- combinedAudioElement = new Audio(url);
1653
- combinedAudioElement.preservesPitch = true;
1654
- combinedAudioElement.mozPreservesPitch = true;
1655
- combinedAudioElement.webkitPreservesPitch = true;
1656
- combinedAudioElement.playbackRate = currentPlaybackRate;
1657
-
1658
- isAudioCombined = true;
1659
- combineStatus.textContent = "音声の合成が完了しました";
1660
- enablePlayerControls();
1661
-
1662
- combineButton.disabled = false;
1663
-
1664
  document.addEventListener('visibilitychange', async () => {
1665
  if (document.hidden) {
1666
  // タブが非表示になった場合
@@ -1716,682 +1727,687 @@ window.addEventListener('load', async () => {
1716
  }
1717
  });
1718
 
 
 
1719
 
1720
- // 動画終了時に自動的にPiPを閉じる(次回再開のため)
1721
- video.addEventListener('ended', exitPiP);
1722
- // 合成後に音量と再生速度を適用
1723
- applyVolume();
1724
- applyPlaybackRate();
1725
-
1726
- } catch (error) {
1727
- console.error('音声合成エラー:', error);
1728
- combineStatus.textContent = "音声の合成に失敗しました";
1729
- combineButton.disabled = false;
1730
- }
1731
- }
1732
-
1733
- function bufferToWave(abuffer) {
1734
- const numOfChan = abuffer.numberOfChannels,
1735
- length = abuffer.length * numOfChan * 2 + 44,
1736
- buffer = new ArrayBuffer(length),
1737
- view = new DataView(buffer),
1738
- channels = [],
1739
- sampleRate = abuffer.sampleRate;
1740
-
1741
- // posをletで宣言(constから変更)
1742
- let pos = 0;
1743
-
1744
- // write WAV header
1745
- setUint32(0x46464952); // "RIFF"
1746
- setUint32(length - 8); // file length - 8
1747
- setUint32(0x45564157); // "WAVE"
1748
-
1749
- setUint32(0x20746d66); // "fmt " chunk
1750
- setUint32(16); // length = 16
1751
- setUint16(1); // PCM (uncompressed)
1752
- setUint16(numOfChan);
1753
- setUint32(sampleRate);
1754
- setUint32(sampleRate * 2 * numOfChan);
1755
- setUint16(numOfChan * 2);
1756
- setUint16(16);
1757
-
1758
- setUint32(0x61746164); // "data" - chunk
1759
- setUint32(length - pos - 4);
1760
-
1761
- // write interleaved data
1762
- for (let i = 0; i < abuffer.length; i++) {
1763
- for (let channel = 0; channel < numOfChan; channel++) {
1764
- let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
1765
- if (sample < -32768) sample = -32768;
1766
- if (sample > 32767) sample = 32767;
1767
- view.setInt16(pos, sample, true);
1768
- pos += 2;
1769
- }
1770
- }
1771
-
1772
- function setUint16(data) {
1773
- view.setUint16(pos, data, true);
1774
- pos += 2;
1775
- }
1776
-
1777
- function setUint32(data) {
1778
- view.setUint32(pos, data, true);
1779
- pos += 4;
1780
- }
1781
-
1782
- return new Blob([buffer], { type: 'audio/wav' });
1783
- }
1784
-
1785
- function applyVolume() {
1786
- if (!isAudioCombined) return;
1787
-
1788
- // ベース音量 (0-1)
1789
- const baseVolume = parseFloat(volumeSlider.value);
1790
- // グローバル音量係数 (0-10)
1791
- const globalVolume = parseFloat(globalVolumeSlider.value) / 10; // 0-1に変換
1792
-
1793
- // 最終音量 (0-1)
1794
- const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
1795
-
1796
- // 動画と音声の音量を設定
1797
- video.volume = finalVolume;
1798
- if (combinedAudioElement) {
1799
- combinedAudioElement.volume = finalVolume;
1800
- }
1801
-
1802
- // 音量アイコンを更新
1803
- updateVolumeIcon();
1804
- }
1805
-
1806
- function applyPlaybackRate() {
1807
- if (!isAudioCombined) return;
1808
-
1809
- const speed = parseFloat(playbackSpeedSlider.value);
1810
- currentPlaybackRate = speed;
1811
- video.playbackRate = speed;
1812
-
1813
- if (combinedAudioElement) {
1814
- combinedAudioElement.playbackRate = speed;
1815
- }
1816
-
1817
- speedValue.textContent = speed.toFixed(2) + 'x';
1818
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1819
- speedSlider.value = speed;
1820
- }
1821
-
1822
- // プレイヤーコントロールを有効化
1823
- function enablePlayerControls() {
1824
- disabledOverlay.style.display = 'none';
1825
- playPauseBtn.disabled = false;
1826
- volumeBtn.disabled = false;
1827
- volumeSlider.disabled = false;
1828
- speedSlider.disabled = false;
1829
- fullscreenBtn.disabled = false;
1830
- startTimeInput.disabled = false;
1831
- endTimeInput.disabled = false;
1832
- resetEndTimeBtn.disabled = false;
1833
- loopCheckbox.disabled = false;
1834
- globalVolumeSlider.disabled = false;
1835
- setStartTimeBtn.disabled = false;
1836
- setEndTimeBtn.disabled = false;
1837
- playbackSpeedSlider.disabled = false;
1838
- applyTimeBtn.disabled = false;
1839
- }
1840
-
1841
- // プレビュー再生
1842
- function togglePreview() {
1843
- if (!isAudioCombined || !combinedAudioElement) return;
1844
-
1845
- if (previewButton.textContent === '▶') {
1846
- // 再生
1847
- combinedAudioElement.currentTime = 0;
1848
- combinedAudioElement.play()
1849
- .then(() => {
1850
- previewButton.textContent = '⏸';
1851
-
1852
- // プレビューの時間表示を更新
1853
- const updatePreviewTime = () => {
1854
- if (!combinedAudioElement || !isAudioCombined) return;
1855
-
1856
- const currentTime = combinedAudioElement.currentTime;
1857
- const duration = combinedAudioElement.duration;
1858
-
1859
- if (currentTime >= duration) {
1860
- previewButton.textContent = '▶';
1861
- previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1862
- return;
1863
- }
1864
-
1865
- previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1866
- requestAnimationFrame(updatePreviewTime);
1867
- };
1868
-
1869
- updatePreviewTime();
1870
- })
1871
- .catch(e => console.error('プレビュー再生エラー:', e));
1872
- } else {
1873
- // 一時停止
1874
- combinedAudioElement.pause();
1875
- previewButton.textContent = '▶';
1876
- }
1877
- }
1878
-
1879
- // 時間をフォーマットするヘルパー関数
1880
- function formatTime(seconds) {
1881
- const mins = Math.floor(seconds / 60);
1882
- const secs = Math.floor(seconds % 60);
1883
- return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
1884
- }
1885
 
1886
- // 動画のメタデータが読み込まれたら
1887
- video.addEventListener('loadedmetadata', function() {
1888
- try {
1889
- videoDuration = video.duration;
1890
- endTimeInput.value = videoDuration.toFixed(2);
1891
- endTimeInput.max = videoDuration;
1892
- startTimeInput.max = videoDuration - 0.1;
1893
- updateTimeDisplay();
1894
- checkLoadingComplete();
1895
- } catch (error) {
1896
- handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
1897
- }
1898
- });
1899
-
1900
- // 動画エラー処理
1901
- video.addEventListener('error', function() {
1902
- handleError(video.error, '動画読み込み中にエラーが発生しました');
1903
- });
1904
-
1905
- // 再生ボタンクリック
1906
- playPauseBtn.addEventListener('click', function() {
1907
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
1908
- if (video.currentTime >= endTime) {
1909
- const startTime = parseFloat(startTimeInput.value) || 0;
1910
- seekMedia(startTime);
1911
- }
1912
- togglePlayPause();
1913
- });
1914
-
1915
- // 時間表示を更新
1916
- function updateTimeDisplay() {
1917
- const now = performance.now();
1918
- if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
1919
- lastUpdateTime = now;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1920
 
1921
- try {
1922
- const currentTime = video.currentTime;
1923
- const duration = video.duration || videoDuration;
1924
 
1925
- const currentMinutes = Math.floor(currentTime / 60);
1926
- const currentSeconds = Math.floor(currentTime % 60);
1927
- const currentMilliseconds = Math.floor((currentTime % 1) * 100);
1928
- const durationMinutes = Math.floor(duration / 60);
1929
- const durationSeconds = Math.floor(duration % 60);
1930
- const durationMilliseconds = Math.floor((duration % 1) * 100);
1931
 
1932
- timeDisplay.textContent =
1933
- `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
1934
- `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
 
 
1935
 
1936
- const progressPercent = (currentTime / duration) * 100;
1937
- progressBar.style.width = `${progressPercent}%`;
1938
- } catch (error) {
1939
- console.error('時間表示更新エラー:', error);
1940
- }
1941
- }
1942
-
1943
- // 再生/一時停止をトグル
1944
- function togglePlayPause() {
1945
- if (isPlaying) {
1946
- pauseMedia();
1947
- } else {
1948
- playMedia();
1949
- }
1950
- }
1951
-
1952
- function playMedia() {
1953
- try {
1954
- const duration = video.duration || videoDuration;
1955
- const startTime = parseFloat(startTimeInput.value) || 0;
1956
- const endTime = parseFloat(endTimeInput.value) || duration;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1957
 
1958
- if (video.currentTime >= endTime) {
1959
- video.currentTime = startTime;
 
 
1960
  }
1961
 
1962
- const playPromise = video.play();
1963
 
1964
- if (playPromise !== undefined) {
1965
- playPromise.then(() => {
1966
- isPlaying = true;
1967
- playPauseBtn.textContent = '⏸';
1968
-
1969
- // 音声を同期して再生
1970
- if (isAudioCombined) {
1971
- combinedAudioElement.currentTime = video.currentTime;
1972
- combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1973
- }
1974
-
1975
- startSyncCheck();
1976
-
1977
- video.playbackRate = currentPlaybackRate;
1978
- }).catch(error => {
1979
- console.error('動画再生エラー:', error);
1980
- isPlaying = false;
1981
- playPauseBtn.textContent = '▶';
1982
- });
1983
- }
1984
- } catch (error) {
1985
- console.error('メディア再生エラー:', error);
1986
  isPlaying = false;
1987
  playPauseBtn.textContent = '▶';
1988
- }
1989
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
1990
 
1991
- function pauseMedia() {
1992
- try {
1993
- video.pause();
1994
- isPlaying = false;
1995
- playPauseBtn.textContent = '';
1996
- stopSyncCheck();
1997
-
1998
- if (combinedAudioElement) {
1999
- combinedAudioElement.pause();
2000
- }
2001
- } catch (error) {
2002
- console.error('メディア一時停止エラー:', error);
 
 
 
 
 
2003
  }
 
 
 
2004
  }
2005
-
2006
- // 時間更新時の処理
2007
- video.addEventListener('timeupdate', function() {
2008
- const duration = video.duration || videoDuration;
2009
- const endTime = parseFloat(endTimeInput.value) || duration;
2010
 
2011
- if (video.currentTime >= endTime && endTime > 0) {
2012
- if (loopCheckbox.checked) {
2013
- const startTime = parseFloat(startTimeInput.value) || 0;
2014
- video.currentTime = startTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2015
  if (combinedAudioElement) {
2016
- combinedAudioElement.currentTime = startTime;
 
 
 
2017
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2018
  } else {
2019
- pauseMedia();
2020
- video.currentTime = endTime;
 
 
 
 
 
 
 
 
 
 
 
 
2021
  }
2022
  }
2023
 
2024
- updateTimeDisplay();
2025
- });
2026
-
2027
- // プログレスバークリックでシーク
2028
- progressContainer.addEventListener('click', function(e) {
2029
- if (!video.duration) return;
2030
-
2031
- const rect = this.getBoundingClientRect();
2032
- const pos = (e.clientX - rect.left) / rect.width;
2033
- const seekTime = pos * video.duration;
2034
-
2035
- seekMedia(seekTime);
2036
- });
2037
-
2038
- // 指定した時間にシーク (改良版)
2039
- function seekMedia(time) {
2040
- try {
2041
- const duration = video.duration || videoDuration;
2042
- const startTime = parseFloat(startTimeInput.value) || 0;
2043
- const endTime = parseFloat(endTimeInput.value) || duration;
2044
-
2045
- const seekTime = Math.max(startTime, Math.min(time, endTime));
2046
- video.currentTime = seekTime;
2047
-
2048
- if (combinedAudioElement) {
2049
- combinedAudioElement.currentTime = seekTime;
2050
- if (isPlaying) {
2051
- combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
2052
- }
2053
- }
2054
- } catch (error) {
2055
- console.error('メディアシークエラー:', error);
2056
- }
2057
- }
2058
-
2059
- // プログレスバー上でマウス移動時に時間を表示
2060
- progressContainer.addEventListener('mousemove', function(e) {
2061
- if (!video.duration) return;
2062
-
2063
- const rect = this.getBoundingClientRect();
2064
- const pos = (e.clientX - rect.left) / rect.width;
2065
- const hoverTime = pos * video.duration;
2066
-
2067
- const minutes = Math.floor(hoverTime / 60);
2068
- const seconds = Math.floor(hoverTime % 60);
2069
- const milliseconds = Math.floor((hoverTime % 1) * 100);
2070
-
2071
- progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
2072
- progressTime.style.display = 'block';
2073
- progressTime.style.left = `${pos * 100}%`;
2074
- });
2075
-
2076
- progressContainer.addEventListener('mouseleave', function() {
2077
- progressTime.style.display = 'none';
2078
- });
2079
-
2080
- // 動画クリックで再生/一時停止
2081
- video.addEventListener('click', function() {
2082
- togglePlayPause();
2083
- });
2084
 
2085
- volumeSlider.addEventListener('input', function() {
2086
- if (!isAudioCombined) return;
2087
-
2088
- lastVolume = parseFloat(this.value);
2089
- applyVolume();
2090
- });
2091
-
2092
- volumeBtn.addEventListener('click', function() {
2093
- if (!isAudioCombined) return;
2094
-
2095
- if (video.volume > 0) {
2096
- lastVolume = parseFloat(volumeSlider.value);
2097
- volumeSlider.value = 0;
2098
- } else {
2099
- volumeSlider.value = lastVolume;
2100
- }
2101
-
2102
- applyVolume();
2103
- });
2104
-
2105
- // 音量アイコンを更新
2106
- function updateVolumeIcon() {
2107
- if (video.volume === 0) {
2108
- volumeBtn.textContent = '🔇';
2109
- } else if (video.volume < 3) {
2110
- volumeBtn.textContent = '🔈';
2111
- } else {
2112
- volumeBtn.textContent = '🔊';
2113
- }
2114
- }
2115
-
2116
- // 再生速度スライダー (動画プレイヤー)
2117
- speedSlider.addEventListener('input', function() {
2118
- if (!isAudioCombined) return;
2119
-
2120
- const speed = parseFloat(this.value);
2121
- speedValue.textContent = speed.toFixed(2) + 'x';
2122
- playbackSpeedSlider.value = speed;
2123
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2124
- updatePlaybackRate(speed);
2125
- });
2126
-
2127
- // 再生速度スライダー (設定メニュー)
2128
- playbackSpeedSlider.addEventListener('input', function() {
2129
- if (!isAudioCombined) return;
2130
-
2131
- const speed = parseFloat(this.value);
2132
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2133
- speedSlider.value = speed;
2134
- speedValue.textContent = speed.toFixed(2) + 'x';
2135
- updatePlaybackRate(speed);
2136
- });
2137
-
2138
- // テンポ入力による再生速度更新
2139
- tempoInput.addEventListener('input', function() {
2140
- const tempo = parseFloat(this.value);
2141
- const baseTempo = isTMode ? 66 : 92;
2142
- const speed = tempo / baseTempo;
2143
-
2144
- const clampedSpeed = Math.max(0.001, Math.min(5.0, speed));
2145
-
2146
- playbackSpeedSlider.value = clampedSpeed;
2147
- playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2148
- speedSlider.value = clampedSpeed;
2149
- speedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2150
- tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2151
-
2152
- updatePlaybackRate(clampedSpeed);
2153
- });
2154
-
2155
- function updatePlaybackRate(speed) {
2156
- if (!isAudioCombined) return;
2157
-
2158
- currentPlaybackRate = speed;
2159
- video.playbackRate = speed;
2160
-
2161
- // ピッチ保持を再設定
2162
- video.preservesPitch = true;
2163
- video.mozPreservesPitch = true;
2164
- video.webkitPreservesPitch = true;
2165
-
2166
- if (combinedAudioElement) {
2167
- combinedAudioElement.playbackRate = speed;
2168
-
2169
- // 合成音声のピッチ保持を設定
2170
- combinedAudioElement.preservesPitch = true;
2171
- combinedAudioElement.mozPreservesPitch = true;
2172
- combinedAudioElement.webkitPreservesPitch = true;
2173
- }
2174
- }
2175
-
2176
- // 全画面ボタン
2177
- fullscreenBtn.addEventListener('click', function() {
2178
- if (!isFullscreen) {
2179
- if (videoContainer.requestFullscreen) {
2180
- videoContainer.requestFullscreen();
2181
- } else if (videoContainer.webkitRequestFullscreen) {
2182
- videoContainer.webkitRequestFullscreen();
2183
- } else if (videoContainer.msRequestFullscreen) {
2184
- videoContainer.msRequestFullscreen();
2185
- }
2186
- } else {
2187
- if (document.exitFullscreen) {
2188
- document.exitFullscreen();
2189
- } else if (document.webkitExitFullscreen) {
2190
- document.webkitExitFullscreen();
2191
- } else if (document.msExitFullscreen) {
2192
- document.msExitFullscreen();
2193
- }
2194
- }
2195
- });
2196
-
2197
- // 全画面変更イベント
2198
- document.addEventListener('fullscreenchange', handleFullscreenChange);
2199
- document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
2200
- document.addEventListener('msfullscreenchange', handleFullscreenChange);
2201
-
2202
- function handleFullscreenChange() {
2203
- isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
2204
- fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
2205
- video.controls = false;
2206
-
2207
- // 全画面時にロックボタンを表示
2208
- lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none';
2209
-
2210
- // 全画面時にコントロールバー自動非表示機能を有効化
2211
- if (isFullscreen) {
2212
- resetControlsHideTimer();
2213
- document.addEventListener('mousemove', handleFullscreenMouseMove);
2214
- } else {
2215
- document.removeEventListener('mousemove', handleFullscreenMouseMove);
2216
- clearTimeout(controlsHideTimeout);
2217
- showControls();
2218
- }
2219
- }
2220
-
2221
- // 全画面時のマウス移動処理
2222
- function handleFullscreenMouseMove() {
2223
- if (!isControlsLocked) {
2224
- showControls();
2225
- resetControlsHideTimer();
2226
- }
2227
- }
2228
-
2229
- // コントロールバーを表示
2230
- function showControls() {
2231
- if (!controlsVisible) {
2232
- videoControls.style.opacity = '1';
2233
- controlsVisible = true;
2234
- }
2235
- }
2236
-
2237
- // コントロールバーを非表示
2238
- function hideControls() {
2239
- if (!isControlsLocked && controlsVisible) {
2240
- videoControls.style.opacity = '0';
2241
- controlsVisible = false;
2242
- }
2243
- }
2244
-
2245
- // コントロールバー非表示タイマーをリセット
2246
- function resetControlsHideTimer() {
2247
- clearTimeout(controlsHideTimeout);
2248
- if (!isControlsLocked) {
2249
- controlsHideTimeout = setTimeout(hideControls, 1500); // 1.5秒後に非表示
2250
- }
2251
- }
2252
-
2253
- // ロックボタンのクリック処理
2254
- lockControlsBtn.addEventListener('click', function() {
2255
- isControlsLocked = !isControlsLocked;
2256
- this.classList.toggle('locked', isControlsLocked);
2257
-
2258
- if (isControlsLocked) {
2259
- showControls();
2260
- clearTimeout(controlsHideTimeout);
2261
- } else {
2262
- resetControlsHideTimer();
2263
- }
2264
- });
2265
-
2266
- // キーボードイベント (ESCで全画面終了)
2267
- document.addEventListener('keydown', function(e) {
2268
- if (e.key === 'Escape' && isFullscreen) {
2269
- if (document.exitFullscreen) {
2270
- document.exitFullscreen();
2271
- } else if (document.webkitExitFullscreen) {
2272
- document.webkitExitFullscreen();
2273
- } else if (document.msExitFullscreen) {
2274
- document.msExitFullscreen();
2275
- }
2276
- }
2277
- });
2278
-
2279
- // ボリュームスライダーのイベント
2280
- audioSliders.forEach((slider, index) => {
2281
- slider.addEventListener('input', function() {
2282
- const value = parseFloat(this.value);
2283
- volumeValues[index].textContent = value.toFixed(2);
2284
-
2285
- const percent = value * 100;
2286
- this.style.backgroundSize = `${percent}% 100%`;
2287
- });
2288
- });
2289
-
2290
- globalVolumeSlider.addEventListener('input', function() {
2291
- const value = parseFloat(this.value);
2292
- globalVolumeValue.textContent = value.toFixed(1);
2293
-
2294
- const percent = (value - this.min) / (this.max - this.min) * 100;
2295
- this.style.backgroundSize = `${percent}% 100%`;
2296
-
2297
- applyVolume();
2298
- });
2299
-
2300
- // ループ設定変更時
2301
- loopCheckbox.addEventListener('change', function() {
2302
- // 合成音声ではループは動画に依存する
2303
- });
2304
-
2305
- // 現在の秒数を開始時間に設定
2306
- setStartTimeBtn.addEventListener('click', function() {
2307
- startTimeInput.value = video.currentTime.toFixed(2);
2308
- updateProgressMarkers();
2309
- });
2310
-
2311
- // 現在の秒数を終了時間に設定
2312
- setEndTimeBtn.addEventListener('click', function() {
2313
- endTimeInput.value = video.currentTime.toFixed(2);
2314
- updateProgressMarkers();
2315
- });
2316
-
2317
- // 終了時間を動画の長さにリセット
2318
- resetEndTimeBtn.addEventListener('click', function() {
2319
- endTimeInput.value = video.duration.toFixed(2);
2320
- updateProgressMarkers();
2321
- });
2322
-
2323
- // プログレスバーのマーカーを更新
2324
- function updateProgressMarkers() {
2325
- const duration = video.duration || videoDuration;
2326
- const startTime = parseFloat(startTimeInput.value) || 0;
2327
- const endTime = parseFloat(endTimeInput.value) || duration;
2328
-
2329
- if (duration > 0) {
2330
- startMarker.style.left = `${(startTime / duration) * 100}%`;
2331
- endMarker.style.left = `${(endTime / duration) * 100}%`;
2332
-
2333
- startMarker.style.display = 'block';
2334
- endMarker.style.display = 'block';
2335
- }
2336
- }
2337
-
2338
- // 開始/終了時間変更時にマーカーを更新
2339
- startTimeInput.addEventListener('input', updateProgressMarkers);
2340
- endTimeInput.addEventListener('input', updateProgressMarkers);
2341
-
2342
- // 合成ボタンクリック
2343
- combineButton.addEventListener('click', combineAudio);
2344
-
2345
- // プレビューボタンクリック
2346
- previewButton.addEventListener('click', togglePreview);
2347
-
2348
- // 同期ステータスを閉じる
2349
- syncStatusClose.addEventListener('click', function() {
2350
- syncStatus.style.display = 'none';
2351
- });
2352
-
2353
- // 初期化
2354
- loadAudioFiles();
2355
- updateVolumeIcon();
2356
- volumeSlider.value = video.volume;
2357
- video.controls = false;
2358
 
2359
- // スライダーの背景を初期化
2360
- function initSliderBackgrounds() {
2361
- const sliders = [
2362
- volumeSlider,
2363
- speedSlider,
2364
- globalVolumeSlider,
2365
- playbackSpeedSlider,
2366
- ...audioSliders
2367
- ];
2368
-
2369
- sliders.forEach(slider => {
2370
- if (slider) {
2371
- slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
2372
- slider.style.backgroundRepeat = 'no-repeat';
2373
-
2374
- if (slider === globalVolumeSlider) {
2375
- const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
2376
- slider.style.backgroundSize = `${percent}% 100%`;
2377
- globalVolumeValue.textContent = slider.value;
2378
- } else {
2379
- slider.style.backgroundSize = `${slider.value * 100}% 100%`;
2380
- }
2381
- }
2382
- });
2383
  }
2384
-
2385
- initSliderBackgrounds();
2386
- startSyncCheck(); // 同期チェックを開始
2387
-
2388
- // 初期テンポ設定
2389
- tempoInput.value = isTMode ? 66 : 92;
2390
- tempoInput.dispatchEvent(new Event('input'));
2391
- });
2392
- // タイムマーカー関連のコードを修正
 
 
 
 
2393
  document.addEventListener('DOMContentLoaded', function() {
2394
- // まず必要なDOM要素を取得
2395
  const startMarker = document.getElementById('start-marker');
2396
  const endMarker = document.getElementById('end-marker');
2397
  const video = document.getElementById('video');
@@ -2491,12 +2507,22 @@ document.addEventListener('DOMContentLoaded', function() {
2491
  if(startMarker) startMarker.style.left = `${(startTime / duration) * 100}%`;
2492
  if(endMarker) endMarker.style.left = `${(endTime / duration) * 100}%`;
2493
 
2494
- if(startMarker) startMarker.style.display = 'block';
2495
- if(endMarker) endMarker.style.display = 'block';
 
 
 
 
 
 
 
 
 
 
 
2496
  }
2497
  }
2498
  });
2499
-
2500
  </script>
2501
  </body>
2502
  </html>
 
379
  /* プログレスバーのマーカー */
380
  .progress-marker {
381
  position: absolute;
382
+ bottom: 0px;
383
  width: 0;
384
  height: 0;
385
  border-left: 5px solid transparent;
 
1200
  <button class="time-set-button" id="reset-end-time" disabled>動画の長さに戻す</button>
1201
  </div>
1202
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1203
  <div class="setting-item">
1204
  <label for="loop">ループ再生:</label>
1205
  <input type="checkbox" id="loop" disabled>
1206
  </div>
1207
+ <div class="setting-item">
1208
+ <button class="combine-button" id="apply-time-btn" disabled>時間設定を適用</button>
1209
+ </div>
1210
+ <h2>設定</h2>
1211
  <div class="setting-item">
1212
  <div class="global-volume-container">
1213
  <label>全体音量係数:</label>
 
1234
  <!-- 全画面時のロックボタン -->
1235
  <button class="lock-controls-btn" id="lock-controls-btn" title="コントロールバーを固定">🔒</button>
1236
  <script>
1237
+ document.addEventListener('DOMContentLoaded', function() {
1238
+ // 同期管理用の変数
1239
+ let lastSyncTime = 0;
1240
+ let isBuffering = false;
1241
+ let syncDriftLog = [];
1242
+ let syncCheckInterval;
1243
+ let audioContext;
1244
+ let controlsHideTimeout;
1245
+ let isControlsLocked = false;
1246
+ let controlsVisible = true;
1247
+ let isCheckingSync = false;
1248
+ let isInBackgroundTab = false;
1249
+ let isBlankMode = false; // 消画モードフラグ
1250
+
1251
+ try {
1252
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
1253
+ } catch (e) {
1254
+ console.error('Web Audio APIがサポートされていません:', e);
1255
+ }
1256
+
1257
+ // テクノロジー風背景を生成
1258
+ function createTechBackground() {
1259
+ const bg = document.getElementById('techBg');
1260
+
1261
+ for (let i = 0; i < 200; i++) {
1262
+ const line = document.createElement('div');
1263
+ line.className = 'circuit-line';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1264
 
1265
+ const isHorizontal = Math.random() > 0.5;
1266
+ if (isHorizontal) {
1267
+ line.style.width = `${Math.random() * 300 + 100}px`;
1268
+ line.style.height = '1px';
1269
+ } else {
1270
+ line.style.width = '1px';
1271
+ line.style.height = `${Math.random() * 300 + 100}px`;
1272
  }
1273
 
1274
+ line.style.left = `${Math.random() * 100}%`;
1275
+ line.style.top = `${Math.random() * 100}%`;
1276
+ line.style.opacity = Math.random() * 0.5 + 0.1;
1277
+ bg.appendChild(line);
1278
+ }
1279
+
1280
+ for (let i = 0; i < 200; i++) {
1281
+ const dot = document.createElement('div');
1282
+ dot.className = 'grid-dot';
1283
+ dot.style.left = `${Math.random() * 100}%`;
1284
+ dot.style.top = `${Math.random() * 100}%`;
1285
+ bg.appendChild(dot);
1286
+ }
 
 
 
 
 
1287
 
1288
+ for (let i = 0; i < 15; i++) {
1289
+ const hex = document.createElement('div');
1290
+ hex.className = 'hexagon';
1291
+ hex.style.left = `${Math.random() * 100}%`;
1292
+ hex.style.top = `${Math.random() * 100}%`;
1293
+ hex.style.transform = `rotate(${Math.random() * 360}deg)`;
1294
+ hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
1295
+ bg.appendChild(hex);
1296
+ }
1297
+
1298
+ for (let i = 0; i < 8; i++) {
1299
+ const pulse = document.createElement('div');
1300
+ pulse.className = 'pulse';
1301
+ pulse.style.left = `${Math.random() * 100}%`;
1302
+ pulse.style.top = `${Math.random() * 100}%`;
1303
+ pulse.style.animationDelay = `${Math.random() * 2}s`;
1304
+ bg.appendChild(pulse);
1305
+ }
1306
  }
1307
 
1308
+ createTechBackground();
1309
+
1310
+ const urlParams = new URLSearchParams(window.location.search);
1311
+ const isTMode = urlParams.has('mode') && urlParams.get('mode') === 't';
1312
+ const basePath = isTMode ? '/t/' : '/';
1313
+
1314
+ // ローディング状態を管理
1315
+ let loadingCount = 0;
1316
+ let totalToLoad = 6; // 動画 + 5つの音声ファイル
1317
+ let lastUpdateTime = 0;
1318
+ const updateInterval = 1;
1319
 
1320
+ function checkLoadingComplete() {
1321
+ loadingCount++;
1322
+ if (loadingCount >= totalToLoad) {
1323
+ setTimeout(function() {
1324
+ const loadingOverlay = document.getElementById('loadingOverlay');
1325
+ loadingOverlay.style.opacity = '0';
1326
+ setTimeout(function() {
1327
+ loadingOverlay.style.display = 'none';
1328
+ }, 1000);
1329
+ }, 500);
1330
+ }
1331
+ }
1332
+
1333
+ function handleError(error, message) {
1334
+ console.error(message, error);
1335
+ window.alert(`${message}\n\nエラー詳細: ${error.message}`);
1336
+ }
1337
+
1338
+ // 要素を取得
1339
+ const video = document.getElementById('video');
1340
+ video.preservesPitch = true;
1341
+ video.mozPreservesPitch = true; // Firefox用
1342
+ video.webkitPreservesPitch = true; // 古いWebKit用
1343
+ const videoContainer = document.getElementById('video-container');
1344
+ const playPauseBtn = document.getElementById('play-pause-btn');
1345
+ const timeDisplay = document.getElementById('time-display');
1346
+ const progressContainer = document.getElementById('progress-container');
1347
+ const progressBar = document.getElementById('progress-bar');
1348
+ const progressTime = document.getElementById('progress-time');
1349
+ const volumeBtn = document.getElementById('volume-btn');
1350
+ const volumeSlider = document.getElementById('volume-slider');
1351
+ const speedSlider = document.getElementById('speed-slider');
1352
+ const speedValue = document.getElementById('speed-value');
1353
+ const playbackSpeedSlider = document.getElementById('playback-speed');
1354
+ const playbackSpeedValue = document.getElementById('playback-speed-value');
1355
+ const fullscreenBtn = document.getElementById('fullscreen-btn');
1356
+ const startTimeInput = document.getElementById('start-time');
1357
+ const endTimeInput = document.getElementById('end-time');
1358
+ const loopCheckbox = document.getElementById('loop');
1359
+ const globalVolumeSlider = document.getElementById('global-volume');
1360
+ const globalVolumeValue = document.getElementById('global-volume-value');
1361
+ const audioSliders = document.querySelectorAll('.audio-slider');
1362
+ const volumeValues = document.querySelectorAll('.volume-value');
1363
+ const setStartTimeBtn = document.getElementById('set-start-time');
1364
+ const setEndTimeBtn = document.getElementById('set-end-time');
1365
+ const resetEndTimeBtn = document.getElementById('reset-end-time');
1366
+ const disabledOverlay = document.getElementById('disabledOverlay');
1367
+ const combineButton = document.getElementById('combine-button');
1368
+ const combineStatus = document.getElementById('combine-status');
1369
+ const previewSection = document.getElementById('preview-section');
1370
+ const previewButton = document.getElementById('preview-button');
1371
+ const previewTime = document.getElementById('preview-time');
1372
+ const bufferingIndicator = document.getElementById('buffering-indicator');
1373
+ const syncStatus = document.getElementById('sync-status');
1374
+ const syncStatusText = document.getElementById('sync-status-text');
1375
+ const syncStatusClose = document.getElementById('sync-status-close');
1376
+ const lockControlsBtn = document.getElementById('lock-controls-btn');
1377
+ const startMarker = document.getElementById('start-marker');
1378
+ const endMarker = document.getElementById('end-marker');
1379
+ const tempoInput = document.getElementById('tempo');
1380
+ const tempoSpeedValue = document.getElementById('tempo-speed-value');
1381
+ const videoControls = document.querySelector('.video-controls');
1382
+ const applyTimeBtn = document.getElementById('apply-time-btn');
1383
+ const blankModeBtn = document.getElementById('blank-mode-btn'); // 消画モードボタン
1384
+
1385
+ // 音声オブジェクトを作成
1386
+ const audioElements = {};
1387
+ const audioBuffers = {};
1388
+ const audioFiles = ['p', 'a', 't', 's', 'k'];
1389
+ let combinedAudioElement = null;
1390
+ let isAudioCombined = false;
1391
+ let currentVolumes = { p: 0, a: 1, t: 1, s: 1, k: 0 };
1392
+ let appliedStartTime = 0;
1393
+ let appliedEndTime = 0;
1394
+
1395
+ // 初期化
1396
+ let videoDuration = 0;
1397
+ let isPlaying = false;
1398
+ let lastVolume = 1;
1399
+ let currentPlaybackRate = 1;
1400
+ let isFullscreen = false;
1401
+
1402
+ // 消画モード切り替え
1403
+ function toggleBlankMode() {
1404
+ isBlankMode = !isBlankMode;
1405
+ if (isBlankMode) {
1406
+ video.style.backgroundColor = 'black';
1407
+ video.style.opacity = '0';
1408
+ blankModeBtn.textContent = '消画モード: ON';
1409
+ blankModeBtn.classList.add('active');
1410
+ } else {
1411
+ video.style.backgroundColor = '';
1412
+ video.style.opacity = '1';
1413
+ blankModeBtn.textContent = '消画モード: OFF';
1414
+ blankModeBtn.classList.remove('active');
1415
  }
1416
  }
1417
+
1418
+ async function enterPiP() {
1419
+ if (!document.pictureInPictureElement && !video.paused) {
1420
+ try {
1421
+ await video.requestPictureInPicture();
1422
+ } catch (err) {
1423
+ console.warn('PiP開始失敗:', err);
1424
+ }
1425
+ }
1426
+ }
1427
+
1428
+ async function exitPiP() {
1429
+ if (document.pictureInPictureElement) {
1430
+ try {
1431
+ await document.exitPictureInPicture();
1432
+ } catch (err) {
1433
+ console.warn('PiP終了失敗:', err);
1434
+ }
1435
+ }
1436
+ }
1437
+
1438
+ // 動画のバッファリング状態を監視
1439
+ video.addEventListener('waiting', function() {
1440
+ isBuffering = true;
1441
+ bufferingIndicator.style.display = 'block';
1442
  if (combinedAudioElement) {
1443
+ combinedAudioElement.pause();
1444
+ }
1445
+ });
1446
+
1447
+ video.addEventListener('playing', function() {
1448
+ isBuffering = false;
1449
+ bufferingIndicator.style.display = 'none';
1450
+ if (combinedAudioElement && isPlaying) {
1451
+ syncAudioWithVideo();
1452
+ }
1453
+ });
1454
+
1455
+ video.addEventListener('suspend', function() {
1456
+ console.log('動画の読み込みが一時停止しました');
1457
+ });
1458
+
1459
+ video.addEventListener('stalled', function() {
1460
+ console.log('動画の読み込みが停滞しました');
1461
+ if (isPlaying) {
1462
+ pauseMedia();
1463
  }
1464
+ });
1465
+
1466
+ function startSyncCheck() {
1467
+ if (isCheckingSync) return;
1468
+ isCheckingSync = true;
1469
+ if (syncCheckInterval) clearInterval(syncCheckInterval);
1470
+ syncCheckInterval = setInterval(checkSync, 1000);
1471
  }
1472
 
1473
+ function stopSyncCheck() {
1474
+ isCheckingSync = false;
1475
+ if (syncCheckInterval) clearInterval(syncCheckInterval);
1476
  }
1477
 
1478
+ function checkSync() {
1479
+ if (!isAudioCombined || !isPlaying || isBuffering || isInBackgroundTab) return;
1480
+
1481
+ const videoTime = video.currentTime;
1482
+ const audioTime = combinedAudioElement.currentTime;
1483
+ const drift = videoTime - audioTime;
1484
+
1485
+ syncDriftLog.push(drift);
1486
+ if (syncDriftLog.length > 5) syncDriftLog.shift();
1487
+
1488
+ const avgDrift = syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
1489
+ syncStatusText.textContent = `同期ズレ: ${avgDrift.toFixed(3)}秒`;
1490
+
1491
+ if (Math.abs(avgDrift) > 0.1) {
1492
+ console.log(`同期ズレを修正: ${avgDrift.toFixed(3)}秒`);
1493
+ syncAudioWithVideo();
1494
+ }
1495
+ }
1496
 
1497
+ applyTimeBtn.addEventListener('click', function() {
1498
+ // 現在再生中なら一時停止
1499
+ const wasPlaying = isPlaying;
1500
+ if (isPlaying) {
1501
+ pauseMedia();
1502
+ }
1503
+
1504
+ // 開始時間と終了時間を取得
1505
+ appliedStartTime = parseFloat(startTimeInput.value) || 0;
1506
+ appliedEndTime = parseFloat(endTimeInput.value) || video.duration;
1507
+
1508
+ // 現在位置が開始時間より前なら開始時間に移動
1509
+ if (video.currentTime < appliedStartTime) {
1510
+ video.currentTime = appliedStartTime;
1511
+ if (combinedAudioElement) {
1512
+ combinedAudioElement.currentTime = appliedStartTime;
1513
+ }
1514
+ }
1515
+ // 現在位置が終了時間より後なら開始時間に移動
1516
+ else if (video.currentTime > appliedEndTime) {
1517
+ video.currentTime = appliedStartTime;
1518
+ if (combinedAudioElement) {
1519
+ combinedAudioElement.currentTime = appliedStartTime;
1520
+ }
1521
+ }
1522
+
1523
+ // 再生中だった場合は再開
1524
+ if (wasPlaying) {
1525
+ playMedia();
1526
+ }
1527
+
1528
+ // マーカーを更新
1529
+ updateProgressMarkers();
1530
+ });
1531
+
1532
+ // 音声を動画に同期させる関数
1533
+ function syncAudioWithVideo() {
1534
+ if (!isAudioCombined || !isPlaying) return;
1535
+
1536
+ const currentTime = video.currentTime;
1537
+
1538
+ if (combinedAudioElement) {
1539
+ combinedAudioElement.currentTime = currentTime;
1540
+ if (isPlaying) {
1541
+ combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1542
+ }
1543
+ }
1544
+
1545
+ // ズレ記録をリセット
1546
+ syncDriftLog = [];
1547
+ lastSyncTime = performance.now();
1548
+ }
1549
+
1550
+ // 音声ファイルをロード
1551
+ function loadAudioFiles() {
1552
+ audioFiles.forEach(file => {
1553
+ try {
1554
+ const audio = new Audio(`${basePath}${file}.mp3`);
1555
+ audio.preload = 'auto';
1556
+ audio.loop = false;
1557
+ audioElements[file] = audio;
1558
+
1559
+ audio.addEventListener('loadedmetadata', function() {
1560
+ console.log(`${basePath}${file}.mp3 loaded`);
1561
+ checkLoadingComplete();
1562
+ });
1563
+
1564
+ audio.addEventListener('error', function() {
1565
+ console.error(`音声ファイル読み込みエラー (${basePath}${file}.mp3):`, audio.error);
1566
+ checkLoadingComplete();
1567
+ });
1568
+ } catch (error) {
1569
+ console.error(`音声ファイル初期化エラー (${basePath}${file}.mp3):`, error);
1570
+ checkLoadingComplete();
1571
+ }
1572
+ });
1573
+ }
1574
+
1575
+ // 音声を結合する関数
1576
+ async function combineAudio() {
1577
+ // 既存の音声を停止して解放
1578
+ if (combinedAudioElement) {
1579
+ combinedAudioElement.pause();
1580
+ combinedAudioElement.src = '';
1581
+ combinedAudioElement = null;
1582
+ }
1583
+
1584
+ combineButton.disabled = true;
1585
+ combineStatus.textContent = "音声を合成中...";
1586
+
1587
+ try {
1588
+ // 現在の音量設定を保存
1589
+ audioFiles.forEach(file => {
1590
+ currentVolumes[file] = parseFloat(document.querySelector(`.audio-slider[data-audio="${file}"]`).value);
1591
+ });
1592
+
1593
+ // 各音声ファイルをデコード
1594
+ const audioBufferPromises = audioFiles.map(async file => {
1595
+ const audio = audioElements[file];
1596
+ if (!audio) return null;
1597
+
1598
+ // 音量が0の場合はスキップ
1599
+ if (currentVolumes[file] === 0) return null;
1600
+
1601
+ const response = await fetch(`${basePath}${file}.mp3`);
1602
+ const arrayBuffer = await response.arrayBuffer();
1603
+ return await audioContext.decodeAudioData(arrayBuffer);
1604
+ });
1605
+
1606
+ // すべての音声バッファを取得
1607
+ const buffers = await Promise.all(audioBufferPromises);
1608
+ audioFiles.forEach((file, index) => {
1609
+ audioBuffers[file] = buffers[index];
1610
+ });
1611
+
1612
+ // 最長の音声バッファの長さを取得
1613
+ const maxDuration = Math.max(...buffers.filter(b => b).map(b => b.duration));
1614
+
1615
+ // 新しい音声バッファを作成
1616
+ const combinedAudioBuffer = audioContext.createBuffer(
1617
+ 2, // ステレオ
1618
+ audioContext.sampleRate * maxDuration,
1619
+ audioContext.sampleRate
1620
+ );
1621
+
1622
+ // 各音声バッファを結合
1623
+ for (let file of audioFiles) {
1624
+ if (!audioBuffers[file] || currentVolumes[file] === 0) continue;
1625
+
1626
+ const buffer = audioBuffers[file];
1627
+ const volume = currentVolumes[file];
1628
+
1629
+ // 各チャンネルに音声を加算
1630
+ for (let channel = 0; channel < 2; channel++) {
1631
+ const inputData = buffer.getChannelData(channel % buffer.numberOfChannels);
1632
+ const outputData = combinedAudioBuffer.getChannelData(channel);
1633
+
1634
+ for (let i = 0; i < inputData.length; i++) {
1635
+ outputData[i] += inputData[i] * volume;
1636
  }
 
 
 
 
1637
  }
1638
+ }
1639
+
1640
+ // 音量を正規化 (クリッピング防止)
1641
+ for (let channel = 0; channel < 2; channel++) {
1642
+ const outputData = combinedAudioBuffer.getChannelData(channel);
1643
+ let max = 0;
1644
+
1645
+ for (let i = 0; i < outputData.length; i++) {
1646
+ if (Math.abs(outputData[i]) > max) {
1647
+ max = Math.abs(outputData[i]);
1648
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
1649
  }
1650
+
1651
+ if (max > 1) {
1652
+ for (let i = 0; i < outputData.length; i++) {
1653
+ outputData[i] /= max;
1654
+ }
1655
+ }
1656
+ }
1657
+
1658
+ // AudioBufferをBlobに変換
1659
+ const blob = bufferToWave(combinedAudioBuffer);
1660
+ const url = URL.createObjectURL(blob);
1661
+
1662
+ // 新しいaudio要素を作成
1663
+ combinedAudioElement = new Audio(url);
1664
+ combinedAudioElement.preservesPitch = true;
1665
+ combinedAudioElement.mozPreservesPitch = true;
1666
+ combinedAudioElement.webkitPreservesPitch = true;
1667
+ combinedAudioElement.playbackRate = currentPlaybackRate;
1668
+
1669
+ isAudioCombined = true;
1670
+ combineStatus.textContent = "音声の合成が完了しました";
1671
+ enablePlayerControls();
1672
+
1673
+ combineButton.disabled = false;
1674
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1675
  document.addEventListener('visibilitychange', async () => {
1676
  if (document.hidden) {
1677
  // タブが非表示になった場合
 
1727
  }
1728
  });
1729
 
1730
+ // 動画終了時に自動的にPiPを閉じる(次回再開のため)
1731
+ video.addEventListener('ended', exitPiP);
1732
 
1733
+ // 合成後に音量と再生速度を適用
1734
+ applyVolume();
1735
+ applyPlaybackRate();
1736
+
1737
+ } catch (error) {
1738
+ console.error('音声合成エラー:', error);
1739
+ combineStatus.textContent = "音声の合成に失敗しました";
1740
+ combineButton.disabled = false;
1741
+ }
1742
+ }
1743
+
1744
+ function bufferToWave(abuffer) {
1745
+ const numOfChan = abuffer.numberOfChannels,
1746
+ length = abuffer.length * numOfChan * 2 + 44,
1747
+ buffer = new ArrayBuffer(length),
1748
+ view = new DataView(buffer),
1749
+ channels = [],
1750
+ sampleRate = abuffer.sampleRate;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1751
 
1752
+ // posをletで宣言(constから変更)
1753
+ let pos = 0;
1754
+
1755
+ // write WAV header
1756
+ setUint32(0x46464952); // "RIFF"
1757
+ setUint32(length - 8); // file length - 8
1758
+ setUint32(0x45564157); // "WAVE"
1759
+
1760
+ setUint32(0x20746d66); // "fmt " chunk
1761
+ setUint32(16); // length = 16
1762
+ setUint16(1); // PCM (uncompressed)
1763
+ setUint16(numOfChan);
1764
+ setUint32(sampleRate);
1765
+ setUint32(sampleRate * 2 * numOfChan);
1766
+ setUint16(numOfChan * 2);
1767
+ setUint16(16);
1768
+
1769
+ setUint32(0x61746164); // "data" - chunk
1770
+ setUint32(length - pos - 4);
1771
+
1772
+ // write interleaved data
1773
+ for (let i = 0; i < abuffer.length; i++) {
1774
+ for (let channel = 0; channel < numOfChan; channel++) {
1775
+ let sample = abuffer.getChannelData(channel)[i] * 0x7fff;
1776
+ if (sample < -32768) sample = -32768;
1777
+ if (sample > 32767) sample = 32767;
1778
+ view.setInt16(pos, sample, true);
1779
+ pos += 2;
1780
+ }
1781
+ }
1782
+
1783
+ function setUint16(data) {
1784
+ view.setUint16(pos, data, true);
1785
+ pos += 2;
1786
+ }
1787
+
1788
+ function setUint32(data) {
1789
+ view.setUint32(pos, data, true);
1790
+ pos += 4;
1791
+ }
1792
+
1793
+ return new Blob([buffer], { type: 'audio/wav' });
1794
+ }
1795
+
1796
+ function applyVolume() {
1797
+ if (!isAudioCombined) return;
1798
+
1799
+ // ベース音量 (0-1)
1800
+ const baseVolume = parseFloat(volumeSlider.value);
1801
+ // グローバル音量係数 (0-10)
1802
+ const globalVolume = parseFloat(globalVolumeSlider.value) / 10; // 0-1に変換
1803
+
1804
+ // 最終音量 (0-1)
1805
+ const finalVolume = Math.max(0, Math.min(1, baseVolume * globalVolume));
1806
+
1807
+ // 動画と音声の音量を設定
1808
+ video.volume = finalVolume;
1809
+ if (combinedAudioElement) {
1810
+ combinedAudioElement.volume = finalVolume;
1811
+ }
1812
+
1813
+ // 音量アイコンを更新
1814
+ updateVolumeIcon();
1815
+ }
1816
+
1817
+ function applyPlaybackRate() {
1818
+ if (!isAudioCombined) return;
1819
+
1820
+ const speed = parseFloat(playbackSpeedSlider.value);
1821
+ currentPlaybackRate = speed;
1822
+ video.playbackRate = speed;
1823
+
1824
+ if (combinedAudioElement) {
1825
+ combinedAudioElement.playbackRate = speed;
1826
+ }
1827
+
1828
+ speedValue.textContent = speed.toFixed(2) + 'x';
1829
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1830
+ speedSlider.value = speed;
1831
+ }
1832
+
1833
+ // プレイヤーコントロールを有効化
1834
+ function enablePlayerControls() {
1835
+ disabledOverlay.style.display = 'none';
1836
+ playPauseBtn.disabled = false;
1837
+ volumeBtn.disabled = false;
1838
+ volumeSlider.disabled = false;
1839
+ speedSlider.disabled = false;
1840
+ fullscreenBtn.disabled = false;
1841
+ startTimeInput.disabled = false;
1842
+ endTimeInput.disabled = false;
1843
+ resetEndTimeBtn.disabled = false;
1844
+ loopCheckbox.disabled = false;
1845
+ globalVolumeSlider.disabled = false;
1846
+ setStartTimeBtn.disabled = false;
1847
+ setEndTimeBtn.disabled = false;
1848
+ playbackSpeedSlider.disabled = false;
1849
+ applyTimeBtn.disabled = false;
1850
+ blankModeBtn.disabled = false;
1851
+ }
1852
+
1853
+ // プレビュー再生
1854
+ function togglePreview() {
1855
+ if (!isAudioCombined || !combinedAudioElement) return;
1856
+
1857
+ if (previewButton.textContent === '▶') {
1858
+ // 再生
1859
+ combinedAudioElement.currentTime = 0;
1860
+ combinedAudioElement.play()
1861
+ .then(() => {
1862
+ previewButton.textContent = '⏸';
1863
 
1864
+ // プレビューの時間表示を更新
1865
+ const updatePreviewTime = () => {
1866
+ if (!combinedAudioElement || !isAudioCombined) return;
1867
 
1868
+ const currentTime = combinedAudioElement.currentTime;
1869
+ const duration = combinedAudioElement.duration;
 
 
 
 
1870
 
1871
+ if (currentTime >= duration) {
1872
+ previewButton.textContent = '';
1873
+ previewTime.textContent = `00:00 / ${formatTime(duration)}`;
1874
+ return;
1875
+ }
1876
 
1877
+ previewTime.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
1878
+ requestAnimationFrame(updatePreviewTime);
1879
+ };
1880
+
1881
+ updatePreviewTime();
1882
+ })
1883
+ .catch(e => console.error('プレビュー再生エラー:', e));
1884
+ } else {
1885
+ // 一時停止
1886
+ combinedAudioElement.pause();
1887
+ previewButton.textContent = '▶';
1888
+ }
1889
+ }
1890
+
1891
+ // 時間をフォーマットするヘルパー関数
1892
+ function formatTime(seconds) {
1893
+ const mins = Math.floor(seconds / 60);
1894
+ const secs = Math.floor(seconds % 60);
1895
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
1896
+ }
1897
+
1898
+ // 動画のメタデータが読み込まれたら
1899
+ video.addEventListener('loadedmetadata', function() {
1900
+ try {
1901
+ videoDuration = video.duration;
1902
+ endTimeInput.value = videoDuration.toFixed(2);
1903
+ endTimeInput.max = videoDuration;
1904
+ startTimeInput.max = videoDuration - 0.1;
1905
+ updateTimeDisplay();
1906
+ checkLoadingComplete();
1907
+ } catch (error) {
1908
+ handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
1909
+ }
1910
+ });
1911
+
1912
+ // 動画エラー処理
1913
+ video.addEventListener('error', function() {
1914
+ handleError(video.error, '動画読み込み中にエラーが発生しました');
1915
+ });
1916
+
1917
+ // 再生ボタンクリック
1918
+ playPauseBtn.addEventListener('click', function() {
1919
+ if (video.currentTime >= appliedEndTime) {
1920
+ video.currentTime = appliedStartTime;
1921
+ if (combinedAudioElement) {
1922
+ combinedAudioElement.currentTime = appliedStartTime;
1923
+ }
1924
+ }
1925
+ togglePlayPause();
1926
+ });
1927
+
1928
+ // 時間表示を更新
1929
+ function updateTimeDisplay() {
1930
+ const now = performance.now();
1931
+ if (now - lastUpdateTime < updateInterval && !isFullscreen) return;
1932
+ lastUpdateTime = now;
1933
+
1934
+ try {
1935
+ const currentTime = video.currentTime;
1936
+ const duration = video.duration || videoDuration;
1937
+
1938
+ const currentMinutes = Math.floor(currentTime / 60);
1939
+ const currentSeconds = Math.floor(currentTime % 60);
1940
+ const currentMilliseconds = Math.floor((currentTime % 1) * 100);
1941
+ const durationMinutes = Math.floor(duration / 60);
1942
+ const durationSeconds = Math.floor(duration % 60);
1943
+ const durationMilliseconds = Math.floor((duration % 1) * 100);
1944
+
1945
+ timeDisplay.textContent =
1946
+ `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
1947
+ `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
1948
+
1949
+ const progressPercent = (currentTime / duration) * 100;
1950
+ progressBar.style.width = `${progressPercent}%`;
1951
+ } catch (error) {
1952
+ console.error('時間表示更新エラー:', error);
1953
+ }
1954
+ }
1955
+
1956
+ // 再生/一時停止をトグル
1957
+ function togglePlayPause() {
1958
+ if (isPlaying) {
1959
+ pauseMedia();
1960
+ } else {
1961
+ playMedia();
1962
+ }
1963
+ }
1964
+
1965
+ function playMedia() {
1966
+ try {
1967
+ const duration = video.duration || videoDuration;
1968
+
1969
+ if (video.currentTime >= appliedEndTime) {
1970
+ video.currentTime = appliedStartTime;
1971
+ }
1972
+
1973
+ const playPromise = video.play();
1974
+
1975
+ if (playPromise !== undefined) {
1976
+ playPromise.then(() => {
1977
+ isPlaying = true;
1978
+ playPauseBtn.textContent = '⏸';
1979
 
1980
+ // 音声を同期して再生
1981
+ if (isAudioCombined) {
1982
+ combinedAudioElement.currentTime = video.currentTime;
1983
+ combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
1984
  }
1985
 
1986
+ startSyncCheck();
1987
 
1988
+ video.playbackRate = currentPlaybackRate;
1989
+ }).catch(error => {
1990
+ console.error('動画再生エラー:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1991
  isPlaying = false;
1992
  playPauseBtn.textContent = '▶';
1993
+ });
1994
  }
1995
+ } catch (error) {
1996
+ console.error('メディア再生エラー:', error);
1997
+ isPlaying = false;
1998
+ playPauseBtn.textContent = '▶';
1999
+ }
2000
+ }
2001
+
2002
+ function pauseMedia() {
2003
+ try {
2004
+ video.pause();
2005
+ isPlaying = false;
2006
+ playPauseBtn.textContent = '▶';
2007
+ stopSyncCheck();
2008
 
2009
+ if (combinedAudioElement) {
2010
+ combinedAudioElement.pause();
2011
+ }
2012
+ } catch (error) {
2013
+ console.error('メディア一時停止エラー:', error);
2014
+ }
2015
+ }
2016
+
2017
+ // 時間更新時の処理
2018
+ video.addEventListener('timeupdate', function() {
2019
+ const duration = video.duration || videoDuration;
2020
+
2021
+ if (video.currentTime >= appliedEndTime && appliedEndTime > 0) {
2022
+ if (loopCheckbox.checked) {
2023
+ video.currentTime = appliedStartTime;
2024
+ if (combinedAudioElement) {
2025
+ combinedAudioElement.currentTime = appliedStartTime;
2026
  }
2027
+ } else {
2028
+ pauseMedia();
2029
+ video.currentTime = appliedEndTime;
2030
  }
2031
+ }
2032
+
2033
+ updateTimeDisplay();
2034
+ });
 
2035
 
2036
+ // プログレスバークリックでシーク
2037
+ progressContainer.addEventListener('click', function(e) {
2038
+ if (!video.duration) return;
2039
+
2040
+ const rect = this.getBoundingClientRect();
2041
+ const pos = (e.clientX - rect.left) / rect.width;
2042
+ const seekTime = pos * video.duration;
2043
+
2044
+ seekMedia(seekTime);
2045
+ });
2046
+
2047
+ // 指定した時間にシーク (改良���)
2048
+ function seekMedia(time) {
2049
+ try {
2050
+ const duration = video.duration || videoDuration;
2051
+ const seekTime = Math.max(appliedStartTime, Math.min(time, appliedEndTime));
2052
+ video.currentTime = seekTime;
2053
+
2054
  if (combinedAudioElement) {
2055
+ combinedAudioElement.currentTime = seekTime;
2056
+ if (isPlaying) {
2057
+ combinedAudioElement.play().catch(e => console.error('音声再生エラー:', e));
2058
+ }
2059
  }
2060
+ } catch (error) {
2061
+ console.error('メディアシークエラー:', error);
2062
+ }
2063
+ }
2064
+
2065
+ // プログレスバー上でマウス移動時に時間を表示
2066
+ progressContainer.addEventListener('mousemove', function(e) {
2067
+ if (!video.duration) return;
2068
+
2069
+ const rect = this.getBoundingClientRect();
2070
+ const pos = (e.clientX - rect.left) / rect.width;
2071
+ const hoverTime = pos * video.duration;
2072
+
2073
+ const minutes = Math.floor(hoverTime / 60);
2074
+ const seconds = Math.floor(hoverTime % 60);
2075
+ const milliseconds = Math.floor((hoverTime % 1) * 100);
2076
+
2077
+ progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
2078
+ progressTime.style.display = 'block';
2079
+ progressTime.style.left = `${pos * 100}%`;
2080
+ });
2081
+
2082
+ progressContainer.addEventListener('mouseleave', function() {
2083
+ progressTime.style.display = 'none';
2084
+ });
2085
+
2086
+ // 動画クリックで再生/一時停止
2087
+ video.addEventListener('click', function() {
2088
+ togglePlayPause();
2089
+ });
2090
+
2091
+ volumeSlider.addEventListener('input', function() {
2092
+ if (!isAudioCombined) return;
2093
+
2094
+ lastVolume = parseFloat(this.value);
2095
+ applyVolume();
2096
+ });
2097
+
2098
+ volumeBtn.addEventListener('click', function() {
2099
+ if (!isAudioCombined) return;
2100
+
2101
+ if (video.volume > 0) {
2102
+ lastVolume = parseFloat(volumeSlider.value);
2103
+ volumeSlider.value = 0;
2104
  } else {
2105
+ volumeSlider.value = lastVolume;
2106
+ }
2107
+
2108
+ applyVolume();
2109
+ });
2110
+
2111
+ // 音量アイコンを更新
2112
+ function updateVolumeIcon() {
2113
+ if (video.volume === 0) {
2114
+ volumeBtn.textContent = '🔇';
2115
+ } else if (video.volume < 3) {
2116
+ volumeBtn.textContent = '🔈';
2117
+ } else {
2118
+ volumeBtn.textContent = '🔊';
2119
  }
2120
  }
2121
 
2122
+ // 再生速度スライダー (動画プレイヤー)
2123
+ speedSlider.addEventListener('input', function() {
2124
+ if (!isAudioCombined) return;
2125
+
2126
+ const speed = parseFloat(this.value);
2127
+ speedValue.textContent = speed.toFixed(2) + 'x';
2128
+ playbackSpeedSlider.value = speed;
2129
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2130
+ updatePlaybackRate(speed);
2131
+ });
2132
+
2133
+ // 再生速度スライダー (設定メニュー)
2134
+ playbackSpeedSlider.addEventListener('input', function() {
2135
+ if (!isAudioCombined) return;
2136
+
2137
+ const speed = parseFloat(this.value);
2138
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
2139
+ speedSlider.value = speed;
2140
+ speedValue.textContent = speed.toFixed(2) + 'x';
2141
+ updatePlaybackRate(speed);
2142
+ });
2143
+
2144
+ // テンポ入力による再生速度更新
2145
+ tempoInput.addEventListener('input', function() {
2146
+ const tempo = parseFloat(this.value);
2147
+ const baseTempo = isTMode ? 66 : 92;
2148
+ const speed = tempo / baseTempo;
2149
+
2150
+ const clampedSpeed = Math.max(0.001, Math.min(5.0, speed));
2151
+
2152
+ playbackSpeedSlider.value = clampedSpeed;
2153
+ playbackSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2154
+ speedSlider.value = clampedSpeed;
2155
+ speedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2156
+ tempoSpeedValue.textContent = clampedSpeed.toFixed(2) + 'x';
2157
+
2158
+ updatePlaybackRate(clampedSpeed);
2159
+ });
2160
+
2161
+ function updatePlaybackRate(speed) {
2162
+ if (!isAudioCombined) return;
2163
+
2164
+ currentPlaybackRate = speed;
2165
+ video.playbackRate = speed;
2166
+
2167
+ // ピッチ保持を再設定
2168
+ video.preservesPitch = true;
2169
+ video.mozPreservesPitch = true;
2170
+ video.webkitPreservesPitch = true;
2171
+
2172
+ if (combinedAudioElement) {
2173
+ combinedAudioElement.playbackRate = speed;
 
 
 
 
 
 
 
 
2174
 
2175
+ // 合成音声のピッチ保持を設定
2176
+ combinedAudioElement.preservesPitch = true;
2177
+ combinedAudioElement.mozPreservesPitch = true;
2178
+ combinedAudioElement.webkitPreservesPitch = true;
2179
+ }
2180
+ }
2181
+
2182
+ // 全画面ボタン
2183
+ fullscreenBtn.addEventListener('click', function() {
2184
+ if (!isFullscreen) {
2185
+ if (videoContainer.requestFullscreen) {
2186
+ videoContainer.requestFullscreen();
2187
+ } else if (videoContainer.webkitRequestFullscreen) {
2188
+ videoContainer.webkitRequestFullscreen();
2189
+ } else if (videoContainer.msRequestFullscreen) {
2190
+ videoContainer.msRequestFullscreen();
2191
+ }
2192
+ } else {
2193
+ if (document.exitFullscreen) {
2194
+ document.exitFullscreen();
2195
+ } else if (document.webkitExitFullscreen) {
2196
+ document.webkitExitFullscreen();
2197
+ } else if (document.msExitFullscreen) {
2198
+ document.msExitFullscreen();
2199
+ }
2200
+ }
2201
+ });
2202
+
2203
+ // 全画面変更イベント
2204
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
2205
+ document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
2206
+ document.addEventListener('msfullscreenchange', handleFullscreenChange);
2207
+
2208
+ function handleFullscreenChange() {
2209
+ isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
2210
+ fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
2211
+ video.controls = false;
2212
+
2213
+ // 全画面時にロックボタンを表示
2214
+ lockControlsBtn.style.display = isFullscreen ? 'flex' : 'none';
2215
+
2216
+ // 全画面時にコントロールバー自動非表示機能を有効化
2217
+ if (isFullscreen) {
2218
+ resetControlsHideTimer();
2219
+ document.addEventListener('mousemove', handleFullscreenMouseMove);
2220
+ } else {
2221
+ document.removeEventListener('mousemove', handleFullscreenMouseMove);
2222
+ clearTimeout(controlsHideTimeout);
2223
+ showControls();
2224
+ }
2225
+ }
2226
+
2227
+ // 全画面時のマウス移動処理
2228
+ function handleFullscreenMouseMove() {
2229
+ if (!isControlsLocked) {
2230
+ showControls();
2231
+ resetControlsHideTimer();
2232
+ }
2233
+ }
2234
+
2235
+ // コントロールバーを表示
2236
+ function showControls() {
2237
+ if (!controlsVisible) {
2238
+ videoControls.style.opacity = '1';
2239
+ controlsVisible = true;
2240
+ }
2241
+ }
2242
+
2243
+ // コントロールバーを非表示
2244
+ function hideControls() {
2245
+ if (!isControlsLocked && controlsVisible) {
2246
+ videoControls.style.opacity = '0';
2247
+ controlsVisible = false;
2248
+ }
2249
+ }
2250
+
2251
+ // コントロールバー非表示タイマーをリセット
2252
+ function resetControlsHideTimer() {
2253
+ clearTimeout(controlsHideTimeout);
2254
+ if (!isControlsLocked) {
2255
+ controlsHideTimeout = setTimeout(hideControls, 1500); // 1.5秒後に非表示
2256
+ }
2257
+ }
2258
+
2259
+ // ロックボタンのクリック処理
2260
+ lockControlsBtn.addEventListener('click', function() {
2261
+ isControlsLocked = !isControlsLocked;
2262
+ this.classList.toggle('locked', isControlsLocked);
2263
+
2264
+ if (isControlsLocked) {
2265
+ showControls();
2266
+ clearTimeout(controlsHideTimeout);
2267
+ } else {
2268
+ resetControlsHideTimer();
2269
+ }
2270
+ });
2271
+
2272
+ // キーボードイベント (ESCで全画面終了)
2273
+ document.addEventListener('keydown', function(e) {
2274
+ if (e.key === 'Escape' && isFullscreen) {
2275
+ if (document.exitFullscreen) {
2276
+ document.exitFullscreen();
2277
+ } else if (document.webkitExitFullscreen) {
2278
+ document.webkitExitFullscreen();
2279
+ } else if (document.msExitFullscreen) {
2280
+ document.msExitFullscreen();
2281
+ }
2282
+ }
2283
+ });
2284
+
2285
+ // ボリュームスライダーのイベント
2286
+ audioSliders.forEach((slider, index) => {
2287
+ slider.addEventListener('input', function() {
2288
+ const value = parseFloat(this.value);
2289
+ volumeValues[index].textContent = value.toFixed(2);
2290
+
2291
+ const percent = value * 100;
2292
+ this.style.backgroundSize = `${percent}% 100%`;
2293
+ });
2294
+ });
2295
+
2296
+ globalVolumeSlider.addEventListener('input', function() {
2297
+ const value = parseFloat(this.value);
2298
+ globalVolumeValue.textContent = value.toFixed(1);
2299
+
2300
+ const percent = (value - this.min) / (this.max - this.min) * 100;
2301
+ this.style.backgroundSize = `${percent}% 100%`;
2302
+
2303
+ applyVolume();
2304
+ });
2305
+
2306
+ // ループ設定変更時
2307
+ loopCheckbox.addEventListener('change', function() {
2308
+ // 合成音声ではループは動画に依存する
2309
+ });
2310
+
2311
+ // 現在の秒数を開始時間に設定
2312
+ setStartTimeBtn.addEventListener('click', function() {
2313
+ startTimeInput.value = video.currentTime.toFixed(2);
2314
+ updateProgressMarkers();
2315
+ });
2316
+
2317
+ // 現在の秒数を終了時間に設定
2318
+ setEndTimeBtn.addEventListener('click', function() {
2319
+ endTimeInput.value = video.currentTime.toFixed(2);
2320
+ updateProgressMarkers();
2321
+ });
2322
+
2323
+ // 終了時間を動画の長さにリセット
2324
+ resetEndTimeBtn.addEventListener('click', function() {
2325
+ endTimeInput.value = video.duration.toFixed(2);
2326
+ updateProgressMarkers();
2327
+ });
2328
+
2329
+ // プログレスバーのマーカーを更新
2330
+ function updateProgressMarkers() {
2331
+ const duration = video.duration || videoDuration;
2332
+ const startTime = parseFloat(startTimeInput.value) || 0;
2333
+ const endTime = parseFloat(endTimeInput.value) || duration;
2334
+
2335
+ if (duration > 0) {
2336
+ // 開始時間が0の場合はマーカーを非表示
2337
+ if (startTime <= 0) {
2338
+ startMarker.style.display = 'none';
2339
+ } else {
2340
+ startMarker.style.left = `${(startTime / duration) * 100}%`;
2341
+ startMarker.style.display = 'block';
2342
+ }
2343
+
2344
+ // 終了時間が動画の長さと同じ場合はマーカーを非表示
2345
+ if (endTime >= duration) {
2346
+ endMarker.style.display = 'none';
2347
+ } else {
2348
+ endMarker.style.left = `${(endTime / duration) * 100}%`;
2349
+ endMarker.style.display = 'block';
2350
+ }
2351
+ }
2352
+ }
2353
+
2354
+ // 消画モードボタンのクリック処理
2355
+ blankModeBtn.addEventListener('click', toggleBlankMode);
2356
+
2357
+ // 合成ボタンクリック
2358
+ combineButton.addEventListener('click', combineAudio);
2359
+
2360
+ // プレビューボタンクリック
2361
+ previewButton.addEventListener('click', togglePreview);
2362
+
2363
+ // 同期ステータスを閉じる
2364
+ syncStatusClose.addEventListener('click', function() {
2365
+ syncStatus.style.display = 'none';
2366
+ });
2367
+
2368
+ // 初期化
2369
+ loadAudioFiles();
2370
+ updateVolumeIcon();
2371
+ volumeSlider.value = video.volume;
2372
+ video.controls = false;
2373
+
2374
+ // スライダーの背景を初期化
2375
+ function initSliderBackgrounds() {
2376
+ const sliders = [
2377
+ volumeSlider,
2378
+ speedSlider,
2379
+ globalVolumeSlider,
2380
+ playbackSpeedSlider,
2381
+ ...audioSliders
2382
+ ];
2383
+
2384
+ sliders.forEach(slider => {
2385
+ if (slider) {
2386
+ slider.style.backgroundImage = 'linear-gradient(#64ffda, #64ffda)';
2387
+ slider.style.backgroundRepeat = 'no-repeat';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2388
 
2389
+ if (slider === globalVolumeSlider) {
2390
+ const percent = (slider.value - slider.min) / (slider.max - slider.min) * 100;
2391
+ slider.style.backgroundSize = `${percent}% 100%`;
2392
+ globalVolumeValue.textContent = slider.value;
2393
+ } else {
2394
+ slider.style.backgroundSize = `${slider.value * 100}% 100%`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2395
  }
2396
+ }
2397
+ });
2398
+ }
2399
+
2400
+ initSliderBackgrounds();
2401
+ startSyncCheck(); // 同期チェックを開始
2402
+
2403
+ // 初期テンポ設定
2404
+ tempoInput.value = isTMode ? 66 : 92;
2405
+ tempoInput.dispatchEvent(new Event('input'));
2406
+ });
2407
+
2408
+ // タイムマーカー関連のコード
2409
  document.addEventListener('DOMContentLoaded', function() {
2410
+ // 必要なDOM要素を取得
2411
  const startMarker = document.getElementById('start-marker');
2412
  const endMarker = document.getElementById('end-marker');
2413
  const video = document.getElementById('video');
 
2507
  if(startMarker) startMarker.style.left = `${(startTime / duration) * 100}%`;
2508
  if(endMarker) endMarker.style.left = `${(endTime / duration) * 100}%`;
2509
 
2510
+ // 開始時間が0の場合はマーカーを非表示
2511
+ if(startTime <= 0 && startMarker) {
2512
+ startMarker.style.display = 'none';
2513
+ } else if(startMarker) {
2514
+ startMarker.style.display = 'block';
2515
+ }
2516
+
2517
+ // 終了時間が動画の長さと同じ場合はマーカーを非表示
2518
+ if(endTime >= duration && endMarker) {
2519
+ endMarker.style.display = 'none';
2520
+ } else if(endMarker) {
2521
+ endMarker.style.display = 'block';
2522
+ }
2523
  }
2524
  }
2525
  });
 
2526
  </script>
2527
  </body>
2528
  </html>