簡介
觀看手游直播時,我們觀眾端看到的是選手的屏幕上的內(nèi)容洒疚,這是如何實現(xiàn)的呢歹颓?這篇博客將手寫一個錄屏直播 Demo,實現(xiàn)類似手游直播的效果
獲取屏幕數(shù)據(jù)很簡單油湖,Android 系統(tǒng)有提供對應(yīng)的服務(wù)巍扛,難點在于傳輸數(shù)據(jù)到直播服務(wù)器,我們使用 RtmpDump 來傳輸 Rtmp 數(shù)據(jù)乏德,由于 RtmpDump 使用 C 語言實現(xiàn)撤奸,我們還需要用到 NDK 開發(fā),單單用 Java 無法實現(xiàn)哈鹅经,當(dāng)然如果不怕麻煩的話寂呛,還可以自己編譯 Ffmpeg 實現(xiàn) Rtmp 推流怎诫,B 站開源的 ijkplayer 播放器也是基于 Ffmpeg 來開發(fā)的
實現(xiàn)效果
最終我們推流到 B 站直播間瘾晃,在直播間能夠?qū)崟r看到我們手機屏幕上的畫面
基本流程
- 獲取錄屏數(shù)據(jù)
- 對數(shù)據(jù)進(jìn)行 h264 編碼
- Rtmp 數(shù)據(jù)包
- 上傳到直播服務(wù)器推流地址
獲取錄屏數(shù)據(jù)
通過 Intent 獲取到 MediaProjectionService,繼而獲取到 Mediaprojection 的 VirtualCanvas幻妓,我們錄屏的原始數(shù)據(jù)就是從中得來的
private void initLive() {
mediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
Intent screenRecordIntent = mediaProjectionManager.createScreenCaptureIntent();
startActivityForResult(screenRecordIntent,100);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100 && resultCode == Activity.RESULT_OK) {
//MediaProjection--->產(chǎn)生錄屏數(shù)據(jù)
mediaProjection = mediaProjectionManager.getMediaProjection
(resultCode, data);
}
}
對數(shù)據(jù)進(jìn)行 h264 編碼
通過 MediaProjection 獲取到的 YUV 裸數(shù)據(jù)蹦误,我們先需要對其進(jìn)行 h264 編碼,此時我們使用原生 MediaCodec 進(jìn)行硬件編碼
public void start(MediaProjection mediaProjection){
this.mediaProjection = mediaProjection;
// 配置 MediaCodec
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC,width,height);
// 顏色格式
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, 400_000);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
// 設(shè)置觸發(fā)關(guān)鍵幀的時間間隔為 2 s
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);
// 創(chuàng)建編碼器
try {
mediaCodec = MediaCodec.createEncoderByType("video/avc");
mediaCodec.configure(mediaFormat,null,null,MediaCodec.CONFIGURE_FLAG_ENCODE);
Surface surface = mediaCodec.createInputSurface();
mediaProjection.createVirtualDisplay(TAG,width,height,1, DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
,surface,null,null);
} catch (IOException e) {
e.printStackTrace();
}
start();
}
@Override
public void run() {
isLiving = true;
mediaCodec.start();
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (isLiving){
//若時間差大于 2 s肉津,則通知編碼器强胰,生成 I 幀
if (System.currentTimeMillis() - timeStamp >= 2000){
// Bundle 通知 Dsp
Bundle msgBundle = new Bundle();
msgBundle.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME,0);
mediaCodec.setParameters(msgBundle);
timeStamp = System.currentTimeMillis();
}
// 接下來就是 MediaCodec 常規(guī)操作,獲取 Buffer 可用索引妹沙,這里不需要獲取輸出索引偶洋,內(nèi)部已經(jīng)操作了
int outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo,100_000);
if (outputBufferIndex >=0){
// 獲取到了
ByteBuffer byteBuffer = mediaCodec.getOutputBuffer(outputBufferIndex);
byte[] outData = new byte[bufferInfo.size];
byteBuffer.get(outData);
}
}
}
Rtmp 數(shù)據(jù)包
經(jīng)過上面兩步,我們獲得了編碼好的 h264 數(shù)據(jù)距糖,接下來封裝 Rtmp 就比較頭疼了(Ndk 的知識都忘得差不多了)
首先我們在項目的 cpp 文件中玄窝,把 Rtmpdump 的源代碼導(dǎo)入,我們使用 rtmpdump 連接服務(wù)器悍引,以及傳輸 Rtmp 數(shù)據(jù)恩脂,要知道目前手里的數(shù)據(jù)還是 h264 碼流,是無法直接傳輸趣斤,需要封裝成 Rtmp 數(shù)據(jù)包
使用第三方庫 Rtmpdump 來實現(xiàn)推流到直播服務(wù)器俩块,由于 Rtmpdump 的代碼量不是很多,我們直接拷貝源代碼到 Android 的 cpp 文件,如果需要用到 Ffmpeg 不能才用該種調(diào)用方式了玉凯,需要提前編譯好 so 庫文件
對 Rtmp 暫時不需要做太深刻的理解,因為很容易給自己繞進(jìn)去,用 Rtmp 傳輸 h264 數(shù)據(jù),那么 sps,pps ,以及關(guān)鍵幀怎么擺放,Rtmp 都已經(jīng)規(guī)定好了,我們就需要使用 NDK 的方式,實現(xiàn) rtmp 數(shù)據(jù)的填充
RtmpDump 的使用
- 連接服務(wù)器
- RTMP_Init(RTMP *r) 初始化
- RTMP_EnableWrite(RTMP *r) 配置開啟數(shù)據(jù)寫入
- RTMP_Connect(RTMP *r, RTMPPacket *cp)
- RTMP_ConnectStream(RTMP *r, int seekTime)
- 發(fā)送數(shù)據(jù)
- RTMPPacket_Alloc(RTMPPacket *p, int nSize)
- RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
- RTMPPacket_Free(RTMPPacket *p)
連接直播服務(wù)器
這一步中势腮,需要預(yù)先準(zhǔn)備直播推流地址,然后實現(xiàn) native 方法
extern "C" JNIEXPORT jboolean JNICALL
Java_com_bailun_kai_rtmplivedemo_RtmpPack2Remote_connectLiveServer(JNIEnv *env, jobject thiz,
jstring url) {
// 首先 Java 的轉(zhuǎn)成 C 的字符串漫仆,不然無法使用
const char *live_url = env->GetStringUTFChars(url,0);
int result;
do {
// 結(jié)構(gòu)體對象分配內(nèi)存
livePack = (LivePack *)(malloc(sizeof(LivePack)));
// 清空內(nèi)存上的臟數(shù)據(jù)
memset(livePack,0,sizeof(LivePack));
// Rtmp 申請內(nèi)存
livePack->rtmp = RTMP_Alloc();
RTMP_Init(livePack->rtmp);
// 設(shè)置 rtmp 初始化參數(shù)嫉鲸,比如超時時間、url
livePack->rtmp->Link.timeout = 10;
LOGI("connect %s", url);
if (!(result = RTMP_SetupURL(livePack->rtmp,(char *)live_url))){
break;
}
// 開啟 Rtmp 寫入
RTMP_EnableWrite(livePack->rtmp);
LOGI("RTMP_Connect");
if (!(result = RTMP_Connect(livePack->rtmp,0))){
break;
}
LOGI("RTMP_ConnectStream ");
if (!(result = RTMP_ConnectStream(livePack->rtmp, 0)))
break;
LOGI("connect success");
}while (0);
if (!result && livePack){
free(livePack);
livePack = nullptr;
}
env->ReleaseStringUTFChars(url,live_url);
return result;
}
發(fā)送數(shù)據(jù)到直播服務(wù)器
有意思的是歹啼,Rtmp 協(xié)議中不需要傳遞分隔符(h264 分隔符為 0 0 0 1),并且推流的第一個 Rtmp 包的內(nèi)容為 sps玄渗、pps 等
// 發(fā)送 rtmp 數(shù)據(jù)到服務(wù)器
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_bailun_kai_rtmplivedemo_RtmpPack2Remote_sendData2Server(JNIEnv *env, jobject thiz,
jbyteArray buffer, jint length,
jlong tms) {
int result;
// 拷貝數(shù)據(jù)
jbyte *bufferArray = env->GetByteArrayElements(buffer, 0);
result = sendDataInner(bufferArray,length,tms);
//釋放內(nèi)存
env->ReleaseByteArrayElements(buffer,bufferArray,0);
return result;
}
int sendDataInner(jbyte *array, jint length, jlong tms) {
int result = 0;
//處理 sps、pps
if (array[4] == 0x67){
// 讀取 sps狸眼,pps 數(shù)據(jù)藤树,保存到結(jié)構(gòu)體中
readSpsPps(array,length,livePack);
return result;
}
//處理 I幀,其他幀
if(array[4] == 0x65){
RTMPPacket * spsPpsPacket = createRtmpSteramPack(livePack);
sendPack(spsPpsPacket);
}
RTMPPacket* rtmpPacket = createRtmpPack(array,length,tms,livePack);
result = sendPack(rtmpPacket);
return result;
}
int sendPack(RTMPPacket *pPacket) {
int result = RTMP_SendPacket(livePack->rtmp,pPacket,1);
RTMPPacket_Free(pPacket);
free(pPacket);
return result;
}
// 發(fā)送 sps 拓萌,pps 對應(yīng)的 Rtmp 包
RTMPPacket *createRtmpSteramPack(LivePack *pack) {
// 創(chuàng)建 Rtmp 數(shù)據(jù)包岁钓,對應(yīng) RtmpDump 庫的 RTMPPacket 結(jié)構(gòu)體
int body_size = 16 + pack->sps_len + pack->pps_len;
RTMPPacket *rtmpPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(rtmpPacket,body_size);
int index = 0;
rtmpPacket->m_body[index++] = 0x17;
//AVC sequence header 設(shè)置為0x00
rtmpPacket->m_body[index++] = 0x00;
//CompositionTime
rtmpPacket->m_body[index++] = 0x00;
rtmpPacket->m_body[index++] = 0x00;
rtmpPacket->m_body[index++] = 0x00;
//AVC sequence header
rtmpPacket->m_body[index++] = 0x01;
// 原始 操作
rtmpPacket->m_body[index++] = pack->sps[1]; //profile 如baseline、main微王、 high
rtmpPacket->m_body[index++] = pack->sps[2]; //profile_compatibility 兼容性
rtmpPacket->m_body[index++] = pack->sps[3]; //profile level
rtmpPacket->m_body[index++] = 0xFF;//已經(jīng)給你規(guī)定好了
rtmpPacket->m_body[index++] = 0xE1; //reserved(111) + lengthSizeMinusOne(5位 sps 個數(shù)) 總是0xe1
//高八位
rtmpPacket->m_body[index++] = (pack->sps_len >> 8) & 0xFF;
// 低八位
rtmpPacket->m_body[index++] = pack->sps_len & 0xff;
// 拷貝sps的內(nèi)容
memcpy(&rtmpPacket->m_body[index], pack->sps, pack->sps_len);
index +=pack->sps_len;
// pps
rtmpPacket->m_body[index++] = 0x01; //pps number
//rtmp 協(xié)議
//pps length
rtmpPacket->m_body[index++] = (pack->pps_len >> 8) & 0xff;
rtmpPacket->m_body[index++] = pack->pps_len & 0xff;
// 拷貝pps內(nèi)容
memcpy(&rtmpPacket->m_body[index], pack->pps, pack->pps_len);
//packaet
//視頻類型
rtmpPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
//
rtmpPacket->m_nBodySize = body_size;
// 視頻 04
rtmpPacket->m_nChannel = 0x04;
rtmpPacket->m_nTimeStamp = 0;
rtmpPacket->m_hasAbsTimestamp = 0;
rtmpPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
rtmpPacket->m_nInfoField2 = livePack->rtmp->m_stream_id;
return rtmpPacket;
}
RTMPPacket *createRtmpPack(jbyte *array, jint length, jlong tms, LivePack *pack) {
array += 4;
RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
int body_size = length + 9;
RTMPPacket_Alloc(packet, body_size);
if (array[0] == 0x65) {
packet->m_body[0] = 0x17;
LOGI("發(fā)送關(guān)鍵幀 data");
} else{
packet->m_body[0] = 0x27;
LOGI("發(fā)送非關(guān)鍵幀 data");
}
// 固定的大小
packet->m_body[1] = 0x01;
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//長度
packet->m_body[5] = (length >> 24) & 0xff;
packet->m_body[6] = (length >> 16) & 0xff;
packet->m_body[7] = (length >> 8) & 0xff;
packet->m_body[8] = (length) & 0xff;
//數(shù)據(jù)
memcpy(&packet->m_body[9], array, length);
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = body_size;
packet->m_nChannel = 0x04;
packet->m_nTimeStamp = tms;
packet->m_hasAbsTimestamp = 0;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_nInfoField2 = pack->rtmp->m_stream_id;
return packet;
}
void readSpsPps(jbyte *array, jint length, LivePack *pack) {
for (int i = 0; i < length; i++) {
if (i+4 < length){
// 找到 pps 的下標(biāo)
if (array[i] == 0x00
&& array[i+1] == 0x00
&& array[i+2] == 0x00
&& array[i+3] == 0x01
&& array[i+4] == 0x68
){
// 保存 sps
livePack->sps_len = i - 4;
livePack->sps = static_cast<int8_t *>(malloc(livePack->sps_len));
memcpy(livePack->sps,array + 4,livePack->sps_len);
// 保存 pps
livePack->pps_len = length -(livePack->sps_len+4) - 4;
livePack->pps = static_cast<int8_t *>(malloc(livePack->pps_len));
memcpy(livePack->pps,array+4+livePack->sps_len+4,livePack->pps_len);
LOGI("sps:%d pps:%d", livePack->sps_len, livePack->pps_len);
}
}
}
指針的使用
- malloc 只管分配內(nèi)存屡限,并不能對所得的內(nèi)存進(jìn)行初始化,所以得到的一片新內(nèi)存中炕倘,其值將是隨機的钧大,申請的內(nèi)存是連續(xù)的
- 返回類型是 void* 類型,void* 表示未確定類型的指針罩旋。C/C++ 規(guī)定啊央,void* 類型可以強制轉(zhuǎn)換為任何其它類型的指針,malloc 函數(shù)返回的是 void * 類型涨醋,C++:p = malloc (sizeof(int)); 則程序無法通過編譯瓜饥,報錯:“不能將 void* 賦值給 int * 類型變量”。所以必須通過 (int *) 來將強制轉(zhuǎn)換
總結(jié)
首先我們通過系統(tǒng)服務(wù)拿到手機屏幕的畫面浴骂,此時取到的原始數(shù)據(jù)還無法進(jìn)行網(wǎng)絡(luò)傳輸乓土,在對其進(jìn)行 h264 編碼后,封裝 Rtmp 包溯警,然后按照 Rtmp 協(xié)議規(guī)定的方式進(jìn)行傳輸
相關(guān)鏈接
C語言 malloc()趣苏、memcpy()、free()
java 并發(fā)之阻塞隊列 LinkedBlockingQueue