如何生成视频缩略图并在鼠标悬停进度条时预览它们?

6
我希望能够生成视频缩略图,并在鼠标悬停在进度条上时预览它,就像YouTube视频一样。

1

我曾尝试使用videojs-thumbnails进行测试,但失败了。 README 文件中没有足够的信息来修复它。
我还尝试使用关键词video thumbnail progress bar在Google上搜索。 在SO上有一些相关问题,但我找不到解决此问题的方法。
我发现一个JavaScript库videojs,其中包含进度条上的事件悬停:
videojs('video').ready(function () {
    $(document).on('mousemove', '.vjs-progress-control', function() { 
        // How can I generate video thumbnails and preview them here?
    });
});

1
请查看plyr.io...它支持从精灵表中显示缩略图 - 简单设置和部署。 - Offbeatmammal
我想在 React JS 中使用 React-Player 来实现相同的功能。是否有任何想法如何实现? - Muhammad Usama Rabani
3个回答

9

目前(2019年12月),并没有太多支持在视频进度条上悬停时添加缩略图的Javascript库(免费和付费版本均有)。

但是您可以遵循 videojs 的路线。他们已经支持在视频进度条上悬停时添加工具提示。您所能做的一切就是生成视频缩略图并将它们添加到控制栏以进行预览。

在这个例子中,我们将解释如何从 <input type="file" /> 生成视频缩略图。虽然我们可以使用带有直接链接的视频源,在测试期间,我们使用 canvas.toDataURL() 会有一些问题,导致出现 tainted canvases may not be exported

videojs 完成初始化后,您可以从源代码克隆一个新视频并将其附加到正文中。只需播放并捕获 loadeddata 事件:

videojs('video').ready(function () {
    var that = this;

    var videoSource = this.player_.children_[0];

    var video = $(videoSource).clone().css('display', 'none').appendTo('body')[0];

    video.addEventListener('loadeddata', async function() { 
        // asynchronous code...
    });

    video.play();
});

像YouTube视频缩略图一样,我们将生成一个作为图片的缩略图文件。此图片的大小为:

(水平项数*缩略图宽度)x(垂直项数*缩略图高度) = (5*158)x(5*90)

因此,790x450 是包含25个子缩略图(YouTube使用158作为缩略图宽度和 90 作为缩略图高度)的图片的尺寸。示例如下:

1

然后,我们会根据视频时长生成视频快照。在这个例子中,我们每秒生成一个缩略图。

由于生成视频缩略图需要基于视频时长和质量花费较长时间,因此我们可以制作一个默认的黑色主题缩略图来等待。

.vjs-control-bar .vjs-thumbnail {
  position: absolute;
  width: 158px;
  height: 90px;
  top: -100px;
  background-color: #000;
  display: none;
}

在获取视频时长后:
var duration = parseInt(that.duration());

我们需要在使用循环之前将其解析为int,因为该值可能是14.036
其他内容是:设置新视频的currentTime值并将视频转换为画布。
因为默认情况下1个画布元素最多可以包含25个缩略图,所以我们必须逐个将25个缩略图添加到画布中(从左到右,从上到下)。然后我们将它存储到数组中。
如果仍有另一个缩略图,则创建另一个画布并重复此操作。

var thumbnails = [];

var thumbnailWidth = 158;
var thumbnailHeight = 90;
var horizontalItemCount = 5;
var verticalItemCount = 5;

var init = function () {
    videojs('video').ready(function() {
        var that = this;

        var videoSource = this.player_.children_[0];

        var video = $(videoSource).clone().css('display', 'none').appendTo('body')[0];

        // videojs element
        var root = $(videoSource).closest('.video-js');

        // control bar element
        var controlBar = root.find('.vjs-control-bar');

        // thumbnail element
        controlBar.append('<div class="vjs-thumbnail"></div>');

        //
        controlBar.on('mousemove', '.vjs-progress-control', function() {
            // getting time 
            var time = $(this).find('.vjs-mouse-display .vjs-time-tooltip').text();

            // 
            var temp = null;

            // format: 09
            if (/^\d+$/.test(time)) {
                // re-format to: 0:0:09
                time = '0:0:' + time;
            } 
            // format: 1:09
            else if (/^\d+:\d+$/.test(time)) {
                // re-format to: 0:1:09
                time = '0:' + time;
            }

            //
            temp = time.split(':');

            // calculating to get seconds
            time = (+temp[0]) * 60 * 60 + (+temp[1]) * 60 + (+temp[2]);

            //
            for (var item of thumbnails) {
                //
                var data = item.sec.find(x => x.index === time);

                // thumbnail found
                if (data) {
                    // getting mouse position based on "vjs-mouse-display" element
                    var position = controlBar.find('.vjs-mouse-display').position();

                    // updating thumbnail css
                    controlBar.find('.vjs-thumbnail').css({
                        'background-image': 'url(' + item.data + ')',
                        'background-position-x': data.backgroundPositionX,
                        'background-position-y': data.backgroundPositionY,
                        'left': position.left + 10,
                        'display': 'block'
                    });

                    // exit
                    return;
                }
            }
        });

        // mouse leaving the control bar
        controlBar.on('mouseout', '.vjs-progress-control', function() {
            // hidding thumbnail
            controlBar.find('.vjs-thumbnail').css('display', 'none');
        });

        video.addEventListener('loadeddata', async function() {            
            //
            video.pause();

            //
            var count = 1;

            //
            var id = 1;

            //
            var x = 0, y = 0;

            //
            var array = [];

            //
            var duration = parseInt(that.duration());

            //
            for (var i = 1; i <= duration; i++) {
                array.push(i);
            }

            //
            var canvas;

            //
            var i, j;

            for (i = 0, j = array.length; i < j; i += horizontalItemCount) {
                //
                for (var startIndex of array.slice(i, i + horizontalItemCount)) {
                    //
                    var backgroundPositionX = x * thumbnailWidth;

                    //
                    var backgroundPositionY = y * thumbnailHeight;

                    //
                    var item = thumbnails.find(x => x.id === id);

                    if (!item) {
                        // 

                        //
                        canvas = document.createElement('canvas');

                        //
                        canvas.width = thumbnailWidth * horizontalItemCount;
                        canvas.height = thumbnailHeight * verticalItemCount;

                        //
                        thumbnails.push({
                            id: id,
                            canvas: canvas,
                            sec: [{
                                index: startIndex,
                                backgroundPositionX: -backgroundPositionX,
                                backgroundPositionY: -backgroundPositionY
                            }]
                        });
                    } else {
                        //

                        //
                        canvas = item.canvas;

                        //
                        item.sec.push({
                            index: startIndex,
                            backgroundPositionX: -backgroundPositionX,
                            backgroundPositionY: -backgroundPositionY
                        });
                    }

                    //
                    var context = canvas.getContext('2d');

                    //
                    video.currentTime = startIndex;

                    //
                    await new Promise(function(resolve) {
                        var event = function() {
                            //
                            context.drawImage(video, backgroundPositionX, backgroundPositionY, 
                                thumbnailWidth, thumbnailHeight);

                            //
                            x++;

                            // removing duplicate events
                            video.removeEventListener('canplay', event);

                            // 
                            resolve();
                        };

                        // 
                        video.addEventListener('canplay', event);
                    });


                    // 1 thumbnail is generated completely
                    count++;
                }

                // reset x coordinate
                x = 0;

                // increase y coordinate
                y++;

                // checking for overflow
                if (count > horizontalItemCount * verticalItemCount) {
                    //
                    count = 1;

                    //
                    x = 0;

                    //
                    y = 0;

                    //
                    id++;
                }

            }

            // looping through thumbnail list to update thumbnail
            thumbnails.forEach(function(item) {
                // converting canvas to blob to get short url
                item.canvas.toBlob(blob => item.data = URL.createObjectURL(blob), 'image/jpeg');

                // deleting unused property
                delete item.canvas;
            });

            
            
            console.log('done...');
        });

        // playing video to hit "loadeddata" event
        video.play();
    });
};

$('[type=file]').on('change', function() {
    var file = this.files[0];
    $('video source').prop('src', URL.createObjectURL(file));

    init();
});
.vjs-control-bar .vjs-thumbnail {
  position: absolute;
  width: 158px;
  height: 90px;
  top: -100px;
  background-color: #000;
  display: none;
}
<link rel="stylesheet" href="https://vjs.zencdn.net/7.5.5/video-js.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://vjs.zencdn.net/7.5.5/video.js"></script>

<input type="file" accept=".mp4" />
<video id="video" class="video-js vjs-default-skin" width="500" height="250" controls> 
    <source src="" type='video/mp4'>
</video>

代码示例


2

我在一年前也遇到了这个问题。基于这个帖子:如何为VideoJS生成视频预览缩略图?我最终决定采用离线生成缩略图的方法,因为这比尝试实时提取缩略图要容易得多。

我在这里对那场斗争进行了技术讨论/解释:http://weasel.firmfriends.us/GeeksHomePages/subj-video-and-audio.html#implementing-video-thumbnails

我的原型示例在这里:https://weasel.firmfriends.us/Private3-BB/

编辑:此外,我无法解决如何绑定到video-js中现有的seekBar,所以我添加了自己的专用滑块来查看缩略图。这个决定主要是基于需要使用“悬停”/“onMouseOver”,如果想要使用video-js的seekbar,而这些手势在触摸屏(移动设备)上不太适合。

编辑:我现在已经解决了如何绑定现有的seekBar的问题,所以我已经将该逻辑添加到上述的原型示例中。

祝好。希望这有所帮助。


0

对于任何正在寻找Angular解决方案的人。我已经发布了一个npm包,让你在视频进度条悬停时创建缩略图快照。

npm: ngx-thumbnail-video

您可以通过以下方式安装它

npm i ngx-thumbnail-video

并将其包含到您的模块中:

import { NgxThumbnailVideoModule } from 'ngx-thumbnail-video';

@NgModule({
   imports: [NgxThumbnailVideoModule]
})

并以这种方式使用:

<ngx-thumbnail-video url="assets/video.mp4" [options]="options"></ngx-thumbnail-video>

它的样子是这样的:在此输入图像描述


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接