【问题标题】:How to generate video thumbnails and preview them while hovering on the progress bar?如何在进度条上悬停时生成视频缩略图并预览它们?
【发布时间】:2020-04-14 03:00:04
【问题描述】:

我想生成视频缩略图并在像 YouTube 视频一样将鼠标悬停在进度条上进行预览:

我尝试使用videojs-thumbnails 进行测试,但失败了。 README 文件没有包含足够的信息来修复它。

我也尝试在 Google 上搜索关键字:video thumbnail progress bar。有一些关于 SO 的相关问题,但我找不到这个案例的解决方案。

我找到了一个 JavaScript 库 videojs,其中包含悬停在​​进度条上的事件:

videojs('video').ready(function () {
    $(document).on('mousemove', '.vjs-progress-control', function() { 
        // How can I generate video thumbnails and preview them here?
    });
});

【问题讨论】:

  • 看看 plyr.io ...它支持从 spritesheet 显示缩略图 - 易于设置和部署
  • 我想用 react-player 在 react js 中实现相同的功能。有什么想法可以实现吗?

标签: javascript jquery video youtube video.js


【解决方案1】:

目前(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 视频缩略图一样,我们会将缩略图文件生成为图像。此图片的尺寸:

(horizontalItemCount*thumbnailWidth)x(verticalItemCount*thumbnailHeight) = (5*158)x(5*90)

所以790x450 是包含 25 个子缩略图的图像大小(YouTube 使用 158 作为缩略图的宽度,90 作为缩略图的高度)。像这样:

然后,我们将根据视频时长拍摄视频快照。在这个例子中,我们每秒生成一个缩略图(每一秒都有一个缩略图)

由于根据视频时长和质量生成视频缩略图需要很长时间,所以我们可以制作一个带有深色主题的默认缩略图等待。

.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 个 canvas 元素默认最多可以包含 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>

Fiddle

【讨论】:

    【解决方案2】:

    大约一年前,我在这个确切的问题上苦苦挣扎。基于此处的线程:如何生成用于 VideoJS 的视频预览缩略图?我最终决定使用离线生成缩略图,因为它比尝试即时提取它们要容易得多。

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

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

    编辑:另外,我无法解决如何绑定到 video-js 中现有的 seekBar,所以我添加了自己的专用滑块来查看缩略图。这一决定主要是基于如果想要使用 video-js 的搜索栏需要使用“悬停”/“onMouseOver”,而这些手势不能很好地转化为触摸屏(移动设备)。

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

    干杯。希望这会有所帮助。

    【讨论】:

      【解决方案3】:

      对于正在寻找 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>
      

      长什么样子:

      【讨论】:

        猜你喜欢
        • 2020-06-29
        • 2019-01-25
        • 2019-03-24
        • 2023-04-09
        • 2017-09-13
        • 1970-01-01
        • 2013-01-28
        • 2011-11-04
        相关资源
        最近更新 更多