diff --git a/README.md b/README.md index 0bf4b90..187c5b6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ # cordova-plugin-trtc +腾讯实时音视频(TRTC)封装cordova插件 (示例) + +具体原生项目参考tencent的demo, 源码大部分从这里搬运: + +[https://github.com/zhaoyang21cn/Android_TRTC](https://github.com/zhaoyang21cn/Android_TRTC) + + +[https://github.com/zhaoyang21cn/iOS_TRTC](https://github.com/zhaoyang21cn/iOS_TRTC) + +iOS的sdk需要自己添加到src/ios/libs下, 可以从官网获取 diff --git a/package.json b/package.json new file mode 100644 index 0000000..57a232e --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "cordova-plugin-trtc", + "version": "0.0.1", + "description": "", + "cordova": { + "id": "cordova-plugin-trtc", + "platforms": [ + "android", + "ios" + ] + }, + "keywords": [ + "ecosystem:cordova", + "cordova-android" + ], + "author": "", + "license": "ISC" +} diff --git a/plugin.xml b/plugin.xml new file mode 100644 index 0000000..f683193 --- /dev/null +++ b/plugin.xml @@ -0,0 +1,291 @@ + + + + Trtc + TRTC Plugin + cordova,TRTC,live + MIT License + + + + + + + + + + + + + + + + + + + + + + + + + + $PERMISSION_MIC_DESC + + + $PERMISSION_CAMERA_DESC + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $APP_ID + + + + + + + + + + + + + + + + + + + + + + + + + + $APP_ID + 腾讯视频通话 + 房间名 + 房间ID + 创建房间 + 翻转 + 聊天 + 白板 + 消息 + 输入文字内容 + 美颜 + 日志 + 声音 + 输入房间号 + 进入房间 + 配置 + 反馈 + 请选择要配置的分辨率 + 聊天 + 请选择反馈类型 + 请输入具体问题描述 + 非常感谢您的反馈 + 登录成功 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/Trtc.java b/src/android/Trtc.java new file mode 100644 index 0000000..3557f83 --- /dev/null +++ b/src/android/Trtc.java @@ -0,0 +1,55 @@ +package com.chuwa.cordova.trtc; + +import android.app.Activity; +import android.content.Intent; + +import com.tencent.trtc.videocall.VideoCallingEnterActivity; +import com.tencent.trtc.videocall.VideoCallingActivity; + +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CallbackContext; + +import org.apache.cordova.CordovaWebView; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + + +public class Trtc extends CordovaPlugin { + + static final String ACTION_SHOW_CREATE_PAGE = "showCreatePage"; // for test + static final String ACTION_JOIN_CHANNEL = "joinChannel"; + + @Override + public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { + Activity activity = this.cordova.getActivity(); + +// if (ACTION_SHOW_CREATE_PAGE.equals(action)) { +// Intent intent = new Intent(activity, VideoCallingEnterActivity.class); +// cordova.startActivityForResult(this, intent, 666); +// } else + if (ACTION_JOIN_CHANNEL.equals(action)) { + Intent intent = new Intent(activity, VideoCallingActivity.class); + JSONObject arg = args.getJSONObject(0); + String ROOM_ID = arg.getString("ROOM_ID"); + String USER_ID = arg.getString("USER_ID"); + int SDK_APP_ID = arg.getInt("SDK_APP_ID"); + String USER_SIG = arg.getString("USER_SIG"); + intent.putExtra("room_id", ROOM_ID); + intent.putExtra("user_id", USER_ID); + intent.putExtra("sdk_app_id", SDK_APP_ID); + intent.putExtra("user_sig", USER_SIG); + cordova.startActivityForResult(this, intent, 666); + } + + return false; + } + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + } + + +} diff --git a/src/android/java/com/example/basic/TRTCBaseActivity.java b/src/android/java/com/example/basic/TRTCBaseActivity.java new file mode 100644 index 0000000..52cb357 --- /dev/null +++ b/src/android/java/com/example/basic/TRTCBaseActivity.java @@ -0,0 +1,71 @@ +package android.java.com.example.basic; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.os.Build; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; + +import java.util.ArrayList; +import java.util.List; + +public abstract class TRTCBaseActivity extends AppCompatActivity { + + protected static final int REQ_PERMISSION_CODE = 0x1000; + protected int mGrantedCount = 0; + + protected abstract void onPermissionGranted(); + + private int getId(String idName, String type) { + return getResources().getIdentifier(idName, type, getPackageName()); + } + + protected boolean checkPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + List permissions = new ArrayList<>(); + if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); + } + if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)) { + permissions.add(Manifest.permission.CAMERA); + } + if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)) { + permissions.add(Manifest.permission.RECORD_AUDIO); + } + if (PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)) { + permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE); + } + if (permissions.size() != 0) { + ActivityCompat.requestPermissions(TRTCBaseActivity.this, + permissions.toArray(new String[0]), + REQ_PERMISSION_CODE); + return false; + } + } + return true; + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case REQ_PERMISSION_CODE: + for (int ret : grantResults) { + if (PackageManager.PERMISSION_GRANTED == ret) { + mGrantedCount ++; + } + } + if (mGrantedCount == permissions.length) { + onPermissionGranted(); + } else { + Toast.makeText(this, getString(getId("common_please_input_roomid_and_userid", "string")), Toast.LENGTH_SHORT).show(); + } + mGrantedCount = 0; + break; + default: + break; + } + } +} diff --git a/src/android/java/com/tencent/trtc/debug/Constant.java b/src/android/java/com/tencent/trtc/debug/Constant.java new file mode 100644 index 0000000..d5e4120 --- /dev/null +++ b/src/android/java/com/tencent/trtc/debug/Constant.java @@ -0,0 +1,27 @@ +package com.tencent.trtc.debug; + +public class Constant { + + public static final String ROOM_ID = "room_id"; + public static final String USER_ID = "user_id"; + public static final String SDK_APP_ID = "sdk_app_id"; + public static final String USER_SIG = "user_sig"; + public static final String ROLE_TYPE = "role_type"; + public static final String CUSTOM_VIDEO = "custom_video"; + + // 美颜风格.三种美颜风格:0 :光滑 1:自然 2:朦胧 + public static final int BEAUTY_STYLE_SMOOTH = 0; + public static final int BEAUTY_STYLE_NATURE = 1; + + public static final int VIDEO_FPS = 15; + // RTC 通话场景:640*360,15fps,550kbps + public static final int RTC_VIDEO_BITRATE = 550; + + // 直播场景:连麦小主播:270*480, 15pfs, 400kbps + public static final int LIVE_270_480_VIDEO_BITRATE = 400; + public static final int LIVE_360_640_VIDEO_BITRATE = 800; + // 直播场景:大主播:默认 540*960, 15fps,1200kbps + public static final int LIVE_540_960_VIDEO_BITRATE = 900; + public static final int LIVE_720_1280_VIDEO_BITRATE = 1500; + +} diff --git a/src/android/java/com/tencent/trtc/debug/GenerateTestUserSig.java b/src/android/java/com/tencent/trtc/debug/GenerateTestUserSig.java new file mode 100644 index 0000000..0826965 --- /dev/null +++ b/src/android/java/com/tencent/trtc/debug/GenerateTestUserSig.java @@ -0,0 +1,280 @@ +package com.tencent.trtc.debug; + + +import android.util.Base64; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.zip.Deflater; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/** + * Module: GenerateTestUserSig + * + * Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。 + * 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。 + * + * Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下: + * + * 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品, + * 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。 + * 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。 + * + * 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。 + * 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。 + * + * Reference:https://cloud.tencent.com/document/product/647/17275#Server + */ + +/** + * Module: GenerateTestUserSig + * + * Description: generates UserSig for testing. UserSig is a security signature designed by Tencent Cloud for its cloud services. + * It is calculated based on `SDKAppID`, `UserID`, and `EXPIRETIME` using the HMAC-SHA256 encryption algorithm. + * + * Attention: do not use the code below in your commercial app. This is because: + * + * The code may be able to calculate UserSig correctly, but it is only for quick testing of the SDK’s basic features, not for commercial apps. + * `SECRETKEY` in client code can be easily decompiled and reversed, especially on web. + * Once your key is disclosed, attackers will be able to steal your Tencent Cloud traffic. + * + * The correct method is to deploy the `UserSig` calculation code and encryption key on your project server so that your app can request from your server a `UserSig` that is calculated whenever one is needed. + * Given that it is more difficult to hack a server than a client app, server-end calculation can better protect your key. + * + * Reference: https://cloud.tencent.com/document/product/647/17275#Server + */ +public class GenerateTestUserSig { + + /** + * 配置为CDN发布、混流的域名 + * + */ + + /** + * Domain name for CDN publishing and stream mixing + */ + public static final String CDN_DOMAIN_NAME = "testuser"; + + /** + * CDN发布功能 混流bizId + */ + + /** + * `bizId` for CDN publishing and stream mixing + */ + public static final int BIZID = 111; + + /** + * CDN发布功能 混流appId + */ + + /** + * `appId` for CDN publishing and stream mixing + */ + public static final int APPID = 111; + + /** + * 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。 + * + * 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ) 创建应用,即可看到 SDKAppId, + * 它是腾讯云用于区分客户的唯一标识。 + */ + + /** + * Tencent Cloud `SDKAppID`. Set it to the `SDKAppID` of your account. + * + * You can view your `SDKAppID` after creating an application in the [TRTC console](https://console.cloud.tencent.com/rav). + * `SDKAppID` uniquely identifies a Tencent Cloud account. + */ + public static final int SDKAPPID = 1400593044; + + /** + * 签名过期时间,建议不要设置的过短 + *

+ * 时间单位:秒 + * 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天 + */ + + /** + * Signature validity period, which should not be set too short + *

+ * Unit: second + * Default value: 604800 (7 days) + */ + private static final int EXPIRETIME = 604800; + + + /** + * 计算签名用的加密密钥,获取步骤如下: + * + * step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/rav ),如果还没有应用就创建一个, + * step2. 单击应用信息,并进一步找到“快速上手”部分。 + * step3. 点击“复制密钥”按钮,复制密钥,请将其拷贝并复制到如下的变量中 + * + * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。 + * 文档:https://cloud.tencent.com/document/product/647/17275#Server + */ + + /** + * Follow the steps below to obtain the key required for UserSig calculation. + * + * Step 1. Log in to the [TRTC console](https://console.cloud.tencent.com/rav), and create an application if you don’t have one. + * Step 2. Find your application, click “Application Info”, and click the “Quick Start” tab. + * Step 3. Copy and paste the key to the code, as shown below. + * + * Note: this method is for testing only. Before commercial launch, please migrate the UserSig calculation code and key to your backend server to prevent key disclosure and traffic stealing. + * Reference: https://cloud.tencent.com/document/product/647/17275#Server + */ + public static final String SECRETKEY = "f814a0480cfa95a3ffffd8891844f3cd36888166dbef5347303043744f247c06"; + + /** + * 计算 UserSig 签名 + * + * 函数内部使用 HMAC-SHA256 非对称加密算法,对 SDKAPPID、userId 和 EXPIRETIME 进行加密。 + * + * @note: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下: + * + * 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品, + * 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。 + * 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。 + * + * 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。 + * 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。 + * + * 文档:https://cloud.tencent.com/document/product/647/17275#Server + */ + + /** + * Calculating UserSig + * + * The asymmetric encryption algorithm HMAC-SHA256 is used in the function to calculate UserSig based on `SDKAppID`, `UserID`, and `EXPIRETIME`. + * + * @note: do not use the code below in your commercial app. This is because: + * + * The code may be able to calculate UserSig correctly, but it is only for quick testing of the SDK’s basic features, not for commercial apps. + * `SECRETKEY` in client code can be easily decompiled and reversed, especially on web. + * Once your key is disclosed, attackers will be able to steal your Tencent Cloud traffic. + * + * The correct method is to deploy the `UserSig` calculation code on your project server so that your app can request from your server a `UserSig` that is calculated whenever one is needed. + * Given that it is more difficult to hack a server than a client app, server-end calculation can better protect your key. + * + * Reference: https://cloud.tencent.com/document/product/647/17275#Server + */ + public static String genTestUserSig(String userId) { + return GenTLSSignature(SDKAPPID, userId, EXPIRETIME, null, SECRETKEY); + } + + /** + * 生成 tls 票据 + * + * @param sdkappid 应用的 appid + * @param userId 用户 id + * @param expire 有效期,单位是秒 + * @param userbuf 默认填写null + * @param priKeyContent 生成 tls 票据使用的私钥内容 + * @return 如果出错,会返回为空,或者有异常打印,成功返回有效的票据 + */ + + /** + * Generating a TLS Ticket + * + * @param sdkappid `appid` of your application + * @param userId User ID + * @param expire Validity period, in seconds + * @param userbuf `null` by default + * @param priKeyContent Private key required for generating a TLS ticket + * @return If an error occurs, an empty string will be returned or exceptions printed. If the operation succeeds, a valid ticket will be returned. + */ + private static String GenTLSSignature(long sdkappid, String userId, long expire, byte[] userbuf, String priKeyContent) { + long currTime = System.currentTimeMillis() / 1000; + JSONObject sigDoc = new JSONObject(); + try { + sigDoc.put("TLS.ver", "2.0"); + sigDoc.put("TLS.identifier", userId); + sigDoc.put("TLS.sdkappid", sdkappid); + sigDoc.put("TLS.expire", expire); + sigDoc.put("TLS.time", currTime); + } catch (JSONException e) { + e.printStackTrace(); + } + + String base64UserBuf = null; + if (null != userbuf) { + base64UserBuf = Base64.encodeToString(userbuf, Base64.NO_WRAP); + try { + sigDoc.put("TLS.userbuf", base64UserBuf); + } catch (JSONException e) { + e.printStackTrace(); + } + } + String sig = hmacsha256(sdkappid, userId, currTime, expire, priKeyContent, base64UserBuf); + if (sig.length() == 0) { + return ""; + } + try { + sigDoc.put("TLS.sig", sig); + } catch (JSONException e) { + e.printStackTrace(); + } + Deflater compressor = new Deflater(); + compressor.setInput(sigDoc.toString().getBytes(Charset.forName("UTF-8"))); + compressor.finish(); + byte[] compressedBytes = new byte[2048]; + int compressedBytesLength = compressor.deflate(compressedBytes); + compressor.end(); + return new String(base64EncodeUrl(Arrays.copyOfRange(compressedBytes, 0, compressedBytesLength))); + } + + + private static String hmacsha256(long sdkappid, String userId, long currTime, long expire, String priKeyContent, String base64Userbuf) { + String contentToBeSigned = "TLS.identifier:" + userId + "\n" + + "TLS.sdkappid:" + sdkappid + "\n" + + "TLS.time:" + currTime + "\n" + + "TLS.expire:" + expire + "\n"; + if (null != base64Userbuf) { + contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n"; + } + try { + byte[] byteKey = priKeyContent.getBytes("UTF-8"); + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256"); + hmac.init(keySpec); + byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes("UTF-8")); + return new String(Base64.encode(byteSig, Base64.NO_WRAP)); + } catch (UnsupportedEncodingException e) { + return ""; + } catch (NoSuchAlgorithmException e) { + return ""; + } catch (InvalidKeyException e) { + return ""; + } + } + + private static byte[] base64EncodeUrl(byte[] input) { + byte[] base64 = new String(Base64.encode(input, Base64.NO_WRAP)).getBytes(); + for (int i = 0; i < base64.length; ++i) + switch (base64[i]) { + case '+': + base64[i] = '*'; + break; + case '/': + base64[i] = '-'; + break; + case '=': + base64[i] = '_'; + break; + default: + break; + } + return base64; + } + +} diff --git a/src/android/java/com/tencent/trtc/videocall/BuildConfig.java b/src/android/java/com/tencent/trtc/videocall/BuildConfig.java new file mode 100644 index 0000000..1371d8a --- /dev/null +++ b/src/android/java/com/tencent/trtc/videocall/BuildConfig.java @@ -0,0 +1,18 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.tencent.trtc.videocall; + +public final class BuildConfig { + public static final boolean DEBUG = Boolean.parseBoolean("true"); + public static final String LIBRARY_PACKAGE_NAME = "com.tencent.trtc.videocall"; + /** + * @deprecated APPLICATION_ID is misleading in libraries. For the library package name use LIBRARY_PACKAGE_NAME + */ + @Deprecated + public static final String APPLICATION_ID = "com.tencent.trtc.videocall"; + public static final String BUILD_TYPE = "debug"; + public static final String FLAVOR = ""; + public static final int VERSION_CODE = 1; + public static final String VERSION_NAME = "1.0"; +} diff --git a/src/android/java/com/tencent/trtc/videocall/FloatingView.java b/src/android/java/com/tencent/trtc/videocall/FloatingView.java new file mode 100644 index 0000000..2259163 --- /dev/null +++ b/src/android/java/com/tencent/trtc/videocall/FloatingView.java @@ -0,0 +1,193 @@ +package com.tencent.trtc.videocall; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.PixelFormat; +import android.graphics.drawable.BitmapDrawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.PopupWindow; + +/** + * 悬浮球,点击可以弹出菜单 + */ +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +public class FloatingView extends FrameLayout implements GestureDetector.OnGestureListener { + + private Context mContext; + private WindowManager mWindowManager; + private GestureDetector mGestureDetector; + private WindowManager.LayoutParams mLayoutParams; + private float mLastX; + private float mLastY; + private PopupWindow mPopupWindow; + private long mTapOutsideTime; + private boolean mIsShowing = false; + + public FloatingView(Context context) { + super(context); + this.mContext = context; + this.mGestureDetector = new GestureDetector(context, this); + } + + public FloatingView(Context context, AttributeSet attrs) { + super(context, attrs); + this.mContext = context; + this.mGestureDetector = new GestureDetector(context, this); + } + + public FloatingView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + this.mContext = context; + this.mGestureDetector = new GestureDetector(context, this); + } + + public FloatingView(Context context, int viewResId) { + super(context); + this.mContext = context; + View.inflate(context, viewResId, this); + this.mGestureDetector = new GestureDetector(context, this); + } + + public void showView(View view) { + showView(view, WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); + } + + public void showView(View view, int width, int height) { + mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); + int type = WindowManager.LayoutParams.TYPE_TOAST; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { + type = WindowManager.LayoutParams.TYPE_PHONE; + } + mLayoutParams = new WindowManager.LayoutParams(type); + mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; + mLayoutParams.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; + mLayoutParams.width = width; + mLayoutParams.height = height; + mLayoutParams.format = PixelFormat.TRANSLUCENT; + mWindowManager.addView(view, mLayoutParams); + } + + public void hideView() { + if (null != mWindowManager) { + mWindowManager.removeViewImmediate(this); + } + mWindowManager = null; + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + + @Override + public boolean onDown(MotionEvent e) { + mLastX = e.getRawX(); + mLastY = e.getRawY(); + return false; + } + + @Override + public void onShowPress(MotionEvent e) { + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + float nowX, nowY, tranX, tranY; + nowX = e2.getRawX(); + nowY = e2.getRawY(); + tranX = nowX - mLastX; + tranY = nowY - mLastY; + mLayoutParams.x += tranX; + mLayoutParams.y += tranY; + mWindowManager.updateViewLayout(this, mLayoutParams); + mLastX = nowX; + mLastY = nowY; + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + return false; + } + + public void setPopupWindow(int id) { + mPopupWindow = new PopupWindow(this); + mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); + mPopupWindow.setTouchable(true); + mPopupWindow.setOutsideTouchable(true); + mPopupWindow.setFocusable(false); + mPopupWindow.setBackgroundDrawable(new BitmapDrawable()); + mPopupWindow.setContentView(LayoutInflater.from(getContext()).inflate(id, null)); + mPopupWindow.setTouchInterceptor(new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (event.getAction() == MotionEvent.ACTION_OUTSIDE) { + mPopupWindow.dismiss(); + mTapOutsideTime = System.currentTimeMillis(); + return true; + } + return false; + } + }); + } + + public View getPopupView() { + return mPopupWindow.getContentView(); + } + + public void setOnPopupItemClickListener(OnClickListener listener) { + if (mPopupWindow == null) + return; + + ViewGroup layout = (ViewGroup) mPopupWindow.getContentView(); + for (int i = 0; i < layout.getChildCount(); i++) { + layout.getChildAt(i).setOnClickListener(listener); + } + } + + public void show() { + if (!mIsShowing) { + showView(this); + } + mIsShowing = true; + } + + public void dismiss() { + if (mIsShowing) { + hideView(); + } + mIsShowing = false; + ViewGroup layout = (ViewGroup) mPopupWindow.getContentView(); + for (int i = 0; i < layout.getChildCount(); i++) { + layout.getChildAt(i).setOnClickListener(null); + } + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (null != mPopupWindow) + mPopupWindow.dismiss(); + if (!(System.currentTimeMillis() - mTapOutsideTime < 80)) { + mPopupWindow.showAtLocation(this, Gravity.NO_GRAVITY, 100, 0); + } + return false; + } + + +} diff --git a/src/android/java/com/tencent/trtc/videocall/VideoCallingActivity.java b/src/android/java/com/tencent/trtc/videocall/VideoCallingActivity.java new file mode 100644 index 0000000..4072c97 --- /dev/null +++ b/src/android/java/com/tencent/trtc/videocall/VideoCallingActivity.java @@ -0,0 +1,397 @@ +package com.tencent.trtc.videocall; + +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; + +import android.java.com.example.basic.TRTCBaseActivity; + +import com.tencent.liteav.TXLiteAVCode; +import com.tencent.liteav.device.TXDeviceManager; +import com.tencent.rtmp.ui.TXCloudVideoView; +import com.tencent.trtc.TRTCCloud; +import com.tencent.trtc.TRTCCloudDef; +import com.tencent.trtc.TRTCCloudListener; +import com.tencent.trtc.debug.Constant; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * TRTC视频通话的主页面 + *

+ * 包含如下简单功能: + * - 进入视频通话房间{@link VideoCallingActivity#enterRoom()} + * - 退出视频通话房间{@link VideoCallingActivity#exitRoom()} + * - 切换前置/后置摄像头{@link VideoCallingActivity#switchCamera()} + * - 打开/关闭摄像头{@link VideoCallingActivity#muteVideo()} + * - 打开/关闭麦克风{@link VideoCallingActivity#muteAudio()} + * - 显示房间内其他用户的视频画面(当前示例最多可显示6个其他用户的视频画面){@link TRTCCloudImplListener#refreshRemoteVideoViews()} + *

+ * - 详见接入文档{https://cloud.tencent.com/document/product/647/42045} + */ + +/** + * Video Call + * + * Features: + * - Enter a video call room: {@link VideoCallingActivity#enterRoom()} + * - Exit a video call room: {@link VideoCallingActivity#exitRoom()} + * - Switch between the front and rear cameras: {@link VideoCallingActivity#switchCamera()} + * - Turn on/off the camera: {@link VideoCallingActivity#muteVideo()} + * - Turn on/off the mic: {@link VideoCallingActivity#muteAudio()} + * - Display the video of other users (max. 6) in the room: {@link TRTCCloudImplListener#refreshRemoteVideoViews()} + * + * - For more information, please see the integration document {https://cloud.tencent.com/document/product/647/42045}. + */ +public class VideoCallingActivity extends TRTCBaseActivity implements View.OnClickListener { + + private static final String TAG = "VideoCallingActivity"; + private static final int OVERLAY_PERMISSION_REQ_CODE = 1234; + + private TextView mTextTitle; + private TXCloudVideoView mTXCVVLocalPreviewView; + private ImageView mImageBack; + private ImageView mButtonMuteVideo; + private ImageView mButtonMuteAudio; + private ImageView mButtonSwitchCamera; + private ImageView mButtonAudioRoute; + + private TRTCCloud mTRTCCloud; + private TXDeviceManager mTXDeviceManager; + private boolean mIsFrontCamera = true; + private List mRemoteUidList; + private List mRemoteViewList; + private int mUserCount = 0; + private String mRoomId; + private String mUserId; + private int sdkAppId; + private String userSig; + private boolean mAudioRouteFlag = true; + private FloatingView mFloatingView; + + private int getId(String idName, String type) { + return getResources().getIdentifier(idName, type, getPackageName()); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); +// R.layout.videocall_activity_calling + setContentView(getId("videocall_activity_calling", "layout")); + getSupportActionBar().hide(); + handleIntent(); + + if (checkPermission()) { + initView(); + enterRoom(); + } + } + + private void handleIntent() { + Intent intent = getIntent(); + if (null != intent) { + if (intent.getStringExtra(Constant.USER_ID) != null) { + mUserId = intent.getStringExtra(Constant.USER_ID); + } + if (intent.getStringExtra(Constant.ROOM_ID) != null) { + mRoomId = intent.getStringExtra(Constant.ROOM_ID); + } + + if (intent.getIntExtra(Constant.SDK_APP_ID, 0) != 0) { + sdkAppId = intent.getIntExtra(Constant.SDK_APP_ID, 0); + } + + if (intent.getStringExtra(Constant.USER_SIG) != null) { + userSig = intent.getStringExtra(Constant.USER_SIG); + } + } + System.out.println("sdkAppId = " + sdkAppId); + System.out.println("userSig = " + userSig); + } + + private void initView() { +// R.id.tv_room_number + mTextTitle = findViewById(getId("tv_room_number", "id")); +// R.id.iv_back + mImageBack = findViewById(getId("iv_back", "id")); +// R.id.txcvv_main + mTXCVVLocalPreviewView = findViewById(getId("txcvv_main", "id")); +// R.id.btn_mute_video + mButtonMuteVideo = findViewById(getId("btn_mute_video", "id")); +// R.id.btn_mute_audio + mButtonMuteAudio = findViewById(getId("btn_mute_audio", "id")); +// R.id.btn_switch_camera + mButtonSwitchCamera = findViewById(getId("btn_switch_camera", "id")); +// R.id.btn_audio_route + mButtonAudioRoute = findViewById(getId("btn_audio_route", "id")); + + if (!TextUtils.isEmpty(mRoomId)) { + mTextTitle.setText(getString(getId("videocall_roomid", "string")) + mRoomId); + } + mImageBack.setOnClickListener(this); + mButtonMuteVideo.setOnClickListener(this); + mButtonMuteAudio.setOnClickListener(this); + mButtonSwitchCamera.setOnClickListener(this); + mButtonAudioRoute.setOnClickListener(this); + + mRemoteUidList = new ArrayList<>(); + mRemoteViewList = new ArrayList<>(); +// R.id.trtc_view_1 + mRemoteViewList.add((TXCloudVideoView) findViewById(getId("trtc_view_1", "id"))); + mRemoteViewList.add((TXCloudVideoView) findViewById(getId("trtc_view_2", "id"))); + mRemoteViewList.add((TXCloudVideoView) findViewById(getId("trtc_view_3", "id"))); + mRemoteViewList.add((TXCloudVideoView) findViewById(getId("trtc_view_4", "id"))); + mRemoteViewList.add((TXCloudVideoView) findViewById(getId("trtc_view_5", "id"))); + mRemoteViewList.add((TXCloudVideoView) findViewById(getId("trtc_view_6", "id"))); + +// R.layout.videocall_view_floating_default + mFloatingView = new FloatingView(getApplicationContext(), getId("videocall_view_floating_default", "layout")); +// R.layout.videocall_popup_layout + mFloatingView.setPopupWindow(getId("videocall_popup_layout", "layout")); + mFloatingView.setOnPopupItemClickListener(this); + } + + private void enterRoom() { + mTRTCCloud = TRTCCloud.sharedInstance(getApplicationContext()); + mTRTCCloud.setListener(new TRTCCloudImplListener(VideoCallingActivity.this)); + mTXDeviceManager = mTRTCCloud.getDeviceManager(); + + TRTCCloudDef.TRTCParams trtcParams = new TRTCCloudDef.TRTCParams(); + trtcParams.sdkAppId = this.sdkAppId; + trtcParams.userId = mUserId; + trtcParams.roomId = Integer.parseInt(mRoomId); + trtcParams.userSig = this.userSig; + + mTRTCCloud.startLocalPreview(mIsFrontCamera, mTXCVVLocalPreviewView); + mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_SPEECH); + mTRTCCloud.enterRoom(trtcParams, TRTCCloudDef.TRTC_APP_SCENE_VIDEOCALL); + } + + @Override + protected void onStop() { + super.onStop(); + requestDrawOverLays(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mFloatingView != null && mFloatingView.isShown()) { + mFloatingView.dismiss(); + } + exitRoom(); + } + + private void exitRoom() { + if (mTRTCCloud != null) { + mTRTCCloud.stopLocalAudio(); + mTRTCCloud.stopLocalPreview(); + mTRTCCloud.exitRoom(); + mTRTCCloud.setListener(null); + } + mTRTCCloud = null; + TRTCCloud.destroySharedInstance(); + } + + @Override + protected void onResume() { + super.onResume(); + if (mFloatingView != null && mFloatingView.isShown()) { + mFloatingView.dismiss(); + } + } + + @Override + protected void onPermissionGranted() { + initView(); + enterRoom(); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == getId("iv_back", "id")) { + finish(); + } else if (id == getId("btn_mute_video", "id")) { + muteVideo(); + } else if (id == getId("btn_mute_audio", "id")) { + muteAudio(); + } else if (id == getId("btn_switch_camera", "id")) { + switchCamera(); + } else if (id == getId("btn_audio_route", "id")) { + audioRoute(); + } else if (id == getId("iv_return", "id")) { + floatViewClick(); + } + } + + private void floatViewClick() { + Intent intent = new Intent(this, VideoCallingActivity.class); + if (intent != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + } + try { + startActivity(intent); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void muteVideo() { + boolean isSelected = mButtonMuteVideo.isSelected(); + if (!isSelected) { + mButtonMuteVideo.setImageDrawable(getDrawable(getId("view_open", "mipmap"))); + closeRemoteVideoViews(); + } else { + mButtonMuteVideo.setImageDrawable(getDrawable(getId("view_close", "mipmap"))); + refreshRemoteVideoViews(); + } + mButtonMuteVideo.setSelected(!isSelected); + } + + + private void closeRemoteVideoViews() { + for (int i = 0; i < mRemoteViewList.size(); i++) { + if (i < mRemoteUidList.size()) { + mRemoteViewList.get(i).setVisibility(View.INVISIBLE); + } else { + mRemoteViewList.get(i).setVisibility(View.GONE); + } + } + } + + private void refreshRemoteVideoViews() { + for (int i = 0; i < mRemoteViewList.size(); i++) { + if (i < mRemoteUidList.size()) { + String remoteUid = mRemoteUidList.get(i); + mRemoteViewList.get(i).setVisibility(View.VISIBLE); + } else { + mRemoteViewList.get(i).setVisibility(View.GONE); + } + } + } + + private void muteAudio() { + boolean isSelected = mButtonMuteAudio.isSelected(); + if (!isSelected) { + mTRTCCloud.muteLocalAudio(true); + mButtonMuteAudio.setImageDrawable(getDrawable(getId("mac_on", "mipmap"))); + } else { + mTRTCCloud.muteLocalAudio(false); + mButtonMuteAudio.setImageDrawable(getDrawable(getId("mac_off", "mipmap"))); + } + mButtonMuteAudio.setSelected(!isSelected); + } + + private void switchCamera() { + mIsFrontCamera = !mIsFrontCamera; + mTXDeviceManager.switchCamera(mIsFrontCamera); + } + + private void audioRoute() { + if (mAudioRouteFlag) { + mAudioRouteFlag = false; + mTXDeviceManager.setAudioRoute(TXDeviceManager.TXAudioRoute.TXAudioRouteEarpiece); + mButtonAudioRoute.setImageDrawable(getDrawable(getId("earpiece", "mipmap"))); + } else { + mAudioRouteFlag = true; + mTXDeviceManager.setAudioRoute(TXDeviceManager.TXAudioRoute.TXAudioRouteSpeakerphone); + mButtonAudioRoute.setImageDrawable(getDrawable(getId("speaker", "mipmap"))); + } + } + + private class TRTCCloudImplListener extends TRTCCloudListener { + + private WeakReference mContext; + + public TRTCCloudImplListener(VideoCallingActivity activity) { + super(); + mContext = new WeakReference<>(activity); + } + + @Override + public void onUserVideoAvailable(String userId, boolean available) { + Log.d(TAG, "onUserVideoAvailable userId " + userId + ", mUserCount " + mUserCount + ",available " + available); + int index = mRemoteUidList.indexOf(userId); + if (available) { + if (index != -1) { + return; + } + mRemoteUidList.add(userId); + refreshRemoteVideoViews(); + } else { + if (index == -1) { + return; + } + mTRTCCloud.stopRemoteView(userId); + mRemoteUidList.remove(index); + refreshRemoteVideoViews(); + } + + } + + private void refreshRemoteVideoViews() { + for (int i = 0; i < mRemoteViewList.size(); i++) { + if (i < mRemoteUidList.size()) { + String remoteUid = mRemoteUidList.get(i); + mRemoteViewList.get(i).setVisibility(View.VISIBLE); + mTRTCCloud.startRemoteView(remoteUid, TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SMALL, mRemoteViewList.get(i)); + } else { + mRemoteViewList.get(i).setVisibility(View.GONE); + } + } + } + + @Override + public void onError(int errCode, String errMsg, Bundle extraInfo) { + Log.d(TAG, "sdk callback onError"); + VideoCallingActivity activity = mContext.get(); + if (activity != null) { + Toast.makeText(activity, "onError: " + errMsg + "[" + errCode + "]", Toast.LENGTH_SHORT).show(); + finish(); + } + } + + @Override + public void onExitRoom(int reason) { + super.onExitRoom(reason); + if (reason == 2) { + VideoCallingActivity activity = mContext.get(); + Toast.makeText(activity, "远程协助已结束", Toast.LENGTH_LONG).show(); + } + finish(); + } + + } + + public void requestDrawOverLays() { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N && !Settings.canDrawOverlays(VideoCallingActivity.this)) { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + VideoCallingActivity.this.getPackageName())); + startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); + } else { + showFloatingView(); + } + } + + private void showFloatingView() { + if (mFloatingView != null && !mFloatingView.isShown()) { + if ((null != mTRTCCloud)) { + mFloatingView.show(); + mFloatingView.setOnPopupItemClickListener(this); + } + } + } +} diff --git a/src/android/java/com/tencent/trtc/videocall/VideoCallingEnterActivity.java b/src/android/java/com/tencent/trtc/videocall/VideoCallingEnterActivity.java new file mode 100644 index 0000000..709329f --- /dev/null +++ b/src/android/java/com/tencent/trtc/videocall/VideoCallingEnterActivity.java @@ -0,0 +1,93 @@ +package com.tencent.trtc.videocall; + +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.tencent.trtc.debug.Constant; + +/** + * TRTC视频通话的入口页面(可以设置房间id和用户id) + * + * - 可跳转TRTC视频通话页面{@link VideoCallingActivity} + */ + +/** + * Video Call Entrance View (set room ID and user ID) + * + * - Direct to the video call view: {@link VideoCallingActivity} + */ +public class VideoCallingEnterActivity extends AppCompatActivity { + + private EditText mEditInputUserId; + private EditText mEditInputRoomId; + + private int getId(String idName, String type) { + return getResources().getIdentifier(idName, type, getPackageName()); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); +// setContentView(R.layout.videocall_activit_enter); + setContentView(getId("videocall_activit_enter", "layout")); + getSupportActionBar().hide(); +// mEditInputUserId = findViewById(R.id.et_input_username); +// mEditInputRoomId = findViewById(R.id.et_input_room_id); + mEditInputUserId = findViewById(getId("et_input_username", "id")); + mEditInputRoomId = findViewById(getId("et_input_room_id", "id")); +// R.id.btn_enter_room + findViewById(getId("btn_enter_room", "id")).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startEnterRoom(); + } + }); +// R.id.rl_entrance_main + findViewById(getId("rl_entrance_main", "id")).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + hideInput(); + } + }); +// R.id.iv_back + findViewById(getId("iv_back", "id")).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + finish(); + } + }); + mEditInputRoomId.setText("1256732"); + String time = String.valueOf(System.currentTimeMillis()); + String userId = time.substring(time.length() - 8); + mEditInputUserId.setText(userId); + } + + private void startEnterRoom() { + if (TextUtils.isEmpty(mEditInputUserId.getText().toString().trim()) + || TextUtils.isEmpty(mEditInputRoomId.getText().toString().trim())) { +// R.string.videocall_room_input_error_tip + Toast.makeText(VideoCallingEnterActivity.this, getString(getId("videocall_room_input_error_tip", "string")), Toast.LENGTH_LONG).show(); + return; + } + Intent intent = new Intent(VideoCallingEnterActivity.this, VideoCallingActivity.class); + intent.putExtra(Constant.ROOM_ID, mEditInputRoomId.getText().toString().trim()); + intent.putExtra(Constant.USER_ID, mEditInputUserId.getText().toString().trim()); + startActivity(intent); + } + + protected void hideInput() { + InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); + View v = getWindow().peekDecorView(); + if (null != v) { + imm.hideSoftInputFromWindow(v.getWindowToken(), 0); + } + } + +} diff --git a/src/android/libs/trtc.gradle b/src/android/libs/trtc.gradle new file mode 100644 index 0000000..39a6773 --- /dev/null +++ b/src/android/libs/trtc.gradle @@ -0,0 +1,10 @@ +dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile 'androidx.appcompat:appcompat:1.1.0' + compile 'androidx.constraintlayout:constraintlayout:1.1.3' + compile 'com.android.support:multidex:1.0.1' + compile 'com.tencent.ilivesdk:ilivesdk:1.9.3' + compile 'com.tencent.ilivefilter:liteav_normal:1.1.21' + compile 'com.google.android.material:material:1.3.0' + compile 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release' +} diff --git a/src/android/liteavsdk.jar b/src/android/liteavsdk.jar new file mode 100644 index 0000000..ad86eb4 Binary files /dev/null and b/src/android/liteavsdk.jar differ diff --git a/src/android/res/drawable/common_button_bg.xml b/src/android/res/drawable/common_button_bg.xml new file mode 100644 index 0000000..afd55ca --- /dev/null +++ b/src/android/res/drawable/common_button_bg.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/src/android/res/drawable/common_edit_bg.xml b/src/android/res/drawable/common_edit_bg.xml new file mode 100644 index 0000000..e72ac4d --- /dev/null +++ b/src/android/res/drawable/common_edit_bg.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/android/res/drawable/common_seekbar_style.xml b/src/android/res/drawable/common_seekbar_style.xml new file mode 100644 index 0000000..2407a4d --- /dev/null +++ b/src/android/res/drawable/common_seekbar_style.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/android/res/drawable/common_seekbar_thumb.xml b/src/android/res/drawable/common_seekbar_thumb.xml new file mode 100644 index 0000000..070b130 --- /dev/null +++ b/src/android/res/drawable/common_seekbar_thumb.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/android/res/drawable/common_selector_radio_bg.xml b/src/android/res/drawable/common_selector_radio_bg.xml new file mode 100644 index 0000000..6512454 --- /dev/null +++ b/src/android/res/drawable/common_selector_radio_bg.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/src/android/res/drawable/videocall_background.xml b/src/android/res/drawable/videocall_background.xml new file mode 100644 index 0000000..cb7f0ee --- /dev/null +++ b/src/android/res/drawable/videocall_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/android/res/layout/videocall_activit_enter.xml b/src/android/res/layout/videocall_activit_enter.xml new file mode 100644 index 0000000..63ad531 --- /dev/null +++ b/src/android/res/layout/videocall_activit_enter.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +