0606 updated
parent
b9836ef7ce
commit
f71cf6205a
|
@ -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 许可证。
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
})
|
|
@ -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"
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -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;
|
||||
}
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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 并重新加入频道')
|
||||
// });
|
||||
|
||||
}
|
||||
})
|
|
@ -0,0 +1,4 @@
|
|||
{"usingComponents": {
|
||||
"agora-pusher": "../../components/agora-pusher/agora-pusher",
|
||||
"agora-player": "../../components/agora-player/agora-player"
|
||||
}}
|
|
@ -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
|
@ -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();
|
||||
}
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
/* pages/test/test.wxss */
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||
"rules": [{
|
||||
"action": "allow",
|
||||
"page": "*"
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
const APPID = "";
|
||||
|
||||
if(APPID === ""){
|
||||
wx.showToast({
|
||||
title: `请在config.js中提供正确的appid`,
|
||||
icon: 'none',
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APPID: APPID
|
||||
}
|
|
@ -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 } };
|
|
@ -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;
|
|
@ -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();
|
|
@ -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
|
||||
};
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue