准确记录用户观看视频内容时长
文章目錄
- 問題的產(chǎn)生
- 一、從最簡單的開始
- 二、天真可愛法
- 三、錄點法
- 四、打點法
- 五、暴力打點法
- 六、小結(jié)
- 七、大結(jié)
問題的產(chǎn)生
to be or not to be, that is a question. 不是問題解決不了,只是你自己不夠努力,當(dāng)然,也可能是你不夠聰明0.0。有效地記錄用戶觀看某一視頻的總時長,問題的來源在于用戶擁有自由意志,可以隨意對觀看的視頻進(jìn)行 快進(jìn) 快退,倍數(shù)播放等等。那么,對于要拿視頻賣錢的主兒們來說,怎么才能精確記錄下來呢?
閑話少敘,讓我們騎上心愛的小摩托,溫柔地開到主題中來。根據(jù)愛因斯坦的相對論,我把解決這個問題的方案,大致分為四種:一、天真可愛法。二、錄點法。三、打點法。四、暴力打點法。
一、從最簡單的開始
正常情況,用戶(假設(shè)是小王好了)一個視頻的播放進(jìn)度如下:設(shè) 視頻總時長為10,小王從0開始,看到6他就退出了,也就是A點,那么現(xiàn)在的觀看總時長t=6-0=6,如下圖;
也就是說視頻的播放進(jìn)度 p 和觀看時長 t 是相等的。如果小王在A點沒有退出,而是直接快進(jìn)到了8,然后又看到了9,這時才退出,那么現(xiàn)在的觀看總時長t=(6-0)+(9-8)=7;那么這時播放進(jìn)度 p(=9),和總觀看時長t(=7),就顯然不一致了。如果小王在B點,也就是8的時候開啟了 2倍速度,那么現(xiàn)在的總時長就應(yīng)該是 t= 6+1*(1/2)=6.5了。。。他和觀看進(jìn)度p(=9)之間似乎更是無情地天人永隔了。
不過,先別慌,問題不大,我們的難點其實是下面的。我們假設(shè)小王看的是一段小視頻,其中的某個片斷引起了他身體的激烈共鳴,于是他就將這個片斷重播了N次,如下圖 ,3-5這一段他看了整整30遍,然后在7的時候退出。那么這時p=7,t=7+(5-3)*29=65,這里減掉本來看的一次所以乘29。對于只關(guān)心內(nèi)容進(jìn)度的來說,重復(fù)的29次我并不關(guān)心,我想要的是數(shù)據(jù)是7。。。
本文討論的重點也是針對小王到底看了哪些內(nèi)容,而不是他重復(fù)了多少遍。
二、天真可愛法
剛開始,你也許會想,不如我拿一個小王開始播放的時間點 startPoint,再拿一個結(jié)束播放的時間點endPoint,兩者一減,得一個值 T0,再看一下進(jìn)度T1,然后看根據(jù)產(chǎn)品大人的需要,兩者之間取個最小值 或 最大值 好了。對于本文關(guān)心的問題,也就是取較小值。即 t=min(T0,T1)。考慮到用戶小王同志一次看不完,那這個數(shù)據(jù)得存服務(wù)器,aka 后臺。于是最終的計算公式為 t=min(T0,T1),其中T0=endPoint- startPoint + t0-tp,t0就是從后臺取的上次的播放時長,tp為中間視頻暫停的時長,也就是pauseTime。結(jié)束播放時將t丟給后臺。但是別忘了我們一開始提出的問題,重復(fù)播放和小王對進(jìn)度條的大氣而任性地隨意拖動。所以這種方案我們只能希望小王是個天真的人,而使用這個方案的,則是比較可愛的團(tuán)隊。
當(dāng)然如果只關(guān)心用戶實際觀看總時長(即與內(nèi)容無關(guān),重復(fù)也算上,也與倍數(shù)、播放進(jìn)度、快進(jìn)快退無關(guān)),用這種方案就可以了。下面的東西主要是針對內(nèi)容敏感的時長,也就是需要計算用戶 【播放內(nèi)容】 占 【視頻總時長】 的百分比。
三、錄點法
其實我們通過畫的進(jìn)度條圖,已經(jīng)可以發(fā)現(xiàn)一些事情的端倪。假設(shè) 上一次小王已經(jīng)看了1秒鐘,t0=1,進(jìn)入播放時直接是1,播放到3又快退到2,然后播放到4又快進(jìn)到6,看到8的時候離開。如下圖。那么他現(xiàn)在一共看的時長應(yīng)該是t=t0+(3-1)+(4-2)+(8-6),發(fā)現(xiàn)在什么了沒有,對的,需要去除重復(fù)的播放時段。我們只需要記錄下用戶播放時的進(jìn)度改變點jumpPoint,然后做成區(qū)間,對這個區(qū)間再做一個去除重復(fù),就萬事大吉了。
比如我當(dāng)前記錄了用戶的一次播放記錄為列表list ={[0,7],[1,10],[12,15]}因為[0,7]這個區(qū)間在和[1,10]有重復(fù)部分,所以我們算的時候就先對記錄的表里的數(shù)據(jù)做一次合并去重算法M,現(xiàn)在的列表就為list={[0,10],[12,15]},現(xiàn)在的總時長很明顯就是t=(10-0)+(15-12)。如下圖:
好了,我們的方案就是這么簡單,在每一次用戶發(fā)生jumpPoint的時候,記錄下當(dāng)前的點,放進(jìn)一個list表中,最后計算總時長。具體怎么做呢?仔細(xì)看看以上灑家車的幾張簡單又優(yōu)美的刻度圖,您就會有所了悟了。那就是在發(fā)生 開始播放、 快進(jìn)、快退、步進(jìn)、步退,暫停,退出等行為時,記錄下當(dāng)前視頻進(jìn)度點,為了方便,后面 快進(jìn)快退步進(jìn)步退 統(tǒng)一稱為 jumpPoint,因為他們的實質(zhì)是一樣的,而暫停、退出、結(jié)束統(tǒng)一稱為pausePoint,因為對于我們研究的問題來講,他們也是一個東西的。
好了,有了方法了,具體要怎么搞呢?就算你給我一個漂亮的機器人女友,不能不教我怎么激活呀。為了方便理解,本文用全世界都明白的java作示例。首先我們定義一個Section對象,section 有兩個字段:startPoint,和endPoint,用來記錄一次區(qū)間的發(fā)生,如section=[3,5],那么startPoint=3,endPoint=5,然后我們需要一個漂亮的列表,為了印象深刻一點,我們就叫它紅茶表list吧,用這個list來存放這些Section。看圖說話,由A點開始,視頻開始播放了,我們先來一個section0,它的startPoint就記錄下來為1(A點),section0=[1,]。假設(shè)看到4(B點)的時候,快退到C點,這時我們section0的endPoint就記錄為4(B點)section0=[1,4],同時再來一個section1,將section1的startPoint記錄為jump行為發(fā)生后的進(jìn)度3(C點)section1=[3,],假設(shè)看到5(D點)用戶又快進(jìn)到7(E點),這時就將section1的endPoint記錄為5(D點) section1=[3,5],同時將section2的startPoint記錄為7(E點) section2=[7,],9的時候pause了,記錄為section2的endPoint , section2=[7,9]。這時我們就得到了list ={[1,4],[3,5],[7,9]}這樣一個紅茶表。我們將表里的點做一次merge去重,得到list ={[1,5],[7,9]},這就是最終我們要的東西了。
好了,你說的道理我都懂了,但是臣妾真的做不到呀。這些奇奇怪怪的點要怎么錄呀?考慮到很多人會用第三方的播放sdk,或者你說我根本就拿不到這些點的數(shù)據(jù),那么您先別急,下邊還有辦法。這里先把能拿到這些點的東西講完。
拿到這些點了,你以為就完了嗎? 當(dāng)然如果正常的話,沒有其他情況,用戶默默地看完了一整個視頻,那么我們每次的紅茶表里應(yīng)該就一個section記錄,終點減起點,over找小妹子去了。但考慮到這是個復(fù)雜的社會,很多人都有著復(fù)雜的人生,他們的自由意志不是我們能控制的。萬一他就是不停地點拖進(jìn)度條呢?那你最后的列表不是特別大?那算起來不是很麻煩?所以針對這種情況,我們可以每次有新的露點記錄進(jìn)入紅茶表時,都做一次去重算法M,大大優(yōu)化性能。比如正常情況,第一次錄點為,[1,3],list={[1,3]},第二次露點為[2,4],那么第二個點入表的時候,我們可以直接M算法merge掉它,那么紅茶表里就是list={[1,4]}。
在實際中,如果不涉及離線視頻播放,也就是用戶可以下載下來視頻觀看,那么前端直接算好時長丟給后臺就好了,涉及到離線視頻播放,那我們就需要把這個做完最后M算法的紅茶表丟給后臺,后臺拿到這個表后,取出數(shù)據(jù)庫的之前存的表,做一次M算法,更新入庫。而前端下一次播放的時候,也會先請求后臺的表,拿到上次的記錄做為初始表,使用此表進(jìn)行本次優(yōu)雅的露點行為。考慮到有網(wǎng)絡(luò)錯誤,上一次沒上傳成功的情況,初始的時候還需要檢查本地是否存了沒有上傳成功的表,和后臺拿回來的表做一次M算法,做為初始表。
好了,露點法已經(jīng)介紹完了,相關(guān)的示例代碼會用java寫在最后。至于倍速問題,請耐心看到最后哦親~
這種方法的性能最高,資源消耗最小,比較推薦。這邊呢,建議您仔細(xì)研究您的視頻播放Sdk,看能不能使用代碼注入,方法重載加接口,反射等方式順利進(jìn)行露點行為。
四、打點法
在明白了我們要干的情事之后,面臨的另一個問題是,很多點我拿不到,于是打點(醬油)法就應(yīng)運而生。
這里直接說我們要的干的事情,然后再解釋它的道理。
我們需要一條計時線程,從視頻開始播放后,每隔一段時間d,就打下一個點,記錄入表。這里為了方便,取d=5s,也就是每隔5秒打一個點。最初的樣子如下:A點打下sA=[0,],B點打下sA=[0,5],sB=[5,],C點打下sB=[5,10],sC=[10,],以次類推。。。初始表如下list={[0,5],[5,10],[10,15]},做一次M算法后,list={[0,15]},最終結(jié)果就是15秒。請注意灑家畫的乃是一條射線了。
如圖,d=5,那么我們的counter在打點間隔5s內(nèi),就重復(fù)計數(shù)1,2,3,4,5,數(shù)到5再循環(huán)1,2,3,4,5。正常打點都沒什么問題,如圖,假設(shè)我們正常打了點,紅茶表list={[0,5],[5,10]},優(yōu)化起見,每次新點入表進(jìn)行一次M算法,現(xiàn)在list={[0,10]},然后在未來的某個點X,小王同志發(fā)現(xiàn)昨晚忘了喂貓,于是快退到了6s,這個G點。如圖,我們A,B,C,每個點都打好了點,現(xiàn)在的狀態(tài)用偽代碼可表示為list={[0,10],[10,]},其中sX=[10,]。那我們要做的,就是找出X點的值,因為讓人興奮的G點是點擊跳轉(zhuǎn)的點,它的值我們肯定拿得到。所以jump行為發(fā)生后,我們需要保持list表現(xiàn)在的狀態(tài),重新打個新點,即在G點處,新記一個section,sG=[6,],然后我們那條計時的線程從C點開始的一個d(=5s)跑完了,到了H,這個時候,敲黑板!就比較重要了!我們看圖說話,從C點到X,G,H,現(xiàn)在經(jīng)過的總時間是5s,假設(shè)現(xiàn)在H點的值是8,那么t(H-G)=8-6=2,那么t(X-C)=d-t(H-G)=5-2=3,那么X點的值,pX=10+t(X-C)=10+3=13,實際上 t(XD)=t(GH),這時我們就知道了sG=[6,8],sX=[10,13],將兩個section入表list={[0,10],[10,13],[6,8]},同時H點重新開始新一輪的打點sH=[8,],list做一次合并merge算法list={[0,13],[8,]}。
大家可能會問,那快退呢?快退其實是一樣的道理,這理不再重復(fù)了,有興趣可以自己畫圖算一下就很明白了。那么我們的counter來干嘛?counter沒卵用嗎?還是拿剛剛的例子,counter的作用,是在于X點跳轉(zhuǎn)后,用戶在未到達(dá)H點的時候就pause掉了。假如我在P點pause掉了,現(xiàn)在一個d還沒計時完,所以剛剛的算法就沒辦法進(jìn)行,思前想后,也只好加個counter。比如小王在C點之后,看到X點發(fā)生了jump,jump到G點,還沒到我們的d跑完,也就是還沒到打點的地方,沒辦法只能看現(xiàn)在counter的值了,比如counter現(xiàn)在的值是4,P的值是7.1,那么t(P-G)=7.1-6=1.1,,那么t(X-C)=4-1.1=2.9,那么現(xiàn)在sG=[6,7.1],sX=[10,12.9],那么list={[0,10],[10,12.9],[6,7.1]},做一次M算法之后,list={[0,12.9]}。
很明顯,這種方式會有誤差,而且誤差與counter的精細(xì)度成反比,比如counter每隔0.5s計一次,那么由于pause造成的誤差就會更小。
另外,由于有倍速播放,所以我們計算t(X-C)的時候,應(yīng)該除以倍數(shù)speed,才能得到更為精確的值。拿最開始的例子來講,從C到X,到G到H,其他值不變,但若是1.5倍播放,那么t(H-G)=(8-6)/1.5=1.33,那么 t(X-C)=5-1.33,即d(X-C)=t(X-C)/1.5=5.5,這里的d(X-C),表示從C到X進(jìn)度條往前跑的長度,所以X的值就是15.5,那么現(xiàn)在list={[0,10],[6,8],[10,15.5]},使用SM算法之,list={[0,15.5]}。
綜上,打點法,很考驗初中數(shù)學(xué)知識,要精確點的話,很煩瑣。而且每次pause行為都會面臨不同程度的精度丟失。
五、暴力打點法
打點法真的是一種無比煩瑣而蛋疼的打法呀,那有沒有簡單粗暴一點的辦法呢?誒,今天您算是問對人了。下面呀,就讓我們跟隨作者的腳步,一起走進(jìn) 暴力打點法 背后,那不為人知的秘密。
很簡單,在視頻開始播放后,直接開一條線程,每隔1s打一個點,如果某個section的startPoint和endPoint差值的絕對值大于1,則直接丟棄該section。如下圖:正常情況,我們會得到一個這樣的紅茶表list={[0,1],[1,2],[2,3],[3,4]…}如果某個section長成這樣[5,9],或者[6,4.5]那我們就判定為這兩個section是發(fā)生了快進(jìn) 或者快退之類的jump行為,直接無情地丟掉它。list={[0,1],[1,2],[2,8],[8,4],[4,5]}那么丟棄掉[2,8],[8,4]后,list={[0,1],[1,2],[4,5]},使用SM算法暴之,list={[0,2],[4,5]}。實際上每打一個點我們就可以做一次M算法,大大提高性能,需要丟棄的section直接判斷不入list即可。在實際應(yīng)用中,播放器的時間單位基本上都是ms,所以判斷某個section該不該丟棄,可以這樣 abs(startPoint-endPoint)>1000 ms,考慮到播放進(jìn)度中的網(wǎng)絡(luò)延遲等情況,可以將判斷標(biāo)準(zhǔn)適當(dāng)放寬一點,比如 abs(startPoint-endPoint)>1100 ms。這樣基本就o了,非要再精確點,再除以現(xiàn)在的倍數(shù)就好了,即 abs(startPoint-endPoint)>1100 ms/speed。
這種方式相較于打點法,簡單易行,就是性能上會差些,畢竟每隔1s都要做一次記錄section入表和M算法,而且用戶每一次jump行為就會丟失一次精度,增加一丟丟誤差。
六、小結(jié)
實際上,不同前端可以選擇自己中意的方式進(jìn)行錄表,比如pc端可以用打點法,IOS可以用錄點法,他們都是基于錄點去重的方案,這也是此種方案的優(yōu)點之一。
下面簡單地寫兩段代碼好了,用以卑微地表示這是一篇技術(shù)文章:
Section可以定義成這樣:為什么要放speed在這里我也忘了,也許用得上,也許用不上,望少俠自行斟酌。
public class Section {private long startPoint; private long endPoint;private float speed;public Section() {}public Section(float speed) {this.speed = speed;}public Section(long startPoint, long endPoint) {this.startPoint = startPoint;this.endPoint = endPoint;}/*get set 省略*/ }然后我們的M法算長這樣。這里我只是隨手寫了一種比較容易理解的方式,當(dāng)然還有其他很多高效的算法,各位大佬都那么聰明,請自行研究了。簡單的測試了下,好像沒啥問題,實際不知道,有問題歡迎隨時反饋在下邊的評論里,以便我及時的不作處理。
import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator;/*** Author:v* Time:2021/4/22*/ public class MergeUtil {public static void main(String[] args) {final LinkedList<Section> redTeaList0 = recordedList0();final List<Section> redTeaList1 = recordedList1();// mergeList(redTeaList0); // getTotalTime(redTeaList0);mergeList(redTeaList1);getTotalTime(redTeaList1);}private static long getTotalTime(List<Section> list) {if (list == null || list.size() == 0) {return 0L;}long t = 0L;for (Section s : list) {t += s.getEndPoint() - s.getStartPoint();}System.out.println("Total time is:" + t);return t;}public static List<Section> mergeList(List<Section> rawList) {if (rawList == null || rawList.size() == 0) {return rawList;}System.out.println("********before sort*******************");printList(rawList);rawList.sort(new SectionComparator());System.out.println("********after sort*******************");printList(rawList);merge(rawList);System.out.println("********after merge*******************");printList(rawList);return rawList;}private static void merge(List<Section> rawList) {ListIterator<Section> iterator = rawList.listIterator();Section tmp = iterator.next();//System.out.println("tmp " + tmp.toString());while (iterator.hasNext()) {Section next = iterator.next();// System.out.println("next " + next.toString());if (mergeSuccess(tmp, next)) {iterator.remove();} else {tmp = next;}}}private static boolean mergeSuccess(Section pre, Section next) {if (next.getStartPoint() <= pre.getEndPoint()) {pre.setEndPoint(Math.max(pre.getEndPoint(), next.getEndPoint()));return true;}return false;}/*** @return 模擬記錄的表*/private static LinkedList<Section> recordedList0() {final LinkedList<Section> ret = new LinkedList<>();Section s1 = new Section(0L, 12_000L);Section s2 = new Section(12_000L, 53_000L);Section s3 = new Section(56_000L, 99_100L);Section s4 = new Section(99_100L, 120_000L);ret.add(s2);ret.add(s1);ret.add(s4);ret.add(s3);return ret;}/*** @return 模擬記錄的表*/private static LinkedList<Section> recordedList1() {final LinkedList<Section> ret = new LinkedList<>();Section s1 = new Section(1_000L, 5_000L);Section s2 = new Section(3_000L, 10_000L);Section s3 = new Section(0L, 6_000L);Section s4 = new Section(11_000L, 15_000L);ret.add(s1);ret.add(s2);ret.add(s3);ret.add(s4);return ret;}private static void printList(List<Section> list) {for (Section s : list) {System.out.println(s.toString());}}/*** 按 startPoint 由低到高排序*/private static final class SectionComparator implements Comparator<Section> {@Overridepublic int compare(Section o1, Section o2) {return (int) (o1.getStartPoint() - o2.getStartPoint());}} } }下面是簡單測試的兩個紅茶表的輸出,各位可以多加點用例測試一下,看看有沒有啥問題。
********before sort******************* Section{startPoint=12000, endPoint=53000} Section{startPoint=0, endPoint=12000} Section{startPoint=99100, endPoint=120000} Section{startPoint=56000, endPoint=99100} ********after sort******************* Section{startPoint=0, endPoint=12000} Section{startPoint=12000, endPoint=53000} Section{startPoint=56000, endPoint=99100} Section{startPoint=99100, endPoint=120000} ********after merge******************* Section{startPoint=0, endPoint=53000} Section{startPoint=56000, endPoint=120000} Total time is:117000//第二個 ********before sort******************* Section{startPoint=1000, endPoint=5000} Section{startPoint=3000, endPoint=10000} Section{startPoint=0, endPoint=6000} Section{startPoint=11000, endPoint=15000} ********after sort******************* Section{startPoint=0, endPoint=6000} Section{startPoint=1000, endPoint=5000} Section{startPoint=3000, endPoint=10000} Section{startPoint=11000, endPoint=15000} ********after merge******************* Section{startPoint=0, endPoint=10000} Section{startPoint=11000, endPoint=15000} Total time is:14000七、大結(jié)
End
總結(jié)
以上是生活随笔為你收集整理的准确记录用户观看视频内容时长的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IE浏览器不用迅雷下载
- 下一篇: 关于微软黑屏的延伸?