soiz1's picture
Update index.html
f930612
raw
history blame
30.3 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>高度な音声動画プレイヤー</title>
<style>
body {
font-family: 'Arial', sans-serif;
background-color: #0a192f;
color: #e6f1ff;
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
color: #64ffda;
text-align: center;
margin-bottom: 30px;
border-bottom: 1px solid #64ffda;
padding-bottom: 10px;
width: 100%;
}
.container {
display: flex;
flex-direction: column;
width: 100%;
max-width: 800px;
background-color: #112240;
border-radius: 10px;
padding: 20px;
box-shadow: 0 0 20px rgba(100, 255, 218, 0.2);
}
.video-container {
position: relative;
width: 100%;
margin-bottom: 20px;
}
video {
width: 100%;
border-radius: 5px;
background-color: #000;
display: block;
cursor: pointer;
}
.video-controls {
background-color: rgba(17, 34, 64, 0.9);
padding: 10px;
border-radius: 0 0 5px 5px;
display: flex;
flex-direction: column;
gap: 10px;
}
.progress-container {
width: 100%;
height: 10px;
background-color: #1e2a47;
border-radius: 5px;
cursor: pointer;
position: relative;
}
.progress-bar {
height: 100%;
background-color: #64ffda;
border-radius: 5px;
width: 0%;
position: relative;
}
.progress-time {
position: absolute;
top: -25px;
transform: translateX(-50%);
background-color: rgba(30, 42, 71, 0.9);
padding: 3px 6px;
border-radius: 3px;
font-size: 12px;
display: none;
white-space: nowrap;
}
.main-controls {
display: flex;
align-items: center;
gap: 15px;
}
.control-button {
background: none;
border: none;
color: #e6f1ff;
font-size: 18px;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s;
}
.control-button:hover {
background-color: rgba(100, 255, 218, 0.2);
}
.time-display {
font-size: 14px;
color: #ccd6f6;
white-space: nowrap;
}
.volume-control {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
}
.volume-button {
background: none;
border: none;
color: #e6f1ff;
font-size: 18px;
cursor: pointer;
padding: 5px;
}
.volume-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 3px;
outline: none;
opacity: 0;
transition: opacity 0.3s, width 0.3s;
}
.volume-control:hover .volume-slider {
opacity: 1;
width: 100px;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.speed-control {
display: flex;
align-items: center;
gap: 5px;
}
.speed-slider {
width: 80px;
height: 6px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 3px;
outline: none;
}
.speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.speed-value {
font-size: 14px;
min-width: 30px;
text-align: center;
}
.fullscreen-button {
margin-left: 10px;
}
.audio-controls {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 15px;
}
.audio-item {
display: flex;
align-items: center;
gap: 10px;
}
.audio-item label {
min-width: 50px;
color: #64ffda;
}
.audio-slider {
flex-grow: 1;
height: 8px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 5px;
outline: none;
}
.audio-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.settings {
background-color: #1e2a47;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.setting-item:last-child {
margin-bottom: 0;
}
.setting-item label {
color: #ccd6f6;
}
.global-volume-container, .playback-speed-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.global-volume-slider, .playback-speed-slider {
flex-grow: 1;
height: 8px;
-webkit-appearance: none;
background: #1e2a47;
border-radius: 5px;
outline: none;
}
.global-volume-slider::-webkit-slider-thumb,
.playback-speed-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: #64ffda;
border-radius: 50%;
cursor: pointer;
}
.slider-value {
min-width: 40px;
text-align: right;
}
input[type="number"], input[type="checkbox"], select {
background-color: #112240;
border: 1px solid #64ffda;
color: #e6f1ff;
padding: 5px;
border-radius: 3px;
}
.tech-decoration {
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #64ffda, transparent);
margin: 20px 0;
}
/* 全画面時のスタイル */
.video-container:-webkit-full-screen {
width: 100%;
height: 100%;
background-color: black;
}
.video-container:-webkit-full-screen video {
width: 100%;
height: 100%;
}
.video-container:-webkit-full-screen .video-controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
border-radius: 0;
}
</style>
</head>
<body>
<h1>高度な音声動画プレイヤー</h1>
<div class="container">
<div class="video-container" id="video-container">
<video id="video" muted>
<source src="v.mp4" type="video/mp4">
</video>
<div class="video-controls">
<div class="progress-container" id="progress-container">
<div class="progress-bar" id="progress-bar"></div>
<div class="progress-time" id="progress-time">00:00</div>
</div>
<div class="main-controls">
<button class="control-button" id="play-pause-btn"></button>
<div class="time-display" id="time-display">00:00 / 00:00</div>
<div class="volume-control">
<button class="volume-button" id="volume-btn">🔊</button>
<input type="range" class="volume-slider" id="volume-slider" min="0" max="1" step="0.01" value="1">
</div>
<div class="speed-control">
<span class="speed-value" id="speed-value">1.00x</span>
<input type="range" class="speed-slider" id="speed-slider" min="0.25" max="3" step="0.05" value="1">
</div>
<button class="control-button fullscreen-button" id="fullscreen-btn"></button>
</div>
</div>
</div>
<div class="tech-decoration"></div>
<div class="settings">
<h2>設定</h2>
<div class="setting-item">
<label for="start-time">再生開始秒数:</label>
<input type="number" id="start-time" min="0" value="0" step="0.1">
</div>
<div class="setting-item">
<label for="end-time">再生終了秒数:</label>
<input type="number" id="end-time" min="0" value="0" step="0.1">
</div>
<div class="setting-item">
<label for="loop">ループ再生:</label>
<input type="checkbox" id="loop">
</div>
<div class="setting-item">
<div class="global-volume-container">
<label>全体音量係数:</label>
<input type="range" class="global-volume-slider" id="global-volume" min="0" max="3" step="0.1" value="1">
<span class="slider-value" id="global-volume-value">1.0</span>
</div>
</div>
<div class="setting-item">
<div class="playback-speed-container">
<label>再生速度:</label>
<input type="range" class="playback-speed-slider" id="playback-speed" min="0.25" max="3" step="0.05" value="1">
<span class="slider-value" id="playback-speed-value">1.00x</span>
</div>
</div>
</div>
<div class="tech-decoration"></div>
<div class="audio-controls">
<h2>音声コントロール</h2>
<div class="audio-item">
<label>p.mp3</label>
<input type="range" class="audio-slider" data-audio="p" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>a.mp3</label>
<input type="range" class="audio-slider" data-audio="a" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>t.mp3</label>
<input type="range" class="audio-slider" data-audio="t" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>s.mp3</label>
<input type="range" class="audio-slider" data-audio="s" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
<div class="audio-item">
<label>k.mp3</label>
<input type="range" class="audio-slider" data-audio="k" min="0" max="1" step="0.01" value="1">
<span class="slider-value volume-value">1.00</span>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 要素を取得
const video = document.getElementById('video');
const videoContainer = document.getElementById('video-container');
const playPauseBtn = document.getElementById('play-pause-btn');
const timeDisplay = document.getElementById('time-display');
const progressContainer = document.getElementById('progress-container');
const progressBar = document.getElementById('progress-bar');
const progressTime = document.getElementById('progress-time');
const volumeBtn = document.getElementById('volume-btn');
const volumeSlider = document.getElementById('volume-slider');
const speedSlider = document.getElementById('speed-slider');
const speedValue = document.getElementById('speed-value');
const playbackSpeedSlider = document.getElementById('playback-speed');
const playbackSpeedValue = document.getElementById('playback-speed-value');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const startTimeInput = document.getElementById('start-time');
const endTimeInput = document.getElementById('end-time');
const loopCheckbox = document.getElementById('loop');
const globalVolumeSlider = document.getElementById('global-volume');
const globalVolumeValue = document.getElementById('global-volume-value');
const audioSliders = document.querySelectorAll('.audio-slider');
const volumeValues = document.querySelectorAll('.volume-value');
// 音声オブジェクトを作成
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const audioBuffers = {};
const audioSources = {};
const gainNodes = {};
const pitchShiftNodes = {};
// 音声ファイル名の配列
const audioFiles = ['p', 'a', 't', 's', 'k'];
// 初期化
let videoDuration = 0;
let isPlaying = false;
let isVideoPlaying = false;
let lastVolume = 1;
let shouldPlayFromStartTime = false;
// 動画のメタデータが読み込まれたら
video.addEventListener('loadedmetadata', function() {
videoDuration = video.duration;
endTimeInput.value = videoDuration.toFixed(1);
endTimeInput.max = videoDuration;
startTimeInput.max = videoDuration - 0.1;
updateTimeDisplay();
});
// 時間表示を更新
function updateTimeDisplay() {
const currentTime = video.currentTime;
const duration = video.duration || videoDuration;
const currentMinutes = Math.floor(currentTime / 60);
const currentSeconds = Math.floor(currentTime % 60);
const durationMinutes = Math.floor(duration / 60);
const durationSeconds = Math.floor(duration % 60);
timeDisplay.textContent =
`${String(currentMinutes).padStart(2, '0')}:${String(currentSeconds).padStart(2, '0')} / ` +
`${String(durationMinutes).padStart(2, '0')}:${String(durationSeconds).padStart(2, '0')}`;
// プログレスバーを更新
const progressPercent = (currentTime / duration) * 100;
progressBar.style.width = `${progressPercent}%`;
}
// 動画の時間更新イベント
video.addEventListener('timeupdate', updateTimeDisplay);
// 動画終了時の処理
video.addEventListener('ended', function() {
const startTime = parseFloat(startTimeInput.value) || 0;
if (!loopCheckbox.checked) {
stopMedia();
video.currentTime = startTime;
updateTimeDisplay();
}
});
// プログレスバークリックでシーク
progressContainer.addEventListener('click', function(e) {
if (!video.duration) return;
const rect = this.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const seekTime = pos * video.duration;
seekMedia(seekTime);
});
// 指定した時間にシーク
function seekMedia(time) {
const duration = video.duration || videoDuration;
const startTime = parseFloat(startTimeInput.value) || 0;
const endTime = parseFloat(endTimeInput.value) || duration;
// 範囲内に制限
const seekTime = Math.max(startTime, Math.min(time, endTime));
video.currentTime = seekTime;
// 音声も同期
audioFiles.forEach(file => {
if (audioSources[file]) {
audioSources[file].stop();
if (isPlaying) {
playAudio(file, seekTime);
}
}
});
}
// プログレスバー上でマウス移動時に時間を表示
progressContainer.addEventListener('mousemove', function(e) {
if (!video.duration) return;
const rect = this.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
const hoverTime = pos * video.duration;
const minutes = Math.floor(hoverTime / 60);
const seconds = Math.floor(hoverTime % 60);
progressTime.textContent = `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
progressTime.style.display = 'block';
progressTime.style.left = `${pos * 100}%`;
});
progressContainer.addEventListener('mouseleave', function() {
progressTime.style.display = 'none';
});
// 動画クリックで再生/一時停止
video.addEventListener('click', function() {
if (isVideoPlaying) {
pauseMedia();
} else {
playMedia();
}
});
// 再生/一時停止ボタン
playPauseBtn.addEventListener('click', function() {
if (isVideoPlaying) {
pauseMedia();
} else {
playMedia();
}
});
// 音量コントロール
volumeSlider.addEventListener('input', function() {
video.volume = this.value;
lastVolume = this.value;
updateVolumeIcon();
});
// 音量ボタン
volumeBtn.addEventListener('click', function() {
if (video.volume > 0) {
lastVolume = video.volume;
video.volume = 0;
volumeSlider.value = 0;
} else {
video.volume = lastVolume;
volumeSlider.value = lastVolume;
}
updateVolumeIcon();
});
// 音量アイコンを更新
function updateVolumeIcon() {
if (video.volume === 0) {
volumeBtn.textContent = '🔇';
} else if (video.volume < 0.5) {
volumeBtn.textContent = '🔈';
} else {
volumeBtn.textContent = '🔊';
}
}
// 再生速度スライダー (動画プレイヤー)
speedSlider.addEventListener('input', function() {
const speed = parseFloat(this.value);
speedValue.textContent = speed.toFixed(2) + 'x';
playbackSpeedSlider.value = speed;
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
updatePlaybackRate(speed);
});
// 再生速度スライダー (設定メニュー)
playbackSpeedSlider.addEventListener('input', function() {
const speed = parseFloat(this.value);
playbackSpeedValue.textContent = speed.toFixed(2) + 'x';
speedSlider.value = speed;
speedValue.textContent = speed.toFixed(2) + 'x';
updatePlaybackRate(speed);
});
// 再生速度を更新 (音の高さを維持)
function updatePlaybackRate(speed) {
video.playbackRate = speed;
// 音声の再生速度も更新 (音の高さは維持)
audioFiles.forEach(file => {
if (audioSources[file]) {
audioSources[file].playbackRate.value = speed;
}
});
}
// 全画面ボタン
fullscreenBtn.addEventListener('click', function() {
if (videoContainer.requestFullscreen) {
videoContainer.requestFullscreen();
} else if (videoContainer.webkitRequestFullscreen) {
videoContainer.webkitRequestFullscreen();
} else if (videoContainer.msRequestFullscreen) {
videoContainer.msRequestFullscreen();
}
});
// 全画面変更イベント
document.addEventListener('fullscreenchange', handleFullscreenChange);
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
document.addEventListener('msfullscreenchange', handleFullscreenChange);
function handleFullscreenChange() {
const isFullscreen = document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
if (isFullscreen) {
// 全画面時の処理
video.controls = false;
} else {
// 通常表示時の処理
video.controls = false;
}
}
// 音声ファイルを読み込む
function loadAudioFiles() {
audioFiles.forEach(file => {
fetch(`${file}.mp3`)
.then(response => response.arrayBuffer())
.then(arrayBuffer => audioContext.decodeAudioData(arrayBuffer))
.then(audioBuffer => {
audioBuffers[file] = audioBuffer;
gainNodes[file] = audioContext.createGain();
gainNodes[file].gain.value = 1;
})
.catch(error => console.error(`Error loading ${file}.mp3:`, error));
});
}
// 音声を再生
function playAudio(file, startTime) {
if (!audioBuffers[file]) return;
const source = audioContext.createBufferSource();
source.buffer = audioBuffers[file];
// ボリュームスライダーの値を取得
const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
const volume = parseFloat(volumeSlider.value) * parseFloat(globalVolumeSlider.value);
// ゲインノードを設定
gainNodes[file].gain.value = volume;
// 接続
source.connect(gainNodes[file]);
gainNodes[file].connect(audioContext.destination);
// 再生
const currentTime = video.currentTime;
const duration = video.duration || videoDuration;
const endTime = parseFloat(endTimeInput.value) || duration;
const loop = loopCheckbox.checked;
// 再生範囲を計算
const playStartTime = Math.max(startTime, parseFloat(startTimeInput.value) || 0);
const playDuration = Math.min(endTime, duration) - playStartTime;
source.start(0, playStartTime, playDuration);
// ループ設定
source.loop = loop;
if (loop) {
const start = parseFloat(startTimeInput.value) || 0;
source.loopStart = start;
source.loopEnd = endTime;
}
// 再生速度を同期 (音の高さは維持)
source.playbackRate.value = video.playbackRate;
audioSources[file] = source;
}
// 再生関数
function playMedia() {
const startTime = shouldPlayFromStartTime ? (parseFloat(startTimeInput.value) || 0) : video.currentTime;
let endTime = parseFloat(endTimeInput.value) || videoDuration;
shouldPlayFromStartTime = false;
// 終了時間が動画の長さを超えないように
endTime = Math.min(endTime, videoDuration);
// 動画を設定
video.currentTime = startTime;
video.muted = true;
// 音声を再生
audioFiles.forEach(file => {
if (audioBuffers[file]) {
// 既存のソースがあれば停止
if (audioSources[file]) {
audioSources[file].stop();
}
playAudio(file, startTime);
}
});
// 動画を再生
video.play();
isPlaying = true;
isVideoPlaying = true;
playPauseBtn.textContent = '⏸';
}
// 一時停止関数
function pauseMedia() {
if (!isPlaying) return;
video.pause();
audioFiles.forEach(file => {
if (audioSources[file]) {
audioSources[file].stop();
audioSources[file] = null;
}
});
isPlaying = false;
isVideoPlaying = false;
playPauseBtn.textContent = '▶';
shouldPlayFromStartTime = true;
}
// 停止関数
function stopMedia() {
pauseMedia();
const startTime = parseFloat(startTimeInput.value) || 0;
video.currentTime = startTime;
updateTimeDisplay();
}
// ボリュームスライダーのイベント
audioSliders.forEach((slider, index) => {
slider.addEventListener('input', function() {
const value = parseFloat(this.value);
volumeValues[index].textContent = value.toFixed(2);
if (isPlaying && audioSources[this.dataset.audio] && gainNodes[this.dataset.audio]) {
const globalVolume = parseFloat(globalVolumeSlider.value);
gainNodes[this.dataset.audio].gain.value = value * globalVolume;
}
});
});
// 全体音量スライダーのイベント
globalVolumeSlider.addEventListener('input', function() {
const value = parseFloat(this.value);
globalVolumeValue.textContent = value.toFixed(1);
if (isPlaying) {
audioFiles.forEach(file => {
if (gainNodes[file]) {
const volumeSlider = document.querySelector(`.audio-slider[data-audio="${file}"]`);
const volume = parseFloat(volumeSlider.value) * value;
gainNodes[file].gain.value = volume;
}
});
}
});
// 初期化
loadAudioFiles();
updateVolumeIcon();
volumeSlider.value = video.volume;
video.controls = false;
});
</script>
</body>
</html>