soiz1 commited on
Commit
cff74b6
·
verified ·
1 Parent(s): 1e0c7b5

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +432 -507
index.html CHANGED
@@ -619,540 +619,465 @@
619
  <video id="video-for-thumbnail" src="v.mp4" preload="auto" style="display:none;">
620
  </video>
621
  <script>
622
- const video = document.getElementById('videoPlayer');
623
- const videoSelect = document.getElementById('videoSelect');
624
- const speedRange = document.getElementById('speedRange');
625
- const speedInput = document.getElementById('speedInput');
626
- const volumeRange = document.getElementById('volumeRange');
627
- const volumeInput = document.getElementById('volumeInput');
628
- const loopCheckbox = document.getElementById('loopCheckbox');
629
- const playPauseBtn = document.getElementById('playPauseBtn');
630
- const progressBar = document.getElementById('progressBar');
631
- const progressContainer = document.getElementById('progressContainer');
632
- const timeDisplay = document.getElementById('timeDisplay');
633
- const volumeBtn = document.getElementById('volumeBtn');
634
- const volumeSlider = document.getElementById('volumeSlider');
635
- const fullscreenBtn = document.getElementById('fullscreenBtn');
636
- const subtitleBtn = document.getElementById('subtitleBtn');
637
- const subtitleToggle = document.getElementById('subtitleToggle');
638
- const subtitleSize = document.getElementById('subtitleSize');
639
- const subtitleSizeInput = document.getElementById('subtitleSizeInput');
640
- const subtitleTrack = document.getElementById('subtitleTrack');
641
- const subtitleTrackElement = document.getElementById('subtitleTrackElement');
642
- const videoContainer = document.querySelector('.video-container');
643
- const framePreview = document.getElementById('framePreview');
644
- const previewImage = document.getElementById('previewImage');
645
- const frameTime = document.getElementById('frameTime');
646
- const audioOnlyModeIndicator = document.getElementById('audioOnlyModeIndicator');
647
- const contextMenu = document.getElementById('contextMenu');
648
- const previewContainer = document.getElementById('previewContainer');
649
- const preview = document.getElementById('preview');
650
- const previewTime = document.getElementById('previewTime');
651
- const VideoForThumbnail = document.getElementById('video-for-thumbnail');
652
- const canvas = document.getElementById('canvas');
653
- const ctx = canvas.getContext('2d');
654
-
655
- // 初期設定
656
- video.controls = false;
657
- let isDragging = false;
658
- let subtitlesEnabled = true;
659
- let normalVideoWidth = videoContainer.clientWidth;
660
- let isAudioOnlyMode = false;
661
- let frameCache = {};
662
- let isHoveringProgress = false;
663
- let hoverTimeout;
664
- let videoBlob = null;
665
-
666
- // ローディングアニメーションをフェードアウト
667
- window.addEventListener('load', function() {
668
- setTimeout(function() {
669
- const loadingOverlay = document.getElementById('loadingOverlay');
670
- loadingOverlay.style.opacity = '0';
671
- setTimeout(function() {
672
- loadingOverlay.style.display = 'none';
673
- }, 1000);
674
- }, 1500);
675
-
676
- // 動画をBlobとしてキャッシュ
677
- fetch(video.src)
678
- .then(response => response.blob())
679
- .then(blob => {
680
- videoBlob = blob;
681
- });
682
- });
683
-
684
- // 波紋エフェクトのコードは元のままなので省略...
685
-
686
- function updatePlaybackRate(value) {
687
- const speed = parseFloat(value);
688
- speedInput.value = speed;
689
- speedRange.value = speed;
690
- video.playbackRate = speed;
691
- }
 
 
 
 
 
 
692
 
693
- function updateVolume(value) {
694
- const volume = parseFloat(value);
695
- volumeInput.value = volume;
696
- volumeRange.value = volume;
697
- volumeSlider.value = volume;
698
- video.volume = volume;
699
-
700
- if (volume === 0) {
701
- volumeBtn.textContent = '🔇';
702
- } else if (volume < 0.5) {
703
- volumeBtn.textContent = '🔈';
704
- } else {
705
- volumeBtn.textContent = '🔊';
706
- }
707
- }
708
 
709
- // 動画ソース変更時にサムネイル用動画も更新
710
- function handleVideoChange() {
711
- const selected = videoSelect.value;
712
 
713
- if (selected === 'v-2.mp4') {
714
- const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
715
- if (!confirmPlay) {
716
- videoSelect.value = video.src.split('/').pop();
717
- return;
718
- }
719
- }
 
 
 
 
720
 
721
- video.src = selected;
722
- VideoForThumbnail.src = selected;
723
- video.load();
724
- VideoForThumbnail.load();
725
- video.play().then(() => {
726
- playPauseBtn.textContent = '⏸';
727
- }).catch(e => console.log(e));
728
- }
729
-
730
- function togglePlayPause() {
731
- if (video.paused) {
732
- video.play();
733
- playPauseBtn.textContent = '⏸';
734
- } else {
735
- video.pause();
736
- playPauseBtn.textContent = '▶';
737
- }
738
- hideContextMenu();
739
- }
740
-
741
- function updateProgress() {
742
- const percent = (video.currentTime / video.duration) * 100;
743
- progressBar.style.width = `${percent}%`;
744
-
745
- const currentMinutes = Math.floor(video.currentTime / 60);
746
- const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
747
- const durationMinutes = Math.floor(video.duration / 60);
748
- const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
749
-
750
- timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
751
- }
752
-
753
- function setProgress(e) {
754
- const width = progressContainer.clientWidth;
755
- const clickX = e.offsetX;
756
- const duration = video.duration;
757
- video.currentTime = (clickX / width) * duration;
758
- }
759
-
760
- function toggleMute() {
761
- video.muted = !video.muted;
762
- if (video.muted) {
763
- volumeBtn.textContent = '🔇';
764
- volumeSlider.value = 0;
765
- } else {
766
- updateVolume(video.volume);
767
- }
768
- hideContextMenu();
769
- }
770
-
771
- function handleVolumeChange() {
772
- video.muted = false;
773
- updateVolume(volumeSlider.value);
774
- }
775
-
776
- function goFullscreen() {
777
- if (
778
- document.fullscreenElement ||
779
- document.webkitFullscreenElement ||
780
- document.msFullscreenElement
781
- ) {
782
- // フルスクリーンを解除
783
- if (document.exitFullscreen) {
784
- document.exitFullscreen();
785
- } else if (document.webkitExitFullscreen) {
786
- document.webkitExitFullscreen();
787
- } else if (document.msExitFullscreen) {
788
- document.msExitFullscreen();
789
- }
790
- } else {
791
- // フルスクリーンにする
792
- if (videoContainer.requestFullscreen) {
793
- videoContainer.requestFullscreen();
794
- } else if (videoContainer.webkitRequestFullscreen) {
795
- videoContainer.webkitRequestFullscreen();
796
- } else if (videoContainer.msRequestFullscreen) {
797
- videoContainer.msRequestFullscreen();
798
- }
799
- }
800
- hideContextMenu();
801
- }
802
- function setupFullscreenContextMenu() {
803
- const fullscreenElement = document.fullscreenElement ||
804
- document.webkitFullscreenElement ||
805
- document.msFullscreenElement;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
 
807
- if (fullscreenElement) {
808
- fullscreenElement.addEventListener('contextmenu', showContextMenu);
 
 
 
 
 
 
 
 
 
 
809
  }
 
 
810
  }
 
 
811
  function updateSubtitleScaleForFullscreen() {
812
  if (document.fullscreenElement || document.webkitFullscreenElement ||
813
  document.mozFullScreenElement || document.msFullscreenElement) {
814
- // 全画面モード
815
  const fullscreenWidth = window.innerWidth;
816
  const scaleFactor = fullscreenWidth / normalVideoWidth;
817
  document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
818
-
819
- // 全画面要素にイベントリスナーを追加
820
- const fsElement = document.fullscreenElement || document.webkitFullscreenElement ||
821
- document.mozFullScreenElement || document.msFullscreenElement;
822
- fsElement.addEventListener('contextmenu', showContextMenu);
823
  } else {
824
- // 通常モード
825
  document.documentElement.style.setProperty('--fullscreen-scale', 1);
826
  }
827
  }
828
- function setupFramePreview() {
829
- let previewTimeout;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
830
 
831
- progressContainer.addEventListener('mousemove', (e) => {
832
- if (!videoBlob || !video.duration) return;
833
-
834
- clearTimeout(previewTimeout);
835
-
836
- const progressRect = progressContainer.getBoundingClientRect();
837
- const clickX = Math.max(0, Math.min(e.clientX - progressRect.left, progressRect.width));
838
- const previewTime = (clickX / progressRect.width) * video.duration;
839
-
840
- // 時間表示を更新
841
- const previewMinutes = Math.floor(previewTime / 60);
842
- const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
843
- frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
844
-
845
- // プレビュー位置を更新
846
- framePreview.style.left = `${e.clientX - 80}px`; // 中央寄せ
847
- framePreview.style.display = 'block';
848
-
849
- // キャッシュがあればそれを使う
850
- const cacheKey = Math.floor(previewTime);
851
- if (frameCache[cacheKey]) {
852
- previewImage.src = frameCache[cacheKey];
853
- return;
854
  }
855
-
856
- // フレームを取得
857
- VideoForThumbnail.currentTime = previewTime;
858
-
859
- VideoForThumbnail.addEventListener('seeked', function() {
860
- canvas.width = VideoForThumbnail.videoWidth;
861
- canvas.height = VideoForThumbnail.videoHeight;
862
- ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
863
- const imageData = canvas.toDataURL('image/jpeg');
864
- previewImage.src = imageData;
865
- frameCache[cacheKey] = imageData; // キャッシュに保存
866
- }, { once: true });
867
- });
868
-
869
- progressContainer.addEventListener('mouseleave', () => {
870
- previewTimeout = setTimeout(() => {
871
- framePreview.style.display = 'none';
872
- }, 300);
873
- });
874
 
875
- framePreview.addEventListener('mouseenter', () => {
876
- clearTimeout(previewTimeout);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
877
  });
878
-
879
- framePreview.addEventListener('mouseleave', () => {
880
- framePreview.style.display = 'none';
 
 
 
 
881
  });
882
- }
883
- // 字幕関連の関数
884
- function toggleSubtitles() {
885
- subtitlesEnabled = !subtitlesEnabled;
886
- subtitleToggle.checked = subtitlesEnabled;
887
- subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
888
- subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
889
- hideContextMenu();
890
- }
891
-
892
- function updateSubtitleSize(value) {
893
- const size = parseFloat(value);
894
- subtitleSizeInput.value = size;
895
- subtitleSize.value = size;
896
-
897
- // 字幕サイズを制御
898
- document.documentElement.style.setProperty('--subtitle-scale', size);
899
-
900
- // VTTCueのlineプロパティには数値のみを設定
901
- const track = subtitleTrackElement.track;
902
- if (track && track.cues) {
903
- for (let i = 0; i < track.cues.length; i++) {
904
- track.cues[i].line = 90;
905
- track.cues[i].snapToLines = false;
906
- }
907
- }
908
- }
909
-
910
- function changeSubtitleTrack() {
911
- const selectedTrack = subtitleTrack.value;
912
- subtitleTrackElement.src = selectedTrack;
913
- subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
914
-
915
- // トラック変更後に再度読み込み
916
- video.textTracks[0].mode = 'hidden';
917
- if (selectedTrack) {
918
- video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
919
- }
920
- }
921
-
922
- function toggleSubtitleMenu() {
923
- document.getElementById('subtitleToggle').checked ^= true;
924
- toggleSubtitles();
925
- }
926
-
927
- // フレームプレビュー関連
928
- function showFramePreview(e) {
929
- if (!videoBlob) return;
930
-
931
- const progressRect = progressContainer.getBoundingClientRect();
932
- const clickX = e.clientX - progressRect.left;
933
- const duration = video.duration;
934
- const previewTime = (clickX / progressRect.width) * duration;
935
-
936
- // 時間表示を更新
937
- const previewMinutes = Math.floor(previewTime / 60);
938
- const previewSeconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
939
- frameTime.textContent = `${previewMinutes}:${previewSeconds}`;
940
-
941
- // プレビュー位置を更新
942
- framePreview.style.left = `${e.clientX}px`;
943
- framePreview.style.display = 'block';
944
-
945
- // キャッシュがあればそれを使う
946
- const cacheKey = Math.floor(previewTime);
947
- if (frameCache[cacheKey]) {
948
- previewImage.src = frameCache[cacheKey];
949
- return;
950
- }
951
-
952
- // フレームを取得
953
- videoFrames({
954
- url: URL.createObjectURL(videoBlob),
955
- count: 1,
956
- startTime: previewTime,
957
- endTime: previewTime + 0.1
958
- }).then((frames) => {
959
- if (frames.length > 0) {
960
- previewImage.src = frames[0].image;
961
- frameCache[cacheKey] = frames[0].image; // キャッシュに保存
962
- }
963
- }).catch(err => {
964
- console.error('Error getting video frame:', err);
965
- });
966
- }
967
-
968
- function hideFramePreview() {
969
- framePreview.style.display = 'none';
970
- }
971
-
972
- // 右クリックメニュー関連
973
- function showContextMenu(e) {
974
- e.preventDefault();
975
- contextMenu.style.display = 'block';
976
- contextMenu.style.left = `${e.clientX}px`;
977
- contextMenu.style.top = `${e.clientY}px`;
978
- }
979
-
980
- function hideContextMenu() {
981
- contextMenu.style.display = 'none';
982
- }
983
-
984
- // 音声/字幕のみモード
985
- function toggleAudioOnlyMode() {
986
- isAudioOnlyMode = !isAudioOnlyMode;
987
-
988
- if (isAudioOnlyMode) {
989
- video.style.opacity = '0';
990
- audioOnlyModeIndicator.classList.add('active');
991
- } else {
992
- video.style.opacity = '1';
993
- audioOnlyModeIndicator.classList.remove('active');
994
- }
995
-
996
- hideContextMenu();
997
- }
998
-
999
- // イベントリスナー
1000
- videoSelect.addEventListener('change', handleVideoChange);
1001
-
1002
- ['input', 'change', 'mouseup'].forEach(eventName => {
1003
- speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1004
- volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1005
- subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1006
- });
1007
-
1008
- speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1009
- volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1010
- subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1011
-
1012
- loopCheckbox.addEventListener('change', () => {
1013
- video.loop = loopCheckbox.checked;
1014
- });
1015
-
1016
- subtitleToggle.addEventListener('change', toggleSubtitles);
1017
- subtitleTrack.addEventListener('change', changeSubtitleTrack);
1018
- subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1019
-
1020
- playPauseBtn.addEventListener('click', togglePlayPause);
1021
- video.addEventListener('click', togglePlayPause);
1022
- video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1023
- video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1024
- video.addEventListener('timeupdate', updateProgress);
1025
- progressContainer.addEventListener('click', setProgress);
1026
- progressContainer.addEventListener('mousedown', () => isDragging = true);
1027
- document.addEventListener('mouseup', () => isDragging = false);
1028
- // マウスホバー時のプレビュー表示
1029
- progressContainer.addEventListener('mousemove', function(e) {
1030
- if (isDragging) {
1031
- const width = progressContainer.clientWidth;
1032
- const clickX = e.offsetX;
1033
- const duration = video.duration;
1034
- const previewTime = (clickX / width) * duration;
1035
-
1036
- // プレビュー位置を更新
1037
- previewContainer.style.left = `${e.clientX - 100}px`;
1038
- previewContainer.style.bottom = '60px';
1039
- previewContainer.style.display = 'block';
1040
-
1041
- // 時間表示を更新
1042
- const minutes = Math.floor(previewTime / 60);
1043
- const seconds = Math.floor(previewTime % 60).toString().padStart(2, '0');
1044
- document.getElementById('previewTime').textContent = `${minutes}:${seconds}`;
1045
-
1046
- // サムネイル画像を更新
1047
- updateThumbnail(previewTime);
1048
- } else {
1049
- previewContainer.style.display = 'none';
1050
- }
1051
- });
1052
-
1053
- // サムネイル画像更新関数
1054
- function updateThumbnail(time) {
1055
- VideoForThumbnail.currentTime = time;
1056
-
1057
- VideoForThumbnail.addEventListener('seeked', function() {
1058
- canvas.width = VideoForThumbnail.videoWidth;
1059
- canvas.height = VideoForThumbnail.videoHeight;
1060
- ctx.drawImage(VideoForThumbnail, 0, 0, canvas.width, canvas.height);
1061
- preview.src = canvas.toDataURL('image/jpeg');
1062
- }, { once: true });
1063
- }
1064
-
1065
-
1066
- // プログレスバーのホバーイベント
1067
- progressContainer.addEventListener('mouseenter', () => {
1068
- isHoveringProgress = true;
1069
- clearTimeout(hoverTimeout);
1070
- });
1071
-
1072
- progressContainer.addEventListener('mouseleave', () => {
1073
- isHoveringProgress = false;
1074
- hoverTimeout = setTimeout(() => {
1075
- if (!isDragging) hideFramePreview();
1076
- }, 300);
1077
- });
1078
-
1079
- volumeBtn.addEventListener('click', toggleMute);
1080
- volumeSlider.addEventListener('input', handleVolumeChange);
1081
- fullscreenBtn.addEventListener('click', goFullscreen);
1082
-
1083
- // 全画面変更イベントを監視
1084
- document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1085
- document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1086
- document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1087
- document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1088
-
1089
- // 右クリックメニューイベント
1090
- videoContainer.addEventListener('contextmenu', showContextMenu);
1091
- document.addEventListener('click', hideContextMenu);
1092
- document.addEventListener('keydown', (e) => {
1093
- if (e.key === 'Escape') hideContextMenu();
1094
- });
1095
-
1096
- video.addEventListener('loadedmetadata', () => {
1097
- updatePlaybackRate(speedRange.value);
1098
- updateVolume(volumeRange.value);
1099
- updateSubtitleSize(subtitleSize.value);
1100
- video.loop = loopCheckbox.checked;
1101
- toggleSubtitles();
1102
- updateProgress();
1103
- normalVideoWidth = videoContainer.clientWidth;
1104
- });
1105
- video.addEventListener("loadeddata", async () => {
1106
- const response = await fetch(video.src);
1107
- videoBlob = await response.blob();
1108
- });
1109
-
1110
- // 保存
1111
- video.addEventListener('timeupdate', () => {
1112
- localStorage.setItem('radioTaisoTime', video.currentTime);
1113
- });
1114
-
1115
- // 復元
1116
- window.addEventListener('load', () => {
1117
- setupFramePreview();
1118
- setupFullscreenContextMenu();
1119
- const savedTime = parseFloat(localStorage.getItem('radioTaisoTime'));
1120
- if (!isNaN(savedTime)) {
1121
- video.currentTime = savedTime;
1122
- }
1123
 
1124
- document.addEventListener('fullscreenchange', () => {
1125
- updateSubtitleScaleForFullscreen();
1126
- setupFullscreenContextMenu();
 
 
 
 
 
 
 
 
 
 
 
 
 
1127
  });
1128
- document.addEventListener('webkitfullscreenchange', () => {
1129
- updateSubtitleScaleForFullscreen();
1130
- setupFullscreenContextMenu();
 
 
 
 
 
 
 
 
 
1131
  });
1132
- document.addEventListener('mozfullscreenchange', () => {
1133
- updateSubtitleScaleForFullscreen();
1134
- setupFullscreenContextMenu();
1135
  });
1136
- document.addEventListener('MSFullscreenChange', () => {
1137
- updateSubtitleScaleForFullscreen();
1138
- setupFullscreenContextMenu();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1139
  });
1140
- });
1141
- document.addEventListener('keydown', (e) => {
1142
- if (e.target.tagName === 'INPUT') return; // 入力中は無視
1143
- switch (e.key.toLowerCase()) {
1144
- case ' ': e.preventDefault(); togglePlayPause(); break;
1145
- case 'f': goFullscreen(); break;
1146
- case 'm': toggleMute(); break;
1147
- case 'arrowright': video.currentTime += 5; break;
1148
- case 'arrowleft': video.currentTime -= 5; break;
1149
- }
1150
- });
1151
-
1152
- // CSS変数を設定
1153
- document.documentElement.style.setProperty('--subtitle-scale', '1');
1154
- document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1155
- document.documentElement.style.setProperty('--fullscreen-scale', '1');
1156
  </script>
1157
  </body>
1158
 
 
619
  <video id="video-for-thumbnail" src="v.mp4" preload="auto" style="display:none;">
620
  </video>
621
  <script>
622
+ // 要素取得
623
+ const video = document.getElementById('videoPlayer');
624
+ const videoSelect = document.getElementById('videoSelect');
625
+ const speedRange = document.getElementById('speedRange');
626
+ const speedInput = document.getElementById('speedInput');
627
+ const volumeRange = document.getElementById('volumeRange');
628
+ const volumeInput = document.getElementById('volumeInput');
629
+ const loopCheckbox = document.getElementById('loopCheckbox');
630
+ const playPauseBtn = document.getElementById('playPauseBtn');
631
+ const progressBar = document.getElementById('progressBar');
632
+ const progressContainer = document.getElementById('progressContainer');
633
+ const timeDisplay = document.getElementById('timeDisplay');
634
+ const volumeBtn = document.getElementById('volumeBtn');
635
+ const volumeSlider = document.getElementById('volumeSlider');
636
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
637
+ const subtitleBtn = document.getElementById('subtitleBtn');
638
+ const subtitleToggle = document.getElementById('subtitleToggle');
639
+ const subtitleSize = document.getElementById('subtitleSize');
640
+ const subtitleSizeInput = document.getElementById('subtitleSizeInput');
641
+ const subtitleTrack = document.getElementById('subtitleTrack');
642
+ const subtitleTrackElement = document.getElementById('subtitleTrackElement');
643
+ const videoContainer = document.getElementById('videoContainer');
644
+ const videoPlaceholder = document.getElementById('videoPlaceholder');
645
+ const previewTooltip = document.getElementById('previewTooltip');
646
+ const previewTime = document.getElementById('previewTime');
647
+ const previewFrame = document.getElementById('previewFrame');
648
+ const audioOnlyBtn = document.getElementById('audioOnlyBtn');
649
+ const fullscreenContextMenu = document.getElementById('fullscreenContextMenu');
650
+ const exitFullscreenBtn = document.getElementById('exitFullscreenBtn');
651
+ const showVideoBtn = document.getElementById('showVideoBtn');
652
+ const closeContextMenuBtn = document.getElementById('closeContextMenuBtn');
653
+
654
+ // プレビュー用動画を動的に作成
655
+ const previewVideo = document.createElement('video');
656
+ previewVideo.id = 'previewVideo';
657
+ previewVideo.className = 'thumbnail-preview';
658
+ previewVideo.muted = true;
659
+ previewVideo.preload = 'auto';
660
+ videoContainer.appendChild(previewVideo);
661
+
662
+ // 初期設定
663
+ video.controls = false;
664
+ previewVideo.src = video.src;
665
+ let isDragging = false;
666
+ let subtitlesEnabled = true;
667
+ let normalVideoWidth = videoContainer.clientWidth;
668
+ let isAudioOnlyMode = false;
669
+ let hoverTimeout;
670
+ let canvas = null;
671
+ let previewCanvas = null;
672
+ let previewContext = null;
673
+ let isGeneratingPreview = false;
674
+
675
+ // プレビュー用キャンバスを作成
676
+ function createPreviewCanvas() {
677
+ if (!canvas) {
678
+ canvas = document.createElement('canvas');
679
+ canvas.width = video.videoWidth || 640;
680
+ canvas.height = video.videoHeight || 360;
681
+ }
682
+
683
+ if (!previewCanvas) {
684
+ previewCanvas = document.createElement('canvas');
685
+ previewCanvas.width = 160;
686
+ previewCanvas.height = 90;
687
+ previewContext = previewCanvas.getContext('2d');
688
+ }
689
+ }
690
+
691
+ // プレビュー画像を生成
692
+ function generatePreview(time) {
693
+ if (isGeneratingPreview || !video.readyState) return;
694
+
695
+ try {
696
+ isGeneratingPreview = true;
697
+ createPreviewCanvas();
698
 
699
+ const currentTime = video.currentTime;
700
+ video.currentTime = time;
 
 
 
 
 
 
 
 
 
 
 
 
 
701
 
702
+ const onSeeked = () => {
703
+ video.removeEventListener('seeked', onSeeked);
 
704
 
705
+ try {
706
+ const ctx = canvas.getContext('2d');
707
+ canvas.width = video.videoWidth;
708
+ canvas.height = video.videoHeight;
709
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
710
+ previewContext.drawImage(canvas, 0, 0, previewCanvas.width, previewCanvas.height);
711
+ previewFrame.style.backgroundImage = `url(${previewCanvas.toDataURL()})`;
712
+ } catch (e) {
713
+ console.error('Preview generation error:', e);
714
+ previewFrame.style.backgroundImage = 'linear-gradient(to bottom, #0066ff, #00aaff)';
715
+ }
716
 
717
+ video.currentTime = currentTime;
718
+ isGeneratingPreview = false;
719
+ };
720
+
721
+ video.addEventListener('seeked', onSeeked);
722
+ } catch (e) {
723
+ console.error('Preview error:', e);
724
+ isGeneratingPreview = false;
725
+ }
726
+ }
727
+
728
+ // プレビューツールチップを表示
729
+ function showPreviewTooltip(e) {
730
+ if (!video.duration) return;
731
+
732
+ const rect = progressContainer.getBoundingClientRect();
733
+ const percent = (e.clientX - rect.left) / rect.width;
734
+ const time = Math.max(0, Math.min(percent, 1)) * video.duration;
735
+
736
+ const tooltipWidth = previewTooltip.offsetWidth;
737
+ let left = e.clientX - rect.left;
738
+ left = Math.max(tooltipWidth / 2, Math.min(left, rect.width - tooltipWidth / 2));
739
+
740
+ previewTooltip.style.left = `${left}px`;
741
+
742
+ const minutes = Math.floor(time / 60);
743
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
744
+ previewTime.textContent = `${minutes}:${seconds}`;
745
+
746
+ generatePreview(time);
747
+ previewTooltip.style.display = 'block';
748
+ }
749
+
750
+ // プレビューツールチップを非表示
751
+ function hidePreviewTooltip() {
752
+ previewTooltip.style.display = 'none';
753
+ }
754
+
755
+ // ホバー時の時間表示設定
756
+ function setupHoverTime() {
757
+ const hoverTime = document.createElement('div');
758
+ hoverTime.className = 'hover-time';
759
+ progressContainer.appendChild(hoverTime);
760
+
761
+ progressContainer.addEventListener("mousemove", (e) => {
762
+ if (!video.duration) return;
763
+
764
+ const rect = progressContainer.getBoundingClientRect();
765
+ const pos = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
766
+ const time = pos * video.duration;
767
+
768
+ const minutes = Math.floor(time / 60);
769
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
770
+ hoverTime.textContent = `${minutes}:${seconds}`;
771
+ hoverTime.style.display = 'block';
772
+ hoverTime.style.left = `${e.clientX - rect.left}px`;
773
+
774
+ previewVideo.style.display = "block";
775
+ previewVideo.style.left = `${e.clientX}px`;
776
+
777
+ clearTimeout(hoverTimeout);
778
+ hoverTimeout = setTimeout(() => {
779
+ previewVideo.currentTime = time;
780
+ }, 50);
781
+ });
782
+
783
+ progressContainer.addEventListener("mouseleave", () => {
784
+ const hoverTime = document.querySelector('.hover-time');
785
+ if (hoverTime) hoverTime.style.display = "none";
786
+ previewVideo.style.display = "none";
787
+ clearTimeout(hoverTimeout);
788
+ });
789
+ }
790
+
791
+ // 再生速度を更新
792
+ function updatePlaybackRate(value) {
793
+ const speed = parseFloat(value);
794
+ speedInput.value = speed;
795
+ speedRange.value = speed;
796
+ video.playbackRate = speed;
797
+ }
798
+
799
+ // 音量を更新
800
+ function updateVolume(value) {
801
+ const volume = parseFloat(value);
802
+ volumeInput.value = volume;
803
+ volumeRange.value = volume;
804
+ volumeSlider.value = volume;
805
+ video.volume = volume;
806
+
807
+ if (volume === 0) {
808
+ volumeBtn.textContent = '🔇';
809
+ } else if (volume < 0.5) {
810
+ volumeBtn.textContent = '🔈';
811
+ } else {
812
+ volumeBtn.textContent = '🔊';
813
+ }
814
+ }
815
+
816
+ // 動画変更処理
817
+ function handleVideoChange() {
818
+ const selected = videoSelect.value;
819
+
820
+ if (selected === 'v-2.mp4') {
821
+ const confirmPlay = confirm("この動画は音量が大きいです。あらかじめ、デバイスの音量をある程度下げてください。また、音割れが起きます。再生してもよろしいですか?");
822
+ if (!confirmPlay) {
823
+ videoSelect.value = video.src.split('/').pop();
824
+ return;
825
+ }
826
+ }
827
+
828
+ video.src = selected;
829
+ previewVideo.src = selected;
830
+ video.load();
831
+ previewVideo.load();
832
+ video.play().catch(e => console.log(e));
833
+ }
834
+
835
+ // 再生/一時停止を切り替え
836
+ function togglePlayPause() {
837
+ if (video.paused) {
838
+ video.play().then(() => {
839
+ playPauseBtn.textContent = '⏸';
840
+ }).catch(e => console.log(e));
841
+ } else {
842
+ video.pause();
843
+ playPauseBtn.textContent = '▶';
844
+ }
845
+ }
846
+
847
+ // 進捗バーを更新
848
+ function updateProgress() {
849
+ if (!video.duration) return;
850
+
851
+ const percent = (video.currentTime / video.duration) * 100;
852
+ progressBar.style.width = `${percent}%`;
853
+
854
+ const currentMinutes = Math.floor(video.currentTime / 60);
855
+ const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
856
+ const durationMinutes = Math.floor(video.duration / 60);
857
+ const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
858
+
859
+ timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
860
+ }
861
+
862
+ // 進捗バーを設定
863
+ function setProgress(e) {
864
+ const width = progressContainer.clientWidth;
865
+ const clickX = e.offsetX;
866
+ const duration = video.duration;
867
+ video.currentTime = (clickX / width) * duration;
868
+ }
869
+
870
+ // ミュートを切り替え
871
+ function toggleMute() {
872
+ video.muted = !video.muted;
873
+ if (video.muted) {
874
+ volumeBtn.textContent = '🔇';
875
+ volumeSlider.value = 0;
876
+ } else {
877
+ updateVolume(video.volume);
878
+ }
879
+ }
880
+
881
+ // 音量変更を処理
882
+ function handleVolumeChange() {
883
+ video.muted = false;
884
+ updateVolume(volumeSlider.value);
885
+ }
886
+
887
+ // 全画面表示を切り替え
888
+ function goFullscreen() {
889
+ if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
890
+ if (document.exitFullscreen) {
891
+ document.exitFullscreen();
892
+ } else if (document.webkitExitFullscreen) {
893
+ document.webkitExitFullscreen();
894
+ } else if (document.msExitFullscreen) {
895
+ document.msExitFullscreen();
896
+ }
897
+ } else {
898
+ if (videoContainer.requestFullscreen) {
899
+ videoContainer.requestFullscreen();
900
+ } else if (videoContainer.webkitRequestFullscreen) {
901
+ videoContainer.webkitRequestFullscreen();
902
+ } else if (videoContainer.msRequestFullscreen) {
903
+ videoContainer.msRequestFullscreen();
904
+ }
905
+ }
906
+ }
907
+
908
+ // 全画面コンテキストメニューを表示
909
+ function showContextMenu(e) {
910
+ if (!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement)) {
911
+ return;
912
+ }
913
+
914
+ e.preventDefault();
915
+
916
+ fullscreenContextMenu.style.display = 'block';
917
+ fullscreenContextMenu.style.left = `${e.clientX}px`;
918
+ fullscreenContextMenu.style.top = `${e.clientY}px`;
919
+ }
920
+
921
+ // 全画面コンテキストメニューを非表示
922
+ function hideContextMenu() {
923
+ fullscreenContextMenu.style.display = 'none';
924
+ }
925
+
926
+ // 音声/字幕のみモードを切り替え
927
+ function toggleAudioOnlyMode() {
928
+ isAudioOnlyMode = !isAudioOnlyMode;
929
 
930
+ if (isAudioOnlyMode) {
931
+ videoContainer.classList.add('audio-only-mode');
932
+ videoPlaceholder.style.display = 'flex';
933
+ video.style.display = 'none';
934
+ audioOnlyBtn.textContent = '🎥';
935
+ audioOnlyBtn.title = '動画表示モード';
936
+ } else {
937
+ videoContainer.classList.remove('audio-only-mode');
938
+ videoPlaceholder.style.display = 'none';
939
+ video.style.display = 'block';
940
+ audioOnlyBtn.textContent = '🔈';
941
+ audioOnlyBtn.title = '音声/字幕のみ';
942
  }
943
+
944
+ hideContextMenu();
945
  }
946
+
947
+ // 全画面時の字幕サイズ調整
948
  function updateSubtitleScaleForFullscreen() {
949
  if (document.fullscreenElement || document.webkitFullscreenElement ||
950
  document.mozFullScreenElement || document.msFullscreenElement) {
 
951
  const fullscreenWidth = window.innerWidth;
952
  const scaleFactor = fullscreenWidth / normalVideoWidth;
953
  document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
 
 
 
 
 
954
  } else {
 
955
  document.documentElement.style.setProperty('--fullscreen-scale', 1);
956
  }
957
  }
958
+
959
+ // 字幕表示を切り替え
960
+ function toggleSubtitles() {
961
+ subtitlesEnabled = subtitleToggle.checked;
962
+ if (subtitleTrackElement.track) {
963
+ subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
964
+ }
965
+ subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
966
+ }
967
+
968
+ // 字幕サイズを更新
969
+ function updateSubtitleSize(value) {
970
+ const size = parseFloat(value);
971
+ subtitleSizeInput.value = size;
972
+ subtitleSize.value = size;
973
+ document.documentElement.style.setProperty('--subtitle-scale', size);
974
 
975
+ const track = subtitleTrackElement.track;
976
+ if (track && track.cues) {
977
+ for (let i = 0; i < track.cues.length; i++) {
978
+ track.cues[i].line = 90;
979
+ track.cues[i].snapToLines = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  }
981
+ }
982
+ }
983
+
984
+ // 字幕トラックを変更
985
+ function changeSubtitleTrack() {
986
+ const selectedTrack = subtitleTrack.value;
987
+ subtitleTrackElement.src = selectedTrack;
 
 
 
 
 
 
 
 
 
 
 
 
988
 
989
+ if (video.textTracks.length > 0) {
990
+ video.textTracks[0].mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
991
+ }
992
+ }
993
+
994
+ // 字幕メニューを切り替え
995
+ function toggleSubtitleMenu() {
996
+ subtitleToggle.checked = !subtitleToggle.checked;
997
+ toggleSubtitles();
998
+ }
999
+
1000
+ // イベントリスナーを設定
1001
+ function setupEventListeners() {
1002
+ videoSelect.addEventListener('change', handleVideoChange);
1003
+
1004
+ ['input', 'change'].forEach(eventName => {
1005
+ speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1006
+ volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1007
+ subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1008
  });
1009
+
1010
+ speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1011
+ volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1012
+ subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1013
+
1014
+ loopCheckbox.addEventListener('change', () => {
1015
+ video.loop = loopCheckbox.checked;
1016
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1017
 
1018
+ subtitleToggle.addEventListener('change', toggleSubtitles);
1019
+ subtitleTrack.addEventListener('change', changeSubtitleTrack);
1020
+ subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1021
+
1022
+ playPauseBtn.addEventListener('click', togglePlayPause);
1023
+ video.addEventListener('click', togglePlayPause);
1024
+ video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1025
+ video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1026
+ video.addEventListener('timeupdate', updateProgress);
1027
+
1028
+ progressContainer.addEventListener('click', setProgress);
1029
+ progressContainer.addEventListener('mousedown', () => isDragging = true);
1030
+ document.addEventListener('mouseup', () => isDragging = false);
1031
+ progressContainer.addEventListener('mousemove', (e) => {
1032
+ if (isDragging) setProgress(e);
1033
+ showPreviewTooltip(e);
1034
  });
1035
+ progressContainer.addEventListener('mouseenter', showPreviewTooltip);
1036
+ progressContainer.addEventListener('mouseleave', hidePreviewTooltip);
1037
+
1038
+ volumeBtn.addEventListener('click', toggleMute);
1039
+ volumeSlider.addEventListener('input', handleVolumeChange);
1040
+ fullscreenBtn.addEventListener('click', goFullscreen);
1041
+ audioOnlyBtn.addEventListener('click', toggleAudioOnlyMode);
1042
+
1043
+ videoContainer.addEventListener('contextmenu', showContextMenu);
1044
+ exitFullscreenBtn.addEventListener('click', () => {
1045
+ if (document.exitFullscreen) document.exitFullscreen();
1046
+ hideContextMenu();
1047
  });
1048
+ showVideoBtn.addEventListener('click', () => {
1049
+ if (isAudioOnlyMode) toggleAudioOnlyMode();
1050
+ hideContextMenu();
1051
  });
1052
+ closeContextMenuBtn.addEventListener('click', hideContextMenu);
1053
+ document.addEventListener('click', hideContextMenu);
1054
+
1055
+ document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1056
+ document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1057
+ document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1058
+ document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1059
+
1060
+ video.addEventListener('loadedmetadata', () => {
1061
+ updatePlaybackRate(speedRange.value);
1062
+ updateVolume(volumeRange.value);
1063
+ updateSubtitleSize(subtitleSize.value);
1064
+ video.loop = loopCheckbox.checked;
1065
+ toggleSubtitles();
1066
+ updateProgress();
1067
+ normalVideoWidth = videoContainer.clientWidth;
1068
+ createPreviewCanvas();
1069
  });
1070
+ }
1071
+
1072
+ // CSS変数を初期設定
1073
+ document.documentElement.style.setProperty('--subtitle-scale', '1');
1074
+ document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1075
+ document.documentElement.style.setProperty('--fullscreen-scale', '1');
1076
+
1077
+ // 初期化
1078
+ setupHoverTime();
1079
+ setupEventListeners();
1080
+ createPreviewCanvas();
 
 
 
 
 
1081
  </script>
1082
  </body>
1083