Android 即时音视频解决方案2——腾讯云
上一篇文章介绍了环信的解决方案,见Android 即时音视频解决方案1——环信,这篇文章,介绍一下更加靠谱,也就是腾讯云的解决方案,毕竟腾讯是是这方面的头头,比较靠谱。当然,集成腾讯云比集成环信稍微复杂那么一点,需要有一点点的耐心。官方地址音视频云通信 AVCSDK下载AV Andriod1.3文档地址音视频云通讯先讲讲腾讯云的原理,使用腾讯云的时候,要有一个账号体系,这个账号体系比较灵活,可以使
上一篇文章介绍了环信的解决方案,见Android 即时音视频解决方案1——环信,这篇文章,介绍一下更加靠谱,也就是腾讯云的解决方案,毕竟腾讯是是这方面的头头,比较靠谱。当然,集成腾讯云比集成环信稍微复杂那么一点,需要有一点点的耐心。
官方地址音视频云通信 AVC
SDK下载AV Andriod1.3
文档地址音视频云通讯
先讲讲腾讯云的原理,使用腾讯云的时候,要有一个账号体系,这个账号体系比较灵活,可以使用独立模式也可以只用第三方账号体系,这里使用独立模式。
使用独立模式,要使用腾讯云的服务的时候,我们无需将用户的账号密码同步到腾讯,但是我们的服务端需要进行一定的处理。 用户在APP客户端输入帐号密码后到APP自有帐号登录服务器验证,验证成功后自有帐号登录服务器使用私钥派发签名(sig)给客户端;客户端提交用户帐号和私钥签名IM云(通过IMSDK或者音视频SDK接口),验证签名成功后向终端派发相应票据,进而使用IM云服务。
- 开发者需要保证私钥安全,腾讯完全信赖私钥签名;
- 签名有效期由开发者指定,并加密在私钥签名中;
- 该集成方式无需将APP自有帐号的帐号密码同步腾讯,最大程度保证开发者用户数据的私密性。
下面贴出官方文档的一个例子说明
假设开发者开发的APP为天天互动,天天互动自有服务器(开发自己开发)支持用户注册功能,同时天天互动接入了腾讯音视频通讯服务。某一天用户A在天天互动注册了帐号,登录天天互动后使用腾讯的音视频服务与家人视频。那么天天互动是如何实现这一功能呢?下面描述具体登录流程:
- 用户A在天天互动的客户端输入自己的帐号及密码后,客户端传给天天互动的帐号服务器进行验证;
- 天天互动的帐号服务器验证用户A的信息成功后,使用天天互动的私钥通过TLS提供的后台API生成签名(sig);
- 天天互动的帐号服务器将生成的签名派发给天天互动的客户端;
- 天天互动的客户端使用音视频云通讯相关服务时,需要提交帐号类型(accounttype)、sdkappid、identifier(也就是我们常说的用户id)和签名(由天天互动账号服务器调用TLS后台API生成并派发的)到音视频SDK或者IMSDK的login接口进行验证,验证成功后即可使用相关服务。
下面我们进行服务器端的编码
几个有用的文档和链接
- TLS后台API开发指引
- windows 64位 API
- 音视频云通讯账号登录集成
服务器端签名的函数,这里使用PHP
<?php
function signature($account_type, $identifier, $appid_at_3rd,
$sdk_appid, $expiry_after, $private_key_path){
$command = 'signature.exe'
. ' '. escapeshellarg($private_key_path)
. ' ' . escapeshellarg($expiry_after)
. ' ' . escapeshellarg($sdk_appid)
. ' ' . escapeshellarg($account_type)
. ' ' . escapeshellarg($appid_at_3rd)
. ' ' .escapeshellarg($identifier);
$ret = exec($command, $out, $status);
if( $status == -1){
return null;
}
return $ret;
}
?>
当然你还需要将服务器sdk中的signarure.exe放到当前目录下,此外还有私钥
当我们服务接收到客户端请求时,登陆成功后会进行签名,需要将签名下发到客户端,客户端利用该签名向腾讯服务器验证
<?php
include_once('function.php');
if(isset($_POST['username']) && isset($_POST['password'])){
$account['username']=$_POST['username'] ;
$account['password']=$_POST['password'];
//这里处理自己服务器登录的流程
//自己服务器登录成功后向客户端下发加密的rsa串
$result=signature("1071",$account['username'],"1400001973","1400001973",60*60*24*30,"./private_key");
if($result==null){
$res['status']=500;
$res['message']="server error";
echo json_encode($res);
}else{
$res['status']=200;
$res['message']=$result;
echo json_encode($res);
}
}else{
$res['status']=404;
$res['message']="params is not right";
echo json_encode($res);
}
?>
签名的参数在腾讯云的后台管理可以看到
注意有一个时间戳参数,该参数代表多久之后过期,由开发者控制。
接下来就是客户端的事了。首先就是集成
权限
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.GET_TASKS"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BROADCAST_STICKY"/>
声明组件
<application
android:name=".app.App">
</application >
<activity
android:name=".activity.AvActivity"
android:configChanges="keyboardHidden|orientation|locale|screenSize"
android:screenOrientation="portrait" />
App类的内容很简单,就是获得QavsdkControl
public class App extends Application{
private QavsdkControl mQavsdkControl = null;
@Override
public void onCreate() {
super.onCreate();
mQavsdkControl = new QavsdkControl(this);
}
public QavsdkControl getQavsdkControl() {
return mQavsdkControl;
}
}
然后需要拷几个现成的类
你需要修改内容,让他不报错,大部分都是资源相关的东西。
然后需要修改两个变量,共两处,值在腾讯云后台获得
客户端的注册功能由开发者自己实现,因为腾讯云不干涉注册,而登陆需要先登录自己的服务器,然后拿签名向腾讯服务器验证。
private void login() {
String u=username.getText().toString();
String p=password.getText().toString();
if (TextUtils.isEmpty(u)||TextUtils.isEmpty(p)){
Toast.makeText(getApplicationContext(),"账号或密码不能为空!",Toast.LENGTH_LONG).show();
return ;
}
//首先登录自己的服务器
//自己服务器登录成功后,服务器需要返回rsa加密后的串
RequestBody requestBody= new FormEncodingBuilder()
.add("username",u)
.add("password",p)
.build();
String url="http://10.0.0.24/tencent/index.php";
Request request=new Request.Builder().url(url).post(requestBody).build();
mOkHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Request request, IOException e) {
Log.e("TAG", "Error,register failure.");
}
@Override
public void onResponse(Response response) throws IOException {
String result = response.body().string();
LoginModel bean = gson.fromJson(result, LoginModel.class);
Message message = Message.obtain();
message.obj = bean;
message.what = LOGIN;
mHandler.sendMessage(message);
}
});
//获得rsa加密串后发起音视频前需要向腾讯服务器验证。
}
private Handler mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case LOGIN:
LoginModel bean= (LoginModel) msg.obj;
if (bean.getStatus()==200){
sign=bean.getMessage();
//保存sign
Log.e("TAG","sign:"+sign);
Toast.makeText(getApplicationContext(),"登录成功,请稍后!",Toast.LENGTH_LONG).show();
startTencentContext();
}else{
Toast.makeText(getApplicationContext(),"登录失败!"+bean.getMessage(),Toast.LENGTH_LONG).show();
}
break;
default:
break;
}
super.handleMessage(msg);
}
};
登陆成功后就可以拿到该签名了。之后开始向腾讯服务验证
private void startTencentContext() {
//发起音视频前需要向腾讯服务器验证。
String u=username.getText().toString();
String p=password.getText().toString();
if (TextUtils.isEmpty(u)||TextUtils.isEmpty(p)){
Toast.makeText(getApplicationContext(),"账号或密码不能为空!",Toast.LENGTH_LONG).show();
return ;
}
if(TextUtils.isEmpty(sign)){
Toast.makeText(MainActivity.this, "请先登录", Toast.LENGTH_SHORT).show();
return ;
}
if (!mQavsdkControl.hasAVContext()) {
if (!mQavsdkControl.isDefaultAppid()) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_appid_not_default), Toast.LENGTH_LONG).show();
}
if (!mQavsdkControl.isDefaultUid()) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_uid_not_default), Toast.LENGTH_LONG).show();
}
mLoginErrorCode = mQavsdkControl.startContext(u, sign);
if (mLoginErrorCode != AVConstants.AV_ERROR_OK) {
Toast.makeText(getApplicationContext(),"错误码:"+mLoginErrorCode, Toast.LENGTH_LONG).show();
}
}
}
验证的结果是广播异步返回的,因此我们需要注册一个广播接收器,接收这个消息
private void initStartContextBroadcast() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Util.ACTION_START_CONTEXT_COMPLETE);
intentFilter.addAction(Util.ACTION_CLOSE_CONTEXT_COMPLETE);
registerReceiver(mStartContextBroadcastReceiver, intentFilter);
}
private BroadcastReceiver mStartContextBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.e("TAG", "WL_DEBUG onReceive action = " + action);
Log.e("TAG", "WL_DEBUG ANR StartContextActivity onReceive action = " + action + " In");
if (action.equals(Util.ACTION_START_CONTEXT_COMPLETE)) {
mLoginErrorCode = intent.getIntExtra( Util.EXTRA_AV_ERROR_RESULT, AVConstants.AV_ERROR_OK);
if (mLoginErrorCode == AVConstants.AV_ERROR_OK) {
Intent i=new Intent(MainActivity.this,CallActivity.class);
Bundle bundle=new Bundle();
bundle.putString("from",username.getText().toString());
i.putExtras(bundle);
startActivity(i);
} else {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_login_error), Toast.LENGTH_LONG).show();
}
} else if (action.equals(Util.ACTION_CLOSE_CONTEXT_COMPLETE)) {
}
Log.e("TAG", "WL_DEBUG ANR StartContextActivity onReceive action = " + action + " Out");
}
};
之后我们就跳到了另外一个Activity当中,这个Activity中用来处理发起视频通话,处理通话失败等特殊情况的逻辑了,基本上直接从原Demo中复制代码修改即可,直接看代码
public class CallActivity extends AppCompatActivity implements View.OnClickListener{
private String from;
private EditText to;
private Button video,voice;
private QavsdkControl mQavsdkControl;
private int mCreateRoomErrorCode = AVConstants.AV_ERROR_OK;
private int mCloseRoomErrorCode = AVConstants.AV_ERROR_OK;
private int mAcceptErrorCode = AVConstants.AV_ERROR_OK;
private int mInviteErrorCode = AVConstants.AV_ERROR_OK;
private int mRefuseErrorCode = AVConstants.AV_ERROR_OK;
private int mJoinRoomErrorCode = AVConstants.AV_ERROR_OK;
private String mReceiveIdentifier = "";
private String mSelfIdentifier = "";
private boolean mIsVideo = false;
private boolean isSender = false;
private boolean isReceiver = false;
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.e("TAG", "WL_DEBUG onReceive action = " + action);
Log.e("TAG", "WL_DEBUG ANR CreateRoomActivity onReceive action = " + action + " In");
if (action.equals(Util.ACTION_ACCEPT_COMPLETE)) {
String identifier = intent.getStringExtra(Util.EXTRA_IDENTIFIER);
mSelfIdentifier = intent.getStringExtra(Util.EXTRA_SELF_IDENTIFIER);
mReceiveIdentifier = identifier;
long roomId = intent.getLongExtra(Util.EXTRA_ROOM_ID, -1);
mAcceptErrorCode = intent.getIntExtra(
Util.EXTRA_AV_ERROR_RESULT, AVConstants.AV_ERROR_OK);
if (mAcceptErrorCode == AVConstants.AV_ERROR_OK) {
mQavsdkControl.enterRoom(roomId, identifier, mIsVideo);
} else {
Toast.makeText(CallActivity.this,R.string.accept_failed,Toast.LENGTH_SHORT).show();
}
} else if (action.equals(Util.ACTION_CLOSE_ROOM_COMPLETE)) {
} else if (action.equals(Util.ACTION_INVITE_ACCEPTED)) {
startActivity(mReceiveIdentifier,mSelfIdentifier);
} else if (action.equals(Util.ACTION_INVITE_CANCELED)) {
Toast.makeText(getApplicationContext(), R.string.invite_canceled_toast,
Toast.LENGTH_LONG).show();
} else if (action.equals(Util.ACTION_INVITE_COMPLETE)) {
if (isReceiver) {
Toast.makeText(getApplicationContext(), R.string.notify_conflict,
Toast.LENGTH_LONG).show();
}
mInviteErrorCode = intent.getIntExtra(
Util.EXTRA_AV_ERROR_RESULT, AVConstants.AV_ERROR_OK);
if (mInviteErrorCode == AVConstants.AV_ERROR_OK) {
//等待对方接受邀请
Toast.makeText(getApplicationContext(), R.string.dialog_waitting_title,
Toast.LENGTH_LONG).show();
} else {
//邀请失败
Toast.makeText(getApplicationContext(), R.string.invite_failed,
Toast.LENGTH_LONG).show();
closeRoom();
}
} else if (action.equals(Util.ACTION_INVITE_REFUSED)) {
Toast.makeText(getApplicationContext(), R.string.invite_refused_toast,
Toast.LENGTH_LONG).show();
} else if (action.equals(Util.ACTION_RECV_INVITE)) {
if (isSender) {
Toast.makeText(getApplicationContext(), R.string.notify_conflict,
Toast.LENGTH_LONG).show();
}
isReceiver = true;
mIsVideo = intent.getBooleanExtra(Util.EXTRA_IS_VIDEO, false);
new AlertDialog.Builder(CallActivity.this)
.setTitle(R.string.invite_title)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
mQavsdkControl.accept();
}
})
.setNegativeButton(android.R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
mQavsdkControl.refuse();
isSender = isReceiver = false;
}
})
.setOnCancelListener(
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
Log.e("TAG", "WL_DEBUG onCancel");
mQavsdkControl.refuse();
isSender = isReceiver = false;
}
}).create().show();
} else if (action.equals(Util.ACTION_REFUSE_COMPLETE)) {
mRefuseErrorCode = intent.getIntExtra(
Util.EXTRA_AV_ERROR_RESULT, AVConstants.AV_ERROR_OK);
if (mRefuseErrorCode != AVConstants.AV_ERROR_OK) {
Toast.makeText(getApplicationContext(), R.string.refuse_failed,
Toast.LENGTH_LONG).show();
}
} else if (action.equals(Util.ACTION_ROOM_CREATE_COMPLETE)) {
mCreateRoomErrorCode = intent.getIntExtra(
Util.EXTRA_AV_ERROR_RESULT, AVConstants.AV_ERROR_OK);
if (mCreateRoomErrorCode != AVConstants.AV_ERROR_OK) {
Toast.makeText(getApplicationContext(), R.string.create_room_failed,
Toast.LENGTH_LONG).show();
}
} else if (action.equals(Util.ACTION_ROOM_JOIN_COMPLETE)) {
mJoinRoomErrorCode = intent.getIntExtra(
Util.EXTRA_AV_ERROR_RESULT, AVConstants.AV_ERROR_OK);
if (mJoinRoomErrorCode != AVConstants.AV_ERROR_OK) {
Toast.makeText(getApplicationContext(), R.string.join_room_failed,
Toast.LENGTH_LONG).show();
} else {
startActivity(mReceiveIdentifier,mSelfIdentifier);
}
}
Log.e("TAG", "WL_DEBUG ANR CreateRoomActivity onReceive action = " + action + " Out");
}
};
private void closeRoom() {
mCloseRoomErrorCode = mQavsdkControl.exitRoom();
if (mCloseRoomErrorCode != AVConstants.AV_ERROR_OK) {
Toast.makeText(CallActivity.this,R.string.close_room_failed,Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_call);
initIntent();
initView();
initInviteBroadcast();
mQavsdkControl = ((App) getApplication()).getQavsdkControl();
}
private void initIntent() {
Intent intent=getIntent();
Bundle bundle=intent.getExtras();
from=bundle.getString("from");
Log.e("TAG","from:"+from);
}
private void initInviteBroadcast() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Util.ACTION_ACCEPT_COMPLETE);
intentFilter.addAction(Util.ACTION_CLOSE_ROOM_COMPLETE);
intentFilter.addAction(Util.ACTION_INVITE_ACCEPTED);
intentFilter.addAction(Util.ACTION_INVITE_CANCELED);
intentFilter.addAction(Util.ACTION_INVITE_COMPLETE);
intentFilter.addAction(Util.ACTION_INVITE_REFUSED);
intentFilter.addAction(Util.ACTION_RECV_INVITE);
intentFilter.addAction(Util.ACTION_REFUSE_COMPLETE);
intentFilter.addAction(Util.ACTION_ROOM_CREATE_COMPLETE);
intentFilter.addAction(Util.ACTION_ROOM_JOIN_COMPLETE);
registerReceiver(mBroadcastReceiver, intentFilter);
}
private void initView() {
to= (EditText) findViewById(R.id.to);
video= (Button) findViewById(R.id.video);
voice= (Button) findViewById(R.id.voice);
video.setOnClickListener(this);
voice.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.voice:
voice();
break;
case R.id.video:
video();
break;
default:
break;
}
}
private void voice() {
mSelfIdentifier=from;
mReceiveIdentifier=to.getText().toString();
mIsVideo=false;
if (Util.isNetworkAvailable(getApplicationContext())) {
if (TextUtils.isEmpty(mSelfIdentifier)) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_send_account_error), Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(mReceiveIdentifier)) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_recv_account_error), Toast.LENGTH_SHORT).show();
} else {
if (mSelfIdentifier.equals(mReceiveIdentifier)) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_account_send_equal_recv), Toast.LENGTH_SHORT).show();
} else {
invite(mIsVideo);
isSender = true;
}
}
} else {
Toast.makeText(getApplicationContext(), getString(R.string.notify_no_network), Toast.LENGTH_SHORT).show();
}
}
private void video() {
mSelfIdentifier=from;
mReceiveIdentifier=to.getText().toString();
mIsVideo=true;
if (Util.isNetworkAvailable(getApplicationContext())) {
if (TextUtils.isEmpty(mSelfIdentifier)) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_send_account_error), Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(mReceiveIdentifier)) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_recv_account_error), Toast.LENGTH_SHORT).show();
} else {
if (mSelfIdentifier.equals(mReceiveIdentifier)) {
Toast.makeText(getApplicationContext(), getString(R.string.help_msg_account_send_equal_recv), Toast.LENGTH_SHORT).show();
} else {
invite(mIsVideo);
isSender = true;
}
}
} else {
Toast.makeText(getApplicationContext(), getString(R.string.notify_no_network), Toast.LENGTH_SHORT).show();
}
}
private void startActivity(String mReceiveIdentifier,String mSelfIdentifier) {
Log.e("TAG", "WL_DEBUG startActivity");
if ((mQavsdkControl != null) && (mQavsdkControl.getAVContext() != null) && (mQavsdkControl.getAVContext().getAudioCtrl() != null)) {
mQavsdkControl.getAVContext().getAudioCtrl().startTRAEService();
}
startActivityForResult(
new Intent(Intent.ACTION_MAIN)
.putExtra(Util.EXTRA_IDENTIFIER, mReceiveIdentifier)
.putExtra(Util.EXTRA_SELF_IDENTIFIER, mSelfIdentifier)
.setClass(this, AvActivity.class),
0);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBroadcastReceiver != null) {
unregisterReceiver(mBroadcastReceiver);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Log.e("TAG", "WL_DEBUG onActivityResult");
isSender = isReceiver = false;
if ((mQavsdkControl != null) && (mQavsdkControl.getAVContext() != null) && (mQavsdkControl.getAVContext().getAudioCtrl() != null)) {
mQavsdkControl.getAVContext().getAudioCtrl().stopTRAEService();
}
closeRoom();
}
private void invite(boolean isVideo) {
if (TextUtils.isEmpty(mReceiveIdentifier))
return;
mQavsdkControl.invite(mReceiveIdentifier, isVideo);
}
}
这样就差不多可以跑一个试试了。
其他细节看腾讯的文档把,多看几次就能把握个大概了。
更多推荐
所有评论(0)