打通小程序音视频和webRTC

腾讯视频云终端技术总监,rexchang(常青), 2008 年毕业加入腾讯,一直从事客户端研发相关工作,先后参与过 PC QQ、手机QQ、QQ物联 等产品项目,目前在腾讯视频云团队负责音视频终端解决方案的优化和落地工作,帮助客户在可控的研发成本投入之下,获得业内一流的音视频解决方案,目前我们的产品线包括:互动直播、点播、短视频、实时视频通话,图像处理,AI 等等。

本篇文章的思维导图

分开做一下介绍

小程序音视频是什么?

2017年腾讯视频云团队跟微信团队联合,将视频云 SDK 跟微信小程序整合在一起,并通过 <live-pusher> 和 <live-player> 两个标签的形式开放内部的功能。通过这两个标签,开发者可以实现在线直播、低延时监控、双人视频通话以及多人视频会议等功能。

那么WebRTC又是什么?

WebRTC(Web Real-Time Communication),是一个支持网页浏览器进行实时语音对话或视频对话的技术,是谷歌收购 GIPS 公司而获得的一项技术,在 Chrome 浏览器上无需安装插件,通过 javascript 就可以编写实时音视频通话程序。

两者区别在哪里?

如果您跟我一样是一个实用主义者,那我就简单从实用主义角度说一下我的结论:小程序搞定了手机,WebRTC拿下了PC。

如果你对技术比较感兴趣,那我们就可以从多个技术的角度去列举两者的区别,下面是一张详细对比的表格:

  • 实现原理: 小程序音视频是将腾讯视频云的 liteavsdk 嵌入到微信内部实现的,然后通过 <live-pusher> 和 <live-player> 两个标签将 SDK 内部的音视频能力开放出来。所以小程序的标签起到了开发者 API 的作用,而内部的 SDK 则是真正用来实现音视频功能。

WebRTC 由谷歌收购 GIPS 得来(这里不得不提一下,我加入腾讯时所在的第一个团队就是 QQ 团队,当时 QQ 的音视频还是购买的 GIPS 公司的产品,不过由于各种不靠谱,后来就转为自研路线了)。所以其技术被完整的保留并且加入到了 Google 的 Chrome 浏览器内核当中。而且最近苹果也已经开始在 Safari 浏览器中支持 WebRTC 的相关能力。

  • 底层协议 小程序音视频的主要协议是目前在直播领域最为常用的 RTMP 推流协议,以及 HTTP-FLV 播放协议,这两种协议都已经有多年的沉淀而且在互联网上的资料也是汗牛充栋。

WebRTC的底层则是使用RTP和RTCP两种数据协议,其中RTP主要用于音视频数据传输,而RTCP则一般用于控制。

  • 移动端碎片化问题 小程序音视频由于是微信统一实现的,而且微信团队每个版本都尽量要求功能对齐,否则宁可不上,所以在碎片化问题上基本不存在。

WebRTC在这里则要尴尬的多,一方面Android系统的碎片化本身让WebRTC的具体表现呈现“百花齐放”的景象,同时,iOS 目前的内嵌WebView(也就是在微信等APP里打开的各种内嵌网页)不支持WebRTC也还是个很麻烦的问题。

  • 扩展性 小程序音视频跟随微信的版本发布,有什么问题一般是当前代码流修正,然后跟随下一个版本发布,所以一般一个功能点(比如给 pusher 加一个美颜的功能)或者一个问题点(比如不支持手势放大)从确立到最终实现(或解决)仅需要一个月的时间,而且微信APP新版本的覆盖速度也确实挺快。

相比之下,WebRTC则不是一个团队或者一家公司的问题了,因为它现在已经走标准路线,所以每一个新特性都是先确定标准,然后再推动浏览器厂商(包括苹果)进行跟随。这里面的故事就多了,时间也就更久了。

  • 桌面浏览器 相信您已经发现,在前面几个问题的分析上,我的观点都倾向小程序音视频。确实,在目前国内的移动领域里,谷歌和苹果都不能一家说了算,真正说了算的还是微信

但是在桌面浏览器这个部分,Chrome目前在PC浏览器市场上留到地位的存在决定了 WebRTC 的优势就很大了,开发者可以在不安装插件的情况下就可以实现自己想要的功能。

相比之下,由于没有 Chrome 的原生支持,所以如果我们要在 PC 上对接小程序音视频,就需要安装浏览器插件或者通过 wxlite://start 这样的伪协议唤起本地 exe 应用程序(类似在网页上打开 QQ 聊天窗口)。

并非零和博弈

小程序音视频和WebRTC支架并非零和博艺,双方都有自己的优势和不足,所以本着“打不过他们,就加入他们”的思路,腾讯视频云团队在2018年春节回来后,就马不停蹄地开始了小程序音视频和WebRTC互通的相关工作。

目前,需要向各位开发者汇报的是,在最新版本的微信中,小程序音视频已经可以跟WebRTC打通,目前在PC 的Chrome浏览器上就可以跟小程序进行实时音视频互通。

// to-do

当然,如果您想知道这个功能是怎么实现的,可以继续看下去:

充分了解WebRTC

就像结婚一样,既然你决定要选择另一个人作为人生下半辈子的伴侣,那你肯定会先深入地了解一下TA这个人,比如性格,脾气,爱好等各个方面。

同样,我们要想很好的将小程序音视频和WebRTC打通,那也必须要多了解一下WebRTC,这里我就说一下我对 WebRTC 这个“人” 在性格上的一些理解。

首先,她虽然长得不太好看,但很有内涵。

说WebRTC长得不好看,只是我的一种比喻,我的意思是想说WebRTC的学习成本不低,虽然Google做了很多浅显易懂的PPT来教你怎么 Getting Start,但真要完整的学进去,还是需要静下心来,慢慢地把她当成自己认可的目标去学下去。但是如果你是第一次恋爱(也就是第一次接触实时音视频),你会发现学习WebRTC的过程,本身就是了解一个实时音视频技术细节的过程。

其次,她非常喜欢迁就别人,各种架构方案她都能支持到。

说WebRTC喜欢迁就比人,也是一种比喻,WebRTC所支持的后台架构非常多(比如 Mixer, Mesh,Router),而且谷歌认为这些后台实现都比较简单,所以既没有开放后台相关的源码,也没有提供统一的后台解决方案。这种开放式的设计思路非常好,但副作用就是实现成本高。在真刀真枪的项目落地时,小规模的公司或者开发者就很容易被这种技术门槛挡在门外。尤其是想要将 WebRTC 真正应用到企业级解决方案中,面对录制和存档的刚性需求,就需要花费大量时间进行定制开发。

方案的确立

了解到 WebRTC 的这些特点后,我们的互通方案也就比较清晰了:

首先,小程序音视频的特点是接口简单,快速上手,这是小程序的优势;而这一点恰恰是WebRTC的劣势,所以我们没有必要在小程序端为WebRTC暴露十几个接口类,而是继续采用小程序音视频的 <live-pusher> 和 <live-player> 标签来解决问题。

其次,WebRTC 的后台没有官方实现,那就意味着这里有很大的发挥空间,腾讯视频云就可以实现一套WebRTC后台并将其同小程序音视频所使用RTMP后台进行打通。简单来说,腾讯视频云要在小程序音视频和WebRTC之间充当红娘(更确切的说,应该是翻译员)的角色。

但是看过《新闻联播》里国家领导人之间谈话镜头的人都知道,这种翻译是会影响交流速度的。小程序音视频和WebRTC之间互通,中间引入一个翻译员,是不是通讯延时也就增加了?

其实不会,因为小程序音视频和WebRTC的视频编码标准在常规应用场景中是一致的,都是H.264标准,这是音频格式不同而已。这就意味着,翻译员要做的事情很少,两边基本都能挺对对方在说什么,所以延时不会增加太多。

成功的握手

下图所展示的就是腾讯视频云在小程序音视频和WebRTC互通问题上所采取的方案:

(1)首先,微信端的小程序通过腾讯视频云SDK将音视频流推送到腾讯云 RTMP 服务器。

(2)其次,腾讯云 RTMP 服务器的会对音视频数据进行初步的转化处理,然后透传给腾讯视频云的实时音视频后台集群。

(3)再次,实时音视频后台会再次将数据交给一个叫做 WebRTC-Proxy 的模块,就在这里, WebRTC-Proxy 要将来自小程序音视频的音视频数据翻译成 WebRTC 理解的“语言”。

(4)最后,在PC上的Chrome浏览器,就可以通过浏览器内置的WebRTC模块跟 WebRTC-Proxy 通讯,进而看到小程序端的视频影像。

(5)上面的四个过程倒过来,就可以实现双向视频通话;而将腾讯视频云作为星型结构的中心节点,多个端(不管是小程序还是Chrome浏览器)都接入进来,那就可以形成多人音视频解决方案。

打通房间逻辑

仅仅完成了音视频数据在小程序和WebRTC之间的握手还远远不够,因为在一次成功的音视频通话背后,不仅仅是把一端的音视频数据传递到另一端这么简单,还有状态的同步和成员间的状态协同。

比如多人视频通话中,涉及到呼叫和接通的流程,其中一方如果挂断了,其他人要收到挂断的通知。同时,如果有新的参与者加入,那么其他人也要收到相应的通知。WebRTC 中有很多组件,比如 RTCPeerConnection 就在处理上诉林林种种的逻辑。但是 WebRTC 的接口中引入的新名词非常多,对于初学者来说还是有一定的入门门槛,为了简化这里的逻辑,我们引入一个叫做“房间”的概念。

所谓房间(Room),就是把同时参与视频通话的各方圈在一起的一个东西。比如双人通话中,通话中的两个人 A 和 B 就可以认为在一个房间中。再比如在多人通话中,通话中的五个人(A B C D E)也可以认为是在一个房间里。

有了房间的概念,那我们就可以对刚才说的状态协同用两个简单的动作描述一下:如果有一个人加入了视频通话,那么就可以理解为他/她已经进房(EnterRoom)了;如果有一个退出了视频通话,那么就可以理解为他/她已经离开房间(LeaveRoom)了。而房间的门板上始终写着:“目前在房间里有哪几个人”。

有了房间的概念,我们就可以将小程序的两个简单的 <live-pusher> 和 <live-player> 标签,同 WebRTC 那一套复杂的 API 进行功能上的对齐,我们甚至不需要修改我们在第一版中定义的接口,就可以达成这个目标:

(1)<live-pusher> 的 url 接口不再传递 rtmp:// 协议的推流地址,而是传递 room:// 协议的推流地址。room:// 协议的使用方式可以参考我们的原理版文档 DOC

(2)<live-pusher> 标签在 start 成功之后,就相当于成功进入一个 room,之后,您可以通过 onPushEvent (PUSH_EVT_ROOM_USERLIST = 1020) 事件,收到房间里还有那些人的信息。在视频通话期间,房间内各个成员的进进出出,也都会通过这个事件通知给您的小程序代码。

(3)ROOM_USERLIST 里每一项都是一个二元组(如果是 1v1 的视频通话,ROOM_USERLIST 里只会有一个人): userid 和 playurl。 userid 代表是哪个用户, playurl 则是这个用户远程画面的播放地址。您要做的只是使用 <live-player> 标签播放这些远程画面的图像和声音而已。

(4)在 WebRTC 这一端,您可以参考我们的 webrtc API,这套 API 相对于 WebRTC 原生的 API,更适合初学者使用。

如何快速接入?

如果您希望一天内就打通 webrtc 和 小程序音视频 的互通,那么我推荐您不要从零开始,因为那会耗费您太多时间去踩坑和 bugfix,推荐您直接使用我们封装好的 <webrtc-room> ,这套方案既可以帮助您完成快速接入,又能满足一定的定制需求。

另外,不要忘记在微信=>发现=>小程序=>腾讯云视频云,体验一下腾讯云官方 Demo 中的 WebRTC 互通效果哦。

标签说明

<webrtc-room> 标签是基于 <live-pusher> 和 <live-player> 实现的用于 WebRTC 互通的自定义组件。如果您希望直接使用 <live-pusher> 和 <live-player> 标签完成对接,或者想要了解 <webrtc-room> 的内部原理,可以参考 DOC

版本要求

  • 微信 6.6.6 版本开始支持。

效果演示

  • PC 端 用 Chrome 浏览器打开 体验页面 可以体验桌面版 WebRTC 的效果。
  • 微信端 发现=>小程序=>搜索“腾讯视频云”,点击 WebRTC 功能卡,就可以体验跟桌面版 Chrome 互通的效果了。

对接资料

对接资料

说明

下载链接

小程序源码

包含<webrtc-room>的组件源码以及demo源码

PC端源码

基于WebrtcAPI实现的Chrome版WebRTC接入源码(其中 component/WebRTCRoom.js 实现了一个简单的房间管理功能,component/mainwindow.js包含了对 WebRTC API 的使用代码)

后台源码

实现了一个简单的房间列表功能,同时包含<webrtc-room>几个所需参数的生成代码

标签详解

属性定义

属性

类型

说明

template

String

'1v3'

必要,标识组件使用的界面模版(用户如果需要自定义界面,请看 界面定制

sdkAppID

String

‘’

必要,开通 IM 服务所获取到的 AppID

userID

String

''

必要,用户 ID

userSig

String

‘’

必要,身份签名,相当于登录密码的作用

roomID

Number

‘’

必要,房间号

privateMapKey

String

‘’

必要,房间权限 key,相当于进入指定房间 roomID 的钥匙

beauty

Number

0~5

可选,默认 5, 美颜级别 0~5

muted

Boolean

true, false

可选,默认 false,是否静音

debug

Boolean

true, false

可选,默认 false,是否打印推流 debug 信息

bindRoomEvent

function

必要,监听 <webrtc-room> 组件返回的事件

enableIM

Boolean

true, false

可选,默认false

bindIMEvent

function

当IM开启时必要,监听 IM 返回的事件

操作接口

<webrtc-room> 组件包含如下操作接口,您需要先通过 selectComponent 获取 <webrtc-room> 标签的引用,之后就可以进行相应的操作了。

函数名

说明

start()

启动

pause()

暂停

resume()

恢复

stop()

停止

switchCamera()

切换摄像头

var webrtcroom = this.selectComponent("#webrtcroomid")
webrtcroom.pause();

事件通知

<webrtc-room> 标签通过 onRoomEvent 返回内部事件,通过 onIMEvent 返回 IM 消息事件,事件参数格式如下

"detail": {
  "tag": "事件tag标识,具有唯一性",
  "code": "事件代码",
  "detail": "对应事件的详细参数"
}

示例代码

// Page.wxml 文件
<webrtc-room id="webrtcroom"
	roomID="{{roomID}}"
	userID="{{userID}}"
	userSig="{{userSig}}"
	sdkAppID="{{sdkAppID}}"
	privateMapKey="{{privateMapKey}}"
	template="1v3"
	beauty="{{beauty}}"
	muted="{{muted}}"
	debug="{{debug}}"
	bindRoomEvent="onRoomEvent"
	enableIM="{{enableIM}}"
	bindIMEvent="onIMEvent">
</webrtc-room>


// Page.js 文件
Page({
	data: {
		//...
		roomID: '',
		userID: '',
		userSig: '',
		sdkAppID: '',
		beauty: 3,
		muted: false,
		debug: false,
		enableIM: false
	},
    onRoomEvent: function(e){
        switch(e.detail.tag){
            case 'error': {
                //发生错误
                var code = e.detail.code;
                var detail = e.detail.detail;
                break;
            }
        }
    },
    onIMEvent: function(e){
        switch(e.detail.tag){
            case 'big_group_msg_notify': 
                //收到群组消息
                console.debug( e.detail.detail )
                break;
            case 'login_event': 
                //登录事件通知
                console.debug( e.detail.detail )
                break;
            case 'connection_event': 
                //连接状态事件
                console.debug( e.detail.detail )
                break;
            case 'join_group_event': 
                //进群事件通知
                console.debug( e.detail.detail )
                break;
        }
    },

  onLoad: function (options) {
		self.setData({
			userID: self.data.userID,
			userSig: self.data.userSig,
			sdkAppID: self.data.sdkAppID,
			roomID: self.data.roomID,
			privateMapKey: res.data.privateMapKey
		}, function() {
			var webrtcroomCom = this.selectComponent('#webrtcroom');
			if (webrtcroomCom) {
				webrtcroomCom.start();
			}
		})
	},
  	
})

使用指引

请确认已经参照 Demo部署 开通了相关服务和并正确的完成了配置。

step1: 下载自定义组件源码

<webrtc-room> 并非微信小程序原生提供的标签,而是一个自定义组件,所以您需要额外的代码来支持这个标签。点击 小程序源码 下载源码包,您可以在 wxlite 文件夹下获取到所需文件。

step2: 在工程中引入组件

  • 在 page 目录下的 json 配置文件内引用组件,这一步是必须的,因为 <webrtc-room> 并非原生标签。 "usingComponents": { "webrtc-room": "/pages/webrtc-room/webrtc-room" }
  • 在 page 目录下的 wxml 文件中使用标签<webrtc-room id="webrtcroomid" roomID="{{roomID}}" userID="{{userID}}" userSig="{{userSig}}" sdkAppID="{{sdkAppID}}" privateMapKey="{{privateMapKey}}" template="1v3" beauty="{{beauty}}" muted="{{muted}}" debug="{{debug}}" bindRoomEvent="onRoomEvent" enableIM="{{enableIM}}" bindIMEvent="onIMEvent"> </webrtc-room>

step3: 获取 key 信息

按照如下表格获取关键的 key 信息,这是使用腾讯云互通直播服务所必须的几个信息:

KEY

示例

作用

获取方案

sdkAppID

1400087915

用于计费和业务区分

step1 中获取

userID

xiaoming

用户名

可以由您的服务器指定,或者使用小程序的openid

userSig

加密字符串

相当于 userid 对应的登录密码

由您的服务器签发(PHP / JAVA

roomID

12345

房间号

可以由您的服务器指定

privateMapKey

加密字符串

进房票据:相当于是进入 roomid 的钥匙

由您的服务器签发(PHP / JAVA

下载 sign_src.zip 可以获得服务端签发 userSig 和 privateMapKey 的计算代码(生成 userSig 和 privateMapKey 的签名算法是 ECDSA-SHA256)。

step4: 进入房间

self.setData({
	userID: userID,
	userSig: userSig,
	sdkAppID: sdkAppID,
	roomID: roomID,
	privateMapKey: privateMapKey
}, function() {
	var webrtcroomCom = this.selectComponent('#webrtcroomid');
	if (webrtcroomCom) {
		webrtcroomCom.start();
	}
})

<h2 id="CustomUI"> 界面定制 </h2>

  • 创建界面模版
//第一步:新建 /pages/templates/mytemplate 文件夹,并创建 mytemplate.wxml 和 mytemplate.wxss 文件

//第二步:编写 mytemplate.wxml 和 mytemplate.wxss 文件
//mytemplate.wxml
<template name='mytemplate'>
    <view class='videoview'>
        <view class="pusher-box">
            <live-pusher
                id="rtcpusher"
                autopush
                mode="RTC"
                url="{{pushURL}}"
                aspect="{{aspect}}"
                min-bitrate="{{minBitrate}}"
                max-bitrate="{{maxBitrate}}"
                audio-quality="high"
                beauty="{{beauty}}"
                muted="{{muted}}"
                waiting-image="https://mc.qcloudimg.com/static/img/
								     daeed8616ac5df256c0591c22a65c4d3/pause_publish.jpg"
                background-mute="{{true}}"
                debug="{{debug}}"
                bindstatechange="onPush"
                binderror="onError">
                <cover-image  class='character' src="/pages/Resources/mask.png"></cover-image>
                <cover-view class='character' style='padding: 0 5px;'>我</cover-view>
            </live-pusher>
        </view>
        <view class="player-box" wx:for="{{members}}" wx:key="userID"> 
            <view class='poster'>
                <cover-image class='set'
			       src="https://miniprogram-1252463788.file.myqcloud.com/roomset_{{index + 2}}.png">
				</cover-image>
            </view>
            <live-player
                id="{{item.userID}}"
                autoplay
                mode="RTC"
                wx:if="{{item.accelerateURL}}"
                object-fit="fillCrop"
                min-cache="0.1"
                max-cache="0.3"
                src="{{item.accelerateURL}}"
                debug="{{debug}}"
                background-mute="{{true}}"
                bindstatechange="onPlay">
                <cover-view class='loading' wx:if="{{item.loading}}">
                    <cover-image src="/pages/Resources/loading_image0.png"></cover-image>
                </cover-view>
                <cover-image  class='character' src="/pages/Resources/mask.png"></cover-image>
                <cover-view class='character' style='padding: 0 5px;'>{{item.userName}}</cover-view>
            </live-player>  
        </view> 
    </view>
</template>

//mytemplate.wxss
.videoview {
  background-repeat:no-repeat;
  background-size: cover;
  width: 100%;
  height: 100%;
}
  • webrtc-room 组件引入模版
//为 <webrtc-room> 组件中的 webrtcroom.wxml 文件添加自定义模版
<import src='/pages/templates/mytemplate/mytemplate.wxml'/>
<view class='conponent-box'>
    <view styles="width:100%;height=100%;" wx:if="{{template=='1v3'}}">
        <template is='mytemplate' data="{{pushURL, aspect, 
				      minBitrate, maxBitrate, beauty, muted, debug, members}}"/>
    </view>
</view>

//为 <webrtc-room> 组件中的 webrtcroom.wxss 文件添加自定义样式
@import "../templates/mytemplate/mytemplate.wxss";

Chrome端对接

了解腾讯云官网的 WebrtcAPI ,可以对接 Chrome 端的 H5 视频通话,因为不是本文档的重点,此处不做赘述。

实时音视频产品开通

想要尝试这些接入,首先要开通腾讯云实时音视频,快来接入吧~

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏媒矿工厂

演进中视频流媒体容器格式与传输协议

最近几年,在线视频行业发展十分迅速,无论是视频播放设备还是视频传输技术都在不断革新,从60英寸的UHD平面屏幕到平板电脑或者手机,从光纤网络到3G,4G的蜂窝网...

9813
来自专栏IT大咖说

WebRTC会成主流吗?众包CDN时代到了!

摘要 WebRTC把实时流媒体和P2P等能力带入了Web前端,开发者只需编写简单的JavaScript程序即可开发出丰富的实时多媒体应用。本次大会想跟大家分享我...

7149
来自专栏开源项目

码云周刊 | 面试之前,或许该高效率地学点干货!

一周热门资讯回顾 1、程序员多大年纪算高龄,届时该何去何从? ? 随着年龄的增长,程序员会相对难以保持技能更新。许多人宁愿留在自己的舒适区,不冒任何风险。即使...

3036
来自专栏BIT泽清

app 上架ios时隐藏开关会不会暴露? 提审时暴露了怎么办?

# ipv6被拒绝,2.1大礼包,后台定位被拒绝,免费的终极解决方案,苹果审核被拒绝了如何取解决,大家齐心协力让天下没有解决不了的问题!

3.1K1
来自专栏BIT泽清

2018年【开发者必看】金融p2p无资质上架app store已过审经历教程

自从国内上架金融理财贷款P2P类型的App必须要具备金融资质后,现在想要上架到App Store已经很难了,就算你有了资质还不一定能够。下面就给大家分享一下关于...

1.1K15
来自专栏SDNLAB

为什么软件定义网络在网络功能虚拟化中很重要?

编者按:在数据中心领域,网络只是一项资源。但在通信服务提供商 (CSP) 这里,网络就是正题。这一思维上的差异意味着软件定义网络 (SDN)已经可以有效解决数据...

3748
来自专栏云计算D1net

SDN快速发展 网络虚拟化准备就绪

SDN在创立之初就受到了全球瞩目并发展迅速,SDN的发展壮大带来网络产业格局重大调整的同时,也势必会波及到网络安全设备行业。网络侧安全产品在本质上是一种特殊用途...

4136
来自专栏SDNLAB

网络功能虚拟化系列:NFV的开源软件包

我们在之前的系列里面讨论了OPNFV(网络功能虚拟化的网络平台),主要覆盖了有关虚拟架构和相应的支持NFV(网络功能虚拟化)组织管理。本文将主要介绍组建不同虚拟...

44211
来自专栏数据科学与人工智能

【数据】常用API接口汇总

下面列举了100多个国内常用API接口,并按照 笔记、出行、词典、电商、地图、电影、即时通讯、开发者网站、快递查询、旅游、社交、视频、天气、团队协作、图片与图像...

6.5K10
来自专栏瓜大三哥

MIG例化技巧

Unbuffered DIMM,定位于桌面市场,指地址和控制信号没有经过缓冲器,没有做任何时序调整(缓冲器延迟是有的),直接到达DIMM上的RAM芯片。而Reg...

1175

扫码关注云+社区

领取腾讯云代金券