├── .gitignore ├── README.md ├── package.json ├── plugin.xml ├── src ├── android │ ├── TencentMLVB.java │ ├── build.gradle │ ├── hooks │ │ └── before_plugin_install │ │ │ └── unzip_libs.sh │ ├── libs.zip │ └── res │ │ └── layout │ │ └── layout_video.xml └── ios │ ├── TXRTMPSDK.framework.zip │ ├── TencentMLVB.h │ ├── TencentMLVB.m │ └── hooks │ └── before_plugin_install │ └── unzip_libs.sh └── www └── mlvb.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio Navigation editor temp files 32 | .navigation/ 33 | 34 | # Android Studio captures folder 35 | captures/ 36 | 37 | # Intellij 38 | *.iml 39 | .idea/workspace.xml 40 | .idea/tasks.xml 41 | .idea/gradle.xml 42 | .idea/dictionaries 43 | .idea/libraries 44 | 45 | # Keystore files 46 | *.jks 47 | 48 | # External native build folder generated in Android Studio 2.2 and later 49 | .externalNativeBuild 50 | 51 | # Google Services (e.g. APIs or Firebase) 52 | google-services.json 53 | 54 | # Freeline 55 | freeline.py 56 | freeline/ 57 | freeline_project_description.json 58 | 59 | 60 | /.idea 61 | /src/android/libs 62 | /src/ios/TXRTMPSDK.framework 63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cordova Tencent MLVB Plugin 2 | =========================== 3 | 4 | 腾讯云《移动直播》 Cordova 插件 5 | ------------------------------- 6 | 7 | 本插件是腾讯云《移动直播》SDK 的 Cordova 封装。 8 | 9 | 腾讯云官方文档: 10 | 11 | ### 主要问题答疑 12 | 13 | **腾讯云移动直播的主要工作原理是怎样的?** 14 | 15 | 主要是实现了 rtmp 协议,通过手机 APP(主要是 iOS,Android)在手机平台上实现推流和播放的接口, 16 | 同时绑定了摄像头等硬件可以直接在界面上展示直播内容。 17 | 18 | **这个插件通过什么方式集成到 Cordova 应用中呢?** 19 | 20 | 这个插件可以通过 js 调用开启直播推流或者播放视频流,展示的界面元素会叠放在 CordovaWebView 的后面, 21 | 同时能够自动将 webView 设置成透明。webView 实现直播交互的功能组件即可。 22 | 23 | **支持的 rmtp 流 url 格式是怎样的?** 24 | 25 | 注册开通腾讯云的视频直播应用后,访问, 26 | 可以看到具体生成推流地址的逻辑。 27 | 28 | 推流地址格式: 29 | 30 | ``` 31 | rtmp://1234.livepush.myqcloud.com/live/1234_Room47?bizid=1234&txSecret=XXXXXX&txTime=XXXXXXXX 32 | ``` 33 | 34 | 其中 `bizid=1234` 四位数字是创建的应用编号,`Room47` 是房间号,可以根据需要自己生成。 35 | 36 | `txSecret` 是经过验签的推流秘钥串,看文档用简单的 md5 即可生成。`txTime` 是加密的时间串。 37 | 38 | 播放地址格式(支持 RTMP/FLV/HLS 三种协议): 39 | 40 | ``` 41 | rtmp://1234.liveplay.myqcloud.com/live/1234_Room47 42 | http://1234.liveplay.myqcloud.com/live/1234_Room47.flv 43 | http://1234.liveplay.myqcloud.com/live/1234_Room47.m3u8 44 | ``` 45 | 46 | ### 安装插件 47 | 48 | #### 方法一:直接命令行安装插件 49 | 50 | ``` 51 | cordova plugin add --save https://github.com/easecloud/cordova-plugin-tencent-mlvb.git 52 | ``` 53 | 54 | #### 方法二:手动添加 `config.xml` 配置 55 | 56 | ``` 57 | 59 | ``` 60 | 61 | 然后通过 `cordova prepare` 即可安装。 62 | 63 | #### 方法三:手动下载项目源码整个目录,放在 cordova 项目的 plugins 目录下 64 | 65 | ### API 66 | 67 | 使用 `window.TencentMLVB` 对象调用相关的方法。 68 | 69 | #### 开始直播 70 | 71 | ``` 72 | TencentMLVB.startPush(url, successCallback, errorCallback); 73 | ``` 74 | 75 | > 注意,为了使得背后的视频 UI 层能够显露出来,请务必把 HTML body 的背景设置为透明。 76 | 77 | > Android: 完成, iOS: 完成 78 | 79 | #### 结束直播 80 | 81 | ``` 82 | TencentMLVB.stopPush(); 83 | ``` 84 | 85 | > Android: 完成, iOS: 开发中 86 | 87 | #### 开始观看 88 | 89 | ``` 90 | TencentMLVB.startPush(url, playUrlType, successCallback, errorCallback); 91 | ``` 92 | 93 | 需要手动指定拉流的类型,`TencentMLVB.PLAY_URL_TYPE` 列举了相关的枚举值: 94 | 95 | ``` 96 | PLAY_URL_TYPE: { 97 | PLAY_TYPE_LIVE_RTMP: 0, // 传入的URL为RTMP直播地址 98 | PLAY_TYPE_LIVE_FLV: 1, // 传入的URL为FLV直播地址 99 | PLAY_TYPE_VOD_FLV: 2, // 传入的URL为RTMP点播地址 100 | PLAY_TYPE_VOD_HLS: 3, // 传入的URL为HLS(m3u8)点播地址 101 | PLAY_TYPE_VOD_MP4: 4, // 传入的URL为MP4点播地址 102 | PLAY_TYPE_LIVE_RTMP_ACC: 5, // 低延迟连麦链路直播地址(仅适合于连麦场景) 103 | PLAY_TYPE_LOCAL_VIDEO: 6 // 手机本地视频文件 104 | }, 105 | ``` 106 | 107 | 例如: 108 | 109 | ``` 110 | TencentMLVB.startPlay( 111 | 'http://1234.liveplay.myqcloud.com/live/1234_Room47.flv', 112 | TencentMLVB.PLAY_URL_TYPE.PLAY_TYPE_LIVE_FLV, 113 | function (msgSuccess) { 114 | // ... 115 | }, 116 | function (msgError) { 117 | // ... 118 | }, 119 | ) 120 | ``` 121 | 122 | #### 结束观看 123 | 124 | ``` 125 | TencentMLVB.stopPlay(); 126 | ``` 127 | 128 | > Android: 完成, iOS: 开发中 129 | 130 | #### \[开发中] 推流事件处理 onPushEvent 131 | 132 | #### \[开发中] 播放事件处理 onPlayEvent 133 | 134 | #### \[开发中] 设定清晰度 setVideoQuality 135 | 136 | #### \[计划V2] 设定美颜级别 setBeautyFilterDepth 137 | 138 | #### \[计划V2] 设定美白级别 setWhiteningFilterDepth 139 | 140 | #### \[计划V3] 设定滤镜 setFilter 141 | 142 | #### \[开发中] 切换镜头 switchCamera 143 | 144 | #### \[计划V3] 开关闪光灯 toggleTorch 145 | 146 | #### \[计划V4] 设置手动对焦位置 setFocusPosition 147 | 148 | #### \[计划V4] 设置视频水印 setWaterMark 149 | 150 | #### \[计划V2] 设置暂停时后台推流图片 setPaushImage 151 | 152 | #### \[计划V2] 修改推流屏幕的位置大小 resize 153 | 154 | #### \[开发中] 暂停播放 pause 155 | 156 | #### \[开发中] 继续播放 resume 157 | 158 | #### \[计划V2] 窗口适配 setRenderMode 159 | 160 | #### \[计划V2] 旋转屏幕 setRenderRotation 161 | 162 | #### \[计划V2] 修改播放进度 seek 163 | 164 | #### \[计划V2] 开关硬件加速 enableHWAcceleration 165 | 166 | #### \[计划V2] 开始录音 startRecord 167 | 168 | #### \[计划V2] 结束录音 stopRecord 169 | 170 | ### 更多 171 | 172 | 如何创建 Cordova 项目请看[文档](http://cordova.apache.org/docs/en/latest/guide/cli/index.html) 173 | 174 | Cordova 插件相关信息请看[文档](http://cordova.apache.org/docs/en/latest/guide/hybrid/plugins/index.html) 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-tencent-mlvb", 3 | "version": "0.0.1", 4 | "description": "Cordova Tencent MLVB Plugin", 5 | "cordova": { 6 | "id": "cordova-plugin-tencent-mlvb", 7 | "platforms": [ 8 | "android", 9 | "ios" 10 | ] 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Easecloud/cordova-plugin-tencent-mlvb" 15 | }, 16 | "keywords": [ 17 | "cordova", 18 | "live", 19 | "video", 20 | "tencent", 21 | "qq", 22 | "mlvb", 23 | "lvb", 24 | "cordova-android", 25 | "cordova-ios", 26 | "rtmp" 27 | ], 28 | "author": "Foshan EaseCloud Computer Technology Co., Ltd.", 29 | "license": "Apache-2.0", 30 | "engines": { 31 | "cordovaDependencies": { 32 | "2.0.0": { 33 | "cordova": ">100" 34 | } 35 | } 36 | }, 37 | "devDependencies": {}, 38 | "dependencies": {} 39 | } 40 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | TencentMLVB 6 | Tencent MLVB Plugin 7 | Apache 2.0 8 | cordova,live,video,tencent,qq,mlvb,lvb 9 | https://github.com/EaseCloud/cordova-plugin-tencent-mlvb.git 10 | https://github.com/EaseCloud/cordova-plugin-tencent-mlvb/issues 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 直播需要访问您的照相机 98 | 99 | 100 | 直播需要访问您的麦克风 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/android/TencentMLVB.java: -------------------------------------------------------------------------------- 1 | package cn.easecloud.cordova.tencent; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.content.Intent; 9 | import android.graphics.Color; 10 | import android.util.Log; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.ViewGroup; 14 | import android.view.ViewManager; 15 | import android.webkit.WebSettings; 16 | import android.webkit.WebView; 17 | import android.widget.FrameLayout; 18 | 19 | import org.apache.cordova.*; 20 | 21 | import org.json.JSONArray; 22 | import org.json.JSONObject; 23 | import org.json.JSONException; 24 | import org.json.JSONTokener; 25 | 26 | import com.google.gson.Gson; 27 | 28 | //import com.tencent.av.sdk.*; 29 | //import com.tencent.ilivesdk.*; 30 | //import com.tencent.ilivesdk.core.*; 31 | //import com.tencent.livesdk.*; 32 | 33 | import com.tencent.rtmp.*; 34 | import com.tencent.rtmp.ui.*; 35 | 36 | public class TencentMLVB extends CordovaPlugin { 37 | 38 | private Context context; 39 | private Activity activity; 40 | private CordovaInterface cordova; 41 | private CordovaWebView cordovaWebView; 42 | private ViewGroup rootView; 43 | private WebView webView; 44 | private WebSettings settings; 45 | private CallbackContext callbackContext; 46 | 47 | private TXCloudVideoView videoView = null; 48 | private TXLivePusher mLivePusher = null; 49 | private TXLivePlayer mLivePlayer = null; 50 | 51 | private String[] permissions = { 52 | Manifest.permission.INTERNET, 53 | Manifest.permission.ACCESS_NETWORK_STATE, 54 | Manifest.permission.ACCESS_WIFI_STATE, 55 | Manifest.permission.READ_PHONE_STATE, 56 | // Manifest.permission.CALL_PHONE, 57 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 58 | // Manifest.permission.READ_LOGS, 59 | Manifest.permission.RECORD_AUDIO, 60 | Manifest.permission.CAMERA 61 | }; 62 | 63 | /** 64 | * Sets the context of the Command. This can then be used to do things like 65 | * get file paths associated with the Activity. 66 | * 67 | * @param cordova The context of the main Activity. 68 | * @param webView The CordovaWebView Cordova is running in. 69 | */ 70 | @Override 71 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 72 | super.initialize(cordova, webView); 73 | this.cordovaWebView = webView; 74 | this.cordova = cordova; 75 | this.activity = cordova.getActivity(); 76 | this.context = this.activity.getApplicationContext(); 77 | this.rootView = (ViewGroup) activity.findViewById(android.R.id.content); 78 | this.webView = (WebView) rootView.getChildAt(0); 79 | } 80 | 81 | /** 82 | * Executes the request and returns PluginResult. 83 | * 84 | * @param action The action to execute. 85 | * @param args JSONArry of arguments for the plugin. 86 | * @param callbackContext The callback id used when calling back into JavaScript. 87 | * @return True if the action was valid, false if not. 88 | */ 89 | @Override 90 | public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException { 91 | 92 | if (!hasPermisssion()) { 93 | requestPermissions(0); 94 | } 95 | 96 | this.callbackContext = callbackContext; 97 | 98 | if (action.equals("getVersion")) { 99 | return getVersion(callbackContext); 100 | } else if (action.equals("startPush")) { 101 | final String url = args.getString(0); 102 | return startPush(url, callbackContext); 103 | } else if (action.equals("stopPush")) { 104 | return stopPush(callbackContext); 105 | } else if (action.equals("onPushEvent")) { 106 | alert("尚未实现"); 107 | } else if (action.equals("startPlay")) { 108 | final String url = args.getString(0); 109 | final int playType = args.getInt(1); 110 | return startPlay(url, playType, callbackContext); 111 | } else if (action.equals("stopPlay")) { 112 | return stopPlay(callbackContext); 113 | } else if (action.equals("onPlayEvent")) { 114 | alert("尚未实现"); 115 | } else if (action.equals("setVideoQuality")) { 116 | if (mLivePusher == null) return false; 117 | final int quality = args.getInt(0); 118 | final int adjustBitrate = args.getInt(1); 119 | final int adjustResolution = args.getInt(2); 120 | // mLivePusher.setVideoQuality(quality, adjustBitrate, adjustResolution); 121 | mLivePusher.setVideoQuality(quality); 122 | } else if (action.equals("setBeautyFilterDepth")) { 123 | if (mLivePusher == null) return false; 124 | final int beautyDepth = args.getInt(0); 125 | final int whiteningDepth = args.getInt(1); 126 | mLivePusher.setBeautyFilter(beautyDepth, whiteningDepth); 127 | } else if (action.equals("setExposureCompensation")) { 128 | // TODO: 尚未测试 129 | if (mLivePusher == null) return false; 130 | final float depth = (float) args.getDouble(0); 131 | mLivePusher.setExposureCompensation(depth); 132 | } else if (action.equals("setFilter")) { 133 | alert("尚未实现"); 134 | } else if (action.equals("switchCamera")) { 135 | if (mLivePusher == null) return false; 136 | mLivePusher.switchCamera(); 137 | } else if (action.equals("toggleTorch")) { 138 | // TODO: 尚未测试 139 | if (mLivePusher == null) return false; 140 | final boolean enabled = args.getBoolean(0); 141 | mLivePusher.turnOnFlashLight(enabled); 142 | } else if (action.equals("setFocusPosition")) { 143 | alert("尚未实现"); 144 | } else if (action.equals("setWaterMark")) { 145 | alert("尚未实现"); 146 | } else if (action.equals("setPauseImage")) { 147 | alert("尚未实现"); 148 | } else if (action.equals("resize")) { 149 | alert("尚未实现"); 150 | } else if (action.equals("pause")) { 151 | alert("尚未实现"); 152 | } else if (action.equals("resume")) { 153 | alert("尚未实现"); 154 | } else if (action.equals("setRenderMode")) { 155 | alert("尚未实现"); 156 | } else if (action.equals("setRenderRotation")) { 157 | alert("尚未实现"); 158 | } else if (action.equals("enableHWAcceleration")) { 159 | alert("尚未实现"); 160 | } else if (action.equals("startRecord")) { 161 | // return startRecord(callbackContext); 162 | } else if (action.equals("stopRecord")) { 163 | // return stopRecord(callbackContext); 164 | } else if (action.equals("fixMinFontSize")) { 165 | // return stopRecord(callbackContext); 166 | return fixMinFontSize(callbackContext); 167 | } 168 | 169 | callbackContext.error("Undefined action: " + action); 170 | return false; 171 | 172 | } 173 | 174 | @Override 175 | public void onActivityResult(int requestCode, int resultCode, Intent intent) { 176 | String statusCode; 177 | switch (requestCode) { 178 | case 990: // demoPush 179 | if (resultCode == 1) { 180 | statusCode = "success"; 181 | callbackContext.success(statusCode); 182 | } 183 | break; 184 | default: 185 | break; 186 | } 187 | } 188 | 189 | /** 190 | * 在当前 Activity 底部 UI 层注册一个 TXCloudVideoView 以供直播渲染 191 | */ 192 | private void prepareVideoView() { 193 | if (videoView != null) return; 194 | // 通过 layout 文件插入 videoView 195 | LayoutInflater layoutInflater = LayoutInflater.from(activity); 196 | videoView = (TXCloudVideoView) layoutInflater.inflate(_R("layout", "layout_video"), null); 197 | // 设置 webView 透明 198 | videoView.setLayoutParams(new FrameLayout.LayoutParams( 199 | FrameLayout.LayoutParams.FILL_PARENT, 200 | FrameLayout.LayoutParams.FILL_PARENT 201 | )); 202 | // 插入视图 203 | rootView.addView(videoView); 204 | videoView.setVisibility(View.VISIBLE); 205 | // 设置 webView 透明 206 | webView.setBackgroundColor(Color.TRANSPARENT); 207 | // 关闭 webView 的硬件加速(否则不能透明) 208 | webView.setLayerType(WebView.LAYER_TYPE_SOFTWARE, null); 209 | // 将 webView 提到顶层 210 | webView.bringToFront(); 211 | } 212 | 213 | /** 214 | * 销毁 videoView 215 | */ 216 | private void destroyVideoView() { 217 | if (videoView == null) return; 218 | videoView.onDestroy(); 219 | rootView.removeView(videoView); 220 | videoView = null; 221 | // 把 webView 变回白色 222 | webView.setBackgroundColor(Color.WHITE); 223 | } 224 | 225 | /** 226 | * 返回 MLVB SDK 版本字符串 227 | * 228 | * @param callbackContext 229 | * @return 230 | */ 231 | private boolean getVersion(final CallbackContext callbackContext) { 232 | int[] sdkver = TXLivePusher.getSDKVersion(); 233 | if (sdkver != null && sdkver.length > 0) { 234 | String ver = "" + sdkver[0]; 235 | for (int i = 1; i < sdkver.length; ++i) { 236 | ver += "." + sdkver[i]; 237 | } 238 | callbackContext.success(ver); 239 | return true; 240 | } 241 | callbackContext.error("Cannot get rtmp sdk version."); 242 | return false; 243 | } 244 | 245 | /** 246 | * 设置最小字号 247 | * 248 | * @param callbackContext 249 | * @return 250 | */ 251 | private boolean fixMinFontSize(final CallbackContext callbackContext) { 252 | try { 253 | settings = ((WebView) cordovaWebView.getEngine().getView()).getSettings(); 254 | settings.setMinimumFontSize(1); 255 | settings.setMinimumLogicalFontSize(1); 256 | } catch (Exception error) { 257 | callbackContext.error("10003"); 258 | return false; 259 | } 260 | return true; 261 | } 262 | 263 | /** 264 | * 开始推流,并且在垫底的 videoView 显示视频 265 | * 会在当前对象上下文注册一个 TXLivePusher 266 | * 267 | * @param url 推流URL 268 | * @param callbackContext 269 | * @return 270 | */ 271 | private boolean startPush(final String url, final CallbackContext callbackContext) { 272 | if (mLivePusher != null) { 273 | callbackContext.error("10002"); 274 | return false; 275 | } 276 | // 准备 videoView,没有的话生成 277 | activity.runOnUiThread(new Runnable() { 278 | public void run() { 279 | prepareVideoView(); 280 | // 开始推流 281 | mLivePusher = new TXLivePusher(activity); 282 | TXLivePushConfig mLivePushConfig = new TXLivePushConfig(); 283 | mLivePusher.setConfig(mLivePushConfig); 284 | mLivePusher.startPusher(url); 285 | // 将视频绑定到 videoView 286 | mLivePusher.startCameraPreview(videoView); 287 | } 288 | }); 289 | return true; 290 | } 291 | 292 | /** 293 | * 停止推流,并且注销 mLivePusher 对象 294 | * 295 | * @param callbackContext 296 | * @return 297 | */ 298 | private boolean stopPush(final CallbackContext callbackContext) { 299 | if (mLivePusher == null) { 300 | callbackContext.error("10003"); 301 | return false; 302 | } 303 | activity.runOnUiThread(new Runnable() { 304 | public void run() { 305 | // 停止摄像头预览 306 | mLivePusher.stopCameraPreview(true); 307 | // 停止推流 308 | mLivePusher.stopPusher(); 309 | // 解绑 Listener 310 | mLivePusher.setPushListener(null); 311 | // 移除 pusher 引用 312 | mLivePusher = null; 313 | // 销毁 videoView 314 | destroyVideoView(); 315 | } 316 | }); 317 | return true; 318 | } 319 | 320 | /** 321 | * 开始播放,在垫底的 videoView 显示视频 322 | * 会在当前对象上下文注册一个 TXLivePlayer 323 | * 324 | * @param url 播放URL 325 | * @param playType 播放类型,参见 mlvb.js 相关的枚举定义 326 | * @param callbackContext 327 | * @return 328 | */ 329 | private boolean startPlay(final String url, final int playType, final CallbackContext callbackContext) { 330 | if (mLivePlayer != null) { 331 | callbackContext.error("10004"); 332 | return false; 333 | } 334 | // 准备 videoView,没有的话生成 335 | activity.runOnUiThread(new Runnable() { 336 | public void run() { 337 | prepareVideoView(); 338 | // 开始推流 339 | mLivePlayer = new TXLivePlayer(activity); 340 | TXLivePushConfig mLivePushConfig = new TXLivePushConfig(); 341 | // 将视频绑定到 videoView 342 | mLivePlayer.setPlayerView(videoView); 343 | mLivePlayer.startPlay(url, playType); 344 | } 345 | }); 346 | return true; 347 | } 348 | 349 | /** 350 | * 停止推流,并且注销 mLivePlay 对象 351 | * 352 | * @param callbackContext 353 | * @return 354 | */ 355 | private boolean stopPlay(final CallbackContext callbackContext) { 356 | if (mLivePlayer == null) { 357 | callbackContext.error("10005"); 358 | return false; 359 | } 360 | activity.runOnUiThread(new Runnable() { 361 | public void run() { 362 | // 停止播放 363 | mLivePlayer.stopPlay(true); 364 | // 销毁 videoView 365 | destroyVideoView(); 366 | // 移除 pusher 引用 367 | mLivePlayer = null; 368 | } 369 | }); 370 | return true; 371 | } 372 | 373 | // /** 374 | // * 开始录制 375 | // * 376 | // * @param callbackContext 377 | // * @return 378 | // */ 379 | // private boolean startRecord(final CallbackContext callbackContext) { 380 | // if (mLivePlayer != null) { 381 | // callbackContext.error("10006"); 382 | // return false; 383 | // } 384 | // activity.runOnUiThread(new Runnable() { 385 | // public void run() { 386 | // //指定一个 ITXVideoRecordListener 用于同步录制的进度和结果 387 | // mLivePlayer.setVideoRecordListener(recordListener); 388 | // //启动录制,目前只支持录制视频源,弹幕消息等等目前还不支持 389 | // mLivePlayer.startRecord(int recordType); 390 | // } 391 | // }); 392 | // return true; 393 | // } 394 | 395 | // /** 396 | // * 结束录制 397 | // * 398 | // * @param callbackContext 399 | // * @return 400 | // */ 401 | // private boolean stopRecord(final CallbackContext callbackContext) { 402 | // if (mLivePlayer != null) { 403 | // callbackContext.error("10007"); 404 | // return false; 405 | // } 406 | // activity.runOnUiThread(new Runnable() { 407 | // public void run() { 408 | // mLivePlayer.stopRecord(); 409 | // } 410 | // }); 411 | // return true; 412 | // } 413 | 414 | /** 415 | * check application's permissions 416 | */ 417 | public boolean hasPermisssion() { 418 | for (String p : permissions) { 419 | if (!PermissionHelper.hasPermission(this, p)) { 420 | return false; 421 | } 422 | } 423 | return true; 424 | } 425 | 426 | /** 427 | * We override this so that we can access the permissions variable, which no longer exists in 428 | * the parent class, since we can't initialize it reliably in the constructor! 429 | * 430 | * @param requestCode The code to get request action 431 | */ 432 | public void requestPermissions(int requestCode) { 433 | PermissionHelper.requestPermissions(this, requestCode, permissions); 434 | } 435 | 436 | public void alert(String msg, String title) { 437 | new AlertDialog.Builder(this.activity) 438 | .setTitle(title) 439 | .setMessage(msg)//设置显示的内容 440 | .setPositiveButton("确定", new DialogInterface.OnClickListener() {//添加确定按钮 441 | @Override 442 | public void onClick(DialogInterface dialog, int which) {//确定按钮的响应事件 443 | // TODO Auto-generated method stub 444 | // finish(); 445 | } 446 | }).show();//在按键响应事件中显示此对话框 447 | } 448 | 449 | public void alert(String msg) { 450 | alert(msg, "系统提示"); 451 | } 452 | 453 | public String jsonEncode(Object obj) { 454 | Gson gson = new Gson(); 455 | return gson.toJson(obj); 456 | } 457 | 458 | public int _R(String defType, String name) { 459 | return activity.getApplication().getResources().getIdentifier( 460 | name, defType, activity.getApplication().getPackageName()); 461 | } 462 | 463 | // public static void printViewHierarchy(ViewGroup vg, String prefix) { 464 | // for (int i = 0; i < vg.getChildCount(); i++) { 465 | // View v = vg.getChildAt(i); 466 | // String desc = prefix + " | " + "[" + i + "/" + (vg.getChildCount() - 1) + "] " + v.getClass().getSimpleName() + " " + v.getId(); 467 | // Log.v("x", desc); 468 | // 469 | // if (v instanceof ViewGroup) { 470 | // printViewHierarchy((ViewGroup) v, desc); 471 | // } 472 | // } 473 | // } 474 | 475 | 476 | } 477 | -------------------------------------------------------------------------------- /src/android/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | jcenter() 3 | } 4 | 5 | dependencies { 6 | // compile 'com.android.support:appcompat-v7:25.0.1' 7 | // compile 'com.facebook.android:facebook-android-sdk:4.8.1' 8 | compile 'com.google.code.gson:gson:+' 9 | compile 'com.squareup.okhttp3:okhttp:3.6.+' 10 | compile 'com.squareup.okio:okio:+' 11 | // compile 'com.google.android.gms:play-services-auth:10.0.1' 12 | // compile 'com.google.android.gms:play-services-games:10.0.1' 13 | // compile 'com.android.support:support-v4:25.1.0' 14 | // compile 'com.android.support:appcompat-v7:25.1.0' 15 | } 16 | 17 | android { 18 | packagingOptions { 19 | exclude 'META-INF/NOTICE' 20 | exclude 'META-INF/LICENSE' 21 | } 22 | } -------------------------------------------------------------------------------- /src/android/hooks/before_plugin_install/unzip_libs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0) 4 | cd ../.. 5 | 6 | if [ ! -e libs ]; then unzip libs.zip; fi 7 | if [ -e libs.zip ]; then rm libs.zip; fi 8 | -------------------------------------------------------------------------------- /src/android/libs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EaseCloud/cordova-plugin-tencent-mlvb/3e23172d13acacf86b0ed77a0cd57dc4a33c3a16/src/android/libs.zip -------------------------------------------------------------------------------- /src/android/res/layout/layout_video.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | -------------------------------------------------------------------------------- /src/ios/TXRTMPSDK.framework.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EaseCloud/cordova-plugin-tencent-mlvb/3e23172d13acacf86b0ed77a0cd57dc4a33c3a16/src/ios/TXRTMPSDK.framework.zip -------------------------------------------------------------------------------- /src/ios/TencentMLVB.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "TXRTMPSDK/TXLivePush.h" 3 | #import "TXRTMPSDK/TXLivePlayer.h" 4 | 5 | @interface TencentMLVB : CDVPlugin 6 | 7 | @property UIView* videoView; 8 | @property TXLivePush* livePusher; 9 | @property TXLivePlayer* livePlayer; 10 | 11 | - (void) getVersion:(CDVInvokedUrlCommand*)command; 12 | - (void) prepareVideoView; 13 | - (void) destroyVideoView; 14 | - (void) startPush:(CDVInvokedUrlCommand*)command; 15 | - (void) stopPush:(CDVInvokedUrlCommand*)command; 16 | - (void) startPlay:(CDVInvokedUrlCommand*)command; 17 | - (void) stopPlay:(CDVInvokedUrlCommand*)command; 18 | - (void) setVideoQuality:(CDVInvokedUrlCommand*)command; 19 | - (void) setBeautyFilterDepth:(CDVInvokedUrlCommand*)command; 20 | - (void) setFilter:(CDVInvokedUrlCommand*)command; 21 | - (void) switchCamera:(CDVInvokedUrlCommand*)command; 22 | - (void) toggleTorch:(CDVInvokedUrlCommand*)command; 23 | - (void) setFocusPosition:(CDVInvokedUrlCommand*)command; 24 | - (void) setWaterMark:(CDVInvokedUrlCommand*)command; 25 | - (void) setPauseImage:(CDVInvokedUrlCommand*)command; 26 | - (void) pause:(CDVInvokedUrlCommand*)command; 27 | - (void) resume:(CDVInvokedUrlCommand*)command; 28 | - (void) setRenderMode:(CDVInvokedUrlCommand*)command; 29 | - (void) setRenderRotation:(CDVInvokedUrlCommand*)command; 30 | - (void) seek:(CDVInvokedUrlCommand*)command; 31 | - (void) enableHWAcceleration:(CDVInvokedUrlCommand*)command; 32 | - (void) startRecord:(CDVInvokedUrlCommand*)command; 33 | - (void) stopRecord:(CDVInvokedUrlCommand*)command; 34 | - (void) alert:(NSString*)message title:(NSString*)title; 35 | - (void) alert:(NSString*)message; 36 | 37 | @end -------------------------------------------------------------------------------- /src/ios/TencentMLVB.m: -------------------------------------------------------------------------------- 1 | #import "TencentMLVB.h" 2 | 3 | #import "MainViewController.h" 4 | 5 | @implementation MainViewController(CDVViewController) 6 | - (void) viewDidLoad { 7 | [super viewDidLoad]; 8 | self.webView.backgroundColor = [UIColor clearColor]; 9 | self.webView.opaque = NO; 10 | } 11 | @end 12 | 13 | 14 | @implementation TencentMLVB 15 | 16 | @synthesize videoView; 17 | @synthesize livePusher; 18 | @synthesize livePlayer; 19 | 20 | //- (void) greet:(CDVInvokedUrlCommand*)command { 21 | // NSString* name = [[command arguments] objectAtIndex:0]; 22 | // NSString* msg = [NSString stringWithFormat: @"Hello, %@", name]; 23 | // CDVPluginResult* result = [CDVPluginResult 24 | // resultWithStatus:CDVCommandStatus_OK 25 | // messageAsString:msg]; 26 | // [self alert:msg]; 27 | // [self.commandDelegate sendPluginResult:result callbackId:command.callbackId]; 28 | //} 29 | 30 | - (void) prepareVideoView { 31 | if (self.videoView) return; 32 | self.videoView = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]]; 33 | [self.webView.superview addSubview:self.videoView]; 34 | [self.webView.superview bringSubviewToFront:self.webView]; 35 | } 36 | 37 | - (void) destroyVideoView { 38 | if (!self.videoView) return; 39 | [self.videoView removeFromSuperview]; 40 | self.videoView = nil; 41 | // 把 webView 变回白色 42 | // [self.webView setBackgroundColor:[UIColor whiteColor]]; 43 | } 44 | 45 | - (void) getVersion:(CDVInvokedUrlCommand*)command { 46 | NSString* version = [[TXLivePush getSDKVersion] componentsJoinedByString:@"."]; 47 | CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:version]; 48 | [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; 49 | } 50 | 51 | - (void) startPush:(CDVInvokedUrlCommand*)command { 52 | if (self.livePusher) return; 53 | NSString* url = [command.arguments objectAtIndex:0]; 54 | [self prepareVideoView]; 55 | TXLivePushConfig* _config = [[TXLivePushConfig alloc] init]; 56 | self.livePusher = [[TXLivePush alloc] initWithConfig: _config]; 57 | [self.livePusher startPreview:videoView]; 58 | [self.livePusher startPush:url]; 59 | } 60 | 61 | - (void) stopPush:(CDVInvokedUrlCommand*)command { 62 | if (!self.livePusher) return; 63 | [self.livePusher stopPreview]; 64 | [self.livePusher stopPush]; 65 | self.livePusher.delegate = nil; 66 | self.livePusher = nil; 67 | [self destroyVideoView]; 68 | } 69 | 70 | - (void) startPlay:(CDVInvokedUrlCommand*)command { 71 | if (self.livePlayer) return; 72 | NSString* url = [command.arguments objectAtIndex:0]; 73 | // TX_Enum_PlayType playUrlType = (TX_Enum_PlayType)[command.arguments objectAtIndex:1]; 74 | // NSInteger playUrlType = (NSInteger)[command.arguments objectAtIndex:1]; 75 | 76 | [self prepareVideoView]; 77 | 78 | self.livePlayer = [[TXLivePlayer alloc] init]; 79 | [self.livePlayer setupVideoWidget:CGRectMake(0, 0, 0, 0) containView:videoView insertIndex:0]; 80 | [self.livePlayer startPlay:url type:PLAY_TYPE_LIVE_FLV]; 81 | } 82 | 83 | - (void) stopPlay:(CDVInvokedUrlCommand*)command { 84 | if (!self.livePlayer) return; 85 | [self.livePlayer stopPlay]; 86 | [self.livePlayer removeVideoWidget]; 87 | self.livePlayer = nil; 88 | [self destroyVideoView]; 89 | } 90 | 91 | - (void) setVideoQuality:(CDVInvokedUrlCommand*)command { 92 | } 93 | 94 | - (void) setBeautyFilterDepth:(CDVInvokedUrlCommand*)command { 95 | } 96 | 97 | - (void) setWhiteningFilterDepth:(CDVInvokedUrlCommand*)command { 98 | } 99 | 100 | - (void) setFilter:(CDVInvokedUrlCommand*)command { 101 | } 102 | 103 | - (void) switchCamera:(CDVInvokedUrlCommand*)command { 104 | if (!self.livePusher) return; 105 | [self.livePusher switchCamera]; 106 | } 107 | 108 | - (void) toggleTorch:(CDVInvokedUrlCommand*)command { 109 | } 110 | 111 | - (void) setFocusPosition:(CDVInvokedUrlCommand*)command { 112 | } 113 | 114 | - (void) setWaterMark:(CDVInvokedUrlCommand*)command { 115 | } 116 | 117 | - (void) setPauseImage:(CDVInvokedUrlCommand*)command { 118 | } 119 | 120 | - (void) resize:(CDVInvokedUrlCommand*)command { 121 | } 122 | 123 | - (void) pause:(CDVInvokedUrlCommand*)command { 124 | } 125 | 126 | - (void) resume:(CDVInvokedUrlCommand*)command { 127 | } 128 | 129 | - (void) setRenderMode:(CDVInvokedUrlCommand*)command { 130 | } 131 | 132 | - (void) setRenderRotation:(CDVInvokedUrlCommand*)command { 133 | } 134 | 135 | - (void) seek:(CDVInvokedUrlCommand*)command { 136 | } 137 | 138 | - (void) enableHWAcceleration:(CDVInvokedUrlCommand*)command { 139 | } 140 | 141 | - (void) startRecord:(CDVInvokedUrlCommand*)command { 142 | } 143 | 144 | - (void) stopRecord:(CDVInvokedUrlCommand*)command { 145 | } 146 | 147 | - (void) alert:(NSString*)message title:(NSString*)title { 148 | UIAlertView* alert = [ 149 | [UIAlertView alloc] 150 | initWithTitle:title 151 | message:message 152 | delegate:nil 153 | cancelButtonTitle:@"OK" 154 | otherButtonTitles:nil 155 | ]; 156 | [alert show]; 157 | //[alert release]; 158 | } 159 | 160 | - (void) alert:(NSString*)message { 161 | [self alert:message title:@"系统消息"]; 162 | } 163 | 164 | @end 165 | -------------------------------------------------------------------------------- /src/ios/hooks/before_plugin_install/unzip_libs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cd $(dirname $0) 4 | cd ../.. 5 | 6 | if [ ! -e TXRTMPSDK.framework ]; then unzip TXRTMPSDK.framework.zip; fi 7 | if [ -e TXRTMPSDK.framework.zip ]; then rm TXRTMPSDK.framework.zip; fi 8 | -------------------------------------------------------------------------------- /www/mlvb.js: -------------------------------------------------------------------------------- 1 | /*global cordova, module*/ 2 | 3 | module.exports = { 4 | 5 | // 错误处理 6 | ERROR: { 7 | // 10001: { msg: '开启推流失败:视图 videoView 已存在' }, 8 | 10002: { msg: '开启推流失败:pusher 已存在' }, 9 | 10003: { msg: '停止推流失败:pusher 不存在' }, 10 | 10004: { msg: '停止播放失败:player 已存在' }, 11 | 10005: { msg: '停止播放失败:player 不存在' }, 12 | }, 13 | 14 | // 获取 SDK 版本号 15 | getVersion: function(successCallback, errorCallback) { 16 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "getVersion", []); 17 | }, 18 | 19 | // 设置最小字号以解决兼容 BUG 20 | fixMinFontSize: function(successCallback, errorCallback) { 21 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "fixMinFontSize", []); 22 | }, 23 | 24 | // 推流类方法 25 | startPush: function(url, successCallback, errorCallback) { 26 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "startPush", [url]); 27 | }, 28 | 29 | stopPush: function(successCallback, errorCallback) { 30 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "stopPush", []); 31 | }, 32 | 33 | // 推流事件处理 34 | // https://cloud.tencent.com/document/product/454/7879#.E4.BA.8B.E4.BB.B6.E5.A4.84.E7.90.86 35 | onPushEvent: function(eventCallback) { 36 | }, 37 | 38 | // 播放类方法 39 | 40 | PLAY_URL_TYPE: { 41 | PLAY_TYPE_LIVE_RTMP: 0, // 传入的URL为RTMP直播地址 42 | PLAY_TYPE_LIVE_FLV: 1, // 传入的URL为FLV直播地址 43 | PLAY_TYPE_VOD_FLV: 2, // 传入的URL为RTMP点播地址 44 | PLAY_TYPE_VOD_HLS: 3, // 传入的URL为HLS(m3u8)点播地址 45 | PLAY_TYPE_VOD_MP4: 4, // 传入的URL为MP4点播地址 46 | PLAY_TYPE_LIVE_RTMP_ACC: 5, // 低延迟连麦链路直播地址(仅适合于连麦场景) 47 | PLAY_TYPE_LOCAL_VIDEO: 6 // 手机本地视频文件 48 | }, 49 | 50 | startPlay: function(url, playUrlType, successCallback, errorCallback) { 51 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "startPlay", [url, playUrlType]); 52 | }, 53 | 54 | stopPlay: function(successCallback, errorCallback) { 55 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "stopPlay", []); 56 | }, 57 | 58 | // 播放事件处理 59 | // https://cloud.tencent.com/document/product/454/7880#.E7.8A.B6.E6.80.81.E7.9B.91.E5.90.AC 60 | onPlayEvent: function(eventCallback) { 61 | }, 62 | 63 | // 调整类方法 64 | 65 | // 推流 66 | // 设定清晰度 67 | // https://cloud.tencent.com/document/product/454/7879#step-4.3A-.E8.AE.BE.E5.AE.9A.E6.B8.85.E6.99.B0.E5.BA.A6 68 | setVideoQuality: function(quality, adjustBitrate, adjustResolution, 69 | successCallback, errorCallback) { 70 | cordova.exec( 71 | successCallback, errorCallback, "TencentMLVB", "setVideoQuality", 72 | [quality, adjustBitrate, adjustResolution] 73 | ); 74 | }, 75 | 76 | // 推流 77 | // 设定美颜级别 78 | // beautyDepth: 美颜级别 0-9,0 为关闭 79 | // whiteningDepth: 美白级别 0-9,0 为关闭 80 | setBeautyFilterDepth: function(beautyDepth, whiteningDepth, successCallback, errorCallback) { 81 | cordova.exec( 82 | successCallback, errorCallback, "TencentMLVB", "setBeautyFilterDepth", 83 | [beautyDepth, whiteningDepth] 84 | ); 85 | }, 86 | 87 | // 推流 88 | // 设定曝光级别 89 | // depth: -1 ~ 1 的浮点数 90 | setExposureCompensation: function(depth, successCallback, errorCallback) { 91 | cordova.exec( 92 | successCallback, errorCallback, "TencentMLVB", "setExposureCompensation", 93 | [depth] 94 | ); 95 | }, 96 | 97 | // 推流 98 | // 设定滤镜 99 | setFilter: function(filterUrl, successCallback, errorCallback) { 100 | cordova.exec( 101 | successCallback, errorCallback, "TencentMLVB", "setFilter", 102 | [filterUrl] 103 | ); 104 | }, 105 | 106 | // 推流 107 | // 切换前后置镜头,调用一次切换一次 108 | // callback 返回调用之后的镜头是 front 还是 back 109 | switchCamera: function(successCallback, errorCallback) { 110 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "switchCamera", []); 111 | }, 112 | 113 | // 推流 114 | // 开启/关闭闪光灯 115 | toggleTorch: function(enabled, successCallback, errorCallback) { 116 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "toggleTorch", [enabled]); 117 | }, 118 | 119 | // 推流 120 | // 设置手动对焦位置(输入百分比坐标值) 121 | setFocusPosition: function(top, left, successCallback, errorCallback) { 122 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "setFocusPosition", [top, left]); 123 | }, 124 | 125 | // 推流 126 | // 设置视频水印 127 | setWaterMark: function(imgUrl, top, left, successCallback, errorCallback) { 128 | cordova.exec( 129 | successCallback, errorCallback, "TencentMLVB", "setWaterMark", 130 | [imgUrl, top, left] 131 | ); 132 | }, 133 | 134 | // 推流 135 | // 设置暂停时的后台推流图片 136 | setPauseImage: function(imgUrl, successCallback, errorCallback) { 137 | cordova.exec( 138 | successCallback, errorCallback, "TencentMLVB", "setPauseImage", 139 | [imgUrl] 140 | ); 141 | }, 142 | 143 | // 推流 + 播放 144 | // 调整播放或者推流屏幕的大小,支持 vw, vh 两种单位,表示 1 单位的宽度百分比和 1 单位的高度百分比 145 | resize: function(top, left, width, height, successCallback, errorCallback) { 146 | cordova.exec( 147 | successCallback, errorCallback, "TencentMLVB", "resizePlay", 148 | [top, left, width, height] 149 | ); 150 | }, 151 | 152 | pause: function(successCallback, errorCallback) { 153 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "pausePlay", []); 154 | }, 155 | 156 | resume: function(successCallback, errorCallback) { 157 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "resumePlay", []); 158 | }, 159 | 160 | RENDER_MODE: { 161 | FILL_SCREEN: 'RENDER_MODE_FILL_SCREEN', // css cover 162 | FILL_EDGE: 'RENDER_MODE_FILL_EDGE', // css contain 163 | }, 164 | 165 | setRenderMode: function(mode, successCallback, errorCallback) { 166 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "setRenderMode", [mode]); 167 | }, 168 | 169 | ROTATION_MODE: { 170 | LANDSCAPE: 'LANDSCAPE', // 横屏 171 | PORTRAIT: 'PORTRAIT', // 竖屏 172 | }, 173 | 174 | setRenderRotation: function(mode, successCallback, errorCallback) { 175 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "setRenderRotation", [mode]); 176 | }, 177 | 178 | seek: function(progress, successCallback, errorCallback) { 179 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "seekPlay", [progress]); 180 | }, 181 | 182 | // 推流 + 播放 183 | enableHWAcceleration: function(enabled, successCallback, errorCallback) { 184 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "enableHWAcceleration", [enabled]); 185 | }, 186 | 187 | startRecord: function(successCallback, errorCallback) { 188 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "startRecord", []); 189 | }, 190 | 191 | stopRecord: function(successCallback, errorCallback) { 192 | cordova.exec(successCallback, errorCallback, "TencentMLVB", "stopRecord", []); 193 | }, 194 | 195 | }; 196 | --------------------------------------------------------------------------------