集成完直播后提交代码
This commit is contained in:
22
LiveCommon/live_commonbiz/src/main/AndroidManifest.xml
Normal file
22
LiveCommon/live_commonbiz/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.alivc.live.commonbiz">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.alivc.live.commonbiz.backdoor.BackDoorActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:host="backdoor"
|
||||
android:scheme="livepush" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.alivc.live.commonbiz;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* @note 该类为读取本地音视频流的实现类,用于外部音视频推流
|
||||
* 注意:为了适配本地不同格式的音视频流,需要传入对应的profiles才能正常播放
|
||||
* 如:当前项目中,capture0.yuv为720p,请注意调节demo配置页中的分辨率为720p
|
||||
*/
|
||||
public class LocalStreamReader {
|
||||
|
||||
private static final String TAG = "LocalStreamReader";
|
||||
|
||||
private boolean videoThreadOn = false;
|
||||
private boolean audioThreadOn = false;
|
||||
private int videoWidth;
|
||||
private int videoHeight;
|
||||
private int videoStride;
|
||||
private int videoSize;
|
||||
private int videoRotation;
|
||||
private int audioSampleRate;
|
||||
private int audioChannel;
|
||||
private int audioBufferSize;
|
||||
|
||||
public static final class Builder {
|
||||
private final LocalStreamReader localStreamReader = new LocalStreamReader();
|
||||
|
||||
public Builder setVideoWith(int videoWith) {
|
||||
localStreamReader.videoWidth = videoWith;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVideoHeight(int videoHeight) {
|
||||
localStreamReader.videoHeight = videoHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVideoSize(int videoSize) {
|
||||
localStreamReader.videoSize = videoSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVideoStride(int videoStride) {
|
||||
localStreamReader.videoStride = videoStride;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setVideoRotation(int videoRotation) {
|
||||
localStreamReader.videoRotation = videoRotation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAudioSampleRate(int audioSampleRate) {
|
||||
localStreamReader.audioSampleRate = audioSampleRate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAudioChannel(int audioChannel) {
|
||||
localStreamReader.audioChannel = audioChannel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAudioBufferSize(int audioBufferSize) {
|
||||
localStreamReader.audioBufferSize = audioBufferSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LocalStreamReader build() {
|
||||
return localStreamReader;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void readYUVData(File f, ReadYUVFileListener listener) {
|
||||
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;
|
||||
InputStream myInput;
|
||||
try {
|
||||
myInput = new FileInputStream(f);
|
||||
byte[] buffer = new byte[videoSize];
|
||||
int length = myInput.read(buffer);
|
||||
//发数据
|
||||
while (length > 0 && videoThreadOn) {
|
||||
long pts = System.currentTimeMillis() * 1000;
|
||||
listener.onVideoStreamData(buffer, pts, videoWidth, videoHeight, videoStride, videoSize, videoRotation);
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void readPCMData(File f, ReadPCMFileListener listener) {
|
||||
new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
|
||||
private AtomicInteger atoInteger = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("LivePushActivity-readPCM-Thread" + atoInteger.getAndIncrement());
|
||||
return t;
|
||||
}
|
||||
}).execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
audioThreadOn = true;
|
||||
int allSended = 0;
|
||||
int sizePerSecond = audioSampleRate * 2;
|
||||
InputStream myInput;
|
||||
long startPts = System.nanoTime() / 1000;
|
||||
try {
|
||||
myInput = new FileInputStream(f);
|
||||
byte[] buffer = new byte[audioBufferSize];
|
||||
int length = myInput.read(buffer, 0, audioBufferSize);
|
||||
if (audioSampleRate <= 0) {
|
||||
Log.e(TAG, "audio sample rate error cause return: " + audioSampleRate);
|
||||
return;
|
||||
}
|
||||
double sleep_time = 1000 / (audioSampleRate / 1000);
|
||||
while (length > 0 && audioThreadOn) {
|
||||
long start = System.nanoTime();
|
||||
long pts = System.currentTimeMillis() * 1000;
|
||||
listener.onAudioStreamData(buffer, length, pts, audioSampleRate, audioChannel);
|
||||
allSended += length;
|
||||
if ((allSended * 1000000L / sizePerSecond - 50000) > (pts - startPts)) {
|
||||
try {
|
||||
Thread.sleep(45);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
length = myInput.read(buffer);
|
||||
if (length < audioBufferSize) {
|
||||
myInput.close();
|
||||
myInput = new FileInputStream(f);
|
||||
length = myInput.read(buffer);
|
||||
}
|
||||
long end = System.nanoTime();
|
||||
try {
|
||||
long real_sleep_time = (long) (sleep_time - (end - start) / 1000 / 1000);
|
||||
if (real_sleep_time > 0) {
|
||||
Thread.sleep(real_sleep_time);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
myInput.close();
|
||||
audioThreadOn = false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "audio IOException: " + e.getLocalizedMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopYUV() {
|
||||
videoThreadOn = false;
|
||||
}
|
||||
|
||||
public void stopPcm() {
|
||||
audioThreadOn = false;
|
||||
}
|
||||
|
||||
public interface ReadYUVFileListener {
|
||||
void onVideoStreamData(byte[] buffer, long pts, int videoWidth, int videoHeight, int videoStride, int videoSize, int videoRotation);
|
||||
}
|
||||
|
||||
public interface ReadPCMFileListener {
|
||||
void onAudioStreamData(byte[] buffer, int length, long pts, int audioSampleRate, int audioChannel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.alivc.live.commonbiz;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class ResourcesConst {
|
||||
|
||||
private ResourcesConst() {
|
||||
}
|
||||
|
||||
/**
|
||||
* capture0.yuv 文件本地保存路径
|
||||
*/
|
||||
public static File localCaptureYUVFilePath(Context context) {
|
||||
return new File(context.getApplicationContext().getExternalFilesDir(null), "capture0.yuv");
|
||||
}
|
||||
|
||||
/**
|
||||
* 441.pcm 文件本地保存路径
|
||||
*/
|
||||
public static File localCapturePCMFilePath(Context context) {
|
||||
return new File(context.getFilesDir().getPath() + File.separator + "alivc_resource/441.pcm");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.alivc.live.commonbiz;
|
||||
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.alivc.live.commonutils.DownloadUtil;
|
||||
import com.alivc.live.commonutils.HttpUtils;
|
||||
import com.alivc.live.commonutils.FileUtil;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import okhttp3.Response;
|
||||
|
||||
/**
|
||||
* 远端资源下载管理类
|
||||
*
|
||||
* @note 用于下载存放在云端的yuv文件,该yuv文件在外部音视频推流时使用
|
||||
*/
|
||||
public class ResourcesDownload {
|
||||
|
||||
private static final String TAG = "ResourcesDownload";
|
||||
|
||||
private static final String BASIC_YUV_OSS_URL = "https://alivc-demo-cms.alicdn.com/versionProduct/resources/livePush/capture0.yuv";
|
||||
private static final String GLOBAL_YUV_OSS_URL = "https://alivc-demo-cms.alicdn.com/versionProduct/resources/livePush/capture0_SG.yuv";
|
||||
private static final String FINAL_OSS_YUV_URL = BASIC_YUV_OSS_URL;
|
||||
|
||||
private static final Handler mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private ResourcesDownload() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载yuv文件
|
||||
*/
|
||||
public static long downloadCaptureYUV(Context context, DownloadUtil.OnDownloadListener listener) {
|
||||
DownloadUtil downloadUtil = new DownloadUtil(context.getApplicationContext());
|
||||
File file = ResourcesConst.localCaptureYUVFilePath(context);
|
||||
Log.d(TAG, "downloadYUV file : " + file.getAbsolutePath());
|
||||
|
||||
downloadUtil.setDownloadListener(new DownloadUtil.OnDownloadListener() {
|
||||
@Override
|
||||
public void onDownloadSuccess(long downloadId) {
|
||||
Log.d(TAG, "onDownloadSuccess : " + downloadId);
|
||||
verifyFile(downloadUtil, file, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadProgress(long downloadId, double percent) {
|
||||
Log.i(TAG, "onDownloadProgress : " + downloadId + ", " + percent);
|
||||
if (listener != null) {
|
||||
mHandler.post(() -> listener.onDownloadProgress(downloadId, percent));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadError(long downloadId, int errorCode, String errorMsg) {
|
||||
Log.e(TAG, "onDownloadError : " + downloadId + ", " + errorCode + ", " + errorMsg);
|
||||
if (listener != null) {
|
||||
mHandler.post(() -> listener.onDownloadError(downloadId, errorCode, errorMsg));
|
||||
}
|
||||
FileUtil.safeDeleteFile(file);
|
||||
}
|
||||
});
|
||||
|
||||
if (file.exists()) {
|
||||
//文件存在,验证完整性
|
||||
verifyFile(downloadUtil, file, listener);
|
||||
return -1;
|
||||
} else {
|
||||
return downloadUtil.startDownload(FINAL_OSS_YUV_URL, file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文件完整性
|
||||
* 通过 Response 中 etag 字段获取 OSS 文件的 MD5
|
||||
*/
|
||||
private static void verifyFile(DownloadUtil downloadUtil, File file, DownloadUtil.OnDownloadListener listener) {
|
||||
HttpUtils.get(FINAL_OSS_YUV_URL, new HttpUtils.OnNetWorkListener() {
|
||||
@Override
|
||||
public void onSuccess(Object obj) {
|
||||
Response response = (Response) obj;
|
||||
|
||||
// 1、通过oss url获取etag,和本地文件的md5进行比对,是否一致
|
||||
String responseMD5 = "";
|
||||
try {
|
||||
responseMD5 = response.header("etag").replace("\"", "");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (!TextUtils.isEmpty(responseMD5)) {
|
||||
String fileMD5 = FileUtil.getFileMD5(file).trim();
|
||||
if (responseMD5.equalsIgnoreCase(fileMD5)) {
|
||||
if (listener != null) {
|
||||
mHandler.post(() -> listener.onDownloadSuccess(-1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2、有时候etag是个非法值,并不代表文件的实际md5(坑),那么直接对比文件的大小是否一致
|
||||
long serverFileSize = -1;
|
||||
try {
|
||||
serverFileSize = Long.parseLong(response.header("Content-Length").trim());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (file != null && serverFileSize == file.length()) {
|
||||
if (listener != null) {
|
||||
mHandler.post(() -> listener.onDownloadSuccess(-1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
FileUtil.safeDeleteFile(file);
|
||||
downloadUtil.startDownload(FINAL_OSS_YUV_URL, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(int code, String msg) {
|
||||
if (listener != null) {
|
||||
mHandler.post(() -> listener.onDownloadError(-1, DownloadManager.ERROR_HTTP_DATA_ERROR, "code=" + code + ",msg=" + msg));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError() {
|
||||
if (listener != null) {
|
||||
mHandler.post(() -> listener.onDownloadError(-1, DownloadManager.ERROR_HTTP_DATA_ERROR, "NetWork error"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.alivc.live.commonbiz;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.alivc.live.commonutils.SharedPrefUtils;
|
||||
|
||||
/**
|
||||
* 推流Demo业务SharedPreference
|
||||
*/
|
||||
public class SharedPreferenceUtils {
|
||||
private static final String AUTO_FOCUS = "autofocus";
|
||||
private static final String PREVIEW_MIRROR = "previewmirror";
|
||||
private static final String PUSH_MIRROR = "pushmirror";
|
||||
private static final String TARGET_BIT = "target_bit";
|
||||
private static final String MIN_BIT = "min_bit";
|
||||
private static final String SHOW_GUIDE = "guide";
|
||||
private static final String BEAUTY_ON = "beautyon";
|
||||
private static final String HINT_TARGET_BIT = "hint_target_bit";
|
||||
private static final String HINT_MIN_BIT = "hint_min_bit";
|
||||
private static final String DISPLAY_FIT = "display_fit";
|
||||
private static final String APP_ID = "app_id";
|
||||
private static final String APP_KEY = "app_key";
|
||||
private static final String PLAY_DOMAIN = "play_domain";
|
||||
|
||||
private static final String BARE_STREAM = "bare_stream";
|
||||
private static final String USE_MULTI_PK_16IN = "useMultiPK16IN";
|
||||
private static final String FORCE_RTC_PRE_ENVIRONMENT = "force_rtc_pre_environment";
|
||||
|
||||
public static void setPreviewMirror(@NonNull Context context, boolean previewMirror) {
|
||||
SharedPrefUtils.saveData(context, PREVIEW_MIRROR, previewMirror);
|
||||
}
|
||||
|
||||
public static boolean isPreviewMirror(@NonNull Context context, boolean defaultValue) {
|
||||
return SharedPrefUtils.getBooleanData(context, PREVIEW_MIRROR, defaultValue);
|
||||
}
|
||||
|
||||
public static void setPushMirror(@NonNull Context context, boolean pushMirror) {
|
||||
SharedPrefUtils.saveData(context, PUSH_MIRROR, pushMirror);
|
||||
}
|
||||
|
||||
public static boolean isPushMirror(@NonNull Context context, boolean defaultValue) {
|
||||
return SharedPrefUtils.getBooleanData(context, PUSH_MIRROR, defaultValue);
|
||||
}
|
||||
|
||||
public static void setAutofocus(@NonNull Context context, boolean autofocus) {
|
||||
SharedPrefUtils.saveData(context, AUTO_FOCUS, autofocus);
|
||||
}
|
||||
|
||||
public static boolean isAutoFocus(@NonNull Context context, boolean defaultValue) {
|
||||
return SharedPrefUtils.getBooleanData(context, AUTO_FOCUS, defaultValue);
|
||||
}
|
||||
|
||||
public static void setTargetBit(@NonNull Context context, int target) {
|
||||
SharedPrefUtils.saveData(context, TARGET_BIT, target);
|
||||
}
|
||||
|
||||
public static int getTargetBit(@NonNull Context context) {
|
||||
return SharedPrefUtils.getIntData(context, TARGET_BIT, 0);
|
||||
}
|
||||
|
||||
public static void setMinBit(@NonNull Context context, int min) {
|
||||
SharedPrefUtils.saveData(context, MIN_BIT, min);
|
||||
}
|
||||
|
||||
public static int getMinBit(@NonNull Context context) {
|
||||
return SharedPrefUtils.getIntData(context, MIN_BIT, 0);
|
||||
}
|
||||
|
||||
public static void setHintTargetBit(@NonNull Context context, int hintTarget) {
|
||||
SharedPrefUtils.saveData(context, HINT_TARGET_BIT, hintTarget);
|
||||
}
|
||||
|
||||
public static int getHintTargetBit(@NonNull Context context) {
|
||||
return SharedPrefUtils.getIntData(context, HINT_TARGET_BIT, 0);
|
||||
}
|
||||
|
||||
public static void setHintMinBit(@NonNull Context context, int hintMin) {
|
||||
SharedPrefUtils.saveData(context, HINT_MIN_BIT, hintMin);
|
||||
}
|
||||
|
||||
public static int getHintMinBit(@NonNull Context context) {
|
||||
return SharedPrefUtils.getIntData(context, HINT_MIN_BIT, 0);
|
||||
}
|
||||
|
||||
public static void setGuide(@NonNull Context context, boolean guide) {
|
||||
SharedPrefUtils.saveData(context, SHOW_GUIDE, guide);
|
||||
}
|
||||
|
||||
public static boolean isGuide(@NonNull Context context) {
|
||||
return SharedPrefUtils.getBooleanData(context, SHOW_GUIDE, true);
|
||||
}
|
||||
|
||||
public static void setBeautyOn(@NonNull Context context, boolean beautyOn) {
|
||||
SharedPrefUtils.saveData(context, BEAUTY_ON, beautyOn);
|
||||
}
|
||||
|
||||
public static boolean isBeautyOn(@NonNull Context context) {
|
||||
return SharedPrefUtils.getBooleanData(context, BEAUTY_ON, true);
|
||||
}
|
||||
|
||||
public static void setDisplayFit(@NonNull Context context, int displayfit) {
|
||||
SharedPrefUtils.saveData(context, DISPLAY_FIT, displayfit);
|
||||
}
|
||||
|
||||
public static int getDisplayFit(@NonNull Context context, int defaultValue) {
|
||||
return SharedPrefUtils.getIntData(context, DISPLAY_FIT, defaultValue);
|
||||
}
|
||||
|
||||
public static void setAppInfo(@NonNull Context context, String appId, String appKey, String playDomain) {
|
||||
SharedPrefUtils.saveData(context, APP_ID, appId);
|
||||
SharedPrefUtils.saveData(context, APP_KEY, appKey);
|
||||
SharedPrefUtils.saveData(context, PLAY_DOMAIN, playDomain);
|
||||
}
|
||||
|
||||
public static String getAppId(@NonNull Context context) {
|
||||
return SharedPrefUtils.getStringData(context, APP_ID, "");
|
||||
}
|
||||
|
||||
public static String getAppKey(@NonNull Context context) {
|
||||
return SharedPrefUtils.getStringData(context, APP_KEY, "");
|
||||
}
|
||||
|
||||
public static String getPlayDomain(@NonNull Context context) {
|
||||
return SharedPrefUtils.getStringData(context, PLAY_DOMAIN, "");
|
||||
}
|
||||
|
||||
public static void setMultiPK16IN(@NonNull Context context, boolean value) {
|
||||
SharedPrefUtils.saveData(context, USE_MULTI_PK_16IN, value);
|
||||
}
|
||||
|
||||
public static boolean getMultiPK16IN(@NonNull Context context) {
|
||||
return SharedPrefUtils.getBooleanData(context, USE_MULTI_PK_16IN, false);
|
||||
}
|
||||
|
||||
public static void setForceRTCPreEnvironment(@NonNull Context context, boolean value) {
|
||||
SharedPrefUtils.saveData(context, FORCE_RTC_PRE_ENVIRONMENT, value);
|
||||
}
|
||||
|
||||
public static boolean getForceRTCPreEnvironment(@NonNull Context context) {
|
||||
return SharedPrefUtils.getBooleanData(context, FORCE_RTC_PRE_ENVIRONMENT, false);
|
||||
}
|
||||
|
||||
public static void clear(@NonNull Context context) {
|
||||
SharedPrefUtils.clear(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.alivc.live.commonbiz.backdoor;
|
||||
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.alivc.live.commonbiz.R;
|
||||
import com.alivc.live.commonbiz.SharedPreferenceUtils;
|
||||
import com.alivc.live.commonui.utils.StatusBarUtil;
|
||||
import com.alivc.live.commonui.widgets.LivePushTextSwitch;
|
||||
import com.alivc.live.commonutils.AppUtil;
|
||||
import com.alivc.live.commonutils.FileUtil;
|
||||
import com.alivc.live.commonutils.ToastUtils;
|
||||
import com.alivc.live.pusher.AlivcLiveBase;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/10/13
|
||||
* @brief
|
||||
*/
|
||||
public class BackDoorActivity extends AppCompatActivity {
|
||||
|
||||
private static final String SD_CARD_LOG_SUFFIX = "device-info.log";
|
||||
|
||||
private TextView mInfoTv;
|
||||
|
||||
private LivePushTextSwitch mForceRTCPreEnvSw;
|
||||
private LivePushTextSwitch mMultiPK16InSw;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
StatusBarUtil.translucent(this, Color.TRANSPARENT);
|
||||
|
||||
setContentView(R.layout.push_activity_backdoor);
|
||||
initViews();
|
||||
initData();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
mForceRTCPreEnvSw = findViewById(R.id.sw_force_rtc_pre_env);
|
||||
mForceRTCPreEnvSw.setTextViewText(getString(R.string.backdoor_rtc_force_pre_env));
|
||||
mForceRTCPreEnvSw.setOnSwitchToggleListener(isChecked -> {
|
||||
if (BackDoorInstance.getInstance().isForceRTCPreEnvironment() != isChecked) {
|
||||
BackDoorInstance.getInstance().setForceRTCPreEnvironment(isChecked);
|
||||
// 清除当前连麦配置信息
|
||||
SharedPreferenceUtils.setAppInfo(getApplicationContext(), "", "", "");
|
||||
// 退出APP
|
||||
killApp();
|
||||
}
|
||||
});
|
||||
|
||||
mMultiPK16InSw = findViewById(R.id.sw_multi_pk_16in);
|
||||
mMultiPK16InSw.setTextViewText(getString(R.string.backdoor_multi_pk_16in));
|
||||
mMultiPK16InSw.setOnSwitchToggleListener(isChecked -> {
|
||||
if (BackDoorInstance.getInstance().isUseMultiPK16IN() != isChecked) {
|
||||
BackDoorInstance.getInstance().setUseMultiPK16IN(isChecked);
|
||||
}
|
||||
});
|
||||
|
||||
mInfoTv = findViewById(R.id.info_tv);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(getDemoBuildInfo());
|
||||
sb.append("\n\n");
|
||||
sb.append(getSdkBuildInfo());
|
||||
sb.append("\n\n");
|
||||
sb.append(AppUtil.getBriefDeviceInfo());
|
||||
sb.append("\n\n");
|
||||
sb.append(AppUtil.getDeviceInfo());
|
||||
sb.append("\n\n");
|
||||
sb.append(AppUtil.getFullCPUInfo());
|
||||
sb.append("\n\n");
|
||||
sb.append(AppUtil.getOpenGLESInfo(this));
|
||||
sb.append("\n\n");
|
||||
mInfoTv.setText(sb.toString());
|
||||
mInfoTv.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
copyInfo2Clipboard();
|
||||
dumpInfo2SdCard();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
mForceRTCPreEnvSw.setSwitchChecked(BackDoorInstance.getInstance().isForceRTCPreEnvironment());
|
||||
mMultiPK16InSw.setSwitchChecked(BackDoorInstance.getInstance().isUseMultiPK16IN());
|
||||
}
|
||||
|
||||
private static String getDemoBuildInfo() {
|
||||
return String.format("%s%n%s%n%s%n%s",
|
||||
"Demo Build Type: " + com.alivc.live.commonbiz.BuildConfig.BUILD_TYPE,
|
||||
"Demo Build AIO: " + com.alivc.live.commonbiz.BuildConfig.MTL_BUILD_FOR_AIO,
|
||||
"Demo Build ID: " + com.alivc.live.commonbiz.BuildConfig.MTL_BUILD_ID,
|
||||
"Demo Build Timestamp: " + com.alivc.live.commonbiz.BuildConfig.MTL_BUILD_TIMESTAMP
|
||||
);
|
||||
}
|
||||
|
||||
private static String getSdkBuildInfo() {
|
||||
return String.format("%s%n%s%n%s%n%s%n%s%n%s",
|
||||
"SDK Version: " + AlivcLiveBase.getSDKVersion(),
|
||||
"SDK Build Type: " + com.alivc.live.pusher.BuildConfig.BUILD_TYPE,
|
||||
"SDK Build Interactive: " + com.alivc.live.pusher.BuildConfig.BUILD_INTERACTIVE,
|
||||
"SDK Pre Environment: " + com.alivc.live.pusher.BuildConfig.PUSH_SDK_PRE_ENV,
|
||||
"SDK Head Commit ID: " + com.alivc.live.pusher.BuildConfig.HEAD_COMMIT_ID,
|
||||
"SDK Build ID: " + com.alivc.live.pusher.BuildConfig.MTL_BUILD_ID,
|
||||
"SDK Build Timestamp: " + com.alivc.live.pusher.BuildConfig.MTL_BUILD_TIMESTAMP
|
||||
);
|
||||
}
|
||||
|
||||
private void copyInfo2Clipboard() {
|
||||
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (cm == null) {
|
||||
return;
|
||||
}
|
||||
cm.setText(mInfoTv.getText());
|
||||
ToastUtils.show("copy to clipboard success!");
|
||||
}
|
||||
|
||||
private void dumpInfo2SdCard() {
|
||||
removeOldSdCardLogs();
|
||||
String folderPath = getSdCardLogPath();
|
||||
String filename = String.format("%s-%s", new SimpleDateFormat("yyMMddHHmmss").format(new Date()), SD_CARD_LOG_SUFFIX);
|
||||
String finalPath = FileUtil.combinePaths(folderPath, filename);
|
||||
|
||||
FileWriter fr = null;
|
||||
try {
|
||||
fr = new FileWriter(finalPath);
|
||||
fr.write(mInfoTv.getText().toString());
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
fr.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeOldSdCardLogs() {
|
||||
String folderPath = getSdCardLogPath();
|
||||
ArrayList<File> files = FileUtil.listAllFiles(new File(folderPath), false);
|
||||
if (files != null && !files.isEmpty()) {
|
||||
for (File file : files) {
|
||||
String path = file.getAbsolutePath();
|
||||
if (path.endsWith(SD_CARD_LOG_SUFFIX)) {
|
||||
FileUtil.safeDeleteFile(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getSdCardLogPath() {
|
||||
String parentPath = getExternalFilesDir("").getAbsolutePath();
|
||||
String folderPath = FileUtil.combinePaths(parentPath, "Ali_RTS_Log");
|
||||
FileUtil.safeCreateFolder(folderPath);
|
||||
return folderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制退出APP
|
||||
*/
|
||||
private static void killApp() {
|
||||
try {
|
||||
Thread.sleep(2 * 1000L);
|
||||
android.os.Process.killProcess(android.os.Process.myPid());
|
||||
System.exit(0);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alivc.live.commonbiz.backdoor;
|
||||
|
||||
import com.alivc.live.commonbiz.SharedPreferenceUtils;
|
||||
import com.alivc.live.commonutils.ContextUtils;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/10/13
|
||||
* @brief
|
||||
*/
|
||||
public class BackDoorInstance {
|
||||
|
||||
private boolean forceRTCPreEnvironment;
|
||||
|
||||
private boolean useMultiPK16IN;
|
||||
|
||||
private BackDoorInstance() {
|
||||
forceRTCPreEnvironment = SharedPreferenceUtils.getForceRTCPreEnvironment(ContextUtils.getContext());
|
||||
useMultiPK16IN = SharedPreferenceUtils.getMultiPK16IN(ContextUtils.getContext());
|
||||
}
|
||||
|
||||
public static BackDoorInstance getInstance() {
|
||||
return Inner.instance;
|
||||
}
|
||||
|
||||
public boolean isForceRTCPreEnvironment() {
|
||||
return this.forceRTCPreEnvironment;
|
||||
}
|
||||
|
||||
public void setForceRTCPreEnvironment(boolean forceRTCPreEnvironment) {
|
||||
this.forceRTCPreEnvironment = forceRTCPreEnvironment;
|
||||
SharedPreferenceUtils.setForceRTCPreEnvironment(ContextUtils.getContext(), forceRTCPreEnvironment);
|
||||
}
|
||||
|
||||
public boolean isUseMultiPK16IN() {
|
||||
return this.useMultiPK16IN;
|
||||
}
|
||||
|
||||
public void setUseMultiPK16IN(boolean useMultiPK16IN) {
|
||||
this.useMultiPK16IN = useMultiPK16IN;
|
||||
SharedPreferenceUtils.setMultiPK16IN(ContextUtils.getContext(), useMultiPK16IN);
|
||||
}
|
||||
|
||||
private static class Inner {
|
||||
private static final BackDoorInstance instance = new BackDoorInstance();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.alivc.live.commonbiz.seidelay;
|
||||
|
||||
import com.alivc.live.commonbiz.seidelay.api.ISEIDelayEventListener;
|
||||
import com.alivc.live.commonbiz.seidelay.handle.SEIDelayProvider;
|
||||
import com.alivc.live.commonbiz.seidelay.handle.SEIDelayReceiver;
|
||||
import com.alivc.live.commonbiz.seidelay.time.SEIDelayTimeHandler;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public class SEIDelayManager {
|
||||
private SEIDelayProvider mProvider;
|
||||
private SEIDelayReceiver mReceiver;
|
||||
|
||||
public SEIDelayManager() {
|
||||
requestNTPTime();
|
||||
}
|
||||
|
||||
public void registerProvider(String src, ISEIDelayEventListener listener) {
|
||||
mProvider = new SEIDelayProvider();
|
||||
mProvider.addListener(src, listener);
|
||||
}
|
||||
|
||||
public void registerReceiver(ISEIDelayEventListener listener) {
|
||||
mReceiver = new SEIDelayReceiver();
|
||||
mReceiver.addListener("receiver", listener);
|
||||
}
|
||||
|
||||
public void receiveSEI(SEISourceType sourceType, String sei) {
|
||||
if (mReceiver != null) {
|
||||
mReceiver.receiveSEI(sourceType, sei);
|
||||
}
|
||||
}
|
||||
|
||||
private void requestNTPTime() {
|
||||
try {
|
||||
new Thread(SEIDelayTimeHandler::requestNTPTime).start();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (mProvider != null) {
|
||||
mProvider.destroy();
|
||||
mProvider = null;
|
||||
}
|
||||
if (mReceiver != null) {
|
||||
mReceiver.destroy();
|
||||
mReceiver = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.alivc.live.commonbiz.seidelay;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public enum SEISourceType {
|
||||
CDN,
|
||||
RTC
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alivc.live.commonbiz.seidelay.api;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public interface ISEIDelayEventListener {
|
||||
/**
|
||||
* SEI delay event callback
|
||||
*
|
||||
* @param src SEI delay event source
|
||||
* @param type SEI delay event type
|
||||
* @param msg SEI delay event message
|
||||
*/
|
||||
void onEvent(String src, String type, String msg);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.alivc.live.commonbiz.seidelay.api;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public abstract class ISEIDelayHandle {
|
||||
public final Map<String, ISEIDelayEventListener> listenerHashMap = new HashMap<>();
|
||||
|
||||
public void addListener(String src, ISEIDelayEventListener listener) {
|
||||
listenerHashMap.put(src, listener);
|
||||
}
|
||||
|
||||
public void removeListener(String src) {
|
||||
listenerHashMap.remove(src);
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
listenerHashMap.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.alivc.live.commonbiz.seidelay.data;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public class SEIDelayProtocol {
|
||||
|
||||
private static final String SEI_DELAY = "sei_delay";
|
||||
|
||||
private static final String TIMESTAMP = "tm";
|
||||
private static final String SRC = "src";
|
||||
|
||||
public String src = null;
|
||||
public long tm = -1L;
|
||||
|
||||
public SEIDelayProtocol(String src, long tm) {
|
||||
this.src = src;
|
||||
this.tm = tm;
|
||||
}
|
||||
|
||||
public SEIDelayProtocol(String json) {
|
||||
try {
|
||||
JSONObject jsonObject = new JSONObject(json);
|
||||
String seiDelay = jsonObject.getString(SEI_DELAY);
|
||||
|
||||
JSONObject jsonObject2 = new JSONObject(seiDelay);
|
||||
this.src = jsonObject2.getString(SRC);
|
||||
this.tm = jsonObject2.getLong(TIMESTAMP);
|
||||
} catch (JSONException e) {
|
||||
}
|
||||
}
|
||||
|
||||
private String generate() {
|
||||
JSONObject jsonObject1 = new JSONObject();
|
||||
try {
|
||||
jsonObject1.put(SRC, this.src);
|
||||
jsonObject1.put(TIMESTAMP, this.tm);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
JSONObject jsonObject2 = new JSONObject();
|
||||
try {
|
||||
jsonObject2.put(SEI_DELAY, jsonObject1.toString());
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return jsonObject2.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return generate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.alivc.live.commonbiz.seidelay.handle;
|
||||
|
||||
import com.alivc.live.commonbiz.seidelay.api.ISEIDelayEventListener;
|
||||
import com.alivc.live.commonbiz.seidelay.api.ISEIDelayHandle;
|
||||
import com.alivc.live.commonbiz.seidelay.data.SEIDelayProtocol;
|
||||
import com.alivc.live.commonbiz.seidelay.time.SEIDelayTimeHandler;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Handles SEIDelay tasks and events.
|
||||
* 对象 SEIDelayProvider 是一个处理 SEIDelay 任务和事件的类。
|
||||
* 它使用一个计划好的任务执行器来定期运行任务。
|
||||
* 当有监听器被添加或移除时,它会相应地启动或停止任务。
|
||||
* 这个类主要处理 SEI 延迟相关的逻辑。
|
||||
*
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public class SEIDelayProvider extends ISEIDelayHandle {
|
||||
private static final long SCHEDULED_EXECUTOR_SERVICE_PERIOD = 2 * 1000L;
|
||||
private static final int CORE_POOL_SIZE = 1;
|
||||
|
||||
private ScheduledThreadPoolExecutor mScheduledExecutorService;
|
||||
private volatile boolean mTaskRun = false;
|
||||
|
||||
@Override
|
||||
public void addListener(String src, ISEIDelayEventListener listener) {
|
||||
super.addListener(src, listener);
|
||||
if (!mTaskRun && !listenerHashMap.isEmpty()) {
|
||||
mTaskRun = true;
|
||||
startTask();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(String src) {
|
||||
super.removeListener(src);
|
||||
if (mTaskRun && listenerHashMap.isEmpty()) {
|
||||
mTaskRun = false;
|
||||
stopTask();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
stopTask();
|
||||
}
|
||||
|
||||
private void startTask() {
|
||||
stopTask();
|
||||
mScheduledExecutorService = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(CORE_POOL_SIZE);
|
||||
mScheduledExecutorService.scheduleAtFixedRate(this::provideSEI, 0, SCHEDULED_EXECUTOR_SERVICE_PERIOD, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void stopTask() {
|
||||
if (mScheduledExecutorService != null) {
|
||||
mScheduledExecutorService.shutdown();
|
||||
try {
|
||||
if (!mScheduledExecutorService.awaitTermination(1, TimeUnit.SECONDS)) {
|
||||
mScheduledExecutorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
mScheduledExecutorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
mScheduledExecutorService = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void provideSEI() {
|
||||
for (Map.Entry<String, ISEIDelayEventListener> entry : listenerHashMap.entrySet()) {
|
||||
ISEIDelayEventListener eventListener = entry.getValue();
|
||||
if (eventListener != null && SEIDelayTimeHandler.isNtpTimeUpdated()) {
|
||||
long ntpTimestamp = SEIDelayTimeHandler.getCurrentTimestamp();
|
||||
SEIDelayProtocol dataProtocol = new SEIDelayProtocol(entry.getKey(), ntpTimestamp);
|
||||
eventListener.onEvent(dataProtocol.src, dataProtocol.src, dataProtocol.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.alivc.live.commonbiz.seidelay.handle;
|
||||
|
||||
import com.alivc.live.commonbiz.seidelay.SEISourceType;
|
||||
import com.alivc.live.commonbiz.seidelay.api.ISEIDelayEventListener;
|
||||
import com.alivc.live.commonbiz.seidelay.api.ISEIDelayHandle;
|
||||
import com.alivc.live.commonbiz.seidelay.data.SEIDelayProtocol;
|
||||
import com.alivc.live.commonbiz.seidelay.time.SEIDelayTimeHandler;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SEIDelayReceiver handles the reception and processing of SEI data.
|
||||
* It notifies listeners with the delay information.
|
||||
*
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
public class SEIDelayReceiver extends ISEIDelayHandle {
|
||||
|
||||
private static final long INVALID_THRESH_HOLD = 1000 * 60L;
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives SEI data and processes it to calculate delay and notify listeners.
|
||||
*
|
||||
* @param sourceType The type of SEI source.
|
||||
* @param sei The SEI data.
|
||||
*/
|
||||
public void receiveSEI(SEISourceType sourceType, String sei) {
|
||||
if (listenerHashMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SEIDelayTimeHandler.isNtpTimeUpdated()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SEIDelayProtocol dataProtocol = new SEIDelayProtocol(sei);
|
||||
long ntpTimestamp = SEIDelayTimeHandler.getCurrentTimestamp();
|
||||
long delay = ntpTimestamp - dataProtocol.tm;
|
||||
if (delay > INVALID_THRESH_HOLD) {
|
||||
return;
|
||||
}
|
||||
|
||||
notifyListeners(dataProtocol, sourceType, delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all registered listeners with the delay information.
|
||||
*
|
||||
* @param dataProtocol The SEIDelayProtocol containing SEI data.
|
||||
* @param sourceType The type of SEI source.
|
||||
* @param delay The calculated delay.
|
||||
*/
|
||||
private void notifyListeners(SEIDelayProtocol dataProtocol, SEISourceType sourceType, long delay) {
|
||||
for (Map.Entry<String, ISEIDelayEventListener> entry : listenerHashMap.entrySet()) {
|
||||
ISEIDelayEventListener eventListener = entry.getValue();
|
||||
if (eventListener != null) {
|
||||
eventListener.onEvent(dataProtocol.src, sourceType.toString(), String.valueOf(delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.alivc.live.commonbiz.seidelay.time;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2023/12/5
|
||||
* @brief
|
||||
*/
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.net.ntp.NTPUDPClient;
|
||||
import org.apache.commons.net.ntp.TimeInfo;
|
||||
|
||||
import java.net.InetAddress;
|
||||
|
||||
public class SEIDelayTimeHandler {
|
||||
|
||||
private static final String TAG = "SEIDelayTimeHandler";
|
||||
private static final String NTP_HOST = "ntp.aliyun.com";
|
||||
|
||||
private static long NTP_DELAY = 0L;
|
||||
private static boolean NTP_TIME_UPDATED = false;
|
||||
|
||||
private SEIDelayTimeHandler() {
|
||||
}
|
||||
|
||||
public static void requestNTPTime() {
|
||||
NTPUDPClient client = new NTPUDPClient();
|
||||
client.setDefaultTimeout(5000);
|
||||
try {
|
||||
InetAddress ntpServerAddress = InetAddress.getByName(NTP_HOST);
|
||||
Log.i(TAG, "Connecting to NTP server: " + NTP_HOST);
|
||||
|
||||
TimeInfo timeInfo = client.getTime(ntpServerAddress);
|
||||
timeInfo.computeDetails();
|
||||
long returnTime = timeInfo.getMessage().getTransmitTimeStamp().getTime();
|
||||
long systemTime = System.currentTimeMillis();
|
||||
NTP_DELAY = returnTime - systemTime;
|
||||
NTP_TIME_UPDATED = true;
|
||||
Log.i(TAG, "Current time (from " + NTP_HOST + "): " + returnTime);
|
||||
Log.i(TAG, "Current time (from system): " + systemTime);
|
||||
Log.i(TAG, "Delay (NTP-system): " + NTP_DELAY + "ms");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isNtpTimeUpdated() {
|
||||
return NTP_TIME_UPDATED;
|
||||
}
|
||||
|
||||
public static long getCurrentTimestamp() {
|
||||
return System.currentTimeMillis() + NTP_DELAY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package com.alivc.live.commonbiz.test;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class AliLiveStreamURLUtil {
|
||||
|
||||
/**
|
||||
* 连麦推拉流地址的APPID/APPKEY/PLAY_DOMAIN
|
||||
*/
|
||||
public static final String APP_ID = PushDemoTestConstants.getTestInteractiveAppID();
|
||||
public static final String APP_KEY = PushDemoTestConstants.getTestInteractiveAppKey();
|
||||
public static final String PLAY_DOMAIN = PushDemoTestConstants.getTestInteractivePlayDomain();
|
||||
|
||||
/**
|
||||
* 连麦推拉流地址的RTMP/HTTP-FLV/ARTC协议头
|
||||
*/
|
||||
private static final String RTMP = "rtmp://";
|
||||
private static final String HTTP = "http://";
|
||||
private static final String FLV = ".flv";
|
||||
private static final String ARTC = "artc://";
|
||||
|
||||
/**
|
||||
* DOMAIN 固定字段。
|
||||
*/
|
||||
public static final String ALILIVE_INTERACTIVE_DOMAIN = "live.aliyun.com";
|
||||
|
||||
/**
|
||||
* 连麦推拉流地址的二级域名(Second-level domain)
|
||||
*/
|
||||
private static final String PULL_SLD = "play";
|
||||
private static final String PUSH_SLD = "push";
|
||||
|
||||
/**
|
||||
* 连麦推拉流地址的参数配置key
|
||||
*/
|
||||
public static final String BASIC_DOMAIN = "basicDomain";
|
||||
public static final String SDK_APP_ID = "sdkAppId";
|
||||
public static final String USER_ID = "userId";
|
||||
public static final String TIMESTAMP = "timestamp";
|
||||
public static final String TOKEN = "token";
|
||||
|
||||
private static final String APP_NAME = "live";
|
||||
|
||||
/**
|
||||
* 旁路混流地址用,纯音频--->audio,音视频--->camera
|
||||
*/
|
||||
private static final String STREAM_TASK_TYPE_CAMERA = "camera";
|
||||
private static final String STREAM_TASK_TYPE_AUDIO = "audio";
|
||||
|
||||
private AliLiveStreamURLUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 APPID/APPKEY/PLAY_DOMAIN 连麦配置信息,生成连麦推流地址
|
||||
* <p>
|
||||
* 需提前配置好以下连麦配置:
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_APPID}
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_APPKEY}
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_PLAY_DOMAIN}
|
||||
*
|
||||
* @param channelId 频道 ID
|
||||
* @param userId 用户 ID
|
||||
* @return 连麦推流地址
|
||||
*/
|
||||
public static String generateInteractivePushUrl(String channelId, String userId) {
|
||||
return generateInteractiveUrl(channelId, userId, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 APPID/APPKEY/PLAY_DOMAIN 连麦配置信息,生成连麦拉流地址
|
||||
* <p>
|
||||
* 需提前配置好以下连麦配置:
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_APPID}
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_APPKEY}
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_PLAY_DOMAIN}
|
||||
*
|
||||
* @param channelId 频道 ID
|
||||
* @param userId 用户 ID
|
||||
* @return 连麦实时拉流地址,RTC用户实时互通用
|
||||
*/
|
||||
public static String generateInteractivePullUrl(String channelId, String userId) {
|
||||
return generateInteractiveUrl(channelId, userId, false);
|
||||
}
|
||||
|
||||
private static String generateInteractiveUrl(String channelId, String userId, boolean isPush) {
|
||||
String sld = isPush ? PUSH_SLD : PULL_SLD;
|
||||
long timestamp = AliLiveUserSigGenerate.getTimesTamp();
|
||||
String token = AliLiveUserSigGenerate.createToken(APP_ID, APP_KEY, channelId, userId, timestamp);
|
||||
|
||||
return new StringBuilder(ARTC)
|
||||
.append(ALILIVE_INTERACTIVE_DOMAIN).append("/")
|
||||
.append(sld).append("/")
|
||||
.append(channelId)
|
||||
.append("?")
|
||||
.append(SDK_APP_ID).append("=").append(APP_ID).append("&")
|
||||
.append(USER_ID).append("=").append(userId).append("&")
|
||||
.append(TIMESTAMP).append("=").append(timestamp).append("&")
|
||||
.append(TOKEN).append("=").append(token)
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 APPID/APPKEY/PLAY_DOMAIN 连麦配置信息,生成普通观众的CDN拉流地址
|
||||
* <p>
|
||||
* 需提前配置好以下连麦配置:
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_APPID}
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_APPKEY}
|
||||
* {@link AliLiveUserSigGenerate#ALILIVE_PLAY_DOMAIN}
|
||||
*
|
||||
* @param channelId 频道 ID
|
||||
* @param userId 用户 ID
|
||||
* @return 旁路直播拉流地址,普通观众用
|
||||
*/
|
||||
public static String generateCDNPullUrl(String channelId, String userId, boolean isAudioOnly) {
|
||||
/**
|
||||
* 建议将rtmp换成http-flv的形式。理由如下:
|
||||
* 在阿里云点播控制台生成地址时,会同时生成RTMP与HTTP-FLV的地址,这两个协议里包含的数据内容是一致的,只是网络协议通道不一样。
|
||||
* <p>
|
||||
* HTTP协议是互联网主要协议,CDN、运营商、中间网络设备等链路中都对HTTP有很长时间的网络优化,
|
||||
* HTTP的默认80/443端口号也是常见白名单端口,不容易被禁用,而RTMP协议比较老,其默认端口号是1935有可能被防火墙等设备禁用,导致异常。
|
||||
* 因此在综合网络环境下,HTTP-FLV的稳定性、性能(卡顿、延时)会比RTMP更好。
|
||||
*/
|
||||
return generateCDNFlvPullUrl(channelId, userId, isAudioOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse url into auth info
|
||||
*
|
||||
* @param url url
|
||||
* @return url params
|
||||
*/
|
||||
public static HashMap<String, String> parseUrl(String url) {
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
return map;
|
||||
}
|
||||
|
||||
String[] urlParts = url.trim().split("\\?");
|
||||
map.put(BASIC_DOMAIN, urlParts[0]);
|
||||
if (urlParts.length == 1) {
|
||||
return map;
|
||||
}
|
||||
|
||||
String[] params = urlParts[1].split("&");
|
||||
for (String param : params) {
|
||||
String[] keyValue = param.split("=");
|
||||
if (keyValue.length < 2) {
|
||||
continue;
|
||||
}
|
||||
map.put(keyValue[0], keyValue[1]);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse stream name from url params
|
||||
*
|
||||
* @param params url params
|
||||
* @return stream name
|
||||
*/
|
||||
public static String parseURLStreamName(HashMap<String, String> params) {
|
||||
if (params == null || !params.containsKey(BASIC_DOMAIN)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String domain = params.get(BASIC_DOMAIN);
|
||||
if (TextUtils.isEmpty(domain)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String[] parts = domain.split("/");
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
private static String generateCDNFlvPullUrl(String channelId, String userId, boolean isAudioOnly) {
|
||||
String streamTaskType = isAudioOnly ? STREAM_TASK_TYPE_AUDIO : STREAM_TASK_TYPE_CAMERA;
|
||||
|
||||
return new StringBuilder(HTTP)
|
||||
.append(PLAY_DOMAIN).append('/')
|
||||
.append(APP_NAME).append('/')
|
||||
.append(APP_ID).append('_')
|
||||
.append(channelId).append('_')
|
||||
.append(userId).append('_')
|
||||
.append(streamTaskType)
|
||||
.append(FLV)
|
||||
.toString();
|
||||
}
|
||||
|
||||
private static String generateCDNRtmpPullUrl(String channelId, String userId, boolean isAudioOnly) {
|
||||
String streamTaskType = isAudioOnly ? STREAM_TASK_TYPE_AUDIO : STREAM_TASK_TYPE_CAMERA;
|
||||
|
||||
return new StringBuilder(RTMP)
|
||||
.append(PLAY_DOMAIN).append('/')
|
||||
.append(APP_NAME).append('/')
|
||||
.append(APP_ID).append('_')
|
||||
.append(channelId).append('_')
|
||||
.append(userId).append('_')
|
||||
.append(streamTaskType)
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.alivc.live.commonbiz.test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* @note 备注:该文件被直播连麦控制台的demo试用所链接,请谨慎改动此文件的目录位置。
|
||||
*/
|
||||
public class AliLiveUserSigGenerate {
|
||||
|
||||
/**
|
||||
* APP_ID,在阿里云控制台应用管理页面创建和查看。
|
||||
*/
|
||||
public static final String ALILIVE_APPID = "PLACEHOLDER";
|
||||
/**
|
||||
* APP_KEY,在阿里云控制台应用管理页面创建和查看。
|
||||
*/
|
||||
public static final String ALILIVE_APPKEY = "PLACEHOLDER";
|
||||
|
||||
/**
|
||||
* CDN_DOMAIN,在阿里云控制台应用管理页面创建和查看。
|
||||
*/
|
||||
public static final String ALILIVE_PLAY_DOMAIN = "PLACEHOLDER";
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
* 时间单位秒,代表令牌有效时间。可设置最大范围是小于等于1天,建议不要设置的过短或超过1天,超过1天会不安全。
|
||||
* 默认时间1天。1天 = 60 x 60 x 24。
|
||||
*/
|
||||
public static long getTimesTamp() {
|
||||
return System.currentTimeMillis() / 1000 + 60 * 60 * 24;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 appid,appkey,channelId,userId,nonc,timestamp 生层 token
|
||||
*
|
||||
* @param appid 应用ID。在控制台应用管理页面创建和查看。
|
||||
* @param appkey 在控制台应用管理页面创建和查看。
|
||||
* @param channelId 频道 ID
|
||||
* @param userId 用户 ID
|
||||
* @param timestamp 过期时间戳
|
||||
* @return token
|
||||
*/
|
||||
public static String createToken(String appid, String appkey, String channelId, String userId, long timestamp) {
|
||||
StringBuilder stringBuilder = new StringBuilder()
|
||||
.append(appid)
|
||||
.append(appkey)
|
||||
.append(channelId)
|
||||
.append(userId)
|
||||
.append(timestamp);
|
||||
return getSHA256(stringBuilder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 字符串签名
|
||||
*
|
||||
* @param str 输入源
|
||||
* @return 返回签名
|
||||
*/
|
||||
public static String getSHA256(String str) {
|
||||
try {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));
|
||||
return byte2Hex(hash);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Consider logging the exception and/or re-throwing as a RuntimeException
|
||||
e.printStackTrace();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String byte2Hex(byte[] bytes) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
// Use single quote for char
|
||||
stringBuilder.append('0');
|
||||
}
|
||||
stringBuilder.append(hex);
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.alivc.live.commonbiz.test;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.alivc.live.commonbiz.BuildConfig;
|
||||
import com.alivc.live.commonbiz.R;
|
||||
import com.alivc.live.commonbiz.SharedPreferenceUtils;
|
||||
import com.alivc.live.commonutils.ContextUtils;
|
||||
|
||||
|
||||
/**
|
||||
* Created by baorunchen on 2022/8/31.
|
||||
* <p>
|
||||
* Develop test data class. ONLY FOR TEST!!!!
|
||||
* <p>
|
||||
* First, we store dev test data into `local.properties` file, it's a gitignore file.
|
||||
* <p>
|
||||
* Such as:
|
||||
* sdk.dir=/Users/keria/Library/Android/sdk
|
||||
* push.url=rtmp://push-demo-rtmp.aliyunlive.com/test/stream/keriatest?&auth_key=xxxx
|
||||
* pull.url=
|
||||
* <p>
|
||||
* And then, we can parse it from `build.gradle` file,
|
||||
* if `local.properties` hasn't the property, it will use the default value.
|
||||
*/
|
||||
public class PushDemoTestConstants {
|
||||
private static final String PLACEHOLDER = "PLACEHOLDER";
|
||||
|
||||
/**
|
||||
* Get test push url, you won't need to scan url again!!!
|
||||
* <p>
|
||||
* put your test push url into `local.properties`
|
||||
* such as: push.url=rtmp://xxx
|
||||
*
|
||||
* @return test push url
|
||||
*/
|
||||
public static String getTestPushUrl() {
|
||||
Context context = ContextUtils.getContext();
|
||||
return (context != null) ? context.getString(R.string.test_push_url) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test pull url, you won't need to scan url again!!!
|
||||
* <p>
|
||||
* put your test pull url into `local.properties`
|
||||
* such as: pull.url=rtmp://xxx
|
||||
*
|
||||
* @return test push url
|
||||
*/
|
||||
public static String getTestPullUrl() {
|
||||
Context context = ContextUtils.getContext();
|
||||
return (context != null) ? context.getString(R.string.test_pull_url) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test interactive app id
|
||||
* <p>
|
||||
* put your app id into `local.properties`
|
||||
* such as: interactive.appid=keriatest-appid
|
||||
* <p>
|
||||
* or, you can export it into system environment
|
||||
* such as: export INTERACTIVE_APP_ID=keriatest-appid
|
||||
*
|
||||
* @return interactive app id
|
||||
*/
|
||||
public static String getTestInteractiveAppID() {
|
||||
if (!checkIsPlaceholder(AliLiveUserSigGenerate.ALILIVE_APPID)) {
|
||||
return AliLiveUserSigGenerate.ALILIVE_APPID;
|
||||
}
|
||||
|
||||
if(!TextUtils.isEmpty(SharedPreferenceUtils.getAppId(ContextUtils.getApplicationContext()))){
|
||||
return SharedPreferenceUtils.getAppId(ContextUtils.getApplicationContext());
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(BuildConfig.INTERACTIVE_APP_ID)) {
|
||||
return BuildConfig.INTERACTIVE_APP_ID;
|
||||
}
|
||||
|
||||
Context context = ContextUtils.getContext();
|
||||
if (context != null) {
|
||||
return context.getString(R.string.interactive_appid);
|
||||
}
|
||||
|
||||
return AliLiveUserSigGenerate.ALILIVE_APPID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test interactive app key
|
||||
* <p>
|
||||
* put your app key into `local.properties`
|
||||
* such as: interactive.appkey=keriatest-appkey
|
||||
* <p>
|
||||
* or, you can export it into system environment
|
||||
* such as: export INTERACTIVE_APP_KEY=keriatest-appkey
|
||||
*
|
||||
* @return interactive app key
|
||||
*/
|
||||
public static String getTestInteractiveAppKey() {
|
||||
if (!checkIsPlaceholder(AliLiveUserSigGenerate.ALILIVE_APPKEY)) {
|
||||
return AliLiveUserSigGenerate.ALILIVE_APPKEY;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(SharedPreferenceUtils.getAppKey(ContextUtils.getApplicationContext()))) {
|
||||
return SharedPreferenceUtils.getAppKey(ContextUtils.getApplicationContext());
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(BuildConfig.INTERACTIVE_APP_KEY)) {
|
||||
return BuildConfig.INTERACTIVE_APP_KEY;
|
||||
}
|
||||
|
||||
Context context = ContextUtils.getContext();
|
||||
if (context != null) {
|
||||
return context.getString(R.string.interactive_appkey);
|
||||
}
|
||||
|
||||
return AliLiveUserSigGenerate.ALILIVE_APPKEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get test interactive play domain
|
||||
* <p>
|
||||
* put your app key into `local.properties`
|
||||
* such as: interactive.playdomain=pullkeriatest.alivecdn.com
|
||||
* <p>
|
||||
* or, you can export it into system environment
|
||||
* such as: export INTERACTIVE_PLAY_DOMAIN=pullkeriatest.alivecdn.com
|
||||
*
|
||||
* @return interactive play domain
|
||||
*/
|
||||
public static String getTestInteractivePlayDomain() {
|
||||
if (!checkIsPlaceholder(AliLiveUserSigGenerate.ALILIVE_PLAY_DOMAIN)) {
|
||||
return AliLiveUserSigGenerate.ALILIVE_PLAY_DOMAIN;
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(SharedPreferenceUtils.getPlayDomain(ContextUtils.getApplicationContext()))) {
|
||||
return SharedPreferenceUtils.getPlayDomain(ContextUtils.getApplicationContext());
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(BuildConfig.INTERACTIVE_PLAY_DOMAIN)) {
|
||||
return BuildConfig.INTERACTIVE_PLAY_DOMAIN;
|
||||
}
|
||||
|
||||
Context context = ContextUtils.getContext();
|
||||
if (context != null) {
|
||||
return context.getString(R.string.interactive_play_domain);
|
||||
}
|
||||
|
||||
return AliLiveUserSigGenerate.ALILIVE_PLAY_DOMAIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether configuration value is placeholder or not
|
||||
*
|
||||
* @param configuration configuration value
|
||||
* @return true, false
|
||||
*/
|
||||
public static boolean checkIsPlaceholder(String configuration) {
|
||||
return TextUtils.isEmpty(configuration) || TextUtils.equals(configuration, PLACEHOLDER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.alivc.live.commonbiz.testapi;
|
||||
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.EGLConfig;
|
||||
import android.opengl.EGLContext;
|
||||
import android.opengl.EGLDisplay;
|
||||
import android.opengl.GLUtils;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* @author keria
|
||||
* @date 2024/7/5
|
||||
* @brief FIXME keria:测试代码
|
||||
*/
|
||||
public class EGLContextTest {
|
||||
|
||||
private static final String TAG = "EGLContextTest";
|
||||
|
||||
private EGLContextTest() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 EGLContext
|
||||
*/
|
||||
public static void testGLContext() {
|
||||
|
||||
int maxContexts = 0;
|
||||
|
||||
// 假设尝试创建最多1000个上下文
|
||||
EGLContext[] contexts = new EGLContext[1000];
|
||||
|
||||
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
||||
if (eglDisplay == EGL14.EGL_NO_DISPLAY) {
|
||||
Log.e(TAG, "Unable to get EGL14 display");
|
||||
return;
|
||||
}
|
||||
|
||||
int[] version = new int[2];
|
||||
if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) {
|
||||
Log.e(TAG, "Unable to initialize EGL14");
|
||||
return;
|
||||
}
|
||||
|
||||
int[] configAttribs = {
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
||||
EGL14.EGL_RED_SIZE, 8,
|
||||
EGL14.EGL_GREEN_SIZE, 8,
|
||||
EGL14.EGL_BLUE_SIZE, 8,
|
||||
EGL14.EGL_ALPHA_SIZE, 8,
|
||||
EGL14.EGL_DEPTH_SIZE, 24,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
|
||||
EGLConfig[] configs = new EGLConfig[1];
|
||||
int[] numConfigs = new int[1];
|
||||
if (!EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0)) {
|
||||
Log.e(TAG, "Unable to choose config");
|
||||
return;
|
||||
}
|
||||
|
||||
int[] contextAttribs = {
|
||||
EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
|
||||
for (int i = 0; i < contexts.length; i++) {
|
||||
contexts[i] = EGL14.eglCreateContext(eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
|
||||
if (contexts[i] == EGL14.EGL_NO_CONTEXT) {
|
||||
Log.e(TAG, "Failed to create EGL context at index " + i + ": " + EGL14.eglGetError() + " - " + GLUtils.getEGLErrorString(EGL14.eglGetError()));
|
||||
break;
|
||||
}
|
||||
maxContexts++;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Maximum number of OpenGL ES contexts supported: " + maxContexts);
|
||||
|
||||
// 清理所有创建的上下文
|
||||
for (int i = 0; i < maxContexts; i++) {
|
||||
if (contexts[i] != EGL14.EGL_NO_CONTEXT) {
|
||||
EGL14.eglDestroyContext(eglDisplay, contexts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
EGL14.eglTerminate(eglDisplay);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="30dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/backdoor_title"
|
||||
android:textColor="#747A8C"
|
||||
android:textSize="20dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_tip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_margin="5dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/backdoor_tip"
|
||||
android:textColor="#B2B7C4"
|
||||
android:textSize="11dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:background="#3A3D48"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<com.alivc.live.commonui.widgets.LivePushTextSwitch
|
||||
android:id="@+id/sw_force_rtc_pre_env"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.alivc.live.commonui.widgets.LivePushTextSwitch
|
||||
android:id="@+id/sw_multi_pk_16in"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="6dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="6dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/info_tv"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:lineSpacingMultiplier="1.5"
|
||||
android:padding="6dp"
|
||||
android:textSize="12dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="backdoor_title">Live Push Demo Configuration Page</string>
|
||||
<string name="backdoor_tip">Used to manually configure demo corresponding functions</string>
|
||||
<string name="backdoor_rtc_force_pre_env">Force RTC Pre Environment</string>
|
||||
<string name="backdoor_multi_pk_16in">Multi PK 16IN</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="backdoor_title">Live Push Demo配置页面</string>
|
||||
<string name="backdoor_tip">用于手动配置demo相应功能</string>
|
||||
<string name="backdoor_rtc_force_pre_env">强制RTC预发环境</string>
|
||||
<string name="backdoor_multi_pk_16in">16方多人PK</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="backdoor_title">Live Push Demo Configuration Page</string>
|
||||
<string name="backdoor_tip">Used to manually configure demo corresponding functions</string>
|
||||
<string name="backdoor_rtc_force_pre_env">Force RTC Pre Environment</string>
|
||||
<string name="backdoor_multi_pk_16in">Multi PK 16IN</string>
|
||||
|
||||
</resources>
|
||||
Reference in New Issue
Block a user