javascript - 在本地 html 视频之上添加视频控件
问题描述
以下链接显示了在浏览器中播放本地视频的一个很好的示例:
http://jsfiddle.net/dsbonev/cCCZ2/
<h1>HTML5 local video file player example</h1>
<div id="message"></div>
<input type="file" accept="video/*"/>
<video controls autoplay></video>
但是,除此之外,我希望允许用户创建他们视频特定片段的“预告片”。在这方面,我想要某种可调节的播放头,如下所示:
这是一个做同样事情的网站:https ://www.flexclip.com/editor/app?ratio=landscape 。任何关于我将如何构建这样的东西来播放本地文件并允许我选择它的片段的任何见解?
解决方案
有一些不同的东西,虽然特定的问题似乎是如何绘制本地视频的预览栏。
本地文件
从您问题中的示例中,您可以看到使用本地文件与使用远程文件并没有太大区别;您可以通过创建本地文件的对象 URL,URL.createObjectURL
然后像使用普通视频链接一样使用它。
绘制预览栏
绘制预览栏应该包括寻找视频中的适当时间,然后将该帧绘制到画布上。.currentTime
您可以通过设置视频的属性并等待seeked
事件来寻找特定点。然后,您可以使用canvasContext.drawImage
方法使用视频元素绘制图像。
我还建议为此使用未附加到 DOM 的视频元素,以获得更好的用户体验,并且屏幕上的视频不会跳转到不同的时间。
它可能看起来像这样:
function goToTime(video, time) {
return new Promise ((resolve) => {
function cb () {
video.removeEventListener('seeked', cb);
resolve();
}
video.addEventListener('seeked', cb);
video.currentTime = time;
});
}
async function drawPreviewBar(video) {
const { duration, videoWidth, videoHeight } = video;
const previewBarFrameWidth = previewBarHeight * videoWidth / videoHeight;
const previewBarFrames = previewBarWidth / previewBarFrameWidth;
for (let i = 0; i < previewBarFrames; i++) {
await goToTime(video, i * duration * previewBarFrameWidth / previewBarWidth);
previewBarContext.drawImage(video, 0, 0,
videoWidth, videoHeight,
previewBarFrameWidth * i, 0,
previewBarFrameWidth, previewBarHeight
);
}
}
调整/移动选定剪辑的大小
如果要创建 UI 以调整剪辑大小或在剪辑周围移动,可以使用标准拖动处理程序。
例子
这是一个基本示例:
const previewBarWidth = 600;
const previewBarHeight = 40;
const videoElement = document.getElementById('video');
const inputElement = document.getElementById('file-picker');
const invisibleVideo = document.createElement('video');
const previewBarCanvas = document.getElementById('preview-bar-canvas');
previewBarCanvas.width = previewBarWidth;
previewBarCanvas.height = previewBarHeight;
const selector = document.getElementById('selector');
const previewBarContext = previewBarCanvas.getContext('2d');
const topHandle = document.getElementById('selector-top-handle');
const leftHandle = document.getElementById('selector-left-handle');
const rightHandle = document.getElementById('selector-right-handle');
const leftMask = document.getElementById('selector-left-mask');
const rightMask = document.getElementById('selector-right-mask');
var selectorLeft = 0, selectorWidth = previewBarWidth;
var minimumPreviewBarWidth = 80; // may want to dynamically change this based on video duration
var videoLoaded = false;
function goToTime(video, time) {
return new Promise ((resolve) => {
function cb () {
video.removeEventListener('seeked', cb);
resolve();
}
video.addEventListener('seeked', cb);
video.currentTime = time;
});
}
async function drawPreviewBar(video) {
const { duration, videoWidth, videoHeight } = video;
const previewBarFrameWidth = previewBarHeight * videoWidth / videoHeight;
const previewBarFrames = previewBarWidth / previewBarFrameWidth;
for (let i = 0; i < previewBarFrames; i++) {
await goToTime(video, i * duration * previewBarFrameWidth / previewBarWidth);
previewBarContext.drawImage(video, 0, 0, videoWidth, videoHeight, previewBarFrameWidth * i, 0, previewBarFrameWidth, previewBarHeight);
}
}
function loadVideo(file) {
var src = URL.createObjectURL(file);
var loaded = new Promise(function (resolve) {
invisibleVideo.addEventListener('loadedmetadata', resolve);
}).then(function () {
videoLoaded = true;
document.body.classList.add('loaded');
return drawPreviewBar(invisibleVideo);
});
videoElement.src = src;
invisibleVideo.src = src;
return loaded;
}
function updateSelectorBar() {
selector.style.width = selectorWidth + 'px';
selector.style.left = selectorLeft + 'px';
leftMask.style.width = selectorLeft + 'px';
rightMask.style.left = (selectorLeft + selectorWidth) + 'px';
rightMask.style.width = (previewBarWidth - selectorWidth - selectorLeft) + 'px';
}
function selectorUpdated() {
if (!videoLoaded) {
return;
}
var startFraction = selectorLeft / previewBarWidth;
var durationFraction = selectorWidth / previewBarWidth;
var startTime = startFraction * invisibleVideo.duration;
var duration = durationFraction * invisibleVideo.duration;
var endTime = startTime + duration;
// do something with startTime, endTime, and duration, maybe;
videoElement.currentTime = startTime;
}
function addDragHandler (event, cb, ecb) {
var startX = event.clientX;
function dragged(e) {
cb(e.clientX - startX);
}
window.addEventListener('mousemove', dragged);
window.addEventListener('mouseup', function ended() {
window.removeEventListener('mousemove', dragged);
window.removeEventListener('mouseup', ended);
ecb();
});
}
updateSelectorBar();
topHandle.addEventListener('mousedown', function (e) {
var startLeft = selectorLeft;
addDragHandler(e, function (dx) {
selectorLeft = Math.max(0, Math.min(previewBarWidth - selectorWidth, startLeft + dx));
updateSelectorBar();
}, selectorUpdated);
});
leftHandle.addEventListener('mousedown', function (e) {
var startLeft = selectorLeft;
var startWidth = selectorWidth;
addDragHandler(e, function (dx) {
selectorLeft = Math.max(0, Math.min(selectorLeft + selectorWidth - minimumPreviewBarWidth, startLeft + dx));
selectorWidth = (startWidth + startLeft - selectorLeft);
updateSelectorBar();
}, selectorUpdated);
});
rightHandle.addEventListener('mousedown', function (e) {
var startWidth = selectorWidth;
addDragHandler(e, function (dx) {
selectorWidth = Math.max(minimumPreviewBarWidth, Math.min(previewBarWidth - selectorLeft, startWidth + dx));
updateSelectorBar();
}, selectorUpdated);
});
var pendingLoad = Promise.resolve();
inputElement.addEventListener('change', function () {
let file = inputElement.files[0];
pendingLoad = pendingLoad.then(function () {
return loadVideo(file)
});
});
#video {
width: 100%;
height: auto;
}
#preview-bar {
position: relative;
margin-top: 10px;
}
.canvas-container {
position: relative;
left: 5px;
}
#preview-bar-canvas {
position: relative;
top: 2px;
}
#selector {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 100%;
border: 2px solid orange;
border-left-width: 5px;
border-right-width: 5px;
border-radius: 3px;
overflow: show;
opacity: 0;
transition: opacity 1s;
pointer-events: none;
}
.loaded #selector{
opacity: 1;
pointer-events: initial;
}
#selector-top-handle {
position: absolute;
top: 0;
height: 10px;
width: 100%;
max-width: 30px;
left: 50%;
transform: translate(-50%,-100%);
background: darkgreen;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
cursor: move;
}
.resize-handle {
position: absolute;
top: 0;
bottom: 0;
width: 5px;
cursor: ew-resize;
background: darkgreen;
opacity: 0.75;
transition: opacity 1s;
}
.resize-handle:hover {
opacity: 1;
}
.preview-mask {
position: absolute;
background: black;
opacity: 0.6;
top: 0;
bottom: 0;
}
#selector-left-mask {
left: 0;
}
#selector-left-handle {
left: -5px;
}
#selector-right-handle {
right: -5px;
}
<input type="file" id="file-picker" />
<video id="video"></video>
<div id="preview-bar">
<div class="canvas-container">
<canvas id="preview-bar-canvas"></canvas>
<div id="selector-left-mask" class="preview-mask"></div>
<div id="selector-right-mask" class="preview-mask"></div>
</div>
<div id ="selector">
<div id="selector-top-handle"></div>
<div id="selector-left-handle" class="resize-handle"></div>
<div id="selector-right-handle" class="resize-handle"></div>
</div>
</div>
推荐阅读
- reactjs - 基于用户提供的配置为我的 React 组件(一个 NPM 包)派生类型?
- ruby-on-rails - 如何在一个视图中为一个模型创建多个表单
- python - 停止python中所有其他函数的函数
- json - 是否有任何用于动态键的嵌套 json 解析库?
- node.js - 为什么快递中间件不能在单独的文件中工作
- lucene - Lucene 7 得分与 Lucene 4
- android - 提取“.aab”文件时出错:dex 文件索引无效,需要文件“classes٢.dex”但找到“classes2.dex”
- asp.net - 发布 .NET Core 项目后无法访问错误
- python - 我需要在 Python 中使用 PWM 控制 2 个电机,用于树莓派 4。我使用 sys.argv [1]。PWM 必须在 while 循环内
- css - Tailwind CSS:如何在填充中使用自定义百分比?