腾讯云移动直播微信小程序源码解析(三)

关键字:腾讯云、移动直播、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。

live-room标签模板

页面中通过 isCaster&&mainPusherInfo.url 来区分大主播、小主播对应的页面。

大主播(isCaster=true)看到的页面,大主播为live-pusher,视频影像在上图中紫色框中。小主播为live-player,视频影像在上图中红色框中的一个小框中。

小主播(members)看到的页面,大主播为 liver-player(此时大主播变为了 VisualPlayers),视频影像仍在上图中紫色框中。小主播自己为 live-pusher ,视频影像在上图中红色框的第一个小框中。下面两个小框为其它小主播。

也就是说,大主播和小主播看到的右侧红色框中主播的顺序是不一致的。

角色 大主播 小主播
位置 左侧大框 右侧小框中的一个
名称 -- members

对于大主播来讲:

角色 大主播 小主播
位置 左侧大框 右侧小框中的一个
名称 -- members

对于小主播来将:

角色 大主播 小主播
位置 左侧大框 右侧小框第一个
名称 VisualPlayers members
角色 大主播 小主播
位置 整个页面
名称 VisualPlayers

对于观众来讲:

角色 大主播 小主播
位置 整个页面
名称 VisualPlayers

未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • 转载链接 注:本文转载知乎上的回答 作者:初雪 链接:https://www.zhihu.com/question...
    pengshuangta阅读 28,475评论 9 295
  • 笔名:秋水 1 公司有一位员工要离职。 员工离职本是再平常不过的事儿,少有人会像A一样情绪如此失控。 泪如雨下,半...
    天地一苍鹰阅读 494评论 0 4
  • 工作台上摆放的笔记和订单,以老旧的古董秤砣为镇纸,它们整齐摆放的样子很符合一个强迫症患者的审美
    CNBLUEone阅读 223评论 0 0
  • 今天看了《感动中国》,其实每年都要看,每年都有不一样的感动。今年印象最深刻的是那个村官大学生,耶鲁大学生甘愿当大学...
    最喜不过淡雅阅读 235评论 2 1
  • 摘自《瓦尔登湖》第四页。 我怕,在美的季候里,我没有收到通知,就开始丢失自己。我怕,在时代的波澜里,我连呼救的机会...
    翔于阅读 737评论 0 1