如今,在国内移动互联网发展了几年的时间,移动开发技术也相对的成熟,在咱们日常使用的手机App中也少不了直播的功能,不管是娱乐类、游戏类、体育类还是教育类等的App都会有直播的功能,可以说直播的功能在一些商业应用或者非商业应用中都是不可或缺的功能。目前国内比较火直播App有如:斗鱼(游戏直播)、YY直播(全民娱乐直播)、虎牙(游戏+电竞直播)以及映客(娱乐直播)等直播。
而要想在自己的Android应用中实现直播的功能,那么就少不了对目前市面上直播推流SDK做一个技术的选型(如果有条件的公司可以自己开发一个直播推流平台,就可以不用第三方推流SDK),由于我们公司的项目是用到阿里云推流SDK的,所以下面我就简单的介绍如何在Android里接入阿里云推流SDK实现直播推流的功能。
首先打开阿里云视频云直播控制台 https://livenext.console.aliyun.com ,然后开通直播推流的功能,并且绑定域名(由于目前国内互联网方面管的比较严,所以必须绑定域名才能进行推流的操作),然后记录下推流域名和播(拉)流域名以及推流鉴权Key和播(拉)流鉴权Key。最后下载直播推流的Android SDK【点击下载】。
首先将下载的Android推流SDK相关的jar包、aar包以及so文件拷贝到AS工程的libs目录下,如下图:
然后在AS的内层build.gradle里添加推流相关的依赖
android {
........
sourceSets {
main {
jniLibs.srcDir 'libs'
}
}
repositories {
flatDir {
dirs 'libs'
}
}
}
dependencies {
........
implementation files('libs/live-beauty-3.4.0.jar')
implementation files('libs/live-face-3.4.0.jar')
implementation files('libs/live-pusher-3.4.0.jar')
implementation(name: 'alivc-core-pusher', ext: 'aar')
implementation(name: 'AlivcPlayer', ext: 'aar')
implementation(name: 'AlivcReporter', ext: 'aar')
implementation(name: 'live-pusher-resources-3.4.0', ext: 'aar')
}
最后在清单文件AndroidManifest.xml中添加直播推流所需的相应权限
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REORDER_TASKS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!--<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
注意:直播推流在Android 6.0 以上在相机界面预览前和发起直播推流之前必须在Java代码或者Kotlin代码里动态授予相机权限(Manifest.permission.CAMERA)和麦克风权限(Manifest.permission.RECORD_AUDIO),否则应用将会闪退。
还有,由于当前使用阿里云最新的推流SDK是V3.4.0版本,该版本暂未适配 Android 10.0 系统,所以需要在AS的内层build.gradle里的targetSdkVersion改为29以下,我这边改为28(即Android 9.0),这样无论是Android 10.0 系统的设备还是Android 10.0以下的设备都能进行推流操作。
在编码实现的这一环节,大家可以直接下载阿里云提供的推流Demo来参考接入(Demo在下载推流SDK时目录里已经包含了),也可以直接看阿里云提供的接入文档【文档地址】,不过个人建议最好直接看阿里云提供的接入文档来接入项目推流功能,因为阿里云提供的推流Demo里的代码写的太乱了。
阿里云直播的推流和拉流的地址生成的工具代码如下:
public class AliyunPushUtils {
/**
* 推流域名 阿里云配置的推流域名
*/
private static final String pushDomain = ShareUtils.getString(LivePusherApplication.getInstance(), "PushDomain", "");
/**
* 拉流域名 阿里云配置的拉流域名
*/
private static final String pullDomain = ShareUtils.getString(LivePusherApplication.getInstance(), "LiveDomain", "");
/**
* appName
*/
private static final String appName = "android";
/**
* 鉴权key: 阿里云创建了推流域名和播流域名过后,他给生成的,每个域名一个,
* 推流用推流的key,播流用播流的key
*/
private static final String pushKey = ShareUtils.getString(LivePusherApplication.getInstance(), "PushKey", "");
private static final String pullKey = ShareUtils.getString(LivePusherApplication.getInstance(), "LiveKey", "");
/**
* @param time 十位数的时间戳
* @return 推流的地址
*/
public static String CreatePushUrl(String streamName, String time) {
String strpush = "/" + appName + "/" + streamName + "-" + time + "-0-0-" + pushKey;
String pushUrl = "rtmp://" + pushDomain + "/" + appName + "/" + streamName + "?auth_key=" + time + "-0-0-" + MD5Utils.getMD5(strpush);
return pushUrl;
}
/**
* @param time 十位数的时间戳
* // * @param rand 这是用来标识的 否则同一个时间戳 生成的地址总是相同的
* 随机数,建议使用UUID(不能包含中划线“-”,例如: 477b3bbc253f467b8def6711128c7bec 格式)
* @return 播放流的地址 默认是flv 也可以更改此代码
*/
public static String GetPlayUrl(String streamName, String time) {
String strviewrtmp1 = null;
String strviewflv1 = null;
String strviewm3u81 = null;
String rtmpurl1 = null;
String flvurl1 = null;
String m3u8url1 = null;
strviewrtmp1 = "/" + appName + "/" + streamName + "-" + time + "-0-0-" + pullKey;
strviewflv1 = "/" + appName + "/" + streamName + ".flv-" + time + "-0-0-" + pullKey;
strviewm3u81 = "/" + appName + "/" + streamName + ".m3u8-" + time + "-0-0-" + pullKey;
String rtmpAuthKey = time + "-0-0-" + MD5Utils.getMD5(strviewrtmp1);
String flvAuthKey = time + "-0-0-" + MD5Utils.getMD5(strviewflv1);
String m3u8AuthKey = time + "-0-0-" + MD5Utils.getMD5(strviewm3u81);
rtmpurl1 = "rtmp://" + pullDomain + "/" + appName + "/" + streamName + "?auth_key=" + rtmpAuthKey;
flvurl1 = "http://" + pullDomain + "/" + appName + "/" + streamName + ".flv?auth_key=" + flvAuthKey;
m3u8url1 = "http://" + pullDomain + "/" + appName + "/" + streamName + ".m3u8?auth_key=" + m3u8AuthKey;
return "rtm拉流:" + rtmpurl1 + "\n" + "flv拉流:" + flvurl1 + "\n" + "m3u8拉流:" + m3u8url1;
}
}
使用MD5加密生成推流和拉流鉴权串的工具代码如下:
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.UUID;
public class MD5Utils {
public static String getMD5(String str) {
try {
// 生成一个MD5加密计算摘要
MessageDigest md = MessageDigest.getInstance("MD5");
// 计算md5函数
md.update(str.getBytes());
// digest()最后确定返回md5 hash值,返回值为8为字符串。因为md5 hash值是16位的hex值,实际上就是8位的字符
// BigInteger函数则将8位的字符串转换成16位hex值,用字符串来表示;得到字符串形式的hash值
String md5 = new BigInteger(1, md.digest()).toString(16);
//BigInteger会把0省略掉,需补全至32位
return fillMD5(md5);
} catch (Exception e) {
throw new RuntimeException("MD5加密错误:" + e.getMessage(), e);
}
}
private static String fillMD5(String md5) {
return md5.length() == 32 ? md5 : fillMD5("0" + md5);
}
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
生成的推流地址的格式是rtmp,如:rtmp://push1.test.fukaimei.top/android/test123?auth_key=鉴权串信息,生成的拉流地址有三种格式,分别是rtmp、flv以及m3u8,如:
rtmp拉流:rtmp://live1.test.fukaimei.top/android/test123?auth_key=鉴权串信息
flv拉流:http://live1.test.fukaimei.top/android/test123.flv?auth_key=鉴权串信息
m3u8拉流:http://live1.test.fukaimei.top/android/test123.m3u8?auth_key=鉴权串信息
这三种拉流格式也是有区别的,rtmp拉流和flv拉流大概延时5秒左右,而m3u8拉流延时大概在30秒左右,如果在实际的直播拉流项目开发过程中,建议选择rtmp拉流或flv拉流比较好一点。
如果在实际的项目开发的过程中,推流和拉流鉴权地址生成最好写在后台服务端里,然后由服务端把推流和拉流地址返回给客户端调用。由于这里是Demo演示,所以我这边为了方便直接写在Android客户端里。
接着创建一个xml布局文件(activity_live_pusher.xml),该布局文件中的控件SurfaceView的作用就是用来实时渲染相机采集到画面信息。布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/preview_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="25dp"
android:layout_marginTop="25dp"
android:layout_marginEnd="25dp"
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/tv_push_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/black"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="25dp"
android:orientation="vertical"
android:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal"
android:visibility="visible">
<Button
android:id="@+id/btnStartPush"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:text="开始推流" />
<Button
android:id="@+id/btnSwitchCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="切换摄像头" />
<Button
android:id="@+id/btnStopPush"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:text="停止推流" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
推流逻辑代码如下:
public class LivePusherActivity extends BaseActivity {
public static LivePusherActivity instance;
private AlivcLivePushConfig mAlivcLivePushConfig;
private AlivcLivePusher mAlivcLivePusher = null;
private SurfaceStatus mSurfaceStatus = SurfaceStatus.UNINITED;
private boolean mAsync = false;
private boolean videoThreadOn = false;
private String pusherDirection;
@BindView(R.id.preview_view)
SurfaceView mPreviewView;
@BindView(R.id.tv_push_info)
TextView tv_push_info;
@Override
protected int getLayoutId() {
instance = this;
hiddenStatusBar();
pusherDirection = ShareUtils.getString(this, "PusherDirection", "landscape");
if ("landscape".equals(pusherDirection)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); // 横屏
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); // 竖屏
}
return R.layout.activity_live_pusher;
}
@Override
protected void initView() {
ButterKnife.bind(this);
}
@Override
protected void initData() {
// 初始化直播推流设置信息
initAlivcLivePushConfig();
}
private void initAlivcLivePushConfig() {
mPreviewView.getHolder().addCallback(mCallback);
String resolution = ShareUtils.getString(this, "Resolution", "540P");
mAlivcLivePushConfig = new AlivcLivePushConfig();
if ("landscape".equals(pusherDirection)) {
mAlivcLivePushConfig.setPreviewOrientation(AlivcPreviewOrientationEnum.ORIENTATION_LANDSCAPE_HOME_RIGHT); // 横屏推流
} else {
mAlivcLivePushConfig.setPreviewOrientation(AlivcPreviewOrientationEnum.ORIENTATION_PORTRAIT); // 竖屏推流
}
if (resolution.equals("180P")) {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_180P);
} else if (resolution.equals("240P")) {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_240P);
} else if (resolution.equals("360P")) {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_360P);
} else if (resolution.equals("480P")) {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_480P);
} else if (resolution.equals("540P")) {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_540P);
} else if (resolution.equals("720P")) {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_720P);
} else {
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_540P);
}
mAlivcLivePusher = new AlivcLivePusher();
try {
mAlivcLivePusher.init(getApplicationContext(), mAlivcLivePushConfig);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
SurfaceHolder.Callback mCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
if (mSurfaceStatus == SurfaceStatus.UNINITED) {
mSurfaceStatus = SurfaceStatus.CREATED;
if (mAlivcLivePusher != null) {
try {
if (mAsync) {
mAlivcLivePusher.startPreviewAysnc(mPreviewView);
} else {
mAlivcLivePusher.startPreview(mPreviewView);
}
if (mAlivcLivePushConfig.isExternMainStream()) {
startYUV(getApplicationContext());
}
} catch (IllegalArgumentException e) {
e.toString();
} catch (IllegalStateException e) {
e.toString();
}
}
} else if (mSurfaceStatus == SurfaceStatus.DESTROYED) {
mSurfaceStatus = SurfaceStatus.RECREATED;
}
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
mSurfaceStatus = SurfaceStatus.CHANGED;
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
mSurfaceStatus = SurfaceStatus.DESTROYED;
}
};
@OnClick({R.id.btnStartPush, R.id.btnStopPush, R.id.btnSwitchCamera})
public void onClick(View v) {
switch (v.getId()) {
// 开始推流
case R.id.btnStartPush:
if (!CheckNetwork.isNetworkConnected(this)) {
ToastUtil.showToastLong(getString(R.string.network_unavailable));
return;
}
startPush();
break;
// 停止推流
case R.id.btnStopPush:
stopPush();
break;
// 切换摄像头
case R.id.btnSwitchCamera:
switchCamera();
break;
default:
break;
}
}
public void startPush() {
String time = BaseTools.dateToStamp();
// 第一个参数传递流的名称(相对于直播时的房间号),第二个参数传递过期十位数的时间戳
String pushUrl = AliyunPushUtils.CreatePushUrl("test123", time);
// 传递的参数值必须和推流的一致,第一个参数传递流的名称(相对于直播时的房间号),第二个参数传递过期十位数的时间戳
String liveUrl = AliyunPushUtils.GetPlayUrl("test123", time);
L.i("获取推流地址:" + pushUrl);
L.i("获取拉流地址:\n" + liveUrl);
String pushInfo = "推流URL:" + pushUrl + "\n" + liveUrl;
tv_push_info.setText(String.format("推流和拉流地址:\n%s", pushInfo));
try {
mAlivcLivePusher.startPush(pushUrl);
} catch (Exception e) {
e.printStackTrace();
}
ToastUtil.showToast("已成功推流");
}
public void stopPush() {
try {
mAlivcLivePusher.stopPush();
} catch (Exception e) {
e.printStackTrace();
}
tv_push_info.setText("");
ToastUtil.showToast("已停止推流");
}
public void switchCamera() {
try {
mAlivcLivePusher.switchCamera();
} catch (Exception e) {
e.printStackTrace();
}
ToastUtil.showToast("已切换摄像头");
}
public void startYUV(final Context context) {
new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
private AtomicInteger atoInteger = new AtomicInteger(0);
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("LivePushActivity-readYUV-Thread" + atoInteger.getAndIncrement());
return t;
}
}).execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
videoThreadOn = true;
byte[] yuv;
InputStream myInput = null;
try {
File f = new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "alivc_resource/capture0.yuv");
myInput = new FileInputStream(f);
byte[] buffer = new byte[1280 * 720 * 3 / 2];
int length = myInput.read(buffer);
//发数据
while (length > 0 && videoThreadOn) {
mAlivcLivePusher.inputStreamVideoData(buffer, 720, 1280, 720, 1280 * 720 * 3 / 2, System.nanoTime() / 1000, 0);
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
//发数据
length = myInput.read(buffer);
if (length <= 0) {
myInput.close();
myInput = new FileInputStream(f);
length = myInput.read(buffer);
}
}
myInput.close();
videoThreadOn = false;
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
@Override
protected void onDestroy() {
if (mAlivcLivePusher != null) {
// 停止推流
mAlivcLivePusher.stopPush();
// 停止预览
try {
mAlivcLivePusher.stopPreview();
} catch (Exception e) {
e.printStackTrace();
}
// 释放推流
mAlivcLivePusher.destroy();
mAlivcLivePusher.setLivePushInfoListener(null);
mAlivcLivePusher = null;
}
super.onDestroy();
}
}
在推流的逻辑代码需要的接口说明如下图:
在推流代码中要调用阿里云推流的功能首先要初始化和创建AlivcLivePushConfig对象,并设置相关参数,参数可以设置推流分辨率、推流码率控制模式、视频帧率、是否打开美颜、美颜模式以及添加水印信息等功能。
创建AlivcLivePushConfig对象的代码如下:
mAlivcLivePushConfig = new AlivcLivePushConfig();
设置推流分辨率的代码如下:
mAlivcLivePushConfig.setResolution(AlivcResolutionEnum.RESOLUTION_540P);
阿里云推流分辨如果不设置的话默认是544x960,SDK支持的直播推流分辨率如下图所示:
接着当AlivcLivePushConfig对象创建成功并设置了初始化参数后,根据AlivcLivePushConfig对象来创建AlivcLivePusher对象,代码如下所示:
mAlivcLivePusher = new AlivcLivePusher();
try {
mAlivcLivePusher.init(getApplicationContext(), mAlivcLivePushConfig);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
当推流SDK初始成功之后,就是进入相机预览的环节,预览使用外部传入一个SurfaceView,调用startPreview接口后SDK内部会自动打开相机采集,并将采集画面渲染到预览 SurfaceView 上。代码如下所示:
if(mAsync) {
mAlivcLivePusher.startPreviewAysnc(mPreviewView);
} else {
mAlivcLivePusher.startPreview(mPreviewView);
}
当前相机预览成功后,就可以调用AlivcLivePusher的startPush接口开始进行推流操作,代码如下所示:
mAlivcLivePusher.startPush("推流地址");
停止推流代码:
mAlivcLivePusher.stopPush();
重新推流代码:
mAlivcLivePusher.restartPush();
在推流过程中,通过调用AlivcLivePusher的pause和resume接口来控制推流的暂停和恢复。代码如下所示:
暂停代码:
mAlivcLivePusher.pause();
恢复代码:
mAlivcLivePusher.resume();
除此之外,还可以调用AlivcLivePusher的switchCamera和
setFlash接口来设置切换相机和开关手电筒的功能,代码如下所示:
切换相机代码:
mAlivcLivePusher.switchCamera();
设置手电筒代码:
mAlivcLivePusher.setFlash(true);
界面运行效果图如下:
打开网址(http://www.cutv.com/demo/live_test.swf)输入三种拉流格式中的任意一个拉流地址观看测试推流是否成功,如果能观看到画面则是推流成功。画面如下:
可以扫描以下二维码进行下载安装,或者点击以下链接 http://app.fukaimei.top/livepushertest 进行下载安装体验。
———————— The end ————————
码字不易,如果您觉得这篇博客写的比较好的话,可以赞赏一杯咖啡吧~~
Demo程序源码下载地址一(GitHub)
Demo程序源码下载地址二(码云)