注意事項(xiàng)
- 視頻地址必須同源或者是支持跨域訪(fǎng)問(wèn)哩俭。
- 設(shè)置視頻播放時(shí)間后,再監(jiān)聽(tīng)canplay事件拳恋。
- 尋找合適幀需要加載時(shí)間凡资。
實(shí)現(xiàn)步驟
一、獲取視頻基本信息(分辨率谬运、時(shí)長(zhǎng))
// 獲取視頻基本信息
function getVideoBasicInfo(videoSrc) {
return new Promise((resolve, reject) => {
const video = document.createElement('video')
video.src = videoSrc
// 視頻一定要添加預(yù)加載
video.preload = 'auto'
// 視頻一定要同源或者必須允許跨域
video.crossOrigin = 'Anonymous'
// 監(jiān)聽(tīng):異常
video.addEventListener('error', error => {
reject(error)
})
// 監(jiān)聽(tīng):加載完成基本信息,設(shè)置要播放的時(shí)常
video.addEventListener('loadedmetadata', () => {
const videoInfo = {
video,
width: video.videoWidth,
height: video.videoHeight,
duration: video.duration
}
resolve(videoInfo)
})
})
}
二隙赁、將視頻繪入canvas用以生成圖片地址
這里需要等待視頻canplay
事件后好渠,再截取芍锚,否則會(huì)黑屏
// 獲取視頻當(dāng)前幀圖像信息與飽和度
function getVideoPosterInfo(videoInfo) {
return new Promise(resolve => {
const { video, width, height } = videoInfo
video.addEventListener('canplay', () => {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
// 將視頻對(duì)象直接繪入canvas
ctx.drawImage(video, 0, 0, width, height)
// 獲取圖像的整體平均飽和度
const saturation = getImageSaturation(canvas)
const posterUrl = canvas.toDataURL('image/jpg')
resolve({ posterUrl, saturation })
})
})
}
三擂啥、“合適的幀”
這里我們產(chǎn)品提出需要以顏色稍微“靚麗”耐亏,經(jīng)過(guò)苦思冥想,何為“靚麗”蹲坷,眾里尋她千百度绿聘,終于尋到“飽和度”
飽和度:色彩的飽和度(saturation)指色彩的鮮艷程度爬凑,也稱(chēng)作純度级解。
- 將繪制好的
canvas
冒黑,通過(guò)getImageData
獲取到其像素?cái)?shù)據(jù)。 - 將像素?cái)?shù)據(jù)整理好成
rgba
形式的數(shù)據(jù)勤哗。 - 將
rgb
數(shù)據(jù)轉(zhuǎn)換成hsl
數(shù)據(jù) - 提取
hsl
數(shù)據(jù)的s
抡爹,即飽和度數(shù)據(jù),求整體平均值
1芒划、獲取canvas像素?cái)?shù)據(jù)
這里我們通過(guò)調(diào)用getImageData
這個(gè)API
冬竟,獲取像素?cái)?shù)據(jù),也就是一整個(gè)畫(huà)布的每個(gè)像素點(diǎn)的顏色民逼。他返回的是一個(gè)Uint8ClampedArray
(8位無(wú)符號(hào)整型固定數(shù)組),我們可以將其理解成為一個(gè)類(lèi)數(shù)組泵殴,其每0、1缴挖、2袋狞、3位數(shù)據(jù)剛好可以對(duì)應(yīng)rgba
,即Uint8ClampedArray[0]
可以對(duì)應(yīng)上RGBA
的R
映屋,以此類(lèi)推苟鸯,剛好可以獲取整個(gè)畫(huà)布的像素顏色情況。
// 獲取一個(gè)圖片的平均飽和度
function getImageSaturation(canvas) {
const ctx = canvas.getContext('2d')
const uint8ClampedArray = ctx.getImageData(0, 0, canvas.width, canvas.height).data
// ....
}
2棚点、將Uint8ClampedArray整理成rgba形式
這里我們通過(guò)遍歷早处,根據(jù)下標(biāo)整理數(shù)據(jù),轉(zhuǎn)換成rgba形式瘫析,方便后續(xù)操作
// 封裝砌梆,將無(wú)符號(hào)整形數(shù)組轉(zhuǎn)換成rgba數(shù)組
function binary2rgba(uint8ClampedArray) {
const rgbaList = []
for (let i = 0; i < uint8ClampedArray.length; i++) {
if (i % 4 === 0) {
rgbaList.push({ r: uint8ClampedArray[i] })
continue
}
const currentRgba = rgbaList[rgbaList.length - 1]
if (i % 4 === 1) {
currentRgba.g = uint8ClampedArray[i]
continue
}
if (i % 4 === 2) {
currentRgba.b = uint8ClampedArray[i]
continue
}
if (i % 4 === 3) {
currentRgba.a = uint8ClampedArray[i]
continue
}
}
return rgbaList
}
// 獲取一個(gè)圖片的平均飽和度
function getImageSaturation(canvas) {
const ctx = canvas.getContext('2d')
const uint8ClampedArray = ctx.getImageData(0, 0, canvas.width, canvas.height).data
const rgbaList = binary2rgba(uint8ClampedArray)
// ....
}
3、將RGB轉(zhuǎn)換成HSL贬循,并求平均值
HSL即色相咸包、飽和度、亮度(英語(yǔ):Hue, Saturation, Lightness)杖虾。色相(H)是色彩的基本屬性烂瘫,就是平常所說(shuō)的顏色名稱(chēng),如紅色奇适、黃色等坟比。飽和度(S)是指色彩的純度,越高色彩越純嚷往,低則逐漸變灰葛账,取0-100%的數(shù)值。明度(V)皮仁,亮度(L)籍琳,取0-100%。
// 將rgb轉(zhuǎn)換成hsl
function rgb2hsl(r, g, b) {
r = r / 255;
g = g / 255;
b = b / 255;
var min = Math.min(r, g, b);
var max = Math.max(r, g, b);
var l = (min + max) / 2;
var difference = max - min;
var h, s, l;
if (max == min) {
h = 0;
s = 0;
} else {
s = l > 0.5 ? difference / (2.0 - max - min) : difference / (max + min);
switch (max) {
case r: h = (g - b) / difference + (g < b ? 6 : 0); break;
case g: h = 2.0 + (b - r) / difference; break;
case b: h = 4.0 + (r - g) / difference; break;
}
h = Math.round(h * 60);
}
s = Math.round(s * 100);//轉(zhuǎn)換成百分比的形式
l = Math.round(l * 100);
return { h, s, l };
}
// 獲取一個(gè)圖片的平均飽和度
function getImageSaturation(canvas) {
const ctx = canvas.getContext('2d')
const uint8ClampedArray = ctx.getImageData(0, 0, canvas.width, canvas.height).data
const rgbaList = binary2rgba(uint8ClampedArray)
const hslList = rgbaList.map(item => {
return rgb2hsl(item.r, item.g, item.b)
})
// 求平均值
const avarageSaturation = hslList.reduce((total, curr) => total + curr.s, 0) / hslList.length
return avarageSaturation
}
四贷祈、傳入視頻地址與第N秒巩割,獲取第N秒的圖片地址與飽和度
// 根據(jù)視頻地址與播放時(shí)長(zhǎng)獲取圖片信息與圖片平均飽和度
function getVideoPosterByFrame(videoSrc, targetTime) {
return getVideoBasicInfo(videoSrc).then(videoInfo => {
const { video, duration } = videoInfo
video.currentTime = targetTime
return getVideoPosterInfo(videoInfo)
})
}
五、傳入視頻地址與指定飽和度品質(zhì)付燥,截取指定飽和度的視頻作為封面
async function getBestPoster(videoSrc, targetSaturation) {
const videoInfo = await getVideoBasicInfo(videoSrc)
const { duration } = videoInfo
for (let i = 0; i <= duration; i++) {
const posterInfo = await getVideoPosterByFrame(videoSrc, i)
const { posterUrl, saturation } = posterInfo
if (saturation >= targetSaturation) {
return posterUrl
}
}
}
整體代碼與測(cè)試
// 獲取視頻基本信息
function getVideoBasicInfo(videoSrc) {
return new Promise((resolve, reject) => {
const video = document.createElement('video')
video.src = videoSrc
// 視頻一定要添加預(yù)加載
video.preload = 'auto'
// 視頻一定要同源或者必須允許跨域
video.crossOrigin = 'Anonymous'
// 監(jiān)聽(tīng):異常
video.addEventListener('error', error => {
reject(error)
})
// 監(jiān)聽(tīng):加載完成基本信息,設(shè)置要播放的時(shí)常
video.addEventListener('loadedmetadata', () => {
const videoInfo = {
video,
width: video.videoWidth,
height: video.videoHeight,
duration: video.duration
}
resolve(videoInfo)
})
})
}
// 將獲取到的視頻信息宣谈,轉(zhuǎn)化為圖片地址
function getVideoPosterInfo(videoInfo) {
return new Promise(resolve => {
const { video, width, height } = videoInfo
video.addEventListener('canplay', () => {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0, width, height)
const saturation = getImageSaturation(canvas)
const posterUrl = canvas.toDataURL('image/jpg')
resolve({ posterUrl, saturation })
})
})
}
// 獲取一個(gè)圖片的平均飽和度
function getImageSaturation(canvas) {
const ctx = canvas.getContext('2d')
const uint8ClampedArray = ctx.getImageData(0, 0, canvas.width, canvas.height).data
console.log(uint8ClampedArray)
const rgbaList = binary2rgba(uint8ClampedArray)
const hslList = rgbaList.map(item => {
return rgb2hsl(item.r, item.g, item.b)
})
const avarageSaturation = hslList.reduce((total, curr) => total + curr.s, 0) / hslList.length
return avarageSaturation
}
function rgb2hsl(r, g, b) {
r = r / 255;
g = g / 255;
b = b / 255;
var min = Math.min(r, g, b);
var max = Math.max(r, g, b);
var l = (min + max) / 2;
var difference = max - min;
var h, s, l;
if (max == min) {
h = 0;
s = 0;
} else {
s = l > 0.5 ? difference / (2.0 - max - min) : difference / (max + min);
switch (max) {
case r: h = (g - b) / difference + (g < b ? 6 : 0); break;
case g: h = 2.0 + (b - r) / difference; break;
case b: h = 4.0 + (r - g) / difference; break;
}
h = Math.round(h * 60);
}
s = Math.round(s * 100);//轉(zhuǎn)換成百分比的形式
l = Math.round(l * 100);
return { h, s, l };
}
function binary2rgba(uint8ClampedArray) {
const rgbaList = []
for (let i = 0; i < uint8ClampedArray.length; i++) {
if (i % 4 === 0) {
rgbaList.push({ r: uint8ClampedArray[i] })
continue
}
const currentRgba = rgbaList[rgbaList.length - 1]
if (i % 4 === 1) {
currentRgba.g = uint8ClampedArray[i]
continue
}
if (i % 4 === 2) {
currentRgba.b = uint8ClampedArray[i]
continue
}
if (i % 4 === 3) {
currentRgba.a = uint8ClampedArray[i]
continue
}
}
return rgbaList
}
// 根據(jù)視頻地址與播放時(shí)長(zhǎng)獲取圖片信息與圖片平均飽和度
function getVideoPosterByFrame(videoSrc, targetTime) {
return getVideoBasicInfo(videoSrc).then(videoInfo => {
const { video, duration } = videoInfo
video.currentTime = targetTime
return getVideoPosterInfo(videoInfo)
})
}
async function getBestPoster(videoSrc, targetSaturation) {
const videoInfo = await getVideoBasicInfo(videoSrc)
const { duration } = videoInfo
for (let i = 0; i <= duration; i++) {
const posterInfo = await getVideoPosterByFrame(videoSrc, i)
const { posterUrl, saturation } = posterInfo
// 判斷當(dāng)前飽和度是否大于等于期望的飽和度
if (saturation >= targetSaturation) {
return posterUrl
}
}
}
// 這里通過(guò)http-server將視頻地址與js進(jìn)行同源
const videoSrc = 'http://192.168.2.1:8081/trailer.mp4'
// 飽和度品質(zhì) 0/10/30/50
const targetSaturation = 0
getBestPoster(videoSrc, targetSaturation).then(posterUrl => {
const image = new Image()
image.src = posterUrl
document.body.append(image)
}).catch(error => {
console.log(error)
})