如何实现抖音 '回到刚刚查看的' 位置

Posted 10后程序员劝退师

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何实现抖音 '回到刚刚查看的' 位置相关的知识,希望对你有一定的参考价值。

实现效果,假如当前浏览的是第121条视频

 

首先需要先准备要用到的工具函数,和模拟接口回调函数

api.js

//模拟根据当前 id返回 该id前面有多少条视频
export function getOffset(id) 
    return new Promise(resolve => 
        resolve(121)
    )




//模拟返回一个具有分页功能的图片列表的函数
export function getVideo(pageNo, pageSize) 
    return new Promise(resolve => 
        // let page = Math.ceil(pageNo/pageSize) //Calculates the number of pages.
        let start = (pageNo - 1) * pageSize; //Calculates the start index. 
        let end = start + pageSize; //Calculates the end index. 
        let arr = []; //Declaring an array. 

        for (let i = start; i < end; i++)  //For loop to add the image to the array. 
            let obj =  id: i, cover: `https://picsum.photos/200/$i` 
            arr.push(obj)

        
        resolve(arr) //Resolve the promise.
    )

工具函数

utils.js


//防抖
export function debounce(fn, delay = 500) 
    let timer = null;

    return function () 
        if (timer) 
            clearTimeout(timer)
        
        timer = setTimeout(() => 
            fn.apply(this, arguments)
            timer = null
        , delay)
    



//根据页码,和数量获取到当前 对应的下标范围
export function getIndexRange(pageNo, pageSize) 
    const start = (pageNo - 1) * pageSize; // -1 to compensate for 0 indexing.  0-0 is 0.  So -
    const end = start + pageSize - 1; // compensate for 0-0 being 0.  So end is the same as start.  So end = start.  So
    return [start, end]



//根据下标,和数量,计算出在哪一页
export function getPage(index, size) 
    return Math.ceil((index + 1) / size)

 

主要运用逻辑在两个函数

createElement 动态创建列表节点
loadPages  加载对应索引页的内容
 
主函数页面
import  getOffset, getVideo  from \'./api.js\' 
import  debounce, getIndexRange, getPage  from \'./utils.js\'

const currentId = 100 //当前看过视频的id
const SIZE = 10  //分页的size
const continer = document.querySelector(\'.continer\')
const indicator = document.querySelector(\'.indicator\')


let visibleIndex = new Set() //用于存放出现在视口中元素的 索引值

const ob = new IntersectionObserver(entires =>     //创建一个元素监听器,监听 每个视频的item

    for(const entry of entires)
        const index = +entry.target.dataset.index;
        if(entry.isIntersecting)    // isIntersecting:表示这个元素是否出现在视口中
            visibleIndex.add(index)
         else 
            visibleIndex.delete(index)
        
    
    loadPagesDebounce() //加载这些索引对应的内容
    
)

function getRange()   //获取视口中索引值集合的 最大和最小
    if(visibleIndex.size === 0) return [0,0]
    const min = Math.min(...visibleIndex)
    const max = Math.max(...visibleIndex)
    return [min, max]


function createElement(page)    //手动创建 需要加载的所有节点
    const childrenLen = continer.children.length
    const count = page * SIZE - childrenLen;
    for (let i = 0; i < count; i++) 
        const item = document.createElement(\'div\')
        item.className = \'item\'
        item.dataset.index = i + childrenLen
        continer.appendChild(item)
        ob.observe(item)  //为所有节点添加监听器
    



function loadPages()
    //得到当前可以看到的索引范围的内容
    const [minIndex, maxIndex] = getRange()
    const pages = new Set()
    for (let i = minIndex; i <= maxIndex; i++) 
        pages.add(getPage(i, SIZE))  //获取当前视口中需要加载的页面集合pages
    
    for (const page of pages) 
        const [minIndex, maxIndex] = getIndexRange(page, SIZE)
        //   console.log(minIndex, maxIndex)
        if (continer.children[minIndex].dataset.loaded)  //防止已经加载过的重复加载
            continue;
        
        continer.children[minIndex].dataset.loaded = true

        getVideo(page, SIZE).then(async (res) =>    //循坏模拟获取数据的列表

            for (let i = minIndex; i <= maxIndex; i++) 
                const item = continer.children[i] 	//item is a div element.
                const offset = await getOffset(currentId)
                item.innerHTML = `
                    <img src="$res[i - minIndex].cover"  />
                    <span style=\'color:#f00\'>$i $item.dataset.index == offset ? \'---刚刚看过!\' : \'\'</span>
                `
            
        )
    
    setIndicatorVisible()



//判断当前位置是否需要展示刚刚看过的按钮
async function setIndicatorVisible()
    const offset = await getOffset(currentId)
    const [minIndex, maxIndex] = getRange() 	//currentId is a div element.  offset is the top left of the element.  (0,0) is the top left of
    const page = getPage(offset, SIZE)
    // console.log(minIndex, maxIndex, offset)
    if(offset>=minIndex && offset<=maxIndex)
     indicator.style.display = \'none\'	//display the indicator element.  (the div element)  (the top left of the element)  (0,
     else indicator.style.display = \'block\'	
 
    indicator.dataset.page = page
    indicator.dataset.index = offset
 

indicator.onclick = () => 
  const page = +indicator.dataset.page 	//page is a number.  + indicator.dataset.page is a string.  + indicator
  const index = +indicator.dataset.index 	//index is a number.  + indicator.dataset.index is a string
  createElement(page)
  continer.children[index].scrollIntoView(
    behavior:\'smooth\',
    block:\'center\'
  )


const loadPagesDebounce = debounce(loadPages, 300)

createElement(1) //首次先加载一页
 
html代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<style>
    /* body,html
        position: relative;
     */
    .continer
        display: grid;
        /* grid-gap: 20px; */
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 10px;
        grid-row-gap: 15px;
        /* flex-wrap: wrap; */
        padding: 0 200px;
        box-sizing: border-box;
    
    .item
        width: 200px;
        height: 300px;
        border: 1px solid #000;
        margin-bottom: 20px;
        padding-bottom: 20px;
    
    .item img
        width: 200px;
        height: 280px;
        object-fit: cover;
    
    .indicator
        position: fixed;
        left: 0;
        right: 0;
        bottom: 20px;
        width: 100px;
        margin: 0 auto;
        text-align: center;
        /* display: none; */
        background: #f00;
        color: #fff; /* for IE */
    
</style>
<body>
    <div class="continer">
        
    </div>
   <div class="indicator">前往刚刚看过</div>
</body>
<script type="module" src="./loadPages/index.js"></script>
<script type="module" src="./loadPages/api.js"></script>
</html>

 

探秘HDR:西瓜抖音是如何做到让视频的画质堪比影院大片的?

仅根据下面的两幅画面,你能辨别出,哪个是短视频,哪个是电影画面吗?

左右滑动查看更多剧照

如果你出现了分辨困难,不必怀疑自己。在技术参数上,右侧这帧来自西瓜视频的图像,在画质上的确堪比院线大片。

不止西瓜视频,眼下,字节跳动旗下的抖音、剪映也能实现电影级画质的视频制作和播放。这是怎样做到的呢?

首先认识一下HDR

视频画质绕不开5个元素:分辨率、位深、帧率、色域和亮度。其中,分辨率影响图像细节的精细程度(即清晰度),位深影响色彩渐变的精细程度,帧率体现视频动作的流畅度,色域显示视频能表达的颜色范围,亮度表示人类眼睛所能感知的最暗和最亮物体之间的差异范围。总的来说,这5个方面的技术参数越高,视觉上的表现力越好。

影响视频画质的5个元素

当前,分辨率、位深、色域和帧率方面的技术已经相对成熟,亮度成为决定视频表现力的关键因素。

这并不难理解,视觉影像本就是光与影的艺术。我们过去常常觉得视频和图片不如现实中的风景生动,主要原因就是技术无法逼真还原自然界的真实光影。

现在,行业普遍通过高动态范围成像(High Dynamic Range Imaging,简称HDRI或HDR)技术解决这一难题。

在计算机图形学与电影摄影术中,HDR是用来实现比普通数位图像技术更大的曝光动态范围(即更大的明暗差别)的一组技术。高动态范围成像的目的就是要正确地表示真实世界中从太阳光直射到最暗的阴影这样大的范围亮度。

相比于此前业内通用的SDR(Standard Dynamic Range,标准动态范围图像),HDR图像能够呈现的明暗细节更多,色彩更丰富,能够最大程度地还原真实场景。 

从iPhone12开始,HDR拍摄正逐渐成为趋势,目前已经有越来越多设备(iOS、Android、专业相机)开始支持HDR的拍摄。

在播放上,HDR视频已经广泛普及。不论是国外的 YouTube 和 Netflix,还是国内的西瓜视频、抖音等平台,都已经支持 HDR 视频播放。

但在创作层面,HDR视频的编辑、合成依然面临着不小的技术挑战。

字节跳动智能创作团队如何实现全链路支持HDR

HDR标准存在多种协议,再加上多种色域的影响,在混合多种不同格式视频的场景,如果处理不慎,就会出现生成视频颜色和亮度跳变不和谐的情况。此外,在不支持HDR显示的硬件,如果也笼统的采用HDR的处理方式,会出现过曝的现象。即使同样是HDR的视频,也会因为技术差异,显示的画质效果差异巨大,譬如由于位深不足,可能会导致某些场景下出现明显的颜色渐变分层的现象。在短视频从编辑、发布到消费的全链路处理过程中,要如何因应硬件和视频条件,还原出原生HDR视频程度的颜色和亮度,是目前HDR视频处理的难点所在。下面我们就着这几个难点问题,分别讲一下字节跳动智能创作团队是如何解决的。

视频编辑环节:

• 兼容主要的HDR标准

HDR有很多标准,也有很多扩展标准,分别适用于不同的应用场景,比较常见的主要包括:

    ○ HLG:HLG的全称是Hybrid Log Gamma,是由英国BBC和日本NHK电视台联合开发的高动态范围HDR标准。HLG不需要元数据,能后向兼容SDR,相比HDR10,即使在现有的SDR显示设备上,HLG画面也能呈现得艳丽动人。

    ○ HDR10:HDR10的全称是HDR10媒体档案,由美国消费电子协会在2016年公布。该标准建议使用2020色彩空间,感性量化(Perceptual Quantizer:PQ)和10位的位深度,本文中用PQ来表示。

    ○ Dolby Vision:Dolby Vision的中文名是杜比视界,是美国杜比实验室推出的影像画质技术,通过提升亮度、扩展动态范围来提升影像效果。它可以提升视频信号保真度,从而让图像看起来非常逼真,但主要应用于影院场景。

这里介绍日常更容易接触到的HDR标准——HLG和PQ。 

HLG和PQ的线性光曲线:

HLG视频一般亮度峰值在1000nits,而PQ的亮度峰值可达10000nits。对视频创作平台来说,只有兼容以上HDR标准的算法,才能够真正实现支持HDR视频创作。

上面介绍了主流的HDR标准,要进行高质量的HDR视频编辑还必须解决如下问题:

支持10bit位深的视频解码和渲染

HDR视频并不一定必须是10bit位深,但是10bit位深能让视频呈现出更好的色彩渐变精细度,尤其是在纯色的背景下更为明显。这需要将传统的8bit位深链路改造成10bit位深,打通全链路的10bit HDR,令HDR视频在全链路编辑中没有精度的损失,保持最佳效果。

字节跳动智能创作团队通过改造渲染链路,支持10bit位深的渲染,在视频处理的整个链路中,减少精度的丢失,最大程度还原真实的原视频效果。

左右滑动查看更多照片

左右滑动查看更多照片

左图为10bit位深,右图为8bit位深

支持色域

除了位深,为了兼容多种色域视频的混剪,字节跳动智能创作团队支持了更多的色域转换,如常见的BT709、Display P3、BT2020等。

支持Tone Mapping的转换

Tone Mapping的转换是HDR支持的关键点。显示HDR视频的主要流程如下: 

由于HDR在亮度显示上是有要求的,而目前主流显示器大部分都无法达到显示HDR亮度的标准,需要通过Tone Mapping算法,将超出的部分Mapping到对应显示器的范围,而不是直接丢弃。

Tone Mapping算法对画面最终呈现的效果起到关键作用,如果算法兼容性差,就会产生色彩偏差、细节丢失等问题。字节跳动智能创作团队通过对各种场景的反复比对、调整,优化了Tone Mapping的算法,实现不管是SDR转换成HDR还是HDR多种格式互转,最终都能呈现最接近原生相机的HDR效果。 

左右滑动查看更多照片

上图为原视频,下左为字节跳动自研的ToneMapping效果,下右为某App的ToneMapping效果

由于平台上的视频源有多种类型(如传统的SDR、HLG、PQ等),在做不同色域的视频混合编辑时,如果不去兼容不同的色域,就会出现如下情况:

左右滑动查看更多剧照

左为原视频,中为未做兼容的混剪视频,右为兼容后视频

支持特效素材HDR

传统的素材基本上都是基于sRGB的色域设计的,在HDR视频场景,直接应用到BT2020的色域上,必然存在颜色不匹配的问题。

目前抖音、西瓜线上素材的种类非常繁多,但光采用上述的Tone Mapping将SDR素材实时转换成HDR素材,也是没法完全解决所有素材的效果问题,甚至还会出现转换后效果不如之前。所以我们还采用了素材重新设计的方式,将Tone Mapping效果不好的素材重新设计。字节跳动智能创作团队也是行业内第一家支持特效素材展示HDR效果。经过这些措施,大家就能在抖音、西瓜上添加特效的时候,看到最真实的HDR效果。

目前,西瓜视频、抖音和剪映已经较好地解决了上述HDR视频编辑的难题。以抖音为例,抖音的视频编辑能力无论是在多种不同色域视频混排、还是单一HDR视频显示效果、以及ToneMapping算法的调优方面,均达到行业领先水平:

左右滑动查看更多照片

左为经抖音编辑后的HDR效果,右为传统SDR

视频消费环节:

由于HDR视频的播放对屏幕所能支持的亮度有要求,在消费环节,最大的技术调整是兼容性。字节跳动智能创作团队通过ToneMapping的方式,可以让不支持HDR视频亮度的屏幕也能展示出较好的色彩。

左右滑动查看更多照片

左为HDR效果,右为不支持HDR的手机播放HDR

值得一提的是,目前,字节跳动已经行业首家实现同一套解决方案/代码在多个平台(Windows、MacOS、iOS、Android),西瓜、剪映、抖音(开放测试中)多个业务中同时全链路支持HDR视频。上述能力已免费面向用户开放。

Mac  

Windows

伴随HDR视频序幕的拉开,相信在不远的将来,HDR直播也会实现普及。创作者可以用更接近真实的效果来展示自己的作品,为视频用户带来一个色彩斑斓的新世界。

字节跳动智能创作团队

智能创作团队是字节跳动音视频创新技术和业务中台,覆盖了计算机视觉、图形学、语音、拍摄编辑、特效、客户端、服务端工程等技术领域,在部门内部实现了前沿算法-工程系统-产品全链路的闭环,旨在以多种形式向公司内部各业务线以及外部合作客户提供业界最前沿的内容理解、内容创作、互动体验与消费的能力和行业解决方案。

目前,智能创作团队已通过字节跳动旗下的火山引擎向企业开放技术能力和服务。

以上是关于如何实现抖音 '回到刚刚查看的' 位置的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序 回到顶部 实现方法

微信小程序 回到顶部 实现方法

autoJS抖音极速版自动刷频

移动端swiper做页面切换,如何让做最后一页滑动时回到第一页

回到顶部|回到底部功能的实现(Vue)

抖音无水印视频解析php源码