1
0
Fork 0
Agora-Miniapp-Tutorial/pages/meeting/meeting.js

875 lines
23 KiB
JavaScript
Raw Normal View History

2022-06-06 11:51:36 +08:00
// 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 并重新加入频道')
// });
}
})