日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

FFmpeg优化 苏宁PP体育视频剪切效率提升技巧

發布時間:2024/4/11 65 豆豆
生活随笔 收集整理的這篇文章主要介紹了 FFmpeg优化 苏宁PP体育视频剪切效率提升技巧 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.


FFmpeg功能強大,社區活躍,在多媒體處理業務中扮演著不可或缺的角色。但沒有優化過的FFmpeg在生產環境下有很多性能瓶頸,因此對其進行優化勢在必行。蘇寧旗下PP體育音視頻技術負責人田釗撰文分享了團隊在處理海量視頻切割過程中遇到的挑戰及優化方法。感謝OnVideo視頻創作云平臺聯合創始人、FFmpeg Maintainer劉歧對本文的技術審校。


文 / 田釗

審校 / 劉歧


一、前言


蘇寧旗下PP體育所在的直播行業,每天有無數視頻原始數據需要進行分類存儲、渲染處理。處理這些視頻,一個很重要的方面,就是要將長時段的直播視頻切割成不定時長,不定畫面組的短視頻,以匹配現代用戶碎片化的消費時間。尤其是體育賽事直播行業,在直播前的墊場片花、直播中的即時快看、直播后的全場集錦和精華鏡頭,都需要對大量的視頻作剪切/壓制處理。而且因為體育賽事直播行業的特殊性,對于直播中和直播后的精彩鏡頭,集錦類視頻片段,要求必須能及時處理視頻,并發布到用戶端。這對視頻的處理效率提出了非常高的要求。


在PP體育,我們在使用與業界同樣高效的設計模式和優化方案的同時,另外嘗試了換一種角度來思考這個問題,并進行了實踐。下面我們來針對這部分的構思和實踐中碰到的問題,來做個分享。


二、背景基礎知識


先簡單說一下我們對視頻在數據層面上的理解。對于視頻來說,無論是何種編碼,何種封裝格式,拆分開看,都是由音頻流和視頻流來組合而成的。從數據的最低層級往上推,會發現一個視頻文件會由以下幾個層面的數據組成。



1. 第一層是亂序的二進制數據層。基本看不出來是啥數據。

2. 第二層是未經編碼的音視頻數據層。這里就有了數據源出來的原始音頻、視頻等數據。原始音視頻流數據量很大。

3. 第三層是編碼數據層。通常音頻使用AAC編碼,視頻使用H.264/265編碼后,音視頻流數據量就已經比較小了。

4. 第四層是封裝層。將編碼后的音視頻數據”打包“封裝成不同的封裝格式。這里就是我們通常所看到的.ts/.mp4/.flv/.mkv等視頻文件。這些文件里封裝著M路編碼的視頻流和N路編碼的音頻流。當然也可以有其它的數據流,如字幕流,附加信息流等。


三、常規做法簡述


視頻的切割/轉碼/壓制,目前業界通常的處理方式是在云端服務器,直接通過云轉碼模塊集成的視頻剪切服務來處理。通常使用FFmpeg套件改造而成。而且部分視頻云服務廠商為提升轉碼效率,會用到云端轉碼集群。通過將完整的長段視頻先進行切割,再將切割完的小段視頻再通過分布式集群進行轉碼,合并,壓制操作。其中,轉碼壓制部分,由蘇寧視頻云服務提供的業界領先的分布式轉碼集群來完成?;A的轉碼業務圖如下:



其中,轉碼部分,多數視頻云服務廠商采用了分布式轉碼服務,來進行效率優化的提升。對于切割部分,卻不一定重視。部分方案會和轉碼模塊合并到一起,也有的廠商兩樣將分析視頻的結果列表,也利用服務器集群來進行并發的切割操作。通常這種方案會直接使用FFmpeg套件來完成切割的動作。所以,對視頻云廠商來說,FFmpeg套件切割視頻功能的優化是提升切割效率的核心。各大廠商的業界大牛們為此做了不同的嘗試,也取得了不錯的效果。


典型的切割服務,多在音視頻分層圖的第三層作數據拷貝處理,典型如下列指令:

ffmpeg?-ss?00:10:24?-i?input.mp4?-vcodec?copy?-acodec?copy?-t?00:95:27?output.mp4

此切割指令使用FFmpeg套件對視頻數據中的音視頻,按音視頻幀級數據包直接拷貝來處理。此種方式有優點也有缺陷。


缺點在于:經常會有比較明顯的視頻切割誤差。因為視頻GOP長度因素存在,經常會出現起始點視頻幀并非關鍵幀。而FFmpeg切割程序代碼需要找到切割起始點的視頻關鍵幀,才能正常完成視頻幀層面的切割動作。所以FFmpeg程序會計算查找當前視頻幀的GOP關鍵幀后,再以此GOP關鍵幀為起始點來作為切割起始點。此種方式下會導致真實切割點與原始需求切割點是不一致的情況。導致切割出來的視頻起止點并不精確。


優點也很明顯:因為不對已編碼的音視頻數據進行解碼再編碼的操作,所以效率已經非常不錯。并且在此基礎上,進一步的優化方案,可以將FFmpeg套件按多進程模型來使用,利用服務器的多核性能來并行調用多個FFmpeg進程進行多路切割操作,縮短總體切割時間,以提升切割性能;再利用服務器集群,進行多服務器規模并行處理,進一步提高切割效率。


四、優化方法與實踐


我們的優化做法,與上述情況在原理上是一致的,但是在細節上有做了微創新。


首先,我們沒用使用FFmpeg套件來做核心切割功能服務。如上所述,業界通常利用FFmpeg套件切割視頻文件時,是在視頻分層圖的第三層編碼數據層對視頻文件按”幀“級數據作拷貝處理。我們對生產環境及直播鏈路進行梳理后發現,視頻的數據封裝格式基本只有MP4/FLV/TS三種。而此三種封裝格式里,除MP4封裝稍復雜外,FLV/TS的封裝相對容易分析處理。所以我們大膽地嘗試了在視頻分層圖的第四層——封裝層做分析處理。將視頻切割動作分解為對封裝數據的切分。


1. 分析視頻封裝里的詳細描述信息;

2. 根據封裝詳細描述信息,對起止切割點進行計算;

3. 找到切割點二進制數據起止點;

4. 復制出起止點間二進制數據;

5. 重新描述起止切割點的封裝信息,并與復制出的二進制數據進行拼合。


上述操作完成后,最終得到切割后的視頻。這種操作方法,實際是將視頻文件分解為兩層,封裝層和二進制數據層。切割工具從封裝層得到描述信息后,對視頻數據進行最底層的二進制數據拷貝,其中不涉及任何幀的處理。切割起始點與終止點的計算,以及拷貝數據拼合成新的視頻,是這里的關鍵。典型代碼片段如下:?


func?CopyStructureData(src?*demux.VideoStructure,?dst?*demux.VideoStructure)?{?
????copy(dst.FTYP.CompatibleBrands,?src.FTYP.CompatibleBrands)?

????copy(dst.MOOV.MVHD.Flags,?src.MOOV.MVHD.Flags)?

????copy(dst.MOOV.MVHD.Reserved,?src.MOOV.MVHD.Reserved)?

????copy(dst.MOOV.MVHD.Matrix,?src.MOOV.MVHD.Matrix)?

????copy(dst.MOOV.MVHD.PreDefined,?src.MOOV.MVHD.PreDefined)?

????for?i?:=?0;?i?<?len(src.MOOV.TRAK);?i?++?{?

????????copy(dst.MOOV.TRAK[i].TKHD.Flags,?src.MOOV.TRAK[i].TKHD.Flags)?

????????copy(dst.MOOV.TRAK[i].TKHD.Reserved1,?src.MOOV.TRAK[i].TKHD.Reserved1)?

????????copy(dst.MOOV.TRAK[i].TKHD.Reserved2,?src.MOOV.TRAK[i].TKHD.Reserved2)?

????????copy(dst.MOOV.TRAK[i].TKHD.Reserved3,?src.MOOV.TRAK[i].TKHD.Reserved3)?

????????copy(dst.MOOV.TRAK[i].TKHD.Matrix,?src.MOOV.TRAK[i].TKHD.Matrix)?

????????copy(dst.MOOV.TRAK[i].EDTS.ELST.Flags,?src.MOOV.TRAK[i].EDTS.ELST.Flags)?

????????copy(dst.MOOV.TRAK[i].EDTS.ELST.TrackDurations,?src.MOOV.TRAK[i].EDTS.ELST.TrackDurations)?

????????copy(dst.MOOV.TRAK[i].EDTS.ELST.Times,?src.MOOV.TRAK[i].EDTS.ELST.Times)?

????????copy(dst.MOOV.TRAK[i].EDTS.ELST.Speeds,?src.MOOV.TRAK[i].EDTS.ELST.Speeds)?

????????copy(dst.MOOV.TRAK[i].MDIA.MDHD.Flags,?src.MOOV.TRAK[i].MDIA.MDHD.Flags)?

????????copy(dst.MOOV.TRAK[i].MDIA.HDLR.ComponentName,?src.MOOV.TRAK[i].MDIA.HDLR.ComponentName)?

????????copy(dst.MOOV.TRAK[i].MDIA.MINF.SMHD.Flags,?src.MOOV.TRAK[i].MDIA.MINF.SMHD.Flags)?

????????copy(dst.MOOV.TRAK[i].MDIA.MINF.SMHD.Balance,?src.MOOV.TRAK[i].MDIA.MINF.SMHD.Balance)?

????????copy(dst.MOOV.TRAK[i].MDIA.MINF.SMHD.Reserved,?src.MOOV.TRAK[i].MDIA.MINF.SMHD.Reserved)?

????????copy(dst.MOOV.TRAK[i].MDIA.MINF.VMHD.Flags,?src.MOOV.TRAK[i].MDIA.MINF.VMHD.Flags)?

????????......?
????}?
}

//?生成片段視頻文件?
func?Generate(clipVideo?*demux.VideoStructure,?videoSampleOffsets,?audioSampleOffsets?*SampleOffsets,?videoSampleIndexRange,?audioSampleIndexRange?*SampleIndexRange,?clipPath,?clipPrefix?string,?clipTime?*ClipTime,?videoFilePath?string)?{?
????//?拼接完整路徑名稱?
????clipVideoPath?:=?fmt.Sprintf("%s%s%d-%d.mp4",?clipPath,?clipPrefix,?clipTime.Start,?clipTime.Stop)?
????//?創建文件寫入對象?
????writer,?err?:=?NewFileWriter(clipVideoPath)?
????if?err?!=?nil?{?
????????fmt.Print(err)?
????????return?
????}?
????defer?writer.Close()?

????writeFTYP(writer,?clipVideo)?
????writeFREE(writer,?clipVideo)?
????writeMDAT(writer,?clipVideo,?videoSampleOffsets,?audioSampleOffsets,?videoSampleIndexRange,?audioSampleIndexRange,?videoFilePath)?
????writeMOOV(writer,?clipVideo)?
????writeMVHD(writer,?clipVideo)?
????for?_,?track?:=?range?clipVideo.MOOV.TRAK?{?
????????if?track.MDIA.HDLR.ComponentSubtype?==?"vide"?{?
????????????writeTRAK(writer,?&track,?true)?
????????}?
????????if?track.MDIA.HDLR.ComponentSubtype?==?"soun"?{?
????????????writeTRAK(writer,?&track,?false)?
????????}?
????}?
}?


//?從原視頻結構體中取出片段幀的偏移,再從原視頻中拷貝幀數據到片段視頻?
func?writeMDAT(writer?*FileWriter,?clipVideo?*demux.VideoStructure,?videoSampleOffsets,?audioSampleOffsets?*SampleOffsets,?videoSampleIndexRange,?audioSampleIndexRange?*SampleIndexRange,?videoFilePath?string)?{?
????//?重算音視頻幀總長度?
????sampleTotalSize?:=?uint32(0)?
????for?_,?track?:=?range?clipVideo.MOOV.TRAK?{?
????????for?_,?sampleSize?:=?range?track.MDIA.MINF.STBL.STSZ.SampleSize?{?
????????????sampleTotalSize?+=?sampleSize?
????????}?
????}?
????writer.WriteUint32BE(8?+?sampleTotalSize)?
????writer.WriteString("mdat")?
????//?從原視頻中拷貝幀數據?
????reader,?err?:=?NewRawReader(videoFilePath)?
????if?err?!=?nil?{?
????????fmt.Print(err)?
????????return?
????}?
????//?視頻數據如果連續,則合并長度,減少讀取次數?
????currentOffset?:=?int64(0)?
????currentLength?:=?int64(0)?
????for?index,?offset?:=?range?videoSampleOffsets.Offset[videoSampleIndexRange.Start:videoSampleIndexRange.Stop]?{?
????????for?_,?track?:=?range?clipVideo.MOOV.TRAK?{?
????????????//?視頻track?
????????????if?track.MDIA.HDLR.ComponentSubtype?==?"vide"?{?
????????????????if?currentOffset?==?0?{?
????????????????????currentOffset?=?int64(offset)?
????????????????????currentLength?=?int64(track.MDIA.MINF.STBL.STSZ.SampleSize[index])?
????????????????}?
????????????????//?如果內存是連續的則合并長度待最后一次性讀取?
????????????????if?index+1?<=?videoSampleIndexRange.Stop-videoSampleIndexRange.Start?&&?uint64(track.MDIA.MINF.STBL.STSZ.SampleSize[index])+offset?==?videoSampleOffsets.Offset[index+1]?{?
????????????????????if?currentOffset?>?0?{?
????????????????????????currentLength?+=?int64(track.MDIA.MINF.STBL.STSZ.SampleSize[index])?
????????????????????}?
????????????????}?else?{?
????????????????????sampleContent?:=?reader.ReadBytesAt(currentLength,?currentOffset)?
????????????????????writer.WriteBytes(sampleContent)?
????????????????????currentOffset?=?0?
????????????????????currentLength?=?0?
????????????????}?
????????????????break?
????????????}?
????????}?
????}?
????//?音頻數據如果連續,則合并長度,減少讀取次數?
????currentOffset?=?int64(0)?
????currentLength?=?int64(0)?

????//多音軌視頻,某音軌長度不足造成越界,直接補0?
????if?audioSampleIndexRange.Start?>?len(audioSampleOffsets.Offset)?||?audioSampleIndexRange.Stop?>?len(audioSampleOffsets.Offset)?{?
????????log.Println("Current?audio?track?length?not?enough?to?fit?the?cut?range!")?

????????for?_,?track?:=?range?clipVideo.MOOV.TRAK?{?
????????????if?track.MDIA.HDLR.ComponentSubtype?==?"soun"?{?
????????????????for?_,?sampleSize?:=?range?track.MDIA.MINF.STBL.STSZ.SampleSize?{?
????????????????????buf?:=?make([]byte,?sampleSize)?
????????????????????writer.WriteBytes(buf)?
????????????????}?
????????????}?
????????}?

????????return?
????}?

????for?index,?offset?:=?range?audioSampleOffsets.Offset[audioSampleIndexRange.Start:audioSampleIndexRange.Stop]?{?
????????for?_,?track?:=?range?clipVideo.MOOV.TRAK?{?
????????????//?音頻track?
????????????if?track.MDIA.HDLR.ComponentSubtype?==?"soun"?{?
????????????????if?currentOffset?==?0?{?
????????????????????currentOffset?=?int64(offset)?
????????????????????currentLength?=?int64(track.MDIA.MINF.STBL.STSZ.SampleSize[index])?
????????????????}?
????????????????//?如果內存是連續的則合并長度待最后一次性讀取?
????????????????if?index+1?<=?audioSampleIndexRange.Stop-audioSampleIndexRange.Start?&&?uint64(track.MDIA.MINF.STBL.STSZ.SampleSize[index])+offset?==?audioSampleOffsets.Offset[index+1]?{?
????????????????????if?currentOffset?>?0?{?
????????????????????????currentLength?+=?int64(track.MDIA.MINF.STBL.STSZ.SampleSize[index])?
????????????????????}?
????????????????}?else?{?
????????????????????sampleContent?:=?reader.ReadBytesAt(currentLength,?currentOffset)?
????????????????????writer.WriteBytes(sampleContent)?
????????????????????currentOffset?=?0?
????????????????????currentLength?=?0?
????????????????}?
????????????????break?
????????????}?
????????}?
????}?
}

這樣就模擬了最原始的數據拷貝動作。實際應用效果對比看,優化后的切割方式,比使用FFmpeg套件,效率提升了近2倍。這是對切割操作思路的一種轉換。


但是,這并不是優化的結束。我們前面談到業界通行做法,都用到了服務器的多核處理。多核優化利用了機器的最大性能,是最基本的優化方式。那么,我們能不能在這方面再考慮入手呢?


是的,我們又在編程語言上微創新了一下。巧合的是,我們當時正在準備用Golang來做長鏈接系統的服務。程序員靈光乍現,用Golang實現了上述操作邏輯,順便開了“一些” goroutine來做復制切割數據的動作。把每個goroutine模擬成一個FFmpeg切割進程,這樣在同一臺服務器上,每個內核線程上就運行著多個"goroutine形式的FFmpeg"切割JOB。簡略的主流程代碼如下:


func?main()?{?
//?解碼源視頻?
????rawVideo?:=?new(demux.VideoStructure)?
????demux.Demux(rawVideo,?videoFilePath)?

????//?并發編碼多個片段視頻?
????start?=?time.Now()?
????pool?:=?util.NewRoutinePool(len(clipTimes))?
????for?_,?clipTime?:=?range?clipTimes?{?
????????go?func(clipTime?*remux.ClipTime)?{?
????????????pool.AddOne()?
????????????defer?pool.DelOne()?

????????????//?編碼片段視頻?
????????????clipVideo?:=?new(demux.VideoStructure)?
????????????videoOffsets,?audioOffsets,?videoRange,?audioRange,?err?:=?remux.Remux(rawVideo,?clipVideo,?clipTime)?

????????????//?導出片段視頻?
????????????remux.Generate(clipVideo,videoOffsets,audioOffsets,videoRange,audioRange,clipPath,?clipPrefix,?clipTime,?videoFilePath)?
????????}(clipTime)?
????}?
????pool.Wait()?
}


經過此番轉換后,一臺服務器上的剪切視頻操作,就從FFmpeg切割方案的“單進程/M線程”轉換成“M線程xN協程"模式。(M為CPU內核數,N為單內核上的goroutine數)


在編程語言層面上的”誤打誤撞“并發處理后,切割效率又得到了進一步的提升。經過效果對比驗證,比使用FFmpeg套件的單進程方式,效率提升了20~80倍。最終影響整個切割效率,成為瓶頸的,是硬盤的IO性能。



在此基礎上,將單臺服務器擴展至分布式服務集群。這樣的視頻切割JOB集群,帶來的是超高效率的視頻切割處理流程。


五、存在的問題


方案經過優化后,在視頻切割方面,已經將效率提高了至少10倍以上。但同時優化過程中也有一些問題呈現出來。


1. 首先,就是適配的視頻封裝格式單一的問題。因為我們的數據源比較單一,基本是MP4封裝格式,所以在初期,切割程序只需要解析MP4封裝格式相關定義字段即可。不過網絡上視頻流媒體格式非常豐富,即使常用的也有4、5種。對此,我們后續添加了對另2種比較常見的FLV與TS封裝格式的支持,滿足了業務的正常需求。但是,仍然與FFmpeg套件的廣泛適用性相去甚遠。畢竟FFmpeg積累這么些年兼容了幾乎所有的媒體格式,這也是用FFmpeg套件被廣泛選擇,且相對更簡潔易用的原因。


2. 另外,在實際計算起止切割點時,往往會出現當前切割點的時間上并不是關鍵幀,導致部分數據無法被正確解碼的問題。對此,我們也做了簡單的處理:對于切割點上非關鍵幀的情況,我們的程序會自動往前/往后找到上一個/下一個關鍵幀的時間點,并以此時間點為基準,重新計算數據后再行切割。這樣才能保證所有切割出來的視頻是確定能被解碼的。經過測試,對切割效率的影響幾乎可以忽略不計。并且,我們也正在著手進行優化的“補幀”形式的精確起止點方案。


3. 還有,視頻媒體源文件非標的處理問題。實際生產過程中,經常會發現數據源提供的視頻文件里,有1路以上的音頻流,而且經常性出現幾路音頻流中,都是無效的錯誤數據。這種情況在實際生產中會影響到數據切割后的音視頻同步出錯,導致無法切割成功,或者播放失敗。我們對不同的情況進行分析后,找到幾種思路/模式來解決:


(1)分析并保留正確的音頻流數據。這對部分非現場錄制的視頻文件比較有效,絕大多數PGC生產的視頻文件均可適用此模式。


(2)切割拷貝數據時不包括音頻流數據。這意味著切割后的視頻沒有聲音。大多數賽事直播現場錄制的視頻可應用此模式。


(3)對于無法分析正確且不能丟棄原始音頻流數據的文件,作“降級”處理,改用FFmpeg套件接手切割工作,保證生產出正確的視頻文件。


六、分析與小結


從解決方案的拆分模塊角度看,任何環節的優化提升都是對整個方案的效率有積極的促進作用。故而,我們對整個視頻剪切流程進行梳理劃分。整理出視頻數據切割操作中的不同模塊。


優化方案的核心思路,主要是對數據處理模塊進行效率提升。其關鍵點在于:


1. 單個剪切需求轉換為數據拷貝的JOB。

2. JOB由進程轉換為協程化處理。

3. 集群分布式處理JOB列表。


雖然在實際生產使用過程中,仍然不斷有出現或大或小的坑,但是這都不影響我們在追求更高生產效率的路上繼續前行。只要能提升效率,任何微小的創新都在我們的持續不懈的優化范圍之中,這也正是蘇寧的造極精神的體現。



總結

以上是生活随笔為你收集整理的FFmpeg优化 苏宁PP体育视频剪切效率提升技巧的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。