美文网首页JavaScript
JS截取视频靓丽的帧作为封面

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

作者: zhao_ran | 来源:发表于2021-12-05 16:38 被阅读0次
    注意事项
    • 视频地址必须同源或者是支持跨域访问。
    • 设置视频播放时间后,再监听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]可以对应上RGBAR,以此类推,刚好可以获取整个画布的像素颜色情况。

    // 获取一个图片的平均饱和度
    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)
    })
    
    
    

    相关文章

      网友评论

        本文标题:JS截取视频靓丽的帧作为封面

        本文链接:https://www.haomeiwen.com/subject/ciaixrtx.html