soiz1 commited on
Commit
6f9b6b5
·
verified ·
1 Parent(s): 6f71c05

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +388 -419
index.html CHANGED
@@ -783,215 +783,201 @@
783
  </div>
784
  </div>
785
  <script>
786
- const video = document.getElementById('videoPlayer');
787
- const previewVideo = document.getElementById('previewVideo');
788
- const videoSelect = document.getElementById('videoSelect');
789
- const speedRange = document.getElementById('speedRange');
790
- const speedInput = document.getElementById('speedInput');
791
- const volumeRange = document.getElementById('volumeRange');
792
- const volumeInput = document.getElementById('volumeInput');
793
- const loopCheckbox = document.getElementById('loopCheckbox');
794
- const playPauseBtn = document.getElementById('playPauseBtn');
795
- const progressBar = document.getElementById('progressBar');
796
- const progressContainer = document.getElementById('progressContainer');
797
- const timeDisplay = document.getElementById('timeDisplay');
798
- const volumeBtn = document.getElementById('volumeBtn');
799
- const volumeSlider = document.getElementById('volumeSlider');
800
- const fullscreenBtn = document.getElementById('fullscreenBtn');
801
- const subtitleBtn = document.getElementById('subtitleBtn');
802
- const subtitleToggle = document.getElementById('subtitleToggle');
803
- const subtitleSize = document.getElementById('subtitleSize');
804
- const subtitleSizeInput = document.getElementById('subtitleSizeInput');
805
- const subtitleTrack = document.getElementById('subtitleTrack');
806
- const subtitleTrackElement = document.getElementById('subtitleTrackElement');
807
- const videoContainer = document.getElementById('videoContainer');
808
- const videoPlaceholder = document.getElementById('videoPlaceholder');
809
- const previewTooltip = document.getElementById('previewTooltip');
810
- const previewTime = document.getElementById('previewTime');
811
- const previewFrame = document.getElementById('previewFrame');
812
- const audioOnlyBtn = document.getElementById('audioOnlyBtn');
813
- const fullscreenContextMenu = document.getElementById('fullscreenContextMenu');
814
- const exitFullscreenBtn = document.getElementById('exitFullscreenBtn');
815
- const showVideoBtn = document.getElementById('showVideoBtn');
816
- const closeContextMenuBtn = document.getElementById('closeContextMenuBtn');
817
- const contextAudioOnlyBtn = document.getElementById('audioOnlyBtn');
818
-
819
- // 初期設定
820
- video.controls = false;
821
- let isDragging = false;
822
- let subtitlesEnabled = true;
823
- let normalVideoWidth = videoContainer.clientWidth;
824
- let isAudioOnlyMode = false;
825
- let canvas = null;
826
- let previewCanvas = null;
827
- let previewContext = null;
828
- let isGeneratingPreview = false;
829
-
830
- // プレビュー用キャンバスを作成
831
- function createPreviewCanvas() {
832
- if (!canvas) {
833
- canvas = document.createElement('canvas');
834
- canvas.width = video.videoWidth || 640;
835
- canvas.height = video.videoHeight || 360;
836
- }
837
-
838
- if (!previewCanvas) {
839
- previewCanvas = document.createElement('canvas');
840
- previewCanvas.width = 160;
841
- previewCanvas.height = 90;
842
- previewContext = previewCanvas.getContext('2d');
843
- }
844
- }
845
- function setupHoverTime() {
846
- progressContainer.addEventListener("mousemove", (e) => {
847
- if (!video.duration) return;
848
-
849
- const rect = progressContainer.getBoundingClientRect();
850
- const pos = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
851
- const time = pos * video.duration;
852
-
853
- // 時間表示
854
- const minutes = Math.floor(time / 60);
855
- const seconds = Math.floor(time % 60).toString().padStart(2, '0');
856
- hoverTime.textContent = `${minutes}:${seconds}`;
857
- hoverTime.style.display = 'block';
858
- hoverTime.style.left = `${e.clientX - rect.left}px`;
859
-
860
- // プレビュー動画の位置設定
861
- previewVideo.style.display = "block";
862
- previewVideo.style.left = `${e.clientX}px`;
863
-
864
- // スムーズなプレビュー更新のため、デバウンス処理
865
- clearTimeout(hoverTimeout);
866
- hoverTimeout = setTimeout(() => {
867
- previewVideo.currentTime = time;
868
- }, 50);
869
- });
870
-
871
- progressContainer.addEventListener("mouseleave", () => {
872
- hoverTime.style.display = "none";
873
- previewVideo.style.display = "none";
874
- clearTimeout(hoverTimeout);
875
- });
876
- }
877
- video.controls = false;
878
- previewVideo.src = video.src; // プレビュー用動画に同じソースを設定
879
  let isDragging = false;
880
  let subtitlesEnabled = true;
881
  let normalVideoWidth = videoContainer.clientWidth;
882
- let audioOnlyMode = false;
883
  let hoverTimeout;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
  // プレビュー画像を生成
885
  function generatePreview(time) {
886
- if (isGeneratingPreview) return;
887
 
888
  try {
889
  isGeneratingPreview = true;
890
  createPreviewCanvas();
891
 
892
- // 動画の現在の時間を保存
893
  const currentTime = video.currentTime;
894
-
895
- // 指定された時間にシーク
896
  video.currentTime = time;
897
 
898
- // フレームが更新されるのを待つ
899
- video.addEventListener('seeked', function onSeeked() {
900
  video.removeEventListener('seeked', onSeeked);
901
 
902
  try {
903
- // キャンバスにフレームを描画
904
  const ctx = canvas.getContext('2d');
905
  canvas.width = video.videoWidth;
906
  canvas.height = video.videoHeight;
907
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
908
-
909
- // プレビュー用に縮小
910
  previewContext.drawImage(canvas, 0, 0, previewCanvas.width, previewCanvas.height);
911
-
912
- try {
913
- // プレビューフレームに表示
914
- previewFrame.style.backgroundImage = `url(${previewCanvas.toDataURL()})`;
915
- } catch (e) {
916
- console.error('Canvas export error:', e);
917
- // エラー時の代替処理(単色背景など)
918
- previewFrame.style.backgroundImage = 'linear-gradient(to bottom, #0066ff, #00aaff)';
919
- }
920
  } catch (e) {
921
- console.error('Canvas drawing error:', e);
 
922
  }
923
 
924
- // 元の時間に戻す
925
  video.currentTime = currentTime;
926
  isGeneratingPreview = false;
927
- });
 
 
928
  } catch (e) {
929
- console.error('Preview generation error:', e);
930
  isGeneratingPreview = false;
931
  }
932
  }
933
 
934
- // プレビューツールチップを表示
935
- function showPreviewTooltip(e) {
936
- if (!video.duration) return;
937
-
938
- const rect = progressContainer.getBoundingClientRect();
939
- const percent = (e.clientX - rect.left) / rect.width;
940
- const time = percent * video.duration;
941
-
942
- // ツールチップの位置を設定
943
- const tooltipWidth = previewTooltip.offsetWidth;
944
- let left = e.clientX - rect.left;
945
-
946
- // ツールチップがコンテナからはみ出ないように調整
947
- if (left < tooltipWidth / 2) {
948
- left = tooltipWidth / 2;
949
- } else if (left > rect.width - tooltipWidth / 2) {
950
- left = rect.width - tooltipWidth / 2;
951
- }
952
-
953
- previewTooltip.style.left = `${left}px`;
954
-
955
- // 時間を表示
956
- const minutes = Math.floor(time / 60);
957
- const seconds = Math.floor(time % 60).toString().padStart(2, '0');
958
- previewTime.textContent = `${minutes}:${seconds}`;
959
-
960
- // プレビュー画像を生成
961
- generatePreview(time);
962
-
963
- // ツールチップを表示
964
- previewTooltip.style.display = 'block';
965
- }
966
 
967
- // プレビューツールチップを非表示
968
- function hidePreviewTooltip() {
969
- previewTooltip.style.display = 'none';
970
- }
971
 
972
- function updatePlaybackRate(value) {
973
- const speed = parseFloat(value);
974
- speedInput.value = speed;
975
- speedRange.value = speed;
976
- video.playbackRate = speed;
977
- }
978
 
979
- function updateVolume(value) {
980
- const volume = parseFloat(value);
981
- volumeInput.value = volume;
982
- volumeRange.value = volume;
983
- volumeSlider.value = volume;
984
- video.volume = volume;
985
-
986
- if (volume === 0) {
987
- volumeBtn.textContent = '🔇';
988
- } else if (volume < 0.5) {
989
- volumeBtn.textContent = '🔈';
990
- } else {
991
- volumeBtn.textContent = '🔊';
992
- }
993
- }
 
 
 
 
 
 
994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
995
  function handleVideoChange() {
996
  const selected = videoSelect.value;
997
 
@@ -1004,275 +990,258 @@ function handleVideoChange() {
1004
  }
1005
 
1006
  video.src = selected;
1007
- previewVideo.src = selected; // プレビュー用動画も更新
1008
  video.load();
1009
  previewVideo.load();
1010
  video.play().catch(e => console.log(e));
1011
  }
1012
 
1013
- video.src = selected;
1014
- video.load();
1015
- video.play().then(() => {
1016
- playPauseBtn.textContent = '⏸';
1017
- }).catch(e => console.log(e));
1018
- }
 
 
 
 
 
1019
 
1020
- function togglePlayPause() {
1021
- if (video.paused) {
1022
- video.play();
1023
- playPauseBtn.textContent = '⏸';
1024
- } else {
1025
- video.pause();
1026
- playPauseBtn.textContent = '▶';
1027
- }
1028
- }
 
 
 
 
 
1029
 
1030
- function updateProgress() {
1031
- const percent = (video.currentTime / video.duration) * 100;
1032
- progressBar.style.width = `${percent}%`;
1033
-
1034
- const currentMinutes = Math.floor(video.currentTime / 60);
1035
- const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
1036
- const durationMinutes = Math.floor(video.duration / 60);
1037
- const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
1038
-
1039
- timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
1040
- }
1041
 
1042
- function setProgress(e) {
1043
- const width = progressContainer.clientWidth;
1044
- const clickX = e.offsetX;
1045
- const duration = video.duration;
1046
- video.currentTime = (clickX / width) * duration;
1047
- }
 
 
 
 
1048
 
1049
- function toggleMute() {
1050
- video.muted = !video.muted;
1051
- if (video.muted) {
1052
- volumeBtn.textContent = '🔇';
1053
- volumeSlider.value = 0;
1054
- } else {
1055
- updateVolume(video.volume);
1056
- }
1057
- }
1058
 
1059
- function handleVolumeChange() {
1060
- video.muted = false;
1061
- updateVolume(volumeSlider.value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1062
  }
 
 
1063
 
1064
- function goFullscreen() {
1065
- if (
1066
- document.fullscreenElement ||
1067
- document.webkitFullscreenElement ||
1068
- document.msFullscreenElement
1069
- ) {
1070
- // フルスクリーンを解除
1071
- if (document.exitFullscreen) {
1072
- document.exitFullscreen();
1073
- } else if (document.webkitExitFullscreen) {
1074
- document.webkitExitFullscreen();
1075
- } else if (document.msExitFullscreen) {
1076
- document.msExitFullscreen();
1077
- }
1078
- } else {
1079
- // フルスクリーンにする
1080
- if (videoContainer.requestFullscreen) {
1081
- videoContainer.requestFullscreen();
1082
- } else if (videoContainer.webkitRequestFullscreen) {
1083
- videoContainer.webkitRequestFullscreen();
1084
- } else if (videoContainer.msRequestFullscreen) {
1085
- videoContainer.msRequestFullscreen();
1086
- }
1087
- }
1088
- }
1089
-
1090
- // 全画面コンテキストメニューを表示
1091
- function showContextMenu(e) {
1092
- // 全画面時のみコンテキストメニューを表示
1093
- if (!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement)) {
1094
- return;
1095
- }
1096
-
1097
- e.preventDefault();
1098
-
1099
- // メニューの位置を設定
1100
- fullscreenContextMenu.style.display = 'block';
1101
- fullscreenContextMenu.style.left = `${e.clientX}px`;
1102
- fullscreenContextMenu.style.top = `${e.clientY}px`;
1103
- }
1104
-
1105
- // 全画面コンテキストメニューを非表示
1106
- function hideContextMenu() {
1107
- fullscreenContextMenu.style.display = 'none';
1108
- }
1109
-
1110
- // 音声/字幕のみモードを切り替え
1111
- function toggleAudioOnlyMode() {
1112
- isAudioOnlyMode = !isAudioOnlyMode;
1113
-
1114
- if (isAudioOnlyMode) {
1115
- videoContainer.classList.add('audio-only-mode');
1116
- videoPlaceholder.style.display = 'flex';
1117
- video.style.display = 'none';
1118
- audioOnlyBtn.textContent = '🎥';
1119
- audioOnlyBtn.title = '動画表示モード';
1120
- } else {
1121
- videoContainer.classList.remove('audio-only-mode');
1122
- videoPlaceholder.style.display = 'none';
1123
- video.style.display = 'block';
1124
- audioOnlyBtn.textContent = '🔈';
1125
- audioOnlyBtn.title = '音声/字幕のみ';
1126
- }
1127
-
1128
- // コンテキストメニューも更新
1129
- hideContextMenu();
1130
- }
1131
 
1132
- // 全画面変更時の字幕サイズ調整
1133
- function updateSubtitleScaleForFullscreen() {
1134
- if (document.fullscreenElement || document.webkitFullscreenElement ||
1135
- document.mozFullScreenElement || document.msFullscreenElement) {
1136
- // 全画面モード
1137
- const fullscreenWidth = window.innerWidth;
1138
- const scaleFactor = fullscreenWidth / normalVideoWidth;
1139
- document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
1140
- } else {
1141
- // 通常モード
1142
- document.documentElement.style.setProperty('--fullscreen-scale', 1);
1143
- }
1144
- }
1145
 
1146
- // 字幕関連の関数
1147
- function toggleSubtitles() {
1148
- subtitlesEnabled = subtitleToggle.checked;
1149
- subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
1150
- subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
1151
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1152
 
1153
- function updateSubtitleSize(value) {
1154
- const size = parseFloat(value);
1155
- subtitleSizeInput.value = size;
1156
- subtitleSize.value = size;
1157
-
1158
- // 字幕サイズを制御
1159
- document.documentElement.style.setProperty('--subtitle-scale', size);
1160
-
1161
- // VTTCueのlineプロパティには数値のみを設定('bottom'は無効)
1162
- const track = subtitleTrackElement.track;
1163
- if (track && track.cues) {
1164
- for (let i = 0; i < track.cues.length; i++) {
1165
- // 画面下部に表示するため、適切な数値を設定。とりあえず90。
1166
- track.cues[i].line = 90;
1167
- track.cues[i].snapToLines = false; // ラインスナップを無効に
1168
- }
1169
- }
1170
- }
1171
-
1172
- function changeSubtitleTrack() {
1173
- const selectedTrack = subtitleTrack.value;
1174
- subtitleTrackElement.src = selectedTrack;
1175
- subtitleTrackElement.track.mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
1176
-
1177
- // トラック変更後に再度読み込み
1178
- video.textTracks[0].mode = 'hidden';
1179
- if (selectedTrack) {
1180
- video.textTracks[0].mode = subtitlesEnabled ? 'showing' : 'hidden';
1181
- }
1182
- }
1183
 
1184
- function toggleSubtitleMenu() {
1185
- document.getElementById('subtitleToggle').checked ^= true;
1186
- toggleSubtitles();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1187
  }
 
 
1188
 
1189
- // イベントリスナー
1190
- videoSelect.addEventListener('change', handleVideoChange);
 
 
 
 
 
 
 
1191
 
1192
- ['input', 'change', 'mouseup'].forEach(eventName => {
1193
- speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1194
- volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1195
- subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1196
- });
1197
 
1198
- speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1199
- volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1200
- subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1201
 
1202
- loopCheckbox.addEventListener('change', () => {
1203
- video.loop = loopCheckbox.checked;
1204
- });
 
 
1205
 
1206
- subtitleToggle.addEventListener('change', toggleSubtitles);
1207
- subtitleTrack.addEventListener('change', changeSubtitleTrack);
1208
- subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1209
-
1210
- playPauseBtn.addEventListener('click', togglePlayPause);
1211
- video.addEventListener('click', togglePlayPause);
1212
- video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1213
- video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1214
- video.addEventListener('timeupdate', updateProgress);
1215
- progressContainer.addEventListener('click', setProgress);
1216
- progressContainer.addEventListener('mousedown', () => isDragging = true);
1217
- document.addEventListener('mouseup', () => isDragging = false);
1218
- progressContainer.addEventListener('mousemove', (e) => {
1219
- if (isDragging) {
1220
- setProgress(e);
1221
- }
1222
- showPreviewTooltip(e);
1223
- });
1224
- progressContainer.addEventListener('mouseenter', showPreviewTooltip);
1225
- progressContainer.addEventListener('mouseleave', hidePreviewTooltip);
1226
- volumeBtn.addEventListener('click', toggleMute);
1227
- volumeSlider.addEventListener('input', handleVolumeChange);
1228
- fullscreenBtn.addEventListener('click', goFullscreen);
1229
- audioOnlyBtn.addEventListener('click', toggleAudioOnlyMode);
1230
-
1231
- // 全画面コンテキストメニュー関連
1232
- videoContainer.addEventListener('contextmenu', showContextMenu);
1233
- exitFullscreenBtn.addEventListener('click', () => {
1234
- if (document.exitFullscreen) {
1235
- document.exitFullscreen();
1236
- } else if (document.webkitExitFullscreen) {
1237
- document.webkitExitFullscreen();
1238
- } else if (document.msExitFullscreen) {
1239
- document.msExitFullscreen();
1240
- }
1241
- hideContextMenu();
1242
- });
1243
- showVideoBtn.addEventListener('click', () => {
1244
- if (isAudioOnlyMode) {
1245
- toggleAudioOnlyMode();
1246
- }
1247
- hideContextMenu();
1248
- });
1249
- contextAudioOnlyBtn.addEventListener('click', toggleAudioOnlyMode);
1250
- closeContextMenuBtn.addEventListener('click', hideContextMenu);
1251
- document.addEventListener('click', hideContextMenu);
1252
-
1253
- // 全画面変更イベントを監視
1254
- document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1255
- document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1256
- document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1257
- document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1258
-
1259
- video.addEventListener('loadedmetadata', () => {
1260
- updatePlaybackRate(speedRange.value);
1261
- updateVolume(volumeRange.value);
1262
- updateSubtitleSize(subtitleSize.value);
1263
- video.loop = loopCheckbox.checked;
1264
- toggleSubtitles();
1265
- updateProgress();
1266
- // 通常時の動画幅を記録
1267
- normalVideoWidth = videoContainer.clientWidth;
1268
- // プレビュー用キャンバスを作成
1269
- createPreviewCanvas();
1270
- });
 
1271
 
1272
- // CSS変数を設定
1273
- document.documentElement.style.setProperty('--subtitle-scale', '1');
1274
- document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1275
- document.documentElement.style.setProperty('--fullscreen-scale', '1');
1276
  </script>
1277
  </body>
1278
 
 
783
  </div>
784
  </div>
785
  <script>
786
+ // 要素取得
787
+ const video = document.getElementById('videoPlayer');
788
+ const videoSelect = document.getElementById('videoSelect');
789
+ const speedRange = document.getElementById('speedRange');
790
+ const speedInput = document.getElementById('speedInput');
791
+ const volumeRange = document.getElementById('volumeRange');
792
+ const volumeInput = document.getElementById('volumeInput');
793
+ const loopCheckbox = document.getElementById('loopCheckbox');
794
+ const playPauseBtn = document.getElementById('playPauseBtn');
795
+ const progressBar = document.getElementById('progressBar');
796
+ const progressContainer = document.getElementById('progressContainer');
797
+ const timeDisplay = document.getElementById('timeDisplay');
798
+ const volumeBtn = document.getElementById('volumeBtn');
799
+ const volumeSlider = document.getElementById('volumeSlider');
800
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
801
+ const subtitleBtn = document.getElementById('subtitleBtn');
802
+ const subtitleToggle = document.getElementById('subtitleToggle');
803
+ const subtitleSize = document.getElementById('subtitleSize');
804
+ const subtitleSizeInput = document.getElementById('subtitleSizeInput');
805
+ const subtitleTrack = document.getElementById('subtitleTrack');
806
+ const subtitleTrackElement = document.getElementById('subtitleTrackElement');
807
+ const videoContainer = document.getElementById('videoContainer');
808
+ const videoPlaceholder = document.getElementById('videoPlaceholder');
809
+ const previewTooltip = document.getElementById('previewTooltip');
810
+ const previewTime = document.getElementById('previewTime');
811
+ const previewFrame = document.getElementById('previewFrame');
812
+ const audioOnlyBtn = document.getElementById('audioOnlyBtn');
813
+ const fullscreenContextMenu = document.getElementById('fullscreenContextMenu');
814
+ const exitFullscreenBtn = document.getElementById('exitFullscreenBtn');
815
+ const showVideoBtn = document.getElementById('showVideoBtn');
816
+ const closeContextMenuBtn = document.getElementById('closeContextMenuBtn');
817
+
818
+ // プレビュー用動画を動的に作成
819
+ const previewVideo = document.createElement('video');
820
+ previewVideo.id = 'previewVideo';
821
+ previewVideo.className = 'thumbnail-preview';
822
+ previewVideo.muted = true;
823
+ previewVideo.preload = 'auto';
824
+ videoContainer.appendChild(previewVideo);
825
+
826
+ // 初期設定
827
+ video.controls = false;
828
+ previewVideo.src = video.src;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
829
  let isDragging = false;
830
  let subtitlesEnabled = true;
831
  let normalVideoWidth = videoContainer.clientWidth;
832
+ let isAudioOnlyMode = false;
833
  let hoverTimeout;
834
+ let canvas = null;
835
+ let previewCanvas = null;
836
+ let previewContext = null;
837
+ let isGeneratingPreview = false;
838
+
839
+ // プレビュー用キャンバスを作成
840
+ function createPreviewCanvas() {
841
+ if (!canvas) {
842
+ canvas = document.createElement('canvas');
843
+ canvas.width = video.videoWidth || 640;
844
+ canvas.height = video.videoHeight || 360;
845
+ }
846
+
847
+ if (!previewCanvas) {
848
+ previewCanvas = document.createElement('canvas');
849
+ previewCanvas.width = 160;
850
+ previewCanvas.height = 90;
851
+ previewContext = previewCanvas.getContext('2d');
852
+ }
853
+ }
854
+
855
  // プレビュー画像を生成
856
  function generatePreview(time) {
857
+ if (isGeneratingPreview || !video.readyState) return;
858
 
859
  try {
860
  isGeneratingPreview = true;
861
  createPreviewCanvas();
862
 
 
863
  const currentTime = video.currentTime;
 
 
864
  video.currentTime = time;
865
 
866
+ const onSeeked = () => {
 
867
  video.removeEventListener('seeked', onSeeked);
868
 
869
  try {
 
870
  const ctx = canvas.getContext('2d');
871
  canvas.width = video.videoWidth;
872
  canvas.height = video.videoHeight;
873
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
 
 
874
  previewContext.drawImage(canvas, 0, 0, previewCanvas.width, previewCanvas.height);
875
+ previewFrame.style.backgroundImage = `url(${previewCanvas.toDataURL()})`;
 
 
 
 
 
 
 
 
876
  } catch (e) {
877
+ console.error('Preview generation error:', e);
878
+ previewFrame.style.backgroundImage = 'linear-gradient(to bottom, #0066ff, #00aaff)';
879
  }
880
 
 
881
  video.currentTime = currentTime;
882
  isGeneratingPreview = false;
883
+ };
884
+
885
+ video.addEventListener('seeked', onSeeked);
886
  } catch (e) {
887
+ console.error('Preview error:', e);
888
  isGeneratingPreview = false;
889
  }
890
  }
891
 
892
+ // プレビューツールチップを表示
893
+ function showPreviewTooltip(e) {
894
+ if (!video.duration) return;
895
+
896
+ const rect = progressContainer.getBoundingClientRect();
897
+ const percent = (e.clientX - rect.left) / rect.width;
898
+ const time = Math.max(0, Math.min(percent, 1)) * video.duration;
899
+
900
+ const tooltipWidth = previewTooltip.offsetWidth;
901
+ let left = e.clientX - rect.left;
902
+ left = Math.max(tooltipWidth / 2, Math.min(left, rect.width - tooltipWidth / 2));
903
+
904
+ previewTooltip.style.left = `${left}px`;
905
+
906
+ const minutes = Math.floor(time / 60);
907
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
908
+ previewTime.textContent = `${minutes}:${seconds}`;
909
+
910
+ generatePreview(time);
911
+ previewTooltip.style.display = 'block';
912
+ }
 
 
 
 
 
 
 
 
 
 
 
913
 
914
+ // プレビューツールチップを非表示
915
+ function hidePreviewTooltip() {
916
+ previewTooltip.style.display = 'none';
917
+ }
918
 
919
+ // ホバー時の時間表示設定
920
+ function setupHoverTime() {
921
+ const hoverTime = document.createElement('div');
922
+ hoverTime.className = 'hover-time';
923
+ progressContainer.appendChild(hoverTime);
 
924
 
925
+ progressContainer.addEventListener("mousemove", (e) => {
926
+ if (!video.duration) return;
927
+
928
+ const rect = progressContainer.getBoundingClientRect();
929
+ const pos = Math.min(Math.max((e.clientX - rect.left) / rect.width, 0), 1);
930
+ const time = pos * video.duration;
931
+
932
+ const minutes = Math.floor(time / 60);
933
+ const seconds = Math.floor(time % 60).toString().padStart(2, '0');
934
+ hoverTime.textContent = `${minutes}:${seconds}`;
935
+ hoverTime.style.display = 'block';
936
+ hoverTime.style.left = `${e.clientX - rect.left}px`;
937
+
938
+ previewVideo.style.display = "block";
939
+ previewVideo.style.left = `${e.clientX}px`;
940
+
941
+ clearTimeout(hoverTimeout);
942
+ hoverTimeout = setTimeout(() => {
943
+ previewVideo.currentTime = time;
944
+ }, 50);
945
+ });
946
 
947
+ progressContainer.addEventListener("mouseleave", () => {
948
+ const hoverTime = document.querySelector('.hover-time');
949
+ if (hoverTime) hoverTime.style.display = "none";
950
+ previewVideo.style.display = "none";
951
+ clearTimeout(hoverTimeout);
952
+ });
953
+ }
954
+
955
+ // 再生速度を更新
956
+ function updatePlaybackRate(value) {
957
+ const speed = parseFloat(value);
958
+ speedInput.value = speed;
959
+ speedRange.value = speed;
960
+ video.playbackRate = speed;
961
+ }
962
+
963
+ // 音量を更新
964
+ function updateVolume(value) {
965
+ const volume = parseFloat(value);
966
+ volumeInput.value = volume;
967
+ volumeRange.value = volume;
968
+ volumeSlider.value = volume;
969
+ video.volume = volume;
970
+
971
+ if (volume === 0) {
972
+ volumeBtn.textContent = '🔇';
973
+ } else if (volume < 0.5) {
974
+ volumeBtn.textContent = '🔈';
975
+ } else {
976
+ volumeBtn.textContent = '🔊';
977
+ }
978
+ }
979
+
980
+ // 動画変更処理
981
  function handleVideoChange() {
982
  const selected = videoSelect.value;
983
 
 
990
  }
991
 
992
  video.src = selected;
993
+ previewVideo.src = selected;
994
  video.load();
995
  previewVideo.load();
996
  video.play().catch(e => console.log(e));
997
  }
998
 
999
+ // 再生/一時停止を切り替え
1000
+ function togglePlayPause() {
1001
+ if (video.paused) {
1002
+ video.play().then(() => {
1003
+ playPauseBtn.textContent = '⏸';
1004
+ }).catch(e => console.log(e));
1005
+ } else {
1006
+ video.pause();
1007
+ playPauseBtn.textContent = '▶';
1008
+ }
1009
+ }
1010
 
1011
+ // 進捗バーを更新
1012
+ function updateProgress() {
1013
+ if (!video.duration) return;
1014
+
1015
+ const percent = (video.currentTime / video.duration) * 100;
1016
+ progressBar.style.width = `${percent}%`;
1017
+
1018
+ const currentMinutes = Math.floor(video.currentTime / 60);
1019
+ const currentSeconds = Math.floor(video.currentTime % 60).toString().padStart(2, '0');
1020
+ const durationMinutes = Math.floor(video.duration / 60);
1021
+ const durationSeconds = Math.floor(video.duration % 60).toString().padStart(2, '0');
1022
+
1023
+ timeDisplay.textContent = `${currentMinutes}:${currentSeconds} / ${durationMinutes}:${durationSeconds}`;
1024
+ }
1025
 
1026
+ // 進捗バーを設定
1027
+ function setProgress(e) {
1028
+ const width = progressContainer.clientWidth;
1029
+ const clickX = e.offsetX;
1030
+ const duration = video.duration;
1031
+ video.currentTime = (clickX / width) * duration;
1032
+ }
 
 
 
 
1033
 
1034
+ // ミュートを切り替え
1035
+ function toggleMute() {
1036
+ video.muted = !video.muted;
1037
+ if (video.muted) {
1038
+ volumeBtn.textContent = '🔇';
1039
+ volumeSlider.value = 0;
1040
+ } else {
1041
+ updateVolume(video.volume);
1042
+ }
1043
+ }
1044
 
1045
+ // 音量変更を処理
1046
+ function handleVolumeChange() {
1047
+ video.muted = false;
1048
+ updateVolume(volumeSlider.value);
1049
+ }
 
 
 
 
1050
 
1051
+ // 全画面表示を切り替え
1052
+ function goFullscreen() {
1053
+ if (document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement) {
1054
+ if (document.exitFullscreen) {
1055
+ document.exitFullscreen();
1056
+ } else if (document.webkitExitFullscreen) {
1057
+ document.webkitExitFullscreen();
1058
+ } else if (document.msExitFullscreen) {
1059
+ document.msExitFullscreen();
1060
+ }
1061
+ } else {
1062
+ if (videoContainer.requestFullscreen) {
1063
+ videoContainer.requestFullscreen();
1064
+ } else if (videoContainer.webkitRequestFullscreen) {
1065
+ videoContainer.webkitRequestFullscreen();
1066
+ } else if (videoContainer.msRequestFullscreen) {
1067
+ videoContainer.msRequestFullscreen();
1068
  }
1069
+ }
1070
+ }
1071
 
1072
+ // 全画面コンテキストメニューを表示
1073
+ function showContextMenu(e) {
1074
+ if (!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement)) {
1075
+ return;
1076
+ }
1077
+
1078
+ e.preventDefault();
1079
+
1080
+ fullscreenContextMenu.style.display = 'block';
1081
+ fullscreenContextMenu.style.left = `${e.clientX}px`;
1082
+ fullscreenContextMenu.style.top = `${e.clientY}px`;
1083
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1084
 
1085
+ // 全画面コンテキストメニューを非表示
1086
+ function hideContextMenu() {
1087
+ fullscreenContextMenu.style.display = 'none';
1088
+ }
 
 
 
 
 
 
 
 
 
1089
 
1090
+ // 音声/字幕のみモードを切り替え
1091
+ function toggleAudioOnlyMode() {
1092
+ isAudioOnlyMode = !isAudioOnlyMode;
1093
+
1094
+ if (isAudioOnlyMode) {
1095
+ videoContainer.classList.add('audio-only-mode');
1096
+ videoPlaceholder.style.display = 'flex';
1097
+ video.style.display = 'none';
1098
+ audioOnlyBtn.textContent = '🎥';
1099
+ audioOnlyBtn.title = '動画表示モード';
1100
+ } else {
1101
+ videoContainer.classList.remove('audio-only-mode');
1102
+ videoPlaceholder.style.display = 'none';
1103
+ video.style.display = 'block';
1104
+ audioOnlyBtn.textContent = '🔈';
1105
+ audioOnlyBtn.title = '音声/字幕のみ';
1106
+ }
1107
+
1108
+ hideContextMenu();
1109
+ }
1110
 
1111
+ // 全画面時の字幕サイズ調整
1112
+ function updateSubtitleScaleForFullscreen() {
1113
+ if (document.fullscreenElement || document.webkitFullscreenElement ||
1114
+ document.mozFullScreenElement || document.msFullscreenElement) {
1115
+ const fullscreenWidth = window.innerWidth;
1116
+ const scaleFactor = fullscreenWidth / normalVideoWidth;
1117
+ document.documentElement.style.setProperty('--fullscreen-scale', scaleFactor);
1118
+ } else {
1119
+ document.documentElement.style.setProperty('--fullscreen-scale', 1);
1120
+ }
1121
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1122
 
1123
+ // 字幕表示を切り替え
1124
+ function toggleSubtitles() {
1125
+ subtitlesEnabled = subtitleToggle.checked;
1126
+ if (subtitleTrackElement.track) {
1127
+ subtitleTrackElement.track.mode = subtitlesEnabled ? 'showing' : 'hidden';
1128
+ }
1129
+ subtitleBtn.style.color = subtitlesEnabled ? '#00ccff' : '#666';
1130
+ }
1131
+
1132
+ // 字幕サイズを更新
1133
+ function updateSubtitleSize(value) {
1134
+ const size = parseFloat(value);
1135
+ subtitleSizeInput.value = size;
1136
+ subtitleSize.value = size;
1137
+ document.documentElement.style.setProperty('--subtitle-scale', size);
1138
+
1139
+ const track = subtitleTrackElement.track;
1140
+ if (track && track.cues) {
1141
+ for (let i = 0; i < track.cues.length; i++) {
1142
+ track.cues[i].line = 90;
1143
+ track.cues[i].snapToLines = false;
1144
  }
1145
+ }
1146
+ }
1147
 
1148
+ // 字幕トラックを変更
1149
+ function changeSubtitleTrack() {
1150
+ const selectedTrack = subtitleTrack.value;
1151
+ subtitleTrackElement.src = selectedTrack;
1152
+
1153
+ if (video.textTracks.length > 0) {
1154
+ video.textTracks[0].mode = selectedTrack && subtitlesEnabled ? 'showing' : 'hidden';
1155
+ }
1156
+ }
1157
 
1158
+ // 字幕メニューを切り替え
1159
+ function toggleSubtitleMenu() {
1160
+ subtitleToggle.checked = !subtitleToggle.checked;
1161
+ toggleSubtitles();
1162
+ }
1163
 
1164
+ // イベントリスナーを設定
1165
+ function setupEventListeners() {
1166
+ videoSelect.addEventListener('change', handleVideoChange);
1167
 
1168
+ ['input', 'change'].forEach(eventName => {
1169
+ speedRange.addEventListener(eventName, () => updatePlaybackRate(speedRange.value));
1170
+ volumeRange.addEventListener(eventName, () => updateVolume(volumeRange.value));
1171
+ subtitleSize.addEventListener(eventName, () => updateSubtitleSize(subtitleSize.value));
1172
+ });
1173
 
1174
+ speedInput.addEventListener('input', () => updatePlaybackRate(speedInput.value));
1175
+ volumeInput.addEventListener('input', () => updateVolume(volumeInput.value));
1176
+ subtitleSizeInput.addEventListener('input', () => updateSubtitleSize(subtitleSizeInput.value));
1177
+
1178
+ loopCheckbox.addEventListener('change', () => {
1179
+ video.loop = loopCheckbox.checked;
1180
+ });
1181
+
1182
+ subtitleToggle.addEventListener('change', toggleSubtitles);
1183
+ subtitleTrack.addEventListener('change', changeSubtitleTrack);
1184
+ subtitleBtn.addEventListener('click', toggleSubtitleMenu);
1185
+
1186
+ playPauseBtn.addEventListener('click', togglePlayPause);
1187
+ video.addEventListener('click', togglePlayPause);
1188
+ video.addEventListener('play', () => playPauseBtn.textContent = '⏸');
1189
+ video.addEventListener('pause', () => playPauseBtn.textContent = '▶');
1190
+ video.addEventListener('timeupdate', updateProgress);
1191
+
1192
+ progressContainer.addEventListener('click', setProgress);
1193
+ progressContainer.addEventListener('mousedown', () => isDragging = true);
1194
+ document.addEventListener('mouseup', () => isDragging = false);
1195
+ progressContainer.addEventListener('mousemove', (e) => {
1196
+ if (isDragging) setProgress(e);
1197
+ showPreviewTooltip(e);
1198
+ });
1199
+ progressContainer.addEventListener('mouseenter', showPreviewTooltip);
1200
+ progressContainer.addEventListener('mouseleave', hidePreviewTooltip);
1201
+
1202
+ volumeBtn.addEventListener('click', toggleMute);
1203
+ volumeSlider.addEventListener('input', handleVolumeChange);
1204
+ fullscreenBtn.addEventListener('click', goFullscreen);
1205
+ audioOnlyBtn.addEventListener('click', toggleAudioOnlyMode);
1206
+
1207
+ videoContainer.addEventListener('contextmenu', showContextMenu);
1208
+ exitFullscreenBtn.addEventListener('click', () => {
1209
+ if (document.exitFullscreen) document.exitFullscreen();
1210
+ hideContextMenu();
1211
+ });
1212
+ showVideoBtn.addEventListener('click', () => {
1213
+ if (isAudioOnlyMode) toggleAudioOnlyMode();
1214
+ hideContextMenu();
1215
+ });
1216
+ closeContextMenuBtn.addEventListener('click', hideContextMenu);
1217
+ document.addEventListener('click', hideContextMenu);
1218
+
1219
+ document.addEventListener('fullscreenchange', updateSubtitleScaleForFullscreen);
1220
+ document.addEventListener('webkitfullscreenchange', updateSubtitleScaleForFullscreen);
1221
+ document.addEventListener('mozfullscreenchange', updateSubtitleScaleForFullscreen);
1222
+ document.addEventListener('MSFullscreenChange', updateSubtitleScaleForFullscreen);
1223
+
1224
+ video.addEventListener('loadedmetadata', () => {
1225
+ updatePlaybackRate(speedRange.value);
1226
+ updateVolume(volumeRange.value);
1227
+ updateSubtitleSize(subtitleSize.value);
1228
+ video.loop = loopCheckbox.checked;
1229
+ toggleSubtitles();
1230
+ updateProgress();
1231
+ normalVideoWidth = videoContainer.clientWidth;
1232
+ createPreviewCanvas();
1233
+ });
1234
+ }
1235
+
1236
+ // CSS変数を初期設定
1237
+ document.documentElement.style.setProperty('--subtitle-scale', '1');
1238
+ document.documentElement.style.setProperty('--subtitle-border-radius', '10px');
1239
+ document.documentElement.style.setProperty('--fullscreen-scale', '1');
1240
 
1241
+ // 初期化
1242
+ setupHoverTime();
1243
+ setupEventListeners();
1244
+ createPreviewCanvas();
1245
  </script>
1246
  </body>
1247