騰訊云移動(dòng)直播微信小程序源碼解析(三)

關(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è)面

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

觀眾看到的頁(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。

live-room標(biāo)簽?zāi)0?/div>

頁(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ù)...

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(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...
    pengshuangta閱讀 29,314評(píng)論 9 295
  • 筆名:秋水 1 公司有一位員工要離職。 員工離職本是再平常不過(guò)的事兒,少有人會(huì)像A一樣情緒如此失控。 淚如雨下,半...
    天地一蒼鷹閱讀 558評(píng)論 0 4
  • 工作臺(tái)上擺放的筆記和訂單,以老舊的古董秤砣為鎮(zhèn)紙,它們整齊擺放的樣子很符合一個(gè)強(qiáng)迫癥患者的審美
    CNBLUEone閱讀 260評(píng)論 0 0
  • 今天看了《感動(dòng)中國(guó)》,其實(shí)每年都要看,每年都有不一樣的感動(dòng)。今年印象最深刻的是那個(gè)村官大學(xué)生,耶魯大學(xué)生甘愿當(dāng)大學(xué)...
    最喜不過(guò)淡雅閱讀 300評(píng)論 2 1
  • 摘自《瓦爾登湖》第四頁(yè)。 我怕,在美的季候里,我沒(méi)有收到通知,就開始丟失自己。我怕,在時(shí)代的波瀾里,我連呼救的機(jī)會(huì)...
    翔于閱讀 885評(píng)論 0 1

友情鏈接更多精彩內(nèi)容