soiz1 commited on
Commit
aaffddf
·
verified ·
1 Parent(s): 9f19fb0

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +507 -432
index.html CHANGED
@@ -619,465 +619,540 @@
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
 
 
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