soiz1 commited on
Commit
b37f884
·
1 Parent(s): b2580a7

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +380 -322
index.html CHANGED
@@ -2,10 +2,11 @@
2
  <html lang="ja">
3
  <head>
4
  <meta charset="UTF-8">
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>高度な音声動画プレイヤー</title>
7
- <script src="https://soiz1-eruda3.hf.space/eruda.js"></script>
8
  <style>
 
9
  body {
10
  font-family: 'Arial', sans-serif;
11
  background-color: #0a192f;
@@ -320,28 +321,7 @@
320
  width: 100%;
321
  border-radius: 0;
322
  }
323
-
324
- .time-input-container {
325
- display: flex;
326
- align-items: center;
327
- gap: 5px;
328
- }
329
-
330
- .set-time-btn {
331
- background-color: #112240;
332
- border: 1px solid #64ffda;
333
- color: #e6f1ff;
334
- padding: 5px 8px;
335
- border-radius: 3px;
336
- cursor: pointer;
337
- font-size: 12px;
338
- transition: background-color 0.3s;
339
- }
340
-
341
- .set-time-btn:hover {
342
- background-color: rgba(100, 255, 218, 0.2);
343
- }
344
-
345
  /* ローディングアニメーション */
346
  .loading-overlay {
347
  position: fixed;
@@ -433,6 +413,22 @@
433
  transform: rotate(360deg);
434
  }
435
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  </style>
437
  </head>
438
  <body>
@@ -482,16 +478,16 @@
482
  <h2>設定</h2>
483
  <div class="setting-item">
484
  <label for="start-time">再生開始秒数:</label>
485
- <div class="time-input-container">
486
  <input type="number" id="start-time" min="0" value="0" step="0.1">
487
- <button class="set-time-btn" id="set-start-btn">現在の秒数に設定</button>
488
  </div>
489
  </div>
490
  <div class="setting-item">
491
  <label for="end-time">再生終了秒数:</label>
492
- <div class="time-input-container">
493
  <input type="number" id="end-time" min="0" value="0" step="0.1">
494
- <button class="set-time-btn" id="set-end-btn">現在の秒数に設定</button>
495
  </div>
496
  </div>
497
  <div class="setting-item">
@@ -548,6 +544,45 @@
548
 
549
  <script>
550
  document.addEventListener('DOMContentLoaded', function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  // 要素を取得
552
  const video = document.getElementById('video');
553
  const videoContainer = document.getElementById('video-container');
@@ -565,19 +600,16 @@
565
  const fullscreenBtn = document.getElementById('fullscreen-btn');
566
  const startTimeInput = document.getElementById('start-time');
567
  const endTimeInput = document.getElementById('end-time');
568
- const setStartBtn = document.getElementById('set-start-btn');
569
- const setEndBtn = document.getElementById('set-end-btn');
570
  const loopCheckbox = document.getElementById('loop');
571
  const globalVolumeSlider = document.getElementById('global-volume');
572
  const globalVolumeValue = document.getElementById('global-volume-value');
573
  const audioSliders = document.querySelectorAll('.audio-slider');
574
  const volumeValues = document.querySelectorAll('.volume-value');
575
- const loadingOverlay = document.getElementById('loadingOverlay');
 
576
 
577
  // 音声オブジェクトを作成
578
  const audioElements = {};
579
- const audioContexts = {};
580
- const audioGains = {};
581
 
582
  // 音声ファイル名の配列
583
  const audioFiles = ['p', 'a', 't', 's', 'k'];
@@ -588,80 +620,89 @@
588
  let isVideoPlaying = false;
589
  let lastVolume = 1;
590
  let currentPlaybackRate = 1;
591
- let allAudioLoaded = false;
592
- let videoLoaded = false;
593
-
594
- // ローディングアニメーションを非表示にする関数
595
- function hideLoadingOverlay() {
596
- loadingOverlay.style.opacity = '0';
597
- setTimeout(function() {
598
- loadingOverlay.style.display = 'none';
599
- }, 1000);
600
- }
601
-
602
- // すべてのメディアが読み込まれたかチェック
603
- function checkAllMediaLoaded() {
604
- if (allAudioLoaded && videoLoaded) {
605
- setTimeout(hideLoadingOverlay, 500);
606
- }
607
- }
608
 
609
  // 動画のメタデータが読み込まれたら
610
  video.addEventListener('loadedmetadata', function() {
611
- videoDuration = video.duration;
612
- endTimeInput.value = videoDuration.toFixed(1);
613
- endTimeInput.max = videoDuration;
614
- startTimeInput.max = videoDuration - 0.1;
615
- updateTimeDisplay();
616
- videoLoaded = true;
617
- checkAllMediaLoaded();
 
 
 
618
  });
619
 
620
- // 動画のエラー処理
621
  video.addEventListener('error', function() {
622
- console.error('動画の読み込み中にエラーが発生しました');
623
- videoLoaded = true;
624
- checkAllMediaLoaded();
625
  });
626
 
627
- // 再生/一時停止ボタン
628
- playPauseBtn.addEventListener('click', function(e) {
629
- e.stopPropagation();
630
- togglePlayPause();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  });
632
 
633
- // 時間表示を更新 (小数点以下2桁表示)
634
  function updateTimeDisplay() {
635
- const currentTime = video.currentTime;
636
- const duration = video.duration || videoDuration;
637
-
638
- const currentMinutes = Math.floor(currentTime / 60);
639
- const currentSeconds = Math.floor(currentTime % 60);
640
- const currentMilliseconds = Math.floor((currentTime % 1) * 100);
641
- const durationMinutes = Math.floor(duration / 60);
642
- const durationSeconds = Math.floor(duration % 60);
643
- const durationMilliseconds = Math.floor((duration % 1) * 100);
644
-
645
- timeDisplay.textContent =
646
- `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
647
- `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
648
-
649
- // プログレスバーを更新
650
- const progressPercent = (currentTime / duration) * 100;
651
- progressBar.style.width = `${progressPercent}%`;
652
-
653
- // 現在の秒数が終了秒数を超えている場合、一時停止
654
- const endTime = parseFloat(endTimeInput.value) || duration;
655
- if (currentTime >= endTime) {
656
- if (loopCheckbox.checked) {
657
- const startTime = parseFloat(startTimeInput.value) || 0;
658
- seekMedia(startTime);
659
- if (!isPlaying) {
660
- playMedia();
 
 
 
 
661
  }
662
- } else {
663
- pauseMedia();
664
  }
 
 
665
  }
666
  }
667
 
@@ -672,58 +713,74 @@
672
 
673
  // 動画終了時の処理
674
  video.addEventListener('ended', function() {
675
- if (!loopCheckbox.checked) {
676
- const startTime = parseFloat(startTimeInput.value) || 0;
677
- seekMedia(startTime);
678
- pauseMedia();
 
 
 
 
679
  }
680
  });
681
 
682
  // プログレスバークリックでシーク
683
  progressContainer.addEventListener('click', function(e) {
684
- if (!video.duration) return;
685
-
686
- const rect = this.getBoundingClientRect();
687
- const pos = (e.clientX - rect.left) / rect.width;
688
- const seekTime = pos * video.duration;
689
-
690
- seekMedia(seekTime);
 
 
 
 
691
  });
692
 
693
  // 指定した時間にシーク
694
  function seekMedia(time) {
695
- const duration = video.duration || videoDuration;
696
- const startTime = parseFloat(startTimeInput.value) || 0;
697
- const endTime = parseFloat(endTimeInput.value) || duration;
698
-
699
- // 範囲内に制限
700
- const seekTime = Math.max(startTime, Math.min(time, endTime));
701
-
702
- video.currentTime = seekTime;
703
-
704
- // 音声も同期
705
- audioFiles.forEach(file => {
706
- if (audioElements[file]) {
707
- audioElements[file].currentTime = seekTime;
708
- }
709
- });
 
 
 
 
710
  }
711
 
712
  // プログレスバー上でマウス移動時に時間を表示
713
  progressContainer.addEventListener('mousemove', function(e) {
714
- if (!video.duration) return;
715
-
716
- const rect = this.getBoundingClientRect();
717
- const pos = (e.clientX - rect.left) / rect.width;
718
- const hoverTime = pos * video.duration;
719
-
720
- const minutes = Math.floor(hoverTime / 60);
721
- const seconds = Math.floor(hoverTime % 60);
722
- const milliseconds = Math.floor((hoverTime % 1) * 100);
723
-
724
- progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
725
- progressTime.style.display = 'block';
726
- progressTime.style.left = `${pos * 100}%`;
 
 
 
 
727
  });
728
 
729
  progressContainer.addEventListener('mouseleave', function() {
@@ -746,22 +803,30 @@
746
 
747
  // 音量コントロール
748
  volumeSlider.addEventListener('input', function() {
749
- video.volume = this.value;
750
- lastVolume = this.value;
751
- updateVolumeIcon();
 
 
 
 
752
  });
753
 
754
  // 音量ボタン
755
  volumeBtn.addEventListener('click', function() {
756
- if (video.volume > 0) {
757
- lastVolume = video.volume;
758
- video.volume = 0;
759
- volumeSlider.value = 0;
760
- } else {
761
- video.volume = lastVolume;
762
- volumeSlider.value = lastVolume;
 
 
 
 
 
763
  }
764
- updateVolumeIcon();
765
  });
766
 
767
  // 音量アイコンを更新
@@ -777,43 +842,59 @@
777
 
778
  // 再生速度スライダー (動画プレイヤー)
779
  speedSlider.addEventListener('input', function() {
780
- const speed = parseFloat(this.value);
781
- speedValue.textContent = speed.toFixed(2) + 'x';
782
- playbackSpeedSlider.value = speed;
783
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
784
- updatePlaybackRate(speed);
 
 
 
 
785
  });
786
 
787
  // 再生速度スライダー (設定メニュー)
788
  playbackSpeedSlider.addEventListener('input', function() {
789
- const speed = parseFloat(this.value);
790
- playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
791
- speedSlider.value = speed;
792
- speedValue.textContent = speed.toFixed(2) + 'x';
793
- updatePlaybackRate(speed);
 
 
 
 
794
  });
795
 
796
  function updatePlaybackRate(speed) {
797
- currentPlaybackRate = speed;
798
- video.playbackRate = speed;
799
-
800
- // 音声の再生速度を変更
801
- audioFiles.forEach(file => {
802
- if (audioElements[file]) {
803
- audioElements[file].playbackRate = speed;
804
- audioElements[file].preservesPitch = true; // ピッチを保持
805
- }
806
- });
 
 
 
 
807
  }
808
 
809
  // 全画面ボタン
810
  fullscreenBtn.addEventListener('click', function() {
811
- if (videoContainer.requestFullscreen) {
812
- videoContainer.requestFullscreen();
813
- } else if (videoContainer.webkitRequestFullscreen) {
814
- videoContainer.webkitRequestFullscreen();
815
- } else if (videoContainer.msRequestFullscreen) {
816
- videoContainer.msRequestFullscreen();
 
 
 
 
817
  }
818
  });
819
 
@@ -835,119 +916,94 @@
835
 
836
  // 音声ファイルをロード
837
  function loadAudioFiles() {
838
- let loadedCount = 0;
839
-
840
  audioFiles.forEach(file => {
841
- const audio = new Audio(`${file}.mp3`);
842
- audio.preload = 'auto';
843
- audio.preservesPitch = true; // ピッチを保持
844
- audio.loop = false;
845
- audioElements[file] = audio;
846
-
847
- // Web Audio API コンテキストを作成
848
  try {
849
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
850
- const source = audioContext.createMediaElementSource(audio);
851
- const gainNode = audioContext.createGain();
 
 
852
 
853
- source.connect(gainNode);
854
- gainNode.connect(audioContext.destination);
 
 
 
855
 
856
- audioContexts[file] = audioContext;
857
- audioGains[file] = gainNode;
858
- } catch (e) {
859
- console.error('Web Audio API の初期化に失敗しました:', e);
 
 
 
 
860
  }
861
-
862
- // 音声が読み込まれたら
863
- audio.addEventListener('loadedmetadata', function() {
864
- console.log(`${file}.mp3 loaded`);
865
- loadedCount++;
866
-
867
- if (loadedCount === audioFiles.length) {
868
- allAudioLoaded = true;
869
- checkAllMediaLoaded();
870
- }
871
- });
872
-
873
- // エラー処理
874
- audio.addEventListener('error', function() {
875
- console.error(`Error loading ${file}.mp3`);
876
- loadedCount++;
877
-
878
- if (loadedCount === audioFiles.length) {
879
- allAudioLoaded = true;
880
- checkAllMediaLoaded();
881
- }
882
- });
883
  });
884
  }
885
 
886
  function playAudio(file, startTime) {
887
  if (!audioElements[file]) return;
888
 
889
- const audio = audioElements[file];
890
- const duration = video.duration || videoDuration;
891
- const endTime = parseFloat(endTimeInput.value) || duration;
892
-
893
- // ボリューム設定
894
- const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
895
- const volume = parseFloat(volumeSlider.value) * parseFloat(globalVolumeSlider.value);
896
- if (audioGains[file]) {
897
- audioGains[file].gain.value = volume;
898
- } else {
899
  audio.volume = volume;
900
- }
901
-
902
- // 再生速度設定
903
- audio.playbackRate = currentPlaybackRate;
904
- audio.preservesPitch = true;
905
-
906
- // 再生時間計算
907
- const playStartTime = Math.max(startTime, parseFloat(startTimeInput.value) || 0);
908
- const playEndTime = Math.min(endTime, duration);
909
-
910
- audio.currentTime = playStartTime;
911
-
912
- // 再生
913
- const playPromise = audio.play();
914
-
915
- if (playPromise !== undefined) {
916
- playPromise.catch(e => {
917
- console.log("Audio play failed:", e);
918
- // ユーザー操作がない場合、音声をミュートにして再試行
919
- audio.muted = true;
920
- audio.play().catch(e => console.log("Retry with mute failed:", e));
921
- });
922
- }
923
-
924
- // ループ設定
925
- audio.loop = loopCheckbox.checked;
926
-
927
- if (!loopCheckbox.checked) {
928
- // ループ再生でない場合、終了時間に達したら停止
929
- setTimeout(() => {
930
- if (audio.currentTime >= playEndTime) {
931
- audio.pause();
932
  }
933
- }, (playEndTime - playStartTime) * 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
934
  }
935
  }
936
 
937
  // 再生関数
938
  function playMedia() {
939
- const currentTime = video.currentTime;
940
- const duration = video.duration || videoDuration;
941
- const endTime = parseFloat(endTimeInput.value) || duration;
942
-
943
- // 現在位置が終了時間を超えている場合、開始位置に戻る
944
- if (currentTime >= endTime) {
945
- const startTime = parseFloat(startTimeInput.value) || 0;
946
- seekMedia(startTime);
947
- }
948
-
949
- // 動画を再生
950
- video.play().then(() => {
 
 
951
  isPlaying = true;
952
  isVideoPlaying = true;
953
  playPauseBtn.textContent = '⏸';
@@ -956,79 +1012,95 @@
956
  audioFiles.forEach(file => {
957
  playAudio(file, video.currentTime);
958
  });
959
- }).catch(e => {
960
- console.error("Video play failed:", e);
961
- });
962
  }
963
 
964
  // 一時停止関数
965
  function pauseMedia() {
966
  if (!isPlaying) return;
967
 
968
- video.pause();
969
- audioFiles.forEach(file => {
970
- if (audioElements[file]) {
971
- audioElements[file].pause();
972
- }
973
- });
974
-
975
- isPlaying = false;
976
- isVideoPlaying = false;
977
- playPauseBtn.textContent = '▶';
 
 
 
 
978
  }
979
 
980
  // ボリュームスライダーのイベント
981
  audioSliders.forEach((slider, index) => {
982
  slider.addEventListener('input', function() {
983
- const value = parseFloat(this.value);
984
- volumeValues[index].textContent = value.toFixed(2);
985
-
986
- if (audioElements[this.dataset.audio]) {
987
- const globalVolume = parseFloat(globalVolumeSlider.value);
988
- if (audioGains[this.dataset.audio]) {
989
- audioGains[this.dataset.audio].gain.value = value * globalVolume;
990
- } else {
991
  audioElements[this.dataset.audio].volume = value * globalVolume;
992
  }
 
 
993
  }
994
  });
995
  });
996
 
997
  // 全体音量スライダーのイベント
998
  globalVolumeSlider.addEventListener('input', function() {
999
- const value = parseFloat(this.value);
1000
- globalVolumeValue.textContent = value.toFixed(1);
1001
-
1002
- audioFiles.forEach(file => {
1003
- if (audioElements[file]) {
1004
- const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1005
- const volume = parseFloat(volumeSlider.value) * value;
1006
- if (audioGains[file]) {
1007
- audioGains[file].gain.value = volume;
1008
- } else {
1009
  audioElements[file].volume = volume;
1010
  }
1011
- }
1012
- });
 
 
1013
  });
1014
 
1015
  // ループ設定変更時
1016
  loopCheckbox.addEventListener('change', function() {
1017
- audioFiles.forEach(file => {
1018
- if (audioElements[file]) {
1019
- audioElements[file].loop = this.checked;
1020
- }
1021
- });
 
 
 
 
1022
  });
1023
 
1024
- // 開始秒数を現在の秒数に設定
1025
- setStartBtn.addEventListener('click', function() {
1026
- startTimeInput.value = video.currentTime.toFixed(1);
 
 
 
 
1027
  });
1028
 
1029
- // 終了秒数を現在の秒数に設定
1030
- setEndBtn.addEventListener('click', function() {
1031
- endTimeInput.value = video.currentTime.toFixed(1);
 
 
 
 
1032
  });
1033
 
1034
  // 初期化
@@ -1036,20 +1108,6 @@
1036
  updateVolumeIcon();
1037
  volumeSlider.value = video.volume;
1038
  video.controls = false;
1039
-
1040
- // 現在の秒数が終了秒数より後なら、一時停止させる
1041
- function checkCurrentTime() {
1042
- const duration = video.duration || videoDuration;
1043
- const endTime = parseFloat(endTimeInput.value) || duration;
1044
-
1045
- if (video.currentTime >= endTime) {
1046
- seekMedia(endTime);
1047
- pauseMedia();
1048
- }
1049
- }
1050
-
1051
- // 定期的にチェック
1052
- setInterval(checkCurrentTime, 1000);
1053
  });
1054
  </script>
1055
  </body>
 
2
  <html lang="ja">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <script src="https://soiz1-eruda3.hf.space/eruda.js"></script>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
  <title>高度な音声動画プレイヤー</title>
 
8
  <style>
9
+ /* 既存のスタイルはそのまま保持 */
10
  body {
11
  font-family: 'Arial', sans-serif;
12
  background-color: #0a192f;
 
321
  width: 100%;
322
  border-radius: 0;
323
  }
324
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  /* ローディングアニメーション */
326
  .loading-overlay {
327
  position: fixed;
 
413
  transform: rotate(360deg);
414
  }
415
  }
416
+
417
+ .time-set-button {
418
+ background-color: #112240;
419
+ border: 1px solid #64ffda;
420
+ color: #e6f1ff;
421
+ padding: 5px 10px;
422
+ border-radius: 3px;
423
+ cursor: pointer;
424
+ font-size: 12px;
425
+ margin-left: 5px;
426
+ transition: background-color 0.3s;
427
+ }
428
+
429
+ .time-set-button:hover {
430
+ background-color: rgba(100, 255, 218, 0.2);
431
+ }
432
  </style>
433
  </head>
434
  <body>
 
478
  <h2>設定</h2>
479
  <div class="setting-item">
480
  <label for="start-time">再生開始秒数:</label>
481
+ <div>
482
  <input type="number" id="start-time" min="0" value="0" step="0.1">
483
+ <button class="time-set-button" id="set-start-time">現在の秒数に設定</button>
484
  </div>
485
  </div>
486
  <div class="setting-item">
487
  <label for="end-time">再生終了秒数:</label>
488
+ <div>
489
  <input type="number" id="end-time" min="0" value="0" step="0.1">
490
+ <button class="time-set-button" id="set-end-time">現在の秒数に設定</button>
491
  </div>
492
  </div>
493
  <div class="setting-item">
 
544
 
545
  <script>
546
  document.addEventListener('DOMContentLoaded', function() {
547
+ // ローディング状態を管理
548
+ let loadingCount = 0;
549
+ let totalToLoad = 6; // 動画 + 5音声ファイル
550
+
551
+ // ローディング完了チェック
552
+ function checkLoadingComplete() {
553
+ loadingCount++;
554
+ if (loadingCount >= totalToLoad) {
555
+ setTimeout(function() {
556
+ const loadingOverlay = document.getElementById('loadingOverlay');
557
+ loadingOverlay.style.opacity = '0';
558
+ setTimeout(function() {
559
+ loadingOverlay.style.display = 'none';
560
+ }, 1000);
561
+ }, 500);
562
+ }
563
+ }
564
+
565
+ // エラーハンドリング関数
566
+ function handleError(error, message) {
567
+ console.error(message, error);
568
+ window.alert(`${message}\n\nエラー詳細: ${error.message}`);
569
+ }
570
+
571
+ // ピッチ設定関数
572
+ function setPreservesPitch(el, value) {
573
+ try {
574
+ if ('preservesPitch' in el) {
575
+ el.preservesPitch = value; // モダンブラウザ
576
+ } else if ('webkitPreservesPitch' in el) {
577
+ el.webkitPreservesPitch = value; // 古い WebKit (Chrome <86 など)
578
+ } else if ('mozPreservesPitch' in el) {
579
+ el.mozPreservesPitch = value; // 古い Gecko (Firefox ≤100)
580
+ }
581
+ } catch (error) {
582
+ handleError(error, 'ピッチ設定中にエラーが発生しました');
583
+ }
584
+ }
585
+
586
  // 要素を取得
587
  const video = document.getElementById('video');
588
  const videoContainer = document.getElementById('video-container');
 
600
  const fullscreenBtn = document.getElementById('fullscreen-btn');
601
  const startTimeInput = document.getElementById('start-time');
602
  const endTimeInput = document.getElementById('end-time');
 
 
603
  const loopCheckbox = document.getElementById('loop');
604
  const globalVolumeSlider = document.getElementById('global-volume');
605
  const globalVolumeValue = document.getElementById('global-volume-value');
606
  const audioSliders = document.querySelectorAll('.audio-slider');
607
  const volumeValues = document.querySelectorAll('.volume-value');
608
+ const setStartTimeBtn = document.getElementById('set-start-time');
609
+ const setEndTimeBtn = document.getElementById('set-end-time');
610
 
611
  // 音声オブジェクトを作成
612
  const audioElements = {};
 
 
613
 
614
  // 音声ファイル名の配列
615
  const audioFiles = ['p', 'a', 't', 's', 'k'];
 
620
  let isVideoPlaying = false;
621
  let lastVolume = 1;
622
  let currentPlaybackRate = 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
623
 
624
  // 動画のメタデータが読み込まれたら
625
  video.addEventListener('loadedmetadata', function() {
626
+ try {
627
+ videoDuration = video.duration;
628
+ endTimeInput.value = videoDuration.toFixed(1);
629
+ endTimeInput.max = videoDuration;
630
+ startTimeInput.max = videoDuration - 0.1;
631
+ updateTimeDisplay();
632
+ checkLoadingComplete();
633
+ } catch (error) {
634
+ handleError(error, '動画メタデータ読み込み中にエラーが発生しました');
635
+ }
636
  });
637
 
638
+ // 動画エラー処理
639
  video.addEventListener('error', function() {
640
+ handleError(video.error, '動画読み込み中にエラーが発生しました');
 
 
641
  });
642
 
643
+ // 再生ボタンクリック
644
+ playPauseBtn.addEventListener('click', function() {
645
+ try {
646
+ // 現在の秒数が終了秒数を超えている場合は開始位置に戻す
647
+ const endTime = parseFloat(endTimeInput.value) || videoDuration;
648
+ if (video.currentTime >= endTime) {
649
+ const startTime = parseFloat(startTimeInput.value) || 0;
650
+ seekMedia(startTime);
651
+ }
652
+
653
+ togglePlayPause();
654
+
655
+ // 音声要素の再生を許可
656
+ audioFiles.forEach(file => {
657
+ if (audioElements[file]) {
658
+ audioElements[file].play().catch(e => {
659
+ if (e.name !== 'AbortError') {
660
+ handleError(e, `音声再生中にエラーが発生しました (${file}.mp3)`);
661
+ }
662
+ });
663
+ }
664
+ });
665
+ } catch (error) {
666
+ handleError(error, '再生/一時停止操作中にエラーが発生しました');
667
+ }
668
  });
669
 
670
+ // 時間表示を更新
671
  function updateTimeDisplay() {
672
+ try {
673
+ const currentTime = video.currentTime;
674
+ const duration = video.duration || videoDuration;
675
+
676
+ const currentMinutes = Math.floor(currentTime / 60);
677
+ const currentSeconds = Math.floor(currentTime % 60);
678
+ const currentMilliseconds = Math.floor((currentTime % 1) * 100);
679
+ const durationMinutes = Math.floor(duration / 60);
680
+ const durationSeconds = Math.floor(duration % 60);
681
+ const durationMilliseconds = Math.floor((duration % 1) * 100);
682
+
683
+ timeDisplay.textContent =
684
+ `${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')}.${String(currentMilliseconds).padStart(2, '0')} / ` +
685
+ `${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}.${String(durationMilliseconds).padStart(2, '0')}`;
686
+
687
+ // プログレスバーを更新
688
+ const progressPercent = (currentTime / duration) * 100;
689
+ progressBar.style.width = `${progressPercent}%`;
690
+
691
+ // 現在の秒数が終了秒数を超えている場合
692
+ const endTime = parseFloat(endTimeInput.value) || duration;
693
+ if (currentTime >= endTime && endTime > 0) {
694
+ if (loopCheckbox.checked) {
695
+ const startTime = parseFloat(startTimeInput.value) || 0;
696
+ seekMedia(startTime);
697
+ if (!isPlaying) {
698
+ playMedia();
699
+ }
700
+ } else {
701
+ pauseMedia();
702
  }
 
 
703
  }
704
+ } catch (error) {
705
+ handleError(error, '時間表示更新中にエラーが発生しました');
706
  }
707
  }
708
 
 
713
 
714
  // 動画終了時の処理
715
  video.addEventListener('ended', function() {
716
+ try {
717
+ if (!loopCheckbox.checked) {
718
+ const startTime = parseFloat(startTimeInput.value) || 0;
719
+ seekMedia(startTime);
720
+ pauseMedia();
721
+ }
722
+ } catch (error) {
723
+ handleError(error, '動画終了処理中にエラーが発生しました');
724
  }
725
  });
726
 
727
  // プログレスバークリックでシーク
728
  progressContainer.addEventListener('click', function(e) {
729
+ try {
730
+ if (!video.duration) return;
731
+
732
+ const rect = this.getBoundingClientRect();
733
+ const pos = (e.clientX - rect.left) / rect.width;
734
+ const seekTime = pos * video.duration;
735
+
736
+ seekMedia(seekTime);
737
+ } catch (error) {
738
+ handleError(error, 'シーク操作中にエラーが発生しました');
739
+ }
740
  });
741
 
742
  // 指定した時間にシーク
743
  function seekMedia(time) {
744
+ try {
745
+ const duration = video.duration || videoDuration;
746
+ const startTime = parseFloat(startTimeInput.value) || 0;
747
+ const endTime = parseFloat(endTimeInput.value) || duration;
748
+
749
+ // 範囲内に制限
750
+ const seekTime = Math.max(startTime, Math.min(time, endTime));
751
+
752
+ video.currentTime = seekTime;
753
+
754
+ // 音声も同期
755
+ audioFiles.forEach(file => {
756
+ if (audioElements[file]) {
757
+ audioElements[file].currentTime = seekTime;
758
+ }
759
+ });
760
+ } catch (error) {
761
+ handleError(error, 'メディアシーク中にエラーが発生しました');
762
+ }
763
  }
764
 
765
  // プログレスバー上でマウス移動時に時間を表示
766
  progressContainer.addEventListener('mousemove', function(e) {
767
+ try {
768
+ if (!video.duration) return;
769
+
770
+ const rect = this.getBoundingClientRect();
771
+ const pos = (e.clientX - rect.left) / rect.width;
772
+ const hoverTime = pos * video.duration;
773
+
774
+ const minutes = Math.floor(hoverTime / 60);
775
+ const seconds = Math.floor(hoverTime % 60);
776
+ const milliseconds = Math.floor((hoverTime % 1) * 100);
777
+
778
+ progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(milliseconds).padStart(2, '0')}`;
779
+ progressTime.style.display = 'block';
780
+ progressTime.style.left = `${pos * 100}%`;
781
+ } catch (error) {
782
+ handleError(error, 'プログレスバー操作中にエラーが発生しました');
783
+ }
784
  });
785
 
786
  progressContainer.addEventListener('mouseleave', function() {
 
803
 
804
  // 音量コントロール
805
  volumeSlider.addEventListener('input', function() {
806
+ try {
807
+ video.volume = this.value;
808
+ lastVolume = this.value;
809
+ updateVolumeIcon();
810
+ } catch (error) {
811
+ handleError(error, '音量設定中にエラーが発生しました');
812
+ }
813
  });
814
 
815
  // 音量ボタン
816
  volumeBtn.addEventListener('click', function() {
817
+ try {
818
+ if (video.volume > 0) {
819
+ lastVolume = video.volume;
820
+ video.volume = 0;
821
+ volumeSlider.value = 0;
822
+ } else {
823
+ video.volume = lastVolume;
824
+ volumeSlider.value = lastVolume;
825
+ }
826
+ updateVolumeIcon();
827
+ } catch (error) {
828
+ handleError(error, '音量切り替え中にエラーが発生しました');
829
  }
 
830
  });
831
 
832
  // 音量アイコンを更新
 
842
 
843
  // 再生速度スライダー (動画プレイヤー)
844
  speedSlider.addEventListener('input', function() {
845
+ try {
846
+ const speed = parseFloat(this.value);
847
+ speedValue.textContent = speed.toFixed(2) + 'x';
848
+ playbackSpeedSlider.value = speed;
849
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
850
+ updatePlaybackRate(speed);
851
+ } catch (error) {
852
+ handleError(error, '再生速度設定中にエラーが発生しました');
853
+ }
854
  });
855
 
856
  // 再生速度スライダー (設定メニュー)
857
  playbackSpeedSlider.addEventListener('input', function() {
858
+ try {
859
+ const speed = parseFloat(this.value);
860
+ playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
861
+ speedSlider.value = speed;
862
+ speedValue.textContent = speed.toFixed(2) + 'x';
863
+ updatePlaybackRate(speed);
864
+ } catch (error) {
865
+ handleError(error, '再生速度設定中にエラーが発生しました');
866
+ }
867
  });
868
 
869
  function updatePlaybackRate(speed) {
870
+ try {
871
+ currentPlaybackRate = speed;
872
+ video.playbackRate = speed;
873
+
874
+ // 音声の再生速度を変更
875
+ audioFiles.forEach(file => {
876
+ if (audioElements[file]) {
877
+ audioElements[file].playbackRate = speed;
878
+ setPreservesPitch(audioElements[file], true); // ピッチを保持
879
+ }
880
+ });
881
+ } catch (error) {
882
+ handleError(error, '再生速度更新中にエラーが発生しました');
883
+ }
884
  }
885
 
886
  // 全画面ボタン
887
  fullscreenBtn.addEventListener('click', function() {
888
+ try {
889
+ if (videoContainer.requestFullscreen) {
890
+ videoContainer.requestFullscreen();
891
+ } else if (videoContainer.webkitRequestFullscreen) {
892
+ videoContainer.webkitRequestFullscreen();
893
+ } else if (videoContainer.msRequestFullscreen) {
894
+ videoContainer.msRequestFullscreen();
895
+ }
896
+ } catch (error) {
897
+ handleError(error, '全画面表示中にエラーが発生しました');
898
  }
899
  });
900
 
 
916
 
917
  // 音声ファイルをロード
918
  function loadAudioFiles() {
 
 
919
  audioFiles.forEach(file => {
 
 
 
 
 
 
 
920
  try {
921
+ const audio = new Audio(`${file}.mp3`);
922
+ audio.preload = 'auto';
923
+ setPreservesPitch(audio, true); // ピッチを保持
924
+ audio.loop = false;
925
+ audioElements[file] = audio;
926
 
927
+ // 音声が読み込まれたら
928
+ audio.addEventListener('loadedmetadata', function() {
929
+ console.log(`${file}.mp3 loaded`);
930
+ checkLoadingComplete();
931
+ });
932
 
933
+ // エラー処理
934
+ audio.addEventListener('error', function() {
935
+ handleError(audio.error, `音声ファイル読み込み中にエラーが発生しました (${file}.mp3)`);
936
+ checkLoadingComplete(); // エラー時もカウント
937
+ });
938
+ } catch (error) {
939
+ handleError(error, `音声ファイル初期化中にエラーが発生しました (${file}.mp3)`);
940
+ checkLoadingComplete(); // エラー時もカウント
941
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
942
  });
943
  }
944
 
945
  function playAudio(file, startTime) {
946
  if (!audioElements[file]) return;
947
 
948
+ try {
949
+ const audio = audioElements[file];
950
+ const duration = video.duration || videoDuration;
951
+ const endTime = parseFloat(endTimeInput.value) || duration;
952
+
953
+ // ボリューム設定
954
+ const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
955
+ const volume = parseFloat(volumeSlider.value) * parseFloat(globalVolumeSlider.value);
 
 
956
  audio.volume = volume;
957
+
958
+ // 再生速度設定
959
+ audio.playbackRate = currentPlaybackRate;
960
+ setPreservesPitch(audio, true);
961
+
962
+ // 再生時間計算
963
+ const playStartTime = Math.max(startTime, parseFloat(startTimeInput.value) || 0);
964
+ const playEndTime = Math.min(endTime, duration);
965
+
966
+ audio.currentTime = playStartTime;
967
+
968
+ // 再生
969
+ audio.play().catch(e => {
970
+ if (e.name !== 'AbortError') {
971
+ handleError(e, `音声再生中にエラーが発生しました (${file}.mp3)`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
972
  }
973
+ });
974
+
975
+ // ループ設定
976
+ audio.loop = loopCheckbox.checked;
977
+
978
+ if (!loopCheckbox.checked) {
979
+ // ループ再生でない場合、終了時間に達したら停止
980
+ setTimeout(() => {
981
+ if (audio.currentTime >= playEndTime) {
982
+ audio.pause();
983
+ }
984
+ }, (playEndTime - playStartTime) * 1000);
985
+ }
986
+ } catch (error) {
987
+ handleError(error, `音声再生中にエラーが発生しました (${file}.mp3)`);
988
  }
989
  }
990
 
991
  // 再生関数
992
  function playMedia() {
993
+ try {
994
+ const startTime = video.currentTime;
995
+ const endTime = parseFloat(endTimeInput.value) || videoDuration;
996
+
997
+ // 終了時間が動画の長さを超えないように
998
+ const actualEndTime = Math.min(endTime, videoDuration);
999
+
1000
+ // 現在位置が終了時間を超えている場合、開始位置に戻る
1001
+ if (startTime >= actualEndTime) {
1002
+ video.currentTime = parseFloat(startTimeInput.value) || 0;
1003
+ }
1004
+
1005
+ // 動画を再生
1006
+ video.play();
1007
  isPlaying = true;
1008
  isVideoPlaying = true;
1009
  playPauseBtn.textContent = '⏸';
 
1012
  audioFiles.forEach(file => {
1013
  playAudio(file, video.currentTime);
1014
  });
1015
+ } catch (error) {
1016
+ handleError(error, 'メディア再生中にエラーが発生しました');
1017
+ }
1018
  }
1019
 
1020
  // 一時停止関数
1021
  function pauseMedia() {
1022
  if (!isPlaying) return;
1023
 
1024
+ try {
1025
+ video.pause();
1026
+ audioFiles.forEach(file => {
1027
+ if (audioElements[file]) {
1028
+ audioElements[file].pause();
1029
+ }
1030
+ });
1031
+
1032
+ isPlaying = false;
1033
+ isVideoPlaying = false;
1034
+ playPauseBtn.textContent = '▶';
1035
+ } catch (error) {
1036
+ handleError(error, 'メディア一時停止中にエラーが発生しました');
1037
+ }
1038
  }
1039
 
1040
  // ボリュームスライダーのイベント
1041
  audioSliders.forEach((slider, index) => {
1042
  slider.addEventListener('input', function() {
1043
+ try {
1044
+ const value = parseFloat(this.value);
1045
+ volumeValues[index].textContent = value.toFixed(2);
1046
+
1047
+ if (audioElements[this.dataset.audio]) {
1048
+ const globalVolume = parseFloat(globalVolumeSlider.value);
 
 
1049
  audioElements[this.dataset.audio].volume = value * globalVolume;
1050
  }
1051
+ } catch (error) {
1052
+ handleError(error, '音声ボリューム設定中にエラーが発生しました');
1053
  }
1054
  });
1055
  });
1056
 
1057
  // 全体音量スライダーのイベント
1058
  globalVolumeSlider.addEventListener('input', function() {
1059
+ try {
1060
+ const value = parseFloat(this.value);
1061
+ globalVolumeValue.textContent = value.toFixed(1);
1062
+
1063
+ audioFiles.forEach(file => {
1064
+ if (audioElements[file]) {
1065
+ const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
1066
+ const volume = parseFloat(volumeSlider.value) * value;
 
 
1067
  audioElements[file].volume = volume;
1068
  }
1069
+ });
1070
+ } catch (error) {
1071
+ handleError(error, '全体音量設定中にエラーが発生しました');
1072
+ }
1073
  });
1074
 
1075
  // ループ設定変更時
1076
  loopCheckbox.addEventListener('change', function() {
1077
+ try {
1078
+ audioFiles.forEach(file => {
1079
+ if (audioElements[file]) {
1080
+ audioElements[file].loop = this.checked;
1081
+ }
1082
+ });
1083
+ } catch (error) {
1084
+ handleError(error, 'ループ設定変更中にエラーが発生しました');
1085
+ }
1086
  });
1087
 
1088
+ // 現在の秒数を開始時間に設定
1089
+ setStartTimeBtn.addEventListener('click', function() {
1090
+ try {
1091
+ startTimeInput.value = video.currentTime.toFixed(1);
1092
+ } catch (error) {
1093
+ handleError(error, '開始時間設定中にエラーが発生しました');
1094
+ }
1095
  });
1096
 
1097
+ // 現在の秒数を終了時間に設定
1098
+ setEndTimeBtn.addEventListener('click', function() {
1099
+ try {
1100
+ endTimeInput.value = video.currentTime.toFixed(1);
1101
+ } catch (error) {
1102
+ handleError(error, '終了時間設定中にエラーが発生しました');
1103
+ }
1104
  });
1105
 
1106
  // 初期化
 
1108
  updateVolumeIcon();
1109
  volumeSlider.value = video.volume;
1110
  video.controls = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1111
  });
1112
  </script>
1113
  </body>