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

878 lines
23 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// pages/meeting/meeting.js
const app = getApp()
const Utils = require('../../utils/util.js')
// 微信平台 SDK
// const AgoraMiniappSDK = require("../../lib/mini-app-sdk-production.js");
// 凡泰平台 SDK
const AgoraMiniappSDK = require("../../lib/finclip-agora-client-sdk");
const max_user = 10;
const Layouter = require("../../utils/layout.js");
const { APPID, getToken } = require("../../utils/config.js");
/**
* 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`);
client.join(getToken(), 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(getToken(), 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 并重新加入频道')
// });
}
})