Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
閱讀本文你將獲得以下知識:
了解視頻的基本原理。
了解 FFmpeg 是什么,和一些常用的用法。
用 FFmpeg 搭建簡單的視頻直播推流。
FFmpeg 在 NodeJS 中的一些用法。
短視頻大行其道的年代,作為程序員勢必需要了解:視頻編輯背后的原理和技術。本文簡略的描述了視頻的組成原理和常用的視頻編輯工具,以及在 NodeJS 中的用法。
想要了解視頻原理,首先應該從圖像原理開始說起。
圖像畫面由一個數字序列表示的圖像中的一個最小單位色塊,被稱之像素(pixel/px)。
注意:像素只有位圖才會有,是用來記錄位圖圖像的。
我們所說的圖像大小為1920*1080,指的就是長寬各有 1920 和 1080 的像素點,那么一張1920*1080的圖片總共有的像素點為:1920*1080=2073600個像素點。
圖像的大?。合袼財盗?* 像素大小=圖片大小,而 像素大小 和 像素深度1有關系。RGB表示的真彩色能表示256×256×256=16,777,216,就是我們常見的1600萬色,是人眼可見的全部色彩,超出沒有意義。RGB的像素深度有1bit、4bit、8bit、16bit、24bit、32bit,如在ps中下圖在新建一張畫布選擇8bit(指 rgb 每種顏色占 8bit),那這樣1 px=3 * 8bit=24bit,俗稱24 位圖。根據以上公式就能算出如下圖圖像的大?。?00 * 378 * 24 / 8=567000Byte=567000Byte / 1024=553.7109375 Kb,和 ps 顯示的圖像大小一樣。
但往往真實的圖片大小遠比以上計算的結果小很多, 這是因為導出的圖片都經過壓縮的,關于圖片壓縮技術可自行搜索學習。
視頻就是圖片一幀一幀連起來的產物,連起來的越快看著越流暢,用 幀率(就是每秒播放圖片的數量 FPS)來衡量視頻的流暢度。那么根據圖片大小的算法就能算出視頻的大小。
視頻的大小=時長(秒) * 幀率(FPS)* 圖片大小;
那么1920×1280分辨率, 30FPS,時長 1 秒的視頻的大小就是:1920 * 1280 * 24 / 8 * 30 / 1024 / 1024=210.9375 M,那么 1 小時的影片需要:210.9 * 60 * 60 / 1024=741.4453125 G,不禁產生疑問,為啥我下載的大片才 1G 多?莫慌,視頻要是這么簡單,那我們就太天真了,所以就有了下文 「視頻編碼」 。
視頻和音頻就像是飯和菜,封裝格式就相當于碗。
注意: 下文所有視頻均代表包含音頻的視頻。
常見封裝格式有 MP4、AVI、FLV、mov、RMVB、MKV、WMV、3GP、ASF 等。
視頻編碼是對采用視頻壓縮算法將一種視頻格式轉換成另一種視頻格式的描述,音頻編碼同理。
常見的視頻編碼格式有:AC-1、MPEG2/H.262、VP8、MPEG4、VP9、H.261、H.263、H.264、H.265 等。
常見的音頻編碼格式有:WMA、MP3、AC-3、AAC、APE、FLAC、WAV 等。
主要是將視頻像素數據(RGB,YUV 等)壓縮成為視頻碼流,從而降低視頻的數據量,也就是處理像素。
YUV: 和RGB一樣是一種顏色編碼格式,相比RGB更利于壓縮。其中"Y"表示明亮度(Lumina nce 或 Luma),也就是灰階值;而"U"和"V"表示的則是色度(Chrominance 或 Chroma),作用是描述影像色彩及飽和度,用于指定像素的顏色。
視頻壓縮分為下面兩種類型:
1. 幀內壓縮
也叫空間壓縮,類似于圖像壓縮,屬于有損壓縮算法,達不到很高的壓縮比。
2. 幀間壓縮
主要是通過記錄關鍵幀,通過壓縮關鍵幀之間連續幀的冗余信息(連續幀內相同的像素區域)的過程。
為了記錄關鍵幀,將視頻的畫面幀分為三類:
GOP(Group of Pictures)值
編碼器將多張圖像進行編碼后生產成一段一段的 GOP ,每一組 IPB 幀的序列包含多少幀,也就是一個 I 幀結束后需要經過多少幀才能出現下一個 I 幀。所以同碼率下 GOP 值越大,B 幀和 P 幀越多,視頻質量越高。
在壓縮或者解壓縮視頻的過程用到編解碼器(Codec)??偟倪^程可以:
視頻的編碼的過程:
下圖來源于即時通訊網2。
視頻解碼的過程:
音頻壓縮是在保證信號在聽覺方面不產生失真的前提下,對音頻數據信號進行盡可能大的壓縮, 去除冗余信息。冗余信號包含人耳聽覺范圍外的音頻信號以及被掩蔽掉的音頻信號等。例如,人耳所能察覺的聲音信號的頻率范圍為 20Hz ~ 20KHz,除此之外的其它頻率人耳無法察覺,都可視為冗余信號。此外,根據人耳聽覺的生理和心理聲學現象,當一個強音信號與一個弱音信號同時存在時,弱音信號將被強音信號所掩蔽而聽不見,這樣弱音信號就可以視為冗余信號而不用傳送。
音頻壓縮不是今天的主角,想深入學習可參考如下鏈接:
https://baike.baidu.com/item/%E9%9F%B3%E9%A2%91%E5%8E%8B%E7%BC%A9/392863
https://www.kamilet.cn/how-audio-compression-works-and-can-you-really-tell-the-difference/
FFmpeg is a collection of libraries and tools to process multimedia content such as audio, video, subtitles and related metadata.
簡單說就是一個跨平臺的視頻處理的程序。
整個過程基本可以說成:解復用=> 解碼=> 編碼=> 復用器。
_______ ______________
| | | |
| input | demuxer | encoded data | decoder
| file | ---------> | packets | -----+
|_______| |______________| |
v
_________
| |
| decoded |
| frames |
|_________|
________ ______________ |
| | | | |
| output | <-------- | encoded data | <----+
| file | muxer | packets | encoder
|________| |______________|
FFmpeg 分為 3 個版本:Static、 Shared、 Dev
Mac 安裝:
brew install ffmpeg
其他安裝請參考官網3。
它能分別對視頻的的各個組成進行編碼,它對音視頻的編碼格式支持也比較全面。例如:對視頻容器的轉換、音視頻的壓縮、視頻截取、截圖、濾鏡、音頻提取等等,非常強大。
命令行語法:
ffmpeg [全局參數] [輸入文件參數] -i [輸入文件] [輸出文件參數] [輸出文件]
視頻信息:
// 獲取視頻信息
ffmpeg -i input.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input2.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2mp41
encoder : Lavf58.29.100
description : Packed by Bilibili XCoder v2.0.2
Duration: 00:08:24.45, start: 0.000000, bitrate: 2180 kb/s // 時長,碼率
Stream #0:0(und): Video: hevc (Main) (hev1 / 0x31766568), yuv420p(tv), 1920x1080 [SAR 1:1 DAR 16:9], 2046 kb/s, 25 fps, 25 tbr, 16k tbn, 25 tbc (default) // 第一個流是視頻流,編碼格式是hevc(封裝格式為hev1),每一幀表示為yuv420p,分辨率1920*1080,碼率2046kb/s, fps為25。
Metadata:
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default) // 第二個流是音頻流,編碼格式是aac(封裝格式為mp4a)采樣率是44100 Hz,聲道是立體聲,碼率92Kbit/s。
Metadata:
handler_name : SoundHandler
碼率的轉換:
ffmpeg -i input.mp4 -b:v 64k -bufsize 64k output.mp4
幀率轉換:
ffmpeg -i input.mp4 -r 5 output.mp4
分辨率轉換:
ffmpeg -i input.mp4 -vf scale=480:-1 output.mp4 // 1080p 轉為 480p
視頻倍速:
ffmpeg -i test1 "setpts=PTS/5" test4.mp4 // 視頻5倍速轉換
fmpeg -i input.mp4 -filter:a "atempo=2.0" 4s.mp4 // 音頻2倍速播放
ffmpeg -i input.mp4 -filter_complex "[0:v]setpts=0.5*PTS[v];[0:a]atempo=2.0[a]" -map "[v]" -map "[a]" -vn 4s.mp4 // 音視頻同時2倍速
提取音視頻:
ffmpeg -i input.mp4 -an output.mp4 //提取視頻
ffmpeg -i input.mp4 -vn output.mp3 //提取音頻
視頻比例轉換:
ffmpeg -i input.mp4 -aspect 21:9 output.mp4
視頻容器轉換:
ffmpeg -i input.mp4 output.avi
視頻截圖:
ffmpeg -ss 00:00:05 -i input.mp4 -vframes 1 -q:v 5 -f image2 pic-%03d.jpeg
// -ss 00:00:05 從第五秒開始 -vframes 1 只截取1幀 -q:v 5 圖片質量1-5
視頻截?。?/strong>
ffmpeg -ss 00:00:02 -i input.mp4 -t 6.5 -c copy cut.mp4
ffmpeg -ss 00:00:02 -i input.mp4 -to 00:00:10 -c copy cut.mp4
連續圖片或視頻生成 gif 圖:
ffmpeg -i output.mp4 -to 10 -r 30 -vf scale=100:-1 gg.gif // 截取視頻某個部分生成gif 100:-1 指定寬度,高度保持原始比例
ffmpeg -r 5 -i pic-%03d.jpeg 11.gif // 多圖生成gif
// 圖片還可生成視頻
ffmpeg -r 20 -i pic-%03d.jpeg gif.mp4
ffmpeg -f concat -i "concat:part1.mp4|part2.mp4|3.mp4|part4.mp4" -c copy output.mp4 // 多個視頻拼接成一個
圖片或視頻加濾鏡:
// 模糊濾鏡
ffmpeg -y -i pic-012.jpeg -vf boxblur=7 blur.jpeg
// 變色
ffmpeg -i pic-012.jpeg -vf colorbalance=rm=1 colorbalance1.jpg // 調整rgb某個維度的權重實現變色。
ffmpeg -i pic-012.jpeg -vf colorchannelmixer=.3:.4:.3:0:.3:.4:.3:0:.3:.4:.3 colorchannelmixer1.jpg // 對rgba四個通道進行重新計算,并分別給定權重比例。
ffmpeg -i pic-012.jpeg -vf hue=h=30:s=1 hue1.jpg // 改變色調,相當在調色板上調色
ffmpeg -i pic-012.jpeg -vf lutyuv="y=negval:u=negval:v=negval" lutyuv1.jpg // lutyuv用于yuv顏色空間
ffmpeg -i pic-012.jpeg -vf negate=0 negate1.jpg // 反轉
ffmpeg -i pic-012.jpeg -vf swapuv swapuv1.jpg // UV 互換
ffmpeg -i pic-012.jpeg -vf crop=w=200:h=300:x=500:y=800 crop1.jpg // 裁剪
添加水印:
ffmpeg -i input.mp4 -i pic-012.jpeg -filter_complex "[1:v] scale=176:144 [logo];[0:v][logo]overlay=x=0:y=0" out.mp4 //給視頻添加圖片水印
ffmpeg -i input.mp4 -vf "drawtext=fontsize=100:fontcolor=white:alpha=0.3:text='%{localtime\:%Y\-%m\-%d %H-%M-%S}':y=h-line_h-100:x=(w-text_w)/2" output22.mp4// 添加文字水印
ffmpeg -i input.mp4 -i pic-012.jpeg -filter_complex "[1:v] scale=176:144 [logo];[0:v][logo]overlay=x=0:y=0" out.mp4
ffmpeg -i input.mp4 -vf drawtext="fontsize=100:text='我是水印':fontcolor=green:enable=lt(mod(t\,3)\,1)" interval-sy.mp4
// t 時間,s
// mod(t\,2) 計算t%2
// lt(mod(t\,2)\,1) 如果mod(t\,2)<1,返回1,否則返回0
// enable=lt(mod(t\,2)\,1) 每隔1s顯示一次水印,enable=lt(mod(t\,3)\,1) 每隔3s.
添加字幕:
// 第一步 用you-get下載B站視頻
// 第二步 用 danmaku2ass.py 轉換彈幕 https://github.com/m13253/danmaku2ass
// 第三步 可以用ffmpeg轉換彈幕
ffpmeg -i input.ass input.srt
// 第四步 給視頻添加字幕或彈幕 字幕可添加多個
ffmpeg -i input.mp4 -vf subtitles=input.ass output.mp4
為音頻添加封面:
ffmpeg -loop 1 -i cover.jpg -i input.mp3 -c:v libx264 -c:a aac -b:a 192k -shortest output.mp4
// -loop 1表示一直循環, -shortest 音頻結束視頻輸出就結束
視頻畫中畫:
ffmpeg -re -i input.mp4 -vf "movie=output.mp4,scale=480*320[test]; [in][test] overlay [out]" -vcodec libx264 videoInvideo.mp4
多宮格:
ffmpeg -y -i input.mp4 -i input.mp4 \
-i input.mp4 -i input.mp4 \
-filter_complex "nullsrc=size=640x480[base]; \
[0:v]scale=320x240[topleft]; \
[1:v]scale=320x240[topright]; \
[2:v]scale=320x240[bottomleft]; \
[3:v]scale=320x240[bottomright]; \
[base][topleft]overlay=shortest=1[tmp1]; \
[tmp1][topright]overlay=shortest=1:x=320[tmp2]; \
[tmp2][bottomleft]overlay=shortest=1:y=240[tmp3]; \
[tmp3][bottomright]overlay=shortest=1:x=320:y=240" \
-vcodec libx264 9_video_filtered.flv
// nullsrc創建畫布
視頻壓縮:
ffmpeg -i input.mp3 -ab 128 output.mp3 // 壓縮音頻
ffmpeg -i input.mp4 -vf scale=1280:-1 -c:v libx264 -preset veryslow -crf 24 output.mp4 // 壓縮視頻
視頻直播推流:
// 錄制視頻保存在本地
ffmpeg -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -f h264 -r 30 ~/Downloads/test.h264
// 推送已下載在文件夾的視頻
ffmpeg -re -i ~/Downloads/xxx.mp4 -vcodec libx264 -acodec aac -strict -2 -f flv rtmp://localhost:1935/live
// 錄制桌面
ffmpeg -f avfoundation -i "1" -vcodec libx264 -preset ultrafast -acodec libfaac -f flv rtmp://localhost:1935/rtmplive/room
// 錄制桌面和麥克風
ffmpeg -f avfoundation -i "1:0" -vcodec libx264 -preset ultrafast -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:1935/live/room
// 錄制桌面和麥克風,并打開攝像頭拍攝
ffmpeg -f avfoundation -framerate 30 -i "1:0" \-f avfoundation -framerate 30 -video_size 640x480 -i "0" \-c:v libx264 -preset ultrafast \-filter_complex 'overlay=main_w-overlay_w-10:main_h-overlay_h-10' -acodec libmp3lame -ar 44100 -ac 1 -f flv rtmp://localhost:2016/rtmplive/room
直播 DEMO:
一個簡單的直播 demo 就跑起來了。
Fluent-ffmpeg4
Fluent-ffmpeg 是將復雜的 ffmpeg 命令抽象成 NodeJS 的模塊,前提是系統已安裝 FFmpeg。
一些簡單的用法
// 視頻信息
ffmpeg.ffprobe(input, function (err, metadata) {
console.dir(metadata);
});
// 提取音頻
ffmpeg(input)
.audioCodec("libmp3lame")
.on("error", function (err) {
console.log("發生錯誤: " + err.message);
})
.on("end", function () {
console.log("提取音頻完成 !");
})
.save(resOut);
// 提取視頻
ffmpeg(input)
.noAudio()
.on("error", function (err) {
console.log("發生錯誤: " + err.message);
})
.on("end", function () {
console.log("提取視頻完成 !");
})
.save(resOut);
回顧一下,廣義上的視頻:由音頻和視頻兩部分組成,它們分別有對應的各自的編碼規范,視頻容器是將不同編碼格式的音、視頻組合在一起的一種封裝格式。視頻編碼格式主要是對視頻的大小進行壓縮,分為幀內壓縮和幀間壓縮,幀間壓縮主要是通過記錄關鍵幀形式來進行壓縮。
FFmpeg 是處理音視頻編碼的一種程序,主要原理:demuxer=> decoder=> encoder=> muxer。
Fluent-ffmpeg 是將復雜的 ffmpeg 命令抽象成 nodeJs 的模塊,前提是系統已安裝 FFmpeg,這對于前端工程師來說,可以用它處理眾多音視頻操作。
下一篇預告:FFmpeg 和 wasm 在瀏覽器中的碰撞
1. 即時通訊網-史上最通俗視頻編碼技術入門:http://www.52im.net/thread-2840-1-1.html
2. 簡書-音視頻基礎知識:https://www.jianshu.com/p/614b3e6e641a
3. 濾鏡實現各種圖片效果 | Video-Filters | avfilter | 變色:https://www.geek-share.com/detail/2763908000.html
4. 阮一峰:FFmpeg 視頻處理入門教程:http://www.ruanyifeng.com/blog/2020/01/ffmpeg.html
.環境準備
需要在執行路徑下,準備一個xxx.yuv的文件。然后在qt的命令行這里,輸入如下:
點擊運行,編碼出h264文件??闯鰜磉@個壓縮比達到1:87,效果還是不錯的。
下面是一些發幀的時間,編碼時間等信息。
發了很多次數據,才開始編碼成正真的數據packet。
當文件讀取完時,必須要去沖刷編碼器,把更多的緩存packet取出來。
2.FFmpeg編碼視頻流程
從本地讀取YUV數據編碼為h264格式的數據,然后再存?到本地,編碼后的數據有帶startcode。
與FFmpeg ?頻編碼的流程基本?致。
主要API說明:
(1)avcodec_find_encoder_by_name:根據指定的編碼器名稱查找注冊的編碼器。
(2)avcodec_alloc_context3:為AVCodecContext分配內存。
(3)avcodec_open2:打開編解碼器。
(4)avcodec_send_frame:將AVFrame?壓縮數據給編碼器。
(5)avcodec_receive_packet:獲取到編碼后的AVPacket數據。
(6)av_frame_get_buffer: 為?頻或視頻數據分配新的buffer。在調?這個函數之前,必須在AVFame上設置好以下屬性:format(視頻為像素格式,?頻為樣本格式)、nb_samples(樣本個數,針對?頻)、channel_layout(通道類型,針對?頻)、width/height(寬?,針對視頻)。因為這個Buf是根據這些參數來計算。
(7)av_frame_make_writable:確保AVFrame是可寫的,盡可能避免數據的復制。如果AVFrame不是可寫的,將分配新的buffer和復制數據,避免數據沖突。
(8)av_image_fill_arrays:存儲?幀像素數據存儲到AVFrame對應的data buffer。
(9)av_image_get_buffer_size,通過指定像素格式、圖像寬、圖像?來計算所需的內存??。
int av_image_get_buffer_size(enum AVPixelFormat pix_fmt, int width, int height, int align)
重點說明?個參數align:
此參數是設定內存對?的對?數,也就是按多?的字節進?內存對?。
?如設置為1,表示按1字節對?,那么得到的結果就是與實際的內存???樣。
再?如設置為4,表示按4字節對?。也就是內存的起始地址必須是4的整倍數。
(9)av_image_alloc
av_image_alloc()是這樣定義的。此函數的功能是按照指定的寬、?、像素格式來分配圖像內存。
int av_image_alloc(uint8_t *pointers[4], int linesizes[4], int w, int h, enum AVPixelFormat pix_fmt,
int align);
pointers[4]:保存圖像通道的地址。如果是RGB,則前三個指針分別指向R,G,B的內存地址。第四個指針保留不?。
linesizes[4]:保存圖像每個通道的內存對?的步?,即??的對?內存的寬度,此值??等于圖像寬度。
w: 要申請內存的圖像寬度。
h: 要申請內存的圖像?度。
pix_fmt: 要申請內存的圖像的像素格式。
align: ?于內存對?的值。
返回值:所申請的內存空間的總??。如果是負值,表示申請失敗。
(10)av_image_fill_arrays
int av_image_fill_arrays(uint8_t *dst_data[4], int dst_linesize[4], const uint8_t *src, enum
AVPixelFormat pix_fmt, int width, int height, int align);
av_image_fill_arrays()
函數?身不具備內存申請的功能,此函數類似于格式化已經申請的內存,即通過av_malloc()函數申請的內存空間,或者av_frame_get_buffer()函數申請的內存空間。
dst_data[4]: [out]對申請的內存格式化為三個通道后,分別保存其地址。
dst_linesize[4]: [out]格式化的內存的步?(即內存對?后的寬度)。
*src: [in]av_alloc()函數申請的內存地址。
pix_fmt: [in] 申請 src內存時的像素格式
width: [in]申請src內存時指定的寬度
height: [in]申請scr內存時指定的?度
align: [in]申請src內存時指定的對?字節數。
H.264 碼率設置
一般在實際的直播項目中,如果為了匹配不同分辨率,一般都是設置動態緩沖區適配,這樣可以把不同分辨率,不同碼率的延遲降到最低。這就是一個實戰中得出的優化經驗。
什么是視頻碼率?
視頻碼率是視頻數據(包含視頻?彩量、亮度量、像素量)每秒輸出的位數。?般?的單位是kbps。
設置視頻碼率的必要性
在?絡視頻應?中,視頻質量和?絡帶寬占?是相?盾的。通常情況下,視頻流占?的帶寬越?則視頻質量也越?,需要的?絡帶寬也越?,解決這??盾的鑰匙當然是視頻編解碼技術。評判?種視頻編解碼技術的優劣,是?較在相同的帶寬條件下,哪個視頻質量更好;在相同的視頻質量條件下,哪個占?的?絡帶寬更少(?件體積?)。
是不是視頻碼率越?,質量越好呢?
理論上是這樣的。然?在我們?眼分辨的范圍內,當碼率?到?定程度時,就沒有什么差別了。所以碼率設置有它的最優值,H.264(也叫AVC或X264)的?件中,視頻的建議碼率如下:
?機設置碼率建議
通過上?的介紹,結合我做過的?些?機項?,我總結了?套設置碼率的公式,分享給?家如下:
如果1080P,碼率低于2M,實際效果很差。不建議這么做,那樣沒什么意義。
FFmpeg與H264編碼指南
鑒于x264的參數眾多,各種參數的配合復雜,為了使?者?便,x264建議如?特別需要可使?preset和tune設置。這套開發者推薦的參數較為合理,可在此基礎上在調整?些具體參數以符合??需要,?動設定的參數會覆蓋preset和tune?的參數。
使?ffmpeg -h encoder=libx264 命令查詢相關?持的參數。
英?地址:https://trac.ffmpeg.org/wiki/Encode/H.264。內容有?定出?,但是可以借鑒學習。
x264是?個 H.264/MPEG4 AVC 編碼器,本指南將指導新?如何創建?質量的H.264視頻。
對于普通?戶通常有兩種碼率控制模式:CRF(Constant Rate Factor)和Two pass ABR。碼率控制是?種決定為每?個視頻幀分配多少?特數的?法,它將決定?件的??和質量的分配。
如果你在編譯和安裝libx264 ??需要幫助,請查看ffmpeg和x264編譯指南:
http://ffmpeg.org/trac/ffmpeg/wiki/CompilationGuide
CRF
量化?例的范圍為0~51,其中0為?損模式,23為缺省值,51可能是最差的。該數字越?,圖像質量越好。從主觀上講,18~28是?個合理的范圍。18往往被認為從視覺上看是?損的,它的輸出視頻質量和輸?視頻?模?樣或者說相差??。但從技術的?度來講,它依然是有損壓縮。
若CRF值加6,輸出碼率?概減少?半;若CRF值減6,輸出碼率翻倍。通常是在保證可接受視頻質量的前提下選擇?個最?的CRF值,如果輸出視頻質量很好,那就嘗試?個更?的值,如果看起來很糟,那就嘗試?個??點值。
注意:本?所提到的量化?例只適?于8-bit x264(10-bit x264的量化?例 為0~63),你可以使?x264--help命令在Output bit depth選項查看輸出位深,在各種版本中,8bit是最常?的。
preset
預設是?系列參數的集合,這個集合能夠在編碼速度和壓縮率之間做出?個權衡。?個編碼速度稍慢的預設會提供更?的壓縮效率(壓縮效率是以?件??來衡量的)。這就是說,假如你想得到?個指定??的?件或者采?恒定?特率編碼模式,你可以采??個較慢的預設來獲得更好的質量。同樣的,對于恒定質量編碼模式,你可以通過選擇?個較慢的預設輕松地節省?特率。這里后面會根據代碼來設置。
通常的建議是使?最慢的預設。?前所有的預設按照編碼速度降序排列為:
ultrafast
superfast
veryfast
faster
fast
medium – default preset
slow
slower
veryslow
placebo - ignore this as it is not useful (see FAQ)
默認為medium級別。
你可以使?--preset來查看預設列表,也可以通過x264 --fullhelp來查看預設所采?的參數配置。
tune
tune是x264中重要性僅次于preset的選項,它是視覺優化的參數,tune可以理解為視頻偏好(或者視頻類型),tune不是?個單?的參數,?是由?組參數構成-tune來改變參數設置。當前的 tune包括:
film:電影類型,對視頻的質量?常嚴格時使?該選項
animation:動畫?,壓縮的視頻是動畫?時使?該選項
grain:顆粒物很重,該選項適?于顆粒感很重的視頻
stillimage:靜態圖像,該選項主要?于靜?畫??較多的視頻
psnr:提?psnr,該選項編碼出來的視頻psnr?較?
ssim:提?ssim,該選項編碼出來的視頻ssim?較?
fastdecode:快速解碼,該選項有利于快速解碼
zerolatency:零延遲,該選項主要?于視頻直播
如果你不確定使?哪個選項或者說你的輸?與所有的tune皆不匹配,你可以忽略--tune 選項。你可以使?-tune來查看tune列表,也可以通過x264 --fullhelp來查看tune所采?的參數配置。
profile
另外?個可選的參數是-profile:v,它可以將你的輸出限制到?個特定的 H.264 profile。?些?常?的或者要被淘汰的設備僅?持有限的選項,?如只?持baseline或者main。
所有的profile 包括:
baseline profile:基本畫質。?持I/P 幀,只?持?交錯(Progressive)和CAVLC
extended profile:進階畫質。?持I/P/B/SP/SI 幀,只?持?交錯(Progressive)和CAVLC
main profile:主流畫質。提供I/P/B 幀,?持?交錯(Progressive)和交錯(Interlaced),也?持CAVLC 和CABAC 的?持。
high profile:?級畫質。在main Profile 的基礎上增加了8x8內部預測、?定義量化、 ?損視頻編碼和更多的YUV 格式。
想要說明H.264 high profile與H.264 main profile的區別就要講到H.264的技術發展了。JVT于2003年完成H.264基本部分標準制定?作,包含baseline profile、extended profile和main profile,分別包括不同的編碼?具。之后JVT?完成了H.264 FRExt(即:Fidelity Range Extensions)擴展部分(Amendment)的制定?作,包括high profile(HP)、high 10 profile(Hi10P)、high 4:2:2profile(Hi422P)、high 4:4:4 profile(Hi444P)4個profile。
H.264 baseline profile、extended profile和main profile都是針對8位樣本數據、4:2:0格式的視頻序列,FRExt將其擴展到8~12位樣本數據,視頻格式可以為4:2:0、4:2:2、4:4:4,設?了highprofile(HP)、high 10 profile(Hi10P)、high 4:2:2 profile(Hi422P)、high 4:4:4profile(Hi444P) 4個profile,這4個profile都以main profile為基礎
在相同配置情況下,High profile(HP)可以?Main profile(MP)節省10%的碼流量,編碼時間可能更長,?MPEG-2MP節省60%的碼流量,具有更好的編碼性能。根據應?領域的不同:
baseline profile:多應?于實時通信領域
main profile:多應?于流媒體領域
high profile:則多應?于?電和存儲領域
關于profile和level控制,可以參考這篇文章:
https://www.jianshu.com/p/48d723bb2740
低延遲
x264提供了?個 -tune zerolatency 選項。
兼容性
如果你想讓你的視頻最?化的和?標播放設備兼容(?如?版本的的ios或者所有的android 設備),那么你可以這做
-profile:v baseline
這將會關閉很多?級特性,但是它會提供很好的兼容性。也許你可能不需要這些設置,因為?旦你?了這些設置,在同樣的視頻質量下與更?的編碼檔次相?會使?特率稍有增加。
關于profile列表和關于它們的描述,你可以運?x264 --fullhelp
要牢記apple quick time 對于x264編碼的視頻只?持 YUV 420顏?空間,?且不?持任何?于 mian profile編碼檔次。這樣對于quick time 只留下了兩個兼容選項baseline和 main。其他的編碼檔次qucik time均不?持,雖然它們均可以在其它的播放設備上回放。
有些問題,通過解答的形式給出。
兩遍編碼模式能夠?CRF模式提供更好的質量嗎?
不能,但它可以更加精確地控制?標?件??。
為什么 placebo 是?個浪費時間的玩意??
與 veryslow相?,它以極?的編碼時間為代價換取了?概1%的視頻質量提升,這是?種收益遞減準則,veryslow 與 slower相?提升了3%;slower 與 slow相?提升了5%;slow 與 medium相?提升了5%~10%。
為什么我的?損輸出看起來是?損的?
這是由于rgb->yuv的轉換,如果你轉換到yuv444,它依然是?損的。
顯卡能夠加速x264的編碼嗎?
不,x264沒有使?(?少現在沒有),有?些私有編碼器使?了GPU加快了編碼速度,但這并不意味著它們經過良好的優化。也有可能還不如x264,或許速度更慢??偟膩碚f,ffmpeg到?前為?還不?持GPU。
翻譯注釋:x264在2013版中已經開始?持基于opencl的顯卡加速,?于幀類型的判定。
為Quick time 播放器壓制視頻
你需要使?-pix_fmt yuv420p來是你的輸出?持QT 播放器。這是因為對于H.264視頻剪輯蘋果的Quicktime只?持 YUV420顏?空間。否則ffmpeg會根據你的視頻源輸出與Quick time 不兼容的視頻格式或者不是基于ffmpeg的視頻。
X264參數之zerolatency的分析
加?zerolatency之后,轉碼路數會明顯降低。
我們都知道,加?zerolatency的?的是為了降低在線轉碼的編碼延遲,那么,該參數是如何影響到x264的轉碼性能了呢?
?先,先來看看代碼中編碼延遲的影響因素:
設置zerolatency后,相應的參數配置如下:
下?我們來看?下zerolatency設置中各個參數的意義:
rc_lookahead: Set number of frames to look ahead for frametype and ratecontrol.
該參數為mb-tree碼率控制和vbv-lookahead設置可?的幀數量,最?值為250。對于mbi-tree來說,rc_lookahead值越?,會得到更準確的結果,但編碼速度也會更慢,因為編碼器需要緩存慢rc_lookahead幀數據后,才會開始第?幀編碼,增加編碼延時,因此在實時視頻通信中將其設置為0。
sync_lookahead: 設置?于線程預測的幀緩存??,最?值為250。在第?遍及更多遍編碼或基于分?線程時?動關閉。sync_lookahead=0為關閉線程預測,可減?延遲,但也會降低性能。
bframes: I幀和P幀或者兩個P幀之間可?的最?連續B幀數量,默認值為3。B幀可使?雙向預測,從?顯著提?壓縮率,但由于需要緩存更多的幀數以及重排序的原因,會降低編碼速度,增加編碼延遲,因此在實時編碼時也建議將該值設置為0。
sliced_threads: 基于分?的線程,默認值為off,開啟該?法在壓縮率和編碼效率上都略低于默認?法,但沒有編碼延時。除?在編碼實時流或者對低延遲要求較?的場合開啟該?法,?般情況下建議設為off。
vfr_input: 與force-cfr選項相對應:
vfr_input=1時,為可變幀率,使?timebase和timestamps做碼率控制;vfr_input=0時,為固定幀率,使?fps做碼率控制。
mb_tree: 基于宏塊樹的碼率控制。對于每個MB,向前預測?定數量的幀(該數量由rc_lookahead和keyint中的較?值決定),計算該MB被引?的次數,根據被引?次數的多少決定為該MB分配的量化QP值。該?法會?成?個臨時stats?件,記錄了每個P幀中每個MB被參考的情況。使?mb_tree的?法能夠節約?概30%的碼率,但同時也會增加編碼延遲,因此實時流編碼時也將其關閉。
詳細可以參考這個:https://slhck.info/video/2017/02/24/crf-guide.html
3.源碼解讀
根據傳參,查找指定編碼器。并分配編碼器上下文。
給編碼器上下文設置相關信息,如,寬高,time_base,B幀個數,gop等。
如果想一直保持I幀進行編碼,就需要設置frame->pict_type設置為AV_PICTURE_TYPE_I, 則忽略gop_size的設置。
這里的time_base與幀率設置為倒數關系即可。一般做直播都是把B幀設置為0,因為b幀會緩存較多,然后會帶來延時。
設置0延遲,畫質效果一般,精細度不夠好。
一般編碼時間與畫質是成一個反比關系,就是說如果使用ultrafast,那畫質可能就差點,使用veryslow,那畫質可能就好點。代碼設置如下:
根據debug結果顯示,ultrafast模式,編碼時間為2270ms,medium模式,編碼時間為5815ms,veryslow模式,編碼時間為19836ms。直播時,一般都設置zerolatency,為0延遲。
從圖中可以看出,當其他參數固定時,選擇不同的preset,對應的碼率和編碼時間都不?樣。
可以使?--preset來查看預設列表,也可以通過x264 --fullhelp來查看預設所采?的參數配置。一般在做直播,可以使用ultrafast、superfast、veryfast,但是就一般不會有特別復雜的算法。
碼率設置,一般需要根據分辨率去配置,也會有一般值,最小值等,當然這跟動態碼率,固定碼率這些有關系。編碼器上下文還可以設置線程數,編碼速度會快,開了多線程后也會導致幀輸出延遲, 需要緩存thread_count幀后再編程,所以直播為了降低延遲,一般是不去開啟多線程。設置AV_CODEC_FLAG_GLOBAL_HEADER,表示只有第一個I幀,具有SPS/PPS信息,此時sps pps需要從codec_ctx->extradata讀取,如果不設置,那就是每個I幀都有sps、pps 、sei。是否設置,根據自己情況去看,一般存儲本地文件時,不要去設置。
如果沒有設置AV_CODEC_FLAG_GLOBAL_HEADER,就可以看到每個I幀都有SPS和PPS,SEI信息。
這個就是SEI信息。
如果設置AV_CODEC_FLAG_GLOBAL_HEADER,存放到本地,PKT里沒有SPS和PPS信息,但是SEI信息。
這時的SPS和PPS信息,是在codec_ctx->extradata,從這里面去獲取。
注意,這個時候,是不會寫到文件里的。
打開輸入和輸出文件。
分配PKT和frame
根據設置的參數,給frame分配buffer。
按照設置的字節對齊,計算出每一幀的數據=像素格式 * 寬 * 高
分配了一個yuv_buf,用來讀取文件,存儲使用。
當緩存B幀時,為了防止外面寫數據與緩存沖突, 如果編碼器內部保持了內存參考計數,則需要重新拷貝一個備份。一般如果是X264編碼,一般是不會沖突,這里是為了起一個保險作用。
再把文件中讀取的數據,存到 yuv_buf,然后再去填充到frame里去。這里也有字節對齊設置。
這里就開始設置pts,獲取開始時間。一般pts還是要根據duration去計算比較好,如果實時流,直接從采集端拿到,然后配置下去。
編碼視頻。通過查閱代碼,使用x264進行編碼時,具體緩存幀是在x264源碼進行,不會增加avframe對應buffer的reference。一般能夠播放的h264是帶有startcode,但是從flv里抽出的h264前,是要添加startcode才行。
注意:編碼時,不管是編碼還是解碼,如果有B幀,都是要相互參考,可能輸入多幾幀進去,才會編碼出來一幀。
本篇文章就分析到這里,歡迎關注,點贊,收藏,轉發。如果需要測試代碼,那需要私信。
WebAssembly 就是運行在 Web 平臺上的 Assembly。Assembly 是指匯編代碼,是直接操作 CPU 的指令代碼,比如 x86 指令集上的匯編代碼有指令集、寄存器、棧等等設計,CPU 根據匯編代碼的指導進行運算。匯編代碼相當于 CPU 執行的機器碼能夠轉換成的人類適合讀的一種語言。
Wasm的技術優勢: 性能高效:WASM采用二進制編碼,在程序執行過程中的性能優越; 存儲成本低:相對于文本格式,二進制編碼的文本占用的存儲空間更小; 多語言支持:用戶可以使用 C/C++/RUST/Go等多種語言編寫智能合約并編譯成WASM格式的字節碼;
(1)CSDN上的下載地址
下載地址: https://download.csdn.net/download/xiaolong1126626497/82868215
(2)GitHub倉庫下載地址
https://github.com/wang-bin/avbuild
https://sourceforge.net/projects/avbuild/files/
https://sourceforge.net/projects/avbuild/files/wasm/
(3)這里有編譯好的ffmpeg.wasm文件,前端JS可以直接調用完成視頻轉碼等功能 https://github.com/ffmpegwasm/ffmpeg.wasm
const fs=require('fs');
const { createFFmpeg, fetchFile }=require('@ffmpeg/ffmpeg');
const ffmpeg=createFFmpeg({ log: true });
(async ()=> {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'test.avi', await fetchFile('./test.avi'));
await ffmpeg.run('-i', 'test.avi', 'test.mp4');
await fs.promises.writeFile('./test.mp4', ffmpeg.FS('readFile', 'test.mp4'));
process.exit(0);
})();
(4)ffmpeg編譯wasm文件的源碼,可以自行編譯wasm文件: https://github.com/ffmpegwasm/ffmpeg.wasm-core
下面只是編寫了一個打印版本號的函數,用于測試ffmpeg的庫和相關函數是否可以正常調用。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <emscripten/emscripten.h>
#include <libavcodec/version.h>
//獲取版本號
void print_version()
{
unsigned codecVer=avcodec_version();
int ver_major, ver_minor, ver_micro;
ver_major=(codecVer >> 16) & 0xff;
ver_minor=(codecVer >> 8) & 0xff;
ver_micro=(codecVer) & 0xff;
printf("當前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);
}
emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_print_version']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js
編譯成功后生成的wasm和js文件:
編寫HTML文件調用js文件里的接口。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js調用c語言函數示例</title>
</head>
<body>
<script type='text/javascript'>
function run1()
{
_print_version();
}
</script>
<input type="button" value="打印版本號" onclick="run1()" />
<script async type="text/javascript" src="ffmpeg_decoder.js"></script>
</body>
</html>
cmd命令行運行python,開啟http服務器。
python -m http.server
打開谷歌瀏覽器,輸入http://127.0.0.1:8000/index.html地址,按下F12打開控制臺,點擊頁面上的按鈕看控制臺輸出。
完成調用,已成功打印版本號。
wasm編譯的ffmpeg代碼,不能使用avformat_open_input 直接打開文件地址,打開網絡地址,只能從內存中讀取數據進行解碼。前端js加載了本地磁盤文件后,需要通過內存方式傳遞給wasm-ffmpeg接口里,然后ffmpeg再進行解碼。
下面C語言代碼里演示了調用ffmpeg解碼內存里視頻文件過程,解碼讀取分辨率、總時間,解幀數據等。代碼只是為了演示如何調用ffmpeg的測試代碼,代碼比較簡單,只是解碼了第一幀數據,得到了YUV420P數據,然后保存在文件中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <emscripten/emscripten.h>
#include <libavcodec/version.h>
//EMSCRIPTEN_KEEPALIVE
/*
存儲視頻文件到磁盤
參數:
char *name 文件名稱
char *buf 寫入的數據
unsigned int len 寫入長度
*/
int write_file(char *name, char *buf, unsigned int len)
{
//創建文件
FILE *new_fp=fopen(name, "wb");
if (new_fp==NULL)
{
printf("%s 文件創建失敗.\n", name);
return -1;
}
else
{
printf("%s 文件創建成功.\n", name);
}
//寫入磁盤
int cnt=fwrite(buf, 1, len, new_fp);
printf("成功寫入=%d 字節\n", cnt);
//關閉文件
fclose(new_fp);
return cnt;
}
/*
獲取文件大小
*/
long get_FileSize(char *name)
{
/*1. 打開文件*/
FILE *fp=fopen(name, "rb");
if (fp==NULL)
{
printf("% 文件不存在.\n", name);
return -1;
}
/*2. 將文件指針偏移到文件結尾*/
fseek(fp, 0, SEEK_END);
/*3. 獲取當前文件指針距離文件頭的字節偏移量*/
long byte=ftell(fp);
/*4. 關閉文件*/
fclose(fp);
return byte;
}
/*
讀文件
char *buf
*/
unsigned char *read_file(char *name)
{
//創建文件
FILE *fp=fopen(name, "rb");
if (fp==NULL)
{
printf("%s 文件打開失敗.\n", name);
return -1;
}
//獲取文件大小
int size=get_FileSize(name);
//申請空間
unsigned char *buf=(unsigned char *)malloc(size);
if (buf==NULL)
{
printf("空間申請失敗:%d byte.\n", size);
return NULL;
}
//讀取文件到內存
int cnt=fread(buf, 1, size, fp);
printf("成功讀取=%d 字節\n", cnt);
//關閉文件
fclose(fp);
return buf;
}
//獲取版本號
void print_version()
{
unsigned codecVer=avcodec_version();
int ver_major, ver_minor, ver_micro;
ver_major=(codecVer >> 16) & 0xff;
ver_minor=(codecVer >> 8) & 0xff;
ver_micro=(codecVer) & 0xff;
printf("當前ffmpeg的版本:avcodec version is: %d=%d.%d.%d\n", codecVer, ver_major, ver_minor, ver_micro);
}
int ffmpeg_laliu_run_flag=1;
/*
功能: 這是FFMPEG回調函數,返回1表示超時 0表示正常
ffmpeg阻塞完成一些任務的時候,可以快速強制退出.
*/
static int interrupt_cb(void *ctx)
{
if (ffmpeg_laliu_run_flag==0)return 1;
return 0;
}
//存放視頻解碼的詳細信息
struct M_VideoInfo
{
int64_t duration;
int video_width;
int video_height;
};
struct M_VideoInfo m_VideoInfo;
//讀取數據的回調函數-------------------------
//AVIOContext使用的回調函數!
//注意:返回值是讀取的字節數
//手動初始化AVIOContext只需要兩個東西:內容來源的buffer,和讀取這個Buffer到FFmpeg中的函數
//回調函數,功能就是:把buf_size字節數據送入buf即可
//第一個參數(void *opaque)一般情況下可以不用
/*正確方式*/
struct buffer_data
{
uint8_t *ptr; /* 文件中對應位置指針 */
size_t size; ///< size left in the buffer /* 文件當前指針到末尾 */
};
// 重點,自定的buffer數據要在外面這里定義
struct buffer_data bd={ 0 };
//用來將內存buffer的數據拷貝到buf
int read_packet(void *opaque, uint8_t *buf, int buf_size)
{
buf_size=FFMIN(buf_size, bd.size);
if (!buf_size)
return AVERROR_EOF;
printf("ptr:%p size:%zu bz%zu\n", bd.ptr, bd.size, buf_size);
/* copy internal buffer data to buf */
memcpy(buf, bd.ptr, buf_size);
bd.ptr +=buf_size;
bd.size -=buf_size;
return buf_size;
}
//ffmpeg解碼使用的全局變量
unsigned char * iobuffer;
AVFormatContext * format_ctx;
int video_width=0;
int video_height=0;
int video_stream_index=-1;
char* video_buffer;
/*
函數功能: 初始化解碼環境
函數參數:
unsigned char *buf 視頻文件的內存地址
unsigned int len 視頻文件長度
*/
int initDecoder(unsigned char *buf,unsigned int len)
{
int ret=0;
bd.ptr=buf; /* will be grown as needed by the realloc above */
bd.size=len; /* no data at this point */
//注冊ffmpeg
av_register_all();
unsigned int version=avformat_version();
printf("ffmpeg版本: %d\r\n",version);
// Allocate an AVFormatContext
format_ctx=avformat_alloc_context();
if (format_ctx==NULL)
{
printf("avformat_alloc_context 失敗.\n");
return -1;
}
iobuffer=(unsigned char *)av_malloc(32768);
AVIOContext *avio=avio_alloc_context(iobuffer, 32768, 0, NULL, read_packet, NULL, NULL);
format_ctx->pb=avio;
ret=avformat_open_input(&format_ctx, "nothing", NULL, NULL);
format_ctx->interrupt_callback.callback=interrupt_cb; //--------注冊回調函數
AVDictionary* options=NULL;
//ret=avformat_open_input(&format_ctx, url, NULL, NULL);
if (ret !=0)
{
char buf[1024];
av_strerror(ret, buf, 1024);
printf("無法打開視頻內存,return value: %d \n",ret);
return -1;
}
printf("正在讀取媒體文件的數據包以獲取流信息.\n");
// 讀取媒體文件的數據包以獲取流信息
ret=avformat_find_stream_info(format_ctx, NULL);
if (ret < 0)
{
printf("無法獲取流信息: %d\n",ret);
return -1;
}
AVCodec *video_pCodec;
// audio/video stream index
printf("視頻中流的數量: %d\n",format_ctx->nb_streams);
printf("視頻總時間:%lld 秒\n",format_ctx->duration / AV_TIME_BASE);
//得到秒單位的總時間
m_VideoInfo.duration=format_ctx->duration / AV_TIME_BASE;
for (int i=0; i < format_ctx->nb_streams; ++i)
{
const AVStream* stream=format_ctx->streams[i];
printf("編碼數據的類型: %d\n",stream->codecpar->codec_id);
if (stream->codecpar->codec_type==AVMEDIA_TYPE_VIDEO)
{
//查找解碼器
video_pCodec=avcodec_find_decoder(AV_CODEC_ID_H264);
//打開解碼器
int err=avcodec_open2(stream->codec, video_pCodec, NULL);
if (err !=0)
{
printf("H264解碼器打開失敗.\n");
return 0;
}
video_stream_index=i;
//得到視頻幀的寬高
video_width=stream->codecpar->width;
video_height=stream->codecpar->height;
//保存寬和高
m_VideoInfo.video_height=video_height;
m_VideoInfo.video_width=video_width;
//解碼后的YUV數據存放空間
video_buffer=malloc(video_height * video_width * 3 / 2);
printf("視頻幀的尺寸(以像素為單位): (寬X高)%dx%d 像素格式: %d\n",
stream->codecpar->width,stream->codecpar->height,stream->codecpar->format);
}
else if (stream->codecpar->codec_type==AVMEDIA_TYPE_AUDIO)
{
}
}
if (video_stream_index==-1)
{
printf("沒有檢測到視頻流.\n");
return -1;
}
printf("初始化成功.\n");
return 0;
}
//獲取視頻總時長
int64_t GetVideoDuration()
{
return m_VideoInfo.duration;
}
//獲取視頻寬
int64_t GetVideoWidth()
{
return m_VideoInfo.video_width;
}
//獲取視頻高
int64_t GetVideoHeight()
{
return m_VideoInfo.video_height;
}
//獲取視頻幀
//傳入參數時間單位--秒
unsigned char *GetVideoFrame(int time)
{
AVPacket pkt;
double video_clock;
AVFrame *SRC_VIDEO_pFrame=av_frame_alloc();
printf("開始解碼.\n");
printf("跳轉狀態:%d\n",av_seek_frame(format_ctx, -1, time*AV_TIME_BASE, AVSEEK_FLAG_ANY));
while (1)
{
int var=av_read_frame(format_ctx, &pkt);
//讀取一幀數據
if (var < 0)
{
printf("數據讀取完畢:%d\n", var);
break;
}
printf("開始..\n");
//如果是視頻流節點
if (pkt.stream_index==video_stream_index)
{
//當前時間
video_clock=av_q2d(format_ctx->streams[video_stream_index]->time_base) * pkt.pts;
printf("pkt.pts=%0.2f,video_clock=%0.2f\n", pkt.pts, video_clock);
//解碼視頻 frame
//發送視頻幀
if (avcodec_send_packet(format_ctx->streams[video_stream_index]->codec, &pkt) !=0)
{
av_packet_unref(&pkt);//不成功就釋放這個pkt
continue;
}
//接受后對視頻幀進行解碼
if (avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, SRC_VIDEO_pFrame) !=0)
{
av_packet_unref(&pkt);//不成功就釋放這個pkt
continue;
}
//轉格式
/* sws_scale(img_convert_ctx,
(uint8_t const **)SRC_VIDEO_pFrame->data,
SRC_VIDEO_pFrame->linesize, 0,video_height, RGB24_pFrame->data,
RGB24_pFrame->linesize);*/
memset(video_buffer, 0, video_height * video_width * 3 / 2);
int height=video_height;
int width=video_width;
printf("decode video ok\n");
int a=0, i;
for (i=0; i < height; i++)
{
memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[0] + i * SRC_VIDEO_pFrame->linesize[0], width);
a +=width;
}
for (i=0; i < height / 2; i++)
{
memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[1] + i * SRC_VIDEO_pFrame->linesize[1], width / 2);
a +=width / 2;
}
for (i=0; i < height / 2; i++)
{
memcpy(video_buffer + a, SRC_VIDEO_pFrame->data[2] + i * SRC_VIDEO_pFrame->linesize[2], width / 2);
a +=width / 2;
}
//保存在文件中:
//write_file("./666.yuv", video_buffer, video_height * video_width * 3 / 2);
printf("退出成功....\n");
break;
}
//釋放包
av_packet_unref(&pkt);
}
av_free(SRC_VIDEO_pFrame);
return video_buffer;
}
//銷毀內存
void DeleteMemory()
{
//釋放空間
av_free(iobuffer);
}
emcc wasm_ffmpeg/wasm_ffmpeg.c ffmpeg-4.4-wasm/lib/libavformat.a ffmpeg-4.4-wasm/lib/libavcodec.a ffmpeg-4.4-wasm/lib/libswresample.a ffmpeg-4.4-wasm/lib/libavutil.a -I "ffmpeg-4.4-wasm/include" -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString','_initDecoder','_write_file','_print_version','_get_FileSize','_read_file','_GetVideoFrame','_GetVideoWidth','_GetVideoDuration','_GetVideoHeight','_DeleteMemory']" -s WASM=1 -s ASSERTIONS=0 -s TOTAL_MEMORY=167772160 -s ALLOW_MEMORY_GROWTH=1 -o out/ffmpeg_decoder.js
編譯成功后生成的wasm和js文件:
完成了視頻選擇,播放,調用了C語言編寫的接口完成解碼返回,但是沒有渲染。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js調用c語言函數示例</title>
</head>
<body>
<input id="myfile" type="file"/>
<video id="output-video" width="300" controls></video>
<div><canvas id="glcanvas" width="640" height="480"></canvas></div>
<script>
//代碼摘自:https://github.com/ivan-94/video-push/blob/master/yuv/index.html#L312
const video=document.getElementById('glcanvas');
let renderer;
class WebglScreen {
constructor(canvas) {
this.canvas=canvas;
this.gl= canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl');
this._init();
}
_init() {
let gl=this.gl;
if (!gl) {
console.log('gl not support!');
return;
}
// 圖像預處理
gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
// GLSL 格式的頂點著色器代碼
let vertexShaderSource=`
attribute lowp vec4 a_vertexPosition;
attribute vec2 a_texturePosition;
varying vec2 v_texCoord;
void main() {
gl_Position=a_vertexPosition;
v_texCoord=a_texturePosition;
}
`;
let fragmentShaderSource=`
precision lowp float;
uniform sampler2D samplerY;
uniform sampler2D samplerU;
uniform sampler2D samplerV;
varying vec2 v_texCoord;
void main() {
float r,g,b,y,u,v,fYmul;
y=texture2D(samplerY, v_texCoord).r;
u=texture2D(samplerU, v_texCoord).r;
v=texture2D(samplerV, v_texCoord).r;
fYmul=y * 1.1643828125;
r=fYmul + 1.59602734375 * v - 0.870787598;
g=fYmul - 0.39176171875 * u - 0.81296875 * v + 0.52959375;
b=fYmul + 2.01723046875 * u - 1.081389160375;
gl_FragColor=vec4(r, g, b, 1.0);
}
`;
let vertexShader=this._compileShader(
vertexShaderSource,
gl.VERTEX_SHADER,
);
let fragmentShader=this._compileShader(
fragmentShaderSource,
gl.FRAGMENT_SHADER,
);
let program=this._createProgram(vertexShader, fragmentShader);
this._initVertexBuffers(program);
// 激活指定的紋理單元
gl.activeTexture(gl.TEXTURE0);
gl.y=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program, 'samplerY'), 0);
gl.activeTexture(gl.TEXTURE1);
gl.u=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program, 'samplerU'), 1);
gl.activeTexture(gl.TEXTURE2);
gl.v=this._createTexture();
gl.uniform1i(gl.getUniformLocation(program, 'samplerV'), 2);
}
/**
* 初始化頂點 buffer
* @param {glProgram} program 程序
*/
_initVertexBuffers(program)
{
let gl=this.gl;
let vertexBuffer=gl.createBuffer();
let vertexRectangle=new Float32Array([
1.0,
1.0,
0.0,
-1.0,
1.0,
0.0,
1.0,
-1.0,
0.0,
-1.0,
-1.0,
0.0,
]);
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向緩沖區寫入數據
gl.bufferData(gl.ARRAY_BUFFER, vertexRectangle, gl.STATIC_DRAW);
// 找到頂點的位置
let vertexPositionAttribute=gl.getAttribLocation(
program,
'a_vertexPosition',
);
// 告訴顯卡從當前綁定的緩沖區中讀取頂點數據
gl.vertexAttribPointer(
vertexPositionAttribute,
3,
gl.FLOAT,
false,
0,
0,
);
// 連接vertexPosition 變量與分配給它的緩沖區對象
gl.enableVertexAttribArray(vertexPositionAttribute);
let textureRectangle=new Float32Array([
1.0,
0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
1.0,
]);
let textureBuffer=gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer);
gl.bufferData(gl.ARRAY_BUFFER, textureRectangle, gl.STATIC_DRAW);
let textureCoord=gl.getAttribLocation(program, 'a_texturePosition');
gl.vertexAttribPointer(textureCoord, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(textureCoord);
}
/**
* 創建并編譯一個著色器
* @param {string} shaderSource GLSL 格式的著色器代碼
* @param {number} shaderType 著色器類型, VERTEX_SHADER 或 FRAGMENT_SHADER。
* @return {glShader} 著色器。
*/
_compileShader(shaderSource, shaderType)
{
// 創建著色器程序
let shader=this.gl.createShader(shaderType);
// 設置著色器的源碼
this.gl.shaderSource(shader, shaderSource);
// 編譯著色器
this.gl.compileShader(shader);
const success=this.gl.getShaderParameter(
shader,
this.gl.COMPILE_STATUS,
);
if (!success) {
let err=this.gl.getShaderInfoLog(shader);
this.gl.deleteShader(shader);
console.error('could not compile shader', err);
return;
}
return shader;
}
/**
* 從 2 個著色器中創建一個程序
* @param {glShader} vertexShader 頂點著色器。
* @param {glShader} fragmentShader 片斷著色器。
* @return {glProgram} 程序
*/
_createProgram(vertexShader, fragmentShader)
{
const gl=this.gl;
let program=gl.createProgram();
// 附上著色器
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
// 將 WebGLProgram 對象添加到當前的渲染狀態中
gl.useProgram(program);
const success=this.gl.getProgramParameter(
program,
this.gl.LINK_STATUS,
);
if (!success) {
console.err(
'program fail to link' + this.gl.getShaderInfoLog(program),
);
return;
}
return program;
}
/**
* 設置紋理
*/
_createTexture(filter=this.gl.LINEAR)
{
let gl=this.gl;
let t=gl.createTexture();
// 將給定的 glTexture 綁定到目標(綁定點
gl.bindTexture(gl.TEXTURE_2D, t);
// 紋理包裝 參考https://github.com/fem-d/webGL/blob/master/blog/WebGL基礎學習篇(Lesson%207).md -> Texture wrapping
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// 設置紋理過濾方式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
return t;
}
/**
* 渲染圖片出來
* @param {number} width 寬度
* @param {number} height 高度
*/
renderImg(width, height, data)
{
let gl=this.gl;
// 設置視口,即指定從標準設備到窗口坐標的x、y仿射變換
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// 設置清空顏色緩沖時的顏色值
gl.clearColor(0, 0, 0, 0);
// 清空緩沖
gl.clear(gl.COLOR_BUFFER_BIT);
let uOffset=width * height;
let vOffset=(width >> 1) * (height >> 1);
gl.bindTexture(gl.TEXTURE_2D, gl.y);
// 填充紋理
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width,
height,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data.subarray(0, uOffset),
);
gl.bindTexture(gl.TEXTURE_2D, gl.u);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width >> 1,
height >> 1,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data.subarray(uOffset, uOffset + vOffset),
);
gl.bindTexture(gl.TEXTURE_2D, gl.v);
gl.texImage2D(
gl.TEXTURE_2D,
0,
gl.LUMINANCE,
width >> 1,
height >> 1,
0,
gl.LUMINANCE,
gl.UNSIGNED_BYTE,
data.subarray(uOffset + vOffset, data.length),
);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
}
/**
* 根據重新設置 canvas 大小
* @param {number} width 寬度
* @param {number} height 高度
* @param {number} maxWidth 最大寬度
*/
setSize(width, height, maxWidth)
{
let canvasWidth=Math.min(maxWidth, width);
this.canvas.width=canvasWidth;
this.canvas.height=(canvasWidth * height) / width;
}
destroy()
{
const { gl }=this;
gl.clear(
gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT,
);
}
} // end of webgl
const initialCanvas=(canvas, width, height)=> {
canvas.width=width;
canvas.height=height;
return new WebglScreen(canvas);
};
const render=(buff,width,height)=>
{
if (renderer==null) {
return;
}
renderer.renderImg(width, height, buff);
};
</script>
<script type='text/javascript'>
function run1()
{
}
function run2()
{
}
//加載本地文件
var file=document.getElementById("myfile");
file.onchange=function(event){
let fileReader=new FileReader();
fileReader.onload=function(){
// 當 FileReader 讀取文件時候,讀取的結果會放在 FileReader.result 屬性中
var fileArray=this.result;
console.log(fileArray);
let fileBuffer=new Uint8Array(this.result);
console.log(fileBuffer);
//申請空間
var fileBufferPtr=_malloc(fileBuffer.length)
//將fileBuffer里的內容拷貝到fileBufferPtr里
Module.HEAP8.set(fileBuffer,fileBufferPtr)
//1. 寫文件
//申請空間,存放字符串
//var name=allocate(intArrayFromString("./tmp.mp4"), ALLOC_NORMAL);
//var run_var=_write_file(name,fileBufferPtr,fileBuffer.length);
//console.log('寫文件成功字節數:',run_var);
//2. 獲取文件大小
//var file_size=_get_FileSize(name);
//console.log('獲取文件大小:',file_size);
//const data=ffmpeg.FS('readFile', 'output.mp4');
//3. 讀取文文件
//const data=_read_file(name);
// const video=document.getElementById('output-video');
//video.src=URL.createObjectURL(new Blob([fileBuffer.buffer], { type: 'video/mp4' }));
//加載內存數據
// Module.HEAPU8.subarray(imgBufferPtr, data);
//4. 初始化解碼器,加載文件
_initDecoder(fileBufferPtr,fileBuffer.length);
//5. 獲取總時間
var time=_GetVideoDuration();
console.log('視頻總時間:'+time);
//6. 獲取視頻寬
var Width=_GetVideoWidth();
console.log('視頻寬:'+Width);
//7. 獲取視頻高
var Height=_GetVideoHeight();
console.log('視頻高:'+Height);
renderer=initialCanvas(video,Width,Height);
//申請空間,存放字符串
//var name_file=allocate(intArrayFromString("./666.yuv"), ALLOC_NORMAL);
//讀取文件
//var yuv_wasm_data=_read_file(name_file);
//8. 獲取視頻幀
var yuv_wasm_data=_GetVideoFrame(10);
var renderlength=Width*Height*3/2;
var RenderBuffer=new Uint8Array (Module.HEAPU8.subarray(yuv_wasm_data,yuv_wasm_data + renderlength + 1) );
console.log(RenderBuffer);
render(RenderBuffer,Width,Height);
};
fileReader.readAsArrayBuffer(this.files[0]);
}
</script>
<input type="button" value="載入文件初始化解碼器" onclick="run1()" />
<script async type="text/javascript" src="ffmpeg_decoder.js"></script>
</body>
</html>
命令行運行命令,開啟HTTP服務器,方便測試:
python -m http.server
打開谷歌瀏覽器,輸入http://127.0.0.1:8000/index.html地址,按下F12打開控制臺,點擊頁面上的按鈕看控制臺輸出。
(1)輸入地址,打開網頁
(2)按下F12,打開控制臺
(3)選擇一個MP4文件載入測試。獲取一幀圖片。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。