JS перехватывает красивый кадр видео в качестве обложки

JavaScript

введение

Во время недавней разработки я столкнулся с необходимостью захватить первый кадр видео в качестве обложки видео, но первый кадр был черным экраном, поэтому продукт предложил, я надеюсь, что кадр с чуть более красивым цветом черный экран можно захватить как обложку. Итак разбираем по шагам:

  1. Захват N-й секунды в качестве обложки видео.
  2. Выберите подходящий кадр в качестве обложки видео.

Меры предосторожности

  • URL-адрес видео должен同源или支持跨域访问.
  • После установки времени воспроизведения видео, мониторcanplayмероприятие.
  • Поиск правильного кадра требует времени загрузки.

Этапы реализации

1. Получите основную информацию о видео (разрешение, продолжительность)

// 获取视频基本信息
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)
        })
    })
}

2. Нарисуйте видео на холсте, чтобы сгенерировать адрес изображения.

Нужно дождаться видео здесь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 })
        })
    })
}

3. «Подходящие кадры»

Здесь наш продукт должен быть слегка "красивым" по цвету.После долгих размышлений о том, что такое "красивое", мы искали ее тысячи раз, и, наконец, нашли "насыщенность"

Насыщенность: Насыщенность цвета относится к яркости цвета, также известной как чистота.

  • Нарисованный холст, черезgetImageDataвозьми像素数据.
  • Организуйте данные пикселей в данные формы rgba.
  • Преобразование данных rgb в данные hsl
  • Извлеките данные hsl, то есть данные о насыщенности, и найдите общее среднее значение.


1. Получите данные о пикселях холста

Здесь мы звонимgetImageDataЭтот API получает данные о пикселях, то есть цвет каждого пикселя всего холста. Он вернулсяUint8ClampedArray(8位无符号整型固定数组), мы можем понимать его как类数组, и каждый 0, 1, 2 и 3 бит данных может соответствовать rgba, то естьUint8ClampedArray[0]может соответствоватьRGBA的Rи так далее, просто чтобы получить цвет пикселя всего холста.
image.png

// 获取一个图片的平均饱和度
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 — это оттенок, насыщенность, яркость.
Оттенок (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
}

4. Передайте адрес видео и N-ю секунду и получите адрес изображения и насыщенность N-й секунды.

// 根据视频地址与播放时长获取图片信息与图片平均饱和度
function getVideoPosterByFrame(videoSrc, targetTime) {
    return getVideoBasicInfo(videoSrc).then(videoInfo => {
        const { video, duration } = videoInfo
        video.currentTime = targetTime
        return getVideoPosterInfo(videoInfo)
    })
}

5. Введите адрес видео и указанное качество насыщенности и перехватите видео с указанной насыщенностью в качестве обложки.

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

image.png

Насыщенность: 10

image.png

Насыщенность: 30

image.png

Насыщенность: 50

image.png

использованная литература

Энциклопедия Baidu: Насыщенность
Энциклопедия Baidu: HSL (цветной режим)
JS реализует взаимное преобразование RGB, HSL, HSB