ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JS截取视频靓丽的帧作为封面

2021-12-06 09:06:26  阅读:214  来源: 互联网

标签:canvas const 靓丽 截取 JS return video 饱和度 uint8ClampedArray


引言
最近开发的时候遇到了一个需求,截取视频第一帧作为视频的封面,结果第一帧是黑屏,所以产品提出,希望可以截取的不是黑屏颜色稍微靓丽帧的作为封面。于是我们进行步骤拆解:

截取第N秒作为视频封面。
选择合适的帧作为视频封面。

注意事项
视频地址必须同源或者是支持跨域访问。
设置视频播放时间后,再监听canplay事件。
寻找合适帧需要加载时间。

实现步骤

一、获取视频基本信息(分辨率、时长)

// 获取视频基本信息
function getVideoBasicInfo(videoSrc) {
    return new Promise((resolve, reject) => {
        const video = document.createElement('video')
        video.src = videoSrc
        // 视频一定要添加预加载
        video.preload = 'auto'
        // 视频一定要同源或者必须允许跨域
        video.crossOrigin = 'Anonymous'
        // 监听:异常
        video.addEventListener('error', error => {
            reject(error)
        })
        // 监听:加载完成基本信息,设置要播放的时常
        video.addEventListener('loadedmetadata', () => {
            const videoInfo = {
                video,
                width: video.videoWidth,
                height: video.videoHeight,
                duration: video.duration
            }
            resolve(videoInfo)
        })
    })
}

复制代码
 

二、将视频绘入canvas用以生成图片地址
这里需要等待视频canplay事件后,再截取,否则会黑屏

// 获取视频当前帧图像信息与饱和度
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')
            // 将视频对象直接绘入canvas
            ctx.drawImage(video, 0, 0, width, height)
          	// 获取图像的整体平均饱和度
            const saturation = getImageSaturation(canvas)
            const posterUrl = canvas.toDataURL('image/jpg')
            resolve({ posterUrl, saturation })
        })
    })
}
复制代码

三、“合适的帧”
这里我们产品提出需要以颜色稍微“靓丽”,经过苦思冥想,何为“靓丽”,众里寻她千百度,终于寻到“饱和度”

饱和度:色彩的饱和度(saturation)指色彩的鲜艳程度,也称作纯度。

将绘制好的canvas,通过getImageData获取到其像素数据。
将像素数据整理好成rgba形式的数据。
将rgb数据转换成hsl数据
提取hsl数据的s,即饱和度数据,求整体平均值

1、获取canvas像素数据
这里我们通过调用getImageData这个API,获取像素数据,也就是一整个画布的每个像素点的颜色。他返回的是一个Uint8ClampedArray(8位无符号整型固定数组),我们可以将其理解成为一个类数组,其每0、1、2、3位数据刚好可以对应rgba,即Uint8ClampedArray[0]可以对应上RGBA的R,以此类推,刚好可以获取整个画布的像素颜色情况。

在这里插入图片描述

// 获取一个图片的平均饱和度
function getImageSaturation(canvas) {
    const ctx = canvas.getContext('2d')
    const uint8ClampedArray = ctx.getImageData(0, 0, canvas.width, canvas.height).data
    // ....
}
复制代码

2、将Uint8ClampedArray整理成rgba形式
这里我们通过遍历,根据下标整理数据,转换成rgba形式,方便后续操作

// 封装,将无符号整形数组转换成rgba数组
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
}

// 获取一个图片的平均饱和度
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转换成HSL,并求平均值
HSL即色相、饱和度、亮度(英语:Hue, Saturation, Lightness)。
色相(H)是色彩的基本属性,就是平常所说的颜色名称,如红色、黄色等。
饱和度(S)是指色彩的纯度,越高色彩越纯,低则逐渐变灰,取0-100%的数值。
明度(V),亮度(L),取0-100%。

// 将rgb转换成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);//转换成百分比的形式
    l = Math.round(l * 100);
    return { h, s, l };
}
// 获取一个图片的平均饱和度
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秒的图片地址与饱和度

// 根据视频地址与播放时长获取图片信息与图片平均饱和度
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
        if (saturation >= targetSaturation) {
            return posterUrl
        }
    }
}
复制代码

整体代码与测试

// 获取视频基本信息
function getVideoBasicInfo(videoSrc) {
    return new Promise((resolve, reject) => {
        const video = document.createElement('video')
        video.src = videoSrc
        // 视频一定要添加预加载
        video.preload = 'auto'
        // 视频一定要同源或者必须允许跨域
        video.crossOrigin = 'Anonymous'
        // 监听:异常
        video.addEventListener('error', error => {
            reject(error)
        })
        // 监听:加载完成基本信息,设置要播放的时常
        video.addEventListener('loadedmetadata', () => {
            const videoInfo = {
                video,
                width: video.videoWidth,
                height: video.videoHeight,
                duration: video.duration
            }
            resolve(videoInfo)
        })
    })
}

// 将获取到的视频信息,转化为图片地址
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 })
        })
    })
}
// 获取一个图片的平均饱和度
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);//转换成百分比的形式
    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
}

// 根据视频地址与播放时长获取图片信息与图片平均饱和度
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
        // 判断当前饱和度是否大于等于期望的饱和度
        if (saturation >= targetSaturation) {
            return posterUrl
        }
    }
}
// 这里通过http-server将视频地址与js进行同源
const videoSrc = 'http://192.168.2.1:8081/trailer.mp4'
// 饱和度品质 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)
})

复制代码

饱和度:0

在这里插入图片描述

饱和度:10

在这里插入图片描述

饱和度:30

在这里插入图片描述

饱和度:50

在这里插入图片描述

最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http://github.crmeb.net/u/defu不胜感激 !

标签:canvas,const,靓丽,截取,JS,return,video,饱和度,uint8ClampedArray
来源: https://blog.csdn.net/qq_39221436/article/details/121739490

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有