码迷,mamicode.com
首页 > 移动开发 > 详细

Android 即时音视频解决方案2——腾讯云

时间:2015-09-30 11:12:44      阅读:857      评论:0      收藏:0      [点我收藏+]

标签:android   腾讯云   视频   语音   签名   

上一篇文章介绍了环信的解决方案,见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);

    }

}

这样就差不多可以跑一个试试了。

技术分享

技术分享

技术分享

技术分享

技术分享

技术分享

技术分享

其他细节看腾讯的文档把,多看几次就能把握个大概了。

源码下载
http://download.csdn.net/detail/sbsujjbcy/9139613

Github
https://github.com/lizhangqu/VoiceAndVideo

版权声明:本文为博主原创文章,未经博主允许不得转载。

Android 即时音视频解决方案2——腾讯云

标签:android   腾讯云   视频   语音   签名   

原文地址:http://blog.csdn.net/sbsujjbcy/article/details/48735039

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!