JS截取視頻靚麗的幀作為封面

注意事項(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)上RGBAR映屋,以此類(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)
})


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市键科,隨后出現(xiàn)的幾起案子闻丑,更是在濱河造成了極大的恐慌,老刑警劉巖勋颖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嗦嗡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡饭玲,警方通過(guò)查閱死者的電腦和手機(jī)侥祭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人矮冬,你說(shuō)我怎么就攤上這事谈宛。” “怎么了胎署?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵吆录,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我琼牧,道長(zhǎng)恢筝,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任巨坊,我火速辦了婚禮撬槽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘趾撵。我一直安慰自己侄柔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布鼓寺。 她就那樣靜靜地躺著勋拟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪妈候。 梳的紋絲不亂的頭發(fā)上敢靡,一...
    開(kāi)封第一講書(shū)人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音苦银,去河邊找鬼啸胧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛幔虏,可吹牛的內(nèi)容都是我干的纺念。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼想括,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼陷谱!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瑟蜈,我...
    開(kāi)封第一講書(shū)人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤烟逊,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后铺根,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體宪躯,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年位迂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了访雪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片详瑞。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖臣缀,靈堂內(nèi)的尸體忽然破棺而出坝橡,到底是詐尸還是另有隱情,我是刑警寧澤肝陪,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布驳庭,位于F島的核電站刑顺,受9級(jí)特大地震影響氯窍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹲堂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一狼讨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧柒竞,春花似錦政供、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至稼虎,卻和暖如春衅檀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背霎俩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工哀军, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人打却。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓杉适,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親柳击。 傳聞我的和親對(duì)象是個(gè)殘疾皇子猿推,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容