技术标签: css java js html javascript
最近在搬砖时遇到一个问题,在商详页面有些商品只有视频,没有封面图。
我们的交互是用户点击视频封面图调用 native
播放器播放视频,没有封面图视频就没有了载体,就不能展示了。
这个问题有3个解决方案
后端处理:这种方案虽然可行,但是会影响接口性能,在商详这种关键页面得不偿失。
客户端处理:客户端处理需要在进入商详页面前预加载视频,会影响页面响应速度,也不太合理。
前端处理:前端处理必然会用到 video 标签来承载视频,考虑到 video 标签在移动端会有很多兼容性问题,处理起来很复杂,同时也会带来加载的性能消耗。
基于上述三个方案,我们决定在源头解决这个问题,在创建商品时动态获取视频封面并保存。
考虑到这是一个公共能力,我们把处理逻辑写成公共的 npm
包 video-cover
(https://www.npmjs.com/package/video-cover) ,有兴趣的可以去 npm
上下载和使用。
接下来给大家分享一下这个包的实现方案和使用方法。
首先我们看下实际使用效果。
获取视频首帧整体过程是比较流畅的,当然,具体的获取时间取决于视频的质量和网速。
动态创建 video 标签,加载视频。通过 video 的 timeupdate
事件来获取截取图片的时机。
创建 canvas 画布,通过 drawImage
来绘制图像,然后通过 toDataURL
来导出图像信息。
最后封装一些功能方法来方面使用。
canvas 对我们来说是一个熟悉又陌生的技术,在一些技术文档种经常有它的身影出现,但是业务中使用到的地方又不是很多。
要使用这个技术,首先我们来看下兼容性,兼容性不好的话再好的技术也难以运用到业务中去。
从上图看来大部分浏览器兼容性没问题,话不多说,开始上代码
首先我们要创建
video
标签,这步是关键,图像能不能截取成功就取决于视频能不能展示了。
如果你使用的是网络图片,并且是跨域的。
必须要设置 video
的 crossOrigin = 'Anonymous'
,对此元素的 CORS
请求将不设置凭据标志。
否则使用canvas的 toDataURL
api会报错 提示:
Uncaught DOMException: Failed to execute 'toDataURL' on 'HTMLCanvasElement': Tainted canvases may not be exported
大概意思就是画布被污染。
文档:参考文档[1]
getVideoCover(callback) {
const self = this;
const video = this.video || document.createElement("video");
const currentTime = self.currentTime;
video.src = self.url;
video.style.cssText = `position: fixed; top: -100%; width: 400px; visibility: hidden;`;
video.controls = "controls";
// 此处是设置跨域,防止污染canvas画布
video.crossOrigin = "Anonymous";
// 设置视频播放进度
video.currentTime = currentTime;
// 监听播放进度改变,获取对应帧的截图
video.addEventListener("timeupdate", () => {
self.setVideoInfo();
if (self.currentTime <= self.duration) {
self.generateCanvas(callback);
} else {
self.nextTime()
}
});
this.video = video;
// 此处必须要append到页面中去,否则会由兼容性问题
document.body.appendChild(video);
}
通过修改 video
的 currentTime
属性,来切换视频的进度,从而触发 timeupdate
事件,完成截图操作。
创建完 video
,接下来就是最关键的部分了,图片生成。
主要思路:
首先创建一个 canvas
。
添加对应的尺寸,这个尺寸尽量和视频的比例保持一致,否则生成的图片会变形。
通过 toDataURL
生成 base64
,直接在页面展示就可以了。
generateCanvas(callback) {
const self = this;
const canvas = this.canvas || document.createElement("canvas");
// 此处添加 alpha 属性,可以忽略透明度,减少图片体积
const ctx = canvas.getContext("2d", { alpha: false });
const imgWidth = this.imgWidth;
const isCheckImageColor = this.isCheckImageColor;
let videoWidth = this.videoWidth;
let videoHeight = this.videoHeight;
if (!this.canvas) {
if (imgWidth) {
videoHeight = imgWidth / (videoWidth / videoHeight);
videoWidth = imgWidth;
}
canvas.width = videoWidth;
canvas.height = videoHeight;
}
ctx.drawImage(this.video, 0, 0, canvas.width, canvas.height);
// 如果开启图片校验模式
if (isCheckImageColor) {
const checkImageResult = this.checkImage(ctx, videoWidth, videoHeight);
// 如果图片是纯色图片,会获取切换播放时间,获取下一秒的截图
if (!checkImageResult) {
this.nextTime();
return;
}
}
const img = canvas.toDataURL(this.imageType, self.quality);
callback && callback(img);
}
写到此处,基本的功能已经实现了,接下来就介绍一些工具方法来让我们的功能更加的完善。
图片内容校验。
有些视频的前 1s 是黑屏,这样截下来的图片是没有任何价值的,为了避免这种问题。我对视频做了是否是纯色的校验。
使用 canvas
的 getImageData
来获取图片像素信息。
getImageData
会返回一个 Uint8ClampedArray
的类型化数组。
每一个值存储的是 0-255 的整型。
每4个为一组,分别代表 R G B A。
遍历整个数组,如果发现颜色值的种类超过配置的数量,即为有图像的图,否则为纯色图。
如果大家有更好的处理方案,欢迎在评论区域留言。
checkImage(ctx, width, height) {
const imgData = ctx.getImageData(0, 0, width, height);
const imgDataContent = imgData.data;
const rgbObj = {};
let differentLen = 0;
for (let i = 0, len = imgDataContent.length; i < len; i += 4) {
const key = imgDataContent.slice(i, i + 4).join("");
if (!rgbObj[key]) {
rgbObj[key] = 1;
differentLen++;
}
// 判断如果颜色超出100种,不是纯图
if (differentLen > 100) {
return true;
}
}
return false;
}
base64
转 Blob
对象。
图片本地下载和保存到服务器都需要将base64转换成 Blob对象。
具体实现步骤为:
static base64ToBlob(code) {
if (!code) {
console.warn("base64不能为空");
return;
}
let parts = code.split(";base64,");
// 获取图片类型
let contentType = parts[0].split(":")[1];
/**
* 解码base64
* Window atob() 方法
* encodedStr: 必需,是一个通过 btoa() 方法编码的字符串。
* 该方法返回一个解码的字符串。
*/
let raw = window.atob(parts[1]);
let rawLength = raw.length;
// Uint8Array 数组类型表示一个8位无符号整型数组,创建时内容被初始化为0。
let uInt8Array = new Uint8Array(rawLength);
// 将字符转换成unicode值
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], {
type: contentType,
});
}
通过 window.atob
方法将 base64
解码。
初始化一个 Uint8Array
类型化数组。
遍历解码之后的 base64
,将每个字符转换成 Unicode
码,并 push
到 Uint8Array
中。
最后通过 new Blob()
来生成 Blob
对象。
下载文件
static downloadFile(code) {
const fileName = Date.now();
if (!code) {
console.warn("base64不能为空");
return;
}
let aLink = document.createElement("a");
let blob = this.base64ToBlob(code);
let evt = document.createEvent("HTMLEvents");
evt.initEvent("click", true, true); //initEvent 不加后两个参数在FF下会报错 事件类型,是否冒泡,是否阻止浏览器的默认行为
aLink.download = fileName;
aLink.href = URL.createObjectURL(blob);
aLink.click();
}
接下来介绍下使用方法
通过 new VideoCover()
来初始化配置信息
const cover = new VideoCover({
// 视频链接
url: 'https://cdn.huodao.hk/zhaoliangjiadv2.mp4',
// 初始截图位置,取值范围 1- 视频长度,默认为 1
currentTime: 1,
// 生成图片宽度,高度按照视频比例自动计算,默认为800px
imgWidth: 600,
// 图片质量,范围 0.2-0.95,默认为 0.95
quality: 0.9,
// 图片类型,默认为 image/jpeg
imageType: 'image/jpeg',
// 是否开始图片检查,如果为纯图自动获取下一秒,默认为false
isCheckImageColor: true,
})
api方法
生成截图
getVideoCover
参数:
@param { Function } callback 回调函数 示例:
cover.getVideoCover((res) => {
console.log(res)
})
获取下一秒视频截图 nextTime 参数:无 示例:
cover.nextTime()
获取指定位置视频截图 jumpTime 参数:@param { Number } time 时间秒数 示例:
cover.jumpTime(20)
最后试了一下,在 chrome,firefox
等主流浏览器都是可以的。
由于 video
在移动端的兼容性不是很好,此插件适用于 PC
端。
未来我们会兼容移动端,希望能在更多的平台得到运用。
本文源码:https://github.com/18823752727/video-cover
npm包地址:https://www.npmjs.com/package/video-cover
参考文档:
安全性和“被污染”的 canvas
https://developer.mozilla.org/zh-CN/docs/Web/HTML/CORS_enabled_image
Uint8Array
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array
Blob
https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/Blob
Base64的编码与解码
https://developer.mozilla.org/zh-CN/docs/Glossary/Base64
createObjectURL https://developer.mozilla.org/zh-CN/docs/Web/API/URL/createObjectURL
文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99
文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效
文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是
文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件
文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件
文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码
文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware
文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停
文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待
文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析
文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code
文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象