作者:位元組流動
來源:
https://blog.csdn.net/Kennethdroid/article/details/86418725流媒體伺服器測試
首先利用
快直播 app(其他支援 RTMP 推流與引流的 app 亦可)和 ffplay.exe 對流媒體伺服器進行測試。
快直播的推流界面和引流界面

Windows 下利用 ffplay 進行引流,指令行執行:
ffplay rtmp://192.168.0.0/live/test
# ip 位址換成流媒體伺服器的位址, test 表示直播房間号
測試結果:
推流
本文直播推流步驟:
- 使用 AudioRecord 采集音頻,使用 Camera API 采集視訊資料
- 分别使用 faac 和 xh264 第三方庫在 Native 層對音頻和視訊進行編碼
- 利用 rtmp-dump 第三方庫進行打包和推流
工程目錄:
主要的 JNI 方法:
public class NativePush {
public native void startPush(String url);
public native void stopPush();
public native void release();
/**
* 設定視訊參數
* @param width
* @param height
* @param bitrate
* @param fps
*/
public native void setVideoOptions(int width, int height, int bitrate, int fps);
/**
* 設定音頻參數
* @param sampleRateInHz
* @param channel
*/
public native void setAudioOptions(int sampleRateInHz, int channel);
/**
* 發送視訊資料
* @param data
*/
public native void fireVideo(byte[] data);
/**
* 發送音頻資料
* @param data
* @param len
*/
public native void fireAudio(byte[] data, int len);
}
視訊采集
視訊采集主要基于 Camera 相關 API ,利用 SurfaceView 進行預覽,通過 PreviewCallback 擷取相機預覽資料。
視訊預覽主要代碼實作:
public void startPreview(){
try {
mCamera = Camera.open(mVideoParams.getCameraId());
Camera.Parameters param = mCamera.getParameters();
List<Camera.Size> previewSizes = param.getSupportedPreviewSizes();
int length = previewSizes.size();
for (int i = 0; i < length; i++) {
Log.i(TAG, "SupportedPreviewSizes : " + previewSizes.get(i).width + "x" + previewSizes.get(i).height);
}
mVideoParams.setWidth(previewSizes.get(0).width);
mVideoParams.setHeight(previewSizes.get(0).height);
param.setPreviewFormat(ImageFormat.NV21);
param.setPreviewSize(mVideoParams.getWidth(), mVideoParams.getHeight());
mCamera.setParameters(param);
//mCamera.setDisplayOrientation(90); // 豎屏
mCamera.setPreviewDisplay(mSurfaceHolder);
buffer = new byte[mVideoParams.getWidth() * mVideoParams.getHeight() * 4];
mCamera.addCallbackBuffer(buffer);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.startPreview();
} catch (IOException e) {
e.printStackTrace();
}
}
利用 FrameCallback 擷取預覽資料傳入 Native 層,然後進行編碼:
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
if (mCamera != null) {
mCamera.addCallbackBuffer(buffer);
}
if (mIsPushing) {
mNativePush.fireVideo(bytes);
}
}
音頻采集
音頻采集基于 AudioRecord 實作,在一個子線程采集音頻 PCM 資料,并将資料不斷傳入 Native 層進行編碼。
private class AudioRecordRunnable implements Runnable {
@Override
public void run() {
mAudioRecord.startRecording();
while (mIsPushing) {
//通過AudioRecord不斷讀取音頻資料
byte[] buffer = new byte[mMinBufferSize];
int length = mAudioRecord.read(buffer, 0, buffer.length);
if (length > 0) {
//傳遞給 Native 代碼,進行音頻編碼
mNativePush.fireAudio(buffer, length);
}
}
}
}
編碼和推流
音視訊資料編碼和推流在 Native 層實作,首先添加 faac , x264 , librtmp 第三方庫到 AS 工程,然後初始化相關設定,基于生産者與消費者模式,将編碼後的音視訊資料,在生産者線程中打包 RTMPPacket 放入雙向連結清單,在消費者線程中從連結清單中取 RTMPPacket ,通過 RTMP_SendPacket 方法發送給伺服器。
x264 初始化:
JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setVideoOptions(JNIEnv *env, jobject instance, jint width,
jint height, jint bitRate, jint fps) {
x264_param_t param;
//x264_param_default_preset 設定
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
//編碼輸入的像素格式YUV420P
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
y_len = width * height;
u_len = y_len / 4;
v_len = u_len;
//參數i_rc_method表示碼率控制,CQP(恒定品質),CRF(恒定碼率),ABR(平均碼率)
//恒定碼率,會盡量控制在固定碼率
param.rc.i_rc_method = X264_RC_CRF;
param.rc.i_bitrate = bitRate / 1000; //* 碼率(比特率,機關Kbps)
param.rc.i_vbv_max_bitrate = bitRate / 1000 * 1.2; //瞬時最大碼率
//碼率控制不通過timebase和timestamp,而是fps
param.b_vfr_input = 0;
param.i_fps_num = fps; //* 幀率分子
param.i_fps_den = 1; //* 幀率分母
param.i_timebase_den = param.i_fps_num;
param.i_timebase_num = param.i_fps_den;
param.i_threads = 1;//并行編碼線程數量,0預設為多線程
//是否把SPS和PPS放入每一個關鍵幀
//SPS Sequence Parameter Set 序列參數集,PPS Picture Parameter Set 圖像參數集
//為了提高圖像的糾錯能力
param.b_repeat_headers = 1;
//設定Level級别
param.i_level_idc = 51;
//設定Profile檔次
//baseline級别,沒有B幀,隻有 I 幀和 P 幀
x264_param_apply_profile(¶m, "baseline");
//x264_picture_t(輸入圖像)初始化
x264_picture_alloc(&pic_in, param.i_csp, param.i_width, param.i_height);
pic_in.i_pts = 0;
//打開編碼器
video_encode_handle = x264_encoder_open(¶m);
if (video_encode_handle) {
LOGI("打開視訊編碼器成功");
} else {
throwNativeError(env, INIT_FAILED);
}
}
faac 初始化:
JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_setAudioOptions(JNIEnv *env, jobject instance,
jint sampleRateInHz, jint channel) {
audio_encode_handle = faacEncOpen(sampleRateInHz, channel, &nInputSamples,
&nMaxOutputBytes);
if (!audio_encode_handle) {
LOGE("音頻編碼器打開失敗");
return;
}
//設定音頻編碼參數
faacEncConfigurationPtr p_config = faacEncGetCurrentConfiguration(audio_encode_handle);
p_config->mpegVersion = MPEG4;
p_config->allowMidside = 1;
p_config->aacObjectType = LOW;
p_config->outputFormat = 0; //輸出是否包含ADTS頭
p_config->useTns = 1; //時域噪音控制,大概就是消爆音
p_config->useLfe = 0;
// p_config->inputFormat = FAAC_INPUT_16BIT;
p_config->quantqual = 100;
p_config->bandWidth = 0; //頻寬
p_config->shortctl = SHORTCTL_NORMAL;
if (!faacEncSetConfiguration(audio_encode_handle, p_config)) {
LOGE("%s", "音頻編碼器配置失敗..");
throwNativeError(env, INIT_FAILED);
return;
}
LOGI("%s", "音頻編碼器配置成功");
}
對視訊資料進行編碼打包,通過 add_rtmp_packet 放傳入連結表:
JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_fireVideo(JNIEnv *env, jobject instance, jbyteArray buffer_) {
//視訊資料轉為YUV420P
//NV21->YUV420P
jbyte *nv21_buffer = (*env)->GetByteArrayElements(env, buffer_, NULL);
jbyte *u = pic_in.img.plane[1];
jbyte *v = pic_in.img.plane[2];
//nv21 4:2:0 Formats, 12 Bits per Pixel
//nv21與yuv420p,y個數一緻,uv位置對調
//nv21轉yuv420p y = w*h,u/v=w*h/4
//nv21 = yvu yuv420p=yuv y=y u=y+1+1 v=y+1
//如果要進行圖像處理(美顔),可以再轉換為RGB
//還可以結合OpenCV識别人臉等等
memcpy(pic_in.img.plane[0], nv21_buffer, y_len);
int i;
for (i = 0; i < u_len; i++) {
*(u + i) = *(nv21_buffer + y_len + i * 2 + 1);
*(v + i) = *(nv21_buffer + y_len + i * 2);
}
//h264編碼得到NALU數組
x264_nal_t *nal = NULL; //NAL
int n_nal = -1; //NALU的個數
//進行h264編碼
if (x264_encoder_encode(video_encode_handle, &nal, &n_nal, &pic_in, &pic_out) < 0) {
LOGE("%s", "編碼失敗");
return;
}
//使用rtmp協定将h264編碼的視訊資料發送給流媒體伺服器
//幀分為關鍵幀和普通幀,為了提高畫面的糾錯率,關鍵幀應包含SPS和PPS資料
int sps_len, pps_len;
unsigned char sps[100];
unsigned char pps[100];
memset(sps, 0, 100);
memset(pps, 0, 100);
pic_in.i_pts += 1; //順序累加
//周遊NALU數組,根據NALU的類型判斷
for (i = 0; i < n_nal; i++) {
if (nal[i].i_type == NAL_SPS) {
//複制SPS資料,序列參數集(Sequence parameter set)
sps_len = nal[i].i_payload - 4;
memcpy(sps, nal[i].p_payload + 4, sps_len); //不複制四位元組起始碼
} else if (nal[i].i_type == NAL_PPS) {
//複制PPS資料,圖像參數集(Picture parameter set)
pps_len = nal[i].i_payload - 4;
memcpy(pps, nal[i].p_payload + 4, pps_len); //不複制四位元組起始碼
//發送序列資訊
//h264關鍵幀會包含SPS和PPS資料
add_264_sequence_header(pps, sps, pps_len, sps_len);
} else {
//發送幀資訊
add_264_body(nal[i].p_payload, nal[i].i_payload);
}
}
(*env)->ReleaseByteArrayElements(env, buffer_, nv21_buffer, 0);
}
同樣,對音頻資料進行編碼打包放傳入連結表:
JNIEXPORT void JNICALL
Java_com_haohao_live_jni_NativePush_fireAudio(JNIEnv *env, jobject instance, jbyteArray buffer_,
jint length) {
int *pcmbuf;
unsigned char *bitbuf;
jbyte *b_buffer = (*env)->GetByteArrayElements(env, buffer_, 0);
pcmbuf = (short *) malloc(nInputSamples * sizeof(int));
bitbuf = (unsigned char *) malloc(nMaxOutputBytes * sizeof(unsigned char));
int nByteCount = 0;
unsigned int nBufferSize = (unsigned int) length / 2;
unsigned short *buf = (unsigned short *) b_buffer;
while (nByteCount < nBufferSize) {
int audioLength = nInputSamples;
if ((nByteCount + nInputSamples) >= nBufferSize) {
audioLength = nBufferSize - nByteCount;
}
int i;
for (i = 0; i < audioLength; i++) {//每次從實時的pcm音頻隊列中讀出量化位數為8的pcm資料。
int s = ((int16_t *) buf + nByteCount)[i];
pcmbuf[i] = s << 8;//用8個二進制位來表示一個采樣量化點(模數轉換)
}
nByteCount += nInputSamples;
//利用FAAC進行編碼,pcmbuf為轉換後的pcm流資料,audioLength為調用faacEncOpen時得到的輸入采樣數,bitbuf為編碼後的資料buff,nMaxOutputBytes為調用faacEncOpen時得到的最大輸出位元組數
int byteslen = faacEncEncode(audio_encode_handle, pcmbuf, audioLength,
bitbuf, nMaxOutputBytes);
if (byteslen < 1) {
continue;
}
add_aac_body(bitbuf, byteslen);//從bitbuf中得到編碼後的aac資料流,放到資料隊列
}
if (bitbuf)
free(bitbuf);
if (pcmbuf)
free(pcmbuf);
(*env)->ReleaseByteArrayElements(env, buffer_, b_buffer, 0);
}
消費者線程不斷從連結清單中取 RTMPPacket 發送給伺服器:
void *push_thread(void *arg) {
JNIEnv *env;//擷取目前線程JNIEnv
(*javaVM)->AttachCurrentThread(javaVM, &env, NULL);
//建立RTMP連接配接
RTMP *rtmp = RTMP_Alloc();
if (!rtmp) {
LOGE("rtmp初始化失敗");
goto end;
}
RTMP_Init(rtmp);
rtmp->Link.timeout = 5; //連接配接逾時的時間
//設定流媒體位址
RTMP_SetupURL(rtmp, rtmp_path);
//釋出rtmp資料流
RTMP_EnableWrite(rtmp);
//建立連接配接
if (!RTMP_Connect(rtmp, NULL)) {
LOGE("%s", "RTMP 連接配接失敗");
throwNativeError(env, CONNECT_FAILED);
goto end;
}
//計時
start_time = RTMP_GetTime();
if (!RTMP_ConnectStream(rtmp, 0)) { //連接配接流
LOGE("%s", "RTMP ConnectStream failed");
throwNativeError(env, CONNECT_FAILED);
goto end;
}
is_pushing = TRUE;
//發送AAC頭資訊
add_aac_sequence_header();
while (is_pushing) {
//發送
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);
//取出隊列中的RTMPPacket
RTMPPacket *packet = queue_get_first();
if (packet) {
queue_delete_first(); //移除
packet->m_nInfoField2 = rtmp->m_stream_id; //RTMP協定,stream_id資料
int i = RTMP_SendPacket(rtmp, packet, TRUE); //TRUE放入librtmp隊列中,并不是立即發送
if (!i) {
LOGE("RTMP 斷開");
RTMPPacket_Free(packet);
pthread_mutex_unlock(&mutex);
goto end;
} else {
LOGI("%s", "rtmp send packet");
}
RTMPPacket_Free(packet);
}
pthread_mutex_unlock(&mutex);
}
end:
LOGI("%s", "釋放資源");
free(rtmp_path);
RTMP_Close(rtmp);
RTMP_Free(rtmp);
(*javaVM)->DetachCurrentThread(javaVM);
return 0;
}
引流
這裡引流就不做展開講,可以通過 QLive 的 SDK 或者 vitamio (小楠總)等第三方庫實作。
基于 vitamio 實作引流:
private void init(){
mVideoView = (VideoView) findViewById(R.id.live_player_view);
mVideoView.setVideoPath(SPUtils.getInstance(this).getString(SPUtils.KEY_NGINX_SER_URI));
mVideoView.setMediaController(new MediaController(this));
mVideoView.requestFocus();
mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.setPlaybackSpeed(1.0f);
}
});
}
PS:源碼位址:
https://github.com/githubhaohao/NDKLiveNDK 開發系列文章:
- NDK 編譯的三種方式
- NDK 開發中引入第三方靜态庫和動态庫
- NDK 開發中 Native 與 Java 互動
- NDK POSIX 多線程程式設計
- NDK Android OpenSL ES 音頻采集與播放
- NDK FFmpeg 編譯
- NDK FFmpeg 音視訊解碼
- NDK 直播流媒體伺服器搭建
- NDK 直播推流與引流
- NDK 開發中快速定位 Crash 問題
「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。