下面的代码可以实现
<!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> 网友回复


