现在直播挺火的,也有很多成熟的SDK
百度,阿里,腾讯等等都有自己的SDK
我今天演示的是七牛SDK,为什么选用七牛?
在很久以前云存储,图片处理的时候就看上了七牛,做的真不错,而且有免费额度,所以选择七牛。
介绍下直播的架构:
主要分为四部分:
-
业务服务器
负责协调直播类应用的业务逻辑,包括但不限于:- 创建直播房间
- 返回直播房间播放地址列表
- 关闭直播房间
-
LiveNet 实时流网络
负责流媒体的分发、直播流的创建、查询等相关操作 -
采集端
负责采集和推送流媒体 -
播放端
负责拉取并播放流媒体
简单点说,就是 采集端(比如一个主播拿着手机进行直播)开始采集视频,然后实时传递给LiveNet 实时流网络,这个过程可称为 RTMP推流。
然后播放端(观看直播的吃瓜群众)连接LiveNet 实时流网络 进行播放视频流。
业务服务器呢,就是提供一些诸如登录注册,主播创建房间等等的操作。
1.配置直播空间
登录七牛
选择直播云服务,新建直播空间
注意这里的域名要填写 自己购买的域名,并且这个域名还要备案完成。推荐购买域名和备案使用阿里云:https://wanwang.aliyun.com/domain/
下一步
这里可以选择直接重新建立一个存储空间,这个空间是用来存放直播视频切片的。
下一步
直播封面功能可以打开。 其他的可以暂时都不用填写。
下一步·创建直播空间
创建好之后,注意下红色部分的域名,需要进行CNAME,CNAME是干什么呢?这里我简要说一下 ,假如你购买的域名叫 abc.com 那么把 blog.abc.com 这个二级域名CNAME 成:www.baidu.com , 那么在浏览器 访问 blog.abc.com的时候,会转而访问到 百度,所以这里七牛让我们CNAME域名的意思就是说,直播的域名都可以使用我们自己的域名,但是其实功能是七牛帮我们做的直播功能,我们在访问我们的直播域名的时候,其实是转到七牛的服务器来处理。 那么CNAME怎么来操作呢?上面图片里面有个查看如何CNAME,其实就是在 我们的域名注册商(比如我们刚刚推荐使用阿里云购买域名,那么就去阿里云后台)那里进行配置。
我这里上一张我CNAME的示例图:
进入直播空间
如果CNAME没有完成的话,情况如下:
CNAME成功之后如下: 就可以开发了
2.Android 端推流开发
所谓推流就是android手机采集视频上传到 LiveNet 实时流网络
这是七牛的推流SDK文档: https://developer.qiniu.com/pili/sdk/the-sdk-android-push-flow照着上面写竟然不行,坑爹啊,问了客服竟然是过时了。推流端SDK地址是https://github.com/pili-engineering/PLDroidMediaStreaming 这上面的快速开发指南也是过时的。下载的demo虽然能用,但是对于新手来讲特别不友好,代码冗余,逻辑混乱。
下面是整理的从0开始搭建的推流app
首先是下载SDK,主要的就是jar包和so 库。下载的文件如下:
(1)首先是新建工程,然后在工程里导入jar 包
① 复制进libs文件夹。
②然后右击jar包,选择Add As Library,然后确定
(2)添加so库
在 app/src/main 目录下创建 jniLibs 目录。然后复制进去
(3)添加权限
<uses-permission android:id="android.permission.INTERNET" /> <uses-permission android:id="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:id="android.permission.RECORD_AUDIO" /> <uses-permission android:id="android.permission.CAMERA" /> <uses-permission android:id="android.permission.WAKE_LOCK" /> <uses-feature android:id="android.hardware.camera.autofocus" /> <uses-feature android:glEsVersion="0x00020000" android:required="true" /> <uses-permission android:id="android.permission.ACCESS_NETWORK_STATE"/>
(4)添加happy-dns依赖
在 app 目录下的 build.gradle ,并且按照如下修改:
主要是添加了这2句:
compile 'com.qiniu:happy-dns:0.2.+' compile 'com.qiniu.pili:pili-android-qos:0.8.16'
(5)实现自己的Application
public class StreamingApplication extends Application { @Override public void onCreate() { super.onCreate(); StreamingEnv.init(getApplicationContext()); } }
记得在AndroidManifest文件里把application的name修改下:
(6)在MainActivity里面 获取RTMP推流地址
推流地址就是 指把手机采集的视频发送到那里的服务器地址
这个时候需要服务器配合,因为这个推流地址是需要使用 服务端SDK生成的。
生成推流地址的过程就相当于 主播创建房间,准备开直播
这里我给出服务端PHP sdk生成 参考代码:
七牛直播PHP sdk连接:https://github.com/pili-engineering/pili-sdk-php.v2
这样我们就可以用类似于 这样的地址 http://www.a1anwang.com/getRTMP.php访问服务器来获取 推流地址(就跟登录注册一样)。
有些人可能会说:老子又不是搞服务器的,这么复杂
好消息就是,如果我们不会搭建服务器,那么我们可以借助 七牛的控制台来完成服务器的生成推流地址等工作
如下:
点击直播流管理 里面的 添加流
添加好之后,
在流属性里面即可看到 推流地址,这个是和之前讲的使用PHP SDK生成的地址是一样的。
(7)获取到推流地址之后,开始推流
上一步我们在MainActivity里面获取了推流地址,那么我们获取到推流地址之后 点击开始直播按钮,进入直播页面StreamingActivity
如果我们不会搭建服务器而是使用的控制台生成推流地址的话,那么我们可以这样,在 assets文件夹里创建一个txt文件,把控制台的推流地址放进去,然后在推流页面StreamingActivity里面读取出来,为什么要用文件来存储这个定死的推流地址,我直接用一个string 固定不就行了吗?
行,当然行,使用String 双引号里面需要注意字符转义,我是怕你们搞错了,使用文件不会有这个问题。如下:
而且这样有一个好处,因为推流地址是有 时效性的,所以经常会过期,修改的话,我们只需要直接修改这个文件即可,如果使用String变量的话,还得再次注意转义字符。
public class MUtils { public static String loadAssets(Context context, String fileName) { try { InputStreamReader inputReader = new InputStreamReader( context.getResources().getAssets().open(fileName)); BufferedReader bufReader = new BufferedReader(inputReader); String line = ""; String Result = ""; while ((line = bufReader.readLine()) != null) Result += line; return Result; } catch (Exception e) { e.printStackTrace(); } return ""; } }
新建StreamingActivity ,完整的StreamingActivity代码如下:
package com.a1anwang.freelive; import android.hardware.Camera; import android.opengl.GLSurfaceView; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.util.Log; import com.qiniu.android.dns.DnsManager; import com.qiniu.android.dns.IResolver; import com.qiniu.android.dns.NetworkInfo; import com.qiniu.android.dns.http.DnspodFree; import com.qiniu.android.dns.local.AndroidDnsServer; import com.qiniu.android.dns.local.Resolver; import com.qiniu.pili.droid.streaming.AVCodecType; import com.qiniu.pili.droid.streaming.CameraStreamingSetting; import com.qiniu.pili.droid.streaming.MediaStreamingManager; import com.qiniu.pili.droid.streaming.MicrophoneStreamingSetting; import com.qiniu.pili.droid.streaming.StreamingProfile; import com.qiniu.pili.droid.streaming.StreamingState; import com.qiniu.pili.droid.streaming.StreamingStateChangedListener; import com.qiniu.pili.droid.streaming.widget.AspectFrameLayout; import java.io.IOException; import java.net.InetAddress; import java.net.URISyntaxException; /** * Created by Alan on 2017/3/1. */ public class StreamingActivity extends AppCompatActivity implements StreamingStateChangedListener { private String TAG="StreamingActivity"; StreamingProfile mProfile; private int mCurrentCamFacingIndex; CameraStreamingSetting mCameraStreamingSetting; MicrophoneStreamingSetting mMicrophoneStreamingSetting; protected MediaStreamingManager mMediaStreamingManager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_streaming); String rtmpurl=MUtils.loadAssets(this,"rtmpurl.txt"); mProfile = new StreamingProfile(); try { mProfile.setPublishUrl(rtmpurl); } catch (URISyntaxException e) { e.printStackTrace(); } StreamingProfile.AudioProfile aProfile = new StreamingProfile.AudioProfile(44100, 96 * 1024); StreamingProfile.VideoProfile vProfile = new StreamingProfile.VideoProfile(30, 1000 * 1024, 48); StreamingProfile.AVProfile avProfile = new StreamingProfile.AVProfile(vProfile, aProfile); mProfile.setVideoQuality(StreamingProfile.VIDEO_QUALITY_MEDIUM2) .setAudioQuality(StreamingProfile.AUDIO_QUALITY_MEDIUM2) // .setAVProfile(avProfile) // .setPreferredVideoEncodingSize(960, 544) .setEncodingSizeLevel(StreamingProfile.VIDEO_ENCODING_HEIGHT_480) .setEncoderRCMode(StreamingProfile.EncoderRCModes.BITRATE_PRIORITY) .setDnsManager(getMyDnsManager()) .setAdaptiveBitrateEnable(true) .setFpsControllerEnable(true) .setStreamStatusConfig(new StreamingProfile.StreamStatusConfig(3)) // .setEncodingOrientation(StreamingProfile.ENCODING_ORIENTATION.PORT) .setSendingBufferProfile(new StreamingProfile.SendingBufferProfile(0.2f, 0.8f, 3.0f, 20 * 1000)); CameraStreamingSetting.CAMERA_FACING_ID cameraFacingId = chooseCameraFacingId(); mCurrentCamFacingIndex = cameraFacingId.ordinal(); mCameraStreamingSetting = new CameraStreamingSetting(); mCameraStreamingSetting.setCameraId(Camera.CameraInfo.CAMERA_FACING_FRONT) .setContinuousFocusModeEnabled(true) .setRecordingHint(false) .setCameraFacingId(cameraFacingId) // .setCameraSourceImproved(true) .setResetTouchFocusDelayInMs(3000) // .setFocusMode(CameraStreamingSetting.FOCUS_MODE_CONTINUOUS_PICTURE) .setCameraPrvSizeLevel(CameraStreamingSetting.PREVIEW_SIZE_LEVEL.MEDIUM) .setCameraPrvSizeRatio(CameraStreamingSetting.PREVIEW_SIZE_RATIO.RATIO_16_9) .setBuiltInFaceBeautyEnabled(true) .setFaceBeautySetting(new CameraStreamingSetting.FaceBeautySetting(1.0f, 1.0f, 0.8f)) .setVideoFilter(CameraStreamingSetting.VIDEO_FILTER_TYPE.VIDEO_FILTER_BEAUTY); mMicrophoneStreamingSetting = new MicrophoneStreamingSetting(); mMicrophoneStreamingSetting.setBluetoothSCOEnabled(false); AspectFrameLayout afl = (AspectFrameLayout) findViewById(R.id.cameraPreview_afl); afl.setShowMode(AspectFrameLayout.SHOW_MODE.REAL); GLSurfaceView glSurfaceView = (GLSurfaceView) findViewById(R.id.cameraPreview_surfaceView); // WatermarkSetting watermarksetting = new WatermarkSetting(this); // watermarksetting.setResourceId(R.drawable.ic_launcher) // .setAlpha(100) // .setSize(WatermarkSetting.WATERMARK_SIZE.MEDIUM) // .setCustomPosition(0.5f, 0.5f); mMediaStreamingManager = new MediaStreamingManager(this, afl, glSurfaceView, AVCodecType.SW_VIDEO_WITH_SW_AUDIO_CODEC); // sw codec // mMediaStreamingManager.prepare(mCameraStreamingSetting, mMicrophoneStreamingSetting, watermarksetting, mProfile, new PreviewAppearance(0.0f, 0.0f, 0.5f, 0.5f, PreviewAppearance.ScaleType.FIT)); //mMediaStreamingManager.prepare(mCameraStreamingSetting, mMicrophoneStreamingSetting,watermarksetting, mProfile); mMediaStreamingManager.prepare(mCameraStreamingSetting, mMicrophoneStreamingSetting, mProfile); mMediaStreamingManager.setStreamingStateListener(this); // mMediaStreamingManager.setSurfaceTextureCallback(this); // mMediaStreamingManager.setStreamingSessionListener(this); // mMediaStreamingManager.setNativeLoggingEnabled(false); // mMediaStreamingManager.setStreamStatusCallback(this); // mMediaStreamingManager.setAudioSourceCallback(this); // update the StreamingProfile // mProfile.setStream(new Stream(mJSONObject1)); // mMediaStreamingManager.setStreamingProfile(mProfile); } @Override protected void onResume() { super.onResume(); mMediaStreamingManager.resume(); } @Override protected void onPause() { super.onPause(); mMediaStreamingManager.pause(); } @Override protected void onDestroy() { super.onDestroy(); mMediaStreamingManager.destroy(); } private static DnsManager getMyDnsManager() { IResolver r0 = new DnspodFree(); IResolver r1 = AndroidDnsServer.defaultResolver(); IResolver r2 = null; try { r2 = new Resolver(InetAddress.getByName("119.29.29.29")); } catch (IOException ex) { ex.printStackTrace(); } return new DnsManager(NetworkInfo.normal, new IResolver[]{r0, r1, r2}); } private CameraStreamingSetting.CAMERA_FACING_ID chooseCameraFacingId() { if (CameraStreamingSetting.hasCameraFacing(CameraStreamingSetting.CAMERA_FACING_ID.CAMERA_FACING_3RD)) { return CameraStreamingSetting.CAMERA_FACING_ID.CAMERA_FACING_3RD; } else if (CameraStreamingSetting.hasCameraFacing(CameraStreamingSetting.CAMERA_FACING_ID.CAMERA_FACING_FRONT)) { return CameraStreamingSetting.CAMERA_FACING_ID.CAMERA_FACING_FRONT; } else { return CameraStreamingSetting.CAMERA_FACING_ID.CAMERA_FACING_BACK; } } @Override public void onStateChanged(StreamingState streamingState, Object o) { Log.e(TAG, "StreamingState :" + streamingState ); switch (streamingState){ case READY: ToastUtils.showToast(StreamingActivity.this,"ready",2000); new Thread(new Runnable() { @Override public void run() { if (mMediaStreamingManager != null) { mMediaStreamingManager.startStreaming(); } } }).start(); break; } } }
布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" > <com.qiniu.pili.droid.streaming.widget.AspectFrameLayout android:id="@+id/cameraPreview_afl" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_horizontal" > <android.opengl.GLSurfaceView android:id="@+id/cameraPreview_surfaceView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" /> </com.qiniu.pili.droid.streaming.widget.AspectFrameLayout> </RelativeLayout>这个时候进入 StreamingActivity就已经在推流了, 播放端可以看到直播了, 那么如何验证呢?
我们可以使用能播放rtmp流的播放器来验证一下
我用的是PotPlayer(win7)(当然用手机上的支持rtmp的app也可以):
这个 播放链接填写什么呢?填写的是RTMP播放地址, 这个在正式环境中也是通过服务端SDK (PHP SDK)来获取的
依然地,我们可以在控制台得到:
这样,就可以使用播放器进行播放了, 我们注意到,还有HLS,FLV的播放形式,有兴趣的可以试下
播放画面如下
好,到了这里我们已经完成了基础版的推流APP了,直播的时候还有很多其他设置,比如美颜,镜像,水印等等等等。
当然啦,有兴趣的同学,还可以在我们自己的网站上实现直播功能如下:
这个原理就是在网页上使用了一个支持rtmp播放的播放器
这样是不是已经基本搭建出一个自己的直播平台了呢?
请期待下一篇
原文地址《七牛直播SDK Android试练 1》
发表评论