關(guān)鍵字:騰訊云、移動(dòng)直播、liveroom
本文只涉及騰訊云 <live-room> 標(biāo)簽代碼結(jié)構(gòu)講解,只需要有基本的程序結(jié)構(gòu)思維即可。
本文使用騰訊云最新公開的小程序源碼1.2.639,這個(gè)版本的小程序頁(yè)面與訪問(wèn)騰訊云視頻小程序看到的頁(yè)面稍有不同,但不影響標(biāo)簽分析。
基本概念
<live-room> 是騰訊云基于微信小程序內(nèi)置的<live-pusher>和<live-player>標(biāo)簽開發(fā)用于雙人和多人音視頻通話的自定義組件。
<live-room> 主要用于一對(duì)多音視頻通過(guò)場(chǎng)景下。騰訊云視頻 小程序的 手機(jī)直播 和 PC直播使用的就是 <live-room> 標(biāo)簽。
使用方法
登錄房間服務(wù)
第一步需要登錄房間服務(wù)。
調(diào)用 /utils/liveroom.js 的 login 方法進(jìn)行登錄,登錄的目的是要連接后臺(tái)房間服務(wù)(RoomService)。
var liveroom = require('/utils/liveroom.js');
...
liveroom.login({
serverDomain: '',
userID: '',
userSig: '',
sdkAppID: '',
accType: '',
userName: '' //用戶昵稱,由客戶自定義
});
注意,這里的 UserSig 需要通過(guò)請(qǐng)求《騰訊云移動(dòng)直播微信小程序源碼解析(一)》中講到的 Django 后臺(tái)獲取。
小程序端
在小程序中,可以這樣實(shí)現(xiàn)后臺(tái)請(qǐng)求:
qcloud.request({
// login:true,
url: config.serverUrl + '/accounts/genesig',
method: 'GET',
header: {
'content-type': 'application/json' // 默認(rèn)值
},
success: function (ret) {
console.log('get user ');
if (ret.data.code) {
console.log('獲取登錄信息失敗,調(diào)試期間請(qǐng)點(diǎn)擊右上角三個(gè)點(diǎn)按鈕,選擇打開調(diào)試');
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 后臺(tái)發(fā)送 GET 請(qǐng)求,請(qǐng)求地址為 config.serverUrl + '/accounts/genesig';
b) 如果成功返回,檢查是否返回了錯(cuò)誤碼;
i. 如果返回錯(cuò)誤碼,則報(bào)錯(cuò);
ii. 沒(méi)有沒(méi)有返回錯(cuò)誤碼,則登錄房間服務(wù)(RoomService);
Django 后臺(tái)
在 Django 后臺(tái),可以這樣處理:
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 即為實(shí)現(xiàn)視圖,它繼承的 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 # 默認(rèn)一個(gè)月,需要調(diào)整請(qǐng)自行修改
__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 文件中進(jìn)行配置
在 page 目錄下的 json配置文件內(nèi)引用組件:
{
"navigationBarTitleText": "在線課堂",
"usingComponents": {
"live-room": "/pages/components/live-room/liveroom"
}
}
在 WXML 文件中使用
在 page 目錄下的 wxml 文件中使用標(biāo)簽 <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> 標(biāo)簽可以正常使用了。
直播頁(yè)面
live-room 標(biāo)簽正常工作時(shí),用戶分為三類:
大主播:可以理解為直播間的主人,權(quán)限較高;
小主播:可以與大主播互動(dòng)的用戶。
觀眾:不可以與大主播互動(dòng)的用戶,只能觀看大主播的直播。
<live-room> 標(biāo)簽需要大主播創(chuàng)建直播間,只有大主播進(jìn)入直播間,其它用戶才能進(jìn)入房間。
大主播、小主播看到的頁(yè)面如下圖所示,其中大主播在左側(cè),小主播(最多3個(gè))在右側(cè)。

觀眾看到的頁(yè)面如下圖所示,只能看到大主播。

觀眾可以點(diǎn)擊頁(yè)面上的麥克圖標(biāo)向大主播申請(qǐng)連麥,大主播端后顯示小主播的連麥請(qǐng)求,如果大主播同意連麥,則該觀眾成為小主播。如果存在多于3個(gè)小主播,目前只能顯示3個(gè)小主播。
大主播可以通過(guò)踢人操作將小主播踢出,小主播此時(shí)變?yōu)橛^眾。
這樣,我們可以順暢的使用 <live-room>標(biāo)簽了。下面的內(nèi)容只是為了了解 live-room 標(biāo)簽如何工作及進(jìn)一步定制代碼。
代碼結(jié)構(gòu)
<live-room>標(biāo)簽代碼位于 pages/components/live-room文件夾內(nèi),該文件夾用于創(chuàng)建live-room自定義組件。
自定義組件的詳細(xì)介紹見微信小程序文檔。
live-room 自定義組件包含 vertical1v3template 文件夾、liveroom.js、liveroom.json、liveroom.wxml 及 liveroom.wxss。從結(jié)構(gòu)來(lái)看,除了vertical1v3template 文件夾,該自定義組件的結(jié)構(gòu)與微信小程序頁(yè)面類似。
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'>翻轉(zhuǎn)</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'>翻轉(zhuǎn)</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>
模板的邏輯是這樣的:
對(duì)于整個(gè)頁(yè)面,邏輯為:
判斷 linkPusherInfo.url 或 isCaster 是否為 true,如果是,使用v-full2,否則使用 v-full。
其中小主播具有 linkPusherInfo.url ,用戶為大主播時(shí) isCaster 為 true。
也就是說(shuō),對(duì)于大主播和小主播 class = v-full2;對(duì)于觀眾,class = v-full。
即 我們上面看到的不同用戶角色對(duì)應(yīng)的不同頁(yè)面。
對(duì)于大主播、小主播的頁(yè)面,如下圖所示,左側(cè)紫色框部分 class = v-main-video,右側(cè)紅色框部分為 v-sub-video-list。

頁(yè)面中通過(guò) isCaster&&mainPusherInfo.url 來(lái)區(qū)分大主播、小主播對(duì)應(yīng)的頁(yè)面。
大主播(isCaster=true)看到的頁(yè)面,大主播為live-pusher,視頻影像在上圖中紫色框中。小主播為live-player,視頻影像在上圖中紅色框中的一個(gè)小框中。
小主播(members)看到的頁(yè)面,大主播為 liver-player(此時(shí)大主播變?yōu)榱?VisualPlayers),視頻影像仍在上圖中紫色框中。小主播自己為 live-pusher ,視頻影像在上圖中紅色框的第一個(gè)小框中。下面兩個(gè)小框?yàn)槠渌≈鞑ァ?/p>
也就是說(shuō),大主播和小主播看到的右側(cè)紅色框中主播的順序是不一致的。
| 角色 | 大主播 | 小主播 |
|---|---|---|
| 位置 | 左側(cè)大框 | 右側(cè)小框中的一個(gè) |
| 名稱 | -- | members |
對(duì)于大主播來(lái)講:
| 角色 | 大主播 | 小主播 |
|---|---|---|
| 位置 | 左側(cè)大框 | 右側(cè)小框中的一個(gè) |
| 名稱 | -- | members |
對(duì)于小主播來(lái)將:
| 角色 | 大主播 | 小主播 |
|---|---|---|
| 位置 | 左側(cè)大框 | 右側(cè)小框第一個(gè) |
| 名稱 | VisualPlayers | members |
| 角色 | 大主播 | 小主播 |
|---|---|---|
| 位置 | 整個(gè)頁(yè)面 | 無(wú) |
| 名稱 | VisualPlayers | 無(wú) |
對(duì)于觀眾來(lái)講:
| 角色 | 大主播 | 小主播 |
|---|---|---|
| 位置 | 整個(gè)頁(yè)面 | 無(wú) |
| 名稱 | VisualPlayers | 無(wú) |
未完待續(xù)...
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
相關(guān)閱讀更多精彩內(nèi)容
- 轉(zhuǎn)載鏈接 注:本文轉(zhuǎn)載知乎上的回答 作者:初雪 鏈接:https://www.zhihu.com/question...
- 筆名:秋水 1 公司有一位員工要離職。 員工離職本是再平常不過(guò)的事兒,少有人會(huì)像A一樣情緒如此失控。 淚如雨下,半...
- 工作臺(tái)上擺放的筆記和訂單,以老舊的古董秤砣為鎮(zhèn)紙,它們整齊擺放的樣子很符合一個(gè)強(qiáng)迫癥患者的審美
- 今天看了《感動(dòng)中國(guó)》,其實(shí)每年都要看,每年都有不一樣的感動(dòng)。今年印象最深刻的是那個(gè)村官大學(xué)生,耶魯大學(xué)生甘愿當(dāng)大學(xué)...
- 摘自《瓦爾登湖》第四頁(yè)。 我怕,在美的季候里,我沒(méi)有收到通知,就開始丟失自己。我怕,在時(shí)代的波瀾里,我連呼救的機(jī)會(huì)...