1
0
Fork 0

0606 updated

main
Wannz 2022-06-06 11:51:36 +08:00
parent b9836ef7ce
commit f71cf6205a
38 changed files with 2548 additions and 2 deletions

73
README.CN.md 100755
View File

@ -0,0 +1,73 @@
# Agora Miniapp Tutorial × FinClip
*Read this in other languages [English](README.md)*
## 简介
本 Demo 基于 Agora Miniapp SDK 开发,能帮助开发者在 [FinClip 小程序](https://www.finclip.com/)中实现视频通话及互动直播等功能。
本页演示如下内容:
* 集成 Agora Miniapp SDK
* 加入频道
* 推流
* 订阅远端流
* 离开频道
## 准备开发环境
1. 请确保本地已安装微信开发者工具
2. 请确保有一个支持 **live-pusher****live-player** 组件的微信公众平台账号。只有特定行业的认证企业账号才可使用这两个组件。详情请[点击这里](https://www.finclip.com/mop/document/develop/component/media.html#live-pusher)
3. 请确保在微信公众平台账号的开发设置中,给予以下域名请求权限:
* https://miniapp.agoraio.cn
* https://uni-webcollector.agora.io
* wss://miniapp.agoraio.cn
4. 若使用的是1.1.2 BETA后的版本则需要额外添加以下域名
* https://miniapp-1.agoraio.cn
* https://miniapp-2.agoraio.cn
* https://miniapp-3.agoraio.cn
* https://miniapp-4.agoraio.cn
## 运行示例程序
1. 在 [FinClip](https://www.finclip.com/) 与 [Agora.io](http://dashboard.agora.io/signin/) 注册账号,并创建自己的测试项目,获取 App ID。如需获取 Token 或 Channel Key请启用 App Certificate
2. 下载本页示例程序
3. 打开 *utils* 文件夹,在 *config.js* 文件中填入获取到的 App ID
const APPID = 'abcdefg'
4. 下载 [Agora Miniapp SDK](https://docs.agora.io/cn/Agora%20Platform/downloads),并将 SDK 重新命名为 “mini-app-sdk-production.js"
5. 将更名后的 "mini-app-sdk-production.js" 文件保存在本示例程序的 *lib* 文件夹下
6. 启动微信开发者工具并导入该示例程序
7. 输入频道名,加入频道。邀请你的朋友加入同一个频道,就可以开始视频互通了。
**声网的 Native SDK 可以直接与小程序互通。**
## 关于 Token/Dynamic Key
如果启用了 App Certificate还需要在服务端生成 Token 或 Dynamic Key 用于鉴权。将生成的 Token 或 Dynamic Key 填入如下方法中:
//...
client.join(<your key/access token here>, channel, uid, () => {
//...
关于如何生成 Token 或 Dynamic Key 详见 [Token](https://docs.agora.io/cn/2.2/product/Video/Agora%20Basics/key_native?platform=Android) 或 [Dynamic Key](https://docs.agora.io/cn/2.2/product/Video/Agora%20Basics/key_web?platform=Web)。
## 反馈
如果你有任何问题或建议,可以通过 [issue](https://github.com/AgoraIO/Agora-Miniapp-Tutorial/issues) 的形式反馈。
## 相关资源
- 你可以先参阅 [常见问题](https://docs.agora.io/cn/faq)
- 如果你想了解更多官方示例,可以参考 [官方 SDK 示例](https://github.com/AgoraIO)
- 如果你想了解声网 SDK 在复杂场景下的应用,可以参考 [官方场景案例](https://github.com/AgoraIO-usecase)
- 如果你想了解声网的一些社区开发者维护的项目,可以查看 [社区](https://github.com/AgoraIO-Community)
- 若遇到问题需要开发者帮助,你可以到 [开发者社区](https://rtcdeveloper.com/) 提问
- 如果需要售后技术支持, 你可以联系 FinClip
## 代码许可
示例项目遵守 MIT 许可证。

70
README.md 100644 → 100755
View File

@ -1,2 +1,68 @@
# Agora-Miniapp-Tutorial
Hello world for Agora SDK running in FinClip
# Agora Miniapp Tutorial × FinClip
*其他语言版本:[简体中文](README.CN.md)*
## Introduction
Built upon the Agora Miniapp SDK, the Agora Miniapp Sample App is an open-source demo that integrates video chat and live broadcast into your [FinClip Mini Application](https://www.finclip.com/).
With this sample app, you can:
* Integrate the Agora Miniapp SDK
* Join a channel
* Push your local stream to the channel
* Subscribe to remote streams in the same channel
* Leave a channel
## Preparing the Developer Environment
1. Ensure that you have installed the [FIDE](https://www.finclip.com/downloads/?activeTab=ide).
2. Ensure that you have a wechat OpenPlatform account that supports **live-pusher** and **live-player**. Only certified corporate accounts in certain industry have access to these two components. For details, click [here](https://www.finclip.com/mop/document/develop/component/media.html#live-pusher) .
3. Ensure that you have granted access to the following domains in your OpenPlatform account:
* https://miniapp.agoraio.cn
* wss://miniapp.agoraio.cn
## Running the App
1. Create a developer account at [FinClip](https://www.finclip.com/) / [Agora.io](http://dashboard.agora.io/signin/) , create a new project and obtain an App ID, and enable the App Certificate.
2. Download this project.
3. Fill in the App ID in *config.js* in the *utils* folder of this project:
const APPID = 'abcdefg'
4. Contact contact@finogeeks.com / sales@agora.io to abtain the Agora Miniapp SDK, and rename the SDK to "mini-app-sdk-production.js".
5. Save the "mini-app-sdk-production.js" under the *lib* folder of this project.
6. Start the WeChat Developer Tool and import this project.
7. Enter a channel name and join a channel. Invite your friend to join in the same channel and you will be able to see each other.
## About the Token/Dynamic Key
If you have enabled the App Certificate, you will need to generate the Token/Dynamic Key at the server for authentication purposes. Use it in the following method:
//...
client.join(<your key/access token here>, channel, uid, () => {
//...
See [Token](https://docs.agora.io/en/2.2/product/Video/Agora%20Basics/key_native?platform=Android) or [Dynamic Key](https://docs.agora.io/en/2.2/product/Video/Agora%20Basics/key_web?platform=Web) for generating the Token or Key at the server.
## Feedback
If you have any problems or suggestions regarding the sample projects, feel free to file an [issue](https://github.com/AgoraIO/Agora-Miniapp-Tutorial/issues).
## Related resources
- Check our [FAQ](https://docs.agora.io/en/faq) to see if your issue has been recorded.
- Dive into [Agora SDK Samples](https://github.com/AgoraIO) to see more tutorials
- Take a look at [Agora Use Case](https://github.com/AgoraIO-usecase) for more complicated real use case
- Repositories managed by developer communities can be found at [Agora Community](https://github.com/AgoraIO-Community)
- If you encounter problems during integration, feel free to ask questions in [Stack Overflow](https://stackoverflow.com/questions/tagged/agora.io)
## License
The sample projects are under the MIT license.

23
app.js 100755
View File

@ -0,0 +1,23 @@
const Utils = require("./utils/util.js");
//app.js
App({
onLaunch: function () {
// 展示本地存储能力
Utils.checkSystemInfo(this);
wx.authorize({
scope: 'scope.record',
});
// 登录
wx.login({
success: res => {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
}
})
},
globalData: {
userInfo: null
}
})

15
app.json 100755
View File

@ -0,0 +1,15 @@
{
"pages": [
"pages/index/index",
"pages/meeting/meeting",
"pages/test/test"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#8FD3F5",
"navigationBarTitleText": "",
"navigationBarTextStyle": "white",
"backgroundColor": "#8FD3F5"
},
"sitemapLocation": "sitemap.json"
}

10
app.wxss 100755
View File

@ -0,0 +1,10 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}

20
common.wxss 100755
View File

@ -0,0 +1,20 @@
.agora-bg {
width: 100%;
height: 100%;
position: absolute;
background: linear-gradient(to bottom, #8FD3F5, #D3FDFF);
color: #5083AA;
}
.h1{
font-size: 32rpx;
}
.h2{
font-size: 24rpx;
}
.flex-center-column{
display: flex;
flex-direction: column;
align-items: center;
}

View File

@ -0,0 +1,153 @@
// components/agora-player/agora-player.js
const Utils = require("../../utils/util.js")
Component({
/**
* 组件的属性列表
*/
properties: {
width: {
type: Number,
value: 0
},
height: {
type: Number,
value: 0
},
x: {
type: Number,
value: 0
},
y: {
type: Number,
value: 0
},
debug: {
type: Boolean,
value: !1
},
/**
* 0 - loading, 1 - ok, 2 - error
*/
status: {
type: String,
value: "loading",
observer: function (newVal, oldVal, changedPath) {
Utils.log(`player status changed from ${oldVal} to ${newVal}`);
}
},
orientation: {
type: String,
value: "vertical"
},
name: {
type: String,
value: ""
},
uid: {
type: String,
value: ""
},
url: {
type: String,
value: "",
observer: function (newVal, oldVal, changedPath) {
// 属性被改变时执行的函数可选也可以写成在methods段中定义的方法名字符串, 如:'_propertyChange'
// 通常 newVal 就是新设置的数据, oldVal 是旧数据
Utils.log(`player url changed from ${oldVal} to ${newVal}, path: ${changedPath}`);
}
}
},
/**
* 组件的初始数据
*/
data: {
playContext: null,
detached: false
},
/**
* 组件的方法列表
*/
methods: {
/**
* start live player via context
* in most cases you should not call this manually in your page
* as this will be automatically called in component ready method
*/
start: function () {
const uid = this.data.uid;
Utils.log(`starting player ${uid}`);
if (this.data.status === "ok") {
Utils.log(`player ${uid} already started`);
return;
}
if (this.data.detached) {
Utils.log(`try to start pusher while component already detached`);
return;
}
this.data.playContext.play();
},
/**
* stop live pusher context
*/
stop: function () {
const uid = this.data.uid;
Utils.log(`stopping player ${uid}`);
this.data.playContext.stop();
},
/**
* rotate video by rotation
*/
rotate: function (rotation) {
let orientation = rotation === 90 || rotation === 270 ? "horizontal" : "vertical";
Utils.log(`rotation: ${rotation}, orientation: ${orientation}, uid: ${this.data.uid}`);
this.setData({
orientation: orientation
});
},
/**
* 播放器状态更新回调
*/
playerStateChange: function (e) {
Utils.log(`live-player id: ${e.target.id}, code: ${e.detail.code}`)
let uid = parseInt(e.target.id.split("-")[1]);
if (e.detail.code === 2004) {
Utils.log(`live-player ${uid} started playing`);
if(this.data.status === "loading") {
this.setData({
status: "ok"
});
}
} else if (e.detail.code === -2301) {
Utils.log(`live-player ${uid} stopped`, "error");
this.setData({
status: "error"
})
}
},
},
/**
* 组件生命周期
*/
ready: function () {
Utils.log(`player ${this.data.uid} ready`);
this.data.playContext || (this.data.playContext = wx.createLivePlayerContext(`player-${this.data.uid}`, this));
// if we already have url when component mounted, start directly
if(this.data.url) {
this.start();
}
},
moved: function () {
Utils.log(`player ${this.data.uid} moved`);
},
detached: function () {
Utils.log(`player ${this.data.uid} detached`);
// auto stop player when detached
this.data.playContext && this.data.playContext.stop();
this.data.detached = true;
}
})

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,11 @@
<!--components/agora-player/agora-player.wxml-->
<view class="play-container" style="left:{{x}}px; top:{{y}}px; width: {{width}}px; height: {{height}}px; ">
<live-player wx:if="{{url!==''}}" id="player-{{uid}}" src="{{url}}" mode="RTC" class="player" orientation="{{orientation}}" bindstatechange="playerStateChange" object-fit="fillCrop" style="height:{{height}}px; position: absolute; width: 100%; top: 0; left: 0;"
debug="{{debug}}" autoplay="true"/>
<cover-view wx-if="{{status !== 'ok'}}" class="sud flex-center-column" style="position: absolute; width: 100%; height:{{height}}px;justify-content:center">
<cover-image style="width: 128px;height:103px" src="../../images/{{status}}.png"></cover-image>
</cover-view>
<cover-view class="" style="position: absolute;top:10px;left:10px;font-size: 28rpx; right: 10px">
{{name}}({{uid}})
</cover-view>
</view>

View File

@ -0,0 +1,12 @@
/* components/agora-player/agora-player.wxss */
@import "../../common.wxss";
.play-container{
background: black;
display: block;
position: absolute;
}
.sud{
background-color: #1B2A38;
opacity:0.65;
}

View File

@ -0,0 +1,158 @@
// components/agora-pusher.js
const Utils = require("../../utils/util.js")
Component({
/**
* 组件的属性列表
*/
properties: {
minBitrate: {
type: Number,
value: 200
},
maxBitrate: {
type: Number,
value: 500
},
width: {
type: Number,
value: 0
},
height: {
type: Number,
value: 0
},
x: {
type: Number,
value: 0
},
y: {
type: Number,
value: 0
},
muted: {
type: Boolean,
value: !1
},
debug: {
type: Boolean,
value: !1
},
beauty: {
type: String,
value: 0
},
aspect: {
type: String,
value: "3:4"
},
/**
* 0 - loading, 1 - ok, 2 - error
*/
status: {
type: String,
value: "loading",
observer: function (newVal, oldVal, changedPath) {
Utils.log(`player status changed from ${oldVal} to ${newVal}`);
}
},
url: {
type: String,
value: "",
observer: function (newVal, oldVal, changedPath) {
// 属性被改变时执行的函数可选也可以写成在methods段中定义的方法名字符串, 如:'_propertyChange'
// 通常 newVal 就是新设置的数据, oldVal 是旧数据
Utils.log(`pusher url changed from ${oldVal} to ${newVal}, path: ${changedPath}`);
}
}
},
/**
* 组件的初始数据
*/
data: {
pusherContext: null,
detached: false
},
/**
* 组件的方法列表
*/
methods: {
/**
* start live pusher via context
* in most cases you should not call this manually in your page
* as this will be automatically called in component ready method
*/
start() {
Utils.log(`starting pusher`);
this.data.pusherContext.stop();
if (this.data.detached) {
Utils.log(`try to start pusher while component already detached`);
return;
}
this.data.pusherContext.start();
},
/**
* stop live pusher context
*/
stop() {
Utils.log(`stopping pusher`);
this.data.pusherContext.stop();
},
/**
* switch camera direction
*/
switchCamera() {
this.data.pusherContext.switchCamera();
},
/**
* 推流状态更新回调
*/
recorderStateChange: function (e) {
Utils.log(`live-pusher code: ${e.detail.code} - ${e.detail.message}`)
if (e.detail.code === -1307) {
//re-push
Utils.log('live-pusher stopped', "error");
this.setData({
status: "error"
})
//emit event
this.triggerEvent('pushfailed');
}
if (e.detail.code === 1008) {
//started
Utils.log(`live-pusher started`);
if(this.data.status === "loading") {
this.setData({
status: "ok"
})
}
}
},
recorderNetChange: function(e) {
Utils.log(`network: ${JSON.stringify(e.detail)}`);
}
},
/**
* 组件生命周期
*/
ready: function () {
Utils.log("pusher ready");
this.data.pusherContext || (this.data.pusherContext = wx.createLivePusherContext(this));
},
moved: function () {
Utils.log("pusher moved");
},
detached: function () {
Utils.log("pusher detached");
// auto stop pusher when detached
this.data.pusherContext && this.data.pusherContext.stop();
this.data.detached = true;
}
})

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,8 @@
<!--components/agora-pusher.wxml-->
<view class="pusher-container" id="rtcpusher" style="top: {{y}}px; left: {{x}}px; width: {{width}}px; height: {{height}}px; position: absolute;">
<live-pusher wx:if="{{url!==''}}" style="height:{{height}}px; position: absolute; width: 100%; " url="{{url}}" mode="RTC" aspect="{{aspect}}" class="camera" bindstatechange="recorderStateChange" bindnetstatus="recorderNetChange" background-mute="true" muted="{{muted}}" beauty="{{beauty}}"
max-bitrate="500" min-bitrate="200" waiting-image="https://webdemo.agora.io/away.png" debug="{{debug}}" autopush="true" />
<cover-view wx-if="{{status !== 'ok'}}" class="sud flex-center-column" style="position: absolute; width: 100%; height: 100%;justify-content:center">
<cover-image style="width: 128px;height:103px" src="../../images/{{status}}.png"></cover-image>
</cover-view>
</view>

View File

@ -0,0 +1,12 @@
/* components/agora-pusher.wxss */
@import "../../common.wxss";
.pusher-container{
background: black;
display: block;
position: absolute;
}
.sud{
background-color: #1B2A38;
opacity:0.65;
}

BIN
images/cover.png 100755

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
images/error.png 100755

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/loading.png 100755

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/logo.png 100755

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
images/network.png 100755

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,137 @@
const app = getApp()
const Utils = require('../../utils/util.js')
// pages/index/index.js.js
Page({
/**
* 页面的初始数据
*/
data: {
// used to store user info like portrait & nickname
userInfo: {},
hasUserInfo: false,
// whether to disable join btn or not
disableJoin: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this.channel = "";
this.uid = Utils.getUid();
this.lock = false;
let userInfo = wx.getStorageSync("userInfo");
if (userInfo){
this.setData({
hasUserInfo: true,
userInfo: userInfo
});
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 只有提供了该回调才会出现转发选项
*/
onShareAppMessage() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* callback to get user info
* using wechat open-type
*/
onGotUserInfo: function(e){
let userInfo = e.detail.userInfo || {};
// store data for next launch use
wx.setStorage({
key: 'userInfo',
data: userInfo,
})
this.onJoin(userInfo);
},
/**
* check if join is locked now, this is mainly to prevent from clicking join btn to start multiple new pages
*/
checkJoinLock: function() {
return !(this.lock || false);
},
lockJoin: function() {
this.lock = true;
},
unlockJoin: function() {
this.lock = false;
},
onJoin: function (userInfo) {
userInfo = userInfo || {};
let value = this.channel || "";
let uid = this.uid;
if (!value) {
wx.showToast({
title: '请提供一个有效的房间名',
icon: 'none',
duration: 2000
})
} else {
if(this.checkJoinLock()) {
this.lockJoin();
if (value === "agora") {
// go to test page if channel name is agora
wx.navigateTo({
url: `../test/test`
});
} else if (value === "agora2") {
// go to test page if channel name is agora
wx.navigateTo({
url: `../test2/test2`
});
} else {
wx.showModal({
title: '是否推流',
content: '选择取消则作为观众加入,观众模式不推流',
showCancel: true,
success: function (res) {
let role = "audience";
if (res.confirm) {
role = "broadcaster";
}
wx.navigateTo({
url: `../meeting/meeting?channel=${value}&uid=${uid}&role=${role}`
});
}
})
}
}
}
},
onInputChannel: function (e) {
let value = e.detail.value;
this.channel = value;
}
})

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,22 @@
<!--index.wxml-->
<view class=" agora-bg">
<view class="content flex-center-column">
<view class="logo-section flex-center-column">
<image class="logo" style="width: 300rpx; height: 200rpx;" mode="aspectFit" src="../../images/logo.png"></image>
<text class="h1">声网小程序实时连麦</text>
</view>
<view class="user-section flex-center-column">
<image class="userinfo-avatar" src="{{userInfo.avatarUrl}}" background-size="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</view>
<view class="form-section flex-center-column">
<view class="inputWrapper">
<input placeholder-style='color:#A3D1E0' class="channelInput" placeholder='输入房间名' bindinput="onInputChannel" bindconfirm="onInputChannel" bindblur="onInputChannel"></input>
</view>
<button plain="true" open-type="getUserInfo" bindgetuserinfo="onGotUserInfo" disabled="{{disableJoin}}" class="joinBtn">加入房间</button>
</view>
<view class="footer flex-center-column">
<text>Powered by Agora. Build v1.1.1180907</text>
</view>
</view>
</view>

View File

@ -0,0 +1,91 @@
@import "../../common.wxss";
page {
height: 100%;
}
.content {
position: absolute;
left: 80rpx;
right: 80rpx;
width: auto;
height: 100%;
}
.content .logo-section .logo{
margin-bottom: 20rpx;
}
.content .logo-section .h1{
margin-bottom: 10rpx;
}
.content .user-section{
margin-bottom: 40rpx;
}
.content .userinfo-avatar {
width: 128rpx;
height: 128rpx;
margin: 40rpx 20rpx 20rpx 20rpx;
border-radius: 128rpx;
border: 2px solid white;
box-shadow: 1px 1px 1px rgba(0,0,0,0.3);
}
.content .userinfo-nickname {
color: #2F597A;
font-size: 28rpx;
text-align: center;
}
.content .form-section{
width: 100%;
}
.content .inputWrapper{
width: 100%;
border-radius: 10rpx;
background-color: rgba(255,255,255,0.4);
border: 1px solid #98BECA;
}
.content .channelInput{
font-size: 28rpx;
padding: 0 30rpx;
height: 80rpx;
color: #5083AA;
}
.content .joinBtn{
background-color: #FEFFFE;
color: #5083AA;
width: 100%;
height: 80rpx;
font-size: 28rpx;
margin-top: 32rpx;
line-height: 80rpx;
box-shadow: 0px 4px 4px rgba(84,163,186,0.15);
font-weight: bold;
border: 0;
}
.content .envBtn{
/* background-color: white; */
color: white;
height: 80rpx;
font-size: 28rpx;
margin-top: 32rpx;
line-height: 80rpx;
border: 0;
margin-bottom: -10rpx;
}
.content .footer{
justify-content: flex-end;
flex-grow: 1;
font-size: 24rpx;
margin-bottom: 32rpx;
color: #63C5E6;
}

View File

@ -0,0 +1,875 @@
// pages/meeting/meeting.js
const app = getApp()
const Utils = require('../../utils/util.js')
const AgoraMiniappSDK = require("../../lib/mini-app-sdk-production.js");
const max_user = 10;
const Layouter = require("../../utils/layout.js");
const APPID = require("../../utils/config.js").APPID;
/**
* log relevant, remove these part and relevant code if not needed
*/
const Uploader = require("../../utils/uploader.js")
const LogUploader = Uploader.LogUploader;
const LogUploaderTask = Uploader.LogUploaderTask;
Page({
/**
* 页面的初始数据
*/
data: {
/**
* media objects array
* this involves both player & pusher data
* we use type to distinguish
* a sample media object
* {
* key: **important, change this key only when you want to completely refresh your dom**,
* type: 0 - pusher, 1 - player,
* uid: uid of stream,
* holding: when set to true, the block will stay while native control hidden, used when needs a placeholder for media block,
* url: url of pusher/player
* left: x of pusher/player
* top: y of pusher/player
* width: width of pusher/player
* height: height of pusher/player
* }
*/
media: [],
/**
* muted
*/
muted: false,
/**
* beauty 0 - 10
*/
beauty: 0,
totalUser: 1,
/**
* debug
*/
debug: false
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function(options) {
Utils.log(`onLoad`);
// get channel from page query param
this.channel = options.channel;
// default role to broadcaster
this.role = options.role || "broadcaster";
// get pre-gened uid, this uid will be different every time the app is started
this.uid = Utils.getUid();
// store agora client
this.client = null;
// store layouter control
this.layouter = null;
// prevent user from clicking leave too fast
this.leaving = false;
// page setup
wx.setNavigationBarTitle({
title: `${this.channel}(${this.uid})`
});
wx.setKeepScreenOn({
keepScreenOn: true
});
/**
* please remove this part in your production environment
*/
if (/^sdktest.*$/.test(this.channel)) {
this.testEnv = true
wx.showModal({
title: '提示',
content: '您正处于测试环境',
showCancel: false
})
}
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function() {
let channel = this.channel;
let uid = this.uid;
Utils.log(`onReady`);
// schedule log auto update, remove this if this is not needed
this.logTimer = setInterval(() => {
this.uploadLogs();
}, 60 * 60 * 1000);
// init layouter control
this.initLayouter();
// init agora channel
this.initAgoraChannel(uid, channel).then(url => {
Utils.log(`channel: ${channel}, uid: ${uid}`);
Utils.log(`pushing ${url}`);
let ts = new Date().getTime();
if (this.isBroadcaster()) {
// first time init, add pusher media to view
this.addMedia(0, this.uid, url, {
key: ts
});
}
}).catch(e => {
Utils.log(`init agora client failed: ${e}`);
wx.showToast({
title: `客户端初始化失败`,
icon: 'none',
duration: 5000
});
});
},
/**
* 只有提供了该回调才会出现转发选项
*/
onShareAppMessage() {
},
/**
* calculate size based on current media length
* sync the layout info into each media object
*/
syncLayout(media) {
let sizes = this.layouter.getSize(media.length);
for (let i = 0; i < sizes.length; i++) {
let size = sizes[i];
let item = media[i];
if (item.holding) {
//skip holding item
continue;
}
item.left = parseFloat(size.x).toFixed(2);
item.top = parseFloat(size.y).toFixed(2);
item.width = parseFloat(size.width).toFixed(2);
item.height = parseFloat(size.height).toFixed(2);
}
return media;
},
/**
* check if current media list has specified uid & mediaType component
*/
hasMedia(mediaType, uid) {
let media = this.data.media || [];
return media.filter(item => {
return item.type === mediaType && `${item.uid}` === `${uid}`
}).length > 0
},
/**
* add media to view
* type: 0 - pusher, 1 - player
* *important* here we use ts as key, when the key changes
* the media component will be COMPLETELY refreshed
* this is useful when your live-player or live-pusher
* are in a bad status - say -1307. In this case, update the key
* property of media object to fully refresh it. The old media
* component life cycle event detached will be called, and
* new media component life cycle event ready will then be called
*/
addMedia(mediaType, uid, url, options) {
Utils.log(`add media ${mediaType} ${uid} ${url}`);
let media = this.data.media || [];
if (mediaType === 0) {
//pusher
media.splice(0, 0, {
key: options.key,
type: mediaType,
uid: `${uid}`,
holding: false,
url: url,
left: 0,
top: 0,
width: 0,
height: 0
});
} else {
//player
media.push({
key: options.key,
rotation: options.rotation,
type: mediaType,
uid: `${uid}`,
holding: false,
url: url,
left: 0,
top: 0,
width: 0,
height: 0
});
}
media = this.syncLayout(media);
return this.refreshMedia(media);
},
/**
* remove media from view
*/
removeMedia: function(uid) {
Utils.log(`remove media ${uid}`);
let media = this.data.media || [];
media = media.filter(item => {
return `${item.uid}` !== `${uid}`
});
if (media.length !== this.data.media.length) {
media = this.syncLayout(media);
this.refreshMedia(media);
} else {
Utils.log(`media not changed: ${JSON.stringify(media)}`)
return Promise.resolve();
}
},
/**
* update media object
* the media component will be fully refreshed if you try to update key
* property.
*/
updateMedia: function(uid, options) {
Utils.log(`update media ${uid} ${JSON.stringify(options)}`);
let media = this.data.media || [];
let changed = false;
for (let i = 0; i < media.length; i++) {
let item = media[i];
if (`${item.uid}` === `${uid}`) {
media[i] = Object.assign(item, options);
changed = true;
Utils.log(`after update media ${uid} ${JSON.stringify(item)}`)
break;
}
}
if (changed) {
return this.refreshMedia(media);
} else {
Utils.log(`media not changed: ${JSON.stringify(media)}`)
return Promise.resolve();
}
},
/**
* call setData to update a list of media to this.data.media
* this will trigger UI re-rendering
*/
refreshMedia: function(media) {
return new Promise((resolve) => {
for (let i = 0; i < media.length; i++) {
if (i < max_user) {
//show
media[i].holding = false;
} else {
//hide
media[i].holding = true;
}
}
if (media.length > max_user) {
wx.showToast({
title: '由于房内人数超过7人部分视频未被加载显示',
});
}
Utils.log(`updating media: ${JSON.stringify(media)}`);
this.setData({
media: media
}, () => {
resolve();
});
});
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function() {
let media = this.data.media || [];
media.forEach(item => {
if (item.type === 0) {
//return for pusher
return;
}
let player = this.getPlayerComponent(item.uid);
if (!player) {
Utils.log(`player ${item.uid} component no longer exists`, "error");
} else {
// while in background, the player maybe added but not starting
// in this case we need to start it once come back
player.start();
}
});
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function() {
},
onError: function(e) {
Utils.log(`error: ${JSON.stringify(e)}`);
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function() {
Utils.log(`onUnload`);
clearInterval(this.logTimer);
clearTimeout(this.reconnectTimer);
this.logTimer = null;
this.reconnectTimer = null;
// unlock index page join button
let pages = getCurrentPages();
if (pages.length > 1) {
//unlock join
let indexPage = pages[0];
indexPage.unlockJoin();
}
// unpublish sdk and leave channel
if (this.isBroadcaster()) {
try {
this.client && this.client.unpublish();
} catch (e) {
Utils.log(`unpublish failed ${e}`);
}
}
this.client && this.client.leave();
},
/**
* callback when leave button called
*/
onLeave: function() {
if (!this.leaving) {
this.leaving = true;
this.navigateBack();
}
},
/**
* navigate to previous page
* if started from shared link, it's possible that
* we have no page to go back, in this case just redirect
* to index page
*/
navigateBack: function() {
Utils.log(`attemps to navigate back`);
if (getCurrentPages().length > 1) {
//have pages on stack
wx.navigateBack({});
} else {
//no page on stack, usually means start from shared links
wx.redirectTo({
url: '../index/index',
});
}
},
/**
* 推流状态更新回调
*/
onPusherFailed: function() {
Utils.log('pusher failed completely', "error");
wx.showModal({
title: '发生错误',
content: '推流发生错误,请重新进入房间重试',
showCancel: false,
success: () => {
this.navigateBack();
}
})
},
/**
* 静音回调
*/
onMute: function() {
if (!this.data.muted) {
this.client.muteLocal('audio', () => {
console.log('muteLocal success')
}, err => {
console.log(err)
});
} else {
this.client.unmuteLocal('audio', () => {
console.log('unmuteLocal success')
}, err => {
console.log(err)
});
}
this.setData({
muted: !this.data.muted
})
},
/**
* 摄像头方向切换回调
*/
onSwitchCamera: function() {
Utils.log(`switching camera`);
// get pusher component via id
const agoraPusher = this.getPusherComponent();
agoraPusher && agoraPusher.switchCamera();
},
/**
* 美颜回调
*/
onMakeup: function() {
let beauty = this.data.beauty == 5 ? 0 : 5;
this.setData({
beauty: beauty
})
},
/**
* 上传日志
*/
uploadLogs: function() {
let logs = Utils.getLogs();
Utils.clearLogs();
let totalLogs = logs.length;
let tasks = [];
let part = 0;
let ts = new Date().getTime();
// 1w logs per task slice
const sliceSize = 500;
do {
let content = logs.splice(0, sliceSize);
tasks.push(new LogUploaderTask(content, this.channel, part++, ts, this.uid));
} while (logs.length > sliceSize)
wx.showLoading({
title: '0%',
mask: true
})
LogUploader.off("progress").on("progress", e => {
let remain = e.remain;
let total = e.total;
Utils.log(`log upload progress ${total - remain}/${total}`);
if (remain === 0) {
wx.hideLoading();
wx.showToast({
title: `上传成功`,
});
} else {
wx.showLoading({
mask: true,
title: `${((total - remain) / total * 100).toFixed(2)}%`,
})
}
});
LogUploader.on("error"), e => {
wx.hideLoading();
wx.showToast({
title: `上传失败: ${e}`,
});
}
LogUploader.scheduleTasks(tasks);
},
/**
* 上传日志回调
*/
onSubmitLog: function() {
let page = this;
let mediaAction = this.isBroadcaster() ? "下麦" : "上麦"
wx.showActionSheet({
itemList: [mediaAction, "上传日志"],
success: res => {
let tapIndex = res.tapIndex;
if (tapIndex == 0) {
if (this.isBroadcaster()) {
this.becomeAudience().then(() => {
this.removeMedia(this.uid);
}).catch(e => {
Utils.log(`switch to audience failed ${e.stack}`);
})
} else {
let ts = new Date().getTime();
this.becomeBroadcaster().then(url => {
this.addMedia(0, this.uid, url, {
key: ts
});
}).catch(e => {
Utils.log(`switch to broadcaster failed ${e.stack}`);
})
}
} else if (tapIndex === 1) {
this.setData({
debug: !this.data.debug
})
wx.showModal({
title: '遇到使用问题?',
content: '点击确定可以上传日志,帮助我们了解您在使用过程中的问题',
success: function(res) {
if (res.confirm) {
console.log('用户点击确定')
page.uploadLogs();
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
}
}
})
},
/**
* 获取屏幕尺寸以用于之后的视窗计算
*/
initLayouter: function() {
// get window size info from systemInfo
const systemInfo = app.globalData.systemInfo;
// 64 is the height of bottom toolbar
this.layouter = new Layouter(systemInfo.windowWidth, systemInfo.windowHeight - 64);
},
/**
* 初始化sdk推流
*/
initAgoraChannel: function(uid, channel) {
return new Promise((resolve, reject) => {
let client = {}
if (this.testEnv) {
client = new AgoraMiniappSDK.Client({
servers: ["wss://miniapp.agoraio.cn/120-131-14-112/api"]
});
} else {
client = new AgoraMiniappSDK.Client()
}
//subscribe stream events
this.subscribeEvents(client);
AgoraMiniappSDK.LOG.onLog = (text) => {
// callback to expose sdk logs
Utils.log(text);
};
AgoraMiniappSDK.LOG.setLogLevel(-1);
this.client = client;
client.init(APPID, () => {
Utils.log(`client init success`);
// pass key instead of undefined if certificate is enabled
client.join(undefined, channel, uid, () => {
client.setRole(this.role);
Utils.log(`client join channel success`);
//and get my stream publish url
if (this.isBroadcaster()) {
client.publish(url => {
Utils.log(`client publish success`);
resolve(url);
}, e => {
Utils.log(`client publish failed: ${e.code} ${e.reason}`);
reject(e)
});
} else {
resolve();
}
}, e => {
Utils.log(`client join channel failed: ${e.code} ${e.reason}`);
reject(e)
})
}, e => {
Utils.log(`client init failed: ${e} ${e.code} ${e.reason}`);
reject(e);
});
});
},
reinitAgoraChannel: function(uid, channel) {
return new Promise((resolve, reject) => {
let client = {}
if (this.testEnv) {
client = new AgoraMiniappSDK.Client({
servers: ["wss://miniapp.agoraio.cn/120-131-14-112/api"]
});
} else {
client = new AgoraMiniappSDK.Client()
}
//subscribe stream events
this.subscribeEvents(client);
AgoraMiniappSDK.LOG.onLog = (text) => {
// callback to expose sdk logs
Utils.log(text);
};
AgoraMiniappSDK.LOG.setLogLevel(-1);
let uids = this.data.media.map(item => {
return item.uid;
});
this.client = client;
client.setRole(this.role);
client.init(APPID, () => {
Utils.log(`client init success`);
// pass key instead of undefined if certificate is enabled
Utils.log(`rejoin with uids: ${JSON.stringify(uids)}`);
client.rejoin(undefined, channel, uid, uids, () => {
Utils.log(`client join channel success`);
if (this.isBroadcaster()) {
client.publish(url => {
Utils.log(`client publish success`);
resolve(url);
}, e => {
Utils.log(`client publish failed: ${e.code} ${e.reason}`);
reject(e)
});
} else {
resolve();
}
}, e => {
Utils.log(`client join channel failed: ${e.code} ${e.reason}`);
reject(e)
})
}, e => {
Utils.log(`client init failed: ${e} ${e.code} ${e.reason}`);
reject(e);
});
});
},
/**
* return player component via uid
*/
getPlayerComponent: function(uid) {
const agoraPlayer = this.selectComponent(`#rtc-player-${uid}`);
return agoraPlayer;
},
/**
* return pusher component
*/
getPusherComponent: function() {
const agorapusher = this.selectComponent(`#rtc-pusher`);
return agorapusher;
},
becomeBroadcaster: function() {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error("no client available"))
}
let client = this.client
this.role = "broadcaster"
client.setRole(this.role, ({updateURL}) => {
Utils.log(`client switching role to ${this.role}`);
setTimeout(()=>{
client.publish(updateURL => {
Utils.log(`client publish success`);
resolve(updateURL);
}, e => {
Utils.log(`client publish failed: ${e.code} ${e.reason}`);
reject(e)
});
}, 2000)
})
})
},
becomeAudience: function() {
return new Promise((resolve, reject) => {
if (!this.client) {
return reject(new Error("no client available"))
}
let client = this.client
client.unpublish(() => {
Utils.log(`client unpublish success`);
this.role = "audience"
Utils.log(`client switching role to ${this.role}`);
client.setRole(this.role)
resolve();
}, e => {
Utils.log(`client unpublish failed: ${e.code} ${e.reason}`);
reject(e)
});
})
},
/**
* reconnect when bad things happens...
*/
reconnect: function() {
wx.showToast({
title: `尝试恢复链接...`,
icon: 'none',
duration: 5000
});
// always destroy client first
// *important* miniapp supports 2 websockets maximum at same time
// do remember to destroy old client first before creating new ones
this.client && this.client.destroy();
this.reconnectTimer = setTimeout(() => {
let uid = this.uid;
let channel = this.channel;
this.reinitAgoraChannel(uid, channel).then(url => {
Utils.log(`channel: ${channel}, uid: ${uid}`);
Utils.log(`pushing ${url}`);
let ts = new Date().getTime();
if (this.isBroadcaster()) {
if (this.hasMedia(0, this.uid)) {
// pusher already exists in media list
this.updateMedia(this.uid, {
url: url,
key: ts,
});
} else {
// pusher not exists in media list
Utils.log(`pusher not yet exists when rejoin...adding`);
this.addMedia(0, this.uid, url, {
key: ts
});
}
}
}).catch(e => {
Utils.log(`reconnect failed: ${e}`);
return this.reconnect();
});
}, 1 * 1000);
},
/**
* 如果
*/
isBroadcaster: function() {
return this.role === "broadcaster";
},
/**
* 注册stream事件
*/
subscribeEvents: function(client) {
/**
* sometimes the video could be rotated
* this event will be fired with ratotion
* angle so that we can rotate the video
* NOTE video only supportes vertical or horizontal
* in case of 270 degrees, the video could be
* up side down
*/
client.on("video-rotation", (e) => {
Utils.log(`video rotated: ${e.rotation} ${e.uid}`)
setTimeout(() => {
const player = this.getPlayerComponent(e.uid);
player && player.rotate(e.rotation);
}, 1000);
});
/**
* fired when new stream join the channel
*/
client.on("stream-added", e => {
let uid = e.uid;
const ts = new Date().getTime();
Utils.log(`stream ${uid} added`);
/**
* subscribe to get corresponding url
*/
client.subscribe(uid, (url, rotation) => {
Utils.log(`stream ${uid} subscribed successful`);
let media = this.data.media || [];
let matchItem = null;
for (let i = 0; i < media.length; i++) {
let item = this.data.media[i];
if (`${item.uid}` === `${uid}`) {
//if existing, record this as matchItem and break
matchItem = item;
break;
}
}
if (!matchItem) {
//if not existing, add new media
this.addMedia(1, uid, url, {
key: ts,
rotation: rotation
})
} else {
// if existing, update property
// change key property to refresh live-player
this.updateMedia(matchItem.uid, {
url: url,
key: ts,
});
}
}, e => {
Utils.log(`stream subscribed failed ${e} ${e.code} ${e.reason}`);
});
});
/**
* remove stream when it leaves the channel
*/
client.on("stream-removed", e => {
let uid = e.uid;
Utils.log(`stream ${uid} removed`);
this.removeMedia(uid);
});
/**
* when bad thing happens - we recommend you to do a
* full reconnect when meeting such error
* it's also recommended to wait for few seconds before
* reconnect attempt
*/
client.on("error", err => {
let errObj = err || {};
let code = errObj.code || 0;
let reason = errObj.reason || "";
Utils.log(`error: ${code}, reason: ${reason}`);
let ts = new Date().getTime();
if (code === 501 || code === 904) {
this.reconnect();
}
});
/**
* there are cases when server require you to update
* player url, when receiving such event, update url into
* corresponding live-player, REMEMBER to update key property
* so that live-player is properly refreshed
* NOTE you can ignore such event if it's for pusher or happens before
* stream-added
*/
client.on('update-url', e => {
Utils.log(`update-url: ${JSON.stringify(e)}`);
let uid = e.uid;
let url = e.url;
let ts = new Date().getTime();
if (`${uid}` === `${this.uid}`) {
// if it's not pusher url, update
Utils.log(`ignore update-url`);
} else {
this.updateMedia(uid, {
url: url,
key: ts,
});
}
});
// token 过期
// 开启此监听需要获取 2.4.7 版本 sdk
// client.on("onTokenPrivilegeDidExpire", () => {
// console.log('当前 token 已过期,请更新 token 并重新加入频道')
// });
}
})

View File

@ -0,0 +1,4 @@
{"usingComponents": {
"agora-pusher": "../../components/agora-pusher/agora-pusher",
"agora-player": "../../components/agora-player/agora-player"
}}

View File

@ -0,0 +1,20 @@
<!--index.wxml-->
<view id="main" class="content agora-bg flex-center-column">
<view id="video-container" class="video-container n{{totalUser}}">
<block wx:for="{{media}}" wx:key="key">
<agora-pusher wx:if="{{item.type === 0 && !item.holding}}" id="rtc-pusher" x="{{item.left}}" y="{{item.top}}" width="{{item.width}}" height="{{item.height}}" url="{{item.url}}" muted="{{muted}}" beauty="{{beauty}}" debug="{{debug}}" bindpushfailed="onPusherFailed">
</agora-pusher>
<agora-player wx:if="{{item.type === 1 && !item.holding}}" id="rtc-player-{{item.uid}}" uid="{{item.uid}}" x="{{item.left}}" y="{{item.top}}" width="{{item.width}}" height="{{item.height}}" debug="{{debug}}" url="{{item.url}}">
</agora-player>
</block>
</view>
<view class="footer flex-center-column">
<view class="toolbar">
<button plain="true" class="mic {{muted?'muted': ''}} btn" bindtap='onMute'></button>
<button plain="true" hover-class="hover" class="camera btn" bindtap='onSwitchCamera'></button>
<button plain="true" hover-class="hover" class="hangup btn" bindtap='onLeave'></button>
<button plain="true" class="makeup {{beauty === 5 ?'':'off'}} btn" bindtap='onMakeup'></button>
<button plain="true" hover-class="hover" class="log btn" bindtap='onSubmitLog'></button>
</view>
</view>
</view>

File diff suppressed because one or more lines are too long

119
pages/test/test.js 100755
View File

@ -0,0 +1,119 @@
// pages/test/test.js
Page({
/**
* 页面的初始数据
*/
data: {
playUrl: "",
pushUrl: "",
debug: true
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
wx.setNavigationBarTitle({
title: "测试页面"
});
wx.setKeepScreenOn({
keepScreenOn: true
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
let pages = getCurrentPages();
if (pages.length > 1) {
//unlock join
let indexPage = pages[0];
indexPage.unlockJoin();
}
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
},
switchDebug() {
this.setData({
debug: !this.data.debug
})
},
startPushing: function(e) {
let url = e.detail.value;
this.setData({
pushUrl: url
}, () => {
let context = wx.createLivePusherContext(this);
context.start();
});
},
onStopPushing: function(e) {
let context = wx.createLivePusherContext(this);
context.stop();
},
startPlaying: function(e) {
let url = e.detail.value;
this.setData({
playUrl: url
}, () => {
let context = wx.createLivePlayerContext("player", this);
context.play();
});
},
onPause: function() {
let context = wx.createLivePusherContext(this);
context && context.pause();
},
onResume: function() {
let context = wx.createLivePusherContext(this);
context && context.resume();
}
})

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,21 @@
<!--pages/test/test.wxml-->
<view style="display: flex">
<view style="width: 50%; height: 250px; padding: 20rpx 10rpx 20rpx 20rpx">
<live-pusher audio-quality="high" style="height:250px; width: 100%" url="{{pushUrl}}" mode="RTC" debug="{{debug}}" max-bitrate="500" min-bitrate="200"/>
</view>
<view style="width: 50%; height: 250px; padding: 20rpx 20rpx 20rpx 10rpx">
<live-player id="player" src="{{playUrl}}" mode="RTC" min-cache="0.1" max-cache="0.3" bindstatechange="playerStateChange" object-fit="fillCrop" style="height:250px; width: 100%;" debug="{{debug}}" />
</view>
</view>
<view style="padding: 0rpx 20rpx;border-bottom: 1px solid rgba(0,0,0,0.1);border-top: 1px solid rgba(0,0,0,0.1);">
<input placeholder="推流地址 (rtmp/flv)" bindconfirm='startPushing' placeholder-style='font-size: 28rpx; color: rgba(0,0,0,0.3)' style="font-size: 32rpx; color: #666; padding: 10rpx 0"></input>
</view>
<view style="padding: 0rpx 20rpx;border-bottom: 1px solid rgba(0,0,0,0.1);">
<input placeholder="拉流地址 (rtmp/flv)" bindconfirm='startPlaying' placeholder-style='font-size: 28rpx; color: rgba(0,0,0,0.3)' style="font-size: 32rpx; color: #666; padding: 10rpx 0"></input>
</view>
<view style="padding: 0rpx 20rpx; margin-top: 40rpx">
<text style="font-size: 30rpx; margin-right: 10rpx">Debug:</text> <switch checked="{{debug}}" bindchange="switchDebug" />
<button bindtap="onPause">Pause</button>
<button bindtap="onResume">Resume</button>
<button bindtap="onStopPushing">Stop</button>
</view>

View File

@ -0,0 +1 @@
/* pages/test/test.wxss */

7
sitemap.json 100755
View File

@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}

13
utils/config.js 100755
View File

@ -0,0 +1,13 @@
const APPID = "";
if(APPID === ""){
wx.showToast({
title: `请在config.js中提供正确的appid`,
icon: 'none',
duration: 5000
});
}
module.exports = {
APPID: APPID
}

1
utils/event.js 100755
View File

@ -0,0 +1 @@
module.exports = function (n) { var t = {}, e = []; n = n || this, n.on = function (e, r, l) { return (t[e] = t[e] || []).push([r, l]), n }, n.off = function (r, l) { r || (t = {}); for (var o = t[r] || e, u = o.length = l ? o.length : 0; u--;)l == o[u][0] && o.splice(u, 1); return n }, n.emit = function (r) { for (var l, o = t[r] || e, u = o.length > 0 ? o.slice(0, o.length) : o, i = 0; l = u[i++];)l[0].apply(l[1], e.slice.call(arguments, 1)); return n } };

350
utils/layout.js 100755
View File

@ -0,0 +1,350 @@
class Layouter {
constructor(containerWidth, containerHeight) {
this.containerWidth = containerWidth;
this.containerHeight = containerHeight;
}
getSize(totalUser) {
let sizes = [];
let videoContainerHeight = this.containerHeight;
let videoContainerWidth = this.containerWidth;
switch (totalUser) {
case 0:
return [];
case 1:
return [{
x: 0,
y: 0,
width: videoContainerWidth,
height: videoContainerHeight
}];
case 2:
let y = (videoContainerHeight - videoContainerWidth / 2 * (4 / 3)) / 2;
let height = videoContainerWidth / 2 * (4 / 3);
return [{
x: 0,
y: y,
width: videoContainerWidth / 2,
height: height
},
{
x: videoContainerWidth / 2,
y: y,
width: videoContainerWidth / 2,
height: height
}
];
case 3:
return [{
x: 0,
y: 0,
width: videoContainerWidth,
height: videoContainerHeight - videoContainerWidth / 2
}, {
x: 0,
y: videoContainerHeight - videoContainerWidth / 2,
width: videoContainerWidth / 2,
height: videoContainerWidth / 2
}, {
x: videoContainerWidth / 2,
y: videoContainerHeight - videoContainerWidth / 2,
width: videoContainerWidth / 2,
height: videoContainerWidth / 2
}];
case 4:
return [{
x: 0,
y: 0,
width: videoContainerWidth / 2,
height: videoContainerHeight / 2
}, {
x: videoContainerWidth / 2,
y: 0,
width: videoContainerWidth / 2,
height: videoContainerHeight / 2
}, {
x: 0,
y: videoContainerHeight / 2,
width: videoContainerWidth / 2,
height: videoContainerHeight / 2
}, {
x: videoContainerWidth / 2,
y: videoContainerHeight / 2,
width: videoContainerWidth / 2,
height: videoContainerHeight / 2
}];
case 5:
return [{
x: 0,
y: 0,
width: videoContainerWidth / 2,
height: videoContainerHeight * 3 / 5
}, {
x: videoContainerWidth / 2,
y: 0,
width: videoContainerWidth / 2,
height: videoContainerHeight * 3 / 5
}, {
x: 0,
y: videoContainerHeight * 3 / 5,
width: videoContainerWidth / 3,
height: videoContainerHeight * 2 / 5
}, {
x: videoContainerWidth / 3,
y: videoContainerHeight * 3 / 5,
width: videoContainerWidth / 3,
height: videoContainerHeight * 2 / 5
}, {
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 3 / 5,
width: videoContainerWidth / 3,
height: videoContainerHeight * 2 / 5
}];
case 6:
return [{
x: 0,
y: 0,
width: videoContainerWidth * 2 / 3,
height: videoContainerHeight * 3 / 5
},
{
x: videoContainerWidth * 2 / 3,
y: 0,
width: videoContainerWidth * 1 / 3,
height: videoContainerHeight * 3 / 5 / 2
},
{
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 3 / 5 / 2,
width: videoContainerWidth * 1 / 3,
height: videoContainerHeight * 3 / 5 / 2
}, {
x: 0,
y: videoContainerHeight * 3 / 5,
width: videoContainerWidth / 3,
height: videoContainerHeight * 2 / 5
}, {
x: videoContainerWidth / 3,
y: videoContainerHeight * 3 / 5,
width: videoContainerWidth / 3,
height: videoContainerHeight * 2 / 5
}, {
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 3 / 5,
width: videoContainerWidth / 3,
height: videoContainerHeight * 2 / 5
}
];
case 7:
return [
{
x: 0,
y: 0,
width: videoContainerWidth / 2,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 2,
y: 0,
width: videoContainerWidth / 2,
height: videoContainerHeight / 3
},
{
x: 0,
y: videoContainerHeight / 3,
width: videoContainerWidth / 2,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 2,
y: videoContainerHeight / 3,
width: videoContainerWidth / 2,
height: videoContainerHeight / 3
}, {
x: 0,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}
];
case 8:
return [
{
x: 0,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 3,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 2 * videoContainerWidth / 3,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 0,
y: videoContainerHeight / 3,
width: videoContainerWidth / 2,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 2,
y: videoContainerHeight / 3,
width: videoContainerWidth / 2,
height: videoContainerHeight / 3
}, {
x: 0,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}
];
case 9:
return [
{
x: 0,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 3,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 2 * videoContainerWidth / 3,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 0,
y: videoContainerHeight / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 3,
y: videoContainerHeight / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 2 * videoContainerWidth / 3,
y: videoContainerHeight / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 0,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}
];
case 10:
return [
{
x: 0,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 6
},
{
x: 0,
y: videoContainerHeight / 6,
width: videoContainerWidth / 3,
height: videoContainerHeight / 6
},
{
x: videoContainerWidth / 3,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 2 * videoContainerWidth / 3,
y: 0,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 0,
y: videoContainerHeight / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: videoContainerWidth / 3,
y: videoContainerHeight / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 2 * videoContainerWidth / 3,
y: videoContainerHeight / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
},
{
x: 0,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}, {
x: videoContainerWidth * 2 / 3,
y: videoContainerHeight * 2 / 3,
width: videoContainerWidth / 3,
height: videoContainerHeight / 3
}
];
}
}
}
module.exports = Layouter;

24
utils/perf.js 100755
View File

@ -0,0 +1,24 @@
const Utils = require("./util.js")
class Perf {
constructor() {
this.init();
}
init() {
this.perf = [];
this.ts = new Date().getTime();
}
profile(event) {
const ts = new Date().getTime();
this.perf.push(`${event}: ${ts - this.ts}ms`);
}
dump() {
Utils.log(`${JSON.stringify(this.perf)}`);
}
}
module.exports = new Perf();

91
utils/uploader.js 100755
View File

@ -0,0 +1,91 @@
const Event = require("./event");
const Utils = require("./util");
class LogUploaderTask {
constructor(content, channel, part, ts, uid) {
this.content = content;
this.channel = channel;
this.part = part;
this.ts = ts;
this.uid = uid;
}
process() {
return new Promise((resolve, reject) => {
wx.request({
url: 'https://webdemo.agora.io/miniapps/restful/v1/logs',
method: 'post',
data: {
logs: this.content,
channel: this.channel,
part: this.part,
ts: this.ts,
uid: this.uid
},
success: function (res) {
resolve();
},
fail: function (e) {
reject(e);
}
})
});
}
}
class LogUploader {
constructor() {
this.total = 0;
this.tasks = [];
this.events = new Event();
this.processingTask = null;
this.subscribeEvents();
}
scheduleTasks(tasks) {
this.tasks = tasks || [];
this.total = this.tasks.length;
this.events.emit("next");
}
processNextTask() {
if (this.tasks.length === 0) {
Utils.log(`all task consumed`);
return;
}
let task = this.tasks.splice(0, 1)[0];
this.processingTask = task;
task.process().then(() => {
this.processingTask = null;
this.events.emit("progress", {remain: this.tasks.length, total: this.total});
this.events.emit("next");
}).catch( e => {
this.events.emit("error", e);
this.tasks = [];
this.total = 0;
this.processingTask = null;
});
}
subscribeEvents() {
this.events.on("next", () => {
if(this.processingTask){
Utils("already processing, wait for this one to finish")
} else {
this.processNextTask();
}
});
}
on(event, cb) {
this.events.on(event, cb);
return this;
}
off(event, cb) {
this.events.off(event, cb);
return this;
}
}
let uploader = new LogUploader();
module.exports = {
LogUploader: uploader,
LogUploaderTask: LogUploaderTask
};

112
utils/util.js 100755
View File

@ -0,0 +1,112 @@
let logitems = [];
let dbgRtmp = false;
let systemInfoChecked = false;
let uid = `${parseInt(Math.random() * 1000000)}`;
let timer;
const debounce = function(fn, delay) {
return function () {
let context = this
let args = arguments
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}
const formatTime = date => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
const millisecond = date.getMilliseconds();
return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second, millisecond].map(formatNumber).join(':')
}
const formatNumber = n => {
n = n.toString()
return n[1] ? n : '0' + n
}
const requestPermission = (scope, cb) => {
wx.getSetting({
success(res) {
if (res.authSetting[scope]) {
cb && cb();
} else {
wx.authorize({
scope: scope,
success() {
cb && cb();
}
})
}
}
})
}
const log = (msg, level) => {
let time = formatTime(new Date());
logitems.push(`${time}: ${msg}`);
if (level === "error") {
console.error(`${time}: ${msg}`);
} else {
console.log(`${time}: ${msg}`);
}
}
const getUid = () => {
return uid;
}
const mashupUrl = (url, channel) => {
return url;
}
const checkSystemInfo = (app) => {
if (!systemInfoChecked) {
systemInfoChecked = true;
wx.getSystemInfo({
success: function (res) {
log(`${JSON.stringify(res)}`);
let sdkVersion = res.SDKVersion;
let version_items = sdkVersion.split(".");
let major_version = parseInt(version_items[0]);
let minor_version = parseInt(version_items[1]);
app.globalData.systemInfo = res;
if (major_version <= 1 && minor_version < 7) {
wx.showModal({
title: '版本过低',
content: '微信版本过低,部分功能可能无法工作',
success: function (res) {
if (res.confirm) {
console.log('用户点击确定')
} else if (res.cancel) {
console.log('用户点击取消')
}
}
})
}
}
})
}
}
module.exports = {
getUid: getUid,
checkSystemInfo: checkSystemInfo,
formatTime: formatTime,
requestPermission: requestPermission,
log: log,
clearLogs: function () {logitems = []},
getLogs: function () { return logitems },
mashupUrl: mashupUrl,
debounce: debounce
}