Android Media 09 --- RTMP推流(librtmp+录屏)

一.CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)
add_subdirectory(librtmp)
add_library(
       native-lib
       SHARED
       native-lib.cpp )

find_library(
       log-lib
       log )

target_link_libraries(
       native-lib
       ${log-lib}
       rtmp)

二.MainActivity

public class MainActivity extends AppCompatActivity   {
   private MediaProjectionManager mediaProjectionManager;
   private MediaProjection mediaProjection;
   ScreenLive screenLive;
   String url = "rtmp://live-push.bilivideo.com/live-bvc/?streamname=live_436361523_69672384&key=654ce7137e852ab60fde72836c815a63&schedule=rtmp";

   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       checkPermission();
   }

   @Override
   protected void onActivityResult(int requestCode, int resultCode, Intent data) {
       super.onActivityResult(requestCode, resultCode, data);
       if (requestCode == 100 && resultCode == Activity.RESULT_OK) {
           mediaProjection = mediaProjectionManager.getMediaProjection
                   (resultCode, data);
           screenLive = new ScreenLive();
           screenLive.startLive(url, mediaProjection);
       }
   }

   public void startLive(View view) {
       this.mediaProjectionManager = (MediaProjectionManager)getSystemService(Context.MEDIA_PROJECTION_SERVICE);
       Intent captureIntent = mediaProjectionManager.createScreenCaptureIntent();
       startActivityForResult(captureIntent, 100);
   }

   public boolean checkPermission() {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && checkSelfPermission(
               Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
           requestPermissions(new String[]{
                   Manifest.permission.READ_EXTERNAL_STORAGE,
                   Manifest.permission.WRITE_EXTERNAL_STORAGE,
                   Manifest.permission.CAMERA
           }, 1);
       }
       return false;
   }


   public void stopLive(View view) {
   }
}

三.RTMPPackage

public class RTMPPackage {
   private byte[] buffer;
   private long tms;

   public RTMPPackage(byte[] buffer, long tms) {
       this.buffer = buffer;
       this.tms = tms;
   }

   public byte[] getBuffer() {
       return buffer;
   }

   public void setBuffer(byte[] buffer) {
       this.buffer = buffer;
   }

   public long getTms() {
       return tms;
   }

   public void setTms(long tms) {
       this.tms = tms;
   }
}

四.VideoCodec

public class VideoCodec extends  Thread {
   private MediaProjection mediaProjection;
   private VirtualDisplay virtualDisplay;
   private MediaCodec mediaCodec;
   private ScreenLive screenLive;
   private boolean isLiving;
   private long timeStamp;
   private long startTime;
   public VideoCodec(ScreenLive screenLive) {
       this.screenLive = screenLive;
   }

   public void startLive(MediaProjection mediaProjection) {
       this.mediaProjection = mediaProjection;
       MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 720, 1280);
       format.setInteger(MediaFormat.KEY_COLOR_FORMAT,  MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
       format.setInteger(MediaFormat.KEY_BIT_RATE, 400_000);
       format.setInteger(MediaFormat.KEY_FRAME_RATE, 15);
       format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2);
       try {
           mediaCodec = MediaCodec.createEncoderByType("video/avc");
           mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
           Surface surface = mediaCodec.createInputSurface();
           virtualDisplay = mediaProjection.createVirtualDisplay(
                   "screen-codec",
                   720, 1280, 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) {
           if (System.currentTimeMillis() - timeStamp >= 2000) {
               Bundle params = new Bundle();
               params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
               //dsp 芯片触发I帧
               mediaCodec.setParameters(params);
               timeStamp = System.currentTimeMillis();
           }
           int index = mediaCodec.dequeueOutputBuffer(bufferInfo, 100000);
           if (index >= 0) {
               if (startTime == 0) {
                   startTime = bufferInfo.presentationTimeUs / 1000;
               }
               ByteBuffer buffer = mediaCodec.getOutputBuffer(index);
               byte[] outData = new byte[bufferInfo.size];
               buffer.get(outData);
               RTMPPackage rtmpPackage = new RTMPPackage(outData, (bufferInfo.presentationTimeUs / 1000) - startTime);
               screenLive.addPackage(rtmpPackage);
               mediaCodec.releaseOutputBuffer(index, false);
           }
       }
       isLiving = false;
       mediaCodec.stop();
       mediaCodec.release();
       mediaCodec = null;
       virtualDisplay.release();
       virtualDisplay = null;
       mediaProjection.stop();
       mediaProjection = null;
       startTime = 0;
   }


}

五.ScreenLive

public class ScreenLive extends Thread {
   private String url;
   private MediaProjection mediaProjection;
   private LinkedBlockingQueue<RTMPPackage> queue = new LinkedBlockingQueue<>();
   private boolean isLiving;
   static {
       System.loadLibrary("native-lib");
   }

   public void startLive(String url, MediaProjection mediaProjection) {
       this.url = url;
       this.mediaProjection = mediaProjection;
       start();
   }

   @Override
   public void run() {
       if (!connect(url)) {
           Log.i("liuyi", "run: ----------->推送失败");
           return;
       }

       VideoCodec videoCodec = new VideoCodec(this);
       videoCodec.startLive(mediaProjection);

       isLiving = true;
       while (isLiving) {
           RTMPPackage rtmpPackage = null;
           try {
               rtmpPackage = queue.take();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           if (rtmpPackage.getBuffer() != null && rtmpPackage.getBuffer().length != 0) {
               Log.i("liuyi","java sendData");
               sendData(rtmpPackage.getBuffer(), rtmpPackage.getBuffer() .length , rtmpPackage.getTms());
           }
       }
   }

   public void addPackage(RTMPPackage rtmpPackage) {
       if (!isLiving) {
           return;
       }
       queue.add(rtmpPackage);
   }

   private native boolean connect(String url);

   private native boolean sendData(byte[] data, int len, long tms);

}

六.native-lib.cpp

#include <jni.h>
#include <string>
#include <android/log.h>
#define LOGE(...) __android_log_print(ANDROID_LOG_INFO,"liuyi",__VA_ARGS__)

extern "C"{
#include  "librtmp/rtmp.h"
}

typedef  struct {
   RTMP *rtmp;
   int16_t sps_len;
   int8_t *sps;
   int16_t pps_len;
   int8_t *pps;
}Live;
Live *live = NULL;

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_luisliuyi_demo_camera1_ScreenLive_connect(JNIEnv *env, jobject thiz, jstring url_) {
   const char *url = env->GetStringUTFChars(url_, 0);
   int ret;
   do {
       live = (Live*)malloc(sizeof(Live));
       memset(live, 0, sizeof(Live));

       live->rtmp = RTMP_Alloc();
       RTMP_Init(live->rtmp);

       live->rtmp->Link.timeout = 10;

       LOGE("connect %s", url);
       if (!(ret = RTMP_SetupURL(live->rtmp, (char*)url))) break;

       RTMP_EnableWrite(live->rtmp);

       LOGE("RTMP_Connect");
       if (!(ret = RTMP_Connect(live->rtmp, 0))) break;

       LOGE("RTMP_ConnectStream ");
       if (!(ret = RTMP_ConnectStream(live->rtmp, 0))) break;
       LOGE("connect success");
   }  while (0);

   if (!ret && live) {
       free(live);
       live = nullptr;
   }

   env->ReleaseStringUTFChars(url_, url);
   return ret;
}

// 传递第一帧 00 00 00 01 67 64 00 28ACB402201E3CBCA41408081B4284D4  00000001 68 EE 06 F2 C0
void prepareVideo(int8_t *data, int len, Live *live) {
   for (int i = 0; i < len; i++) {
       if (i + 4 < len) {
           if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
               if (data[i + 4]  == 0x68) {
                   //sps解析
                   live->sps_len = i - 4;
                   live->sps = static_cast<int8_t *>(malloc(live->sps_len));
                   memcpy(live->sps, data + 4, live->sps_len);

                   //pps解析
                   live->pps_len = len - (4 + live->sps_len) - 4;
                   live->pps = static_cast<int8_t *>(malloc(live->pps_len));
                   memcpy(live->pps, data + 4 + live->sps_len + 4, live->pps_len);
                   break;
               }
           }
       }
   }
}

//sps  pps 的 packaet
RTMPPacket *createVideoPackage(Live *live) {
   int body_size = 16 + live->sps_len + live->pps_len; //为什么是16????参考rtmp视频包结构
   RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
   RTMPPacket_Alloc(packet, body_size);
   int i = 0;
   packet->m_body[i++] = 0x17;
   packet->m_body[i++] = 0x00;
   packet->m_body[i++] = 0x00;
   packet->m_body[i++] = 0x00;
   packet->m_body[i++] = 0x00;

   packet->m_body[i++] = 0x01;

   packet->m_body[i++] = live->sps[1]; //profile 如baseline、main、 high
   packet->m_body[i++] = live->sps[2]; //profile_compatibility 兼容性
   packet->m_body[i++] = live->sps[3]; //profile level

   packet->m_body[i++] = 0xFF;
   packet->m_body[i++] = 0xE1;

   //sps length
   packet->m_body[i++] = (live->sps_len >> 8) & 0xFF;//高八位
   packet->m_body[i++] = live->sps_len & 0xff;//低八位

   //拷贝sps的内容
   memcpy(&packet->m_body[i], live->sps, live->sps_len);

   i +=live->sps_len;

   packet->m_body[i++] = 0x01;

   //pps length
   packet->m_body[i++] = (live->pps_len >> 8) & 0xff; //高八位
   packet->m_body[i++] = live->pps_len & 0xff;//低八位

   // 拷贝pps内容
   memcpy(&packet->m_body[i], live->pps, live->pps_len);

   //视频类型
   packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
   packet->m_nBodySize = body_size;
   packet->m_nChannel = 0x04;
   packet->m_nTimeStamp = 0;
   packet->m_hasAbsTimestamp = 0;
   packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
   packet->m_nInfoField2 = live->rtmp->m_stream_id;
   return packet;
}

RTMPPacket *createVideoPackage(int8_t *buf, int len, const long tms, Live *live) {
   buf += 4;
   RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket));
   int body_size = len + 9;

   //初始化RTMP内部的body数组
   RTMPPacket_Alloc(packet, body_size);

   if (buf[0] == 0x65) {//
       packet->m_body[0] = 0x17;
       LOGE("发送关键帧 data");
   } else{
       packet->m_body[0] = 0x27;
       LOGE("发送非关键帧 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] = (len >> 24) & 0xff;
   packet->m_body[6] = (len >> 16) & 0xff;
   packet->m_body[7] = (len >> 8) & 0xff;
   packet->m_body[8] = (len) & 0xff;

   //数据
   memcpy(&packet->m_body[9], buf, len);//为什么是9????参考rtmp视频包结构

   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 = live->rtmp->m_stream_id;
   return packet;
}

int sendPacket(RTMPPacket *packet) {
   int r = RTMP_SendPacket(live->rtmp, packet, 1);
   if(r){
       LOGE("发送rtmp包成功");
   }
   RTMPPacket_Free(packet);
   free(packet);
   return r;
}

// 传递第一帧 00 00 00 01 67 64 00 28ACB402201E3CBCA41408081B4284D4  0000000168 EE 06 F2 C0
int sendVideo(int8_t *buf, int len, long tms) {
   int ret = 0;
   if (buf[4] == 0x67) {
       // 缓存sps 和pps 到全局遍历 不需要推流
       if (live && (!live->pps || !live->sps)) {
           prepareVideo(buf, len, live);
       }
       return ret;
   }

   if (buf[4] == 0x65) {//关键帧
       RTMPPacket *packet = createVideoPackage(live);
       sendPacket(packet);
   }

   RTMPPacket *packet2 = createVideoPackage(buf, len, tms, live);
   ret = sendPacket(packet2);
   return ret;
}

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_luisliuyi_demo_camera1_ScreenLive_sendData(JNIEnv *env, jobject thiz, jbyteArray data_,
                                                   jint len, jlong tms) {
   int ret;
   jbyte *data = env->GetByteArrayElements(data_, NULL);
   ret = sendVideo(data, len, tms);
   env->ReleaseByteArrayElements(data_, data, 0);
   return ret;
}

七.代码地址

https://gitee.com/luisliuyi/android-rtmp-mediaprojection.git
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容