|
<!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; |
|
} |
|
|
|
.video-controls { |
|
background-color: rgba(0, 0, 0, 0.7); |
|
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; |
|
background-color: rgba(0, 0, 0, 0.8); |
|
padding: 2px 5px; |
|
border-radius: 3px; |
|
font-size: 12px; |
|
display: none; |
|
} |
|
|
|
.controls-row { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
.left-controls, .right-controls { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
} |
|
|
|
.control-button { |
|
background: none; |
|
border: none; |
|
color: #e6f1ff; |
|
font-size: 16px; |
|
cursor: pointer; |
|
padding: 5px; |
|
border-radius: 3px; |
|
transition: all 0.2s; |
|
} |
|
|
|
.control-button:hover { |
|
background-color: rgba(100, 255, 218, 0.2); |
|
} |
|
|
|
.time-display { |
|
font-size: 14px; |
|
color: #e6f1ff; |
|
font-family: monospace; |
|
} |
|
|
|
.volume-control { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
} |
|
|
|
.volume-slider { |
|
width: 80px; |
|
height: 5px; |
|
-webkit-appearance: none; |
|
background: #1e2a47; |
|
border-radius: 5px; |
|
outline: none; |
|
opacity: 0; |
|
transition: opacity 0.2s; |
|
} |
|
|
|
.volume-control:hover .volume-slider { |
|
opacity: 1; |
|
} |
|
|
|
.volume-slider::-webkit-slider-thumb { |
|
-webkit-appearance: none; |
|
width: 12px; |
|
height: 12px; |
|
background: #64ffda; |
|
border-radius: 50%; |
|
cursor: pointer; |
|
} |
|
|
|
.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-volume-slider { |
|
flex-grow: 1; |
|
height: 8px; |
|
-webkit-appearance: none; |
|
background: #1e2a47; |
|
border-radius: 5px; |
|
outline: none; |
|
} |
|
|
|
.audio-volume-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; |
|
} |
|
|
|
input[type="number"], input[type="checkbox"], select { |
|
background-color: #112240; |
|
border: 1px solid #64ffda; |
|
color: #e6f1ff; |
|
padding: 5px; |
|
border-radius: 3px; |
|
} |
|
|
|
.buttons { |
|
display: flex; |
|
gap: 10px; |
|
justify-content: center; |
|
} |
|
|
|
.tech-decoration { |
|
width: 100%; |
|
height: 2px; |
|
background: linear-gradient(90deg, transparent, #64ffda, transparent); |
|
margin: 20px 0; |
|
} |
|
|
|
.fullscreen { |
|
position: fixed; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
background-color: black; |
|
z-index: 1000; |
|
display: flex; |
|
justify-content: center; |
|
align-items: center; |
|
} |
|
|
|
.fullscreen video { |
|
width: 100%; |
|
height: 100%; |
|
max-width: 100%; |
|
max-height: 100%; |
|
} |
|
|
|
.fullscreen .video-container { |
|
width: 100%; |
|
height: 100%; |
|
max-width: 100%; |
|
max-height: 100%; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>音声動画プレイヤー</h1> |
|
|
|
<div class="container"> |
|
<div class="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="controls-row"> |
|
<div class="left-controls"> |
|
<button class="control-button" id="play-pause-btn">▶</button> |
|
<span class="time-display" id="time-display">00:00 / 00:00</span> |
|
</div> |
|
<div class="right-controls"> |
|
<div class="volume-control"> |
|
<button class="control-button" id="volume-btn">🔊</button> |
|
<input type="range" class="volume-slider" id="video-volume" min="0" max="1" step="0.01" value="1"> |
|
</div> |
|
<button class="control-button" id="fullscreen-btn">⛶</button> |
|
</div> |
|
</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"> |
|
<label for="playback-rate">再生速度:</label> |
|
<select id="playback-rate"> |
|
<option value="0.25">0.25x</option> |
|
<option value="0.5">0.5x</option> |
|
<option value="0.75">0.75x</option> |
|
<option value="1" selected>1x</option> |
|
<option value="1.25">1.25x</option> |
|
<option value="1.5">1.5x</option> |
|
<option value="2">2x</option> |
|
<option value="3">3x</option> |
|
</select> |
|
</div> |
|
<div class="setting-item"> |
|
<label for="global-volume">全体音量係数:</label> |
|
<input type="range" id="global-volume" min="0" max="3" step="0.1" value="1"> |
|
<span id="global-volume-value">1</span> |
|
</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-volume-slider" data-audio="p" min="0" max="1" step="0.01" value="1"> |
|
<span class="volume-value">1</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>a.mp3</label> |
|
<input type="range" class="audio-volume-slider" data-audio="a" min="0" max="1" step="0.01" value="1"> |
|
<span class="volume-value">1</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>t.mp3</label> |
|
<input type="range" class="audio-volume-slider" data-audio="t" min="0" max="1" step="0.01" value="1"> |
|
<span class="volume-value">1</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>s.mp3</label> |
|
<input type="range" class="audio-volume-slider" data-audio="s" min="0" max="1" step="0.01" value="1"> |
|
<span class="volume-value">1</span> |
|
</div> |
|
<div class="audio-item"> |
|
<label>k.mp3</label> |
|
<input type="range" class="audio-volume-slider" data-audio="k" min="0" max="1" step="0.01" value="1"> |
|
<span class="volume-value">1</span> |
|
</div> |
|
</div> |
|
|
|
<div class="tech-decoration"></div> |
|
|
|
<div class="buttons"> |
|
<button id="play-btn">再生</button> |
|
<button id="pause-btn">一時停止</button> |
|
<button id="stop-btn">停止</button> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const video = document.getElementById('video'); |
|
const playPauseBtn = document.getElementById('play-pause-btn'); |
|
const fullscreenBtn = document.getElementById('fullscreen-btn'); |
|
const progressContainer = document.getElementById('progress-container'); |
|
const progressBar = document.getElementById('progress-bar'); |
|
const progressTime = document.getElementById('progress-time'); |
|
const timeDisplay = document.getElementById('time-display'); |
|
const volumeBtn = document.getElementById('volume-btn'); |
|
const videoVolumeSlider = document.getElementById('video-volume'); |
|
|
|
const playBtn = document.getElementById('play-btn'); |
|
const pauseBtn = document.getElementById('pause-btn'); |
|
const stopBtn = document.getElementById('stop-btn'); |
|
const startTimeInput = document.getElementById('start-time'); |
|
const endTimeInput = document.getElementById('end-time'); |
|
const loopCheckbox = document.getElementById('loop'); |
|
const playbackRateSelect = document.getElementById('playback-rate'); |
|
const globalVolumeSlider = document.getElementById('global-volume'); |
|
const globalVolumeValue = document.getElementById('global-volume-value'); |
|
const volumeSliders = document.querySelectorAll('.audio-volume-slider'); |
|
const volumeValues = document.querySelectorAll('.volume-value'); |
|
|
|
|
|
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
|
const audioBuffers = {}; |
|
const audioSources = {}; |
|
const gainNodes = {}; |
|
|
|
|
|
const audioFiles = ['p', 'a', 't', 's', 'k']; |
|
|
|
|
|
let videoDuration = 0; |
|
let isPlaying = false; |
|
let isFullscreen = false; |
|
let lastVolume = 1; |
|
|
|
|
|
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; |
|
|
|
timeDisplay.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`; |
|
progressBar.style.width = `${(currentTime / duration) * 100}%`; |
|
} |
|
|
|
|
|
function formatTime(seconds) { |
|
const mins = Math.floor(seconds / 60); |
|
const secs = Math.floor(seconds % 60); |
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; |
|
} |
|
|
|
|
|
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 playMedia() { |
|
if (isPlaying) return; |
|
|
|
const startTime = parseFloat(startTimeInput.value) || 0; |
|
let endTime = parseFloat(endTimeInput.value) || videoDuration; |
|
const loop = loopCheckbox.checked; |
|
const playbackRate = parseFloat(playbackRateSelect.value); |
|
const globalVolume = parseFloat(globalVolumeSlider.value); |
|
|
|
|
|
endTime = Math.min(endTime, videoDuration); |
|
|
|
|
|
video.currentTime = startTime; |
|
video.playbackRate = playbackRate; |
|
video.muted = true; |
|
|
|
|
|
audioFiles.forEach(file => { |
|
if (audioBuffers[file]) { |
|
|
|
if (audioSources[file]) { |
|
audioSources[file].stop(); |
|
} |
|
|
|
const source = audioContext.createBufferSource(); |
|
source.buffer = audioBuffers[file]; |
|
|
|
|
|
const volumeSlider = document.querySelector(`.audio-volume-slider[data-audio="${file}"]`); |
|
const volume = parseFloat(volumeSlider.value) * globalVolume; |
|
|
|
|
|
gainNodes[file].gain.value = volume; |
|
|
|
|
|
source.connect(gainNodes[file]); |
|
gainNodes[file].connect(audioContext.destination); |
|
|
|
|
|
source.start(0, startTime, endTime - startTime); |
|
|
|
|
|
source.loop = loop; |
|
if (loop) { |
|
source.loopStart = startTime; |
|
source.loopEnd = endTime; |
|
} |
|
|
|
audioSources[file] = source; |
|
} |
|
}); |
|
|
|
|
|
video.play(); |
|
isPlaying = true; |
|
playPauseBtn.textContent = '⏸'; |
|
|
|
|
|
video.ontimeupdate = function() { |
|
updateTimeDisplay(); |
|
|
|
if (video.currentTime >= endTime) { |
|
if (!loop) { |
|
stopMedia(); |
|
} else { |
|
video.currentTime = startTime; |
|
} |
|
} |
|
}; |
|
} |
|
|
|
|
|
function pauseMedia() { |
|
if (!isPlaying) return; |
|
|
|
video.pause(); |
|
audioFiles.forEach(file => { |
|
if (audioSources[file]) { |
|
audioSources[file].stop(); |
|
audioSources[file] = null; |
|
} |
|
}); |
|
|
|
isPlaying = false; |
|
playPauseBtn.textContent = '▶'; |
|
} |
|
|
|
|
|
function stopMedia() { |
|
pauseMedia(); |
|
video.currentTime = parseFloat(startTimeInput.value) || 0; |
|
updateTimeDisplay(); |
|
} |
|
|
|
|
|
playPauseBtn.addEventListener('click', function() { |
|
if (isPlaying) { |
|
pauseMedia(); |
|
} else { |
|
playMedia(); |
|
} |
|
}); |
|
|
|
|
|
progressContainer.addEventListener('click', function(e) { |
|
if (!videoDuration) return; |
|
|
|
const rect = this.getBoundingClientRect(); |
|
const pos = (e.clientX - rect.left) / rect.width; |
|
const seekTime = pos * videoDuration; |
|
|
|
video.currentTime = seekTime; |
|
|
|
if (!isPlaying) { |
|
updateTimeDisplay(); |
|
} |
|
}); |
|
|
|
|
|
progressContainer.addEventListener('mousemove', function(e) { |
|
if (!videoDuration) return; |
|
|
|
const rect = this.getBoundingClientRect(); |
|
const pos = (e.clientX - rect.left) / rect.width; |
|
const seekTime = pos * videoDuration; |
|
|
|
progressTime.textContent = formatTime(seekTime); |
|
progressTime.style.left = `${e.clientX - rect.left}px`; |
|
progressTime.style.display = 'block'; |
|
}); |
|
|
|
progressContainer.addEventListener('mouseout', function() { |
|
progressTime.style.display = 'none'; |
|
}); |
|
|
|
|
|
volumeBtn.addEventListener('click', function() { |
|
if (video.volume > 0) { |
|
lastVolume = video.volume; |
|
video.volume = 0; |
|
videoVolumeSlider.value = 0; |
|
volumeBtn.textContent = '🔇'; |
|
} else { |
|
video.volume = lastVolume; |
|
videoVolumeSlider.value = lastVolume; |
|
volumeBtn.textContent = lastVolume > 0.5 ? '🔊' : '🔉'; |
|
} |
|
}); |
|
|
|
videoVolumeSlider.addEventListener('input', function() { |
|
const volume = parseFloat(this.value); |
|
video.volume = volume; |
|
lastVolume = volume; |
|
|
|
if (volume === 0) { |
|
volumeBtn.textContent = '🔇'; |
|
} else if (volume > 0.5) { |
|
volumeBtn.textContent = '🔊'; |
|
} else { |
|
volumeBtn.textContent = '🔉'; |
|
} |
|
}); |
|
|
|
|
|
fullscreenBtn.addEventListener('click', function() { |
|
if (!isFullscreen) { |
|
enterFullscreen(); |
|
} else { |
|
exitFullscreen(); |
|
} |
|
}); |
|
|
|
function enterFullscreen() { |
|
const elem = video.parentElement; |
|
|
|
if (elem.requestFullscreen) { |
|
elem.requestFullscreen(); |
|
} else if (elem.webkitRequestFullscreen) { |
|
elem.webkitRequestFullscreen(); |
|
} else if (elem.msRequestFullscreen) { |
|
elem.msRequestFullscreen(); |
|
} |
|
|
|
isFullscreen = true; |
|
} |
|
|
|
function exitFullscreen() { |
|
if (document.exitFullscreen) { |
|
document.exitFullscreen(); |
|
} else if (document.webkitExitFullscreen) { |
|
document.webkitExitFullscreen(); |
|
} else if (document.msExitFullscreen) { |
|
document.msExitFullscreen(); |
|
} |
|
|
|
isFullscreen = false; |
|
} |
|
|
|
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange); |
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange); |
|
document.addEventListener('msfullscreenchange', handleFullscreenChange); |
|
|
|
function handleFullscreenChange() { |
|
isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement); |
|
} |
|
|
|
|
|
volumeSliders.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-volume-slider[data-audio="${file}"]`); |
|
const volume = parseFloat(volumeSlider.value) * value; |
|
gainNodes[file].gain.value = volume; |
|
} |
|
}); |
|
} |
|
}); |
|
|
|
|
|
playBtn.addEventListener('click', playMedia); |
|
pauseBtn.addEventListener('click', pauseMedia); |
|
stopBtn.addEventListener('click', stopMedia); |
|
|
|
|
|
loadAudioFiles(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |