下面的代码可以实现
<!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> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Arial', sans-serif; background-color: #f5f5f5; color: #333; padding: 20px; max-width: 1200px; margin: 0 auto; } h1 { text-align: center; margin-bottom: 20px; color: #2c3e50; } .container { display: flex; flex-direction: column; gap: 20px; } .upload-section { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .upload-btn { background-color: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s; } .upload-btn:hover { background-color: #2980b9; } .preview-section { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); } .video-container { width: 100%; max-width: 800px; margin: 0 auto; background-color: #000; border-radius: 8px; overflow: hidden; } #previewVideo { width: 100%; display: block; } .timeline-container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); margin-top: 20px; } .timeline { width: 100%; min-height: 100px; background-color: #ecf0f1; border-radius: 4px; padding: 10px; display: flex; gap: 5px; overflow-x: auto; position: relative; } .timeline-indicator { position: absolute; width: 2px; height: 100%; background-color: red; top: 0; left: 0; pointer-events: none; z-index: 10; } .video-clip { min-width: 120px; height: 80px; background-color: #3498db; border-radius: 4px; display: flex; flex-direction: column; padding: 5px; cursor: pointer; position: relative; overflow: hidden; } .video-clip.active { border: 2px solid #e74c3c; } .video-clip-thumbnail { width: 100%; height: 50px; background-color: #2c3e50; border-radius: 2px; overflow: hidden; } .video-clip-thumbnail img { width: 100%; height: 100%; object-fit: cover; } .video-clip-title { margin-top: 5px; font-size: 12px; color: white; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .video-clip-duration { position: absolute; bottom: 5px; right: 5px; font-size: 10px; background-color: rgba(0, 0, 0, 0.6); color: white; padding: 2px 4px; border-radius: 2px; } .controls { display: flex; gap: 10px; margin-top: 10px; justify-content: center; } button { background-color: #3498db; color: white; border: none; padding: 8px 15px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; } button:hover { background-color: #2980b9; } .timeline-scrubber { width: 100%; margin-top: 10px; } #timelineScrubber { width: 100%; } .video-list { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; } .video-item { background-color: #ecf0f1; padding: 10px; border-radius: 4px; width: calc(33.33% - 10px); min-width: 200px; display: flex; align-items: center; gap: 10px; } .video-item-thumbnail { width: 80px; height: 60px; background-color: #2c3e50; border-radius: 4px; overflow: hidden; } .video-item-thumbnail video { width: 100%; height: 100%; object-fit: cover; } .video-item-info { flex: 1; } .video-item-title { font-weight: bold; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .video-item-actions { display: flex; gap: 5px; } .add-to-timeline-btn { background-color: #2ecc71; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; transition: background-color 0.3s; } .add-to-timeline-btn:hover { background-color: #27ae60; } .remove-btn { background-color: #e74c3c; } .remove-btn:hover { background-color: #c0392b; } /* 响应式设计 */ @media (max-width: 768px) { .video-item { width: 100%; } .upload-section, .preview-section, .timeline-container { padding: 15px; } .video-clip { min-width: 100px; } } </style> </head> <body> <h1>视频合并预览工具</h1> <div class="container"> <div class="upload-section"> <input type="file" id="videoInput" accept="video/*" multiple style="display: none;"> <button class="upload-btn" id="uploadBtn">选择视频文件</button> <div class="video-list" id="videoList"> <!-- 上传的视频将在此显示 --> </div> </div> <div class="timeline-container"> <h2>时间轴</h2> <div class="timeline" id="timeline"> <div class="timeline-indicator" id="timelineIndicator"></div> <!-- 时间轴上的视频片段将在此显示 --> </div> <div class="timeline-scrubber"> <input type="range" id="timelineScrubber" min="0" max="100" value="0" step="0.1"> </div> <div class="controls"> <button id="playBtn">播放</button> <button id="pauseBtn">暂停</button> <button id="clearTimelineBtn" class="remove-btn">清空时间轴</button> </div> </div> <div class="preview-section"> <h2>预览</h2> <div class="video-container"> <video id="previewVideo" controls></video> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // 获取DOM元素 const videoInput = document.getElementById('videoInput'); const uploadBtn = document.getElementById('uploadBtn'); const videoList = document.getElementById('videoList'); const timeline = document.getElementById('timeline'); const timelineIndicator = document.getElementById('timelineIndicator'); const timelineScrubber = document.getElementById('timelineScrubber'); const playBtn = document.getElementById('playBtn'); const pauseBtn = document.getElementById('pauseBtn'); const clearTimelineBtn = document.getElementById('clearTimelineBtn'); const previewVideo = document.getElementById('previewVideo'); // 存储上传的视频 const uploadedVideos = []; // 存储时间轴上的视频片段 const timelineClips = []; // 当前正在播放的片段索引 let currentClipIndex = 0; // 总时长(毫秒) let totalDuration = 0; // 当前播放时间(毫秒) let currentTime = 0; // 播放状态 let isPlaying = false; // 播放定时器 let playTimer; // 点击上传按钮触发文件选择 uploadBtn.addEventListener('click', function() { videoInput.click(); }); // 处理选择的视频文件 videoInput.addEventListener('change', function(e) { const files = e.target.files; if (files.length === 0) return; for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type.startsWith('video/')) { const videoURL = URL.createObjectURL(file); const videoObj = { id: 'video_' + Date.now() + '_' + i, file: file, url: videoURL, name: file.name, duration: 0 }; // 获取视频时长 const tempVideo = document.createElement('video'); tempVideo.src = videoURL; tempVideo.addEventListener('loadedmetadata', function() { videoObj.duration = tempVideo.duration * 1000; // 转换为毫秒 addVideoToList(videoObj); }); } } // 清空input,允许重新选择相同文件 videoInput.value = ''; }); // 将视频添加到视频列表 function addVideoToList(videoObj) { uploadedVideos.push(videoObj); const videoItem = document.createElement('div'); videoItem.className = 'video-item'; videoItem.dataset.id = videoObj.id; const thumbnailDiv = document.createElement('div'); thumbnailDiv.className = 'video-item-thumbnail'; const videoElement = document.createElement('video'); videoElement.src = videoObj.url; videoElement.muted = true; thumbnailDiv.appendChild(videoElement); const infoDiv = document.createElement('div'); infoDiv.className = 'video-item-info'; const titleDiv = document.createElement('div'); titleDiv.className = 'video-item-title'; titleDiv.textContent = videoObj.name; const durationDiv = document.createElement('div'); durationDiv.className = 'video-item-duration'; durationDiv.textContent = formatDuration(videoObj.duration); const actionsDiv = document.createElement('div'); actionsDiv.className = 'video-item-actions'; const addBtn = document.createElement('button'); addBtn.className = 'add-to-timeline-btn'; addBtn.textContent = '添加到时间轴'; addBtn.addEventListener('click', function() { addToTimeline(videoObj); }); infoDiv.appendChild(titleDiv); infoDiv.appendChild(durationDiv); actionsDiv.appendChild(addBtn); infoDiv.appendChild(actionsDiv); videoItem.appendChild(thumbnailDiv); videoItem.appendChild(infoDiv); videoList.appendChild(videoItem); // 设置视频缩略图 videoElement.currentTime = 1; } // 将视频添加到时间轴 function addToTimeline(videoObj) { const clipObj = { id: 'clip_' + Date.now(), videoId: videoObj.id, video: videoObj, startTime: totalDuration, duration: videoObj.duration }; timelineClips.push(clipObj); totalDuration += clipObj.duration; // 创建视频片段元素 const clipElement = document.createElement('div'); clipElement.className = 'video-clip'; clipElement.dataset.id = clipObj.id; clipElement.style.width = (clipObj.duration / 1000) * 30 + 'px'; // 30px 每秒 const thumbnailDiv = document.createElement('div'); thumbnailDiv.className = 'video-clip-thumbnail'; const imgElement = document.createElement('img'); // 使用视频的第一帧作为缩略图 const tempVideo = document.createElement('video'); tempVideo.src = videoObj.url; tempVideo.currentTime = 1; tempVideo.addEventListener('seeked', function() { const canvas = document.createElement('canvas'); canvas.width = 160; canvas.height = 90; canvas.getContext('2d').drawImage(tempVideo, 0, 0, canvas.width, canvas.height); imgElement.src = canvas.toDataURL(); }); thumbnailDiv.appendChild(imgElement); const titleDiv = document.createElement('div'); titleDiv.className = 'video-clip-title'; titleDiv.textContent = videoObj.name; const durationDiv = document.createElement('div'); durationDiv.className = 'video-clip-duration'; durationDiv.textContent = formatDuration(clipObj.duration); clipElement.appendChild(thumbnailDiv); clipElement.appendChild(titleDiv); clipElement.appendChild(durationDiv); // 点击视频片段可以设置当前播放位置 clipElement.addEventListener('click', function() { const index = timelineClips.findIndex(clip => clip.id === clipObj.id); if (index !== -1) { seekToClip(index); } }); timeline.appendChild(clipElement); // 更新时间轴滑块的最大值 timelineScrubber.max = totalDuration; // 自动滚动到最新添加的片段 timeline.scrollLeft = timeline.scrollWidth; // 更新预览视频 updatePreviewVideo(); } // 更新预览视频 function updatePreviewVideo() { if (timelineClips.length === 0) { previewVideo.src = ''; return; } // 设置预览视频为当前片段 seekToTime(currentTime); } // 跳转到指定片段 function seekToClip(index) { if (index < 0 || index >= timelineClips.length) return; currentClipIndex = index; currentTime = timelineClips[index].startTime; timelineScrubber.value = currentTime; // 更新预览视频 updateTimelineIndicator(); loadCurrentClip(); } // 跳转到指定时间 function seekToTime(time) { currentTime = time; // 找到对应的片段 let foundIndex = -1; for (let i = 0; i < timelineClips.length; i++) { const clip = timelineClips[i]; if (time >= clip.startTime && time < clip.startTime + clip.duration) { foundIndex = i; break; } } if (foundIndex !== -1) { currentClipIndex = foundIndex; loadCurrentClip(); } } // 加载当前片段 function loadCurrentClip() { if (currentClipIndex < 0 || currentClipIndex >= timelineClips.length) return; const clip = timelineClips[currentClipIndex]; previewVideo.src = clip.video.url; // 设置视频起始时间 const clipLocalTime = currentTime - clip.startTime; previewVideo.currentTime = clipLocalTime / 1000; // 转换为秒 // 高亮当前片段 document.querySelectorAll('.video-clip').forEach(el => { el.classList.remove('active'); }); const activeClip = document.querySelector(`.video-clip[data-id="${clip.id}"]`); if (activeClip) { activeClip.classList.add('active'); } // 如果正在播放,继续播放 if (isPlaying) { previewVideo.play(); } } // 更新时间轴指示器位置 function updateTimelineIndicator() { // 计算指示器位置 const position = (currentTime / 1000) * 30; // 30px 每秒 timelineIndicator.style.left = position + 'px'; // 确保时间轴可见 const timelineRect = timeline.getBoundingClientRect(); const indicatorLeft = position; if (indicatorLeft < timeline.scrollLeft) { timeline.scrollLeft = indicatorLeft; } else if (indicatorLeft > timeline.scrollLeft + timelineRect.width - 10) { timeline.scrollLeft = indicatorLeft - timelineRect.width + 10; } } // 播放预览 function playPreview() { if (timelineClips.length === 0) return; isPlaying = true; previewVideo.play(); // 清除之前的定时器 if (playTimer) clearInterval(playTimer); // 设置定时器更新进度 playTimer = setInterval(function() { if (!isPlaying) return; // 更新当前时间 const clip = timelineClips[currentClipIndex]; const clipLocalTime = previewVideo.currentTime * 1000; // 转换为毫秒 currentTime = clip.startTime + clipLocalTime; // 更新时间轴滑块 timelineScrubber.value = currentTime; // 更新时间轴指示器 updateTimelineIndicator(); // 检查是否需要切换到下一个片段 if (clipLocalTime >= clip.duration) { if (currentClipIndex < timelineClips.length - 1) { // 切换到下一个片段 currentClipIndex++; loadCurrentClip(); } else { // 播放结束 pausePreview(); currentTime = 0; timelineScrubber.value = 0; currentClipIndex = 0; loadCurrentClip(); } } }, 50); } // 暂停预览 function pausePreview() { isPlaying = false; previewVideo.pause(); if (playTimer) { clearInterval(playTimer); playTimer = null; } } // 格式化时间(毫秒 -> MM:SS) function formatDuration(ms) { const seconds = Math.floor(ms / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`; } // 事件监听 playBtn.addEventListener('click', playPreview); pauseBtn.addEventListener('click', pausePreview); clearTimelineBtn.addEventListener('click', function() { pausePreview(); timelineClips.length = 0; totalDuration = 0; currentTime = 0; currentClipIndex = 0; timelineScrubber.value = 0; timelineScrubber.max = 100; timeline.innerHTML = '<div class="timeline-indicator" id="timelineIndicator"></div>'; timelineIndicator = document.getElementById('timelineIndicator'); previewVideo.src = ''; }); // 时间轴滑块事件 timelineScrubber.addEventListener('input', function() { currentTime = parseFloat(this.value); updateTimelineIndicator(); // 如果正在播放,暂停播放 if (isPlaying) { pausePreview(); } }); timelineScrubber.addEventListener('change', function() { currentTime = parseFloat(this.value); seekToTime(currentTime); }); // 视频结束事件 previewVideo.addEventListener('ended', function() { if (currentClipIndex < timelineClips.length - 1) { // 切换到下一个片段 currentClipIndex++; loadCurrentClip(); } else { // 播放结束 pausePreview(); currentTime = 0; timelineScrubber.value = 0; currentClipIndex = 0; loadCurrentClip(); } }); // 点击时间轴跳转 timeline.addEventListener('click', function(e) { if (e.target === timeline || e.target === timelineIndicator) { const rect = timeline.getBoundingClientRect(); const clickPosition = e.clientX - rect.left + timeline.scrollLeft; const clickTime = (clickPosition / 30) * 1000; // 转换为毫秒 if (clickTime >= 0 && clickTime <= totalDuration) { currentTime = clickTime; timelineScrubber.value = currentTime; seekToTime(currentTime); } } }); }); </script> </body> </html>
网友回复
为啥所有的照片分辨率提升工具都会修改照片上的图案细节?
js如何在浏览器中将webm视频的声音分离为单独音频?
微信小程序如何播放第三方域名url的mp4视频?
ai多模态大模型能实时识别视频中的手语为文字吗?
如何远程调试别人的chrome浏览器获取调试信息?
为啥js打开新网页window.open设置窗口宽高无效?
浏览器中js的navigator.mediaDevices.getDisplayMedia屏幕录像无法录制SpeechSynthesisUtterance产生的说话声音?
js中mediaRecorder如何录制window.speechSynthesis声音音频并下载?
python如何直接获取抖音短视频的音频文件url?
js在浏览器中如何使用MediaStream与MediaRecorder实现声音音频多轨道混流?