soiz1 commited on
Commit
4a5cd2d
·
1 Parent(s): 3f75413

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +202 -230
index.html CHANGED
@@ -642,7 +642,6 @@
642
  function createTechBackground() {
643
  const bg = document.getElementById('techBg');
644
 
645
- // 回路風の線を生成
646
  for (let i = 0; i < 200; i++) {
647
  const line = document.createElement('div');
648
  line.className = 'circuit-line';
@@ -659,11 +658,9 @@
659
  line.style.left = `${Math.random() * 100}%`;
660
  line.style.top = `${Math.random() * 100}%`;
661
  line.style.opacity = Math.random() * 0.5 + 0.1;
662
-
663
  bg.appendChild(line);
664
  }
665
-
666
- // グリッドドットを生成
667
  for (let i = 0; i < 200; i++) {
668
  const dot = document.createElement('div');
669
  dot.className = 'grid-dot';
@@ -671,8 +668,7 @@
671
  dot.style.top = `${Math.random() * 100}%`;
672
  bg.appendChild(dot);
673
  }
674
-
675
- // 六角形を生成
676
  for (let i = 0; i < 15; i++) {
677
  const hex = document.createElement('div');
678
  hex.className = 'hexagon';
@@ -682,8 +678,7 @@
682
  hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
683
  bg.appendChild(hex);
684
  }
685
-
686
- // パルスする点を生成
687
  for (let i = 0; i < 8; i++) {
688
  const pulse = document.createElement('div');
689
  pulse.className = 'pulse';
@@ -698,11 +693,10 @@
698
 
699
  // ローディング状態を管理
700
  let loadingCount = 0;
701
- let totalToLoad = 6; // 動画 + 5音声ファイル
702
  let lastUpdateTime = 0;
703
- const updateInterval = 1; // 50msごとに更新 (20fps)
704
 
705
- // ローディング完了チェック
706
  function checkLoadingComplete() {
707
  loadingCount++;
708
  if (loadingCount >= totalToLoad) {
@@ -716,27 +710,19 @@
716
  }
717
  }
718
 
719
- // エラーハンドリング関数
720
  function handleError(error, message) {
721
  console.error(message, error);
722
  window.alert(`${message}\n\nエラー詳細: ${error.message}`);
723
  }
724
 
725
- // ピッチ設定関数
726
- function setPreservesPitch(el, value) {
727
- try {
728
- if ('preservesPitch' in el) {
729
- el.preservesPitch = value; // モダンブラウザ
730
- } else if ('webkitPreservesPitch' in el) {
731
- el.webkitPreservesPitch = value; // 古い WebKit (Chrome <86 など)
732
- } else if ('mozPreservesPitch' in el) {
733
- el.mozPreservesPitch = value; // 古い Gecko (Firefox ≤100)
734
- }
735
- } catch (error) {
736
- handleError(error, 'ピッチ設定中にエラーが発生しました');
737
- }
738
  }
739
-
740
  // 要素を取得
741
  const video = document.getElementById('video');
742
  const videoContainer = document.getElementById('video-container');
@@ -764,8 +750,7 @@
764
 
765
  // 音声オブジェクトを作成
766
  const audioElements = {};
767
-
768
- // 音声ファイル名の配列
769
  const audioFiles = ['p', 'a', 't', 's', 'k'];
770
 
771
  // 初期化
@@ -774,6 +759,44 @@
774
  let lastVolume = 1;
775
  let currentPlaybackRate = 1;
776
  let isFullscreen = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
777
 
778
  // 動画のメタデータが読み込まれたら
779
  video.addEventListener('loadedmetadata', function() {
@@ -796,18 +819,12 @@
796
 
797
  // 再生ボタンクリック
798
  playPauseBtn.addEventListener('click', function() {
799
- try {
800
- // 現在の秒数が終了秒数を超えている場合は開始位置に戻す
801
- const endTime = parseFloat(endTimeInput.value) || videoDuration;
802
- if (video.currentTime >= endTime) {
803
- const startTime = parseFloat(startTimeInput.value) || 0;
804
- seekMedia(startTime);
805
- }
806
-
807
- togglePlayPause();
808
- } catch (error) {
809
- handleError(error, '再生/一時停止操作中にエラーが発生しました');
810
  }
 
811
  });
812
 
813
  // 時間表示を更新
@@ -831,11 +848,13 @@
831
  `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
832
  `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
833
 
834
- // プログレスバーを更新
835
  const progressPercent = (currentTime / duration) * 100;
836
  progressBar.style.width = `${progressPercent}%`;
 
 
 
837
  } catch (error) {
838
- handleError(error, '時間表示更新中にエラーが発生しました');
839
  }
840
  }
841
 
@@ -848,37 +867,48 @@
848
  }
849
  }
850
 
851
- // 再生関数
852
  function playMedia() {
853
  try {
854
  const duration = video.duration || videoDuration;
855
  const startTime = parseFloat(startTimeInput.value) || 0;
856
  const endTime = parseFloat(endTimeInput.value) || duration;
857
 
858
- // 現在位置が終了時間を超えている場合、開始位置に戻る
859
  if (video.currentTime >= endTime) {
860
  video.currentTime = startTime;
861
  }
862
 
863
  // 動画を再生
864
- video.play().then(() => {
865
- isPlaying = true;
866
- playPauseBtn.textContent = '⏸';
867
-
868
- // 音声を再生
869
- audioFiles.forEach(file => {
870
- if (audioElements[file]) {
871
- audioElements[file].currentTime = video.currentTime;
872
- audioElements[file].play().catch(e => {
873
- if (e.name !== 'AbortError') {
874
- console.error(`音声再生エラー (${file}.mp3):`, e);
 
 
 
 
 
 
 
 
 
875
  }
876
- });
877
- }
 
 
 
 
 
878
  });
879
- }).catch(error => {
880
- console.error('動画再生エラー:', error);
881
- });
882
  } catch (error) {
883
  console.error('メディア再生エラー:', error);
884
  }
@@ -891,7 +921,6 @@
891
  isPlaying = false;
892
  playPauseBtn.textContent = '▶';
893
 
894
- // 音声を一時停止
895
  audioFiles.forEach(file => {
896
  if (audioElements[file]) {
897
  audioElements[file].pause();
@@ -907,13 +936,11 @@
907
  const duration = video.duration || videoDuration;
908
  const endTime = parseFloat(endTimeInput.value) || duration;
909
 
910
- // 終了時間に達した場合の処理
911
  if (video.currentTime >= endTime && endTime > 0) {
912
  if (loopCheckbox.checked) {
913
  const startTime = parseFloat(startTimeInput.value) || 0;
914
  video.currentTime = startTime;
915
 
916
- // 音声も同期
917
  audioFiles.forEach(file => {
918
  if (audioElements[file]) {
919
  audioElements[file].currentTime = startTime;
@@ -937,61 +964,61 @@
937
 
938
  // プログレスバークリックでシーク
939
  progressContainer.addEventListener('click', function(e) {
940
- try {
941
- if (!video.duration) return;
942
-
943
- const rect = this.getBoundingClientRect();
944
- const pos = (e.clientX - rect.left) / rect.width;
945
- const seekTime = pos * video.duration;
946
-
947
- seekMedia(seekTime);
948
- } catch (error) {
949
- handleError(error, 'シーク操作中にエラーが発生しました');
950
- }
951
  });
952
 
953
- // 指定した時間にシーク
954
  function seekMedia(time) {
955
  try {
956
  const duration = video.duration || videoDuration;
957
  const startTime = parseFloat(startTimeInput.value) || 0;
958
  const endTime = parseFloat(endTimeInput.value) || duration;
959
 
960
- // 範囲内に制限
961
  const seekTime = Math.max(startTime, Math.min(time, endTime));
962
-
963
  video.currentTime = seekTime;
964
 
965
- // 音声も同期
966
  audioFiles.forEach(file => {
967
  if (audioElements[file]) {
968
  audioElements[file].currentTime = seekTime;
969
  }
970
  });
 
 
 
 
 
 
 
 
 
 
 
971
  } catch (error) {
972
- handleError(error, 'メディアシーク中にエラーが発生しました');
973
  }
974
  }
975
 
976
  // プログレスバー上でマウス移動時に時間を表示
977
  progressContainer.addEventListener('mousemove', function(e) {
978
- try {
979
- if (!video.duration) return;
980
-
981
- const rect = this.getBoundingClientRect();
982
- const pos = (e.clientX - rect.left) / rect.width;
983
- const hoverTime = pos * video.duration;
984
-
985
- const minutes = Math.floor(hoverTime / 60);
986
- const seconds = Math.floor(hoverTime % 60);
987
- const milliseconds = Math.floor((hoverTime % 1) * 100);
988
-
989
- progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
990
- progressTime.style.display = 'block';
991
- progressTime.style.left = `${pos * 100}%`;
992
- } catch (error) {
993
- handleError(error, 'プログレスバー操作中にエラーが発生しました');
994
- }
995
  });
996
 
997
  progressContainer.addEventListener('mouseleave', function() {
@@ -1005,30 +1032,22 @@
1005
 
1006
  // 音量コントロール
1007
  volumeSlider.addEventListener('input', function() {
1008
- try {
1009
- video.volume = this.value;
1010
- lastVolume = this.value;
1011
- updateVolumeIcon();
1012
- } catch (error) {
1013
- handleError(error, '音量設定中にエラーが発生しました');
1014
- }
1015
  });
1016
 
1017
  // 音量ボタン
1018
  volumeBtn.addEventListener('click', function() {
1019
- try {
1020
- if (video.volume > 0) {
1021
- lastVolume = video.volume;
1022
- video.volume = 0;
1023
- volumeSlider.value = 0;
1024
- } else {
1025
- video.volume = lastVolume;
1026
- volumeSlider.value = lastVolume;
1027
- }
1028
- updateVolumeIcon();
1029
- } catch (error) {
1030
- handleError(error, '音量切り替え中にエラーが発生しました');
1031
  }
 
1032
  });
1033
 
1034
  // 音量アイコンを更新
@@ -1044,69 +1063,51 @@
1044
 
1045
  // 再生速度スライダー (動画プレイヤー)
1046
  speedSlider.addEventListener('input', function() {
1047
- try {
1048
- const speed = parseFloat(this.value);
1049
- speedValue.textContent = speed.toFixed(2) + 'x';
1050
- playbackSpeedSlider.value = speed;
1051
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1052
- updatePlaybackRate(speed);
1053
- } catch (error) {
1054
- handleError(error, '再生速度設定中にエラーが発生しました');
1055
- }
1056
  });
1057
 
1058
  // 再生速度スライダー (設定メニュー)
1059
  playbackSpeedSlider.addEventListener('input', function() {
1060
- try {
1061
- const speed = parseFloat(this.value);
1062
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1063
- speedSlider.value = speed;
1064
- speedValue.textContent = speed.toFixed(2) + 'x';
1065
- updatePlaybackRate(speed);
1066
- } catch (error) {
1067
- handleError(error, '再生速度設定中にエラーが発生しました');
1068
- }
1069
  });
1070
 
1071
  function updatePlaybackRate(speed) {
1072
- try {
1073
- currentPlaybackRate = speed;
1074
- video.playbackRate = speed;
1075
-
1076
- // 音声の再生速度を変更
1077
- audioFiles.forEach(file => {
1078
- if (audioElements[file]) {
1079
- audioElements[file].playbackRate = speed;
1080
- setPreservesPitch(audioElements[file], true); // ピッチを保持
1081
- }
1082
- });
1083
- } catch (error) {
1084
- handleError(error, '再生速度更新中にエラーが発生しました');
1085
- }
1086
  }
1087
 
1088
  // 全画面ボタン
1089
  fullscreenBtn.addEventListener('click', function() {
1090
- try {
1091
- if (!isFullscreen) {
1092
- if (videoContainer.requestFullscreen) {
1093
- videoContainer.requestFullscreen();
1094
- } else if (videoContainer.webkitRequestFullscreen) {
1095
- videoContainer.webkitRequestFullscreen();
1096
- } else if (videoContainer.msRequestFullscreen) {
1097
- videoContainer.msRequestFullscreen();
1098
- }
1099
- } else {
1100
- if (document.exitFullscreen) {
1101
- document.exitFullscreen();
1102
- } else if (document.webkitExitFullscreen) {
1103
- document.webkitExitFullscreen();
1104
- } else if (document.msExitFullscreen) {
1105
- document.msExitFullscreen();
1106
- }
1107
  }
1108
- } catch (error) {
1109
- handleError(error, '全画面表示中にエラーが発生しました');
1110
  }
1111
  });
1112
 
@@ -1118,14 +1119,7 @@
1118
  function handleFullscreenChange() {
1119
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
1120
  fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
1121
-
1122
- if (isFullscreen) {
1123
- // 全画面時の処理
1124
- video.controls = false;
1125
- } else {
1126
- // 通常表示時の処理
1127
- video.controls = false;
1128
- }
1129
  }
1130
 
1131
  // キーボードイベント (ESCで全画面終了)
@@ -1141,30 +1135,27 @@
1141
  }
1142
  });
1143
 
1144
- // 音声ファイルをロード
1145
  function loadAudioFiles() {
1146
  audioFiles.forEach(file => {
1147
  try {
1148
  const audio = new Audio(`${file}.mp3`);
1149
  audio.preload = 'auto';
1150
- setPreservesPitch(audio, true); // ピッチを保持
1151
  audio.loop = false;
1152
  audioElements[file] = audio;
1153
 
1154
- // 音声が読み込まれたら
1155
  audio.addEventListener('loadedmetadata', function() {
1156
  console.log(`${file}.mp3 loaded`);
1157
  checkLoadingComplete();
1158
  });
1159
 
1160
- // エラー処理
1161
  audio.addEventListener('error', function() {
1162
- handleError(audio.error, `音声ファイル読み込み中にエラーが発生しました (${file}.mp3)`);
1163
- checkLoadingComplete(); // エラー時もカウント
1164
  });
1165
  } catch (error) {
1166
- handleError(error, `音声ファイル初期化中にエラーが発生しました (${file}.mp3)`);
1167
- checkLoadingComplete(); // エラー時もカウント
1168
  }
1169
  });
1170
  }
@@ -1172,88 +1163,63 @@
1172
  // ボリュームスライダーのイベント
1173
  audioSliders.forEach((slider, index) => {
1174
  slider.addEventListener('input', function() {
1175
- try {
1176
- const value = parseFloat(this.value);
1177
- volumeValues[index].textContent = value.toFixed(2);
1178
-
1179
- if (audioElements[this.dataset.audio]) {
1180
- const globalVolume = parseFloat(globalVolumeSlider.value) /10;
1181
- audioElements[this.dataset.audio].volume = value * globalVolume;
1182
- }
1183
- } catch (error) {
1184
- handleError(error, '音声ボリューム設定中にエラーが発生しました');
1185
  }
1186
  });
1187
  });
1188
 
1189
  // 全体音量スライダーのイベント
1190
  globalVolumeSlider.addEventListener('input', function() {
1191
- try {
1192
- const value = parseFloat(this.value);
1193
- globalVolumeValue.textContent = value.toFixed(1);
1194
-
1195
- audioFiles.forEach(file => {
1196
- if (audioElements[file]) {
1197
- const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1198
- const volume = parseFloat(volumeSlider.value) * (value /10);
1199
- audioElements[file].volume = volume;
1200
- }
1201
- });
1202
- } catch (error) {
1203
- handleError(error, '全体音量設定中にエラーが発生しました');
1204
- }
1205
  });
1206
 
1207
  // ループ設定変更時
1208
  loopCheckbox.addEventListener('change', function() {
1209
- try {
1210
- audioFiles.forEach(file => {
1211
- if (audioElements[file]) {
1212
- audioElements[file].loop = this.checked;
1213
- }
1214
- });
1215
- } catch (error) {
1216
- handleError(error, 'ループ設定変更中にエラーが発生しました');
1217
- }
1218
  });
1219
 
1220
  // 現在の秒数を開始時間に設定
1221
  setStartTimeBtn.addEventListener('click', function() {
1222
- try {
1223
- startTimeInput.value = video.currentTime.toFixed(2);
1224
- } catch (error) {
1225
- handleError(error, '開始時間設定中にエラーが発生しました');
1226
- }
1227
  });
1228
 
1229
  // 現在の秒数を終了時間に設定
1230
  setEndTimeBtn.addEventListener('click', function() {
1231
- try {
1232
- endTimeInput.value = video.currentTime.toFixed(2);
1233
- } catch (error) {
1234
- handleError(error, '終了時間設定中にエラーが発生しました');
1235
- }
1236
  });
1237
 
1238
  // スライダーの背景を更新
1239
  function updateSliderBackgrounds() {
1240
- // ボリュームスライダー
1241
  const volumePercent = volumeSlider.value * 100;
1242
  volumeSlider.style.backgroundSize = `${volumePercent}% 100%`;
1243
 
1244
- // 再生速度スライダー
1245
  const speedPercent = (speedSlider.value - speedSlider.min) / (speedSlider.max - speedSlider.min) * 100;
1246
  speedSlider.style.backgroundSize = `${speedPercent}% 100%`;
1247
 
1248
- // 全体音量スライダー
1249
  const globalVolumePercent = (globalVolumeSlider.value - globalVolumeSlider.min) / (globalVolumeSlider.max - globalVolumeSlider.min) * 100;
1250
  globalVolumeSlider.style.backgroundSize = `${globalVolumePercent}% 100%`;
1251
 
1252
- // 再生速度スライダー (設定)
1253
  const playbackSpeedPercent = (playbackSpeedSlider.value - playbackSpeedSlider.min) / (playbackSpeedSlider.max - playbackSpeedSlider.min) * 100;
1254
  playbackSpeedSlider.style.backgroundSize = `${playbackSpeedPercent}% 100%`;
1255
 
1256
- // 各音声スライダー
1257
  audioSliders.forEach(slider => {
1258
  const percent = slider.value * 100;
1259
  slider.style.backgroundSize = `${percent}% 100%`;
@@ -1262,7 +1228,6 @@
1262
 
1263
  // スライダーの初期背景を設定
1264
  function initSliderBackgrounds() {
1265
- // すべてのスライダーに背景を設定
1266
  const sliders = [
1267
  volumeSlider,
1268
  speedSlider,
@@ -1302,6 +1267,13 @@
1302
  volumeSlider.value = video.volume;
1303
  video.controls = false;
1304
  initSliderBackgrounds();
 
 
 
 
 
 
 
1305
  });
1306
  </script>
1307
  </body>
 
642
  function createTechBackground() {
643
  const bg = document.getElementById('techBg');
644
 
 
645
  for (let i = 0; i < 200; i++) {
646
  const line = document.createElement('div');
647
  line.className = 'circuit-line';
 
658
  line.style.left = `${Math.random() * 100}%`;
659
  line.style.top = `${Math.random() * 100}%`;
660
  line.style.opacity = Math.random() * 0.5 + 0.1;
 
661
  bg.appendChild(line);
662
  }
663
+
 
664
  for (let i = 0; i < 200; i++) {
665
  const dot = document.createElement('div');
666
  dot.className = 'grid-dot';
 
668
  dot.style.top = `${Math.random() * 100}%`;
669
  bg.appendChild(dot);
670
  }
671
+
 
672
  for (let i = 0; i < 15; i++) {
673
  const hex = document.createElement('div');
674
  hex.className = 'hexagon';
 
678
  hex.style.animation = `float ${Math.random() * 10 + 5}s infinite ease-in-out`;
679
  bg.appendChild(hex);
680
  }
681
+
 
682
  for (let i = 0; i < 8; i++) {
683
  const pulse = document.createElement('div');
684
  pulse.className = 'pulse';
 
693
 
694
  // ローディング状態を管理
695
  let loadingCount = 0;
696
+ let totalToLoad = 6;
697
  let lastUpdateTime = 0;
698
+ const updateInterval = 1;
699
 
 
700
  function checkLoadingComplete() {
701
  loadingCount++;
702
  if (loadingCount >= totalToLoad) {
 
710
  }
711
  }
712
 
 
713
  function handleError(error, message) {
714
  console.error(message, error);
715
  window.alert(`${message}\n\nエラー詳細: ${error.message}`);
716
  }
717
 
718
+ // Web Audio Context の初期化
719
+ let audioContext;
720
+ try {
721
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
722
+ } catch (e) {
723
+ console.error('Web Audio APIがサポートされていません:', e);
 
 
 
 
 
 
 
724
  }
725
+
726
  // 要素を取得
727
  const video = document.getElementById('video');
728
  const videoContainer = document.getElementById('video-container');
 
750
 
751
  // 音声オブジェクトを作成
752
  const audioElements = {};
753
+ const audioSources = {};
 
754
  const audioFiles = ['p', 'a', 't', 's', 'k'];
755
 
756
  // 初期化
 
759
  let lastVolume = 1;
760
  let currentPlaybackRate = 1;
761
  let isFullscreen = false;
762
+ let lastSyncTime = 0;
763
+ let syncDriftLog = [];
764
+
765
+ // 同期チェック関数
766
+ function checkSync() {
767
+ if (!isPlaying) return;
768
+
769
+ const now = performance.now();
770
+ if (now - lastSyncTime < 500) return; // 0.5秒ごとにチェック
771
+ lastSyncTime = now;
772
+
773
+ const videoTime = video.currentTime;
774
+ let maxDrift = 0;
775
+
776
+ audioFiles.forEach(file => {
777
+ if (audioElements[file]) {
778
+ const audioTime = audioElements[file].currentTime;
779
+ const drift = Math.abs(audioTime - videoTime);
780
+
781
+ if (drift > maxDrift) maxDrift = drift;
782
+
783
+ // 50ms以上のずれを修正
784
+ if (drift > 0.05) {
785
+ audioElements[file].currentTime = videoTime;
786
+ console.log(`同期補正: ${file} (差: ${drift.toFixed(3)}秒)`);
787
+ }
788
+ }
789
+ });
790
+
791
+ syncDriftLog.push(maxDrift);
792
+ if (syncDriftLog.length > 10) syncDriftLog.shift();
793
+ }
794
+
795
+ // 平均ずれを計算
796
+ function getAverageDrift() {
797
+ if (syncDriftLog.length === 0) return 0;
798
+ return syncDriftLog.reduce((a, b) => a + b, 0) / syncDriftLog.length;
799
+ }
800
 
801
  // 動画のメタデータが読み込まれたら
802
  video.addEventListener('loadedmetadata', function() {
 
819
 
820
  // 再生ボタンクリック
821
  playPauseBtn.addEventListener('click', function() {
822
+ const endTime = parseFloat(endTimeInput.value) || videoDuration;
823
+ if (video.currentTime >= endTime) {
824
+ const startTime = parseFloat(startTimeInput.value) || 0;
825
+ seekMedia(startTime);
 
 
 
 
 
 
 
826
  }
827
+ togglePlayPause();
828
  });
829
 
830
  // 時間表示を更新
 
848
  `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
849
  `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
850
 
 
851
  const progressPercent = (currentTime / duration) * 100;
852
  progressBar.style.width = `${progressPercent}%`;
853
+
854
+ // 同期チェックを実行
855
+ checkSync();
856
  } catch (error) {
857
+ console.error('時間表示更新エラー:', error);
858
  }
859
  }
860
 
 
867
  }
868
  }
869
 
870
+ // 再生関数 (改良版)
871
  function playMedia() {
872
  try {
873
  const duration = video.duration || videoDuration;
874
  const startTime = parseFloat(startTimeInput.value) || 0;
875
  const endTime = parseFloat(endTimeInput.value) || duration;
876
 
 
877
  if (video.currentTime >= endTime) {
878
  video.currentTime = startTime;
879
  }
880
 
881
  // 動画を再生
882
+ const playPromise = video.play();
883
+
884
+ if (playPromise !== undefined) {
885
+ playPromise.then(() => {
886
+ isPlaying = true;
887
+ playPauseBtn.textContent = '⏸';
888
+
889
+ // 音声を再生 (Web Audio APIを使用)
890
+ const currentTime = video.currentTime;
891
+ audioFiles.forEach(file => {
892
+ if (audioElements[file]) {
893
+ audioElements[file].currentTime = currentTime;
894
+ const playPromise = audioElements[file].play();
895
+
896
+ if (playPromise !== undefined) {
897
+ playPromise.catch(e => {
898
+ if (e.name !== 'AbortError') {
899
+ console.error(`音声再生エラー (${file}.mp3):`, e);
900
+ }
901
+ });
902
  }
903
+ }
904
+ });
905
+
906
+ // 初回同期を即時実行
907
+ setTimeout(checkSync, 100);
908
+ }).catch(error => {
909
+ console.error('動画再生エラー:', error);
910
  });
911
+ }
 
 
912
  } catch (error) {
913
  console.error('メディア再生エラー:', error);
914
  }
 
921
  isPlaying = false;
922
  playPauseBtn.textContent = '▶';
923
 
 
924
  audioFiles.forEach(file => {
925
  if (audioElements[file]) {
926
  audioElements[file].pause();
 
936
  const duration = video.duration || videoDuration;
937
  const endTime = parseFloat(endTimeInput.value) || duration;
938
 
 
939
  if (video.currentTime >= endTime && endTime > 0) {
940
  if (loopCheckbox.checked) {
941
  const startTime = parseFloat(startTimeInput.value) || 0;
942
  video.currentTime = startTime;
943
 
 
944
  audioFiles.forEach(file => {
945
  if (audioElements[file]) {
946
  audioElements[file].currentTime = startTime;
 
964
 
965
  // プログレスバークリックでシーク
966
  progressContainer.addEventListener('click', function(e) {
967
+ if (!video.duration) return;
968
+
969
+ const rect = this.getBoundingClientRect();
970
+ const pos = (e.clientX - rect.left) / rect.width;
971
+ const seekTime = pos * video.duration;
972
+
973
+ seekMedia(seekTime);
 
 
 
 
974
  });
975
 
976
+ // 指定した時間にシーク (改良版)
977
  function seekMedia(time) {
978
  try {
979
  const duration = video.duration || videoDuration;
980
  const startTime = parseFloat(startTimeInput.value) || 0;
981
  const endTime = parseFloat(endTimeInput.value) || duration;
982
 
 
983
  const seekTime = Math.max(startTime, Math.min(time, endTime));
 
984
  video.currentTime = seekTime;
985
 
 
986
  audioFiles.forEach(file => {
987
  if (audioElements[file]) {
988
  audioElements[file].currentTime = seekTime;
989
  }
990
  });
991
+
992
+ // シーク後の同期を強化
993
+ if (isPlaying) {
994
+ setTimeout(() => {
995
+ audioFiles.forEach(file => {
996
+ if (audioElements[file]) {
997
+ audioElements[file].currentTime = video.currentTime;
998
+ }
999
+ });
1000
+ }, 50);
1001
+ }
1002
  } catch (error) {
1003
+ console.error('メディアシークエラー:', error);
1004
  }
1005
  }
1006
 
1007
  // プログレスバー上でマウス移動時に時間を表示
1008
  progressContainer.addEventListener('mousemove', function(e) {
1009
+ if (!video.duration) return;
1010
+
1011
+ const rect = this.getBoundingClientRect();
1012
+ const pos = (e.clientX - rect.left) / rect.width;
1013
+ const hoverTime = pos * video.duration;
1014
+
1015
+ const minutes = Math.floor(hoverTime / 60);
1016
+ const seconds = Math.floor(hoverTime % 60);
1017
+ const milliseconds = Math.floor((hoverTime % 1) * 100);
1018
+
1019
+ progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
1020
+ progressTime.style.display = 'block';
1021
+ progressTime.style.left = `${pos * 100}%`;
 
 
 
 
1022
  });
1023
 
1024
  progressContainer.addEventListener('mouseleave', function() {
 
1032
 
1033
  // 音量コントロール
1034
  volumeSlider.addEventListener('input', function() {
1035
+ video.volume = this.value;
1036
+ lastVolume = this.value;
1037
+ updateVolumeIcon();
 
 
 
 
1038
  });
1039
 
1040
  // 音量ボタン
1041
  volumeBtn.addEventListener('click', function() {
1042
+ if (video.volume > 0) {
1043
+ lastVolume = video.volume;
1044
+ video.volume = 0;
1045
+ volumeSlider.value = 0;
1046
+ } else {
1047
+ video.volume = lastVolume;
1048
+ volumeSlider.value = lastVolume;
 
 
 
 
 
1049
  }
1050
+ updateVolumeIcon();
1051
  });
1052
 
1053
  // 音量アイコンを更新
 
1063
 
1064
  // 再生速度スライダー (動画プレイヤー)
1065
  speedSlider.addEventListener('input', function() {
1066
+ const speed = parseFloat(this.value);
1067
+ speedValue.textContent = speed.toFixed(2) + 'x';
1068
+ playbackSpeedSlider.value = speed;
1069
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1070
+ updatePlaybackRate(speed);
 
 
 
 
1071
  });
1072
 
1073
  // 再生速度スライダー (設定メニュー)
1074
  playbackSpeedSlider.addEventListener('input', function() {
1075
+ const speed = parseFloat(this.value);
1076
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
1077
+ speedSlider.value = speed;
1078
+ speedValue.textContent = speed.toFixed(2) + 'x';
1079
+ updatePlaybackRate(speed);
 
 
 
 
1080
  });
1081
 
1082
  function updatePlaybackRate(speed) {
1083
+ currentPlaybackRate = speed;
1084
+ video.playbackRate = speed;
1085
+
1086
+ audioFiles.forEach(file => {
1087
+ if (audioElements[file]) {
1088
+ audioElements[file].playbackRate = speed;
1089
+ }
1090
+ });
 
 
 
 
 
 
1091
  }
1092
 
1093
  // 全画面ボタン
1094
  fullscreenBtn.addEventListener('click', function() {
1095
+ if (!isFullscreen) {
1096
+ if (videoContainer.requestFullscreen) {
1097
+ videoContainer.requestFullscreen();
1098
+ } else if (videoContainer.webkitRequestFullscreen) {
1099
+ videoContainer.webkitRequestFullscreen();
1100
+ } else if (videoContainer.msRequestFullscreen) {
1101
+ videoContainer.msRequestFullscreen();
1102
+ }
1103
+ } else {
1104
+ if (document.exitFullscreen) {
1105
+ document.exitFullscreen();
1106
+ } else if (document.webkitExitFullscreen) {
1107
+ document.webkitExitFullscreen();
1108
+ } else if (document.msExitFullscreen) {
1109
+ document.msExitFullscreen();
 
 
1110
  }
 
 
1111
  }
1112
  });
1113
 
 
1119
  function handleFullscreenChange() {
1120
  isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
1121
  fullscreenBtn.textContent = isFullscreen ? '⛶' : '⛶';
1122
+ video.controls = false;
 
 
 
 
 
 
 
1123
  }
1124
 
1125
  // キーボードイベント (ESCで全画面終了)
 
1135
  }
1136
  });
1137
 
1138
+ // 音声ファイルをロード (改良版)
1139
  function loadAudioFiles() {
1140
  audioFiles.forEach(file => {
1141
  try {
1142
  const audio = new Audio(`${file}.mp3`);
1143
  audio.preload = 'auto';
 
1144
  audio.loop = false;
1145
  audioElements[file] = audio;
1146
 
 
1147
  audio.addEventListener('loadedmetadata', function() {
1148
  console.log(`${file}.mp3 loaded`);
1149
  checkLoadingComplete();
1150
  });
1151
 
 
1152
  audio.addEventListener('error', function() {
1153
+ console.error(`音声ファイル読み込みエラー (${file}.mp3):`, audio.error);
1154
+ checkLoadingComplete();
1155
  });
1156
  } catch (error) {
1157
+ console.error(`音声ファイル初期化エラー (${file}.mp3):`, error);
1158
+ checkLoadingComplete();
1159
  }
1160
  });
1161
  }
 
1163
  // ボリュームスライダーのイベント
1164
  audioSliders.forEach((slider, index) => {
1165
  slider.addEventListener('input', function() {
1166
+ const value = parseFloat(this.value);
1167
+ volumeValues[index].textContent = value.toFixed(2);
1168
+
1169
+ if (audioElements[this.dataset.audio]) {
1170
+ const globalVolume = parseFloat(globalVolumeSlider.value) /10;
1171
+ audioElements[this.dataset.audio].volume = value * globalVolume;
 
 
 
 
1172
  }
1173
  });
1174
  });
1175
 
1176
  // 全体音量スライダーのイベント
1177
  globalVolumeSlider.addEventListener('input', function() {
1178
+ const value = parseFloat(this.value);
1179
+ globalVolumeValue.textContent = value.toFixed(1);
1180
+
1181
+ audioFiles.forEach(file => {
1182
+ if (audioElements[file]) {
1183
+ const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1184
+ const volume = parseFloat(volumeSlider.value) * (value /10);
1185
+ audioElements[file].volume = volume;
1186
+ }
1187
+ });
 
 
 
 
1188
  });
1189
 
1190
  // ループ設定変更時
1191
  loopCheckbox.addEventListener('change', function() {
1192
+ audioFiles.forEach(file => {
1193
+ if (audioElements[file]) {
1194
+ audioElements[file].loop = this.checked;
1195
+ }
1196
+ });
 
 
 
 
1197
  });
1198
 
1199
  // 現在の秒数を開始時間に設定
1200
  setStartTimeBtn.addEventListener('click', function() {
1201
+ startTimeInput.value = video.currentTime.toFixed(2);
 
 
 
 
1202
  });
1203
 
1204
  // 現在の秒数を終了時間に設定
1205
  setEndTimeBtn.addEventListener('click', function() {
1206
+ endTimeInput.value = video.currentTime.toFixed(2);
 
 
 
 
1207
  });
1208
 
1209
  // スライダーの背景を更新
1210
  function updateSliderBackgrounds() {
 
1211
  const volumePercent = volumeSlider.value * 100;
1212
  volumeSlider.style.backgroundSize = `${volumePercent}% 100%`;
1213
 
 
1214
  const speedPercent = (speedSlider.value - speedSlider.min) / (speedSlider.max - speedSlider.min) * 100;
1215
  speedSlider.style.backgroundSize = `${speedPercent}% 100%`;
1216
 
 
1217
  const globalVolumePercent = (globalVolumeSlider.value - globalVolumeSlider.min) / (globalVolumeSlider.max - globalVolumeSlider.min) * 100;
1218
  globalVolumeSlider.style.backgroundSize = `${globalVolumePercent}% 100%`;
1219
 
 
1220
  const playbackSpeedPercent = (playbackSpeedSlider.value - playbackSpeedSlider.min) / (playbackSpeedSlider.max - playbackSpeedSlider.min) * 100;
1221
  playbackSpeedSlider.style.backgroundSize = `${playbackSpeedPercent}% 100%`;
1222
 
 
1223
  audioSliders.forEach(slider => {
1224
  const percent = slider.value * 100;
1225
  slider.style.backgroundSize = `${percent}% 100%`;
 
1228
 
1229
  // スライダーの初期背景を設定
1230
  function initSliderBackgrounds() {
 
1231
  const sliders = [
1232
  volumeSlider,
1233
  speedSlider,
 
1267
  volumeSlider.value = video.volume;
1268
  video.controls = false;
1269
  initSliderBackgrounds();
1270
+
1271
+ // 定期的に同期状態をログ出力
1272
+ setInterval(() => {
1273
+ if (isPlaying) {
1274
+ console.log(`平均同期ずれ: ${getAverageDrift().toFixed(4)}秒`);
1275
+ }
1276
+ }, 5000);
1277
  });
1278
  </script>
1279
  </body>