FFmpeg

一馆截、簡介

官方文檔資料:http://ffmpeg.org/documentation.html
FFmpeg官方wiki:https://trac.ffmpeg.org
雷宵華博士總結(jié)的資料:http://blog.csdn.net/leixiaohua1020
羅索實(shí)驗(yàn)室:http://www.rosoo.net
ChinaFFmpeg:http://bbs.chinaffmpeg.com

FFmpeg名稱中的mpeg來自于視頻編碼標(biāo)準(zhǔn)MPEG,而前綴FF是Fast Forward的首字母縮寫。FFmpeg既是一款音視頻編解碼工具,同時(shí)也是一組音視頻編解碼開發(fā)套件蜡娶,作為編解碼開發(fā)套件混卵,它為開發(fā)者提供了豐富的音視頻處理的調(diào)用接口。

FFmpeg轉(zhuǎn)碼工作流程

讀取文件——>解封裝——>解碼——>轉(zhuǎn)換參數(shù)——>新編碼——>封裝——>寫入文件

FFmpeg的主要模塊
  • AVUtil:核心工具類窖张,該模塊是最基礎(chǔ)的模塊之一幕随,下面的許多其他模塊都會依賴該庫做一些基本的音視頻處理操作。

  • AVFormat:文件格式和協(xié)議庫宿接,該模塊是最重要的模塊之一赘淮,封裝了Protocol層和Demuxer(解封裝)和Muxer(封裝)層。根據(jù)實(shí)際需求睦霎,可進(jìn)行媒體封裝格式的擴(kuò)展梢卸,增加自己定制的封裝格式,即在AVFormat中增加自己的封裝處理模塊碎赢。

  • AVCodec:編解碼庫低剔,該模塊也是最重要的模塊之一,封裝了Codec層肮塞,但是有一些Codec是具備自己的License的襟齿,F(xiàn)Fmpeg是不會默認(rèn)添加像libx264、FDK-AAC枕赵、lame等庫的猜欺,但是FFmpeg就像一個平臺一樣,可以將其他的第三方的Codec以插件的方式添加進(jìn)來拷窜,然后為開發(fā)者提供統(tǒng)一的接口开皿。

  • AVFilter:音視頻濾鏡庫,該模塊提供了包括音頻特效和視頻特效的處理篮昧,在使用FFmpeg的API進(jìn)行編解碼的過程中赋荆,直接使用該模塊為音視頻數(shù)據(jù)做特效處理是非常方便同時(shí)也非常高效的一種方式。

  • AVDevice:輸入輸出設(shè)備庫懊昨,比如窄潭,需要編譯出播放聲音或者視頻的工具ffplay,就需要確保該模塊是打開的,同時(shí)也需要libSDL的預(yù)先編譯酵颁,因?yàn)樵撛O(shè)備模塊播放聲音與播放視頻使用的是libSDL庫嫉你。

  • SwrRessample:該模塊可用于音頻重采樣,可以對數(shù)字音頻進(jìn)行聲道數(shù)躏惋、數(shù)據(jù)格式盯质、采樣率等多種基本信息的轉(zhuǎn)換跟畅。

  • SWScale:該模塊是將圖像進(jìn)行格式轉(zhuǎn)換的模塊麻惶,比如喇嘱,可以將YUV的數(shù)據(jù)轉(zhuǎn)換為RGB的數(shù)據(jù)。

  • PostProc:該模塊可用于進(jìn)行后期處理,當(dāng)我們使用AVFilter的時(shí)候需要打開該模塊開關(guān)准潭,因?yàn)镕ilter中會使用到該模塊的一些基礎(chǔ)函數(shù)攘乒。

二、FFmpeg命令行工具

在Mac OS系統(tǒng)上直接在命令行下鍵入以下命令:

brew install ffmpeg

就可以安裝命令行工具了惋鹅。

1.ffprobe

首先用ffprobe查看一個音頻的文件:

ffprobe ~/Desktop/32037.mp3

鍵入上述命令之后则酝,先看如下這行信息:

Duration:00:05:14.83,start:0.000000,bitrate:64kb/s

這行信息表明,該音頻文件的時(shí)長是5分14秒零830毫秒闰集,開始播放時(shí)間是0沽讹,整個媒體文件的比特率64Kbit/s,然后再看另外一行:

Stream#0:0  Audio:mp3, 24000Hz,stereo,s16p,64kb/s

這行信息表明,第一個流是音頻流武鲁,編碼格式是MP3格式爽雄,采樣率是24kHz,聲道是立體聲,采樣表示格式是SInt16(short)的planner(平鋪格式)沐鼠,這路流的比特率是64Kbit/s挚瘟。
然后在使用ffprobe查看一個視頻的文件:

ffprobe  ~/Desktop/32037.mp4

鍵入上述命令之后,可以看到第一部分的信息是Metadata信息:

Metadata:
        major_brand:isom
        minor_version:512
        compatible_brands:isomiso2avc1mp41
        encoder:Lavf55.12.100

這行信息表明了該文件的Metadata信息饲梭,比如encoder是Lavf55.12.100,其中Lavf代表的是FFmpeg輸出的文件乘盖,后面的編號代表了FFmpeg的版本代號,接下來的一行信息如下:

Duration:00:04:34.56  start:0.023220,bitrate:577kb/s

上面一行的內(nèi)容表示Duration是4分34秒560毫秒憔涉,開始播放的時(shí)間是從23ms開始播放的订框,整個文件的比特率是577Kbit/s,緊接著再看下一行:

Stream#0:0(un):Video:h264(avc1/0x31637661),yuv420p,480*480,508kb/s,24fps

這行信息表示第一個stream是視頻流,編碼方式是H264的格式(封裝格式是AVC1)兜叨,每一幀的數(shù)據(jù)表示是YUV420P的格式穿扳,分辨率是480x480,這路流的比特率是508Kbit/s,幀率是每秒鐘24幀国旷,緊接著再來看下一行:

Stream#0:1(und):Audio:aac(LC)(mp4a/0x6134706D),44100Hz,stereo,fltp,63kb/s

這行信息表示第二個stream是音頻流矛物,編碼方式是AAC(封裝格式是MP4A),并且采用的Profile是LC規(guī)格,采樣率是44100Hz,聲道數(shù)是立體聲跪但,數(shù)據(jù)表示格式是浮點(diǎn)型履羞,這路音頻流的比特率是63Kbit/s.

以上就是使用ffprobe來提取音頻文件和視頻文件頭信息的方式,以及提取出來信息的含義特漩。當(dāng)然ffprobe還有比較高級的用法吧雹,下面就來介紹幾個:

ffprobe -show_format 32037.mp4

上述命令可以輸出格式信息format_name骨杂、時(shí)間長度duration涂身、文件大小size、比特率bit_rate搓蚪、流的數(shù)目nb_streams等蛤售。

ffprobe -print_format json -show_streams 32037.mp4 

上述命令可以以JSON格式的形式輸出具體每一個流最詳細(xì)的信息,視頻中會有視頻的寬高信息、是否有b幀悴能、視頻幀的總數(shù)目揣钦、視頻的編碼格式、顯示比例漠酿、比特率等信息冯凹,音頻中會有音頻的編碼格式、表示格式炒嘲、聲道數(shù)宇姚、時(shí)間長度、比特率夫凸、幀的總數(shù)目等信息
顯示幀信息的命令如下:

ffprobe -show_frames sample.mp4

查看包信息的命令如下:

ffprobe -show_packets sample.mp4

2.ffplay

播放一個音頻文件

ffplay  32037.mp3

這時(shí)候會彈出一個窗口浑劳,一邊播放MP3文件,一邊將播放聲音的語譜圖畫到該窗口是哪個夭拌。針對該窗口的操作如下魔熏,點(diǎn)擊窗口的任意一個位置,ffplay會按照點(diǎn)擊的位置計(jì)算出時(shí)間的進(jìn)度鸽扁,然后跳(seek)到這個時(shí)間點(diǎn)上繼續(xù)播放蒜绽;按下鍵盤上的右鍵會默認(rèn)快進(jìn)10s,左鍵默認(rèn)后退10s,上鍵默認(rèn)快進(jìn)1min,下鍵默認(rèn)后退1min;按ESC鍵就是退出播放進(jìn)程桶现;如果按w鍵則將繪制音頻的波形圖等滓窍。
播放一個視頻的命令如下所示:

ffplay 32037.mp4

這時(shí)候會直接在新彈出的窗口上播放該視頻,如果想要同時(shí)播放多個文件巩那,那么只需要在多個命令行下同時(shí)執(zhí)行ffplay就可以了吏夯,在對比多個視頻質(zhì)量的時(shí)候這是一個操作技巧,此外即横,如果按s鍵則可以進(jìn)入frame-step模式噪生,即按s鍵一次就會播放下一幀圖像,這在觀察某些視頻內(nèi)部的幀內(nèi)容時(shí)也是常用的技巧东囚。

ffplay 12345.mp4 -loop 10

上述命令代表播放視頻結(jié)束之后會從頭再次播放跺嗽,共循環(huán)播放10次。

ffplay 大話西游.mkv -ast 1

上述命令表示播放視頻中的第一路音頻流页藻,如果參數(shù)ast后面跟的是2桨嫁,那么就播放第二路音頻流,如果沒有第二路音頻流的話份帐,就會靜音璃吧,同樣也可以設(shè)置參數(shù)vst,比如:

ffplay  大話西游.mkv -vst 1

上述命令表示播放視頻中的第一路視頻流,如果參數(shù)vst后面跟的是2废境,那么就播放第二路視頻流畜挨,就會是黑屏即什么都不顯示筒繁。
音頻pcm文件的播放命令

ffplay song.pcm -f  s16le -channels 2 -ar 44100

僅鍵入上述這行命令其實(shí)就可以正常播放了,前提是格式(-f)巴元、聲道數(shù)(-channels)毡咏、采樣率(-ar)必須設(shè)置正確。
一幀YUV視頻幀的播放

ffplay -f rawvideo -pixel_format yuv420p -s 480*480 texture.yuv

格式(-f rawvideo代表原始格式)逮刨、表示格式(-pixel_format yuv420p)呕缭、寬高(-s 480*480)。
對于RGB表示的圖像修己,其實(shí)是一樣的臊旭。

ffplay -f rawvideo -pixel_format rgb24 -s 480*480 texture.rgb

在ffplay中音畫同步實(shí)現(xiàn)方式有三種,分別是:以音頻為主時(shí)間軸作為同步源箩退;以視頻為主時(shí)間軸作為同步源离熏;以外部時(shí)鐘為時(shí)間軸作為同步源。默認(rèn)是以音頻為主時(shí)間軸戴涝。

首先要聲明的是滋戳,播放器接收到的視頻幀或者音頻幀,內(nèi)部都會有時(shí)間戳(PTS時(shí)鐘)來標(biāo)識它實(shí)際應(yīng)該在什么時(shí)刻進(jìn)行展示啥刻。在實(shí)際的對齊策略如下:比較視頻當(dāng)前的播放時(shí)間和音頻當(dāng)前的播放時(shí)間奸鸯,如果視頻播放過快,增通過加大延遲或者重復(fù)播放來降低來降低視頻播放速度可帽;如果視頻播放慢了娄涩,則通過減小延遲或者丟幀來追趕音頻播放的時(shí)間點(diǎn)。關(guān)鍵在于音視頻時(shí)間的比較以及延遲的計(jì)算映跟,當(dāng)然在比較的過程中蓄拣,會設(shè)置一個閾值,若超過預(yù)設(shè)的閾值就應(yīng)該做調(diào)整(丟幀渲染或者重復(fù)渲染)努隙,這就是整個對齊策略球恤。

以音頻為主時(shí)間軸作為同步源:

ffplay 12345.mp4 -sync audio

以視頻為主時(shí)間軸作為同步源

ffplay 12345.mp4 -sync video

以外部時(shí)鐘為時(shí)間軸作為同步源

ffplay 12345.mp4 -sync ext

3.ffmpeg

ffmpeg其實(shí)是這三個命令行工具里最強(qiáng)大的一個工具,如果說ffprobe是用于探測媒體文件的格式以及詳細(xì)信息荸镊,ffplay是一個播放媒體文件的工具咽斧,那么ffmpeg就是強(qiáng)大的媒體文件轉(zhuǎn)換工具。它可以轉(zhuǎn)換任何格式的媒體文件躬存,并且還可以用自己的AudioFilter以及VideoFilter進(jìn)行處理和編輯张惹,總之一句話,有了它岭洲,進(jìn)行離線處理視頻時(shí)可以做你任何你想做的事情了宛逗。下面先介紹總體的參數(shù),然后再列出經(jīng)典場景下的使用案例钦椭。
(1)通用參數(shù)
指定格式(音頻或者視頻格式)

-f fmt

指定輸入文件名拧额,在Linux下當(dāng)然也能指定:0.0(屏幕錄制)或攝像頭。

-i filename 

覆蓋已有文件

-y

指定時(shí)長

-t duration

設(shè)置文件大小的上限

-fs limit_size

從指定的時(shí)間(單位為秒)開始彪腔,也支持[-]hh:mm:ss[.xxx]的格式

-ss time_off

代表按照幀率發(fā)送侥锦,尤其在作為推流工具的時(shí)候一定要加入該參數(shù),否則ffmpeg會按照最高速率向流媒體服務(wù)器不停的發(fā)送數(shù)據(jù)德挣。

-re

指定輸出文件的流映射關(guān)系恭垦。例如:“-map 1:0 -map 1:1”要求將第二個輸入文件的第一個流和第二個流寫入輸出文件。如果沒有-map 選項(xiàng)格嗅,則ffmpeg采用默認(rèn)的映射關(guān)系番挺。

-map

(2)視頻參數(shù)

  • -b :指定比特率(bit/s),ffmpeg是自動使用VBR的,若指定了該參數(shù)則使用平均比特率屯掖。
  • -bitexact:使用標(biāo)準(zhǔn)比特率玄柏。
  • -vb:指定視頻比特率。
  • -r rate:幀速率贴铜。
  • -s size:指定分辨率(320x240)粪摘。
  • -aspect aspect:設(shè)置視頻長寬比(4:3,16:9或1.3333绍坝,1.7777)徘意。
  • -croptop size:設(shè)置頂部切除尺寸(in pixels)
  • -cropbottom size:設(shè)置底部切除尺寸(in pixels)
  • -cropleft size:設(shè)置左切除尺寸(in pixels)
  • -cropright size:設(shè)置右切除尺寸(in pixels)
  • -padtop size:設(shè)置頂部補(bǔ)齊尺寸(in pixels)
  • -padbottom size:設(shè)置底部補(bǔ)齊尺寸(in pixels)
  • -padleft size:左補(bǔ)齊(in pixels)
  • -padright size:右補(bǔ)齊(in pixels)
  • -padcolor color:補(bǔ)齊帶顏色(000000-FFFFFF)
  • -vn:取消視頻的輸出。
  • -vcodec codec:強(qiáng)制使用codec編碼方式('copy'代表不進(jìn)行重新編碼)

(3)音頻參數(shù)

  • -ab:設(shè)置比特率(單位為bit/s,老版的單位可能是Kbit/s),對于MP3格式轩褐,若要聽到較高品質(zhì)的聲音則建議設(shè)置為160Kbit/s(單聲道則設(shè)置為80Kbit/s)以上椎咧。
  • -aq quality:設(shè)置音頻質(zhì)量(指定編碼)。
  • -ar rate:設(shè)置音頻采樣率(單位為Hz).
  • -ar channels:設(shè)置聲道數(shù)把介,1就是單聲道勤讽,2就是立體聲。
  • -an:取消音頻軌拗踢。
  • -acodec codec:指定音頻編碼('copy'代表不做音頻轉(zhuǎn)碼地技,直接復(fù)制)。
  • -vol volume:設(shè)置錄制音量大忻氚巍(默認(rèn)為256)<百分比>莫矗。

1)列出ffmpeg支持的所有格式:

ffmpeg -formats

2)剪切一段媒體文件,可以是音頻或者視頻文件

ffmpeg -i input.mp4 -ss 00:00:50.0 -codec copy -t 20 output.mp4

表示將文件input.mp4從第50s開始剪切20s的時(shí)間砂缩,輸出到文件output.mp4中作谚,其中-ss指定偏移時(shí)間(time Offset),-t指定的時(shí)長(duration)。
3)如果在手機(jī)中錄制了一個時(shí)間比較長的視頻無法分享到微信中庵芭,那么可以使用ffmpeg將該視頻文件切割為多個文件:

ffmpeg -i input.mp4 -t 00:00:50 -c copy small-1mp4 -ss 00:00:50 -codec copy small-2.mp4

4)提取一個視頻文件中的音頻文件:

ffmpeg -i input.mp4 -vn -acodec copy output.m4a

5)使一個視頻的音頻靜音妹懒,即只保留視頻:

ffmpeg -i input.mp4 -an -vcodec copy output.m4a

6)從MP4文件中抽取視頻流導(dǎo)出為裸H264數(shù)據(jù):

ffmpeg -i output.mp4 -an -vcodec copy -bsf:v h264_mp4toannexb output.h264

7)使用AAC音頻數(shù)據(jù)和H264的視頻生成MP4文件:

ffmpeg -i test.aac -i test.h264 -acodec copy -bsf:a aac_adtstoasc -vcodec copy -f mp4 output.mp4

8)對音頻文件的編碼格式做轉(zhuǎn)換:

ffmpeg -i input.wav -acodec libfdk_aac output.aac

9)從WAV音頻文件中導(dǎo)出PCM裸數(shù)據(jù)

ffmpeg -i input.wav -acodec pcm_s16le -f s16le ouput.pcm

這樣就導(dǎo)出了用16個bit來表示一個sample的PCM數(shù)據(jù)了,并且每個sample的字節(jié)排列順序都是小尾端表示的格式双吆,聲道數(shù)和采樣率使用的都是原始WAV文件的聲道數(shù)和采樣率的PCM數(shù)據(jù)眨唬。
10)重新編碼視頻文件会前,復(fù)制音頻流,同時(shí)封裝到MP4格式的文件中:

ffmpeg -i input.flv -vcodec libx264 -acodec copy output.mp4

11)將一個MP4格式的視頻轉(zhuǎn)換成為gif格式的動圖:

ffmpeg -i input.mp4 -vf scale=100:-1 -t 5 -r 10 image.gif

上述命令按照分辨比例不動寬度改為100(使用VideoFilter的scaleFilter),幀率改為10(-r),只處理前5秒鐘(-t)的視頻匾竿,生成gif.
12)將一個視頻的畫面部分生成圖片瓦宜,比如要分析一個視頻里面的每一幀都是什么內(nèi)容的時(shí)候,可能就需要用到這個命令了

ffmpeg -i output.mp4 -r 0.25 frames_%04d.png

上述命令每4秒鐘截取一幀視頻畫面生成一張圖片岭妖,生成的圖片從frame_0001.png開始一直遞增下去临庇。
13.使用一組圖片可以組成一個gif,如果你連拍了一組圖片昵慌,就可以用下面的這行命令生成一個gif:

ffmpeg -i frames_%04d.png -r 5 output.gif

14.使用音頻效果器假夺,可以改變一個音頻媒體文件中的音量

ffmpeg -i input.wav -af 'volume=0.5' output.wav

15.淡入效果器的使用:

ffmpeg -i input.wav -filter_complex afade=t=in:ss=0:d=5 output.wav

上述命令可以將input.wav文件中的前5s做一個淡入效果,輸出到output.wav中
16.淡出效果器的使用

ffmpeg -i input.wav -filter_complex afade=t=out:ss=200:d=5 output.wav

17.將兩路聲音進(jìn)行合并斋攀,比如要給一段聲音加上背景音樂:

ffmpeg -i vocal.wav -i accompany.wav -filter_complex amix=inputs=2:duration=shortest output.wav

上述命令是將vocal.wav和accompany.wav兩個文件進(jìn)行mix,按照時(shí)間長度較短的音頻文件的時(shí)間長度作為最終輸出的output.wav的時(shí)間長度已卷。
18)對聲音進(jìn)行變速但不變調(diào)效果器的使用:

ffmpeg -i vocal.wav -filter_complex atempo=0.5 output.wav

上述命令是將vocal.wav按照0.5倍的速度進(jìn)行處理生成output.wav,時(shí)間長度將會變?yōu)檩斎氲?倍。但是音高是不變的淳蔼,這就是大家常說的變速不變調(diào)悼尾。
19)為視頻增加水印效果

ffmpeg -i input.mp4 -i changba_icon.png -filter_complex '[0:v][1:v]overlay=main_w-overlay_w-10:10:1[out]' -map '[out]' output.mp4

上述命令包含了幾個內(nèi)置參數(shù),main_w代表主視頻寬度肖方,overlay_w代表水印寬度闺魏,main_h代表主視頻高度,overlay_h代表水印高度俯画。
20)視頻提亮效果器的使用

ffmpeg -i input.flv -c:v libx264 -b:v 800k -c:a libfdk_aac -vf eq=brightness=0.25 -f mp4 output.mp4

提亮參數(shù)是brightness,取值范圍是從-1.0到1.0析桥,默認(rèn)值是0.
21)為視頻增加對比度效果

ffmpeg -i input.flv -c:v libx264 -b:v 800k -c:a libfdk_aac -vf eq=contrast=1.5 -f mp4 output.mp4

對比度參數(shù)是contrast,取值范圍是從-2.0到2.0,默認(rèn)值是1.0.
22)視頻旋轉(zhuǎn)效果器的使用

ffmpeg -i input.mp4 -vf "transpose=1" -b:v 600k output.mp4

23)視頻剪裁器的使用:

ffmpeg -i input.mp4 -an -vf "crop=240:480:120:0" -vcodec libx264 -b:v 600k output.mp4

24)將一張RGBA格式表示的數(shù)據(jù)轉(zhuǎn)換為JPEG格式的圖片

ffmpeg -f rawvideo -pix_fmt rgba -s 480*480 -i texture.rgb -f image2 -vcodec mjpeg output.jpg

25)將一個YUV格式表示的數(shù)據(jù)轉(zhuǎn)換為JPEG格式的圖片:

ffmpeg -f rawvideo -pix_fmt yuv420p -s 480*480 -i texture.yuv -f image1 -vcodec mjpeg output.jpg

26)將一段視頻推送到流媒體服務(wù)器上:

ffmpeg -re -i input.mp4 -acodec copy -vcodec copy -f flv rtmp://xxx

上述代碼中艰垂,rtmp://xxx代表流媒體服務(wù)器的地址泡仗,加上-re參數(shù)代表將實(shí)際媒體文件的播放速度作為推流速度進(jìn)行推送。
27)將流媒體服務(wù)器上的流dump到本地:

ffmpeg -i http://xxx/xxx.flv -acodec copy -vcodec copy -f flv output.flv

28)將兩個音頻文件以兩路流的形式封裝到一個文件中猜憎,比如在K歌的應(yīng)用場景中娩怎,原伴唱實(shí)時(shí)切換的場景下,可以使用一個文件包含兩路流胰柑,一路是伴奏流截亦,另外一路是原唱流。

ffmpeg -i 131.mp3 -i 134.mp3 -map 0:a -c:a:0 libfdk_aac -b:a:0 96k -map 1:a -c:a:1 libfdk_aac -b:a:1 64k -vn -f mp4 output.m4a

三柬讨、FFmpeg API的介紹與使用

在FFmpeg中崩瓤,AVFormatContext就是對容器或者說媒體文件層次的一個抽象,該文件中(或者說在這個容器里面)包含了多路流(音頻流踩官、視頻流却桶、字幕流等),對流的抽象就是AVStream;在每一路流中都會描述這路流的編碼格式,對編解碼格式以及編解碼器的抽象就是AVCodecContext與AVCodec;對編解器或者解碼器的輸入輸出部分颖系,也就是壓縮數(shù)據(jù)以及原始數(shù)據(jù)的抽象就是AVPacket與AVFrame嗅剖。

當(dāng)然除了編解碼之外,對于音視頻的處理肯定是針對于原始數(shù)據(jù)的處理嘁扼,也就是針對于AVFrame的處理信粮,使用的就是AVFilter。

下面來介紹一個解碼的實(shí)例偷拔,該實(shí)例把一個視頻文件解碼成單獨(dú)的音頻PCM文件和視頻YUV文件蒋院。

1.引用頭文件

這里直接將文件(include文件夾與libffmpeg.a靜態(tài)庫文件)拿過來使用亏钩×拢可在工程文件中的配置中修改Header Search Path。

#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libswresample/swresample.h"
#include "libavutil/pixdesc.h"
2.注冊協(xié)議姑丑、格式與解編碼器

使用FFmpeg的API蛤签,首先要調(diào)用FFmpeg的注冊協(xié)議、格式與編碼器的方法栅哀,確保所有的格式與編解碼器都被注冊到了FFmpeg框架中震肮,當(dāng)然如果需要用到網(wǎng)絡(luò)的操作,那么也應(yīng)該將網(wǎng)絡(luò)協(xié)議部分注冊到FFmpeg框架留拾,以便于后續(xù)再去查找對應(yīng)的格式戳晌。代碼如下:

avformat_network_init();
av_register_all();

文檔中還有一個方法是avcodec_register_all(),其用于將所有編解碼器注冊到FFmpeg框架中,但是av_register_all方法內(nèi)部已經(jīng)調(diào)用了avcodec_register_all方法痴柔,所以其實(shí)只需要調(diào)用av_register_all就可以了沦偎。

3.打開媒體文件源,并設(shè)置超時(shí)回調(diào)

注冊了格式以及編碼接之后咳蔚,接下來就應(yīng)該打開對應(yīng)的媒體文件了豪嚎,當(dāng)然該文件既可能是本地磁盤的文件,也可能是網(wǎng)絡(luò)媒體資源的一個鏈接谈火,如果是網(wǎng)絡(luò)鏈接侈询,則會涉及不同的協(xié)議,比如RTMP糯耍、HTTP等協(xié)議的視頻源扔字。打開媒體資源以及設(shè)置超時(shí)回調(diào)的代碼如下:

AVFormatContext *formatCtx = avformat_alloc_context();
AVIOInterruptCB int_cb = {interrupt_callback,(__bridge void *)(self)};
formatCtx->interrupt_callback = int_cb;
avformat_open_input(formatCtx,path,NULL,NULL);
avformat_find_steam_info(formatCtx,NULL);
4.尋找各個流,并且打開對應(yīng)的解碼器

上一步已打開了媒體文件温技,相當(dāng)于打開了一根電線啦租,這根電線里面其實(shí)還有一條紅色的線和藍(lán)色的線,這就和媒體文件中的流非常類似了荒揣,紅色的線代表音頻流篷角,藍(lán)色的線代表視頻流。所以這一步我們就要尋找出各個流系任,然后找到流中對應(yīng)的解碼器恳蹲,并且打開它虐块。
尋找音視頻流:

for (int i = 0;i < formatCtx->nb_streams; i++ ) {
    AVStream *stream = formatCtx->streams[i];
    if(AVMEDIA_TYPE_VIDEO == stream->codec->codec_type) {
        // 視頻流
        videoStreamIndex = i;
    }else{
       // 音頻流
        audioStreamIndex = i;
    }
}

打開音頻流解碼器

AVCodecContext *audioCodecCtx = audioStream->codec;
AVCodec *codec = avcodec_find_decoder(audioCodecCtx->codec_id);
if(!codec){
        // 找不到對應(yīng)的音頻解碼器
}
int openCodecErrCode = 0;
if((openCodecErrCode = avcodec_open2(codecCtx,codec,NULL)) < 0){
      // 打開音頻解碼器失敗
}

打開視頻流解碼器

AVCodecContext *videoCodecCtx = videoStream->codec;
AVCodec *codec = avcodec_find_decoder(videoCodecCtx->codec_id);
if(!codec){
        // 找不到對應(yīng)的視頻解碼器
}
int openCodecErrCode = 0;
if((openCodecErrCode = avcodec_open2(codecCtx,codec,NULL)) < 0){
      // 打開視頻解碼器失敗
}
5.初始化解碼后數(shù)據(jù)的結(jié)構(gòu)體

知道了音視頻解碼器的信息之后,下面需要分配出解碼之后的數(shù)據(jù)所存放的內(nèi)存空間嘉蕾,以及進(jìn)行格式轉(zhuǎn)換需要用到的對象贺奠。
構(gòu)建音頻的格式轉(zhuǎn)換對象以及音頻解碼后數(shù)據(jù)存放的對象:

SwrContext *swrContext = NULL;
if(audioCodecCtx->sample_fmt != AV_SAMPLE_FMT_S16){
    // 如果不是我們需要的數(shù)據(jù)格式
    swrContext = swr_alloc_set_opts(NULL,outputChannel,AV_SAMPLE_FMT_S16,outSampleRate,in_ch_layout,in_sample_fmt,in_sample_rate,0,NULL);
     if(!swrContext || swr_init(swrContext)){
            if(swrContext){
                    swr_free(&swrContext);
            }
      }
      audioFrame = avcodec_alloc_frame();
}

構(gòu)建視頻的格式轉(zhuǎn)換對象以及視頻解碼后數(shù)據(jù)存放的對象:

AVPicture picture;
bool pictureVaild = avpicture_alloc(&picture,PIX_FMT_YUV420P,videoCodecCtx->width,videoCodecCtx->height) == 0;
if(!pictureValid){
      // 分配失敗
      return false;
}

swsContext = sws_getCachedContext(swsContext,videoCodecCtx->width,videoCodecCtx->height,
videoCodecCtx->pix_fmt,
videoCodecCtx->width
videoCodecCtx->height,
PIX_FMT_YUV420P,
SWS_FAST_BILINEAR,
NULL,NULL,NULL);

videoFrame = avcodec_alloc_frame();
6.讀取流內(nèi)容并且解碼

打開了解碼器之后,就可以讀取一部分流中的數(shù)據(jù)(壓縮數(shù)據(jù))错忱,然后將壓縮數(shù)據(jù)作為解碼器的輸入儡率,解碼器將其解碼為原始數(shù)據(jù)(裸數(shù)據(jù) ),之后就可以將原始數(shù)據(jù)寫入文件了:

AVPacket packet;
int gotFrame = 0;
while(true){
    if(av_read_frame(formatContext,&packet)){
          //End of File
          break;
    }
    int packetStreamIndex = packet.stream_index;
    if(packetStreamIndex == videoStreamIndex) {
            int len = avcodec_decode_video2(videoCodecCtx,videoFrame,&gotFrame,&packet);
          if(len<0){
              break;
          }
          if(gotFrame){
              self->handleVideoFrame();
          }
    }else if(packetStreamIndex == audioStreamIndex){
          int len = avcodec_decode_audio4(audioCodecCtx,audioFrame,&gotFrame,&packet);
         if(len < 0){
              break;
          }
         if(gotFrame){
             self->handleVideoFrame(); 
        }
    }
}
7.處理解碼后的裸數(shù)據(jù)

解碼之后會得到裸數(shù)據(jù)以清,音頻就是PCM數(shù)據(jù)儿普,視頻就是YUV數(shù)據(jù)。下面將其處理成我們所需要的格式并且進(jìn)行寫文件掷倔。
音頻裸數(shù)據(jù)的處理:

void * audioData;
int numFrames;
if(swrContext) {
      int bufSize = av_samples_get_buffer_size(NULL,channels,(int)(audioFrame->nb_samples * channels),AV_SAMPLE_FMT_S16,1);
      if(!_swrBuffer || _swrBufferSize <bufSize){
            swrBufferSize = bufSize;
            swrBuffer = realloc(_swrBuffer,_swrBufferSize);
      }
      Byte *outbuf[2] = {_swrBuffer,0};
      nunFrames = swr_convert(_swrContext,outbuf,(int)(audioFrame->nb_samples *channels),(const uint8_t **)_audioFrame->data,audioFrame->nb_samples);
audioData = swrBuffer;
} else {
      audioData = audioFrame->data[0];
      numFrames = audioFrame->nb_samples;
}

接收到音頻裸數(shù)據(jù)之后眉孩,就可以直接寫文件了,比如寫到文件audio.pcm中勒葱。
視頻裸數(shù)據(jù)的處理:

 uint8_t* luma;
uint8_t* chromaB;
uint8_t* chromaR;
if(videoCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P || videoCodecCtx->pix_fmt == AV_PIX_FMT_YUVJ420P){

    luma = copyFrameData(videoFrame->data[0],
                    videoFrame->linesize[0],
                    videoCodecCtx->width,
                    videoCodecCtx->width,
                    videoCodecCtx->height);
    chromaB =copyFrameData(videoFrame->data[1],
                    videoFrame->linesize[1],
                    videoCodecCtx->width,
                    videoCodecCtx->width/2,
                    videoCodecCtx->height/2);
    chromaR =copyFrameData(videoFrame->data[2],
                    videoFrame->linesize[2],
                    videoCodecCtx->width,
                    videoCodecCtx->width/2,
                    videoCodecCtx->height/2);
} else {
      sws_scale(_swsContext,(const uint8_t **)videoFrame->data,videoFrame->linesize,0,videoCodecCtx->height,picture.data,picture,linesize);
      luma = copyFrameData(picture.data[0],
                    picture.linesize[0],
                    videoCodecCtx->width,
                    videoCodecCtx->width,
                    videoCodecCtx->height);
    chromaB =copyFrameData(picture.data[1],
                    picture.linesize[1],
                    videoCodecCtx->width,
                    videoCodecCtx->width/2,
                    videoCodecCtx->height/2);
    chromaR =copyFrameData(picture.data[2],
                    picture.linesize[2],
                    videoCodecCtx->width,
                    videoCodecCtx->width/2,
                    videoCodecCtx->height/2);

}

接收到Y(jié)UV數(shù)據(jù)之后也可以直接寫入文件了浪汪,比如寫到文件video.yuv中

8.關(guān)閉所有資源

解碼完畢之后,或者在解碼過程中不想繼續(xù)解碼了凛虽,可以退出程序死遭,當(dāng)然退出的時(shí)候,要將用到的FFmpeg框架中的資源凯旋,包括FFmpeg框架對外的連接資源等全都釋放掉呀潭。
關(guān)閉音頻資源:

if(swrBuffer){
    free(swrBuffer);
     swrBuffer = NULL;
     swrBufferSize = 0;
}

if(swrContext){
    swr_free(&swrContext);
    swrContext = NULL;
}

if (audioFrame){
    av_free(audioFrame);
    audioFrame = NULL;
}

if (audioCodecCtx){
    avcodec_close(audioCodecCtx);
    audioCodecCtx = NULL;
}

關(guān)閉視頻資源

if(swrContext){
    swr_freeContext(&swrContext);
    swrContext = NULL;
}

if(pictureValid) {
    avpicture_free(&picture);
    pictureValid = false;
}

if(videoFrame){
    av_free(videoFrame);
    videoFrame = NULL;
}

if (videoCodecCtx) {
    avcodec_close(videoCodecCtx);
    videoCodecCtx = NULL;
}

關(guān)閉連接資源

if(formatCtx){
    avformat_close_input(&formatCtx);
    formatCtx = NULL;
}

以上就是利用FFmpeg解碼的全部過程了,其中包括打開文件流瓦阐、解析格式蜗侈、解析流并且打開解碼器、解碼和處理睡蟋,以及最終關(guān)閉所有資源的操作踏幻。

四、FFmpeg源碼結(jié)構(gòu)

1.libavformat與libavcodec介紹

AVFormatContext是API層直接接觸到的結(jié)構(gòu)體戳杀,它會進(jìn)行格式的封裝與解封裝该面,它的數(shù)據(jù)部分由底層提供,底層使用了AVIOContext,這個AVIOContext實(shí)際上就是普通的I/O增加了一層Buffer緩沖區(qū)信卡,再往底層就是URLContext,也就是到達(dá)了協(xié)議層隔缀,協(xié)議層的具體實(shí)現(xiàn)有很多,包括rtmp傍菇、http猾瘸、hls、file等。
AVCodecContext是包含在一個AVStream里面的牵触,即描述了這路流的編碼格式是什么淮悼,其中存放了具體的編碼格式信息,根據(jù)Codec的信息可以打開編碼器或者解碼器揽思,然后利用該編碼器或者解碼器進(jìn)行AVPacket與AVFrame之間的轉(zhuǎn)換(實(shí)際上就是解碼或者編碼的過程)

2.FFmpeg通用API分析

(1)av_register_all分析

編譯FFmpeg的時(shí)候袜腥,做了configure的配置,其中開啟(enable)或者關(guān)閉(disable)了很多選項(xiàng)钉汗,configure的配置會生成兩個文件:config.mk與config.h羹令。config.mk實(shí)際上就是makefile文件需要包含進(jìn)去的子模塊,會作用在編譯階段损痰,幫助開發(fā)者編譯出正確的庫福侈;而config.h是作用在運(yùn)行階段,這一階段將確定需要注冊哪些容器以及編解碼格式到FFmpeg框架中徐钠。所以該函數(shù)的內(nèi)部實(shí)現(xiàn)會先調(diào)用avcodec_register_all來注冊所有config.h里面開放的編解碼器癌刽,然后會注冊所有的Muxer和Demuxer(也就是封裝格式)役首,最后注冊所有的Protocol(即協(xié)議層的東西)尝丐。這樣一來,在configure過程中開啟(enable)或者關(guān)閉(disable)的選項(xiàng)就作用到了運(yùn)行時(shí)衡奥,該函數(shù)的源碼分析涉及的源碼文件包括:url.c爹袁、allformats.c、mux.c矮固、format.c等文件失息。

(2)av_find_codec分析

這里面其實(shí)包含了兩部分的內(nèi)容:一部分是尋找解碼器,一部分是尋找編碼器档址。其實(shí)在第一步的av_register_all函數(shù)里面已經(jīng)把編碼器和解碼器都存放到一個鏈表中了盹兢,在這里尋找編碼器或者解碼器都是從第一步構(gòu)造的鏈表中進(jìn)行遍歷,通過Codec的ID或者name進(jìn)行條件匹配守伸,最終返回對應(yīng)的Codec绎秒。

(3)avcodec_open2分析

該函數(shù)是打開編解碼器(Codec)的函數(shù),無論是編碼過程還是解碼過程尼摹,都會用到該函數(shù)见芹,該函數(shù)的輸入?yún)?shù)有三個:第一個是AVCodecContext,解碼過程由FFmpeg引擎填充,編碼過程由開發(fā)者自己構(gòu)造蠢涝,如果想要傳入私有參數(shù)玄呛,則為它的priv_data設(shè)置參數(shù),比如在libx264編碼器中設(shè)置prest和二、tune徘铝、profile等;第二個參數(shù)是上一步通過av_find_codec尋找出來的編解碼器(Codec);第三個參數(shù)一般會傳遞NULL。具體到改函數(shù)的實(shí)現(xiàn)時(shí)惕它,就會找到對應(yīng)的實(shí)現(xiàn)文件场晶,那么其實(shí)如何找到實(shí)現(xiàn)文件的呢?這就需要回到第一步中來看看是如何注冊的怠缸,比如libx264的編碼器诗轻,查看其注冊會發(fā)現(xiàn)ff_libx264_encoder結(jié)構(gòu)體的定義存在于libx264.c中,所以該Codec的生命周期方法就會委托給該結(jié)構(gòu)體對應(yīng)的函數(shù)指針?biāo)赶虻暮瘮?shù)揭北,open對應(yīng)的就是init函數(shù)指針?biāo)赶虻暮瘮?shù)扳炬,該函數(shù)里面就會調(diào)用具體的編碼庫的API,而LAME這個Codec會調(diào)用LAME的編碼庫的API搔体,并且會以對應(yīng)的AVCodecContext中的priv_data來填充對應(yīng)第三方庫所需要的私有參數(shù)恨樟,如果開發(fā)者沒有對屬性priv_data填充值,那么就會使用默認(rèn)值疚俱。

(4)avcodec_close分析

如果理解了avcodec_open,那么對應(yīng)的close就是一個逆過程劝术,找到對應(yīng)實(shí)現(xiàn)文件中的close函數(shù)指針?biāo)赶虻暮瘮?shù),然后該函數(shù)會調(diào)用對應(yīng)第三方庫的API來關(guān)閉掉對應(yīng)的編碼庫呆奕。其實(shí)FFmpeg所做的事情就是透明化所有的編解碼庫养晋,用自己的封裝來為開發(fā)者提供統(tǒng)一的接口。開發(fā)者使用不同的編碼庫時(shí)梁钾,只需要指明要使用哪一個即可绳泉,這也充分體現(xiàn)了面向?qū)ο缶幊讨械姆庋b特性。

3.調(diào)用FFmpeg解碼時(shí)用到的函數(shù)分析

(1)avformat_open_input分析

函數(shù)avformat_open_input會根據(jù)所提供的文件路徑判斷文件的格式姆泻,其實(shí)就是通過這一步來決定到底使用的是哪一個Demuxer.舉例來說零酪,如果是flv,那么Demuxer就會使用對應(yīng)的ff_flv_demuxer,所以對應(yīng)的關(guān)鍵生命周期的方法read_header拇勃、read_packet四苇、read_seek、read_close都會使用該flv的Demuxer中函數(shù)指針指定的函數(shù)方咆。read_header函數(shù)會將AVStream結(jié)構(gòu)體構(gòu)造好月腋,以便后續(xù)的步驟繼續(xù)使用AVStream作為輸入?yún)?shù)。

(2)avformat_find_stream_info分析

該方法的作用是把所有Stream的MetaData信息填充好峻呛。方法內(nèi)部會先查找對應(yīng)的解碼器罗售,然后打開對應(yīng)的解碼器,緊接著會利用Demuxer中的read_packet函數(shù)讀取一段數(shù)據(jù)進(jìn)行解碼钩述,當(dāng)然解碼的數(shù)據(jù)越多寨躁,分析出的流信息就會越準(zhǔn)確,如果是本地資源牙勘,那么很快就可以得到分成準(zhǔn)確的信息了职恳,但是對于網(wǎng)絡(luò)資源來說所禀,則會比較慢,因此該函數(shù)有幾個參數(shù)可以控制讀取數(shù)據(jù)的長度放钦,一個是probe size,一個是max_analyze_duration,還有一個是fps_probe_size,這三個參數(shù)共同控制解碼數(shù)據(jù)的長度色徘,當(dāng)然,如果配置這幾個參數(shù)的值越小操禀,那么這個函數(shù)執(zhí)行的時(shí)間就會越快褂策,但是會導(dǎo)致AVStream結(jié)構(gòu)體里面一些信息(視頻的寬、高颓屑、fps斤寂、編碼類型等)不準(zhǔn)確。
(3)av_read_frame分析
使用該方法讀取出來的數(shù)據(jù)是AVPacket,該函數(shù)的實(shí)現(xiàn)首先會委托到Demuxer的read_packet方法中去揪惦,當(dāng)然read_packet通過解復(fù)用層和協(xié)議層的處理之后遍搞,會將數(shù)據(jù)返回到這里,在該函數(shù)中進(jìn)行數(shù)據(jù)緩沖處理器腋。對于音頻流溪猿,一個AVPacket可能包含多個AVFrame,但是對于視頻流,一個AVPacket只包含一個AVFrame,該函數(shù)最終只會返回一個AVPacket結(jié)構(gòu)體纫塌。
(4)avcodec_decode分析
該方法包含了兩部分內(nèi)容诊县,一部分是解碼視頻护戳,一部分是解碼音頻翎冲,在上面的函數(shù)分析中媳荒,我們知道钳枕,解碼時(shí)會委托給對應(yīng)的解碼器來實(shí)施的,在打開解碼器的時(shí)候就會找到對應(yīng)解碼器的實(shí)現(xiàn),比如對于解碼H264來講自晰,會找到ff_h264_decoder,其中會有對應(yīng)的生命周期函數(shù)的實(shí)現(xiàn),最重要的是init稍坯、decode酬荞、close這三個方法搓劫,分別對應(yīng)于打開解碼器、解碼以及關(guān)閉解碼器的操作混巧,而解碼過程就是調(diào)用decode方法枪向。
(5)avformat_close_input分析
該函數(shù)負(fù)責(zé)釋放對應(yīng)的資源,首先會調(diào)用對應(yīng)的Demuxer中的生命周期read_close方法咧党,然后釋放掉AVFormatContext,最后關(guān)閉文件或者遠(yuǎn)程網(wǎng)絡(luò)連接秘蛔。

4.調(diào)用FFmpeg編碼時(shí)用到的函數(shù)分析

avformat_alloc_output_context2分析

該函數(shù)內(nèi)部需要調(diào)用方法avformat_alloc_context來分配一個AVFormatContext結(jié)構(gòu)體,當(dāng)然最關(guān)鍵的還是根據(jù)上一步注冊的Muxer和Demuxer部分(也就是封裝格式部分)去找到對應(yīng)的格式傍衡,如果找不到對應(yīng)的格式缠犀,那么這里會返回找不到對應(yīng)各式的錯誤提示。
源碼解析的更多內(nèi)容聪舒,請看雷宵華的博客辨液。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市箱残,隨后出現(xiàn)的幾起案子滔迈,更是在濱河造成了極大的恐慌,老刑警劉巖被辑,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件燎悍,死亡現(xiàn)場離奇詭異,居然都是意外死亡盼理,警方通過查閱死者的電腦和手機(jī)谈山,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宏怔,“玉大人奏路,你說我怎么就攤上這事‰铮” “怎么了鸽粉?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長抓艳。 經(jīng)常有香客問我触机,道長,這世上最難降的妖魔是什么玷或? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任儡首,我火速辦了婚禮,結(jié)果婚禮上偏友,老公的妹妹穿的比我還像新娘蔬胯。我一直安慰自己,他們只是感情好约谈,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布笔宿。 她就那樣靜靜地躺著犁钟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泼橘。 梳的紋絲不亂的頭發(fā)上涝动,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音炬灭,去河邊找鬼醋粟。 笑死,一個胖子當(dāng)著我的面吹牛重归,可吹牛的內(nèi)容都是我干的米愿。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼鼻吮,長吁一口氣:“原來是場噩夢啊……” “哼育苟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起椎木,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤违柏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后香椎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漱竖,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年畜伐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了馍惹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡玛界,死狀恐怖万矾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脚仔,我是刑警寧澤勤众,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站鲤脏,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏吕朵。R本人自食惡果不足惜猎醇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望努溃。 院中可真熱鬧硫嘶,春花似錦、人聲如沸梧税。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哮塞,卻和暖如春刨秆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背忆畅。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工衡未, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人家凯。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓缓醋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親绊诲。 傳聞我的和親對象是個殘疾皇子送粱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

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