前 言
如今,在国内移动互联网发展了几年的时间,移动开发技术也相对的成熟,在咱们日常使用的手机App中也少不了直播的功能,不管是娱乐类、游戏类、体育类还是教育类等的App都会有直播的功能,可以说直播的功能在一些商业应用或者非商业应用中都是不可或缺的功能。目前国内比较火直播App有如:斗鱼(游戏直播)、YY直播(全民娱乐直播)、虎牙(游戏+电竞直播)以及映客(娱乐直播)等直播。
而要想在自己的Android应用中实现直播的功能,那么就少不了对目前市面上直播推流SDK做一个技术的选型(如果有条件的公司可以自己开发一个直播推流平台,就可以不用第三方推流SDK),由于之前我们公司的项目需要用到直播推流SDK,所以笔者对目前市场的各大直播推流SDK颇有了解。所以下面我就简单的介绍如何在Android里接入腾讯云推流SDK实现直播推流的功能。(P.S.如果想了解阿里云推流SDK实现直播推流的功能的话可以看笔者之前写的这篇博客:https://blog.csdn.net/fukaimei/article/details/103237654
)
接入推流SDK前的准备工作
-
开通腾讯云直播服务的功能
云直播的服务本质是一个广播的过程,类似于电视台的直播节目通过有线电视网发送给千家万户。为了完成这个过程,云直播需要有采集和推流设备(类似摄像头)、云直播服务(类似电视台的有线电视网)和播放设备(类似电视)。而采集和推流设备以及播放设备可以是手机、PC、Pad 等智能终端以及 Web 浏览器。
开通腾讯云直播服务地址如下:https://console.cloud.tencent.com/live/livestat?from=product-banner-use-lvb -
绑定推流和拉流的域名
绑定推流和拉流的域名的前提条件是自己的域名必须是已经备案好的域名才能绑定,然后选择 【域名管理】,单击【添加域名】添加您已备案的推流域名,详细请参见 添加自有域名。 -
申请移动直播 SDK License 的 Key 值和 LicenseUrl 值
SDK License 的 Key 值和 LicenseUrl 值是在 SDK 初始化时会使用到。详细申请流程请参见 License 使用指南。
开始接入推流SDK
首先将下载的腾讯云 Android 推流 SDK 相关的 aar 文件拷贝到 AS 工程的 libs 目录下,如下图:
然后在AS的内层build.gradle里添加推流相关的依赖
android {
........
defaultConfig {
........
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
sourceSets {
main {
jniLibs.srcDir 'libs'
}
}
repositories {
flatDir {
dirs 'libs'
}
}
}
dependencies {
........
implementation(name: 'LiteAVSDK_Smart', ext: 'aar')
}
最后在清单文件AndroidManifest.xml中添加直播推流所需的相应权限
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_LOGS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
<uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
注意:直播推流在 Android 6.0 以上在相机界面预览前和发起直播推流之前必须在 Java 代码或者 Kotlin 代码里动态授予相机权限(Manifest.permission.CAMERA)和麦克风权限(Manifest.permission.RECORD_AUDIO),否则应用将会闪退。
编码实现
在编码实现的这一环节,可以直接查看 腾讯云推流开发文档 来接入,也可以下载他们提供的Demo源码来接入。
- 添加 Layout 布局
为了能够展示播放器的视频画面,我们第一步要做的就是在布局 xml 文件里加入如下一段代码:
<com.tencent.rtmp.ui.TXCloudVideoView
android:id="@+id/pusher_tx_cloud_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 初始化 SDK 并创建 Player
// 设置 SDK License
String licenceURL = ShareUtils.getString(this, "LicenceURL", "");
String licenceKey = ShareUtils.getString(this, "LicenceKey", "");
TXLiveBase.getInstance().setLicence(this, licenceURL, licenceKey);
mLivePushConfig = new TXLivePushConfig();
// 允许双指手势放大预览画面
mLivePushConfig.setEnableZoom(true);
// 设置噪声抑制
mLivePushConfig.enableAEC(true);
// 开启硬件加速
mLivePushConfig.setHardwareAcceleration(TXLiveConstants.ENCODE_VIDEO_HARDWARE);
// 开启 MainProfile 硬编码模式
mLivePushConfig.enableVideoHardEncoderMainProfile(true);
mLivePusher = new TXLivePusher(this);
mLivePusher.setConfig(mLivePushConfig);
mLivePusher.startCameraPreview(mPusherView);
视频云 SDK 中的 TXLivePlayer 模块负责实现直播播放功能,并使用 setPlayerView 接口将它与我们刚刚添加到界面上的 pusher_tx_cloud_view 控件进行关联。
- 获取推拉流拼接地址
获取推拉流拼接地址=推拉流域名+房间号+加密信息及有效时间戳的鉴权串信息,生成的推拉推工具代码如下:
public class PushUrlToken {
public static String getUrlToken() {
String pushKey = ShareUtils.getString(PushApplication.getInstance(), "PushKey", "");
String urlNO = ShareUtils.getString(PushApplication.getInstance(), "UrlNO", "");
String urlToken = getSafeUrl(pushKey, urlNO, dateToStamp());
return urlToken;
}
private static final char[] DIGITS_LOWER =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static String getSafeUrl(String key, String streamName, long txTime) {
String input = new StringBuilder().
append(key).
append(streamName).
append(Long.toHexString(txTime).toUpperCase()).toString();
String txSecret = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
txSecret = byteArrayToHexString(
messageDigest.digest(input.getBytes("UTF-8")));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return txSecret == null ? "" :
new StringBuilder().
append("txSecret=").
append(txSecret).
append("&").
append("txTime=").
append(Long.toHexString(txTime).toUpperCase()).
toString();
}
private static String byteArrayToHexString(byte[] data) {
char[] out = new char[data.length << 1];
for (int i = 0, j = 0; i < data.length; i++) {
out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];
out[j++] = DIGITS_LOWER[0x0F & data[i]];
}
return new String(out);
}
private static long dateToStamp() {
Long time = System.currentTimeMillis();
time += 30 * 1000 * 60;
String res = "";
try {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = simpleDateFormat.parse(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(time));
long ts = date.getTime();
ts = ts / 1000;
res = String.valueOf(ts);
} catch (Exception e) {
e.printStackTrace();
}
return Long.valueOf(res).longValue();
}
}
- 开始直播推流
private void startPush() {
if (isPush) {
mLivePusher = new TXLivePusher(this);
mLivePusher.setConfig(mLivePushConfig);
mLivePusher.startCameraPreview(mPusherView);
}
int ret = mLivePusher.startPusher(pushUrl.trim());
L.i("ret:" + ret);
if (ret == -5) {
L.i("startRTMPPush: license 校验失败");
ToastUtil.showToastLong("startRTMPPush: license 校验失败");
}
}
在开始直播推流之前首先先校验 SDK 是否已经初始化了,接入校验 SDK License 是否有效,如果 mLivePusher.startPusher(pushUrl.trim()) 的返回值等于 -5 的话说明 License 校验失败,这时就到腾讯云直播平台检查 License 是否已经过期或者配置 Android 包名是否与自己在 AS 创建项目的包名是否一致。其中的 pushUrl 是直播的推流地址,直播的推流地址格式可以是 RTMP、FLV 或者 m3u8 格式,如果对直播推流时延要求比较高的话建议使用前两种格式。
- 停止直播推流
private void stopPush() {
mLivePusher.stopPusher();
// 如果已经启动了摄像头预览,请在结束推流时将其关闭
mLivePusher.stopCameraPreview(true);
isPush = true;
}
停止直播推流调用了 TXLivePusher 的 stopPusher() 方法,如果当前有相机画面在预览的话则要调用 TXLivePusher 的 stopCameraPreview(true) 方法进行关闭。
- 切换直播推流前后置摄像头
private void switchCamera() {
mLivePusher.switchCamera();
}
切换前后置摄像头比较简单,只需一行代码调用 TXLivePusher 的 switchCamera() 方法就可以实现。
- 直播横屏或者竖屏推流
private void onOrientationChange(boolean isPortrait) {
if (isPortrait) {
mLivePushConfig.setHomeOrientation(TXLiveConstants.VIDEO_ANGLE_HOME_DOWN);
mLivePusher.setConfig(mLivePushConfig);
mLivePusher.setRenderRotation(0);
} else {
mLivePushConfig.setHomeOrientation(TXLiveConstants.VIDEO_ANGLE_HOME_RIGHT);
mLivePusher.setConfig(mLivePushConfig);
// 因为采集旋转了,为了保证本地渲染是正的,则设置渲染角度为90度。
mLivePusher.setRenderRotation(90);
}
}
直播横屏或者竖屏推流采集到的画面预览在主播端可能看不到效果,但在观众端就能看到效果,比如主播设置为横屏推流时,那么观众端看到的画面是主播端看到的画面旋转 90 度。
- 推流逻辑完整代码如下:
public class CameraPusherActivity extends BaseActivity {
@BindView(R.id.pusher_tx_cloud_view)
TXCloudVideoView mPusherView;
@BindView(R.id.tv_play_url)
TextView tvPlayUrl;
private TXLivePusher mLivePusher;
private TXLivePushConfig mLivePushConfig;
// 推流url
private String bsePushUrl;
private String pushUrl;
// 拉流url
private String basePlayUrl;
private String playUrl;
private String urlNO;
// 鉴权串信息
private String urlToken;
// 是否已经推流
private boolean isPush;
@Override
protected int getLayoutId() {
return R.layout.activity_camera_pusher;
}
@Override
protected void initView() {
ButterKnife.bind(this);
// 如何获取License? 请参考官网指引 https://cloud.tencent.com/document/product/454/34750
String licenceURL = ShareUtils.getString(this, "LicenceURL", "");
String licenceKey = ShareUtils.getString(this, "LicenceKey", "");
TXLiveBase.getInstance().setLicence(this, licenceURL, licenceKey);
mLivePushConfig = new TXLivePushConfig();
// 允许双指手势放大预览画面
mLivePushConfig.setEnableZoom(true);
// 设置噪声抑制
mLivePushConfig.enableAEC(true);
// 开启硬件加速
mLivePushConfig.setHardwareAcceleration(TXLiveConstants.ENCODE_VIDEO_HARDWARE);
// 开启 MainProfile 硬编码模式
mLivePushConfig.enableVideoHardEncoderMainProfile(true);
mLivePusher = new TXLivePusher(this);
mLivePusher.setConfig(mLivePushConfig);
mLivePusher.startCameraPreview(mPusherView);
isPush = false;
}
@Override
protected void initData() {
bsePushUrl = ShareUtils.getString(this, "PushDomain", "");
basePlayUrl = ShareUtils.getString(this, "LiveDomain", "");
urlNO = ShareUtils.getString(this, "UrlNO", "");
urlToken = PushUrlToken.getUrlToken();
pushUrl = bsePushUrl + urlNO + "?" + urlToken;
playUrl = basePlayUrl + urlNO + "?" + urlToken;
L.i("拉流地址:" + playUrl);
// ToastUtil.showToastLong("拉流地址:" + playUrl);
}
private void startPush() {
if (isPush) {
mLivePusher = new TXLivePusher(this);
mLivePusher.setConfig(mLivePushConfig);
mLivePusher.startCameraPreview(mPusherView);
}
int ret = mLivePusher.startPusher(pushUrl.trim());
L.i("ret:" + ret);
if (ret == -5) {
L.i("startRTMPPush: license 校验失败");
ToastUtil.showToastLong("startRTMPPush: license 校验失败");
}
tvPlayUrl.setText(String.format("拉流地址:%s%s", basePlayUrl, urlNO));
L.i(String.format("拉流地址:%s%s", basePlayUrl, urlNO));
}
private void stopPush() {
mLivePusher.stopPusher();
// 如果已经启动了摄像头预览,请在结束推流时将其关闭
mLivePusher.stopCameraPreview(true);
tvPlayUrl.setText("");
isPush = true;
ToastUtil.showToast("已停止推流");
}
private void switchCamera() {
mLivePusher.switchCamera();
ToastUtil.showToast("已切换摄像头");
}
private void onOrientationChange(boolean isPortrait) {
if (isPortrait) {
mLivePushConfig.setHomeOrientation(TXLiveConstants.VIDEO_ANGLE_HOME_DOWN);
mLivePusher.setConfig(mLivePushConfig);
mLivePusher.setRenderRotation(0);
} else {
mLivePushConfig.setHomeOrientation(TXLiveConstants.VIDEO_ANGLE_HOME_RIGHT);
mLivePusher.setConfig(mLivePushConfig);
// 因为采集旋转了,为了保证本地渲染是正的,则设置渲染角度为90度。
mLivePusher.setRenderRotation(90);
}
}
@OnClick({R.id.btn_start_push, R.id.btn_stop_push, R.id.btn_switch_camera
, R.id.btn_horizontal, R.id.btn_portrait})
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_start_push:
ToastUtil.showToast("开始推流");
startPush();
break;
case R.id.btn_stop_push:
stopPush();
break;
case R.id.btn_switch_camera:
switchCamera();
break;
case R.id.btn_horizontal:
// 横屏推流
onOrientationChange(false);
ToastUtil.showToast("已横屏推流");
break;
case R.id.btn_portrait:
// 竖屏推流
onOrientationChange(true);
ToastUtil.showToast("已竖屏推流");
break;
default:
break;
}
}
@Override
protected void onDestroy() {
stopPush();
ToastUtil.showToast("已停止推流");
super.onDestroy();
}
}
界面运行效果图如下:
打开网址(http://www.cutv.com/demo/live_test.swf)输入拉流地址观看测试推流是否成功,如果能观看到画面则是推流成功。画面如下:
apk安装包下载体验地址:
可以扫描以下二维码进行下载安装,或者点击以下链接 http://app.fukaimei.top/tcpush 进行下载安装体验。
———————— The end ————————
码字不易,如果您觉得这篇博客写的比较好的话,可以赞赏一杯咖啡吧~~
Demo程序源码下载地址一(GitHub)
Demo程序源码下载地址二(码云)