通過攝像頭直播推流的場景中,需要先從攝像頭獲取去視頻元數據,然後交給x264編碼器(加入用的視頻編碼器是x264)編碼,最後經RTMP封包後發送給服務器.
我們使用CameraX來獲取攝像頭數據,對於CameraX的使用,參考官方文檔:
https://developer.android.google.cn/training/camerax
CameraX 是一個 Jetpack 支持庫,旨在幫助您簡化相機應用的開發工作。它提供一致且易於使用的 API 界面,適用於大多數 Android 設備,並可向後兼容至 Android 5.0(API 級別 21).
CameraX 引入了多個用例,
預覽:在顯示屏上顯示圖片
圖片分析:無縫訪問緩衝區以便在算法中使用,
圖片拍攝:保存優質圖片
一,
在直播這個應用中,主要關注的圖片分析.簡單列出CameraX使用的代碼:
<uses-permission android:name="android.permission.CAMERA"/>
佈局文件中使用TextureView顯示預覽效果:
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
在MainActivity中使用
CameraX.bindToLifecycle((LifecycleOwner) this, getPreview(), getAnalysis());
//Analysis和Priview中設置的分辨率可以不同,這說明分析圖片和預覽圖片是可以分開應用的。
private ImageAnalysis getAnalysis() {
ImageAnalysisConfig analysisConfig = new ImageAnalysisConfig.Builder()
.setCallbackHandler(new Handler(handlerThread.getLooper()))
.setLensFacing(CameraX.LensFacing.BACK)
.setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
.setTargetResolution(new Size(480, 640))
.build();
ImageAnalysis imageAnalysis = new ImageAnalysis(analysisConfig);
imageAnalysis.setAnalyzer(this);
return imageAnalysis;
}
private Preview getPreview() {
//這裏給出的分辨率,並不是最終的值,CameraX會根據設備的支持情況,設置一個最接近你給定參數的值。
PreviewConfig previewConfig= new PreviewConfig.Builder().
setTargetResolution(new Size(480, 640)).
setLensFacing(CameraX.LensFacing.BACK).build();
Preview preview = new Preview(previewConfig);
preview.setOnPreviewOutputUpdateListener(this);
return preview;
}
二,粗略看下預覽的實現,
這個紋理,就是攝像頭採集到的一張圖片,把這個紋理設置到TextureView,就可以預覽出圖像 ,SurfaceTexture對數據流的處理,並不是直接顯示,而是轉爲GL的外部紋理, 因此可用於圖像數據的二次處理(濾鏡,美顏), Camera的預覽數據,一種情況,可以變成紋理後交給GLSurfaceView直接顯示,還有一種情況, 預覽數據通過SurfaceTexture交給TextureView作爲View Heirachy的一個硬件加速層來顯示, CamerX的預覽就是後一種情況.
TextureView可以把內容流直接投影到View中。 SurfaceTexture從圖像流(Camera預覽,視頻解碼,GL繪製)中獲得幀數據,當調用UpdateTexImage時, 根據內容流最近的圖像更新SurfaceTexture對應的紋理對象,然後就可以向操作普通紋理一樣去操作它了。 TextureView不會在WMS中創建單獨的窗口,而且必須在硬件加速窗口中,在它的draw方法中, 把SurfaceTexture中收到的圖像數據作爲紋理更新到對應的Hardwarelayer中。
SurfaceTexture,TextureView的詳細介紹:https://www.cnblogs.com/wytiger/p/5693569.html
@Override
public void onUpdated(Preview.PreviewOutput output) {
SurfaceTexture surfaceTexture = output.getSurfaceTexture();
if (mTextureView.getSurfaceTexture() != surfaceTexture) {
if (mTextureView.isAvailable()) {
//這裏的處理,避免切換攝像頭時出錯。
ViewGroup parent = (ViewGroup) mTextureView.getParent();
parent.removeView(mTextureView);
parent.addView(mTextureView, 0);
parent.requestLayout();
}
mTextureView.setSurfaceTexture(surfaceTexture);
}
}
三,圖像分析,
x264編碼器需要的數據格式是I420, 通過Camera進行直播的過程,首先,從Camera獲取數據,這個數據就是一個byte數組,然後把byte數據送去編碼, 編碼之後,按照rtmp的格式進行封包,把封包後的數據通過socket發送出去。 這裏藉助CameraX來獲取圖像數據,通過CameraX的圖像分析接口得到的數據是ImageProxy, 通過ImageProxy可以得到我們想要的圖像的byte數組。因爲CameraX生成是YUV420-888的格式的圖片,所以我們得 到的圖像數據格式就是YUV格式的。我們要從YUV_420_888數據中提取出byte數組,交給x264去編碼。
//這裏可以得到圖片的數據,寬高,格式,時間戳等,及旋轉角度。
@Override
public void analyze(ImageProxy imageProxy, int rotationDegrees) {
Image image = imageProxy.getImage();
//這個數組的第0個元素保存的是Y數據,data[0].getBuffer();
//這個數組的第1個元素保存的是U數據,
//這個數組的第2個元素保存的是V數據,
//Image.Plane[] data = image.getPlanes();
byte[] dataI420 = ImageUtils.getBytes(imageProxy, rotationDegrees, mWidth, mHeight);
}
從YUV420-888中提取byte[]數組.
YUV420這類格式的圖像,4個Y分量共用一組UV分量,根據顏色數據的存儲順序不同,又分了幾種不同格式,這些格式實際存儲的信息是一樣的,如:4*4的圖片,在YUV420下,任何格式都是16個Y值,4個U值,4個V值,區別只是Y,U,V的排列順序不同.YUV420是一類顏色格式的集合,並不能完全確定顏色數據的存儲順序.
CameraX的應用中,YUV三個分量的數據分別保存在imageProxy.getPlanes()對應的數組中.
YUV420中,Y數據長度爲: width*height , 而U、V都爲:width / 2 * height / 2。
planes[0]一定是Y,planes[1]一定是U,planes[2]一定是V。且對於plane [0],Y分量數據一定是連續存儲的,中間不會有U或V數據穿插,也就是說一定能夠一次性得到所有Y分量的值.
但是對於UV數據,可能存在以下兩種情況:
1. planes[1] = {UUUU...},planes[2] = {VVVV...};
2. planes[1] = {UVUV...},planes[2] = {VUVU...}。
具體UV的排列順序是那種情況,需要根據int pixelStride = plane.getPixelStride();來判斷:
pixelStride 爲1:表示無間隔取值,即爲上面的第一種情況
pixelStride 爲 2: 表示需要間隔一個數據取值,即爲上面的第二種情況
但是,考慮到Camera有不同的分辨率,所以在YUV數據存儲時有字節對齊的想象,就要考慮佔位符的問題. 因爲涉及補位的問題,要去考慮行步長planes[0].getRowStride()。 當Camera的分辨率不同時,補位數據的長度就會不一樣,planes[0].getRowStride()行步長也就會不一樣。
首先分析Y數據, RowStride表示行步長,Y數據對應的行步長可能爲:
1. 等於 imageProxy.getWidth() ;
2. 大於imageProxy.getWidth() ;
以4x4的I420爲例,其數據可以看爲:
rowStride等於width的情況,直接通過 planes[0].getBuffer() 獲得Y數據沒有問題
rowStride大於width的情況,此時讀取數據時就要跳過尾部的佔位符,不然可能會報指針越界異常.
然後看UV數據,對於U與V數據,對應的行步長可能爲:
1. 等於Width;
2. 大於Width;
3. 等於Width/2;
4. 大於Width/2
在RowStride等於width時,並且pixelStride==2,表示UV交叉存放,如圖planes[1],此時在獲取U數據時,只獲取偶數位置的數據,奇數位置的數據丟棄.只從planes[1]中取U數據,
同理,我們只從planes[2]中取V數據,獲取偶數位置的數據,奇數位置的數據丟棄.
當RowStride大於width時,planes[1],同樣要跳過尾部的佔位符,注意最後一行沒有佔位符,不需要跳過.
當RowStride大於width時,planes[2],同樣要跳過尾部的佔位符,注意最後一行沒有佔位符,不需要跳過.
當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,如:planes[1]
當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,如planes[2]
當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,但是有佔位符,此時,需要跳過佔位符,如planes[1]
當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,但是有佔位符,此時,需要跳過佔位符,,如planes[2]
有了上面的分析,再看代碼實現,相信就不難理解了:
下面的函數是從ImageProxy中提取I420字節數組:
public static byte[] getBytes(ImageProxy imageProxy, int rotationDegrees, int mWidth, int mHeight) {
//獲取圖像格式
int format = imageProxy.getFormat();
//根據CameraX的官方文檔,CameraX返回的數據格式:YUV_420_888
if (format != ImageFormat.YUV_420_888) {
//異常處理,如果有廠商修改了CameraX返回的數據格式。
}
//I420的數據格式,4個Y共用一組UV,其中Y數據的大小是width * height,
// U V數據的大小都是 (width/2) * (height/2)
//這個數組的第0個元素保存的是Y數據,data[0].getBuffer(); Y分量數據時連續存儲的,
//這個數組的第1個元素保存的是U數據,U V分量可能出現交叉存儲,
//這個數組的第2個元素保存的是V數據,
//如果按照上面的方式,簡單的獲取到Y U V分量的字節數組,然後拼接到一起,在多數情況下可能是正常的,
// 但是不能兼容多種camera分辨率,因爲涉及補位的問題,要去考慮行步長planes[0].getRowStride()。
// 當Camera的分辨率不同時,補位數據的長度就會不一樣,planes[0].getRowStride()行步長也就會不一樣。
ImageProxy.PlaneProxy[] planes = imageProxy.getPlanes();
//一個YUV420數據,需要的字節大小,
int size = imageProxy.getWidth() * imageProxy.getHeight() * 3/ 2;
if (null == yuvI420 || yuvI420.capacity() < size) {
yuvI420 = ByteBuffer.allocate(size);
}
yuvI420.position(0);
//獲取Y數據,getPixelStride 在Y分量下,總是1,表示Y數據時連續存放的。
int pixelStride = planes[0].getPixelStride();
ByteBuffer yBuffer = planes[0].getBuffer();
//行步長,表示一行的最大寬度,可能等於imageProxy.getWidth(),
// 也可能大於imageProxy.getWidth(),比如有補位的情況。
int rowStride = planes[0].getRowStride();
//這個字節數組表示,在讀取Y分量數據時,跳過補位的部分,因爲這些補位的部分沒有實際數據,只是爲了字節對齊,
// 如果沒有補位 rowStride 等於 imageProxy.getWidth(),這就是一個空數組,否則,數組的長度就剛好是要跳過的長度。
byte[] skipRow = new byte[rowStride - imageProxy.getWidth()];
//這個數組,表示了這一行真實有效的數據。
byte[] row = new byte[imageProxy.getWidth()];
//循環讀取每一行
for (int i = 0; i < imageProxy.getHeight(); i++) {
yBuffer.get(row);
//每一行有效的數據拼接起來就是Y數據。
yuvI420.put(row);
//因爲最後一行,沒有無效的補位數據,不需要跳過,不是最後一行,才需要跳過無效的佔位數據。
if (i < imageProxy.getHeight() - 1) {
yBuffer.get(skipRow);
}
}
// 獲取U V數據
// Y分量數據時連續存儲的,U V分量可能出現交叉存儲,
pixelStride = planes[1].getPixelStride();
// 如果pixelStride值爲1,表示UV是分別存儲的,planes[1] ={UUUU},planes[2]={VVVV},
// 這個情況還是比較容易獲取的,
// 如果pixelStride 爲2,表示UV是交叉存儲的,planes[1] ={UVUV},planes[2]={VUVU},
// 這個情況,要獲取UV,就要拿一個,丟一個,交替取出,同時,也需要考慮跳過無效的補位數據。
for (int i =0; i< 3; i++) {
ImageProxy.PlaneProxy planeProxy = planes[i];
int uvPixelStride = planeProxy.getPixelStride();
//如果U V是交錯存放,行步長就等於imageProxy.getWidth(),同時要考慮有佔位數據,會大於imageProxy.getWidth()
// 如果U V是分離存放,行步長就等於imageProxy.getWidth() /2,同時要考慮有佔位數據,會大於imageProxy.getWidth()/2
int uvRowStride = planeProxy.getRowStride();
ByteBuffer uvBuffer = planeProxy.getBuffer();
//一行一行的處理,uvWidth表示了有效數據的長度。
int uvWidth = imageProxy.getWidth() / 2;
int uvHeight = imageProxy.getHeight() / 2;
for (int j = 0; j< uvHeight; j++) {
//每次處理一行中的一個字節。
for (int k =0; k < uvRowStride; k++) {
//跳過最後一行沒有佔位的數據,
if (j == uvHeight -1) {
//UV沒有混合在一起,
if (uvPixelStride == 1) {
//大於有效數據後,跳出內層循環(k < uvRowStride),不用關心最後的佔位數據了。
// 因爲最後一行沒有佔位數據。
if (k >= uvWidth) {
break;
}
} else if (uvPixelStride == 2) {
//UV沒有混合在一起,大於有效數據後,跳出內層循環(k < uvRowStride),
// 不用關心最後的佔位數據了。
// 因爲最後一行沒有佔位數據。注意這裏的有效數據的寬度是imageProxy.getWidth()。
//這裏爲什麼要減1呢?因爲在UV混合模式下,常規情況是UVUV,但是可能存在UVU的情況,
// 就是最後的V是沒有的,如果不在這裏 減1,在接下來get時,會報越界異常。
if (k >= imageProxy.getWidth() -1) {
break;
}
}
}
//對每一個字節,分別取出U數據,V數據。
byte bt =uvBuffer.get();
//uvPixelStride == 1表示U V沒有混合在一起
if (uvPixelStride == 1) {
//k < uvWidth表示是在有效範圍內的字節。
if (k < uvWidth) {
yuvI420.put(bt);
}
} else if (uvPixelStride == 2) {
//uvPixelStride == 2 表示U V混合在一起。只取偶數位小標的數據,纔是U / V數據,
// 奇數位,佔位符數據都丟棄,同時這裏的有效數據長度是imageProxy.getWidth(),
// 而不是imageProxy.getWidth() /2,
if (k < imageProxy.getWidth() && (k % 2 == 0)){
yuvI420.put(bt);
}
}
}
}
}
//全部讀取到YUV數據,以I420格式存儲的字節數組。
byte[] result = yuvI420.array();
//Camera 角度的旋轉處理。分別以順時針旋轉Y, U ,V。
if (rotationDegrees == 90 || rotationDegrees == 270) {
result = rotation(result, imageProxy.getWidth(), imageProxy.getHeight(), rotationDegrees);
}
return result;
}
在獲取到攝像頭數據的byte數組後,還要考慮角度旋轉,藉助libyuv實現角度的順時針旋轉
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_test_cameraxlive_ImageUtils_rotation(JNIEnv *env, jclass clazz, jbyteArray data_,
jint width, jint height, jint degree) {
jbyte *data = env->GetByteArrayElements(data_, 0);
uint8_t *src = reinterpret_cast<uint8_t *>(data);
int ySize = width * height;
int uSize = (width >>1) * (height >>1);
//Y U V總的數據大小
int size = (ySize * 3) >> 1;
uint8_t dst[size];
//原始數據的Y數據,U數據,V數據
uint8_t *src_y = src;
uint8_t *src_u = src + ySize;
uint8_t *src_v = src + ySize + uSize;
//旋轉後的Y數據,U數據,V數據。
uint8_t *dst_y = dst;
uint8_t *dst_u = dst + ySize;
uint8_t *dst_v = dst +ySize + uSize;
libyuv::I420Rotate(src_y, width,
src_u, width>>1,
src_v, width>>1,
dst_y, height,
dst_u, height>>1,
dst_v, height>>1,
width, height, static_cast<libyuv::RotationMode>(degree));
jbyteArray result = env->NewByteArray(size);
env->SetByteArrayRegion(result, 0, size, reinterpret_cast<const jbyte *>(dst));
env->ReleaseByteArrayElements(data_, data, 0);
return result;
}
到這裏,就完成了攝像頭數據的byte[]數組的提取.
接下來通過x264庫完成編碼:
H264的基礎知識,參考:https://blog.csdn.net/qq_29350001/article/details/78226286
使用X264編碼的流程:
1,設置 x264_param_t編碼器參數,通過 x264_encoder_open 創建一個 x264_t *codec編碼器
2,通過 x264_picture_alloc爲編碼器的輸入數據 x264_picture_t pic_in 申請內存,
3,通過 x264_encoder_encode完成編碼,
創建編碼器的代碼:
void VideoChannel::openCodec(int width, int height, int fps, int bitrate) {
//編碼器的參數,以延遲最低爲目標配置。
x264_param_t param;
//第二,三個參數來自這兩個數組:x264_preset_names[],x264_tune_names[] ,表示編碼速度,和質量控制,
// zerolatency,無延遲編碼,主要用於實時通訊
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
//指定編碼規格,base_line 3.2 ,無B幀(雙向參考幀),數據量小,但是解碼速度慢。
param.i_level_idc = 32;
//輸入數據格式
param.i_csp = X264_CSP_I420;
//寬高,
param.i_width = width;
param.i_height = height;
//指定無B幀
param.i_bframe = 0;
//表示碼率控制,CQP(恆定質量),CRF(恆定碼率),ABR(平均碼率)
param.rc.i_rc_method = X264_RC_ABR;
//碼率,單位kbps,
param.rc.i_bitrate = bitrate / 1000;
//最大碼率,
param.rc.i_vbv_max_bitrate = (bitrate / 1000) * 1.2;
//幀率
param.i_fps_num = fps;
param.i_fps_den = 1;
//打開log輸出,查看編碼過程的日誌,這裏是指定日誌的回調。
//param.pf_log = x264_log_default2;
//關鍵幀間隔,
param.i_keyint_max = fps * 2;
//是否複製sps和pps放在每個關鍵幀的前面 該參數設置是讓每個關鍵幀(I幀)都附帶sps/pps。
param.b_repeat_headers = 1;
//不使用並行編碼。zerolatency場景下設置param.rc.i_lookahead=0;
// 那麼編碼器來一幀編碼一幀,無並行、無延時
param.i_threads = 1;
param.rc.i_lookahead = 0;
x264_param_apply_profile(¶m, "baseline");
codec = x264_encoder_open(¶m);
ySize = width * height;
uSize = (width >> 1) * (height >> 1);
this->width = width;
this->height = height;
}
編碼數據的實現:
void VideoChannel::encode(uint8_t *data) {
//要編碼的輸入數據
x264_picture_t pic_in;
//爲輸入數據,申請內存,指定格式,寬高,
x264_picture_alloc(&pic_in, X264_CSP_I420, width, height);
//把I420數據中YUV塞到pic_in結構體中
pic_in.img.plane[0] = data;
pic_in.img.plane[1] = data + ySize;
pic_in.img.plane[2] = data + ySize + uSize;
//這個pts每次編碼時需要增加,編碼器把它當做圖像的序號。
pic_in.i_pts = i_pts++;
//也可以從pic_out中拿到編碼後的數據,我們這裏是從pp_nal中獲取。
x264_picture_t pic_out;
//二級指針,保存編碼後的數據
x264_nal_t *pp_nal;
//編碼後的數組有幾個元素
int pi_nal;
int error = x264_encoder_encode(codec, &pp_nal, &pi_nal, &pic_in, &pic_out);
if (error <=0) {
return;
}
int spslen;
int ppslen;
uint8_t *sps;
uint8_t *pps;
//拿到編碼後的數據
for (int i = 0; i < pi_nal; ++i) {
//開始碼之後的第一個字節的低5位,表示了NAL的類型,7(sps)或者 8(pps),
int type = pp_nal[i].i_type;
//對應幀的數據,
uint8_t *p_payload = pp_nal[i].p_payload;
//對應幀數據的長度,其中SPS,PPS不屬於幀的範疇。
int i_payload = pp_nal[i].i_payload;
if (type == NAL_SPS) {
//得到SPS,不能直接發送出去,而是要等到跟PPS,組成一個RTMP_packet一起發送給服務器,
// 所以這裏先把sps保存下來
//H264的數據中,每個NAL之間是由00 00 00 01或者 00 00 01來分割,在00 00 00 01後面跟着就是
// 這一幀的類型,
spslen = i_payload - 4; //去掉間隔 00 00 00 01
sps = (uint8_t *)alloca(spslen);//在棧上申請內存,不用手動釋放。
memcpy(sps, p_payload +4, spslen);
} else if (type == NAL_PPS) {
ppslen = i_payload -4;//去掉間隔 00 00 00 01
pps = (uint8_t *) alloca(ppslen);//在棧上申請內存,不用手動釋放。
memcpy(pps, p_payload +4, ppslen);
//sps,pps後面接着肯定是I幀,所以在發送I幀之前,先把sps,pps發送出去。
sendVideoConfig(sps, pps, spslen, ppslen);
} else {
//發送正常的數據幀,包括關鍵幀,普通幀。
sendFrame(type, p_payload, i_payload);
}
}
}
編碼完成後,通過RTMP發送到服務器,除了正常的數據幀,還有AVC序列頭信息。
對RTMP視頻的封包,參考flv的格式文檔:
對視頻數據封包,只需關注數據區部分,前面的11個字節,不用考慮。
視頻數據部分,分關鍵幀,非關鍵幀,
視頻數據中0x17:其中 1表示關鍵幀,7 表示 高級視頻編碼AVC,對於普通幀則是0x27。
AVCVIDEOPACKET的格式定義:
如果類型是0,表示接下來這一段數據時AVC序列頭,如果是0,表示接下來一段數據是視頻幀(關鍵幀,非關鍵幀都是0)。
AVC序列頭格式定義:
SPS,PPS是在編碼H264視頻數據時,放在關鍵幀前面的信息,指導解碼器如何參考這個關鍵幀解碼出B幀,P幀的內容。
通常情況,RTMP封包的視頻結構體:
所以,對視頻幀(關鍵幀,非關鍵幀)的RTMPPacket的字節大小是 5 + 4 + 裸數據。
對於SPS,PPS的RTMPPacket的字節大小是 5+ 8 +3+ spslen + ppslen,其中 8 +3 是AVC序列頭定義的長度。
AVC序列頭定義的 8 + 3 個字節具體定義:
依據上面的分析,再來看下面的封包代碼,應該很容易理解了:
視頻配置信息的封包發送,在發送I幀之前,要先發送SPS ,PPS,
void VideoChannel::sendVideoConfig(uint8_t *sps, uint8_t *pps, int spslen, int ppslen) {
//把SPS,PPS封裝成一個RTMPPacket發送出去,要發送的這個數據的總大小,除了spslen,ppslen,還有AVC序列頭的長度,
//AVC序列頭的長度,根據結構體定位是5+ 8 +3 ,所以總的數據包大小是5+ 8 +3+ spslen + ppslen
int bodySize = 13 + spslen + 3 + ppslen;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
//往rtmppacket中裝入數據
int index = 0;
packet->m_body[index++] = 0x17; //固定頭
packet->m_body[index++] = 0x00;//類型,
//composition time 0x000000
packet->m_body[index++] = 0x00;
packet->m_body[index++] = 0x00;
packet->m_body[index++] = 0x00;
//版本
packet->m_body[index++] = 0x01;
//編碼規格
packet->m_body[index++] = sps[1];
packet->m_body[index++] = sps[2];
packet->m_body[index++] = sps[3];
packet->m_body[index++] = 0xFF;
//整個sps
packet->m_body[index++] = 0xE1;
//sps長度,2個字節
packet->m_body[index++] = (spslen >> 8) & 0xFF;
packet->m_body[index++] = spslen & 0xFF;
memcpy(&packet->m_body[index], sps, spslen);
index += spslen;
//裝入pps
packet->m_body[index++] = 0x01;
//長度,同樣佔兩個字節。
packet->m_body[index++] = (ppslen >> 8) & 0xFF;
packet->m_body[index++] = ppslen & 0xFF;
memcpy(&packet->m_body[index], pps, ppslen);
//設置RTMPPacket的參數
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = bodySize;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
//時間戳,解碼器端將根據這個時間戳來播放視頻,這裏sps,pps不是圖像幀,所以不需要時間戳。
packet->m_nTimeStamp = 0;
//使用相對時間
packet->m_hasAbsTimestamp = 0;
//這個通道的值沒有特別要求,但是不能跟rtmp.c中使用的相同。
packet->m_nChannel = 0x10;
//在使用完packet,要釋放。
callback(packet);
}
視頻 關鍵幀,普通幀的封包發送:
void VideoChannel::sendFrame(int type, uint8_t *p_payload, int i_payload) {
//關鍵幀,普通幀的發送
//去掉間隔符號,當間隔符是0x00 0x00 0x00 0x01,有4個字節,
if (p_payload[2] == 0x00) {
i_payload -= 4;
p_payload += 4;
} else if (p_payload[2] == 0x01) {
//當間隔符是0x00 0x00 0x01,有3個字節,
i_payload -= 3;
p_payload += 3;
}
//往RTMPPacket中裝入數據
RTMPPacket *packet = new RTMPPacket;
//對於關鍵幀,非關鍵幀,根據RTMPPacket的結構定義,僅有第一個字節0x17(關鍵幀), 0x27的區別,
// 總數據的大小是5 + 4(數據長度)+裸數據
int bodySize = 9 + i_payload;
RTMPPacket_Alloc(packet, bodySize);
RTMPPacket_Reset(packet);
//非關鍵幀,0x27
packet->m_body[0] = 0x27;
//如果是關鍵幀,0x17
if (type == NAL_SLICE_IDR) {
packet->m_body[0] = 0x17;
}
//關鍵幀,非關鍵幀的類型都是0x01, sps,pps的類型是0x00
packet->m_body[1] = 0x01;
//時間戳,
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//數據長度,佔4個字節,相當於把int轉成4個字節的byte數組
packet->m_body[5] = (i_payload >> 24) & 0xFF;//先取高位1個字節,
packet->m_body[6] = (i_payload >> 16) & 0xFF;
packet->m_body[7] = (i_payload >> 8) & 0xFF;
packet->m_body[8] = i_payload & 0xFF; //最後去低8位。
//填入裸數據
memcpy(&packet->m_body[9], p_payload, i_payload);
//設置RTMPPacket的參數
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodySize;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nChannel = 0x10;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
//在使用完packet,要釋放。
callback(packet);
}
如果在X264編碼過程中需要調試,可以通過指定日誌回調,來查看編碼過程:
//打開log輸出,查看編碼過程的日誌,這裏是指定日誌的回調。
//param.pf_log = x264_log_default2;
//打印x264編碼的異常輸出。得到整個編碼的過程日誌。
void x264_log_default2(void *, int i_level, const char *psz, va_list list) {
__android_log_vprint(ANDROID_LOG_ERROR, "X264", psz, list);
}