feat: init

This commit is contained in:
yanghao05
2025-06-06 16:59:09 +08:00
parent d3dd322649
commit b0d5bceb09
40 changed files with 11149 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
*.mkv
*.mp4
*.mp3
*.aac
http_demo
!assets/*.mp3

BIN
assets/audio01.mp3 Normal file

Binary file not shown.

BIN
assets/audio02.mp3 Normal file

Binary file not shown.

BIN
assets/cover.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

198
assets/info.json Normal file
View File

@@ -0,0 +1,198 @@
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"film_grain": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "27:32",
"display_aspect_ratio": "3:2",
"pix_fmt": "yuvj420p",
"level": 40,
"color_range": "pc",
"color_space": "bt470bg",
"chroma_location": "center",
"field_order": "progressive",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"id": "0x1",
"r_frame_rate": "25/1",
"avg_frame_rate": "25/1",
"time_base": "1/12800",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 1721856,
"duration": "134.520000",
"bit_rate": "728434",
"bits_per_raw_sample": "8",
"nb_frames": "3363",
"extradata_size": 55,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0,
"multilayer": 0
},
"tags": {
"language": "und",
"handler_name": "VideoHandler",
"vendor_id": "[0][0][0][0]",
"encoder": "Lavc61.19.101 libx264"
},
"side_data_list": [
{
"side_data_type": "ICC Profile"
}
]
},
{
"index": 1,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": "44100",
"channels": 2,
"channel_layout": "stereo",
"bits_per_sample": 0,
"initial_padding": 0,
"id": "0x2",
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/44100",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 5833724,
"duration": "132.283991",
"bit_rate": "130883",
"nb_frames": "5698",
"extradata_size": 5,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0,
"multilayer": 0
},
"tags": {
"language": "und",
"handler_name": "SoundHandler",
"vendor_id": "[0][0][0][0]"
}
},
{
"index": 2,
"codec_name": "aac",
"codec_long_name": "AAC (Advanced Audio Coding)",
"profile": "LC",
"codec_type": "audio",
"codec_tag_string": "mp4a",
"codec_tag": "0x6134706d",
"sample_fmt": "fltp",
"sample_rate": "44100",
"channels": 2,
"channel_layout": "stereo",
"bits_per_sample": 0,
"initial_padding": 0,
"id": "0x3",
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/44100",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 5833724,
"duration": "132.283991",
"bit_rate": "132605",
"nb_frames": "5698",
"extradata_size": 5,
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0,
"multilayer": 0
},
"tags": {
"language": "und",
"handler_name": "SoundHandler",
"vendor_id": "[0][0][0][0]"
}
}
],
"format": {
"filename": "dual-tracks.mp4",
"nb_streams": 3,
"nb_programs": 0,
"nb_stream_groups": 0,
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"format_long_name": "QuickTime / MOV",
"start_time": "0.000000",
"duration": "134.520000",
"size": "16756214",
"bit_rate": "996503",
"probe_score": 100,
"tags": {
"major_brand": "isom",
"minor_version": "512",
"compatible_brands": "isomiso2avc1mp41",
"encoder": "Lavf61.7.100"
}
}
}

26
assets/run.sh Normal file
View File

@@ -0,0 +1,26 @@
# # 1. 生成视频
# -vf "scale=iw:ih:force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2" \
ffmpeg -loop 1 -i cover.jpg \
-vf "scale=1280:720:force_original_aspect_ratio=decrease" \
-r 1 \
-t 60 \
-c:v libx264 \
-pix_fmt yuv420p \
-y video.mp4
# 同时添加两条音轨
ffmpeg -i video.mp4 \
-i audio01.mp3 \
-i audio02.mp3 \
-map 0:v \
-map 1:a \
-map 2:a \
-c:v copy \
-c:a aac \
-shortest \
-metadata:s:a:0 title="Default" \
-metadata:s:a:0 language=chi \
-metadata:s:a:1 title="Second" \
-metadata:s:a:1 language=eng \
-disposition:a:0 default \
-y final.mp4

36
gen.sh Normal file
View File

@@ -0,0 +1,36 @@
rm -rf hls
mkdir -p hls/video
ffmpeg -i assets/final.mp4 \
-map 0:v:0 -map 0:a:0 \
-c:v libx264 -c:a aac \
-vf "fps=25" \
-f hls \
-hls_time 10 \
-hls_list_size 0 \
-hls_flags independent_segments \
-hls_segment_type mpegts \
-hls_segment_filename hls/video/%03d.ts \
hls/video/index.m3u8
mkdir -p hls/audio
ffmpeg -i assets/final.mp4 \
-map 0:a:1 \
-c:a aac -vn \
-f hls \
-hls_time 10 \
-hls_list_size 0 \
-hls_flags independent_segments \
-hls_segment_type mpegts \
-hls_segment_filename hls/audio/%03d.ts \
hls/audio/index.m3u8
cat >hls/variant.m3u8 <<EOF
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="zh",NAME="主音轨",AUTOSELECT=YES,DEFAULT=YES,URI="video/index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="zh",NAME="第二音轨",AUTOSELECT=NO,DEFAULT=NO,URI="audio/index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=800000,CODECS="avc1.64001f,mp4a.40.2",AUDIO="audio"
video/index.m3u8
EOF

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module http_demo
go 1.24.1

BIN
hls/audio/000.ts Normal file

Binary file not shown.

BIN
hls/audio/001.ts Normal file

Binary file not shown.

BIN
hls/audio/002.ts Normal file

Binary file not shown.

BIN
hls/audio/003.ts Normal file

Binary file not shown.

BIN
hls/audio/004.ts Normal file

Binary file not shown.

BIN
hls/audio/005.ts Normal file

Binary file not shown.

17
hls/audio/index.m3u8 Normal file
View File

@@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.007800,
000.ts
#EXTINF:10.007800,
001.ts
#EXTINF:9.984589,
002.ts
#EXTINF:10.007800,
003.ts
#EXTINF:10.007800,
004.ts
#EXTINF:9.985556,
005.ts
#EXT-X-ENDLIST

7
hls/variant.m3u8 Normal file
View File

@@ -0,0 +1,7 @@
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="zh",NAME="主音轨",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="zh",NAME="第二音轨",AUTOSELECT=NO,DEFAULT=NO,URI="audio/index.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=800000,CODECS="avc1.64001f,mp4a.40.2",AUDIO="audio"
video/index.m3u8

BIN
hls/video/000.ts Normal file

Binary file not shown.

BIN
hls/video/001.ts Normal file

Binary file not shown.

BIN
hls/video/002.ts Normal file

Binary file not shown.

BIN
hls/video/003.ts Normal file

Binary file not shown.

BIN
hls/video/004.ts Normal file

Binary file not shown.

BIN
hls/video/005.ts Normal file

Binary file not shown.

18
hls/video/index.m3u8 Normal file
View File

@@ -0,0 +1,18 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:10.000000,
000.ts
#EXTINF:10.000000,
001.ts
#EXTINF:10.000000,
002.ts
#EXTINF:10.000000,
003.ts
#EXTINF:10.000000,
004.ts
#EXTINF:10.000000,
005.ts
#EXT-X-ENDLIST

1201
index.html Normal file

File diff suppressed because it is too large Load Diff

565
index.html.v1 Normal file
View File

@@ -0,0 +1,565 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#trackSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" controls crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
<span>音轨:</span>
<select id="trackSelect" disabled></select>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackSelect = document.getElementById('trackSelect');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
const VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 连接音频节点链:视频 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
trackSelect.disabled = true;
trackSelect.innerHTML = '';
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
trackSelect.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
trackSelect.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const option = document.createElement('option');
option.value = i;
option.text = track.label || track.language || `音轨 ${i + 1}`;
option.selected = track.enabled;
trackSelect.appendChild(option);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
}
// 音轨切换事件
trackSelect.addEventListener('change', (e) => {
const selectedIndex = parseInt(e.target.value);
console.log('切换到音轨:', selectedIndex);
// 禁用所有音轨
for (let i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = false;
}
// 启用选中的音轨
if (audioTracks[selectedIndex]) {
audioTracks[selectedIndex].enabled = true;
console.log('已启用音轨:', selectedIndex);
}
});
}
// 如果没有多个音轨,显示默认选项
else {
const option = document.createElement('option');
option.value = 0;
option.text = '默认音轨';
trackSelect.appendChild(option);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

1022
index.html.v10 Normal file

File diff suppressed because it is too large Load Diff

1098
index.html.v11 Normal file

File diff suppressed because it is too large Load Diff

603
index.html.v2 Normal file
View File

@@ -0,0 +1,603 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#trackSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
#playbackRateSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 100px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
<span>音轨:</span>
<select id="trackSelect" disabled></select>
<span>播放速度:</span>
<select id="playbackRateSelect">
<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>
</select>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackSelect = document.getElementById('trackSelect');
const playbackRateSelect = document.getElementById('playbackRateSelect');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
const VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 连接音频节点链:视频 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
trackSelect.disabled = true;
trackSelect.innerHTML = '';
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
trackSelect.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
trackSelect.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const option = document.createElement('option');
option.value = i;
option.text = track.label || track.language || `音轨 ${i + 1}`;
option.selected = track.enabled;
trackSelect.appendChild(option);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
}
// 音轨切换事件
trackSelect.addEventListener('change', (e) => {
const selectedIndex = parseInt(e.target.value);
console.log('切换到音轨:', selectedIndex);
// 禁用所有音轨
for (let i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = false;
}
// 启用选中的音轨
if (audioTracks[selectedIndex]) {
audioTracks[selectedIndex].enabled = true;
console.log('已启用音轨:', selectedIndex);
}
});
}
// 如果没有多个音轨,显示默认选项
else {
const option = document.createElement('option');
option.value = 0;
option.text = '默认音轨';
trackSelect.appendChild(option);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
playbackRateSelect.addEventListener('change', (e) => {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
statusText.textContent = `正在播放 (${rateText})`;
statusText.style.color = '#4CAF50';
}
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新状态显示,包含当前播放速度
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

705
index.html.v3 Normal file
View File

@@ -0,0 +1,705 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#trackSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
#playbackRateSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 100px;
}
#pitchShiftSelect {
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 120px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
<span>音轨:</span>
<select id="trackSelect" disabled></select>
<span>播放速度:</span>
<select id="playbackRateSelect">
<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>
</select>
<span>音调:</span>
<select id="pitchShiftSelect">
<option value="-12">降一个八度</option>
<option value="-7">降五度</option>
<option value="-5">降四度</option>
<option value="-3">降小三度</option>
<option value="-2">降大二度 (C→B♭)</option>
<option value="-1">降小二度 (C→B)</option>
<option value="0" selected>原调</option>
<option value="1">升小二度 (C→C#)</option>
<option value="2">升大二度 (C→D)</option>
<option value="3">升小三度 (C→D#)</option>
<option value="4">升大三度 (C→E)</option>
<option value="5">升四度</option>
<option value="7">升五度</option>
<option value="12">升一个八度</option>
</select>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackSelect = document.getElementById('trackSelect');
const playbackRateSelect = document.getElementById('playbackRateSelect');
const pitchShiftSelect = document.getElementById('pitchShiftSelect');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
const VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let pitchShifterNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 创建音调变换处理器
function createPitchShifter(audioContext) {
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
let phase = 0;
let lastInputSample = 0;
scriptProcessor.onaudioprocess = function (event) {
const inputBuffer = event.inputBuffer;
const outputBuffer = event.outputBuffer;
const inputData = inputBuffer.getChannelData(0);
const outputData = outputBuffer.getChannelData(0);
const pitchRatio = Math.pow(2, currentPitchShift / 12);
for (let i = 0; i < inputData.length; i++) {
if (currentPitchShift === 0) {
// 无音调变换,直接复制
outputData[i] = inputData[i];
} else {
// 简单的音调变换实现
// 这是一个基础实现,真实环境中可能需要更复杂的算法
const sampleIndex = i * pitchRatio;
const baseIndex = Math.floor(sampleIndex);
const fraction = sampleIndex - baseIndex;
if (baseIndex < inputData.length - 1) {
// 线性插值
outputData[i] = inputData[baseIndex] * (1 - fraction) +
inputData[baseIndex + 1] * fraction;
} else {
outputData[i] = lastInputSample;
}
}
}
if (inputData.length > 0) {
lastInputSample = inputData[inputData.length - 1];
}
};
return scriptProcessor;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 创建音调变换节点
pitchShifterNode = createPitchShifter(audioContext);
console.log('音调变换节点已创建');
// 连接音频节点链:视频 -> 音调变换 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(pitchShifterNode);
pitchShifterNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(包含音调变换)');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
trackSelect.disabled = true;
trackSelect.innerHTML = '';
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
trackSelect.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
trackSelect.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const option = document.createElement('option');
option.value = i;
option.text = track.label || track.language || `音轨 ${i + 1}`;
option.selected = track.enabled;
trackSelect.appendChild(option);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
}
// 音轨切换事件
trackSelect.addEventListener('change', (e) => {
const selectedIndex = parseInt(e.target.value);
console.log('切换到音轨:', selectedIndex);
// 禁用所有音轨
for (let i = 0; i < audioTracks.length; i++) {
audioTracks[i].enabled = false;
}
// 启用选中的音轨
if (audioTracks[selectedIndex]) {
audioTracks[selectedIndex].enabled = true;
console.log('已启用音轨:', selectedIndex);
}
});
}
// 如果没有多个音轨,显示默认选项
else {
const option = document.createElement('option');
option.value = 0;
option.text = '默认音轨';
trackSelect.appendChild(option);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
playbackRateSelect.addEventListener('change', (e) => {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
const currentPitchShift = parseInt(pitchShiftSelect.value);
const pitchText = currentPitchShift === 0 ? '原调' :
(currentPitchShift > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(currentPitchShift)}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
});
// 音调变换控制
pitchShiftSelect.addEventListener('change', (e) => {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新状态显示,包含当前播放速度和音调
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = parseInt(pitchShiftSelect.value);
const pitchText = currentPitchShift === 0 ? '原调' :
(currentPitchShift > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(currentPitchShift)}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

824
index.html.v4 Normal file
View File

@@ -0,0 +1,824 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.control-group h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
margin: 0;
}
.radio-item label {
font-size: 13px;
cursor: pointer;
user-select: none;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
<div class="control-group">
<h4>音轨选择</h4>
<div class="radio-group" id="trackRadioGroup">
<div class="radio-item">
<input type="radio" id="track0" name="audioTrack" value="0" checked>
<label for="track0">默认音轨</label>
</div>
</div>
</div>
<div class="control-group">
<h4>播放速度</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="rate05" name="playbackRate" value="0.5">
<label for="rate05">0.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate075" name="playbackRate" value="0.75">
<label for="rate075">0.75x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate1" name="playbackRate" value="1" checked>
<label for="rate1">1x (正常)</label>
</div>
<div class="radio-item">
<input type="radio" id="rate125" name="playbackRate" value="1.25">
<label for="rate125">1.25x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate15" name="playbackRate" value="1.5">
<label for="rate15">1.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate2" name="playbackRate" value="2">
<label for="rate2">2x</label>
</div>
</div>
</div>
<div class="control-group">
<h4>音调变换</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="pitch-12" name="pitchShift" value="-12">
<label for="pitch-12">降一个八度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-7" name="pitchShift" value="-7">
<label for="pitch-7">降五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-5" name="pitchShift" value="-5">
<label for="pitch-5">降四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-3" name="pitchShift" value="-3">
<label for="pitch-3">降小三度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-2" name="pitchShift" value="-2">
<label for="pitch-2">降大二度 (C→B♭)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-1" name="pitchShift" value="-1">
<label for="pitch-1">降小二度 (C→B)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch0" name="pitchShift" value="0" checked>
<label for="pitch0">原调</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch1" name="pitchShift" value="1">
<label for="pitch1">升小二度 (C→C#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch2" name="pitchShift" value="2">
<label for="pitch2">升大二度 (C→D)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch3" name="pitchShift" value="3">
<label for="pitch3">升小三度 (C→D#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch4" name="pitchShift" value="4">
<label for="pitch4">升大三度 (C→E)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch5" name="pitchShift" value="5">
<label for="pitch5">升四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch7" name="pitchShift" value="7">
<label for="pitch7">升五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch12" name="pitchShift" value="12">
<label for="pitch12">升一个八度</label>
</div>
</div>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackRadioGroup = document.getElementById('trackRadioGroup');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
const VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let pitchShifterNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 创建音调变换处理器
function createPitchShifter(audioContext) {
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
let phase = 0;
let lastInputSample = 0;
scriptProcessor.onaudioprocess = function (event) {
const inputBuffer = event.inputBuffer;
const outputBuffer = event.outputBuffer;
const inputData = inputBuffer.getChannelData(0);
const outputData = outputBuffer.getChannelData(0);
const pitchRatio = Math.pow(2, currentPitchShift / 12);
for (let i = 0; i < inputData.length; i++) {
if (currentPitchShift === 0) {
// 无音调变换,直接复制
outputData[i] = inputData[i];
} else {
// 简单的音调变换实现
// 这是一个基础实现,真实环境中可能需要更复杂的算法
const sampleIndex = i * pitchRatio;
const baseIndex = Math.floor(sampleIndex);
const fraction = sampleIndex - baseIndex;
if (baseIndex < inputData.length - 1) {
// 线性插值
outputData[i] = inputData[baseIndex] * (1 - fraction) +
inputData[baseIndex + 1] * fraction;
} else {
outputData[i] = lastInputSample;
}
}
}
if (inputData.length > 0) {
lastInputSample = inputData[inputData.length - 1];
}
};
return scriptProcessor;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 创建音调变换节点
pitchShifterNode = createPitchShifter(audioContext);
console.log('音调变换节点已创建');
// 连接音频节点链:视频 -> 音调变换 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(pitchShifterNode);
pitchShifterNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(包含音调变换)');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
// 清空现有的音轨选项
trackRadioGroup.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = `track${i}`;
radio.value = i;
radio.checked = track.enabled;
const label = document.createElement('label');
label.htmlFor = `track${i}`;
label.textContent = track.label || track.language || `音轨 ${i + 1}`;
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
// 添加音轨切换事件
radio.addEventListener('change', () => {
if (radio.checked) {
console.log('切换到音轨:', i);
// 禁用所有音轨
for (let j = 0; j < audioTracks.length; j++) {
audioTracks[j].enabled = false;
}
// 启用选中的音轨
audioTracks[i].enabled = true;
console.log('已启用音轨:', i);
}
});
}
}
// 如果没有多个音轨,显示默认选项
else {
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = 'track0';
radio.value = '0';
radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'track0';
label.textContent = '默认音轨';
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
document.querySelectorAll('input[name="playbackRate"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
}
});
});
// 音调变换控制
document.querySelectorAll('input[name="pitchShift"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
}
});
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新状态显示,包含当前播放速度和音调
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

849
index.html.v5 Normal file
View File

@@ -0,0 +1,849 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.control-group h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
margin: 0;
}
.radio-item label {
font-size: 13px;
cursor: pointer;
user-select: none;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
<div class="control-group">
<h4>音轨选择</h4>
<div class="radio-group" id="trackRadioGroup">
<div class="radio-item">
<input type="radio" id="track0" name="audioTrack" value="0" checked>
<label for="track0">默认音轨</label>
</div>
</div>
</div>
<div class="control-group">
<h4>播放速度</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="rate05" name="playbackRate" value="0.5">
<label for="rate05">0.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate075" name="playbackRate" value="0.75">
<label for="rate075">0.75x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate1" name="playbackRate" value="1" checked>
<label for="rate1">1x (正常)</label>
</div>
<div class="radio-item">
<input type="radio" id="rate125" name="playbackRate" value="1.25">
<label for="rate125">1.25x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate15" name="playbackRate" value="1.5">
<label for="rate15">1.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate2" name="playbackRate" value="2">
<label for="rate2">2x</label>
</div>
</div>
</div>
<div class="control-group">
<h4>音调变换</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="pitch-12" name="pitchShift" value="-12">
<label for="pitch-12">降一个八度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-7" name="pitchShift" value="-7">
<label for="pitch-7">降五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-5" name="pitchShift" value="-5">
<label for="pitch-5">降四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-3" name="pitchShift" value="-3">
<label for="pitch-3">降小三度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-2" name="pitchShift" value="-2">
<label for="pitch-2">降大二度 (C→B♭)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-1" name="pitchShift" value="-1">
<label for="pitch-1">降小二度 (C→B)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch0" name="pitchShift" value="0" checked>
<label for="pitch0">原调</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch1" name="pitchShift" value="1">
<label for="pitch1">升小二度 (C→C#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch2" name="pitchShift" value="2">
<label for="pitch2">升大二度 (C→D)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch3" name="pitchShift" value="3">
<label for="pitch3">升小三度 (C→D#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch4" name="pitchShift" value="4">
<label for="pitch4">升大三度 (C→E)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch5" name="pitchShift" value="5">
<label for="pitch5">升四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch7" name="pitchShift" value="7">
<label for="pitch7">升五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch12" name="pitchShift" value="12">
<label for="pitch12">升一个八度</label>
</div>
</div>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackRadioGroup = document.getElementById('trackRadioGroup');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
const VIDEO_URL = 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4';
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let pitchShifterNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 创建音调变换处理器
function createPitchShifter(audioContext) {
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
let phase = 0;
let lastInputSample = 0;
scriptProcessor.onaudioprocess = function (event) {
const inputBuffer = event.inputBuffer;
const outputBuffer = event.outputBuffer;
const inputData = inputBuffer.getChannelData(0);
const outputData = outputBuffer.getChannelData(0);
const pitchRatio = Math.pow(2, currentPitchShift / 12);
for (let i = 0; i < inputData.length; i++) {
if (currentPitchShift === 0) {
// 无音调变换,直接复制
outputData[i] = inputData[i];
} else {
// 简单的音调变换实现
// 这是一个基础实现,真实环境中可能需要更复杂的算法
const sampleIndex = i * pitchRatio;
const baseIndex = Math.floor(sampleIndex);
const fraction = sampleIndex - baseIndex;
if (baseIndex < inputData.length - 1) {
// 线性插值
outputData[i] = inputData[baseIndex] * (1 - fraction) +
inputData[baseIndex + 1] * fraction;
} else {
outputData[i] = lastInputSample;
}
}
}
if (inputData.length > 0) {
lastInputSample = inputData[inputData.length - 1];
}
};
return scriptProcessor;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 创建音调变换节点
pitchShifterNode = createPitchShifter(audioContext);
console.log('音调变换节点已创建');
// 连接音频节点链:视频 -> 音调变换 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(pitchShifterNode);
pitchShifterNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(包含音调变换)');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
// 清空现有的音轨选项
trackRadioGroup.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = `track${i}`;
radio.value = i;
radio.checked = track.enabled;
const label = document.createElement('label');
label.htmlFor = `track${i}`;
label.textContent = track.label || track.language || `音轨 ${i + 1}`;
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
// 添加音轨切换事件
radio.addEventListener('change', () => {
if (radio.checked) {
console.log('切换到音轨:', i);
// 禁用所有音轨
for (let j = 0; j < audioTracks.length; j++) {
audioTracks[j].enabled = false;
}
// 启用选中的音轨
audioTracks[i].enabled = true;
console.log('已启用音轨:', i);
}
});
}
}
// 如果没有多个音轨,显示默认选项
else {
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = 'track0';
radio.value = '0';
radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'track0';
label.textContent = '默认音轨';
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
document.querySelectorAll('input[name="playbackRate"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
}
});
});
// 音调变换控制
document.querySelectorAll('input[name="pitchShift"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
}
});
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
// 更新状态显示,包含当前播放速度和音调
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
// 重置按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

853
index.html.v6 Normal file
View File

@@ -0,0 +1,853 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<script>
// const VIDEO_URL = 'http://localhost:8000/demo01.mp4';
// const VIDEO_URL = 'http://localhost:8000/demo02.mp4';
const VIDEO_URL = 'http://localhost:8000/demo03.mp4';
</script>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.control-group h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
margin: 0;
}
.radio-item label {
font-size: 13px;
cursor: pointer;
user-select: none;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
<div class="control-group">
<h4>音轨选择</h4>
<div class="radio-group" id="trackRadioGroup">
<div class="radio-item">
<input type="radio" id="track0" name="audioTrack" value="0" checked>
<label for="track0">默认音轨</label>
</div>
</div>
</div>
<div class="control-group">
<h4>播放速度</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="rate05" name="playbackRate" value="0.5">
<label for="rate05">0.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate075" name="playbackRate" value="0.75">
<label for="rate075">0.75x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate1" name="playbackRate" value="1" checked>
<label for="rate1">1x (正常)</label>
</div>
<div class="radio-item">
<input type="radio" id="rate125" name="playbackRate" value="1.25">
<label for="rate125">1.25x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate15" name="playbackRate" value="1.5">
<label for="rate15">1.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate2" name="playbackRate" value="2">
<label for="rate2">2x</label>
</div>
</div>
</div>
<div class="control-group">
<h4>音调变换</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="pitch-12" name="pitchShift" value="-12">
<label for="pitch-12">降一个八度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-7" name="pitchShift" value="-7">
<label for="pitch-7">降五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-5" name="pitchShift" value="-5">
<label for="pitch-5">降四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-3" name="pitchShift" value="-3">
<label for="pitch-3">降小三度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-2" name="pitchShift" value="-2">
<label for="pitch-2">降大二度 (C→B♭)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-1" name="pitchShift" value="-1">
<label for="pitch-1">降小二度 (C→B)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch0" name="pitchShift" value="0" checked>
<label for="pitch0">原调</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch1" name="pitchShift" value="1">
<label for="pitch1">升小二度 (C→C#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch2" name="pitchShift" value="2">
<label for="pitch2">升大二度 (C→D)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch3" name="pitchShift" value="3">
<label for="pitch3">升小三度 (C→D#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch4" name="pitchShift" value="4">
<label for="pitch4">升大三度 (C→E)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch5" name="pitchShift" value="5">
<label for="pitch5">升四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch7" name="pitchShift" value="7">
<label for="pitch7">升五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch12" name="pitchShift" value="12">
<label for="pitch12">升一个八度</label>
</div>
</div>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackRadioGroup = document.getElementById('trackRadioGroup');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let pitchShifterNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 创建音调变换处理器
function createPitchShifter(audioContext) {
const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1);
let phase = 0;
let lastInputSample = 0;
scriptProcessor.onaudioprocess = function (event) {
const inputBuffer = event.inputBuffer;
const outputBuffer = event.outputBuffer;
const inputData = inputBuffer.getChannelData(0);
const outputData = outputBuffer.getChannelData(0);
const pitchRatio = Math.pow(2, currentPitchShift / 12);
for (let i = 0; i < inputData.length; i++) {
if (currentPitchShift === 0) {
// 无音调变换,直接复制
outputData[i] = inputData[i];
} else {
// 简单的音调变换实现
// 这是一个基础实现,真实环境中可能需要更复杂的算法
const sampleIndex = i * pitchRatio;
const baseIndex = Math.floor(sampleIndex);
const fraction = sampleIndex - baseIndex;
if (baseIndex < inputData.length - 1) {
// 线性插值
outputData[i] = inputData[baseIndex] * (1 - fraction) +
inputData[baseIndex + 1] * fraction;
} else {
outputData[i] = lastInputSample;
}
}
}
if (inputData.length > 0) {
lastInputSample = inputData[inputData.length - 1];
}
};
return scriptProcessor;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize);
// 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 创建音调变换节点
pitchShifterNode = createPitchShifter(audioContext);
console.log('音调变换节点已创建');
// 连接音频节点链:视频 -> 音调变换 -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(pitchShifterNode);
pitchShifterNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(包含音调变换)');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
// 清空现有的音轨选项
trackRadioGroup.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = `track${i}`;
radio.value = i;
radio.checked = track.enabled;
const label = document.createElement('label');
label.htmlFor = `track${i}`;
label.textContent = track.label || track.language || `音轨 ${i + 1}`;
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
// 添加音轨切换事件
radio.addEventListener('change', () => {
if (radio.checked) {
console.log('切换到音轨:', i);
// 禁用所有音轨
for (let j = 0; j < audioTracks.length; j++) {
audioTracks[j].enabled = false;
}
// 启用选中的音轨
audioTracks[i].enabled = true;
console.log('已启用音轨:', i);
}
});
}
}
// 如果没有多个音轨,显示默认选项
else {
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = 'track0';
radio.value = '0';
radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'track0';
label.textContent = '默认音轨';
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
document.querySelectorAll('input[name="playbackRate"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
}
});
});
// 音调变换控制
document.querySelectorAll('input[name="pitchShift"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
}
});
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
// 更新状态显示,包含当前播放速度和音调
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
// 重置按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

863
index.html.v7 Normal file
View File

@@ -0,0 +1,863 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<script>
// const VIDEO_URL = 'http://localhost:8000/demo01.mp4';
const VIDEO_URL = 'http://localhost:8000/demo02.mp4';
// const VIDEO_URL = 'http://localhost:8000/demo03.mp4';
</script>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.control-group h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
margin: 0;
}
.radio-item label {
font-size: 13px;
cursor: pointer;
user-select: none;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
<div class="control-group">
<h4>音轨选择</h4>
<div class="radio-group" id="trackRadioGroup">
<div class="radio-item">
<input type="radio" id="track0" name="audioTrack" value="0" checked>
<label for="track0">默认音轨</label>
</div>
</div>
</div>
<div class="control-group">
<h4>播放速度</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="rate05" name="playbackRate" value="0.5">
<label for="rate05">0.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate075" name="playbackRate" value="0.75">
<label for="rate075">0.75x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate1" name="playbackRate" value="1" checked>
<label for="rate1">1x (正常)</label>
</div>
<div class="radio-item">
<input type="radio" id="rate125" name="playbackRate" value="1.25">
<label for="rate125">1.25x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate15" name="playbackRate" value="1.5">
<label for="rate15">1.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate2" name="playbackRate" value="2">
<label for="rate2">2x</label>
</div>
</div>
</div>
<div class="control-group">
<h4>音调变换</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="pitch-12" name="pitchShift" value="-12">
<label for="pitch-12">降一个八度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-7" name="pitchShift" value="-7">
<label for="pitch-7">降五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-5" name="pitchShift" value="-5">
<label for="pitch-5">降四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-3" name="pitchShift" value="-3">
<label for="pitch-3">降小三度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-2" name="pitchShift" value="-2">
<label for="pitch-2">降大二度 (C→B♭)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-1" name="pitchShift" value="-1">
<label for="pitch-1">降小二度 (C→B)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch0" name="pitchShift" value="0" checked>
<label for="pitch0">原调</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch1" name="pitchShift" value="1">
<label for="pitch1">升小二度 (C→C#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch2" name="pitchShift" value="2">
<label for="pitch2">升大二度 (C→D)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch3" name="pitchShift" value="3">
<label for="pitch3">升小三度 (C→D#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch4" name="pitchShift" value="4">
<label for="pitch4">升大三度 (C→E)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch5" name="pitchShift" value="5">
<label for="pitch5">升四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch7" name="pitchShift" value="7">
<label for="pitch7">升五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch12" name="pitchShift" value="12">
<label for="pitch12">升一个八度</label>
</div>
</div>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackRadioGroup = document.getElementById('trackRadioGroup');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
// 固定视频地址 - 使用支持CORS的视频源
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let soundTouchNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
let isWorkletRegistered = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 创建 SoundTouch AudioWorkletNode (现代化音调变换)
async function createSoundTouchNode(audioContext) {
try {
// 首先注册 AudioWorklet 处理器
if (!isWorkletRegistered) {
console.log('注册 SoundTouch AudioWorklet...');
await audioContext.audioWorklet.addModule('https://cdn.jsdelivr.net/npm/@soundtouchjs/audio-worklet@0.2.1/dist/soundtouch-worklet.js');
isWorkletRegistered = true;
console.log('SoundTouch AudioWorklet 注册成功');
}
// 创建 AudioWorkletNode
const workletNode = new AudioWorkletNode(audioContext, 'soundtouch-processor');
console.log('SoundTouch AudioWorkletNode 创建成功');
return workletNode;
} catch (error) {
console.error('创建 SoundTouch 节点失败:', error);
// 如果失败,返回一个直通节点
return createPassThroughNode(audioContext);
}
}
// 创建直通节点作为后备方案
function createPassThroughNode(audioContext) {
console.log('创建直通节点作为后备方案');
const gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
return gainNode;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 如果有 SoundTouch 节点,设置音调参数
if (soundTouchNode && soundTouchNode.parameters) {
try {
// 使用 pitchSemitones 参数来设置半音变换
const pitchSemitonesParam = soundTouchNode.parameters.get('pitchSemitones');
if (pitchSemitonesParam) {
pitchSemitonesParam.value = semitones;
console.log('SoundTouch 音调半音设置为:', semitones);
} else {
console.warn('pitchSemitones 参数不可用,尝试使用 pitch 参数');
// 备选方案:使用 pitch 参数(比率)
const pitchParam = soundTouchNode.parameters.get('pitch');
if (pitchParam) {
const pitchRatio = Math.pow(2, semitones / 12);
pitchParam.value = pitchRatio;
console.log('SoundTouch 音调比率设置为:', pitchRatio);
}
}
} catch (error) {
console.warn('设置 SoundTouch 参数时出错:', error);
}
}
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize); // 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 创建 SoundTouch 音调变换节点
soundTouchNode = await createSoundTouchNode(audioContext);
console.log('SoundTouch 节点已创建');
// 连接音频节点链:视频 -> SoundTouch -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(soundTouchNode);
soundTouchNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(使用 SoundTouch AudioWorklet');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
// console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
// 清空现有的音轨选项
trackRadioGroup.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = `track${i}`;
radio.value = i;
radio.checked = track.enabled;
const label = document.createElement('label');
label.htmlFor = `track${i}`;
label.textContent = track.label || track.language || `音轨 ${i + 1}`;
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
// 添加音轨切换事件
radio.addEventListener('change', () => {
if (radio.checked) {
console.log('切换到音轨:', i);
// 禁用所有音轨
for (let j = 0; j < audioTracks.length; j++) {
audioTracks[j].enabled = false;
}
// 启用选中的音轨
audioTracks[i].enabled = true;
console.log('已启用音轨:', i);
}
});
}
}
// 如果没有多个音轨,显示默认选项
else {
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = 'track0';
radio.value = '0';
radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'track0';
label.textContent = '默认音轨';
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
document.querySelectorAll('input[name="playbackRate"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
}
});
});
// 音调变换控制
document.querySelectorAll('input[name="pitchShift"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
}
});
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
// 更新状态显示,包含当前播放速度和音调
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
// 重置按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
});
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

961
index.html.v8 Normal file
View File

@@ -0,0 +1,961 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自定义视频音频播放器</title>
<script>
const VIDEO_URL = 'http://localhost:8000/demo01.mp4';
// const VIDEO_URL = 'http://localhost:8000/demo02.mp4';
// const VIDEO_URL = 'http://localhost:8000/demo03.mp4';
</script>
<style>
.container {
max-width: 1000px;
margin: 20px auto;
padding: 0 20px;
}
.video-box {
width: 100%;
position: relative;
}
video {
width: 100%;
border-radius: 8px;
}
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-group {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 6px;
background: #fafafa;
}
.control-group h4 {
margin: 0 0 8px 0;
font-size: 14px;
color: #333;
}
.radio-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.radio-item {
display: flex;
align-items: center;
gap: 5px;
}
.radio-item input[type="radio"] {
margin: 0;
}
.radio-item label {
font-size: 13px;
cursor: pointer;
user-select: none;
}
button {
padding: 8px 16px;
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
background: #f5f5f5;
}
button:hover {
background: #eee;
}
input[type="text"] {
flex: 1;
max-width: 400px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
input[type="range"] {
width: 150px;
}
#visualizer {
border: 1px solid #eee;
margin-top: 20px;
width: 100%;
height: 150px;
border-radius: 4px;
}
.status {
color: #666;
margin-top: 5px;
}
.progress-container {
margin: 15px 0;
position: relative;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #45a049);
border-radius: 4px;
width: 0%;
transition: width 0.1s ease;
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 5px;
font-size: 12px;
color: #666;
}
.time-display {
font-family: monospace;
}
</style>
</head>
<body>
<div class="container">
<h2>自定义视频音频播放器</h2>
<div class="status" id="statusText">正在自动加载视频...</div>
<div class="video-box">
<video id="video" crossorigin="anonymous"></video>
</div>
<div class="controls">
<button id="playBtn" disabled>播放</button>
<button id="pauseBtn" disabled>暂停</button>
<button id="muteBtn">静音</button>
<button id="testAudioBtn">测试音频</button>
<span>音量:</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
<div class="progress-container">
<div class="progress-bar" id="progressBar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-info">
<span class="time-display" id="currentTime">00:00</span>
<span class="time-display" id="duration">00:00</span>
</div>
</div>
<div class="control-group">
<h4>音轨选择</h4>
<div class="radio-group" id="trackRadioGroup">
<div class="radio-item">
<input type="radio" id="track0" name="audioTrack" value="0" checked>
<label for="track0">默认音轨</label>
</div>
</div>
</div>
<div class="control-group">
<h4>播放速度</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="rate05" name="playbackRate" value="0.5">
<label for="rate05">0.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate075" name="playbackRate" value="0.75">
<label for="rate075">0.75x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate1" name="playbackRate" value="1" checked>
<label for="rate1">1x (正常)</label>
</div>
<div class="radio-item">
<input type="radio" id="rate125" name="playbackRate" value="1.25">
<label for="rate125">1.25x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate15" name="playbackRate" value="1.5">
<label for="rate15">1.5x</label>
</div>
<div class="radio-item">
<input type="radio" id="rate2" name="playbackRate" value="2">
<label for="rate2">2x</label>
</div>
</div>
</div>
<div class="control-group">
<h4>音调变换</h4>
<div class="radio-group">
<div class="radio-item">
<input type="radio" id="pitch-12" name="pitchShift" value="-12">
<label for="pitch-12">降一个八度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-7" name="pitchShift" value="-7">
<label for="pitch-7">降五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-5" name="pitchShift" value="-5">
<label for="pitch-5">降四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-3" name="pitchShift" value="-3">
<label for="pitch-3">降小三度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-2" name="pitchShift" value="-2">
<label for="pitch-2">降大二度 (C→B♭)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch-1" name="pitchShift" value="-1">
<label for="pitch-1">降小二度 (C→B)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch0" name="pitchShift" value="0" checked>
<label for="pitch0">原调</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch1" name="pitchShift" value="1">
<label for="pitch1">升小二度 (C→C#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch2" name="pitchShift" value="2">
<label for="pitch2">升大二度 (C→D)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch3" name="pitchShift" value="3">
<label for="pitch3">升小三度 (C→D#)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch4" name="pitchShift" value="4">
<label for="pitch4">升大三度 (C→E)</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch5" name="pitchShift" value="5">
<label for="pitch5">升四度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch7" name="pitchShift" value="7">
<label for="pitch7">升五度</label>
</div>
<div class="radio-item">
<input type="radio" id="pitch12" name="pitchShift" value="12">
<label for="pitch12">升一个八度</label>
</div>
</div>
</div>
<h3>音频频谱可视化</h3>
<canvas id="visualizer"></canvas>
</div>
<script>
const video = document.getElementById('video');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const muteBtn = document.getElementById('muteBtn');
const testAudioBtn = document.getElementById('testAudioBtn');
const volumeInput = document.getElementById('volume');
const trackRadioGroup = document.getElementById('trackRadioGroup');
const statusText = document.getElementById('statusText');
const canvas = document.getElementById('visualizer');
const ctx = canvas.getContext('2d');
const progressBar = document.getElementById('progressBar');
const progressFill = document.getElementById('progressFill');
const currentTimeDisplay = document.getElementById('currentTime');
const durationDisplay = document.getElementById('duration');
// 固定视频地址 - 使用支持CORS的视频源
let audioContext;
let analyser;
let mediaElementSource;
let animationId;
let gainNode;
let soundTouchNode;
let currentPitchShift = 0;
let isAudioContextInitialized = false;
let isWorkletRegistered = false;
// 设置画布尺寸
function resizeCanvas() {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
}
// 格式化时间显示(秒转换为 mm:ss 格式)
function formatTime(seconds) {
if (isNaN(seconds) || !isFinite(seconds)) {
return '00:00';
}
const minutes = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 更新进度条
function updateProgress() {
if (video.duration && isFinite(video.duration)) {
const progress = (video.currentTime / video.duration) * 100;
progressFill.style.width = `${progress}%`;
currentTimeDisplay.textContent = formatTime(video.currentTime);
durationDisplay.textContent = formatTime(video.duration);
}
}
// 处理进度条点击跳转
function handleProgressBarClick(event) {
const rect = progressBar.getBoundingClientRect();
const clickX = event.clientX - rect.left;
const progressBarWidth = rect.width;
const clickRatio = clickX / progressBarWidth;
if (video.duration && isFinite(video.duration)) {
const newTime = clickRatio * video.duration;
video.currentTime = Math.max(0, Math.min(newTime, video.duration));
console.log(`跳转到: ${formatTime(newTime)}`);
}
}
// 创建 SoundTouch AudioWorkletNode (现代化音调变换)
async function createSoundTouchNode(audioContext) {
try {
// 首先注册 AudioWorklet 处理器
if (!isWorkletRegistered) {
console.log('注册 SoundTouch AudioWorklet...');
await audioContext.audioWorklet.addModule('https://cdn.jsdelivr.net/npm/@soundtouchjs/audio-worklet@0.2.1/dist/soundtouch-worklet.js');
isWorkletRegistered = true;
console.log('SoundTouch AudioWorklet 注册成功');
}
// 创建 AudioWorkletNode
const workletNode = new AudioWorkletNode(audioContext, 'soundtouch-processor');
console.log('SoundTouch AudioWorkletNode 创建成功');
return workletNode;
} catch (error) {
console.error('创建 SoundTouch 节点失败:', error);
// 如果失败,返回一个直通节点
return createPassThroughNode(audioContext);
}
}
// 创建直通节点作为后备方案
function createPassThroughNode(audioContext) {
console.log('创建直通节点作为后备方案');
const gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
return gainNode;
}
// 应用音调变换
function applyPitchShift(semitones) {
currentPitchShift = semitones;
console.log('音调变换设置为:', semitones, '半音');
// 如果有 SoundTouch 节点,设置音调参数
if (soundTouchNode && soundTouchNode.parameters) {
try {
// 使用 pitchSemitones 参数来设置半音变换
const pitchSemitonesParam = soundTouchNode.parameters.get('pitchSemitones');
if (pitchSemitonesParam) {
pitchSemitonesParam.value = semitones;
console.log('SoundTouch 音调半音设置为:', semitones);
} else {
console.warn('pitchSemitones 参数不可用,尝试使用 pitch 参数');
// 备选方案:使用 pitch 参数(比率)
const pitchParam = soundTouchNode.parameters.get('pitch');
if (pitchParam) {
const pitchRatio = Math.pow(2, semitones / 12);
pitchParam.value = pitchRatio;
console.log('SoundTouch 音调比率设置为:', pitchRatio);
}
}
} catch (error) {
console.warn('设置 SoundTouch 参数时出错:', error);
}
}
// 更新状态显示
if (!video.paused) {
const pitchText = semitones === 0 ? '原调' :
(semitones > 0 ? `升${semitones}半音` : `降${Math.abs(semitones)}半音`);
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
}
}
// 初始化音频上下文(需用户交互触发)
async function initAudioContext() {
try {
if (isAudioContextInitialized) {
console.log('音频上下文已经初始化,跳过');
return;
}
console.log('开始初始化音频上下文...');
audioContext = new (window.AudioContext || window.webkitAudioContext)();
// 如果音频上下文被暂停,恢复它
if (audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 创建媒体元素源
mediaElementSource = audioContext.createMediaElementSource(video);
console.log('媒体元素源已创建');
// 创建分析器节点
analyser = audioContext.createAnalyser();
analyser.fftSize = 1024; // 增加分辨率
analyser.smoothingTimeConstant = 0.8;
console.log('分析器节点已创建fftSize:', analyser.fftSize); // 创建增益节点(用于音量控制)
gainNode = audioContext.createGain();
gainNode.gain.value = 1.0;
console.log('增益节点已创建');
// 创建 SoundTouch 音调变换节点
soundTouchNode = await createSoundTouchNode(audioContext);
console.log('SoundTouch 节点已创建');
// 连接音频节点链:视频 -> SoundTouch -> 分析器 -> 增益 -> 输出
mediaElementSource.connect(soundTouchNode);
soundTouchNode.connect(analyser);
analyser.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('音频节点链已连接(使用 SoundTouch AudioWorklet');
isAudioContextInitialized = true;
console.log('音频上下文初始化成功!');
// 立即开始可视化
startVisualization();
// 测试音频数据
setTimeout(() => {
if (analyser) {
const testArray = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testArray);
const hasData = testArray.some(value => value > 0);
console.log('测试音频数据 - 有数据:', hasData);
if (hasData) {
console.log('前10个频谱值:', Array.from(testArray.slice(0, 10)));
}
}
}, 2000);
} catch (error) {
console.error('音频上下文初始化失败:', error);
// 如果音频上下文初始化失败,至少确保视频能播放
isAudioContextInitialized = false;
}
}
// 启动可视化
function startVisualization() {
if (!analyser) {
console.warn('分析器节点不存在,启动模拟可视化');
startMockVisualization();
return;
}
// 如果已经在运行动画,先停止
if (animationId) {
cancelAnimationFrame(animationId);
}
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
console.log('可视化已启动,缓冲区长度:', bufferLength);
function draw() {
if (!analyser) {
console.warn('分析器节点丢失,停止绘制');
return;
}
// 获取频谱数据
analyser.getByteFrequencyData(dataArray);
// 检查是否有音频数据
let hasAudioData = false;
let maxValue = 0;
for (let i = 0; i < dataArray.length; i++) {
if (dataArray[i] > 0) {
hasAudioData = true;
maxValue = Math.max(maxValue, dataArray[i]);
}
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (hasAudioData) {
// console.log('检测到音频数据,最大值:', maxValue);
// 绘制频谱柱状图
const barWidth = (canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * canvas.height;
// 创建渐变色
const hue = (i / bufferLength) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth - 1, barHeight);
x += barWidth;
}
} else {
// 显示"无音频数据"提示
ctx.fillStyle = '#999';
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.fillText('等待音频数据...', canvas.width / 2, canvas.height / 2);
// 如果视频正在播放但没有音频数据,可能有问题
if (!video.paused) {
console.log('视频正在播放但没有音频数据');
}
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 模拟可视化当Web Audio API不可用时
function startMockVisualization() {
console.log('启动模拟可视化');
function draw() {
if (video.paused) {
return;
}
// 清空画布
ctx.fillStyle = 'rgb(240, 240, 240)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 生成模拟频谱数据
const barCount = 50;
const barWidth = canvas.width / barCount;
for (let i = 0; i < barCount; i++) {
// 根据时间生成动态数据
const time = Date.now() * 0.001;
const frequency = i * 0.2;
const amplitude = Math.sin(time + frequency) * 0.5 + 0.5;
const barHeight = amplitude * canvas.height * 0.8;
// 创建渐变色
const hue = (i / barCount) * 360;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
ctx.fillRect(i * barWidth, canvas.height - barHeight, barWidth - 1, barHeight);
}
animationId = requestAnimationFrame(draw);
}
draw();
}
// 停止可视化
function stopVisualization() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
// 自动加载视频
async function loadVideo() {
statusText.textContent = '加载中...';
statusText.style.color = '#666';
playBtn.disabled = true;
pauseBtn.disabled = true;
try {
// 使用固定的视频地址添加CORS支持
video.crossOrigin = 'anonymous';
video.src = VIDEO_URL;
console.log('开始加载视频:', VIDEO_URL);
await new Promise((resolve, reject) => {
video.onloadedmetadata = () => {
console.log('视频元数据加载完成');
console.log('视频时长:', video.duration);
console.log('视频尺寸:', video.videoWidth, 'x', video.videoHeight);
resolve();
};
video.onerror = (e) => {
console.error('视频加载错误:', e);
reject(e);
};
// 添加超时处理
setTimeout(() => reject(new Error('加载超时')), 15000);
});
statusText.textContent = '加载完成,可以开始播放';
statusText.style.color = 'green';
playBtn.disabled = false;
} catch (err) {
statusText.textContent = `加载失败:${err.message || '视频地址无效或网络错误'}`;
statusText.style.color = 'red';
console.error('视频加载错误:', err);
}
}
// 视频元数据加载完成后初始化音轨
video.addEventListener('loadedmetadata', () => {
console.log('视频元数据已加载');
// 初始化进度条
updateProgress();
// 检查并显示可用的音频轨道信息
const audioTracks = video.audioTracks;
const textTracks = video.textTracks;
console.log('AudioTracks数量:', audioTracks ? audioTracks.length : 0);
console.log('TextTracks数量:', textTracks ? textTracks.length : 0);
// 清空现有的音轨选项
trackRadioGroup.innerHTML = '';
// 尝试使用audioTracks
if (audioTracks && audioTracks.length > 0) {
console.log('使用AudioTracks');
for (let i = 0; i < audioTracks.length; i++) {
const track = audioTracks[i];
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = `track${i}`;
radio.value = i;
radio.checked = track.enabled;
const label = document.createElement('label');
label.htmlFor = `track${i}`;
label.textContent = track.label || track.language || `音轨 ${i + 1}`;
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log(`音轨 ${i}:`, track.label, track.language, track.enabled);
// 添加音轨切换事件
radio.addEventListener('change', () => {
if (radio.checked) {
console.log('切换到音轨:', i);
// 禁用所有音轨
for (let j = 0; j < audioTracks.length; j++) {
audioTracks[j].enabled = false;
}
// 启用选中的音轨
audioTracks[i].enabled = true;
console.log('已启用音轨:', i);
}
});
}
}
// 如果没有多个音轨,显示默认选项
else {
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.id = 'track0';
radio.value = '0';
radio.checked = true;
const label = document.createElement('label');
label.htmlFor = 'track0';
label.textContent = '默认音轨';
radioItem.appendChild(radio);
radioItem.appendChild(label);
trackRadioGroup.appendChild(radioItem);
console.log('只有一个默认音轨');
}
});
// 播放控制
playBtn.addEventListener('click', async () => {
try {
console.log('点击播放按钮');
// 初始化音频上下文(必须在用户交互后创建)
if (!isAudioContextInitialized) {
console.log('初始化音频上下文...');
await initAudioContext();
}
// 确保音频上下文在运行状态
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文已恢复');
}
// 播放视频
await video.play();
console.log('视频开始播放');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
statusText.textContent = '正在播放,音频频谱可视化已启动';
statusText.style.color = 'green';
} catch (err) {
console.error('播放失败:', err);
statusText.textContent = `播放失败:${err.message}`;
statusText.style.color = 'red';
}
});
pauseBtn.addEventListener('click', () => {
video.pause();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
// 测试音频按钮
testAudioBtn.addEventListener('click', async () => {
console.log('=== 音频调试信息 ===');
console.log('视频元素状态:');
console.log('- 是否暂停:', video.paused);
console.log('- 是否静音:', video.muted);
console.log('- 音量:', video.volume);
console.log('- 当前时间:', video.currentTime);
console.log('- 总时长:', video.duration);
console.log('音频上下文状态:');
console.log('- 是否初始化:', isAudioContextInitialized);
console.log('- 状态:', audioContext ? audioContext.state : '未创建');
console.log('- 采样率:', audioContext ? audioContext.sampleRate : 'N/A');
console.log('音频节点状态:');
console.log('- MediaElementSource:', !!mediaElementSource);
console.log('- Analyser:', !!analyser);
console.log('- GainNode:', !!gainNode);
if (analyser) {
const testData = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(testData);
const hasData = testData.some(v => v > 0);
const maxVal = Math.max(...testData);
console.log('- 频谱数据:', hasData ? `有数据 (最大值: ${maxVal})` : '无数据');
}
// 尝试播放测试音频
if (!video.paused) {
console.log('视频正在播放,检查音频输出...');
setTimeout(() => {
if (analyser) {
const data = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(data);
console.log('延迟检查频谱数据:', data.slice(0, 10));
}
}, 1000);
}
});
volumeInput.addEventListener('input', (e) => {
const volume = parseFloat(e.target.value);
video.volume = volume;
// 如果有增益节点,也同步更新
if (gainNode) {
gainNode.gain.value = volume;
console.log('音量已更新:', volume);
}
});
// 播放速度控制
document.querySelectorAll('input[name="playbackRate"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const playbackRate = parseFloat(e.target.value);
video.playbackRate = playbackRate;
const rateText = playbackRate === 1 ? '正常速度' : `${playbackRate}x 速度`;
console.log('播放速度已改变为:', rateText);
// 如果视频正在播放,显示速度变化提示
if (!video.paused) {
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
}
}
});
});
// 音调变换控制
document.querySelectorAll('input[name="pitchShift"]').forEach(radio => {
radio.addEventListener('change', (e) => {
if (e.target.checked) {
const pitchShift = parseInt(e.target.value);
applyPitchShift(pitchShift);
}
});
});
// 监听视频播放状态变化
video.addEventListener('play', async () => {
console.log('视频播放事件触发');
// 更新按钮状态
playBtn.disabled = true;
pauseBtn.disabled = false;
// 更新状态显示,包含当前播放速度和音调
const currentRate = video.playbackRate;
const rateText = currentRate === 1 ? '正常速度' : `${currentRate}x 速度`;
const currentPitchShift = document.querySelector('input[name="pitchShift"]:checked')?.value || '0';
const pitchText = currentPitchShift === '0' ? '原调' :
(parseInt(currentPitchShift) > 0 ? `升${currentPitchShift}半音` : `降${Math.abs(parseInt(currentPitchShift))}半音`);
statusText.textContent = `正在播放 (${rateText}, ${pitchText})`;
statusText.style.color = '#4CAF50';
// 检查音频状态
console.log('视频音量:', video.volume, '静音状态:', video.muted);
console.log('音频上下文状态:', audioContext ? audioContext.state : '未初始化');
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('音频上下文在播放时恢复');
}
if (isAudioContextInitialized && analyser) {
console.log('重新启动可视化');
startVisualization();
}
});
video.addEventListener('pause', () => {
console.log('视频暂停');
stopVisualization();
// 更新按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '视频已暂停';
statusText.style.color = '#666';
});
video.addEventListener('ended', () => {
console.log('视频播放结束');
stopVisualization();
// 重置按钮状态
playBtn.disabled = false;
pauseBtn.disabled = true;
statusText.textContent = '播放结束';
statusText.style.color = '#666';
});
// 添加音频相关的事件监听
video.addEventListener('volumechange', () => {
console.log('音量变化:', video.volume, '静音:', video.muted);
});
video.addEventListener('loadstart', () => {
console.log('开始加载视频');
});
video.addEventListener('canplay', () => {
console.log('视频可以播放');
console.log('视频时长:', video.duration);
console.log('视频是否有音频:', !video.muted && video.volume > 0);
// 更新进度条显示
updateProgress();
});
// 进度条相关事件监听
video.addEventListener('timeupdate', updateProgress);
video.addEventListener('durationchange', updateProgress);
video.addEventListener('loadeddata', updateProgress);
// 进度条点击跳转
progressBar.addEventListener('click', handleProgressBarClick);
// 画布尺寸适配
window.addEventListener('resize', resizeCanvas);
// 初始化画布尺寸
resizeCanvas();
// 如果DOM已经加载完成立即加载视频
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadVideo);
} else {
loadVideo();
}
</script>
</body>
</html>

1015
index.html.v9 Normal file

File diff suppressed because it is too large Load Diff

120
nginx_access.log Normal file
View File

@@ -0,0 +1,120 @@
127.0.0.1 - - [06/Jun/2025:10:56:53 +0800] "GET / HTTP/1.1" 200 47534 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:54 +0800] "GET /hls/variant.m3u8 HTTP/1.1" 200 352 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:54 +0800] "GET /hls/video/index.m3u8 HTTP/1.1" 200 394 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:55 +0800] "GET /hls/video/main.ts HTTP/1.1" 206 2632 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:55 +0800] "GET /favicon.ico HTTP/1.1" 404 555 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:59 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:59 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:56:59 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:00 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:01 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:01 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190792 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:02 +0800] "GET /demo/main.ts HTTP/1.1" 206 1213540 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:07 +0800] "GET /demo/main.ts HTTP/1.1" 206 1194176 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:07 +0800] "GET /demo/audio.m3u8 HTTP/1.1" 200 10948 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:12 +0800] "GET /demo/main.aac HTTP/1.1" 206 136770 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:22 +0800] "GET / HTTP/1.1" 200 47534 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:24 +0800] "GET /hls/variant.m3u8 HTTP/1.1" 200 352 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:24 +0800] "GET /hls/video/index.m3u8 HTTP/1.1" 200 394 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:24 +0800] "GET /hls/video/main.ts HTTP/1.1" 206 2632 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:30 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:30 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:30 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:30 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:37 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:37 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190792 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:37 +0800] "GET /demo/main.ts HTTP/1.1" 206 1213540 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:42 +0800] "GET /demo/main.ts HTTP/1.1" 206 1194176 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:42 +0800] "GET /demo/audio.m3u8 HTTP/1.1" 200 10948 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:47 +0800] "GET /demo/main.aac HTTP/1.1" 206 136770 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:52 +0800] "GET /demo/main.aac HTTP/1.1" 206 137219 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:57:57 +0800] "GET /demo/main.aac HTTP/1.1" 206 137132 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:58:02 +0800] "GET /demo/main.aac HTTP/1.1" 206 136217 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:58:07 +0800] "GET /demo/main.aac HTTP/1.1" 206 136863 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:17 +0800] "GET / HTTP/1.1" 200 47534 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:20 +0800] "GET /hls/variant.m3u8 HTTP/1.1" 200 352 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:20 +0800] "GET /hls/video/index.m3u8 HTTP/1.1" 200 394 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:20 +0800] "GET /hls/video/main.ts HTTP/1.1" 206 2632 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:22 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:22 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:22 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:22 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:25 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:25 +0800] "GET /demo/audio.m3u8 HTTP/1.1" 200 10948 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:25 +0800] "GET /demo/main.aac HTTP/1.1" 206 136567 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:25 +0800] "GET /demo/main.aac HTTP/1.1" 206 136954 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:30 +0800] "GET /demo/main.aac HTTP/1.1" 206 137116 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:35 +0800] "GET /demo/main.aac HTTP/1.1" 206 136770 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:40 +0800] "GET /demo/main.aac HTTP/1.1" 206 137219 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:45 +0800] "GET /demo/main.aac HTTP/1.1" 206 137132 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:48 +0800] "GET /demo/main.aac HTTP/1.1" 206 136567 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:48 +0800] "GET /demo/main.aac HTTP/1.1" 206 136954 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:48 +0800] "GET /demo/main.aac HTTP/1.1" 206 137116 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:48 +0800] "GET /demo/main.aac HTTP/1.1" 206 136770 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:48 +0800] "GET /demo/main.aac HTTP/1.1" 206 137219 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:10:59:48 +0800] "GET /demo/main.aac HTTP/1.1" 206 137132 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:04 +0800] "GET / HTTP/1.1" 200 47534 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:05 +0800] "GET /hls/variant.m3u8 HTTP/1.1" 200 352 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:05 +0800] "GET /hls/video/index.m3u8 HTTP/1.1" 200 394 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:06 +0800] "GET /hls/video/main.ts HTTP/1.1" 206 2632 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:06 +0800] "GET /favicon.ico HTTP/1.1" 404 555 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:10 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:10 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:10 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:10 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:15 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:15 +0800] "GET /demo/audio.m3u8 HTTP/1.1" 200 10948 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:15 +0800] "GET /demo/main.aac HTTP/1.1" 206 136567 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:15 +0800] "GET /demo/main.aac HTTP/1.1" 206 136954 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:20 +0800] "GET /demo/main.aac HTTP/1.1" 206 137116 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:25 +0800] "GET /demo/main.aac HTTP/1.1" 206 136770 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:30 +0800] "GET /demo/main.aac HTTP/1.1" 206 137219 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:35 +0800] "GET /demo/main.aac HTTP/1.1" 206 137132 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:38 +0800] "GET /demo/main.aac HTTP/1.1" 206 136567 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:38 +0800] "GET /demo/main.aac HTTP/1.1" 206 136954 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:38 +0800] "GET /demo/main.aac HTTP/1.1" 206 137116 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:38 +0800] "GET /demo/main.aac HTTP/1.1" 206 136770 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:38 +0800] "GET /demo/main.aac HTTP/1.1" 206 137219 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:01:38 +0800] "GET /demo/main.aac HTTP/1.1" 206 137132 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:02:24 +0800] "GET / HTTP/1.1" 200 47534 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:02:26 +0800] "GET /hls/variant.m3u8 HTTP/1.1" 200 352 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:02:26 +0800] "GET /hls/video/index.m3u8 HTTP/1.1" 200 394 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:02:26 +0800] "GET /hls/video/main.ts HTTP/1.1" 206 2632 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:04:54 +0800] "GET /hls/video/main.ts HTTP/1.1" 304 0 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:04:56 +0800] "GET / HTTP/1.1" 200 48475 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:03 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:03 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:03 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:03 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:05 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:05 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190792 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:05 +0800] "GET /demo/main.ts HTTP/1.1" 206 1213540 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:24 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:24 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:24 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:24 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:26 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:26 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190792 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:26 +0800] "GET /demo/main.ts HTTP/1.1" 206 1213540 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:31 +0800] "GET /demo/main.ts HTTP/1.1" 206 1194176 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:31 +0800] "GET /demo/audio.m3u8 HTTP/1.1" 200 10948 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:33 +0800] "GET / HTTP/1.1" 200 48475 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:36 +0800] "GET /hls/variant.m3u8 HTTP/1.1" 200 352 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:36 +0800] "GET /hls/video/index.m3u8 HTTP/1.1" 200 394 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:36 +0800] "GET /hls/video/main.ts HTTP/1.1" 206 2632 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:42 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:42 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:42 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:42 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:47 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:47 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190792 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:05:47 +0800] "GET /demo/main.ts HTTP/1.1" 206 1213540 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:04 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:04 +0800] "GET /demo/variant.m3u8 HTTP/1.1" 200 320 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:04 +0800] "GET /demo/video.m3u8 HTTP/1.1" 200 10787 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:04 +0800] "GET /demo/main.ts HTTP/1.1" 206 1210156 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:05 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190040 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:05 +0800] "GET /demo/main.ts HTTP/1.1" 206 1190792 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:05 +0800] "GET /demo/main.ts HTTP/1.1" 206 1213540 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:10 +0800] "GET /demo/main.ts HTTP/1.1" 206 1194176 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"
127.0.0.1 - - [06/Jun/2025:11:06:10 +0800] "GET /demo/audio.m3u8 HTTP/1.1" 200 10948 "http://localhost:8888/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0"

0
nginx_error.log Normal file
View File

23
serve.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"log"
"net/http"
)
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
fs := http.FileServer(http.Dir("."))
http.Handle("/", loggingMiddleware(fs))
log.Println("Serving on http://localhost:8888 ...")
err := http.ListenAndServe(":8888", nil)
if err != nil {
log.Fatal(err)
}
}

116
test.html Normal file
View File

@@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>多音轨 HLS 测试</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
video {
width: 100%;
max-width: 800px;
height: auto;
}
.controls {
margin: 20px 0;
}
.audio-tracks {
margin: 10px 0;
}
.audio-tracks label {
display: block;
margin: 5px 0;
}
.info {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
margin: 20px 0;
}
</style>
</head>
<body>
<h1>多音轨 HLS 视频播放器测试</h1>
<video id="video" controls></video>
<div class="controls">
<div class="audio-tracks">
<h3>音轨选择:</h3>
<div id="audioTracks"></div>
</div>
</div>
<div class="info">
<h3>说明:</h3>
<p>此页面用于测试从 merged_dual_track.mp4 生成的多音轨 HLS 流</p>
<p>HLS 主播放列表: <code>hls_output/master.m3u8</code></p>
<p>包含两个音轨Audio Track 1 (默认) 和 Audio Track 2</p>
</div>
<script>
const video = document.getElementById('video');
const audioTracksDiv = document.getElementById('audioTracks');
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource('hls_output/master.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function () {
console.log('HLS manifest parsed');
console.log('Audio tracks:', hls.audioTracks);
updateAudioTrackControls(hls);
});
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, function () {
console.log('Audio tracks updated');
updateAudioTrackControls(hls);
});
function updateAudioTrackControls(hls) {
audioTracksDiv.innerHTML = '';
hls.audioTracks.forEach((track, index) => {
const label = document.createElement('label');
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = 'audioTrack';
radio.value = index;
radio.checked = index === hls.audioTrack;
radio.addEventListener('change', function () {
if (this.checked) {
hls.audioTrack = parseInt(this.value);
console.log('Switched to audio track:', track);
}
});
label.appendChild(radio);
label.appendChild(document.createTextNode(` ${track.name || `Audio Track ${index + 1}`}`));
audioTracksDiv.appendChild(label);
});
}
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
// 原生 HLS 支持 (Safari)
video.src = 'hls_output/master.m3u8';
} else {
alert('您的浏览器不支持 HLS 播放');
}
</script>
</body>
</html>

20
video.conf Normal file
View File

@@ -0,0 +1,20 @@
# Nginx 配置示例,服务于当前目录
server {
listen 8888;
server_name localhost;
root /Users/rogee/Projects/self/video-player;
autoindex on;
# 支持 Range 请求(默认支持)
location / {
try_files $uri $uri/ =404;
}
# 为 .aac 文件添加 location 匹配,强制返回 Content-Type 为 audio/aac
location ~* \.aac$ {
default_type "";
add_header Content-Type audio/aac;
try_files $uri =404;
}
# 可选:日志配置
access_log /Users/rogee/Projects/self/video-player/nginx_access.log;
error_log /Users/rogee/Projects/self/video-player/nginx_error.log;
}