关键字:腾讯云、移动直播、liveroom
本文只涉及腾讯云 <live-room> 标签代码结构讲解,只需要有基本的程序结构思维即可。
本文使用腾讯云最新公开的小程序源码1.2.639,这个版本的小程序页面与访问腾讯云视频小程序看到的页面稍有不同,但不影响标签分析。
基本概念
<live-room> 是腾讯云基于微信小程序内置的<live-pusher>和<live-player>标签开发用于双人和多人音视频通话的自定义组件。
<live-room> 主要用于一对多音视频通过场景下。腾讯云视频 小程序的 手机直播 和 PC直播使用的就是 <live-room> 标签。
使用方法
登录房间服务
第一步需要登录房间服务。
调用 /utils/liveroom.js 的 login 方法进行登录,登录的目的是要连接后台房间服务(RoomService)。
var liveroom = require('/utils/liveroom.js');
...
liveroom.login({
serverDomain: '',
userID: '',
userSig: '',
sdkAppID: '',
accType: '',
userName: '' //用户昵称,由客户自定义
});
注意,这里的 UserSig 需要通过请求《腾讯云移动直播微信小程序源码解析(一)》中讲到的 Django 后台获取。
小程序端
在小程序中,可以这样实现后台请求:
qcloud.request({
// login:true,
url: config.serverUrl + '/accounts/genesig',
method: 'GET',
header: {
'content-type': 'application/json' // 默认值
},
success: function (ret) {
console.log('get user ');
if (ret.data.code) {
console.log('获取登录信息失败,调试期间请点击右上角三个点按钮,选择打开调试');
options.fail && options.fail({
errCode: ret.data.code,
errMsg: ret.data.message + '[' + ret.data.code + ']'
});
return;
}
ret.data.serverDomain = config.roomServiceUrl + '/weapp/' + options.type + '/';
liveroom.login({
data: ret.data,
success: options.success,
fail: options.fail
});
}
逻辑是这样的:
a) 向 django 后台发送 GET 请求,请求地址为 config.serverUrl + '/accounts/genesig';
b) 如果成功返回,检查是否返回了错误码;
i. 如果返回错误码,则报错;
ii. 没有没有返回错误码,则登录房间服务(RoomService);
Django 后台
在 Django 后台,可以这样处理:
class GeneSigView(WeappMixin, View):
def get(self, request, *args, **kwargs):
self._get_openid(request)
result = {}
if self.openid is None:
result = {'code': -1, 'message': 'get openid error'}
return JsonResponse(data=result)
try:
tls_api = tls_sig.TLSSigAPI(settings.IM_SDKAPPID,
settings.PRIVATEKEY)
sig = tls_api.tls_gen_sig(self.openid)
result.update({'userSig': sig.decode(), 'userID': self.openid,
'sdkAppID': settings.IM_SDKAPPID,
'accType': settings.IM_ACCOUNTTYPE})
user = Account.objects.get(openid=self.openid)
result.update(
{'userName': user.nickname, 'userAvatar': user.avatarurl})
except:
result = {'code': -1, 'message': 'calc usersig error'}
return JsonResponse(data=result)
GeneSigView 即为实现视图,它继承的 WeappMixin 用户获取 openid :
class WeappMixin(object):
openid = None
def _get_openid(self, request):
session_id = request.META.get('HTTP_X_WX_SKEY')
try:
self.openid = redis_api.get_str(session_id).decode().split(':')[0]
except (ValueError, AttributeError):
self.openid = None
代码中的 tls_sig 为 腾讯云提供的 sig 生成器,代码是这样的:
#! /usr/bin/python
# coding:utf-8
__author__ = "tls@tencent.com"
__date__ = "$Mar 3, 2016 03:00:43 PM"
import OpenSSL
import base64
import zlib
import json
import time
ecdsa_pri_key = """
-----BEGIN EC PARAMETERS-----
BgUrgQQACg==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIEJDBDY4KVdj3dPBacADreB772ok45A57YWrUUvc5fMQoAcGBSuBBAAK
oUQDQgAEaPVFHhWqRDnKnVlyU5JIzXOUyOJd/pPUwhLUovf+PYBm7otRBptnvJ4E
oJ4qeSJNG0v4XdiqM3mtChkhUEFT3Q==
-----END EC PRIVATE KEY-----
"""
privateKey = '-----BEGIN PRIVATE KEY-----\r\n' + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQga7O4tX0KQH/Bhbq5\r\n' + 'zfP5nBDeAiBs6R8wO7zpd7PIB+GhRANCAAQI0AnMVO1km7iAMatqV3FcVrAC3B8/\r\n' + '1OShs1hr3Envd+KlUHtcZZ780G3+yc0nCo2NPYPCEODUm36oQ+iIhU+h\r\n' + '-----END PRIVATE KEY-----\r\n'
def list_all_curves():
list = OpenSSL.crypto.get_elliptic_curves()
for element in list:
print(element)
def get_secp256k1():
print(OpenSSL.crypto.get_elliptic_curve('secp256k1'))
def base64_encode_url(data):
base64_data = base64.b64encode(data)
base64_data = base64_data.replace(b'+', b'*')
base64_data = base64_data.replace(b'/', b'-')
base64_data = base64_data.replace(b'=', b'_')
return base64_data
def base64_decode_url(base64_data):
base64_data = base64_data.replace(b'*', b'+')
base64_data = base64_data.replace(b'-', b'/')
base64_data = base64_data.replace(b'_', b'=')
raw_data = base64.b64decode(base64_data)
return raw_data
class TLSSigAPI:
""""""
__acctype = 0
__identifier = ""
__appid3rd = ""
__sdkappid = 0
__version = 20151204
__expire = 3600 * 24 * 30 # 默认一个月,需要调整请自行修改
__pri_key = ""
__pub_key = ""
_err_msg = "ok"
def __get_pri_key(self):
return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, self.__pri_key)
def __init__(self, sdkappid, pri_key):
self.__sdkappid = sdkappid
self.__pri_key = pri_key
def __create_dict(self):
return {"TLS.account_type": "%d" % self.__acctype, "TLS.identifier": "%s" % self.__identifier,
"TLS.appid_at_3rd": "%s" % self.__appid3rd, "TLS.sdk_appid": "%d" % self.__sdkappid,
"TLS.expire_after": "%d" % self.__expire, "TLS.version": "%d" % self.__version,
"TLS.time": "%d" % time.time()}
def __encode_to_fix_str(self, m):
fix_str = "TLS.appid_at_3rd:" + m["TLS.appid_at_3rd"] + "\n" \
+ "TLS.account_type:" + m["TLS.account_type"] + "\n" \
+ "TLS.identifier:" + m["TLS.identifier"] + "\n" \
+ "TLS.sdk_appid:" + m["TLS.sdk_appid"] + "\n" \
+ "TLS.time:" + m["TLS.time"] + "\n" \
+ "TLS.expire_after:" + m["TLS.expire_after"] + "\n"
return fix_str
def tls_gen_sig(self, identifier):
self.__identifier = identifier
m = self.__create_dict()
fix_str = self.__encode_to_fix_str(m)
pk_loaded = self.__get_pri_key()
sig_field = OpenSSL.crypto.sign(pk_loaded, fix_str, "sha256")
sig_field_base64 = base64.b64encode(sig_field)
m["TLS.sig"] = sig_field_base64.decode('utf-8')
json_str = json.dumps(m)
sig_compressed = zlib.compress(json_str.encode('utf-8'))
base64_sig = base64_encode_url(sig_compressed)
return base64_sig
def main():
api = TLSSigAPI(1400001052, privateKey)
sig = api.tls_gen_sig("xiaojun")
print(sig)
if __name__ == "__main__":
main()
在JSON 文件中进行配置
在 page 目录下的 json配置文件内引用组件:
{
"navigationBarTitleText": "在线课堂",
"usingComponents": {
"live-room": "/pages/components/live-room/liveroom"
}
}
在 WXML 文件中使用
在 page 目录下的 wxml 文件中使用标签 <live-room>:
<live-room id="id_liveroom" wx:if="{{showLiveRoom}}" roomid="{{roomID}}" role="{{role}}" roomname="{{roomName}}" pureaudio="{{pureAudio}}" debug="{{debug}}" muted="{{muted}}" beauty="{{beauty}}" template="vertical1v3" bindRoomEvent="onRoomEvent">
到这里,<live-room> 标签可以正常使用了。
直播页面
live-room 标签正常工作时,用户分为三类:
大主播:可以理解为直播间的主人,权限较高;
小主播:可以与大主播互动的用户。
观众:不可以与大主播互动的用户,只能观看大主播的直播。
<live-room> 标签需要大主播创建直播间,只有大主播进入直播间,其它用户才能进入房间。
大主播、小主播看到的页面如下图所示,其中大主播在左侧,小主播(最多3个)在右侧。
观众看到的页面如下图所示,只能看到大主播。
观众可以点击页面上的麦克图标向大主播申请连麦,大主播端后显示小主播的连麦请求,如果大主播同意连麦,则该观众成为小主播。如果存在多于3个小主播,目前只能显示3个小主播。
大主播可以通过踢人操作将小主播踢出,小主播此时变为观众。
这样,我们可以顺畅的使用 <live-room>标签了。下面的内容只是为了了解 live-room 标签如何工作及进一步定制代码。
代码结构
<live-room>标签代码位于 pages/components/live-room文件夹内,该文件夹用于创建live-room自定义组件。
自定义组件的详细介绍见微信小程序文档。
live-room 自定义组件包含 vertical1v3template 文件夹、liveroom.js、liveroom.json、liveroom.wxml 及 liveroom.wxss。从结构来看,除了vertical1v3template 文件夹,该自定义组件的结构与微信小程序页面类似。
vertical1v3template 为腾讯云的 live-room 模板。模板的代码是这样的:
<template name="vertical1v3">
<view class="{{linkPusherInfo.url || isCaster ? 'v-full2': 'v-full'}}">
<view wx:if="{{isCaster}}" class='v-main-video'>
<live-pusher wx:if="{{isCaster&&mainPusherInfo.url}}" id="pusher" mode="RTC" url="{{mainPusherInfo.url}}" min-bitrate="850" min-bitrate="1200" beauty="{{beauty}}" enable-camera="{{!pureaudio}}" muted="{{muted}}" aspect="9:16" waiting-image="https://mc.qcloudimg.com/static/img/daeed8616ac5df256c0591c22a65c4d3/pause_publish.jpg"
background-mute="{{true}}" debug="{{debug}}" bindstatechange="onMainPush" binderror="onMainError">
<cover-view class='character' style='padding: 0 5px;'>我({{userName}})</cover-view>
<cover-view class="operate">
<cover-view class='img-box'>
<cover-image class='img-view' src='/pages/Resources/camera.png' bindtap="switchCamera"></cover-image>
<!-- <cover-view class='text-view'>翻转</cover-view> -->
</cover-view>
<cover-view class='img-box'>
<cover-image class='img-view' src='/pages/Resources/{{beauty > 0? "beauty" : "beauty-dis"}}.png' bindtap="toggleBeauty"></cover-image>
<!-- <cover-view class='text-view'>美颜</cover-view> -->
</cover-view>
<!-- <cover-view class='img-box'>
<cover-image class='img-view' src='/pages/Resources/{{muted ? "mic-dis" : "mic"}}.png' bindtap="toggleMuted"></cover-image>
<cover-view class='text-view'>声音</cover-view>
</cover-view> -->
<cover-view class='img-box'>
<cover-image class='img-view' src='/pages/Resources/{{debug? "log" : "log2"}}.png' bindtap="toggleDebug"></cover-image>
<!-- <cover-view class='text-view'>日志</cover-view> -->
</cover-view>
</cover-view>
</live-pusher>
</view>
<view wx:for="{{visualPlayers}}" wx:key="{{index}}" class="{{linkPusherInfo.url ? 'v-main-video' : 'v-full'}}">
<live-player wx:if="{{item.url}}" autoplay id="player" mode="{{item.mode}}" min-cache="{{item.minCache}}" max-cache="{{item.maxCache}}" object-fit="{{item.objectFit}}" src="{{item.url}}" debug="{{debug}}" muted="{{muted}}" background-mute="{{item.mute}}" bindstatechange="onMainPlayState"
binderror="onMainPlayError">
<cover-view class="operate">
<cover-view wx:if="{{linkPusherInfo.url}}" class='img-box'>
<cover-image class='img-view' src='/pages/Resources/camera.png' bindtap="switchCamera"></cover-image>
<!-- <cover-view class='text-view'>翻转</cover-view> -->
</cover-view>
<cover-view wx:if="{{linkPusherInfo.url}}" class='img-box'>
<cover-image class='img-view' src='/pages/Resources/{{beauty > 0? "beauty" : "beauty-dis"}}.png' bindtap="toggleBeauty"></cover-image>
<!-- <cover-view class='text-view'>美颜</cover-view> -->
</cover-view>
<!-- <cover-view class='img-box'>
<cover-image class='img-view' src='/pages/Resources/{{muted ? "mic-dis" : "mic"}}.png' bindtap="toggleMuted"></cover-image>
<cover-view class='text-view'>声音</cover-view>
</cover-view> -->
<cover-view wx:if="{{!linkPusherInfo.url}}" class='img-box'>
<cover-image class='img-view' src='/pages/Resources/mic.png' bindtap="requestJionPusher"></cover-image>
<!-- <cover-view class='text-view'>连麦</cover-view> -->
</cover-view>
<cover-view class='img-box'>
<cover-image class='img-view' src='/pages/Resources/{{debug? "log" : "log2"}}.png' bindtap="toggleDebug"></cover-image>
<!-- <cover-view class='text-view'>日志</cover-view> -->
</cover-view>
</cover-view>
</live-player>
</view>
</view>
<view wx:if="{{linkPusherInfo.url || isCaster}}" class='v-sub-video-list'>
<view class='.v-sub-video' wx:if="{{!isCaster && linkPusherInfo.url}}">
<live-pusher wx:if="{{!isCaster && linkPusherInfo.url}}" min-bitrate="400" min-bitrate="200" id="audience_pusher" mode="RTC" min-bitrate="900" url="{{linkPusherInfo.url}}" beauty="{{beauty}}" enable-camera="{{!pureaudio}}" muted="{{muted}}" aspect="9:16"
waiting-image="https://mc.qcloudimg.com/static/img/daeed8616ac5df256c0591c22a65c4d3/pause_publish.jpg" background-mute="true" debug="{{debug}}" bindstatechange="onLinkPush" binderror="onLinkError">
<cover-image class='character' src="/pages/Resources/mask.png"></cover-image>
<cover-view class='character' style='padding: 0 5px;'>我({{userName}})</cover-view>
<cover-view class='close-ico' bindtap="quitLink">x</cover-view>
</live-pusher>
</view>
<view class='.v-sub-video' wx:for="{{members}}" wx:key="{{item.userID}}">
<view class='poster'>
<cover-image wx:if="{{ index < 4 }}" class='set' src="https://miniprogram-1252463788.file.myqcloud.com/roomset_{{index + 1}}.png"></cover-image>
</view>
<live-player wx:if="{{item.accelerateURL}}" id="{{item.userID}}" autoplay mode="RTC" object-fit="fillCrop" min-cache="0.1" max-cache="0.3" src="{{item.accelerateURL}}" debug="{{debug}}" background-mute="{{true}}">
<cover-view class="close-ico" wx:if="{{item.userID == userID || isCaster}}" bindtap="kickoutSubPusher" data-userid="{{item.userID}}">x</cover-view>
<cover-view class='loading' wx:if="{{false}}">
<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>
模板的逻辑是这样的:
对于整个页面,逻辑为:
判断 linkPusherInfo.url 或 isCaster 是否为 true,如果是,使用v-full2,否则使用 v-full。
其中小主播具有 linkPusherInfo.url ,用户为大主播时 isCaster 为 true。
也就是说,对于大主播和小主播 class = v-full2;对于观众,class = v-full。
即 我们上面看到的不同用户角色对应的不同页面。
对于大主播、小主播的页面,如下图所示,左侧紫色框部分 class = v-main-video,右侧红色框部分为 v-sub-video-list。
页面中通过 isCaster&&mainPusherInfo.url 来区分大主播、小主播对应的页面。
大主播(isCaster=true)看到的页面,大主播为live-pusher,视频影像在上图中紫色框中。小主播为live-player,视频影像在上图中红色框中的一个小框中。
小主播(members)看到的页面,大主播为 liver-player(此时大主播变为了 VisualPlayers),视频影像仍在上图中紫色框中。小主播自己为 live-pusher ,视频影像在上图中红色框的第一个小框中。下面两个小框为其它小主播。
也就是说,大主播和小主播看到的右侧红色框中主播的顺序是不一致的。
角色 | 大主播 | 小主播 |
---|---|---|
位置 | 左侧大框 | 右侧小框中的一个 |
名称 | -- | members |
对于大主播来讲:
角色 | 大主播 | 小主播 |
---|---|---|
位置 | 左侧大框 | 右侧小框中的一个 |
名称 | -- | members |
对于小主播来将:
角色 | 大主播 | 小主播 |
---|---|---|
位置 | 左侧大框 | 右侧小框第一个 |
名称 | VisualPlayers | members |
角色 | 大主播 | 小主播 |
---|---|---|
位置 | 整个页面 | 无 |
名称 | VisualPlayers | 无 |
对于观众来讲:
角色 | 大主播 | 小主播 |
---|---|---|
位置 | 整个页面 | 无 |
名称 | VisualPlayers | 无 |
未完待续...