前 言
如今,在國內移動互聯網發展了幾年的時間,移動開發技術也相對的成熟,在咱們日常使用的手機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'};
/*
* KEY+ streamName + txTime
*/
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);
}
/**
* 十位數的時間戳
*
* @return
* @throws ParseException
*/
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() 方法就可以實現。
- 直播橫屏或者豎屏推流
/**
* 橫豎屏推流切換
*
* @param isPortrait
*/
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("已切換攝像頭");
}
/**
* 橫豎屏推流切換
*
* @param isPortrait
*/
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 ————————
碼字不易,如果您覺得這篇博客寫的比較好的話,可以讚賞一杯咖啡吧~~